diff --git a/packages/playwright-core/src/tools/backend/navigate.ts b/packages/playwright-core/src/tools/backend/navigate.ts index 70e5e92b28371..ba74e4c64c552 100644 --- a/packages/playwright-core/src/tools/backend/navigate.ts +++ b/packages/playwright-core/src/tools/backend/navigate.ts @@ -32,18 +32,7 @@ const navigate = defineTool({ handle: async (context, params, response) => { const tab = await context.ensureTab(); - let url = params.url; - try { - new URL(url); - } catch (e) { - if (url.startsWith('localhost')) - url = 'http://' + url; - else - url = 'https://' + url; - } - - context.checkUrlAllowed(url); - await tab.navigate(url); + const url = await tab.checkUrlAndNavigate(params.url); response.setIncludeSnapshot(); response.addCode(`await page.goto('${url}');`); diff --git a/packages/playwright-core/src/tools/backend/tab.ts b/packages/playwright-core/src/tools/backend/tab.ts index a4e2b5b726e75..87400f8b097f2 100644 --- a/packages/playwright-core/src/tools/backend/tab.ts +++ b/packages/playwright-core/src/tools/backend/tab.ts @@ -293,6 +293,20 @@ export class Tab extends EventEmitter { await this.page.waitForLoadState(state, options).catch(e => debug('pw:tools:error')(e)); } + async checkUrlAndNavigate(url: string): Promise { + try { + new URL(url); + } catch (e) { + if (url.startsWith('localhost')) + url = 'http://' + url; + else + url = 'https://' + url; + } + this.context.checkUrlAllowed(url); + await this.navigate(url); + return url; + } + async navigate(url: string) { await this._initializedPromise; diff --git a/packages/playwright-core/src/tools/backend/tabs.ts b/packages/playwright-core/src/tools/backend/tabs.ts index 0fcd98dbf7829..23543db3af709 100644 --- a/packages/playwright-core/src/tools/backend/tabs.ts +++ b/packages/playwright-core/src/tools/backend/tabs.ts @@ -28,6 +28,7 @@ const browserTabs = defineTool({ inputSchema: z.object({ action: z.enum(['list', 'new', 'close', 'select']).describe('Operation to perform'), index: z.number().optional().describe('Tab index, used for close/select. If omitted for close, current tab is closed.'), + url: z.string().optional().describe('URL to navigate to in the new tab, used for new.'), }), type: 'action', }, @@ -39,7 +40,12 @@ const browserTabs = defineTool({ break; } case 'new': { - await context.newTab(); + const tab = await context.newTab(); + if (params.url) { + const url = await tab.checkUrlAndNavigate(params.url); + response.setIncludeSnapshot(); + response.addCode(`await page.goto('${url}');`); + } break; } case 'close': { diff --git a/tests/mcp/cli-navigation.spec.ts b/tests/mcp/cli-navigation.spec.ts index 07c9264a09b8c..74f606fadf051 100644 --- a/tests/mcp/cli-navigation.spec.ts +++ b/tests/mcp/cli-navigation.spec.ts @@ -40,6 +40,13 @@ test('open without url opens about:blank', async ({ cli }) => { expect(output).toContain('- Page URL: about:blank'); }); +test('tab-new with url', async ({ cli, server }) => { + await cli('open'); + const { output } = await cli('tab-new', server.HELLO_WORLD); + expect(output).toContain(`- 0: [](about:blank)`); + expect(output).toContain(`- 1: (current) [Title](${server.HELLO_WORLD})`); +}); + test('run-code', async ({ cli, server }) => { await cli('open', server.HELLO_WORLD); const { output } = await cli('run-code', '() => page.title()'); diff --git a/tests/mcp/tabs.spec.ts b/tests/mcp/tabs.spec.ts index a5562f47e5792..3743dd1f25fea 100644 --- a/tests/mcp/tabs.spec.ts +++ b/tests/mcp/tabs.spec.ts @@ -78,6 +78,19 @@ test('create new tab', async ({ client }) => { }); }); +test('create new tab with url', async ({ client }) => { + expect(await client.callTool({ + name: 'browser_tabs', + arguments: { + action: 'new', + url: `data:text/html,Tab oneBody one`, + }, + })).toHaveResponse({ + result: `- 0: [](about:blank) +- 1: (current) [Tab one](data:text/html,Tab oneBody one)`, + }); +}); + test('select tab', async ({ client }) => { await createTab(client, 'Tab one', 'Body one'); await createTab(client, 'Tab two', 'Body two');