Skip to content

Commit c3e57c5

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 59f6477 commit c3e57c5

6 files changed

Lines changed: 119 additions & 14 deletions

File tree

docs/tool-reference.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<!-- AUTO GENERATED DO NOT EDIT - run 'npm run docs' to update-->
22

3-
# Chrome DevTools MCP Tool Reference (~6719 cl100k_base tokens)
3+
# Chrome DevTools MCP Tool Reference (~6742 cl100k_base tokens)
44

55
- **[Input automation](#input-automation)** (8 tools)
66
- [`click`](#click)
@@ -190,11 +190,11 @@
190190

191191
### `wait_for`
192192

193-
**Description:** Wait for the specified text to appear on the selected page.
193+
**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.
194194

195195
**Parameters:**
196196

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

200200
---

scripts/generate-docs.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ interface ZodDef {
7272
checks?: ZodCheck[];
7373
values?: string[];
7474
type?: ZodSchema;
75+
options?: ZodSchema[];
7576
innerType?: ZodSchema;
7677
schema?: ZodSchema;
7778
defaultValue?: () => unknown;
@@ -292,6 +293,22 @@ function getZodTypeInfo(schema: ZodSchema): TypeInfo {
292293
result.items = getZodTypeInfo(def.type);
293294
}
294295
break;
296+
case 'ZodUnion':
297+
if (def.options?.length) {
298+
result.type = def.options
299+
.map(option => {
300+
const optionInfo = getZodTypeInfo(option);
301+
if (optionInfo.type === 'array') {
302+
const itemType = optionInfo.items?.type ?? 'unknown';
303+
return `array of ${itemType}`;
304+
}
305+
return optionInfo.type;
306+
})
307+
.join(' | ');
308+
} else {
309+
result.type = 'unknown';
310+
}
311+
break;
295312
default:
296313
result.type = 'unknown';
297314
}

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)