- Split browser.ts (700 lines) into src/browser/ module (page, mcp, errors, discover, tabs, index) - Add unified error handling: CliError base class + logger module - Enable TypeScript strict mode, fix 12 type errors - Extract inline build scripts to scripts/clean-yaml.cjs and copy-yaml.cjs - All 178 unit tests pass, build produces 83 entries across 19 sites
148 lines
4.4 KiB
TypeScript
148 lines
4.4 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { PlaywrightMCP, __test__ } from './browser/index.js';
|
|
|
|
describe('browser helpers', () => {
|
|
it('creates JSON-RPC requests with unique ids', () => {
|
|
const first = __test__.createJsonRpcRequest('tools/call', { name: 'browser_tabs' });
|
|
const second = __test__.createJsonRpcRequest('tools/call', { name: 'browser_snapshot' });
|
|
|
|
expect(second.id).toBe(first.id + 1);
|
|
expect(first.message).toContain(`"id":${first.id}`);
|
|
expect(second.message).toContain(`"id":${second.id}`);
|
|
});
|
|
|
|
it('extracts tab entries from string snapshots', () => {
|
|
const entries = __test__.extractTabEntries('Tab 0 https://example.com\nTab 1 Chrome Extension');
|
|
|
|
expect(entries).toEqual([
|
|
{ index: 0, identity: 'https://example.com' },
|
|
{ index: 1, identity: 'Chrome Extension' },
|
|
]);
|
|
});
|
|
|
|
it('extracts tab entries from MCP markdown format', () => {
|
|
const entries = __test__.extractTabEntries(
|
|
'- 0: (current) [Playwright MCP extension](chrome-extension://abc/connect.html)\n- 1: [知乎 - 首页](https://www.zhihu.com/)'
|
|
);
|
|
|
|
expect(entries).toEqual([
|
|
{ index: 0, identity: '(current) [Playwright MCP extension](chrome-extension://abc/connect.html)' },
|
|
{ index: 1, identity: '[知乎 - 首页](https://www.zhihu.com/)' },
|
|
]);
|
|
});
|
|
|
|
it('closes only tabs that were opened during the session', () => {
|
|
const tabsToClose = __test__.diffTabIndexes(
|
|
['https://example.com', 'Chrome Extension'],
|
|
[
|
|
{ index: 0, identity: 'https://example.com' },
|
|
{ index: 1, identity: 'Chrome Extension' },
|
|
{ index: 2, identity: 'https://target.example/page' },
|
|
{ index: 3, identity: 'chrome-extension://bridge' },
|
|
],
|
|
);
|
|
|
|
expect(tabsToClose).toEqual([3, 2]);
|
|
});
|
|
|
|
it('keeps only the tail of stderr buffers', () => {
|
|
expect(__test__.appendLimited('12345', '67890', 8)).toBe('34567890');
|
|
});
|
|
|
|
it('builds extension MCP args in local mode (no CI)', () => {
|
|
const savedCI = process.env.CI;
|
|
delete process.env.CI;
|
|
try {
|
|
expect(__test__.buildMcpArgs({
|
|
mcpPath: '/tmp/cli.js',
|
|
executablePath: '/mnt/c/Program Files/Google/Chrome/Application/chrome.exe',
|
|
})).toEqual([
|
|
'/tmp/cli.js',
|
|
'--extension',
|
|
'--executable-path',
|
|
'/mnt/c/Program Files/Google/Chrome/Application/chrome.exe',
|
|
]);
|
|
|
|
expect(__test__.buildMcpArgs({
|
|
mcpPath: '/tmp/cli.js',
|
|
})).toEqual([
|
|
'/tmp/cli.js',
|
|
'--extension',
|
|
]);
|
|
} finally {
|
|
if (savedCI !== undefined) {
|
|
process.env.CI = savedCI;
|
|
} else {
|
|
delete process.env.CI;
|
|
}
|
|
}
|
|
});
|
|
|
|
it('builds standalone MCP args in CI mode', () => {
|
|
const savedCI = process.env.CI;
|
|
process.env.CI = 'true';
|
|
try {
|
|
// CI mode: no --extension — browser launches in standalone headed mode
|
|
expect(__test__.buildMcpArgs({
|
|
mcpPath: '/tmp/cli.js',
|
|
})).toEqual([
|
|
'/tmp/cli.js',
|
|
]);
|
|
|
|
expect(__test__.buildMcpArgs({
|
|
mcpPath: '/tmp/cli.js',
|
|
executablePath: '/usr/bin/chromium',
|
|
})).toEqual([
|
|
'/tmp/cli.js',
|
|
'--executable-path',
|
|
'/usr/bin/chromium',
|
|
]);
|
|
} finally {
|
|
if (savedCI !== undefined) {
|
|
process.env.CI = savedCI;
|
|
} else {
|
|
delete process.env.CI;
|
|
}
|
|
}
|
|
});
|
|
|
|
it('times out slow promises', async () => {
|
|
await expect(__test__.withTimeoutMs(new Promise(() => {}), 10, 'timeout')).rejects.toThrow('timeout');
|
|
});
|
|
});
|
|
|
|
describe('PlaywrightMCP state', () => {
|
|
it('transitions to closed after close()', async () => {
|
|
const mcp = new PlaywrightMCP();
|
|
|
|
expect(mcp.state).toBe('idle');
|
|
|
|
await mcp.close();
|
|
|
|
expect(mcp.state).toBe('closed');
|
|
});
|
|
|
|
it('rejects connect() after the session has been closed', async () => {
|
|
const mcp = new PlaywrightMCP();
|
|
await mcp.close();
|
|
|
|
await expect(mcp.connect()).rejects.toThrow('Playwright MCP session is closed');
|
|
});
|
|
|
|
it('rejects connect() while already connecting', async () => {
|
|
const mcp = new PlaywrightMCP();
|
|
(mcp as any)._state = 'connecting';
|
|
|
|
await expect(mcp.connect()).rejects.toThrow('Playwright MCP is already connecting');
|
|
});
|
|
|
|
it('rejects connect() while closing', async () => {
|
|
const mcp = new PlaywrightMCP();
|
|
(mcp as any)._state = 'closing';
|
|
|
|
await expect(mcp.connect()).rejects.toThrow('Playwright MCP is closing');
|
|
});
|
|
|
|
|
|
});
|