mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
e3cdbcdb37
`assertElementTextAfterScrolling` waited 1000ms per attempt. The wait's whole budget goes to its first capture, so that budget has to cover one snapshot of the current surface; on a loaded simulator it does not, and every attempt fails with `wait_capture_stalled` instead of reporting the element as off-screen. This broke the iOS Smoke Tests lane on main. #1484 moved the smoke automation scenario onto this helper for `automation-press` and `automation-longpress`; before that it was only reached from the full lane, so the tight budget went unnoticed. Every sibling wait in the same scenario already budgets 2500ms or more (`label="Settings"` 10000, `alert wait` 5000, `wait text` 2500). Two changes: - Raise the per-attempt budget to 2500ms, matching the nearest sibling. - Stop spending a scroll attempt on a stalled capture. A stall means the snapshot never came back, so the surface was never read — it is not evidence the element is off-screen, and scrolling on it moves the surface for an unrelated reason. Two stall retries absorb a slow runner; a genuine absence still consumes attempts and still fails. The final assertion now carries the last wait's JSON, so a future failure says whether it stalled or genuinely never found the element. Claude-Session: https://claude.ai/code/session_01RXQLYV7etZx3gcXsUsrQJ8 Co-authored-by: Claude <noreply@anthropic.com>
99 lines
3.8 KiB
TypeScript
99 lines
3.8 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
|
|
import { PUBLIC_COMMANDS } from '../../../src/command-catalog.ts';
|
|
import {
|
|
assertFilesDiffer,
|
|
assertJsonContains,
|
|
assertMp4File,
|
|
assertNonEmptyFile,
|
|
createLiveDeviceAssertions,
|
|
} from '../live-device-e2e/assertions.ts';
|
|
import type { CliJsonResult } from '../cli-json.ts';
|
|
import type { IosSimulatorBehaviorId } from './behavior-coverage.ts';
|
|
import { type LiveContext, runStep, verifyCommand } from './live-harness.ts';
|
|
|
|
export { assertFilesDiffer, assertJsonContains, assertMp4File, assertNonEmptyFile };
|
|
|
|
export const { assertElementText, assertWaitText, capturePng } = createLiveDeviceAssertions<
|
|
IosSimulatorBehaviorId,
|
|
LiveContext
|
|
>(runStep, verifyCommand, PUBLIC_COMMANDS.wait);
|
|
|
|
// The whole budget goes to the first capture of each attempt, so it has to cover one snapshot of
|
|
// the current surface — 1s did not on a loaded simulator, and every sibling wait in these scenarios
|
|
// already budgets 2.5s or more.
|
|
const SCROLL_SEARCH_WAIT_MS = 2500;
|
|
const SCROLL_SEARCH_ATTEMPTS = 4;
|
|
// A stalled capture says nothing about where the element is, so it must not consume the scroll
|
|
// budget outright; a couple of retries absorb a slow runner without masking a real absence.
|
|
const SCROLL_SEARCH_STALL_RETRIES = 2;
|
|
|
|
export async function assertElementTextAfterScrolling(
|
|
context: LiveContext,
|
|
selector: string,
|
|
expected: string,
|
|
): Promise<void> {
|
|
let stallRetriesLeft = SCROLL_SEARCH_STALL_RETRIES;
|
|
let lastFailure: CliJsonResult | undefined;
|
|
|
|
for (let attempt = 1; attempt <= SCROLL_SEARCH_ATTEMPTS; ) {
|
|
const visible = await runStep(
|
|
context,
|
|
`wait for ${selector} after scroll (attempt ${attempt})`,
|
|
['wait', selector, String(SCROLL_SEARCH_WAIT_MS)],
|
|
{ allowFailure: true },
|
|
);
|
|
if (visible.status === 0) {
|
|
await assertElementText(context, selector, expected);
|
|
return;
|
|
}
|
|
lastFailure = visible;
|
|
|
|
// The snapshot never came back, so the surface was never read. Scrolling here would move the
|
|
// surface for a reason unrelated to visibility and spend an attempt on no evidence.
|
|
if (visible.json?.error?.details?.captureStalled === true && stallRetriesLeft > 0) {
|
|
stallRetriesLeft -= 1;
|
|
continue;
|
|
}
|
|
|
|
attempt += 1;
|
|
if (attempt <= SCROLL_SEARCH_ATTEMPTS) {
|
|
await runStep(context, `scroll toward ${selector}`, ['scroll', 'down', '0.75']);
|
|
}
|
|
}
|
|
assert.fail(
|
|
`${selector} did not become visible after scrolling\nlast wait: ${JSON.stringify(lastFailure?.json ?? null)}`,
|
|
);
|
|
}
|
|
|
|
function requireNode(
|
|
result: CliJsonResult,
|
|
identifier: string,
|
|
): { label?: unknown; rect?: { height: number; width: number; x: number; y: number } } {
|
|
const nodes = Array.isArray(result.json?.data?.nodes) ? result.json.data.nodes : [];
|
|
const node = nodes.find(
|
|
(candidate: { identifier?: unknown }) => candidate.identifier === identifier,
|
|
);
|
|
assert.ok(node, `snapshot missing ${identifier}: ${JSON.stringify(result.json)}`);
|
|
return node;
|
|
}
|
|
|
|
export function requireNodeRect(
|
|
result: CliJsonResult,
|
|
identifier: string,
|
|
): { height: number; width: number; x: number; y: number } {
|
|
const rect = requireNode(result, identifier).rect;
|
|
assert.ok(rect, `snapshot node ${identifier} has no rect: ${JSON.stringify(result.json)}`);
|
|
for (const value of [rect.x, rect.y, rect.width, rect.height]) {
|
|
assert.ok(Number.isFinite(value), `snapshot node ${identifier} has invalid rect`);
|
|
}
|
|
return rect;
|
|
}
|
|
|
|
export function requireDevice(result: CliJsonResult, udid: string): { booted?: unknown } {
|
|
const devices = Array.isArray(result.json?.data?.devices) ? result.json.data.devices : [];
|
|
const device = devices.find((candidate: { id?: unknown }) => candidate.id === udid);
|
|
assert.ok(device, `device inventory missing ${udid}: ${JSON.stringify(result.json)}`);
|
|
return device;
|
|
}
|