Skip to content

Commit 26459db

Browse files
committed
fix(ios-e2e): recover readable deep-link timeouts
1 parent 3e57c2c commit 26459db

2 files changed

Lines changed: 74 additions & 22 deletions

File tree

‎test/integration/ios-simulator-e2e-deep-link-confirmation.test.ts‎

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ const LAUNCH_PENDING = result(1, {
2121
const WRONG_ROUTE = result(1, {
2222
error: { code: 'COMMAND_FAILED', details: { reason: 'wait_target_absent' } },
2323
});
24+
const READABLE_TIMEOUT = result(1, {
25+
error: {
26+
code: 'COMMAND_FAILED',
27+
details: { reason: 'wait_deadline_exceeded', readableCaptures: 5, captureTruncated: true },
28+
},
29+
});
2430
const OPEN_PROMPT = result(0, {
2531
data: { message: 'Open in “Agent Device Tester”?', items: ['Cancel', 'Open'] },
2632
});
@@ -56,7 +62,47 @@ test('a destination that arrives never probes for the confirmation', async () =>
5662

5763
await answerDeepLinkConfirmation(device);
5864

59-
assert.deepEqual(log, ['wait for the deep-link destination (1/4)']);
65+
assert.deepEqual(log, ['wait for the deep-link destination (1/5)']);
66+
});
67+
68+
test('a readable destination timeout still answers a real Open confirmation', async () => {
69+
const { device, log } = simulator([READABLE_TIMEOUT, LANDED]);
70+
71+
await answerDeepLinkConfirmation(device);
72+
73+
assert.deepEqual(log, [
74+
'wait for the deep-link destination (1/5)',
75+
'alert get',
76+
'alert accept',
77+
'wait for the deep-link destination (2/5)',
78+
]);
79+
});
80+
81+
test('a readable no-match that answers Open waits for the released launch', async () => {
82+
const { device, log } = simulator([WRONG_ROUTE, LAUNCH_PENDING, LANDED]);
83+
84+
await answerDeepLinkConfirmation(device);
85+
86+
assert.deepEqual(log, [
87+
'wait for the deep-link destination (1/5)',
88+
'alert get',
89+
'alert accept',
90+
'wait for the deep-link destination (2/5)',
91+
'wait for the deep-link destination (3/5)',
92+
]);
93+
});
94+
95+
test('a truncated capture retries for four bounded waits without accepting a missing alert', async () => {
96+
const { device, log } = simulator(
97+
[READABLE_TIMEOUT, READABLE_TIMEOUT, READABLE_TIMEOUT, READABLE_TIMEOUT, LANDED],
98+
[NO_ALERT, NO_ALERT, NO_ALERT, NO_ALERT],
99+
);
100+
101+
await answerDeepLinkConfirmation(device);
102+
103+
assert.equal(waits(log), 5);
104+
assert.equal(log.filter((step) => step === 'alert get').length, 4);
105+
assert.equal(log.includes('alert accept'), false);
60106
});
61107

62108
test('the launch an accepted confirmation releases is waited for until it lands', async () => {
@@ -66,29 +112,29 @@ test('the launch an accepted confirmation releases is waited for until it lands'
66112
await answerDeepLinkConfirmation(device);
67113

68114
assert.deepEqual(log, [
69-
'wait for the deep-link destination (1/4)',
115+
'wait for the deep-link destination (1/5)',
70116
'alert get',
71117
'alert accept',
72-
'wait for the deep-link destination (2/4)',
73-
'wait for the deep-link destination (3/4)',
74-
'wait for the deep-link destination (4/4)',
118+
'wait for the deep-link destination (2/5)',
119+
'wait for the deep-link destination (3/5)',
120+
'wait for the deep-link destination (4/5)',
75121
]);
76122
});
77123

78-
test('a miss that is not a pending launch neither probes nor waits again', async () => {
79-
const { device, log } = simulator([WRONG_ROUTE]);
124+
test('a readable miss probes once and leaves a wrong route to the caller', async () => {
125+
const { device, log } = simulator([WRONG_ROUTE], [NO_ALERT]);
80126

81127
await answerDeepLinkConfirmation(device);
82128

83-
assert.deepEqual(log, ['wait for the deep-link destination (1/4)']);
129+
assert.deepEqual(log, ['wait for the deep-link destination (1/5)', 'alert get']);
84130
});
85131

86-
test('after the accept, a miss that is not a pending launch earns no further wait', async () => {
87-
const { device, log } = simulator([LAUNCH_PENDING, WRONG_ROUTE]);
132+
test('after the accept, a readable no-match still gets a bounded launch wait', async () => {
133+
const { device, log } = simulator([LAUNCH_PENDING, WRONG_ROUTE, LANDED]);
88134

89135
await answerDeepLinkConfirmation(device);
90136

91-
assert.equal(waits(log), 2);
137+
assert.equal(waits(log), 3);
92138
});
93139

94140
test('a confirmation that appears late is still answered once', async () => {
@@ -110,7 +156,7 @@ test('the wait budget is bounded when the app never starts', async () => {
110156

111157
await answerDeepLinkConfirmation(device);
112158

113-
assert.equal(waits(log), 4);
159+
assert.equal(waits(log), 5);
114160
});
115161

116162
test('a prompt that is not the deep-link confirmation is never accepted', async () => {

‎test/integration/ios-simulator-e2e/live-deep-link-confirmation.ts‎

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ import { type LiveContext, runStep } from './live-harness.ts';
66
/** One destination wait; generous because the WebView lab took over 2.5 s to mount on cold CI. */
77
const DEEP_LINK_DESTINATION_WAIT_MS = '15000';
88
/**
9-
* One wait finds the confirmation holding the launch; three more cover the launch its answer
10-
* releases. That launch usually reaches the foreground within 3 s of the tap, but a loaded host has
11-
* held it for 20.7 s (CI run 35991523779) and 27.4 s (a local run), and the route renders after that.
9+
* One wait can find the confirmation holding the launch; four more cover its release or a stalled
10+
* runner restart. The answered launch usually reaches the foreground within 3 s, but a loaded host
11+
* held it for 20.7 s (CI run 35991523779) and 27.4 s (a local run).
1212
*/
13-
const DESTINATION_WAITS = 4;
13+
const DESTINATION_WAITS = 5;
1414
/** `details.runnerErrorCode` of a read the runner refused because the session app is not running. */
1515
const APP_NOT_RUNNING = 'APP_NOT_RUNNING';
1616

@@ -43,11 +43,9 @@ export function acceptDeepLinkConfirmationIfPresent(
4343
}
4444

4545
/**
46-
* Only a destination wait that ends on `APP_NOT_RUNNING` — the launch is still pending — leads to
47-
* the alert probe or to another wait. Any other outcome returns, so an arrived route skips the probe
48-
* and a wrong route or a stalled capture fails on the caller's own destination assertion. The probe
49-
* therefore never queries a rendered screen, where `alert get` against a live WKWebView exceeds the
50-
* runner's execution watchdog and leaves later commands refused as `RUNNER_BUSY` (#2484 follow-up).
46+
* A readable destination timeout can still leave the launch behind a system confirmation. Probe
47+
* once per miss until it is answered; a truncated or stalled capture then gets another bounded
48+
* wait, while a readable wrong-route miss without a prompt goes to the caller's assertion.
5149
*/
5250
export async function answerDeepLinkConfirmation(
5351
device: DeepLinkConfirmationDevice,
@@ -57,8 +55,16 @@ export async function answerDeepLinkConfirmation(
5755
const arrived = await device.waitForDestination(
5856
`wait for the deep-link destination (${wait}/${DESTINATION_WAITS})`,
5957
);
60-
if (arrived.json?.error?.details?.runnerErrorCode !== APP_NOT_RUNNING) return;
58+
if (arrived.status === 0) return;
59+
const details = arrived.json?.error?.details;
60+
const launchPending = details?.runnerErrorCode === APP_NOT_RUNNING;
61+
const reason = details?.reason;
62+
const readableMiss = reason === 'wait_target_absent' || reason === 'wait_deadline_exceeded';
63+
const interruptedCapture =
64+
reason === 'wait_capture_stalled' || reason === 'wait_runner_restart_exhausted';
65+
if (!launchPending && !readableMiss && !interruptedCapture) return;
6166
if (!answered) answered = await acceptOpenConfirmation(device);
67+
if (!answered && reason === 'wait_target_absent') return;
6268
}
6369
}
6470

0 commit comments

Comments
 (0)