Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions test/integration/ios-simulator-e2e-deep-link-confirmation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,12 +112,29 @@ test('target discovery exhaustion still answers a deep-link confirmation', async
]);
});

test('runner startup exhaustion returns without probing for a deep-link confirmation', async () => {
const { device, log } = simulator([RUNNER_START_TIMEOUT]);
test('runner startup exhaustion waits once more, then answers the confirmation the runner sees', async () => {
const { device, log } = simulator([RUNNER_START_TIMEOUT, LAUNCH_PENDING, LANDED]);

await answerDeepLinkConfirmation(device);

assert.deepEqual(log, ['wait for the deep-link destination (1/5)']);
assert.deepEqual(log, [
'wait for the deep-link destination (1/5)',
'wait for the deep-link destination (2/5)',
'alert get',
'alert accept',
'wait for the deep-link destination (3/5)',
]);
});

test('a second runner startup exhaustion returns without probing for a deep-link confirmation', async () => {
const { device, log } = simulator([RUNNER_START_TIMEOUT, RUNNER_START_TIMEOUT]);

await answerDeepLinkConfirmation(device);

assert.deepEqual(log, [
'wait for the deep-link destination (1/5)',
'wait for the deep-link destination (2/5)',
]);
});

test('a readable no-match that answers Open waits for the released launch', async () => {
Expand Down
57 changes: 45 additions & 12 deletions test/integration/ios-simulator-e2e/live-deep-link-confirmation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,27 +61,60 @@ export async function answerDeepLinkConfirmation(
device: DeepLinkConfirmationDevice,
): Promise<void> {
let answered = false;
let runnerStartWaited = false;
for (let wait = 1; wait <= DESTINATION_WAITS; wait += 1) {
const arrived = await device.waitForDestination(
`wait for the deep-link destination (${wait}/${DESTINATION_WAITS})`,
);
if (arrived.status === 0) return;
const details = arrived.json?.error?.details;
const launchPending = details?.runnerErrorCode === APP_NOT_RUNNING;
const reason = details?.reason;
const readableMiss =
reason === WAIT_REASONS.targetAbsent || reason === WAIT_REASONS.deadlineExceeded;
const interruptedObservation =
reason === WAIT_REASONS.captureStalled ||
(reason === WAIT_REASONS.readinessExhausted &&
details?.readinessPhase === 'target-discovery') ||
reason === WAIT_REASONS.runnerRestartExhausted;
if (!launchPending && !readableMiss && !interruptedObservation) return;
const miss = classifyDestinationMiss(arrived.json?.error?.details);
// A relaunch can restart the runner, and on a loaded host its start outlasts one wait; the
// next wait is the first that can see the confirmation. A second start timeout is a failure.
if (miss === 'runner-start' && !runnerStartWaited) {
runnerStartWaited = true;
continue;
}
if (miss === 'final' || miss === 'runner-start') return;
if (!answered) answered = await acceptOpenConfirmation(device);
if (!answered && reason === WAIT_REASONS.targetAbsent) return;
if (!answered && miss === 'target-absent') return;
}
}

/** Why a destination wait missed, as far as the confirmation loop acts on it. */
type DestinationMiss = 'probe' | 'target-absent' | 'runner-start' | 'final';

type DestinationMissDetails = {
reason?: string;
readinessPhase?: string;
runnerErrorCode?: string;
};

/** Wait failures that can leave the launch behind the confirmation: worth one `alert get`. */
const PROBE_REASONS: ReadonlySet<string | undefined> = new Set([
WAIT_REASONS.deadlineExceeded,
WAIT_REASONS.captureStalled,
WAIT_REASONS.runnerRestartExhausted,
]);

/**
* Sorts a destination wait's failure: a pending launch, a readable miss, or an interrupted
* observation may hide the confirmation (`probe`, or `target-absent` for a readable no-match);
* a runner still starting saw nothing yet; anything else is the caller's to assert.
*/
function classifyDestinationMiss(details: DestinationMissDetails | undefined): DestinationMiss {
const readinessPhase = readinessExhaustedPhase(details);
if (readinessPhase === 'runner-start') return 'runner-start';
if (details?.reason === WAIT_REASONS.targetAbsent) return 'target-absent';
if (details?.runnerErrorCode === APP_NOT_RUNNING) return 'probe';
if (PROBE_REASONS.has(details?.reason) || readinessPhase === 'target-discovery') return 'probe';
return 'final';
}

/** The readiness phase a wait ran out of time in, when readiness is what it exhausted. */
function readinessExhaustedPhase(details: DestinationMissDetails | undefined): string | undefined {
return details?.reason === WAIT_REASONS.readinessExhausted ? details.readinessPhase : undefined;
}

async function acceptOpenConfirmation(device: DeepLinkConfirmationDevice): Promise<boolean> {
const alert = await device.inspectAlert();
if (alert.status !== 0) return false;
Expand Down
Loading