Files
AI/참고/playwright-main/tests/mcp/cli-fixtures.ts
2026-05-12 19:40:31 +09:00

305 lines
11 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 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<string, string>,
startDashboardServer: (options?: { cwd?: string, session?: string }) => Promise<Page>,
connectToDashboard: (bindTitle: string) => Promise<Browser>;
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<string[]>(() => [])) {
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<string, string>, 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 = `<!DOCTYPE html>
<html>
<body style="width: 400px; height: 400px; margin: 0; padding: 0;">
<div id='square'style="width: 100px; height: 100px;"></div>
<div id='log'></div>
<script>
const logElement = document.querySelector('#log');
const log = (...args) => {
const el = document.createElement('div');
el.textContent = args.join(' ');
logElement.appendChild(el);
};
document.body.addEventListener('mousemove', (event) => {
log('mouse move', event.clientX, event.clientY);
});
document.body.addEventListener('mousedown', () => {
log('mouse down');
});
document.body.addEventListener('mouseup', () => {
log('mouse up');
});
document.body.addEventListener('wheel', (event) => {
log('wheel', event.deltaX, event.deltaY);
});
document.body.addEventListener('click', () => {
log('click', event.button);
});
document.body.addEventListener('dblclick', (event) => {
log('dblclick', event.button);
});
</script>
</body>
</html>
`;
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<Buffer>> {
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<void> {
await page.evaluate(() => {
(window as any).showSaveFilePicker = async () => {
throw new DOMException('The user aborted a request.', 'AbortError');
};
});
}