chore: generalize status & action as code (#188)

This commit is contained in:
Pavel Feldman 2025-04-15 12:54:45 -07:00 committed by GitHub
parent 4a19e18999
commit 795a9d578a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 187 additions and 88 deletions

View File

@ -33,7 +33,6 @@ type PageOrFrameLocator = playwright.Page | playwright.FrameLocator;
type RunOptions = { type RunOptions = {
captureSnapshot?: boolean; captureSnapshot?: boolean;
waitForCompletion?: boolean; waitForCompletion?: boolean;
status?: string;
noClearFileChooser?: boolean; noClearFileChooser?: boolean;
}; };
@ -79,8 +78,8 @@ export class Context {
async listTabs(): Promise<string> { async listTabs(): Promise<string> {
if (!this._tabs.length) if (!this._tabs.length)
return 'No tabs open'; return '### No tabs open';
const lines: string[] = ['Open tabs:']; const lines: string[] = ['### Open tabs'];
for (let i = 0; i < this._tabs.length; i++) { for (let i = 0; i < this._tabs.length; i++) {
const tab = this._tabs[i]; const tab = this._tabs[i];
const title = await tab.page.title(); const title = await tab.page.title();
@ -172,6 +171,10 @@ export class Context {
} }
} }
type RunResult = {
code: string[];
};
class Tab { class Tab {
readonly context: Context; readonly context: Context;
readonly page: playwright.Page; readonly page: playwright.Page;
@ -207,33 +210,33 @@ class Tab {
await this.page.waitForLoadState('load', { timeout: 5000 }).catch(() => {}); await this.page.waitForLoadState('load', { timeout: 5000 }).catch(() => {});
} }
async run(callback: (tab: Tab) => Promise<void | string>, options?: RunOptions): Promise<ToolResult> { async run(callback: (tab: Tab) => Promise<RunResult>, options?: RunOptions): Promise<ToolResult> {
let actionCode: string | undefined; let runResult: RunResult | undefined;
try { try {
if (!options?.noClearFileChooser) if (!options?.noClearFileChooser)
this._fileChooser = undefined; this._fileChooser = undefined;
if (options?.waitForCompletion) if (options?.waitForCompletion)
actionCode = await waitForCompletion(this.page, () => callback(this)) ?? undefined; runResult = await waitForCompletion(this.page, () => callback(this)) ?? undefined;
else else
actionCode = await callback(this) ?? undefined; runResult = await callback(this) ?? undefined;
} finally { } finally {
if (options?.captureSnapshot) if (options?.captureSnapshot)
this._snapshot = await PageSnapshot.create(this.page); this._snapshot = await PageSnapshot.create(this.page);
} }
const result: string[] = []; const result: string[] = [];
if (options?.status) result.push(`- Ran code:
result.push(options.status, ''); \`\`\`js
${runResult.code.join('\n')}
\`\`\`
`);
if (this.context.tabs().length > 1) if (this.context.tabs().length > 1)
result.push(await this.context.listTabs(), ''); result.push(await this.context.listTabs(), '');
if (actionCode)
result.push('- Action: ' + actionCode, '');
if (this._snapshot) { if (this._snapshot) {
if (this.context.tabs().length > 1) if (this.context.tabs().length > 1)
result.push('Current tab:'); result.push('### Current tab');
result.push(this._snapshot.text({ hasFileChooser: !!this._fileChooser })); result.push(this._snapshot.text({ hasFileChooser: !!this._fileChooser }));
} }
@ -245,14 +248,14 @@ class Tab {
}; };
} }
async runAndWait(callback: (tab: Tab) => Promise<void | string>, options?: RunOptions): Promise<ToolResult> { async runAndWait(callback: (tab: Tab) => Promise<RunResult>, options?: RunOptions): Promise<ToolResult> {
return await this.run(callback, { return await this.run(callback, {
waitForCompletion: true, waitForCompletion: true,
...options, ...options,
}); });
} }
async runAndWaitWithSnapshot(callback: (snapshot: PageSnapshot) => Promise<void | string>, options?: RunOptions): Promise<ToolResult> { async runAndWaitWithSnapshot(callback: (snapshot: PageSnapshot) => Promise<RunResult>, options?: RunOptions): Promise<ToolResult> {
return await this.run(tab => callback(tab.lastSnapshot()), { return await this.run(tab => callback(tab.lastSnapshot()), {
captureSnapshot: true, captureSnapshot: true,
waitForCompletion: true, waitForCompletion: true,

View File

@ -78,13 +78,16 @@ const resize: ToolFactory = captureSnapshot => ({
const validatedParams = resizeSchema.parse(params); const validatedParams = resizeSchema.parse(params);
const tab = context.currentTab(); const tab = context.currentTab();
return await tab.run( return await tab.run(async tab => {
tab => tab.page.setViewportSize({ width: validatedParams.width, height: validatedParams.height }), await tab.page.setViewportSize({ width: validatedParams.width, height: validatedParams.height });
{ const code = [
status: `Resized browser window`, `// Resize browser window to ${validatedParams.width}x${validatedParams.height}`,
`await page.setViewportSize({ width: ${validatedParams.width}, height: ${validatedParams.height} });`
];
return { code };
}, {
captureSnapshot, captureSnapshot,
} });
);
}, },
}); });

View File

@ -35,8 +35,11 @@ const uploadFile: ToolFactory = captureSnapshot => ({
const tab = context.currentTab(); const tab = context.currentTab();
return await tab.runAndWait(async () => { return await tab.runAndWait(async () => {
await tab.submitFileChooser(validatedParams.paths); await tab.submitFileChooser(validatedParams.paths);
const code = [
`// <internal code to chose files ${validatedParams.paths.join(', ')}`,
];
return { code };
}, { }, {
status: `Chose files ${validatedParams.paths.join(', ')}`,
captureSnapshot, captureSnapshot,
noClearFileChooser: true, noClearFileChooser: true,
}); });

View File

@ -34,9 +34,12 @@ const pressKey: ToolFactory = captureSnapshot => ({
const validatedParams = pressKeySchema.parse(params); const validatedParams = pressKeySchema.parse(params);
return await context.currentTab().runAndWait(async tab => { return await context.currentTab().runAndWait(async tab => {
await tab.page.keyboard.press(validatedParams.key); 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, captureSnapshot,
}); });
}, },

View File

@ -35,9 +35,12 @@ const navigate: ToolFactory = captureSnapshot => ({
const currentTab = await context.ensureTab(); const currentTab = await context.ensureTab();
return await currentTab.run(async tab => { return await currentTab.run(async tab => {
await tab.navigate(validatedParams.url); 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, captureSnapshot,
}); });
}, },
@ -55,9 +58,12 @@ const goBack: ToolFactory = snapshot => ({
handle: async context => { handle: async context => {
return await context.currentTab().runAndWait(async tab => { return await context.currentTab().runAndWait(async tab => {
await tab.page.goBack(); await tab.page.goBack();
return `await page.goBack();`; const code = [
`// Navigate back`,
`await page.goBack();`,
];
return { code };
}, { }, {
status: 'Navigated back',
captureSnapshot: snapshot, captureSnapshot: snapshot,
}); });
}, },
@ -75,9 +81,12 @@ const goForward: ToolFactory = snapshot => ({
handle: async context => { handle: async context => {
return await context.currentTab().runAndWait(async tab => { return await context.currentTab().runAndWait(async tab => {
await tab.page.goForward(); await tab.page.goForward();
return `await page.goForward();`; const code = [
`// Navigate forward`,
`await page.goForward();`,
];
return { code };
}, { }, {
status: 'Navigated forward',
captureSnapshot: snapshot, captureSnapshot: snapshot,
}); });
}, },

View File

@ -79,11 +79,16 @@ const click: Tool = {
handle: async (context, params) => { handle: async (context, params) => {
return await context.currentTab().runAndWait(async tab => { return await context.currentTab().runAndWait(async tab => {
const validatedParams = clickSchema.parse(params); 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.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 { code };
status: 'Clicked mouse',
}); });
}, },
}; };
@ -110,8 +115,14 @@ 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();
}, { const code = [
status: `Dragged mouse from (${validatedParams.startX}, ${validatedParams.startY}) to (${validatedParams.endX}, ${validatedParams.endY})`, `// 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) => { handle: async (context, params) => {
const validatedParams = typeSchema.parse(params); const validatedParams = typeSchema.parse(params);
return await context.currentTab().runAndWait(async tab => { 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); 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'); await tab.page.keyboard.press('Enter');
}, { }
status: `Typed text "${validatedParams.text}"`, return { code };
}); });
}, },
}; };

View File

@ -32,7 +32,10 @@ const snapshot: Tool = {
handle: async context => { handle: async context => {
const tab = await context.ensureTab(); const tab = await context.ensureTab();
return await tab.run(async () => {}, { captureSnapshot: true }); return await tab.run(async () => {
const code = [`// <internal code to capture accessibility snapshot>`];
return { code };
}, { captureSnapshot: true });
}, },
}; };
@ -53,11 +56,12 @@ const click: Tool = {
const validatedParams = elementSchema.parse(params); const validatedParams = elementSchema.parse(params);
return await context.currentTab().runAndWaitWithSnapshot(async snapshot => { return await context.currentTab().runAndWaitWithSnapshot(async snapshot => {
const locator = snapshot.refLocator(validatedParams.ref); 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(); await locator.click();
return action; return { code };
}, {
status: `Clicked "${validatedParams.element}"`,
}); });
}, },
}; };
@ -82,11 +86,12 @@ const drag: Tool = {
return await context.currentTab().runAndWaitWithSnapshot(async snapshot => { return await context.currentTab().runAndWaitWithSnapshot(async snapshot => {
const startLocator = snapshot.refLocator(validatedParams.startRef); const startLocator = snapshot.refLocator(validatedParams.startRef);
const endLocator = snapshot.refLocator(validatedParams.endRef); 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); await startLocator.dragTo(endLocator);
return action; return { code };
}, {
status: `Dragged "${validatedParams.startElement}" to "${validatedParams.endElement}"`,
}); });
}, },
}; };
@ -103,11 +108,12 @@ const hover: Tool = {
const validatedParams = elementSchema.parse(params); const validatedParams = elementSchema.parse(params);
return await context.currentTab().runAndWaitWithSnapshot(async snapshot => { return await context.currentTab().runAndWaitWithSnapshot(async snapshot => {
const locator = snapshot.refLocator(validatedParams.ref); 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(); await locator.hover();
return action; return { code };
}, {
status: `Hovered over "${validatedParams.element}"`,
}); });
}, },
}; };
@ -131,21 +137,22 @@ const type: Tool = {
return await context.currentTab().runAndWaitWithSnapshot(async snapshot => { return await context.currentTab().runAndWaitWithSnapshot(async snapshot => {
const locator = snapshot.refLocator(validatedParams.ref); const locator = snapshot.refLocator(validatedParams.ref);
let action = ''; const code: string[] = [];
if (validatedParams.slowly) { 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); await locator.pressSequentially(validatedParams.text);
} else { } 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); await locator.fill(validatedParams.text);
} }
if (validatedParams.submit) { 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'); await locator.press('Enter');
} }
return action; return { code };
}, {
status: `Typed "${validatedParams.text}" into "${validatedParams.element}"`,
}); });
}, },
}; };
@ -166,11 +173,12 @@ const selectOption: Tool = {
const validatedParams = selectOptionSchema.parse(params); const validatedParams = selectOptionSchema.parse(params);
return await context.currentTab().runAndWaitWithSnapshot(async snapshot => { return await context.currentTab().runAndWaitWithSnapshot(async snapshot => {
const locator = snapshot.refLocator(validatedParams.ref); 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); await locator.selectOption(validatedParams.values);
return action; return { code };
}, {
status: `Selected option in "${validatedParams.element}"`,
}); });
}, },
}; };

View File

@ -51,7 +51,12 @@ const selectTab: ToolFactory = captureSnapshot => ({
const validatedParams = selectTabSchema.parse(params); const validatedParams = selectTabSchema.parse(params);
await context.selectTab(validatedParams.index); await context.selectTab(validatedParams.index);
const currentTab = await context.ensureTab(); const currentTab = await context.ensureTab();
return await currentTab.run(async () => {}, { captureSnapshot }); return await currentTab.run(async () => {
const code = [
`// <internal code to select tab ${validatedParams.index}>`,
];
return { code };
}, { captureSnapshot });
}, },
}); });
@ -71,7 +76,12 @@ const newTab: Tool = {
await context.newTab(); await context.newTab();
if (validatedParams.url) if (validatedParams.url)
await context.currentTab().navigate(validatedParams.url); await context.currentTab().navigate(validatedParams.url);
return await context.currentTab().run(async () => {}, { captureSnapshot: true }); return await context.currentTab().run(async () => {
const code = [
`// <internal code to open a new tab>`,
];
return { code };
}, { captureSnapshot: true });
}, },
}; };
@ -90,8 +100,14 @@ const closeTab: ToolFactory = captureSnapshot => ({
const validatedParams = closeTabSchema.parse(params); const validatedParams = closeTabSchema.parse(params);
await context.closeTab(validatedParams.index); await context.closeTab(validatedParams.index);
const currentTab = context.currentTab(); const currentTab = context.currentTab();
if (currentTab) if (currentTab) {
return await currentTab.run(async () => {}, { captureSnapshot }); return await currentTab.run(async () => {
const code = [
`// <internal code to close tab ${validatedParams.index}>`,
];
return { code };
}, { captureSnapshot });
}
return { return {
content: [{ content: [{
type: 'text', type: 'text',

View File

@ -24,9 +24,11 @@ test('browser_navigate', async ({ client }) => {
url: 'data:text/html,<html><title>Title</title><body>Hello, world!</body></html>', url: 'data:text/html,<html><title>Title</title><body>Hello, world!</body></html>',
}, },
})).toHaveTextContent(` })).toHaveTextContent(`
Navigated to data:text/html,<html><title>Title</title><body>Hello, world!</body></html> - Ran code:
\`\`\`js
- Action: await page.goto('data:text/html,<html><title>Title</title><body>Hello, world!</body></html>'); // Navigate to data:text/html,<html><title>Title</title><body>Hello, world!</body></html>
await page.goto('data:text/html,<html><title>Title</title><body>Hello, world!</body></html>');
\`\`\`
- Page URL: data:text/html,<html><title>Title</title><body>Hello, world!</body></html> - Page URL: data:text/html,<html><title>Title</title><body>Hello, world!</body></html>
- Page Title: Title - Page Title: Title
@ -53,9 +55,11 @@ test('browser_click', async ({ client }) => {
ref: 's1e3', ref: 's1e3',
}, },
})).toHaveTextContent(` })).toHaveTextContent(`
Clicked "Submit button" - Ran code:
\`\`\`js
- Action: await page.getByRole('button', { name: 'Submit' }).click(); // Click Submit button
await page.getByRole('button', { name: 'Submit' }).click();
\`\`\`
- Page URL: data:text/html,<html><title>Title</title><button>Submit</button></html> - Page URL: data:text/html,<html><title>Title</title><button>Submit</button></html>
- Page Title: Title - Page Title: Title
@ -83,9 +87,11 @@ test('browser_select_option', async ({ client }) => {
values: ['bar'], values: ['bar'],
}, },
})).toHaveTextContent(` })).toHaveTextContent(`
Selected option in "Select" - Ran code:
\`\`\`js
- Action: await page.getByRole('combobox').selectOption(['bar']); // Select options [bar] in Select
await page.getByRole('combobox').selectOption(['bar']);
\`\`\`
- Page URL: data:text/html,<html><title>Title</title><select><option value="foo">Foo</option><option value="bar">Bar</option></select></html> - Page URL: data:text/html,<html><title>Title</title><select><option value="foo">Foo</option><option value="bar">Bar</option></select></html>
- Page Title: Title - Page Title: Title
@ -114,9 +120,11 @@ test('browser_select_option (multiple)', async ({ client }) => {
values: ['bar', 'baz'], values: ['bar', 'baz'],
}, },
})).toHaveTextContent(` })).toHaveTextContent(`
Selected option in "Select" - Ran code:
\`\`\`js
- Action: await page.getByRole('listbox').selectOption(['bar', 'baz']); // Select options [bar, baz] in Select
await page.getByRole('listbox').selectOption(['bar', 'baz']);
\`\`\`
- Page URL: data:text/html,<html><title>Title</title><select multiple><option value="foo">Foo</option><option value="bar">Bar</option><option value="baz">Baz</option></select></html> - Page URL: data:text/html,<html><title>Title</title><select multiple><option value="foo">Foo</option><option value="bar">Bar</option><option value="baz">Baz</option></select></html>
- Page Title: Title - Page Title: Title
@ -260,6 +268,10 @@ test('browser_resize', async ({ client }) => {
height: 780, 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'); await expect.poll(() => client.callTool({ name: 'browser_snapshot' })).toContainTextContent('Window size: 390x780');
}); });

View File

@ -41,6 +41,11 @@ test('cdp server reuse tab', async ({ cdpEndpoint, startClient }) => {
name: 'browser_snapshot', name: 'browser_snapshot',
arguments: {}, arguments: {},
})).toHaveTextContent(` })).toHaveTextContent(`
- Ran code:
\`\`\`js
// <internal code to capture accessibility snapshot>
\`\`\`
- Page URL: data:text/html,hello world - Page URL: data:text/html,hello world
- Page Title: - Page Title:
- Page Snapshot - Page Snapshot

View File

@ -39,5 +39,5 @@ test('stitched aria frames', async ({ client }) => {
element: 'World', element: 'World',
ref: 'f1s1e3', ref: 'f1s1e3',
}, },
})).toContainTextContent('Clicked "World"'); })).toContainTextContent(`// Click World`);
}); });

View File

@ -31,11 +31,16 @@ async function createTab(client: Client, title: string, body: string) {
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(`
Open tabs: - Ran code:
\`\`\`js
// <internal code to open a new tab>
\`\`\`
### Open tabs
- 1: [] (about:blank) - 1: [] (about:blank)
- 2: (current) [Tab one] (data:text/html,<title>Tab one</title><body>Body one</body>) - 2: (current) [Tab one] (data:text/html,<title>Tab one</title><body>Body one</body>)
Current tab: ### Current tab
- Page URL: data:text/html,<title>Tab one</title><body>Body one</body> - Page URL: data:text/html,<title>Tab one</title><body>Body one</body>
- Page Title: Tab one - Page Title: Tab one
- Page Snapshot - Page Snapshot
@ -44,12 +49,17 @@ Current tab:
\`\`\``); \`\`\``);
expect(await createTab(client, 'Tab two', 'Body two')).toHaveTextContent(` expect(await createTab(client, 'Tab two', 'Body two')).toHaveTextContent(`
Open tabs: - Ran code:
\`\`\`js
// <internal code to open a new tab>
\`\`\`
### Open tabs
- 1: [] (about:blank) - 1: [] (about:blank)
- 2: [Tab one] (data:text/html,<title>Tab one</title><body>Body one</body>) - 2: [Tab one] (data:text/html,<title>Tab one</title><body>Body one</body>)
- 3: (current) [Tab two] (data:text/html,<title>Tab two</title><body>Body two</body>) - 3: (current) [Tab two] (data:text/html,<title>Tab two</title><body>Body two</body>)
Current tab: ### Current tab
- Page URL: data:text/html,<title>Tab two</title><body>Body two</body> - Page URL: data:text/html,<title>Tab two</title><body>Body two</body>
- Page Title: Tab two - Page Title: Tab two
- Page Snapshot - Page Snapshot
@ -67,12 +77,17 @@ test('select tab', async ({ client }) => {
index: 2, index: 2,
}, },
})).toHaveTextContent(` })).toHaveTextContent(`
Open tabs: - Ran code:
\`\`\`js
// <internal code to select tab 2>
\`\`\`
### Open tabs
- 1: [] (about:blank) - 1: [] (about:blank)
- 2: (current) [Tab one] (data:text/html,<title>Tab one</title><body>Body one</body>) - 2: (current) [Tab one] (data:text/html,<title>Tab one</title><body>Body one</body>)
- 3: [Tab two] (data:text/html,<title>Tab two</title><body>Body two</body>) - 3: [Tab two] (data:text/html,<title>Tab two</title><body>Body two</body>)
Current tab: ### Current tab
- Page URL: data:text/html,<title>Tab one</title><body>Body one</body> - Page URL: data:text/html,<title>Tab one</title><body>Body one</body>
- Page Title: Tab one - Page Title: Tab one
- Page Snapshot - Page Snapshot
@ -90,11 +105,16 @@ test('close tab', async ({ client }) => {
index: 3, index: 3,
}, },
})).toHaveTextContent(` })).toHaveTextContent(`
Open tabs: - Ran code:
\`\`\`js
// <internal code to close tab 3>
\`\`\`
### Open tabs
- 1: [] (about:blank) - 1: [] (about:blank)
- 2: (current) [Tab one] (data:text/html,<title>Tab one</title><body>Body one</body>) - 2: (current) [Tab one] (data:text/html,<title>Tab one</title><body>Body one</body>)
Current tab: ### Current tab
- Page URL: data:text/html,<title>Tab one</title><body>Body one</body> - Page URL: data:text/html,<title>Tab one</title><body>Body one</body>
- Page Title: Tab one - Page Title: Tab one
- Page Snapshot - Page Snapshot