Skip to content

Commit f142240

Browse files
committed
check if element is selector through ax node and extract method
1 parent e784581 commit f142240

File tree

3 files changed

+58
-37
lines changed

3 files changed

+58
-37
lines changed

src/McpContext.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,10 @@ export class McpContext implements Context {
260260
return page.getDefaultNavigationTimeout();
261261
}
262262

263+
getAXNodeByUid(uid: string) {
264+
return this.#textSnapshot?.idToNode.get(uid);
265+
}
266+
263267
async getElementByUid(uid: string): Promise<ElementHandle<Element>> {
264268
if (!this.#textSnapshot?.idToNode.size) {
265269
throw new Error(

src/tools/ToolDefinition.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import type {Dialog, ElementHandle, Page} from 'puppeteer-core';
88
import z from 'zod';
99

10+
import type {TextSnapshotNode} from '../McpContext.js';
1011
import type {TraceResult} from '../trace-processing/parse.js';
1112

1213
import type {ToolCategories} from './categories.js';
@@ -68,6 +69,7 @@ export type Context = Readonly<{
6869
closePage(pageIdx: number): Promise<void>;
6970
setSelectedPageIdx(idx: number): void;
7071
getElementByUid(uid: string): Promise<ElementHandle<Element>>;
72+
getAXNodeByUid(uid: string): TextSnapshotNode | undefined;
7173
setNetworkConditions(conditions: string | null): void;
7274
setCpuThrottlingRate(rate: number): void;
7375
saveTemporaryFile(

src/tools/input.ts

Lines changed: 52 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import type {ElementHandle} from 'puppeteer-core';
88
import z from 'zod';
99

10+
import type {McpContext} from '../McpContext.js';
11+
1012
import {ToolCategories} from './categories.js';
1113
import {defineTool} from './ToolDefinition.js';
1214

@@ -78,6 +80,40 @@ export const hover = defineTool({
7880
},
7981
});
8082

83+
async function fillFormElement(
84+
uid: string,
85+
value: string,
86+
context: McpContext,
87+
) {
88+
const handle = await context.getElementByUid(uid);
89+
try {
90+
// The AXNode for an option doesn't contain its `value`. We set text content of the option as value.
91+
// If the form is a combobox, we need to find the correct option by its text value.
92+
// To do that, loop through the children while checking which child's text matches the requested value (requested value is actually the text content).
93+
// When the correct option is found, use the element handle to get the real value.
94+
const aXNode = context.getAXNodeByUid(uid);
95+
if (aXNode && aXNode.role === 'combobox' && aXNode.children) {
96+
for (const child of aXNode.children) {
97+
if (child.role === 'option' && child.name === value && child.value) {
98+
const childHandle = await child.elementHandle();
99+
if (childHandle) {
100+
const childValueHandle = await childHandle.getProperty('value');
101+
const childValue = await childValueHandle.jsonValue();
102+
if (childValue) {
103+
await handle.asLocator().fill(childValue.toString());
104+
}
105+
break;
106+
}
107+
}
108+
}
109+
} else {
110+
await handle.asLocator().fill(value);
111+
}
112+
} finally {
113+
void handle.dispose();
114+
}
115+
}
116+
81117
export const fill = defineTool({
82118
name: 'fill',
83119
description: `Type text into a input, text area or select an option from a <select> element.`,
@@ -94,35 +130,15 @@ export const fill = defineTool({
94130
value: z.string().describe('The value to fill in'),
95131
},
96132
handler: async (request, response, context) => {
97-
const handle = await context.getElementByUid(request.params.uid);
98-
try {
99-
await context.waitForEventsAfterAction(async () => {
100-
// The AXNode for an option doesn't contain its `value`. We set text content of the option as value.
101-
// To fill the form, get the correct option by its text value.
102-
const isSelectElement = await handle.evaluate(
103-
el => el.tagName === 'SELECT',
104-
);
105-
if (isSelectElement) {
106-
const value = await handle.evaluate((el, text) => {
107-
const option = Array.from((el as HTMLSelectElement).options).find(
108-
option => option.text === text,
109-
);
110-
if (option) {
111-
return option.value;
112-
}
113-
return request.params.value;
114-
}, request.params.value);
115-
116-
await handle.asLocator().fill(value);
117-
} else {
118-
await handle.asLocator().fill(request.params.value);
119-
}
120-
});
121-
response.appendResponseLine(`Successfully filled out the element`);
122-
response.setIncludeSnapshot(true);
123-
} finally {
124-
void handle.dispose();
125-
}
133+
await context.waitForEventsAfterAction(async () => {
134+
await fillFormElement(
135+
request.params.uid,
136+
request.params.value,
137+
context as McpContext,
138+
);
139+
});
140+
response.appendResponseLine(`Successfully filled out the element`);
141+
response.setIncludeSnapshot(true);
126142
},
127143
});
128144

@@ -174,14 +190,13 @@ export const fillForm = defineTool({
174190
},
175191
handler: async (request, response, context) => {
176192
for (const element of request.params.elements) {
177-
const handle = await context.getElementByUid(element.uid);
178-
try {
179-
await context.waitForEventsAfterAction(async () => {
180-
await handle.asLocator().fill(element.value);
181-
});
182-
} finally {
183-
void handle.dispose();
184-
}
193+
await context.waitForEventsAfterAction(async () => {
194+
await fillFormElement(
195+
element.uid,
196+
element.value,
197+
context as McpContext,
198+
);
199+
});
185200
}
186201
response.appendResponseLine(`Successfully filled out the form`);
187202
response.setIncludeSnapshot(true);

0 commit comments

Comments
 (0)