Skip to content

Commit 5546d2c

Browse files
committed
test: handle keyboard prompt in JS API integration
1 parent 94db823 commit 5546d2c

1 file changed

Lines changed: 145 additions & 0 deletions

File tree

scripts/integration/js-api.mjs

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ async function main() {
224224
await measuredStep("JS focus URL and type", async () => {
225225
await retryAsync(
226226
async () => {
227+
await resolveKnownSystemPrompts("JS focus URL and type before focus");
227228
await session.tapElement(
228229
simulatorUDID,
229230
{ id: "fixture.message" },
@@ -234,9 +235,12 @@ async function main() {
234235
durationMs: 30,
235236
},
236237
);
238+
await resolveKnownSystemPrompts("JS focus URL and type after tap");
237239
await session.openUrl(simulatorUDID, fixtureFocusUrl);
240+
await resolveKnownSystemPrompts("JS focus URL and type after open URL");
238241
await expectFixtureText("Message Focused", { timeoutMs: 20_000 });
239242
await sleep(1_000);
243+
await resolveKnownSystemPrompts("JS focus URL and type before type");
240244
await session.batch(simulatorUDID, [
241245
{ action: "type", text: "agent-ready", delayMs: 20 },
242246
]);
@@ -363,6 +367,147 @@ async function expectElementContains(selector, text, options = {}) {
363367
);
364368
}
365369

370+
async function resolveKnownSystemPrompts(label) {
371+
await resolveOpenUrlPrompt(label);
372+
await resolveKeyboardTipPrompt(label);
373+
}
374+
375+
async function resolveOpenUrlPrompt(label) {
376+
const matches = await safeQuery({ label: "Open" }, { maxDepth: 6 });
377+
if (matches.length === 0) {
378+
return;
379+
}
380+
console.log(`[prompt] handling open-url prompt during ${label}`);
381+
await session.tapElement(
382+
simulatorUDID,
383+
{ label: "Open" },
384+
{
385+
source: "native-ax",
386+
maxDepth: 6,
387+
waitTimeoutMs: 2_000,
388+
durationMs: 80,
389+
},
390+
);
391+
await sleep(500);
392+
}
393+
394+
async function resolveKeyboardTipPrompt(label) {
395+
const snapshot = await safeQuery({}, { maxDepth: 8 });
396+
if (!looksLikeKeyboardTipQuery(snapshot)) {
397+
return;
398+
}
399+
400+
console.log(`[prompt] handling keyboard-tip prompt during ${label}`);
401+
const target = keyboardTipContinueTapTarget(snapshot);
402+
if (target) {
403+
await session.tap(simulatorUDID, target.x, target.y);
404+
} else {
405+
await session.tapElement(
406+
simulatorUDID,
407+
{ label: "Continue" },
408+
{
409+
source: "native-ax",
410+
maxDepth: 8,
411+
waitTimeoutMs: 2_000,
412+
durationMs: 80,
413+
},
414+
);
415+
}
416+
await sleep(500);
417+
}
418+
419+
async function safeQuery(selector, options = {}) {
420+
try {
421+
return await session.query(simulatorUDID, selector, {
422+
source: "native-ax",
423+
...options,
424+
});
425+
} catch {
426+
return [];
427+
}
428+
}
429+
430+
function looksLikeKeyboardTipQuery(snapshot) {
431+
const text = JSON.stringify(snapshot);
432+
return /Speed up your typing/i.test(text) && /\bContinue\b/.test(text);
433+
}
434+
435+
function keyboardTipContinueTapTarget(snapshot) {
436+
const nodes = compactQueryNodes(snapshot);
437+
const rootFrame = nodes
438+
.map((node) => node.frame)
439+
.filter(validFrame)
440+
.sort((a, b) => b.width * b.height - a.width * a.height)[0];
441+
const continueButton =
442+
nodes.find(
443+
(node) =>
444+
node.label === "Continue" &&
445+
node.id !== "fixture.continue" &&
446+
String(node.role ?? "")
447+
.toLowerCase()
448+
.includes("button") &&
449+
validFrame(node.frame),
450+
) ??
451+
nodes.find(
452+
(node) =>
453+
node.label === "Continue" &&
454+
String(node.role ?? "")
455+
.toLowerCase()
456+
.includes("button") &&
457+
validFrame(node.frame),
458+
);
459+
460+
if (!rootFrame || !continueButton?.frame) {
461+
return null;
462+
}
463+
464+
return {
465+
x:
466+
(continueButton.frame.x + continueButton.frame.width / 2 - rootFrame.x) /
467+
rootFrame.width,
468+
y:
469+
(continueButton.frame.y + continueButton.frame.height / 2 - rootFrame.y) /
470+
rootFrame.height,
471+
};
472+
}
473+
474+
function compactQueryNodes(snapshot) {
475+
const nodes = [];
476+
const visit = (node) => {
477+
if (!node || typeof node !== "object") {
478+
return;
479+
}
480+
if (
481+
"role" in node ||
482+
"id" in node ||
483+
"label" in node ||
484+
"value" in node ||
485+
"frame" in node
486+
) {
487+
nodes.push(node);
488+
}
489+
for (const child of Array.isArray(node.children) ? node.children : []) {
490+
visit(child);
491+
}
492+
};
493+
for (const match of Array.isArray(snapshot) ? snapshot : []) {
494+
visit(match);
495+
}
496+
return nodes;
497+
}
498+
499+
function validFrame(frame) {
500+
return (
501+
frame &&
502+
Number.isFinite(frame.x) &&
503+
Number.isFinite(frame.y) &&
504+
Number.isFinite(frame.width) &&
505+
Number.isFinite(frame.height) &&
506+
frame.width > 0 &&
507+
frame.height > 0
508+
);
509+
}
510+
366511
function buildFixtureApp() {
367512
return buildCachedFixtureApp({
368513
root,

0 commit comments

Comments
 (0)