playwriter 0.0.16 → 0.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cdp-session.d.ts +21 -0
- package/dist/cdp-session.d.ts.map +1 -0
- package/dist/cdp-session.js +131 -0
- package/dist/cdp-session.js.map +1 -0
- package/dist/cdp-types.d.ts +15 -0
- package/dist/cdp-types.d.ts.map +1 -1
- package/dist/cdp-types.js.map +1 -1
- package/dist/create-logger.d.ts +9 -0
- package/dist/create-logger.d.ts.map +1 -0
- package/dist/create-logger.js +43 -0
- package/dist/create-logger.js.map +1 -0
- package/dist/extension/cdp-relay.d.ts +7 -3
- package/dist/extension/cdp-relay.d.ts.map +1 -1
- package/dist/extension/cdp-relay.js +22 -12
- package/dist/extension/cdp-relay.js.map +1 -1
- package/dist/mcp.js +86 -44
- package/dist/mcp.js.map +1 -1
- package/dist/mcp.test.d.ts.map +1 -1
- package/dist/mcp.test.js +669 -183
- package/dist/mcp.test.js.map +1 -1
- package/dist/prompt.md +38 -8
- package/dist/selector-generator.js +331 -0
- package/dist/start-relay-server.d.ts +1 -3
- package/dist/start-relay-server.d.ts.map +1 -1
- package/dist/start-relay-server.js +3 -16
- package/dist/start-relay-server.js.map +1 -1
- package/dist/utils.d.ts +3 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +36 -0
- package/dist/utils.js.map +1 -1
- package/dist/wait-for-page-load.d.ts +16 -0
- package/dist/wait-for-page-load.d.ts.map +1 -0
- package/dist/wait-for-page-load.js +126 -0
- package/dist/wait-for-page-load.js.map +1 -0
- package/package.json +16 -12
- package/src/cdp-session.ts +156 -0
- package/src/cdp-types.ts +6 -0
- package/src/create-logger.ts +56 -0
- package/src/debugger.md +453 -0
- package/src/extension/cdp-relay.ts +32 -14
- package/src/mcp.test.ts +795 -189
- package/src/mcp.ts +101 -47
- package/src/prompt.md +38 -8
- package/src/snapshots/shadcn-ui-accessibility.md +94 -91
- package/src/start-relay-server.ts +3 -20
- package/src/utils.ts +45 -0
- package/src/wait-for-page-load.ts +173 -0
package/dist/mcp.test.js
CHANGED
|
@@ -7,7 +7,10 @@ import path from 'node:path';
|
|
|
7
7
|
import fs from 'node:fs';
|
|
8
8
|
import os from 'node:os';
|
|
9
9
|
import { getCdpUrl } from './utils.js';
|
|
10
|
-
import {
|
|
10
|
+
import { imageSize } from 'image-size';
|
|
11
|
+
import { getCDPSessionForPage } from './cdp-session.js';
|
|
12
|
+
import { startPlayWriterCDPRelayServer } from './extension/cdp-relay.js';
|
|
13
|
+
import { createFileLogger } from './create-logger.js';
|
|
11
14
|
const execAsync = promisify(exec);
|
|
12
15
|
async function getExtensionServiceWorker(context) {
|
|
13
16
|
let serviceWorkers = context.serviceWorkers().filter(sw => sw.url().startsWith('chrome-extension://'));
|
|
@@ -17,6 +20,15 @@ async function getExtensionServiceWorker(context) {
|
|
|
17
20
|
predicate: (sw) => sw.url().startsWith('chrome-extension://')
|
|
18
21
|
});
|
|
19
22
|
}
|
|
23
|
+
for (let i = 0; i < 50; i++) {
|
|
24
|
+
const isReady = await serviceWorker.evaluate(() => {
|
|
25
|
+
// @ts-ignore
|
|
26
|
+
return typeof globalThis.toggleExtensionForActiveTab === 'function';
|
|
27
|
+
});
|
|
28
|
+
if (isReady)
|
|
29
|
+
break;
|
|
30
|
+
await new Promise(r => setTimeout(r, 100));
|
|
31
|
+
}
|
|
20
32
|
return serviceWorker;
|
|
21
33
|
}
|
|
22
34
|
function js(strings, ...values) {
|
|
@@ -36,98 +48,95 @@ async function killProcessOnPort(port) {
|
|
|
36
48
|
// No process running on port or already killed
|
|
37
49
|
}
|
|
38
50
|
}
|
|
51
|
+
async function setupTestContext({ tempDirPrefix }) {
|
|
52
|
+
await killProcessOnPort(19988);
|
|
53
|
+
console.log('Building extension...');
|
|
54
|
+
await execAsync('TESTING=1 pnpm build', { cwd: '../extension' });
|
|
55
|
+
console.log('Extension built');
|
|
56
|
+
const localLogPath = path.join(process.cwd(), 'relay-server.log');
|
|
57
|
+
const logger = createFileLogger({ logFilePath: localLogPath });
|
|
58
|
+
const relayServer = await startPlayWriterCDPRelayServer({ port: 19988, logger });
|
|
59
|
+
const userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), tempDirPrefix));
|
|
60
|
+
const extensionPath = path.resolve('../extension/dist');
|
|
61
|
+
const browserContext = await chromium.launchPersistentContext(userDataDir, {
|
|
62
|
+
channel: 'chromium',
|
|
63
|
+
headless: !process.env.HEADFUL,
|
|
64
|
+
colorScheme: 'dark',
|
|
65
|
+
args: [
|
|
66
|
+
`--disable-extensions-except=${extensionPath}`,
|
|
67
|
+
`--load-extension=${extensionPath}`,
|
|
68
|
+
],
|
|
69
|
+
});
|
|
70
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext);
|
|
71
|
+
const page = await browserContext.newPage();
|
|
72
|
+
await page.goto('about:blank');
|
|
73
|
+
await serviceWorker.evaluate(async () => {
|
|
74
|
+
await globalThis.toggleExtensionForActiveTab();
|
|
75
|
+
});
|
|
76
|
+
return { browserContext, userDataDir, relayServer };
|
|
77
|
+
}
|
|
78
|
+
async function cleanupTestContext(ctx, cleanup) {
|
|
79
|
+
if (ctx?.browserContext) {
|
|
80
|
+
await ctx.browserContext.close();
|
|
81
|
+
}
|
|
82
|
+
if (ctx?.relayServer) {
|
|
83
|
+
ctx.relayServer.close();
|
|
84
|
+
}
|
|
85
|
+
if (ctx?.userDataDir) {
|
|
86
|
+
try {
|
|
87
|
+
fs.rmSync(ctx.userDataDir, { recursive: true, force: true });
|
|
88
|
+
}
|
|
89
|
+
catch (e) {
|
|
90
|
+
console.error('Failed to cleanup user data dir:', e);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (cleanup) {
|
|
94
|
+
await cleanup();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
39
97
|
describe('MCP Server Tests', () => {
|
|
40
98
|
let client;
|
|
41
99
|
let cleanup = null;
|
|
42
|
-
let
|
|
43
|
-
let userDataDir;
|
|
44
|
-
let relayServerProcess;
|
|
100
|
+
let testCtx = null;
|
|
45
101
|
beforeAll(async () => {
|
|
46
|
-
await
|
|
47
|
-
// Build extension
|
|
48
|
-
console.log('Building extension...');
|
|
49
|
-
await execAsync('TESTING=1 pnpm build', { cwd: '../extension' });
|
|
50
|
-
console.log('Extension built');
|
|
51
|
-
// Start Relay Server manually
|
|
52
|
-
relayServerProcess = spawn('pnpm', ['tsx', 'src/start-relay-server.ts'], {
|
|
53
|
-
cwd: process.cwd(),
|
|
54
|
-
stdio: 'inherit'
|
|
55
|
-
});
|
|
56
|
-
// Wait for port 19988 to be ready
|
|
57
|
-
await new Promise((resolve, reject) => {
|
|
58
|
-
let retries = 0;
|
|
59
|
-
const interval = setInterval(async () => {
|
|
60
|
-
try {
|
|
61
|
-
const { stdout } = await execAsync('lsof -ti:19988');
|
|
62
|
-
if (stdout.trim()) {
|
|
63
|
-
clearInterval(interval);
|
|
64
|
-
resolve();
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
catch {
|
|
68
|
-
// ignore
|
|
69
|
-
}
|
|
70
|
-
retries++;
|
|
71
|
-
if (retries > 30) {
|
|
72
|
-
clearInterval(interval);
|
|
73
|
-
reject(new Error('Relay server failed to start'));
|
|
74
|
-
}
|
|
75
|
-
}, 1000);
|
|
76
|
-
});
|
|
102
|
+
testCtx = await setupTestContext({ tempDirPrefix: 'pw-test-' });
|
|
77
103
|
const result = await createMCPClient();
|
|
78
104
|
client = result.client;
|
|
79
105
|
cleanup = result.cleanup;
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
106
|
+
}, 600000);
|
|
107
|
+
afterAll(async () => {
|
|
108
|
+
await cleanupTestContext(testCtx, cleanup);
|
|
109
|
+
cleanup = null;
|
|
110
|
+
testCtx = null;
|
|
111
|
+
});
|
|
112
|
+
const getBrowserContext = () => {
|
|
113
|
+
if (!testCtx?.browserContext)
|
|
114
|
+
throw new Error('Browser not initialized');
|
|
115
|
+
return testCtx.browserContext;
|
|
116
|
+
};
|
|
117
|
+
it('should inject script via addScriptTag through CDP relay', async () => {
|
|
118
|
+
const browserContext = getBrowserContext();
|
|
91
119
|
const serviceWorker = await getExtensionServiceWorker(browserContext);
|
|
92
|
-
// Wait for extension to initialize global functions
|
|
93
|
-
for (let i = 0; i < 50; i++) {
|
|
94
|
-
const isReady = await serviceWorker.evaluate(() => {
|
|
95
|
-
// @ts-ignore
|
|
96
|
-
return typeof globalThis.toggleExtensionForActiveTab === 'function';
|
|
97
|
-
});
|
|
98
|
-
if (isReady)
|
|
99
|
-
break;
|
|
100
|
-
await new Promise(r => setTimeout(r, 100));
|
|
101
|
-
}
|
|
102
|
-
// Create a page to attach to
|
|
103
120
|
const page = await browserContext.newPage();
|
|
104
|
-
await page.
|
|
105
|
-
|
|
121
|
+
await page.setContent('<html><body><button id="btn">Click</button></body></html>');
|
|
122
|
+
await page.bringToFront();
|
|
106
123
|
await serviceWorker.evaluate(async () => {
|
|
107
124
|
await globalThis.toggleExtensionForActiveTab();
|
|
108
125
|
});
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
await
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
console.error('Failed to cleanup user data dir:', e);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
if (cleanup) {
|
|
127
|
-
await cleanup();
|
|
128
|
-
cleanup = null;
|
|
129
|
-
}
|
|
130
|
-
});
|
|
126
|
+
await new Promise(r => setTimeout(r, 500));
|
|
127
|
+
const browser = await chromium.connectOverCDP(getCdpUrl());
|
|
128
|
+
const cdpPage = browser.contexts()[0].pages().find(p => {
|
|
129
|
+
return p.url().startsWith('about:');
|
|
130
|
+
});
|
|
131
|
+
expect(cdpPage).toBeDefined();
|
|
132
|
+
const hasGlobalBefore = await cdpPage.evaluate(() => !!globalThis.__testGlobal);
|
|
133
|
+
expect(hasGlobalBefore).toBe(false);
|
|
134
|
+
await cdpPage.addScriptTag({ content: 'globalThis.__testGlobal = { foo: "bar" };' });
|
|
135
|
+
const hasGlobalAfter = await cdpPage.evaluate(() => globalThis.__testGlobal);
|
|
136
|
+
expect(hasGlobalAfter).toEqual({ foo: 'bar' });
|
|
137
|
+
await browser.close();
|
|
138
|
+
await page.close();
|
|
139
|
+
}, 60000);
|
|
131
140
|
it('should execute code and capture console output', async () => {
|
|
132
141
|
await client.callTool({
|
|
133
142
|
name: 'execute',
|
|
@@ -169,8 +178,7 @@ describe('MCP Server Tests', () => {
|
|
|
169
178
|
expect(result.content).toBeDefined();
|
|
170
179
|
}, 30000);
|
|
171
180
|
it('should show extension as connected for pages created via newPage()', async () => {
|
|
172
|
-
|
|
173
|
-
throw new Error('Browser not initialized');
|
|
181
|
+
const browserContext = getBrowserContext();
|
|
174
182
|
const serviceWorker = await getExtensionServiceWorker(browserContext);
|
|
175
183
|
// Create a page via MCP (which uses context.newPage())
|
|
176
184
|
await client.callTool({
|
|
@@ -290,9 +298,7 @@ describe('MCP Server Tests', () => {
|
|
|
290
298
|
});
|
|
291
299
|
});
|
|
292
300
|
it('should handle new pages and toggling with new connections', async () => {
|
|
293
|
-
|
|
294
|
-
throw new Error('Browser not initialized');
|
|
295
|
-
// Find the correct service worker by URL
|
|
301
|
+
const browserContext = getBrowserContext();
|
|
296
302
|
const serviceWorker = await getExtensionServiceWorker(browserContext);
|
|
297
303
|
// 1. Create a new page
|
|
298
304
|
const page = await browserContext.newPage();
|
|
@@ -356,8 +362,7 @@ describe('MCP Server Tests', () => {
|
|
|
356
362
|
await page.close();
|
|
357
363
|
});
|
|
358
364
|
it('should handle new pages and toggling with persistent connection', async () => {
|
|
359
|
-
|
|
360
|
-
throw new Error('Browser not initialized');
|
|
365
|
+
const browserContext = getBrowserContext();
|
|
361
366
|
const serviceWorker = await getExtensionServiceWorker(browserContext);
|
|
362
367
|
// Connect once
|
|
363
368
|
const directBrowser = await chromium.connectOverCDP(getCdpUrl());
|
|
@@ -420,8 +425,7 @@ describe('MCP Server Tests', () => {
|
|
|
420
425
|
await directBrowser.close();
|
|
421
426
|
});
|
|
422
427
|
it('should maintain connection across reloads and navigation', async () => {
|
|
423
|
-
|
|
424
|
-
throw new Error('Browser not initialized');
|
|
428
|
+
const browserContext = getBrowserContext();
|
|
425
429
|
const serviceWorker = await getExtensionServiceWorker(browserContext);
|
|
426
430
|
// 1. Setup page
|
|
427
431
|
const page = await browserContext.newPage();
|
|
@@ -459,83 +463,7 @@ describe('MCP Server Tests', () => {
|
|
|
459
463
|
await page.close();
|
|
460
464
|
});
|
|
461
465
|
it('should support multiple concurrent tabs', async () => {
|
|
462
|
-
|
|
463
|
-
throw new Error('Browser not initialized');
|
|
464
|
-
const serviceWorker = await getExtensionServiceWorker(browserContext);
|
|
465
|
-
await new Promise(resolve => setTimeout(resolve, 500));
|
|
466
|
-
// Tab A
|
|
467
|
-
const pageA = await browserContext.newPage();
|
|
468
|
-
await pageA.goto('https://example.com/tab-a');
|
|
469
|
-
await pageA.bringToFront();
|
|
470
|
-
await new Promise(resolve => setTimeout(resolve, 500));
|
|
471
|
-
await serviceWorker.evaluate(async () => {
|
|
472
|
-
await globalThis.toggleExtensionForActiveTab();
|
|
473
|
-
});
|
|
474
|
-
// Tab B
|
|
475
|
-
const pageB = await browserContext.newPage();
|
|
476
|
-
await pageB.goto('https://example.com/tab-b');
|
|
477
|
-
await pageB.bringToFront();
|
|
478
|
-
await new Promise(resolve => setTimeout(resolve, 500));
|
|
479
|
-
await serviceWorker.evaluate(async () => {
|
|
480
|
-
await globalThis.toggleExtensionForActiveTab();
|
|
481
|
-
});
|
|
482
|
-
// Get target IDs for both
|
|
483
|
-
const targetIds = await serviceWorker.evaluate(async () => {
|
|
484
|
-
const state = globalThis.getExtensionState();
|
|
485
|
-
const chrome = globalThis.chrome;
|
|
486
|
-
const tabs = await chrome.tabs.query({});
|
|
487
|
-
const tabA = tabs.find((t) => t.url?.includes('tab-a'));
|
|
488
|
-
const tabB = tabs.find((t) => t.url?.includes('tab-b'));
|
|
489
|
-
return {
|
|
490
|
-
idA: state.tabs.get(tabA?.id ?? -1)?.targetId,
|
|
491
|
-
idB: state.tabs.get(tabB?.id ?? -1)?.targetId
|
|
492
|
-
};
|
|
493
|
-
});
|
|
494
|
-
expect(targetIds).toMatchInlineSnapshot({
|
|
495
|
-
idA: expect.any(String),
|
|
496
|
-
idB: expect.any(String)
|
|
497
|
-
}, `
|
|
498
|
-
{
|
|
499
|
-
"idA": Any<String>,
|
|
500
|
-
"idB": Any<String>,
|
|
501
|
-
}
|
|
502
|
-
`);
|
|
503
|
-
expect(targetIds.idA).not.toBe(targetIds.idB);
|
|
504
|
-
// Verify independent connections
|
|
505
|
-
const browser = await chromium.connectOverCDP(getCdpUrl());
|
|
506
|
-
const pages = browser.contexts()[0].pages();
|
|
507
|
-
const results = await Promise.all(pages.map(async (p) => ({
|
|
508
|
-
url: p.url(),
|
|
509
|
-
title: await p.title()
|
|
510
|
-
})));
|
|
511
|
-
expect(results).toMatchInlineSnapshot(`
|
|
512
|
-
[
|
|
513
|
-
{
|
|
514
|
-
"title": "",
|
|
515
|
-
"url": "about:blank",
|
|
516
|
-
},
|
|
517
|
-
{
|
|
518
|
-
"title": "Example Domain",
|
|
519
|
-
"url": "https://example.com/tab-a",
|
|
520
|
-
},
|
|
521
|
-
{
|
|
522
|
-
"title": "Example Domain",
|
|
523
|
-
"url": "https://example.com/tab-b",
|
|
524
|
-
},
|
|
525
|
-
]
|
|
526
|
-
`);
|
|
527
|
-
// Verify execution on both pages
|
|
528
|
-
const pageA_CDP = pages.find(p => p.url().includes('tab-a'));
|
|
529
|
-
const pageB_CDP = pages.find(p => p.url().includes('tab-b'));
|
|
530
|
-
expect(await pageA_CDP?.evaluate(() => 10 + 10)).toBe(20);
|
|
531
|
-
expect(await pageB_CDP?.evaluate(() => 20 + 20)).toBe(40);
|
|
532
|
-
await browser.close();
|
|
533
|
-
await pageA.close();
|
|
534
|
-
await pageB.close();
|
|
535
|
-
});
|
|
536
|
-
it('should support multiple concurrent tabs', async () => {
|
|
537
|
-
if (!browserContext)
|
|
538
|
-
throw new Error('Browser not initialized');
|
|
466
|
+
const browserContext = getBrowserContext();
|
|
539
467
|
const serviceWorker = await getExtensionServiceWorker(browserContext);
|
|
540
468
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
541
469
|
// Tab A
|
|
@@ -609,10 +537,8 @@ describe('MCP Server Tests', () => {
|
|
|
609
537
|
await pageB.close();
|
|
610
538
|
});
|
|
611
539
|
it('should show correct url when enabling extension after navigation', async () => {
|
|
612
|
-
|
|
613
|
-
throw new Error('Browser not initialized');
|
|
540
|
+
const browserContext = getBrowserContext();
|
|
614
541
|
const serviceWorker = await getExtensionServiceWorker(browserContext);
|
|
615
|
-
// 1. Open a new page (extension not yet enabled for it)
|
|
616
542
|
const page = await browserContext.newPage();
|
|
617
543
|
const targetUrl = 'https://example.com/late-enable';
|
|
618
544
|
await page.goto(targetUrl);
|
|
@@ -634,10 +560,8 @@ describe('MCP Server Tests', () => {
|
|
|
634
560
|
await page.close();
|
|
635
561
|
});
|
|
636
562
|
it('should be able to reconnect after disconnecting everything', async () => {
|
|
637
|
-
|
|
638
|
-
throw new Error('Browser not initialized');
|
|
563
|
+
const browserContext = getBrowserContext();
|
|
639
564
|
const serviceWorker = await getExtensionServiceWorker(browserContext);
|
|
640
|
-
// 1. Use the existing about:blank page from beforeAll
|
|
641
565
|
const pages = await browserContext.pages();
|
|
642
566
|
expect(pages.length).toBeGreaterThan(0);
|
|
643
567
|
const page = pages[0];
|
|
@@ -1062,8 +986,7 @@ describe('MCP Server Tests', () => {
|
|
|
1062
986
|
});
|
|
1063
987
|
}, 30000);
|
|
1064
988
|
it('should maintain correct page.url() with service worker pages', async () => {
|
|
1065
|
-
|
|
1066
|
-
throw new Error('Browser not initialized');
|
|
989
|
+
const browserContext = getBrowserContext();
|
|
1067
990
|
const serviceWorker = await getExtensionServiceWorker(browserContext);
|
|
1068
991
|
const page = await browserContext.newPage();
|
|
1069
992
|
const targetUrl = 'https://x.com';
|
|
@@ -1083,8 +1006,7 @@ describe('MCP Server Tests', () => {
|
|
|
1083
1006
|
await page.close();
|
|
1084
1007
|
}, 30000);
|
|
1085
1008
|
it('should maintain correct page.url() after repeated connections', async () => {
|
|
1086
|
-
|
|
1087
|
-
throw new Error('Browser not initialized');
|
|
1009
|
+
const browserContext = getBrowserContext();
|
|
1088
1010
|
const serviceWorker = await getExtensionServiceWorker(browserContext);
|
|
1089
1011
|
const page = await browserContext.newPage();
|
|
1090
1012
|
const targetUrl = 'https://example.com/repeated-test';
|
|
@@ -1105,8 +1027,7 @@ describe('MCP Server Tests', () => {
|
|
|
1105
1027
|
await page.close();
|
|
1106
1028
|
}, 30000);
|
|
1107
1029
|
it('should maintain correct page.url() with concurrent MCP and CDP connections', async () => {
|
|
1108
|
-
|
|
1109
|
-
throw new Error('Browser not initialized');
|
|
1030
|
+
const browserContext = getBrowserContext();
|
|
1110
1031
|
const serviceWorker = await getExtensionServiceWorker(browserContext);
|
|
1111
1032
|
const page = await browserContext.newPage();
|
|
1112
1033
|
const targetUrl = 'https://example.com/concurrent-test';
|
|
@@ -1138,8 +1059,7 @@ describe('MCP Server Tests', () => {
|
|
|
1138
1059
|
await page.close();
|
|
1139
1060
|
}, 30000);
|
|
1140
1061
|
it('should maintain correct page.url() with iframe-heavy pages', async () => {
|
|
1141
|
-
|
|
1142
|
-
throw new Error('Browser not initialized');
|
|
1062
|
+
const browserContext = getBrowserContext();
|
|
1143
1063
|
const serviceWorker = await getExtensionServiceWorker(browserContext);
|
|
1144
1064
|
const page = await browserContext.newPage();
|
|
1145
1065
|
const targetUrl = 'https://www.youtube.com';
|
|
@@ -1160,9 +1080,306 @@ describe('MCP Server Tests', () => {
|
|
|
1160
1080
|
}
|
|
1161
1081
|
await page.close();
|
|
1162
1082
|
}, 60000);
|
|
1083
|
+
it('should capture screenshot correctly', async () => {
|
|
1084
|
+
const browserContext = getBrowserContext();
|
|
1085
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext);
|
|
1086
|
+
const page = await browserContext.newPage();
|
|
1087
|
+
await page.goto('https://example.com/');
|
|
1088
|
+
await page.bringToFront();
|
|
1089
|
+
await serviceWorker.evaluate(async () => {
|
|
1090
|
+
await globalThis.toggleExtensionForActiveTab();
|
|
1091
|
+
});
|
|
1092
|
+
await new Promise(r => setTimeout(r, 500));
|
|
1093
|
+
const capturedCommands = [];
|
|
1094
|
+
const commandHandler = ({ command }) => {
|
|
1095
|
+
if (command.method === 'Page.captureScreenshot') {
|
|
1096
|
+
capturedCommands.push(command);
|
|
1097
|
+
}
|
|
1098
|
+
};
|
|
1099
|
+
testCtx.relayServer.on('cdp:command', commandHandler);
|
|
1100
|
+
const browser = await chromium.connectOverCDP(getCdpUrl());
|
|
1101
|
+
const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'));
|
|
1102
|
+
expect(cdpPage).toBeDefined();
|
|
1103
|
+
const viewportSize = cdpPage.viewportSize();
|
|
1104
|
+
console.log('Viewport size:', viewportSize);
|
|
1105
|
+
const viewportScreenshot = await cdpPage.screenshot();
|
|
1106
|
+
expect(viewportScreenshot).toBeDefined();
|
|
1107
|
+
const viewportDimensions = imageSize(viewportScreenshot);
|
|
1108
|
+
console.log('Viewport screenshot dimensions:', viewportDimensions);
|
|
1109
|
+
expect(viewportDimensions.width).toBeGreaterThan(0);
|
|
1110
|
+
expect(viewportDimensions.height).toBeGreaterThan(0);
|
|
1111
|
+
if (viewportSize) {
|
|
1112
|
+
expect(viewportDimensions.width).toBe(viewportSize.width);
|
|
1113
|
+
expect(viewportDimensions.height).toBe(viewportSize.height);
|
|
1114
|
+
}
|
|
1115
|
+
const fullPageScreenshot = await cdpPage.screenshot({ fullPage: true });
|
|
1116
|
+
expect(fullPageScreenshot).toBeDefined();
|
|
1117
|
+
const fullPageDimensions = imageSize(fullPageScreenshot);
|
|
1118
|
+
console.log('Full page screenshot dimensions:', fullPageDimensions);
|
|
1119
|
+
expect(fullPageDimensions.width).toBeGreaterThan(0);
|
|
1120
|
+
expect(fullPageDimensions.height).toBeGreaterThan(0);
|
|
1121
|
+
expect(fullPageDimensions.width).toBeGreaterThanOrEqual(viewportDimensions.width);
|
|
1122
|
+
testCtx.relayServer.off('cdp:command', commandHandler);
|
|
1123
|
+
expect(capturedCommands.length).toBe(2);
|
|
1124
|
+
expect(capturedCommands.map(c => ({
|
|
1125
|
+
method: c.method,
|
|
1126
|
+
params: c.params
|
|
1127
|
+
}))).toMatchInlineSnapshot(`
|
|
1128
|
+
[
|
|
1129
|
+
{
|
|
1130
|
+
"method": "Page.captureScreenshot",
|
|
1131
|
+
"params": {
|
|
1132
|
+
"captureBeyondViewport": false,
|
|
1133
|
+
"clip": {
|
|
1134
|
+
"height": 720,
|
|
1135
|
+
"scale": 1,
|
|
1136
|
+
"width": 1280,
|
|
1137
|
+
"x": 0,
|
|
1138
|
+
"y": 0,
|
|
1139
|
+
},
|
|
1140
|
+
"format": "png",
|
|
1141
|
+
},
|
|
1142
|
+
},
|
|
1143
|
+
{
|
|
1144
|
+
"method": "Page.captureScreenshot",
|
|
1145
|
+
"params": {
|
|
1146
|
+
"captureBeyondViewport": false,
|
|
1147
|
+
"clip": {
|
|
1148
|
+
"height": 528,
|
|
1149
|
+
"scale": 1,
|
|
1150
|
+
"width": 1280,
|
|
1151
|
+
"x": 0,
|
|
1152
|
+
"y": 0,
|
|
1153
|
+
},
|
|
1154
|
+
"format": "png",
|
|
1155
|
+
},
|
|
1156
|
+
},
|
|
1157
|
+
]
|
|
1158
|
+
`);
|
|
1159
|
+
const screenshotPath = path.join(os.tmpdir(), 'playwriter-test-screenshot.png');
|
|
1160
|
+
fs.writeFileSync(screenshotPath, viewportScreenshot);
|
|
1161
|
+
console.log('Screenshot saved to:', screenshotPath);
|
|
1162
|
+
await browser.close();
|
|
1163
|
+
await page.close();
|
|
1164
|
+
}, 60000);
|
|
1165
|
+
it('should capture element screenshot with correct coordinates', async () => {
|
|
1166
|
+
const browserContext = getBrowserContext();
|
|
1167
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext);
|
|
1168
|
+
const target = { x: 200, y: 150, width: 300, height: 100 };
|
|
1169
|
+
const scrolledTarget = { x: 100, y: 1500, width: 200, height: 80 };
|
|
1170
|
+
const page = await browserContext.newPage();
|
|
1171
|
+
await page.setContent(`
|
|
1172
|
+
<html>
|
|
1173
|
+
<head>
|
|
1174
|
+
<style>
|
|
1175
|
+
body { margin: 0; padding: 0; height: 2000px; }
|
|
1176
|
+
#target {
|
|
1177
|
+
position: absolute;
|
|
1178
|
+
top: ${target.y}px;
|
|
1179
|
+
left: ${target.x}px;
|
|
1180
|
+
width: ${target.width}px;
|
|
1181
|
+
height: ${target.height}px;
|
|
1182
|
+
background: red;
|
|
1183
|
+
}
|
|
1184
|
+
#scrolled-target {
|
|
1185
|
+
position: absolute;
|
|
1186
|
+
top: ${scrolledTarget.y}px;
|
|
1187
|
+
left: ${scrolledTarget.x}px;
|
|
1188
|
+
width: ${scrolledTarget.width}px;
|
|
1189
|
+
height: ${scrolledTarget.height}px;
|
|
1190
|
+
background: blue;
|
|
1191
|
+
}
|
|
1192
|
+
</style>
|
|
1193
|
+
</head>
|
|
1194
|
+
<body>
|
|
1195
|
+
<div id="target">Target Element</div>
|
|
1196
|
+
<div id="scrolled-target">Scrolled Target</div>
|
|
1197
|
+
</body>
|
|
1198
|
+
</html>
|
|
1199
|
+
`);
|
|
1200
|
+
await page.bringToFront();
|
|
1201
|
+
await serviceWorker.evaluate(async () => {
|
|
1202
|
+
await globalThis.toggleExtensionForActiveTab();
|
|
1203
|
+
});
|
|
1204
|
+
await new Promise(r => setTimeout(r, 500));
|
|
1205
|
+
const capturedCommands = [];
|
|
1206
|
+
const commandHandler = ({ command }) => {
|
|
1207
|
+
if (command.method === 'Page.captureScreenshot') {
|
|
1208
|
+
capturedCommands.push(command);
|
|
1209
|
+
}
|
|
1210
|
+
};
|
|
1211
|
+
testCtx.relayServer.on('cdp:command', commandHandler);
|
|
1212
|
+
const browser = await chromium.connectOverCDP(getCdpUrl());
|
|
1213
|
+
let cdpPage;
|
|
1214
|
+
for (const p of browser.contexts()[0].pages()) {
|
|
1215
|
+
const html = await p.content();
|
|
1216
|
+
if (html.includes('scrolled-target')) {
|
|
1217
|
+
cdpPage = p;
|
|
1218
|
+
break;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
expect(cdpPage).toBeDefined();
|
|
1222
|
+
await cdpPage.locator('#target').screenshot();
|
|
1223
|
+
await cdpPage.locator('#scrolled-target').screenshot();
|
|
1224
|
+
testCtx.relayServer.off('cdp:command', commandHandler);
|
|
1225
|
+
expect(capturedCommands.length).toBe(2);
|
|
1226
|
+
const targetCmd = capturedCommands[0];
|
|
1227
|
+
expect(targetCmd.method).toBe('Page.captureScreenshot');
|
|
1228
|
+
const targetClip = targetCmd.params.clip;
|
|
1229
|
+
expect(targetClip.x).toBe(target.x);
|
|
1230
|
+
expect(targetClip.y).toBe(target.y);
|
|
1231
|
+
expect(targetClip.width).toBe(target.width);
|
|
1232
|
+
expect(targetClip.height).toBe(target.height);
|
|
1233
|
+
const scrolledCmd = capturedCommands[1];
|
|
1234
|
+
expect(scrolledCmd.method).toBe('Page.captureScreenshot');
|
|
1235
|
+
const scrolledClip = scrolledCmd.params.clip;
|
|
1236
|
+
expect(scrolledClip.x).toBe(scrolledTarget.x);
|
|
1237
|
+
expect(scrolledClip.y).toBe(scrolledTarget.y);
|
|
1238
|
+
expect(scrolledClip.width).toBe(scrolledTarget.width);
|
|
1239
|
+
expect(scrolledClip.height).toBe(scrolledTarget.height);
|
|
1240
|
+
await browser.close();
|
|
1241
|
+
await page.close();
|
|
1242
|
+
}, 60000);
|
|
1243
|
+
it('should get locator string for element using getLocatorStringForElement', async () => {
|
|
1244
|
+
const browserContext = getBrowserContext();
|
|
1245
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext);
|
|
1246
|
+
const page = await browserContext.newPage();
|
|
1247
|
+
await page.setContent(`
|
|
1248
|
+
<html>
|
|
1249
|
+
<body>
|
|
1250
|
+
<button id="test-btn">Click Me</button>
|
|
1251
|
+
<input type="text" placeholder="Enter name" />
|
|
1252
|
+
</body>
|
|
1253
|
+
</html>
|
|
1254
|
+
`);
|
|
1255
|
+
await page.bringToFront();
|
|
1256
|
+
await serviceWorker.evaluate(async () => {
|
|
1257
|
+
await globalThis.toggleExtensionForActiveTab();
|
|
1258
|
+
});
|
|
1259
|
+
await new Promise(r => setTimeout(r, 500));
|
|
1260
|
+
const result = await client.callTool({
|
|
1261
|
+
name: 'execute',
|
|
1262
|
+
arguments: {
|
|
1263
|
+
code: js `
|
|
1264
|
+
let testPage;
|
|
1265
|
+
for (const p of context.pages()) {
|
|
1266
|
+
const html = await p.content();
|
|
1267
|
+
if (html.includes('test-btn')) { testPage = p; break; }
|
|
1268
|
+
}
|
|
1269
|
+
if (!testPage) throw new Error('Test page not found');
|
|
1270
|
+
const btn = testPage.locator('#test-btn');
|
|
1271
|
+
const locatorString = await getLocatorStringForElement(btn);
|
|
1272
|
+
console.log('Locator string:', locatorString);
|
|
1273
|
+
const locatorFromString = eval('testPage.' + locatorString);
|
|
1274
|
+
const count = await locatorFromString.count();
|
|
1275
|
+
console.log('Locator count:', count);
|
|
1276
|
+
const text = await locatorFromString.textContent();
|
|
1277
|
+
console.log('Locator text:', text);
|
|
1278
|
+
`,
|
|
1279
|
+
timeout: 30000,
|
|
1280
|
+
},
|
|
1281
|
+
});
|
|
1282
|
+
expect(result.isError).toBeFalsy();
|
|
1283
|
+
const text = result.content[0]?.text || '';
|
|
1284
|
+
expect(text).toContain('Locator string:');
|
|
1285
|
+
expect(text).toContain("getByRole('button', { name: 'Click Me' })");
|
|
1286
|
+
expect(text).toContain('Locator count: 1');
|
|
1287
|
+
expect(text).toContain('Locator text: Click Me');
|
|
1288
|
+
await page.close();
|
|
1289
|
+
}, 60000);
|
|
1290
|
+
it('should return correct layout metrics via CDP', async () => {
|
|
1291
|
+
const browserContext = getBrowserContext();
|
|
1292
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext);
|
|
1293
|
+
const page = await browserContext.newPage();
|
|
1294
|
+
await page.goto('https://example.com/');
|
|
1295
|
+
await page.bringToFront();
|
|
1296
|
+
await serviceWorker.evaluate(async () => {
|
|
1297
|
+
await globalThis.toggleExtensionForActiveTab();
|
|
1298
|
+
});
|
|
1299
|
+
await new Promise(r => setTimeout(r, 500));
|
|
1300
|
+
const browser = await chromium.connectOverCDP(getCdpUrl());
|
|
1301
|
+
const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'));
|
|
1302
|
+
expect(cdpPage).toBeDefined();
|
|
1303
|
+
const wsUrl = getCdpUrl();
|
|
1304
|
+
const cdpSession = await getCDPSessionForPage({ page: cdpPage, wsUrl });
|
|
1305
|
+
const layoutMetrics = await cdpSession.send('Page.getLayoutMetrics');
|
|
1306
|
+
const normalized = {
|
|
1307
|
+
cssLayoutViewport: layoutMetrics.cssLayoutViewport,
|
|
1308
|
+
cssVisualViewport: layoutMetrics.cssVisualViewport,
|
|
1309
|
+
layoutViewport: layoutMetrics.layoutViewport,
|
|
1310
|
+
visualViewport: layoutMetrics.visualViewport,
|
|
1311
|
+
devicePixelRatio: layoutMetrics.cssVisualViewport.clientWidth > 0
|
|
1312
|
+
? layoutMetrics.visualViewport.clientWidth / layoutMetrics.cssVisualViewport.clientWidth
|
|
1313
|
+
: 1,
|
|
1314
|
+
};
|
|
1315
|
+
expect(normalized).toMatchInlineSnapshot(`
|
|
1316
|
+
{
|
|
1317
|
+
"cssLayoutViewport": {
|
|
1318
|
+
"clientHeight": 720,
|
|
1319
|
+
"clientWidth": 1280,
|
|
1320
|
+
"pageX": 0,
|
|
1321
|
+
"pageY": 0,
|
|
1322
|
+
},
|
|
1323
|
+
"cssVisualViewport": {
|
|
1324
|
+
"clientHeight": 720,
|
|
1325
|
+
"clientWidth": 1280,
|
|
1326
|
+
"offsetX": 0,
|
|
1327
|
+
"offsetY": 0,
|
|
1328
|
+
"pageX": 0,
|
|
1329
|
+
"pageY": 0,
|
|
1330
|
+
"scale": 1,
|
|
1331
|
+
"zoom": 1,
|
|
1332
|
+
},
|
|
1333
|
+
"devicePixelRatio": 1,
|
|
1334
|
+
"layoutViewport": {
|
|
1335
|
+
"clientHeight": 720,
|
|
1336
|
+
"clientWidth": 1280,
|
|
1337
|
+
"pageX": 0,
|
|
1338
|
+
"pageY": 0,
|
|
1339
|
+
},
|
|
1340
|
+
"visualViewport": {
|
|
1341
|
+
"clientHeight": 720,
|
|
1342
|
+
"clientWidth": 1280,
|
|
1343
|
+
"offsetX": 0,
|
|
1344
|
+
"offsetY": 0,
|
|
1345
|
+
"pageX": 0,
|
|
1346
|
+
"pageY": 0,
|
|
1347
|
+
"scale": 1,
|
|
1348
|
+
"zoom": 1,
|
|
1349
|
+
},
|
|
1350
|
+
}
|
|
1351
|
+
`);
|
|
1352
|
+
const windowDpr = await cdpPage.evaluate(() => globalThis.devicePixelRatio);
|
|
1353
|
+
console.log('window.devicePixelRatio:', windowDpr);
|
|
1354
|
+
expect(windowDpr).toBe(1);
|
|
1355
|
+
cdpSession.detach();
|
|
1356
|
+
await browser.close();
|
|
1357
|
+
await page.close();
|
|
1358
|
+
}, 60000);
|
|
1359
|
+
it('should support getCDPSession through the relay', async () => {
|
|
1360
|
+
const browserContext = getBrowserContext();
|
|
1361
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext);
|
|
1362
|
+
const page = await browserContext.newPage();
|
|
1363
|
+
await page.goto('https://example.com/');
|
|
1364
|
+
await page.bringToFront();
|
|
1365
|
+
await serviceWorker.evaluate(async () => {
|
|
1366
|
+
await globalThis.toggleExtensionForActiveTab();
|
|
1367
|
+
});
|
|
1368
|
+
await new Promise(r => setTimeout(r, 500));
|
|
1369
|
+
const browser = await chromium.connectOverCDP(getCdpUrl());
|
|
1370
|
+
const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'));
|
|
1371
|
+
expect(cdpPage).toBeDefined();
|
|
1372
|
+
const wsUrl = getCdpUrl();
|
|
1373
|
+
const client = await getCDPSessionForPage({ page: cdpPage, wsUrl });
|
|
1374
|
+
const layoutMetrics = await client.send('Page.getLayoutMetrics');
|
|
1375
|
+
expect(layoutMetrics.cssVisualViewport).toBeDefined();
|
|
1376
|
+
expect(layoutMetrics.cssVisualViewport.clientWidth).toBeGreaterThan(0);
|
|
1377
|
+
client.detach();
|
|
1378
|
+
await browser.close();
|
|
1379
|
+
await page.close();
|
|
1380
|
+
}, 60000);
|
|
1163
1381
|
it('should work with stagehand', async () => {
|
|
1164
|
-
|
|
1165
|
-
throw new Error('Browser not initialized');
|
|
1382
|
+
const browserContext = getBrowserContext();
|
|
1166
1383
|
const serviceWorker = await getExtensionServiceWorker(browserContext);
|
|
1167
1384
|
await serviceWorker.evaluate(async () => {
|
|
1168
1385
|
await globalThis.disconnectEverything();
|
|
@@ -1201,6 +1418,51 @@ describe('MCP Server Tests', () => {
|
|
|
1201
1418
|
expect(url).toContain('example.com');
|
|
1202
1419
|
await stagehand.close();
|
|
1203
1420
|
}, 60000);
|
|
1421
|
+
it('should preserve system color scheme instead of forcing light mode', async () => {
|
|
1422
|
+
const browserContext = getBrowserContext();
|
|
1423
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext);
|
|
1424
|
+
const page = await browserContext.newPage();
|
|
1425
|
+
await page.goto('https://example.com');
|
|
1426
|
+
await page.bringToFront();
|
|
1427
|
+
const colorSchemeBefore = await page.evaluate(() => {
|
|
1428
|
+
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
1429
|
+
});
|
|
1430
|
+
console.log('Color scheme before MCP connection:', colorSchemeBefore);
|
|
1431
|
+
await serviceWorker.evaluate(async () => {
|
|
1432
|
+
await globalThis.toggleExtensionForActiveTab();
|
|
1433
|
+
});
|
|
1434
|
+
await new Promise(r => setTimeout(r, 500));
|
|
1435
|
+
const result = await client.callTool({
|
|
1436
|
+
name: 'execute',
|
|
1437
|
+
arguments: {
|
|
1438
|
+
code: js `
|
|
1439
|
+
const pages = context.pages();
|
|
1440
|
+
const urls = pages.map(p => p.url());
|
|
1441
|
+
const targetPage = pages.find(p => p.url().includes('example.com'));
|
|
1442
|
+
if (!targetPage) {
|
|
1443
|
+
return { error: 'Page not found', urls };
|
|
1444
|
+
}
|
|
1445
|
+
const isDark = await targetPage.evaluate(() => window.matchMedia('(prefers-color-scheme: dark)').matches);
|
|
1446
|
+
const isLight = await targetPage.evaluate(() => window.matchMedia('(prefers-color-scheme: light)').matches);
|
|
1447
|
+
return { matchesDark: isDark, matchesLight: isLight };
|
|
1448
|
+
`,
|
|
1449
|
+
},
|
|
1450
|
+
});
|
|
1451
|
+
console.log('Color scheme after MCP connection:', result.content);
|
|
1452
|
+
expect(result.content).toMatchInlineSnapshot(`
|
|
1453
|
+
[
|
|
1454
|
+
{
|
|
1455
|
+
"text": "Return value:
|
|
1456
|
+
{
|
|
1457
|
+
"matchesDark": true,
|
|
1458
|
+
"matchesLight": false
|
|
1459
|
+
}",
|
|
1460
|
+
"type": "text",
|
|
1461
|
+
},
|
|
1462
|
+
]
|
|
1463
|
+
`);
|
|
1464
|
+
await page.close();
|
|
1465
|
+
}, 60000);
|
|
1204
1466
|
});
|
|
1205
1467
|
function tryJsonParse(str) {
|
|
1206
1468
|
try {
|
|
@@ -1210,4 +1472,228 @@ function tryJsonParse(str) {
|
|
|
1210
1472
|
return str;
|
|
1211
1473
|
}
|
|
1212
1474
|
}
|
|
1475
|
+
describe('CDP Session Tests', () => {
|
|
1476
|
+
let testCtx = null;
|
|
1477
|
+
beforeAll(async () => {
|
|
1478
|
+
testCtx = await setupTestContext({ tempDirPrefix: 'pw-cdp-test-' });
|
|
1479
|
+
}, 600000);
|
|
1480
|
+
afterAll(async () => {
|
|
1481
|
+
await cleanupTestContext(testCtx);
|
|
1482
|
+
testCtx = null;
|
|
1483
|
+
});
|
|
1484
|
+
const getBrowserContext = () => {
|
|
1485
|
+
if (!testCtx?.browserContext)
|
|
1486
|
+
throw new Error('Browser not initialized');
|
|
1487
|
+
return testCtx.browserContext;
|
|
1488
|
+
};
|
|
1489
|
+
it('should enable debugger and pause on debugger statement via CDP session', async () => {
|
|
1490
|
+
const browserContext = getBrowserContext();
|
|
1491
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext);
|
|
1492
|
+
const page = await browserContext.newPage();
|
|
1493
|
+
await page.goto('https://example.com/');
|
|
1494
|
+
await page.bringToFront();
|
|
1495
|
+
await serviceWorker.evaluate(async () => {
|
|
1496
|
+
await globalThis.toggleExtensionForActiveTab();
|
|
1497
|
+
});
|
|
1498
|
+
await new Promise(r => setTimeout(r, 500));
|
|
1499
|
+
const browser = await chromium.connectOverCDP(getCdpUrl());
|
|
1500
|
+
const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'));
|
|
1501
|
+
expect(cdpPage).toBeDefined();
|
|
1502
|
+
const wsUrl = getCdpUrl();
|
|
1503
|
+
const cdpSession = await getCDPSessionForPage({ page: cdpPage, wsUrl });
|
|
1504
|
+
await cdpSession.send('Debugger.enable');
|
|
1505
|
+
const pausedPromise = new Promise((resolve) => {
|
|
1506
|
+
cdpSession.on('Debugger.paused', (params) => {
|
|
1507
|
+
resolve(params);
|
|
1508
|
+
});
|
|
1509
|
+
});
|
|
1510
|
+
cdpPage.evaluate(`
|
|
1511
|
+
(function testFunction() {
|
|
1512
|
+
const localVar = 'hello';
|
|
1513
|
+
const numberVar = 42;
|
|
1514
|
+
const objVar = { key: 'value', nested: { a: 1 } };
|
|
1515
|
+
debugger;
|
|
1516
|
+
return localVar + numberVar;
|
|
1517
|
+
})()
|
|
1518
|
+
`);
|
|
1519
|
+
const pausedEvent = await Promise.race([
|
|
1520
|
+
pausedPromise,
|
|
1521
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('Debugger.paused timeout')), 5000))
|
|
1522
|
+
]);
|
|
1523
|
+
const stackTrace = pausedEvent.callFrames.map(frame => ({
|
|
1524
|
+
functionName: frame.functionName || '(anonymous)',
|
|
1525
|
+
lineNumber: frame.location.lineNumber,
|
|
1526
|
+
columnNumber: frame.location.columnNumber,
|
|
1527
|
+
}));
|
|
1528
|
+
expect({
|
|
1529
|
+
reason: pausedEvent.reason,
|
|
1530
|
+
stackTrace: stackTrace.slice(0, 3),
|
|
1531
|
+
}).toMatchInlineSnapshot(`
|
|
1532
|
+
{
|
|
1533
|
+
"reason": "other",
|
|
1534
|
+
"stackTrace": [
|
|
1535
|
+
{
|
|
1536
|
+
"columnNumber": 16,
|
|
1537
|
+
"functionName": "testFunction",
|
|
1538
|
+
"lineNumber": 4,
|
|
1539
|
+
},
|
|
1540
|
+
{
|
|
1541
|
+
"columnNumber": 14,
|
|
1542
|
+
"functionName": "(anonymous)",
|
|
1543
|
+
"lineNumber": 6,
|
|
1544
|
+
},
|
|
1545
|
+
{
|
|
1546
|
+
"columnNumber": 29,
|
|
1547
|
+
"functionName": "evaluate",
|
|
1548
|
+
"lineNumber": 289,
|
|
1549
|
+
},
|
|
1550
|
+
],
|
|
1551
|
+
}
|
|
1552
|
+
`);
|
|
1553
|
+
const topFrame = pausedEvent.callFrames[0];
|
|
1554
|
+
const scopeChain = topFrame.scopeChain;
|
|
1555
|
+
const localScope = scopeChain.find(s => s.type === 'local');
|
|
1556
|
+
const localVars = {};
|
|
1557
|
+
if (localScope?.object.objectId) {
|
|
1558
|
+
const propsResult = await cdpSession.send('Runtime.getProperties', {
|
|
1559
|
+
objectId: localScope.object.objectId,
|
|
1560
|
+
ownProperties: true,
|
|
1561
|
+
});
|
|
1562
|
+
for (const prop of propsResult.result) {
|
|
1563
|
+
if (prop.value) {
|
|
1564
|
+
localVars[prop.name] = prop.value.type === 'object'
|
|
1565
|
+
? `[object ${prop.value.className || prop.value.subtype || 'Object'}]`
|
|
1566
|
+
: prop.value.value;
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
expect({
|
|
1571
|
+
scopeTypes: scopeChain.map(s => s.type),
|
|
1572
|
+
localVariables: localVars,
|
|
1573
|
+
}).toMatchInlineSnapshot(`
|
|
1574
|
+
{
|
|
1575
|
+
"localVariables": {
|
|
1576
|
+
"localVar": "hello",
|
|
1577
|
+
"numberVar": 42,
|
|
1578
|
+
"objVar": "[object Object]",
|
|
1579
|
+
},
|
|
1580
|
+
"scopeTypes": [
|
|
1581
|
+
"local",
|
|
1582
|
+
"global",
|
|
1583
|
+
],
|
|
1584
|
+
}
|
|
1585
|
+
`);
|
|
1586
|
+
const evalResult = await cdpSession.send('Debugger.evaluateOnCallFrame', {
|
|
1587
|
+
callFrameId: topFrame.callFrameId,
|
|
1588
|
+
expression: 'localVar + " world " + numberVar',
|
|
1589
|
+
});
|
|
1590
|
+
expect({
|
|
1591
|
+
evaluatedExpression: 'localVar + " world " + numberVar',
|
|
1592
|
+
result: evalResult.result.value,
|
|
1593
|
+
type: evalResult.result.type,
|
|
1594
|
+
}).toMatchInlineSnapshot(`
|
|
1595
|
+
{
|
|
1596
|
+
"evaluatedExpression": "localVar + " world " + numberVar",
|
|
1597
|
+
"result": "hello world 42",
|
|
1598
|
+
"type": "string",
|
|
1599
|
+
}
|
|
1600
|
+
`);
|
|
1601
|
+
await cdpSession.send('Debugger.resume');
|
|
1602
|
+
await cdpSession.send('Debugger.disable');
|
|
1603
|
+
cdpSession.detach();
|
|
1604
|
+
await browser.close();
|
|
1605
|
+
await page.close();
|
|
1606
|
+
}, 60000);
|
|
1607
|
+
it('should profile JavaScript execution using CDP Profiler', async () => {
|
|
1608
|
+
const browserContext = getBrowserContext();
|
|
1609
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext);
|
|
1610
|
+
const page = await browserContext.newPage();
|
|
1611
|
+
await page.goto('https://example.com/');
|
|
1612
|
+
await page.bringToFront();
|
|
1613
|
+
await serviceWorker.evaluate(async () => {
|
|
1614
|
+
await globalThis.toggleExtensionForActiveTab();
|
|
1615
|
+
});
|
|
1616
|
+
await new Promise(r => setTimeout(r, 500));
|
|
1617
|
+
const browser = await chromium.connectOverCDP(getCdpUrl());
|
|
1618
|
+
const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'));
|
|
1619
|
+
expect(cdpPage).toBeDefined();
|
|
1620
|
+
const wsUrl = getCdpUrl();
|
|
1621
|
+
const cdpSession = await getCDPSessionForPage({ page: cdpPage, wsUrl });
|
|
1622
|
+
await cdpSession.send('Profiler.enable');
|
|
1623
|
+
await cdpSession.send('Profiler.start');
|
|
1624
|
+
await cdpPage.evaluate(`
|
|
1625
|
+
(() => {
|
|
1626
|
+
function fibonacci(n) {
|
|
1627
|
+
if (n <= 1) return n
|
|
1628
|
+
return fibonacci(n - 1) + fibonacci(n - 2)
|
|
1629
|
+
}
|
|
1630
|
+
for (let i = 0; i < 5; i++) {
|
|
1631
|
+
fibonacci(20)
|
|
1632
|
+
}
|
|
1633
|
+
for (let i = 0; i < 1000; i++) {
|
|
1634
|
+
document.querySelectorAll('*')
|
|
1635
|
+
}
|
|
1636
|
+
})()
|
|
1637
|
+
`);
|
|
1638
|
+
const stopResult = await cdpSession.send('Profiler.stop');
|
|
1639
|
+
const profile = stopResult.profile;
|
|
1640
|
+
const functionNames = profile.nodes
|
|
1641
|
+
.map(n => n.callFrame.functionName)
|
|
1642
|
+
.filter(name => name && name.length > 0)
|
|
1643
|
+
.slice(0, 10);
|
|
1644
|
+
expect({
|
|
1645
|
+
hasNodes: profile.nodes.length > 0,
|
|
1646
|
+
nodeCount: profile.nodes.length,
|
|
1647
|
+
durationMicroseconds: profile.endTime - profile.startTime,
|
|
1648
|
+
sampleFunctionNames: functionNames,
|
|
1649
|
+
}).toMatchInlineSnapshot(`
|
|
1650
|
+
{
|
|
1651
|
+
"durationMicroseconds": 7500,
|
|
1652
|
+
"hasNodes": true,
|
|
1653
|
+
"nodeCount": 7,
|
|
1654
|
+
"sampleFunctionNames": [
|
|
1655
|
+
"(root)",
|
|
1656
|
+
"(program)",
|
|
1657
|
+
"(idle)",
|
|
1658
|
+
"evaluate",
|
|
1659
|
+
],
|
|
1660
|
+
}
|
|
1661
|
+
`);
|
|
1662
|
+
await cdpSession.send('Profiler.disable');
|
|
1663
|
+
cdpSession.detach();
|
|
1664
|
+
await browser.close();
|
|
1665
|
+
await page.close();
|
|
1666
|
+
}, 60000);
|
|
1667
|
+
it('should click at correct coordinates on high-DPI simulation', async () => {
|
|
1668
|
+
const browserContext = getBrowserContext();
|
|
1669
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext);
|
|
1670
|
+
const page = await browserContext.newPage();
|
|
1671
|
+
await page.goto('https://example.com/');
|
|
1672
|
+
await page.bringToFront();
|
|
1673
|
+
await serviceWorker.evaluate(async () => {
|
|
1674
|
+
await globalThis.toggleExtensionForActiveTab();
|
|
1675
|
+
});
|
|
1676
|
+
await new Promise(r => setTimeout(r, 500));
|
|
1677
|
+
const browser = await chromium.connectOverCDP(getCdpUrl());
|
|
1678
|
+
const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'));
|
|
1679
|
+
expect(cdpPage).toBeDefined();
|
|
1680
|
+
const h1Bounds = await cdpPage.locator('h1').boundingBox();
|
|
1681
|
+
expect(h1Bounds).toBeDefined();
|
|
1682
|
+
console.log('H1 bounding box:', h1Bounds);
|
|
1683
|
+
await cdpPage.evaluate(() => {
|
|
1684
|
+
window.clickedAt = null;
|
|
1685
|
+
document.addEventListener('click', (e) => {
|
|
1686
|
+
window.clickedAt = { x: e.clientX, y: e.clientY };
|
|
1687
|
+
});
|
|
1688
|
+
});
|
|
1689
|
+
await cdpPage.locator('h1').click();
|
|
1690
|
+
const clickedAt = await cdpPage.evaluate(() => window.clickedAt);
|
|
1691
|
+
console.log('Clicked at:', clickedAt);
|
|
1692
|
+
expect(clickedAt).toBeDefined();
|
|
1693
|
+
expect(clickedAt.x).toBeGreaterThan(0);
|
|
1694
|
+
expect(clickedAt.y).toBeGreaterThan(0);
|
|
1695
|
+
await browser.close();
|
|
1696
|
+
await page.close();
|
|
1697
|
+
}, 60000);
|
|
1698
|
+
});
|
|
1213
1699
|
//# sourceMappingURL=mcp.test.js.map
|