/** * 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 crypto from 'crypto'; import fs from 'fs'; import os from 'os'; import path from 'path'; import { test as baseTest, expect } from './fixtures'; import { killProcessGroup } from '../config/commonFixtures'; import { inheritAndCleanEnv } from '../config/utils'; import type { Page, Browser } from 'playwright-core'; import type { CommonFixtures } from '../config/commonFixtures'; export { expect } from './fixtures'; export const test = baseTest.extend<{ boundBrowser: Browser, cliEnv: Record, startDashboardServer: (options?: { cwd?: string, session?: string }) => Promise, connectToDashboard: (bindTitle: string) => Promise; cli: (...args: any[]) => Promise<{ output: string, error: string, exitCode: number | undefined, inlineSnapshot?: string, snapshot?: string, attachments?: { name: string, data: Buffer | null }[], daemonPid?: number, dashboardPid?: number, }>; }>({ cliEnv: async ({}, use) => { await use(cliEnv()); }, startDashboardServer: async ({ childProcess, page }, use) => { await use(async (options?: { cwd?: string, session?: string }) => { const testInfo = test.info(); const showArgs = options?.session ? [`-s=${options.session}`, 'show'] : ['show']; const serverProcess = childProcess({ command: [process.execPath, require.resolve('../../packages/playwright-core/lib/tools/cli-client/cli.js'), ...showArgs, '--port=0'], cwd: options?.cwd ?? testInfo.outputPath(), env: inheritAndCleanEnv(cliEnv()), }); await serverProcess.waitForOutput('Listening on '); await page.goto(serverProcess.output.match(/Listening on (http:\/\/\S+)/)![1]); return page; }); }, connectToDashboard: async ({ cli, playwright }, use) => { await use(async (bindTitle: string) => { let endpoint = ''; await expect(async () => { const { output } = await cli('list', '--all', '--json'); const { servers } = JSON.parse(output); const server = servers.find(s => s.title === bindTitle); endpoint = server.endpoint; }).toPass(); return await playwright.chromium.connect(endpoint); }); await cli('show', '--kill'); }, cli: async ({ mcpBrowser, mcpHeadless, childProcess }, use) => { await fs.promises.mkdir(test.info().outputPath('.playwright'), { recursive: true }); const allPids: number[] = []; await use(async (...args: string[]) => { const cliArgs = args.filter(arg => typeof arg === 'string'); const cliOptions = args.findLast(arg => typeof arg === 'object') || {}; const result = await test.step( `cli ${cliArgs.join(' ')}`, () => runCli(childProcess, cliArgs, cliOptions, { mcpBrowser, mcpHeadless }) ); if (result.daemonPid) allPids.push(result.daemonPid); if (result.dashboardPid) allPids.push(result.dashboardPid); return result; }); for (const pid of allPids) killProcessGroup(pid); const daemonDir = test.info().outputPath('daemon'); for (const dir of await fs.promises.readdir(daemonDir).catch(() => [])) { if (dir.startsWith('ud-')) { await fs.promises.rm(path.join(daemonDir, dir), { recursive: true, force: true }).catch(() => {}); continue; } } }, boundBrowser: async ({ mcpBrowser, playwright }, use) => { const browserName = (mcpBrowser === 'chrome' || mcpBrowser === 'msedge') ? 'chromium' : mcpBrowser; const channel = (mcpBrowser === 'chrome' || mcpBrowser === 'msedge') ? mcpBrowser : undefined; const browser = await playwright[browserName].launch({ channel, headless: true }); for (const [name, value] of Object.entries(cliEnv())) process.env[name] = value; await browser.bind('default'); await use(browser); for (const name of Object.keys(cliEnv())) delete process.env[name]; await browser.close(); }, }); function cliEnv() { return { PLAYWRIGHT_SERVER_REGISTRY: test.info().outputPath('registry'), PWTEST_DASHBOARD_SETTINGS_FILE: test.info().outputPath('dashboard.settings.json'), PLAYWRIGHT_DAEMON_SESSION_DIR: test.info().outputPath('daemon'), PLAYWRIGHT_SOCKETS_DIR: path.join(os.tmpdir(), 'ds-' + crypto.createHash('sha1').update(test.info().outputDir).digest('hex').slice(0, 16)), PWTEST_CLI_CHANNEL_SCAN_DISABLED_FOR_TEST: '1', }; } async function runCli(childProcess: CommonFixtures['childProcess'], args: string[], cliOptions: { cwd?: string, env?: Record, bindTitle?: string }, options: { mcpBrowser: string, mcpHeadless: boolean }) { const testInfo = test.info(); const cli = childProcess({ command: [process.execPath, require.resolve('../../packages/playwright-core/lib/tools/cli-client/cli.js'), ...args], cwd: cliOptions.cwd ?? testInfo.outputPath(), env: inheritAndCleanEnv({ ...cliEnv(), PLAYWRIGHT_MCP_BROWSER: options.mcpBrowser, PLAYWRIGHT_MCP_HEADLESS: String(options.mcpHeadless), PWTEST_PRINT_DASHBOARD_PID_FOR_TEST: '1', PWTEST_DASHBOARD_APP_BIND_TITLE: cliOptions.bindTitle, ...cliOptions.env, }), }); // Wait for the CLI to exit so stdout is complete before we parse it. const exitCode = await cli.exitCode; let snapshot: string | undefined; let inlineSnapshot: string | undefined; if (cli.stdout.includes('### Snapshot')) ({ snapshot, inlineSnapshot } = await loadSnapshot(cli.stdout)); const attachments = loadAttachments(cli.stdout); const browserMatches = cli.stdout.includes('### Browser') ? cli.stdout.match(/Browser `(.+)` opened with pid (\d+)\./) : undefined; const daemonPid = browserMatches?.[2] ?? parseJsonPid(cli.stdout); const dashboardMatches = cli.stdout.includes('### Dashboard') ? cli.stdout.match(/Dashboard opened with pid (\d+)\./) : undefined; const dashboardPid = dashboardMatches?.[1]; return { exitCode, output: cli.stdout.trim(), error: cli.stderr.trim(), snapshot, inlineSnapshot, attachments, daemonPid: daemonPid ? +daemonPid : undefined, dashboardPid: dashboardPid ? +dashboardPid : undefined, }; } function parseJsonPid(stdout: string) { try { return JSON.parse(stdout).pid; } catch { } } function loadAttachments(output: string) { // attachments look like md links - [Page as pdf](.playwright-cli/page-2026-01-22T23-13-56-347Z.pdf) const match = output.match(/- \[(.+)\]\((.+)\)/g); if (!match) return []; return match.map(m => { const [, name, path] = m.match(/- \[(.+)\]\((.+)\)/)!; try { const data = fs.readFileSync(test.info().outputPath(path)); return { name, data }; } catch (e) { return { name, data: null }; } }); } async function loadSnapshot(output: string): Promise<{ snapshot?: string, inlineSnapshot?: string }> { const lines = output.split('\n'); if (!lines.includes('### Snapshot')) throw new Error('Snapshot file not found'); const snapshotIndex = lines.indexOf('### Snapshot') + 1; const fileLine = lines[snapshotIndex]; if (fileLine.startsWith('```yaml')) return { inlineSnapshot: lines.slice(snapshotIndex + 1, lines.indexOf('```', snapshotIndex)).join('\n') }; const fileName = fileLine.match(/- \[(.+)\]\((.+)\)/)![2]; try { return { snapshot: await fs.promises.readFile(test.info().outputPath(fileName), 'utf8').catch(() => undefined) }; } catch (e) { return {}; } } export const eventsPage = `
`; export async function findDefaultSession() { const daemonDir = await daemonFolder(); const fileName = path.join(daemonDir, 'default.session'); return await fs.promises.readFile(fileName, 'utf-8').then(JSON.parse).catch(() => null); } export async function daemonFolder() { const daemonDir = test.info().outputPath('daemon'); const folders = await fs.promises.readdir(daemonDir); for (const folder of folders) { const fullName = path.join(daemonDir, folder); if (fs.lstatSync(path.join(fullName)).isDirectory()) return fullName; } return null; } export async function installSaveFilePickerMock(page: import('playwright-core').Page): Promise<() => Promise> { await page.evaluate(() => { (window as any).__testCaptureBytes = undefined as string | undefined; (window as any).showSaveFilePicker = async () => ({ createWritable: async () => { const chunks: Uint8Array[] = []; return { write: async (chunk: Blob | BufferSource) => { const buf = chunk instanceof Blob ? new Uint8Array(await chunk.arrayBuffer()) : new Uint8Array(chunk instanceof ArrayBuffer ? chunk : (chunk as ArrayBufferView).buffer); chunks.push(buf); }, close: async () => { const total = chunks.reduce((n, c) => n + c.byteLength, 0); const merged = new Uint8Array(total); let offset = 0; for (const c of chunks) { merged.set(c, offset); offset += c.byteLength; } (window as any).__testCaptureBytes = (merged as any).toBase64(); }, }; }, }); }); return async () => { await expect.poll(() => page.evaluate(() => !!(window as any).__testCaptureBytes), { timeout: 10000 }).toBe(true); const b64: string = await page.evaluate(() => (window as any).__testCaptureBytes); return Buffer.from(b64, 'base64'); }; } export async function mockAbortingFilePicker(page: import('playwright-core').Page): Promise { await page.evaluate(() => { (window as any).showSaveFilePicker = async () => { throw new DOMException('The user aborted a request.', 'AbortError'); }; }); }