playwriter 0.0.63 → 0.0.89
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/a11y-client.js +18 -8
- package/dist/aria-snapshot.d.ts +41 -3
- package/dist/aria-snapshot.d.ts.map +1 -1
- package/dist/aria-snapshot.js +134 -55
- package/dist/aria-snapshot.js.map +1 -1
- package/dist/aria-snapshot.test.js +5 -2
- package/dist/aria-snapshot.test.js.map +1 -1
- package/dist/aria-snapshot.unit.test.js +83 -41
- package/dist/aria-snapshot.unit.test.js.map +1 -1
- package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.d.ts +5 -0
- package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.d.ts.map +1 -0
- package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.js +5 -0
- package/dist/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.js.map +1 -0
- package/dist/bippy.js +1 -1
- package/dist/cdp-log.d.ts +1 -1
- package/dist/cdp-log.d.ts.map +1 -1
- package/dist/cdp-log.js +1 -1
- package/dist/cdp-log.js.map +1 -1
- package/dist/cdp-relay.d.ts.map +1 -1
- package/dist/cdp-relay.js +492 -298
- package/dist/cdp-relay.js.map +1 -1
- package/dist/cdp-session.d.ts.map +1 -1
- package/dist/cdp-session.js.map +1 -1
- package/dist/cdp-types.d.ts.map +1 -1
- package/dist/cdp-types.js +7 -7
- package/dist/cdp-types.js.map +1 -1
- package/dist/clean-html.d.ts.map +1 -1
- package/dist/clean-html.js +4 -5
- package/dist/clean-html.js.map +1 -1
- package/dist/cli.js +45 -27
- package/dist/cli.js.map +1 -1
- package/dist/create-logger.d.ts.map +1 -1
- package/dist/create-logger.js +3 -1
- package/dist/create-logger.js.map +1 -1
- package/dist/debugger-examples-types.d.ts.map +1 -1
- package/dist/debugger.d.ts.map +1 -1
- package/dist/debugger.js +1 -3
- package/dist/debugger.js.map +1 -1
- package/dist/diff-utils.d.ts.map +1 -1
- package/dist/diff-utils.js +1 -4
- package/dist/diff-utils.js.map +1 -1
- package/dist/editor-api.md +12 -2
- package/dist/editor-examples.d.ts +1 -1
- package/dist/editor-examples.d.ts.map +1 -1
- package/dist/editor-examples.js +1 -1
- package/dist/editor-examples.js.map +1 -1
- package/dist/editor.d.ts +1 -1
- package/dist/editor.d.ts.map +1 -1
- package/dist/editor.js +1 -1
- package/dist/editor.js.map +1 -1
- package/dist/executor.d.ts +26 -3
- package/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +297 -64
- package/dist/executor.js.map +1 -1
- package/dist/executor.unit.test.js +38 -1
- package/dist/executor.unit.test.js.map +1 -1
- package/dist/extension-connection.test.js +139 -36
- package/dist/extension-connection.test.js.map +1 -1
- package/dist/ffmpeg.d.ts +148 -0
- package/dist/ffmpeg.d.ts.map +1 -0
- package/dist/ffmpeg.js +523 -0
- package/dist/ffmpeg.js.map +1 -0
- package/dist/ghost-browser.d.ts.map +1 -1
- package/dist/ghost-browser.js.map +1 -1
- package/dist/ghost-cursor-client.js +287 -0
- package/dist/ghost-cursor.d.ts +27 -0
- package/dist/ghost-cursor.d.ts.map +1 -0
- package/dist/ghost-cursor.js +63 -0
- package/dist/ghost-cursor.js.map +1 -0
- package/dist/htmlrewrite.d.ts.map +1 -1
- package/dist/htmlrewrite.js +17 -55
- package/dist/htmlrewrite.js.map +1 -1
- package/dist/htmlrewrite.test.js.map +1 -1
- package/dist/kill-port.d.ts.map +1 -1
- package/dist/kill-port.js +1 -3
- package/dist/kill-port.js.map +1 -1
- package/dist/locator-selector.test.d.ts +2 -0
- package/dist/locator-selector.test.d.ts.map +1 -0
- package/dist/locator-selector.test.js +96 -0
- package/dist/locator-selector.test.js.map +1 -0
- package/dist/mcp-client.js.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +8 -3
- package/dist/mcp.js.map +1 -1
- package/dist/on-mouse-action.test.d.ts +2 -0
- package/dist/on-mouse-action.test.d.ts.map +1 -0
- package/dist/on-mouse-action.test.js +155 -0
- package/dist/on-mouse-action.test.js.map +1 -0
- package/dist/page-markdown.js +4 -4
- package/dist/page-markdown.js.map +1 -1
- package/dist/prompt.md +450 -377
- package/dist/protocol.d.ts +4 -0
- package/dist/protocol.d.ts.map +1 -1
- package/dist/readability.js +16 -2
- package/dist/recording-ghost-cursor.d.ts +41 -0
- package/dist/recording-ghost-cursor.d.ts.map +1 -0
- package/dist/recording-ghost-cursor.js +79 -0
- package/dist/recording-ghost-cursor.js.map +1 -0
- package/dist/recording-relay.d.ts.map +1 -1
- package/dist/recording-relay.js +8 -8
- package/dist/recording-relay.js.map +1 -1
- package/dist/relay-client.d.ts +17 -4
- package/dist/relay-client.d.ts.map +1 -1
- package/dist/relay-client.js +45 -11
- package/dist/relay-client.js.map +1 -1
- package/dist/relay-core.test.d.ts.map +1 -1
- package/dist/relay-core.test.js +515 -26
- package/dist/relay-core.test.js.map +1 -1
- package/dist/relay-navigation.test.d.ts.map +1 -1
- package/dist/relay-navigation.test.js +169 -31
- package/dist/relay-navigation.test.js.map +1 -1
- package/dist/relay-session.test.d.ts.map +1 -1
- package/dist/relay-session.test.js +113 -65
- package/dist/relay-session.test.js.map +1 -1
- package/dist/relay-state.d.ts +158 -0
- package/dist/relay-state.d.ts.map +1 -0
- package/dist/relay-state.js +306 -0
- package/dist/relay-state.js.map +1 -0
- package/dist/relay-state.test.d.ts +2 -0
- package/dist/relay-state.test.d.ts.map +1 -0
- package/dist/relay-state.test.js +472 -0
- package/dist/relay-state.test.js.map +1 -0
- package/dist/scoped-fs.d.ts.map +1 -1
- package/dist/scoped-fs.js.map +1 -1
- package/dist/screen-recording.d.ts +66 -4
- package/dist/screen-recording.d.ts.map +1 -1
- package/dist/screen-recording.js +150 -13
- package/dist/screen-recording.js.map +1 -1
- package/dist/screen-recording.test.d.ts +2 -0
- package/dist/screen-recording.test.d.ts.map +1 -0
- package/dist/screen-recording.test.js +102 -0
- package/dist/screen-recording.test.js.map +1 -0
- package/dist/selector-generator.js +1 -1
- package/dist/snapshot-tools.test.js +71 -28
- package/dist/snapshot-tools.test.js.map +1 -1
- package/dist/start-relay-server.d.ts +1 -1
- package/dist/start-relay-server.d.ts.map +1 -1
- package/dist/start-relay-server.js +1 -1
- package/dist/start-relay-server.js.map +1 -1
- package/dist/styles-api.md +8 -1
- package/dist/styles-examples.d.ts +1 -1
- package/dist/styles-examples.d.ts.map +1 -1
- package/dist/styles-examples.js +1 -1
- package/dist/styles-examples.js.map +1 -1
- package/dist/styles.d.ts.map +1 -1
- package/dist/styles.js +1 -3
- package/dist/styles.js.map +1 -1
- package/dist/test-declarations.d.ts.map +1 -1
- package/dist/test-utils.d.ts +1 -1
- package/dist/test-utils.d.ts.map +1 -1
- package/dist/test-utils.js +7 -5
- package/dist/test-utils.js.map +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js.map +1 -1
- package/dist/wait-for-page-load.d.ts.map +1 -1
- package/dist/wait-for-page-load.js +1 -1
- package/dist/wait-for-page-load.js.map +1 -1
- package/package.json +4 -3
- package/src/a11y-client.ts +5 -4
- package/src/aria-snapshot.test.ts +5 -2
- package/src/aria-snapshot.ts +306 -117
- package/src/aria-snapshot.unit.test.ts +199 -141
- package/src/aria-snapshots/github-interactive.txt +2 -0
- package/src/aria-snapshots/github-raw.txt +5 -1
- package/src/aria-snapshots/hackernews-interactive.txt +238 -241
- package/src/aria-snapshots/hackernews-raw.txt +265 -269
- package/src/assets/aria-labels-example.png +0 -0
- package/src/assets/aria-labels-github.png +0 -0
- package/src/assets/aria-labels-hacker-news.png +0 -0
- package/src/assets/aria-labels-old-reddit.png +0 -0
- package/src/assets/cursors/screen-studio/pointer-macos-tahoe-data-url.ts +5 -0
- package/src/assets/cursors/screen-studio/pointer-macos-tahoe.svg +18 -0
- package/src/cdp-log.ts +4 -1
- package/src/cdp-relay.ts +1059 -737
- package/src/cdp-session.ts +12 -3
- package/src/cdp-types.ts +51 -51
- package/src/clean-html.ts +4 -5
- package/src/cli.ts +82 -55
- package/src/create-logger.ts +5 -3
- package/src/debugger-examples-types.ts +4 -1
- package/src/debugger.ts +1 -5
- package/src/diff-utils.ts +2 -5
- package/src/editor-examples.ts +11 -1
- package/src/editor.ts +10 -2
- package/src/executor.ts +374 -73
- package/src/executor.unit.test.ts +48 -1
- package/src/extension-connection.test.ts +612 -488
- package/src/ffmpeg.ts +769 -0
- package/src/ghost-browser.ts +4 -6
- package/src/ghost-cursor-client.ts +369 -0
- package/src/ghost-cursor.ts +110 -0
- package/src/htmlrewrite.test.ts +6 -2
- package/src/htmlrewrite.ts +348 -386
- package/src/kill-port.ts +1 -3
- package/src/locator-selector.test.ts +115 -0
- package/src/mcp-client.ts +1 -1
- package/src/mcp.ts +21 -15
- package/src/on-mouse-action.test.ts +196 -0
- package/src/page-markdown.ts +7 -7
- package/src/protocol.ts +73 -57
- package/src/recording-ghost-cursor.ts +113 -0
- package/src/recording-relay.ts +20 -12
- package/src/relay-client.ts +85 -18
- package/src/relay-core.test.ts +1117 -578
- package/src/relay-navigation.test.ts +648 -483
- package/src/relay-session.test.ts +984 -929
- package/src/relay-state.test.ts +570 -0
- package/src/relay-state.ts +497 -0
- package/src/resource.md +21 -49
- package/src/scoped-fs.ts +9 -3
- package/src/screen-recording.test.ts +111 -0
- package/src/screen-recording.ts +256 -31
- package/src/skill.md +476 -396
- package/src/snapshot-tools.test.ts +580 -528
- package/src/snapshots/shadcn-ui-accessibility-full.md +8 -8
- package/src/snapshots/shadcn-ui-accessibility-interactive.md +8 -8
- package/src/start-relay-server.ts +14 -11
- package/src/styles-examples.ts +8 -1
- package/src/styles.ts +20 -21
- package/src/test-declarations.ts +6 -6
- package/src/test-utils.ts +104 -91
- package/src/utils.ts +2 -1
- package/src/wait-for-page-load.ts +6 -1
|
@@ -8,273 +8,281 @@ import './test-declarations.js'
|
|
|
8
8
|
const TEST_PORT = 19990
|
|
9
9
|
|
|
10
10
|
describe('Extension Connection Tests', () => {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
let client: Awaited<ReturnType<typeof createMCPClient>>['client']
|
|
12
|
+
let cleanup: (() => Promise<void>) | null = null
|
|
13
|
+
let testCtx: TestContext | null = null
|
|
14
|
+
|
|
15
|
+
beforeAll(async () => {
|
|
16
|
+
testCtx = await setupTestContext({ port: TEST_PORT, tempDirPrefix: 'pw-conn-test-', toggleExtension: true })
|
|
17
|
+
|
|
18
|
+
const result = await createMCPClient({ port: TEST_PORT })
|
|
19
|
+
client = result.client
|
|
20
|
+
cleanup = result.cleanup
|
|
21
|
+
}, 600000)
|
|
22
|
+
|
|
23
|
+
afterAll(async () => {
|
|
24
|
+
await cleanupTestContext(testCtx, cleanup)
|
|
25
|
+
cleanup = null
|
|
26
|
+
testCtx = null
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const getBrowserContext = () => {
|
|
30
|
+
if (!testCtx?.browserContext) throw new Error('Browser not initialized')
|
|
31
|
+
return testCtx.browserContext
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
it('should handle new pages and toggling with new connections', async () => {
|
|
35
|
+
const browserContext = getBrowserContext()
|
|
36
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
37
|
+
|
|
38
|
+
// 1. Create a new page
|
|
39
|
+
const page = await browserContext.newPage()
|
|
40
|
+
const testUrl = 'https://example.com/'
|
|
41
|
+
await page.goto(testUrl)
|
|
42
|
+
|
|
43
|
+
await page.bringToFront()
|
|
44
|
+
|
|
45
|
+
// 2. Enable extension on this new tab
|
|
46
|
+
const result = await serviceWorker.evaluate(async () => {
|
|
47
|
+
return await globalThis.toggleExtensionForActiveTab()
|
|
48
|
+
})
|
|
49
|
+
expect(result.isConnected).toBe(true)
|
|
50
|
+
|
|
51
|
+
// 3. Verify we can connect via direct CDP and see the page
|
|
52
|
+
let directBrowser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
53
|
+
let contexts = directBrowser.contexts()
|
|
54
|
+
let pages = contexts[0].pages()
|
|
55
|
+
|
|
56
|
+
let foundPage = pages.find((p) => p.url() === testUrl)
|
|
57
|
+
expect(foundPage).toBeDefined()
|
|
58
|
+
expect(foundPage?.url()).toBe(testUrl)
|
|
59
|
+
|
|
60
|
+
const sum1 = await foundPage?.evaluate(() => 1 + 1)
|
|
61
|
+
expect(sum1).toBe(2)
|
|
62
|
+
|
|
63
|
+
await directBrowser.close()
|
|
64
|
+
|
|
65
|
+
// 4. Disable extension on this tab
|
|
66
|
+
const resultDisabled = await serviceWorker.evaluate(async () => {
|
|
67
|
+
return await globalThis.toggleExtensionForActiveTab()
|
|
68
|
+
})
|
|
69
|
+
expect(resultDisabled.isConnected).toBe(false)
|
|
70
|
+
|
|
71
|
+
// 5. Connect again - page should NOT be visible
|
|
72
|
+
directBrowser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
73
|
+
contexts = directBrowser.contexts()
|
|
74
|
+
pages = contexts[0].pages()
|
|
75
|
+
|
|
76
|
+
foundPage = pages.find((p) => p.url() === testUrl)
|
|
77
|
+
expect(foundPage).toBeUndefined()
|
|
78
|
+
|
|
79
|
+
await directBrowser.close()
|
|
80
|
+
|
|
81
|
+
// 6. Re-enable extension
|
|
82
|
+
const resultEnabled = await serviceWorker.evaluate(async () => {
|
|
83
|
+
return await globalThis.toggleExtensionForActiveTab()
|
|
84
|
+
})
|
|
85
|
+
expect(resultEnabled.isConnected).toBe(true)
|
|
86
|
+
|
|
87
|
+
// 7. Verify page is back
|
|
88
|
+
directBrowser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
89
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
90
|
+
|
|
91
|
+
contexts = directBrowser.contexts()
|
|
92
|
+
if (contexts[0].pages().length === 0) {
|
|
93
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
94
|
+
}
|
|
95
|
+
pages = contexts[0].pages()
|
|
96
|
+
|
|
97
|
+
foundPage = pages.find((p) => p.url() === testUrl)
|
|
98
|
+
expect(foundPage).toBeDefined()
|
|
99
|
+
expect(foundPage?.url()).toBe(testUrl)
|
|
100
|
+
|
|
101
|
+
const sum2 = await foundPage?.evaluate(() => 2 + 2)
|
|
102
|
+
expect(sum2).toBe(4)
|
|
103
|
+
|
|
104
|
+
await directBrowser.close()
|
|
105
|
+
await page.close()
|
|
106
|
+
}, 120000)
|
|
107
|
+
|
|
108
|
+
it('should handle new pages and toggling with persistent connection', async () => {
|
|
109
|
+
const browserContext = getBrowserContext()
|
|
110
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
111
|
+
|
|
112
|
+
const directBrowser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
113
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
114
|
+
|
|
115
|
+
// 1. Create a new page
|
|
116
|
+
const page = await browserContext.newPage()
|
|
117
|
+
const testUrl = 'https://example.com/persistent'
|
|
118
|
+
await page.goto(testUrl)
|
|
119
|
+
await page.bringToFront()
|
|
120
|
+
|
|
121
|
+
// 2. Enable extension
|
|
122
|
+
await serviceWorker.evaluate(async () => {
|
|
123
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
// 3. Verify page appears (polling)
|
|
127
|
+
let foundPage
|
|
128
|
+
for (let i = 0; i < 50; i++) {
|
|
129
|
+
const pages = directBrowser.contexts()[0].pages()
|
|
130
|
+
foundPage = pages.find((p) => p.url() === testUrl)
|
|
131
|
+
if (foundPage) break
|
|
132
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
133
|
+
}
|
|
134
|
+
expect(foundPage).toBeDefined()
|
|
135
|
+
expect(foundPage?.url()).toBe(testUrl)
|
|
136
|
+
|
|
137
|
+
const sum1 = await foundPage?.evaluate(() => 10 + 20)
|
|
138
|
+
expect(sum1).toBe(30)
|
|
14
139
|
|
|
15
|
-
|
|
16
|
-
|
|
140
|
+
// 4. Disable extension
|
|
141
|
+
await serviceWorker.evaluate(async () => {
|
|
142
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
143
|
+
})
|
|
17
144
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
145
|
+
// 5. Verify page disappears (polling)
|
|
146
|
+
for (let i = 0; i < 50; i++) {
|
|
147
|
+
const pages = directBrowser.contexts()[0].pages()
|
|
148
|
+
foundPage = pages.find((p) => p.url() === testUrl)
|
|
149
|
+
if (!foundPage) break
|
|
150
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
151
|
+
}
|
|
152
|
+
expect(foundPage).toBeUndefined()
|
|
22
153
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
testCtx = null
|
|
154
|
+
// 6. Re-enable extension
|
|
155
|
+
await serviceWorker.evaluate(async () => {
|
|
156
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
27
157
|
})
|
|
28
158
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
159
|
+
// 7. Verify page reappears (polling)
|
|
160
|
+
for (let i = 0; i < 50; i++) {
|
|
161
|
+
const pages = directBrowser.contexts()[0].pages()
|
|
162
|
+
foundPage = pages.find((p) => p.url() === testUrl)
|
|
163
|
+
if (foundPage) break
|
|
164
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
32
165
|
}
|
|
166
|
+
expect(foundPage).toBeDefined()
|
|
167
|
+
expect(foundPage?.url()).toBe(testUrl)
|
|
168
|
+
|
|
169
|
+
const sum2 = await foundPage?.evaluate(() => 30 + 40)
|
|
170
|
+
expect(sum2).toBe(70)
|
|
171
|
+
|
|
172
|
+
await page.close()
|
|
173
|
+
await directBrowser.close()
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('should maintain connection across reloads and navigation', async () => {
|
|
177
|
+
const browserContext = getBrowserContext()
|
|
178
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
179
|
+
|
|
180
|
+
// 1. Setup page
|
|
181
|
+
const page = await browserContext.newPage()
|
|
182
|
+
const initialUrl = 'https://example.com/'
|
|
183
|
+
await page.goto(initialUrl)
|
|
184
|
+
await page.bringToFront()
|
|
185
|
+
|
|
186
|
+
// 2. Enable extension
|
|
187
|
+
await serviceWorker.evaluate(async () => {
|
|
188
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
// 3. Connect via CDP
|
|
192
|
+
const cdpUrl = getCdpUrl({ port: TEST_PORT })
|
|
193
|
+
const directBrowser = await chromium.connectOverCDP(cdpUrl)
|
|
194
|
+
const connectedPage = directBrowser
|
|
195
|
+
.contexts()[0]
|
|
196
|
+
.pages()
|
|
197
|
+
.find((p) => p.url() === initialUrl)
|
|
198
|
+
expect(connectedPage).toBeDefined()
|
|
199
|
+
|
|
200
|
+
expect(await connectedPage?.evaluate(() => 1 + 1)).toBe(2)
|
|
201
|
+
|
|
202
|
+
// 4. Reload
|
|
203
|
+
await connectedPage?.reload()
|
|
204
|
+
await connectedPage?.waitForLoadState('domcontentloaded')
|
|
205
|
+
expect(await connectedPage?.title()).toBe('Example Domain')
|
|
206
|
+
|
|
207
|
+
expect(await connectedPage?.evaluate(() => 2 + 2)).toBe(4)
|
|
208
|
+
|
|
209
|
+
// 5. Navigate to new URL
|
|
210
|
+
const newUrl = 'https://example.org/'
|
|
211
|
+
await connectedPage?.goto(newUrl)
|
|
212
|
+
await connectedPage?.waitForLoadState('domcontentloaded')
|
|
213
|
+
|
|
214
|
+
expect(connectedPage?.url()).toBe(newUrl)
|
|
215
|
+
expect(await connectedPage?.title()).toContain('Example Domain')
|
|
216
|
+
|
|
217
|
+
expect(await connectedPage?.evaluate(() => 3 + 3)).toBe(6)
|
|
218
|
+
|
|
219
|
+
await directBrowser.close()
|
|
220
|
+
await page.close()
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('should support multiple concurrent tabs', async () => {
|
|
224
|
+
const browserContext = getBrowserContext()
|
|
225
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
226
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
227
|
+
|
|
228
|
+
// Tab A
|
|
229
|
+
const pageA = await browserContext.newPage()
|
|
230
|
+
await pageA.goto('https://example.com/tab-a')
|
|
231
|
+
await pageA.bringToFront()
|
|
232
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
233
|
+
await serviceWorker.evaluate(async () => {
|
|
234
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
// Tab B
|
|
238
|
+
const pageB = await browserContext.newPage()
|
|
239
|
+
await pageB.goto('https://example.com/tab-b')
|
|
240
|
+
await pageB.bringToFront()
|
|
241
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
242
|
+
await serviceWorker.evaluate(async () => {
|
|
243
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
// Get target IDs for both
|
|
247
|
+
const targetIds = await serviceWorker.evaluate(async () => {
|
|
248
|
+
const state = globalThis.getExtensionState()
|
|
249
|
+
const chrome = globalThis.chrome
|
|
250
|
+
const tabs = await chrome.tabs.query({})
|
|
251
|
+
const tabA = tabs.find((t: any) => t.url?.includes('tab-a'))
|
|
252
|
+
const tabB = tabs.find((t: any) => t.url?.includes('tab-b'))
|
|
253
|
+
return {
|
|
254
|
+
idA: state.tabs.get(tabA?.id ?? -1)?.targetId,
|
|
255
|
+
idB: state.tabs.get(tabB?.id ?? -1)?.targetId,
|
|
256
|
+
}
|
|
257
|
+
})
|
|
33
258
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const testUrl = 'https://example.com/'
|
|
41
|
-
await page.goto(testUrl)
|
|
42
|
-
|
|
43
|
-
await page.bringToFront()
|
|
44
|
-
|
|
45
|
-
// 2. Enable extension on this new tab
|
|
46
|
-
const result = await serviceWorker.evaluate(async () => {
|
|
47
|
-
return await globalThis.toggleExtensionForActiveTab()
|
|
48
|
-
})
|
|
49
|
-
expect(result.isConnected).toBe(true)
|
|
50
|
-
|
|
51
|
-
// 3. Verify we can connect via direct CDP and see the page
|
|
52
|
-
let directBrowser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
53
|
-
let contexts = directBrowser.contexts()
|
|
54
|
-
let pages = contexts[0].pages()
|
|
55
|
-
|
|
56
|
-
let foundPage = pages.find(p => p.url() === testUrl)
|
|
57
|
-
expect(foundPage).toBeDefined()
|
|
58
|
-
expect(foundPage?.url()).toBe(testUrl)
|
|
59
|
-
|
|
60
|
-
const sum1 = await foundPage?.evaluate(() => 1 + 1)
|
|
61
|
-
expect(sum1).toBe(2)
|
|
62
|
-
|
|
63
|
-
await directBrowser.close()
|
|
64
|
-
|
|
65
|
-
// 4. Disable extension on this tab
|
|
66
|
-
const resultDisabled = await serviceWorker.evaluate(async () => {
|
|
67
|
-
return await globalThis.toggleExtensionForActiveTab()
|
|
68
|
-
})
|
|
69
|
-
expect(resultDisabled.isConnected).toBe(false)
|
|
70
|
-
|
|
71
|
-
// 5. Connect again - page should NOT be visible
|
|
72
|
-
directBrowser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
73
|
-
contexts = directBrowser.contexts()
|
|
74
|
-
pages = contexts[0].pages()
|
|
75
|
-
|
|
76
|
-
foundPage = pages.find(p => p.url() === testUrl)
|
|
77
|
-
expect(foundPage).toBeUndefined()
|
|
78
|
-
|
|
79
|
-
await directBrowser.close()
|
|
80
|
-
|
|
81
|
-
// 6. Re-enable extension
|
|
82
|
-
const resultEnabled = await serviceWorker.evaluate(async () => {
|
|
83
|
-
return await globalThis.toggleExtensionForActiveTab()
|
|
84
|
-
})
|
|
85
|
-
expect(resultEnabled.isConnected).toBe(true)
|
|
86
|
-
|
|
87
|
-
// 7. Verify page is back
|
|
88
|
-
directBrowser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
89
|
-
await new Promise(r => setTimeout(r, 100))
|
|
90
|
-
|
|
91
|
-
contexts = directBrowser.contexts()
|
|
92
|
-
if (contexts[0].pages().length === 0) {
|
|
93
|
-
await new Promise(r => setTimeout(r, 100))
|
|
94
|
-
}
|
|
95
|
-
pages = contexts[0].pages()
|
|
96
|
-
|
|
97
|
-
foundPage = pages.find(p => p.url() === testUrl)
|
|
98
|
-
expect(foundPage).toBeDefined()
|
|
99
|
-
expect(foundPage?.url()).toBe(testUrl)
|
|
100
|
-
|
|
101
|
-
const sum2 = await foundPage?.evaluate(() => 2 + 2)
|
|
102
|
-
expect(sum2).toBe(4)
|
|
103
|
-
|
|
104
|
-
await directBrowser.close()
|
|
105
|
-
await page.close()
|
|
106
|
-
}, 120000)
|
|
107
|
-
|
|
108
|
-
it('should handle new pages and toggling with persistent connection', async () => {
|
|
109
|
-
const browserContext = getBrowserContext()
|
|
110
|
-
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
111
|
-
|
|
112
|
-
const directBrowser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
113
|
-
await new Promise(r => setTimeout(r, 100))
|
|
114
|
-
|
|
115
|
-
// 1. Create a new page
|
|
116
|
-
const page = await browserContext.newPage()
|
|
117
|
-
const testUrl = 'https://example.com/persistent'
|
|
118
|
-
await page.goto(testUrl)
|
|
119
|
-
await page.bringToFront()
|
|
120
|
-
|
|
121
|
-
// 2. Enable extension
|
|
122
|
-
await serviceWorker.evaluate(async () => {
|
|
123
|
-
await globalThis.toggleExtensionForActiveTab()
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
// 3. Verify page appears (polling)
|
|
127
|
-
let foundPage
|
|
128
|
-
for (let i = 0; i < 50; i++) {
|
|
129
|
-
const pages = directBrowser.contexts()[0].pages()
|
|
130
|
-
foundPage = pages.find(p => p.url() === testUrl)
|
|
131
|
-
if (foundPage) break
|
|
132
|
-
await new Promise(r => setTimeout(r, 100))
|
|
133
|
-
}
|
|
134
|
-
expect(foundPage).toBeDefined()
|
|
135
|
-
expect(foundPage?.url()).toBe(testUrl)
|
|
136
|
-
|
|
137
|
-
const sum1 = await foundPage?.evaluate(() => 10 + 20)
|
|
138
|
-
expect(sum1).toBe(30)
|
|
139
|
-
|
|
140
|
-
// 4. Disable extension
|
|
141
|
-
await serviceWorker.evaluate(async () => {
|
|
142
|
-
await globalThis.toggleExtensionForActiveTab()
|
|
143
|
-
})
|
|
144
|
-
|
|
145
|
-
// 5. Verify page disappears (polling)
|
|
146
|
-
for (let i = 0; i < 50; i++) {
|
|
147
|
-
const pages = directBrowser.contexts()[0].pages()
|
|
148
|
-
foundPage = pages.find(p => p.url() === testUrl)
|
|
149
|
-
if (!foundPage) break
|
|
150
|
-
await new Promise(r => setTimeout(r, 100))
|
|
151
|
-
}
|
|
152
|
-
expect(foundPage).toBeUndefined()
|
|
153
|
-
|
|
154
|
-
// 6. Re-enable extension
|
|
155
|
-
await serviceWorker.evaluate(async () => {
|
|
156
|
-
await globalThis.toggleExtensionForActiveTab()
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
// 7. Verify page reappears (polling)
|
|
160
|
-
for (let i = 0; i < 50; i++) {
|
|
161
|
-
const pages = directBrowser.contexts()[0].pages()
|
|
162
|
-
foundPage = pages.find(p => p.url() === testUrl)
|
|
163
|
-
if (foundPage) break
|
|
164
|
-
await new Promise(r => setTimeout(r, 100))
|
|
165
|
-
}
|
|
166
|
-
expect(foundPage).toBeDefined()
|
|
167
|
-
expect(foundPage?.url()).toBe(testUrl)
|
|
168
|
-
|
|
169
|
-
const sum2 = await foundPage?.evaluate(() => 30 + 40)
|
|
170
|
-
expect(sum2).toBe(70)
|
|
171
|
-
|
|
172
|
-
await page.close()
|
|
173
|
-
await directBrowser.close()
|
|
174
|
-
})
|
|
175
|
-
|
|
176
|
-
it('should maintain connection across reloads and navigation', async () => {
|
|
177
|
-
const browserContext = getBrowserContext()
|
|
178
|
-
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
179
|
-
|
|
180
|
-
// 1. Setup page
|
|
181
|
-
const page = await browserContext.newPage()
|
|
182
|
-
const initialUrl = 'https://example.com/'
|
|
183
|
-
await page.goto(initialUrl)
|
|
184
|
-
await page.bringToFront()
|
|
185
|
-
|
|
186
|
-
// 2. Enable extension
|
|
187
|
-
await serviceWorker.evaluate(async () => {
|
|
188
|
-
await globalThis.toggleExtensionForActiveTab()
|
|
189
|
-
})
|
|
190
|
-
|
|
191
|
-
// 3. Connect via CDP
|
|
192
|
-
const cdpUrl = getCdpUrl({ port: TEST_PORT })
|
|
193
|
-
const directBrowser = await chromium.connectOverCDP(cdpUrl)
|
|
194
|
-
const connectedPage = directBrowser.contexts()[0].pages().find(p => p.url() === initialUrl)
|
|
195
|
-
expect(connectedPage).toBeDefined()
|
|
196
|
-
|
|
197
|
-
expect(await connectedPage?.evaluate(() => 1 + 1)).toBe(2)
|
|
198
|
-
|
|
199
|
-
// 4. Reload
|
|
200
|
-
await connectedPage?.reload()
|
|
201
|
-
await connectedPage?.waitForLoadState('domcontentloaded')
|
|
202
|
-
expect(await connectedPage?.title()).toBe('Example Domain')
|
|
203
|
-
|
|
204
|
-
expect(await connectedPage?.evaluate(() => 2 + 2)).toBe(4)
|
|
205
|
-
|
|
206
|
-
// 5. Navigate to new URL
|
|
207
|
-
const newUrl = 'https://example.org/'
|
|
208
|
-
await connectedPage?.goto(newUrl)
|
|
209
|
-
await connectedPage?.waitForLoadState('domcontentloaded')
|
|
210
|
-
|
|
211
|
-
expect(connectedPage?.url()).toBe(newUrl)
|
|
212
|
-
expect(await connectedPage?.title()).toContain('Example Domain')
|
|
213
|
-
|
|
214
|
-
expect(await connectedPage?.evaluate(() => 3 + 3)).toBe(6)
|
|
215
|
-
|
|
216
|
-
await directBrowser.close()
|
|
217
|
-
await page.close()
|
|
218
|
-
})
|
|
219
|
-
|
|
220
|
-
it('should support multiple concurrent tabs', async () => {
|
|
221
|
-
const browserContext = getBrowserContext()
|
|
222
|
-
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
223
|
-
await new Promise(resolve => setTimeout(resolve, 100))
|
|
224
|
-
|
|
225
|
-
// Tab A
|
|
226
|
-
const pageA = await browserContext.newPage()
|
|
227
|
-
await pageA.goto('https://example.com/tab-a')
|
|
228
|
-
await pageA.bringToFront()
|
|
229
|
-
await new Promise(resolve => setTimeout(resolve, 100))
|
|
230
|
-
await serviceWorker.evaluate(async () => {
|
|
231
|
-
await globalThis.toggleExtensionForActiveTab()
|
|
232
|
-
})
|
|
233
|
-
|
|
234
|
-
// Tab B
|
|
235
|
-
const pageB = await browserContext.newPage()
|
|
236
|
-
await pageB.goto('https://example.com/tab-b')
|
|
237
|
-
await pageB.bringToFront()
|
|
238
|
-
await new Promise(resolve => setTimeout(resolve, 100))
|
|
239
|
-
await serviceWorker.evaluate(async () => {
|
|
240
|
-
await globalThis.toggleExtensionForActiveTab()
|
|
241
|
-
})
|
|
242
|
-
|
|
243
|
-
// Get target IDs for both
|
|
244
|
-
const targetIds = await serviceWorker.evaluate(async () => {
|
|
245
|
-
const state = globalThis.getExtensionState()
|
|
246
|
-
const chrome = globalThis.chrome
|
|
247
|
-
const tabs = await chrome.tabs.query({})
|
|
248
|
-
const tabA = tabs.find((t: any) => t.url?.includes('tab-a'))
|
|
249
|
-
const tabB = tabs.find((t: any) => t.url?.includes('tab-b'))
|
|
250
|
-
return {
|
|
251
|
-
idA: state.tabs.get(tabA?.id ?? -1)?.targetId,
|
|
252
|
-
idB: state.tabs.get(tabB?.id ?? -1)?.targetId
|
|
253
|
-
}
|
|
254
|
-
})
|
|
255
|
-
|
|
256
|
-
expect(targetIds).toMatchInlineSnapshot({
|
|
257
|
-
idA: expect.any(String),
|
|
258
|
-
idB: expect.any(String)
|
|
259
|
-
}, `
|
|
259
|
+
expect(targetIds).toMatchInlineSnapshot(
|
|
260
|
+
{
|
|
261
|
+
idA: expect.any(String),
|
|
262
|
+
idB: expect.any(String),
|
|
263
|
+
},
|
|
264
|
+
`
|
|
260
265
|
{
|
|
261
266
|
"idA": Any<String>,
|
|
262
267
|
"idB": Any<String>,
|
|
263
268
|
}
|
|
264
|
-
|
|
265
|
-
|
|
269
|
+
`,
|
|
270
|
+
)
|
|
271
|
+
expect(targetIds.idA).not.toBe(targetIds.idB)
|
|
266
272
|
|
|
267
|
-
|
|
268
|
-
|
|
273
|
+
// Verify independent connections
|
|
274
|
+
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
269
275
|
|
|
270
|
-
|
|
276
|
+
const pages = browser.contexts()[0].pages()
|
|
271
277
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
278
|
+
const results = await Promise.all(
|
|
279
|
+
pages.map(async (p) => ({
|
|
280
|
+
url: p.url(),
|
|
281
|
+
title: await p.title(),
|
|
282
|
+
})),
|
|
283
|
+
)
|
|
276
284
|
|
|
277
|
-
|
|
285
|
+
expect(results).toMatchInlineSnapshot(`
|
|
278
286
|
[
|
|
279
287
|
{
|
|
280
288
|
"title": "",
|
|
@@ -291,140 +299,256 @@ describe('Extension Connection Tests', () => {
|
|
|
291
299
|
]
|
|
292
300
|
`)
|
|
293
301
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
302
|
+
// Verify execution on both pages
|
|
303
|
+
const pageA_CDP = pages.find((p) => p.url().includes('tab-a'))
|
|
304
|
+
const pageB_CDP = pages.find((p) => p.url().includes('tab-b'))
|
|
305
|
+
|
|
306
|
+
expect(await pageA_CDP?.evaluate(() => 10 + 10)).toBe(20)
|
|
307
|
+
expect(await pageB_CDP?.evaluate(() => 20 + 20)).toBe(40)
|
|
297
308
|
|
|
298
|
-
|
|
299
|
-
|
|
309
|
+
await browser.close()
|
|
310
|
+
await pageA.close()
|
|
311
|
+
await pageB.close()
|
|
312
|
+
})
|
|
300
313
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
314
|
+
it('should warn and switch page when the active page closes', async () => {
|
|
315
|
+
const browserContext = getBrowserContext()
|
|
316
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
317
|
+
|
|
318
|
+
const pageA = await browserContext.newPage()
|
|
319
|
+
await pageA.goto('https://example.com/close-warning-a')
|
|
320
|
+
await pageA.bringToFront()
|
|
321
|
+
await serviceWorker.evaluate(async () => {
|
|
322
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
304
323
|
})
|
|
305
324
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
325
|
+
const pageB = await browserContext.newPage()
|
|
326
|
+
await pageB.goto('https://example.com/close-warning-b')
|
|
327
|
+
await pageB.bringToFront()
|
|
328
|
+
await serviceWorker.evaluate(async () => {
|
|
329
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
330
|
+
})
|
|
309
331
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
332
|
+
const closeResult = await client.callTool({
|
|
333
|
+
name: 'execute',
|
|
334
|
+
arguments: {
|
|
335
|
+
code: js`
|
|
336
|
+
state.page = page;
|
|
337
|
+
const closedUrl = state.page.url();
|
|
338
|
+
await state.page.close();
|
|
339
|
+
return { closedUrl, remainingPages: context.pages().length };
|
|
340
|
+
`,
|
|
341
|
+
},
|
|
342
|
+
})
|
|
314
343
|
|
|
315
|
-
|
|
344
|
+
const closeOutput = (closeResult as any).content[0].text
|
|
345
|
+
expect(closeOutput).toContain('[WARNING] The current page in state.page was closed')
|
|
346
|
+
expect(closeOutput).toContain('Switched active page to index')
|
|
347
|
+
expect((closeResult as any).isError).not.toBe(true)
|
|
316
348
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
349
|
+
const nextResult = await client.callTool({
|
|
350
|
+
name: 'execute',
|
|
351
|
+
arguments: {
|
|
352
|
+
code: js`
|
|
353
|
+
return { pageUrl: page.url(), pagesCount: context.pages().length };
|
|
354
|
+
`,
|
|
355
|
+
},
|
|
356
|
+
})
|
|
320
357
|
|
|
321
|
-
|
|
322
|
-
|
|
358
|
+
const nextOutput = (nextResult as any).content[0].text
|
|
359
|
+
expect(nextOutput).toContain('pagesCount')
|
|
360
|
+
expect(nextOutput).not.toContain('No Playwright pages are available')
|
|
361
|
+
expect(nextOutput).not.toContain('[WARNING] The current page was closed')
|
|
362
|
+
expect((nextResult as any).isError).not.toBe(true)
|
|
323
363
|
|
|
324
|
-
|
|
364
|
+
if (!pageA.isClosed()) {
|
|
365
|
+
await pageA.close()
|
|
366
|
+
}
|
|
367
|
+
if (!pageB.isClosed()) {
|
|
368
|
+
await pageB.close()
|
|
369
|
+
}
|
|
370
|
+
})
|
|
325
371
|
|
|
326
|
-
|
|
327
|
-
|
|
372
|
+
it('should switch page without warning when closed page is not stored in state', async () => {
|
|
373
|
+
const browserContext = getBrowserContext()
|
|
374
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
328
375
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
376
|
+
const pageA = await browserContext.newPage()
|
|
377
|
+
await pageA.goto('https://example.com/close-no-state-warning-a')
|
|
378
|
+
await pageA.bringToFront()
|
|
379
|
+
await serviceWorker.evaluate(async () => {
|
|
380
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
381
|
+
})
|
|
332
382
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
383
|
+
const pageB = await browserContext.newPage()
|
|
384
|
+
await pageB.goto('https://example.com/close-no-state-warning-b')
|
|
385
|
+
await pageB.bringToFront()
|
|
386
|
+
await serviceWorker.evaluate(async () => {
|
|
387
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
const closeResult = await client.callTool({
|
|
391
|
+
name: 'execute',
|
|
392
|
+
arguments: {
|
|
393
|
+
code: js`
|
|
394
|
+
const closedUrl = page.url();
|
|
395
|
+
await page.close();
|
|
396
|
+
return { closedUrl, remainingPages: context.pages().length };
|
|
397
|
+
`,
|
|
398
|
+
},
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
const closeOutput = (closeResult as any).content[0].text
|
|
402
|
+
expect(closeOutput).not.toContain('[WARNING] The current page in state.page was closed')
|
|
403
|
+
expect(closeOutput).not.toContain('Switched active page to index')
|
|
404
|
+
expect((closeResult as any).isError).not.toBe(true)
|
|
405
|
+
|
|
406
|
+
const nextResult = await client.callTool({
|
|
407
|
+
name: 'execute',
|
|
408
|
+
arguments: {
|
|
409
|
+
code: js`
|
|
410
|
+
return { pageUrl: page.url(), pagesCount: context.pages().length };
|
|
411
|
+
`,
|
|
412
|
+
},
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
const nextOutput = (nextResult as any).content[0].text
|
|
416
|
+
expect(nextOutput).toContain('pagesCount')
|
|
417
|
+
expect((nextResult as any).isError).not.toBe(true)
|
|
418
|
+
|
|
419
|
+
if (!pageA.isClosed()) {
|
|
420
|
+
await pageA.close()
|
|
421
|
+
}
|
|
422
|
+
if (!pageB.isClosed()) {
|
|
423
|
+
await pageB.close()
|
|
424
|
+
}
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
it('should show correct url when enabling extension after navigation', async () => {
|
|
428
|
+
const browserContext = getBrowserContext()
|
|
429
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
336
430
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
431
|
+
const page = await browserContext.newPage()
|
|
432
|
+
const targetUrl = 'https://example.com/late-enable'
|
|
433
|
+
await page.goto(targetUrl)
|
|
434
|
+
await page.bringToFront()
|
|
340
435
|
|
|
341
|
-
|
|
342
|
-
await page.waitForLoadState('domcontentloaded')
|
|
343
|
-
await page.bringToFront()
|
|
436
|
+
await page.waitForLoadState('domcontentloaded')
|
|
344
437
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
438
|
+
await serviceWorker.evaluate(async () => {
|
|
439
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
443
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
444
|
+
|
|
445
|
+
const cdpPage = browser
|
|
446
|
+
.contexts()[0]
|
|
447
|
+
.pages()
|
|
448
|
+
.find((p) => p.url() === targetUrl)
|
|
449
|
+
|
|
450
|
+
expect(cdpPage).toBeDefined()
|
|
451
|
+
expect(cdpPage?.url()).toBe(targetUrl)
|
|
452
|
+
|
|
453
|
+
await browser.close()
|
|
454
|
+
await page.close()
|
|
455
|
+
}, 60000)
|
|
456
|
+
|
|
457
|
+
it('should be able to reconnect after disconnecting everything', async () => {
|
|
458
|
+
const browserContext = getBrowserContext()
|
|
459
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
460
|
+
|
|
461
|
+
const pages = await browserContext.pages()
|
|
462
|
+
expect(pages.length).toBeGreaterThan(0)
|
|
463
|
+
const page = pages[0]
|
|
351
464
|
|
|
352
|
-
|
|
465
|
+
await page.goto('https://example.com/disconnect-test')
|
|
466
|
+
await page.waitForLoadState('domcontentloaded')
|
|
467
|
+
await page.bringToFront()
|
|
353
468
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
469
|
+
// Enable extension on this page
|
|
470
|
+
const initialEnable = await serviceWorker.evaluate(async () => {
|
|
471
|
+
return await globalThis.toggleExtensionForActiveTab()
|
|
472
|
+
})
|
|
473
|
+
console.log('Initial enable result:', initialEnable)
|
|
474
|
+
expect(initialEnable.isConnected).toBe(true)
|
|
475
|
+
|
|
476
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
477
|
+
|
|
478
|
+
// Verify MCP can see the page
|
|
479
|
+
const beforeDisconnect = await client.callTool({
|
|
480
|
+
name: 'execute',
|
|
481
|
+
arguments: {
|
|
482
|
+
code: js`
|
|
359
483
|
const pages = context.pages();
|
|
360
484
|
console.log('Pages before disconnect:', pages.length);
|
|
361
485
|
const testPage = pages.find(p => p.url().includes('disconnect-test'));
|
|
362
486
|
console.log('Found test page:', !!testPage);
|
|
363
487
|
return { pagesCount: pages.length, foundTestPage: !!testPage };
|
|
364
488
|
`,
|
|
365
|
-
|
|
366
|
-
|
|
489
|
+
},
|
|
490
|
+
})
|
|
367
491
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
492
|
+
const beforeOutput = (beforeDisconnect as any).content[0].text
|
|
493
|
+
expect(beforeOutput).toContain('foundTestPage')
|
|
494
|
+
console.log('Before disconnect:', beforeOutput)
|
|
371
495
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
496
|
+
// 2. Disconnect everything
|
|
497
|
+
console.log('Calling disconnectEverything...')
|
|
498
|
+
await serviceWorker.evaluate(async () => {
|
|
499
|
+
await globalThis.disconnectEverything()
|
|
500
|
+
})
|
|
377
501
|
|
|
378
|
-
|
|
502
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
379
503
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
504
|
+
// 3. Verify MCP cannot execute code anymore (no pages available)
|
|
505
|
+
const afterDisconnect = await client.callTool({
|
|
506
|
+
name: 'execute',
|
|
507
|
+
arguments: {
|
|
508
|
+
code: js`
|
|
385
509
|
const pages = context.pages();
|
|
386
510
|
console.log('Pages after disconnect:', pages.length);
|
|
387
511
|
return { pagesCount: pages.length };
|
|
388
512
|
`,
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
513
|
+
},
|
|
514
|
+
})
|
|
515
|
+
|
|
516
|
+
const afterDisconnectOutput = (afterDisconnect as any).content[0].text
|
|
517
|
+
console.log('After disconnect:', afterDisconnectOutput)
|
|
518
|
+
expect((afterDisconnect as any).isError).toBe(true)
|
|
519
|
+
expect(afterDisconnectOutput).toContain('No Playwright pages are available')
|
|
520
|
+
|
|
521
|
+
// 4. Re-enable extension on the same page
|
|
522
|
+
console.log('Re-enabling extension...')
|
|
523
|
+
await page.bringToFront()
|
|
524
|
+
const reconnectResult = await serviceWorker.evaluate(async () => {
|
|
525
|
+
console.log('About to call toggleExtensionForActiveTab')
|
|
526
|
+
const result = await globalThis.toggleExtensionForActiveTab()
|
|
527
|
+
console.log('toggleExtensionForActiveTab result:', result)
|
|
528
|
+
return result
|
|
529
|
+
})
|
|
530
|
+
|
|
531
|
+
console.log('Reconnect result:', reconnectResult)
|
|
532
|
+
expect(reconnectResult.isConnected).toBe(true)
|
|
533
|
+
|
|
534
|
+
console.log('Waiting for reconnection to stabilize...')
|
|
535
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
536
|
+
|
|
537
|
+
// 5. Reset the MCP client's playwright connection
|
|
538
|
+
console.log('Resetting MCP playwright connection...')
|
|
539
|
+
const resetResult = await client.callTool({
|
|
540
|
+
name: 'reset',
|
|
541
|
+
arguments: {},
|
|
542
|
+
})
|
|
543
|
+
console.log('Reset result:', (resetResult as any).content[0].text)
|
|
544
|
+
expect((resetResult as any).content[0].text).toContain('Connection reset successfully')
|
|
545
|
+
|
|
546
|
+
// 6. Verify MCP can see the page again
|
|
547
|
+
console.log('Attempting to access page via MCP...')
|
|
548
|
+
const afterReconnect = await client.callTool({
|
|
549
|
+
name: 'execute',
|
|
550
|
+
arguments: {
|
|
551
|
+
code: js`
|
|
428
552
|
console.log('Checking pages after reconnect...');
|
|
429
553
|
const pages = context.pages();
|
|
430
554
|
console.log('Pages after reconnect:', pages.length);
|
|
@@ -444,38 +568,38 @@ describe('Extension Connection Tests', () => {
|
|
|
444
568
|
|
|
445
569
|
return { pagesCount: pages.length, foundTestPage: false };
|
|
446
570
|
`,
|
|
447
|
-
|
|
448
|
-
})
|
|
449
|
-
|
|
450
|
-
const afterReconnectOutput = (afterReconnect as any).content[0].text
|
|
451
|
-
console.log('After reconnect:', afterReconnectOutput)
|
|
452
|
-
expect(afterReconnectOutput).toContain('foundTestPage')
|
|
453
|
-
expect(afterReconnectOutput).toContain('disconnect-test')
|
|
454
|
-
|
|
455
|
-
// Clean up
|
|
456
|
-
await page.goto('about:blank')
|
|
571
|
+
},
|
|
457
572
|
})
|
|
458
573
|
|
|
459
|
-
|
|
460
|
-
|
|
574
|
+
const afterReconnectOutput = (afterReconnect as any).content[0].text
|
|
575
|
+
console.log('After reconnect:', afterReconnectOutput)
|
|
576
|
+
expect(afterReconnectOutput).toContain('foundTestPage')
|
|
577
|
+
expect(afterReconnectOutput).toContain('disconnect-test')
|
|
578
|
+
|
|
579
|
+
// Clean up
|
|
580
|
+
await page.goto('about:blank')
|
|
581
|
+
})
|
|
461
582
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
await page.goto('https://example.com/auto-reconnect-test')
|
|
465
|
-
await page.waitForLoadState('domcontentloaded')
|
|
466
|
-
await page.bringToFront()
|
|
583
|
+
it('should auto-reconnect MCP after extension WebSocket reconnects', async () => {
|
|
584
|
+
const serviceWorker = await getExtensionServiceWorker(testCtx!.browserContext)
|
|
467
585
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
586
|
+
// 1. Create a test page and enable extension
|
|
587
|
+
const page = await testCtx!.browserContext.newPage()
|
|
588
|
+
await page.goto('https://example.com/auto-reconnect-test')
|
|
589
|
+
await page.waitForLoadState('domcontentloaded')
|
|
590
|
+
await page.bringToFront()
|
|
473
591
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
592
|
+
const initialEnable = await serviceWorker.evaluate(async () => {
|
|
593
|
+
return await globalThis.toggleExtensionForActiveTab()
|
|
594
|
+
})
|
|
595
|
+
expect(initialEnable.isConnected).toBe(true)
|
|
596
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
597
|
+
|
|
598
|
+
// 2. Verify MCP can execute commands
|
|
599
|
+
const beforeResult = await client.callTool({
|
|
600
|
+
name: 'execute',
|
|
601
|
+
arguments: {
|
|
602
|
+
code: js`
|
|
479
603
|
let testPage;
|
|
480
604
|
for (let i = 0; i < 20; i++) {
|
|
481
605
|
const pages = context.pages();
|
|
@@ -486,32 +610,32 @@ describe('Extension Connection Tests', () => {
|
|
|
486
610
|
const pages = context.pages();
|
|
487
611
|
return { pagesCount: pages.length, foundTestPage: !!testPage, url: testPage?.url() };
|
|
488
612
|
`,
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
613
|
+
},
|
|
614
|
+
})
|
|
615
|
+
const beforeOutput = (beforeResult as any).content[0].text
|
|
616
|
+
expect(beforeOutput).toContain('foundTestPage')
|
|
617
|
+
expect(beforeOutput).toContain('true')
|
|
618
|
+
expect(beforeOutput).toContain('auto-reconnect-test')
|
|
619
|
+
|
|
620
|
+
// 3. Simulate extension WebSocket reconnection
|
|
621
|
+
await serviceWorker.evaluate(async () => {
|
|
622
|
+
await globalThis.disconnectEverything()
|
|
623
|
+
})
|
|
624
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
625
|
+
|
|
626
|
+
// Re-enable extension
|
|
627
|
+
await page.bringToFront()
|
|
628
|
+
const reconnectResult = await serviceWorker.evaluate(async () => {
|
|
629
|
+
return await globalThis.toggleExtensionForActiveTab()
|
|
630
|
+
})
|
|
631
|
+
expect(reconnectResult.isConnected).toBe(true)
|
|
632
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
633
|
+
|
|
634
|
+
// 4. Execute command WITHOUT calling resetPlaywright()
|
|
635
|
+
const afterResult = await client.callTool({
|
|
636
|
+
name: 'execute',
|
|
637
|
+
arguments: {
|
|
638
|
+
code: js`
|
|
515
639
|
let testPage;
|
|
516
640
|
for (let i = 0; i < 20; i++) {
|
|
517
641
|
const pages = context.pages();
|
|
@@ -522,112 +646,112 @@ describe('Extension Connection Tests', () => {
|
|
|
522
646
|
const pages = context.pages();
|
|
523
647
|
return { pagesCount: pages.length, foundTestPage: !!testPage, url: testPage?.url() };
|
|
524
648
|
`,
|
|
525
|
-
|
|
526
|
-
|
|
649
|
+
},
|
|
650
|
+
})
|
|
527
651
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
652
|
+
const afterOutput = (afterResult as any).content[0].text
|
|
653
|
+
expect(afterOutput).toContain('foundTestPage')
|
|
654
|
+
expect(afterOutput).toContain('true')
|
|
655
|
+
expect(afterOutput).toContain('auto-reconnect-test')
|
|
656
|
+
expect(afterOutput).not.toContain('Extension not connected')
|
|
657
|
+
expect((afterResult as any).isError).not.toBe(true)
|
|
534
658
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
659
|
+
// Clean up
|
|
660
|
+
await page.goto('about:blank')
|
|
661
|
+
})
|
|
538
662
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
663
|
+
it('should maintain correct page.url() with service worker pages', async () => {
|
|
664
|
+
const browserContext = getBrowserContext()
|
|
665
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
542
666
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
667
|
+
const page = await browserContext.newPage()
|
|
668
|
+
const targetUrl = 'https://example.com/sw-test'
|
|
669
|
+
await page.goto(targetUrl)
|
|
670
|
+
await page.bringToFront()
|
|
547
671
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
672
|
+
await serviceWorker.evaluate(async () => {
|
|
673
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
674
|
+
})
|
|
551
675
|
|
|
552
|
-
|
|
676
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
553
677
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
678
|
+
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
679
|
+
const cdpPages = browser.contexts()[0].pages()
|
|
680
|
+
const testPage = cdpPages.find((p) => p.url().includes('sw-test'))
|
|
557
681
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
682
|
+
expect(testPage).toBeDefined()
|
|
683
|
+
expect(testPage?.url()).toContain('sw-test')
|
|
684
|
+
expect(testPage?.url()).not.toContain('sw.js')
|
|
561
685
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
686
|
+
await browser.close()
|
|
687
|
+
await page.close()
|
|
688
|
+
}, 30000)
|
|
565
689
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
690
|
+
it('should maintain correct page.url() after repeated connections', async () => {
|
|
691
|
+
const browserContext = getBrowserContext()
|
|
692
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
569
693
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
694
|
+
const page = await browserContext.newPage()
|
|
695
|
+
const targetUrl = 'https://example.com/repeated-test'
|
|
696
|
+
await page.goto(targetUrl)
|
|
697
|
+
await page.bringToFront()
|
|
574
698
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
699
|
+
await serviceWorker.evaluate(async () => {
|
|
700
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
701
|
+
})
|
|
578
702
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
703
|
+
for (let i = 0; i < 5; i++) {
|
|
704
|
+
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
705
|
+
const cdpPages = browser.contexts()[0].pages()
|
|
706
|
+
const testPage = cdpPages.find((p) => p.url().includes('repeated-test'))
|
|
583
707
|
|
|
584
|
-
|
|
585
|
-
|
|
708
|
+
expect(testPage).toBeDefined()
|
|
709
|
+
expect(testPage?.url()).toBe(targetUrl)
|
|
586
710
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
711
|
+
await browser.close()
|
|
712
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
713
|
+
}
|
|
590
714
|
|
|
591
|
-
|
|
592
|
-
|
|
715
|
+
await page.close()
|
|
716
|
+
}, 30000)
|
|
593
717
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
718
|
+
it('should maintain correct page.url() with concurrent MCP and CDP connections', async () => {
|
|
719
|
+
const browserContext = getBrowserContext()
|
|
720
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
597
721
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
722
|
+
const page = await browserContext.newPage()
|
|
723
|
+
const targetUrl = 'https://example.com/concurrent-test'
|
|
724
|
+
await page.goto(targetUrl)
|
|
725
|
+
await page.bringToFront()
|
|
602
726
|
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
727
|
+
await serviceWorker.evaluate(async () => {
|
|
728
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
729
|
+
})
|
|
606
730
|
|
|
607
|
-
|
|
731
|
+
await new Promise((r) => setTimeout(r, 400))
|
|
608
732
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
733
|
+
const [mcpResult, cdpBrowser] = await Promise.all([
|
|
734
|
+
client.callTool({
|
|
735
|
+
name: 'execute',
|
|
736
|
+
arguments: {
|
|
737
|
+
code: js`
|
|
614
738
|
const pages = context.pages();
|
|
615
739
|
const testPage = pages.find(p => p.url().includes('concurrent-test'));
|
|
616
740
|
return { url: testPage?.url(), found: !!testPage };
|
|
617
741
|
`,
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
742
|
+
},
|
|
743
|
+
}),
|
|
744
|
+
chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT })),
|
|
745
|
+
])
|
|
622
746
|
|
|
623
|
-
|
|
624
|
-
|
|
747
|
+
const mcpOutput = (mcpResult as any).content[0].text
|
|
748
|
+
expect(mcpOutput).toContain(targetUrl)
|
|
625
749
|
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
750
|
+
const cdpPages = cdpBrowser.contexts()[0].pages()
|
|
751
|
+
const cdpPage = cdpPages.find((p) => p.url().includes('concurrent-test'))
|
|
752
|
+
expect(cdpPage?.url()).toBe(targetUrl)
|
|
629
753
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
754
|
+
await cdpBrowser.close()
|
|
755
|
+
await page.close()
|
|
756
|
+
}, 30000)
|
|
633
757
|
})
|