Skip to content

Commit 6b84e4a

Browse files
committed
fix: support any-match text arrays in wait_for
Implements wait_for text as string | string[] and resolves when any candidate appears. Adds schema/tests/docs updates for #916.
1 parent 7ffdc5e commit 6b84e4a

6 files changed

Lines changed: 118 additions & 13 deletions

File tree

docs/tool-reference.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,11 +191,11 @@
191191

192192
### `wait_for`
193193

194-
**Description:** Wait for the specified text to appear on the selected page.
194+
**Description:** Wait for the specified text to appear on the selected page. You can provide a single text value or a list of texts; the tool resolves when any text appears.
195195

196196
**Parameters:**
197197

198-
- **text** (string) **(required)**: Text to appear on the page
198+
- **text** (string | array of string) **(required)**: Text to appear on the page, or a non-empty list of texts. Resolves when any value appears.
199199
- **timeout** (integer) _(optional)_: Maximum wait time in milliseconds. If set to 0, the default timeout will be used.
200200

201201
---

scripts/generate-docs.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ interface ZodDef {
7474
checks?: ZodCheck[];
7575
values?: string[];
7676
type?: ZodSchema;
77+
options?: ZodSchema[];
7778
innerType?: ZodSchema;
7879
schema?: ZodSchema;
7980
defaultValue?: () => unknown;
@@ -294,6 +295,22 @@ function getZodTypeInfo(schema: ZodSchema): TypeInfo {
294295
result.items = getZodTypeInfo(def.type);
295296
}
296297
break;
298+
case 'ZodUnion':
299+
if (def.options?.length) {
300+
result.type = def.options
301+
.map(option => {
302+
const optionInfo = getZodTypeInfo(option);
303+
if (optionInfo.type === 'array') {
304+
const itemType = optionInfo.items?.type ?? 'unknown';
305+
return `array of ${itemType}`;
306+
}
307+
return optionInfo.type;
308+
})
309+
.join(' | ');
310+
} else {
311+
result.type = 'unknown';
312+
}
313+
break;
297314
default:
298315
result.type = 'unknown';
299316
}

src/McpContext.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -881,15 +881,25 @@ export class McpContext implements Context {
881881
return this.#networkCollector.getIdForResource(request);
882882
}
883883

884-
waitForTextOnPage(text: string, timeout?: number): Promise<Element> {
884+
waitForTextOnPage(
885+
text: string | string[],
886+
timeout?: number,
887+
): Promise<Element> {
885888
const page = this.getSelectedPage();
886889
const frames = page.frames();
890+
const texts = Array.isArray(text) ? text : [text];
891+
892+
if (texts.length === 0) {
893+
throw new Error('At least one text value is required.');
894+
}
887895

888896
let locator = this.#locatorClass.race(
889-
frames.flatMap(frame => [
890-
frame.locator(`aria/${text}`),
891-
frame.locator(`text/${text}`),
892-
]),
897+
frames.flatMap(frame =>
898+
texts.flatMap(value => [
899+
frame.locator(`aria/${value}`),
900+
frame.locator(`text/${value}`),
901+
]),
902+
),
893903
);
894904

895905
if (timeout) {

src/tools/ToolDefinition.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,10 @@ export type Context = Readonly<{
144144
action: () => Promise<unknown>,
145145
options?: {timeout?: number},
146146
): Promise<void>;
147-
waitForTextOnPage(text: string, timeout?: number): Promise<Element>;
147+
waitForTextOnPage(
148+
text: string | string[],
149+
timeout?: number,
150+
): Promise<Element>;
148151
getDevToolsData(): Promise<DevToolsData>;
149152
/**
150153
* Returns a reqid for a cdpRequestId.

src/tools/snapshot.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,17 @@ in the DevTools Elements panel (if any).`,
4343

4444
export const waitFor = defineTool({
4545
name: 'wait_for',
46-
description: `Wait for the specified text to appear on the selected page.`,
46+
description: `Wait for the specified text to appear on the selected page. You can provide a single text value or a list of texts; the tool resolves when any text appears.`,
4747
annotations: {
4848
category: ToolCategory.NAVIGATION,
4949
readOnlyHint: true,
5050
},
5151
schema: {
52-
text: zod.string().describe('Text to appear on the page'),
52+
text: zod
53+
.union([zod.string(), zod.array(zod.string()).nonempty()])
54+
.describe(
55+
'Text to appear on the page, or a non-empty list of texts. Resolves when any value appears.',
56+
),
5357
...timeoutSchema,
5458
},
5559
handler: async (request, response, context) => {
@@ -58,9 +62,15 @@ export const waitFor = defineTool({
5862
request.params.timeout,
5963
);
6064

61-
response.appendResponseLine(
62-
`Element with text "${request.params.text}" found.`,
63-
);
65+
if (Array.isArray(request.params.text)) {
66+
response.appendResponseLine(
67+
`Element matching one of ${JSON.stringify(request.params.text)} found.`,
68+
);
69+
} else {
70+
response.appendResponseLine(
71+
`Element with text "${request.params.text}" found.`,
72+
);
73+
}
6474

6575
response.includeSnapshot();
6676
},

tests/tools/snapshot.test.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@ describe('snapshot', () => {
2020
});
2121
});
2222
describe('browser_wait_for', () => {
23+
it('accepts string or non-empty array text input', () => {
24+
assert.equal(waitFor.schema.text.safeParse('Hello').success, true);
25+
assert.equal(
26+
waitFor.schema.text.safeParse(['Hello', 'World']).success,
27+
true,
28+
);
29+
assert.equal(waitFor.schema.text.safeParse([]).success, false);
30+
});
31+
2332
it('should work', async () => {
2433
await withMcpContext(async (response, context) => {
2534
const page = context.getSelectedPage();
@@ -44,6 +53,62 @@ describe('snapshot', () => {
4453
assert.ok(response.includeSnapshot);
4554
});
4655
});
56+
57+
it('should work with any-match array', async () => {
58+
await withMcpContext(async (response, context) => {
59+
const page = context.getSelectedPage();
60+
61+
await page.setContent(
62+
html`<main><span>Status</span><div>Error</div></main>`,
63+
);
64+
await waitFor.handler(
65+
{
66+
params: {
67+
text: ['Complete', 'Error'],
68+
},
69+
},
70+
response,
71+
context,
72+
);
73+
74+
assert.equal(
75+
response.responseLines[0],
76+
'Element matching one of ["Complete","Error"] found.',
77+
);
78+
assert.ok(response.includeSnapshot);
79+
});
80+
});
81+
82+
it('should work with any-match array when element shows up later', async () => {
83+
await withMcpContext(async (response, context) => {
84+
const page = context.getSelectedPage();
85+
86+
const handlePromise = waitFor.handler(
87+
{
88+
params: {
89+
text: ['Complete', 'Error'],
90+
},
91+
},
92+
response,
93+
context,
94+
);
95+
96+
await page.setContent(
97+
html`<main
98+
><span>Hello</span><span> </span><div>Complete</div></main
99+
>`,
100+
);
101+
102+
await handlePromise;
103+
104+
assert.equal(
105+
response.responseLines[0],
106+
'Element matching one of ["Complete","Error"] found.',
107+
);
108+
assert.ok(response.includeSnapshot);
109+
});
110+
});
111+
47112
it('should work with element that show up later', async () => {
48113
await withMcpContext(async (response, context) => {
49114
const page = context.getSelectedPage();

0 commit comments

Comments
 (0)