Skip to content

Commit 50242fd

Browse files
committed
fix: resolve android snapshot ci regressions
1 parent 01273bd commit 50242fd

4 files changed

Lines changed: 154 additions & 69 deletions

File tree

src/compat/maestro/runtime-assertions.ts

Lines changed: 84 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -29,55 +29,22 @@ export async function invokeMaestroAssertVisible(params: {
2929
invoke: MaestroRuntimeInvoke;
3030
scope?: ReplayVarScope;
3131
}): Promise<DaemonResponse> {
32-
const [selector, timeoutValue = '5000'] = params.positionals;
33-
if (!selector) {
34-
return errorResponse('INVALID_ARGS', 'assertVisible requires a selector.');
35-
}
36-
const timeoutMs = Number(timeoutValue);
37-
if (!Number.isFinite(timeoutMs) || timeoutMs < 0) {
38-
return errorResponse('INVALID_ARGS', 'assertVisible timeout must be a non-negative number.');
39-
}
32+
const args = readAssertVisibleArgs(params.positionals);
33+
if (!args.ok) return args.response;
4034

4135
const startedAt = Date.now();
42-
const deadlineMs = timeoutMs + MAESTRO_ASSERTION_POLICY.assertVisibleGraceMs;
36+
const deadlineMs = args.timeoutMs + MAESTRO_ASSERTION_POLICY.assertVisibleGraceMs;
4337
let lastResponse: DaemonResponse | undefined;
4438
let capturedAfterDeadline = false;
4539
while (true) {
4640
const captureStartedAt = Date.now();
47-
const response = await captureMaestroRawSnapshot(params);
48-
lastResponse = response;
49-
if (response.ok) {
50-
const snapshot = readSnapshotState(response.data);
51-
if (!snapshot) {
52-
return errorResponse('COMMAND_FAILED', 'Unable to read snapshot data for assertVisible.');
53-
}
54-
const target = resolveVisibleMaestroNodeFromSnapshot(
55-
snapshot,
56-
selector,
57-
readMaestroSelectorPlatform(params.baseReq.flags),
58-
getSnapshotReferenceFrame(snapshot),
59-
);
60-
if (target.ok) {
61-
return {
62-
ok: true,
63-
data: {
64-
selector,
65-
matches: target.matches,
66-
nodeIndex: target.node.index,
67-
nodeType: target.node.type,
68-
nodeLabel: target.node.label,
69-
nodeIdentifier: target.node.identifier,
70-
rect: target.rect,
71-
waitedMs: Date.now() - startedAt,
72-
},
73-
};
74-
}
75-
lastResponse = errorResponse('COMMAND_FAILED', target.message, { selector });
76-
}
41+
const attempt = await readAssertVisibleAttempt(params, args.selector, startedAt);
42+
if (attempt.done) return attempt.response;
43+
lastResponse = attempt.response;
7744

7845
const elapsedMs = Date.now() - startedAt;
7946
if (elapsedMs >= deadlineMs) {
80-
if (!capturedAfterDeadline && captureStartedAt - startedAt < deadlineMs) {
47+
if (shouldCaptureOnceAfterDeadline(capturedAfterDeadline, captureStartedAt, startedAt, deadlineMs)) {
8148
capturedAfterDeadline = true;
8249
continue;
8350
}
@@ -88,13 +55,87 @@ export async function invokeMaestroAssertVisible(params: {
8855

8956
return (
9057
lastResponse ??
91-
errorResponse('COMMAND_FAILED', `Expected visible but did not match: ${selector}`, {
92-
selector,
93-
timeoutMs,
58+
errorResponse('COMMAND_FAILED', `Expected visible but did not match: ${args.selector}`, {
59+
selector: args.selector,
60+
timeoutMs: args.timeoutMs,
9461
})
9562
);
9663
}
9764

65+
function readAssertVisibleArgs(
66+
positionals: string[],
67+
):
68+
| { ok: true; selector: string; timeoutMs: number }
69+
| { ok: false; response: DaemonResponse } {
70+
const [selector, timeoutValue = '5000'] = positionals;
71+
if (!selector) {
72+
return { ok: false, response: errorResponse('INVALID_ARGS', 'assertVisible requires a selector.') };
73+
}
74+
const timeoutMs = Number(timeoutValue);
75+
if (!Number.isFinite(timeoutMs) || timeoutMs < 0) {
76+
return {
77+
ok: false,
78+
response: errorResponse('INVALID_ARGS', 'assertVisible timeout must be a non-negative number.'),
79+
};
80+
}
81+
return { ok: true, selector, timeoutMs };
82+
}
83+
84+
async function readAssertVisibleAttempt(
85+
params: {
86+
baseReq: ReplayBaseRequest;
87+
positionals: string[];
88+
invoke: MaestroRuntimeInvoke;
89+
scope?: ReplayVarScope;
90+
},
91+
selector: string,
92+
startedAt: number,
93+
): Promise<{ done: true; response: DaemonResponse } | { done: false; response: DaemonResponse }> {
94+
const response = await captureMaestroRawSnapshot(params);
95+
if (!response.ok) return { done: false, response };
96+
const snapshot = readSnapshotState(response.data);
97+
if (!snapshot) {
98+
return {
99+
done: true,
100+
response: errorResponse('COMMAND_FAILED', 'Unable to read snapshot data for assertVisible.'),
101+
};
102+
}
103+
const target = resolveVisibleMaestroNodeFromSnapshot(
104+
snapshot,
105+
selector,
106+
readMaestroSelectorPlatform(params.baseReq.flags),
107+
getSnapshotReferenceFrame(snapshot),
108+
);
109+
if (!target.ok) {
110+
return { done: false, response: errorResponse('COMMAND_FAILED', target.message, { selector }) };
111+
}
112+
return {
113+
done: true,
114+
response: {
115+
ok: true,
116+
data: {
117+
selector,
118+
matches: target.matches,
119+
nodeIndex: target.node.index,
120+
nodeType: target.node.type,
121+
nodeLabel: target.node.label,
122+
nodeIdentifier: target.node.identifier,
123+
rect: target.rect,
124+
waitedMs: Date.now() - startedAt,
125+
},
126+
},
127+
};
128+
}
129+
130+
function shouldCaptureOnceAfterDeadline(
131+
capturedAfterDeadline: boolean,
132+
captureStartedAt: number,
133+
startedAt: number,
134+
deadlineMs: number,
135+
): boolean {
136+
return !capturedAfterDeadline && captureStartedAt - startedAt < deadlineMs;
137+
}
138+
98139
export async function invokeMaestroAssertNotVisible(params: {
99140
baseReq: ReplayBaseRequest;
100141
positionals: string[];

src/compat/maestro/runtime-targets.ts

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -378,13 +378,37 @@ function chooseMaestroSnapshotMatch(
378378
promoteTapTarget: boolean,
379379
): MaestroResolvedSnapshotMatch | null {
380380
if (index !== undefined) return candidates[index] ?? null;
381-
const best =
382-
promoteTapTarget && visibleTextQuery
383-
? selectLocalizedMaestroVisibleTextMatch(nodes, candidates, visibleTextQuery) ??
384-
selectBestMaestroSnapshotMatch(candidates, visibleTextQuery)
385-
: selectBestMaestroSnapshotMatch(candidates, visibleTextQuery);
386-
if (!promoteTapTarget || !visibleTextQuery || !best) return best;
387-
return inferMaestroMissingTabSlotMatch(nodes, best, visibleTextQuery) ?? best;
381+
const best = selectPreferredMaestroSnapshotMatch(
382+
nodes,
383+
candidates,
384+
visibleTextQuery,
385+
promoteTapTarget,
386+
);
387+
if (!shouldInferMaestroTabSlot(best, visibleTextQuery, promoteTapTarget)) return best;
388+
return inferMaestroMissingTabSlotMatch(nodes, best, visibleTextQuery!) ?? best;
389+
}
390+
391+
function selectPreferredMaestroSnapshotMatch(
392+
nodes: SnapshotState['nodes'],
393+
candidates: MaestroResolvedSnapshotMatch[],
394+
visibleTextQuery: string | null,
395+
promoteTapTarget: boolean,
396+
): MaestroResolvedSnapshotMatch | null {
397+
if (!promoteTapTarget || !visibleTextQuery) {
398+
return selectBestMaestroSnapshotMatch(candidates, visibleTextQuery);
399+
}
400+
return (
401+
selectLocalizedMaestroVisibleTextMatch(nodes, candidates, visibleTextQuery) ??
402+
selectBestMaestroSnapshotMatch(candidates, visibleTextQuery)
403+
);
404+
}
405+
406+
function shouldInferMaestroTabSlot(
407+
match: MaestroResolvedSnapshotMatch | null,
408+
visibleTextQuery: string | null,
409+
promoteTapTarget: boolean,
410+
): match is MaestroResolvedSnapshotMatch {
411+
return Boolean(promoteTapTarget && visibleTextQuery && match);
388412
}
389413

390414
function selectBestMaestroSnapshotMatch(

src/platforms/android/snapshot-helper-capture.ts

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -145,22 +145,8 @@ async function readFallbackHelperOutputOrThrow(
145145
error: unknown,
146146
): Promise<AndroidSnapshotHelperOutput> {
147147
if (error instanceof AppError && result.exitCode !== 0 && error.details?.helper) throw error;
148-
if (result.exitCode === 0 && resolved.outputPath) {
149-
const resultMetadata =
150-
readHelperMetadataFromInstrumentationOutput(`${result.stdout}\n${result.stderr}`) ??
151-
undefined;
152-
const fileOutput = await readHelperOutputFile(options.adb, resolved.outputPath, {
153-
...(resultMetadata ?? {
154-
outputFormat: ANDROID_SNAPSHOT_HELPER_OUTPUT_FORMAT,
155-
waitForIdleTimeoutMs: resolved.waitForIdleTimeoutMs,
156-
waitForIdleQuietMs: resolved.waitForIdleQuietMs,
157-
timeoutMs: resolved.timeoutMs,
158-
maxDepth: resolved.maxDepth,
159-
maxNodes: resolved.maxNodes,
160-
}),
161-
});
162-
if (fileOutput) return fileOutput;
163-
}
148+
const fileOutput = await readFallbackHelperOutputFile(options, resolved, result);
149+
if (fileOutput) return fileOutput;
164150
throw new AppError(
165151
'COMMAND_FAILED',
166152
result.exitCode === 0
@@ -175,6 +161,33 @@ async function readFallbackHelperOutputOrThrow(
175161
);
176162
}
177163

164+
async function readFallbackHelperOutputFile(
165+
options: AndroidSnapshotHelperCaptureOptions,
166+
resolved: AndroidSnapshotHelperResolvedCaptureOptions,
167+
result: Awaited<ReturnType<AndroidSnapshotHelperCaptureOptions['adb']>>,
168+
): Promise<AndroidSnapshotHelperOutput | undefined> {
169+
if (result.exitCode !== 0 || !resolved.outputPath) return undefined;
170+
return await readHelperOutputFile(
171+
options.adb,
172+
resolved.outputPath,
173+
readHelperMetadataFromInstrumentationOutput(`${result.stdout}\n${result.stderr}`) ??
174+
fallbackAndroidSnapshotHelperMetadata(resolved),
175+
);
176+
}
177+
178+
function fallbackAndroidSnapshotHelperMetadata(
179+
resolved: AndroidSnapshotHelperResolvedCaptureOptions,
180+
): AndroidSnapshotHelperMetadata {
181+
return {
182+
outputFormat: ANDROID_SNAPSHOT_HELPER_OUTPUT_FORMAT,
183+
waitForIdleTimeoutMs: resolved.waitForIdleTimeoutMs,
184+
waitForIdleQuietMs: resolved.waitForIdleQuietMs,
185+
timeoutMs: resolved.timeoutMs,
186+
maxDepth: resolved.maxDepth,
187+
maxNodes: resolved.maxNodes,
188+
};
189+
}
190+
178191
async function readHelperOutputFile(
179192
adb: AndroidSnapshotHelperCaptureOptions['adb'],
180193
outputPath: string,

src/platforms/android/ui-hierarchy.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,13 @@ function readNodeAttributes(node: string): Omit<AndroidUiNodeMetadata, 'rect'> {
193193
if (raw === null) return undefined;
194194
return raw === 'true';
195195
};
196+
const optionalBoolAttr = <Key extends keyof AndroidUiNodeMetadata>(
197+
key: Key,
198+
name: string,
199+
): Pick<AndroidUiNodeMetadata, Key> | {} => {
200+
const value = boolAttr(name);
201+
return value === undefined ? {} : { [key]: value };
202+
};
196203
return {
197204
text: getAttr('text'),
198205
desc: getAttr('content-desc'),
@@ -205,9 +212,9 @@ function readNodeAttributes(node: string): Omit<AndroidUiNodeMetadata, 'rect'> {
205212
focusable: boolAttr('focusable'),
206213
focused: boolAttr('focused'),
207214
password: boolAttr('password'),
208-
scrollable: boolAttr('scrollable'),
209-
canScrollForward: boolAttr('can-scroll-forward'),
210-
canScrollBackward: boolAttr('can-scroll-backward'),
215+
...optionalBoolAttr('scrollable', 'scrollable'),
216+
...optionalBoolAttr('canScrollForward', 'can-scroll-forward'),
217+
...optionalBoolAttr('canScrollBackward', 'can-scroll-backward'),
211218
};
212219
}
213220

0 commit comments

Comments
 (0)