Skip to content

Commit bb059ec

Browse files
committed
test(ios-e2e): re-read an unsettled miss instead of scrolling past it
The live scroll search now re-probes the same window once when a miss carries `unsettledGesture`, and its failure lists every probe and scroll.
1 parent 2e05452 commit bb059ec

2 files changed

Lines changed: 75 additions & 9 deletions

File tree

‎test/integration/ios-simulator-e2e-visibility-scroll.test.ts‎

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,62 @@ test('a stalled capture retries without scrolling or consuming an attempt', asyn
5252
assert.deepEqual(probeAttempts, [1, 1]);
5353
assert.deepEqual(scrollAttempts, []);
5454
});
55+
56+
/**
57+
* The CI failure shape (`smoke:automation-input`, id="automation-longpress"): the element is on
58+
* screen only after the first scroll, and the first read after every scroll lands on a surface that
59+
* is still moving, so the daemon answers not-found with `unsettledGesture`. Scrolling on that miss
60+
* carries the element off the top before any settled read sees it.
61+
*/
62+
function listWithUnsettledFirstReads(visibleAtPosition: number | undefined) {
63+
let position = 0;
64+
let readSinceScroll = true;
65+
const probes: number[] = [];
66+
const scrolls: number[] = [];
67+
return {
68+
probes,
69+
scrolls,
70+
probe: async (attempt: number): Promise<CliJsonResult> => {
71+
probes.push(attempt);
72+
if (!readSinceScroll) {
73+
readSinceScroll = true;
74+
return result(1, {
75+
reason: 'selector_not_found',
76+
unsettledGesture: { action: 'scroll', positionals: ['down', '0.75'] },
77+
});
78+
}
79+
return position === visibleAtPosition
80+
? result(0)
81+
: result(1, { reason: 'selector_not_found' });
82+
},
83+
scroll: async (attempt: number) => {
84+
scrolls.push(attempt);
85+
position += 1;
86+
readSinceScroll = false;
87+
},
88+
};
89+
}
90+
91+
test('an unsettled miss right after the scroll that reached the element is re-read, not scrolled past', async () => {
92+
const list = listWithUnsettledFirstReads(1);
93+
94+
await searchForVisibleElement('id="automation-longpress"', list.probe, list.scroll);
95+
96+
assert.deepEqual(list.probes, [1, 2, 2]);
97+
assert.deepEqual(list.scrolls, [1]);
98+
});
99+
100+
test('each scroll window re-reads an unsettled miss once, and a real absence still fails with every step', async () => {
101+
const list = listWithUnsettledFirstReads(undefined);
102+
103+
await assert.rejects(
104+
searchForVisibleElement('id="automation-longpress"', list.probe, list.scroll),
105+
(error: Error) => {
106+
assert.match(error.message, /probe 4: .*selector_not_found/);
107+
assert.match(error.message, /scroll after attempt 3: null/);
108+
return true;
109+
},
110+
);
111+
assert.deepEqual(list.probes, [1, 2, 2, 3, 3, 4, 4]);
112+
assert.deepEqual(list.scrolls, [1, 2, 3]);
113+
});

‎test/integration/ios-simulator-e2e/live-assertions.ts‎

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export async function assertElementTextAfterScrolling(
6363
'scroll',
6464
'down',
6565
'0.75',
66-
]).then(() => undefined),
66+
]).then((result) => result.json?.data),
6767
);
6868
await assertElementText(context, selector, expected);
6969
}
@@ -77,31 +77,38 @@ export async function assertElementTextAfterScrolling(
7777
export async function searchForVisibleElement(
7878
selector: string,
7979
probeVisibility: (attempt: number) => Promise<CliJsonResult>,
80-
scrollAfterAttempt: (attempt: number) => Promise<void>,
80+
scrollAfterAttempt: (attempt: number) => Promise<unknown>,
8181
): Promise<void> {
8282
let stallRetriesLeft = SCROLL_SEARCH_STALL_RETRIES;
83-
let lastFailure: CliJsonResult | undefined;
83+
let unsettledReprobeLeft = true;
84+
const history: string[] = [];
8485

8586
for (let attempt = 1; attempt <= SCROLL_SEARCH_ATTEMPTS;) {
8687
const probe = await probeVisibility(attempt);
88+
history.push(`probe ${attempt}: ${JSON.stringify(probe.json ?? { status: probe.status })}`);
8789
if (probe.status === 0) return;
88-
lastFailure = probe;
8990

91+
const details = probe.json?.error?.details;
9092
// The snapshot never came back, so the surface was never read. Scrolling here would move the
9193
// surface for a reason unrelated to visibility and spend an attempt on no evidence.
92-
if (probe.json?.error?.details?.captureStalled === true && stallRetriesLeft > 0) {
94+
if (details?.captureStalled === true && stallRetriesLeft > 0) {
9395
stallRetriesLeft -= 1;
9496
continue;
9597
}
98+
// The read ran on a surface still moving from the last scroll, so its miss is not an absence.
99+
if (details?.unsettledGesture !== undefined && unsettledReprobeLeft) {
100+
unsettledReprobeLeft = false;
101+
continue;
102+
}
96103

97104
attempt += 1;
98105
if (attempt <= SCROLL_SEARCH_ATTEMPTS) {
99-
await scrollAfterAttempt(attempt - 1);
106+
const scrolled = await scrollAfterAttempt(attempt - 1);
107+
history.push(`scroll after attempt ${attempt - 1}: ${JSON.stringify(scrolled ?? null)}`);
108+
unsettledReprobeLeft = true;
100109
}
101110
}
102-
assert.fail(
103-
`${selector} did not become visible after scrolling\nlast visibility probe: ${JSON.stringify(lastFailure?.json ?? null)}`,
104-
);
111+
assert.fail(`${selector} did not become visible after scrolling\n${history.join('\n')}`);
105112
}
106113

107114
function requireNode(

0 commit comments

Comments
 (0)