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
4 changes: 2 additions & 2 deletions packages/playwright-core/src/server/registry/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,9 @@ type DownloadPathFunction = (params: BrowsersJSONDescriptor) => { path: string,
function cftUrl(suffix: string): DownloadPathFunction {
return ({ browserVersion }) => {
return {
path: `${browserVersion}/${suffix}`,
path: `builds/cft/${browserVersion}/${suffix}`,
mirrors: [
'https://cdn.playwright.dev/chrome-for-testing-public',
'https://cdn.playwright.dev',
],
};
};
Expand Down
8 changes: 6 additions & 2 deletions packages/playwright/src/mcp/browser/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export const defaultConfig: FullConfig = {

const defaultDaemonConfig = (cliOptions: CLIOptions) => mergeConfig(defaultConfig, {
browser: {
userDataDir: cliOptions.extension ? undefined : '<daemon-data-dir>', // Use default user profile with extension.
userDataDir: '<daemon-data-dir>',
launchOptions: {
headless: !cliOptions.daemonHeaded,
},
Expand Down Expand Up @@ -167,7 +167,11 @@ export async function resolveCLIConfig(cliOptions: CLIOptions): Promise<FullConf
if (result.browser.userDataDir === '<daemon-data-dir>') {
// No custom value provided, use the daemon data dir.
const browserToken = result.browser.launchOptions?.channel ?? result.browser?.browserName;
result.browser.userDataDir = `${cliOptions.daemonDataDir}-${browserToken}`;
const userDataDir = `${cliOptions.daemonDataDir}-${browserToken}`;

// Use default user profile with extension.
if (!result.extension)
result.browser.userDataDir = userDataDir;
}

if (result.browser.browserName === 'chromium' && result.browser.launchOptions.chromiumSandbox === undefined) {
Expand Down
5 changes: 4 additions & 1 deletion packages/playwright/src/mcp/terminal/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -535,8 +535,11 @@ const config = declareCommand({
name: 'config',
description: 'Restart session with new config, defaults to `playwright-cli.json`',
category: 'config',
args: z.object({
options: z.object({
browser: z.string().optional().describe('browser or chrome channel to use, possible values: chrome, firefox, webkit, msedge.'),
config: z.string().optional().describe('Path to the configuration file'),
isolated: z.boolean().optional().describe('keep the browser profile in memory, do not save it to disk.'),
headed: z.boolean().optional().describe('run browser in headed mode'),
}),
toolName: '',
toolParams: () => ({}),
Expand Down
63 changes: 46 additions & 17 deletions packages/playwright/src/mcp/terminal/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,14 @@ export type StructuredResponse = {
sections: Section[];
};

type SessionOptions = { config?: string, headed?: boolean, extension?: boolean, daemonVersion: string };
type SessionOptions = {
config?: string;
headed?: boolean;
extension?: boolean;
daemonVersion: string;
browser?: string;
isolated?: boolean;
};

class Session {
readonly name: string;
Expand Down Expand Up @@ -108,12 +115,13 @@ class Session {
await this.stop();

const dataDirs = await fs.promises.readdir(daemonProfilesDir).catch(() => []);
const matchingDirs = dataDirs.filter(dir => dir.startsWith(`ud-${this.name}-`));
if (matchingDirs.length === 0) {
const matchingEntries = dataDirs.filter(file => file === `${this.name}.session` || file.startsWith(`ud-${this.name}-`));
if (matchingEntries.length === 0) {
console.log(`No user data found for session '${this.name}'.`);
return;
}
for (const dir of matchingDirs) {

for (const dir of matchingEntries) {
const userDataDir = path.resolve(daemonProfilesDir, dir);
for (let i = 0; i < 5; i++) {
try {
Expand Down Expand Up @@ -205,6 +213,11 @@ class Session {
const configArg = configFile !== undefined ? [`--config=${configFile}`] : [];
const headedArg = this._options.headed ? [`--daemon-headed`] : [];
const extensionArg = this._options.extension ? [`--extension`] : [];
const isolatedArg = this._options.isolated ? [`--isolated`] : [];
const browserArg = this._options.browser ? [`--browser=${this._options.browser}`] : [];

const sessionOptionsFile = path.resolve(daemonProfilesDir, `${this.name}.session`);
await fs.promises.writeFile(sessionOptionsFile, JSON.stringify({ ...this._options, _: undefined }, null, 2));

const outLog = path.join(daemonProfilesDir, 'out.log');
const errLog = path.join(daemonProfilesDir, 'err.log');
Expand All @@ -221,6 +234,8 @@ class Session {
...configArg,
...headedArg,
...extensionArg,
...isolatedArg,
...browserArg,
], {
detached: true,
stdio: ['ignore', out, err],
Expand Down Expand Up @@ -285,16 +300,24 @@ class SessionManager {
const sessions = new Map<string, Session>([
['default', new Session('default', options)],
]);
try {
const files = await fs.promises.readdir(dir);
for (const file of files) {
const files = await fs.promises.readdir(dir).catch(() => []);
for (const file of files) {
try {
if (file.endsWith('.session')) {
const sessionName = path.basename(file, '.session');
sessions.set(sessionName, new Session(sessionName, options));
continue;
}

// Legacy session support.
if (file.startsWith('ud-')) {
// Session is like ud-<sessionName>-browserName
const sessionName = file.split('-')[1];
sessions.set(sessionName, new Session(sessionName, options));
if (!sessions.has(sessionName))
sessions.set(sessionName, new Session(sessionName, options));
}
} catch {
}
} catch {
}
return new SessionManager(sessions, options);
}
Expand Down Expand Up @@ -341,7 +364,7 @@ class SessionManager {
session = new Session(sessionName, this.options);
this.sessions.set(sessionName, session);
}
await session.restart({ ...this.options, ...args, config: args._[1] });
await session.restart({ ...this.options, ...args });
session.close();
}

Expand Down Expand Up @@ -415,15 +438,21 @@ const daemonProfilesDir = (() => {
return path.join(localCacheDir, 'ms-playwright', 'daemon', installationDirHash);
})();

const booleanOptions = [
'extension',
'headed',
'help',
'isolated',
'version',
];

export async function program(options: { version: string }) {
const argv = process.argv.slice(2);
const args = require('minimist')(argv, {
boolean: ['help', 'version', 'headed', 'extension'],
});
if (!argv.includes('--headed') && !argv.includes('--no-headed'))
delete args.headed;
if (!argv.includes('--extension'))
delete args.extension;
const args = require('minimist')(argv, { boolean: booleanOptions });
for (const option of booleanOptions) {
if (!argv.includes(`--${option}`) && !argv.includes(`--no-${option}`))
delete args[option];
}

const help = require('./help.json');
const commandName = args._[0];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,7 @@ test('install command should work with mirror that uses chunked encoding', async
await exec('npm i playwright');
const server = http.createServer(async (req, res) => {
try {
const upstream = await fetch(
req.url.startsWith('/builds/')
? 'https://cdn.playwright.dev/dbazure/download/playwright' + req.url
: 'https://cdn.playwright.dev/chrome-for-testing-public' + req.url
);
const upstream = await fetch('https://cdn.playwright.dev' + req.url);
const headers = new Headers(upstream.headers);
headers.delete('content-length');
res.writeHead(upstream.status, Object.fromEntries(headers));
Expand Down
20 changes: 19 additions & 1 deletion tests/mcp/cli.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -539,7 +539,7 @@ test.describe('config', () => {
const configPath = testInfo.outputPath('session-config.json');
await fs.promises.writeFile(configPath, JSON.stringify(config, null, 2));

const { output: configureOutput } = await cli('config', configPath);
const { output: configureOutput } = await cli('config', '--config=' + configPath);
expect(configureOutput).toContain(`- Using config file at \`session-config.json\`.`);

await cli('open', server.PREFIX);
Expand Down Expand Up @@ -597,3 +597,21 @@ test.describe('folders', () => {
}
});
});

test.describe('isolated', () => {
test('should not save user data', async ({ cli, server, mcpBrowser }, testInfo) => {
await cli('open', server.HELLO_WORLD, '--isolated');
const dataDir = testInfo.outputPath('daemon', 'ud-default-' + mcpBrowser);
expect(fs.existsSync(dataDir)).toBe(false);
const sessionFile = testInfo.outputPath('daemon', 'default.session');
expect(fs.existsSync(sessionFile)).toBe(true);
const sessionOptions = JSON.parse(await fs.promises.readFile(sessionFile, 'utf-8'));
expect(sessionOptions).toEqual(expect.objectContaining({
isolated: true,
}));

const { output: listOutput } = await cli('session-list');
expect(listOutput).toContain('Sessions:');
expect(listOutput).toContain('default (live)');
});
});
Loading