Skip to content

Commit beba083

Browse files
committed
fix: compose post-gesture snapshots with android freshness
1 parent 87f212b commit beba083

8 files changed

Lines changed: 115 additions & 27 deletions

File tree

src/daemon/__tests__/interaction-outcome-policy.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
buildInteractionSurfaceSignature,
66
classifyInteractionSurfaceChange,
77
markPendingInteractionOutcome,
8-
stripInternalInteractionOutcomeFlags,
8+
stripInternalInteractionFlags,
99
} from '../interaction-outcome-policy.ts';
1010
import type { SessionState } from '../types.ts';
1111
import { IOS_SIMULATOR } from '../../__tests__/test-utils/device-fixtures.ts';
@@ -84,9 +84,9 @@ test('markPendingInteractionOutcome stores retry state only for explicit retry f
8484
assert.equal(longPressSession.pendingInteractionOutcome, undefined);
8585
});
8686

87-
test('stripInternalInteractionOutcomeFlags removes internal interaction controls', () => {
87+
test('stripInternalInteractionFlags removes internal interaction controls', () => {
8888
assert.deepEqual(
89-
stripInternalInteractionOutcomeFlags({
89+
stripInternalInteractionFlags({
9090
platform: 'ios',
9191
interactionOutcome: { retryOnNoChange: true },
9292
postGestureStabilization: true,

src/daemon/handlers/__tests__/snapshot-handler.test.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1032,6 +1032,80 @@ test('captureSnapshot retries pending tap outcome before post-gesture stabilizat
10321032
expect(session.postGestureStabilization).toBeUndefined();
10331033
});
10341034

1035+
test('captureSnapshot composes post-gesture stabilization with Android freshness capture', async () => {
1036+
const sessionName = 'android-post-gesture-freshness';
1037+
const session = makeSession(sessionName, androidDevice);
1038+
const baselineNodes = Array.from({ length: 18 }, (_, index) => ({
1039+
ref: `e${index + 1}`,
1040+
index,
1041+
depth: 0,
1042+
type: 'android.widget.TextView',
1043+
label: `Inbox row ${index + 1}`,
1044+
}));
1045+
const changedNodes = Array.from({ length: 18 }, (_, index) => ({
1046+
ref: `e${index + 1}`,
1047+
index,
1048+
depth: 0,
1049+
type: 'android.widget.TextView',
1050+
label: index === 0 ? 'album-0' : `Album row ${index + 1}`,
1051+
}));
1052+
session.snapshot = {
1053+
nodes: baselineNodes,
1054+
createdAt: Date.now(),
1055+
backend: 'android',
1056+
comparisonSafe: true,
1057+
};
1058+
session.androidSnapshotFreshness = {
1059+
action: 'click',
1060+
markedAt: Date.now(),
1061+
baselineCount: baselineNodes.length,
1062+
baselineSignatures: buildSnapshotSignatures(baselineNodes),
1063+
routeComparable: true,
1064+
};
1065+
session.postGestureStabilization = {
1066+
action: 'click',
1067+
markedAt: Date.now(),
1068+
};
1069+
1070+
mockDispatch
1071+
.mockResolvedValueOnce({
1072+
nodes: baselineNodes,
1073+
truncated: false,
1074+
backend: 'android',
1075+
analysis: { rawNodeCount: 18, maxDepth: 1 },
1076+
})
1077+
.mockResolvedValueOnce({
1078+
nodes: changedNodes,
1079+
truncated: false,
1080+
backend: 'android',
1081+
analysis: { rawNodeCount: 18, maxDepth: 1 },
1082+
})
1083+
.mockResolvedValueOnce({
1084+
nodes: changedNodes,
1085+
truncated: false,
1086+
backend: 'android',
1087+
analysis: { rawNodeCount: 18, maxDepth: 1 },
1088+
});
1089+
1090+
const result = await captureSnapshot({
1091+
device: androidDevice,
1092+
session,
1093+
flags: { snapshotInteractiveOnly: true },
1094+
logPath: '/tmp/daemon.log',
1095+
});
1096+
1097+
expect(result.snapshot.nodes).toEqual(
1098+
expect.arrayContaining([expect.objectContaining({ label: 'album-0' })]),
1099+
);
1100+
expect(mockDispatch.mock.calls.map((call) => call[1])).toEqual([
1101+
'snapshot',
1102+
'snapshot',
1103+
'snapshot',
1104+
]);
1105+
expect(session.androidSnapshotFreshness).toBeUndefined();
1106+
expect(session.postGestureStabilization).toBeUndefined();
1107+
});
1108+
10351109
test('captureSnapshot composes pending outcome retry with Android freshness capture', async () => {
10361110
const sessionName = 'android-lazy-outcome-freshness';
10371111
const session = makeSession(sessionName, androidDevice);

src/daemon/handlers/find.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { captureSnapshot } from './snapshot-capture.ts';
1616
import { setSessionSnapshot } from '../session-snapshot.ts';
1717
import { errorResponse } from './response.ts';
1818
import { getActiveAndroidSnapshotFreshness } from '../android-snapshot-freshness.ts';
19-
import { stripInternalInteractionOutcomeFlags } from '../interaction-outcome-policy.ts';
19+
import { stripInternalInteractionFlags } from '../interaction-outcome-policy.ts';
2020
import { dispatchFindReadOnlyViaRuntime } from '../selector-runtime.ts';
2121
import { PUBLIC_COMMANDS } from '../../command-catalog.ts';
2222

@@ -502,7 +502,7 @@ function recordFindAction(ctx: FindContext, match: ResolvedMatch, action: string
502502
// --- Helpers ---
503503

504504
function publicFindFlags(flags: DaemonRequest['flags']): Record<string, unknown> {
505-
return { ...(stripInternalInteractionOutcomeFlags(flags) ?? {}) };
505+
return { ...(stripInternalInteractionFlags(flags) ?? {}) };
506506
}
507507

508508
function buildAmbiguousMatchError(

src/daemon/handlers/interaction-common.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
} from '../android-snapshot-freshness.ts';
1212
import {
1313
markPendingInteractionOutcome,
14-
stripInternalInteractionOutcomeFlags,
14+
stripInternalInteractionFlags,
1515
} from '../interaction-outcome-policy.ts';
1616
import { markPostGestureStabilization } from '../post-gesture-stabilization.ts';
1717

@@ -99,7 +99,7 @@ export function finalizeTouchInteraction(params: {
9999
actionFinishedAt,
100100
androidFreshnessBaseline,
101101
} = params;
102-
const actionFlags = stripInternalInteractionOutcomeFlags(flags);
102+
const actionFlags = stripInternalInteractionFlags(flags);
103103
sessionStore.recordAction(session, {
104104
command,
105105
positionals,

src/daemon/handlers/snapshot-capture.ts

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ import {
3939
type InteractionSurfaceChange,
4040
} from '../interaction-outcome-policy.ts';
4141
import {
42-
capturePostGestureStabilizedSnapshot,
4342
capturePostGestureStabilizedResult,
4443
} from '../post-gesture-stabilization.ts';
4544
import { findNodeByLabel, pruneGroupNodes, resolveRefLabel } from '../snapshot-processing.ts';
@@ -91,12 +90,7 @@ export async function captureSnapshot(params: CaptureSnapshotParams): Promise<{
9190
(params.device.platform === 'ios' || params.device.platform === 'android') &&
9291
params.session?.postGestureStabilization
9392
) {
94-
return {
95-
snapshot: await capturePostGestureStabilizedSnapshot({
96-
session: params.session,
97-
capture: async () => (await captureSnapshotAttempt(params)).snapshot,
98-
}),
99-
};
93+
return await capturePostGestureAwareSnapshot({ ...params, session: params.session });
10094
}
10195
const freshness = getActiveAndroidSnapshotFreshness(params.session);
10296
if (freshness && params.device.platform === 'android') {
@@ -127,7 +121,7 @@ async function captureInteractionOutcomeAwareSnapshot(
127121
let settled = await waitForDelayedInteractionSurfaceChange(
128122
params,
129123
pending,
130-
await captureSnapshotAttemptForInteractionOutcome(params),
124+
await capturePostActionSnapshotAttempt(params),
131125
);
132126
let latest = settled.latest;
133127
let outcome = await retryPendingInteractionOutcome({
@@ -142,7 +136,7 @@ async function captureInteractionOutcomeAwareSnapshot(
142136
settled = await waitForDelayedInteractionSurfaceChange(
143137
params,
144138
pending,
145-
await captureSnapshotAttemptForInteractionOutcome(params),
139+
await capturePostActionSnapshotAttempt(params),
146140
);
147141
latest = settled.latest;
148142
outcome = await retryPendingInteractionOutcome({
@@ -157,7 +151,7 @@ async function captureInteractionOutcomeAwareSnapshot(
157151
latest = await capturePostGestureStabilizedResult({
158152
session,
159153
initial: latest,
160-
capture: async () => await captureSnapshotAttemptForInteractionOutcome(params),
154+
capture: async () => await capturePostActionSnapshotAttempt(params),
161155
readSnapshot: (attempt) => attempt.snapshot,
162156
});
163157
if (outcome.change !== 'ambiguous' && latest.freshness?.staleAfterRetries !== true) {
@@ -195,7 +189,7 @@ async function waitForDelayedInteractionSurfaceChange(
195189
if (change !== 'unchanged') return { latest, change };
196190

197191
await sleep(INTERACTION_CHANGE_RECHECK_DELAY_MS);
198-
latest = await captureSnapshotAttemptForInteractionOutcome(params);
192+
latest = await capturePostActionSnapshotAttempt(params);
199193
change = classifyInteractionSurfaceChange(
200194
pending.preSignature,
201195
buildInteractionSurfaceSignature(latest.snapshot.nodes),
@@ -292,7 +286,28 @@ async function captureAndroidFreshnessAwareAttempt(
292286
};
293287
}
294288

295-
async function captureSnapshotAttemptForInteractionOutcome(
289+
async function capturePostGestureAwareSnapshot(
290+
params: CaptureSnapshotParams & { session: SessionState },
291+
): Promise<{
292+
snapshot: SnapshotState;
293+
analysis?: AndroidSnapshotAnalysis;
294+
androidSnapshot?: AndroidSnapshotBackendMetadata;
295+
freshness?: AndroidFreshnessCaptureMeta;
296+
}> {
297+
const latest = await capturePostGestureStabilizedResult({
298+
session: params.session,
299+
capture: async () => await capturePostActionSnapshotAttempt(params),
300+
readSnapshot: (attempt) => attempt.snapshot,
301+
});
302+
return {
303+
snapshot: latest.snapshot,
304+
analysis: latest.data.analysis,
305+
androidSnapshot: latest.data.androidSnapshot,
306+
freshness: latest.freshness,
307+
};
308+
}
309+
310+
async function capturePostActionSnapshotAttempt(
296311
params: CaptureSnapshotParams & { session: SessionState },
297312
): Promise<SnapshotAttempt> {
298313
const freshness = getActiveAndroidSnapshotFreshness(params.session);

src/daemon/interaction-outcome-policy.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export function markPendingInteractionOutcome(params: {
3737
action: command,
3838
command: retryCommand,
3939
positionals,
40-
flags: stripInternalInteractionOutcomeFlags(flags),
40+
flags: stripInternalInteractionFlags(flags),
4141
markedAt: Date.now(),
4242
attemptsRemaining: OUTCOME_RETRY_ATTEMPTS,
4343
preSignature,
@@ -134,7 +134,7 @@ export function emitInteractionSettleTimeout(params: {
134134
});
135135
}
136136

137-
export function stripInternalInteractionOutcomeFlags(
137+
export function stripInternalInteractionFlags(
138138
flags: CommandFlags | undefined,
139139
): CommandFlags | undefined {
140140
if (!flags?.interactionOutcome && !flags?.postGestureStabilization) return flags;

src/platforms/ios/runner-client.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { withRetry } from '../../utils/retry.ts';
33
import type { DeviceInfo } from '../../utils/device.ts';
44
import { emitDiagnostic } from '../../utils/diagnostics.ts';
55
import { getRequestSignal } from '../../daemon/request-cancel.ts';
6-
import { RUNNER_COMMAND_TIMEOUT_MS, RUNNER_STARTUP_TIMEOUT_MS } from './runner-transport.ts';
6+
import { RUNNER_COMMAND_TIMEOUT_MS } from './runner-transport.ts';
77
import {
88
type RunnerSessionOptions,
99
type RunnerSession,
@@ -12,6 +12,7 @@ import {
1212
stopIosRunnerSession,
1313
validateRunnerDevice,
1414
executeRunnerCommandWithSession,
15+
readRunnerStartupTimeoutMs,
1516
} from './runner-session.ts';
1617
import {
1718
assertRunnerRequestActive,
@@ -250,10 +251,6 @@ async function executeRunnerCommand(
250251
}
251252
}
252253

253-
function readRunnerStartupTimeoutMs(session: Pick<RunnerSession, 'startupTimeoutMs'>): number {
254-
return session.startupTimeoutMs ?? RUNNER_STARTUP_TIMEOUT_MS;
255-
}
256-
257254
async function handleRunnerTransportErrorAfterCommandSend(
258255
device: DeviceInfo,
259256
session: RunnerSession,

src/platforms/ios/runner-session.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -703,7 +703,9 @@ function markRunnerPreflightError(error: unknown, details: Record<string, unknow
703703
);
704704
}
705705

706-
function readRunnerStartupTimeoutMs(session: Pick<RunnerSession, 'startupTimeoutMs'>): number {
706+
export function readRunnerStartupTimeoutMs(
707+
session: Pick<RunnerSession, 'startupTimeoutMs'>,
708+
): number {
707709
return session.startupTimeoutMs ?? RUNNER_STARTUP_TIMEOUT_MS;
708710
}
709711

0 commit comments

Comments
 (0)