Skip to content

Commit 7f2c2b7

Browse files
authored
refactor: converge Maestro assertion waits (#625)
* refactor: converge Maestro assertion waits * fix: retry maestro assertVisible snapshot failures
1 parent 10fc86f commit 7f2c2b7

5 files changed

Lines changed: 227 additions & 99 deletions

File tree

src/compat/maestro/__tests__/replay-flow.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,21 @@ test('parseMaestroReplayFlow preserves selector state and absolute swipe command
373373
assert.deepEqual(parsed.actionLines, [3, 6]);
374374
});
375375

376+
test('parseMaestroReplayFlow maps extendedWaitUntil.notVisible through Maestro visibility assertions', () => {
377+
const parsed = parseMaestroReplayFlow(`appId: com.callstack.agentdevicelab
378+
---
379+
- extendedWaitUntil:
380+
notVisible:
381+
text: Loading
382+
timeout: 1200
383+
`);
384+
385+
assert.deepEqual(
386+
parsed.actions.map((entry) => [entry.command, entry.positionals]),
387+
[['__maestroAssertNotVisible', ['label="Loading" || text="Loading" || id="Loading"', '1200']]],
388+
);
389+
});
390+
376391
test('parseMaestroReplayFlow rejects deferred Maestro utility commands loudly', () => {
377392
assert.throws(
378393
() => parseMaestroReplayFlow('---\n- assertTrue: "${READY}"\n'),

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

Lines changed: 86 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import assert from 'node:assert/strict';
22
import { afterEach, test, vi } from 'vitest';
3-
import { invokeMaestroAssertNotVisible, invokeMaestroAssertVisible } from '../runtime-assertions.ts';
3+
import {
4+
invokeMaestroAssertNotVisible,
5+
invokeMaestroAssertVisible,
6+
} from '../runtime-assertions.ts';
47
import type { DaemonRequest, DaemonResponse } from '../../../daemon/types.ts';
58

69
afterEach(() => {
710
vi.restoreAllMocks();
11+
vi.useRealTimers();
812
});
913

1014
test('invokeMaestroAssertVisible takes a terminal snapshot when the last miss started before the deadline', async () => {
@@ -56,6 +60,56 @@ test('invokeMaestroAssertVisible takes a terminal snapshot when the last miss st
5660
}
5761
});
5862

63+
test('invokeMaestroAssertVisible retries transient snapshot failures until a later match', async () => {
64+
vi.useFakeTimers();
65+
66+
let snapshots = 0;
67+
const responsePromise = invokeMaestroAssertVisible({
68+
baseReq: {
69+
token: 't',
70+
session: 's',
71+
flags: { platform: 'android' },
72+
},
73+
positionals: ['label="Ready"', '1000'],
74+
invoke: async (): Promise<DaemonResponse> => {
75+
snapshots += 1;
76+
if (snapshots === 1) {
77+
return {
78+
ok: false,
79+
error: { code: 'SNAPSHOT_FAILED', message: 'Snapshot temporarily unavailable.' },
80+
};
81+
}
82+
return {
83+
ok: true,
84+
data: {
85+
createdAt: 2,
86+
nodes: [
87+
{
88+
index: 1,
89+
ref: 'e1',
90+
type: 'android.widget.TextView',
91+
label: 'Ready',
92+
rect: { x: 10, y: 20, width: 120, height: 40 },
93+
depth: 8,
94+
},
95+
],
96+
},
97+
};
98+
},
99+
});
100+
101+
await vi.advanceTimersByTimeAsync(250);
102+
const response = await responsePromise;
103+
104+
assert.equal(response.ok, true);
105+
assert.equal(snapshots, 2);
106+
if (response.ok) {
107+
assert.ok(response.data);
108+
assert.equal(response.data.nodeLabel, 'Ready');
109+
assert.equal(response.data.waitedMs, 250);
110+
}
111+
});
112+
59113
test('invokeMaestroAssertNotVisible passes after a slow hidden sample exhausts the timeout', async () => {
60114
vi.spyOn(Date, 'now').mockReturnValueOnce(0).mockReturnValueOnce(0).mockReturnValueOnce(3500);
61115

@@ -80,9 +134,10 @@ test('invokeMaestroAssertNotVisible passes after a slow hidden sample exhausts t
80134
});
81135

82136
assert.equal(response.ok, true);
83-
assert.deepEqual(calls.map((call) => [call.command, call.positionals]), [
84-
['snapshot', []],
85-
]);
137+
assert.deepEqual(
138+
calls.map((call) => [call.command, call.positionals]),
139+
[['snapshot', []]],
140+
);
86141
if (response.ok) {
87142
assert.ok(response.data);
88143
assert.equal(response.data.stableSamples, 1);
@@ -125,3 +180,30 @@ test('invokeMaestroAssertNotVisible ignores matched nodes without visible rects'
125180
assert.equal(response.data.stableSamples, 1);
126181
}
127182
});
183+
184+
test('invokeMaestroAssertNotVisible accepts timeout overrides for short extended waits', async () => {
185+
vi.spyOn(Date, 'now').mockReturnValueOnce(0).mockReturnValueOnce(0).mockReturnValueOnce(300);
186+
187+
const response = await invokeMaestroAssertNotVisible({
188+
baseReq: {
189+
token: 't',
190+
session: 's',
191+
flags: {},
192+
},
193+
positionals: ['id="toast"', '1'],
194+
invoke: async (): Promise<DaemonResponse> => ({
195+
ok: true,
196+
data: {
197+
createdAt: 1,
198+
nodes: [],
199+
},
200+
}),
201+
});
202+
203+
assert.equal(response.ok, true);
204+
if (response.ok) {
205+
assert.ok(response.data);
206+
assert.equal(response.data.stableSamples, 1);
207+
assert.equal(response.data.timeoutMs, 1);
208+
}
209+
});

src/compat/maestro/interactions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ export function convertExtendedWaitUntil(
142142
const selector = maestroSelector(target, 'extendedWaitUntil', [], context);
143143
const timeoutMs = String(readTimeoutMs(value, 30000));
144144
if (value.notVisible !== undefined) {
145-
return [action('wait', [timeoutMs]), action('is', ['hidden', selector])];
145+
return [action(MAESTRO_RUNTIME_COMMAND.assertNotVisible, [selector, timeoutMs])];
146146
}
147147
return [action(MAESTRO_RUNTIME_COMMAND.assertVisible, [selector, timeoutMs])];
148148
}

0 commit comments

Comments
 (0)