e2e-pilot 0.0.69
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/bin.js +3 -0
- package/dist/aria-snapshot.d.ts +95 -0
- package/dist/aria-snapshot.d.ts.map +1 -0
- package/dist/aria-snapshot.js +490 -0
- package/dist/aria-snapshot.js.map +1 -0
- package/dist/bippy.js +971 -0
- package/dist/cdp-relay.d.ts +16 -0
- package/dist/cdp-relay.d.ts.map +1 -0
- package/dist/cdp-relay.js +715 -0
- package/dist/cdp-relay.js.map +1 -0
- package/dist/cdp-session.d.ts +42 -0
- package/dist/cdp-session.d.ts.map +1 -0
- package/dist/cdp-session.js +154 -0
- package/dist/cdp-session.js.map +1 -0
- package/dist/cdp-types.d.ts +63 -0
- package/dist/cdp-types.d.ts.map +1 -0
- package/dist/cdp-types.js +91 -0
- package/dist/cdp-types.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +213 -0
- package/dist/cli.js.map +1 -0
- package/dist/create-logger.d.ts +9 -0
- package/dist/create-logger.d.ts.map +1 -0
- package/dist/create-logger.js +25 -0
- package/dist/create-logger.js.map +1 -0
- package/dist/debugger-api.md +458 -0
- package/dist/debugger-examples-types.d.ts +24 -0
- package/dist/debugger-examples-types.d.ts.map +1 -0
- package/dist/debugger-examples-types.js +2 -0
- package/dist/debugger-examples-types.js.map +1 -0
- package/dist/debugger-examples.d.ts +6 -0
- package/dist/debugger-examples.d.ts.map +1 -0
- package/dist/debugger-examples.js +53 -0
- package/dist/debugger-examples.js.map +1 -0
- package/dist/debugger.d.ts +381 -0
- package/dist/debugger.d.ts.map +1 -0
- package/dist/debugger.js +633 -0
- package/dist/debugger.js.map +1 -0
- package/dist/editor-api.md +364 -0
- package/dist/editor-examples.d.ts +11 -0
- package/dist/editor-examples.d.ts.map +1 -0
- package/dist/editor-examples.js +124 -0
- package/dist/editor-examples.js.map +1 -0
- package/dist/editor.d.ts +203 -0
- package/dist/editor.d.ts.map +1 -0
- package/dist/editor.js +336 -0
- package/dist/editor.js.map +1 -0
- package/dist/execute.d.ts +50 -0
- package/dist/execute.d.ts.map +1 -0
- package/dist/execute.js +576 -0
- package/dist/execute.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-client.d.ts +20 -0
- package/dist/mcp-client.d.ts.map +1 -0
- package/dist/mcp-client.js +56 -0
- package/dist/mcp-client.js.map +1 -0
- package/dist/mcp.d.ts +5 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +720 -0
- package/dist/mcp.js.map +1 -0
- package/dist/mcp.test.d.ts +10 -0
- package/dist/mcp.test.d.ts.map +1 -0
- package/dist/mcp.test.js +2999 -0
- package/dist/mcp.test.js.map +1 -0
- package/dist/network-capture.d.ts +23 -0
- package/dist/network-capture.d.ts.map +1 -0
- package/dist/network-capture.js +98 -0
- package/dist/network-capture.js.map +1 -0
- package/dist/protocol.d.ts +54 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +2 -0
- package/dist/protocol.js.map +1 -0
- package/dist/react-source.d.ts +13 -0
- package/dist/react-source.d.ts.map +1 -0
- package/dist/react-source.js +68 -0
- package/dist/react-source.js.map +1 -0
- package/dist/scoped-fs.d.ts +94 -0
- package/dist/scoped-fs.d.ts.map +1 -0
- package/dist/scoped-fs.js +356 -0
- package/dist/scoped-fs.js.map +1 -0
- package/dist/selector-generator.js +8126 -0
- package/dist/start-relay-server.d.ts +6 -0
- package/dist/start-relay-server.d.ts.map +1 -0
- package/dist/start-relay-server.js +33 -0
- package/dist/start-relay-server.js.map +1 -0
- package/dist/styles-api.md +117 -0
- package/dist/styles-examples.d.ts +8 -0
- package/dist/styles-examples.d.ts.map +1 -0
- package/dist/styles-examples.js +64 -0
- package/dist/styles-examples.js.map +1 -0
- package/dist/styles.d.ts +27 -0
- package/dist/styles.d.ts.map +1 -0
- package/dist/styles.js +234 -0
- package/dist/styles.js.map +1 -0
- package/dist/trace-utils.d.ts +14 -0
- package/dist/trace-utils.d.ts.map +1 -0
- package/dist/trace-utils.js +21 -0
- package/dist/trace-utils.js.map +1 -0
- package/dist/utils.d.ts +20 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +75 -0
- package/dist/utils.js.map +1 -0
- 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 +127 -0
- package/dist/wait-for-page-load.js.map +1 -0
- package/package.json +67 -0
- package/src/aria-snapshot.ts +610 -0
- package/src/assets/aria-labels-github-snapshot.txt +605 -0
- package/src/assets/aria-labels-github.png +0 -0
- package/src/assets/aria-labels-google-snapshot.txt +49 -0
- package/src/assets/aria-labels-google.png +0 -0
- package/src/assets/aria-labels-hacker-news-snapshot.txt +1023 -0
- package/src/assets/aria-labels-hacker-news.png +0 -0
- package/src/cdp-relay.ts +925 -0
- package/src/cdp-session.ts +203 -0
- package/src/cdp-timing.md +128 -0
- package/src/cdp-types.ts +155 -0
- package/src/cli.ts +250 -0
- package/src/create-logger.ts +36 -0
- package/src/debugger-examples-types.ts +13 -0
- package/src/debugger-examples.ts +66 -0
- package/src/debugger.md +453 -0
- package/src/debugger.ts +713 -0
- package/src/editor-examples.ts +148 -0
- package/src/editor.ts +390 -0
- package/src/execute.ts +763 -0
- package/src/index.ts +10 -0
- package/src/mcp-client.ts +78 -0
- package/src/mcp.test.ts +3596 -0
- package/src/mcp.ts +876 -0
- package/src/network-capture.ts +140 -0
- package/src/prompt.bak.md +323 -0
- package/src/prompt.md +7 -0
- package/src/protocol.ts +63 -0
- package/src/react-source.ts +94 -0
- package/src/resource.md +436 -0
- package/src/scoped-fs.ts +411 -0
- package/src/snapshots/hacker-news-focused-accessibility.md +202 -0
- package/src/snapshots/hacker-news-initial-accessibility.md +11 -0
- package/src/snapshots/hacker-news-tabbed-accessibility.md +202 -0
- package/src/snapshots/shadcn-ui-accessibility.md +11 -0
- package/src/start-relay-server.ts +43 -0
- package/src/styles-examples.ts +77 -0
- package/src/styles.ts +345 -0
- package/src/trace-utils.ts +43 -0
- package/src/utils.ts +91 -0
- package/src/wait-for-page-load.ts +174 -0
package/src/mcp.test.ts
ADDED
|
@@ -0,0 +1,3596 @@
|
|
|
1
|
+
import { createMCPClient } from './mcp-client.js'
|
|
2
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
|
|
3
|
+
import { exec } from 'node:child_process'
|
|
4
|
+
import { promisify } from 'node:util'
|
|
5
|
+
import { chromium, BrowserContext } from 'playwright-core'
|
|
6
|
+
import path from 'node:path'
|
|
7
|
+
import fs from 'node:fs'
|
|
8
|
+
import os from 'node:os'
|
|
9
|
+
import { getCdpUrl } from './utils.js'
|
|
10
|
+
import type { ExtensionState } from 'e2e-pilot-extension/src/types.js'
|
|
11
|
+
import type { Protocol } from 'devtools-protocol'
|
|
12
|
+
import { imageSize } from 'image-size'
|
|
13
|
+
import { getCDPSessionForPage } from './cdp-session.js'
|
|
14
|
+
import { Debugger } from './debugger.js'
|
|
15
|
+
import { Editor } from './editor.js'
|
|
16
|
+
import { startE2EPilotCDPRelayServer, type RelayServer } from './cdp-relay.js'
|
|
17
|
+
import { createFileLogger } from './create-logger.js'
|
|
18
|
+
import type { CDPCommand } from './cdp-types.js'
|
|
19
|
+
import { killPortProcess } from 'kill-port-process'
|
|
20
|
+
|
|
21
|
+
declare const window: any
|
|
22
|
+
declare const document: any
|
|
23
|
+
|
|
24
|
+
const TEST_PORT = 19987
|
|
25
|
+
const TEST_EXTENSION_OUT_DIR = 'dist-test'
|
|
26
|
+
|
|
27
|
+
const execAsync = promisify(exec)
|
|
28
|
+
|
|
29
|
+
async function getExtensionServiceWorker(context: BrowserContext) {
|
|
30
|
+
let serviceWorkers = context.serviceWorkers().filter((sw) => sw.url().startsWith('chrome-extension://'))
|
|
31
|
+
let serviceWorker = serviceWorkers[0]
|
|
32
|
+
if (!serviceWorker) {
|
|
33
|
+
serviceWorker = await context.waitForEvent('serviceworker', {
|
|
34
|
+
predicate: (sw) => sw.url().startsWith('chrome-extension://'),
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
for (let i = 0; i < 50; i++) {
|
|
39
|
+
const isReady = await serviceWorker.evaluate(() => {
|
|
40
|
+
// @ts-ignore
|
|
41
|
+
return typeof globalThis.toggleExtensionForActiveTab === 'function'
|
|
42
|
+
})
|
|
43
|
+
if (isReady) break
|
|
44
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return serviceWorker
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function js(strings: TemplateStringsArray, ...values: any[]): string {
|
|
51
|
+
return strings.reduce((result, str, i) => result + str + (values[i] || ''), '')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function killProcessOnPort(port: number): Promise<void> {
|
|
55
|
+
try {
|
|
56
|
+
await killPortProcess(port)
|
|
57
|
+
console.log(`Killed processes on port ${port}`)
|
|
58
|
+
} catch (err) {
|
|
59
|
+
console.error('Error killing process on port:', err)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface TestContext {
|
|
64
|
+
browserContext: Awaited<ReturnType<typeof chromium.launchPersistentContext>>
|
|
65
|
+
userDataDir: string
|
|
66
|
+
relayServer: RelayServer
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function setupTestContext({ tempDirPrefix }: { tempDirPrefix: string }): Promise<TestContext> {
|
|
70
|
+
await killProcessOnPort(TEST_PORT)
|
|
71
|
+
|
|
72
|
+
console.log('Building extension...')
|
|
73
|
+
await execAsync(
|
|
74
|
+
`TESTING=1 E2E_PILOT_PORT=${TEST_PORT} E2E_PILOT_EXTENSION_OUT_DIR=${TEST_EXTENSION_OUT_DIR} pnpm build`,
|
|
75
|
+
{ cwd: '../extension' },
|
|
76
|
+
)
|
|
77
|
+
console.log('Extension built')
|
|
78
|
+
|
|
79
|
+
const localLogPath = path.join(process.cwd(), 'relay-server.log')
|
|
80
|
+
const logger = createFileLogger({ logFilePath: localLogPath })
|
|
81
|
+
const relayServer = await startE2EPilotCDPRelayServer({ port: TEST_PORT, logger })
|
|
82
|
+
|
|
83
|
+
const userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), tempDirPrefix))
|
|
84
|
+
const extensionPath = path.resolve('../extension', TEST_EXTENSION_OUT_DIR)
|
|
85
|
+
|
|
86
|
+
const browserContext = await chromium.launchPersistentContext(userDataDir, {
|
|
87
|
+
channel: 'chromium',
|
|
88
|
+
headless: !process.env.HEADFUL,
|
|
89
|
+
colorScheme: 'dark',
|
|
90
|
+
args: [`--disable-extensions-except=${extensionPath}`, `--load-extension=${extensionPath}`],
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
94
|
+
|
|
95
|
+
const page = await browserContext.newPage()
|
|
96
|
+
await page.goto('about:blank')
|
|
97
|
+
|
|
98
|
+
await serviceWorker.evaluate(async () => {
|
|
99
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
return { browserContext, userDataDir, relayServer }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function cleanupTestContext(ctx: TestContext | null, cleanup?: (() => Promise<void>) | null): Promise<void> {
|
|
106
|
+
if (ctx?.browserContext) {
|
|
107
|
+
await ctx.browserContext.close()
|
|
108
|
+
}
|
|
109
|
+
if (ctx?.relayServer) {
|
|
110
|
+
ctx.relayServer.close()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (ctx?.userDataDir) {
|
|
114
|
+
try {
|
|
115
|
+
fs.rmSync(ctx.userDataDir, { recursive: true, force: true })
|
|
116
|
+
} catch (e) {
|
|
117
|
+
console.error('Failed to cleanup user data dir:', e)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (cleanup) {
|
|
121
|
+
await cleanup()
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
declare global {
|
|
126
|
+
var toggleExtensionForActiveTab: () => Promise<{ isConnected: boolean; state: ExtensionState }>
|
|
127
|
+
var getExtensionState: () => ExtensionState
|
|
128
|
+
var disconnectEverything: () => Promise<void>
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
describe('MCP Server Tests', () => {
|
|
132
|
+
let client: Awaited<ReturnType<typeof createMCPClient>>['client']
|
|
133
|
+
let cleanup: (() => Promise<void>) | null = null
|
|
134
|
+
let testCtx: TestContext | null = null
|
|
135
|
+
|
|
136
|
+
beforeAll(async () => {
|
|
137
|
+
testCtx = await setupTestContext({ tempDirPrefix: 'pw-test-' })
|
|
138
|
+
|
|
139
|
+
const result = await createMCPClient({ port: TEST_PORT })
|
|
140
|
+
client = result.client
|
|
141
|
+
cleanup = result.cleanup
|
|
142
|
+
}, 600000)
|
|
143
|
+
|
|
144
|
+
afterAll(async () => {
|
|
145
|
+
await cleanupTestContext(testCtx, cleanup)
|
|
146
|
+
cleanup = null
|
|
147
|
+
testCtx = null
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
const getBrowserContext = () => {
|
|
151
|
+
if (!testCtx?.browserContext) throw new Error('Browser not initialized')
|
|
152
|
+
return testCtx.browserContext
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
it('should inject script via addScriptTag through CDP relay', async () => {
|
|
156
|
+
const browserContext = getBrowserContext()
|
|
157
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
158
|
+
|
|
159
|
+
const page = await browserContext.newPage()
|
|
160
|
+
await page.setContent('<html><body><button id="btn">Click</button></body></html>')
|
|
161
|
+
await page.bringToFront()
|
|
162
|
+
|
|
163
|
+
await serviceWorker.evaluate(async () => {
|
|
164
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
165
|
+
})
|
|
166
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
167
|
+
|
|
168
|
+
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
169
|
+
const cdpPage = browser
|
|
170
|
+
.contexts()[0]
|
|
171
|
+
.pages()
|
|
172
|
+
.find((p) => {
|
|
173
|
+
return p.url().startsWith('about:')
|
|
174
|
+
})
|
|
175
|
+
expect(cdpPage).toBeDefined()
|
|
176
|
+
|
|
177
|
+
const hasGlobalBefore = await cdpPage!.evaluate(() => !!(globalThis as any).__testGlobal)
|
|
178
|
+
expect(hasGlobalBefore).toBe(false)
|
|
179
|
+
|
|
180
|
+
await cdpPage!.addScriptTag({ content: 'globalThis.__testGlobal = { foo: "bar" };' })
|
|
181
|
+
|
|
182
|
+
const hasGlobalAfter = await cdpPage!.evaluate(() => (globalThis as any).__testGlobal)
|
|
183
|
+
expect(hasGlobalAfter).toEqual({ foo: 'bar' })
|
|
184
|
+
|
|
185
|
+
await browser.close()
|
|
186
|
+
await page.close()
|
|
187
|
+
}, 60000)
|
|
188
|
+
|
|
189
|
+
it('should execute code and capture console output', async () => {
|
|
190
|
+
await client.callTool({
|
|
191
|
+
name: 'execute',
|
|
192
|
+
arguments: {
|
|
193
|
+
code: js`
|
|
194
|
+
const newPage = await context.newPage();
|
|
195
|
+
state.page = newPage;
|
|
196
|
+
if (!state.pages) state.pages = [];
|
|
197
|
+
state.pages.push(newPage);
|
|
198
|
+
`,
|
|
199
|
+
},
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
const result = await client.callTool({
|
|
203
|
+
name: 'execute',
|
|
204
|
+
arguments: {
|
|
205
|
+
code: js`
|
|
206
|
+
await state.page.goto('https://example.com');
|
|
207
|
+
const title = await state.page.title();
|
|
208
|
+
console.log('Page title:', title);
|
|
209
|
+
return { url: state.page.url(), title };
|
|
210
|
+
`,
|
|
211
|
+
},
|
|
212
|
+
})
|
|
213
|
+
expect(result.content).toMatchInlineSnapshot(`
|
|
214
|
+
[
|
|
215
|
+
{
|
|
216
|
+
"text": "MCP error -32602: Input validation error: Invalid arguments for tool execute: [
|
|
217
|
+
{
|
|
218
|
+
"code": "invalid_type",
|
|
219
|
+
"expected": "string",
|
|
220
|
+
"received": "undefined",
|
|
221
|
+
"path": [
|
|
222
|
+
"intend"
|
|
223
|
+
],
|
|
224
|
+
"message": "Required"
|
|
225
|
+
}
|
|
226
|
+
]",
|
|
227
|
+
"type": "text",
|
|
228
|
+
},
|
|
229
|
+
]
|
|
230
|
+
`)
|
|
231
|
+
expect(result.content).toBeDefined()
|
|
232
|
+
}, 30000)
|
|
233
|
+
|
|
234
|
+
it('should show extension as connected for pages created via newPage()', async () => {
|
|
235
|
+
const browserContext = getBrowserContext()
|
|
236
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
237
|
+
|
|
238
|
+
// Create a page via MCP (which uses context.newPage())
|
|
239
|
+
await client.callTool({
|
|
240
|
+
name: 'execute',
|
|
241
|
+
arguments: {
|
|
242
|
+
code: js`
|
|
243
|
+
const newPage = await context.newPage();
|
|
244
|
+
state.testPage = newPage;
|
|
245
|
+
await newPage.goto('https://example.com/mcp-test');
|
|
246
|
+
return newPage.url();
|
|
247
|
+
`,
|
|
248
|
+
},
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
// Get extension state to verify the page is marked as connected
|
|
252
|
+
const extensionState = await serviceWorker.evaluate(async () => {
|
|
253
|
+
const state = globalThis.getExtensionState()
|
|
254
|
+
const tabs = await chrome.tabs.query({})
|
|
255
|
+
const testTab = tabs.find((t: any) => t.url?.includes('mcp-test'))
|
|
256
|
+
return {
|
|
257
|
+
connected: !!testTab && !!testTab.id && state.tabs.has(testTab.id),
|
|
258
|
+
tabId: testTab?.id,
|
|
259
|
+
tabInfo: testTab?.id ? state.tabs.get(testTab.id) : null,
|
|
260
|
+
connectionState: state.connectionState,
|
|
261
|
+
}
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
expect(extensionState.connected).toBe(true)
|
|
265
|
+
expect(extensionState.tabInfo?.state).toBe('connected')
|
|
266
|
+
expect(extensionState.connectionState).toBe('connected')
|
|
267
|
+
|
|
268
|
+
// Clean up
|
|
269
|
+
await client.callTool({
|
|
270
|
+
name: 'execute',
|
|
271
|
+
arguments: {
|
|
272
|
+
code: js`
|
|
273
|
+
if (state.testPage) {
|
|
274
|
+
await state.testPage.close();
|
|
275
|
+
delete state.testPage;
|
|
276
|
+
}
|
|
277
|
+
`,
|
|
278
|
+
},
|
|
279
|
+
})
|
|
280
|
+
}, 30000)
|
|
281
|
+
|
|
282
|
+
it('should get accessibility snapshot of hacker news', async () => {
|
|
283
|
+
await client.callTool({
|
|
284
|
+
name: 'execute',
|
|
285
|
+
arguments: {
|
|
286
|
+
code: js`
|
|
287
|
+
const newPage = await context.newPage();
|
|
288
|
+
state.page = newPage;
|
|
289
|
+
if (!state.pages) state.pages = [];
|
|
290
|
+
state.pages.push(newPage);
|
|
291
|
+
`,
|
|
292
|
+
},
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
const result = await client.callTool({
|
|
296
|
+
name: 'execute',
|
|
297
|
+
arguments: {
|
|
298
|
+
code: js`
|
|
299
|
+
await state.page.goto('https://news.ycombinator.com/item?id=1', { waitUntil: 'domcontentloaded' });
|
|
300
|
+
const snapshot = await state.page._snapshotForAI();
|
|
301
|
+
return snapshot;
|
|
302
|
+
`,
|
|
303
|
+
},
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
const initialData =
|
|
307
|
+
typeof result === 'object' && result.content?.[0]?.text ? tryJsonParse(result.content[0].text) : result
|
|
308
|
+
await expect(initialData).toMatchFileSnapshot('snapshots/hacker-news-initial-accessibility.md')
|
|
309
|
+
expect(result.content).toBeDefined()
|
|
310
|
+
expect(initialData).toContain('table')
|
|
311
|
+
expect(initialData).toContain('Hacker News')
|
|
312
|
+
}, 30000)
|
|
313
|
+
|
|
314
|
+
it('should get accessibility snapshot of shadcn UI', async () => {
|
|
315
|
+
await client.callTool({
|
|
316
|
+
name: 'execute',
|
|
317
|
+
arguments: {
|
|
318
|
+
code: js`
|
|
319
|
+
const newPage = await context.newPage();
|
|
320
|
+
state.page = newPage;
|
|
321
|
+
if (!state.pages) state.pages = [];
|
|
322
|
+
state.pages.push(newPage);
|
|
323
|
+
`,
|
|
324
|
+
},
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
const snapshot = await client.callTool({
|
|
328
|
+
name: 'execute',
|
|
329
|
+
arguments: {
|
|
330
|
+
code: js`
|
|
331
|
+
await state.page.goto('https://ui.shadcn.com/', { waitUntil: 'domcontentloaded' });
|
|
332
|
+
const snapshot = await state.page._snapshotForAI();
|
|
333
|
+
return snapshot;
|
|
334
|
+
`,
|
|
335
|
+
},
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
const data =
|
|
339
|
+
typeof snapshot === 'object' && snapshot.content?.[0]?.text ? tryJsonParse(snapshot.content[0].text) : snapshot
|
|
340
|
+
await expect(data).toMatchFileSnapshot('snapshots/shadcn-ui-accessibility.md')
|
|
341
|
+
expect(snapshot.content).toBeDefined()
|
|
342
|
+
expect(data).toContain('shadcn')
|
|
343
|
+
}, 30000)
|
|
344
|
+
|
|
345
|
+
it('should close all created pages', async () => {
|
|
346
|
+
const result = await client.callTool({
|
|
347
|
+
name: 'execute',
|
|
348
|
+
arguments: {
|
|
349
|
+
code: js`
|
|
350
|
+
if (state.pages && state.pages.length > 0) {
|
|
351
|
+
for (const page of state.pages) {
|
|
352
|
+
await page.close();
|
|
353
|
+
}
|
|
354
|
+
const closedCount = state.pages.length;
|
|
355
|
+
state.pages = [];
|
|
356
|
+
return { closedCount };
|
|
357
|
+
}
|
|
358
|
+
return { closedCount: 0 };
|
|
359
|
+
`,
|
|
360
|
+
},
|
|
361
|
+
})
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
it('should handle new pages and toggling with new connections', async () => {
|
|
365
|
+
const browserContext = getBrowserContext()
|
|
366
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
367
|
+
|
|
368
|
+
// 1. Create a new page
|
|
369
|
+
const page = await browserContext.newPage()
|
|
370
|
+
const testUrl = 'https://example.com/'
|
|
371
|
+
await page.goto(testUrl)
|
|
372
|
+
|
|
373
|
+
await page.bringToFront()
|
|
374
|
+
|
|
375
|
+
// 2. Enable extension on this new tab
|
|
376
|
+
// Since it's a new page, extension is not connected yet
|
|
377
|
+
const result = await serviceWorker.evaluate(async () => {
|
|
378
|
+
return await globalThis.toggleExtensionForActiveTab()
|
|
379
|
+
})
|
|
380
|
+
expect(result.isConnected).toBe(true)
|
|
381
|
+
|
|
382
|
+
// 3. Verify we can connect via direct CDP and see the page
|
|
383
|
+
|
|
384
|
+
let directBrowser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
385
|
+
let contexts = directBrowser.contexts()
|
|
386
|
+
let pages = contexts[0].pages()
|
|
387
|
+
|
|
388
|
+
// Find our page
|
|
389
|
+
let foundPage = pages.find((p) => p.url() === testUrl)
|
|
390
|
+
expect(foundPage).toBeDefined()
|
|
391
|
+
expect(foundPage?.url()).toBe(testUrl)
|
|
392
|
+
|
|
393
|
+
// Verify execution works
|
|
394
|
+
const sum1 = await foundPage?.evaluate(() => 1 + 1)
|
|
395
|
+
expect(sum1).toBe(2)
|
|
396
|
+
|
|
397
|
+
await directBrowser.close()
|
|
398
|
+
|
|
399
|
+
// 4. Disable extension on this tab
|
|
400
|
+
const resultDisabled = await serviceWorker.evaluate(async () => {
|
|
401
|
+
return await globalThis.toggleExtensionForActiveTab()
|
|
402
|
+
})
|
|
403
|
+
expect(resultDisabled.isConnected).toBe(false)
|
|
404
|
+
|
|
405
|
+
// 5. Try to connect/use the page.
|
|
406
|
+
// connecting to relay will succeed, but listing pages should NOT show our page
|
|
407
|
+
|
|
408
|
+
// Connect to relay again
|
|
409
|
+
directBrowser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
410
|
+
contexts = directBrowser.contexts()
|
|
411
|
+
pages = contexts[0].pages()
|
|
412
|
+
|
|
413
|
+
foundPage = pages.find((p) => p.url() === testUrl)
|
|
414
|
+
expect(foundPage).toBeUndefined()
|
|
415
|
+
|
|
416
|
+
await directBrowser.close()
|
|
417
|
+
|
|
418
|
+
// 6. Re-enable extension
|
|
419
|
+
const resultEnabled = await serviceWorker.evaluate(async () => {
|
|
420
|
+
return await globalThis.toggleExtensionForActiveTab()
|
|
421
|
+
})
|
|
422
|
+
expect(resultEnabled.isConnected).toBe(true)
|
|
423
|
+
|
|
424
|
+
// 7. Verify page is back
|
|
425
|
+
|
|
426
|
+
directBrowser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
427
|
+
// Wait a bit for targets to populate
|
|
428
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
429
|
+
|
|
430
|
+
contexts = directBrowser.contexts()
|
|
431
|
+
// pages() might need a moment if target attached event comes in
|
|
432
|
+
if (contexts[0].pages().length === 0) {
|
|
433
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
434
|
+
}
|
|
435
|
+
pages = contexts[0].pages()
|
|
436
|
+
|
|
437
|
+
foundPage = pages.find((p) => p.url() === testUrl)
|
|
438
|
+
expect(foundPage).toBeDefined()
|
|
439
|
+
expect(foundPage?.url()).toBe(testUrl)
|
|
440
|
+
|
|
441
|
+
// Verify execution works again
|
|
442
|
+
const sum2 = await foundPage?.evaluate(() => 2 + 2)
|
|
443
|
+
expect(sum2).toBe(4)
|
|
444
|
+
|
|
445
|
+
await directBrowser.close()
|
|
446
|
+
await page.close()
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
it('should handle new pages and toggling with persistent connection', async () => {
|
|
450
|
+
const browserContext = getBrowserContext()
|
|
451
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
452
|
+
|
|
453
|
+
// Connect once
|
|
454
|
+
const directBrowser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
455
|
+
// Wait a bit for connection and initial target discovery
|
|
456
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
457
|
+
|
|
458
|
+
// 1. Create a new page
|
|
459
|
+
const page = await browserContext.newPage()
|
|
460
|
+
const testUrl = 'https://example.com/persistent'
|
|
461
|
+
await page.goto(testUrl)
|
|
462
|
+
await page.bringToFront()
|
|
463
|
+
|
|
464
|
+
// 2. Enable extension
|
|
465
|
+
await serviceWorker.evaluate(async () => {
|
|
466
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
// 3. Verify page appears (polling)
|
|
470
|
+
let foundPage
|
|
471
|
+
for (let i = 0; i < 50; i++) {
|
|
472
|
+
const pages = directBrowser.contexts()[0].pages()
|
|
473
|
+
foundPage = pages.find((p) => p.url() === testUrl)
|
|
474
|
+
if (foundPage) break
|
|
475
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
476
|
+
}
|
|
477
|
+
expect(foundPage).toBeDefined()
|
|
478
|
+
expect(foundPage?.url()).toBe(testUrl)
|
|
479
|
+
|
|
480
|
+
// Verify execution works
|
|
481
|
+
const sum1 = await foundPage?.evaluate(() => 10 + 20)
|
|
482
|
+
expect(sum1).toBe(30)
|
|
483
|
+
|
|
484
|
+
// 4. Disable extension
|
|
485
|
+
await serviceWorker.evaluate(async () => {
|
|
486
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
// 5. Verify page disappears (polling)
|
|
490
|
+
for (let i = 0; i < 50; i++) {
|
|
491
|
+
const pages = directBrowser.contexts()[0].pages()
|
|
492
|
+
foundPage = pages.find((p) => p.url() === testUrl)
|
|
493
|
+
if (!foundPage) break
|
|
494
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
495
|
+
}
|
|
496
|
+
expect(foundPage).toBeUndefined()
|
|
497
|
+
|
|
498
|
+
// 6. Re-enable extension
|
|
499
|
+
await serviceWorker.evaluate(async () => {
|
|
500
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
501
|
+
})
|
|
502
|
+
|
|
503
|
+
// 7. Verify page reappears (polling)
|
|
504
|
+
for (let i = 0; i < 50; i++) {
|
|
505
|
+
const pages = directBrowser.contexts()[0].pages()
|
|
506
|
+
foundPage = pages.find((p) => p.url() === testUrl)
|
|
507
|
+
if (foundPage) break
|
|
508
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
509
|
+
}
|
|
510
|
+
expect(foundPage).toBeDefined()
|
|
511
|
+
expect(foundPage?.url()).toBe(testUrl)
|
|
512
|
+
|
|
513
|
+
// Verify execution works again
|
|
514
|
+
const sum2 = await foundPage?.evaluate(() => 30 + 40)
|
|
515
|
+
expect(sum2).toBe(70)
|
|
516
|
+
|
|
517
|
+
await page.close()
|
|
518
|
+
await directBrowser.close()
|
|
519
|
+
})
|
|
520
|
+
it('should maintain connection across reloads and navigation', async () => {
|
|
521
|
+
const browserContext = getBrowserContext()
|
|
522
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
523
|
+
|
|
524
|
+
// 1. Setup page
|
|
525
|
+
const page = await browserContext.newPage()
|
|
526
|
+
const initialUrl = 'https://example.com/'
|
|
527
|
+
await page.goto(initialUrl)
|
|
528
|
+
await page.bringToFront()
|
|
529
|
+
|
|
530
|
+
// 2. Enable extension
|
|
531
|
+
await serviceWorker.evaluate(async () => {
|
|
532
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
533
|
+
})
|
|
534
|
+
|
|
535
|
+
// 3. Connect via CDP
|
|
536
|
+
const cdpUrl = getCdpUrl({ port: TEST_PORT })
|
|
537
|
+
const directBrowser = await chromium.connectOverCDP(cdpUrl)
|
|
538
|
+
const connectedPage = directBrowser
|
|
539
|
+
.contexts()[0]
|
|
540
|
+
.pages()
|
|
541
|
+
.find((p) => p.url() === initialUrl)
|
|
542
|
+
expect(connectedPage).toBeDefined()
|
|
543
|
+
|
|
544
|
+
// Verify execution
|
|
545
|
+
expect(await connectedPage?.evaluate(() => 1 + 1)).toBe(2)
|
|
546
|
+
|
|
547
|
+
// 4. Reload
|
|
548
|
+
// We use a loop to check if it's still connected because reload might cause temporary disconnect/reconnect events
|
|
549
|
+
// that Playwright handles natively if the session ID stays valid.
|
|
550
|
+
await connectedPage?.reload()
|
|
551
|
+
await connectedPage?.waitForLoadState('domcontentloaded')
|
|
552
|
+
expect(await connectedPage?.title()).toBe('Example Domain')
|
|
553
|
+
|
|
554
|
+
// Verify execution after reload
|
|
555
|
+
expect(await connectedPage?.evaluate(() => 2 + 2)).toBe(4)
|
|
556
|
+
|
|
557
|
+
// 5. Navigate to new URL
|
|
558
|
+
const newUrl = 'https://example.org/'
|
|
559
|
+
await connectedPage?.goto(newUrl)
|
|
560
|
+
await connectedPage?.waitForLoadState('domcontentloaded')
|
|
561
|
+
|
|
562
|
+
expect(connectedPage?.url()).toBe(newUrl)
|
|
563
|
+
expect(await connectedPage?.title()).toContain('Example Domain')
|
|
564
|
+
|
|
565
|
+
// Verify execution after navigation
|
|
566
|
+
expect(await connectedPage?.evaluate(() => 3 + 3)).toBe(6)
|
|
567
|
+
|
|
568
|
+
await directBrowser.close()
|
|
569
|
+
await page.close()
|
|
570
|
+
})
|
|
571
|
+
|
|
572
|
+
it('should support multiple concurrent tabs', async () => {
|
|
573
|
+
const browserContext = getBrowserContext()
|
|
574
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
575
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
576
|
+
|
|
577
|
+
// Tab A
|
|
578
|
+
const pageA = await browserContext.newPage()
|
|
579
|
+
await pageA.goto('https://example.com/tab-a')
|
|
580
|
+
await pageA.bringToFront()
|
|
581
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
582
|
+
await serviceWorker.evaluate(async () => {
|
|
583
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
584
|
+
})
|
|
585
|
+
|
|
586
|
+
// Tab B
|
|
587
|
+
const pageB = await browserContext.newPage()
|
|
588
|
+
await pageB.goto('https://example.com/tab-b')
|
|
589
|
+
await pageB.bringToFront()
|
|
590
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
591
|
+
await serviceWorker.evaluate(async () => {
|
|
592
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
593
|
+
})
|
|
594
|
+
|
|
595
|
+
// Get target IDs for both
|
|
596
|
+
const targetIds = await serviceWorker.evaluate(async () => {
|
|
597
|
+
const state = globalThis.getExtensionState()
|
|
598
|
+
const chrome = globalThis.chrome
|
|
599
|
+
const tabs = await chrome.tabs.query({})
|
|
600
|
+
const tabA = tabs.find((t: any) => t.url?.includes('tab-a'))
|
|
601
|
+
const tabB = tabs.find((t: any) => t.url?.includes('tab-b'))
|
|
602
|
+
return {
|
|
603
|
+
idA: state.tabs.get(tabA?.id ?? -1)?.targetId,
|
|
604
|
+
idB: state.tabs.get(tabB?.id ?? -1)?.targetId,
|
|
605
|
+
}
|
|
606
|
+
})
|
|
607
|
+
|
|
608
|
+
expect(targetIds).toMatchInlineSnapshot(
|
|
609
|
+
{
|
|
610
|
+
idA: expect.any(String),
|
|
611
|
+
idB: expect.any(String),
|
|
612
|
+
},
|
|
613
|
+
`
|
|
614
|
+
{
|
|
615
|
+
"idA": Any<String>,
|
|
616
|
+
"idB": Any<String>,
|
|
617
|
+
}
|
|
618
|
+
`,
|
|
619
|
+
)
|
|
620
|
+
expect(targetIds.idA).not.toBe(targetIds.idB)
|
|
621
|
+
|
|
622
|
+
// Verify independent connections
|
|
623
|
+
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
624
|
+
|
|
625
|
+
const pages = browser.contexts()[0].pages()
|
|
626
|
+
|
|
627
|
+
const results = await Promise.all(
|
|
628
|
+
pages.map(async (p) => ({
|
|
629
|
+
url: p.url(),
|
|
630
|
+
title: await p.title(),
|
|
631
|
+
})),
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
expect(results).toMatchInlineSnapshot(`
|
|
635
|
+
[
|
|
636
|
+
{
|
|
637
|
+
"title": "",
|
|
638
|
+
"url": "about:blank",
|
|
639
|
+
},
|
|
640
|
+
{
|
|
641
|
+
"title": "Example Domain",
|
|
642
|
+
"url": "https://example.com/tab-a",
|
|
643
|
+
},
|
|
644
|
+
{
|
|
645
|
+
"title": "Example Domain",
|
|
646
|
+
"url": "https://example.com/tab-b",
|
|
647
|
+
},
|
|
648
|
+
]
|
|
649
|
+
`)
|
|
650
|
+
|
|
651
|
+
// Verify execution on both pages
|
|
652
|
+
const pageA_CDP = pages.find((p) => p.url().includes('tab-a'))
|
|
653
|
+
const pageB_CDP = pages.find((p) => p.url().includes('tab-b'))
|
|
654
|
+
|
|
655
|
+
expect(await pageA_CDP?.evaluate(() => 10 + 10)).toBe(20)
|
|
656
|
+
expect(await pageB_CDP?.evaluate(() => 20 + 20)).toBe(40)
|
|
657
|
+
|
|
658
|
+
await browser.close()
|
|
659
|
+
await pageA.close()
|
|
660
|
+
await pageB.close()
|
|
661
|
+
})
|
|
662
|
+
|
|
663
|
+
it('should show correct url when enabling extension after navigation', async () => {
|
|
664
|
+
const browserContext = getBrowserContext()
|
|
665
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
666
|
+
|
|
667
|
+
const page = await browserContext.newPage()
|
|
668
|
+
const targetUrl = 'https://example.com/late-enable'
|
|
669
|
+
await page.goto(targetUrl)
|
|
670
|
+
await page.bringToFront()
|
|
671
|
+
|
|
672
|
+
// Wait for load
|
|
673
|
+
await page.waitForLoadState('domcontentloaded')
|
|
674
|
+
|
|
675
|
+
// 2. Enable extension for this page
|
|
676
|
+
await serviceWorker.evaluate(async () => {
|
|
677
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
678
|
+
})
|
|
679
|
+
|
|
680
|
+
// 3. Verify via CDP that the correct URL is shown
|
|
681
|
+
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
682
|
+
// Wait for sync
|
|
683
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
684
|
+
|
|
685
|
+
const cdpPage = browser
|
|
686
|
+
.contexts()[0]
|
|
687
|
+
.pages()
|
|
688
|
+
.find((p) => p.url() === targetUrl)
|
|
689
|
+
|
|
690
|
+
expect(cdpPage).toBeDefined()
|
|
691
|
+
expect(cdpPage?.url()).toBe(targetUrl)
|
|
692
|
+
|
|
693
|
+
await browser.close()
|
|
694
|
+
await page.close()
|
|
695
|
+
}, 60000)
|
|
696
|
+
|
|
697
|
+
it('should be able to reconnect after disconnecting everything', async () => {
|
|
698
|
+
const browserContext = getBrowserContext()
|
|
699
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
700
|
+
|
|
701
|
+
const pages = await browserContext.pages()
|
|
702
|
+
expect(pages.length).toBeGreaterThan(0)
|
|
703
|
+
const page = pages[0]
|
|
704
|
+
|
|
705
|
+
await page.goto('https://example.com/disconnect-test')
|
|
706
|
+
await page.waitForLoadState('domcontentloaded')
|
|
707
|
+
await page.bringToFront()
|
|
708
|
+
|
|
709
|
+
// Enable extension on this page
|
|
710
|
+
const initialEnable = await serviceWorker.evaluate(async () => {
|
|
711
|
+
return await globalThis.toggleExtensionForActiveTab()
|
|
712
|
+
})
|
|
713
|
+
console.log('Initial enable result:', initialEnable)
|
|
714
|
+
expect(initialEnable.isConnected).toBe(true)
|
|
715
|
+
|
|
716
|
+
// Wait for extension to fully connect
|
|
717
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
718
|
+
|
|
719
|
+
// Verify MCP can see the page
|
|
720
|
+
const beforeDisconnect = await client.callTool({
|
|
721
|
+
name: 'execute',
|
|
722
|
+
arguments: {
|
|
723
|
+
code: js`
|
|
724
|
+
const pages = context.pages();
|
|
725
|
+
console.log('Pages before disconnect:', pages.length);
|
|
726
|
+
const testPage = pages.find(p => p.url().includes('disconnect-test'));
|
|
727
|
+
console.log('Found test page:', !!testPage);
|
|
728
|
+
return { pagesCount: pages.length, foundTestPage: !!testPage };
|
|
729
|
+
`,
|
|
730
|
+
},
|
|
731
|
+
})
|
|
732
|
+
|
|
733
|
+
const beforeOutput = (beforeDisconnect as any).content[0].text
|
|
734
|
+
expect(beforeOutput).toContain('foundTestPage')
|
|
735
|
+
console.log('Before disconnect:', beforeOutput)
|
|
736
|
+
|
|
737
|
+
// 2. Disconnect everything
|
|
738
|
+
console.log('Calling disconnectEverything...')
|
|
739
|
+
await serviceWorker.evaluate(async () => {
|
|
740
|
+
await globalThis.disconnectEverything()
|
|
741
|
+
})
|
|
742
|
+
|
|
743
|
+
// Wait for disconnect to complete
|
|
744
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
745
|
+
|
|
746
|
+
// 3. Verify MCP cannot see the page anymore
|
|
747
|
+
const afterDisconnect = await client.callTool({
|
|
748
|
+
name: 'execute',
|
|
749
|
+
arguments: {
|
|
750
|
+
code: js`
|
|
751
|
+
const pages = context.pages();
|
|
752
|
+
console.log('Pages after disconnect:', pages.length);
|
|
753
|
+
return { pagesCount: pages.length };
|
|
754
|
+
`,
|
|
755
|
+
},
|
|
756
|
+
})
|
|
757
|
+
|
|
758
|
+
const afterDisconnectOutput = (afterDisconnect as any).content[0].text
|
|
759
|
+
console.log('After disconnect:', afterDisconnectOutput)
|
|
760
|
+
expect(afterDisconnectOutput).toContain('Pages after disconnect: 0')
|
|
761
|
+
|
|
762
|
+
// 4. Re-enable extension on the same page
|
|
763
|
+
console.log('Re-enabling extension...')
|
|
764
|
+
await page.bringToFront()
|
|
765
|
+
const reconnectResult = await serviceWorker.evaluate(async () => {
|
|
766
|
+
console.log('About to call toggleExtensionForActiveTab')
|
|
767
|
+
const result = await globalThis.toggleExtensionForActiveTab()
|
|
768
|
+
console.log('toggleExtensionForActiveTab result:', result)
|
|
769
|
+
return result
|
|
770
|
+
})
|
|
771
|
+
|
|
772
|
+
console.log('Reconnect result:', reconnectResult)
|
|
773
|
+
expect(reconnectResult.isConnected).toBe(true)
|
|
774
|
+
|
|
775
|
+
// Wait for extension to fully reconnect and relay server to be ready
|
|
776
|
+
console.log('Waiting for reconnection to stabilize...')
|
|
777
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
778
|
+
|
|
779
|
+
// 5. Reset the MCP client's playwright connection since it was closed by disconnectEverything
|
|
780
|
+
console.log('Resetting MCP playwright connection...')
|
|
781
|
+
const resetResult = await client.callTool({
|
|
782
|
+
name: 'execute',
|
|
783
|
+
arguments: {
|
|
784
|
+
code: js`
|
|
785
|
+
console.log('Resetting playwright connection');
|
|
786
|
+
const result = await resetPlaywright();
|
|
787
|
+
console.log('Reset complete, checking pages');
|
|
788
|
+
const pages = context.pages();
|
|
789
|
+
console.log('Pages after reset:', pages.length);
|
|
790
|
+
return { reset: true, pagesCount: pages.length };
|
|
791
|
+
`,
|
|
792
|
+
},
|
|
793
|
+
})
|
|
794
|
+
console.log('Reset result:', (resetResult as any).content[0].text)
|
|
795
|
+
|
|
796
|
+
// 6. Verify MCP can see the page again
|
|
797
|
+
console.log('Attempting to access page via MCP...')
|
|
798
|
+
const afterReconnect = await client.callTool({
|
|
799
|
+
name: 'execute',
|
|
800
|
+
arguments: {
|
|
801
|
+
code: js`
|
|
802
|
+
console.log('Checking pages after reconnect...');
|
|
803
|
+
const pages = context.pages();
|
|
804
|
+
console.log('Pages after reconnect:', pages.length);
|
|
805
|
+
|
|
806
|
+
if (pages.length === 0) {
|
|
807
|
+
console.log('No pages found!');
|
|
808
|
+
return { pagesCount: 0, foundTestPage: false };
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
const testPage = pages.find(p => p.url().includes('disconnect-test'));
|
|
812
|
+
console.log('Found test page after reconnect:', !!testPage);
|
|
813
|
+
|
|
814
|
+
if (testPage) {
|
|
815
|
+
console.log('Test page URL:', testPage.url());
|
|
816
|
+
return { pagesCount: pages.length, foundTestPage: true, url: testPage.url() };
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
return { pagesCount: pages.length, foundTestPage: false };
|
|
820
|
+
`,
|
|
821
|
+
},
|
|
822
|
+
})
|
|
823
|
+
|
|
824
|
+
const afterReconnectOutput = (afterReconnect as any).content[0].text
|
|
825
|
+
console.log('After reconnect:', afterReconnectOutput)
|
|
826
|
+
expect(afterReconnectOutput).toContain('foundTestPage')
|
|
827
|
+
expect(afterReconnectOutput).toContain('disconnect-test')
|
|
828
|
+
|
|
829
|
+
// Clean up - navigate page back to about:blank to not interfere with other tests
|
|
830
|
+
await page.goto('about:blank')
|
|
831
|
+
})
|
|
832
|
+
|
|
833
|
+
it('should auto-reconnect MCP after extension WebSocket reconnects', async () => {
|
|
834
|
+
// This test verifies that the MCP automatically reconnects when the browser
|
|
835
|
+
// disconnects (e.g., when the extension WebSocket reconnects and the relay
|
|
836
|
+
// server closes all playwright clients). The fix adds browser.on('disconnected')
|
|
837
|
+
// handler that clears state.isConnected, so ensureConnection() creates a new connection.
|
|
838
|
+
|
|
839
|
+
const serviceWorker = await getExtensionServiceWorker(testCtx!.browserContext)
|
|
840
|
+
|
|
841
|
+
// 1. Create a test page and enable extension
|
|
842
|
+
const page = await testCtx!.browserContext.newPage()
|
|
843
|
+
await page.goto('https://example.com/auto-reconnect-test')
|
|
844
|
+
await page.waitForLoadState('domcontentloaded')
|
|
845
|
+
await page.bringToFront()
|
|
846
|
+
|
|
847
|
+
const initialEnable = await serviceWorker.evaluate(async () => {
|
|
848
|
+
return await globalThis.toggleExtensionForActiveTab()
|
|
849
|
+
})
|
|
850
|
+
expect(initialEnable.isConnected).toBe(true)
|
|
851
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
852
|
+
|
|
853
|
+
// 2. Verify MCP can execute commands
|
|
854
|
+
const beforeResult = await client.callTool({
|
|
855
|
+
name: 'execute',
|
|
856
|
+
arguments: {
|
|
857
|
+
code: js`
|
|
858
|
+
const pages = context.pages();
|
|
859
|
+
const testPage = pages.find(p => p.url().includes('auto-reconnect-test'));
|
|
860
|
+
return { pagesCount: pages.length, foundTestPage: !!testPage };
|
|
861
|
+
`,
|
|
862
|
+
},
|
|
863
|
+
})
|
|
864
|
+
const beforeOutput = (beforeResult as any).content[0].text
|
|
865
|
+
expect(beforeOutput).toContain('foundTestPage')
|
|
866
|
+
expect(beforeOutput).toContain('true')
|
|
867
|
+
|
|
868
|
+
// 3. Simulate extension WebSocket reconnection
|
|
869
|
+
// This causes relay server to close all playwright client WebSockets
|
|
870
|
+
await serviceWorker.evaluate(async () => {
|
|
871
|
+
await globalThis.disconnectEverything()
|
|
872
|
+
})
|
|
873
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
874
|
+
|
|
875
|
+
// Re-enable extension (simulates extension reconnecting)
|
|
876
|
+
await page.bringToFront()
|
|
877
|
+
const reconnectResult = await serviceWorker.evaluate(async () => {
|
|
878
|
+
return await globalThis.toggleExtensionForActiveTab()
|
|
879
|
+
})
|
|
880
|
+
expect(reconnectResult.isConnected).toBe(true)
|
|
881
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
882
|
+
|
|
883
|
+
// 4. Execute command WITHOUT calling resetPlaywright()
|
|
884
|
+
// The browser.on('disconnected') handler should have cleared state.isConnected,
|
|
885
|
+
// causing ensureConnection() to automatically create a new connection
|
|
886
|
+
const afterResult = await client.callTool({
|
|
887
|
+
name: 'execute',
|
|
888
|
+
arguments: {
|
|
889
|
+
code: js`
|
|
890
|
+
const pages = context.pages();
|
|
891
|
+
const testPage = pages.find(p => p.url().includes('auto-reconnect-test'));
|
|
892
|
+
return { pagesCount: pages.length, foundTestPage: !!testPage, url: testPage?.url() };
|
|
893
|
+
`,
|
|
894
|
+
},
|
|
895
|
+
})
|
|
896
|
+
|
|
897
|
+
const afterOutput = (afterResult as any).content[0].text
|
|
898
|
+
// The command should succeed and find our test page
|
|
899
|
+
expect(afterOutput).toContain('foundTestPage')
|
|
900
|
+
expect(afterOutput).toContain('true')
|
|
901
|
+
expect(afterOutput).toContain('auto-reconnect-test')
|
|
902
|
+
// Should NOT contain error about extension not connected
|
|
903
|
+
expect(afterOutput).not.toContain('Extension not connected')
|
|
904
|
+
expect((afterResult as any).isError).not.toBe(true)
|
|
905
|
+
|
|
906
|
+
// Clean up
|
|
907
|
+
await page.goto('about:blank')
|
|
908
|
+
})
|
|
909
|
+
|
|
910
|
+
it('should capture browser console logs with getLatestLogs', async () => {
|
|
911
|
+
// Ensure clean state and clear any existing logs
|
|
912
|
+
const resetResult = await client.callTool({
|
|
913
|
+
name: 'execute',
|
|
914
|
+
arguments: {
|
|
915
|
+
code: js`
|
|
916
|
+
// Clear any existing logs from previous tests
|
|
917
|
+
clearAllLogs();
|
|
918
|
+
console.log('Cleared all existing logs');
|
|
919
|
+
|
|
920
|
+
// Verify connection is working
|
|
921
|
+
const pages = context.pages();
|
|
922
|
+
console.log('Current pages count:', pages.length);
|
|
923
|
+
|
|
924
|
+
return { success: true, pagesCount: pages.length };
|
|
925
|
+
`,
|
|
926
|
+
},
|
|
927
|
+
})
|
|
928
|
+
console.log('Cleanup result:', resetResult)
|
|
929
|
+
|
|
930
|
+
// Create a new page for this test
|
|
931
|
+
await client.callTool({
|
|
932
|
+
name: 'execute',
|
|
933
|
+
arguments: {
|
|
934
|
+
code: js`
|
|
935
|
+
const newPage = await context.newPage();
|
|
936
|
+
state.testLogPage = newPage;
|
|
937
|
+
await newPage.goto('about:blank');
|
|
938
|
+
`,
|
|
939
|
+
},
|
|
940
|
+
})
|
|
941
|
+
|
|
942
|
+
// Generate some console logs in the browser
|
|
943
|
+
await client.callTool({
|
|
944
|
+
name: 'execute',
|
|
945
|
+
arguments: {
|
|
946
|
+
code: js`
|
|
947
|
+
await state.testLogPage.evaluate(() => {
|
|
948
|
+
console.log('Test log 12345');
|
|
949
|
+
console.error('Test error 67890');
|
|
950
|
+
console.warn('Test warning 11111');
|
|
951
|
+
console.log('Test log 2 with', { data: 'object' });
|
|
952
|
+
});
|
|
953
|
+
// Wait for logs to be captured
|
|
954
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
955
|
+
`,
|
|
956
|
+
},
|
|
957
|
+
})
|
|
958
|
+
|
|
959
|
+
// Test getting all logs
|
|
960
|
+
const allLogsResult = await client.callTool({
|
|
961
|
+
name: 'execute',
|
|
962
|
+
arguments: {
|
|
963
|
+
code: js`
|
|
964
|
+
const logs = await getLatestLogs();
|
|
965
|
+
logs.forEach(log => console.log(log));
|
|
966
|
+
`,
|
|
967
|
+
},
|
|
968
|
+
})
|
|
969
|
+
|
|
970
|
+
const output = (allLogsResult as any).content[0].text
|
|
971
|
+
expect(output).toContain('[log] Test log 12345')
|
|
972
|
+
expect(output).toContain('[error] Test error 67890')
|
|
973
|
+
expect(output).toContain('[warning] Test warning 11111')
|
|
974
|
+
|
|
975
|
+
// Test filtering by search string
|
|
976
|
+
const errorLogsResult = await client.callTool({
|
|
977
|
+
name: 'execute',
|
|
978
|
+
arguments: {
|
|
979
|
+
code: js`
|
|
980
|
+
const logs = await getLatestLogs({ search: 'error' });
|
|
981
|
+
logs.forEach(log => console.log(log));
|
|
982
|
+
`,
|
|
983
|
+
},
|
|
984
|
+
})
|
|
985
|
+
|
|
986
|
+
const errorOutput = (errorLogsResult as any).content[0].text
|
|
987
|
+
expect(errorOutput).toContain('[error] Test error 67890')
|
|
988
|
+
expect(errorOutput).not.toContain('[log] Test log 12345')
|
|
989
|
+
|
|
990
|
+
// Test that logs are cleared on page reload
|
|
991
|
+
await client.callTool({
|
|
992
|
+
name: 'execute',
|
|
993
|
+
arguments: {
|
|
994
|
+
code: js`
|
|
995
|
+
// First add a log before reload
|
|
996
|
+
await state.testLogPage.evaluate(() => {
|
|
997
|
+
console.log('Before reload 99999');
|
|
998
|
+
});
|
|
999
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
1000
|
+
`,
|
|
1001
|
+
},
|
|
1002
|
+
})
|
|
1003
|
+
|
|
1004
|
+
// Verify the log exists
|
|
1005
|
+
const beforeReloadResult = await client.callTool({
|
|
1006
|
+
name: 'execute',
|
|
1007
|
+
arguments: {
|
|
1008
|
+
code: js`
|
|
1009
|
+
const logs = await getLatestLogs({ page: state.testLogPage });
|
|
1010
|
+
console.log('Logs before reload:', logs.length);
|
|
1011
|
+
logs.forEach(log => console.log(log));
|
|
1012
|
+
`,
|
|
1013
|
+
},
|
|
1014
|
+
})
|
|
1015
|
+
|
|
1016
|
+
const beforeReloadOutput = (beforeReloadResult as any).content[0].text
|
|
1017
|
+
expect(beforeReloadOutput).toContain('[log] Before reload 99999')
|
|
1018
|
+
|
|
1019
|
+
// Reload the page
|
|
1020
|
+
await client.callTool({
|
|
1021
|
+
name: 'execute',
|
|
1022
|
+
arguments: {
|
|
1023
|
+
code: js`
|
|
1024
|
+
await state.testLogPage.reload();
|
|
1025
|
+
await state.testLogPage.evaluate(() => {
|
|
1026
|
+
console.log('After reload 88888');
|
|
1027
|
+
});
|
|
1028
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
1029
|
+
`,
|
|
1030
|
+
},
|
|
1031
|
+
})
|
|
1032
|
+
|
|
1033
|
+
// Check logs after reload - old logs should be gone
|
|
1034
|
+
const afterReloadResult = await client.callTool({
|
|
1035
|
+
name: 'execute',
|
|
1036
|
+
arguments: {
|
|
1037
|
+
code: js`
|
|
1038
|
+
const logs = await getLatestLogs({ page: state.testLogPage });
|
|
1039
|
+
console.log('Logs after reload:', logs.length);
|
|
1040
|
+
logs.forEach(log => console.log(log));
|
|
1041
|
+
`,
|
|
1042
|
+
},
|
|
1043
|
+
})
|
|
1044
|
+
|
|
1045
|
+
const afterReloadOutput = (afterReloadResult as any).content[0].text
|
|
1046
|
+
expect(afterReloadOutput).toContain('[log] After reload 88888')
|
|
1047
|
+
expect(afterReloadOutput).not.toContain('[log] Before reload 99999')
|
|
1048
|
+
|
|
1049
|
+
// Clean up
|
|
1050
|
+
await client.callTool({
|
|
1051
|
+
name: 'execute',
|
|
1052
|
+
arguments: {
|
|
1053
|
+
code: js`
|
|
1054
|
+
await state.testLogPage.close();
|
|
1055
|
+
delete state.testLogPage;
|
|
1056
|
+
`,
|
|
1057
|
+
},
|
|
1058
|
+
})
|
|
1059
|
+
}, 30000)
|
|
1060
|
+
|
|
1061
|
+
it('should keep logs separate between different pages', async () => {
|
|
1062
|
+
// Clear any existing logs from previous tests
|
|
1063
|
+
await client.callTool({
|
|
1064
|
+
name: 'execute',
|
|
1065
|
+
arguments: {
|
|
1066
|
+
code: js`
|
|
1067
|
+
clearAllLogs();
|
|
1068
|
+
console.log('Cleared all existing logs for second log test');
|
|
1069
|
+
`,
|
|
1070
|
+
},
|
|
1071
|
+
})
|
|
1072
|
+
|
|
1073
|
+
// Create two pages
|
|
1074
|
+
await client.callTool({
|
|
1075
|
+
name: 'execute',
|
|
1076
|
+
arguments: {
|
|
1077
|
+
code: js`
|
|
1078
|
+
state.pageA = await context.newPage();
|
|
1079
|
+
state.pageB = await context.newPage();
|
|
1080
|
+
await state.pageA.goto('about:blank');
|
|
1081
|
+
await state.pageB.goto('about:blank');
|
|
1082
|
+
`,
|
|
1083
|
+
},
|
|
1084
|
+
})
|
|
1085
|
+
|
|
1086
|
+
// Generate logs in page A
|
|
1087
|
+
await client.callTool({
|
|
1088
|
+
name: 'execute',
|
|
1089
|
+
arguments: {
|
|
1090
|
+
code: js`
|
|
1091
|
+
await state.pageA.evaluate(() => {
|
|
1092
|
+
console.log('PageA log 11111');
|
|
1093
|
+
console.error('PageA error 22222');
|
|
1094
|
+
});
|
|
1095
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
1096
|
+
`,
|
|
1097
|
+
},
|
|
1098
|
+
})
|
|
1099
|
+
|
|
1100
|
+
// Generate logs in page B
|
|
1101
|
+
await client.callTool({
|
|
1102
|
+
name: 'execute',
|
|
1103
|
+
arguments: {
|
|
1104
|
+
code: js`
|
|
1105
|
+
await state.pageB.evaluate(() => {
|
|
1106
|
+
console.log('PageB log 33333');
|
|
1107
|
+
console.error('PageB error 44444');
|
|
1108
|
+
});
|
|
1109
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
1110
|
+
`,
|
|
1111
|
+
},
|
|
1112
|
+
})
|
|
1113
|
+
|
|
1114
|
+
// Check logs for page A - should only have page A logs
|
|
1115
|
+
const pageALogsResult = await client.callTool({
|
|
1116
|
+
name: 'execute',
|
|
1117
|
+
arguments: {
|
|
1118
|
+
code: js`
|
|
1119
|
+
const logs = await getLatestLogs({ page: state.pageA });
|
|
1120
|
+
console.log('Page A logs:', logs.length);
|
|
1121
|
+
logs.forEach(log => console.log(log));
|
|
1122
|
+
`,
|
|
1123
|
+
},
|
|
1124
|
+
})
|
|
1125
|
+
|
|
1126
|
+
const pageAOutput = (pageALogsResult as any).content[0].text
|
|
1127
|
+
expect(pageAOutput).toContain('[log] PageA log 11111')
|
|
1128
|
+
expect(pageAOutput).toContain('[error] PageA error 22222')
|
|
1129
|
+
expect(pageAOutput).not.toContain('PageB')
|
|
1130
|
+
|
|
1131
|
+
// Check logs for page B - should only have page B logs
|
|
1132
|
+
const pageBLogsResult = await client.callTool({
|
|
1133
|
+
name: 'execute',
|
|
1134
|
+
arguments: {
|
|
1135
|
+
code: js`
|
|
1136
|
+
const logs = await getLatestLogs({ page: state.pageB });
|
|
1137
|
+
console.log('Page B logs:', logs.length);
|
|
1138
|
+
logs.forEach(log => console.log(log));
|
|
1139
|
+
`,
|
|
1140
|
+
},
|
|
1141
|
+
})
|
|
1142
|
+
|
|
1143
|
+
const pageBOutput = (pageBLogsResult as any).content[0].text
|
|
1144
|
+
expect(pageBOutput).toContain('[log] PageB log 33333')
|
|
1145
|
+
expect(pageBOutput).toContain('[error] PageB error 44444')
|
|
1146
|
+
expect(pageBOutput).not.toContain('PageA')
|
|
1147
|
+
|
|
1148
|
+
// Check all logs - should have logs from both pages
|
|
1149
|
+
const allLogsResult = await client.callTool({
|
|
1150
|
+
name: 'execute',
|
|
1151
|
+
arguments: {
|
|
1152
|
+
code: js`
|
|
1153
|
+
const logs = await getLatestLogs();
|
|
1154
|
+
console.log('All logs:', logs.length);
|
|
1155
|
+
logs.forEach(log => console.log(log));
|
|
1156
|
+
`,
|
|
1157
|
+
},
|
|
1158
|
+
})
|
|
1159
|
+
|
|
1160
|
+
const allOutput = (allLogsResult as any).content[0].text
|
|
1161
|
+
expect(allOutput).toContain('[log] PageA log 11111')
|
|
1162
|
+
expect(allOutput).toContain('[log] PageB log 33333')
|
|
1163
|
+
|
|
1164
|
+
// Test that reloading page A clears only page A logs
|
|
1165
|
+
await client.callTool({
|
|
1166
|
+
name: 'execute',
|
|
1167
|
+
arguments: {
|
|
1168
|
+
code: js`
|
|
1169
|
+
await state.pageA.reload();
|
|
1170
|
+
await state.pageA.evaluate(() => {
|
|
1171
|
+
console.log('PageA after reload 55555');
|
|
1172
|
+
});
|
|
1173
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
1174
|
+
`,
|
|
1175
|
+
},
|
|
1176
|
+
})
|
|
1177
|
+
|
|
1178
|
+
// Check page A logs - should only have new log
|
|
1179
|
+
const pageAAfterReloadResult = await client.callTool({
|
|
1180
|
+
name: 'execute',
|
|
1181
|
+
arguments: {
|
|
1182
|
+
code: js`
|
|
1183
|
+
const logs = await getLatestLogs({ page: state.pageA });
|
|
1184
|
+
console.log('Page A logs after reload:', logs.length);
|
|
1185
|
+
logs.forEach(log => console.log(log));
|
|
1186
|
+
`,
|
|
1187
|
+
},
|
|
1188
|
+
})
|
|
1189
|
+
|
|
1190
|
+
const pageAAfterReloadOutput = (pageAAfterReloadResult as any).content[0].text
|
|
1191
|
+
expect(pageAAfterReloadOutput).toContain('[log] PageA after reload 55555')
|
|
1192
|
+
expect(pageAAfterReloadOutput).not.toContain('[log] PageA log 11111')
|
|
1193
|
+
|
|
1194
|
+
// Check page B logs - should still have original logs
|
|
1195
|
+
const pageBAfterAReloadResult = await client.callTool({
|
|
1196
|
+
name: 'execute',
|
|
1197
|
+
arguments: {
|
|
1198
|
+
code: js`
|
|
1199
|
+
const logs = await getLatestLogs({ page: state.pageB });
|
|
1200
|
+
console.log('Page B logs after A reload:', logs.length);
|
|
1201
|
+
logs.forEach(log => console.log(log));
|
|
1202
|
+
`,
|
|
1203
|
+
},
|
|
1204
|
+
})
|
|
1205
|
+
|
|
1206
|
+
const pageBAfterAReloadOutput = (pageBAfterAReloadResult as any).content[0].text
|
|
1207
|
+
expect(pageBAfterAReloadOutput).toContain('[log] PageB log 33333')
|
|
1208
|
+
expect(pageBAfterAReloadOutput).toContain('[error] PageB error 44444')
|
|
1209
|
+
|
|
1210
|
+
// Test that logs are deleted when page is closed
|
|
1211
|
+
await client.callTool({
|
|
1212
|
+
name: 'execute',
|
|
1213
|
+
arguments: {
|
|
1214
|
+
code: js`
|
|
1215
|
+
// Close page A
|
|
1216
|
+
await state.pageA.close();
|
|
1217
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
1218
|
+
`,
|
|
1219
|
+
},
|
|
1220
|
+
})
|
|
1221
|
+
|
|
1222
|
+
// Check all logs - page A logs should be gone
|
|
1223
|
+
const logsAfterCloseResult = await client.callTool({
|
|
1224
|
+
name: 'execute',
|
|
1225
|
+
arguments: {
|
|
1226
|
+
code: js`
|
|
1227
|
+
const logs = await getLatestLogs();
|
|
1228
|
+
console.log('All logs after closing page A:', logs.length);
|
|
1229
|
+
logs.forEach(log => console.log(log));
|
|
1230
|
+
`,
|
|
1231
|
+
},
|
|
1232
|
+
})
|
|
1233
|
+
|
|
1234
|
+
const logsAfterCloseOutput = (logsAfterCloseResult as any).content[0].text
|
|
1235
|
+
expect(logsAfterCloseOutput).not.toContain('PageA')
|
|
1236
|
+
expect(logsAfterCloseOutput).toContain('[log] PageB log 33333')
|
|
1237
|
+
|
|
1238
|
+
// Clean up remaining page
|
|
1239
|
+
await client.callTool({
|
|
1240
|
+
name: 'execute',
|
|
1241
|
+
arguments: {
|
|
1242
|
+
code: js`
|
|
1243
|
+
await state.pageB.close();
|
|
1244
|
+
delete state.pageA;
|
|
1245
|
+
delete state.pageB;
|
|
1246
|
+
`,
|
|
1247
|
+
},
|
|
1248
|
+
})
|
|
1249
|
+
}, 30000)
|
|
1250
|
+
|
|
1251
|
+
it('should maintain correct page.url() with service worker pages', async () => {
|
|
1252
|
+
const browserContext = getBrowserContext()
|
|
1253
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
1254
|
+
|
|
1255
|
+
const page = await browserContext.newPage()
|
|
1256
|
+
const targetUrl = 'https://example.com/sw-test'
|
|
1257
|
+
await page.goto(targetUrl)
|
|
1258
|
+
await page.bringToFront()
|
|
1259
|
+
|
|
1260
|
+
await serviceWorker.evaluate(async () => {
|
|
1261
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
1262
|
+
})
|
|
1263
|
+
|
|
1264
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
1265
|
+
|
|
1266
|
+
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
1267
|
+
const pages = browser.contexts()[0].pages()
|
|
1268
|
+
const testPage = pages.find((p) => p.url().includes('sw-test'))
|
|
1269
|
+
|
|
1270
|
+
expect(testPage).toBeDefined()
|
|
1271
|
+
expect(testPage?.url()).toContain('sw-test')
|
|
1272
|
+
expect(testPage?.url()).not.toContain('sw.js')
|
|
1273
|
+
|
|
1274
|
+
await browser.close()
|
|
1275
|
+
await page.close()
|
|
1276
|
+
}, 30000)
|
|
1277
|
+
|
|
1278
|
+
it('should maintain correct page.url() after repeated connections', async () => {
|
|
1279
|
+
const browserContext = getBrowserContext()
|
|
1280
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
1281
|
+
|
|
1282
|
+
const page = await browserContext.newPage()
|
|
1283
|
+
const targetUrl = 'https://example.com/repeated-test'
|
|
1284
|
+
await page.goto(targetUrl)
|
|
1285
|
+
await page.bringToFront()
|
|
1286
|
+
|
|
1287
|
+
await serviceWorker.evaluate(async () => {
|
|
1288
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
1289
|
+
})
|
|
1290
|
+
|
|
1291
|
+
for (let i = 0; i < 5; i++) {
|
|
1292
|
+
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
1293
|
+
const pages = browser.contexts()[0].pages()
|
|
1294
|
+
const testPage = pages.find((p) => p.url().includes('repeated-test'))
|
|
1295
|
+
|
|
1296
|
+
expect(testPage).toBeDefined()
|
|
1297
|
+
expect(testPage?.url()).toBe(targetUrl)
|
|
1298
|
+
|
|
1299
|
+
await browser.close()
|
|
1300
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
await page.close()
|
|
1304
|
+
}, 30000)
|
|
1305
|
+
|
|
1306
|
+
it('should maintain correct page.url() with concurrent MCP and CDP connections', async () => {
|
|
1307
|
+
const browserContext = getBrowserContext()
|
|
1308
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
1309
|
+
|
|
1310
|
+
const page = await browserContext.newPage()
|
|
1311
|
+
const targetUrl = 'https://example.com/concurrent-test'
|
|
1312
|
+
await page.goto(targetUrl)
|
|
1313
|
+
await page.bringToFront()
|
|
1314
|
+
|
|
1315
|
+
await serviceWorker.evaluate(async () => {
|
|
1316
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
1317
|
+
})
|
|
1318
|
+
|
|
1319
|
+
await new Promise((r) => setTimeout(r, 400))
|
|
1320
|
+
|
|
1321
|
+
const [mcpResult, cdpBrowser] = await Promise.all([
|
|
1322
|
+
client.callTool({
|
|
1323
|
+
name: 'execute',
|
|
1324
|
+
arguments: {
|
|
1325
|
+
code: js`
|
|
1326
|
+
const pages = context.pages();
|
|
1327
|
+
const testPage = pages.find(p => p.url().includes('concurrent-test'));
|
|
1328
|
+
return { url: testPage?.url(), found: !!testPage };
|
|
1329
|
+
`,
|
|
1330
|
+
},
|
|
1331
|
+
}),
|
|
1332
|
+
chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT })),
|
|
1333
|
+
])
|
|
1334
|
+
|
|
1335
|
+
const mcpOutput = (mcpResult as any).content[0].text
|
|
1336
|
+
expect(mcpOutput).toContain(targetUrl)
|
|
1337
|
+
|
|
1338
|
+
const cdpPages = cdpBrowser.contexts()[0].pages()
|
|
1339
|
+
const cdpPage = cdpPages.find((p) => p.url().includes('concurrent-test'))
|
|
1340
|
+
expect(cdpPage?.url()).toBe(targetUrl)
|
|
1341
|
+
|
|
1342
|
+
await cdpBrowser.close()
|
|
1343
|
+
await page.close()
|
|
1344
|
+
}, 30000)
|
|
1345
|
+
|
|
1346
|
+
it('should be usable after toggle with valid URL', async () => {
|
|
1347
|
+
// This test validates the extension properly waits for valid URLs before
|
|
1348
|
+
// sending Target.attachedToTarget. Uses Discord - a heavy React SPA.
|
|
1349
|
+
//
|
|
1350
|
+
// We use waitForEvent('page') to wait for Playwright to process the event.
|
|
1351
|
+
// The KEY assertion is that when the event fires, the URL is VALID (not empty).
|
|
1352
|
+
// Before the fix: event fired with empty URL -> page broken forever
|
|
1353
|
+
// After the fix: event fires with valid URL -> page works immediately
|
|
1354
|
+
|
|
1355
|
+
const _browserContext = getBrowserContext()
|
|
1356
|
+
const serviceWorker = await getExtensionServiceWorker(_browserContext)
|
|
1357
|
+
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
1358
|
+
const context = browser.contexts()[0]
|
|
1359
|
+
|
|
1360
|
+
const page = await _browserContext.newPage()
|
|
1361
|
+
await page.goto('https://discord.com/login')
|
|
1362
|
+
await page.bringToFront()
|
|
1363
|
+
|
|
1364
|
+
// Set up listener BEFORE toggle
|
|
1365
|
+
const pagePromise = context.waitForEvent('page', { timeout: 10000 })
|
|
1366
|
+
|
|
1367
|
+
// Toggle extension - extension waits for valid URL before sending event
|
|
1368
|
+
await serviceWorker.evaluate(async () => {
|
|
1369
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
1370
|
+
})
|
|
1371
|
+
|
|
1372
|
+
// Wait for page event
|
|
1373
|
+
const targetPage = await pagePromise
|
|
1374
|
+
console.log('Page URL when event fired:', targetPage.url())
|
|
1375
|
+
|
|
1376
|
+
// KEY ASSERTION: URL must NOT be empty - this is what the extension fix guarantees
|
|
1377
|
+
expect(targetPage.url()).not.toBe('')
|
|
1378
|
+
expect(targetPage.url()).not.toBe(':')
|
|
1379
|
+
expect(targetPage.url()).toContain('discord.com')
|
|
1380
|
+
|
|
1381
|
+
// evaluate() works immediately - no waiting needed
|
|
1382
|
+
const result = await targetPage.evaluate(() => window.location.href)
|
|
1383
|
+
expect(result).toContain('discord.com')
|
|
1384
|
+
|
|
1385
|
+
await browser.close()
|
|
1386
|
+
await page.close()
|
|
1387
|
+
}, 60000)
|
|
1388
|
+
|
|
1389
|
+
it('should have non-empty URLs when connecting to already-loaded pages', async () => {
|
|
1390
|
+
// This test validates that when we connect to a browser with already-loaded pages,
|
|
1391
|
+
// all pages have non-empty URLs. Empty URLs break Playwright permanently.
|
|
1392
|
+
|
|
1393
|
+
const _browserContext = getBrowserContext()
|
|
1394
|
+
const serviceWorker = await getExtensionServiceWorker(_browserContext)
|
|
1395
|
+
|
|
1396
|
+
// Create and fully load a heavy page BEFORE connecting
|
|
1397
|
+
const page = await _browserContext.newPage()
|
|
1398
|
+
await page.goto('https://discord.com/login', { waitUntil: 'load' })
|
|
1399
|
+
await page.bringToFront()
|
|
1400
|
+
|
|
1401
|
+
// Toggle extension to attach to the loaded page
|
|
1402
|
+
await serviceWorker.evaluate(async () => {
|
|
1403
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
1404
|
+
})
|
|
1405
|
+
|
|
1406
|
+
// NOW connect via CDP - page should already be attached
|
|
1407
|
+
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
1408
|
+
const context = browser.contexts()[0]
|
|
1409
|
+
|
|
1410
|
+
// Get all pages and verify NONE have empty URLs
|
|
1411
|
+
const pages = context.pages()
|
|
1412
|
+
console.log(
|
|
1413
|
+
'All page URLs:',
|
|
1414
|
+
pages.map((p) => p.url()),
|
|
1415
|
+
)
|
|
1416
|
+
|
|
1417
|
+
expect(pages.length).toBeGreaterThan(0)
|
|
1418
|
+
for (const p of pages) {
|
|
1419
|
+
expect(p.url()).not.toBe('')
|
|
1420
|
+
expect(p.url()).not.toBe(':')
|
|
1421
|
+
expect(p.url()).not.toBeUndefined()
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
// Find Discord page and verify it works
|
|
1425
|
+
const discordPage = pages.find((p) => p.url().includes('discord.com'))
|
|
1426
|
+
expect(discordPage).toBeDefined()
|
|
1427
|
+
|
|
1428
|
+
const result = await discordPage!.evaluate(() => window.location.href)
|
|
1429
|
+
expect(result).toContain('discord.com')
|
|
1430
|
+
|
|
1431
|
+
await browser.close()
|
|
1432
|
+
await page.close()
|
|
1433
|
+
}, 60000)
|
|
1434
|
+
|
|
1435
|
+
it('should maintain correct page.url() with iframe-heavy pages', async () => {
|
|
1436
|
+
const browserContext = getBrowserContext()
|
|
1437
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
1438
|
+
|
|
1439
|
+
const page = await browserContext.newPage()
|
|
1440
|
+
await page.setContent(`
|
|
1441
|
+
<html>
|
|
1442
|
+
<head><title>Iframe Test Page</title></head>
|
|
1443
|
+
<body>
|
|
1444
|
+
<h1>Iframe Heavy Page</h1>
|
|
1445
|
+
<iframe src="about:blank" id="frame1"></iframe>
|
|
1446
|
+
<iframe src="about:blank" id="frame2"></iframe>
|
|
1447
|
+
<iframe src="about:blank" id="frame3"></iframe>
|
|
1448
|
+
</body>
|
|
1449
|
+
</html>
|
|
1450
|
+
`)
|
|
1451
|
+
await page.bringToFront()
|
|
1452
|
+
|
|
1453
|
+
await serviceWorker.evaluate(async () => {
|
|
1454
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
1455
|
+
})
|
|
1456
|
+
|
|
1457
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
1458
|
+
|
|
1459
|
+
for (let i = 0; i < 3; i++) {
|
|
1460
|
+
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
1461
|
+
const pages = browser.contexts()[0].pages()
|
|
1462
|
+
let iframePage
|
|
1463
|
+
for (const p of pages) {
|
|
1464
|
+
const html = await p.content()
|
|
1465
|
+
if (html.includes('Iframe Heavy Page')) {
|
|
1466
|
+
iframePage = p
|
|
1467
|
+
break
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
expect(iframePage).toBeDefined()
|
|
1472
|
+
expect(iframePage?.url()).toContain('about:')
|
|
1473
|
+
|
|
1474
|
+
await browser.close()
|
|
1475
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
await page.close()
|
|
1479
|
+
}, 30000)
|
|
1480
|
+
|
|
1481
|
+
it('should capture screenshot correctly', async () => {
|
|
1482
|
+
const browserContext = getBrowserContext()
|
|
1483
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
1484
|
+
|
|
1485
|
+
const page = await browserContext.newPage()
|
|
1486
|
+
await page.goto('https://example.com/')
|
|
1487
|
+
await page.bringToFront()
|
|
1488
|
+
|
|
1489
|
+
await serviceWorker.evaluate(async () => {
|
|
1490
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
1491
|
+
})
|
|
1492
|
+
|
|
1493
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
1494
|
+
|
|
1495
|
+
const capturedCommands: CDPCommand[] = []
|
|
1496
|
+
const commandHandler = ({ command }: { clientId: string; command: CDPCommand }) => {
|
|
1497
|
+
if (command.method === 'Page.captureScreenshot') {
|
|
1498
|
+
capturedCommands.push(command)
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
testCtx!.relayServer.on('cdp:command', commandHandler)
|
|
1502
|
+
|
|
1503
|
+
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
1504
|
+
const cdpPage = browser
|
|
1505
|
+
.contexts()[0]
|
|
1506
|
+
.pages()
|
|
1507
|
+
.find((p) => p.url().includes('example.com'))
|
|
1508
|
+
|
|
1509
|
+
expect(cdpPage).toBeDefined()
|
|
1510
|
+
|
|
1511
|
+
const viewportSize = cdpPage!.viewportSize()
|
|
1512
|
+
console.log('Viewport size:', viewportSize)
|
|
1513
|
+
|
|
1514
|
+
const viewportScreenshot = await cdpPage!.screenshot()
|
|
1515
|
+
expect(viewportScreenshot).toBeDefined()
|
|
1516
|
+
|
|
1517
|
+
const viewportDimensions = imageSize(viewportScreenshot)
|
|
1518
|
+
console.log('Viewport screenshot dimensions:', viewportDimensions)
|
|
1519
|
+
expect(viewportDimensions.width).toBeGreaterThan(0)
|
|
1520
|
+
expect(viewportDimensions.height).toBeGreaterThan(0)
|
|
1521
|
+
if (viewportSize) {
|
|
1522
|
+
expect(viewportDimensions.width).toBe(viewportSize.width)
|
|
1523
|
+
expect(viewportDimensions.height).toBe(viewportSize.height)
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
const fullPageScreenshot = await cdpPage!.screenshot({ fullPage: true })
|
|
1527
|
+
expect(fullPageScreenshot).toBeDefined()
|
|
1528
|
+
|
|
1529
|
+
const fullPageDimensions = imageSize(fullPageScreenshot)
|
|
1530
|
+
console.log('Full page screenshot dimensions:', fullPageDimensions)
|
|
1531
|
+
expect(fullPageDimensions.width).toBeGreaterThan(0)
|
|
1532
|
+
expect(fullPageDimensions.height).toBeGreaterThan(0)
|
|
1533
|
+
expect(fullPageDimensions.width).toBeGreaterThanOrEqual(viewportDimensions.width!)
|
|
1534
|
+
|
|
1535
|
+
testCtx!.relayServer.off('cdp:command', commandHandler)
|
|
1536
|
+
|
|
1537
|
+
expect(capturedCommands.length).toBe(2)
|
|
1538
|
+
expect(
|
|
1539
|
+
capturedCommands.map((c) => ({
|
|
1540
|
+
method: c.method,
|
|
1541
|
+
params: c.params,
|
|
1542
|
+
})),
|
|
1543
|
+
).toMatchInlineSnapshot(`
|
|
1544
|
+
[
|
|
1545
|
+
{
|
|
1546
|
+
"method": "Page.captureScreenshot",
|
|
1547
|
+
"params": {
|
|
1548
|
+
"captureBeyondViewport": false,
|
|
1549
|
+
"clip": {
|
|
1550
|
+
"height": 720,
|
|
1551
|
+
"scale": 1,
|
|
1552
|
+
"width": 1280,
|
|
1553
|
+
"x": 0,
|
|
1554
|
+
"y": 0,
|
|
1555
|
+
},
|
|
1556
|
+
"format": "png",
|
|
1557
|
+
},
|
|
1558
|
+
},
|
|
1559
|
+
{
|
|
1560
|
+
"method": "Page.captureScreenshot",
|
|
1561
|
+
"params": {
|
|
1562
|
+
"captureBeyondViewport": false,
|
|
1563
|
+
"clip": {
|
|
1564
|
+
"height": 581,
|
|
1565
|
+
"scale": 1,
|
|
1566
|
+
"width": 1280,
|
|
1567
|
+
"x": 0,
|
|
1568
|
+
"y": 0,
|
|
1569
|
+
},
|
|
1570
|
+
"format": "png",
|
|
1571
|
+
},
|
|
1572
|
+
},
|
|
1573
|
+
]
|
|
1574
|
+
`)
|
|
1575
|
+
|
|
1576
|
+
const screenshotPath = path.join(os.tmpdir(), 'e2e-pilot-test-screenshot.png')
|
|
1577
|
+
fs.writeFileSync(screenshotPath, viewportScreenshot)
|
|
1578
|
+
console.log('Screenshot saved to:', screenshotPath)
|
|
1579
|
+
|
|
1580
|
+
await browser.close()
|
|
1581
|
+
await page.close()
|
|
1582
|
+
}, 60000)
|
|
1583
|
+
|
|
1584
|
+
it('should capture element screenshot with correct coordinates', async () => {
|
|
1585
|
+
const browserContext = getBrowserContext()
|
|
1586
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
1587
|
+
|
|
1588
|
+
const target = { x: 200, y: 150, width: 300, height: 100 }
|
|
1589
|
+
const scrolledTarget = { x: 100, y: 1500, width: 200, height: 80 }
|
|
1590
|
+
|
|
1591
|
+
const page = await browserContext.newPage()
|
|
1592
|
+
await page.setContent(`
|
|
1593
|
+
<html>
|
|
1594
|
+
<head>
|
|
1595
|
+
<style>
|
|
1596
|
+
body { margin: 0; padding: 0; height: 2000px; }
|
|
1597
|
+
#target {
|
|
1598
|
+
position: absolute;
|
|
1599
|
+
top: ${target.y}px;
|
|
1600
|
+
left: ${target.x}px;
|
|
1601
|
+
width: ${target.width}px;
|
|
1602
|
+
height: ${target.height}px;
|
|
1603
|
+
background: red;
|
|
1604
|
+
}
|
|
1605
|
+
#scrolled-target {
|
|
1606
|
+
position: absolute;
|
|
1607
|
+
top: ${scrolledTarget.y}px;
|
|
1608
|
+
left: ${scrolledTarget.x}px;
|
|
1609
|
+
width: ${scrolledTarget.width}px;
|
|
1610
|
+
height: ${scrolledTarget.height}px;
|
|
1611
|
+
background: blue;
|
|
1612
|
+
}
|
|
1613
|
+
</style>
|
|
1614
|
+
</head>
|
|
1615
|
+
<body>
|
|
1616
|
+
<div id="target">Target Element</div>
|
|
1617
|
+
<div id="scrolled-target">Scrolled Target</div>
|
|
1618
|
+
</body>
|
|
1619
|
+
</html>
|
|
1620
|
+
`)
|
|
1621
|
+
await page.bringToFront()
|
|
1622
|
+
|
|
1623
|
+
await serviceWorker.evaluate(async () => {
|
|
1624
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
1625
|
+
})
|
|
1626
|
+
|
|
1627
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
1628
|
+
|
|
1629
|
+
const capturedCommands: CDPCommand[] = []
|
|
1630
|
+
const commandHandler = ({ command }: { clientId: string; command: CDPCommand }) => {
|
|
1631
|
+
if (command.method === 'Page.captureScreenshot') {
|
|
1632
|
+
capturedCommands.push(command)
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
testCtx!.relayServer.on('cdp:command', commandHandler)
|
|
1636
|
+
|
|
1637
|
+
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
1638
|
+
let cdpPage
|
|
1639
|
+
for (const p of browser.contexts()[0].pages()) {
|
|
1640
|
+
const html = await p.content()
|
|
1641
|
+
if (html.includes('scrolled-target')) {
|
|
1642
|
+
cdpPage = p
|
|
1643
|
+
break
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
expect(cdpPage).toBeDefined()
|
|
1647
|
+
|
|
1648
|
+
await cdpPage!.locator('#target').screenshot()
|
|
1649
|
+
|
|
1650
|
+
await cdpPage!.locator('#scrolled-target').screenshot()
|
|
1651
|
+
|
|
1652
|
+
testCtx!.relayServer.off('cdp:command', commandHandler)
|
|
1653
|
+
|
|
1654
|
+
expect(capturedCommands.length).toBe(2)
|
|
1655
|
+
|
|
1656
|
+
const targetCmd = capturedCommands[0]
|
|
1657
|
+
expect(targetCmd.method).toBe('Page.captureScreenshot')
|
|
1658
|
+
const targetClip = (targetCmd.params as any).clip
|
|
1659
|
+
expect(targetClip.x).toBe(target.x)
|
|
1660
|
+
expect(targetClip.y).toBe(target.y)
|
|
1661
|
+
expect(targetClip.width).toBe(target.width)
|
|
1662
|
+
expect(targetClip.height).toBe(target.height)
|
|
1663
|
+
|
|
1664
|
+
const scrolledCmd = capturedCommands[1]
|
|
1665
|
+
expect(scrolledCmd.method).toBe('Page.captureScreenshot')
|
|
1666
|
+
const scrolledClip = (scrolledCmd.params as any).clip
|
|
1667
|
+
expect(scrolledClip.x).toBe(scrolledTarget.x)
|
|
1668
|
+
expect(scrolledClip.y).toBe(scrolledTarget.y)
|
|
1669
|
+
expect(scrolledClip.width).toBe(scrolledTarget.width)
|
|
1670
|
+
expect(scrolledClip.height).toBe(scrolledTarget.height)
|
|
1671
|
+
|
|
1672
|
+
await browser.close()
|
|
1673
|
+
await page.close()
|
|
1674
|
+
}, 60000)
|
|
1675
|
+
|
|
1676
|
+
it('should get locator string for element using getLocatorStringForElement', async () => {
|
|
1677
|
+
const browserContext = getBrowserContext()
|
|
1678
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
1679
|
+
|
|
1680
|
+
const page = await browserContext.newPage()
|
|
1681
|
+
await page.setContent(`
|
|
1682
|
+
<html>
|
|
1683
|
+
<body>
|
|
1684
|
+
<button id="test-btn">Click Me</button>
|
|
1685
|
+
<input type="text" placeholder="Enter name" />
|
|
1686
|
+
</body>
|
|
1687
|
+
</html>
|
|
1688
|
+
`)
|
|
1689
|
+
await page.bringToFront()
|
|
1690
|
+
|
|
1691
|
+
await serviceWorker.evaluate(async () => {
|
|
1692
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
1693
|
+
})
|
|
1694
|
+
|
|
1695
|
+
await new Promise((r) => setTimeout(r, 400))
|
|
1696
|
+
|
|
1697
|
+
const result = await client.callTool({
|
|
1698
|
+
name: 'execute',
|
|
1699
|
+
arguments: {
|
|
1700
|
+
intend: 'Get locator string for button element',
|
|
1701
|
+
code: js`
|
|
1702
|
+
let testPage;
|
|
1703
|
+
for (const p of context.pages()) {
|
|
1704
|
+
const html = await p.content();
|
|
1705
|
+
if (html.includes('test-btn')) { testPage = p; break; }
|
|
1706
|
+
}
|
|
1707
|
+
if (!testPage) throw new Error('Test page not found');
|
|
1708
|
+
const btn = testPage.locator('#test-btn');
|
|
1709
|
+
const locatorString = await getLocatorStringForElement(btn);
|
|
1710
|
+
console.log('Locator string:', locatorString);
|
|
1711
|
+
const locatorFromString = eval('testPage.' + locatorString);
|
|
1712
|
+
const count = await locatorFromString.count();
|
|
1713
|
+
console.log('Locator count:', count);
|
|
1714
|
+
const text = await locatorFromString.textContent();
|
|
1715
|
+
console.log('Locator text:', text);
|
|
1716
|
+
`,
|
|
1717
|
+
timeout: 30000,
|
|
1718
|
+
},
|
|
1719
|
+
})
|
|
1720
|
+
|
|
1721
|
+
expect(result.isError).toBeFalsy()
|
|
1722
|
+
const text = (result.content as any)[0]?.text || ''
|
|
1723
|
+
expect(text).toContain('Locator string:')
|
|
1724
|
+
expect(text).toContain("getByRole('button', { name: 'Click Me' })")
|
|
1725
|
+
expect(text).toContain('Locator count: 1')
|
|
1726
|
+
expect(text).toContain('Locator text: Click Me')
|
|
1727
|
+
|
|
1728
|
+
await page.close()
|
|
1729
|
+
}, 60000)
|
|
1730
|
+
|
|
1731
|
+
it('should get styles for element using getStylesForLocator', async () => {
|
|
1732
|
+
const browserContext = getBrowserContext()
|
|
1733
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
1734
|
+
|
|
1735
|
+
const page = await browserContext.newPage()
|
|
1736
|
+
await page.setContent(`
|
|
1737
|
+
<html>
|
|
1738
|
+
<head>
|
|
1739
|
+
<style>
|
|
1740
|
+
body { font-family: Arial, sans-serif; color: #333; }
|
|
1741
|
+
.container { padding: 20px; margin: 10px; }
|
|
1742
|
+
#main-btn { background-color: blue; color: white; border-radius: 4px; }
|
|
1743
|
+
.btn { padding: 8px 16px; }
|
|
1744
|
+
</style>
|
|
1745
|
+
</head>
|
|
1746
|
+
<body>
|
|
1747
|
+
<div class="container">
|
|
1748
|
+
<button id="main-btn" class="btn" style="font-weight: bold;">Click Me</button>
|
|
1749
|
+
</div>
|
|
1750
|
+
</body>
|
|
1751
|
+
</html>
|
|
1752
|
+
`)
|
|
1753
|
+
await page.bringToFront()
|
|
1754
|
+
|
|
1755
|
+
await serviceWorker.evaluate(async () => {
|
|
1756
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
1757
|
+
})
|
|
1758
|
+
|
|
1759
|
+
await new Promise((r) => setTimeout(r, 400))
|
|
1760
|
+
|
|
1761
|
+
const stylesResult = await client.callTool({
|
|
1762
|
+
name: 'execute',
|
|
1763
|
+
arguments: {
|
|
1764
|
+
code: js`
|
|
1765
|
+
let testPage;
|
|
1766
|
+
for (const p of context.pages()) {
|
|
1767
|
+
const html = await p.content();
|
|
1768
|
+
if (html.includes('main-btn')) { testPage = p; break; }
|
|
1769
|
+
}
|
|
1770
|
+
if (!testPage) throw new Error('Test page not found');
|
|
1771
|
+
const btn = testPage.locator('#main-btn');
|
|
1772
|
+
const styles = await getStylesForLocator({ locator: btn });
|
|
1773
|
+
return styles;
|
|
1774
|
+
`,
|
|
1775
|
+
timeout: 30000,
|
|
1776
|
+
},
|
|
1777
|
+
})
|
|
1778
|
+
|
|
1779
|
+
expect(stylesResult.isError).toBeFalsy()
|
|
1780
|
+
const stylesText = (stylesResult.content as any)[0]?.text || ''
|
|
1781
|
+
expect(stylesText).toMatchInlineSnapshot(`
|
|
1782
|
+
"Return value:
|
|
1783
|
+
{
|
|
1784
|
+
"element": "button#main-btn.btn",
|
|
1785
|
+
"inlineStyle": {
|
|
1786
|
+
"font-weight": "bold"
|
|
1787
|
+
},
|
|
1788
|
+
"rules": [
|
|
1789
|
+
{
|
|
1790
|
+
"selector": ".btn",
|
|
1791
|
+
"source": null,
|
|
1792
|
+
"origin": "regular",
|
|
1793
|
+
"declarations": {
|
|
1794
|
+
"padding": "8px 16px",
|
|
1795
|
+
"padding-top": "8px",
|
|
1796
|
+
"padding-right": "16px",
|
|
1797
|
+
"padding-bottom": "8px",
|
|
1798
|
+
"padding-left": "16px"
|
|
1799
|
+
},
|
|
1800
|
+
"inheritedFrom": null
|
|
1801
|
+
},
|
|
1802
|
+
{
|
|
1803
|
+
"selector": "#main-btn",
|
|
1804
|
+
"source": null,
|
|
1805
|
+
"origin": "regular",
|
|
1806
|
+
"declarations": {
|
|
1807
|
+
"background-color": "blue",
|
|
1808
|
+
"color": "white",
|
|
1809
|
+
"border-radius": "4px",
|
|
1810
|
+
"border-top-left-radius": "4px",
|
|
1811
|
+
"border-top-right-radius": "4px",
|
|
1812
|
+
"border-bottom-right-radius": "4px",
|
|
1813
|
+
"border-bottom-left-radius": "4px"
|
|
1814
|
+
},
|
|
1815
|
+
"inheritedFrom": null
|
|
1816
|
+
},
|
|
1817
|
+
{
|
|
1818
|
+
"selector": ".container",
|
|
1819
|
+
"source": null,
|
|
1820
|
+
"origin": "regular",
|
|
1821
|
+
"declarations": {
|
|
1822
|
+
"padding": "20px",
|
|
1823
|
+
"margin": "10px",
|
|
1824
|
+
"padding-top": "20px",
|
|
1825
|
+
"padding-right": "20px",
|
|
1826
|
+
"padding-bottom": "20px",
|
|
1827
|
+
"padding-left": "20px",
|
|
1828
|
+
"margin-top": "10px",
|
|
1829
|
+
"margin-right": "10px",
|
|
1830
|
+
"margin-bottom": "10px",
|
|
1831
|
+
"margin-left": "10px"
|
|
1832
|
+
},
|
|
1833
|
+
"inheritedFrom": "ancestor[1]"
|
|
1834
|
+
},
|
|
1835
|
+
{
|
|
1836
|
+
"selector": "body",
|
|
1837
|
+
"source": null,
|
|
1838
|
+
"origin": "regular",
|
|
1839
|
+
"declarations": {
|
|
1840
|
+
"font-family": "Arial, sans-serif",
|
|
1841
|
+
"color": "rgb(51, 51, 51)"
|
|
1842
|
+
},
|
|
1843
|
+
"inheritedFrom": "ancestor[2]"
|
|
1844
|
+
}
|
|
1845
|
+
]
|
|
1846
|
+
}"
|
|
1847
|
+
`)
|
|
1848
|
+
|
|
1849
|
+
const formattedResult = await client.callTool({
|
|
1850
|
+
name: 'execute',
|
|
1851
|
+
arguments: {
|
|
1852
|
+
code: js`
|
|
1853
|
+
let testPage;
|
|
1854
|
+
for (const p of context.pages()) {
|
|
1855
|
+
const html = await p.content();
|
|
1856
|
+
if (html.includes('main-btn')) { testPage = p; break; }
|
|
1857
|
+
}
|
|
1858
|
+
if (!testPage) throw new Error('Test page not found');
|
|
1859
|
+
const btn = testPage.locator('#main-btn');
|
|
1860
|
+
const styles = await getStylesForLocator({ locator: btn });
|
|
1861
|
+
return formatStylesAsText(styles);
|
|
1862
|
+
`,
|
|
1863
|
+
timeout: 30000,
|
|
1864
|
+
},
|
|
1865
|
+
})
|
|
1866
|
+
|
|
1867
|
+
expect(formattedResult.isError).toBeFalsy()
|
|
1868
|
+
const formattedText = (formattedResult.content as any)[0]?.text || ''
|
|
1869
|
+
expect(formattedText).toMatchInlineSnapshot(`
|
|
1870
|
+
"Return value:
|
|
1871
|
+
Element: button#main-btn.btn
|
|
1872
|
+
|
|
1873
|
+
Inline styles:
|
|
1874
|
+
font-weight: bold
|
|
1875
|
+
|
|
1876
|
+
Matched rules:
|
|
1877
|
+
.btn {
|
|
1878
|
+
padding: 8px 16px;
|
|
1879
|
+
padding-top: 8px;
|
|
1880
|
+
padding-right: 16px;
|
|
1881
|
+
padding-bottom: 8px;
|
|
1882
|
+
padding-left: 16px;
|
|
1883
|
+
}
|
|
1884
|
+
#main-btn {
|
|
1885
|
+
background-color: blue;
|
|
1886
|
+
color: white;
|
|
1887
|
+
border-radius: 4px;
|
|
1888
|
+
border-top-left-radius: 4px;
|
|
1889
|
+
border-top-right-radius: 4px;
|
|
1890
|
+
border-bottom-right-radius: 4px;
|
|
1891
|
+
border-bottom-left-radius: 4px;
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
Inherited from ancestor[1]:
|
|
1895
|
+
.container {
|
|
1896
|
+
padding: 20px;
|
|
1897
|
+
margin: 10px;
|
|
1898
|
+
padding-top: 20px;
|
|
1899
|
+
padding-right: 20px;
|
|
1900
|
+
padding-bottom: 20px;
|
|
1901
|
+
padding-left: 20px;
|
|
1902
|
+
margin-top: 10px;
|
|
1903
|
+
margin-right: 10px;
|
|
1904
|
+
margin-bottom: 10px;
|
|
1905
|
+
margin-left: 10px;
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
Inherited from ancestor[2]:
|
|
1909
|
+
body {
|
|
1910
|
+
font-family: Arial, sans-serif;
|
|
1911
|
+
color: rgb(51, 51, 51);
|
|
1912
|
+
}"
|
|
1913
|
+
`)
|
|
1914
|
+
|
|
1915
|
+
await page.close()
|
|
1916
|
+
}, 60000)
|
|
1917
|
+
|
|
1918
|
+
it('should return correct layout metrics via CDP', async () => {
|
|
1919
|
+
const browserContext = getBrowserContext()
|
|
1920
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
1921
|
+
|
|
1922
|
+
const page = await browserContext.newPage()
|
|
1923
|
+
await page.goto('https://example.com/')
|
|
1924
|
+
await page.bringToFront()
|
|
1925
|
+
|
|
1926
|
+
await serviceWorker.evaluate(async () => {
|
|
1927
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
1928
|
+
})
|
|
1929
|
+
|
|
1930
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
1931
|
+
|
|
1932
|
+
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
1933
|
+
const cdpPage = browser
|
|
1934
|
+
.contexts()[0]
|
|
1935
|
+
.pages()
|
|
1936
|
+
.find((p) => p.url().includes('example.com'))
|
|
1937
|
+
expect(cdpPage).toBeDefined()
|
|
1938
|
+
|
|
1939
|
+
const wsUrl = getCdpUrl({ port: TEST_PORT })
|
|
1940
|
+
const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
|
|
1941
|
+
|
|
1942
|
+
const layoutMetrics = await cdpSession.send('Page.getLayoutMetrics')
|
|
1943
|
+
|
|
1944
|
+
const normalized = {
|
|
1945
|
+
cssLayoutViewport: layoutMetrics.cssLayoutViewport,
|
|
1946
|
+
cssVisualViewport: layoutMetrics.cssVisualViewport,
|
|
1947
|
+
layoutViewport: layoutMetrics.layoutViewport,
|
|
1948
|
+
visualViewport: layoutMetrics.visualViewport,
|
|
1949
|
+
devicePixelRatio:
|
|
1950
|
+
layoutMetrics.cssVisualViewport.clientWidth > 0
|
|
1951
|
+
? layoutMetrics.visualViewport.clientWidth / layoutMetrics.cssVisualViewport.clientWidth
|
|
1952
|
+
: 1,
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
expect(normalized).toMatchInlineSnapshot(`
|
|
1956
|
+
{
|
|
1957
|
+
"cssLayoutViewport": {
|
|
1958
|
+
"clientHeight": 581,
|
|
1959
|
+
"clientWidth": 1280,
|
|
1960
|
+
"pageX": 0,
|
|
1961
|
+
"pageY": 0,
|
|
1962
|
+
},
|
|
1963
|
+
"cssVisualViewport": {
|
|
1964
|
+
"clientHeight": 581,
|
|
1965
|
+
"clientWidth": 1280,
|
|
1966
|
+
"offsetX": 0,
|
|
1967
|
+
"offsetY": 0,
|
|
1968
|
+
"pageX": 0,
|
|
1969
|
+
"pageY": 0,
|
|
1970
|
+
"scale": 1,
|
|
1971
|
+
"zoom": 1,
|
|
1972
|
+
},
|
|
1973
|
+
"devicePixelRatio": 1,
|
|
1974
|
+
"layoutViewport": {
|
|
1975
|
+
"clientHeight": 581,
|
|
1976
|
+
"clientWidth": 1280,
|
|
1977
|
+
"pageX": 0,
|
|
1978
|
+
"pageY": 0,
|
|
1979
|
+
},
|
|
1980
|
+
"visualViewport": {
|
|
1981
|
+
"clientHeight": 581,
|
|
1982
|
+
"clientWidth": 1280,
|
|
1983
|
+
"offsetX": 0,
|
|
1984
|
+
"offsetY": 0,
|
|
1985
|
+
"pageX": 0,
|
|
1986
|
+
"pageY": 0,
|
|
1987
|
+
"scale": 1,
|
|
1988
|
+
"zoom": 1,
|
|
1989
|
+
},
|
|
1990
|
+
}
|
|
1991
|
+
`)
|
|
1992
|
+
|
|
1993
|
+
const windowDpr = await cdpPage!.evaluate(() => (globalThis as any).devicePixelRatio)
|
|
1994
|
+
console.log('window.devicePixelRatio:', windowDpr)
|
|
1995
|
+
expect(windowDpr).toBe(1)
|
|
1996
|
+
|
|
1997
|
+
cdpSession.close()
|
|
1998
|
+
await browser.close()
|
|
1999
|
+
await page.close()
|
|
2000
|
+
}, 60000)
|
|
2001
|
+
|
|
2002
|
+
it('should support getCDPSession through the relay', async () => {
|
|
2003
|
+
const browserContext = getBrowserContext()
|
|
2004
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
2005
|
+
|
|
2006
|
+
const page = await browserContext.newPage()
|
|
2007
|
+
await page.goto('https://example.com/')
|
|
2008
|
+
await page.bringToFront()
|
|
2009
|
+
|
|
2010
|
+
await serviceWorker.evaluate(async () => {
|
|
2011
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
2012
|
+
})
|
|
2013
|
+
|
|
2014
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
2015
|
+
|
|
2016
|
+
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
2017
|
+
const cdpPage = browser
|
|
2018
|
+
.contexts()[0]
|
|
2019
|
+
.pages()
|
|
2020
|
+
.find((p) => p.url().includes('example.com'))
|
|
2021
|
+
expect(cdpPage).toBeDefined()
|
|
2022
|
+
|
|
2023
|
+
const wsUrl = getCdpUrl({ port: TEST_PORT })
|
|
2024
|
+
const client = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
|
|
2025
|
+
|
|
2026
|
+
const layoutMetrics = await client.send('Page.getLayoutMetrics')
|
|
2027
|
+
expect(layoutMetrics.cssVisualViewport).toBeDefined()
|
|
2028
|
+
expect(layoutMetrics.cssVisualViewport.clientWidth).toBeGreaterThan(0)
|
|
2029
|
+
|
|
2030
|
+
client.close()
|
|
2031
|
+
await browser.close()
|
|
2032
|
+
await page.close()
|
|
2033
|
+
}, 60000)
|
|
2034
|
+
|
|
2035
|
+
it('should work with stagehand', async () => {
|
|
2036
|
+
const browserContext = getBrowserContext()
|
|
2037
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
2038
|
+
|
|
2039
|
+
await serviceWorker.evaluate(async () => {
|
|
2040
|
+
await globalThis.disconnectEverything()
|
|
2041
|
+
})
|
|
2042
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
2043
|
+
|
|
2044
|
+
const targetUrl = 'https://example.com/'
|
|
2045
|
+
|
|
2046
|
+
const enableResult = await serviceWorker.evaluate(async (url) => {
|
|
2047
|
+
const tab = await chrome.tabs.create({ url, active: true })
|
|
2048
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
2049
|
+
return await globalThis.toggleExtensionForActiveTab()
|
|
2050
|
+
}, targetUrl)
|
|
2051
|
+
|
|
2052
|
+
console.log('Extension enabled:', enableResult)
|
|
2053
|
+
expect(enableResult.isConnected).toBe(true)
|
|
2054
|
+
|
|
2055
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
2056
|
+
|
|
2057
|
+
const { Stagehand } = await import('@browserbasehq/stagehand')
|
|
2058
|
+
|
|
2059
|
+
const stagehand = new Stagehand({
|
|
2060
|
+
env: 'LOCAL',
|
|
2061
|
+
verbose: 1,
|
|
2062
|
+
disablePino: true,
|
|
2063
|
+
localBrowserLaunchOptions: {
|
|
2064
|
+
cdpUrl: getCdpUrl({ port: TEST_PORT }),
|
|
2065
|
+
},
|
|
2066
|
+
})
|
|
2067
|
+
|
|
2068
|
+
console.log('Initializing Stagehand...')
|
|
2069
|
+
await stagehand.init()
|
|
2070
|
+
console.log('Stagehand initialized')
|
|
2071
|
+
|
|
2072
|
+
const context = stagehand.context
|
|
2073
|
+
// console.log('Stagehand context:', context)
|
|
2074
|
+
expect(context).toBeDefined()
|
|
2075
|
+
|
|
2076
|
+
const pages = context.pages()
|
|
2077
|
+
console.log(
|
|
2078
|
+
'Stagehand pages:',
|
|
2079
|
+
pages.length,
|
|
2080
|
+
pages.map((p) => p.url()),
|
|
2081
|
+
)
|
|
2082
|
+
|
|
2083
|
+
const stagehandPage = pages.find((p) => p.url().includes('example.com'))
|
|
2084
|
+
expect(stagehandPage).toBeDefined()
|
|
2085
|
+
|
|
2086
|
+
const url = stagehandPage!.url()
|
|
2087
|
+
console.log('Stagehand page URL:', url)
|
|
2088
|
+
expect(url).toContain('example.com')
|
|
2089
|
+
|
|
2090
|
+
await stagehand.close()
|
|
2091
|
+
}, 60000)
|
|
2092
|
+
|
|
2093
|
+
it('should preserve system color scheme instead of forcing light mode', async () => {
|
|
2094
|
+
const browserContext = getBrowserContext()
|
|
2095
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
2096
|
+
|
|
2097
|
+
const page = await browserContext.newPage()
|
|
2098
|
+
await page.goto('https://example.com')
|
|
2099
|
+
await page.bringToFront()
|
|
2100
|
+
|
|
2101
|
+
const colorSchemeBefore = await page.evaluate(() => {
|
|
2102
|
+
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
|
2103
|
+
})
|
|
2104
|
+
console.log('Color scheme before MCP connection:', colorSchemeBefore)
|
|
2105
|
+
|
|
2106
|
+
await serviceWorker.evaluate(async () => {
|
|
2107
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
2108
|
+
})
|
|
2109
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
2110
|
+
|
|
2111
|
+
const result = await client.callTool({
|
|
2112
|
+
name: 'execute',
|
|
2113
|
+
arguments: {
|
|
2114
|
+
code: js`
|
|
2115
|
+
const pages = context.pages();
|
|
2116
|
+
const urls = pages.map(p => p.url());
|
|
2117
|
+
const targetPage = pages.find(p => p.url().includes('example.com'));
|
|
2118
|
+
if (!targetPage) {
|
|
2119
|
+
return { error: 'Page not found', urls };
|
|
2120
|
+
}
|
|
2121
|
+
const isDark = await targetPage.evaluate(() => window.matchMedia('(prefers-color-scheme: dark)').matches);
|
|
2122
|
+
const isLight = await targetPage.evaluate(() => window.matchMedia('(prefers-color-scheme: light)').matches);
|
|
2123
|
+
return { matchesDark: isDark, matchesLight: isLight };
|
|
2124
|
+
`,
|
|
2125
|
+
},
|
|
2126
|
+
})
|
|
2127
|
+
|
|
2128
|
+
console.log('Color scheme after MCP connection:', result.content)
|
|
2129
|
+
|
|
2130
|
+
expect(result.content).toMatchInlineSnapshot(`
|
|
2131
|
+
[
|
|
2132
|
+
{
|
|
2133
|
+
"text": "MCP error -32602: Input validation error: Invalid arguments for tool execute: [
|
|
2134
|
+
{
|
|
2135
|
+
"code": "invalid_type",
|
|
2136
|
+
"expected": "string",
|
|
2137
|
+
"received": "undefined",
|
|
2138
|
+
"path": [
|
|
2139
|
+
"intend"
|
|
2140
|
+
],
|
|
2141
|
+
"message": "Required"
|
|
2142
|
+
}
|
|
2143
|
+
]",
|
|
2144
|
+
"type": "text",
|
|
2145
|
+
},
|
|
2146
|
+
]
|
|
2147
|
+
`)
|
|
2148
|
+
|
|
2149
|
+
await page.close()
|
|
2150
|
+
}, 60000)
|
|
2151
|
+
|
|
2152
|
+
it('should get aria ref for locator using getAriaSnapshot', async () => {
|
|
2153
|
+
const browserContext = getBrowserContext()
|
|
2154
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
2155
|
+
|
|
2156
|
+
const page = await browserContext.newPage()
|
|
2157
|
+
await page.setContent(`
|
|
2158
|
+
<html>
|
|
2159
|
+
<body>
|
|
2160
|
+
<button id="submit-btn">Submit Form</button>
|
|
2161
|
+
<a href="/about">About Us</a>
|
|
2162
|
+
<input type="text" placeholder="Enter your name" />
|
|
2163
|
+
</body>
|
|
2164
|
+
</html>
|
|
2165
|
+
`)
|
|
2166
|
+
await page.bringToFront()
|
|
2167
|
+
|
|
2168
|
+
await serviceWorker.evaluate(async () => {
|
|
2169
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
2170
|
+
})
|
|
2171
|
+
await new Promise((r) => setTimeout(r, 400))
|
|
2172
|
+
|
|
2173
|
+
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
2174
|
+
let cdpPage
|
|
2175
|
+
for (const p of browser.contexts()[0].pages()) {
|
|
2176
|
+
const html = await p.content()
|
|
2177
|
+
if (html.includes('submit-btn')) {
|
|
2178
|
+
cdpPage = p
|
|
2179
|
+
break
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
expect(cdpPage).toBeDefined()
|
|
2183
|
+
|
|
2184
|
+
const { getAriaSnapshot } = await import('./aria-snapshot.js')
|
|
2185
|
+
|
|
2186
|
+
// Get aria snapshot and verify we can get refs
|
|
2187
|
+
const ariaResult = await getAriaSnapshot({ page: cdpPage! })
|
|
2188
|
+
|
|
2189
|
+
expect(ariaResult.snapshot).toBeDefined()
|
|
2190
|
+
expect(ariaResult.snapshot.length).toBeGreaterThan(0)
|
|
2191
|
+
expect(ariaResult.snapshot).toContain('Submit Form')
|
|
2192
|
+
|
|
2193
|
+
// Verify refToElement map is populated
|
|
2194
|
+
expect(ariaResult.refToElement.size).toBeGreaterThan(0)
|
|
2195
|
+
console.log('RefToElement map size:', ariaResult.refToElement.size)
|
|
2196
|
+
console.log('RefToElement entries:', [...ariaResult.refToElement.entries()])
|
|
2197
|
+
|
|
2198
|
+
// Verify we can select elements using aria-ref selectors
|
|
2199
|
+
const btnViaAriaRef = cdpPage!.locator('aria-ref=e2')
|
|
2200
|
+
const btnTextViaRef = await btnViaAriaRef.textContent()
|
|
2201
|
+
console.log('Button text via aria-ref=e2:', btnTextViaRef)
|
|
2202
|
+
expect(btnTextViaRef).toBe('Submit Form')
|
|
2203
|
+
|
|
2204
|
+
// Get ref for the submit button using getRefForLocator
|
|
2205
|
+
const submitBtn = cdpPage!.locator('#submit-btn')
|
|
2206
|
+
const btnAriaRef = await ariaResult.getRefForLocator(submitBtn)
|
|
2207
|
+
console.log('Button ariaRef:', btnAriaRef)
|
|
2208
|
+
expect(btnAriaRef).toBeDefined()
|
|
2209
|
+
expect(btnAriaRef?.role).toBe('button')
|
|
2210
|
+
expect(btnAriaRef?.name).toBe('Submit Form')
|
|
2211
|
+
expect(btnAriaRef?.ref).toMatch(/^e\d+$/)
|
|
2212
|
+
|
|
2213
|
+
// Verify the ref matches what we can use to select
|
|
2214
|
+
const btnFromRef = cdpPage!.locator(`aria-ref=${btnAriaRef?.ref}`)
|
|
2215
|
+
const btnText = await btnFromRef.textContent()
|
|
2216
|
+
expect(btnText).toBe('Submit Form')
|
|
2217
|
+
|
|
2218
|
+
// Test getRefStringForLocator
|
|
2219
|
+
const btnRefStr = await ariaResult.getRefStringForLocator(submitBtn)
|
|
2220
|
+
console.log('Button ref string:', btnRefStr)
|
|
2221
|
+
expect(btnRefStr).toBe(btnAriaRef?.ref)
|
|
2222
|
+
|
|
2223
|
+
// Test link
|
|
2224
|
+
const aboutLink = cdpPage!.locator('a')
|
|
2225
|
+
const linkAriaRef = await ariaResult.getRefForLocator(aboutLink)
|
|
2226
|
+
console.log('Link ariaRef:', linkAriaRef)
|
|
2227
|
+
expect(linkAriaRef).toBeDefined()
|
|
2228
|
+
expect(linkAriaRef?.role).toBe('link')
|
|
2229
|
+
expect(linkAriaRef?.name).toBe('About Us')
|
|
2230
|
+
|
|
2231
|
+
// Verify the link ref works
|
|
2232
|
+
const linkFromRef = cdpPage!.locator(`aria-ref=${linkAriaRef?.ref}`)
|
|
2233
|
+
const linkText = await linkFromRef.textContent()
|
|
2234
|
+
expect(linkText).toBe('About Us')
|
|
2235
|
+
|
|
2236
|
+
// Test input field
|
|
2237
|
+
const inputField = cdpPage!.locator('input')
|
|
2238
|
+
const inputAriaRef = await ariaResult.getRefForLocator(inputField)
|
|
2239
|
+
console.log('Input ariaRef:', inputAriaRef)
|
|
2240
|
+
expect(inputAriaRef).toBeDefined()
|
|
2241
|
+
expect(inputAriaRef?.role).toBe('textbox')
|
|
2242
|
+
|
|
2243
|
+
// Test batch getRefsForLocators - single evaluate call for multiple elements
|
|
2244
|
+
const batchRefs = await ariaResult.getRefsForLocators([submitBtn, aboutLink, inputField])
|
|
2245
|
+
console.log('Batch refs:', batchRefs)
|
|
2246
|
+
expect(batchRefs).toHaveLength(3)
|
|
2247
|
+
expect(batchRefs[0]?.ref).toBe(btnAriaRef?.ref)
|
|
2248
|
+
expect(batchRefs[1]?.ref).toBe(linkAriaRef?.ref)
|
|
2249
|
+
expect(batchRefs[2]?.ref).toBe(inputAriaRef?.ref)
|
|
2250
|
+
|
|
2251
|
+
await browser.close()
|
|
2252
|
+
await page.close()
|
|
2253
|
+
}, 60000)
|
|
2254
|
+
|
|
2255
|
+
it('should show aria ref labels on real pages and save screenshots', async () => {
|
|
2256
|
+
const browserContext = getBrowserContext()
|
|
2257
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
2258
|
+
|
|
2259
|
+
const { showAriaRefLabels, hideAriaRefLabels } = await import('./aria-snapshot.js')
|
|
2260
|
+
const fs = await import('node:fs')
|
|
2261
|
+
const path = await import('node:path')
|
|
2262
|
+
|
|
2263
|
+
// Create assets folder for screenshots
|
|
2264
|
+
const assetsDir = path.join(path.dirname(new URL(import.meta.url).pathname), 'assets')
|
|
2265
|
+
if (!fs.existsSync(assetsDir)) {
|
|
2266
|
+
fs.mkdirSync(assetsDir, { recursive: true })
|
|
2267
|
+
}
|
|
2268
|
+
|
|
2269
|
+
const testPages = [
|
|
2270
|
+
{ name: 'hacker-news', url: 'https://news.ycombinator.com/' },
|
|
2271
|
+
{ name: 'google', url: 'https://www.google.com/' },
|
|
2272
|
+
{ name: 'github', url: 'https://github.com/' },
|
|
2273
|
+
]
|
|
2274
|
+
|
|
2275
|
+
// Create all pages and enable extension for each
|
|
2276
|
+
const pages = await Promise.all(
|
|
2277
|
+
testPages.map(async ({ name, url }) => {
|
|
2278
|
+
const page = await browserContext.newPage()
|
|
2279
|
+
await page.goto(url, { waitUntil: 'domcontentloaded' })
|
|
2280
|
+
return { name, url, page }
|
|
2281
|
+
}),
|
|
2282
|
+
)
|
|
2283
|
+
|
|
2284
|
+
// Enable extension for each tab (must be done sequentially as it uses active tab)
|
|
2285
|
+
for (const { page } of pages) {
|
|
2286
|
+
await page.bringToFront()
|
|
2287
|
+
await serviceWorker.evaluate(async () => {
|
|
2288
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
2289
|
+
})
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
// Connect CDP and process all pages concurrently
|
|
2293
|
+
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
2294
|
+
|
|
2295
|
+
await Promise.all(
|
|
2296
|
+
pages.map(async ({ name, url, page }) => {
|
|
2297
|
+
const cdpPage = browser
|
|
2298
|
+
.contexts()[0]
|
|
2299
|
+
.pages()
|
|
2300
|
+
.find((p) => p.url().includes(new URL(url).hostname))
|
|
2301
|
+
|
|
2302
|
+
if (!cdpPage) {
|
|
2303
|
+
console.log(`Could not find CDP page for ${name}, skipping...`)
|
|
2304
|
+
return
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
// Show aria ref labels
|
|
2308
|
+
const { snapshot, labelCount } = await showAriaRefLabels({ page: cdpPage })
|
|
2309
|
+
console.log(`${name}: ${labelCount} labels shown`)
|
|
2310
|
+
expect(labelCount).toBeGreaterThan(0)
|
|
2311
|
+
|
|
2312
|
+
// Take screenshot with labels visible
|
|
2313
|
+
const screenshot = await cdpPage.screenshot({ type: 'png', fullPage: false })
|
|
2314
|
+
const screenshotPath = path.join(assetsDir, `aria-labels-${name}.png`)
|
|
2315
|
+
fs.writeFileSync(screenshotPath, screenshot)
|
|
2316
|
+
console.log(`Screenshot saved: ${screenshotPath}`)
|
|
2317
|
+
|
|
2318
|
+
// Save snapshot text for reference
|
|
2319
|
+
const snapshotPath = path.join(assetsDir, `aria-labels-${name}-snapshot.txt`)
|
|
2320
|
+
fs.writeFileSync(snapshotPath, snapshot)
|
|
2321
|
+
|
|
2322
|
+
// Verify labels are in DOM
|
|
2323
|
+
const labelElements = await cdpPage.evaluate(() => document.querySelectorAll('.__pw_label__').length)
|
|
2324
|
+
expect(labelElements).toBe(labelCount)
|
|
2325
|
+
|
|
2326
|
+
// Cleanup
|
|
2327
|
+
await hideAriaRefLabels({ page: cdpPage })
|
|
2328
|
+
|
|
2329
|
+
// Verify labels removed
|
|
2330
|
+
const labelsAfterHide = await cdpPage.evaluate(() => document.getElementById('__e2e_pilot_labels__'))
|
|
2331
|
+
expect(labelsAfterHide).toBeNull()
|
|
2332
|
+
|
|
2333
|
+
await page.close()
|
|
2334
|
+
}),
|
|
2335
|
+
)
|
|
2336
|
+
|
|
2337
|
+
await browser.close()
|
|
2338
|
+
console.log(`Screenshots saved to: ${assetsDir}`)
|
|
2339
|
+
}, 120000)
|
|
2340
|
+
|
|
2341
|
+
it('should take screenshot with accessibility labels via MCP execute tool', async () => {
|
|
2342
|
+
const browserContext = getBrowserContext()
|
|
2343
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
2344
|
+
|
|
2345
|
+
const page = await browserContext.newPage()
|
|
2346
|
+
await page.setContent(`
|
|
2347
|
+
<html>
|
|
2348
|
+
<head>
|
|
2349
|
+
<style>
|
|
2350
|
+
body {
|
|
2351
|
+
margin: 0;
|
|
2352
|
+
background: #e8f4f8;
|
|
2353
|
+
position: relative;
|
|
2354
|
+
min-height: 100vh;
|
|
2355
|
+
}
|
|
2356
|
+
.controls {
|
|
2357
|
+
padding: 20px;
|
|
2358
|
+
position: relative;
|
|
2359
|
+
z-index: 10;
|
|
2360
|
+
}
|
|
2361
|
+
.grid-marker {
|
|
2362
|
+
position: absolute;
|
|
2363
|
+
background: rgba(255, 100, 100, 0.3);
|
|
2364
|
+
border: 1px solid #ff6464;
|
|
2365
|
+
font-size: 10px;
|
|
2366
|
+
color: #333;
|
|
2367
|
+
display: flex;
|
|
2368
|
+
align-items: center;
|
|
2369
|
+
justify-content: center;
|
|
2370
|
+
}
|
|
2371
|
+
.h-marker {
|
|
2372
|
+
left: 0;
|
|
2373
|
+
width: 100%;
|
|
2374
|
+
height: 20px;
|
|
2375
|
+
}
|
|
2376
|
+
.v-marker {
|
|
2377
|
+
top: 0;
|
|
2378
|
+
height: 100%;
|
|
2379
|
+
width: 20px;
|
|
2380
|
+
}
|
|
2381
|
+
</style>
|
|
2382
|
+
</head>
|
|
2383
|
+
<body>
|
|
2384
|
+
<div class="controls">
|
|
2385
|
+
<button id="submit-btn">Submit Form</button>
|
|
2386
|
+
<a href="/about">About Us</a>
|
|
2387
|
+
<input type="text" placeholder="Enter your name" />
|
|
2388
|
+
</div>
|
|
2389
|
+
<!-- Horizontal markers every 200px -->
|
|
2390
|
+
<div class="grid-marker h-marker" style="top: 200px;">200px</div>
|
|
2391
|
+
<div class="grid-marker h-marker" style="top: 400px;">400px</div>
|
|
2392
|
+
<div class="grid-marker h-marker" style="top: 600px;">600px</div>
|
|
2393
|
+
<!-- Vertical markers every 200px -->
|
|
2394
|
+
<div class="grid-marker v-marker" style="left: 200px;">200</div>
|
|
2395
|
+
<div class="grid-marker v-marker" style="left: 400px;">400</div>
|
|
2396
|
+
<div class="grid-marker v-marker" style="left: 600px;">600</div>
|
|
2397
|
+
<div class="grid-marker v-marker" style="left: 800px;">800</div>
|
|
2398
|
+
<div class="grid-marker v-marker" style="left: 1000px;">1000</div>
|
|
2399
|
+
<div class="grid-marker v-marker" style="left: 1200px;">1200</div>
|
|
2400
|
+
</body>
|
|
2401
|
+
</html>
|
|
2402
|
+
`)
|
|
2403
|
+
await page.bringToFront()
|
|
2404
|
+
|
|
2405
|
+
await serviceWorker.evaluate(async () => {
|
|
2406
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
2407
|
+
})
|
|
2408
|
+
await new Promise((r) => setTimeout(r, 400))
|
|
2409
|
+
|
|
2410
|
+
// Take screenshot with accessibility labels via MCP
|
|
2411
|
+
const result = await client.callTool({
|
|
2412
|
+
name: 'execute',
|
|
2413
|
+
arguments: {
|
|
2414
|
+
code: js`
|
|
2415
|
+
let testPage;
|
|
2416
|
+
for (const p of context.pages()) {
|
|
2417
|
+
const html = await p.content();
|
|
2418
|
+
if (html.includes('submit-btn')) { testPage = p; break; }
|
|
2419
|
+
}
|
|
2420
|
+
if (!testPage) throw new Error('Test page not found');
|
|
2421
|
+
await screenshotWithAccessibilityLabels({ page: testPage });
|
|
2422
|
+
`,
|
|
2423
|
+
timeout: 15000,
|
|
2424
|
+
},
|
|
2425
|
+
})
|
|
2426
|
+
|
|
2427
|
+
expect(result.isError).toBeFalsy()
|
|
2428
|
+
|
|
2429
|
+
// Verify response has both text and image content
|
|
2430
|
+
const content = result.content as any[]
|
|
2431
|
+
expect(content.length).toBe(2)
|
|
2432
|
+
|
|
2433
|
+
// Check text content
|
|
2434
|
+
const textContent = content.find((c) => c.type === 'text')
|
|
2435
|
+
expect(textContent).toBeDefined()
|
|
2436
|
+
expect(textContent.text).toContain('Screenshot saved to:')
|
|
2437
|
+
expect(textContent.text).toContain('.jpg')
|
|
2438
|
+
expect(textContent.text).toContain('Labels shown:')
|
|
2439
|
+
expect(textContent.text).toContain('Accessibility snapshot:')
|
|
2440
|
+
expect(textContent.text).toContain('Submit Form')
|
|
2441
|
+
|
|
2442
|
+
// Check image content
|
|
2443
|
+
const imageContent = content.find((c) => c.type === 'image')
|
|
2444
|
+
expect(imageContent).toBeDefined()
|
|
2445
|
+
expect(imageContent.mimeType).toBe('image/jpeg')
|
|
2446
|
+
expect(imageContent.data).toBeDefined()
|
|
2447
|
+
expect(imageContent.data.length).toBeGreaterThan(100) // base64 data should be substantial
|
|
2448
|
+
|
|
2449
|
+
// Verify the image is valid JPEG by checking base64
|
|
2450
|
+
const buffer = Buffer.from(imageContent.data, 'base64')
|
|
2451
|
+
const dimensions = imageSize(buffer)
|
|
2452
|
+
|
|
2453
|
+
// Get actual viewport size from page
|
|
2454
|
+
const viewport = await page.evaluate(() => ({
|
|
2455
|
+
innerWidth: window.innerWidth,
|
|
2456
|
+
innerHeight: window.innerHeight,
|
|
2457
|
+
outerWidth: window.outerWidth,
|
|
2458
|
+
outerHeight: window.outerHeight,
|
|
2459
|
+
}))
|
|
2460
|
+
console.log('Screenshot dimensions:', dimensions.width, 'x', dimensions.height)
|
|
2461
|
+
console.log('Window viewport:', viewport)
|
|
2462
|
+
|
|
2463
|
+
expect(dimensions.type).toBe('jpg')
|
|
2464
|
+
expect(dimensions.width).toBeGreaterThan(0)
|
|
2465
|
+
expect(dimensions.height).toBeGreaterThan(0)
|
|
2466
|
+
|
|
2467
|
+
await page.close()
|
|
2468
|
+
}, 60000)
|
|
2469
|
+
|
|
2470
|
+
it('should capture network requests during execute', async () => {
|
|
2471
|
+
const browserContext = getBrowserContext()
|
|
2472
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
2473
|
+
|
|
2474
|
+
const page = await browserContext.newPage()
|
|
2475
|
+
await page.setContent(`
|
|
2476
|
+
<html>
|
|
2477
|
+
<body>
|
|
2478
|
+
<button id="fetch-btn">Fetch Data</button>
|
|
2479
|
+
<div id="result"></div>
|
|
2480
|
+
<script>
|
|
2481
|
+
document.getElementById('fetch-btn').addEventListener('click', async () => {
|
|
2482
|
+
const response = await fetch('https://httpbin.org/post', {
|
|
2483
|
+
method: 'POST',
|
|
2484
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2485
|
+
body: JSON.stringify({ test: 'data' })
|
|
2486
|
+
});
|
|
2487
|
+
const data = await response.json();
|
|
2488
|
+
document.getElementById('result').textContent = JSON.stringify(data);
|
|
2489
|
+
});
|
|
2490
|
+
</script>
|
|
2491
|
+
</body>
|
|
2492
|
+
</html>
|
|
2493
|
+
`)
|
|
2494
|
+
await page.bringToFront()
|
|
2495
|
+
|
|
2496
|
+
await serviceWorker.evaluate(async () => {
|
|
2497
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
2498
|
+
})
|
|
2499
|
+
await new Promise((r) => setTimeout(r, 400))
|
|
2500
|
+
|
|
2501
|
+
// Execute code that triggers a network request
|
|
2502
|
+
const result = await client.callTool({
|
|
2503
|
+
name: 'execute',
|
|
2504
|
+
arguments: {
|
|
2505
|
+
intend: 'Test network capture',
|
|
2506
|
+
code: js`
|
|
2507
|
+
let testPage;
|
|
2508
|
+
for (const p of context.pages()) {
|
|
2509
|
+
const html = await p.content();
|
|
2510
|
+
if (html.includes('fetch-btn')) { testPage = p; break; }
|
|
2511
|
+
}
|
|
2512
|
+
if (!testPage) throw new Error('Test page not found');
|
|
2513
|
+
await testPage.click('#fetch-btn');
|
|
2514
|
+
await testPage.waitForSelector('#result:not(:empty)', { timeout: 10000 });
|
|
2515
|
+
`,
|
|
2516
|
+
timeout: 15000,
|
|
2517
|
+
},
|
|
2518
|
+
})
|
|
2519
|
+
|
|
2520
|
+
expect(result.isError).toBeFalsy()
|
|
2521
|
+
|
|
2522
|
+
// Parse the response to get snapshot path
|
|
2523
|
+
const content = result.content as any[]
|
|
2524
|
+
const textContent = content.find((c) => c.type === 'text')
|
|
2525
|
+
expect(textContent).toBeDefined()
|
|
2526
|
+
|
|
2527
|
+
// Extract snapshot path from response
|
|
2528
|
+
const match = textContent.text.match(/Full snapshot: (.+\.e2e-pilot\/snapshots\/[^\/]+)\/full\.txt/)
|
|
2529
|
+
expect(match).toBeTruthy()
|
|
2530
|
+
|
|
2531
|
+
const snapshotDir = match![1]
|
|
2532
|
+
const networksPath = path.join(snapshotDir, 'networks.jsonl')
|
|
2533
|
+
|
|
2534
|
+
// Check that networks.jsonl was created
|
|
2535
|
+
expect(fs.existsSync(networksPath)).toBe(true)
|
|
2536
|
+
|
|
2537
|
+
// Read and verify the network log
|
|
2538
|
+
const networksContent = fs.readFileSync(networksPath, 'utf-8')
|
|
2539
|
+
const lines = networksContent.trim().split('\n')
|
|
2540
|
+
expect(lines.length).toBeGreaterThan(0)
|
|
2541
|
+
|
|
2542
|
+
// Parse and verify the captured request
|
|
2543
|
+
const networkEntry = JSON.parse(lines[0])
|
|
2544
|
+
expect(networkEntry.method).toBe('POST')
|
|
2545
|
+
expect(networkEntry.url).toContain('httpbin.org/post')
|
|
2546
|
+
expect(networkEntry.status).toBe(200)
|
|
2547
|
+
expect(networkEntry.reqBody).toContain('test')
|
|
2548
|
+
expect(networkEntry.resBody).toBeDefined()
|
|
2549
|
+
|
|
2550
|
+
await page.close()
|
|
2551
|
+
}, 60000)
|
|
2552
|
+
})
|
|
2553
|
+
|
|
2554
|
+
function tryJsonParse(str: string) {
|
|
2555
|
+
try {
|
|
2556
|
+
return JSON.parse(str)
|
|
2557
|
+
} catch {
|
|
2558
|
+
return str
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2562
|
+
describe('CDP Session Tests', () => {
|
|
2563
|
+
let testCtx: TestContext | null = null
|
|
2564
|
+
|
|
2565
|
+
beforeAll(async () => {
|
|
2566
|
+
testCtx = await setupTestContext({ tempDirPrefix: 'pw-cdp-test-' })
|
|
2567
|
+
|
|
2568
|
+
const serviceWorker = await getExtensionServiceWorker(testCtx.browserContext)
|
|
2569
|
+
await serviceWorker.evaluate(async () => {
|
|
2570
|
+
await globalThis.disconnectEverything()
|
|
2571
|
+
})
|
|
2572
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
2573
|
+
}, 600000)
|
|
2574
|
+
|
|
2575
|
+
afterAll(async () => {
|
|
2576
|
+
await cleanupTestContext(testCtx)
|
|
2577
|
+
testCtx = null
|
|
2578
|
+
})
|
|
2579
|
+
|
|
2580
|
+
const getBrowserContext = () => {
|
|
2581
|
+
if (!testCtx?.browserContext) throw new Error('Browser not initialized')
|
|
2582
|
+
return testCtx.browserContext
|
|
2583
|
+
}
|
|
2584
|
+
|
|
2585
|
+
it('should use Debugger class to set breakpoints and inspect variables', async () => {
|
|
2586
|
+
const browserContext = getBrowserContext()
|
|
2587
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
2588
|
+
|
|
2589
|
+
const page = await browserContext.newPage()
|
|
2590
|
+
await page.goto('https://example.com/')
|
|
2591
|
+
await page.bringToFront()
|
|
2592
|
+
|
|
2593
|
+
await serviceWorker.evaluate(async () => {
|
|
2594
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
2595
|
+
})
|
|
2596
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
2597
|
+
|
|
2598
|
+
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
2599
|
+
const cdpPage = browser
|
|
2600
|
+
.contexts()[0]
|
|
2601
|
+
.pages()
|
|
2602
|
+
.find((p) => p.url().includes('example.com'))
|
|
2603
|
+
expect(cdpPage).toBeDefined()
|
|
2604
|
+
|
|
2605
|
+
const wsUrl = getCdpUrl({ port: TEST_PORT })
|
|
2606
|
+
const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
|
|
2607
|
+
const dbg = new Debugger({ cdp: cdpSession })
|
|
2608
|
+
|
|
2609
|
+
await dbg.enable()
|
|
2610
|
+
|
|
2611
|
+
expect(dbg.isPaused()).toBe(false)
|
|
2612
|
+
|
|
2613
|
+
const pausedPromise = new Promise<void>((resolve) => {
|
|
2614
|
+
cdpSession.on('Debugger.paused', () => {
|
|
2615
|
+
resolve()
|
|
2616
|
+
})
|
|
2617
|
+
})
|
|
2618
|
+
|
|
2619
|
+
cdpPage!.evaluate(`
|
|
2620
|
+
(function testFunction() {
|
|
2621
|
+
const localVar = 'hello';
|
|
2622
|
+
const numberVar = 42;
|
|
2623
|
+
debugger;
|
|
2624
|
+
return localVar + numberVar;
|
|
2625
|
+
})()
|
|
2626
|
+
`)
|
|
2627
|
+
|
|
2628
|
+
await Promise.race([
|
|
2629
|
+
pausedPromise,
|
|
2630
|
+
new Promise<never>((_, reject) => setTimeout(() => reject(new Error('Debugger.paused timeout')), 5000)),
|
|
2631
|
+
])
|
|
2632
|
+
|
|
2633
|
+
expect(dbg.isPaused()).toBe(true)
|
|
2634
|
+
|
|
2635
|
+
const location = await dbg.getLocation()
|
|
2636
|
+
expect(location.callstack[0].functionName).toBe('testFunction')
|
|
2637
|
+
expect(location.sourceContext).toContain('debugger')
|
|
2638
|
+
|
|
2639
|
+
const vars = await dbg.inspectLocalVariables()
|
|
2640
|
+
expect(vars).toMatchInlineSnapshot(`
|
|
2641
|
+
{
|
|
2642
|
+
"localVar": "hello",
|
|
2643
|
+
"numberVar": 42,
|
|
2644
|
+
}
|
|
2645
|
+
`)
|
|
2646
|
+
|
|
2647
|
+
const evalResult = await dbg.evaluate({ expression: 'localVar + " world"' })
|
|
2648
|
+
expect(evalResult.value).toBe('hello world')
|
|
2649
|
+
|
|
2650
|
+
await dbg.resume()
|
|
2651
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
2652
|
+
expect(dbg.isPaused()).toBe(false)
|
|
2653
|
+
|
|
2654
|
+
cdpSession.close()
|
|
2655
|
+
await browser.close()
|
|
2656
|
+
await page.close()
|
|
2657
|
+
}, 60000)
|
|
2658
|
+
|
|
2659
|
+
it('should list scripts with Debugger class', async () => {
|
|
2660
|
+
const browserContext = getBrowserContext()
|
|
2661
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
2662
|
+
|
|
2663
|
+
const page = await browserContext.newPage()
|
|
2664
|
+
await page.setContent(`
|
|
2665
|
+
<html>
|
|
2666
|
+
<head>
|
|
2667
|
+
<script src="data:text/javascript,function testScript() { return 42; }"></script>
|
|
2668
|
+
</head>
|
|
2669
|
+
<body><h1>Script Test</h1></body>
|
|
2670
|
+
</html>
|
|
2671
|
+
`)
|
|
2672
|
+
await page.bringToFront()
|
|
2673
|
+
|
|
2674
|
+
await serviceWorker.evaluate(async () => {
|
|
2675
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
2676
|
+
})
|
|
2677
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
2678
|
+
|
|
2679
|
+
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
2680
|
+
let cdpPage
|
|
2681
|
+
for (const p of browser.contexts()[0].pages()) {
|
|
2682
|
+
const html = await p.content()
|
|
2683
|
+
if (html.includes('Script Test')) {
|
|
2684
|
+
cdpPage = p
|
|
2685
|
+
break
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
expect(cdpPage).toBeDefined()
|
|
2689
|
+
|
|
2690
|
+
const wsUrl = getCdpUrl({ port: TEST_PORT })
|
|
2691
|
+
const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
|
|
2692
|
+
const dbg = new Debugger({ cdp: cdpSession })
|
|
2693
|
+
|
|
2694
|
+
const scripts = await dbg.listScripts()
|
|
2695
|
+
expect(scripts.length).toBeGreaterThan(0)
|
|
2696
|
+
expect(scripts[0]).toHaveProperty('scriptId')
|
|
2697
|
+
expect(scripts[0]).toHaveProperty('url')
|
|
2698
|
+
|
|
2699
|
+
const dataScripts = await dbg.listScripts({ search: 'data:' })
|
|
2700
|
+
expect(dataScripts.length).toBeGreaterThan(0)
|
|
2701
|
+
|
|
2702
|
+
cdpSession.close()
|
|
2703
|
+
await browser.close()
|
|
2704
|
+
await page.close()
|
|
2705
|
+
}, 60000)
|
|
2706
|
+
|
|
2707
|
+
it('should manage breakpoints with Debugger class', async () => {
|
|
2708
|
+
const browserContext = getBrowserContext()
|
|
2709
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
2710
|
+
|
|
2711
|
+
const page = await browserContext.newPage()
|
|
2712
|
+
await page.setContent(`
|
|
2713
|
+
<html>
|
|
2714
|
+
<head>
|
|
2715
|
+
<script src="data:text/javascript,function testFunc() { return 42; }"></script>
|
|
2716
|
+
</head>
|
|
2717
|
+
<body></body>
|
|
2718
|
+
</html>
|
|
2719
|
+
`)
|
|
2720
|
+
await page.bringToFront()
|
|
2721
|
+
|
|
2722
|
+
await serviceWorker.evaluate(async () => {
|
|
2723
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
2724
|
+
})
|
|
2725
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
2726
|
+
|
|
2727
|
+
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
2728
|
+
let cdpPage
|
|
2729
|
+
for (const p of browser.contexts()[0].pages()) {
|
|
2730
|
+
const html = await p.content()
|
|
2731
|
+
if (html.includes('testFunc')) {
|
|
2732
|
+
cdpPage = p
|
|
2733
|
+
break
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
expect(cdpPage).toBeDefined()
|
|
2737
|
+
|
|
2738
|
+
const wsUrl = getCdpUrl({ port: TEST_PORT })
|
|
2739
|
+
const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
|
|
2740
|
+
const dbg = new Debugger({ cdp: cdpSession })
|
|
2741
|
+
|
|
2742
|
+
await dbg.enable()
|
|
2743
|
+
|
|
2744
|
+
expect(dbg.listBreakpoints()).toHaveLength(0)
|
|
2745
|
+
|
|
2746
|
+
const bpId = await dbg.setBreakpoint({ file: 'https://example.com/test.js', line: 1 })
|
|
2747
|
+
expect(typeof bpId).toBe('string')
|
|
2748
|
+
expect(dbg.listBreakpoints()).toHaveLength(1)
|
|
2749
|
+
expect(dbg.listBreakpoints()[0]).toMatchObject({
|
|
2750
|
+
id: bpId,
|
|
2751
|
+
file: 'https://example.com/test.js',
|
|
2752
|
+
line: 1,
|
|
2753
|
+
})
|
|
2754
|
+
|
|
2755
|
+
await dbg.deleteBreakpoint({ breakpointId: bpId })
|
|
2756
|
+
expect(dbg.listBreakpoints()).toHaveLength(0)
|
|
2757
|
+
|
|
2758
|
+
cdpSession.close()
|
|
2759
|
+
await browser.close()
|
|
2760
|
+
await page.close()
|
|
2761
|
+
}, 60000)
|
|
2762
|
+
|
|
2763
|
+
it('should step through code with Debugger class', async () => {
|
|
2764
|
+
const browserContext = getBrowserContext()
|
|
2765
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
2766
|
+
|
|
2767
|
+
const page = await browserContext.newPage()
|
|
2768
|
+
await page.goto('https://example.com/')
|
|
2769
|
+
await page.bringToFront()
|
|
2770
|
+
|
|
2771
|
+
await serviceWorker.evaluate(async () => {
|
|
2772
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
2773
|
+
})
|
|
2774
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
2775
|
+
|
|
2776
|
+
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
2777
|
+
const cdpPage = browser
|
|
2778
|
+
.contexts()[0]
|
|
2779
|
+
.pages()
|
|
2780
|
+
.find((p) => p.url().includes('example.com'))
|
|
2781
|
+
expect(cdpPage).toBeDefined()
|
|
2782
|
+
|
|
2783
|
+
const wsUrl = getCdpUrl({ port: TEST_PORT })
|
|
2784
|
+
const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
|
|
2785
|
+
const dbg = new Debugger({ cdp: cdpSession })
|
|
2786
|
+
|
|
2787
|
+
await dbg.enable()
|
|
2788
|
+
|
|
2789
|
+
const pausedPromise = new Promise<void>((resolve) => {
|
|
2790
|
+
cdpSession.on('Debugger.paused', () => resolve())
|
|
2791
|
+
})
|
|
2792
|
+
|
|
2793
|
+
cdpPage!.evaluate(`
|
|
2794
|
+
(function outer() {
|
|
2795
|
+
function inner() {
|
|
2796
|
+
const x = 1;
|
|
2797
|
+
debugger;
|
|
2798
|
+
const y = 2;
|
|
2799
|
+
return x + y;
|
|
2800
|
+
}
|
|
2801
|
+
const result = inner();
|
|
2802
|
+
return result;
|
|
2803
|
+
})()
|
|
2804
|
+
`)
|
|
2805
|
+
|
|
2806
|
+
await pausedPromise
|
|
2807
|
+
expect(dbg.isPaused()).toBe(true)
|
|
2808
|
+
|
|
2809
|
+
const location1 = await dbg.getLocation()
|
|
2810
|
+
expect(location1.callstack.length).toBeGreaterThanOrEqual(2)
|
|
2811
|
+
expect(location1.callstack[0].functionName).toBe('inner')
|
|
2812
|
+
expect(location1.callstack[1].functionName).toBe('outer')
|
|
2813
|
+
|
|
2814
|
+
const stepOverPromise = new Promise<void>((resolve) => {
|
|
2815
|
+
cdpSession.on('Debugger.paused', () => resolve())
|
|
2816
|
+
})
|
|
2817
|
+
await dbg.stepOver()
|
|
2818
|
+
await stepOverPromise
|
|
2819
|
+
|
|
2820
|
+
const location2 = await dbg.getLocation()
|
|
2821
|
+
expect(location2.lineNumber).toBeGreaterThan(location1.lineNumber)
|
|
2822
|
+
|
|
2823
|
+
const stepOutPromise = new Promise<void>((resolve) => {
|
|
2824
|
+
cdpSession.on('Debugger.paused', () => resolve())
|
|
2825
|
+
})
|
|
2826
|
+
await dbg.stepOut()
|
|
2827
|
+
await stepOutPromise
|
|
2828
|
+
|
|
2829
|
+
const location3 = await dbg.getLocation()
|
|
2830
|
+
expect(location3.callstack[0].functionName).toBe('outer')
|
|
2831
|
+
|
|
2832
|
+
await dbg.resume()
|
|
2833
|
+
|
|
2834
|
+
cdpSession.close()
|
|
2835
|
+
await browser.close()
|
|
2836
|
+
await page.close()
|
|
2837
|
+
}, 60000)
|
|
2838
|
+
|
|
2839
|
+
it('should profile JavaScript execution using CDP Profiler', async () => {
|
|
2840
|
+
const browserContext = getBrowserContext()
|
|
2841
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
2842
|
+
|
|
2843
|
+
const page = await browserContext.newPage()
|
|
2844
|
+
await page.goto('https://example.com/')
|
|
2845
|
+
await page.bringToFront()
|
|
2846
|
+
|
|
2847
|
+
await serviceWorker.evaluate(async () => {
|
|
2848
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
2849
|
+
})
|
|
2850
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
2851
|
+
|
|
2852
|
+
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
2853
|
+
const cdpPage = browser
|
|
2854
|
+
.contexts()[0]
|
|
2855
|
+
.pages()
|
|
2856
|
+
.find((p) => p.url().includes('example.com'))
|
|
2857
|
+
expect(cdpPage).toBeDefined()
|
|
2858
|
+
|
|
2859
|
+
const wsUrl = getCdpUrl({ port: TEST_PORT })
|
|
2860
|
+
const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
|
|
2861
|
+
await cdpSession.send('Profiler.enable')
|
|
2862
|
+
await cdpSession.send('Profiler.start')
|
|
2863
|
+
|
|
2864
|
+
await cdpPage!.evaluate(`
|
|
2865
|
+
(() => {
|
|
2866
|
+
function fibonacci(n) {
|
|
2867
|
+
if (n <= 1) return n
|
|
2868
|
+
return fibonacci(n - 1) + fibonacci(n - 2)
|
|
2869
|
+
}
|
|
2870
|
+
for (let i = 0; i < 5; i++) {
|
|
2871
|
+
fibonacci(20)
|
|
2872
|
+
}
|
|
2873
|
+
for (let i = 0; i < 1000; i++) {
|
|
2874
|
+
document.querySelectorAll('*')
|
|
2875
|
+
}
|
|
2876
|
+
})()
|
|
2877
|
+
`)
|
|
2878
|
+
|
|
2879
|
+
const stopResult = await cdpSession.send('Profiler.stop')
|
|
2880
|
+
const profile = stopResult.profile
|
|
2881
|
+
|
|
2882
|
+
const functionNames = profile.nodes
|
|
2883
|
+
.map((n) => n.callFrame.functionName)
|
|
2884
|
+
.filter((name) => name && name.length > 0)
|
|
2885
|
+
.slice(0, 10)
|
|
2886
|
+
|
|
2887
|
+
expect(profile.nodes.length).toBeGreaterThan(0)
|
|
2888
|
+
expect(profile.endTime - profile.startTime).toBeGreaterThan(0)
|
|
2889
|
+
expect(functionNames.every((name) => typeof name === 'string')).toBe(true)
|
|
2890
|
+
|
|
2891
|
+
await cdpSession.send('Profiler.disable')
|
|
2892
|
+
cdpSession.close()
|
|
2893
|
+
await browser.close()
|
|
2894
|
+
await page.close()
|
|
2895
|
+
}, 60000)
|
|
2896
|
+
|
|
2897
|
+
it('should update Target.getTargets URL after page navigation', async () => {
|
|
2898
|
+
const browserContext = getBrowserContext()
|
|
2899
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
2900
|
+
|
|
2901
|
+
// Clear any existing connected tabs from previous tests
|
|
2902
|
+
await serviceWorker.evaluate(async () => {
|
|
2903
|
+
await globalThis.disconnectEverything()
|
|
2904
|
+
})
|
|
2905
|
+
|
|
2906
|
+
const page = await browserContext.newPage()
|
|
2907
|
+
await page.goto('https://example.com/')
|
|
2908
|
+
await page.bringToFront()
|
|
2909
|
+
|
|
2910
|
+
await serviceWorker.evaluate(async () => {
|
|
2911
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
2912
|
+
})
|
|
2913
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
2914
|
+
|
|
2915
|
+
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
2916
|
+
const cdpPage = browser
|
|
2917
|
+
.contexts()[0]
|
|
2918
|
+
.pages()
|
|
2919
|
+
.find((p) => p.url().includes('example.com'))
|
|
2920
|
+
expect(cdpPage).toBeDefined()
|
|
2921
|
+
|
|
2922
|
+
const wsUrl = getCdpUrl({ port: TEST_PORT })
|
|
2923
|
+
const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
|
|
2924
|
+
|
|
2925
|
+
const initialTargets = await cdpSession.send('Target.getTargets')
|
|
2926
|
+
const initialPageTarget = initialTargets.targetInfos.find((t) => t.type === 'page' && t.url.includes('example.com'))
|
|
2927
|
+
expect(initialPageTarget?.url).toBe('https://example.com/')
|
|
2928
|
+
|
|
2929
|
+
await cdpPage!.goto('https://example.org/', { waitUntil: 'domcontentloaded' })
|
|
2930
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
2931
|
+
|
|
2932
|
+
const afterNavTargets = await cdpSession.send('Target.getTargets')
|
|
2933
|
+
const allPageTargets = afterNavTargets.targetInfos.filter((t) => t.type === 'page')
|
|
2934
|
+
|
|
2935
|
+
const aboutBlankTargets = allPageTargets.filter((t) => t.url === 'about:blank')
|
|
2936
|
+
expect(aboutBlankTargets).toHaveLength(0)
|
|
2937
|
+
|
|
2938
|
+
const exampleComTargets = allPageTargets.filter((t) => t.url.includes('example.com'))
|
|
2939
|
+
expect(exampleComTargets).toHaveLength(0)
|
|
2940
|
+
|
|
2941
|
+
const exampleOrgTargets = allPageTargets.filter((t) => t.url.includes('example.org'))
|
|
2942
|
+
expect(exampleOrgTargets).toHaveLength(1)
|
|
2943
|
+
|
|
2944
|
+
cdpSession.close()
|
|
2945
|
+
await browser.close()
|
|
2946
|
+
await page.close()
|
|
2947
|
+
}, 60000)
|
|
2948
|
+
|
|
2949
|
+
it('should return correct targets for multiple pages via Target.getTargets', async () => {
|
|
2950
|
+
const browserContext = getBrowserContext()
|
|
2951
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
2952
|
+
|
|
2953
|
+
// Clear any existing connected tabs from previous tests
|
|
2954
|
+
await serviceWorker.evaluate(async () => {
|
|
2955
|
+
await globalThis.disconnectEverything()
|
|
2956
|
+
})
|
|
2957
|
+
|
|
2958
|
+
const page1 = await browserContext.newPage()
|
|
2959
|
+
await page1.goto('https://example.com/')
|
|
2960
|
+
await page1.bringToFront()
|
|
2961
|
+
await serviceWorker.evaluate(async () => {
|
|
2962
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
2963
|
+
})
|
|
2964
|
+
|
|
2965
|
+
const page2 = await browserContext.newPage()
|
|
2966
|
+
await page2.goto('https://example.org/')
|
|
2967
|
+
await page2.bringToFront()
|
|
2968
|
+
await serviceWorker.evaluate(async () => {
|
|
2969
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
2970
|
+
})
|
|
2971
|
+
|
|
2972
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
2973
|
+
|
|
2974
|
+
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
2975
|
+
const cdpPage = browser
|
|
2976
|
+
.contexts()[0]
|
|
2977
|
+
.pages()
|
|
2978
|
+
.find((p) => p.url().includes('example.com'))
|
|
2979
|
+
expect(cdpPage).toBeDefined()
|
|
2980
|
+
|
|
2981
|
+
const wsUrl = getCdpUrl({ port: TEST_PORT })
|
|
2982
|
+
const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
|
|
2983
|
+
|
|
2984
|
+
const { targetInfos } = await cdpSession.send('Target.getTargets')
|
|
2985
|
+
const allPageTargets = targetInfos.filter((t) => t.type === 'page')
|
|
2986
|
+
|
|
2987
|
+
const aboutBlankTargets = allPageTargets.filter((t) => t.url === 'about:blank')
|
|
2988
|
+
expect(aboutBlankTargets).toHaveLength(0)
|
|
2989
|
+
|
|
2990
|
+
const pageTargets = allPageTargets
|
|
2991
|
+
.map((t) => ({ type: t.type, url: t.url }))
|
|
2992
|
+
.sort((a, b) => a.url.localeCompare(b.url))
|
|
2993
|
+
|
|
2994
|
+
expect(pageTargets).toMatchInlineSnapshot(`
|
|
2995
|
+
[
|
|
2996
|
+
{
|
|
2997
|
+
"type": "page",
|
|
2998
|
+
"url": "https://example.com/",
|
|
2999
|
+
},
|
|
3000
|
+
{
|
|
3001
|
+
"type": "page",
|
|
3002
|
+
"url": "https://example.org/",
|
|
3003
|
+
},
|
|
3004
|
+
]
|
|
3005
|
+
`)
|
|
3006
|
+
|
|
3007
|
+
cdpSession.close()
|
|
3008
|
+
await browser.close()
|
|
3009
|
+
await page1.close()
|
|
3010
|
+
await page2.close()
|
|
3011
|
+
}, 60000)
|
|
3012
|
+
|
|
3013
|
+
it('should create CDP session for page after navigation', async () => {
|
|
3014
|
+
const browserContext = getBrowserContext()
|
|
3015
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
3016
|
+
|
|
3017
|
+
const page = await browserContext.newPage()
|
|
3018
|
+
await page.goto('https://example.com/')
|
|
3019
|
+
await page.bringToFront()
|
|
3020
|
+
|
|
3021
|
+
await serviceWorker.evaluate(async () => {
|
|
3022
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
3023
|
+
})
|
|
3024
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
3025
|
+
|
|
3026
|
+
await page.goto('https://example.org/', { waitUntil: 'domcontentloaded' })
|
|
3027
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
3028
|
+
|
|
3029
|
+
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
3030
|
+
const cdpPage = browser
|
|
3031
|
+
.contexts()[0]
|
|
3032
|
+
.pages()
|
|
3033
|
+
.find((p) => p.url().includes('example.org'))
|
|
3034
|
+
expect(cdpPage).toBeDefined()
|
|
3035
|
+
|
|
3036
|
+
const wsUrl = getCdpUrl({ port: TEST_PORT })
|
|
3037
|
+
const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
|
|
3038
|
+
|
|
3039
|
+
const evalResult = await cdpSession.send('Runtime.evaluate', {
|
|
3040
|
+
expression: 'document.title',
|
|
3041
|
+
returnByValue: true,
|
|
3042
|
+
})
|
|
3043
|
+
expect(evalResult.result.value).toContain('Example Domain')
|
|
3044
|
+
|
|
3045
|
+
cdpSession.close()
|
|
3046
|
+
await browser.close()
|
|
3047
|
+
await page.close()
|
|
3048
|
+
}, 60000)
|
|
3049
|
+
|
|
3050
|
+
it('should maintain CDP session functionality after page URL change', async () => {
|
|
3051
|
+
const browserContext = getBrowserContext()
|
|
3052
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
3053
|
+
|
|
3054
|
+
const page = await browserContext.newPage()
|
|
3055
|
+
const initialUrl = 'https://example.com/'
|
|
3056
|
+
await page.goto(initialUrl)
|
|
3057
|
+
await page.bringToFront()
|
|
3058
|
+
|
|
3059
|
+
await serviceWorker.evaluate(async () => {
|
|
3060
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
3061
|
+
})
|
|
3062
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
3063
|
+
|
|
3064
|
+
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
3065
|
+
const cdpPage = browser
|
|
3066
|
+
.contexts()[0]
|
|
3067
|
+
.pages()
|
|
3068
|
+
.find((p) => p.url().includes('example.com'))
|
|
3069
|
+
expect(cdpPage).toBeDefined()
|
|
3070
|
+
|
|
3071
|
+
const wsUrl = getCdpUrl({ port: TEST_PORT })
|
|
3072
|
+
const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
|
|
3073
|
+
|
|
3074
|
+
const initialEvalResult = await cdpSession.send('Runtime.evaluate', {
|
|
3075
|
+
expression: 'document.title',
|
|
3076
|
+
returnByValue: true,
|
|
3077
|
+
})
|
|
3078
|
+
expect(initialEvalResult.result.value).toBe('Example Domain')
|
|
3079
|
+
|
|
3080
|
+
const newUrl = 'https://example.org/'
|
|
3081
|
+
await cdpPage!.goto(newUrl, { waitUntil: 'domcontentloaded' })
|
|
3082
|
+
|
|
3083
|
+
expect(cdpPage!.url()).toBe(newUrl)
|
|
3084
|
+
|
|
3085
|
+
const layoutMetrics = await cdpSession.send('Page.getLayoutMetrics')
|
|
3086
|
+
expect(layoutMetrics.cssVisualViewport).toBeDefined()
|
|
3087
|
+
expect(layoutMetrics.cssVisualViewport.clientWidth).toBeGreaterThan(0)
|
|
3088
|
+
|
|
3089
|
+
const afterNavEvalResult = await cdpSession.send('Runtime.evaluate', {
|
|
3090
|
+
expression: 'document.title',
|
|
3091
|
+
returnByValue: true,
|
|
3092
|
+
})
|
|
3093
|
+
expect(afterNavEvalResult.result.value).toContain('Example Domain')
|
|
3094
|
+
|
|
3095
|
+
const locationResult = await cdpSession.send('Runtime.evaluate', {
|
|
3096
|
+
expression: 'window.location.href',
|
|
3097
|
+
returnByValue: true,
|
|
3098
|
+
})
|
|
3099
|
+
expect(locationResult.result.value).toBe(newUrl)
|
|
3100
|
+
|
|
3101
|
+
cdpSession.close()
|
|
3102
|
+
await browser.close()
|
|
3103
|
+
await page.close()
|
|
3104
|
+
}, 60000)
|
|
3105
|
+
|
|
3106
|
+
it('should pause on all exceptions with setPauseOnExceptions', async () => {
|
|
3107
|
+
const browserContext = getBrowserContext()
|
|
3108
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
3109
|
+
|
|
3110
|
+
const page = await browserContext.newPage()
|
|
3111
|
+
await page.goto('https://example.com/')
|
|
3112
|
+
await page.bringToFront()
|
|
3113
|
+
|
|
3114
|
+
await serviceWorker.evaluate(async () => {
|
|
3115
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
3116
|
+
})
|
|
3117
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
3118
|
+
|
|
3119
|
+
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
3120
|
+
const cdpPage = browser
|
|
3121
|
+
.contexts()[0]
|
|
3122
|
+
.pages()
|
|
3123
|
+
.find((p) => p.url().includes('example.com'))
|
|
3124
|
+
expect(cdpPage).toBeDefined()
|
|
3125
|
+
|
|
3126
|
+
const wsUrl = getCdpUrl({ port: TEST_PORT })
|
|
3127
|
+
const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
|
|
3128
|
+
const dbg = new Debugger({ cdp: cdpSession })
|
|
3129
|
+
|
|
3130
|
+
await dbg.enable()
|
|
3131
|
+
await dbg.setPauseOnExceptions({ state: 'all' })
|
|
3132
|
+
|
|
3133
|
+
const pausedPromise = new Promise<void>((resolve) => {
|
|
3134
|
+
cdpSession.on('Debugger.paused', () => resolve())
|
|
3135
|
+
})
|
|
3136
|
+
|
|
3137
|
+
cdpPage!
|
|
3138
|
+
.evaluate(
|
|
3139
|
+
`
|
|
3140
|
+
(function() {
|
|
3141
|
+
try {
|
|
3142
|
+
throw new Error('Caught test error');
|
|
3143
|
+
} catch (e) {
|
|
3144
|
+
// caught but should still pause with state 'all'
|
|
3145
|
+
}
|
|
3146
|
+
})()
|
|
3147
|
+
`,
|
|
3148
|
+
)
|
|
3149
|
+
.catch(() => {})
|
|
3150
|
+
|
|
3151
|
+
await Promise.race([
|
|
3152
|
+
pausedPromise,
|
|
3153
|
+
new Promise<never>((_, reject) => setTimeout(() => reject(new Error('Debugger.paused timeout')), 5000)),
|
|
3154
|
+
])
|
|
3155
|
+
|
|
3156
|
+
expect(dbg.isPaused()).toBe(true)
|
|
3157
|
+
|
|
3158
|
+
const location = await dbg.getLocation()
|
|
3159
|
+
expect(location.sourceContext).toContain('throw')
|
|
3160
|
+
|
|
3161
|
+
await dbg.resume()
|
|
3162
|
+
|
|
3163
|
+
await dbg.setPauseOnExceptions({ state: 'none' })
|
|
3164
|
+
|
|
3165
|
+
cdpSession.close()
|
|
3166
|
+
await browser.close()
|
|
3167
|
+
await page.close()
|
|
3168
|
+
}, 60000)
|
|
3169
|
+
|
|
3170
|
+
it('should inspect local and global variables with inline snapshots', async () => {
|
|
3171
|
+
const browserContext = getBrowserContext()
|
|
3172
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
3173
|
+
|
|
3174
|
+
const page = await browserContext.newPage()
|
|
3175
|
+
await page.setContent(`
|
|
3176
|
+
<html>
|
|
3177
|
+
<head>
|
|
3178
|
+
<script>
|
|
3179
|
+
const GLOBAL_CONFIG = 'production';
|
|
3180
|
+
function runTest() {
|
|
3181
|
+
const userName = 'Alice';
|
|
3182
|
+
const userAge = 25;
|
|
3183
|
+
const settings = { theme: 'dark', lang: 'en' };
|
|
3184
|
+
const scores = [10, 20, 30];
|
|
3185
|
+
debugger;
|
|
3186
|
+
return userName;
|
|
3187
|
+
}
|
|
3188
|
+
</script>
|
|
3189
|
+
</head>
|
|
3190
|
+
<body>
|
|
3191
|
+
<button onclick="runTest()">Run</button>
|
|
3192
|
+
</body>
|
|
3193
|
+
</html>
|
|
3194
|
+
`)
|
|
3195
|
+
await page.bringToFront()
|
|
3196
|
+
|
|
3197
|
+
await serviceWorker.evaluate(async () => {
|
|
3198
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
3199
|
+
})
|
|
3200
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
3201
|
+
|
|
3202
|
+
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
3203
|
+
let cdpPage
|
|
3204
|
+
for (const p of browser.contexts()[0].pages()) {
|
|
3205
|
+
const html = await p.content()
|
|
3206
|
+
if (html.includes('runTest')) {
|
|
3207
|
+
cdpPage = p
|
|
3208
|
+
break
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
3211
|
+
expect(cdpPage).toBeDefined()
|
|
3212
|
+
|
|
3213
|
+
const wsUrl = getCdpUrl({ port: TEST_PORT })
|
|
3214
|
+
const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
|
|
3215
|
+
const dbg = new Debugger({ cdp: cdpSession })
|
|
3216
|
+
|
|
3217
|
+
await dbg.enable()
|
|
3218
|
+
|
|
3219
|
+
const globalVars = await dbg.inspectGlobalVariables()
|
|
3220
|
+
expect(globalVars).toMatchInlineSnapshot(`
|
|
3221
|
+
[
|
|
3222
|
+
"GLOBAL_CONFIG",
|
|
3223
|
+
]
|
|
3224
|
+
`)
|
|
3225
|
+
|
|
3226
|
+
const pausedPromise = new Promise<void>((resolve) => {
|
|
3227
|
+
cdpSession.on('Debugger.paused', () => resolve())
|
|
3228
|
+
})
|
|
3229
|
+
|
|
3230
|
+
cdpPage!.evaluate('runTest()')
|
|
3231
|
+
|
|
3232
|
+
await pausedPromise
|
|
3233
|
+
expect(dbg.isPaused()).toBe(true)
|
|
3234
|
+
|
|
3235
|
+
const localVars = await dbg.inspectLocalVariables()
|
|
3236
|
+
expect(localVars).toMatchInlineSnapshot(`
|
|
3237
|
+
{
|
|
3238
|
+
"GLOBAL_CONFIG": "production",
|
|
3239
|
+
"scores": "[array]",
|
|
3240
|
+
"settings": "[object]",
|
|
3241
|
+
"userAge": 25,
|
|
3242
|
+
"userName": "Alice",
|
|
3243
|
+
}
|
|
3244
|
+
`)
|
|
3245
|
+
|
|
3246
|
+
await dbg.resume()
|
|
3247
|
+
|
|
3248
|
+
cdpSession.close()
|
|
3249
|
+
await browser.close()
|
|
3250
|
+
await page.close()
|
|
3251
|
+
}, 60000)
|
|
3252
|
+
|
|
3253
|
+
it('should click at correct coordinates on high-DPI simulation', async () => {
|
|
3254
|
+
const browserContext = getBrowserContext()
|
|
3255
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
3256
|
+
|
|
3257
|
+
const page = await browserContext.newPage()
|
|
3258
|
+
await page.goto('https://example.com/')
|
|
3259
|
+
await page.bringToFront()
|
|
3260
|
+
|
|
3261
|
+
await serviceWorker.evaluate(async () => {
|
|
3262
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
3263
|
+
})
|
|
3264
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
3265
|
+
|
|
3266
|
+
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
3267
|
+
const cdpPage = browser
|
|
3268
|
+
.contexts()[0]
|
|
3269
|
+
.pages()
|
|
3270
|
+
.find((p) => p.url().includes('example.com'))
|
|
3271
|
+
expect(cdpPage).toBeDefined()
|
|
3272
|
+
|
|
3273
|
+
const h1Bounds = await cdpPage!.locator('h1').boundingBox()
|
|
3274
|
+
expect(h1Bounds).toBeDefined()
|
|
3275
|
+
console.log('H1 bounding box:', h1Bounds)
|
|
3276
|
+
|
|
3277
|
+
await cdpPage!.evaluate(() => {
|
|
3278
|
+
;(window as any).clickedAt = null
|
|
3279
|
+
document.addEventListener('click', (e) => {
|
|
3280
|
+
;(window as any).clickedAt = { x: e.clientX, y: e.clientY }
|
|
3281
|
+
})
|
|
3282
|
+
})
|
|
3283
|
+
|
|
3284
|
+
await cdpPage!.locator('h1').click()
|
|
3285
|
+
|
|
3286
|
+
const clickedAt = await cdpPage!.evaluate(() => (window as any).clickedAt)
|
|
3287
|
+
console.log('Clicked at:', clickedAt)
|
|
3288
|
+
|
|
3289
|
+
expect(clickedAt).toBeDefined()
|
|
3290
|
+
expect(clickedAt.x).toBeGreaterThan(0)
|
|
3291
|
+
expect(clickedAt.y).toBeGreaterThan(0)
|
|
3292
|
+
|
|
3293
|
+
await browser.close()
|
|
3294
|
+
await page.close()
|
|
3295
|
+
}, 60000)
|
|
3296
|
+
|
|
3297
|
+
it('should use Editor class to list, read, and edit scripts', async () => {
|
|
3298
|
+
const browserContext = getBrowserContext()
|
|
3299
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
3300
|
+
|
|
3301
|
+
const page = await browserContext.newPage()
|
|
3302
|
+
await page.goto('https://example.com/')
|
|
3303
|
+
await page.bringToFront()
|
|
3304
|
+
|
|
3305
|
+
await serviceWorker.evaluate(async () => {
|
|
3306
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
3307
|
+
})
|
|
3308
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
3309
|
+
|
|
3310
|
+
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
3311
|
+
const cdpPage = browser
|
|
3312
|
+
.contexts()[0]
|
|
3313
|
+
.pages()
|
|
3314
|
+
.find((p) => p.url().includes('example.com'))
|
|
3315
|
+
expect(cdpPage).toBeDefined()
|
|
3316
|
+
|
|
3317
|
+
const wsUrl = getCdpUrl({ port: TEST_PORT })
|
|
3318
|
+
const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
|
|
3319
|
+
const editor = new Editor({ cdp: cdpSession })
|
|
3320
|
+
|
|
3321
|
+
await editor.enable()
|
|
3322
|
+
|
|
3323
|
+
await cdpPage!.addScriptTag({
|
|
3324
|
+
content: `
|
|
3325
|
+
function greetUser(name) {
|
|
3326
|
+
console.log('Hello, ' + name);
|
|
3327
|
+
return 'Hello, ' + name;
|
|
3328
|
+
}
|
|
3329
|
+
`,
|
|
3330
|
+
})
|
|
3331
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
3332
|
+
const scripts = await editor.list()
|
|
3333
|
+
expect(scripts.length).toBeGreaterThan(0)
|
|
3334
|
+
|
|
3335
|
+
const matches = await editor.grep({ regex: /greetUser/ })
|
|
3336
|
+
expect(matches.length).toBeGreaterThan(0)
|
|
3337
|
+
|
|
3338
|
+
const match = matches[0]
|
|
3339
|
+
const { content, totalLines } = await editor.read({ url: match.url })
|
|
3340
|
+
expect(content).toContain('greetUser')
|
|
3341
|
+
expect(totalLines).toBeGreaterThan(0)
|
|
3342
|
+
|
|
3343
|
+
await editor.edit({
|
|
3344
|
+
url: match.url,
|
|
3345
|
+
oldString: "console.log('Hello, ' + name);",
|
|
3346
|
+
newString: "console.log('Hello, ' + name); console.log('EDITOR_TEST_MARKER');",
|
|
3347
|
+
})
|
|
3348
|
+
|
|
3349
|
+
const consoleLogs: string[] = []
|
|
3350
|
+
cdpPage!.on('console', (msg) => {
|
|
3351
|
+
consoleLogs.push(msg.text())
|
|
3352
|
+
})
|
|
3353
|
+
|
|
3354
|
+
await cdpPage!.evaluate(() => {
|
|
3355
|
+
;(window as any).greetUser('World')
|
|
3356
|
+
})
|
|
3357
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
3358
|
+
|
|
3359
|
+
expect(consoleLogs).toContain('Hello, World')
|
|
3360
|
+
expect(consoleLogs).toContain('EDITOR_TEST_MARKER')
|
|
3361
|
+
|
|
3362
|
+
cdpSession.close()
|
|
3363
|
+
await browser.close()
|
|
3364
|
+
await page.close()
|
|
3365
|
+
}, 60000)
|
|
3366
|
+
|
|
3367
|
+
it('editor can list, read, and edit CSS stylesheets', async () => {
|
|
3368
|
+
const browserContext = getBrowserContext()
|
|
3369
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
3370
|
+
|
|
3371
|
+
const page = await browserContext.newPage()
|
|
3372
|
+
await page.goto('https://example.com/')
|
|
3373
|
+
await page.bringToFront()
|
|
3374
|
+
|
|
3375
|
+
await serviceWorker.evaluate(async () => {
|
|
3376
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
3377
|
+
})
|
|
3378
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
3379
|
+
|
|
3380
|
+
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
3381
|
+
const cdpPage = browser
|
|
3382
|
+
.contexts()[0]
|
|
3383
|
+
.pages()
|
|
3384
|
+
.find((p) => p.url().includes('example.com'))
|
|
3385
|
+
expect(cdpPage).toBeDefined()
|
|
3386
|
+
|
|
3387
|
+
const wsUrl = getCdpUrl({ port: TEST_PORT })
|
|
3388
|
+
const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
|
|
3389
|
+
const editor = new Editor({ cdp: cdpSession })
|
|
3390
|
+
|
|
3391
|
+
await editor.enable()
|
|
3392
|
+
|
|
3393
|
+
await cdpPage!.addStyleTag({
|
|
3394
|
+
content: `
|
|
3395
|
+
.editor-test-element {
|
|
3396
|
+
color: rgb(255, 0, 0);
|
|
3397
|
+
background-color: rgb(0, 0, 255);
|
|
3398
|
+
}
|
|
3399
|
+
`,
|
|
3400
|
+
})
|
|
3401
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
3402
|
+
const stylesheets = await editor.list({ pattern: /inline-css:/ })
|
|
3403
|
+
expect(stylesheets.length).toBeGreaterThan(0)
|
|
3404
|
+
|
|
3405
|
+
const cssMatches = await editor.grep({ regex: /editor-test-element/, pattern: /inline-css:/ })
|
|
3406
|
+
expect(cssMatches.length).toBeGreaterThan(0)
|
|
3407
|
+
|
|
3408
|
+
const cssMatch = cssMatches[0]
|
|
3409
|
+
const { content, totalLines } = await editor.read({ url: cssMatch.url })
|
|
3410
|
+
expect(content).toContain('editor-test-element')
|
|
3411
|
+
expect(content).toContain('rgb(255, 0, 0)')
|
|
3412
|
+
expect(totalLines).toBeGreaterThan(0)
|
|
3413
|
+
|
|
3414
|
+
await cdpPage!.evaluate(() => {
|
|
3415
|
+
const el = document.createElement('div')
|
|
3416
|
+
el.className = 'editor-test-element'
|
|
3417
|
+
el.id = 'test-div'
|
|
3418
|
+
el.textContent = 'Test'
|
|
3419
|
+
document.body.appendChild(el)
|
|
3420
|
+
})
|
|
3421
|
+
|
|
3422
|
+
const colorBefore = await cdpPage!.evaluate(() => {
|
|
3423
|
+
const el = document.getElementById('test-div')!
|
|
3424
|
+
return window.getComputedStyle(el).color
|
|
3425
|
+
})
|
|
3426
|
+
expect(colorBefore).toBe('rgb(255, 0, 0)')
|
|
3427
|
+
|
|
3428
|
+
await editor.edit({
|
|
3429
|
+
url: cssMatch.url,
|
|
3430
|
+
oldString: 'color: rgb(255, 0, 0);',
|
|
3431
|
+
newString: 'color: rgb(0, 255, 0);',
|
|
3432
|
+
})
|
|
3433
|
+
|
|
3434
|
+
const colorAfter = await cdpPage!.evaluate(() => {
|
|
3435
|
+
const el = document.getElementById('test-div')!
|
|
3436
|
+
return window.getComputedStyle(el).color
|
|
3437
|
+
})
|
|
3438
|
+
expect(colorAfter).toBe('rgb(0, 255, 0)')
|
|
3439
|
+
|
|
3440
|
+
cdpSession.close()
|
|
3441
|
+
await browser.close()
|
|
3442
|
+
await page.close()
|
|
3443
|
+
}, 60000)
|
|
3444
|
+
|
|
3445
|
+
it('should inject bippy and find React fiber with getReactSource', async () => {
|
|
3446
|
+
const browserContext = getBrowserContext()
|
|
3447
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
3448
|
+
|
|
3449
|
+
const page = await browserContext.newPage()
|
|
3450
|
+
await page.setContent(`
|
|
3451
|
+
<!DOCTYPE html>
|
|
3452
|
+
<html>
|
|
3453
|
+
<head>
|
|
3454
|
+
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
|
|
3455
|
+
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
|
|
3456
|
+
</head>
|
|
3457
|
+
<body>
|
|
3458
|
+
<div id="root"></div>
|
|
3459
|
+
<script>
|
|
3460
|
+
function MyComponent() {
|
|
3461
|
+
return React.createElement('button', { id: 'react-btn' }, 'Click me');
|
|
3462
|
+
}
|
|
3463
|
+
const root = ReactDOM.createRoot(document.getElementById('root'));
|
|
3464
|
+
root.render(React.createElement(MyComponent));
|
|
3465
|
+
</script>
|
|
3466
|
+
</body>
|
|
3467
|
+
</html>
|
|
3468
|
+
`)
|
|
3469
|
+
await page.bringToFront()
|
|
3470
|
+
|
|
3471
|
+
await serviceWorker.evaluate(async () => {
|
|
3472
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
3473
|
+
})
|
|
3474
|
+
await new Promise((r) => setTimeout(r, 500))
|
|
3475
|
+
|
|
3476
|
+
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
3477
|
+
const pages = browser.contexts()[0].pages()
|
|
3478
|
+
const cdpPage = pages.find((p) => p.url().startsWith('about:'))
|
|
3479
|
+
expect(cdpPage).toBeDefined()
|
|
3480
|
+
|
|
3481
|
+
const btn = cdpPage!.locator('#react-btn')
|
|
3482
|
+
const btnCount = await btn.count()
|
|
3483
|
+
expect(btnCount).toBe(1)
|
|
3484
|
+
|
|
3485
|
+
const hasBippyBefore = await cdpPage!.evaluate(() => !!(globalThis as any).__bippy)
|
|
3486
|
+
expect(hasBippyBefore).toBe(false)
|
|
3487
|
+
|
|
3488
|
+
const wsUrl = getCdpUrl({ port: TEST_PORT })
|
|
3489
|
+
const cdpSession = await getCDPSessionForPage({ page: cdpPage!, wsUrl })
|
|
3490
|
+
|
|
3491
|
+
const { getReactSource } = await import('./react-source.js')
|
|
3492
|
+
const source = await getReactSource({ locator: btn, cdp: cdpSession })
|
|
3493
|
+
|
|
3494
|
+
const hasBippyAfter = await cdpPage!.evaluate(() => !!(globalThis as any).__bippy)
|
|
3495
|
+
expect(hasBippyAfter).toBe(true)
|
|
3496
|
+
|
|
3497
|
+
const hasFiber = await btn.evaluate((el) => {
|
|
3498
|
+
const bippy = (globalThis as any).__bippy
|
|
3499
|
+
const fiber = bippy.getFiberFromHostInstance(el)
|
|
3500
|
+
return !!fiber
|
|
3501
|
+
})
|
|
3502
|
+
expect(hasFiber).toBe(true)
|
|
3503
|
+
|
|
3504
|
+
const componentName = await btn.evaluate((el) => {
|
|
3505
|
+
const bippy = (globalThis as any).__bippy
|
|
3506
|
+
const fiber = bippy.getFiberFromHostInstance(el)
|
|
3507
|
+
let current = fiber
|
|
3508
|
+
while (current) {
|
|
3509
|
+
if (bippy.isCompositeFiber(current)) {
|
|
3510
|
+
return bippy.getDisplayName(current.type)
|
|
3511
|
+
}
|
|
3512
|
+
current = current.return
|
|
3513
|
+
}
|
|
3514
|
+
return null
|
|
3515
|
+
})
|
|
3516
|
+
expect(componentName).toBe('MyComponent')
|
|
3517
|
+
|
|
3518
|
+
console.log('Component name from fiber:', componentName)
|
|
3519
|
+
console.log('Source location (null for UMD React, works on local dev servers with JSX transform):', source)
|
|
3520
|
+
|
|
3521
|
+
await browser.close()
|
|
3522
|
+
await page.close()
|
|
3523
|
+
}, 60000)
|
|
3524
|
+
})
|
|
3525
|
+
|
|
3526
|
+
describe('Auto-enable Tests', () => {
|
|
3527
|
+
let testCtx: TestContext | null = null
|
|
3528
|
+
|
|
3529
|
+
// Set env var before any setup runs
|
|
3530
|
+
process.env.E2E_PILOT_AUTO_ENABLE = '1'
|
|
3531
|
+
|
|
3532
|
+
beforeAll(async () => {
|
|
3533
|
+
testCtx = await setupTestContext({ tempDirPrefix: 'pw-auto-test-' })
|
|
3534
|
+
|
|
3535
|
+
// Disconnect all tabs to start with a clean state
|
|
3536
|
+
const serviceWorker = await getExtensionServiceWorker(testCtx.browserContext)
|
|
3537
|
+
await serviceWorker.evaluate(async () => {
|
|
3538
|
+
await globalThis.disconnectEverything()
|
|
3539
|
+
})
|
|
3540
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
3541
|
+
}, 600000)
|
|
3542
|
+
|
|
3543
|
+
afterAll(async () => {
|
|
3544
|
+
delete process.env.E2E_PILOT_AUTO_ENABLE
|
|
3545
|
+
await cleanupTestContext(testCtx)
|
|
3546
|
+
testCtx = null
|
|
3547
|
+
})
|
|
3548
|
+
|
|
3549
|
+
const getBrowserContext = () => {
|
|
3550
|
+
if (!testCtx?.browserContext) throw new Error('Browser not initialized')
|
|
3551
|
+
return testCtx.browserContext
|
|
3552
|
+
}
|
|
3553
|
+
|
|
3554
|
+
it('should auto-create a tab when Playwright connects and no tabs exist', async () => {
|
|
3555
|
+
const browserContext = getBrowserContext()
|
|
3556
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
3557
|
+
|
|
3558
|
+
// Ensure clean state - disconnect any tabs from previous tests or setup
|
|
3559
|
+
await serviceWorker.evaluate(async () => {
|
|
3560
|
+
await globalThis.disconnectEverything()
|
|
3561
|
+
})
|
|
3562
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
3563
|
+
|
|
3564
|
+
// Verify no tabs are connected
|
|
3565
|
+
const tabCountBefore = await serviceWorker.evaluate(() => {
|
|
3566
|
+
const state = globalThis.getExtensionState()
|
|
3567
|
+
return state.tabs.size
|
|
3568
|
+
})
|
|
3569
|
+
expect(tabCountBefore).toBe(0)
|
|
3570
|
+
|
|
3571
|
+
// Connect Playwright - this should trigger auto-create
|
|
3572
|
+
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
3573
|
+
|
|
3574
|
+
// Verify a page was auto-created
|
|
3575
|
+
const pages = browser.contexts()[0].pages()
|
|
3576
|
+
expect(pages.length).toBeGreaterThan(0)
|
|
3577
|
+
expect(pages.length).toBe(1)
|
|
3578
|
+
|
|
3579
|
+
const autoCreatedPage = pages[0]
|
|
3580
|
+
expect(autoCreatedPage.url()).toBe('about:blank')
|
|
3581
|
+
|
|
3582
|
+
// Verify extension state shows the tab as connected
|
|
3583
|
+
const tabCountAfter = await serviceWorker.evaluate(() => {
|
|
3584
|
+
const state = globalThis.getExtensionState()
|
|
3585
|
+
return state.tabs.size
|
|
3586
|
+
})
|
|
3587
|
+
expect(tabCountAfter).toBe(1)
|
|
3588
|
+
|
|
3589
|
+
// Verify we can interact with the auto-created page
|
|
3590
|
+
await autoCreatedPage.setContent('<h1>Auto-created page</h1>')
|
|
3591
|
+
const title = await autoCreatedPage.locator('h1').textContent()
|
|
3592
|
+
expect(title).toBe('Auto-created page')
|
|
3593
|
+
|
|
3594
|
+
await browser.close()
|
|
3595
|
+
}, 60000)
|
|
3596
|
+
})
|