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
|
@@ -17,87 +17,92 @@ import './test-declarations.js'
|
|
|
17
17
|
const TEST_PORT = 19991
|
|
18
18
|
|
|
19
19
|
describe('Snapshot & Screenshot Tests', () => {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
20
|
+
let client: Awaited<ReturnType<typeof createMCPClient>>['client']
|
|
21
|
+
let cleanup: (() => Promise<void>) | null = null
|
|
22
|
+
let testCtx: TestContext | null = null
|
|
23
|
+
|
|
24
|
+
beforeAll(async () => {
|
|
25
|
+
testCtx = await setupTestContext({ port: TEST_PORT, tempDirPrefix: 'pw-snap-test-', toggleExtension: true })
|
|
26
|
+
|
|
27
|
+
const result = await createMCPClient({ port: TEST_PORT })
|
|
28
|
+
client = result.client
|
|
29
|
+
cleanup = result.cleanup
|
|
30
|
+
}, 600000)
|
|
31
|
+
|
|
32
|
+
afterAll(async () => {
|
|
33
|
+
await cleanupTestContext(testCtx, cleanup)
|
|
34
|
+
cleanup = null
|
|
35
|
+
testCtx = null
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
const getBrowserContext = () => {
|
|
39
|
+
if (!testCtx?.browserContext) throw new Error('Browser not initialized')
|
|
40
|
+
return testCtx.browserContext
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
it('should capture screenshot correctly', async () => {
|
|
44
|
+
const browserContext = getBrowserContext()
|
|
45
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
46
|
+
|
|
47
|
+
const page = await browserContext.newPage()
|
|
48
|
+
await page.goto('https://example.com/')
|
|
49
|
+
await page.bringToFront()
|
|
50
|
+
|
|
51
|
+
await serviceWorker.evaluate(async () => {
|
|
52
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
36
53
|
})
|
|
37
54
|
|
|
38
|
-
|
|
39
|
-
if (!testCtx?.browserContext) throw new Error('Browser not initialized')
|
|
40
|
-
return testCtx.browserContext
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
it('should capture screenshot correctly', async () => {
|
|
44
|
-
const browserContext = getBrowserContext()
|
|
45
|
-
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
46
|
-
|
|
47
|
-
const page = await browserContext.newPage()
|
|
48
|
-
await page.goto('https://example.com/')
|
|
49
|
-
await page.bringToFront()
|
|
50
|
-
|
|
51
|
-
await serviceWorker.evaluate(async () => {
|
|
52
|
-
await globalThis.toggleExtensionForActiveTab()
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
await new Promise(r => setTimeout(r, 100))
|
|
55
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
57
|
+
const capturedCommands: CDPCommand[] = []
|
|
58
|
+
const commandHandler = ({ command }: { clientId: string; command: CDPCommand }) => {
|
|
59
|
+
if (command.method === 'Page.captureScreenshot') {
|
|
60
|
+
capturedCommands.push(command)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
testCtx!.relayServer.on('cdp:command', commandHandler)
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
66
|
+
const cdpPage = browser
|
|
67
|
+
.contexts()[0]
|
|
68
|
+
.pages()
|
|
69
|
+
.find((p) => p.url().includes('example.com'))
|
|
67
70
|
|
|
68
|
-
|
|
71
|
+
expect(cdpPage).toBeDefined()
|
|
69
72
|
|
|
70
|
-
|
|
71
|
-
|
|
73
|
+
const viewportSize = cdpPage!.viewportSize()
|
|
74
|
+
console.log('Viewport size:', viewportSize)
|
|
72
75
|
|
|
73
|
-
|
|
74
|
-
|
|
76
|
+
const viewportScreenshot = await cdpPage!.screenshot()
|
|
77
|
+
expect(viewportScreenshot).toBeDefined()
|
|
75
78
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
79
|
+
const viewportDimensions = imageSize(viewportScreenshot)
|
|
80
|
+
console.log('Viewport screenshot dimensions:', viewportDimensions)
|
|
81
|
+
expect(viewportDimensions.width).toBeGreaterThan(0)
|
|
82
|
+
expect(viewportDimensions.height).toBeGreaterThan(0)
|
|
83
|
+
if (viewportSize) {
|
|
84
|
+
expect(viewportDimensions.width).toBe(viewportSize.width)
|
|
85
|
+
expect(viewportDimensions.height).toBe(viewportSize.height)
|
|
86
|
+
}
|
|
84
87
|
|
|
85
|
-
|
|
86
|
-
|
|
88
|
+
const fullPageScreenshot = await cdpPage!.screenshot({ fullPage: true })
|
|
89
|
+
expect(fullPageScreenshot).toBeDefined()
|
|
87
90
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
const fullPageDimensions = imageSize(fullPageScreenshot)
|
|
92
|
+
console.log('Full page screenshot dimensions:', fullPageDimensions)
|
|
93
|
+
expect(fullPageDimensions.width).toBeGreaterThan(0)
|
|
94
|
+
expect(fullPageDimensions.height).toBeGreaterThan(0)
|
|
95
|
+
expect(fullPageDimensions.width).toBeGreaterThanOrEqual(viewportDimensions.width!)
|
|
93
96
|
|
|
94
|
-
|
|
97
|
+
testCtx!.relayServer.off('cdp:command', commandHandler)
|
|
95
98
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
99
|
+
expect(capturedCommands.length).toBe(2)
|
|
100
|
+
expect(
|
|
101
|
+
capturedCommands.map((c) => ({
|
|
102
|
+
method: c.method,
|
|
103
|
+
params: c.params,
|
|
104
|
+
})),
|
|
105
|
+
).toMatchInlineSnapshot(`
|
|
101
106
|
[
|
|
102
107
|
{
|
|
103
108
|
"method": "Page.captureScreenshot",
|
|
@@ -130,23 +135,65 @@ describe('Snapshot & Screenshot Tests', () => {
|
|
|
130
135
|
]
|
|
131
136
|
`)
|
|
132
137
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
138
|
+
const screenshotPath = path.join(os.tmpdir(), 'playwriter-test-screenshot.png')
|
|
139
|
+
fs.writeFileSync(screenshotPath, viewportScreenshot)
|
|
140
|
+
console.log('Screenshot saved to:', screenshotPath)
|
|
136
141
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
142
|
+
await browser.close()
|
|
143
|
+
await page.close()
|
|
144
|
+
}, 60000)
|
|
140
145
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
146
|
+
it('should match window.innerWidth/Height without clip param', async () => {
|
|
147
|
+
// Proves that in connectOverCDP mode, Playwright already queries
|
|
148
|
+
// window.innerWidth/innerHeight for viewport screenshots, so passing
|
|
149
|
+
// a manual clip is redundant.
|
|
150
|
+
const browserContext = getBrowserContext()
|
|
151
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
144
152
|
|
|
145
|
-
|
|
146
|
-
|
|
153
|
+
const page = await browserContext.newPage()
|
|
154
|
+
await page.goto('https://example.com/')
|
|
155
|
+
await page.bringToFront()
|
|
147
156
|
|
|
148
|
-
|
|
149
|
-
|
|
157
|
+
await serviceWorker.evaluate(async () => {
|
|
158
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
159
|
+
})
|
|
160
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
161
|
+
|
|
162
|
+
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
163
|
+
const cdpPage = browser
|
|
164
|
+
.contexts()[0]
|
|
165
|
+
.pages()
|
|
166
|
+
.find((p) => p.url().includes('example.com'))
|
|
167
|
+
expect(cdpPage).toBeDefined()
|
|
168
|
+
|
|
169
|
+
// Get actual browser viewport via JS
|
|
170
|
+
const actualViewport = await cdpPage!.evaluate(() => ({
|
|
171
|
+
width: window.innerWidth,
|
|
172
|
+
height: window.innerHeight,
|
|
173
|
+
}))
|
|
174
|
+
console.log('Actual viewport (window.inner*):', actualViewport)
|
|
175
|
+
|
|
176
|
+
// Plain screenshot with scale:'css', NO clip
|
|
177
|
+
const screenshot = await cdpPage!.screenshot({ scale: 'css' })
|
|
178
|
+
const dimensions = imageSize(screenshot)
|
|
179
|
+
console.log('Screenshot dimensions (no clip):', dimensions)
|
|
180
|
+
|
|
181
|
+
expect(dimensions.width).toBe(actualViewport.width)
|
|
182
|
+
expect(dimensions.height).toBe(actualViewport.height)
|
|
183
|
+
|
|
184
|
+
await browser.close()
|
|
185
|
+
await page.close()
|
|
186
|
+
}, 60000)
|
|
187
|
+
|
|
188
|
+
it('should capture element screenshot with correct coordinates', async () => {
|
|
189
|
+
const browserContext = getBrowserContext()
|
|
190
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
191
|
+
|
|
192
|
+
const target = { x: 200, y: 150, width: 300, height: 100 }
|
|
193
|
+
const scrolledTarget = { x: 100, y: 1500, width: 200, height: 80 }
|
|
194
|
+
|
|
195
|
+
const page = await browserContext.newPage()
|
|
196
|
+
await page.setContent(`
|
|
150
197
|
<html>
|
|
151
198
|
<head>
|
|
152
199
|
<style>
|
|
@@ -175,67 +222,67 @@ describe('Snapshot & Screenshot Tests', () => {
|
|
|
175
222
|
</body>
|
|
176
223
|
</html>
|
|
177
224
|
`)
|
|
178
|
-
|
|
225
|
+
await page.bringToFront()
|
|
179
226
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
227
|
+
await serviceWorker.evaluate(async () => {
|
|
228
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
229
|
+
})
|
|
183
230
|
|
|
184
|
-
|
|
231
|
+
await new Promise((r) => setTimeout(r, 400))
|
|
185
232
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
233
|
+
const capturedCommands: CDPCommand[] = []
|
|
234
|
+
const commandHandler = ({ command }: { clientId: string; command: CDPCommand }) => {
|
|
235
|
+
if (command.method === 'Page.captureScreenshot') {
|
|
236
|
+
capturedCommands.push(command)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
testCtx!.relayServer.on('cdp:command', commandHandler)
|
|
240
|
+
|
|
241
|
+
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
242
|
+
let cdpPage
|
|
243
|
+
for (const p of browser.contexts()[0].pages()) {
|
|
244
|
+
const html = await p.content()
|
|
245
|
+
if (html.includes('scrolled-target')) {
|
|
246
|
+
cdpPage = p
|
|
247
|
+
break
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
expect(cdpPage).toBeDefined()
|
|
204
251
|
|
|
205
|
-
|
|
252
|
+
await cdpPage!.locator('#target').screenshot()
|
|
206
253
|
|
|
207
|
-
|
|
254
|
+
await cdpPage!.locator('#scrolled-target').screenshot()
|
|
208
255
|
|
|
209
|
-
|
|
256
|
+
testCtx!.relayServer.off('cdp:command', commandHandler)
|
|
210
257
|
|
|
211
|
-
|
|
258
|
+
expect(capturedCommands.length).toBe(2)
|
|
212
259
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
260
|
+
const targetCmd = capturedCommands[0]
|
|
261
|
+
expect(targetCmd.method).toBe('Page.captureScreenshot')
|
|
262
|
+
const targetClip = (targetCmd.params as any).clip
|
|
263
|
+
expect(targetClip.x).toBe(target.x)
|
|
264
|
+
expect(targetClip.y).toBe(target.y)
|
|
265
|
+
expect(targetClip.width).toBe(target.width)
|
|
266
|
+
expect(targetClip.height).toBe(target.height)
|
|
220
267
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
268
|
+
const scrolledCmd = capturedCommands[1]
|
|
269
|
+
expect(scrolledCmd.method).toBe('Page.captureScreenshot')
|
|
270
|
+
const scrolledClip = (scrolledCmd.params as any).clip
|
|
271
|
+
expect(scrolledClip.x).toBe(scrolledTarget.x)
|
|
272
|
+
expect(scrolledClip.y).toBe(scrolledTarget.y)
|
|
273
|
+
expect(scrolledClip.width).toBe(scrolledTarget.width)
|
|
274
|
+
expect(scrolledClip.height).toBe(scrolledTarget.height)
|
|
228
275
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
276
|
+
await browser.close()
|
|
277
|
+
await page.close()
|
|
278
|
+
}, 60000)
|
|
232
279
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
280
|
+
it('should get locator string for element using getLocatorStringForElement', async () => {
|
|
281
|
+
const browserContext = getBrowserContext()
|
|
282
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
236
283
|
|
|
237
|
-
|
|
238
|
-
|
|
284
|
+
const page = await browserContext.newPage()
|
|
285
|
+
await page.setContent(`
|
|
239
286
|
<html>
|
|
240
287
|
<body>
|
|
241
288
|
<button id="test-btn">Click Me</button>
|
|
@@ -243,18 +290,18 @@ describe('Snapshot & Screenshot Tests', () => {
|
|
|
243
290
|
</body>
|
|
244
291
|
</html>
|
|
245
292
|
`)
|
|
246
|
-
|
|
293
|
+
await page.bringToFront()
|
|
247
294
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
295
|
+
await serviceWorker.evaluate(async () => {
|
|
296
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
297
|
+
})
|
|
251
298
|
|
|
252
|
-
|
|
299
|
+
await new Promise((r) => setTimeout(r, 400))
|
|
253
300
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
301
|
+
const result = await client.callTool({
|
|
302
|
+
name: 'execute',
|
|
303
|
+
arguments: {
|
|
304
|
+
code: js`
|
|
258
305
|
let testPage;
|
|
259
306
|
for (const p of context.pages()) {
|
|
260
307
|
const html = await p.content();
|
|
@@ -270,27 +317,27 @@ describe('Snapshot & Screenshot Tests', () => {
|
|
|
270
317
|
const text = await locatorFromString.textContent();
|
|
271
318
|
console.log('Locator text:', text);
|
|
272
319
|
`,
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
320
|
+
timeout: 30000,
|
|
321
|
+
},
|
|
322
|
+
})
|
|
276
323
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
324
|
+
expect(result.isError).toBeFalsy()
|
|
325
|
+
const text = (result.content as any)[0]?.text || ''
|
|
326
|
+
expect(text).toContain('Locator string:')
|
|
327
|
+
expect(text).toContain("getByRole('button', { name: 'Click Me' })")
|
|
328
|
+
expect(text).toContain('Locator count:')
|
|
329
|
+
expect(text).toContain('Locator text:')
|
|
330
|
+
expect(text).toContain('Click Me')
|
|
284
331
|
|
|
285
|
-
|
|
286
|
-
|
|
332
|
+
await page.close()
|
|
333
|
+
}, 60000)
|
|
287
334
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
335
|
+
it('should get styles for element using getStylesForLocator', async () => {
|
|
336
|
+
const browserContext = getBrowserContext()
|
|
337
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
291
338
|
|
|
292
|
-
|
|
293
|
-
|
|
339
|
+
const page = await browserContext.newPage()
|
|
340
|
+
await page.setContent(`
|
|
294
341
|
<html>
|
|
295
342
|
<head>
|
|
296
343
|
<style>
|
|
@@ -307,18 +354,18 @@ describe('Snapshot & Screenshot Tests', () => {
|
|
|
307
354
|
</body>
|
|
308
355
|
</html>
|
|
309
356
|
`)
|
|
310
|
-
|
|
357
|
+
await page.bringToFront()
|
|
311
358
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
359
|
+
await serviceWorker.evaluate(async () => {
|
|
360
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
361
|
+
})
|
|
315
362
|
|
|
316
|
-
|
|
363
|
+
await new Promise((r) => setTimeout(r, 400))
|
|
317
364
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
365
|
+
const stylesResult = await client.callTool({
|
|
366
|
+
name: 'execute',
|
|
367
|
+
arguments: {
|
|
368
|
+
code: js`
|
|
322
369
|
let testPage;
|
|
323
370
|
for (const p of context.pages()) {
|
|
324
371
|
const html = await p.content();
|
|
@@ -329,13 +376,13 @@ describe('Snapshot & Screenshot Tests', () => {
|
|
|
329
376
|
const styles = await getStylesForLocator({ locator: btn });
|
|
330
377
|
return styles;
|
|
331
378
|
`,
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
379
|
+
timeout: 30000,
|
|
380
|
+
},
|
|
381
|
+
})
|
|
335
382
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
383
|
+
expect(stylesResult.isError).toBeFalsy()
|
|
384
|
+
const stylesText = (stylesResult.content as any)[0]?.text || ''
|
|
385
|
+
expect(stylesText).toMatchInlineSnapshot(`
|
|
339
386
|
"[return value] {
|
|
340
387
|
element: 'button#main-btn.btn',
|
|
341
388
|
inlineStyle: { 'font-weight': 'bold' },
|
|
@@ -397,10 +444,10 @@ describe('Snapshot & Screenshot Tests', () => {
|
|
|
397
444
|
}"
|
|
398
445
|
`)
|
|
399
446
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
447
|
+
const formattedResult = await client.callTool({
|
|
448
|
+
name: 'execute',
|
|
449
|
+
arguments: {
|
|
450
|
+
code: js`
|
|
404
451
|
let testPage;
|
|
405
452
|
for (const p of context.pages()) {
|
|
406
453
|
const html = await p.content();
|
|
@@ -411,13 +458,13 @@ describe('Snapshot & Screenshot Tests', () => {
|
|
|
411
458
|
const styles = await getStylesForLocator({ locator: btn });
|
|
412
459
|
return formatStylesAsText(styles);
|
|
413
460
|
`,
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
461
|
+
timeout: 30000,
|
|
462
|
+
},
|
|
463
|
+
})
|
|
417
464
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
465
|
+
expect(formattedResult.isError).toBeFalsy()
|
|
466
|
+
const formattedText = (formattedResult.content as any)[0]?.text || ''
|
|
467
|
+
expect(formattedText).toMatchInlineSnapshot(`
|
|
421
468
|
"[return value] Element: button#main-btn.btn
|
|
422
469
|
|
|
423
470
|
Inline styles:
|
|
@@ -462,42 +509,46 @@ describe('Snapshot & Screenshot Tests', () => {
|
|
|
462
509
|
}"
|
|
463
510
|
`)
|
|
464
511
|
|
|
465
|
-
|
|
466
|
-
|
|
512
|
+
await page.close()
|
|
513
|
+
}, 60000)
|
|
467
514
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
515
|
+
it('should return correct layout metrics via CDP', async () => {
|
|
516
|
+
const browserContext = getBrowserContext()
|
|
517
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
471
518
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
519
|
+
const page = await browserContext.newPage()
|
|
520
|
+
await page.goto('https://example.com/')
|
|
521
|
+
await page.bringToFront()
|
|
475
522
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
523
|
+
await serviceWorker.evaluate(async () => {
|
|
524
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
525
|
+
})
|
|
479
526
|
|
|
480
|
-
|
|
527
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
481
528
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
529
|
+
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
530
|
+
const cdpPage = browser
|
|
531
|
+
.contexts()[0]
|
|
532
|
+
.pages()
|
|
533
|
+
.find((p) => p.url().includes('example.com'))
|
|
534
|
+
expect(cdpPage).toBeDefined()
|
|
485
535
|
|
|
486
|
-
|
|
536
|
+
const cdpSession = await getCDPSessionForPage({ page: cdpPage! })
|
|
487
537
|
|
|
488
|
-
|
|
538
|
+
const layoutMetrics = await cdpSession.send('Page.getLayoutMetrics')
|
|
489
539
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
540
|
+
const normalized = {
|
|
541
|
+
cssLayoutViewport: layoutMetrics.cssLayoutViewport,
|
|
542
|
+
cssVisualViewport: layoutMetrics.cssVisualViewport,
|
|
543
|
+
layoutViewport: layoutMetrics.layoutViewport,
|
|
544
|
+
visualViewport: layoutMetrics.visualViewport,
|
|
545
|
+
devicePixelRatio:
|
|
546
|
+
layoutMetrics.cssVisualViewport.clientWidth > 0
|
|
547
|
+
? layoutMetrics.visualViewport.clientWidth / layoutMetrics.cssVisualViewport.clientWidth
|
|
548
|
+
: 1,
|
|
549
|
+
}
|
|
499
550
|
|
|
500
|
-
|
|
551
|
+
expect(normalized).toMatchInlineSnapshot(`
|
|
501
552
|
{
|
|
502
553
|
"cssLayoutViewport": {
|
|
503
554
|
"clientHeight": 720,
|
|
@@ -535,59 +586,62 @@ describe('Snapshot & Screenshot Tests', () => {
|
|
|
535
586
|
}
|
|
536
587
|
`)
|
|
537
588
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
await cdpSession.detach()
|
|
543
|
-
await browser.close()
|
|
544
|
-
await page.close()
|
|
545
|
-
}, 60000)
|
|
546
|
-
|
|
547
|
-
it('should support getExistingCDPSession through the relay (reusing Playwright WS)', async () => {
|
|
548
|
-
const browserContext = getBrowserContext()
|
|
549
|
-
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
550
|
-
|
|
551
|
-
const page = await browserContext.newPage()
|
|
552
|
-
await page.goto('https://example.com/')
|
|
553
|
-
await page.bringToFront()
|
|
554
|
-
|
|
555
|
-
await serviceWorker.evaluate(async () => {
|
|
556
|
-
await globalThis.toggleExtensionForActiveTab()
|
|
557
|
-
})
|
|
558
|
-
|
|
559
|
-
await new Promise(r => setTimeout(r, 100))
|
|
560
|
-
|
|
561
|
-
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
562
|
-
const cdpPage = browser.contexts()[0].pages().find(p => p.url().includes('example.com'))
|
|
563
|
-
expect(cdpPage).toBeDefined()
|
|
564
|
-
|
|
565
|
-
// Use the new getCDPSessionForPage which reuses Playwright's internal WS
|
|
566
|
-
const cdpClient = await getCDPSessionForPage({ page: cdpPage! })
|
|
589
|
+
const windowDpr = await cdpPage!.evaluate(() => (globalThis as any).devicePixelRatio)
|
|
590
|
+
console.log('window.devicePixelRatio:', windowDpr)
|
|
591
|
+
expect(windowDpr).toBe(1)
|
|
567
592
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
expect(metrics.cssVisualViewport).toBeDefined()
|
|
573
|
-
expect(metrics.cssVisualViewport!.clientWidth).toBeGreaterThan(0)
|
|
593
|
+
await cdpSession.detach()
|
|
594
|
+
await browser.close()
|
|
595
|
+
await page.close()
|
|
596
|
+
}, 60000)
|
|
574
597
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
598
|
+
it('should support getExistingCDPSession through the relay (reusing Playwright WS)', async () => {
|
|
599
|
+
const browserContext = getBrowserContext()
|
|
600
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
578
601
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
}, 60000)
|
|
602
|
+
const page = await browserContext.newPage()
|
|
603
|
+
await page.goto('https://example.com/')
|
|
604
|
+
await page.bringToFront()
|
|
583
605
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
606
|
+
await serviceWorker.evaluate(async () => {
|
|
607
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
608
|
+
})
|
|
587
609
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
610
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
611
|
+
|
|
612
|
+
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
613
|
+
const cdpPage = browser
|
|
614
|
+
.contexts()[0]
|
|
615
|
+
.pages()
|
|
616
|
+
.find((p) => p.url().includes('example.com'))
|
|
617
|
+
expect(cdpPage).toBeDefined()
|
|
618
|
+
|
|
619
|
+
// Use the new getCDPSessionForPage which reuses Playwright's internal WS
|
|
620
|
+
const cdpClient = await getCDPSessionForPage({ page: cdpPage! })
|
|
621
|
+
|
|
622
|
+
// Should be able to send CDP commands just like the regular getCDPSessionForPage
|
|
623
|
+
const layoutMetrics = await cdpClient.send('Page.getLayoutMetrics')
|
|
624
|
+
expect(layoutMetrics).toBeDefined()
|
|
625
|
+
const metrics = layoutMetrics as { cssVisualViewport?: { clientWidth?: number } }
|
|
626
|
+
expect(metrics.cssVisualViewport).toBeDefined()
|
|
627
|
+
expect(metrics.cssVisualViewport!.clientWidth).toBeGreaterThan(0)
|
|
628
|
+
|
|
629
|
+
// Test DOM access
|
|
630
|
+
const document = await cdpClient.send('DOM.getDocument')
|
|
631
|
+
expect(document).toBeDefined()
|
|
632
|
+
|
|
633
|
+
await cdpClient.detach()
|
|
634
|
+
await browser.close()
|
|
635
|
+
await page.close()
|
|
636
|
+
}, 60000)
|
|
637
|
+
|
|
638
|
+
it('should get aria ref for locator using getAriaSnapshot', async () => {
|
|
639
|
+
const browserContext = getBrowserContext()
|
|
640
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
641
|
+
|
|
642
|
+
const page = await browserContext.newPage()
|
|
643
|
+
// Use data-testid for stable refs, regular id for the button
|
|
644
|
+
await page.setContent(`
|
|
591
645
|
<html>
|
|
592
646
|
<body>
|
|
593
647
|
<button data-testid="submit-btn">Submit Form</button>
|
|
@@ -596,257 +650,255 @@ describe('Snapshot & Screenshot Tests', () => {
|
|
|
596
650
|
</body>
|
|
597
651
|
</html>
|
|
598
652
|
`)
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
await serviceWorker.evaluate(async () => {
|
|
602
|
-
await globalThis.toggleExtensionForActiveTab()
|
|
603
|
-
})
|
|
604
|
-
await new Promise(r => setTimeout(r, 400))
|
|
605
|
-
|
|
606
|
-
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
607
|
-
let cdpPage
|
|
608
|
-
for (const p of browser.contexts()[0].pages()) {
|
|
609
|
-
const html = await p.content()
|
|
610
|
-
if (html.includes('submit-btn')) {
|
|
611
|
-
cdpPage = p
|
|
612
|
-
break
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
expect(cdpPage).toBeDefined()
|
|
616
|
-
|
|
617
|
-
const { getAriaSnapshot } = await import('./aria-snapshot.js')
|
|
618
|
-
|
|
619
|
-
const ariaResult = await getAriaSnapshot({
|
|
620
|
-
page: cdpPage!,
|
|
621
|
-
})
|
|
622
|
-
|
|
623
|
-
expect(ariaResult.snapshot).toBeDefined()
|
|
624
|
-
expect(ariaResult.snapshot.length).toBeGreaterThan(0)
|
|
625
|
-
expect(ariaResult.snapshot).toContain('Submit Form')
|
|
626
|
-
// Snapshot lines include Playwright locators for interactive elements
|
|
627
|
-
expect(ariaResult.snapshot).toContain('[data-testid="submit-btn"]')
|
|
628
|
-
expect(ariaResult.snapshot).toContain('[data-testid="about-link"]')
|
|
629
|
-
expect(ariaResult.snapshot).toContain('[data-testid="name-input"]')
|
|
630
|
-
|
|
631
|
-
const flattenNodes = (nodes: AriaSnapshotNode[]): AriaSnapshotNode[] => {
|
|
632
|
-
return nodes.flatMap((node) => {
|
|
633
|
-
return [node, ...flattenNodes(node.children)]
|
|
634
|
-
})
|
|
635
|
-
}
|
|
653
|
+
await page.bringToFront()
|
|
636
654
|
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
655
|
+
await serviceWorker.evaluate(async () => {
|
|
656
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
657
|
+
})
|
|
658
|
+
await new Promise((r) => setTimeout(r, 400))
|
|
659
|
+
|
|
660
|
+
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
661
|
+
let cdpPage
|
|
662
|
+
for (const p of browser.contexts()[0].pages()) {
|
|
663
|
+
const html = await p.content()
|
|
664
|
+
if (html.includes('submit-btn')) {
|
|
665
|
+
cdpPage = p
|
|
666
|
+
break
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
expect(cdpPage).toBeDefined()
|
|
641
670
|
|
|
642
|
-
|
|
643
|
-
const aboutNode = findByLocator('[data-testid="about-link"]')
|
|
644
|
-
const nameNode = findByLocator('[data-testid="name-input"]')
|
|
671
|
+
const { getAriaSnapshot } = await import('./aria-snapshot.js')
|
|
645
672
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
673
|
+
const ariaResult = await getAriaSnapshot({
|
|
674
|
+
page: cdpPage!,
|
|
675
|
+
})
|
|
649
676
|
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
677
|
+
expect(ariaResult.snapshot).toBeDefined()
|
|
678
|
+
expect(ariaResult.snapshot.length).toBeGreaterThan(0)
|
|
679
|
+
expect(ariaResult.snapshot).toContain('Submit Form')
|
|
680
|
+
// Snapshot lines include Playwright locators for interactive elements
|
|
681
|
+
expect(ariaResult.snapshot).toContain('[data-testid="submit-btn"]')
|
|
682
|
+
expect(ariaResult.snapshot).toContain('[data-testid="about-link"]')
|
|
683
|
+
expect(ariaResult.snapshot).toContain('[data-testid="name-input"]')
|
|
684
|
+
|
|
685
|
+
const flattenNodes = (nodes: AriaSnapshotNode[]): AriaSnapshotNode[] => {
|
|
686
|
+
return nodes.flatMap((node) => {
|
|
687
|
+
return [node, ...flattenNodes(node.children)]
|
|
688
|
+
})
|
|
689
|
+
}
|
|
653
690
|
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
691
|
+
const allNodes = flattenNodes(ariaResult.tree)
|
|
692
|
+
const findByLocator = (locator: string) => {
|
|
693
|
+
return allNodes.find((node) => node.locator === locator)
|
|
694
|
+
}
|
|
657
695
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
696
|
+
const submitNode = findByLocator('[data-testid="submit-btn"]')
|
|
697
|
+
const aboutNode = findByLocator('[data-testid="about-link"]')
|
|
698
|
+
const nameNode = findByLocator('[data-testid="name-input"]')
|
|
661
699
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
700
|
+
expect(submitNode).toBeDefined()
|
|
701
|
+
expect(aboutNode).toBeDefined()
|
|
702
|
+
expect(nameNode).toBeDefined()
|
|
665
703
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
expect(ariaResult.refToElement.has('name-input')).toBe(true)
|
|
704
|
+
const submitLocator = cdpPage!.locator(submitNode!.locator!)
|
|
705
|
+
const aboutLocator = cdpPage!.locator(aboutNode!.locator!)
|
|
706
|
+
const nameLocator = cdpPage!.locator(nameNode!.locator!)
|
|
670
707
|
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
console.log('Button selector:', btnSelector)
|
|
708
|
+
expect(await submitLocator.count()).toBe(1)
|
|
709
|
+
expect(await aboutLocator.count()).toBe(1)
|
|
710
|
+
expect(await nameLocator.count()).toBe(1)
|
|
675
711
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
console.log('Button text via selector:', btnTextViaRef)
|
|
680
|
-
expect(btnTextViaRef).toBe('Submit Form')
|
|
712
|
+
expect(await submitLocator.textContent()).toBe('Submit Form')
|
|
713
|
+
expect(await aboutLocator.textContent()).toBe('About Us')
|
|
714
|
+
expect(await nameLocator.getAttribute('placeholder')).toBe('Enter your name')
|
|
681
715
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
expect(btnInfo?.name).toBe('Submit Form')
|
|
716
|
+
expect(ariaResult.refToElement.size).toBeGreaterThan(0)
|
|
717
|
+
console.log('RefToElement map size:', ariaResult.refToElement.size)
|
|
718
|
+
console.log('RefToElement entries:', [...ariaResult.refToElement.entries()])
|
|
686
719
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
720
|
+
// Verify refs are stable test IDs
|
|
721
|
+
expect(ariaResult.refToElement.has('submit-btn')).toBe(true)
|
|
722
|
+
expect(ariaResult.refToElement.has('about-link')).toBe(true)
|
|
723
|
+
expect(ariaResult.refToElement.has('name-input')).toBe(true)
|
|
690
724
|
|
|
691
|
-
|
|
692
|
-
|
|
725
|
+
// Use getSelectorForRef to get CSS selector for a ref
|
|
726
|
+
const btnSelector = ariaResult.getSelectorForRef('submit-btn')
|
|
727
|
+
expect(btnSelector).toBeDefined()
|
|
728
|
+
console.log('Button selector:', btnSelector)
|
|
693
729
|
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
730
|
+
// Verify the selector works
|
|
731
|
+
const btnViaSelector = cdpPage!.locator(btnSelector!)
|
|
732
|
+
const btnTextViaRef = await btnViaSelector.textContent()
|
|
733
|
+
console.log('Button text via selector:', btnTextViaRef)
|
|
734
|
+
expect(btnTextViaRef).toBe('Submit Form')
|
|
697
735
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
736
|
+
// Test role and name
|
|
737
|
+
const btnInfo = ariaResult.refToElement.get('submit-btn')
|
|
738
|
+
expect(btnInfo?.role).toBe('button')
|
|
739
|
+
expect(btnInfo?.name).toBe('Submit Form')
|
|
701
740
|
|
|
702
|
-
|
|
741
|
+
const linkInfo = ariaResult.refToElement.get('about-link')
|
|
742
|
+
expect(linkInfo?.role).toBe('link')
|
|
743
|
+
expect(linkInfo?.name).toBe('About Us')
|
|
703
744
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
if (!fs.existsSync(assetsDir)) {
|
|
707
|
-
fs.mkdirSync(assetsDir, { recursive: true })
|
|
708
|
-
}
|
|
745
|
+
const inputInfo = ariaResult.refToElement.get('name-input')
|
|
746
|
+
expect(inputInfo?.role).toBe('textbox')
|
|
709
747
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
{ name: 'github', url: 'https://github.com/' },
|
|
714
|
-
]
|
|
715
|
-
|
|
716
|
-
const loadPageWithRetries = async ({ name, url }: { name: string; url: string }) => {
|
|
717
|
-
const page = await browserContext.newPage()
|
|
718
|
-
page.setDefaultNavigationTimeout(60000)
|
|
719
|
-
|
|
720
|
-
const attempts = 2
|
|
721
|
-
let lastError: unknown = null
|
|
722
|
-
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
723
|
-
try {
|
|
724
|
-
console.log(`[labels] opening ${name}: ${url} (attempt ${attempt}/${attempts})`)
|
|
725
|
-
await page.goto(url, { waitUntil: 'domcontentloaded' })
|
|
726
|
-
await page.waitForLoadState('networkidle', { timeout: 15000 })
|
|
727
|
-
console.log(`[labels] loaded ${name}: ${page.url()}`)
|
|
728
|
-
return { name, url, page }
|
|
729
|
-
} catch (error) {
|
|
730
|
-
lastError = error
|
|
731
|
-
}
|
|
732
|
-
}
|
|
748
|
+
await browser.close()
|
|
749
|
+
await page.close()
|
|
750
|
+
}, 60000)
|
|
733
751
|
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
752
|
+
it('should show aria ref labels on real pages and save screenshots', async () => {
|
|
753
|
+
const browserContext = getBrowserContext()
|
|
754
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
737
755
|
|
|
738
|
-
|
|
739
|
-
for (const testPage of testPages) {
|
|
740
|
-
pages.push(await loadPageWithRetries(testPage))
|
|
741
|
-
}
|
|
756
|
+
const { showAriaRefLabels, hideAriaRefLabels } = await import('./aria-snapshot.js')
|
|
742
757
|
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
}
|
|
758
|
+
// Create assets folder for screenshots
|
|
759
|
+
const assetsDir = path.join(path.dirname(new URL(import.meta.url).pathname), 'assets')
|
|
760
|
+
if (!fs.existsSync(assetsDir)) {
|
|
761
|
+
fs.mkdirSync(assetsDir, { recursive: true })
|
|
762
|
+
}
|
|
749
763
|
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
764
|
+
const testPages = [
|
|
765
|
+
{ name: 'old-reddit', url: 'https://old.reddit.com/' },
|
|
766
|
+
{ name: 'hacker-news', url: 'https://news.ycombinator.com/' },
|
|
767
|
+
]
|
|
768
|
+
|
|
769
|
+
const loadPageWithRetries = async ({ name, url }: { name: string; url: string }) => {
|
|
770
|
+
const page = await browserContext.newPage()
|
|
771
|
+
page.setDefaultNavigationTimeout(60000)
|
|
772
|
+
|
|
773
|
+
const attempts = 2
|
|
774
|
+
let lastError: unknown = null
|
|
775
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
776
|
+
try {
|
|
777
|
+
console.log(`[labels] opening ${name}: ${url} (attempt ${attempt}/${attempts})`)
|
|
778
|
+
await page.goto(url, { waitUntil: 'load' })
|
|
779
|
+
console.log(`[labels] loaded ${name}: ${page.url()}`)
|
|
780
|
+
return { name, url, page }
|
|
781
|
+
} catch (error) {
|
|
782
|
+
lastError = error
|
|
767
783
|
}
|
|
784
|
+
}
|
|
768
785
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
if (!cdpPage) {
|
|
776
|
-
throw new Error(`Could not find CDP page for ${name}`)
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
console.log(`[labels] show labels ${name}`)
|
|
780
|
-
const { labelCount } = await withTimeout(
|
|
781
|
-
`showAriaRefLabels(${name})`,
|
|
782
|
-
async () => {
|
|
783
|
-
return await showAriaRefLabels({ page: cdpPage })
|
|
784
|
-
},
|
|
785
|
-
60000
|
|
786
|
-
)
|
|
787
|
-
console.log(`${name}: ${labelCount} labels shown`)
|
|
788
|
-
if (name !== 'google') {
|
|
789
|
-
expect(labelCount).toBeGreaterThan(0)
|
|
790
|
-
}
|
|
786
|
+
await page.close()
|
|
787
|
+
throw new Error(`Failed to load ${name} after ${attempts} attempts`, {
|
|
788
|
+
cause: lastError instanceof Error ? lastError : undefined,
|
|
789
|
+
})
|
|
790
|
+
}
|
|
791
791
|
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
console.log(`[labels] count dom labels ${name}`)
|
|
805
|
-
const labelElements = await withTimeout(
|
|
806
|
-
`countLabels(${name})`,
|
|
807
|
-
async () => {
|
|
808
|
-
return await cdpPage.evaluate(() =>
|
|
809
|
-
document.querySelectorAll('.__pw_label__').length
|
|
810
|
-
)
|
|
811
|
-
},
|
|
812
|
-
10000
|
|
813
|
-
)
|
|
814
|
-
expect(labelElements).toBe(labelCount)
|
|
815
|
-
|
|
816
|
-
console.log(`[labels] hide labels ${name}`)
|
|
817
|
-
await withTimeout(
|
|
818
|
-
`hideAriaRefLabels(${name})`,
|
|
819
|
-
async () => {
|
|
820
|
-
await hideAriaRefLabels({ page: cdpPage })
|
|
821
|
-
},
|
|
822
|
-
10000
|
|
823
|
-
)
|
|
824
|
-
|
|
825
|
-
const labelsAfterHide = await withTimeout(
|
|
826
|
-
`verifyHide(${name})`,
|
|
827
|
-
async () => {
|
|
828
|
-
return await cdpPage.evaluate(() =>
|
|
829
|
-
document.getElementById('__playwriter_labels__')
|
|
830
|
-
)
|
|
831
|
-
},
|
|
832
|
-
10000
|
|
833
|
-
)
|
|
834
|
-
expect(labelsAfterHide).toBeNull()
|
|
792
|
+
const pages = await Promise.all(
|
|
793
|
+
testPages.map((testPage) => {
|
|
794
|
+
return loadPageWithRetries(testPage)
|
|
795
|
+
}),
|
|
796
|
+
)
|
|
797
|
+
|
|
798
|
+
for (const { page } of pages) {
|
|
799
|
+
await page.bringToFront()
|
|
800
|
+
await serviceWorker.evaluate(async () => {
|
|
801
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
802
|
+
})
|
|
803
|
+
}
|
|
835
804
|
|
|
836
|
-
|
|
837
|
-
|
|
805
|
+
const browser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
806
|
+
|
|
807
|
+
const withTimeout = async <T>(label: string, task: () => Promise<T>, timeoutMs: number): Promise<T> => {
|
|
808
|
+
let timeoutId: NodeJS.Timeout | null = null
|
|
809
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
810
|
+
timeoutId = setTimeout(() => {
|
|
811
|
+
reject(new Error(`Timed out after ${timeoutMs}ms: ${label}`))
|
|
812
|
+
}, timeoutMs)
|
|
813
|
+
})
|
|
814
|
+
|
|
815
|
+
try {
|
|
816
|
+
return await Promise.race([task(), timeoutPromise])
|
|
817
|
+
} finally {
|
|
818
|
+
if (timeoutId) {
|
|
819
|
+
clearTimeout(timeoutId)
|
|
838
820
|
}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
const wsUrl = getCdpUrl({ port: TEST_PORT })
|
|
825
|
+
|
|
826
|
+
for (const { name, url, page } of pages) {
|
|
827
|
+
console.log(`[labels] start ${name}`)
|
|
828
|
+
const cdpPage = browser
|
|
829
|
+
.contexts()[0]
|
|
830
|
+
.pages()
|
|
831
|
+
.find((p) => p.url().includes(new URL(url).hostname))
|
|
832
|
+
|
|
833
|
+
if (!cdpPage) {
|
|
834
|
+
throw new Error(`Could not find CDP page for ${name}`)
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
console.log(`[labels] show labels ${name}`)
|
|
838
|
+
const { labelCount } = await withTimeout(
|
|
839
|
+
`showAriaRefLabels(${name})`,
|
|
840
|
+
async () => {
|
|
841
|
+
return await showAriaRefLabels({ page: cdpPage })
|
|
842
|
+
},
|
|
843
|
+
60000,
|
|
844
|
+
)
|
|
845
|
+
console.log(`${name}: ${labelCount} labels shown`)
|
|
846
|
+
expect(labelCount).toBeGreaterThan(0)
|
|
847
|
+
|
|
848
|
+
console.log(`[labels] screenshot ${name}`)
|
|
849
|
+
const screenshot = await withTimeout(
|
|
850
|
+
`screenshot(${name})`,
|
|
851
|
+
async () => {
|
|
852
|
+
return await cdpPage.screenshot({ type: 'png', fullPage: false })
|
|
853
|
+
},
|
|
854
|
+
30000,
|
|
855
|
+
)
|
|
856
|
+
const screenshotPath = path.join(assetsDir, `aria-labels-${name}.png`)
|
|
857
|
+
fs.writeFileSync(screenshotPath, screenshot)
|
|
858
|
+
console.log(`Screenshot saved: ${screenshotPath}`)
|
|
859
|
+
|
|
860
|
+
console.log(`[labels] count dom labels ${name}`)
|
|
861
|
+
const labelElements = await withTimeout(
|
|
862
|
+
`countLabels(${name})`,
|
|
863
|
+
async () => {
|
|
864
|
+
return await cdpPage.evaluate(() => document.querySelectorAll('.__pw_label__').length)
|
|
865
|
+
},
|
|
866
|
+
10000,
|
|
867
|
+
)
|
|
868
|
+
expect(labelElements).toBe(labelCount)
|
|
869
|
+
|
|
870
|
+
console.log(`[labels] hide labels ${name}`)
|
|
871
|
+
await withTimeout(
|
|
872
|
+
`hideAriaRefLabels(${name})`,
|
|
873
|
+
async () => {
|
|
874
|
+
await hideAriaRefLabels({ page: cdpPage })
|
|
875
|
+
},
|
|
876
|
+
10000,
|
|
877
|
+
)
|
|
878
|
+
|
|
879
|
+
const labelsAfterHide = await withTimeout(
|
|
880
|
+
`verifyHide(${name})`,
|
|
881
|
+
async () => {
|
|
882
|
+
return await cdpPage.evaluate(() => document.getElementById('__playwriter_labels__'))
|
|
883
|
+
},
|
|
884
|
+
10000,
|
|
885
|
+
)
|
|
886
|
+
expect(labelsAfterHide).toBeNull()
|
|
887
|
+
|
|
888
|
+
console.log(`[labels] close page ${name}`)
|
|
889
|
+
await page.close()
|
|
890
|
+
}
|
|
839
891
|
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
892
|
+
await browser.close()
|
|
893
|
+
console.log(`Screenshots saved to: ${assetsDir}`)
|
|
894
|
+
}, 180000)
|
|
843
895
|
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
896
|
+
it('should take screenshot with accessibility labels via MCP execute tool', async () => {
|
|
897
|
+
const browserContext = getBrowserContext()
|
|
898
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
847
899
|
|
|
848
|
-
|
|
849
|
-
|
|
900
|
+
const page = await browserContext.newPage()
|
|
901
|
+
await page.setContent(`
|
|
850
902
|
<html>
|
|
851
903
|
<head>
|
|
852
904
|
<style>
|
|
@@ -903,17 +955,17 @@ describe('Snapshot & Screenshot Tests', () => {
|
|
|
903
955
|
</body>
|
|
904
956
|
</html>
|
|
905
957
|
`)
|
|
906
|
-
|
|
958
|
+
await page.bringToFront()
|
|
907
959
|
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
960
|
+
await serviceWorker.evaluate(async () => {
|
|
961
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
962
|
+
})
|
|
963
|
+
await new Promise((r) => setTimeout(r, 400))
|
|
912
964
|
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
965
|
+
const result = await client.callTool({
|
|
966
|
+
name: 'execute',
|
|
967
|
+
arguments: {
|
|
968
|
+
code: js`
|
|
917
969
|
let testPage;
|
|
918
970
|
for (const p of context.pages()) {
|
|
919
971
|
const html = await p.content();
|
|
@@ -922,45 +974,45 @@ describe('Snapshot & Screenshot Tests', () => {
|
|
|
922
974
|
if (!testPage) throw new Error('Test page not found');
|
|
923
975
|
await screenshotWithAccessibilityLabels({ page: testPage });
|
|
924
976
|
`,
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
977
|
+
timeout: 15000,
|
|
978
|
+
},
|
|
979
|
+
})
|
|
980
|
+
|
|
981
|
+
expect(result.isError).toBeFalsy()
|
|
982
|
+
|
|
983
|
+
const content = result.content as any[]
|
|
984
|
+
expect(content.length).toBe(2)
|
|
985
|
+
|
|
986
|
+
const textContent = content.find((c) => c.type === 'text')
|
|
987
|
+
expect(textContent).toBeDefined()
|
|
988
|
+
expect(textContent.text).toContain('Screenshot saved to:')
|
|
989
|
+
expect(textContent.text).toContain('.jpg')
|
|
990
|
+
expect(textContent.text).toContain('Labels shown:')
|
|
991
|
+
expect(textContent.text).toContain('Accessibility snapshot:')
|
|
992
|
+
expect(textContent.text).toContain('Submit Form')
|
|
993
|
+
|
|
994
|
+
const imageContent = content.find((c) => c.type === 'image')
|
|
995
|
+
expect(imageContent).toBeDefined()
|
|
996
|
+
expect(imageContent.mimeType).toBe('image/jpeg')
|
|
997
|
+
expect(imageContent.data).toBeDefined()
|
|
998
|
+
expect(imageContent.data.length).toBeGreaterThan(100)
|
|
999
|
+
|
|
1000
|
+
const buffer = Buffer.from(imageContent.data, 'base64')
|
|
1001
|
+
const dimensions = imageSize(buffer)
|
|
1002
|
+
|
|
1003
|
+
const viewport = await page.evaluate(() => ({
|
|
1004
|
+
innerWidth: window.innerWidth,
|
|
1005
|
+
innerHeight: window.innerHeight,
|
|
1006
|
+
outerWidth: window.outerWidth,
|
|
1007
|
+
outerHeight: window.outerHeight,
|
|
1008
|
+
}))
|
|
1009
|
+
console.log('Screenshot dimensions:', dimensions.width, 'x', dimensions.height)
|
|
1010
|
+
console.log('Window viewport:', viewport)
|
|
1011
|
+
|
|
1012
|
+
expect(dimensions.type).toBe('jpg')
|
|
1013
|
+
expect(dimensions.width).toBeGreaterThan(0)
|
|
1014
|
+
expect(dimensions.height).toBeGreaterThan(0)
|
|
1015
|
+
|
|
1016
|
+
await page.close()
|
|
1017
|
+
}, 60000)
|
|
966
1018
|
})
|