Skip to content

Commit be5f20e

Browse files
committed
fix: stabilize Android Maestro replay context
1 parent aedc56d commit be5f20e

4 files changed

Lines changed: 103 additions & 1 deletion

File tree

src/compat/maestro/runtime-assertions.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ async function invokeNativeMaestroVisibleWaitWithSnapshotFallback(
7878
const nativeStartedAt = Date.now();
7979
const nativeResponse = await runNativeVisibleWait(params, args, nativeWaitQuery);
8080
if (nativeResponse.ok) {
81+
rememberMaestroVisibleContext(params.scope, args.selector);
8182
return visibleAssertionResponse(
8283
{
8384
ok: true,

src/daemon/__tests__/post-gesture-stabilization.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,32 @@ test('capturePostGestureStabilizedSnapshot retries until rects stop moving', asy
6262
assert.equal(session.postGestureStabilization, undefined);
6363
});
6464

65+
test('capturePostGestureStabilizedSnapshot samples again after a slow first capture', async () => {
66+
vi.useFakeTimers();
67+
const session = makeSession('android');
68+
markPostGestureStabilization(session, 'click', [], { postGestureStabilization: true });
69+
let captures = 0;
70+
71+
const promise = capturePostGestureStabilizedSnapshot({
72+
session,
73+
capture: async () => {
74+
captures += 1;
75+
if (captures === 1) {
76+
await new Promise((resolve) => setTimeout(resolve, 1_600));
77+
}
78+
return makeSnapshot(100);
79+
},
80+
});
81+
82+
await vi.advanceTimersByTimeAsync(1_600);
83+
await vi.advanceTimersByTimeAsync(200);
84+
const snapshot = await promise;
85+
86+
assert.equal(captures, 2);
87+
assert.equal(snapshot.nodes[1]?.rect?.y, 100);
88+
assert.equal(session.postGestureStabilization, undefined);
89+
});
90+
6591
function makeSession(platform: 'ios' | 'android' = 'ios'): SessionState {
6692
return {
6793
name: platform,

src/daemon/handlers/__tests__/session-replay-vars.test.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1050,6 +1050,77 @@ test('runReplayScriptFile captures a fresh Maestro snapshot for tapOn after asse
10501050
);
10511051
});
10521052

1053+
test('runReplayScriptFile scopes duplicate tap targets after native Maestro assertVisible', async () => {
1054+
const { response, calls } = await runReplayFixture({
1055+
label: 'maestro-native-assert-context-duplicate-tap',
1056+
script: ['appId: demo.app', '---', '- assertVisible: Albums', '- tapOn: Push article', ''].join(
1057+
'\n',
1058+
),
1059+
flags: { replayBackend: 'maestro', platform: 'android' },
1060+
invoke: async (req) => {
1061+
if (req.command === 'wait') {
1062+
return { ok: true, data: { matched: true } };
1063+
}
1064+
if (req.command === 'snapshot') {
1065+
return {
1066+
ok: true,
1067+
data: {
1068+
nodes: [
1069+
{
1070+
index: 1,
1071+
depth: 1,
1072+
type: 'android.widget.ScrollView',
1073+
rect: { x: 0, y: 0, width: 390, height: 844 },
1074+
},
1075+
{
1076+
index: 2,
1077+
depth: 2,
1078+
parentIndex: 1,
1079+
type: 'android.widget.TextView',
1080+
label: 'Albums',
1081+
rect: { x: 24, y: 120, width: 120, height: 40 },
1082+
},
1083+
{
1084+
index: 3,
1085+
depth: 2,
1086+
parentIndex: 1,
1087+
type: 'android.widget.TextView',
1088+
label: 'Push article',
1089+
rect: { x: 32, y: 220, width: 160, height: 44 },
1090+
},
1091+
{
1092+
index: 10,
1093+
depth: 1,
1094+
type: 'android.widget.ScrollView',
1095+
rect: { x: 0, y: 0, width: 390, height: 844 },
1096+
},
1097+
{
1098+
index: 11,
1099+
depth: 2,
1100+
parentIndex: 10,
1101+
type: 'android.widget.TextView',
1102+
label: 'Push article',
1103+
rect: { x: 32, y: 520, width: 160, height: 44 },
1104+
},
1105+
],
1106+
},
1107+
};
1108+
}
1109+
return { ok: true, data: {} };
1110+
},
1111+
});
1112+
1113+
assert.equal(response.ok, true);
1114+
assert.deepEqual(
1115+
calls.map((call) => [call.command, call.positionals]),
1116+
[
1117+
['wait', ['Albums', '17000']],
1118+
['snapshot', []],
1119+
['click', ['112', '242']],
1120+
],
1121+
);
1122+
});
1123+
10531124
test('runReplayScriptFile treats absent Maestro assertNotVisible targets as passing', async () => {
10541125
const calls: CapturedInvocation[] = [];
10551126
const { response } = await runReplayFixture({

src/daemon/post-gesture-stabilization.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type { SessionState } from './types.ts';
1010

1111
const STABILIZATION_DEADLINE_MS = 1_500;
1212
const STABILIZATION_INTERVAL_MS = 200;
13+
const STABILIZATION_MIN_ATTEMPTS = 2;
1314

1415
export function markPostGestureStabilization(
1516
session: SessionState,
@@ -58,7 +59,10 @@ export async function capturePostGestureStabilizedResult<T>(params: {
5859
let previous = params.initial ?? (await capture());
5960
let previousSignature = buildInteractionSurfaceSignature(params.readSnapshot(previous).nodes);
6061

61-
while (Date.now() - startedAt < STABILIZATION_DEADLINE_MS) {
62+
while (
63+
attempts < STABILIZATION_MIN_ATTEMPTS ||
64+
Date.now() - startedAt < STABILIZATION_DEADLINE_MS
65+
) {
6266
await sleep(STABILIZATION_INTERVAL_MS);
6367
attempts += 1;
6468
const current = await capture();

0 commit comments

Comments
 (0)