diff --git a/src/context.ts b/src/context.ts index 226b107..1feb1d0 100644 --- a/src/context.ts +++ b/src/context.ts @@ -119,7 +119,10 @@ export class Context { async run(tool: Tool, params: Record | undefined) { // Tab management is done outside of the action() call. 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) { return { @@ -132,12 +135,12 @@ export class Context { const tab = this.currentTabOrDie(); // TODO: race against modal dialogs to resolve clicks. - let actionResult: { content?: (ImageContent | TextContent)[] }; + let actionResult: { content?: (ImageContent | TextContent)[] } | undefined; try { if (waitForNetwork) - actionResult = await waitForCompletion(tab.page, () => action()) ?? undefined; + actionResult = await waitForCompletion(tab.page, async () => action?.()) ?? undefined; else - actionResult = await action(); + actionResult = await action?.() ?? undefined; } finally { if (captureSnapshot) await tab.captureSnapshot(); @@ -163,11 +166,16 @@ ${code.join('\n')} if (this.tabs().length > 1) result.push(await this.listTabsMarkdown(), ''); - if (tab.hasSnapshot()) { - if (this.tabs().length > 1) - result.push('### Current tab'); + if (this.tabs().length > 1) + 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()); - } const content = actionResult?.content ?? []; @@ -338,19 +346,12 @@ class PageSnapshot { private async _build(page: playwright.Page) { const yamlDocument = await this._snapshotFrame(page); - const lines = []; - lines.push( - `- Page URL: ${page.url()}`, - `- Page Title: ${await page.title()}` - ); - lines.push( - `- Page Snapshot`, - '```yaml', - yamlDocument.toString().trim(), - '```', - '' - ); - this._text = lines.join('\n'); + this._text = [ + `- Page Snapshot`, + '```yaml', + yamlDocument.toString({ indentSeq: false }).trim(), + '```', + ].join('\n'); } private async _snapshotFrame(frame: playwright.Page | playwright.FrameLocator) { diff --git a/src/tools/common.ts b/src/tools/common.ts index dfeb6ed..03b9dc8 100644 --- a/src/tools/common.ts +++ b/src/tools/common.ts @@ -37,7 +37,6 @@ const wait: ToolFactory = captureSnapshot => ({ await new Promise(f => setTimeout(f, Math.min(10000, validatedParams.time * 1000))); return { code: [`// Waited for ${validatedParams.time} seconds`], - action: async () => ({}), captureSnapshot, waitForNetwork: false, }; @@ -59,7 +58,6 @@ const close: Tool = { await context.close(); return { code: [`// Internal to close the page`], - action: async () => ({}), captureSnapshot: false, waitForNetwork: false, }; @@ -91,7 +89,6 @@ const resize: ToolFactory = captureSnapshot => ({ const action = async () => { await tab.page.setViewportSize({ width: validatedParams.width, height: validatedParams.height }); - return {}; }; return { diff --git a/src/tools/files.ts b/src/tools/files.ts index f866be2..816632f 100644 --- a/src/tools/files.ts +++ b/src/tools/files.ts @@ -45,7 +45,6 @@ const uploadFile: ToolFactory = captureSnapshot => ({ const action = async () => { await modalState.fileChooser.setFiles(validatedParams.paths); context.clearModalState(modalState); - return {}; }; return { diff --git a/src/tools/install.ts b/src/tools/install.ts index f17ba3e..9f15819 100644 --- a/src/tools/install.ts +++ b/src/tools/install.ts @@ -49,7 +49,6 @@ const install: Tool = { }); return { code: [`// Browser ${channel} installed`], - action: async () => ({}), captureSnapshot: false, waitForNetwork: false, }; diff --git a/src/tools/keyboard.ts b/src/tools/keyboard.ts index 972ecc5..07eacd9 100644 --- a/src/tools/keyboard.ts +++ b/src/tools/keyboard.ts @@ -41,7 +41,7 @@ const pressKey: ToolFactory = captureSnapshot => ({ `await page.keyboard.press('${validatedParams.key}');`, ]; - const action = () => tab.page.keyboard.press(validatedParams.key).then(() => ({})); + const action = () => tab.page.keyboard.press(validatedParams.key); return { code, diff --git a/src/tools/navigate.ts b/src/tools/navigate.ts index 664a584..0651c7e 100644 --- a/src/tools/navigate.ts +++ b/src/tools/navigate.ts @@ -44,7 +44,6 @@ const navigate: ToolFactory = captureSnapshot => ({ return { code, - action: async () => ({}), captureSnapshot, waitForNetwork: false, }; @@ -71,7 +70,6 @@ const goBack: ToolFactory = captureSnapshot => ({ return { code, - action: async () => ({}), captureSnapshot, waitForNetwork: false, }; @@ -96,7 +94,6 @@ const goForward: ToolFactory = captureSnapshot => ({ ]; return { code, - action: async () => ({}), captureSnapshot, waitForNetwork: false, }; diff --git a/src/tools/pdf.ts b/src/tools/pdf.ts index 09e5671..66cf02a 100644 --- a/src/tools/pdf.ts +++ b/src/tools/pdf.ts @@ -47,7 +47,7 @@ const pdf: Tool = { return { code, - action: async () => tab.page.pdf({ path: fileName }).then(() => ({})), + action: async () => tab.page.pdf({ path: fileName }).then(() => {}), captureSnapshot: false, waitForNetwork: false, }; diff --git a/src/tools/screen.ts b/src/tools/screen.ts index aea70f6..0cd9320 100644 --- a/src/tools/screen.ts +++ b/src/tools/screen.ts @@ -77,7 +77,7 @@ const moveMouse: Tool = { `// Move mouse to (${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 { code, action, @@ -113,7 +113,6 @@ const click: Tool = { await tab.page.mouse.move(validatedParams.x, validatedParams.y); await tab.page.mouse.down(); await tab.page.mouse.up(); - return {}; }; return { code, @@ -157,7 +156,6 @@ const drag: Tool = { await tab.page.mouse.down(); await tab.page.mouse.move(validatedParams.endX, validatedParams.endY); await tab.page.mouse.up(); - return {}; }; return { @@ -196,7 +194,6 @@ const type: Tool = { await tab.page.keyboard.type(validatedParams.text); if (validatedParams.submit) await tab.page.keyboard.press('Enter'); - return {}; }; if (validatedParams.submit) { diff --git a/src/tools/snapshot.ts b/src/tools/snapshot.ts index 19ee28c..5e6ca1d 100644 --- a/src/tools/snapshot.ts +++ b/src/tools/snapshot.ts @@ -40,7 +40,6 @@ const snapshot: Tool = { return { code: [`// `], - action: async () => ({}), captureSnapshot: true, waitForNetwork: false, }; @@ -72,7 +71,7 @@ const click: Tool = { return { code, - action: () => locator.click().then(() => ({})), + action: () => locator.click(), captureSnapshot: true, waitForNetwork: true, }; @@ -107,7 +106,7 @@ const drag: Tool = { return { code, - action: () => startLocator.dragTo(endLocator).then(() => ({})), + action: () => startLocator.dragTo(endLocator), captureSnapshot: true, waitForNetwork: true, }; @@ -134,7 +133,7 @@ const hover: Tool = { return { code, - action: () => locator.hover().then(() => ({})), + action: () => locator.hover(), captureSnapshot: true, waitForNetwork: true, }; @@ -181,7 +180,7 @@ const type: Tool = { return { code, - action: () => steps.reduce((acc, step) => acc.then(step), Promise.resolve()).then(() => ({})), + action: () => steps.reduce((acc, step) => acc.then(step), Promise.resolve()), captureSnapshot: true, waitForNetwork: true, }; @@ -212,7 +211,7 @@ const selectOption: Tool = { return { code, - action: () => locator.selectOption(validatedParams.values).then(() => ({})), + action: () => locator.selectOption(validatedParams.values).then(() => {}), captureSnapshot: true, waitForNetwork: true, }; diff --git a/src/tools/tabs.ts b/src/tools/tabs.ts index aaab412..aed7180 100644 --- a/src/tools/tabs.ts +++ b/src/tools/tabs.ts @@ -28,12 +28,18 @@ const listTabs: Tool = { inputSchema: zodToJsonSchema(z.object({})), }, - handle: async () => { + handle: async context => { + await context.ensureTab(); return { code: [`// `], - action: async () => ({}), captureSnapshot: false, waitForNetwork: false, + resultOverride: { + content: [{ + type: 'text', + text: await context.listTabsMarkdown(), + }], + }, }; }, }; @@ -60,7 +66,6 @@ const selectTab: ToolFactory = captureSnapshot => ({ return { code, - action: async () => ({}), captureSnapshot, waitForNetwork: false }; @@ -91,7 +96,6 @@ const newTab: ToolFactory = captureSnapshot => ({ ]; return { code, - action: async () => ({}), captureSnapshot, waitForNetwork: false }; @@ -119,7 +123,6 @@ const closeTab: ToolFactory = captureSnapshot => ({ ]; return { code, - action: async () => ({}), captureSnapshot, waitForNetwork: false }; diff --git a/src/tools/tool.ts b/src/tools/tool.ts index a83c6c7..d80fddf 100644 --- a/src/tools/tool.ts +++ b/src/tools/tool.ts @@ -36,9 +36,10 @@ export type ModalState = FileUploadModalState; export type ToolResult = { code: string[]; - action: () => Promise<{ content?: (ImageContent | TextContent)[] }>; + action?: () => Promise<{ content?: (ImageContent | TextContent)[] } | undefined | void>; captureSnapshot: boolean; waitForNetwork: boolean; + resultOverride?: { content?: (ImageContent | TextContent)[] }; }; export type Tool = { diff --git a/tests/core.spec.ts b/tests/core.spec.ts index e741aad..cf3ced2 100644 --- a/tests/core.spec.ts +++ b/tests/core.spec.ts @@ -96,8 +96,8 @@ await page.getByRole('combobox').selectOption(['bar']); - Page Snapshot \`\`\`yaml - combobox [ref=s2e3]: - - option "Foo" [ref=s2e4] - - option "Bar" [selected] [ref=s2e5] + - option "Foo" [ref=s2e4] + - option "Bar" [selected] [ref=s2e5] \`\`\` `); }); @@ -129,9 +129,9 @@ await page.getByRole('listbox').selectOption(['bar', 'baz']); - Page Snapshot \`\`\`yaml - listbox [ref=s2e3]: - - option "Foo" [ref=s2e4] - - option "Bar" [selected] [ref=s2e5] - - option "Baz" [selected] [ref=s2e6] + - option "Foo" [ref=s2e4] + - option "Bar" [selected] [ref=s2e5] + - option "Baz" [selected] [ref=s2e6] \`\`\` `); }); diff --git a/tests/iframes.spec.ts b/tests/iframes.spec.ts index c54dc9f..6133c39 100644 --- a/tests/iframes.spec.ts +++ b/tests/iframes.spec.ts @@ -26,12 +26,11 @@ test('stitched aria frames', async ({ client }) => { \`\`\`yaml - heading "Hello" [level=1] [ref=s1e3] - iframe [ref=s1e4]: - - button "World" [ref=f1s1e3] - - main [ref=f1s1e4]: - - iframe [ref=f1s1e5]: - - paragraph [ref=f2s1e3]: Nested -\`\`\` -`); + - button "World" [ref=f1s1e3] + - main [ref=f1s1e4]: + - iframe [ref=f1s1e5]: + - paragraph [ref=f2s1e3]: Nested +\`\`\``); expect(await client.callTool({ name: 'browser_click', diff --git a/tests/tabs.spec.ts b/tests/tabs.spec.ts index 31912af..ca18989 100644 --- a/tests/tabs.spec.ts +++ b/tests/tabs.spec.ts @@ -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,Tab oneBody one)`); +}); + test('create new tab', async ({ client }) => { expect(await createTab(client, 'Tab one', 'Body one')).toHaveTextContent(` - Ran Playwright code: