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
package/src/relay-core.test.ts
CHANGED
|
@@ -2,115 +2,126 @@ import { createMCPClient } from './mcp-client.js'
|
|
|
2
2
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
|
|
3
3
|
import { getCDPSessionForPage } from './cdp-session.js'
|
|
4
4
|
import { getCdpUrl } from './utils.js'
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
setupTestContext,
|
|
7
|
+
cleanupTestContext,
|
|
8
|
+
getExtensionServiceWorker,
|
|
9
|
+
type TestContext,
|
|
10
|
+
withTimeout,
|
|
11
|
+
js,
|
|
12
|
+
tryJsonParse,
|
|
13
|
+
createSimpleServer,
|
|
14
|
+
} from './test-utils.js'
|
|
6
15
|
import './test-declarations.js'
|
|
7
16
|
|
|
8
17
|
const TEST_PORT = 19987
|
|
9
18
|
|
|
10
19
|
describe('Relay Core Tests', () => {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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-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 inject script via addScriptTag through CDP relay', async () => {
|
|
44
|
+
const browserContext = getBrowserContext()
|
|
45
|
+
const serviceWorker = await withTimeout({
|
|
46
|
+
promise: getExtensionServiceWorker(browserContext),
|
|
47
|
+
timeoutMs: 5000,
|
|
48
|
+
errorMessage: 'Timed out waiting for extension service worker for iframe test',
|
|
27
49
|
})
|
|
28
50
|
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
51
|
+
const page = await browserContext.newPage()
|
|
52
|
+
const html = '<html><body><button id="btn">Click</button></body></html>'
|
|
53
|
+
const dataUrl = `data:text/html,${encodeURIComponent(html)}`
|
|
54
|
+
await page.goto(dataUrl)
|
|
55
|
+
await page.bringToFront()
|
|
56
|
+
|
|
57
|
+
await withTimeout({
|
|
58
|
+
promise: serviceWorker.evaluate(async () => {
|
|
59
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
60
|
+
}),
|
|
61
|
+
timeoutMs: 10000,
|
|
62
|
+
errorMessage: 'Timed out toggling extension for active tab',
|
|
63
|
+
})
|
|
64
|
+
await new Promise((r) => {
|
|
65
|
+
setTimeout(r, 100)
|
|
66
|
+
})
|
|
41
67
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
await withTimeout({
|
|
49
|
-
promise: serviceWorker.evaluate(async () => {
|
|
50
|
-
await globalThis.toggleExtensionForActiveTab()
|
|
51
|
-
}),
|
|
52
|
-
timeoutMs: 10000,
|
|
53
|
-
errorMessage: 'Timed out toggling extension for active tab',
|
|
54
|
-
})
|
|
55
|
-
await new Promise((r) => { setTimeout(r, 100) })
|
|
68
|
+
const cdpSession = await withTimeout({
|
|
69
|
+
promise: getCDPSessionForPage({ page }),
|
|
70
|
+
timeoutMs: 10000,
|
|
71
|
+
errorMessage: 'Timed out creating CDP session for page',
|
|
72
|
+
})
|
|
56
73
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
})
|
|
74
|
+
const hasGlobalBefore = await page.evaluate(() => {
|
|
75
|
+
return Boolean((globalThis as { __testGlobal?: unknown }).__testGlobal)
|
|
76
|
+
})
|
|
77
|
+
expect(hasGlobalBefore).toBe(false)
|
|
62
78
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
await withTimeout({
|
|
69
|
-
promise: (async () => {
|
|
70
|
-
await cdpSession.send('Page.enable')
|
|
71
|
-
await cdpSession.send('Page.addScriptToEvaluateOnNewDocument', {
|
|
72
|
-
source: 'globalThis.__testGlobal = { foo: "bar" }',
|
|
73
|
-
})
|
|
74
|
-
await page.reload({ waitUntil: 'domcontentloaded' })
|
|
75
|
-
})(),
|
|
76
|
-
timeoutMs: 10000,
|
|
77
|
-
errorMessage: 'Timed out injecting script via CDP session',
|
|
79
|
+
await withTimeout({
|
|
80
|
+
promise: (async () => {
|
|
81
|
+
await cdpSession.send('Page.enable')
|
|
82
|
+
await cdpSession.send('Page.addScriptToEvaluateOnNewDocument', {
|
|
83
|
+
source: 'globalThis.__testGlobal = { foo: "bar" }',
|
|
78
84
|
})
|
|
85
|
+
await page.reload({ waitUntil: 'domcontentloaded' })
|
|
86
|
+
})(),
|
|
87
|
+
timeoutMs: 10000,
|
|
88
|
+
errorMessage: 'Timed out injecting script via CDP session',
|
|
89
|
+
})
|
|
79
90
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
91
|
+
const hasGlobalAfter = await page.evaluate(() => {
|
|
92
|
+
return (globalThis as { __testGlobal?: unknown }).__testGlobal
|
|
93
|
+
})
|
|
94
|
+
expect(hasGlobalAfter).toEqual({ foo: 'bar' })
|
|
84
95
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
96
|
+
await cdpSession.detach()
|
|
97
|
+
await page.close()
|
|
98
|
+
}, 60000)
|
|
88
99
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
100
|
+
it('should execute code and capture console output', async () => {
|
|
101
|
+
await client.callTool({
|
|
102
|
+
name: 'execute',
|
|
103
|
+
arguments: {
|
|
104
|
+
code: js`
|
|
94
105
|
const newPage = await context.newPage();
|
|
95
106
|
state.page = newPage;
|
|
96
107
|
if (!state.pages) state.pages = [];
|
|
97
108
|
state.pages.push(newPage);
|
|
98
109
|
`,
|
|
99
|
-
|
|
100
|
-
|
|
110
|
+
},
|
|
111
|
+
})
|
|
101
112
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
113
|
+
const result = await client.callTool({
|
|
114
|
+
name: 'execute',
|
|
115
|
+
arguments: {
|
|
116
|
+
code: js`
|
|
106
117
|
await state.page.goto('https://example.com');
|
|
107
118
|
const title = await state.page.title();
|
|
108
119
|
console.log('Page title:', title);
|
|
109
120
|
return { url: state.page.url(), title };
|
|
110
121
|
`,
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
122
|
+
},
|
|
123
|
+
})
|
|
124
|
+
expect(result.content).toMatchInlineSnapshot(`
|
|
114
125
|
[
|
|
115
126
|
{
|
|
116
127
|
"text": "Console output:
|
|
@@ -121,138 +132,134 @@ describe('Relay Core Tests', () => {
|
|
|
121
132
|
},
|
|
122
133
|
]
|
|
123
134
|
`)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
135
|
+
expect(result.content).toBeDefined()
|
|
136
|
+
}, 30000)
|
|
137
|
+
|
|
138
|
+
it('should show extension as connected for pages created via newPage()', async () => {
|
|
139
|
+
const browserContext = getBrowserContext()
|
|
140
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
141
|
+
|
|
142
|
+
// Create a page via MCP (which uses context.newPage())
|
|
143
|
+
await client.callTool({
|
|
144
|
+
name: 'execute',
|
|
145
|
+
arguments: {
|
|
146
|
+
code: js`
|
|
136
147
|
const newPage = await context.newPage();
|
|
137
148
|
state.testPage = newPage;
|
|
138
149
|
await newPage.goto('https://example.com/mcp-test');
|
|
139
150
|
return newPage.url();
|
|
140
151
|
`,
|
|
141
|
-
|
|
142
|
-
|
|
152
|
+
},
|
|
153
|
+
})
|
|
143
154
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
155
|
+
// Get extension state to verify the page is marked as connected
|
|
156
|
+
const extensionState = await serviceWorker.evaluate(async () => {
|
|
157
|
+
const state = globalThis.getExtensionState()
|
|
158
|
+
const tabs = await chrome.tabs.query({})
|
|
159
|
+
const testTab = tabs.find((t: any) => t.url?.includes('mcp-test'))
|
|
160
|
+
return {
|
|
161
|
+
connected: !!testTab && !!testTab.id && state.tabs.has(testTab.id),
|
|
162
|
+
tabId: testTab?.id,
|
|
163
|
+
tabInfo: testTab?.id ? state.tabs.get(testTab.id) : null,
|
|
164
|
+
connectionState: state.connectionState,
|
|
165
|
+
}
|
|
166
|
+
})
|
|
156
167
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
168
|
+
expect(extensionState.connected).toBe(true)
|
|
169
|
+
expect(extensionState.tabInfo?.state).toBe('connected')
|
|
170
|
+
expect(extensionState.connectionState).toBe('connected')
|
|
160
171
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
172
|
+
// Clean up
|
|
173
|
+
await client.callTool({
|
|
174
|
+
name: 'execute',
|
|
175
|
+
arguments: {
|
|
176
|
+
code: js`
|
|
166
177
|
if (state.testPage) {
|
|
167
178
|
await state.testPage.close();
|
|
168
179
|
delete state.testPage;
|
|
169
180
|
}
|
|
170
181
|
`,
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
182
|
+
},
|
|
183
|
+
})
|
|
184
|
+
}, 30000)
|
|
185
|
+
|
|
186
|
+
const snapshotTestCases = [
|
|
187
|
+
{
|
|
188
|
+
name: 'hacker-news',
|
|
189
|
+
url: 'https://news.ycombinator.com/item?id=1',
|
|
190
|
+
expectedContent: ['role=link', 'Hacker News'],
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
name: 'shadcn-ui',
|
|
194
|
+
url: 'https://ui.shadcn.com/',
|
|
195
|
+
expectedContent: ['shadcn'],
|
|
196
|
+
},
|
|
197
|
+
]
|
|
198
|
+
|
|
199
|
+
for (const testCase of snapshotTestCases) {
|
|
200
|
+
it(`should get accessibility snapshot of ${testCase.name}`, async () => {
|
|
201
|
+
await client.callTool({
|
|
202
|
+
name: 'execute',
|
|
203
|
+
arguments: {
|
|
204
|
+
code: js`
|
|
194
205
|
const newPage = await context.newPage();
|
|
195
206
|
state.page = newPage;
|
|
196
207
|
if (!state.pages) state.pages = [];
|
|
197
208
|
state.pages.push(newPage);
|
|
198
209
|
`,
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
210
|
+
},
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
// Capture interactiveOnly=true snapshot (default)
|
|
214
|
+
const interactiveResult = await client.callTool({
|
|
215
|
+
name: 'execute',
|
|
216
|
+
arguments: {
|
|
217
|
+
code: js`
|
|
207
218
|
await state.page.goto('${testCase.url}', { waitUntil: 'domcontentloaded' });
|
|
208
|
-
const
|
|
209
|
-
return
|
|
219
|
+
const snap = await snapshot({ page: state.page, showDiffSinceLastCall: false, interactiveOnly: true });
|
|
220
|
+
return snap;
|
|
210
221
|
`,
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const snapshot = await accessibilitySnapshot({ page: state.page, showDiffSinceLastCall: false, interactiveOnly: false });
|
|
232
|
-
return snapshot;
|
|
222
|
+
},
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
const interactiveData =
|
|
226
|
+
typeof interactiveResult === 'object' && interactiveResult.content?.[0]?.text
|
|
227
|
+
? tryJsonParse(interactiveResult.content[0].text)
|
|
228
|
+
: interactiveResult
|
|
229
|
+
await expect(interactiveData).toMatchFileSnapshot(`snapshots/${testCase.name}-accessibility-interactive.md`)
|
|
230
|
+
expect(interactiveResult.content).toBeDefined()
|
|
231
|
+
for (const expected of testCase.expectedContent) {
|
|
232
|
+
expect(interactiveData).toContain(expected)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Capture interactiveOnly=false snapshot (full tree)
|
|
236
|
+
const fullResult = await client.callTool({
|
|
237
|
+
name: 'execute',
|
|
238
|
+
arguments: {
|
|
239
|
+
code: js`
|
|
240
|
+
const snap = await snapshot({ page: state.page, showDiffSinceLastCall: false, interactiveOnly: false });
|
|
241
|
+
return snap;
|
|
233
242
|
`,
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}, 60000)
|
|
249
|
-
}
|
|
243
|
+
},
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
const fullData =
|
|
247
|
+
typeof fullResult === 'object' && fullResult.content?.[0]?.text
|
|
248
|
+
? tryJsonParse(fullResult.content[0].text)
|
|
249
|
+
: fullResult
|
|
250
|
+
await expect(fullData).toMatchFileSnapshot(`snapshots/${testCase.name}-accessibility-full.md`)
|
|
251
|
+
expect(fullResult.content).toBeDefined()
|
|
252
|
+
for (const expected of testCase.expectedContent) {
|
|
253
|
+
expect(fullData).toContain(expected)
|
|
254
|
+
}
|
|
255
|
+
}, 60000)
|
|
256
|
+
}
|
|
250
257
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
258
|
+
it('should close all created pages', async () => {
|
|
259
|
+
const result = await client.callTool({
|
|
260
|
+
name: 'execute',
|
|
261
|
+
arguments: {
|
|
262
|
+
code: js`
|
|
256
263
|
if (state.pages && state.pages.length > 0) {
|
|
257
264
|
for (const page of state.pages) {
|
|
258
265
|
await page.close();
|
|
@@ -263,17 +270,16 @@ describe('Relay Core Tests', () => {
|
|
|
263
270
|
}
|
|
264
271
|
return { closedCount: 0 };
|
|
265
272
|
`,
|
|
266
|
-
|
|
267
|
-
})
|
|
268
|
-
|
|
273
|
+
},
|
|
269
274
|
})
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
it('should capture browser console logs with getLatestLogs', async () => {
|
|
278
|
+
// Ensure clean state and clear any existing logs
|
|
279
|
+
const resetResult = await client.callTool({
|
|
280
|
+
name: 'execute',
|
|
281
|
+
arguments: {
|
|
282
|
+
code: js`
|
|
277
283
|
// Clear any existing logs from previous tests
|
|
278
284
|
clearAllLogs();
|
|
279
285
|
console.log('Cleared all existing logs');
|
|
@@ -284,27 +290,27 @@ describe('Relay Core Tests', () => {
|
|
|
284
290
|
|
|
285
291
|
return { success: true, pagesCount: pages.length };
|
|
286
292
|
`,
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
293
|
+
},
|
|
294
|
+
})
|
|
295
|
+
console.log('Cleanup result:', resetResult)
|
|
290
296
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
297
|
+
// Create a new page for this test
|
|
298
|
+
await client.callTool({
|
|
299
|
+
name: 'execute',
|
|
300
|
+
arguments: {
|
|
301
|
+
code: js`
|
|
296
302
|
const newPage = await context.newPage();
|
|
297
303
|
state.testLogPage = newPage;
|
|
298
304
|
await newPage.goto('about:blank');
|
|
299
305
|
`,
|
|
300
|
-
|
|
301
|
-
|
|
306
|
+
},
|
|
307
|
+
})
|
|
302
308
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
309
|
+
// Generate some console logs in the browser
|
|
310
|
+
await client.callTool({
|
|
311
|
+
name: 'execute',
|
|
312
|
+
arguments: {
|
|
313
|
+
code: js`
|
|
308
314
|
await state.testLogPage.evaluate(() => {
|
|
309
315
|
console.log('Test log 12345');
|
|
310
316
|
console.error('Test error 67890');
|
|
@@ -314,325 +320,403 @@ describe('Relay Core Tests', () => {
|
|
|
314
320
|
// Wait for logs to be captured
|
|
315
321
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
316
322
|
`,
|
|
317
|
-
|
|
318
|
-
|
|
323
|
+
},
|
|
324
|
+
})
|
|
319
325
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
326
|
+
// Test getting all logs
|
|
327
|
+
const allLogsResult = await client.callTool({
|
|
328
|
+
name: 'execute',
|
|
329
|
+
arguments: {
|
|
330
|
+
code: js`
|
|
325
331
|
const logs = await getLatestLogs();
|
|
326
332
|
logs.forEach(log => console.log(log));
|
|
327
333
|
`,
|
|
328
|
-
|
|
329
|
-
|
|
334
|
+
},
|
|
335
|
+
})
|
|
330
336
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
337
|
+
const output = (allLogsResult as any).content[0].text
|
|
338
|
+
expect(output).toContain('[log] Test log 12345')
|
|
339
|
+
expect(output).toContain('[error] Test error 67890')
|
|
340
|
+
expect(output).toContain('[warning] Test warning 11111')
|
|
335
341
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
342
|
+
// Test filtering by search string
|
|
343
|
+
const errorLogsResult = await client.callTool({
|
|
344
|
+
name: 'execute',
|
|
345
|
+
arguments: {
|
|
346
|
+
code: js`
|
|
341
347
|
const logs = await getLatestLogs({ search: 'error' });
|
|
342
348
|
logs.forEach(log => console.log(log));
|
|
343
349
|
`,
|
|
344
|
-
|
|
345
|
-
|
|
350
|
+
},
|
|
351
|
+
})
|
|
346
352
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
353
|
+
const errorOutput = (errorLogsResult as any).content[0].text
|
|
354
|
+
expect(errorOutput).toContain('[error] Test error 67890')
|
|
355
|
+
// With context lines (5 above/below), nearby logs are also included
|
|
356
|
+
expect(errorOutput).toContain('[log] Test log 12345')
|
|
351
357
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
358
|
+
// Test that logs are cleared on page reload
|
|
359
|
+
await client.callTool({
|
|
360
|
+
name: 'execute',
|
|
361
|
+
arguments: {
|
|
362
|
+
code: js`
|
|
357
363
|
// First add a log before reload
|
|
358
364
|
await state.testLogPage.evaluate(() => {
|
|
359
365
|
console.log('Before reload 99999');
|
|
360
366
|
});
|
|
361
367
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
362
368
|
`,
|
|
363
|
-
|
|
364
|
-
|
|
369
|
+
},
|
|
370
|
+
})
|
|
365
371
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
372
|
+
// Verify the log exists
|
|
373
|
+
const beforeReloadResult = await client.callTool({
|
|
374
|
+
name: 'execute',
|
|
375
|
+
arguments: {
|
|
376
|
+
code: js`
|
|
371
377
|
const logs = await getLatestLogs({ page: state.testLogPage });
|
|
372
378
|
console.log('Logs before reload:', logs.length);
|
|
373
379
|
logs.forEach(log => console.log(log));
|
|
374
380
|
`,
|
|
375
|
-
|
|
376
|
-
|
|
381
|
+
},
|
|
382
|
+
})
|
|
377
383
|
|
|
378
|
-
|
|
379
|
-
|
|
384
|
+
const beforeReloadOutput = (beforeReloadResult as any).content[0].text
|
|
385
|
+
expect(beforeReloadOutput).toContain('[log] Before reload 99999')
|
|
380
386
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
387
|
+
// Reload the page
|
|
388
|
+
await client.callTool({
|
|
389
|
+
name: 'execute',
|
|
390
|
+
arguments: {
|
|
391
|
+
code: js`
|
|
386
392
|
await state.testLogPage.reload();
|
|
387
393
|
await state.testLogPage.evaluate(() => {
|
|
388
394
|
console.log('After reload 88888');
|
|
389
395
|
});
|
|
390
396
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
391
397
|
`,
|
|
392
|
-
|
|
393
|
-
|
|
398
|
+
},
|
|
399
|
+
})
|
|
394
400
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
401
|
+
// Check logs after reload - old logs should be gone
|
|
402
|
+
const afterReloadResult = await client.callTool({
|
|
403
|
+
name: 'execute',
|
|
404
|
+
arguments: {
|
|
405
|
+
code: js`
|
|
400
406
|
const logs = await getLatestLogs({ page: state.testLogPage });
|
|
401
407
|
console.log('Logs after reload:', logs.length);
|
|
402
408
|
logs.forEach(log => console.log(log));
|
|
403
409
|
`,
|
|
404
|
-
|
|
405
|
-
|
|
410
|
+
},
|
|
411
|
+
})
|
|
406
412
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
413
|
+
const afterReloadOutput = (afterReloadResult as any).content[0].text
|
|
414
|
+
expect(afterReloadOutput).toContain('[log] After reload 88888')
|
|
415
|
+
expect(afterReloadOutput).not.toContain('[log] Before reload 99999')
|
|
410
416
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
417
|
+
// Clean up
|
|
418
|
+
await client.callTool({
|
|
419
|
+
name: 'execute',
|
|
420
|
+
arguments: {
|
|
421
|
+
code: js`
|
|
416
422
|
await state.testLogPage.close();
|
|
417
423
|
delete state.testLogPage;
|
|
418
424
|
`,
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
425
|
+
},
|
|
426
|
+
})
|
|
427
|
+
}, 30000)
|
|
428
|
+
|
|
429
|
+
it('should keep logs separate between different pages', async () => {
|
|
430
|
+
// Clear any existing logs from previous tests
|
|
431
|
+
await client.callTool({
|
|
432
|
+
name: 'execute',
|
|
433
|
+
arguments: {
|
|
434
|
+
code: js`
|
|
429
435
|
clearAllLogs();
|
|
430
436
|
console.log('Cleared all existing logs for second log test');
|
|
431
437
|
`,
|
|
432
|
-
|
|
433
|
-
|
|
438
|
+
},
|
|
439
|
+
})
|
|
434
440
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
441
|
+
// Create two pages
|
|
442
|
+
await client.callTool({
|
|
443
|
+
name: 'execute',
|
|
444
|
+
arguments: {
|
|
445
|
+
code: js`
|
|
440
446
|
state.pageA = await context.newPage();
|
|
441
447
|
state.pageB = await context.newPage();
|
|
442
448
|
await state.pageA.goto('about:blank');
|
|
443
449
|
await state.pageB.goto('about:blank');
|
|
444
450
|
`,
|
|
445
|
-
|
|
446
|
-
|
|
451
|
+
},
|
|
452
|
+
})
|
|
447
453
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
454
|
+
// Generate logs in page A
|
|
455
|
+
await client.callTool({
|
|
456
|
+
name: 'execute',
|
|
457
|
+
arguments: {
|
|
458
|
+
code: js`
|
|
453
459
|
await state.pageA.evaluate(() => {
|
|
454
460
|
console.log('PageA log 11111');
|
|
455
461
|
console.error('PageA error 22222');
|
|
456
462
|
});
|
|
457
463
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
458
464
|
`,
|
|
459
|
-
|
|
460
|
-
|
|
465
|
+
},
|
|
466
|
+
})
|
|
461
467
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
468
|
+
// Generate logs in page B
|
|
469
|
+
await client.callTool({
|
|
470
|
+
name: 'execute',
|
|
471
|
+
arguments: {
|
|
472
|
+
code: js`
|
|
467
473
|
await state.pageB.evaluate(() => {
|
|
468
474
|
console.log('PageB log 33333');
|
|
469
475
|
console.error('PageB error 44444');
|
|
470
476
|
});
|
|
471
477
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
472
478
|
`,
|
|
473
|
-
|
|
474
|
-
|
|
479
|
+
},
|
|
480
|
+
})
|
|
475
481
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
482
|
+
// Check logs for page A - should only have page A logs
|
|
483
|
+
const pageALogsResult = await client.callTool({
|
|
484
|
+
name: 'execute',
|
|
485
|
+
arguments: {
|
|
486
|
+
code: js`
|
|
481
487
|
const logs = await getLatestLogs({ page: state.pageA });
|
|
482
488
|
console.log('Page A logs:', logs.length);
|
|
483
489
|
logs.forEach(log => console.log(log));
|
|
484
490
|
`,
|
|
485
|
-
|
|
486
|
-
|
|
491
|
+
},
|
|
492
|
+
})
|
|
487
493
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
494
|
+
const pageAOutput = (pageALogsResult as any).content[0].text
|
|
495
|
+
expect(pageAOutput).toContain('[log] PageA log 11111')
|
|
496
|
+
expect(pageAOutput).toContain('[error] PageA error 22222')
|
|
497
|
+
expect(pageAOutput).not.toContain('PageB')
|
|
492
498
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
499
|
+
// Check logs for page B - should only have page B logs
|
|
500
|
+
const pageBLogsResult = await client.callTool({
|
|
501
|
+
name: 'execute',
|
|
502
|
+
arguments: {
|
|
503
|
+
code: js`
|
|
498
504
|
const logs = await getLatestLogs({ page: state.pageB });
|
|
499
505
|
console.log('Page B logs:', logs.length);
|
|
500
506
|
logs.forEach(log => console.log(log));
|
|
501
507
|
`,
|
|
502
|
-
|
|
503
|
-
|
|
508
|
+
},
|
|
509
|
+
})
|
|
504
510
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
511
|
+
const pageBOutput = (pageBLogsResult as any).content[0].text
|
|
512
|
+
expect(pageBOutput).toContain('[log] PageB log 33333')
|
|
513
|
+
expect(pageBOutput).toContain('[error] PageB error 44444')
|
|
514
|
+
expect(pageBOutput).not.toContain('PageA')
|
|
509
515
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
516
|
+
// Check all logs - should have logs from both pages
|
|
517
|
+
const allLogsResult = await client.callTool({
|
|
518
|
+
name: 'execute',
|
|
519
|
+
arguments: {
|
|
520
|
+
code: js`
|
|
515
521
|
const logs = await getLatestLogs();
|
|
516
522
|
console.log('All logs:', logs.length);
|
|
517
523
|
logs.forEach(log => console.log(log));
|
|
518
524
|
`,
|
|
519
|
-
|
|
520
|
-
|
|
525
|
+
},
|
|
526
|
+
})
|
|
521
527
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
528
|
+
const allOutput = (allLogsResult as any).content[0].text
|
|
529
|
+
expect(allOutput).toContain('[log] PageA log 11111')
|
|
530
|
+
expect(allOutput).toContain('[log] PageB log 33333')
|
|
525
531
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
532
|
+
// Test that reloading page A clears only page A logs
|
|
533
|
+
await client.callTool({
|
|
534
|
+
name: 'execute',
|
|
535
|
+
arguments: {
|
|
536
|
+
code: js`
|
|
531
537
|
await state.pageA.reload();
|
|
532
538
|
await state.pageA.evaluate(() => {
|
|
533
539
|
console.log('PageA after reload 55555');
|
|
534
540
|
});
|
|
535
541
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
536
542
|
`,
|
|
537
|
-
|
|
538
|
-
|
|
543
|
+
},
|
|
544
|
+
})
|
|
539
545
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
546
|
+
// Check page A logs - should only have new log
|
|
547
|
+
const pageAAfterReloadResult = await client.callTool({
|
|
548
|
+
name: 'execute',
|
|
549
|
+
arguments: {
|
|
550
|
+
code: js`
|
|
545
551
|
const logs = await getLatestLogs({ page: state.pageA });
|
|
546
552
|
console.log('Page A logs after reload:', logs.length);
|
|
547
553
|
logs.forEach(log => console.log(log));
|
|
548
554
|
`,
|
|
549
|
-
|
|
550
|
-
|
|
555
|
+
},
|
|
556
|
+
})
|
|
551
557
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
558
|
+
const pageAAfterReloadOutput = (pageAAfterReloadResult as any).content[0].text
|
|
559
|
+
expect(pageAAfterReloadOutput).toContain('[log] PageA after reload 55555')
|
|
560
|
+
expect(pageAAfterReloadOutput).not.toContain('[log] PageA log 11111')
|
|
555
561
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
562
|
+
// Check page B logs - should still have original logs
|
|
563
|
+
const pageBAfterAReloadResult = await client.callTool({
|
|
564
|
+
name: 'execute',
|
|
565
|
+
arguments: {
|
|
566
|
+
code: js`
|
|
561
567
|
const logs = await getLatestLogs({ page: state.pageB });
|
|
562
568
|
console.log('Page B logs after A reload:', logs.length);
|
|
563
569
|
logs.forEach(log => console.log(log));
|
|
564
570
|
`,
|
|
565
|
-
|
|
566
|
-
|
|
571
|
+
},
|
|
572
|
+
})
|
|
567
573
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
574
|
+
const pageBAfterAReloadOutput = (pageBAfterAReloadResult as any).content[0].text
|
|
575
|
+
expect(pageBAfterAReloadOutput).toContain('[log] PageB log 33333')
|
|
576
|
+
expect(pageBAfterAReloadOutput).toContain('[error] PageB error 44444')
|
|
571
577
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
578
|
+
// Test that logs are deleted when page is closed
|
|
579
|
+
await client.callTool({
|
|
580
|
+
name: 'execute',
|
|
581
|
+
arguments: {
|
|
582
|
+
code: js`
|
|
577
583
|
// Close page A
|
|
578
584
|
await state.pageA.close();
|
|
579
585
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
580
586
|
`,
|
|
581
|
-
|
|
582
|
-
|
|
587
|
+
},
|
|
588
|
+
})
|
|
583
589
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
590
|
+
// Check all logs - page A logs should be gone
|
|
591
|
+
const logsAfterCloseResult = await client.callTool({
|
|
592
|
+
name: 'execute',
|
|
593
|
+
arguments: {
|
|
594
|
+
code: js`
|
|
589
595
|
const logs = await getLatestLogs();
|
|
590
596
|
console.log('All logs after closing page A:', logs.length);
|
|
591
597
|
logs.forEach(log => console.log(log));
|
|
592
598
|
`,
|
|
593
|
-
|
|
594
|
-
|
|
599
|
+
},
|
|
600
|
+
})
|
|
595
601
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
602
|
+
const logsAfterCloseOutput = (logsAfterCloseResult as any).content[0].text
|
|
603
|
+
expect(logsAfterCloseOutput).not.toContain('PageA')
|
|
604
|
+
expect(logsAfterCloseOutput).toContain('[log] PageB log 33333')
|
|
599
605
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
606
|
+
// Clean up remaining page
|
|
607
|
+
await client.callTool({
|
|
608
|
+
name: 'execute',
|
|
609
|
+
arguments: {
|
|
610
|
+
code: js`
|
|
605
611
|
await state.pageB.close();
|
|
606
612
|
delete state.pageA;
|
|
607
613
|
delete state.pageB;
|
|
608
614
|
`,
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
615
|
+
},
|
|
616
|
+
})
|
|
617
|
+
}, 30000)
|
|
618
|
+
|
|
619
|
+
it('should capture console logs from cross-origin iframes', async () => {
|
|
620
|
+
// Two servers on different ports = different origins
|
|
621
|
+
const iframeServer = await createSimpleServer({
|
|
622
|
+
routes: {
|
|
623
|
+
'/iframe.html': `<!doctype html><html><body>
|
|
624
|
+
<script>
|
|
625
|
+
console.log('iframe-log-ALPHA');
|
|
626
|
+
console.error('iframe-error-BETA');
|
|
627
|
+
console.warn('iframe-warn-GAMMA');
|
|
628
|
+
</script>
|
|
629
|
+
<p>cross-origin iframe</p>
|
|
630
|
+
</body></html>`,
|
|
631
|
+
},
|
|
632
|
+
})
|
|
626
633
|
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
634
|
+
const parentServer = await createSimpleServer({
|
|
635
|
+
routes: {
|
|
636
|
+
'/': `<!doctype html><html><body>
|
|
637
|
+
<script>console.log('parent-log-DELTA');</script>
|
|
638
|
+
<iframe src="${iframeServer.baseUrl}/iframe.html"></iframe>
|
|
639
|
+
</body></html>`,
|
|
640
|
+
},
|
|
641
|
+
})
|
|
631
642
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
643
|
+
try {
|
|
644
|
+
// Clear logs and navigate to the parent page with cross-origin iframe
|
|
645
|
+
await client.callTool({
|
|
646
|
+
name: 'execute',
|
|
647
|
+
arguments: {
|
|
648
|
+
code: js`
|
|
649
|
+
clearAllLogs();
|
|
650
|
+
state.iframePage = await context.newPage();
|
|
651
|
+
await state.iframePage.goto('${parentServer.baseUrl}', { waitUntil: 'networkidle' });
|
|
652
|
+
// Wait for iframe to load and logs to be captured
|
|
653
|
+
await state.iframePage.frameLocator('iframe').locator('p').waitFor({ timeout: 5000 });
|
|
654
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
655
|
+
`,
|
|
656
|
+
},
|
|
657
|
+
})
|
|
658
|
+
|
|
659
|
+
// Retrieve logs and verify both parent and iframe logs are captured
|
|
660
|
+
const logsResult = await client.callTool({
|
|
661
|
+
name: 'execute',
|
|
662
|
+
arguments: {
|
|
663
|
+
code: js`
|
|
664
|
+
const logs = await getLatestLogs({ page: state.iframePage });
|
|
665
|
+
console.log('Cross-origin iframe logs count:', logs.length);
|
|
666
|
+
logs.forEach(log => console.log(log));
|
|
667
|
+
`,
|
|
668
|
+
},
|
|
669
|
+
})
|
|
670
|
+
|
|
671
|
+
const output = (logsResult as any).content[0].text
|
|
672
|
+
// Parent page log
|
|
673
|
+
expect(output).toContain('parent-log-DELTA')
|
|
674
|
+
// Cross-origin iframe logs
|
|
675
|
+
expect(output).toContain('iframe-log-ALPHA')
|
|
676
|
+
expect(output).toContain('iframe-error-BETA')
|
|
677
|
+
expect(output).toContain('iframe-warn-GAMMA')
|
|
678
|
+
|
|
679
|
+
// Clean up
|
|
680
|
+
await client.callTool({
|
|
681
|
+
name: 'execute',
|
|
682
|
+
arguments: {
|
|
683
|
+
code: js`
|
|
684
|
+
await state.iframePage.close();
|
|
685
|
+
delete state.iframePage;
|
|
686
|
+
`,
|
|
687
|
+
},
|
|
688
|
+
})
|
|
689
|
+
} finally {
|
|
690
|
+
await Promise.all([parentServer.close(), iframeServer.close()])
|
|
691
|
+
}
|
|
692
|
+
}, 60000)
|
|
693
|
+
|
|
694
|
+
it(
|
|
695
|
+
'should preserve system color scheme instead of forcing light mode',
|
|
696
|
+
async () => {
|
|
697
|
+
const browserContext = getBrowserContext()
|
|
698
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
699
|
+
|
|
700
|
+
const page = await browserContext.newPage()
|
|
701
|
+
await page.goto('https://example.com')
|
|
702
|
+
await page.bringToFront()
|
|
703
|
+
|
|
704
|
+
// test-utils launches with colorScheme: 'dark', so before MCP connection
|
|
705
|
+
// the browser should report dark mode
|
|
706
|
+
const colorSchemeBefore = await page.evaluate(() => {
|
|
707
|
+
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
|
708
|
+
})
|
|
709
|
+
expect(colorSchemeBefore).toBe('dark')
|
|
710
|
+
|
|
711
|
+
await serviceWorker.evaluate(async () => {
|
|
712
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
713
|
+
})
|
|
714
|
+
await new Promise((r) => setTimeout(r, 500))
|
|
715
|
+
|
|
716
|
+
const result = await client.callTool({
|
|
717
|
+
name: 'execute',
|
|
718
|
+
arguments: {
|
|
719
|
+
code: js`
|
|
636
720
|
const pages = context.pages();
|
|
637
721
|
const urls = pages.map(p => p.url());
|
|
638
722
|
const targetPage = pages.find(p => p.url().includes('example.com'));
|
|
@@ -643,29 +727,34 @@ describe('Relay Core Tests', () => {
|
|
|
643
727
|
const isLight = await targetPage.evaluate(() => window.matchMedia('(prefers-color-scheme: light)').matches);
|
|
644
728
|
return { matchesDark: isDark, matchesLight: isLight };
|
|
645
729
|
`,
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
730
|
+
},
|
|
731
|
+
})
|
|
732
|
+
|
|
733
|
+
console.log('Color scheme after MCP connection:', result.content)
|
|
734
|
+
|
|
735
|
+
// After MCP connection, color scheme should NOT be forced to light.
|
|
736
|
+
// The page.ts default is now 'no-override', so the browser's actual
|
|
737
|
+
// color scheme (dark, from test-utils launch config) should be preserved.
|
|
738
|
+
expect(result.content).toMatchInlineSnapshot(`
|
|
739
|
+
[
|
|
740
|
+
{
|
|
741
|
+
"text": "[return value] { matchesDark: true, matchesLight: false }",
|
|
742
|
+
"type": "text",
|
|
743
|
+
},
|
|
744
|
+
]
|
|
745
|
+
`)
|
|
746
|
+
|
|
747
|
+
await page.close()
|
|
748
|
+
},
|
|
749
|
+
60000,
|
|
750
|
+
)
|
|
751
|
+
|
|
752
|
+
it('should get clean HTML with getCleanHTML', async () => {
|
|
753
|
+
const browserContext = getBrowserContext()
|
|
754
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
755
|
+
|
|
756
|
+
const page = await browserContext.newPage()
|
|
757
|
+
await page.setContent(`
|
|
669
758
|
<html>
|
|
670
759
|
<head>
|
|
671
760
|
<style>.hidden { display: none; }</style>
|
|
@@ -681,18 +770,18 @@ describe('Relay Core Tests', () => {
|
|
|
681
770
|
</body>
|
|
682
771
|
</html>
|
|
683
772
|
`)
|
|
684
|
-
|
|
773
|
+
await page.bringToFront()
|
|
685
774
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
775
|
+
await serviceWorker.evaluate(async () => {
|
|
776
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
777
|
+
})
|
|
778
|
+
await new Promise((r) => setTimeout(r, 400))
|
|
690
779
|
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
780
|
+
// Test basic getCleanHTML
|
|
781
|
+
const result = await client.callTool({
|
|
782
|
+
name: 'execute',
|
|
783
|
+
arguments: {
|
|
784
|
+
code: js`
|
|
696
785
|
let testPage;
|
|
697
786
|
for (const p of context.pages()) {
|
|
698
787
|
const html = await p.content();
|
|
@@ -702,15 +791,15 @@ describe('Relay Core Tests', () => {
|
|
|
702
791
|
const html = await getCleanHTML({ locator: testPage.locator('body') });
|
|
703
792
|
return html;
|
|
704
793
|
`,
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
794
|
+
timeout: 15000,
|
|
795
|
+
},
|
|
796
|
+
})
|
|
708
797
|
|
|
709
|
-
|
|
710
|
-
|
|
798
|
+
expect(result.isError).toBeFalsy()
|
|
799
|
+
const text = (result.content as any)[0]?.text || ''
|
|
711
800
|
|
|
712
|
-
|
|
713
|
-
|
|
801
|
+
// Inline snapshot of cleaned HTML
|
|
802
|
+
expect(text).toMatchInlineSnapshot(`
|
|
714
803
|
"[return value] <div data-testid="main">
|
|
715
804
|
<h1>Hello World</h1>
|
|
716
805
|
<button aria-label="Click me">Submit</button>
|
|
@@ -719,16 +808,16 @@ describe('Relay Core Tests', () => {
|
|
|
719
808
|
</div>"
|
|
720
809
|
`)
|
|
721
810
|
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
811
|
+
// Should NOT contain script/style tags (they're removed)
|
|
812
|
+
expect(text).not.toContain('<script')
|
|
813
|
+
expect(text).not.toContain('<style')
|
|
814
|
+
expect(text).not.toContain('console.log')
|
|
726
815
|
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
816
|
+
// Test search functionality
|
|
817
|
+
const searchResult = await client.callTool({
|
|
818
|
+
name: 'execute',
|
|
819
|
+
arguments: {
|
|
820
|
+
code: js`
|
|
732
821
|
let testPage;
|
|
733
822
|
for (const p of context.pages()) {
|
|
734
823
|
const html = await p.content();
|
|
@@ -738,24 +827,24 @@ describe('Relay Core Tests', () => {
|
|
|
738
827
|
const html = await getCleanHTML({ locator: testPage, search: /button/i });
|
|
739
828
|
return html;
|
|
740
829
|
`,
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
830
|
+
timeout: 15000,
|
|
831
|
+
},
|
|
832
|
+
})
|
|
744
833
|
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
834
|
+
expect(searchResult.isError).toBeFalsy()
|
|
835
|
+
const searchText = (searchResult.content as any)[0]?.text || ''
|
|
836
|
+
expect(searchText).toContain('button')
|
|
748
837
|
|
|
749
|
-
|
|
750
|
-
|
|
838
|
+
await page.close()
|
|
839
|
+
}, 60000)
|
|
751
840
|
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
841
|
+
it('should extract page content as markdown with getPageMarkdown', async () => {
|
|
842
|
+
const browserContext = getBrowserContext()
|
|
843
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
755
844
|
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
845
|
+
const page = await browserContext.newPage()
|
|
846
|
+
// Create a realistic article-like page structure
|
|
847
|
+
await page.setContent(`
|
|
759
848
|
<html>
|
|
760
849
|
<head>
|
|
761
850
|
<title>Test Article Title</title>
|
|
@@ -782,18 +871,18 @@ describe('Relay Core Tests', () => {
|
|
|
782
871
|
</body>
|
|
783
872
|
</html>
|
|
784
873
|
`)
|
|
785
|
-
|
|
874
|
+
await page.bringToFront()
|
|
786
875
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
876
|
+
await serviceWorker.evaluate(async () => {
|
|
877
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
878
|
+
})
|
|
879
|
+
await new Promise((r) => setTimeout(r, 400))
|
|
791
880
|
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
881
|
+
// Test basic getPageMarkdown
|
|
882
|
+
const result = await client.callTool({
|
|
883
|
+
name: 'execute',
|
|
884
|
+
arguments: {
|
|
885
|
+
code: js`
|
|
797
886
|
let testPage;
|
|
798
887
|
for (const p of context.pages()) {
|
|
799
888
|
const html = await p.content();
|
|
@@ -803,30 +892,30 @@ describe('Relay Core Tests', () => {
|
|
|
803
892
|
const content = await getPageMarkdown({ page: testPage });
|
|
804
893
|
console.log(content);
|
|
805
894
|
`,
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
895
|
+
timeout: 15000,
|
|
896
|
+
},
|
|
897
|
+
})
|
|
809
898
|
|
|
810
|
-
|
|
811
|
-
|
|
899
|
+
expect(result.isError).toBeFalsy()
|
|
900
|
+
const text = (result.content as any)[0]?.text || ''
|
|
812
901
|
|
|
813
|
-
|
|
814
|
-
|
|
902
|
+
// Snapshot the full output
|
|
903
|
+
await expect(text).toMatchFileSnapshot('./snapshots/page-markdown-output.txt')
|
|
815
904
|
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
905
|
+
// Should contain article content
|
|
906
|
+
expect(text).toContain('Test Article Title')
|
|
907
|
+
expect(text).toContain('first paragraph')
|
|
908
|
+
expect(text).toContain('second paragraph')
|
|
820
909
|
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
910
|
+
// Should NOT contain script/style content
|
|
911
|
+
expect(text).not.toContain('analytics')
|
|
912
|
+
expect(text).not.toContain('background: blue')
|
|
824
913
|
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
914
|
+
// Test search functionality
|
|
915
|
+
const searchResult = await client.callTool({
|
|
916
|
+
name: 'execute',
|
|
917
|
+
arguments: {
|
|
918
|
+
code: js`
|
|
830
919
|
let testPage;
|
|
831
920
|
for (const p of context.pages()) {
|
|
832
921
|
const html = await p.content();
|
|
@@ -836,97 +925,186 @@ describe('Relay Core Tests', () => {
|
|
|
836
925
|
const content = await getPageMarkdown({ page: testPage, search: /important/i, showDiffSinceLastCall: false });
|
|
837
926
|
return content;
|
|
838
927
|
`,
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
928
|
+
timeout: 15000,
|
|
929
|
+
},
|
|
930
|
+
})
|
|
842
931
|
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
932
|
+
expect(searchResult.isError).toBeFalsy()
|
|
933
|
+
const searchText = (searchResult.content as any)[0]?.text || ''
|
|
934
|
+
expect(searchText).toContain('important')
|
|
846
935
|
|
|
847
|
-
|
|
848
|
-
|
|
936
|
+
await page.close()
|
|
937
|
+
}, 60000)
|
|
849
938
|
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
939
|
+
it('should handle default page being closed and switch to another available page', async () => {
|
|
940
|
+
// This test verifies that when the default `page` in MCP scope is closed,
|
|
941
|
+
// the MCP automatically switches to another available page instead of failing
|
|
942
|
+
// with cryptic "page closed" errors.
|
|
854
943
|
|
|
855
|
-
|
|
856
|
-
|
|
944
|
+
const browserContext = getBrowserContext()
|
|
945
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
857
946
|
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
947
|
+
// 1. Disconnect everything to start fresh
|
|
948
|
+
await serviceWorker.evaluate(async () => {
|
|
949
|
+
await globalThis.disconnectEverything()
|
|
950
|
+
})
|
|
951
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
863
952
|
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
953
|
+
// 2. Create first page and enable extension
|
|
954
|
+
const page1 = await browserContext.newPage()
|
|
955
|
+
await page1.goto('https://example.com/first-page')
|
|
956
|
+
await page1.bringToFront()
|
|
868
957
|
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
958
|
+
await serviceWorker.evaluate(async () => {
|
|
959
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
960
|
+
})
|
|
961
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
873
962
|
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
963
|
+
// 3. Reset MCP to ensure page1 becomes the default page (only page available)
|
|
964
|
+
const resetResult = await client.callTool({
|
|
965
|
+
name: 'reset',
|
|
966
|
+
arguments: {},
|
|
967
|
+
})
|
|
968
|
+
expect((resetResult as any).content[0].text).toContain('Connection reset successfully')
|
|
880
969
|
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
970
|
+
// 4. Verify initial page is accessible via default `page`
|
|
971
|
+
const initialResult = await client.callTool({
|
|
972
|
+
name: 'execute',
|
|
973
|
+
arguments: {
|
|
974
|
+
code: js`
|
|
886
975
|
const url = page.url();
|
|
887
976
|
console.log('Initial page URL:', url);
|
|
888
977
|
return { url };
|
|
889
978
|
`,
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
979
|
+
},
|
|
980
|
+
})
|
|
981
|
+
expect((initialResult as any).content[0].text).toContain('first-page')
|
|
893
982
|
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
983
|
+
// 5. Create second page and enable extension
|
|
984
|
+
const page2 = await browserContext.newPage()
|
|
985
|
+
await page2.goto('https://example.com/second-page')
|
|
986
|
+
await page2.bringToFront()
|
|
898
987
|
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
988
|
+
await serviceWorker.evaluate(async () => {
|
|
989
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
990
|
+
})
|
|
991
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
992
|
+
|
|
993
|
+
// 6. Close the first page (which is the default `page` in MCP scope)
|
|
994
|
+
await page1.close()
|
|
995
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
996
|
+
|
|
997
|
+
// 7. Execute code via MCP - should NOT fail with "page closed" error
|
|
998
|
+
// Instead, it should automatically switch to the second page
|
|
999
|
+
const afterCloseResult = await client.callTool({
|
|
1000
|
+
name: 'execute',
|
|
1001
|
+
arguments: {
|
|
1002
|
+
code: js`
|
|
914
1003
|
const url = page.url();
|
|
915
1004
|
console.log('Page URL after close:', url);
|
|
916
1005
|
const title = await page.title();
|
|
917
1006
|
return { url, title };
|
|
918
1007
|
`,
|
|
919
|
-
|
|
920
|
-
|
|
1008
|
+
},
|
|
1009
|
+
})
|
|
921
1010
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
1011
|
+
// Should succeed and return the second page's info
|
|
1012
|
+
expect((afterCloseResult as any).isError).toBeFalsy()
|
|
1013
|
+
const output = (afterCloseResult as any).content[0].text
|
|
1014
|
+
expect(output).toContain('second-page')
|
|
1015
|
+
expect(output).not.toContain('page closed')
|
|
1016
|
+
expect(output).not.toContain('Target closed')
|
|
1017
|
+
|
|
1018
|
+
// Cleanup
|
|
1019
|
+
await page2.close()
|
|
1020
|
+
}, 60000)
|
|
1021
|
+
|
|
1022
|
+
it('should show descriptive error when clicking a hidden element', async () => {
|
|
1023
|
+
// Create a fresh page and set content with a collapsed details element
|
|
1024
|
+
await client.callTool({
|
|
1025
|
+
name: 'execute',
|
|
1026
|
+
arguments: {
|
|
1027
|
+
code: js`
|
|
1028
|
+
state.errorTestPage = await context.newPage();
|
|
1029
|
+
await state.errorTestPage.setContent(\`
|
|
1030
|
+
<details>
|
|
1031
|
+
<summary>Toggle</summary>
|
|
1032
|
+
<button id="hidden-btn">Hidden Button</button>
|
|
1033
|
+
</details>
|
|
1034
|
+
\`);
|
|
1035
|
+
`,
|
|
1036
|
+
},
|
|
1037
|
+
})
|
|
1038
|
+
const result = await client.callTool({
|
|
1039
|
+
name: 'execute',
|
|
1040
|
+
arguments: {
|
|
1041
|
+
code: js`
|
|
1042
|
+
await state.errorTestPage.click('#hidden-btn');
|
|
1043
|
+
`,
|
|
1044
|
+
},
|
|
1045
|
+
})
|
|
1046
|
+
const text = (result as any).content[0].text
|
|
1047
|
+
// Strip stack traces and call logs to only match the descriptive error line
|
|
1048
|
+
const errorLine = text.split('\n').find((l: string) => l.includes('Timeout') || l.includes('not visible') || l.includes('not stable'))
|
|
1049
|
+
expect(errorLine).toMatchInlineSnapshot(`"Error executing code: page.click: Timeout 2000ms exceeded. Element is not visible — it may be hidden by CSS, inside a collapsed <details>, inactive tab, or closed accordion. Try: interact with the page to reveal it first, or use { force: true } to skip visibility checks"`)
|
|
1050
|
+
expect((result as any).isError).toBe(true)
|
|
1051
|
+
// Cleanup
|
|
1052
|
+
await client.callTool({ name: 'execute', arguments: { code: js`await state.errorTestPage.close(); delete state.errorTestPage;` } })
|
|
1053
|
+
}, 30000)
|
|
1054
|
+
|
|
1055
|
+
it('should show descriptive error when clicking an element covered by another', async () => {
|
|
1056
|
+
await client.callTool({
|
|
1057
|
+
name: 'execute',
|
|
1058
|
+
arguments: {
|
|
1059
|
+
code: js`
|
|
1060
|
+
state.errorTestPage = await context.newPage();
|
|
1061
|
+
await state.errorTestPage.setContent(\`
|
|
1062
|
+
<div style="position:relative">
|
|
1063
|
+
<button id="covered-btn" style="position:absolute;top:0;left:0">Covered</button>
|
|
1064
|
+
<div id="overlay" style="position:absolute;top:0;left:0;width:200px;height:200px;background:red;z-index:10">Overlay</div>
|
|
1065
|
+
</div>
|
|
1066
|
+
\`);
|
|
1067
|
+
`,
|
|
1068
|
+
},
|
|
1069
|
+
})
|
|
1070
|
+
const result = await client.callTool({
|
|
1071
|
+
name: 'execute',
|
|
1072
|
+
arguments: {
|
|
1073
|
+
code: js`
|
|
1074
|
+
await state.errorTestPage.click('#covered-btn');
|
|
1075
|
+
`,
|
|
1076
|
+
},
|
|
1077
|
+
})
|
|
1078
|
+
const text = (result as any).content[0].text
|
|
1079
|
+
const errorLine = text.split('\n').find((l: string) => l.includes('Timeout') || l.includes('intercepts'))
|
|
1080
|
+
expect(errorLine).toMatchInlineSnapshot(`"Error executing code: page.click: Timeout 2000ms exceeded. <div id="overlay">Overlay</div> intercepts pointer events"`)
|
|
1081
|
+
expect((result as any).isError).toBe(true)
|
|
1082
|
+
await client.callTool({ name: 'execute', arguments: { code: js`await state.errorTestPage.close(); delete state.errorTestPage;` } })
|
|
1083
|
+
}, 30000)
|
|
1084
|
+
|
|
1085
|
+
it('should show descriptive error when clicking a display:none element', async () => {
|
|
1086
|
+
await client.callTool({
|
|
1087
|
+
name: 'execute',
|
|
1088
|
+
arguments: {
|
|
1089
|
+
code: js`
|
|
1090
|
+
state.errorTestPage = await context.newPage();
|
|
1091
|
+
await state.errorTestPage.setContent('<button id="invisible" style="display:none">Invisible</button>');
|
|
1092
|
+
`,
|
|
1093
|
+
},
|
|
1094
|
+
})
|
|
1095
|
+
const result = await client.callTool({
|
|
1096
|
+
name: 'execute',
|
|
1097
|
+
arguments: {
|
|
1098
|
+
code: js`
|
|
1099
|
+
await state.errorTestPage.click('#invisible');
|
|
1100
|
+
`,
|
|
1101
|
+
},
|
|
1102
|
+
})
|
|
1103
|
+
const text = (result as any).content[0].text
|
|
1104
|
+
const errorLine = text.split('\n').find((l: string) => l.includes('Timeout') || l.includes('not visible'))
|
|
1105
|
+
expect(errorLine).toMatchInlineSnapshot(`"Error executing code: page.click: Timeout 2000ms exceeded. Element is not visible — it may be hidden by CSS, inside a collapsed <details>, inactive tab, or closed accordion. Try: interact with the page to reveal it first, or use { force: true } to skip visibility checks"`)
|
|
1106
|
+
expect((result as any).isError).toBe(true)
|
|
1107
|
+
await client.callTool({ name: 'execute', arguments: { code: js`await state.errorTestPage.close(); delete state.errorTestPage;` } })
|
|
1108
|
+
}, 30000)
|
|
928
1109
|
|
|
929
|
-
// Cleanup
|
|
930
|
-
await page2.close()
|
|
931
|
-
}, 60000)
|
|
932
1110
|
})
|