mirror of
https://github.com/microsoft/playwright-mcp.git
synced 2025-07-26 08:32:26 +08:00
feat(trace): allow saving trajectory as trace (#426)
This commit is contained in:
parent
fea50e6840
commit
aa6ac51f92
@ -146,6 +146,8 @@ Playwright MCP server supports following arguments. They can be provided in the
|
|||||||
example ".com,chromium.org,.domain.com"
|
example ".com,chromium.org,.domain.com"
|
||||||
--proxy-server <proxy> specify proxy server, for example
|
--proxy-server <proxy> specify proxy server, for example
|
||||||
"http://myproxy:3128" or "socks5://myproxy:8080"
|
"http://myproxy:3128" or "socks5://myproxy:8080"
|
||||||
|
--save-trace Whether to save the Playwright Trace of the
|
||||||
|
session into the output directory.
|
||||||
--storage-state <path> path to the storage state file for isolated
|
--storage-state <path> path to the storage state file for isolated
|
||||||
sessions.
|
sessions.
|
||||||
--user-agent <ua string> specify user agent string
|
--user-agent <ua string> specify user agent string
|
||||||
|
5
config.d.ts
vendored
5
config.d.ts
vendored
@ -94,6 +94,11 @@ export type Config = {
|
|||||||
*/
|
*/
|
||||||
vision?: boolean;
|
vision?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to save the Playwright trace of the session into the output directory.
|
||||||
|
*/
|
||||||
|
saveTrace?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The directory to save output files.
|
* The directory to save output files.
|
||||||
*/
|
*/
|
||||||
|
@ -44,6 +44,7 @@ export type CLIOptions = {
|
|||||||
port?: number;
|
port?: number;
|
||||||
proxyBypass?: string;
|
proxyBypass?: string;
|
||||||
proxyServer?: string;
|
proxyServer?: string;
|
||||||
|
saveTrace?: boolean;
|
||||||
storageState?: string;
|
storageState?: string;
|
||||||
userAgent?: string;
|
userAgent?: string;
|
||||||
userDataDir?: string;
|
userDataDir?: string;
|
||||||
@ -67,7 +68,7 @@ const defaultConfig: FullConfig = {
|
|||||||
allowedOrigins: undefined,
|
allowedOrigins: undefined,
|
||||||
blockedOrigins: undefined,
|
blockedOrigins: undefined,
|
||||||
},
|
},
|
||||||
outputDir: path.join(os.tmpdir(), 'playwright-mcp-output'),
|
outputDir: path.join(os.tmpdir(), 'playwright-mcp-output', sanitizeForFilePath(new Date().toISOString())),
|
||||||
};
|
};
|
||||||
|
|
||||||
type BrowserUserConfig = NonNullable<Config['browser']>;
|
type BrowserUserConfig = NonNullable<Config['browser']>;
|
||||||
@ -91,7 +92,8 @@ export async function resolveCLIConfig(cliOptions: CLIOptions): Promise<FullConf
|
|||||||
const cliOverrides = await configFromCLIOptions(cliOptions);
|
const cliOverrides = await configFromCLIOptions(cliOptions);
|
||||||
const result = mergeConfig(mergeConfig(defaultConfig, configInFile), cliOverrides);
|
const result = mergeConfig(mergeConfig(defaultConfig, configInFile), cliOverrides);
|
||||||
// Derive artifact output directory from config.outputDir
|
// Derive artifact output directory from config.outputDir
|
||||||
result.browser.launchOptions.tracesDir = path.join(result.outputDir, 'traces');
|
if (result.saveTrace)
|
||||||
|
result.browser.launchOptions.tracesDir = path.join(result.outputDir, 'traces');
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -189,6 +191,7 @@ export async function configFromCLIOptions(cliOptions: CLIOptions): Promise<Conf
|
|||||||
allowedOrigins: cliOptions.allowedOrigins,
|
allowedOrigins: cliOptions.allowedOrigins,
|
||||||
blockedOrigins: cliOptions.blockedOrigins,
|
blockedOrigins: cliOptions.blockedOrigins,
|
||||||
},
|
},
|
||||||
|
saveTrace: cliOptions.saveTrace,
|
||||||
outputDir: cliOptions.outputDir,
|
outputDir: cliOptions.outputDir,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -262,7 +265,6 @@ function mergeConfig(base: FullConfig, overrides: Config): FullConfig {
|
|||||||
network: {
|
network: {
|
||||||
...pickDefined(base.network),
|
...pickDefined(base.network),
|
||||||
...pickDefined(overrides.network),
|
...pickDefined(overrides.network),
|
||||||
},
|
}
|
||||||
outputDir: overrides.outputDir ?? base.outputDir ?? defaultConfig.outputDir,
|
} as FullConfig;
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ import path from 'node:path';
|
|||||||
|
|
||||||
import * as playwright from 'playwright';
|
import * as playwright from 'playwright';
|
||||||
|
|
||||||
import { waitForCompletion } from './tools/utils.js';
|
import { callOnPageNoTrace, waitForCompletion } from './tools/utils.js';
|
||||||
import { ManualPromise } from './manualPromise.js';
|
import { ManualPromise } from './manualPromise.js';
|
||||||
import { Tab } from './tab.js';
|
import { Tab } from './tab.js';
|
||||||
import { outputFile } from './config.js';
|
import { outputFile } from './config.js';
|
||||||
@ -112,7 +112,7 @@ export class Context {
|
|||||||
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.title();
|
||||||
const url = tab.page.url();
|
const url = tab.page.url();
|
||||||
const current = tab === this._currentTab ? ' (current)' : '';
|
const current = tab === this._currentTab ? ' (current)' : '';
|
||||||
lines.push(`- ${i + 1}:${current} [${title}] (${url})`);
|
lines.push(`- ${i + 1}:${current} [${title}] (${url})`);
|
||||||
@ -149,7 +149,7 @@ export class Context {
|
|||||||
let actionResult: { content?: (ImageContent | TextContent)[] } | undefined;
|
let actionResult: { content?: (ImageContent | TextContent)[] } | undefined;
|
||||||
try {
|
try {
|
||||||
if (waitForNetwork)
|
if (waitForNetwork)
|
||||||
actionResult = await waitForCompletion(this, tab.page, async () => racingAction?.()) ?? undefined;
|
actionResult = await waitForCompletion(this, tab, async () => racingAction?.()) ?? undefined;
|
||||||
else
|
else
|
||||||
actionResult = await racingAction?.() ?? undefined;
|
actionResult = await racingAction?.() ?? undefined;
|
||||||
} finally {
|
} finally {
|
||||||
@ -193,7 +193,7 @@ ${code.join('\n')}
|
|||||||
|
|
||||||
result.push(
|
result.push(
|
||||||
`- Page URL: ${tab.page.url()}`,
|
`- Page URL: ${tab.page.url()}`,
|
||||||
`- Page Title: ${await tab.page.title()}`
|
`- Page Title: ${await tab.title()}`
|
||||||
);
|
);
|
||||||
|
|
||||||
if (captureSnapshot && tab.hasSnapshot())
|
if (captureSnapshot && tab.hasSnapshot())
|
||||||
@ -213,10 +213,14 @@ ${code.join('\n')}
|
|||||||
}
|
}
|
||||||
|
|
||||||
async waitForTimeout(time: number) {
|
async waitForTimeout(time: number) {
|
||||||
if (this._currentTab && !this._javaScriptBlocked())
|
if (!this._currentTab || this._javaScriptBlocked()) {
|
||||||
await this._currentTab.page.evaluate(() => new Promise(f => setTimeout(f, 1000)));
|
|
||||||
else
|
|
||||||
await new Promise(f => setTimeout(f, time));
|
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> {
|
private async _raceAgainstModalDialogs(action: () => Promise<ToolActionResult>): Promise<ToolActionResult> {
|
||||||
@ -288,6 +292,8 @@ ${code.join('\n')}
|
|||||||
this._browserContextPromise = undefined;
|
this._browserContextPromise = undefined;
|
||||||
|
|
||||||
await promise.then(async ({ browserContext, browser }) => {
|
await promise.then(async ({ browserContext, browser }) => {
|
||||||
|
if (this.config.saveTrace)
|
||||||
|
await browserContext.tracing.stop();
|
||||||
await browserContext.close().then(async () => {
|
await browserContext.close().then(async () => {
|
||||||
await browser?.close();
|
await browser?.close();
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
@ -324,6 +330,14 @@ ${code.join('\n')}
|
|||||||
for (const page of browserContext.pages())
|
for (const page of browserContext.pages())
|
||||||
this._onPageCreated(page);
|
this._onPageCreated(page);
|
||||||
browserContext.on('page', page => this._onPageCreated(page));
|
browserContext.on('page', page => this._onPageCreated(page));
|
||||||
|
if (this.config.saveTrace) {
|
||||||
|
await browserContext.tracing.start({
|
||||||
|
name: 'trace',
|
||||||
|
screenshots: false,
|
||||||
|
snapshots: true,
|
||||||
|
sources: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
return { browser, browserContext };
|
return { browser, browserContext };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -394,9 +408,5 @@ async function createUserDataDir(browserConfig: FullConfig['browser']) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateLocator(locator: playwright.Locator): Promise<string> {
|
|
||||||
return (locator as any)._generateLocatorString();
|
|
||||||
}
|
|
||||||
|
|
||||||
const __filename = url.fileURLToPath(import.meta.url);
|
const __filename = url.fileURLToPath(import.meta.url);
|
||||||
export const packageJSON = JSON.parse(fs.readFileSync(path.join(path.dirname(__filename), '..', 'package.json'), 'utf8'));
|
export const packageJSON = JSON.parse(fs.readFileSync(path.join(path.dirname(__filename), '..', 'package.json'), 'utf8'));
|
||||||
|
@ -15,6 +15,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as playwright from 'playwright';
|
import * as playwright from 'playwright';
|
||||||
|
import { callOnPageNoTrace } from './tools/utils.js';
|
||||||
|
|
||||||
|
type PageEx = playwright.Page & {
|
||||||
|
_snapshotForAI: () => Promise<string>;
|
||||||
|
};
|
||||||
|
|
||||||
export class PageSnapshot {
|
export class PageSnapshot {
|
||||||
private _page: playwright.Page;
|
private _page: playwright.Page;
|
||||||
@ -35,11 +40,11 @@ export class PageSnapshot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _build() {
|
private async _build() {
|
||||||
const yamlDocument = await (this._page as any)._snapshotForAI();
|
const snapshot = await callOnPageNoTrace(this._page, page => (page as PageEx)._snapshotForAI());
|
||||||
this._text = [
|
this._text = [
|
||||||
`- Page Snapshot`,
|
`- Page Snapshot`,
|
||||||
'```yaml',
|
'```yaml',
|
||||||
yamlDocument.toString({ indentSeq: false }).trim(),
|
snapshot,
|
||||||
'```',
|
'```',
|
||||||
].join('\n');
|
].join('\n');
|
||||||
}
|
}
|
||||||
|
@ -44,6 +44,7 @@ program
|
|||||||
.option('--port <port>', 'port to listen on for SSE transport.')
|
.option('--port <port>', 'port to listen on for SSE transport.')
|
||||||
.option('--proxy-bypass <bypass>', 'comma-separated domains to bypass proxy, for example ".com,chromium.org,.domain.com"')
|
.option('--proxy-bypass <bypass>', 'comma-separated domains to bypass proxy, for example ".com,chromium.org,.domain.com"')
|
||||||
.option('--proxy-server <proxy>', 'specify proxy server, for example "http://myproxy:3128" or "socks5://myproxy:8080"')
|
.option('--proxy-server <proxy>', 'specify proxy server, for example "http://myproxy:3128" or "socks5://myproxy:8080"')
|
||||||
|
.option('--save-trace', 'Whether to save the Playwright Trace of the session into the output directory.')
|
||||||
.option('--storage-state <path>', 'path to the storage state file for isolated sessions.')
|
.option('--storage-state <path>', 'path to the storage state file for isolated sessions.')
|
||||||
.option('--user-agent <ua string>', 'specify user agent string')
|
.option('--user-agent <ua string>', 'specify user agent string')
|
||||||
.option('--user-data-dir <path>', 'path to the user data directory. If not specified, a temporary directory will be created.')
|
.option('--user-data-dir <path>', 'path to the user data directory. If not specified, a temporary directory will be created.')
|
||||||
|
13
src/tab.ts
13
src/tab.ts
@ -19,6 +19,7 @@ import * as playwright from 'playwright';
|
|||||||
import { PageSnapshot } from './pageSnapshot.js';
|
import { PageSnapshot } from './pageSnapshot.js';
|
||||||
|
|
||||||
import type { Context } from './context.js';
|
import type { Context } from './context.js';
|
||||||
|
import { callOnPageNoTrace } from './tools/utils.js';
|
||||||
|
|
||||||
export class Tab {
|
export class Tab {
|
||||||
readonly context: Context;
|
readonly context: Context;
|
||||||
@ -61,10 +62,18 @@ export class Tab {
|
|||||||
this._onPageClose(this);
|
this._onPageClose(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async title(): Promise<string> {
|
||||||
|
return await callOnPageNoTrace(this.page, page => page.title());
|
||||||
|
}
|
||||||
|
|
||||||
|
async waitForLoadState(state: 'load', options?: { timeout?: number }): Promise<void> {
|
||||||
|
await callOnPageNoTrace(this.page, page => page.waitForLoadState(state, options).catch(() => {}));
|
||||||
|
}
|
||||||
|
|
||||||
async navigate(url: string) {
|
async navigate(url: string) {
|
||||||
this._clearCollectedArtifacts();
|
this._clearCollectedArtifacts();
|
||||||
|
|
||||||
const downloadEvent = this.page.waitForEvent('download').catch(() => {});
|
const downloadEvent = callOnPageNoTrace(this.page, page => page.waitForEvent('download').catch(() => {}));
|
||||||
try {
|
try {
|
||||||
await this.page.goto(url, { waitUntil: 'domcontentloaded' });
|
await this.page.goto(url, { waitUntil: 'domcontentloaded' });
|
||||||
} catch (_e: unknown) {
|
} catch (_e: unknown) {
|
||||||
@ -85,7 +94,7 @@ export class Tab {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cap load event to 5 seconds, the page is operational at this point.
|
// Cap load event to 5 seconds, the page is operational at this point.
|
||||||
await this.page.waitForLoadState('load', { timeout: 5000 }).catch(() => {});
|
await this.waitForLoadState('load', { timeout: 5000 });
|
||||||
}
|
}
|
||||||
|
|
||||||
hasSnapshot(): boolean {
|
hasSnapshot(): boolean {
|
||||||
|
@ -16,8 +16,9 @@
|
|||||||
|
|
||||||
import type * as playwright from 'playwright';
|
import type * as playwright from 'playwright';
|
||||||
import type { Context } from '../context.js';
|
import type { Context } from '../context.js';
|
||||||
|
import type { Tab } from '../tab.js';
|
||||||
|
|
||||||
export async function waitForCompletion<R>(context: Context, page: playwright.Page, callback: () => Promise<R>): Promise<R> {
|
export async function waitForCompletion<R>(context: Context, tab: Tab, callback: () => Promise<R>): Promise<R> {
|
||||||
const requests = new Set<playwright.Request>();
|
const requests = new Set<playwright.Request>();
|
||||||
let frameNavigated = false;
|
let frameNavigated = false;
|
||||||
let waitCallback: () => void = () => {};
|
let waitCallback: () => void = () => {};
|
||||||
@ -36,9 +37,7 @@ export async function waitForCompletion<R>(context: Context, page: playwright.Pa
|
|||||||
frameNavigated = true;
|
frameNavigated = true;
|
||||||
dispose();
|
dispose();
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
void frame.waitForLoadState('load').then(() => {
|
void tab.waitForLoadState('load').then(waitCallback);
|
||||||
waitCallback();
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onTimeout = () => {
|
const onTimeout = () => {
|
||||||
@ -46,15 +45,15 @@ export async function waitForCompletion<R>(context: Context, page: playwright.Pa
|
|||||||
waitCallback();
|
waitCallback();
|
||||||
};
|
};
|
||||||
|
|
||||||
page.on('request', requestListener);
|
tab.page.on('request', requestListener);
|
||||||
page.on('requestfinished', requestFinishedListener);
|
tab.page.on('requestfinished', requestFinishedListener);
|
||||||
page.on('framenavigated', frameNavigateListener);
|
tab.page.on('framenavigated', frameNavigateListener);
|
||||||
const timeout = setTimeout(onTimeout, 10000);
|
const timeout = setTimeout(onTimeout, 10000);
|
||||||
|
|
||||||
const dispose = () => {
|
const dispose = () => {
|
||||||
page.off('request', requestListener);
|
tab.page.off('request', requestListener);
|
||||||
page.off('requestfinished', requestFinishedListener);
|
tab.page.off('requestfinished', requestFinishedListener);
|
||||||
page.off('framenavigated', frameNavigateListener);
|
tab.page.off('framenavigated', frameNavigateListener);
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -79,5 +78,9 @@ export function sanitizeForFilePath(s: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function generateLocator(locator: playwright.Locator): Promise<string> {
|
export async function generateLocator(locator: playwright.Locator): Promise<string> {
|
||||||
return (locator as any)._generateLocatorString();
|
return (locator as any)._frame._wrapApiCall(() => (locator as any)._generateLocatorString(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function callOnPageNoTrace<T>(page: playwright.Page, callback: (page: playwright.Page) => Promise<T>): Promise<T> {
|
||||||
|
return await (page as any)._wrapApiCall(() => callback(page), true);
|
||||||
}
|
}
|
||||||
|
@ -126,7 +126,12 @@ test('clicking on download link emits download', async ({ startClient, localOutp
|
|||||||
- Downloaded file test.txt to ${path.join(outputDir, 'test.txt')}`);
|
- Downloaded file test.txt to ${path.join(outputDir, 'test.txt')}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('navigating to download link emits download', async ({ client, server, mcpBrowser }) => {
|
test('navigating to download link emits download', async ({ startClient, localOutputPath, mcpBrowser, server }) => {
|
||||||
|
const outputDir = localOutputPath('output');
|
||||||
|
const client = await startClient({
|
||||||
|
args: ['--output-dir', outputDir],
|
||||||
|
});
|
||||||
|
|
||||||
test.skip(mcpBrowser === 'webkit' && process.platform === 'linux', 'https://github.com/microsoft/playwright/blob/8e08fdb52c27bb75de9bf87627bf740fadab2122/tests/library/download.spec.ts#L436');
|
test.skip(mcpBrowser === 'webkit' && process.platform === 'linux', 'https://github.com/microsoft/playwright/blob/8e08fdb52c27bb75de9bf87627bf740fadab2122/tests/library/download.spec.ts#L436');
|
||||||
server.route('/download', (req, res) => {
|
server.route('/download', (req, res) => {
|
||||||
res.writeHead(200, {
|
res.writeHead(200, {
|
||||||
|
@ -30,7 +30,12 @@ test('save as pdf unavailable', async ({ startClient, server }) => {
|
|||||||
})).toHaveTextContent(/Tool \"browser_pdf_save\" not found/);
|
})).toHaveTextContent(/Tool \"browser_pdf_save\" not found/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('save as pdf', async ({ client, mcpBrowser, server }) => {
|
test('save as pdf', async ({ startClient, mcpBrowser, server, localOutputPath }) => {
|
||||||
|
const outputDir = localOutputPath('output');
|
||||||
|
const client = await startClient({
|
||||||
|
config: { outputDir },
|
||||||
|
});
|
||||||
|
|
||||||
test.skip(!!mcpBrowser && !['chromium', 'chrome', 'msedge'].includes(mcpBrowser), 'Save as PDF is only supported in Chromium.');
|
test.skip(!!mcpBrowser && !['chromium', 'chrome', 'msedge'].includes(mcpBrowser), 'Save as PDF is only supported in Chromium.');
|
||||||
|
|
||||||
expect(await client.callTool({
|
expect(await client.callTool({
|
||||||
|
@ -18,7 +18,11 @@ import fs from 'fs';
|
|||||||
|
|
||||||
import { test, expect } from './fixtures.js';
|
import { test, expect } from './fixtures.js';
|
||||||
|
|
||||||
test('browser_take_screenshot (viewport)', async ({ client, server }) => {
|
test('browser_take_screenshot (viewport)', async ({ startClient, server, localOutputPath }) => {
|
||||||
|
const outputDir = localOutputPath('output');
|
||||||
|
const client = await startClient({
|
||||||
|
args: ['--output-dir', outputDir],
|
||||||
|
});
|
||||||
expect(await client.callTool({
|
expect(await client.callTool({
|
||||||
name: 'browser_navigate',
|
name: 'browser_navigate',
|
||||||
arguments: { url: server.HELLO_WORLD },
|
arguments: { url: server.HELLO_WORLD },
|
||||||
@ -41,7 +45,11 @@ test('browser_take_screenshot (viewport)', async ({ client, server }) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('browser_take_screenshot (element)', async ({ client, server }) => {
|
test('browser_take_screenshot (element)', async ({ startClient, server, localOutputPath }) => {
|
||||||
|
const outputDir = localOutputPath('output');
|
||||||
|
const client = await startClient({
|
||||||
|
args: ['--output-dir', outputDir],
|
||||||
|
});
|
||||||
expect(await client.callTool({
|
expect(await client.callTool({
|
||||||
name: 'browser_navigate',
|
name: 'browser_navigate',
|
||||||
arguments: { url: server.HELLO_WORLD },
|
arguments: { url: server.HELLO_WORLD },
|
||||||
@ -166,9 +174,10 @@ test('browser_take_screenshot (filename: "output.jpeg")', async ({ startClient,
|
|||||||
expect(files[0]).toMatch(/^output\.jpeg$/);
|
expect(files[0]).toMatch(/^output\.jpeg$/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('browser_take_screenshot (noImageResponses)', async ({ startClient, server }) => {
|
test('browser_take_screenshot (noImageResponses)', async ({ startClient, server, localOutputPath }) => {
|
||||||
const client = await startClient({
|
const client = await startClient({
|
||||||
config: {
|
config: {
|
||||||
|
outputDir: localOutputPath('output'),
|
||||||
noImageResponses: true,
|
noImageResponses: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -194,8 +203,12 @@ test('browser_take_screenshot (noImageResponses)', async ({ startClient, server
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('browser_take_screenshot (cursor)', async ({ startClient, server }) => {
|
test('browser_take_screenshot (cursor)', async ({ startClient, server, localOutputPath }) => {
|
||||||
const client = await startClient({ clientName: 'cursor:vscode' });
|
const outputDir = localOutputPath('output');
|
||||||
|
const client = await startClient({
|
||||||
|
clientName: 'cursor:vscode',
|
||||||
|
config: { outputDir },
|
||||||
|
});
|
||||||
|
|
||||||
expect(await client.callTool({
|
expect(await client.callTool({
|
||||||
name: 'browser_navigate',
|
name: 'browser_navigate',
|
||||||
|
@ -65,11 +65,17 @@ test('streamable http transport', async ({ serverEndpoint }) => {
|
|||||||
expect(transport.sessionId, 'has session support').toBeDefined();
|
expect(transport.sessionId, 'has session support').toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('sse transport via public API', async ({ server }) => {
|
test('sse transport via public API', async ({ server, localOutputPath }) => {
|
||||||
|
const userDataDir = localOutputPath('user-data-dir');
|
||||||
const sessions = new Map<string, SSEServerTransport>();
|
const sessions = new Map<string, SSEServerTransport>();
|
||||||
const mcpServer = http.createServer(async (req, res) => {
|
const mcpServer = http.createServer(async (req, res) => {
|
||||||
if (req.method === 'GET') {
|
if (req.method === 'GET') {
|
||||||
const connection = await createConnection({ browser: { launchOptions: { headless: true } } });
|
const connection = await createConnection({
|
||||||
|
browser: {
|
||||||
|
userDataDir,
|
||||||
|
launchOptions: { headless: true }
|
||||||
|
},
|
||||||
|
});
|
||||||
const transport = new SSEServerTransport('/sse', res);
|
const transport = new SSEServerTransport('/sse', res);
|
||||||
sessions.set(transport.sessionId, transport);
|
sessions.set(transport.sessionId, transport);
|
||||||
await connection.connect(transport);
|
await connection.connect(transport);
|
||||||
|
34
tests/trace.spec.ts
Normal file
34
tests/trace.spec.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
/**
|
||||||
|
* 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 fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
import { test, expect } from './fixtures.js';
|
||||||
|
|
||||||
|
test('check that trace is saved', async ({ startClient, server, localOutputPath }) => {
|
||||||
|
const outputDir = localOutputPath('output');
|
||||||
|
const client = await startClient({
|
||||||
|
args: ['--save-trace', `--output-dir=${outputDir}`],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(await client.callTool({
|
||||||
|
name: 'browser_navigate',
|
||||||
|
arguments: { url: server.HELLO_WORLD },
|
||||||
|
})).toContainTextContent(`Navigate to http://localhost`);
|
||||||
|
|
||||||
|
expect(fs.existsSync(path.join(outputDir, 'traces', 'trace.trace'))).toBeTruthy();
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user