Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 15 additions & 16 deletions packages/playwright/src/mcp/browser/browserContextFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import * as playwright from 'playwright-core';
import { registryDirectory } from 'playwright-core/lib/server/registry/index';
import { startTraceViewerServer } from 'playwright-core/lib/server';
import { logUnhandledError, testDebug } from '../log';
import { outputFile, tmpDir } from './config';
import { outputFile } from './config';
import { firstRootPath } from '../sdk/server';

import type { FullConfig } from './config';
Expand All @@ -44,7 +44,7 @@ export function contextFactory(config: FullConfig): BrowserContextFactory {

export type BrowserContextFactoryResult = {
browserContext: playwright.BrowserContext;
close: (afterClose: () => Promise<void>) => Promise<void>;
close: () => Promise<void>;
};

export interface BrowserContextFactory {
Expand Down Expand Up @@ -94,24 +94,23 @@ class BaseContextFactory implements BrowserContextFactory {
async createContext(clientInfo: ClientInfo): Promise<BrowserContextFactoryResult> {
testDebug(`create browser context (${this._logName})`);
const browser = await this._obtainBrowser(clientInfo);
const browserContext = await this._doCreateContext(browser);
const browserContext = await this._doCreateContext(browser, clientInfo);
await addInitScript(browserContext, this.config.browser.initScript);
return {
browserContext,
close: (afterClose: () => Promise<void>) => this._closeBrowserContext(browserContext, browser, afterClose)
close: () => this._closeBrowserContext(browserContext, browser)
};
}

protected async _doCreateContext(browser: playwright.Browser): Promise<playwright.BrowserContext> {
protected async _doCreateContext(browser: playwright.Browser, clientInfo: ClientInfo): Promise<playwright.BrowserContext> {
throw new Error('Not implemented');
}

private async _closeBrowserContext(browserContext: playwright.BrowserContext, browser: playwright.Browser, afterClose: () => Promise<void>) {
private async _closeBrowserContext(browserContext: playwright.BrowserContext, browser: playwright.Browser) {
testDebug(`close browser context (${this._logName})`);
if (browser.contexts().length === 1)
this._browserPromise = undefined;
await browserContext.close().catch(logUnhandledError);
await afterClose();
if (browser.contexts().length === 0) {
testDebug(`close browser (${this._logName})`);
await browser.close().catch(logUnhandledError);
Expand Down Expand Up @@ -142,8 +141,8 @@ class IsolatedContextFactory extends BaseContextFactory {
});
}

protected override async _doCreateContext(browser: playwright.Browser): Promise<playwright.BrowserContext> {
return browser.newContext(browserContextOptionsFromConfig(this.config));
protected override async _doCreateContext(browser: playwright.Browser, clientInfo: ClientInfo): Promise<playwright.BrowserContext> {
return browser.newContext(await browserContextOptionsFromConfig(this.config, clientInfo));
}
}

Expand Down Expand Up @@ -206,7 +205,7 @@ class PersistentContextFactory implements BrowserContextFactory {
const launchOptions: LaunchOptions & BrowserContextOptions = {
tracesDir,
...this.config.browser.launchOptions,
...browserContextOptionsFromConfig(this.config),
...await browserContextOptionsFromConfig(this.config, clientInfo),
handleSIGINT: false,
handleSIGTERM: false,
ignoreDefaultArgs: [
Expand All @@ -217,7 +216,7 @@ class PersistentContextFactory implements BrowserContextFactory {
try {
const browserContext = await browserType.launchPersistentContext(userDataDir, launchOptions);
await addInitScript(browserContext, this.config.browser.initScript);
const close = (afterClose: () => Promise<void>) => this._closeBrowserContext(browserContext, userDataDir, afterClose);
const close = () => this._closeBrowserContext(browserContext, userDataDir);
return { browserContext, close };
} catch (error: any) {
if (error.message.includes('Executable doesn\'t exist'))
Expand All @@ -233,11 +232,10 @@ class PersistentContextFactory implements BrowserContextFactory {
throw new Error(`Browser is already in use for ${userDataDir}, use --isolated to run multiple instances of the same browser`);
}

private async _closeBrowserContext(browserContext: playwright.BrowserContext, userDataDir: string, afterClose: () => Promise<void>) {
private async _closeBrowserContext(browserContext: playwright.BrowserContext, userDataDir: string) {
testDebug('close browser context (persistent)');
testDebug('release user data dir', userDataDir);
await browserContext.close().catch(() => {});
await afterClose();
this._userDataDirs.delete(userDataDir);
if (process.env.PWMCP_PROFILES_DIR_FOR_TEST && userDataDir.startsWith(process.env.PWMCP_PROFILES_DIR_FOR_TEST))
await fs.promises.rm(userDataDir, { recursive: true }).catch(logUnhandledError);
Expand Down Expand Up @@ -336,7 +334,7 @@ export class SharedContextFactory implements BrowserContextFactory {
if (!contextPromise)
return;
const { close } = await contextPromise;
await close(async () => {});
await close();
}
}

Expand All @@ -346,11 +344,12 @@ async function computeTracesDir(config: FullConfig, clientInfo: ClientInfo): Pro
return await outputFile(config, clientInfo, `traces`, { origin: 'code', reason: 'Collecting trace' });
}

function browserContextOptionsFromConfig(config: FullConfig): playwright.BrowserContextOptions {
async function browserContextOptionsFromConfig(config: FullConfig, clientInfo: ClientInfo): Promise<playwright.BrowserContextOptions> {
const result = { ...config.browser.contextOptions };
if (config.saveVideo) {
const dir = await outputFile(config, clientInfo, `videos`, { origin: 'code', reason: 'Saving video' });
result.recordVideo = {
dir: tmpDir(),
dir,
size: config.saveVideo,
};
}
Expand Down
2 changes: 1 addition & 1 deletion packages/playwright/src/mcp/browser/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ async function loadConfig(configFile: string | undefined): Promise<Config> {
}
}

export function tmpDir(): string {
function tmpDir(): string {
return path.join(process.env.PW_TMPDIR_FOR_TEST ?? os.tmpdir(), 'playwright-mcp-output');
}

Expand Down
27 changes: 1 addition & 26 deletions packages/playwright/src/mcp/browser/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@
* limitations under the License.
*/

import fs from 'fs';

import { debug } from 'playwright-core/lib/utilsBundle';
import { escapeWithQuotes } from 'playwright-core/lib/utils';
import { selectors } from 'playwright-core';
Expand All @@ -25,7 +23,6 @@ import os from 'os';
import { logUnhandledError } from '../log';
import { Tab } from './tab';
import { outputFile } from './config';
import { dateAsFileName } from './tools/utils';

import type * as playwright from '../../../types/test';
import type { FullConfig } from './config';
Expand Down Expand Up @@ -169,29 +166,7 @@ export class Context {
await promise.then(async ({ browserContext, close }) => {
if (this.config.saveTrace)
await browserContext.tracing.stop();
const videos = this.config.saveVideo ? browserContext.pages().map(page => page.video()).filter(video => !!video) : [];
await close(async () => {
for (const video of videos) {
const name = await this.outputFile(dateAsFileName('webm'), { origin: 'code', reason: 'Saving video' });
const p = await video.path();
// video.saveAs() does not work for persistent contexts.
if (fs.existsSync(p)) {
try {
await fs.promises.rename(p, name);
} catch (e) {
if (e.code !== 'EXDEV')
logUnhandledError(e);
// Retry operation (possibly cross-fs) with copy and unlink
try {
await fs.promises.copyFile(p, name);
await fs.promises.unlink(p);
} catch (e) {
logUnhandledError(e);
}
}
}
}
});
await close();
});
}

Expand Down
41 changes: 17 additions & 24 deletions tests/mcp/video.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/

import fs from 'fs';
import path from 'path';
import { test, expect } from './fixtures';
import type { Client } from '@modelcontextprotocol/sdk/client/index.js';

Expand All @@ -31,14 +32,12 @@ for (const mode of ['isolated', 'persistent']) {
});

await navigateToTestPage(client, server);
await expect(async () => {
await produceFrames(client);
await checkIntermediateVideoFileExists();
}).toPass();
await produceFrames(client);
await closeBrowser(client);

const [file] = await fs.promises.readdir(outputDir);
expect(file).toMatch(/page-.*\.webm/);
const videosDir = path.join(outputDir, 'videos');
const [file] = await fs.promises.readdir(videosDir);
expect(file).toMatch(/.*\.webm/);
});

test(`should work with { saveVideo } (${mode})`, async ({ startClient, server }, testInfo) => {
Expand All @@ -53,14 +52,12 @@ for (const mode of ['isolated', 'persistent']) {
});

await navigateToTestPage(client, server);
await expect(async () => {
await produceFrames(client);
await checkIntermediateVideoFileExists();
}).toPass();
await produceFrames(client);
await closeBrowser(client);

const [file] = await fs.promises.readdir(outputDir);
expect(file).toMatch(/page-.*\.webm/);
const videosDir = path.join(outputDir, 'videos');
const [file] = await fs.promises.readdir(videosDir);
expect(file).toMatch(/.*\.webm/);
});

test(`should work with recordVideo (${mode})`, async ({ startClient, server }, testInfo) => {
Expand All @@ -83,10 +80,7 @@ for (const mode of ['isolated', 'persistent']) {
});

await navigateToTestPage(client, server);
await expect(async () => {
await produceFrames(client);
await checkIntermediateVideoFileExists(videosDir);
}).toPass();
await produceFrames(client);
await closeBrowser(client);
});
}
Expand All @@ -113,21 +107,20 @@ async function produceFrames(client: Client) {
name: 'browser_evaluate',
arguments: {
function: `async () => {
async function rafraf(count) {
for (let i = 0; i < count; i++)
await new Promise(f => requestAnimationFrame(() => requestAnimationFrame(f)));
}
document.body.style.backgroundColor = "red";
await new Promise(resolve => setTimeout(resolve, 100));
await rafraf(30);
document.body.style.backgroundColor = "green";
await new Promise(resolve => setTimeout(resolve, 100));
await rafraf(30);
document.body.style.backgroundColor = "blue";
await new Promise(resolve => setTimeout(resolve, 100));
await rafraf(30);
return 'ok';
}`,
},
})).toHaveResponse({
result: '"ok"',
});
}

async function checkIntermediateVideoFileExists(videosDir?: string) {
const files = await fs.promises.readdir(videosDir ?? test.info().outputPath('tmp', 'playwright-mcp-output'));
expect(files[0]).toMatch(/\.webm/);
}