Skip to content

Commit 90be4d3

Browse files
committed
fix: stabilize maestro post-gesture snapshots
1 parent 86db7e8 commit 90be4d3

18 files changed

Lines changed: 390 additions & 73 deletions

src/compat/maestro/__tests__/runtime-interactions.test.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,14 @@ test('invokeMaestroTapOn resolves mutating taps from the current snapshot', asyn
1111
const selector =
1212
'label="Article by Gandalf" || text="Article by Gandalf" || id="Article by Gandalf"';
1313

14-
const { response, clicks, snapshots } = await runTapOn(selector, () =>
14+
const { response, clicks, clickFlags, snapshots } = await runTapOn(selector, () =>
1515
currentBreadcrumbSnapshot(),
1616
);
1717

1818
expect(response.ok).toBe(true);
1919
expect(snapshots).toBe(1);
2020
expect(clicks).toEqual([['86', '89']]);
21+
expect(clickFlags[0]?.postGestureStabilization).toBe(true);
2122
});
2223

2324
test('invokeMaestroTapOn uses optimized interactive snapshots by default', async () => {
@@ -125,6 +126,7 @@ test('invokeMaestroTapOn clicks explicit React Native overlay controls directly'
125126

126127
test('invokeMaestroSwipeScreen maps horizontal directional swipes to native gesture presets', async () => {
127128
const gestures: string[][] = [];
129+
const gestureFlags: Array<DaemonRequest['flags']> = [];
128130
const response = await invokeMaestroSwipeScreen({
129131
baseReq: {
130132
token: 'test',
@@ -135,6 +137,7 @@ test('invokeMaestroSwipeScreen maps horizontal directional swipes to native gest
135137
invoke: async (req: DaemonRequest): Promise<DaemonResponse> => {
136138
if (req.command === 'gesture') {
137139
gestures.push(req.positionals ?? []);
140+
gestureFlags.push(req.flags);
138141
return { ok: true, data: {} };
139142
}
140143
return { ok: false, error: { code: 'UNEXPECTED_COMMAND', message: req.command } };
@@ -143,6 +146,7 @@ test('invokeMaestroSwipeScreen maps horizontal directional swipes to native gest
143146

144147
expect(response.ok).toBe(true);
145148
expect(gestures).toEqual([['swipe', 'left', '300']]);
149+
expect(gestureFlags[0]?.postGestureStabilization).toBeUndefined();
146150
});
147151

148152
test('invokeMaestroSwipeScreen mirrors horizontal directional swipe presets', async () => {
@@ -169,6 +173,7 @@ test('invokeMaestroSwipeScreen mirrors horizontal directional swipe presets', as
169173

170174
test('invokeMaestroSwipeScreen preserves vertical percentage endpoints', async () => {
171175
const swipes: string[][] = [];
176+
const swipeFlags: Array<DaemonRequest['flags']> = [];
172177
const response = await invokeMaestroSwipeScreen({
173178
baseReq: {
174179
token: 'test',
@@ -182,6 +187,7 @@ test('invokeMaestroSwipeScreen preserves vertical percentage endpoints', async (
182187
}
183188
if (req.command === 'swipe') {
184189
swipes.push(req.positionals ?? []);
190+
swipeFlags.push(req.flags);
185191
return { ok: true, data: {} };
186192
}
187193
return { ok: false, error: { code: 'UNEXPECTED_COMMAND', message: req.command } };
@@ -190,6 +196,7 @@ test('invokeMaestroSwipeScreen preserves vertical percentage endpoints', async (
190196

191197
expect(response.ok).toBe(true);
192198
expect(swipes).toEqual([['200', '600', '200', '280', '300']]);
199+
expect(swipeFlags[0]?.postGestureStabilization).toBe(true);
193200
});
194201

195202
test('invokeMaestroSwipeScreen keeps Android horizontal percentage swipes on the content lane', async () => {
@@ -219,6 +226,7 @@ test('invokeMaestroSwipeScreen keeps Android horizontal percentage swipes on the
219226

220227
test('invokeMaestroTapPointPercent shares percentage point geometry without clamping', async () => {
221228
const clicks: string[][] = [];
229+
const clickFlags: Array<DaemonRequest['flags']> = [];
222230
const response = await invokeMaestroTapPointPercent({
223231
baseReq: {
224232
token: 'test',
@@ -232,6 +240,7 @@ test('invokeMaestroTapPointPercent shares percentage point geometry without clam
232240
}
233241
if (req.command === 'click') {
234242
clicks.push(req.positionals ?? []);
243+
clickFlags.push(req.flags);
235244
return { ok: true, data: {} };
236245
}
237246
return { ok: false, error: { code: 'UNEXPECTED_COMMAND', message: req.command } };
@@ -240,6 +249,7 @@ test('invokeMaestroTapPointPercent shares percentage point geometry without clam
240249

241250
expect(response.ok).toBe(true);
242251
expect(clicks).toEqual([['500', '-80']]);
252+
expect(clickFlags[0]?.postGestureStabilization).toBe(true);
243253
});
244254

245255
function currentBreadcrumbSnapshot(): SnapshotState {
@@ -277,10 +287,12 @@ async function runTapOn(
277287
response: DaemonResponse;
278288
commands: string[];
279289
clicks: string[][];
290+
clickFlags: Array<DaemonRequest['flags']>;
280291
snapshots: number;
281292
}> {
282293
const commands: string[] = [];
283294
const clicks: string[][] = [];
295+
const clickFlags: Array<DaemonRequest['flags']> = [];
284296
let snapshots = 0;
285297
const response = await invokeMaestroTapOn({
286298
baseReq: {
@@ -297,12 +309,13 @@ async function runTapOn(
297309
}
298310
if (req.command === 'click') {
299311
clicks.push(req.positionals ?? []);
312+
clickFlags.push(req.flags);
300313
return { ok: true, data: {} };
301314
}
302315
return { ok: false, error: { code: 'UNEXPECTED_COMMAND', message: req.command } };
303316
},
304317
});
305-
return { response, commands, clicks, snapshots };
318+
return { response, commands, clicks, clickFlags, snapshots };
306319
}
307320

308321
function fullScreenSnapshot(width: number, height: number): SnapshotState {

src/compat/maestro/runtime-interactions.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,10 @@ export async function invokeMaestroTapPointPercent(params: {
147147
...params.baseReq,
148148
command: 'click',
149149
positionals: [String(point.x), String(point.y)],
150+
flags: {
151+
...params.baseReq.flags,
152+
postGestureStabilization: true,
153+
},
150154
});
151155
}
152156

@@ -235,6 +239,10 @@ async function invokeSwipeGesture(
235239
String(swipe.end.y),
236240
...(durationMs ? [durationMs] : []),
237241
],
242+
flags: {
243+
...params.baseReq.flags,
244+
postGestureStabilization: true,
245+
},
238246
});
239247
}
240248

@@ -479,6 +487,7 @@ async function clickMaestroSnapshotTarget(
479487
flags: {
480488
...params.baseReq.flags,
481489
interactionOutcome: { retryOnNoChange: true },
490+
postGestureStabilization: true,
482491
},
483492
});
484493
if (response.ok) clearMaestroVisibleContext(params.scope);
@@ -502,6 +511,7 @@ async function invokeMaestroFuzzyTapOn(
502511
...params.baseReq.flags,
503512
findFirst: true,
504513
interactionOutcome: { retryOnNoChange: true },
514+
postGestureStabilization: true,
505515
},
506516
});
507517
emitDiagnostic({

src/core/dispatch-context.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export type CommandFlags = Omit<CliFlags, DaemonExcludedCliFlag> & {
2020
};
2121
launchArgs?: string[];
2222
maestro?: MaestroRuntimeFlags;
23+
postGestureStabilization?: boolean;
2324
replayBackend?: string;
2425
};
2526

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,12 @@ test('markPendingInteractionOutcome stores retry state only for explicit retry f
8484
assert.equal(longPressSession.pendingInteractionOutcome, undefined);
8585
});
8686

87-
test('stripInternalInteractionOutcomeFlags removes internal retry controls', () => {
87+
test('stripInternalInteractionOutcomeFlags removes internal interaction controls', () => {
8888
assert.deepEqual(
8989
stripInternalInteractionOutcomeFlags({
9090
platform: 'ios',
9191
interactionOutcome: { retryOnNoChange: true },
92+
postGestureStabilization: true,
9293
}),
9394
{ platform: 'ios' },
9495
);

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,22 @@ test('markPostGestureStabilization marks Android swipe sessions', () => {
2828
assert.equal(session.postGestureStabilization?.action, 'swipe');
2929
});
3030

31+
test('markPostGestureStabilization marks gesture swipe sessions', () => {
32+
const session = makeSession('android');
33+
34+
markPostGestureStabilization(session, 'gesture', ['swipe', 'left']);
35+
36+
assert.equal(session.postGestureStabilization?.action, 'gesture');
37+
});
38+
39+
test('markPostGestureStabilization ignores non-swipe gesture sessions', () => {
40+
const session = makeSession('android');
41+
42+
markPostGestureStabilization(session, 'gesture', ['pinch', 'in']);
43+
44+
assert.equal(session.postGestureStabilization, undefined);
45+
});
46+
3147
test('capturePostGestureStabilizedSnapshot retries until rects stop moving', async () => {
3248
vi.useFakeTimers();
3349
const session = makeSession();

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1889,9 +1889,17 @@ test('runReplayScriptFile runs Maestro runFlow.when.visible commands when presen
18891889
assert.deepEqual(calls.find((call) => call.command === 'click')?.flags?.interactionOutcome, {
18901890
retryOnNoChange: true,
18911891
});
1892+
assert.equal(
1893+
calls.find((call) => call.command === 'click')?.flags?.postGestureStabilization,
1894+
true,
1895+
);
18921896
assert.deepEqual(calls.find((call) => call.command === 'find')?.flags?.interactionOutcome, {
18931897
retryOnNoChange: true,
18941898
});
1899+
assert.equal(
1900+
calls.find((call) => call.command === 'find')?.flags?.postGestureStabilization,
1901+
true,
1902+
);
18951903
});
18961904

18971905
test('runReplayScriptFile runs nested Maestro runtime commands inside runFlow.when', async () => {

src/daemon/handlers/__tests__/session.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2135,6 +2135,7 @@ test('prepare ios-runner starts the XCTest runner on an explicit iOS selector',
21352135
expect.objectContaining({ platform: 'ios', id: 'sim-1' }),
21362136
{ command: 'uptime' },
21372137
expect.objectContaining({
2138+
cleanStaleBundles: true,
21382139
logPath: expect.stringMatching(/daemon\.log$/),
21392140
requestId: 'prepare-request',
21402141
}),

0 commit comments

Comments
 (0)