mirror of
https://github.com/microsoft/playwright-mcp.git
synced 2025-07-25 07:52:27 +08:00
chore: move state to tab, do not cache snapshot (#730)
This commit is contained in:
parent
cfcca40b90
commit
468c84eb8f
@ -60,13 +60,6 @@ export function createConnection(config: FullConfig, browserContextFactory: Brow
|
||||
if (!tool)
|
||||
return errorResult(`Tool "${request.params.name}" not found`);
|
||||
|
||||
|
||||
const modalStates = context.modalStates().map(state => state.type);
|
||||
if (tool.clearsModalState && !modalStates.includes(tool.clearsModalState))
|
||||
return errorResult(`The tool "${request.params.name}" can only be used when there is related modal state present.`, ...context.modalStatesMarkdown());
|
||||
if (!tool.clearsModalState && modalStates.length)
|
||||
return errorResult(`Tool "${request.params.name}" does not handle the modal state.`, ...context.modalStatesMarkdown());
|
||||
|
||||
try {
|
||||
return await context.run(tool, request.params.arguments);
|
||||
} catch (error) {
|
||||
|
168
src/context.ts
168
src/context.ts
@ -17,19 +17,12 @@
|
||||
import debug from 'debug';
|
||||
import * as playwright from 'playwright';
|
||||
|
||||
import { callOnPageNoTrace, waitForCompletion } from './tools/utils.js';
|
||||
import { ManualPromise } from './manualPromise.js';
|
||||
import { Tab } from './tab.js';
|
||||
import { outputFile } from './config.js';
|
||||
|
||||
import type { ModalState, Tool, ToolActionResult } from './tools/tool.js';
|
||||
import type { Tool } from './tools/tool.js';
|
||||
import type { FullConfig } from './config.js';
|
||||
import type { BrowserContextFactory } from './browserContextFactory.js';
|
||||
|
||||
type PendingAction = {
|
||||
dialogShown: ManualPromise<void>;
|
||||
};
|
||||
|
||||
const testDebug = debug('pw:mcp:test');
|
||||
|
||||
export class Context {
|
||||
@ -39,9 +32,6 @@ export class Context {
|
||||
private _browserContextFactory: BrowserContextFactory;
|
||||
private _tabs: Tab[] = [];
|
||||
private _currentTab: Tab | undefined;
|
||||
private _modalStates: (ModalState & { tab: Tab })[] = [];
|
||||
private _pendingAction: PendingAction | undefined;
|
||||
private _downloads: { download: playwright.Download, finished: boolean, outputFile: string }[] = [];
|
||||
clientVersion: { name: string; version: string; } | undefined;
|
||||
|
||||
constructor(tools: Tool[], config: FullConfig, browserContextFactory: BrowserContextFactory) {
|
||||
@ -51,42 +41,13 @@ export class Context {
|
||||
testDebug('create context');
|
||||
}
|
||||
|
||||
clientSupportsImages(): boolean {
|
||||
if (this.config.imageResponses === 'omit')
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
modalStates(): ModalState[] {
|
||||
return this._modalStates;
|
||||
}
|
||||
|
||||
setModalState(modalState: ModalState, inTab: Tab) {
|
||||
this._modalStates.push({ ...modalState, tab: inTab });
|
||||
}
|
||||
|
||||
clearModalState(modalState: ModalState) {
|
||||
this._modalStates = this._modalStates.filter(state => state !== modalState);
|
||||
}
|
||||
|
||||
modalStatesMarkdown(): string[] {
|
||||
const result: string[] = ['### Modal state'];
|
||||
if (this._modalStates.length === 0)
|
||||
result.push('- There is no modal state present');
|
||||
for (const state of this._modalStates) {
|
||||
const tool = this.tools.find(tool => tool.clearsModalState === state.type);
|
||||
result.push(`- [${state.description}]: can be handled by the "${tool?.schema.name}" tool`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
tabs(): Tab[] {
|
||||
return this._tabs;
|
||||
}
|
||||
|
||||
currentTabOrDie(): Tab {
|
||||
if (!this._currentTab)
|
||||
throw new Error('No current snapshot available. Capture a snapshot or navigate to a new location first.');
|
||||
throw new Error('No open pages available. Use the "browser_navigate" tool to navigate to a page first.');
|
||||
return this._currentTab;
|
||||
}
|
||||
|
||||
@ -109,9 +70,9 @@ export class Context {
|
||||
return this._currentTab!;
|
||||
}
|
||||
|
||||
async listTabsMarkdown(): Promise<string> {
|
||||
async listTabsMarkdown(): Promise<string[]> {
|
||||
if (!this._tabs.length)
|
||||
return '### No tabs open';
|
||||
return ['### No tabs open'];
|
||||
const lines: string[] = ['### Open tabs'];
|
||||
for (let i = 0; i < this._tabs.length; i++) {
|
||||
const tab = this._tabs[i];
|
||||
@ -120,7 +81,7 @@ export class Context {
|
||||
const current = tab === this._currentTab ? ' (current)' : '';
|
||||
lines.push(`- ${i}:${current} [${title}] (${url})`);
|
||||
}
|
||||
return lines.join('\n');
|
||||
return lines;
|
||||
}
|
||||
|
||||
async closeTab(index: number | undefined) {
|
||||
@ -137,28 +98,8 @@ export class Context {
|
||||
if (resultOverride)
|
||||
return resultOverride;
|
||||
|
||||
if (!this._currentTab) {
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: 'No open pages available. Use the "browser_navigate" tool to navigate to a page first.',
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
const tab = this.currentTabOrDie();
|
||||
// TODO: race against modal dialogs to resolve clicks.
|
||||
const actionResult = await this._raceAgainstModalDialogs(async () => {
|
||||
try {
|
||||
if (waitForNetwork)
|
||||
return await waitForCompletion(this, tab, async () => action?.()) ?? undefined;
|
||||
else
|
||||
return await action?.() ?? undefined;
|
||||
} finally {
|
||||
if (captureSnapshot && !this._javaScriptBlocked())
|
||||
await tab.captureSnapshot();
|
||||
}
|
||||
});
|
||||
const { actionResult, snapshot } = await tab.run(action || (() => Promise.resolve()), { waitForNetwork, captureSnapshot });
|
||||
|
||||
const result: string[] = [];
|
||||
result.push(`### Ran Playwright code
|
||||
@ -166,8 +107,8 @@ export class Context {
|
||||
${code.join('\n')}
|
||||
\`\`\``);
|
||||
|
||||
if (this.modalStates().length) {
|
||||
result.push('', ...this.modalStatesMarkdown());
|
||||
if (tab.modalStates().length) {
|
||||
result.push('', ...tab.modalStatesMarkdown());
|
||||
return {
|
||||
content: [{
|
||||
type: 'text',
|
||||
@ -176,37 +117,13 @@ ${code.join('\n')}
|
||||
};
|
||||
}
|
||||
|
||||
const messages = tab.takeRecentConsoleMessages();
|
||||
if (messages.length) {
|
||||
result.push('', `### New console messages`);
|
||||
for (const message of messages)
|
||||
result.push(`- ${trim(message.toString(), 100)}`);
|
||||
}
|
||||
result.push(...tab.takeRecentConsoleMarkdown());
|
||||
result.push(...tab.listDownloadsMarkdown());
|
||||
|
||||
if (this._downloads.length) {
|
||||
result.push('', '### Downloads');
|
||||
for (const entry of this._downloads) {
|
||||
if (entry.finished)
|
||||
result.push(`- Downloaded file ${entry.download.suggestedFilename()} to ${entry.outputFile}`);
|
||||
else
|
||||
result.push(`- Downloading file ${entry.download.suggestedFilename()} ...`);
|
||||
}
|
||||
}
|
||||
|
||||
if (captureSnapshot && tab.hasSnapshot()) {
|
||||
if (snapshot) {
|
||||
if (this.tabs().length > 1)
|
||||
result.push('', await this.listTabsMarkdown());
|
||||
|
||||
if (this.tabs().length > 1)
|
||||
result.push('', '### Current tab');
|
||||
else
|
||||
result.push('', '### Page state');
|
||||
|
||||
result.push(
|
||||
`- Page URL: ${tab.page.url()}`,
|
||||
`- Page Title: ${await tab.title()}`
|
||||
);
|
||||
result.push(tab.snapshotOrDie().text());
|
||||
result.push('', ...(await this.listTabsMarkdown()));
|
||||
result.push('', snapshot);
|
||||
}
|
||||
|
||||
const content = actionResult?.content ?? [];
|
||||
@ -222,58 +139,6 @@ ${code.join('\n')}
|
||||
};
|
||||
}
|
||||
|
||||
async waitForTimeout(time: number) {
|
||||
if (!this._currentTab || this._javaScriptBlocked()) {
|
||||
await new Promise(f => setTimeout(f, time));
|
||||
return;
|
||||
}
|
||||
|
||||
await callOnPageNoTrace(this._currentTab.page, page => {
|
||||
return page.evaluate(() => new Promise(f => setTimeout(f, 1000)));
|
||||
});
|
||||
}
|
||||
|
||||
private async _raceAgainstModalDialogs(action: () => Promise<ToolActionResult>): Promise<ToolActionResult> {
|
||||
this._pendingAction = {
|
||||
dialogShown: new ManualPromise(),
|
||||
};
|
||||
|
||||
let result: ToolActionResult | undefined;
|
||||
try {
|
||||
await Promise.race([
|
||||
action().then(r => result = r),
|
||||
this._pendingAction.dialogShown,
|
||||
]);
|
||||
} finally {
|
||||
this._pendingAction = undefined;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private _javaScriptBlocked(): boolean {
|
||||
return this._modalStates.some(state => state.type === 'dialog');
|
||||
}
|
||||
|
||||
dialogShown(tab: Tab, dialog: playwright.Dialog) {
|
||||
this.setModalState({
|
||||
type: 'dialog',
|
||||
description: `"${dialog.type()}" dialog with message "${dialog.message()}"`,
|
||||
dialog,
|
||||
}, tab);
|
||||
this._pendingAction?.dialogShown.resolve();
|
||||
}
|
||||
|
||||
async downloadStarted(tab: Tab, download: playwright.Download) {
|
||||
const entry = {
|
||||
download,
|
||||
finished: false,
|
||||
outputFile: await outputFile(this.config, download.suggestedFilename())
|
||||
};
|
||||
this._downloads.push(entry);
|
||||
await download.saveAs(entry.outputFile);
|
||||
entry.finished = true;
|
||||
}
|
||||
|
||||
private _onPageCreated(page: playwright.Page) {
|
||||
const tab = new Tab(this, page, tab => this._onPageClosed(tab));
|
||||
this._tabs.push(tab);
|
||||
@ -282,7 +147,6 @@ ${code.join('\n')}
|
||||
}
|
||||
|
||||
private _onPageClosed(tab: Tab) {
|
||||
this._modalStates = this._modalStates.filter(state => state.tab !== tab);
|
||||
const index = this._tabs.indexOf(tab);
|
||||
if (index === -1)
|
||||
return;
|
||||
@ -353,9 +217,3 @@ ${code.join('\n')}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
function trim(text: string, maxLength: number) {
|
||||
if (text.length <= maxLength)
|
||||
return text;
|
||||
return text.slice(0, maxLength) + '...';
|
||||
}
|
||||
|
@ -1,55 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as playwright from 'playwright';
|
||||
import { callOnPageNoTrace } from './tools/utils.js';
|
||||
|
||||
type PageEx = playwright.Page & {
|
||||
_snapshotForAI: () => Promise<string>;
|
||||
};
|
||||
|
||||
export class PageSnapshot {
|
||||
private _page: playwright.Page;
|
||||
private _text!: string;
|
||||
|
||||
constructor(page: playwright.Page) {
|
||||
this._page = page;
|
||||
}
|
||||
|
||||
static async create(page: playwright.Page): Promise<PageSnapshot> {
|
||||
const snapshot = new PageSnapshot(page);
|
||||
await snapshot._build();
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
text(): string {
|
||||
return this._text;
|
||||
}
|
||||
|
||||
private async _build() {
|
||||
const snapshot = await callOnPageNoTrace(this._page, page => (page as PageEx)._snapshotForAI());
|
||||
this._text = [
|
||||
`- Page Snapshot:`,
|
||||
'```yaml',
|
||||
snapshot,
|
||||
'```',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
refLocator(params: { element: string, ref: string }): playwright.Locator {
|
||||
return this._page.locator(`aria-ref=${params.ref}`).describe(params.element);
|
||||
}
|
||||
}
|
183
src/tab.ts
183
src/tab.ts
@ -16,20 +16,33 @@
|
||||
|
||||
import * as playwright from 'playwright';
|
||||
|
||||
import { PageSnapshot } from './pageSnapshot.js';
|
||||
import { callOnPageNoTrace } from './tools/utils.js';
|
||||
import { callOnPageNoTrace, waitForCompletion } from './tools/utils.js';
|
||||
import { logUnhandledError } from './log.js';
|
||||
import { ManualPromise } from './manualPromise.js';
|
||||
import { ModalState } from './tools/tool.js';
|
||||
import { outputFile } from './config.js';
|
||||
|
||||
import type { Context } from './context.js';
|
||||
import type { ToolActionResult } from './tools/tool.js';
|
||||
|
||||
type PageEx = playwright.Page & {
|
||||
_snapshotForAI: () => Promise<string>;
|
||||
};
|
||||
|
||||
type PendingAction = {
|
||||
dialogShown: ManualPromise<void>;
|
||||
};
|
||||
|
||||
export class Tab {
|
||||
readonly context: Context;
|
||||
readonly page: playwright.Page;
|
||||
private _consoleMessages: ConsoleMessage[] = [];
|
||||
private _recentConsoleMessages: ConsoleMessage[] = [];
|
||||
private _pendingAction: PendingAction | undefined;
|
||||
private _requests: Map<playwright.Request, playwright.Response | null> = new Map();
|
||||
private _snapshot: PageSnapshot | undefined;
|
||||
private _onPageClose: (tab: Tab) => void;
|
||||
private _modalStates: ModalState[] = [];
|
||||
private _downloads: { download: playwright.Download, finished: boolean, outputFile: string }[] = [];
|
||||
|
||||
constructor(context: Context, page: playwright.Page, onPageClose: (tab: Tab) => void) {
|
||||
this.context = context;
|
||||
@ -41,20 +54,63 @@ export class Tab {
|
||||
page.on('response', response => this._requests.set(response.request(), response));
|
||||
page.on('close', () => this._onClose());
|
||||
page.on('filechooser', chooser => {
|
||||
this.context.setModalState({
|
||||
this.setModalState({
|
||||
type: 'fileChooser',
|
||||
description: 'File chooser',
|
||||
fileChooser: chooser,
|
||||
}, this);
|
||||
});
|
||||
});
|
||||
page.on('dialog', dialog => this.context.dialogShown(this, dialog));
|
||||
page.on('dialog', dialog => this._dialogShown(dialog));
|
||||
page.on('download', download => {
|
||||
void this.context.downloadStarted(this, download);
|
||||
void this._downloadStarted(download);
|
||||
});
|
||||
page.setDefaultNavigationTimeout(60000);
|
||||
page.setDefaultTimeout(5000);
|
||||
}
|
||||
|
||||
modalStates(): ModalState[] {
|
||||
return this._modalStates;
|
||||
}
|
||||
|
||||
setModalState(modalState: ModalState) {
|
||||
this._modalStates.push(modalState);
|
||||
}
|
||||
|
||||
clearModalState(modalState: ModalState) {
|
||||
this._modalStates = this._modalStates.filter(state => state !== modalState);
|
||||
}
|
||||
|
||||
modalStatesMarkdown(): string[] {
|
||||
const result: string[] = ['### Modal state'];
|
||||
if (this._modalStates.length === 0)
|
||||
result.push('- There is no modal state present');
|
||||
for (const state of this._modalStates) {
|
||||
const tool = this.context.tools.find(tool => tool.clearsModalState === state.type);
|
||||
result.push(`- [${state.description}]: can be handled by the "${tool?.schema.name}" tool`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private _dialogShown(dialog: playwright.Dialog) {
|
||||
this.setModalState({
|
||||
type: 'dialog',
|
||||
description: `"${dialog.type()}" dialog with message "${dialog.message()}"`,
|
||||
dialog,
|
||||
});
|
||||
this._pendingAction?.dialogShown.resolve();
|
||||
}
|
||||
|
||||
private async _downloadStarted(download: playwright.Download) {
|
||||
const entry = {
|
||||
download,
|
||||
finished: false,
|
||||
outputFile: await outputFile(this.context.config, download.suggestedFilename())
|
||||
};
|
||||
this._downloads.push(entry);
|
||||
await download.saveAs(entry.outputFile);
|
||||
entry.finished = true;
|
||||
}
|
||||
|
||||
private _clearCollectedArtifacts() {
|
||||
this._consoleMessages.length = 0;
|
||||
this._recentConsoleMessages.length = 0;
|
||||
@ -105,16 +161,6 @@ export class Tab {
|
||||
await this.waitForLoadState('load', { timeout: 5000 });
|
||||
}
|
||||
|
||||
hasSnapshot(): boolean {
|
||||
return !!this._snapshot;
|
||||
}
|
||||
|
||||
snapshotOrDie(): PageSnapshot {
|
||||
if (!this._snapshot)
|
||||
throw new Error('No snapshot available');
|
||||
return this._snapshot;
|
||||
}
|
||||
|
||||
consoleMessages(): ConsoleMessage[] {
|
||||
return this._consoleMessages;
|
||||
}
|
||||
@ -123,15 +169,102 @@ export class Tab {
|
||||
return this._requests;
|
||||
}
|
||||
|
||||
async captureSnapshot() {
|
||||
this._snapshot = await PageSnapshot.create(this.page);
|
||||
takeRecentConsoleMarkdown(): string[] {
|
||||
if (!this._recentConsoleMessages.length)
|
||||
return [];
|
||||
const result = this._recentConsoleMessages.map(message => {
|
||||
return `- ${trim(message.toString(), 100)}`;
|
||||
});
|
||||
return ['', `### New console messages`, ...result];
|
||||
}
|
||||
|
||||
takeRecentConsoleMessages(): ConsoleMessage[] {
|
||||
const result = this._recentConsoleMessages.slice();
|
||||
this._recentConsoleMessages.length = 0;
|
||||
listDownloadsMarkdown(): string[] {
|
||||
if (!this._downloads.length)
|
||||
return [];
|
||||
|
||||
const result: string[] = ['', '### Downloads'];
|
||||
for (const entry of this._downloads) {
|
||||
if (entry.finished)
|
||||
result.push(`- Downloaded file ${entry.download.suggestedFilename()} to ${entry.outputFile}`);
|
||||
else
|
||||
result.push(`- Downloading file ${entry.download.suggestedFilename()} ...`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async captureSnapshot(): Promise<string> {
|
||||
const snapshot = await (this.page as PageEx)._snapshotForAI();
|
||||
return [
|
||||
`### Page state`,
|
||||
`- Page URL: ${this.page.url()}`,
|
||||
`- Page Title: ${await this.page.title()}`,
|
||||
`- Page Snapshot:`,
|
||||
'```yaml',
|
||||
snapshot,
|
||||
'```',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
private _javaScriptBlocked(): boolean {
|
||||
return this._modalStates.some(state => state.type === 'dialog');
|
||||
}
|
||||
|
||||
private async _raceAgainstModalDialogs<R>(action: () => Promise<R>): Promise<R | undefined> {
|
||||
this._pendingAction = {
|
||||
dialogShown: new ManualPromise(),
|
||||
};
|
||||
|
||||
let result: R | undefined;
|
||||
try {
|
||||
await Promise.race([
|
||||
action().then(r => result = r),
|
||||
this._pendingAction.dialogShown,
|
||||
]);
|
||||
} finally {
|
||||
this._pendingAction = undefined;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async run(callback: () => Promise<ToolActionResult>, options: { waitForNetwork?: boolean, captureSnapshot?: boolean }): Promise<{ actionResult: ToolActionResult | undefined, snapshot: string | undefined }> {
|
||||
let snapshot: string | undefined;
|
||||
const actionResult = await this._raceAgainstModalDialogs(async () => {
|
||||
try {
|
||||
if (options.waitForNetwork)
|
||||
return await waitForCompletion(this, async () => callback?.()) ?? undefined;
|
||||
else
|
||||
return await callback?.() ?? undefined;
|
||||
} finally {
|
||||
if (options.captureSnapshot && !this._javaScriptBlocked())
|
||||
snapshot = await this.captureSnapshot();
|
||||
}
|
||||
});
|
||||
return { actionResult, snapshot };
|
||||
}
|
||||
|
||||
async refLocator(params: { element: string, ref: string }): Promise<playwright.Locator> {
|
||||
return (await this.refLocators([params]))[0];
|
||||
}
|
||||
|
||||
async refLocators(params: { element: string, ref: string }[]): Promise<playwright.Locator[]> {
|
||||
const snapshot = await this.captureSnapshot();
|
||||
return params.map(param => {
|
||||
if (!snapshot.includes(`[ref=${param.ref}]`))
|
||||
throw new Error(`Ref ${param.ref} not found in the current page snapshot. Try capturing new snapshot.`);
|
||||
return this.page.locator(`aria-ref=${param.ref}`).describe(param.element);
|
||||
});
|
||||
}
|
||||
|
||||
async waitForTimeout(time: number) {
|
||||
if (this._javaScriptBlocked()) {
|
||||
await new Promise(f => setTimeout(f, time));
|
||||
return;
|
||||
}
|
||||
|
||||
await callOnPageNoTrace(this.page, page => {
|
||||
return page.evaluate(() => new Promise(f => setTimeout(f, 1000)));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export type ConsoleMessage = {
|
||||
@ -162,3 +295,9 @@ function pageErrorToConsoleMessage(errorOrValue: Error | any): ConsoleMessage {
|
||||
toString: () => String(errorOrValue),
|
||||
};
|
||||
}
|
||||
|
||||
function trim(text: string, maxLength: number) {
|
||||
if (text.length <= maxLength)
|
||||
return text;
|
||||
return text.slice(0, maxLength) + '...';
|
||||
}
|
||||
|
@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import { defineTool } from './tool.js';
|
||||
import { defineTabTool, defineTool } from './tool.js';
|
||||
|
||||
const close = defineTool({
|
||||
capability: 'core',
|
||||
@ -38,7 +38,7 @@ const close = defineTool({
|
||||
},
|
||||
});
|
||||
|
||||
const resize = defineTool({
|
||||
const resize = defineTabTool({
|
||||
capability: 'core',
|
||||
schema: {
|
||||
name: 'browser_resize',
|
||||
@ -51,9 +51,7 @@ const resize = defineTool({
|
||||
type: 'readOnly',
|
||||
},
|
||||
|
||||
handle: async (context, params) => {
|
||||
const tab = context.currentTabOrDie();
|
||||
|
||||
handle: async (tab, params) => {
|
||||
const code = [
|
||||
`// Resize browser window to ${params.width}x${params.height}`,
|
||||
`await page.setViewportSize({ width: ${params.width}, height: ${params.height} });`
|
||||
|
@ -15,9 +15,9 @@
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import { defineTool } from './tool.js';
|
||||
import { defineTabTool } from './tool.js';
|
||||
|
||||
const console = defineTool({
|
||||
const console = defineTabTool({
|
||||
capability: 'core',
|
||||
schema: {
|
||||
name: 'browser_console_messages',
|
||||
@ -26,8 +26,8 @@ const console = defineTool({
|
||||
inputSchema: z.object({}),
|
||||
type: 'readOnly',
|
||||
},
|
||||
handle: async context => {
|
||||
const messages = context.currentTabOrDie().consoleMessages();
|
||||
handle: async tab => {
|
||||
const messages = tab.consoleMessages();
|
||||
const log = messages.map(message => message.toString()).join('\n');
|
||||
return {
|
||||
code: [`// <internal code to get console messages>`],
|
||||
|
@ -15,9 +15,9 @@
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import { defineTool } from './tool.js';
|
||||
import { defineTabTool } from './tool.js';
|
||||
|
||||
const handleDialog = defineTool({
|
||||
const handleDialog = defineTabTool({
|
||||
capability: 'core',
|
||||
|
||||
schema: {
|
||||
@ -31,8 +31,8 @@ const handleDialog = defineTool({
|
||||
type: 'destructive',
|
||||
},
|
||||
|
||||
handle: async (context, params) => {
|
||||
const dialogState = context.modalStates().find(state => state.type === 'dialog');
|
||||
handle: async (tab, params) => {
|
||||
const dialogState = tab.modalStates().find(state => state.type === 'dialog');
|
||||
if (!dialogState)
|
||||
throw new Error('No dialog visible');
|
||||
|
||||
@ -41,7 +41,7 @@ const handleDialog = defineTool({
|
||||
else
|
||||
await dialogState.dialog.dismiss();
|
||||
|
||||
context.clearModalState(dialogState);
|
||||
tab.clearModalState(dialogState);
|
||||
|
||||
const code = [
|
||||
`// <internal code to handle "${dialogState.dialog.type()}" dialog>`,
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
import { defineTool } from './tool.js';
|
||||
import { defineTabTool } from './tool.js';
|
||||
import * as javascript from '../javascript.js';
|
||||
import { generateLocator } from './utils.js';
|
||||
|
||||
@ -28,7 +28,7 @@ const evaluateSchema = z.object({
|
||||
ref: z.string().optional().describe('Exact target element reference from the page snapshot'),
|
||||
});
|
||||
|
||||
const evaluate = defineTool({
|
||||
const evaluate = defineTabTool({
|
||||
capability: 'core',
|
||||
schema: {
|
||||
name: 'browser_evaluate',
|
||||
@ -38,14 +38,12 @@ const evaluate = defineTool({
|
||||
type: 'destructive',
|
||||
},
|
||||
|
||||
handle: async (context, params) => {
|
||||
const tab = context.currentTabOrDie();
|
||||
handle: async (tab, params) => {
|
||||
const code: string[] = [];
|
||||
|
||||
let locator: playwright.Locator | undefined;
|
||||
if (params.ref && params.element) {
|
||||
const snapshot = tab.snapshotOrDie();
|
||||
locator = snapshot.refLocator({ ref: params.ref, element: params.element });
|
||||
locator = await tab.refLocator({ ref: params.ref, element: params.element });
|
||||
code.push(`await page.${await generateLocator(locator)}.evaluate(${javascript.quote(params.function)});`);
|
||||
} else {
|
||||
code.push(`await page.evaluate(${javascript.quote(params.function)});`);
|
||||
|
@ -15,9 +15,9 @@
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import { defineTool } from './tool.js';
|
||||
import { defineTabTool } from './tool.js';
|
||||
|
||||
const uploadFile = defineTool({
|
||||
const uploadFile = defineTabTool({
|
||||
capability: 'core',
|
||||
|
||||
schema: {
|
||||
@ -30,8 +30,8 @@ const uploadFile = defineTool({
|
||||
type: 'destructive',
|
||||
},
|
||||
|
||||
handle: async (context, params) => {
|
||||
const modalState = context.modalStates().find(state => state.type === 'fileChooser');
|
||||
handle: async (tab, params) => {
|
||||
const modalState = tab.modalStates().find(state => state.type === 'fileChooser');
|
||||
if (!modalState)
|
||||
throw new Error('No file chooser visible');
|
||||
|
||||
@ -41,7 +41,7 @@ const uploadFile = defineTool({
|
||||
|
||||
const action = async () => {
|
||||
await modalState.fileChooser.setFiles(params.paths);
|
||||
context.clearModalState(modalState);
|
||||
tab.clearModalState(modalState);
|
||||
};
|
||||
|
||||
return {
|
||||
|
@ -16,12 +16,12 @@
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
import { defineTool } from './tool.js';
|
||||
import { defineTabTool } from './tool.js';
|
||||
import { elementSchema } from './snapshot.js';
|
||||
import { generateLocator } from './utils.js';
|
||||
import * as javascript from '../javascript.js';
|
||||
|
||||
const pressKey = defineTool({
|
||||
const pressKey = defineTabTool({
|
||||
capability: 'core',
|
||||
|
||||
schema: {
|
||||
@ -34,9 +34,7 @@ const pressKey = defineTool({
|
||||
type: 'destructive',
|
||||
},
|
||||
|
||||
handle: async (context, params) => {
|
||||
const tab = context.currentTabOrDie();
|
||||
|
||||
handle: async (tab, params) => {
|
||||
const code = [
|
||||
`// Press ${params.key}`,
|
||||
`await page.keyboard.press('${params.key}');`,
|
||||
@ -59,7 +57,7 @@ const typeSchema = elementSchema.extend({
|
||||
slowly: z.boolean().optional().describe('Whether to type one character at a time. Useful for triggering key handlers in the page. By default entire text is filled in at once.'),
|
||||
});
|
||||
|
||||
const type = defineTool({
|
||||
const type = defineTabTool({
|
||||
capability: 'core',
|
||||
schema: {
|
||||
name: 'browser_type',
|
||||
@ -69,9 +67,8 @@ const type = defineTool({
|
||||
type: 'destructive',
|
||||
},
|
||||
|
||||
handle: async (context, params) => {
|
||||
const snapshot = context.currentTabOrDie().snapshotOrDie();
|
||||
const locator = snapshot.refLocator(params);
|
||||
handle: async (tab, params) => {
|
||||
const locator = await tab.refLocator(params);
|
||||
|
||||
const code: string[] = [];
|
||||
const steps: (() => Promise<void>)[] = [];
|
||||
|
@ -15,13 +15,13 @@
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import { defineTool } from './tool.js';
|
||||
import { defineTabTool } from './tool.js';
|
||||
|
||||
const elementSchema = z.object({
|
||||
element: z.string().describe('Human-readable element description used to obtain permission to interact with the element'),
|
||||
});
|
||||
|
||||
const mouseMove = defineTool({
|
||||
const mouseMove = defineTabTool({
|
||||
capability: 'vision',
|
||||
schema: {
|
||||
name: 'browser_mouse_move_xy',
|
||||
@ -34,8 +34,7 @@ const mouseMove = defineTool({
|
||||
type: 'readOnly',
|
||||
},
|
||||
|
||||
handle: async (context, params) => {
|
||||
const tab = context.currentTabOrDie();
|
||||
handle: async (tab, params) => {
|
||||
const code = [
|
||||
`// Move mouse to (${params.x}, ${params.y})`,
|
||||
`await page.mouse.move(${params.x}, ${params.y});`,
|
||||
@ -50,7 +49,7 @@ const mouseMove = defineTool({
|
||||
},
|
||||
});
|
||||
|
||||
const mouseClick = defineTool({
|
||||
const mouseClick = defineTabTool({
|
||||
capability: 'vision',
|
||||
schema: {
|
||||
name: 'browser_mouse_click_xy',
|
||||
@ -63,8 +62,7 @@ const mouseClick = defineTool({
|
||||
type: 'destructive',
|
||||
},
|
||||
|
||||
handle: async (context, params) => {
|
||||
const tab = context.currentTabOrDie();
|
||||
handle: async (tab, params) => {
|
||||
const code = [
|
||||
`// Click mouse at coordinates (${params.x}, ${params.y})`,
|
||||
`await page.mouse.move(${params.x}, ${params.y});`,
|
||||
@ -85,7 +83,7 @@ const mouseClick = defineTool({
|
||||
},
|
||||
});
|
||||
|
||||
const mouseDrag = defineTool({
|
||||
const mouseDrag = defineTabTool({
|
||||
capability: 'vision',
|
||||
schema: {
|
||||
name: 'browser_mouse_drag_xy',
|
||||
@ -100,9 +98,7 @@ const mouseDrag = defineTool({
|
||||
type: 'destructive',
|
||||
},
|
||||
|
||||
handle: async (context, params) => {
|
||||
const tab = context.currentTabOrDie();
|
||||
|
||||
handle: async (tab, params) => {
|
||||
const code = [
|
||||
`// Drag mouse from (${params.startX}, ${params.startY}) to (${params.endX}, ${params.endY})`,
|
||||
`await page.mouse.move(${params.startX}, ${params.startY});`,
|
||||
|
@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import { defineTool } from './tool.js';
|
||||
import { defineTool, defineTabTool } from './tool.js';
|
||||
|
||||
const navigate = defineTool({
|
||||
capability: 'core',
|
||||
@ -47,7 +47,7 @@ const navigate = defineTool({
|
||||
},
|
||||
});
|
||||
|
||||
const goBack = defineTool({
|
||||
const goBack = defineTabTool({
|
||||
capability: 'core',
|
||||
schema: {
|
||||
name: 'browser_navigate_back',
|
||||
@ -57,8 +57,7 @@ const goBack = defineTool({
|
||||
type: 'readOnly',
|
||||
},
|
||||
|
||||
handle: async context => {
|
||||
const tab = await context.ensureTab();
|
||||
handle: async tab => {
|
||||
await tab.page.goBack();
|
||||
const code = [
|
||||
`// Navigate back`,
|
||||
@ -73,7 +72,7 @@ const goBack = defineTool({
|
||||
},
|
||||
});
|
||||
|
||||
const goForward = defineTool({
|
||||
const goForward = defineTabTool({
|
||||
capability: 'core',
|
||||
schema: {
|
||||
name: 'browser_navigate_forward',
|
||||
@ -82,8 +81,7 @@ const goForward = defineTool({
|
||||
inputSchema: z.object({}),
|
||||
type: 'readOnly',
|
||||
},
|
||||
handle: async context => {
|
||||
const tab = context.currentTabOrDie();
|
||||
handle: async tab => {
|
||||
await tab.page.goForward();
|
||||
const code = [
|
||||
`// Navigate forward`,
|
||||
|
@ -15,11 +15,11 @@
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import { defineTool } from './tool.js';
|
||||
import { defineTabTool } from './tool.js';
|
||||
|
||||
import type * as playwright from 'playwright';
|
||||
|
||||
const requests = defineTool({
|
||||
const requests = defineTabTool({
|
||||
capability: 'core',
|
||||
|
||||
schema: {
|
||||
@ -30,8 +30,8 @@ const requests = defineTool({
|
||||
type: 'readOnly',
|
||||
},
|
||||
|
||||
handle: async context => {
|
||||
const requests = context.currentTabOrDie().requests();
|
||||
handle: async tab => {
|
||||
const requests = tab.requests();
|
||||
const log = [...requests.entries()].map(([request, response]) => renderRequest(request, response)).join('\n');
|
||||
return {
|
||||
code: [`// <internal code to list network requests>`],
|
||||
|
@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import { defineTool } from './tool.js';
|
||||
import { defineTabTool } from './tool.js';
|
||||
|
||||
import * as javascript from '../javascript.js';
|
||||
import { outputFile } from '../config.js';
|
||||
@ -24,7 +24,7 @@ const pdfSchema = z.object({
|
||||
filename: z.string().optional().describe('File name to save the pdf to. Defaults to `page-{timestamp}.pdf` if not specified.'),
|
||||
});
|
||||
|
||||
const pdf = defineTool({
|
||||
const pdf = defineTabTool({
|
||||
capability: 'pdf',
|
||||
|
||||
schema: {
|
||||
@ -35,9 +35,8 @@ const pdf = defineTool({
|
||||
type: 'readOnly',
|
||||
},
|
||||
|
||||
handle: async (context, params) => {
|
||||
const tab = context.currentTabOrDie();
|
||||
const fileName = await outputFile(context.config, params.filename ?? `page-${new Date().toISOString()}.pdf`);
|
||||
handle: async (tab, params) => {
|
||||
const fileName = await outputFile(tab.context.config, params.filename ?? `page-${new Date().toISOString()}.pdf`);
|
||||
|
||||
const code = [
|
||||
`// Save page as ${fileName}`,
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
import { defineTool } from './tool.js';
|
||||
import { defineTabTool } from './tool.js';
|
||||
import * as javascript from '../javascript.js';
|
||||
import { outputFile } from '../config.js';
|
||||
import { generateLocator } from './utils.js';
|
||||
@ -41,7 +41,7 @@ const screenshotSchema = z.object({
|
||||
path: ['fullPage']
|
||||
});
|
||||
|
||||
const screenshot = defineTool({
|
||||
const screenshot = defineTabTool({
|
||||
capability: 'core',
|
||||
schema: {
|
||||
name: 'browser_take_screenshot',
|
||||
@ -51,10 +51,9 @@ const screenshot = defineTool({
|
||||
type: 'readOnly',
|
||||
},
|
||||
|
||||
handle: async (context, params) => {
|
||||
const tab = context.currentTabOrDie();
|
||||
handle: async (tab, params) => {
|
||||
const fileType = params.raw ? 'png' : 'jpeg';
|
||||
const fileName = await outputFile(context.config, params.filename ?? `page-${new Date().toISOString()}.${fileType}`);
|
||||
const fileName = await outputFile(tab.context.config, params.filename ?? `page-${new Date().toISOString()}.${fileType}`);
|
||||
const options: playwright.PageScreenshotOptions = {
|
||||
type: fileType,
|
||||
quality: fileType === 'png' ? undefined : 50,
|
||||
@ -70,14 +69,14 @@ const screenshot = defineTool({
|
||||
];
|
||||
|
||||
// Only get snapshot when element screenshot is needed
|
||||
const locator = params.ref ? tab.snapshotOrDie().refLocator({ element: params.element || '', ref: params.ref }) : null;
|
||||
const locator = params.ref ? await tab.refLocator({ element: params.element || '', ref: params.ref }) : null;
|
||||
|
||||
if (locator)
|
||||
code.push(`await page.${await generateLocator(locator)}.screenshot(${javascript.formatObject(options)});`);
|
||||
else
|
||||
code.push(`await page.screenshot(${javascript.formatObject(options)});`);
|
||||
|
||||
const includeBase64 = context.clientSupportsImages();
|
||||
const includeBase64 = tab.context.config.imageResponses !== 'omit';
|
||||
const action = async () => {
|
||||
const screenshot = locator ? await locator.screenshot(options) : await tab.page.screenshot(options);
|
||||
return {
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
import { z } from 'zod';
|
||||
|
||||
import { defineTool } from './tool.js';
|
||||
import { defineTabTool, defineTool } from './tool.js';
|
||||
import * as javascript from '../javascript.js';
|
||||
import { generateLocator } from './utils.js';
|
||||
|
||||
@ -51,7 +51,7 @@ const clickSchema = elementSchema.extend({
|
||||
button: z.enum(['left', 'right', 'middle']).optional().describe('Button to click, defaults to left'),
|
||||
});
|
||||
|
||||
const click = defineTool({
|
||||
const click = defineTabTool({
|
||||
capability: 'core',
|
||||
schema: {
|
||||
name: 'browser_click',
|
||||
@ -61,9 +61,8 @@ const click = defineTool({
|
||||
type: 'destructive',
|
||||
},
|
||||
|
||||
handle: async (context, params) => {
|
||||
const tab = context.currentTabOrDie();
|
||||
const locator = tab.snapshotOrDie().refLocator(params);
|
||||
handle: async (tab, params) => {
|
||||
const locator = await tab.refLocator(params);
|
||||
const button = params.button;
|
||||
const buttonAttr = button ? `{ button: '${button}' }` : '';
|
||||
|
||||
@ -85,7 +84,7 @@ const click = defineTool({
|
||||
},
|
||||
});
|
||||
|
||||
const drag = defineTool({
|
||||
const drag = defineTabTool({
|
||||
capability: 'core',
|
||||
schema: {
|
||||
name: 'browser_drag',
|
||||
@ -100,10 +99,11 @@ const drag = defineTool({
|
||||
type: 'destructive',
|
||||
},
|
||||
|
||||
handle: async (context, params) => {
|
||||
const snapshot = context.currentTabOrDie().snapshotOrDie();
|
||||
const startLocator = snapshot.refLocator({ ref: params.startRef, element: params.startElement });
|
||||
const endLocator = snapshot.refLocator({ ref: params.endRef, element: params.endElement });
|
||||
handle: async (tab, params) => {
|
||||
const [startLocator, endLocator] = await tab.refLocators([
|
||||
{ ref: params.startRef, element: params.startElement },
|
||||
{ ref: params.endRef, element: params.endElement },
|
||||
]);
|
||||
|
||||
const code = [
|
||||
`// Drag ${params.startElement} to ${params.endElement}`,
|
||||
@ -119,7 +119,7 @@ const drag = defineTool({
|
||||
},
|
||||
});
|
||||
|
||||
const hover = defineTool({
|
||||
const hover = defineTabTool({
|
||||
capability: 'core',
|
||||
schema: {
|
||||
name: 'browser_hover',
|
||||
@ -129,9 +129,8 @@ const hover = defineTool({
|
||||
type: 'readOnly',
|
||||
},
|
||||
|
||||
handle: async (context, params) => {
|
||||
const snapshot = context.currentTabOrDie().snapshotOrDie();
|
||||
const locator = snapshot.refLocator(params);
|
||||
handle: async (tab, params) => {
|
||||
const locator = await tab.refLocator(params);
|
||||
|
||||
const code = [
|
||||
`// Hover over ${params.element}`,
|
||||
@ -151,7 +150,7 @@ const selectOptionSchema = elementSchema.extend({
|
||||
values: z.array(z.string()).describe('Array of values to select in the dropdown. This can be a single value or multiple values.'),
|
||||
});
|
||||
|
||||
const selectOption = defineTool({
|
||||
const selectOption = defineTabTool({
|
||||
capability: 'core',
|
||||
schema: {
|
||||
name: 'browser_select_option',
|
||||
@ -161,9 +160,8 @@ const selectOption = defineTool({
|
||||
type: 'destructive',
|
||||
},
|
||||
|
||||
handle: async (context, params) => {
|
||||
const snapshot = context.currentTabOrDie().snapshotOrDie();
|
||||
const locator = snapshot.refLocator(params);
|
||||
handle: async (tab, params) => {
|
||||
const locator = await tab.refLocator(params);
|
||||
|
||||
const code = [
|
||||
`// Select options [${params.values.join(', ')}] in ${params.element}`,
|
||||
|
@ -37,7 +37,7 @@ const listTabs = defineTool({
|
||||
resultOverride: {
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: await context.listTabsMarkdown(),
|
||||
text: (await context.listTabsMarkdown()).join('\n'),
|
||||
}],
|
||||
},
|
||||
};
|
||||
@ -85,9 +85,9 @@ const newTab = defineTool({
|
||||
},
|
||||
|
||||
handle: async (context, params) => {
|
||||
await context.newTab();
|
||||
const tab = await context.newTab();
|
||||
if (params.url)
|
||||
await context.currentTabOrDie().navigate(params.url);
|
||||
await tab.navigate(params.url);
|
||||
|
||||
const code = [
|
||||
`// <internal code to open a new tab>`,
|
||||
|
@ -19,6 +19,7 @@ import type { z } from 'zod';
|
||||
import type { Context } from '../context.js';
|
||||
import type * as playwright from 'playwright';
|
||||
import type { ToolCapability } from '../../config.js';
|
||||
import type { Tab } from '../tab.js';
|
||||
|
||||
export type ToolSchema<Input extends InputType> = {
|
||||
name: string;
|
||||
@ -64,3 +65,25 @@ export type Tool<Input extends InputType = InputType> = {
|
||||
export function defineTool<Input extends InputType>(tool: Tool<Input>): Tool<Input> {
|
||||
return tool;
|
||||
}
|
||||
|
||||
export type TabTool<Input extends InputType = InputType> = {
|
||||
capability: ToolCapability;
|
||||
schema: ToolSchema<Input>;
|
||||
clearsModalState?: ModalState['type'];
|
||||
handle: (tab: Tab, params: z.output<Input>) => Promise<ToolResult>;
|
||||
};
|
||||
|
||||
export function defineTabTool<Input extends InputType>(tool: TabTool<Input>): Tool<Input> {
|
||||
return {
|
||||
...tool,
|
||||
handle: async (context, params) => {
|
||||
const tab = context.currentTabOrDie();
|
||||
const modalStates = tab.modalStates().map(state => state.type);
|
||||
if (tool.clearsModalState && !modalStates.includes(tool.clearsModalState))
|
||||
throw new Error(`The tool "${tool.schema.name}" can only be used when there is related modal state present.\n` + tab.modalStatesMarkdown().join('\n'));
|
||||
if (!tool.clearsModalState && modalStates.length)
|
||||
throw new Error(`Tool "${tool.schema.name}" does not handle the modal state.\n` + tab.modalStatesMarkdown().join('\n'));
|
||||
return tool.handle(tab, params);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -18,10 +18,9 @@
|
||||
import { asLocator } from 'playwright-core/lib/utils';
|
||||
|
||||
import type * as playwright from 'playwright';
|
||||
import type { Context } from '../context.js';
|
||||
import type { Tab } from '../tab.js';
|
||||
|
||||
export async function waitForCompletion<R>(context: Context, tab: Tab, callback: () => Promise<R>): Promise<R> {
|
||||
export async function waitForCompletion<R>(tab: Tab, callback: () => Promise<R>): Promise<R> {
|
||||
const requests = new Set<playwright.Request>();
|
||||
let frameNavigated = false;
|
||||
let waitCallback: () => void = () => {};
|
||||
@ -65,7 +64,7 @@ export async function waitForCompletion<R>(context: Context, tab: Tab, callback:
|
||||
if (!requests.size && !frameNavigated)
|
||||
waitCallback();
|
||||
await waitBarrier;
|
||||
await context.waitForTimeout(1000);
|
||||
await tab.waitForTimeout(1000);
|
||||
return result;
|
||||
} finally {
|
||||
dispose();
|
||||
|
@ -41,7 +41,7 @@ test('cdp server reuse tab', async ({ cdpServer, startClient, server }) => {
|
||||
element: 'Hello, world!',
|
||||
ref: 'f0',
|
||||
},
|
||||
})).toHaveTextContent(`Error: No current snapshot available. Capture a snapshot or navigate to a new location first.`);
|
||||
})).toHaveTextContent(`Error: No open pages available. Use the \"browser_navigate\" tool to navigate to a page first.`);
|
||||
|
||||
expect(await client.callTool({
|
||||
name: 'browser_snapshot',
|
||||
|
@ -242,7 +242,7 @@ test('old locator error message', async ({ client, server }) => {
|
||||
element: 'Button 2',
|
||||
ref: 'e3',
|
||||
},
|
||||
})).toContainTextContent('Ref not found');
|
||||
})).toContainTextContent('Ref e3 not found in the current page snapshot. Try capturing new snapshot.');
|
||||
});
|
||||
|
||||
test('visibility: hidden > visible should be shown', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright-mcp/issues/535' } }, async ({ client, server }) => {
|
||||
|
@ -38,7 +38,7 @@ test('browser_file_upload', async ({ client, server }, testInfo) => {
|
||||
name: 'browser_file_upload',
|
||||
arguments: { paths: [] },
|
||||
})).toHaveTextContent(`
|
||||
The tool "browser_file_upload" can only be used when there is related modal state present.
|
||||
Error: The tool "browser_file_upload" can only be used when there is related modal state present.
|
||||
### Modal state
|
||||
- There is no modal state present
|
||||
`.trim());
|
||||
@ -88,7 +88,7 @@ The tool "browser_file_upload" can only be used when there is related modal stat
|
||||
},
|
||||
});
|
||||
|
||||
expect(response).toContainTextContent(`Tool "browser_click" does not handle the modal state.
|
||||
expect(response).toContainTextContent(`Error: Tool "browser_click" does not handle the modal state.
|
||||
### Modal state
|
||||
- [File chooser]: can be handled by the "browser_file_upload" tool`);
|
||||
}
|
||||
|
@ -49,7 +49,7 @@ test('create new tab', async ({ client }) => {
|
||||
- 0: [] (about:blank)
|
||||
- 1: (current) [Tab one] (data:text/html,<title>Tab one</title><body>Body one</body>)
|
||||
|
||||
### Current tab
|
||||
### Page state
|
||||
- Page URL: data:text/html,<title>Tab one</title><body>Body one</body>
|
||||
- Page Title: Tab one
|
||||
- Page Snapshot:
|
||||
@ -63,7 +63,7 @@ test('create new tab', async ({ client }) => {
|
||||
- 1: [Tab one] (data:text/html,<title>Tab one</title><body>Body one</body>)
|
||||
- 2: (current) [Tab two] (data:text/html,<title>Tab two</title><body>Body two</body>)
|
||||
|
||||
### Current tab
|
||||
### Page state
|
||||
- Page URL: data:text/html,<title>Tab two</title><body>Body two</body>
|
||||
- Page Title: Tab two
|
||||
- Page Snapshot:
|
||||
@ -75,23 +75,21 @@ test('create new tab', async ({ client }) => {
|
||||
test('select tab', async ({ client }) => {
|
||||
await createTab(client, 'Tab one', 'Body one');
|
||||
await createTab(client, 'Tab two', 'Body two');
|
||||
expect(await client.callTool({
|
||||
|
||||
const result = await client.callTool({
|
||||
name: 'browser_tab_select',
|
||||
arguments: {
|
||||
index: 1,
|
||||
},
|
||||
})).toHaveTextContent(`
|
||||
### Ran Playwright code
|
||||
\`\`\`js
|
||||
// <internal code to select tab 1>
|
||||
\`\`\`
|
||||
|
||||
});
|
||||
expect(result).toContainTextContent(`
|
||||
### Open tabs
|
||||
- 0: [] (about:blank)
|
||||
- 1: (current) [Tab one] (data:text/html,<title>Tab one</title><body>Body one</body>)
|
||||
- 2: [Tab two] (data:text/html,<title>Tab two</title><body>Body two</body>)
|
||||
- 2: [Tab two] (data:text/html,<title>Tab two</title><body>Body two</body>)`);
|
||||
|
||||
### Current tab
|
||||
expect(result).toContainTextContent(`
|
||||
### Page state
|
||||
- Page URL: data:text/html,<title>Tab one</title><body>Body one</body>
|
||||
- Page Title: Tab one
|
||||
- Page Snapshot:
|
||||
@ -103,22 +101,20 @@ test('select tab', async ({ client }) => {
|
||||
test('close tab', async ({ client }) => {
|
||||
await createTab(client, 'Tab one', 'Body one');
|
||||
await createTab(client, 'Tab two', 'Body two');
|
||||
expect(await client.callTool({
|
||||
|
||||
const result = await client.callTool({
|
||||
name: 'browser_tab_close',
|
||||
arguments: {
|
||||
index: 2,
|
||||
},
|
||||
})).toHaveTextContent(`
|
||||
### Ran Playwright code
|
||||
\`\`\`js
|
||||
// <internal code to close tab 2>
|
||||
\`\`\`
|
||||
|
||||
});
|
||||
expect(result).toContainTextContent(`
|
||||
### Open tabs
|
||||
- 0: [] (about:blank)
|
||||
- 1: (current) [Tab one] (data:text/html,<title>Tab one</title><body>Body one</body>)
|
||||
- 1: (current) [Tab one] (data:text/html,<title>Tab one</title><body>Body one</body>)`);
|
||||
|
||||
### Current tab
|
||||
expect(result).toContainTextContent(`
|
||||
### Page state
|
||||
- Page URL: data:text/html,<title>Tab one</title><body>Body one</body>
|
||||
- Page Title: Tab one
|
||||
- Page Snapshot:
|
||||
|
Loading…
x
Reference in New Issue
Block a user