Skip to content

Commit 2f3e652

Browse files
committed
chore: add test for cli --extension
1 parent cd9819d commit 2f3e652

File tree

5 files changed

+188
-22
lines changed

5 files changed

+188
-22
lines changed

package-lock.json

Lines changed: 16 additions & 16 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
],
2525
"devDependencies": {
2626
"@modelcontextprotocol/sdk": "^1.25.2",
27-
"@playwright/test": "1.59.0-alpha-1769452054000",
27+
"@playwright/test": "1.59.0-alpha-1769561805000",
2828
"@types/node": "^24.3.0"
2929
}
3030
}

packages/extension/tests/extension.spec.ts

Lines changed: 167 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,32 @@
1717
import fs from 'fs';
1818
import path from 'path';
1919
import { chromium } from 'playwright';
20+
import { spawn } from 'child_process';
2021
import { test as base, expect } from '../../playwright-mcp/tests/fixtures';
2122

2223
import type { Client } from '@modelcontextprotocol/sdk/client/index.js';
2324
import type { BrowserContext } from 'playwright';
2425
import type { StartClient } from '../../playwright-mcp/tests/fixtures';
26+
import type { ChildProcess } from 'child_process';
2527

2628
type BrowserWithExtension = {
2729
userDataDir: string;
2830
launch: (mode?: 'disable-extension') => Promise<BrowserContext>;
2931
};
3032

33+
type CliResult = {
34+
output: string;
35+
error: string;
36+
snapshot?: string;
37+
attachments?: { name: string, data: Buffer | null }[];
38+
};
39+
3140
type TestFixtures = {
3241
browserWithExtension: BrowserWithExtension,
3342
pathToExtension: string,
3443
useShortConnectionTimeout: (timeoutMs: number) => void
3544
overrideProtocolVersion: (version: number) => void
45+
cli: (...args: string[]) => Promise<CliResult>;
3646
};
3747

3848
const test = base.extend<TestFixtures>({
@@ -85,9 +95,127 @@ const test = base.extend<TestFixtures>({
8595
process.env.PWMCP_TEST_PROTOCOL_VERSION = version.toString();
8696
});
8797
process.env.PWMCP_TEST_PROTOCOL_VERSION = undefined;
88-
}
98+
},
99+
100+
cli: async ({ mcpBrowser }, use, testInfo) => {
101+
const activeSessions: { name: string, process: ChildProcess }[] = [];
102+
103+
await use(async (...args: string[]) => {
104+
return await runCli(args, { mcpBrowser, testInfo }, activeSessions);
105+
});
106+
107+
// Cleanup sessions
108+
for (const session of activeSessions) {
109+
await runCli(['session-stop', session.name], { mcpBrowser, testInfo }, []).catch(() => {});
110+
try {
111+
if (session.process.pid)
112+
process.kill(-session.process.pid);
113+
} catch (e) {
114+
if (e.code !== 'ESRCH')
115+
console.error('error killing session', e);
116+
}
117+
}
118+
119+
const daemonDir = path.join(testInfo.outputDir, 'daemon');
120+
const userDataDirs = await fs.promises.readdir(daemonDir).catch(() => []);
121+
for (const dir of userDataDirs.filter(f => f.startsWith('ud-')))
122+
await fs.promises.rm(path.join(daemonDir, dir), { recursive: true, force: true }).catch(() => {});
123+
},
89124
});
90125

126+
async function runCli(
127+
args: string[],
128+
options: { mcpBrowser?: string, testInfo: any },
129+
activeSessions: { name: string, process: ChildProcess }[]
130+
): Promise<CliResult> {
131+
const stepTitle = `cli ${args.join(' ')}`;
132+
133+
return await test.step(stepTitle, async () => {
134+
const testInfo = options.testInfo;
135+
136+
// Path to the terminal CLI
137+
const cliPath = path.join(__dirname, '../../../node_modules/playwright/lib/mcp/terminal/cli.js');
138+
139+
console.error('cliPath', cliPath);
140+
141+
return new Promise<CliResult>((resolve, reject) => {
142+
let stdout = '';
143+
let stderr = '';
144+
145+
const childProcess = spawn(process.execPath, [cliPath, ...args], {
146+
cwd: testInfo.outputPath(),
147+
env: {
148+
...process.env,
149+
PLAYWRIGHT_DAEMON_INSTALL_DIR: testInfo.outputPath(),
150+
PLAYWRIGHT_DAEMON_SESSION_DIR: testInfo.outputPath('daemon'),
151+
PLAYWRIGHT_DAEMON_SOCKETS_DIR: path.join(testInfo.project.outputDir, 'daemon-sockets'),
152+
PLAYWRIGHT_MCP_BROWSER: options.mcpBrowser,
153+
PLAYWRIGHT_MCP_HEADLESS: 'false',
154+
},
155+
detached: true,
156+
});
157+
158+
childProcess.stdout?.on('data', (data) => {
159+
stdout += data.toString();
160+
});
161+
162+
childProcess.stderr?.on('data', (data) => {
163+
if (process.env.PWMCP_DEBUG)
164+
process.stderr.write(data);
165+
stderr += data.toString();
166+
});
167+
168+
childProcess.on('close', async (code) => {
169+
await testInfo.attach(stepTitle, { body: stdout, contentType: 'text/plain' });
170+
171+
let snapshot: string | undefined;
172+
if (stdout.includes('### Snapshot'))
173+
snapshot = await loadSnapshot(stdout, testInfo);
174+
const attachments = loadAttachments(stdout, testInfo);
175+
176+
const matches = stdout.includes('Daemon for') ? stdout.match(/Daemon for `(.+)` session started with pid (\d+)\./) : undefined;
177+
const [, sessionName, pid] = matches ?? [];
178+
if (sessionName && pid)
179+
activeSessions.push({ name: sessionName, process: childProcess });
180+
181+
resolve({
182+
output: stdout.trim(),
183+
error: stderr.trim(),
184+
snapshot,
185+
attachments
186+
});
187+
});
188+
189+
childProcess.on('error', reject);
190+
});
191+
});
192+
}
193+
194+
function loadAttachments(output: string, testInfo: any) {
195+
const match = output.match(/- \[(.+)\]\((.+)\)/g);
196+
if (!match)
197+
return [];
198+
199+
return match.map(m => {
200+
const [, name, filePath] = m.match(/- \[(.+)\]\((.+)\)/)!;
201+
try {
202+
const data = fs.readFileSync(testInfo.outputPath(filePath));
203+
return { name, data };
204+
} catch (e) {
205+
return { name, data: null };
206+
}
207+
});
208+
}
209+
210+
async function loadSnapshot(output: string, testInfo: any) {
211+
const lines = output.split('\n');
212+
if (!lines.includes('### Snapshot'))
213+
throw new Error('Snapshot file not found');
214+
const fileLine = lines[lines.indexOf('### Snapshot') + 1];
215+
const fileName = fileLine.match(/- \[(.+)\]\((.+)\)/)![2];
216+
return await fs.promises.readFile(testInfo.outputPath(fileName), 'utf8');
217+
}
218+
91219
async function startWithExtensionFlag(browserWithExtension: BrowserWithExtension, startClient: StartClient): Promise<Client> {
92220
const { client } = await startClient({
93221
args: [`--extension`],
@@ -302,3 +430,41 @@ test(`bypass connection dialog with token`, async ({ browserWithExtension, start
302430
snapshot: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`),
303431
});
304432
});
433+
434+
test.describe('CLI with extension', () => {
435+
test('open <url> --extension', async ({ browserWithExtension, cli, server }, testInfo) => {
436+
const browserContext = await browserWithExtension.launch();
437+
438+
// Write config file with userDataDir
439+
const configPath = testInfo.outputPath('cli-config.json');
440+
await fs.promises.writeFile(configPath, JSON.stringify({
441+
browser: {
442+
userDataDir: browserWithExtension.userDataDir,
443+
}
444+
}, null, 2));
445+
446+
const confirmationPagePromise = browserContext.waitForEvent('page', page => {
447+
return page.url().startsWith('chrome-extension://jakfalbnbhgkpmoaakfflhflbfpkailf/connect.html');
448+
});
449+
450+
// Start the CLI command in the background
451+
const cliPromise = cli('open', server.HELLO_WORLD, '--extension', `--config=cli-config.json`);
452+
453+
// Wait for the confirmation page to appear
454+
const confirmationPage = await confirmationPagePromise;
455+
456+
// Click the Allow button
457+
await confirmationPage.getByRole('button', { name: 'Allow' }).click();
458+
459+
// Wait for the CLI command to complete
460+
const { output, snapshot } = await cliPromise;
461+
462+
// Verify the output
463+
expect(output).toContain(`### Page`);
464+
expect(output).toContain(`- Page URL: ${server.HELLO_WORLD}`);
465+
expect(output).toContain(`- Page Title: Title`);
466+
467+
// Verify the snapshot
468+
expect(snapshot).toContain(`- generic [active] [ref=e1]: Hello, world!`);
469+
});
470+
});

packages/playwright-cli/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@
2121
},
2222
"dependencies": {
2323
"minimist": "^1.2.5",
24-
"playwright": "1.59.0-alpha-1769452054000",
25-
"playwright-core": "1.59.0-alpha-1769452054000"
24+
"playwright": "1.59.0-alpha-1769561805000",
25+
"playwright-core": "1.59.0-alpha-1769561805000"
2626
},
2727
"bin": {
2828
"playwright-cli": "playwright-cli.js"

packages/playwright-mcp/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@
3434
}
3535
},
3636
"dependencies": {
37-
"playwright": "1.59.0-alpha-1769452054000",
38-
"playwright-core": "1.59.0-alpha-1769452054000"
37+
"playwright": "1.59.0-alpha-1769561805000",
38+
"playwright-core": "1.59.0-alpha-1769561805000"
3939
},
4040
"bin": {
4141
"playwright-mcp": "cli.js"

0 commit comments

Comments
 (0)