playwriter 0.0.63 → 0.0.80
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/aria-snapshot.d.ts +41 -3
- package/dist/aria-snapshot.d.ts.map +1 -1
- package/dist/aria-snapshot.js +131 -54
- 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 +408 -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 +295 -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 +281 -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 +594 -255
- package/dist/protocol.d.ts +4 -0
- package/dist/protocol.d.ts.map +1 -1
- package/dist/readability.js +1 -1
- 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 +44 -10
- package/dist/relay-client.js.map +1 -1
- package/dist/relay-core.test.d.ts.map +1 -1
- package/dist/relay-core.test.js +187 -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 +54 -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 +42 -4
- package/dist/screen-recording.d.ts.map +1 -1
- package/dist/screen-recording.js +88 -13
- package/dist/screen-recording.js.map +1 -1
- 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 +303 -116
- package/src/aria-snapshot.unit.test.ts +199 -141
- package/src/aria-snapshots/github-raw.txt +1 -1
- package/src/aria-snapshots/hackernews-interactive.txt +240 -240
- package/src/aria-snapshots/hackernews-raw.txt +270 -270
- 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 +949 -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 +372 -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 +368 -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 +107 -0
- package/src/recording-relay.ts +20 -12
- package/src/relay-client.ts +84 -17
- package/src/relay-core.test.ts +761 -583
- package/src/relay-navigation.test.ts +517 -484
- 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.ts +175 -31
- package/src/skill.md +619 -271
- package/src/snapshot-tools.test.ts +580 -528
- package/src/snapshots/shadcn-ui-accessibility-full.md +181 -183
- package/src/snapshots/shadcn-ui-accessibility-interactive.md +119 -121
- 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
|
@@ -1,194 +1,194 @@
|
|
|
1
1
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
|
|
2
2
|
import { chromium, type Page } from '@xmorse/playwright-core'
|
|
3
3
|
import { getCdpUrl } from './utils.js'
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
setupTestContext,
|
|
6
|
+
cleanupTestContext,
|
|
7
|
+
getExtensionServiceWorker,
|
|
8
|
+
type TestContext,
|
|
9
|
+
withTimeout,
|
|
10
|
+
createSimpleServer,
|
|
11
|
+
} from './test-utils.js'
|
|
5
12
|
import './test-declarations.js'
|
|
6
13
|
|
|
7
14
|
const TEST_PORT = 19992
|
|
8
15
|
|
|
9
16
|
describe('Relay Navigation Tests', () => {
|
|
10
|
-
|
|
17
|
+
let testCtx: TestContext | null = null
|
|
11
18
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
19
|
+
beforeAll(async () => {
|
|
20
|
+
testCtx = await setupTestContext({ port: TEST_PORT, tempDirPrefix: 'pw-nav-test-', toggleExtension: true })
|
|
21
|
+
}, 600000)
|
|
15
22
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
23
|
+
afterAll(async () => {
|
|
24
|
+
await cleanupTestContext(testCtx)
|
|
25
|
+
testCtx = null
|
|
26
|
+
})
|
|
20
27
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
28
|
+
const getBrowserContext = () => {
|
|
29
|
+
if (!testCtx?.browserContext) throw new Error('Browser not initialized')
|
|
30
|
+
return testCtx.browserContext
|
|
31
|
+
}
|
|
25
32
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
timeoutMs,
|
|
29
|
-
}: {
|
|
30
|
-
page: Page
|
|
31
|
-
timeoutMs: number
|
|
32
|
-
}) => {
|
|
33
|
-
const startTime = Date.now()
|
|
34
|
-
|
|
35
|
-
while (Date.now() - startTime < timeoutMs) {
|
|
36
|
-
try {
|
|
37
|
-
const readyState = await page.evaluate(() => {
|
|
38
|
-
return document.readyState
|
|
39
|
-
})
|
|
40
|
-
if (readyState !== 'loading') {
|
|
41
|
-
return
|
|
42
|
-
}
|
|
43
|
-
} catch (e) {
|
|
44
|
-
if (!(e instanceof Error) || !e.message.includes('Execution context was destroyed')) {
|
|
45
|
-
throw new Error('Failed while waiting for stable document readyState', { cause: e })
|
|
46
|
-
}
|
|
47
|
-
}
|
|
33
|
+
const waitForStableDocumentReadyState = async ({ page, timeoutMs }: { page: Page; timeoutMs: number }) => {
|
|
34
|
+
const startTime = Date.now()
|
|
48
35
|
|
|
49
|
-
|
|
36
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
37
|
+
try {
|
|
38
|
+
const readyState = await page.evaluate(() => {
|
|
39
|
+
return document.readyState
|
|
40
|
+
})
|
|
41
|
+
if (readyState !== 'loading') {
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
} catch (e) {
|
|
45
|
+
if (!(e instanceof Error) || !e.message.includes('Execution context was destroyed')) {
|
|
46
|
+
throw new Error('Failed while waiting for stable document readyState', { cause: e })
|
|
50
47
|
}
|
|
48
|
+
}
|
|
51
49
|
|
|
52
|
-
|
|
50
|
+
await page.waitForTimeout(100)
|
|
53
51
|
}
|
|
54
52
|
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
throw new Error(`Timed out waiting for stable document readyState after ${timeoutMs}ms`)
|
|
54
|
+
}
|
|
57
55
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
61
|
-
const context = browser.contexts()[0]
|
|
56
|
+
it('should be usable after toggle with valid URL', async () => {
|
|
57
|
+
// Validates the extension waits for a non-empty URL before attaching.
|
|
62
58
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
})
|
|
59
|
+
const browserContext = getBrowserContext()
|
|
60
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
61
|
+
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
62
|
+
const context = browser.contexts()[0]
|
|
68
63
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
64
|
+
const server = await createSimpleServer({
|
|
65
|
+
routes: {
|
|
66
|
+
'/': '<!doctype html><html><body>ok</body></html>',
|
|
67
|
+
},
|
|
68
|
+
})
|
|
73
69
|
|
|
74
|
-
|
|
70
|
+
const page = await browserContext.newPage()
|
|
71
|
+
try {
|
|
72
|
+
await page.goto(server.baseUrl, { waitUntil: 'domcontentloaded' })
|
|
73
|
+
await page.bringToFront()
|
|
75
74
|
|
|
76
|
-
|
|
77
|
-
await globalThis.toggleExtensionForActiveTab()
|
|
78
|
-
})
|
|
75
|
+
const pagePromise = context.waitForEvent('page', { timeout: 5000 })
|
|
79
76
|
|
|
80
|
-
|
|
81
|
-
|
|
77
|
+
await serviceWorker.evaluate(async () => {
|
|
78
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
79
|
+
})
|
|
82
80
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
expect(targetPage.url()).toContain(server.baseUrl)
|
|
81
|
+
const targetPage = await pagePromise
|
|
82
|
+
console.log('Page URL when event fired:', targetPage.url())
|
|
86
83
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
await browser.close()
|
|
91
|
-
await page.close()
|
|
92
|
-
await server.close()
|
|
93
|
-
}
|
|
94
|
-
}, 15000)
|
|
84
|
+
expect(targetPage.url()).not.toBe('')
|
|
85
|
+
expect(targetPage.url()).not.toBe(':')
|
|
86
|
+
expect(targetPage.url()).toContain(server.baseUrl)
|
|
95
87
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
88
|
+
const result = await targetPage.evaluate(() => window.location.href)
|
|
89
|
+
expect(result).toContain(server.baseUrl)
|
|
90
|
+
} finally {
|
|
91
|
+
await browser.close()
|
|
92
|
+
await page.close()
|
|
93
|
+
await server.close()
|
|
94
|
+
}
|
|
95
|
+
}, 15000)
|
|
99
96
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
},
|
|
104
|
-
})
|
|
105
|
-
const childUrl = `${childServer.baseUrl}/child.html`
|
|
97
|
+
it('should expose iframe frames when connecting to an existing page over CDP', async () => {
|
|
98
|
+
const browserContext = getBrowserContext()
|
|
99
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
106
100
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
101
|
+
const childServer = await createSimpleServer({
|
|
102
|
+
routes: {
|
|
103
|
+
'/child.html': '<!doctype html><html><body>child</body></html>',
|
|
104
|
+
},
|
|
105
|
+
})
|
|
106
|
+
const childUrl = `${childServer.baseUrl}/child.html`
|
|
112
107
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
108
|
+
const parentServer = await createSimpleServer({
|
|
109
|
+
routes: {
|
|
110
|
+
'/': `<!doctype html><html><body><iframe src="${childUrl}"></iframe></body></html>`,
|
|
111
|
+
},
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
const page = await browserContext.newPage()
|
|
115
|
+
try {
|
|
116
|
+
await withTimeout({
|
|
117
|
+
promise: page.goto(parentServer.baseUrl, { waitUntil: 'domcontentloaded', timeout: 5000 }),
|
|
118
|
+
timeoutMs: 6000,
|
|
119
|
+
errorMessage: 'Timed out loading parent page for iframe test',
|
|
120
|
+
})
|
|
121
|
+
await withTimeout({
|
|
122
|
+
promise: page.frameLocator('iframe').locator('body').waitFor({ timeout: 5000 }),
|
|
123
|
+
timeoutMs: 6000,
|
|
124
|
+
errorMessage: 'Timed out waiting for iframe to attach in parent page',
|
|
125
|
+
})
|
|
126
|
+
expect(page.frames().map((frame) => frame.url())).toContain(childUrl)
|
|
127
|
+
await page.bringToFront()
|
|
128
|
+
|
|
129
|
+
await withTimeout({
|
|
130
|
+
promise: serviceWorker.evaluate(async () => {
|
|
131
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
132
|
+
}),
|
|
133
|
+
timeoutMs: 5000,
|
|
134
|
+
errorMessage: 'Timed out toggling extension for iframe test',
|
|
135
|
+
})
|
|
136
|
+
await new Promise((r) => {
|
|
137
|
+
setTimeout(r, 400)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
const browser = await withTimeout({
|
|
141
|
+
promise: chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT })),
|
|
142
|
+
timeoutMs: 5000,
|
|
143
|
+
errorMessage: 'Timed out connecting over CDP for iframe test',
|
|
144
|
+
})
|
|
145
|
+
const context = browser.contexts()[0]
|
|
146
|
+
const cdpPage = context.pages().find((candidate) => {
|
|
147
|
+
return candidate.url().startsWith(parentServer.baseUrl)
|
|
148
|
+
})
|
|
149
|
+
expect(cdpPage).toBeDefined()
|
|
150
|
+
|
|
151
|
+
const frames = cdpPage!.frames()
|
|
152
|
+
const childFrame = frames.find((frame) => {
|
|
153
|
+
return frame.url() === childUrl
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
expect(frames.length).toBe(2)
|
|
157
|
+
expect(childFrame).toBeDefined()
|
|
158
|
+
|
|
159
|
+
await withTimeout({
|
|
160
|
+
promise: browser.close(),
|
|
161
|
+
timeoutMs: 5000,
|
|
162
|
+
errorMessage: 'Timed out closing CDP browser for iframe test',
|
|
163
|
+
})
|
|
164
|
+
} finally {
|
|
165
|
+
await withTimeout({
|
|
166
|
+
promise: page.close(),
|
|
167
|
+
timeoutMs: 5000,
|
|
168
|
+
errorMessage: 'Timed out closing page for iframe test',
|
|
169
|
+
})
|
|
170
|
+
await Promise.all([parentServer.close(), childServer.close()])
|
|
171
|
+
}
|
|
172
|
+
}, 60000)
|
|
173
173
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
174
|
+
it('should resolve locators for cross-origin iframe that starts with empty src', async () => {
|
|
175
|
+
const browserContext = getBrowserContext()
|
|
176
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
177
177
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
178
|
+
const childServer = await createSimpleServer({
|
|
179
|
+
routes: {
|
|
180
|
+
'/login.html': '<!doctype html><html><body><button id="login-btn">Login</button></body></html>',
|
|
181
|
+
'/canvas.html': '<!doctype html><html><body><button id="canvas-btn">Canvas</button></body></html>',
|
|
182
|
+
},
|
|
183
|
+
})
|
|
184
|
+
const loginUrl = `${childServer.baseUrl}/login.html`
|
|
185
|
+
const canvasUrl = `${childServer.baseUrl}/canvas.html`
|
|
186
|
+
|
|
187
|
+
const parentServer = await createSimpleServer({
|
|
188
|
+
routes: {
|
|
189
|
+
// Reproduces Framer-like plugin iframes: attached with empty src first,
|
|
190
|
+
// then navigated cross-origin after auto-attach is active.
|
|
191
|
+
'/': `<!doctype html>
|
|
192
192
|
<html>
|
|
193
193
|
<body>
|
|
194
194
|
<iframe id="plugin-frame"></iframe>
|
|
@@ -203,194 +203,200 @@ describe('Relay Navigation Tests', () => {
|
|
|
203
203
|
</script>
|
|
204
204
|
</body>
|
|
205
205
|
</html>`,
|
|
206
|
-
|
|
206
|
+
},
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
const page = await browserContext.newPage()
|
|
210
|
+
try {
|
|
211
|
+
await withTimeout({
|
|
212
|
+
promise: page.goto(parentServer.baseUrl, { waitUntil: 'domcontentloaded', timeout: 5000 }),
|
|
213
|
+
timeoutMs: 6000,
|
|
214
|
+
errorMessage: 'Timed out loading parent page for empty-src iframe test',
|
|
215
|
+
})
|
|
216
|
+
await page.bringToFront()
|
|
217
|
+
|
|
218
|
+
await withTimeout({
|
|
219
|
+
promise: serviceWorker.evaluate(async () => {
|
|
220
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
221
|
+
}),
|
|
222
|
+
timeoutMs: 5000,
|
|
223
|
+
errorMessage: 'Timed out toggling extension for empty-src iframe test',
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
const browser = await withTimeout({
|
|
227
|
+
promise: chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT })),
|
|
228
|
+
timeoutMs: 5000,
|
|
229
|
+
errorMessage: 'Timed out connecting over CDP for empty-src iframe test',
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
const context = browser.contexts()[0]
|
|
234
|
+
const cdpPage = context.pages().find((candidate) => {
|
|
235
|
+
return candidate.url().startsWith(parentServer.baseUrl)
|
|
207
236
|
})
|
|
237
|
+
expect(cdpPage).toBeDefined()
|
|
208
238
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
await page.bringToFront()
|
|
217
|
-
|
|
218
|
-
await withTimeout({
|
|
219
|
-
promise: serviceWorker.evaluate(async () => {
|
|
220
|
-
await globalThis.toggleExtensionForActiveTab()
|
|
221
|
-
}),
|
|
222
|
-
timeoutMs: 5000,
|
|
223
|
-
errorMessage: 'Timed out toggling extension for empty-src iframe test',
|
|
224
|
-
})
|
|
225
|
-
|
|
226
|
-
const browser = await withTimeout({
|
|
227
|
-
promise: chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT })),
|
|
228
|
-
timeoutMs: 5000,
|
|
229
|
-
errorMessage: 'Timed out connecting over CDP for empty-src iframe test',
|
|
230
|
-
})
|
|
231
|
-
|
|
232
|
-
try {
|
|
233
|
-
const context = browser.contexts()[0]
|
|
234
|
-
const cdpPage = context.pages().find((candidate) => {
|
|
235
|
-
return candidate.url().startsWith(parentServer.baseUrl)
|
|
236
|
-
})
|
|
237
|
-
expect(cdpPage).toBeDefined()
|
|
238
|
-
|
|
239
|
-
await withTimeout({
|
|
240
|
-
promise: page.evaluate(() => {
|
|
241
|
-
;(window as Window & { startPluginFlow?: () => void }).startPluginFlow?.()
|
|
242
|
-
}),
|
|
243
|
-
timeoutMs: 3000,
|
|
244
|
-
errorMessage: 'Timed out starting plugin iframe flow',
|
|
245
|
-
})
|
|
246
|
-
|
|
247
|
-
const pluginFrame = await withTimeout({
|
|
248
|
-
promise: (async () => {
|
|
249
|
-
for (let attempt = 0; attempt < 40; attempt += 1) {
|
|
250
|
-
const frame = cdpPage!.frames().find((candidate) => {
|
|
251
|
-
return candidate.url() === loginUrl || candidate.url() === canvasUrl
|
|
252
|
-
})
|
|
253
|
-
if (frame) {
|
|
254
|
-
return frame
|
|
255
|
-
}
|
|
256
|
-
await cdpPage!.waitForTimeout(100)
|
|
257
|
-
}
|
|
258
|
-
throw new Error('Plugin frame did not appear with expected URL')
|
|
259
|
-
})(),
|
|
260
|
-
timeoutMs: 5000,
|
|
261
|
-
errorMessage: 'Timed out waiting for plugin frame URL in empty-src iframe test',
|
|
262
|
-
})
|
|
263
|
-
|
|
264
|
-
await withTimeout({
|
|
265
|
-
promise: pluginFrame.locator('button').first().waitFor({ state: 'attached' }),
|
|
266
|
-
timeoutMs: 5000,
|
|
267
|
-
errorMessage: 'Timed out waiting for button locator in empty-src iframe test',
|
|
268
|
-
})
|
|
269
|
-
|
|
270
|
-
const buttonCount = await pluginFrame.locator('button').count()
|
|
271
|
-
expect(buttonCount).toBe(1)
|
|
272
|
-
} finally {
|
|
273
|
-
await withTimeout({
|
|
274
|
-
promise: browser.close(),
|
|
275
|
-
timeoutMs: 5000,
|
|
276
|
-
errorMessage: 'Timed out closing CDP browser for empty-src iframe test',
|
|
277
|
-
})
|
|
278
|
-
}
|
|
279
|
-
} finally {
|
|
280
|
-
await withTimeout({
|
|
281
|
-
promise: page.close(),
|
|
282
|
-
timeoutMs: 5000,
|
|
283
|
-
errorMessage: 'Timed out closing page for empty-src iframe test',
|
|
284
|
-
})
|
|
285
|
-
await Promise.all([
|
|
286
|
-
parentServer.close(),
|
|
287
|
-
childServer.close(),
|
|
288
|
-
])
|
|
289
|
-
}
|
|
290
|
-
}, 60000)
|
|
239
|
+
await withTimeout({
|
|
240
|
+
promise: page.evaluate(() => {
|
|
241
|
+
;(window as Window & { startPluginFlow?: () => void }).startPluginFlow?.()
|
|
242
|
+
}),
|
|
243
|
+
timeoutMs: 3000,
|
|
244
|
+
errorMessage: 'Timed out starting plugin iframe flow',
|
|
245
|
+
})
|
|
291
246
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
247
|
+
const pluginFrame = await withTimeout({
|
|
248
|
+
promise: (async () => {
|
|
249
|
+
for (let attempt = 0; attempt < 40; attempt += 1) {
|
|
250
|
+
const frame = cdpPage!.frames().find((candidate) => {
|
|
251
|
+
return candidate.url() === loginUrl || candidate.url() === canvasUrl
|
|
252
|
+
})
|
|
253
|
+
if (frame) {
|
|
254
|
+
return frame
|
|
255
|
+
}
|
|
256
|
+
await cdpPage!.waitForTimeout(100)
|
|
257
|
+
}
|
|
258
|
+
throw new Error('Plugin frame did not appear with expected URL')
|
|
259
|
+
})(),
|
|
260
|
+
timeoutMs: 5000,
|
|
261
|
+
errorMessage: 'Timed out waiting for plugin frame URL in empty-src iframe test',
|
|
262
|
+
})
|
|
295
263
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
264
|
+
await withTimeout({
|
|
265
|
+
promise: pluginFrame.locator('button').first().waitFor({ state: 'attached' }),
|
|
266
|
+
timeoutMs: 5000,
|
|
267
|
+
errorMessage: 'Timed out waiting for button locator in empty-src iframe test',
|
|
268
|
+
})
|
|
299
269
|
|
|
300
|
-
await
|
|
301
|
-
|
|
270
|
+
const buttonCount = await pluginFrame.locator('button').count()
|
|
271
|
+
expect(buttonCount).toBe(1)
|
|
272
|
+
} finally {
|
|
273
|
+
await withTimeout({
|
|
274
|
+
promise: browser.close(),
|
|
275
|
+
timeoutMs: 5000,
|
|
276
|
+
errorMessage: 'Timed out closing CDP browser for empty-src iframe test',
|
|
302
277
|
})
|
|
278
|
+
}
|
|
279
|
+
} finally {
|
|
280
|
+
await withTimeout({
|
|
281
|
+
promise: page.close(),
|
|
282
|
+
timeoutMs: 5000,
|
|
283
|
+
errorMessage: 'Timed out closing page for empty-src iframe test',
|
|
284
|
+
})
|
|
285
|
+
await Promise.all([parentServer.close(), childServer.close()])
|
|
286
|
+
}
|
|
287
|
+
}, 60000)
|
|
303
288
|
|
|
304
|
-
|
|
305
|
-
|
|
289
|
+
it('should have non-empty URLs when connecting to already-loaded pages', async () => {
|
|
290
|
+
const _browserContext = getBrowserContext()
|
|
291
|
+
const serviceWorker = await getExtensionServiceWorker(_browserContext)
|
|
306
292
|
|
|
307
|
-
|
|
308
|
-
|
|
293
|
+
const page = await _browserContext.newPage()
|
|
294
|
+
await page.goto('https://discord.com/login', { waitUntil: 'load' })
|
|
295
|
+
await page.bringToFront()
|
|
309
296
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
expect(p.url()).not.toBe(':')
|
|
314
|
-
expect(p.url()).not.toBeUndefined()
|
|
315
|
-
}
|
|
297
|
+
await serviceWorker.evaluate(async () => {
|
|
298
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
299
|
+
})
|
|
316
300
|
|
|
317
|
-
|
|
318
|
-
|
|
301
|
+
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
302
|
+
const context = browser.contexts()[0]
|
|
319
303
|
|
|
320
|
-
|
|
321
|
-
|
|
304
|
+
const pages = context.pages()
|
|
305
|
+
console.log(
|
|
306
|
+
'All page URLs:',
|
|
307
|
+
pages.map((p) => p.url()),
|
|
308
|
+
)
|
|
322
309
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
310
|
+
expect(pages.length).toBeGreaterThan(0)
|
|
311
|
+
for (const p of pages) {
|
|
312
|
+
expect(p.url()).not.toBe('')
|
|
313
|
+
expect(p.url()).not.toBe(':')
|
|
314
|
+
expect(p.url()).not.toBeUndefined()
|
|
315
|
+
}
|
|
326
316
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
317
|
+
const discordPage = pages.find((p) => p.url().includes('discord.com'))
|
|
318
|
+
expect(discordPage).toBeDefined()
|
|
330
319
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
await page.goto(initialUrl)
|
|
334
|
-
await page.bringToFront()
|
|
320
|
+
const result = await discordPage!.evaluate(() => window.location.href)
|
|
321
|
+
expect(result).toContain('discord.com')
|
|
335
322
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
323
|
+
await browser.close()
|
|
324
|
+
await page.close()
|
|
325
|
+
}, 60000)
|
|
339
326
|
|
|
340
|
-
|
|
327
|
+
it('should navigate to notion without hanging', async () => {
|
|
328
|
+
const browserContext = getBrowserContext()
|
|
329
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
341
330
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
331
|
+
const page = await browserContext.newPage()
|
|
332
|
+
const initialUrl = 'https://example.com/notion-repro'
|
|
333
|
+
await page.goto(initialUrl)
|
|
334
|
+
await page.bringToFront()
|
|
345
335
|
|
|
346
|
-
|
|
336
|
+
await serviceWorker.evaluate(async () => {
|
|
337
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
338
|
+
})
|
|
347
339
|
|
|
348
|
-
|
|
349
|
-
const responseUrl = response?.url() ?? ''
|
|
350
|
-
expect(responseUrl).toMatch(/notion\.(so|com)/)
|
|
351
|
-
expect(currentUrl).toMatch(/notion\.(so|com)/)
|
|
352
|
-
expect(await cdpPage!.evaluate(() => document.readyState)).not.toBe('loading')
|
|
340
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
353
341
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
342
|
+
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
343
|
+
const cdpPage = browser
|
|
344
|
+
.contexts()[0]
|
|
345
|
+
.pages()
|
|
346
|
+
.find((p) => p.url() === initialUrl)
|
|
347
|
+
expect(cdpPage).toBeDefined()
|
|
357
348
|
|
|
358
|
-
|
|
359
|
-
const browserContext = getBrowserContext()
|
|
360
|
-
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
349
|
+
const response = await cdpPage!.goto('https://www.notion.so', { waitUntil: 'domcontentloaded', timeout: 20000 })
|
|
361
350
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
351
|
+
const currentUrl = cdpPage!.url()
|
|
352
|
+
const responseUrl = response?.url() ?? ''
|
|
353
|
+
expect(responseUrl).toMatch(/notion\.(so|com)/)
|
|
354
|
+
expect(currentUrl).toMatch(/notion\.(so|com)/)
|
|
355
|
+
expect(await cdpPage!.evaluate(() => document.readyState)).not.toBe('loading')
|
|
365
356
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
357
|
+
await browser.close()
|
|
358
|
+
await page.close()
|
|
359
|
+
}, 60000)
|
|
369
360
|
|
|
370
|
-
|
|
361
|
+
it('should navigate to youtube without hanging', async () => {
|
|
362
|
+
const browserContext = getBrowserContext()
|
|
363
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
371
364
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
365
|
+
const page = await browserContext.newPage()
|
|
366
|
+
await page.goto('about:blank')
|
|
367
|
+
await page.bringToFront()
|
|
368
|
+
|
|
369
|
+
await serviceWorker.evaluate(async () => {
|
|
370
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
371
|
+
})
|
|
375
372
|
|
|
376
|
-
|
|
377
|
-
const currentUrl = cdpPage!.url()
|
|
378
|
-
const responseUrl = response?.url() ?? ''
|
|
373
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
379
374
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
375
|
+
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
376
|
+
const cdpPage = browser
|
|
377
|
+
.contexts()[0]
|
|
378
|
+
.pages()
|
|
379
|
+
.find((p) => p.url().includes('about:'))
|
|
380
|
+
expect(cdpPage).toBeDefined()
|
|
383
381
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
382
|
+
const response = await cdpPage!.goto('https://www.youtube.com', { waitUntil: 'domcontentloaded', timeout: 20000 })
|
|
383
|
+
const currentUrl = cdpPage!.url()
|
|
384
|
+
const responseUrl = response?.url() ?? ''
|
|
387
385
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
386
|
+
expect(responseUrl).toContain('youtube')
|
|
387
|
+
expect(currentUrl).toContain('youtube')
|
|
388
|
+
await waitForStableDocumentReadyState({ page: cdpPage!, timeoutMs: 5000 })
|
|
391
389
|
|
|
392
|
-
|
|
393
|
-
|
|
390
|
+
await browser.close()
|
|
391
|
+
await page.close()
|
|
392
|
+
}, 60000)
|
|
393
|
+
|
|
394
|
+
it('should maintain correct page.url() with iframe-heavy pages', async () => {
|
|
395
|
+
const browserContext = getBrowserContext()
|
|
396
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
397
|
+
|
|
398
|
+
const page = await browserContext.newPage()
|
|
399
|
+
await page.setContent(`
|
|
394
400
|
<html>
|
|
395
401
|
<head><title>Iframe Test Page</title></head>
|
|
396
402
|
<body>
|
|
@@ -401,195 +407,222 @@ describe('Relay Navigation Tests', () => {
|
|
|
401
407
|
</body>
|
|
402
408
|
</html>
|
|
403
409
|
`)
|
|
404
|
-
|
|
410
|
+
await page.bringToFront()
|
|
405
411
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
412
|
+
await serviceWorker.evaluate(async () => {
|
|
413
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
414
|
+
})
|
|
409
415
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
416
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
417
|
+
|
|
418
|
+
for (let i = 0; i < 3; i++) {
|
|
419
|
+
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
420
|
+
const pages = browser.contexts()[0].pages()
|
|
421
|
+
let iframePage
|
|
422
|
+
for (const p of pages) {
|
|
423
|
+
const html = await p.content()
|
|
424
|
+
if (html.includes('Iframe Heavy Page')) {
|
|
425
|
+
iframePage = p
|
|
426
|
+
break
|
|
427
|
+
}
|
|
428
|
+
}
|
|
423
429
|
|
|
424
|
-
|
|
425
|
-
|
|
430
|
+
expect(iframePage).toBeDefined()
|
|
431
|
+
expect(iframePage?.url()).toContain('about:')
|
|
426
432
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
433
|
+
await browser.close()
|
|
434
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
435
|
+
}
|
|
430
436
|
|
|
431
|
-
|
|
432
|
-
|
|
437
|
+
await page.close()
|
|
438
|
+
}, 30000)
|
|
433
439
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
440
|
+
it('should work with stagehand', async () => {
|
|
441
|
+
const browserContext = getBrowserContext()
|
|
442
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
437
443
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
444
|
+
await serviceWorker.evaluate(async () => {
|
|
445
|
+
await globalThis.disconnectEverything()
|
|
446
|
+
})
|
|
447
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
442
448
|
|
|
443
|
-
|
|
449
|
+
const targetUrl = 'https://example.com/'
|
|
444
450
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
451
|
+
const enableResult = await serviceWorker.evaluate(async (url) => {
|
|
452
|
+
const tab = await chrome.tabs.create({ url, active: true })
|
|
453
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
454
|
+
return await globalThis.toggleExtensionForActiveTab()
|
|
455
|
+
}, targetUrl)
|
|
450
456
|
|
|
451
|
-
|
|
452
|
-
|
|
457
|
+
console.log('Extension enabled:', enableResult)
|
|
458
|
+
expect(enableResult.isConnected).toBe(true)
|
|
453
459
|
|
|
454
|
-
|
|
460
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
455
461
|
|
|
456
|
-
|
|
462
|
+
const { Stagehand } = await import('@browserbasehq/stagehand')
|
|
457
463
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
464
|
+
const stagehand = new Stagehand({
|
|
465
|
+
env: 'LOCAL',
|
|
466
|
+
verbose: 1,
|
|
467
|
+
disablePino: true,
|
|
468
|
+
localBrowserLaunchOptions: {
|
|
469
|
+
cdpUrl: getCdpUrl({ port: TEST_PORT }),
|
|
470
|
+
},
|
|
471
|
+
})
|
|
466
472
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
473
|
+
console.log('Initializing Stagehand...')
|
|
474
|
+
await stagehand.init()
|
|
475
|
+
console.log('Stagehand initialized')
|
|
470
476
|
|
|
471
|
-
|
|
472
|
-
|
|
477
|
+
const context = stagehand.context
|
|
478
|
+
expect(context).toBeDefined()
|
|
473
479
|
|
|
474
|
-
|
|
475
|
-
|
|
480
|
+
const pages = context.pages()
|
|
481
|
+
console.log(
|
|
482
|
+
'Stagehand pages:',
|
|
483
|
+
pages.length,
|
|
484
|
+
pages.map((p) => p.url()),
|
|
485
|
+
)
|
|
476
486
|
|
|
477
|
-
|
|
478
|
-
|
|
487
|
+
const stagehandPage = pages.find((p) => p.url().includes('example.com'))
|
|
488
|
+
expect(stagehandPage).toBeDefined()
|
|
479
489
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
490
|
+
const url = stagehandPage!.url()
|
|
491
|
+
console.log('Stagehand page URL:', url)
|
|
492
|
+
expect(url).toContain('example.com')
|
|
483
493
|
|
|
484
|
-
|
|
485
|
-
|
|
494
|
+
await stagehand.close()
|
|
495
|
+
}, 60000)
|
|
486
496
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
497
|
+
it('should expose CDP discovery endpoints /json/version and /json/list', async () => {
|
|
498
|
+
const browserContext = getBrowserContext()
|
|
499
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
490
500
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
501
|
+
const page = await browserContext.newPage()
|
|
502
|
+
await page.goto('https://example.com')
|
|
503
|
+
await page.bringToFront()
|
|
494
504
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
505
|
+
await serviceWorker.evaluate(async () => {
|
|
506
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
507
|
+
})
|
|
508
|
+
await new Promise((r) => setTimeout(r, 200))
|
|
509
|
+
|
|
510
|
+
// Test /json/version
|
|
511
|
+
const versionRes = await fetch(`http://127.0.0.1:${TEST_PORT}/json/version`)
|
|
512
|
+
expect(versionRes.status).toBe(200)
|
|
513
|
+
const versionJson = (await versionRes.json()) as { webSocketDebuggerUrl: string }
|
|
514
|
+
expect(versionJson).toMatchObject({
|
|
515
|
+
Browser: expect.stringContaining('Playwriter/'),
|
|
516
|
+
'Protocol-Version': '1.3',
|
|
517
|
+
webSocketDebuggerUrl: expect.stringContaining('ws://'),
|
|
518
|
+
})
|
|
519
|
+
expect(versionJson.webSocketDebuggerUrl).toContain(`127.0.0.1:${TEST_PORT}/cdp`)
|
|
520
|
+
|
|
521
|
+
// Test /json/version/ (trailing slash)
|
|
522
|
+
const versionSlashRes = await fetch(`http://127.0.0.1:${TEST_PORT}/json/version/`)
|
|
523
|
+
expect(versionSlashRes.status).toBe(200)
|
|
524
|
+
|
|
525
|
+
// Test /json/list
|
|
526
|
+
const listRes = await fetch(`http://127.0.0.1:${TEST_PORT}/json/list`)
|
|
527
|
+
expect(listRes.status).toBe(200)
|
|
528
|
+
const listJson = (await listRes.json()) as Array<{ url?: string }>
|
|
529
|
+
expect(Array.isArray(listJson)).toBe(true)
|
|
530
|
+
expect(listJson.length).toBeGreaterThan(0)
|
|
531
|
+
|
|
532
|
+
const examplePage = listJson.find((t) => t.url?.includes('example.com'))
|
|
533
|
+
expect(examplePage).toBeDefined()
|
|
534
|
+
expect(examplePage).toMatchObject({
|
|
535
|
+
id: expect.any(String),
|
|
536
|
+
type: 'page',
|
|
537
|
+
url: expect.stringContaining('example.com'),
|
|
538
|
+
webSocketDebuggerUrl: expect.stringContaining('ws://'),
|
|
539
|
+
})
|
|
530
540
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
// Test PUT method (Chrome 66+ prefers PUT)
|
|
538
|
-
const putRes = await fetch(`http://127.0.0.1:${TEST_PORT}/json/version`, { method: 'PUT' })
|
|
539
|
-
expect(putRes.status).toBe(200)
|
|
540
|
-
|
|
541
|
-
await page.close()
|
|
542
|
-
}, 60000)
|
|
543
|
-
|
|
544
|
-
// Skip: chrome.tabCapture.getMediaStreamId() requires activeTab permission
|
|
545
|
-
it.skip('should record screen with navigation using chrome.tabCapture', async () => {
|
|
546
|
-
const browserContext = getBrowserContext()
|
|
547
|
-
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
548
|
-
const path = await import('node:path')
|
|
549
|
-
const fs = await import('node:fs')
|
|
550
|
-
|
|
551
|
-
const recordingPage = await browserContext.newPage()
|
|
552
|
-
await recordingPage.goto('https://news.ycombinator.com/', { waitUntil: 'domcontentloaded' })
|
|
553
|
-
await recordingPage.bringToFront()
|
|
554
|
-
|
|
555
|
-
await serviceWorker.evaluate(async () => {
|
|
556
|
-
await globalThis.toggleExtensionForActiveTab()
|
|
557
|
-
})
|
|
558
|
-
await new Promise(r => setTimeout(r, 200))
|
|
541
|
+
// Test /json (alias for /json/list)
|
|
542
|
+
const jsonRes = await fetch(`http://127.0.0.1:${TEST_PORT}/json`)
|
|
543
|
+
expect(jsonRes.status).toBe(200)
|
|
544
|
+
const jsonData = await jsonRes.json()
|
|
545
|
+
expect(Array.isArray(jsonData)).toBe(true)
|
|
559
546
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
}
|
|
547
|
+
// Test PUT method (Chrome 66+ prefers PUT)
|
|
548
|
+
const putRes = await fetch(`http://127.0.0.1:${TEST_PORT}/json/version`, { method: 'PUT' })
|
|
549
|
+
expect(putRes.status).toBe(200)
|
|
564
550
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
551
|
+
await page.close()
|
|
552
|
+
}, 60000)
|
|
553
|
+
|
|
554
|
+
// Skip: chrome.tabCapture.getMediaStreamId() requires activeTab permission
|
|
555
|
+
it.skip('should record screen with navigation using chrome.tabCapture', async () => {
|
|
556
|
+
const browserContext = getBrowserContext()
|
|
557
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
558
|
+
const path = await import('node:path')
|
|
559
|
+
const fs = await import('node:fs')
|
|
560
|
+
|
|
561
|
+
const recordingPage = await browserContext.newPage()
|
|
562
|
+
await recordingPage.goto('https://news.ycombinator.com/', { waitUntil: 'domcontentloaded' })
|
|
563
|
+
await recordingPage.bringToFront()
|
|
564
|
+
|
|
565
|
+
await serviceWorker.evaluate(async () => {
|
|
566
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
567
|
+
})
|
|
568
|
+
await new Promise((r) => setTimeout(r, 200))
|
|
569
|
+
|
|
570
|
+
const outputPath = path.join(process.cwd(), 'tmp', 'test-recording.mp4')
|
|
571
|
+
if (!fs.existsSync(path.dirname(outputPath))) {
|
|
572
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true })
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const { startRecording, stopRecording, isRecording } = await import('./screen-recording.js')
|
|
576
|
+
|
|
577
|
+
const startResult = await startRecording({
|
|
578
|
+
page: recordingPage,
|
|
579
|
+
outputPath,
|
|
580
|
+
frameRate: 30,
|
|
581
|
+
audio: false,
|
|
582
|
+
videoBitsPerSecond: 1500000,
|
|
583
|
+
relayPort: TEST_PORT,
|
|
584
|
+
})
|
|
585
|
+
expect(startResult.isRecording).toBe(true)
|
|
586
|
+
|
|
587
|
+
await recordingPage.locator('.titleline a').first().click()
|
|
588
|
+
await recordingPage.waitForLoadState('domcontentloaded')
|
|
589
|
+
await new Promise((r) => setTimeout(r, 500))
|
|
590
|
+
|
|
591
|
+
await recordingPage.goBack()
|
|
592
|
+
await recordingPage.waitForLoadState('domcontentloaded')
|
|
593
|
+
|
|
594
|
+
const status = await isRecording({ page: recordingPage, relayPort: TEST_PORT })
|
|
595
|
+
expect(status.isRecording).toBe(true)
|
|
596
|
+
|
|
597
|
+
const stopResult = await stopRecording({ page: recordingPage, relayPort: TEST_PORT })
|
|
598
|
+
expect(stopResult.path).toBe(outputPath)
|
|
599
|
+
expect(stopResult.size).toBeGreaterThan(10000)
|
|
600
|
+
expect(fs.existsSync(outputPath)).toBe(true)
|
|
601
|
+
|
|
602
|
+
// Create a sped-up demo video from the recording.
|
|
603
|
+
// We fake executionTimestamps since this test calls screen-recording
|
|
604
|
+
// directly (not via executor sandbox which tracks them automatically).
|
|
605
|
+
const { createDemoVideo } = await import('./ffmpeg.js')
|
|
606
|
+
const demoPath = await createDemoVideo({
|
|
607
|
+
recordingPath: outputPath,
|
|
608
|
+
durationMs: stopResult.duration,
|
|
609
|
+
executionTimestamps: [
|
|
610
|
+
// Simulate two interactions with an idle gap between them
|
|
611
|
+
{ start: 0.5, end: 1.5 },
|
|
612
|
+
{ start: 3, end: 4 },
|
|
613
|
+
],
|
|
614
|
+
speed: 4,
|
|
615
|
+
})
|
|
616
|
+
expect(fs.existsSync(demoPath)).toBe(true)
|
|
617
|
+
expect(demoPath).toContain('-demo')
|
|
618
|
+
|
|
619
|
+
// Verify the demo video is smaller (idle sections were sped up)
|
|
620
|
+
const demoSize = fs.statSync(demoPath).size
|
|
621
|
+
expect(demoSize).toBeGreaterThan(0)
|
|
622
|
+
console.log(`Recording: ${stopResult.size} bytes, Demo: ${demoSize} bytes`)
|
|
623
|
+
|
|
624
|
+
await recordingPage.close()
|
|
625
|
+
fs.unlinkSync(outputPath)
|
|
626
|
+
fs.unlinkSync(demoPath)
|
|
627
|
+
}, 60000)
|
|
595
628
|
})
|