Skip to content

Commit cd3eced

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 5cedcaa commit cd3eced

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 (~6661 cl100k_base tokens)
3+
# Chrome DevTools MCP Tool Reference (~6742 cl100k_base tokens)
44

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

190190
### `wait_for`
191191

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

194194
**Parameters:**
195195

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

199199
---

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
@@ -825,15 +825,25 @@ export class McpContext implements Context {
825825
return this.#networkCollector.getIdForResource(request);
826826
}
827827

828-
waitForTextOnPage(text: string, timeout?: number): Promise<Element> {
828+
waitForTextOnPage(
829+
text: string | string[],
830+
timeout?: number,
831+
): Promise<Element> {
829832
const page = this.getSelectedPage();
830833
const frames = page.frames();
834+
const texts = Array.isArray(text) ? text : [text];
835+
836+
if (texts.length === 0) {
837+
throw new Error('At least one text value is required.');
838+
}
831839

832840
let locator = this.#locatorClass.race(
833-
frames.flatMap(frame => [
834-
frame.locator(`aria/${text}`),
835-
frame.locator(`text/${text}`),
836-
]),
841+
frames.flatMap(frame =>
842+
texts.flatMap(value => [
843+
frame.locator(`aria/${value}`),
844+
frame.locator(`text/${value}`),
845+
]),
846+
),
837847
);
838848

839849
if (timeout) {

src/tools/ToolDefinition.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,10 @@ export type Context = Readonly<{
143143
action: () => Promise<unknown>,
144144
options?: {timeout?: number},
145145
): Promise<void>;
146-
waitForTextOnPage(text: string, timeout?: number): Promise<Element>;
146+
waitForTextOnPage(
147+
text: string | string[],
148+
timeout?: number,
149+
): Promise<Element>;
147150
getDevToolsData(): Promise<DevToolsData>;
148151
/**
149152
* 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)