526 lines
24 KiB
TypeScript
526 lines
24 KiB
TypeScript
|
|
/**
|
||
|
|
* 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 os from 'os';
|
||
|
|
import path from 'path';
|
||
|
|
|
||
|
|
import { test, expect } from './fixtures';
|
||
|
|
|
||
|
|
import { tools } from '../../packages/playwright-core/lib/coreBundle';
|
||
|
|
import type { Config } from '../../packages/playwright-core/src/tools/mcp/config.d';
|
||
|
|
|
||
|
|
const { resolveCLIConfigForCLI, resolveCLIConfigForMCP, isSystemDirectory, outputDir } = tools;
|
||
|
|
|
||
|
|
// Empty env to isolate tests from the host environment.
|
||
|
|
const emptyEnv = {};
|
||
|
|
|
||
|
|
test.skip(({ mcpBrowser }) => mcpBrowser !== 'chrome', 'Channel-agnostic tests.');
|
||
|
|
|
||
|
|
test.describe('browserName and channel', () => {
|
||
|
|
test('no browser option defaults to chromium / chrome', async () => {
|
||
|
|
const config = await resolveCLIConfigForMCP({}, emptyEnv);
|
||
|
|
expect(config.browser.browserName).toBe('chromium');
|
||
|
|
expect(config.browser.launchOptions.channel).toBe('chrome');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('--browser=chrome sets chromium with chrome channel', async () => {
|
||
|
|
const config = await resolveCLIConfigForMCP({ browser: 'chrome' }, emptyEnv);
|
||
|
|
expect(config.browser.browserName).toBe('chromium');
|
||
|
|
expect(config.browser.launchOptions.channel).toBe('chrome');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('--browser=chromium sets chromium with chrome-for-testing channel', async () => {
|
||
|
|
const config = await resolveCLIConfigForMCP({ browser: 'chromium' }, emptyEnv);
|
||
|
|
expect(config.browser.browserName).toBe('chromium');
|
||
|
|
expect(config.browser.launchOptions.channel).toBe('chrome-for-testing');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('--browser=firefox sets firefox without channel', async () => {
|
||
|
|
const config = await resolveCLIConfigForMCP({ browser: 'firefox' }, emptyEnv);
|
||
|
|
expect(config.browser.browserName).toBe('firefox');
|
||
|
|
expect(config.browser.launchOptions.channel).toBeUndefined();
|
||
|
|
});
|
||
|
|
|
||
|
|
test('--browser=webkit sets webkit without channel', async () => {
|
||
|
|
const config = await resolveCLIConfigForMCP({ browser: 'webkit' }, emptyEnv);
|
||
|
|
expect(config.browser.browserName).toBe('webkit');
|
||
|
|
expect(config.browser.launchOptions.channel).toBeUndefined();
|
||
|
|
});
|
||
|
|
|
||
|
|
test('--browser=msedge sets chromium with msedge channel', async () => {
|
||
|
|
const config = await resolveCLIConfigForMCP({ browser: 'msedge' }, emptyEnv);
|
||
|
|
expect(config.browser.browserName).toBe('chromium');
|
||
|
|
expect(config.browser.launchOptions.channel).toBe('msedge');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('config file browserName chromium does not auto-set channel', async ({}, testInfo) => {
|
||
|
|
const configFile = testInfo.outputPath('config.json');
|
||
|
|
await fs.promises.writeFile(configFile, JSON.stringify({ browser: { browserName: 'chromium' } }));
|
||
|
|
const config = await resolveCLIConfigForMCP({ config: configFile }, emptyEnv);
|
||
|
|
expect(config.browser.browserName).toBe('chromium');
|
||
|
|
expect(config.browser.launchOptions.channel).toBeUndefined();
|
||
|
|
});
|
||
|
|
|
||
|
|
test('config file browserName firefox does not set channel', async ({}, testInfo) => {
|
||
|
|
const configFile = testInfo.outputPath('config.json');
|
||
|
|
await fs.promises.writeFile(configFile, JSON.stringify({ browser: { browserName: 'firefox' } }));
|
||
|
|
const config = await resolveCLIConfigForMCP({ config: configFile }, emptyEnv);
|
||
|
|
expect(config.browser.browserName).toBe('firefox');
|
||
|
|
expect(config.browser.launchOptions.channel).toBeUndefined();
|
||
|
|
});
|
||
|
|
|
||
|
|
test('config file browserName + channel are both preserved', async ({}, testInfo) => {
|
||
|
|
const configFile = testInfo.outputPath('config.json');
|
||
|
|
const fileConfig: Config = {
|
||
|
|
browser: { browserName: 'chromium', launchOptions: { channel: 'msedge' } },
|
||
|
|
};
|
||
|
|
await fs.promises.writeFile(configFile, JSON.stringify(fileConfig));
|
||
|
|
const config = await resolveCLIConfigForMCP({ config: configFile }, emptyEnv);
|
||
|
|
expect(config.browser.browserName).toBe('chromium');
|
||
|
|
expect(config.browser.launchOptions.channel).toBe('msedge');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('cli --browser overrides config file browserName', async ({}, testInfo) => {
|
||
|
|
const configFile = testInfo.outputPath('config.json');
|
||
|
|
await fs.promises.writeFile(configFile, JSON.stringify({ browser: { browserName: 'firefox' } }));
|
||
|
|
const config = await resolveCLIConfigForMCP({ config: configFile, browser: 'webkit' }, emptyEnv);
|
||
|
|
expect(config.browser.browserName).toBe('webkit');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Shared behavior — sandbox
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
test.describe('sandbox', () => {
|
||
|
|
test('chromium sandbox enabled for chrome channel', async () => {
|
||
|
|
const config = await resolveCLIConfigForMCP({ browser: 'chrome' }, emptyEnv);
|
||
|
|
expect(config.browser.launchOptions.chromiumSandbox).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('chromium sandbox for chrome-for-testing channel', async () => {
|
||
|
|
const config = await resolveCLIConfigForMCP({ browser: 'chromium' }, emptyEnv);
|
||
|
|
expect(config.browser.launchOptions.channel).toBe('chrome-for-testing');
|
||
|
|
if (process.platform === 'linux')
|
||
|
|
expect(config.browser.launchOptions.chromiumSandbox).toBe(false);
|
||
|
|
else
|
||
|
|
expect(config.browser.launchOptions.chromiumSandbox).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('sandbox not set for non-chromium browsers', async () => {
|
||
|
|
const config = await resolveCLIConfigForMCP({ browser: 'firefox' }, emptyEnv);
|
||
|
|
expect(config.browser.launchOptions.chromiumSandbox).toBeUndefined();
|
||
|
|
});
|
||
|
|
|
||
|
|
test('explicit --sandbox overrides default', async () => {
|
||
|
|
const config = await resolveCLIConfigForMCP({ browser: 'chromium', sandbox: true }, emptyEnv);
|
||
|
|
expect(config.browser.launchOptions.chromiumSandbox).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('explicit --no-sandbox overrides default', async () => {
|
||
|
|
const config = await resolveCLIConfigForMCP({ browser: 'chrome', sandbox: false }, emptyEnv);
|
||
|
|
expect(config.browser.launchOptions.chromiumSandbox).toBe(false);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Shared behavior — timeouts
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
test.describe('timeouts', () => {
|
||
|
|
test('default timeouts', async () => {
|
||
|
|
const config = await resolveCLIConfigForMCP({}, emptyEnv);
|
||
|
|
expect(config.timeouts.action).toBe(5000);
|
||
|
|
expect(config.timeouts.navigation).toBe(60000);
|
||
|
|
expect(config.timeouts.expect).toBe(5000);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('config file timeouts override defaults', async ({}, testInfo) => {
|
||
|
|
const configFile = testInfo.outputPath('config.json');
|
||
|
|
await fs.promises.writeFile(configFile, JSON.stringify({ timeouts: { action: 7000 } }));
|
||
|
|
const config = await resolveCLIConfigForMCP({ config: configFile }, emptyEnv);
|
||
|
|
expect(config.timeouts.action).toBe(7000);
|
||
|
|
expect(config.timeouts.navigation).toBe(60000);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Shared behavior — viewport
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
test.describe('viewport', () => {
|
||
|
|
test('headless sets default viewport 1280x720', async () => {
|
||
|
|
const config = await resolveCLIConfigForMCP({ headless: true }, emptyEnv);
|
||
|
|
expect(config.browser.contextOptions.viewport).toEqual({ width: 1280, height: 720 });
|
||
|
|
});
|
||
|
|
|
||
|
|
test('headed sets viewport to null', async () => {
|
||
|
|
const config = await resolveCLIConfigForMCP({ headless: false }, emptyEnv);
|
||
|
|
expect(config.browser.contextOptions.viewport).toBeNull();
|
||
|
|
});
|
||
|
|
|
||
|
|
test('explicit viewport is preserved', async () => {
|
||
|
|
const config = await resolveCLIConfigForMCP({ headless: true, viewportSize: { width: 800, height: 600 } }, emptyEnv);
|
||
|
|
expect(config.browser.contextOptions.viewport).toEqual({ width: 800, height: 600 });
|
||
|
|
});
|
||
|
|
|
||
|
|
test('config file viewport is preserved', async ({}, testInfo) => {
|
||
|
|
const configFile = testInfo.outputPath('config.json');
|
||
|
|
const fileConfig: Config = {
|
||
|
|
browser: { contextOptions: { viewport: { width: 640, height: 480 } } },
|
||
|
|
};
|
||
|
|
await fs.promises.writeFile(configFile, JSON.stringify(fileConfig));
|
||
|
|
const config = await resolveCLIConfigForMCP({ config: configFile, headless: true }, emptyEnv);
|
||
|
|
expect(config.browser.contextOptions.viewport).toEqual({ width: 640, height: 480 });
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Shared behavior — merge order and config file
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
test.describe('merge order', () => {
|
||
|
|
test('cli overrides config file', async ({}, testInfo) => {
|
||
|
|
const configFile = testInfo.outputPath('config.json');
|
||
|
|
const fileConfig: Config = {
|
||
|
|
timeouts: { action: 1000 },
|
||
|
|
browser: { contextOptions: { viewport: { width: 640, height: 480 } } },
|
||
|
|
};
|
||
|
|
await fs.promises.writeFile(configFile, JSON.stringify(fileConfig));
|
||
|
|
const config = await resolveCLIConfigForMCP({ config: configFile, timeoutAction: 9999 }, emptyEnv);
|
||
|
|
expect(config.timeouts.action).toBe(9999);
|
||
|
|
expect(config.browser.contextOptions.viewport).toEqual({ width: 640, height: 480 });
|
||
|
|
});
|
||
|
|
|
||
|
|
test('config file values preserved when cli does not override', async ({}, testInfo) => {
|
||
|
|
const configFile = testInfo.outputPath('config.json');
|
||
|
|
const fileConfig: Config = {
|
||
|
|
network: { allowedOrigins: ['https://example.com'] },
|
||
|
|
browser: { isolated: true },
|
||
|
|
};
|
||
|
|
await fs.promises.writeFile(configFile, JSON.stringify(fileConfig));
|
||
|
|
const config = await resolveCLIConfigForMCP({ config: configFile }, emptyEnv) as any;
|
||
|
|
expect(config.network?.allowedOrigins).toEqual(['https://example.com']);
|
||
|
|
expect(config.browser.isolated).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('env overrides config file', async ({}, testInfo) => {
|
||
|
|
const configFile = testInfo.outputPath('config.json');
|
||
|
|
await fs.promises.writeFile(configFile, JSON.stringify({ timeouts: { navigation: 1000 } }));
|
||
|
|
const config = await resolveCLIConfigForMCP({ config: configFile }, {
|
||
|
|
PLAYWRIGHT_MCP_TIMEOUT_NAVIGATION: '45000',
|
||
|
|
});
|
||
|
|
expect(config.timeouts.navigation).toBe(45000);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('cli overrides env', async () => {
|
||
|
|
const config = await resolveCLIConfigForMCP({ timeoutAction: 3000, browser: 'firefox' }, {
|
||
|
|
PLAYWRIGHT_MCP_TIMEOUT_ACTION: '9000',
|
||
|
|
PLAYWRIGHT_MCP_BROWSER: 'webkit',
|
||
|
|
});
|
||
|
|
expect(config.timeouts.action).toBe(3000);
|
||
|
|
expect(config.browser.browserName).toBe('firefox');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('file browser.cdpHeaders preserved when env unset', async ({}, testInfo) => {
|
||
|
|
const configFile = testInfo.outputPath('config.json');
|
||
|
|
const fileConfig: Config = {
|
||
|
|
browser: {
|
||
|
|
cdpEndpoint: 'ws://example.invalid',
|
||
|
|
cdpHeaders: { Authorization: 'Bearer token-from-file' },
|
||
|
|
},
|
||
|
|
};
|
||
|
|
await fs.promises.writeFile(configFile, JSON.stringify(fileConfig));
|
||
|
|
const config = await resolveCLIConfigForMCP({ config: configFile }, emptyEnv);
|
||
|
|
expect(config.browser.cdpHeaders).toEqual({ Authorization: 'Bearer token-from-file' });
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Shared behavior — validation
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
test.describe('validation', () => {
|
||
|
|
test('isolated + userDataDir throws', async () => {
|
||
|
|
await expect(resolveCLIConfigForMCP({ isolated: true, userDataDir: '/tmp/data' }, emptyEnv))
|
||
|
|
.rejects.toThrow('Browser userDataDir is not supported in isolated mode.');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Output directory — fallback for unsuitable cwd, throw on explicit system dir
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
test.describe('outputDir', () => {
|
||
|
|
test.skip(process.platform === 'win32', 'POSIX-specific cases');
|
||
|
|
|
||
|
|
test('falls back to tmpdir when cwd is /', () => {
|
||
|
|
const result = outputDir({ config: {}, cwd: '/' });
|
||
|
|
expect(result).toBe(path.join(os.tmpdir(), '.playwright-mcp'));
|
||
|
|
});
|
||
|
|
|
||
|
|
test('uses cwd-relative .playwright-mcp for normal cwd', ({}, testInfo) => {
|
||
|
|
const cwd = testInfo.outputPath('workspace');
|
||
|
|
fs.mkdirSync(cwd, { recursive: true });
|
||
|
|
const result = outputDir({ config: {}, cwd });
|
||
|
|
expect(result).toBe(path.join(cwd, '.playwright-mcp'));
|
||
|
|
});
|
||
|
|
|
||
|
|
test('uses .playwright-cli when skillMode is set', ({}, testInfo) => {
|
||
|
|
const cwd = testInfo.outputPath('workspace');
|
||
|
|
fs.mkdirSync(cwd, { recursive: true });
|
||
|
|
const result = outputDir({ config: { skillMode: true }, cwd });
|
||
|
|
expect(result).toBe(path.join(cwd, '.playwright-cli'));
|
||
|
|
});
|
||
|
|
|
||
|
|
test('skillMode falls back to tmpdir/.playwright-cli when cwd is /', () => {
|
||
|
|
const result = outputDir({ config: { skillMode: true }, cwd: '/' });
|
||
|
|
expect(result).toBe(path.join(os.tmpdir(), '.playwright-cli'));
|
||
|
|
});
|
||
|
|
|
||
|
|
test('explicit outputDir wins regardless of cwd', ({}, testInfo) => {
|
||
|
|
const explicit = testInfo.outputPath('explicit');
|
||
|
|
const result = outputDir({ config: { outputDir: explicit }, cwd: '/' });
|
||
|
|
expect(result).toBe(explicit);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('falls back to tmpdir when cwd is not writable', ({}, testInfo) => {
|
||
|
|
const cwd = testInfo.outputPath('readonly');
|
||
|
|
fs.mkdirSync(cwd, { recursive: true });
|
||
|
|
fs.chmodSync(cwd, 0o500);
|
||
|
|
try {
|
||
|
|
const result = outputDir({ config: {}, cwd });
|
||
|
|
expect(result).toBe(path.join(os.tmpdir(), '.playwright-mcp'));
|
||
|
|
} finally {
|
||
|
|
fs.chmodSync(cwd, 0o700);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test('isSystemDirectory detects /', () => {
|
||
|
|
expect(isSystemDirectory('/')).toBe(true);
|
||
|
|
expect(isSystemDirectory('/tmp')).toBe(false);
|
||
|
|
expect(isSystemDirectory(os.homedir())).toBe(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('resolveCLIConfigForMCP throws when --output-dir is /', async () => {
|
||
|
|
await expect(resolveCLIConfigForMCP({ outputDir: '/' }, emptyEnv))
|
||
|
|
.rejects.toThrow(/--output-dir cannot point to a system directory/);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// MCP-specific: headless platform default, timeout CLI options
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
test.describe('resolveCLIConfigForMCP', () => {
|
||
|
|
test('headless defaults based on platform', async () => {
|
||
|
|
const config = await resolveCLIConfigForMCP({}, emptyEnv);
|
||
|
|
expect(config.browser.launchOptions.headless).toBeDefined();
|
||
|
|
});
|
||
|
|
|
||
|
|
test('cli timeout overrides defaults', async () => {
|
||
|
|
const config = await resolveCLIConfigForMCP({ timeoutAction: 10000, timeoutNavigation: 30000 }, emptyEnv);
|
||
|
|
expect(config.timeouts.action).toBe(10000);
|
||
|
|
expect(config.timeouts.navigation).toBe(30000);
|
||
|
|
expect(config.timeouts.expect).toBe(5000);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('cli timeout overrides config file timeout', async ({}, testInfo) => {
|
||
|
|
const configFile = testInfo.outputPath('config.json');
|
||
|
|
await fs.promises.writeFile(configFile, JSON.stringify({ timeouts: { action: 1000 } }));
|
||
|
|
const config = await resolveCLIConfigForMCP({ config: configFile, timeoutAction: 9999 }, emptyEnv);
|
||
|
|
expect(config.timeouts.action).toBe(9999);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// CLI-specific: headless, isolated, userDataDir, extension, skillMode, snapshot
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
function resolveCLI(profilesDir: string, sessionName: string, options: any) {
|
||
|
|
return resolveCLIConfigForCLI(profilesDir, sessionName, options, emptyEnv);
|
||
|
|
}
|
||
|
|
|
||
|
|
test.describe('resolveCLIConfigForCLI - headless and viewport', () => {
|
||
|
|
test('headless by default', async ({}, testInfo) => {
|
||
|
|
const config = await resolveCLI(testInfo.outputPath('profiles'), 'default', {});
|
||
|
|
expect(config.browser.launchOptions.headless).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('--headed sets headless=false', async ({}, testInfo) => {
|
||
|
|
const config = await resolveCLI(testInfo.outputPath('profiles'), 'default', { headed: true });
|
||
|
|
expect(config.browser.launchOptions.headless).toBe(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('headless viewport defaults to 1280x720', async ({}, testInfo) => {
|
||
|
|
const config = await resolveCLI(testInfo.outputPath('profiles'), 'default', {});
|
||
|
|
expect(config.browser.contextOptions.viewport).toEqual({ width: 1280, height: 720 });
|
||
|
|
});
|
||
|
|
|
||
|
|
test('headed viewport defaults to null', async ({}, testInfo) => {
|
||
|
|
const config = await resolveCLI(testInfo.outputPath('profiles'), 'default', { headed: true });
|
||
|
|
expect(config.browser.contextOptions.viewport).toBeNull();
|
||
|
|
});
|
||
|
|
|
||
|
|
test('config file viewport is preserved', async ({}, testInfo) => {
|
||
|
|
const configFile = testInfo.outputPath('config.json');
|
||
|
|
const fileConfig: Config = {
|
||
|
|
browser: { contextOptions: { viewport: { width: 640, height: 480 } } },
|
||
|
|
};
|
||
|
|
await fs.promises.writeFile(configFile, JSON.stringify(fileConfig));
|
||
|
|
const config = await resolveCLI(testInfo.outputPath('profiles'), 'default', { config: configFile });
|
||
|
|
expect(config.browser.contextOptions.viewport).toEqual({ width: 640, height: 480 });
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
test.describe('resolveCLIConfigForCLI - isolated and userDataDir', () => {
|
||
|
|
test('defaults to isolated when no profile, persistent, userDataDir, or remoteEndpoint', async ({}, testInfo) => {
|
||
|
|
const config = await resolveCLI(testInfo.outputPath('profiles'), 'default', {});
|
||
|
|
expect(config.browser.isolated).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('not isolated when --profile is set', async ({}, testInfo) => {
|
||
|
|
const profileDir = testInfo.outputPath('my-profile');
|
||
|
|
const config = await resolveCLI(testInfo.outputPath('profiles'), 'default', { profile: profileDir });
|
||
|
|
expect(config.browser.isolated).toBe(false);
|
||
|
|
expect(config.browser.userDataDir).toBe(profileDir);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('not isolated when --persistent is set', async ({}, testInfo) => {
|
||
|
|
const config = await resolveCLI(testInfo.outputPath('profiles'), 'default', { persistent: true });
|
||
|
|
expect(config.browser.isolated).toBe(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('not isolated when --attach is set', async ({}, testInfo) => {
|
||
|
|
const config = await resolveCLI(testInfo.outputPath('profiles'), 'default', { endpoint: 'ws://localhost:1234' });
|
||
|
|
expect(config.browser.isolated).toBe(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('not isolated when config file sets userDataDir', async ({}, testInfo) => {
|
||
|
|
const configFile = testInfo.outputPath('config.json');
|
||
|
|
const userDataDir = testInfo.outputPath('custom-data');
|
||
|
|
await fs.promises.writeFile(configFile, JSON.stringify({ browser: { userDataDir } }));
|
||
|
|
const config = await resolveCLI(testInfo.outputPath('profiles'), 'default', { config: configFile });
|
||
|
|
expect(config.browser.isolated).toBe(false);
|
||
|
|
expect(config.browser.userDataDir).toBe(userDataDir);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('auto userDataDir uses daemonProfilesDir with session and browser token', async ({}, testInfo) => {
|
||
|
|
const profilesDir = testInfo.outputPath('profiles');
|
||
|
|
const config = await resolveCLI(profilesDir, 'mysession', { persistent: true, browser: 'chrome' });
|
||
|
|
expect(config.browser.userDataDir).toBe(path.resolve(profilesDir, 'ud-mysession-chrome'));
|
||
|
|
});
|
||
|
|
|
||
|
|
test('auto userDataDir uses browserName when no channel', async ({}, testInfo) => {
|
||
|
|
const profilesDir = testInfo.outputPath('profiles');
|
||
|
|
const config = await resolveCLI(profilesDir, 'default', { persistent: true, browser: 'firefox' });
|
||
|
|
expect(config.browser.userDataDir).toBe(path.resolve(profilesDir, 'ud-default-firefox'));
|
||
|
|
});
|
||
|
|
|
||
|
|
test('auto userDataDir uses undefined token when no browser specified', async ({}, testInfo) => {
|
||
|
|
const profilesDir = testInfo.outputPath('profiles');
|
||
|
|
const config = await resolveCLI(profilesDir, 'default', { persistent: true });
|
||
|
|
expect(config.browser.userDataDir).toBe(path.resolve(profilesDir, 'ud-default-chrome'));
|
||
|
|
});
|
||
|
|
|
||
|
|
test('no auto userDataDir when isolated', async ({}, testInfo) => {
|
||
|
|
const config = await resolveCLI(testInfo.outputPath('profiles'), 'default', {});
|
||
|
|
expect(config.browser.isolated).toBe(true);
|
||
|
|
expect(config.browser.userDataDir).toBeUndefined();
|
||
|
|
});
|
||
|
|
|
||
|
|
test('no auto userDataDir when --profile is set', async ({}, testInfo) => {
|
||
|
|
const profileDir = testInfo.outputPath('my-profile');
|
||
|
|
const config = await resolveCLI(testInfo.outputPath('profiles'), 'default', { profile: profileDir });
|
||
|
|
expect(config.browser.userDataDir).toBe(profileDir);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('no auto userDataDir when remoteEndpoint is set', async ({}, testInfo) => {
|
||
|
|
const config = await resolveCLI(testInfo.outputPath('profiles'), 'default', { attach: 'ws://localhost:1234' });
|
||
|
|
expect(config.browser.userDataDir).toBeUndefined();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
test.describe('resolveCLIConfigForCLI - timeouts', () => {
|
||
|
|
test('config file timeouts override defaults', async ({}, testInfo) => {
|
||
|
|
const configFile = testInfo.outputPath('config.json');
|
||
|
|
await fs.promises.writeFile(configFile, JSON.stringify({ timeouts: { action: 7000 } }));
|
||
|
|
const config = await resolveCLI(testInfo.outputPath('profiles'), 'default', { config: configFile });
|
||
|
|
expect(config.timeouts.action).toBe(7000);
|
||
|
|
expect(config.timeouts.navigation).toBe(60000);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
test.describe('resolveCLIConfigForCLI - skillMode and snapshotMode', () => {
|
||
|
|
test('skillMode is always set', async ({}, testInfo) => {
|
||
|
|
const config = await resolveCLI(testInfo.outputPath('profiles'), 'default', {});
|
||
|
|
expect(config.skillMode).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('snapshot mode is full', async ({}, testInfo) => {
|
||
|
|
const config = await resolveCLI(testInfo.outputPath('profiles'), 'default', {}) as any;
|
||
|
|
expect(config.snapshot?.mode).toBe('full');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
test.describe('resolveCLIConfigForCLI - config file discovery', () => {
|
||
|
|
test('explicit config file is used', async ({}, testInfo) => {
|
||
|
|
const configFile = testInfo.outputPath('config.json');
|
||
|
|
await fs.promises.writeFile(configFile, JSON.stringify({ timeouts: { action: 3000 } }));
|
||
|
|
const config = await resolveCLI(testInfo.outputPath('profiles'), 'default', { config: configFile });
|
||
|
|
expect(config.timeouts.action).toBe(3000);
|
||
|
|
expect(config.configFile).toBe(configFile);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('cli overrides env', async ({}, testInfo) => {
|
||
|
|
const config = await resolveCLIConfigForCLI(testInfo.outputPath('profiles'), 'default', { browser: 'firefox' }, {
|
||
|
|
PLAYWRIGHT_MCP_BROWSER: 'webkit',
|
||
|
|
});
|
||
|
|
expect(config.browser.browserName).toBe('firefox');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('merge order: config file < daemon overrides', async ({}, testInfo) => {
|
||
|
|
const configFile = testInfo.outputPath('config.json');
|
||
|
|
const fileConfig: Config = {
|
||
|
|
browser: { browserName: 'firefox' },
|
||
|
|
timeouts: { action: 1000 },
|
||
|
|
};
|
||
|
|
await fs.promises.writeFile(configFile, JSON.stringify(fileConfig));
|
||
|
|
const config = await resolveCLI(testInfo.outputPath('profiles'), 'default', { config: configFile, browser: 'webkit' });
|
||
|
|
expect(config.browser.browserName).toBe('webkit');
|
||
|
|
expect(config.timeouts.action).toBe(1000);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('config file values preserved when cli does not override', async ({}, testInfo) => {
|
||
|
|
const configFile = testInfo.outputPath('config.json');
|
||
|
|
const fileConfig: Config = {
|
||
|
|
network: { allowedOrigins: ['https://example.com'] },
|
||
|
|
};
|
||
|
|
await fs.promises.writeFile(configFile, JSON.stringify(fileConfig));
|
||
|
|
const config = await resolveCLI(testInfo.outputPath('profiles'), 'default', { config: configFile }) as any;
|
||
|
|
expect(config.network?.allowedOrigins).toEqual(['https://example.com']);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
test.describe('resolveCLIConfigForCLI - extension', () => {
|
||
|
|
test('--extension disables isolated', async ({}, testInfo) => {
|
||
|
|
const config = await resolveCLI(testInfo.outputPath('profiles'), 'default', { extension: true }) as any;
|
||
|
|
expect(config.extension).toBe(true);
|
||
|
|
expect(config.browser.isolated).toBe(false);
|
||
|
|
});
|
||
|
|
});
|