chore: test list tabs (#208)

This commit is contained in:
Pavel Feldman 2025-04-17 00:58:02 -07:00 committed by GitHub
parent 7e4a964b0a
commit 4b261286bf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 66 additions and 58 deletions

View File

@ -119,7 +119,10 @@ export class Context {
async run(tool: Tool, params: Record<string, unknown> | undefined) { async run(tool: Tool, params: Record<string, unknown> | undefined) {
// Tab management is done outside of the action() call. // Tab management is done outside of the action() call.
const toolResult = await tool.handle(this, params); const toolResult = await tool.handle(this, params);
const { code, action, waitForNetwork, captureSnapshot } = toolResult; const { code, action, waitForNetwork, captureSnapshot, resultOverride } = toolResult;
if (resultOverride)
return resultOverride;
if (!this._currentTab) { if (!this._currentTab) {
return { return {
@ -132,12 +135,12 @@ export class Context {
const tab = this.currentTabOrDie(); const tab = this.currentTabOrDie();
// TODO: race against modal dialogs to resolve clicks. // TODO: race against modal dialogs to resolve clicks.
let actionResult: { content?: (ImageContent | TextContent)[] }; let actionResult: { content?: (ImageContent | TextContent)[] } | undefined;
try { try {
if (waitForNetwork) if (waitForNetwork)
actionResult = await waitForCompletion(tab.page, () => action()) ?? undefined; actionResult = await waitForCompletion(tab.page, async () => action?.()) ?? undefined;
else else
actionResult = await action(); actionResult = await action?.() ?? undefined;
} finally { } finally {
if (captureSnapshot) if (captureSnapshot)
await tab.captureSnapshot(); await tab.captureSnapshot();
@ -163,11 +166,16 @@ ${code.join('\n')}
if (this.tabs().length > 1) if (this.tabs().length > 1)
result.push(await this.listTabsMarkdown(), ''); result.push(await this.listTabsMarkdown(), '');
if (tab.hasSnapshot()) { if (this.tabs().length > 1)
if (this.tabs().length > 1) result.push('### Current tab');
result.push('### Current tab');
result.push(
`- Page URL: ${tab.page.url()}`,
`- Page Title: ${await tab.page.title()}`
);
if (captureSnapshot && tab.hasSnapshot())
result.push(tab.snapshotOrDie().text()); result.push(tab.snapshotOrDie().text());
}
const content = actionResult?.content ?? []; const content = actionResult?.content ?? [];
@ -338,19 +346,12 @@ class PageSnapshot {
private async _build(page: playwright.Page) { private async _build(page: playwright.Page) {
const yamlDocument = await this._snapshotFrame(page); const yamlDocument = await this._snapshotFrame(page);
const lines = []; this._text = [
lines.push( `- Page Snapshot`,
`- Page URL: ${page.url()}`, '```yaml',
`- Page Title: ${await page.title()}` yamlDocument.toString({ indentSeq: false }).trim(),
); '```',
lines.push( ].join('\n');
`- Page Snapshot`,
'```yaml',
yamlDocument.toString().trim(),
'```',
''
);
this._text = lines.join('\n');
} }
private async _snapshotFrame(frame: playwright.Page | playwright.FrameLocator) { private async _snapshotFrame(frame: playwright.Page | playwright.FrameLocator) {

View File

@ -37,7 +37,6 @@ const wait: ToolFactory = captureSnapshot => ({
await new Promise(f => setTimeout(f, Math.min(10000, validatedParams.time * 1000))); await new Promise(f => setTimeout(f, Math.min(10000, validatedParams.time * 1000)));
return { return {
code: [`// Waited for ${validatedParams.time} seconds`], code: [`// Waited for ${validatedParams.time} seconds`],
action: async () => ({}),
captureSnapshot, captureSnapshot,
waitForNetwork: false, waitForNetwork: false,
}; };
@ -59,7 +58,6 @@ const close: Tool = {
await context.close(); await context.close();
return { return {
code: [`// Internal to close the page`], code: [`// Internal to close the page`],
action: async () => ({}),
captureSnapshot: false, captureSnapshot: false,
waitForNetwork: false, waitForNetwork: false,
}; };
@ -91,7 +89,6 @@ const resize: ToolFactory = captureSnapshot => ({
const action = async () => { const action = async () => {
await tab.page.setViewportSize({ width: validatedParams.width, height: validatedParams.height }); await tab.page.setViewportSize({ width: validatedParams.width, height: validatedParams.height });
return {};
}; };
return { return {

View File

@ -45,7 +45,6 @@ const uploadFile: ToolFactory = captureSnapshot => ({
const action = async () => { const action = async () => {
await modalState.fileChooser.setFiles(validatedParams.paths); await modalState.fileChooser.setFiles(validatedParams.paths);
context.clearModalState(modalState); context.clearModalState(modalState);
return {};
}; };
return { return {

View File

@ -49,7 +49,6 @@ const install: Tool = {
}); });
return { return {
code: [`// Browser ${channel} installed`], code: [`// Browser ${channel} installed`],
action: async () => ({}),
captureSnapshot: false, captureSnapshot: false,
waitForNetwork: false, waitForNetwork: false,
}; };

View File

@ -41,7 +41,7 @@ const pressKey: ToolFactory = captureSnapshot => ({
`await page.keyboard.press('${validatedParams.key}');`, `await page.keyboard.press('${validatedParams.key}');`,
]; ];
const action = () => tab.page.keyboard.press(validatedParams.key).then(() => ({})); const action = () => tab.page.keyboard.press(validatedParams.key);
return { return {
code, code,

View File

@ -44,7 +44,6 @@ const navigate: ToolFactory = captureSnapshot => ({
return { return {
code, code,
action: async () => ({}),
captureSnapshot, captureSnapshot,
waitForNetwork: false, waitForNetwork: false,
}; };
@ -71,7 +70,6 @@ const goBack: ToolFactory = captureSnapshot => ({
return { return {
code, code,
action: async () => ({}),
captureSnapshot, captureSnapshot,
waitForNetwork: false, waitForNetwork: false,
}; };
@ -96,7 +94,6 @@ const goForward: ToolFactory = captureSnapshot => ({
]; ];
return { return {
code, code,
action: async () => ({}),
captureSnapshot, captureSnapshot,
waitForNetwork: false, waitForNetwork: false,
}; };

View File

@ -47,7 +47,7 @@ const pdf: Tool = {
return { return {
code, code,
action: async () => tab.page.pdf({ path: fileName }).then(() => ({})), action: async () => tab.page.pdf({ path: fileName }).then(() => {}),
captureSnapshot: false, captureSnapshot: false,
waitForNetwork: false, waitForNetwork: false,
}; };

View File

@ -77,7 +77,7 @@ const moveMouse: Tool = {
`// Move mouse to (${validatedParams.x}, ${validatedParams.y})`, `// Move mouse to (${validatedParams.x}, ${validatedParams.y})`,
`await page.mouse.move(${validatedParams.x}, ${validatedParams.y});`, `await page.mouse.move(${validatedParams.x}, ${validatedParams.y});`,
]; ];
const action = () => tab.page.mouse.move(validatedParams.x, validatedParams.y).then(() => ({})); const action = () => tab.page.mouse.move(validatedParams.x, validatedParams.y);
return { return {
code, code,
action, action,
@ -113,7 +113,6 @@ const click: Tool = {
await tab.page.mouse.move(validatedParams.x, validatedParams.y); await tab.page.mouse.move(validatedParams.x, validatedParams.y);
await tab.page.mouse.down(); await tab.page.mouse.down();
await tab.page.mouse.up(); await tab.page.mouse.up();
return {};
}; };
return { return {
code, code,
@ -157,7 +156,6 @@ const drag: Tool = {
await tab.page.mouse.down(); await tab.page.mouse.down();
await tab.page.mouse.move(validatedParams.endX, validatedParams.endY); await tab.page.mouse.move(validatedParams.endX, validatedParams.endY);
await tab.page.mouse.up(); await tab.page.mouse.up();
return {};
}; };
return { return {
@ -196,7 +194,6 @@ const type: Tool = {
await tab.page.keyboard.type(validatedParams.text); await tab.page.keyboard.type(validatedParams.text);
if (validatedParams.submit) if (validatedParams.submit)
await tab.page.keyboard.press('Enter'); await tab.page.keyboard.press('Enter');
return {};
}; };
if (validatedParams.submit) { if (validatedParams.submit) {

View File

@ -40,7 +40,6 @@ const snapshot: Tool = {
return { return {
code: [`// <internal code to capture accessibility snapshot>`], code: [`// <internal code to capture accessibility snapshot>`],
action: async () => ({}),
captureSnapshot: true, captureSnapshot: true,
waitForNetwork: false, waitForNetwork: false,
}; };
@ -72,7 +71,7 @@ const click: Tool = {
return { return {
code, code,
action: () => locator.click().then(() => ({})), action: () => locator.click(),
captureSnapshot: true, captureSnapshot: true,
waitForNetwork: true, waitForNetwork: true,
}; };
@ -107,7 +106,7 @@ const drag: Tool = {
return { return {
code, code,
action: () => startLocator.dragTo(endLocator).then(() => ({})), action: () => startLocator.dragTo(endLocator),
captureSnapshot: true, captureSnapshot: true,
waitForNetwork: true, waitForNetwork: true,
}; };
@ -134,7 +133,7 @@ const hover: Tool = {
return { return {
code, code,
action: () => locator.hover().then(() => ({})), action: () => locator.hover(),
captureSnapshot: true, captureSnapshot: true,
waitForNetwork: true, waitForNetwork: true,
}; };
@ -181,7 +180,7 @@ const type: Tool = {
return { return {
code, code,
action: () => steps.reduce((acc, step) => acc.then(step), Promise.resolve()).then(() => ({})), action: () => steps.reduce((acc, step) => acc.then(step), Promise.resolve()),
captureSnapshot: true, captureSnapshot: true,
waitForNetwork: true, waitForNetwork: true,
}; };
@ -212,7 +211,7 @@ const selectOption: Tool = {
return { return {
code, code,
action: () => locator.selectOption(validatedParams.values).then(() => ({})), action: () => locator.selectOption(validatedParams.values).then(() => {}),
captureSnapshot: true, captureSnapshot: true,
waitForNetwork: true, waitForNetwork: true,
}; };

View File

@ -28,12 +28,18 @@ const listTabs: Tool = {
inputSchema: zodToJsonSchema(z.object({})), inputSchema: zodToJsonSchema(z.object({})),
}, },
handle: async () => { handle: async context => {
await context.ensureTab();
return { return {
code: [`// <internal code to list tabs>`], code: [`// <internal code to list tabs>`],
action: async () => ({}),
captureSnapshot: false, captureSnapshot: false,
waitForNetwork: false, waitForNetwork: false,
resultOverride: {
content: [{
type: 'text',
text: await context.listTabsMarkdown(),
}],
},
}; };
}, },
}; };
@ -60,7 +66,6 @@ const selectTab: ToolFactory = captureSnapshot => ({
return { return {
code, code,
action: async () => ({}),
captureSnapshot, captureSnapshot,
waitForNetwork: false waitForNetwork: false
}; };
@ -91,7 +96,6 @@ const newTab: ToolFactory = captureSnapshot => ({
]; ];
return { return {
code, code,
action: async () => ({}),
captureSnapshot, captureSnapshot,
waitForNetwork: false waitForNetwork: false
}; };
@ -119,7 +123,6 @@ const closeTab: ToolFactory = captureSnapshot => ({
]; ];
return { return {
code, code,
action: async () => ({}),
captureSnapshot, captureSnapshot,
waitForNetwork: false waitForNetwork: false
}; };

View File

@ -36,9 +36,10 @@ export type ModalState = FileUploadModalState;
export type ToolResult = { export type ToolResult = {
code: string[]; code: string[];
action: () => Promise<{ content?: (ImageContent | TextContent)[] }>; action?: () => Promise<{ content?: (ImageContent | TextContent)[] } | undefined | void>;
captureSnapshot: boolean; captureSnapshot: boolean;
waitForNetwork: boolean; waitForNetwork: boolean;
resultOverride?: { content?: (ImageContent | TextContent)[] };
}; };
export type Tool = { export type Tool = {

View File

@ -96,8 +96,8 @@ await page.getByRole('combobox').selectOption(['bar']);
- Page Snapshot - Page Snapshot
\`\`\`yaml \`\`\`yaml
- combobox [ref=s2e3]: - combobox [ref=s2e3]:
- option "Foo" [ref=s2e4] - option "Foo" [ref=s2e4]
- option "Bar" [selected] [ref=s2e5] - option "Bar" [selected] [ref=s2e5]
\`\`\` \`\`\`
`); `);
}); });
@ -129,9 +129,9 @@ await page.getByRole('listbox').selectOption(['bar', 'baz']);
- Page Snapshot - Page Snapshot
\`\`\`yaml \`\`\`yaml
- listbox [ref=s2e3]: - listbox [ref=s2e3]:
- option "Foo" [ref=s2e4] - option "Foo" [ref=s2e4]
- option "Bar" [selected] [ref=s2e5] - option "Bar" [selected] [ref=s2e5]
- option "Baz" [selected] [ref=s2e6] - option "Baz" [selected] [ref=s2e6]
\`\`\` \`\`\`
`); `);
}); });

View File

@ -26,12 +26,11 @@ test('stitched aria frames', async ({ client }) => {
\`\`\`yaml \`\`\`yaml
- heading "Hello" [level=1] [ref=s1e3] - heading "Hello" [level=1] [ref=s1e3]
- iframe [ref=s1e4]: - iframe [ref=s1e4]:
- button "World" [ref=f1s1e3] - button "World" [ref=f1s1e3]
- main [ref=f1s1e4]: - main [ref=f1s1e4]:
- iframe [ref=f1s1e5]: - iframe [ref=f1s1e5]:
- paragraph [ref=f2s1e3]: Nested - paragraph [ref=f2s1e3]: Nested
\`\`\` \`\`\``);
`);
expect(await client.callTool({ expect(await client.callTool({
name: 'browser_click', name: 'browser_click',

View File

@ -29,6 +29,22 @@ async function createTab(client: Client, title: string, body: string) {
}); });
} }
test('list initial tabs', async ({ client }) => {
expect(await client.callTool({
name: 'browser_tab_list',
})).toHaveTextContent(`### Open tabs
- 1: (current) [] (about:blank)`);
});
test('list first tab', async ({ client }) => {
await createTab(client, 'Tab one', 'Body one');
expect(await client.callTool({
name: 'browser_tab_list',
})).toHaveTextContent(`### Open tabs
- 1: [] (about:blank)
- 2: (current) [Tab one] (data:text/html,<title>Tab one</title><body>Body one</body>)`);
});
test('create new tab', async ({ client }) => { test('create new tab', async ({ client }) => {
expect(await createTab(client, 'Tab one', 'Body one')).toHaveTextContent(` expect(await createTab(client, 'Tab one', 'Body one')).toHaveTextContent(`
- Ran Playwright code: - Ran Playwright code: