From 795a9d578a35016b83ffe72411ab3c4918e72f22 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 15 Apr 2025 12:54:45 -0700 Subject: [PATCH] chore: generalize status & action as code (#188) --- src/context.ts | 33 +++++++++++++------------ src/tools/common.ts | 17 +++++++------ src/tools/files.ts | 5 +++- src/tools/keyboard.ts | 7 ++++-- src/tools/navigate.ts | 21 +++++++++++----- src/tools/screen.ts | 31 ++++++++++++++++++------ src/tools/snapshot.ts | 56 ++++++++++++++++++++++++------------------- src/tools/tabs.ts | 24 +++++++++++++++---- tests/basic.spec.ts | 38 +++++++++++++++++++---------- tests/cdp.spec.ts | 5 ++++ tests/iframes.spec.ts | 2 +- tests/tabs.spec.ts | 36 +++++++++++++++++++++------- 12 files changed, 187 insertions(+), 88 deletions(-) diff --git a/src/context.ts b/src/context.ts index 574fd44..11b81d5 100644 --- a/src/context.ts +++ b/src/context.ts @@ -33,7 +33,6 @@ type PageOrFrameLocator = playwright.Page | playwright.FrameLocator; type RunOptions = { captureSnapshot?: boolean; waitForCompletion?: boolean; - status?: string; noClearFileChooser?: boolean; }; @@ -79,8 +78,8 @@ export class Context { async listTabs(): Promise { if (!this._tabs.length) - return 'No tabs open'; - const lines: string[] = ['Open tabs:']; + return '### No tabs open'; + const lines: string[] = ['### Open tabs']; for (let i = 0; i < this._tabs.length; i++) { const tab = this._tabs[i]; const title = await tab.page.title(); @@ -172,6 +171,10 @@ export class Context { } } +type RunResult = { + code: string[]; +}; + class Tab { readonly context: Context; readonly page: playwright.Page; @@ -207,33 +210,33 @@ class Tab { await this.page.waitForLoadState('load', { timeout: 5000 }).catch(() => {}); } - async run(callback: (tab: Tab) => Promise, options?: RunOptions): Promise { - let actionCode: string | undefined; + async run(callback: (tab: Tab) => Promise, options?: RunOptions): Promise { + let runResult: RunResult | undefined; try { if (!options?.noClearFileChooser) this._fileChooser = undefined; if (options?.waitForCompletion) - actionCode = await waitForCompletion(this.page, () => callback(this)) ?? undefined; + runResult = await waitForCompletion(this.page, () => callback(this)) ?? undefined; else - actionCode = await callback(this) ?? undefined; + runResult = await callback(this) ?? undefined; } finally { if (options?.captureSnapshot) this._snapshot = await PageSnapshot.create(this.page); } const result: string[] = []; - if (options?.status) - result.push(options.status, ''); + result.push(`- Ran code: +\`\`\`js +${runResult.code.join('\n')} +\`\`\` +`); if (this.context.tabs().length > 1) result.push(await this.context.listTabs(), ''); - if (actionCode) - result.push('- Action: ' + actionCode, ''); - if (this._snapshot) { if (this.context.tabs().length > 1) - result.push('Current tab:'); + result.push('### Current tab'); result.push(this._snapshot.text({ hasFileChooser: !!this._fileChooser })); } @@ -245,14 +248,14 @@ class Tab { }; } - async runAndWait(callback: (tab: Tab) => Promise, options?: RunOptions): Promise { + async runAndWait(callback: (tab: Tab) => Promise, options?: RunOptions): Promise { return await this.run(callback, { waitForCompletion: true, ...options, }); } - async runAndWaitWithSnapshot(callback: (snapshot: PageSnapshot) => Promise, options?: RunOptions): Promise { + async runAndWaitWithSnapshot(callback: (snapshot: PageSnapshot) => Promise, options?: RunOptions): Promise { return await this.run(tab => callback(tab.lastSnapshot()), { captureSnapshot: true, waitForCompletion: true, diff --git a/src/tools/common.ts b/src/tools/common.ts index 82f79e3..784c890 100644 --- a/src/tools/common.ts +++ b/src/tools/common.ts @@ -78,13 +78,16 @@ const resize: ToolFactory = captureSnapshot => ({ const validatedParams = resizeSchema.parse(params); const tab = context.currentTab(); - return await tab.run( - tab => tab.page.setViewportSize({ width: validatedParams.width, height: validatedParams.height }), - { - status: `Resized browser window`, - captureSnapshot, - } - ); + return await tab.run(async tab => { + await tab.page.setViewportSize({ width: validatedParams.width, height: validatedParams.height }); + const code = [ + `// Resize browser window to ${validatedParams.width}x${validatedParams.height}`, + `await page.setViewportSize({ width: ${validatedParams.width}, height: ${validatedParams.height} });` + ]; + return { code }; + }, { + captureSnapshot, + }); }, }); diff --git a/src/tools/files.ts b/src/tools/files.ts index add4131..fc1af6d 100644 --- a/src/tools/files.ts +++ b/src/tools/files.ts @@ -35,8 +35,11 @@ const uploadFile: ToolFactory = captureSnapshot => ({ const tab = context.currentTab(); return await tab.runAndWait(async () => { await tab.submitFileChooser(validatedParams.paths); + const code = [ + `// ({ const validatedParams = pressKeySchema.parse(params); return await context.currentTab().runAndWait(async tab => { await tab.page.keyboard.press(validatedParams.key); - return `await page.keyboard.press('${validatedParams.key}');`; + const code = [ + `// Press ${validatedParams.key}`, + `await page.keyboard.press('${validatedParams.key}');`, + ]; + return { code }; }, { - status: `Pressed key ${validatedParams.key}`, captureSnapshot, }); }, diff --git a/src/tools/navigate.ts b/src/tools/navigate.ts index e911fd2..3ed8878 100644 --- a/src/tools/navigate.ts +++ b/src/tools/navigate.ts @@ -35,9 +35,12 @@ const navigate: ToolFactory = captureSnapshot => ({ const currentTab = await context.ensureTab(); return await currentTab.run(async tab => { await tab.navigate(validatedParams.url); - return `await page.goto('${validatedParams.url}');`; + const code = [ + `// Navigate to ${validatedParams.url}`, + `await page.goto('${validatedParams.url}');`, + ]; + return { code }; }, { - status: `Navigated to ${validatedParams.url}`, captureSnapshot, }); }, @@ -55,9 +58,12 @@ const goBack: ToolFactory = snapshot => ({ handle: async context => { return await context.currentTab().runAndWait(async tab => { await tab.page.goBack(); - return `await page.goBack();`; + const code = [ + `// Navigate back`, + `await page.goBack();`, + ]; + return { code }; }, { - status: 'Navigated back', captureSnapshot: snapshot, }); }, @@ -75,9 +81,12 @@ const goForward: ToolFactory = snapshot => ({ handle: async context => { return await context.currentTab().runAndWait(async tab => { await tab.page.goForward(); - return `await page.goForward();`; + const code = [ + `// Navigate forward`, + `await page.goForward();`, + ]; + return { code }; }, { - status: 'Navigated forward', captureSnapshot: snapshot, }); }, diff --git a/src/tools/screen.ts b/src/tools/screen.ts index c184475..4924644 100644 --- a/src/tools/screen.ts +++ b/src/tools/screen.ts @@ -79,11 +79,16 @@ const click: Tool = { handle: async (context, params) => { return await context.currentTab().runAndWait(async tab => { const validatedParams = clickSchema.parse(params); + const code = [ + `// Click mouse at coordinates (${validatedParams.x}, ${validatedParams.y})`, + `await page.mouse.move(${validatedParams.x}, ${validatedParams.y});`, + `await page.mouse.down();`, + `await page.mouse.up();`, + ]; await tab.page.mouse.move(validatedParams.x, validatedParams.y); await tab.page.mouse.down(); await tab.page.mouse.up(); - }, { - status: 'Clicked mouse', + return { code }; }); }, }; @@ -110,8 +115,14 @@ const drag: Tool = { await tab.page.mouse.down(); await tab.page.mouse.move(validatedParams.endX, validatedParams.endY); await tab.page.mouse.up(); - }, { - status: `Dragged mouse from (${validatedParams.startX}, ${validatedParams.startY}) to (${validatedParams.endX}, ${validatedParams.endY})`, + const code = [ + `// Drag mouse from (${validatedParams.startX}, ${validatedParams.startY}) to (${validatedParams.endX}, ${validatedParams.endY})`, + `await page.mouse.move(${validatedParams.startX}, ${validatedParams.startY});`, + `await page.mouse.down();`, + `await page.mouse.move(${validatedParams.endX}, ${validatedParams.endY});`, + `await page.mouse.up();`, + ]; + return { code }; }); }, }; @@ -132,11 +143,17 @@ const type: Tool = { handle: async (context, params) => { const validatedParams = typeSchema.parse(params); return await context.currentTab().runAndWait(async tab => { + const code = [ + `// Type ${validatedParams.text}`, + `await page.keyboard.type('${validatedParams.text}');`, + ]; await tab.page.keyboard.type(validatedParams.text); - if (validatedParams.submit) + if (validatedParams.submit) { + code.push(`// Submit text`); + code.push(`await page.keyboard.press('Enter');`); await tab.page.keyboard.press('Enter'); - }, { - status: `Typed text "${validatedParams.text}"`, + } + return { code }; }); }, }; diff --git a/src/tools/snapshot.ts b/src/tools/snapshot.ts index 1732385..857c5d5 100644 --- a/src/tools/snapshot.ts +++ b/src/tools/snapshot.ts @@ -32,7 +32,10 @@ const snapshot: Tool = { handle: async context => { const tab = await context.ensureTab(); - return await tab.run(async () => {}, { captureSnapshot: true }); + return await tab.run(async () => { + const code = [`// `]; + return { code }; + }, { captureSnapshot: true }); }, }; @@ -53,11 +56,12 @@ const click: Tool = { const validatedParams = elementSchema.parse(params); return await context.currentTab().runAndWaitWithSnapshot(async snapshot => { const locator = snapshot.refLocator(validatedParams.ref); - const action = `await page.${await generateLocator(locator)}.click();`; + const code = [ + `// Click ${validatedParams.element}`, + `await page.${await generateLocator(locator)}.click();` + ]; await locator.click(); - return action; - }, { - status: `Clicked "${validatedParams.element}"`, + return { code }; }); }, }; @@ -82,11 +86,12 @@ const drag: Tool = { return await context.currentTab().runAndWaitWithSnapshot(async snapshot => { const startLocator = snapshot.refLocator(validatedParams.startRef); const endLocator = snapshot.refLocator(validatedParams.endRef); - const action = `await page.${await generateLocator(startLocator)}.dragTo(page.${await generateLocator(endLocator)});`; + const code = [ + `// Drag ${validatedParams.startElement} to ${validatedParams.endElement}`, + `await page.${await generateLocator(startLocator)}.dragTo(page.${await generateLocator(endLocator)});` + ]; await startLocator.dragTo(endLocator); - return action; - }, { - status: `Dragged "${validatedParams.startElement}" to "${validatedParams.endElement}"`, + return { code }; }); }, }; @@ -103,11 +108,12 @@ const hover: Tool = { const validatedParams = elementSchema.parse(params); return await context.currentTab().runAndWaitWithSnapshot(async snapshot => { const locator = snapshot.refLocator(validatedParams.ref); - const action = `await page.${await generateLocator(locator)}.hover();`; + const code = [ + `// Hover over ${validatedParams.element}`, + `await page.${await generateLocator(locator)}.hover();` + ]; await locator.hover(); - return action; - }, { - status: `Hovered over "${validatedParams.element}"`, + return { code }; }); }, }; @@ -131,21 +137,22 @@ const type: Tool = { return await context.currentTab().runAndWaitWithSnapshot(async snapshot => { const locator = snapshot.refLocator(validatedParams.ref); - let action = ''; + const code: string[] = []; if (validatedParams.slowly) { - action = `await page.${await generateLocator(locator)}.pressSequentially(${javascript.quote(validatedParams.text)});`; + code.push(`// Press "${validatedParams.text}" sequentially into "${validatedParams.element}"`); + code.push(`await page.${await generateLocator(locator)}.pressSequentially(${javascript.quote(validatedParams.text)});`); await locator.pressSequentially(validatedParams.text); } else { - action = `await page.${await generateLocator(locator)}.fill(${javascript.quote(validatedParams.text)});`; + code.push(`// Fill "${validatedParams.text}" into "${validatedParams.element}"`); + code.push(`await page.${await generateLocator(locator)}.fill(${javascript.quote(validatedParams.text)});`); await locator.fill(validatedParams.text); } if (validatedParams.submit) { - action += `\nawait page.${await generateLocator(locator)}.press('Enter');`; + code.push(`// Submit text`); + code.push(`await page.${await generateLocator(locator)}.press('Enter');`); await locator.press('Enter'); } - return action; - }, { - status: `Typed "${validatedParams.text}" into "${validatedParams.element}"`, + return { code }; }); }, }; @@ -166,11 +173,12 @@ const selectOption: Tool = { const validatedParams = selectOptionSchema.parse(params); return await context.currentTab().runAndWaitWithSnapshot(async snapshot => { const locator = snapshot.refLocator(validatedParams.ref); - const action = `await page.${await generateLocator(locator)}.selectOption(${javascript.formatObject(validatedParams.values)});`; + const code = [ + `// Select options [${validatedParams.values.join(', ')}] in ${validatedParams.element}`, + `await page.${await generateLocator(locator)}.selectOption(${javascript.formatObject(validatedParams.values)});` + ]; await locator.selectOption(validatedParams.values); - return action; - }, { - status: `Selected option in "${validatedParams.element}"`, + return { code }; }); }, }; diff --git a/src/tools/tabs.ts b/src/tools/tabs.ts index ed5281b..22a37e9 100644 --- a/src/tools/tabs.ts +++ b/src/tools/tabs.ts @@ -51,7 +51,12 @@ const selectTab: ToolFactory = captureSnapshot => ({ const validatedParams = selectTabSchema.parse(params); await context.selectTab(validatedParams.index); const currentTab = await context.ensureTab(); - return await currentTab.run(async () => {}, { captureSnapshot }); + return await currentTab.run(async () => { + const code = [ + `// `, + ]; + return { code }; + }, { captureSnapshot }); }, }); @@ -71,7 +76,12 @@ const newTab: Tool = { await context.newTab(); if (validatedParams.url) await context.currentTab().navigate(validatedParams.url); - return await context.currentTab().run(async () => {}, { captureSnapshot: true }); + return await context.currentTab().run(async () => { + const code = [ + `// `, + ]; + return { code }; + }, { captureSnapshot: true }); }, }; @@ -90,8 +100,14 @@ const closeTab: ToolFactory = captureSnapshot => ({ const validatedParams = closeTabSchema.parse(params); await context.closeTab(validatedParams.index); const currentTab = context.currentTab(); - if (currentTab) - return await currentTab.run(async () => {}, { captureSnapshot }); + if (currentTab) { + return await currentTab.run(async () => { + const code = [ + `// `, + ]; + return { code }; + }, { captureSnapshot }); + } return { content: [{ type: 'text', diff --git a/tests/basic.spec.ts b/tests/basic.spec.ts index 71910a0..a37f9d6 100644 --- a/tests/basic.spec.ts +++ b/tests/basic.spec.ts @@ -24,9 +24,11 @@ test('browser_navigate', async ({ client }) => { url: 'data:text/html,TitleHello, world!', }, })).toHaveTextContent(` -Navigated to data:text/html,TitleHello, world! - -- Action: await page.goto('data:text/html,TitleHello, world!'); +- Ran code: +\`\`\`js +// Navigate to data:text/html,TitleHello, world! +await page.goto('data:text/html,TitleHello, world!'); +\`\`\` - Page URL: data:text/html,TitleHello, world! - Page Title: Title @@ -53,9 +55,11 @@ test('browser_click', async ({ client }) => { ref: 's1e3', }, })).toHaveTextContent(` -Clicked "Submit button" - -- Action: await page.getByRole('button', { name: 'Submit' }).click(); +- Ran code: +\`\`\`js +// Click Submit button +await page.getByRole('button', { name: 'Submit' }).click(); +\`\`\` - Page URL: data:text/html,Title - Page Title: Title @@ -83,9 +87,11 @@ test('browser_select_option', async ({ client }) => { values: ['bar'], }, })).toHaveTextContent(` -Selected option in "Select" - -- Action: await page.getByRole('combobox').selectOption(['bar']); +- Ran code: +\`\`\`js +// Select options [bar] in Select +await page.getByRole('combobox').selectOption(['bar']); +\`\`\` - Page URL: data:text/html,Title - Page Title: Title @@ -114,9 +120,11 @@ test('browser_select_option (multiple)', async ({ client }) => { values: ['bar', 'baz'], }, })).toHaveTextContent(` -Selected option in "Select" - -- Action: await page.getByRole('listbox').selectOption(['bar', 'baz']); +- Ran code: +\`\`\`js +// Select options [bar, baz] in Select +await page.getByRole('listbox').selectOption(['bar', 'baz']); +\`\`\` - Page URL: data:text/html,Title - Page Title: Title @@ -260,6 +268,10 @@ test('browser_resize', async ({ client }) => { height: 780, }, }); - expect(response).toContainTextContent('Resized browser window'); + expect(response).toContainTextContent(`- Ran code: +\`\`\`js +// Resize browser window to 390x780 +await page.setViewportSize({ width: 390, height: 780 }); +\`\`\``); await expect.poll(() => client.callTool({ name: 'browser_snapshot' })).toContainTextContent('Window size: 390x780'); }); diff --git a/tests/cdp.spec.ts b/tests/cdp.spec.ts index 4bbe07b..855c568 100644 --- a/tests/cdp.spec.ts +++ b/tests/cdp.spec.ts @@ -41,6 +41,11 @@ test('cdp server reuse tab', async ({ cdpEndpoint, startClient }) => { name: 'browser_snapshot', arguments: {}, })).toHaveTextContent(` +- Ran code: +\`\`\`js +// +\`\`\` + - Page URL: data:text/html,hello world - Page Title: - Page Snapshot diff --git a/tests/iframes.spec.ts b/tests/iframes.spec.ts index dce8ced..c54dc9f 100644 --- a/tests/iframes.spec.ts +++ b/tests/iframes.spec.ts @@ -39,5 +39,5 @@ test('stitched aria frames', async ({ client }) => { element: 'World', ref: 'f1s1e3', }, - })).toContainTextContent('Clicked "World"'); + })).toContainTextContent(`// Click World`); }); diff --git a/tests/tabs.spec.ts b/tests/tabs.spec.ts index ea1f3f6..1092bf6 100644 --- a/tests/tabs.spec.ts +++ b/tests/tabs.spec.ts @@ -31,11 +31,16 @@ async function createTab(client: Client, title: string, body: string) { test('create new tab', async ({ client }) => { expect(await createTab(client, 'Tab one', 'Body one')).toHaveTextContent(` -Open tabs: +- Ran code: +\`\`\`js +// +\`\`\` + +### Open tabs - 1: [] (about:blank) - 2: (current) [Tab one] (data:text/html,Tab oneBody one) -Current tab: +### Current tab - Page URL: data:text/html,Tab oneBody one - Page Title: Tab one - Page Snapshot @@ -44,12 +49,17 @@ Current tab: \`\`\``); expect(await createTab(client, 'Tab two', 'Body two')).toHaveTextContent(` -Open tabs: +- Ran code: +\`\`\`js +// +\`\`\` + +### Open tabs - 1: [] (about:blank) - 2: [Tab one] (data:text/html,Tab oneBody one) - 3: (current) [Tab two] (data:text/html,Tab twoBody two) -Current tab: +### Current tab - Page URL: data:text/html,Tab twoBody two - Page Title: Tab two - Page Snapshot @@ -67,12 +77,17 @@ test('select tab', async ({ client }) => { index: 2, }, })).toHaveTextContent(` -Open tabs: +- Ran code: +\`\`\`js +// +\`\`\` + +### Open tabs - 1: [] (about:blank) - 2: (current) [Tab one] (data:text/html,Tab oneBody one) - 3: [Tab two] (data:text/html,Tab twoBody two) -Current tab: +### Current tab - Page URL: data:text/html,Tab oneBody one - Page Title: Tab one - Page Snapshot @@ -90,11 +105,16 @@ test('close tab', async ({ client }) => { index: 3, }, })).toHaveTextContent(` -Open tabs: +- Ran code: +\`\`\`js +// +\`\`\` + +### Open tabs - 1: [] (about:blank) - 2: (current) [Tab one] (data:text/html,Tab oneBody one) -Current tab: +### Current tab - Page URL: data:text/html,Tab oneBody one - Page Title: Tab one - Page Snapshot