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
package/src/relay-core.test.ts
CHANGED
|
@@ -1,116 +1,304 @@
|
|
|
1
1
|
import { createMCPClient } from './mcp-client.js'
|
|
2
2
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
|
|
3
|
+
import { chromium } from '@xmorse/playwright-core'
|
|
3
4
|
import { getCDPSessionForPage } from './cdp-session.js'
|
|
4
|
-
import { getCdpUrl } from './utils.js'
|
|
5
|
-
import
|
|
5
|
+
import { getCdpUrl, LOG_CDP_FILE_PATH } from './utils.js'
|
|
6
|
+
import fs from 'node:fs'
|
|
7
|
+
import {
|
|
8
|
+
setupTestContext,
|
|
9
|
+
cleanupTestContext,
|
|
10
|
+
getExtensionServiceWorker,
|
|
11
|
+
type TestContext,
|
|
12
|
+
withTimeout,
|
|
13
|
+
js,
|
|
14
|
+
tryJsonParse,
|
|
15
|
+
createSimpleServer,
|
|
16
|
+
} from './test-utils.js'
|
|
6
17
|
import './test-declarations.js'
|
|
7
18
|
|
|
8
19
|
const TEST_PORT = 19987
|
|
9
20
|
|
|
10
21
|
describe('Relay Core Tests', () => {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
22
|
+
let client: Awaited<ReturnType<typeof createMCPClient>>['client']
|
|
23
|
+
let cleanup: (() => Promise<void>) | null = null
|
|
24
|
+
let testCtx: TestContext | null = null
|
|
25
|
+
|
|
26
|
+
beforeAll(async () => {
|
|
27
|
+
testCtx = await setupTestContext({ port: TEST_PORT, tempDirPrefix: 'pw-test-', toggleExtension: true })
|
|
28
|
+
|
|
29
|
+
const result = await createMCPClient({ port: TEST_PORT })
|
|
30
|
+
client = result.client
|
|
31
|
+
cleanup = result.cleanup
|
|
32
|
+
}, 600000)
|
|
33
|
+
|
|
34
|
+
afterAll(async () => {
|
|
35
|
+
await cleanupTestContext(testCtx, cleanup)
|
|
36
|
+
cleanup = null
|
|
37
|
+
testCtx = null
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const getBrowserContext = () => {
|
|
41
|
+
if (!testCtx?.browserContext) throw new Error('Browser not initialized')
|
|
42
|
+
return testCtx.browserContext
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const ensureConnectedTabForExecute = async (): Promise<void> => {
|
|
46
|
+
const browserContext = getBrowserContext()
|
|
47
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
48
|
+
const connectedTabCount = await serviceWorker.evaluate(async () => {
|
|
49
|
+
const state = globalThis.getExtensionState()
|
|
50
|
+
return state.tabs.size
|
|
51
|
+
})
|
|
52
|
+
if (connectedTabCount > 0) {
|
|
53
|
+
return
|
|
54
|
+
}
|
|
14
55
|
|
|
15
|
-
|
|
16
|
-
|
|
56
|
+
const page = await browserContext.newPage()
|
|
57
|
+
await page.goto('about:blank')
|
|
58
|
+
await page.bringToFront()
|
|
17
59
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}, 600000)
|
|
60
|
+
await serviceWorker.evaluate(async () => {
|
|
61
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
62
|
+
})
|
|
22
63
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
64
|
+
await new Promise((r) => {
|
|
65
|
+
setTimeout(r, 100)
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
it('should inject script via addScriptTag through CDP relay', async () => {
|
|
70
|
+
const browserContext = getBrowserContext()
|
|
71
|
+
const serviceWorker = await withTimeout({
|
|
72
|
+
promise: getExtensionServiceWorker(browserContext),
|
|
73
|
+
timeoutMs: 5000,
|
|
74
|
+
errorMessage: 'Timed out waiting for extension service worker for iframe test',
|
|
27
75
|
})
|
|
28
76
|
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
77
|
+
const page = await browserContext.newPage()
|
|
78
|
+
const html = '<html><body><button id="btn">Click</button></body></html>'
|
|
79
|
+
const dataUrl = `data:text/html,${encodeURIComponent(html)}`
|
|
80
|
+
await page.goto(dataUrl)
|
|
81
|
+
await page.bringToFront()
|
|
82
|
+
|
|
83
|
+
await withTimeout({
|
|
84
|
+
promise: serviceWorker.evaluate(async () => {
|
|
85
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
86
|
+
}),
|
|
87
|
+
timeoutMs: 10000,
|
|
88
|
+
errorMessage: 'Timed out toggling extension for active tab',
|
|
89
|
+
})
|
|
90
|
+
await new Promise((r) => {
|
|
91
|
+
setTimeout(r, 100)
|
|
92
|
+
})
|
|
33
93
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
errorMessage: 'Timed out waiting for extension service worker for iframe test',
|
|
40
|
-
})
|
|
94
|
+
const cdpSession = await withTimeout({
|
|
95
|
+
promise: getCDPSessionForPage({ page }),
|
|
96
|
+
timeoutMs: 10000,
|
|
97
|
+
errorMessage: 'Timed out creating CDP session for page',
|
|
98
|
+
})
|
|
41
99
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
await page.bringToFront()
|
|
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) })
|
|
100
|
+
const hasGlobalBefore = await page.evaluate(() => {
|
|
101
|
+
return Boolean((globalThis as { __testGlobal?: unknown }).__testGlobal)
|
|
102
|
+
})
|
|
103
|
+
expect(hasGlobalBefore).toBe(false)
|
|
56
104
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
105
|
+
await withTimeout({
|
|
106
|
+
promise: (async () => {
|
|
107
|
+
await cdpSession.send('Page.enable')
|
|
108
|
+
await cdpSession.send('Page.addScriptToEvaluateOnNewDocument', {
|
|
109
|
+
source: 'globalThis.__testGlobal = { foo: "bar" }',
|
|
61
110
|
})
|
|
111
|
+
await page.reload({ waitUntil: 'domcontentloaded' })
|
|
112
|
+
})(),
|
|
113
|
+
timeoutMs: 10000,
|
|
114
|
+
errorMessage: 'Timed out injecting script via CDP session',
|
|
115
|
+
})
|
|
62
116
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
117
|
+
const hasGlobalAfter = await page.evaluate(() => {
|
|
118
|
+
return (globalThis as { __testGlobal?: unknown }).__testGlobal
|
|
119
|
+
})
|
|
120
|
+
expect(hasGlobalAfter).toEqual({ foo: 'bar' })
|
|
121
|
+
|
|
122
|
+
await cdpSession.detach()
|
|
123
|
+
await page.close()
|
|
124
|
+
}, 60000)
|
|
125
|
+
|
|
126
|
+
it('should emit download events for both Browser and Page domains in extension mode', async () => {
|
|
127
|
+
const browserContext = getBrowserContext()
|
|
128
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
129
|
+
const logFilePath = LOG_CDP_FILE_PATH
|
|
130
|
+
const logLineCountBefore = fs.existsSync(logFilePath)
|
|
131
|
+
? fs
|
|
132
|
+
.readFileSync(logFilePath, 'utf-8')
|
|
133
|
+
.split('\n')
|
|
134
|
+
.filter((line) => {
|
|
135
|
+
return line.trim().length > 0
|
|
136
|
+
}).length
|
|
137
|
+
: 0
|
|
138
|
+
|
|
139
|
+
const server = await createSimpleServer({
|
|
140
|
+
routes: {
|
|
141
|
+
'/': `<!doctype html>
|
|
142
|
+
<html>
|
|
143
|
+
<body>
|
|
144
|
+
<button id="download-button">Download</button>
|
|
145
|
+
<script>
|
|
146
|
+
const button = document.getElementById('download-button');
|
|
147
|
+
button.addEventListener('click', () => {
|
|
148
|
+
const blob = new Blob(['playwriter-download-test'], { type: 'text/plain' });
|
|
149
|
+
const url = URL.createObjectURL(blob);
|
|
150
|
+
const anchor = document.createElement('a');
|
|
151
|
+
anchor.href = url;
|
|
152
|
+
anchor.download = 'playwriter-download-test.txt';
|
|
153
|
+
document.body.appendChild(anchor);
|
|
154
|
+
anchor.click();
|
|
155
|
+
anchor.remove();
|
|
156
|
+
setTimeout(() => {
|
|
157
|
+
URL.revokeObjectURL(url);
|
|
158
|
+
}, 1000);
|
|
159
|
+
});
|
|
160
|
+
</script>
|
|
161
|
+
</body>
|
|
162
|
+
</html>`,
|
|
163
|
+
},
|
|
164
|
+
})
|
|
79
165
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
expect(hasGlobalAfter).toEqual({ foo: 'bar' })
|
|
166
|
+
const page = await browserContext.newPage()
|
|
167
|
+
await page.goto(server.baseUrl, { waitUntil: 'domcontentloaded' })
|
|
168
|
+
await page.bringToFront()
|
|
84
169
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
170
|
+
await serviceWorker.evaluate(async () => {
|
|
171
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
172
|
+
})
|
|
88
173
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
174
|
+
const directBrowser = await withTimeout({
|
|
175
|
+
promise: chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT })),
|
|
176
|
+
timeoutMs: 10000,
|
|
177
|
+
errorMessage: 'Timed out connecting over CDP for download reproduction test',
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
const connectedPage = directBrowser
|
|
181
|
+
.contexts()[0]
|
|
182
|
+
.pages()
|
|
183
|
+
.find((candidatePage) => {
|
|
184
|
+
return candidatePage.url() === server.baseUrl + '/'
|
|
185
|
+
})
|
|
186
|
+
if (!connectedPage) {
|
|
187
|
+
throw new Error('Connected page not found for download reproduction test')
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const downloadResult = await Promise.all([
|
|
191
|
+
connectedPage.waitForEvent('download', { timeout: 3000 }).then(
|
|
192
|
+
(download) => {
|
|
193
|
+
return { timedOut: false, suggestedFilename: download.suggestedFilename() }
|
|
194
|
+
},
|
|
195
|
+
(error: Error) => {
|
|
196
|
+
return { timedOut: true, errorMessage: error.message }
|
|
197
|
+
},
|
|
198
|
+
),
|
|
199
|
+
connectedPage.click('#download-button'),
|
|
200
|
+
])
|
|
201
|
+
|
|
202
|
+
expect(downloadResult[0]).toMatchInlineSnapshot(`
|
|
203
|
+
{
|
|
204
|
+
"suggestedFilename": "playwriter-download-test.txt",
|
|
205
|
+
"timedOut": false,
|
|
206
|
+
}
|
|
207
|
+
`)
|
|
208
|
+
|
|
209
|
+
await directBrowser.close()
|
|
210
|
+
await page.close()
|
|
211
|
+
await server.close()
|
|
212
|
+
|
|
213
|
+
const logLinesAfter = fs
|
|
214
|
+
.readFileSync(logFilePath, 'utf-8')
|
|
215
|
+
.split('\n')
|
|
216
|
+
.filter((line) => {
|
|
217
|
+
return line.trim().length > 0
|
|
218
|
+
})
|
|
219
|
+
.slice(logLineCountBefore)
|
|
220
|
+
|
|
221
|
+
const newEntries = logLinesAfter
|
|
222
|
+
.map((line) => {
|
|
223
|
+
return tryJsonParse(line)
|
|
224
|
+
})
|
|
225
|
+
.filter((entry): entry is { direction: string; message: { method?: string } } => {
|
|
226
|
+
return Boolean(entry && typeof entry === 'object' && 'direction' in entry && 'message' in entry)
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
const methods = newEntries
|
|
230
|
+
.map((entry) => {
|
|
231
|
+
return {
|
|
232
|
+
direction: entry.direction,
|
|
233
|
+
method: typeof entry.message?.method === 'string' ? entry.message.method : 'response',
|
|
234
|
+
}
|
|
235
|
+
})
|
|
236
|
+
.filter((entry) => {
|
|
237
|
+
return (
|
|
238
|
+
entry.method.includes('download') ||
|
|
239
|
+
entry.method === 'Browser.setDownloadBehavior' ||
|
|
240
|
+
entry.method === 'Page.setDownloadBehavior'
|
|
241
|
+
)
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
const summary = {
|
|
245
|
+
hasBrowserSetDownloadBehavior: methods.some((entry) => {
|
|
246
|
+
return entry.direction === 'from-playwright' && entry.method === 'Browser.setDownloadBehavior'
|
|
247
|
+
}),
|
|
248
|
+
hasPageSetDownloadBehavior: methods.some((entry) => {
|
|
249
|
+
return entry.direction === 'to-extension' && entry.method === 'Page.setDownloadBehavior'
|
|
250
|
+
}),
|
|
251
|
+
hasPageDownloadWillBegin: methods.some((entry) => {
|
|
252
|
+
return entry.method === 'Page.downloadWillBegin'
|
|
253
|
+
}),
|
|
254
|
+
hasPageDownloadProgress: methods.some((entry) => {
|
|
255
|
+
return entry.method === 'Page.downloadProgress'
|
|
256
|
+
}),
|
|
257
|
+
hasBrowserDownloadWillBegin: methods.some((entry) => {
|
|
258
|
+
return entry.method === 'Browser.downloadWillBegin'
|
|
259
|
+
}),
|
|
260
|
+
hasBrowserDownloadProgress: methods.some((entry) => {
|
|
261
|
+
return entry.method === 'Browser.downloadProgress'
|
|
262
|
+
}),
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
expect(summary).toMatchInlineSnapshot(`
|
|
266
|
+
{
|
|
267
|
+
"hasBrowserDownloadProgress": false,
|
|
268
|
+
"hasBrowserDownloadWillBegin": false,
|
|
269
|
+
"hasBrowserSetDownloadBehavior": true,
|
|
270
|
+
"hasPageDownloadProgress": false,
|
|
271
|
+
"hasPageDownloadWillBegin": false,
|
|
272
|
+
"hasPageSetDownloadBehavior": true,
|
|
273
|
+
}
|
|
274
|
+
`)
|
|
275
|
+
}, 120000)
|
|
276
|
+
|
|
277
|
+
it('should execute code and capture console output', async () => {
|
|
278
|
+
await client.callTool({
|
|
279
|
+
name: 'execute',
|
|
280
|
+
arguments: {
|
|
281
|
+
code: js`
|
|
94
282
|
const newPage = await context.newPage();
|
|
95
283
|
state.page = newPage;
|
|
96
284
|
if (!state.pages) state.pages = [];
|
|
97
285
|
state.pages.push(newPage);
|
|
98
286
|
`,
|
|
99
|
-
|
|
100
|
-
|
|
287
|
+
},
|
|
288
|
+
})
|
|
101
289
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
290
|
+
const result = await client.callTool({
|
|
291
|
+
name: 'execute',
|
|
292
|
+
arguments: {
|
|
293
|
+
code: js`
|
|
106
294
|
await state.page.goto('https://example.com');
|
|
107
295
|
const title = await state.page.title();
|
|
108
296
|
console.log('Page title:', title);
|
|
109
297
|
return { url: state.page.url(), title };
|
|
110
298
|
`,
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
299
|
+
},
|
|
300
|
+
})
|
|
301
|
+
expect(result.content).toMatchInlineSnapshot(`
|
|
114
302
|
[
|
|
115
303
|
{
|
|
116
304
|
"text": "Console output:
|
|
@@ -121,138 +309,244 @@ describe('Relay Core Tests', () => {
|
|
|
121
309
|
},
|
|
122
310
|
]
|
|
123
311
|
`)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
312
|
+
expect(result.content).toBeDefined()
|
|
313
|
+
}, 30000)
|
|
314
|
+
|
|
315
|
+
// Repro test for https://github.com/remorses/playwriter/issues/66.
|
|
316
|
+
// Current limitation: extension-mode routing does not support root-session
|
|
317
|
+
// Storage.getCookies in playwriter. MUST use Network.getCookies via page CDP
|
|
318
|
+
// session instead (see test below), so this repro stays skipped.
|
|
319
|
+
it.skip('should reproduce page.route failure in MCP execute path (issue #66)', async () => {
|
|
320
|
+
const server = await createSimpleServer({
|
|
321
|
+
routes: {
|
|
322
|
+
'/': '<!doctype html><html><body>route issue repro</body></html>',
|
|
323
|
+
'/api/data': '{"ok":true}',
|
|
324
|
+
},
|
|
325
|
+
})
|
|
130
326
|
|
|
131
|
-
|
|
327
|
+
try {
|
|
328
|
+
const result = await client.callTool({
|
|
329
|
+
name: 'execute',
|
|
330
|
+
arguments: {
|
|
331
|
+
code: js`
|
|
332
|
+
const newPage = await context.newPage();
|
|
333
|
+
state.issue66Page = newPage;
|
|
334
|
+
await newPage.goto('${server.baseUrl}', { waitUntil: 'domcontentloaded' });
|
|
335
|
+
|
|
336
|
+
let routeFetchError = null;
|
|
337
|
+
await newPage.route('**/api/**', async (route) => {
|
|
338
|
+
try {
|
|
339
|
+
const response = await route.fetch();
|
|
340
|
+
await route.fulfill({ response });
|
|
341
|
+
} catch (error) {
|
|
342
|
+
routeFetchError = error instanceof Error ? error.message : String(error);
|
|
343
|
+
await route.abort();
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
await newPage.evaluate(async () => {
|
|
348
|
+
await fetch('/api/data').catch(() => null);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
return { routeFetchError };
|
|
352
|
+
`,
|
|
353
|
+
},
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
const resultWithContent = result as { content?: unknown }
|
|
357
|
+
const content = Array.isArray(resultWithContent.content) ? resultWithContent.content : []
|
|
358
|
+
const firstContent = content[0]
|
|
359
|
+
const output =
|
|
360
|
+
typeof firstContent === 'object' && firstContent !== null && 'text' in firstContent
|
|
361
|
+
? String((firstContent as { text?: unknown }).text ?? '')
|
|
362
|
+
: ''
|
|
363
|
+
expect(output).toContain('routeFetchError')
|
|
364
|
+
expect(output).toContain('Storage.getCookies')
|
|
365
|
+
expect(output).toContain('No tab found for method Storage.getCookies')
|
|
366
|
+
} finally {
|
|
367
|
+
try {
|
|
132
368
|
await client.callTool({
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
369
|
+
name: 'execute',
|
|
370
|
+
arguments: {
|
|
371
|
+
code: js`
|
|
372
|
+
if (state.issue66Page && !state.issue66Page.isClosed()) {
|
|
373
|
+
await state.issue66Page.close();
|
|
374
|
+
}
|
|
375
|
+
delete state.issue66Page;
|
|
376
|
+
`,
|
|
377
|
+
},
|
|
378
|
+
})
|
|
379
|
+
} catch {
|
|
380
|
+
// Ignore cleanup failure if MCP disconnected due to the repro.
|
|
381
|
+
}
|
|
382
|
+
await server.close()
|
|
383
|
+
}
|
|
384
|
+
}, 30000)
|
|
385
|
+
|
|
386
|
+
it('should read cookies via Network.getCookies through page CDP session', async () => {
|
|
387
|
+
const browserContext = getBrowserContext()
|
|
388
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
389
|
+
|
|
390
|
+
const server = await createSimpleServer({
|
|
391
|
+
routes: {
|
|
392
|
+
'/': '<!doctype html><html><body>cookies test</body></html>',
|
|
393
|
+
},
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
const page = await browserContext.newPage()
|
|
397
|
+
try {
|
|
398
|
+
await page.goto(server.baseUrl, { waitUntil: 'domcontentloaded' })
|
|
399
|
+
await page.bringToFront()
|
|
400
|
+
|
|
401
|
+
await serviceWorker.evaluate(async () => {
|
|
402
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
await new Promise((r) => {
|
|
406
|
+
setTimeout(r, 200)
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
await page.evaluate(() => {
|
|
410
|
+
document.cookie = 'issue66=ok; path=/'
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
const cdpSession = await getCDPSessionForPage({ page })
|
|
414
|
+
const cookiesResult = await cdpSession.send('Network.getCookies', { urls: [page.url()] })
|
|
415
|
+
const cookie = cookiesResult.cookies.find((value) => {
|
|
416
|
+
return value.name === 'issue66'
|
|
417
|
+
})
|
|
418
|
+
expect(cookie?.value).toBe('ok')
|
|
419
|
+
} finally {
|
|
420
|
+
await page.close()
|
|
421
|
+
await server.close()
|
|
422
|
+
}
|
|
423
|
+
}, 30000)
|
|
424
|
+
|
|
425
|
+
it('should show extension as connected for pages created via newPage()', async () => {
|
|
426
|
+
const browserContext = getBrowserContext()
|
|
427
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
428
|
+
|
|
429
|
+
// Create a page via MCP (which uses context.newPage())
|
|
430
|
+
await client.callTool({
|
|
431
|
+
name: 'execute',
|
|
432
|
+
arguments: {
|
|
433
|
+
code: js`
|
|
136
434
|
const newPage = await context.newPage();
|
|
137
435
|
state.testPage = newPage;
|
|
138
436
|
await newPage.goto('https://example.com/mcp-test');
|
|
139
437
|
return newPage.url();
|
|
140
438
|
`,
|
|
141
|
-
|
|
142
|
-
|
|
439
|
+
},
|
|
440
|
+
})
|
|
143
441
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
442
|
+
// Get extension state to verify the page is marked as connected
|
|
443
|
+
const extensionState = await serviceWorker.evaluate(async () => {
|
|
444
|
+
const state = globalThis.getExtensionState()
|
|
445
|
+
const tabs = await chrome.tabs.query({})
|
|
446
|
+
const testTab = tabs.find((t: any) => t.url?.includes('mcp-test'))
|
|
447
|
+
return {
|
|
448
|
+
connected: !!testTab && !!testTab.id && state.tabs.has(testTab.id),
|
|
449
|
+
tabId: testTab?.id,
|
|
450
|
+
tabInfo: testTab?.id ? state.tabs.get(testTab.id) : null,
|
|
451
|
+
connectionState: state.connectionState,
|
|
452
|
+
}
|
|
453
|
+
})
|
|
156
454
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
455
|
+
expect(extensionState.connected).toBe(true)
|
|
456
|
+
expect(extensionState.tabInfo?.state).toBe('connected')
|
|
457
|
+
expect(extensionState.connectionState).toBe('connected')
|
|
160
458
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
459
|
+
// Clean up
|
|
460
|
+
await client.callTool({
|
|
461
|
+
name: 'execute',
|
|
462
|
+
arguments: {
|
|
463
|
+
code: js`
|
|
166
464
|
if (state.testPage) {
|
|
167
465
|
await state.testPage.close();
|
|
168
466
|
delete state.testPage;
|
|
169
467
|
}
|
|
170
468
|
`,
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
469
|
+
},
|
|
470
|
+
})
|
|
471
|
+
}, 30000)
|
|
472
|
+
|
|
473
|
+
const snapshotTestCases = [
|
|
474
|
+
{
|
|
475
|
+
name: 'hacker-news',
|
|
476
|
+
url: 'https://news.ycombinator.com/item?id=1',
|
|
477
|
+
expectedContent: ['role=link', 'Hacker News'],
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
name: 'shadcn-ui',
|
|
481
|
+
url: 'https://ui.shadcn.com/',
|
|
482
|
+
expectedContent: ['shadcn'],
|
|
483
|
+
},
|
|
484
|
+
]
|
|
485
|
+
|
|
486
|
+
for (const testCase of snapshotTestCases) {
|
|
487
|
+
it(`should get accessibility snapshot of ${testCase.name}`, async () => {
|
|
488
|
+
await client.callTool({
|
|
489
|
+
name: 'execute',
|
|
490
|
+
arguments: {
|
|
491
|
+
code: js`
|
|
194
492
|
const newPage = await context.newPage();
|
|
195
493
|
state.page = newPage;
|
|
196
494
|
if (!state.pages) state.pages = [];
|
|
197
495
|
state.pages.push(newPage);
|
|
198
496
|
`,
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
497
|
+
},
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
// Capture interactiveOnly=true snapshot (default)
|
|
501
|
+
const interactiveResult = await client.callTool({
|
|
502
|
+
name: 'execute',
|
|
503
|
+
arguments: {
|
|
504
|
+
code: js`
|
|
207
505
|
await state.page.goto('${testCase.url}', { waitUntil: 'domcontentloaded' });
|
|
208
|
-
const
|
|
209
|
-
return
|
|
506
|
+
const snap = await snapshot({ page: state.page, showDiffSinceLastCall: false, interactiveOnly: true });
|
|
507
|
+
return snap;
|
|
210
508
|
`,
|
|
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;
|
|
509
|
+
},
|
|
510
|
+
})
|
|
511
|
+
|
|
512
|
+
const interactiveData =
|
|
513
|
+
typeof interactiveResult === 'object' && interactiveResult.content?.[0]?.text
|
|
514
|
+
? tryJsonParse(interactiveResult.content[0].text)
|
|
515
|
+
: interactiveResult
|
|
516
|
+
await expect(interactiveData).toMatchFileSnapshot(`snapshots/${testCase.name}-accessibility-interactive.md`)
|
|
517
|
+
expect(interactiveResult.content).toBeDefined()
|
|
518
|
+
for (const expected of testCase.expectedContent) {
|
|
519
|
+
expect(interactiveData).toContain(expected)
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Capture interactiveOnly=false snapshot (full tree)
|
|
523
|
+
const fullResult = await client.callTool({
|
|
524
|
+
name: 'execute',
|
|
525
|
+
arguments: {
|
|
526
|
+
code: js`
|
|
527
|
+
const snap = await snapshot({ page: state.page, showDiffSinceLastCall: false, interactiveOnly: false });
|
|
528
|
+
return snap;
|
|
233
529
|
`,
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}, 60000)
|
|
249
|
-
}
|
|
530
|
+
},
|
|
531
|
+
})
|
|
532
|
+
|
|
533
|
+
const fullData =
|
|
534
|
+
typeof fullResult === 'object' && fullResult.content?.[0]?.text
|
|
535
|
+
? tryJsonParse(fullResult.content[0].text)
|
|
536
|
+
: fullResult
|
|
537
|
+
await expect(fullData).toMatchFileSnapshot(`snapshots/${testCase.name}-accessibility-full.md`)
|
|
538
|
+
expect(fullResult.content).toBeDefined()
|
|
539
|
+
for (const expected of testCase.expectedContent) {
|
|
540
|
+
expect(fullData).toContain(expected)
|
|
541
|
+
}
|
|
542
|
+
}, 60000)
|
|
543
|
+
}
|
|
250
544
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
545
|
+
it('should close all created pages', async () => {
|
|
546
|
+
const result = await client.callTool({
|
|
547
|
+
name: 'execute',
|
|
548
|
+
arguments: {
|
|
549
|
+
code: js`
|
|
256
550
|
if (state.pages && state.pages.length > 0) {
|
|
257
551
|
for (const page of state.pages) {
|
|
258
552
|
await page.close();
|
|
@@ -263,17 +557,16 @@ describe('Relay Core Tests', () => {
|
|
|
263
557
|
}
|
|
264
558
|
return { closedCount: 0 };
|
|
265
559
|
`,
|
|
266
|
-
|
|
267
|
-
})
|
|
268
|
-
|
|
560
|
+
},
|
|
269
561
|
})
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
562
|
+
})
|
|
563
|
+
|
|
564
|
+
it('should capture browser console logs with getLatestLogs', async () => {
|
|
565
|
+
// Ensure clean state and clear any existing logs
|
|
566
|
+
const resetResult = await client.callTool({
|
|
567
|
+
name: 'execute',
|
|
568
|
+
arguments: {
|
|
569
|
+
code: js`
|
|
277
570
|
// Clear any existing logs from previous tests
|
|
278
571
|
clearAllLogs();
|
|
279
572
|
console.log('Cleared all existing logs');
|
|
@@ -284,27 +577,27 @@ describe('Relay Core Tests', () => {
|
|
|
284
577
|
|
|
285
578
|
return { success: true, pagesCount: pages.length };
|
|
286
579
|
`,
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
580
|
+
},
|
|
581
|
+
})
|
|
582
|
+
console.log('Cleanup result:', resetResult)
|
|
290
583
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
584
|
+
// Create a new page for this test
|
|
585
|
+
await client.callTool({
|
|
586
|
+
name: 'execute',
|
|
587
|
+
arguments: {
|
|
588
|
+
code: js`
|
|
296
589
|
const newPage = await context.newPage();
|
|
297
590
|
state.testLogPage = newPage;
|
|
298
591
|
await newPage.goto('about:blank');
|
|
299
592
|
`,
|
|
300
|
-
|
|
301
|
-
|
|
593
|
+
},
|
|
594
|
+
})
|
|
302
595
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
596
|
+
// Generate some console logs in the browser
|
|
597
|
+
await client.callTool({
|
|
598
|
+
name: 'execute',
|
|
599
|
+
arguments: {
|
|
600
|
+
code: js`
|
|
308
601
|
await state.testLogPage.evaluate(() => {
|
|
309
602
|
console.log('Test log 12345');
|
|
310
603
|
console.error('Test error 67890');
|
|
@@ -314,325 +607,403 @@ describe('Relay Core Tests', () => {
|
|
|
314
607
|
// Wait for logs to be captured
|
|
315
608
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
316
609
|
`,
|
|
317
|
-
|
|
318
|
-
|
|
610
|
+
},
|
|
611
|
+
})
|
|
319
612
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
613
|
+
// Test getting all logs
|
|
614
|
+
const allLogsResult = await client.callTool({
|
|
615
|
+
name: 'execute',
|
|
616
|
+
arguments: {
|
|
617
|
+
code: js`
|
|
325
618
|
const logs = await getLatestLogs();
|
|
326
619
|
logs.forEach(log => console.log(log));
|
|
327
620
|
`,
|
|
328
|
-
|
|
329
|
-
|
|
621
|
+
},
|
|
622
|
+
})
|
|
330
623
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
624
|
+
const output = (allLogsResult as any).content[0].text
|
|
625
|
+
expect(output).toContain('[log] Test log 12345')
|
|
626
|
+
expect(output).toContain('[error] Test error 67890')
|
|
627
|
+
expect(output).toContain('[warning] Test warning 11111')
|
|
335
628
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
629
|
+
// Test filtering by search string
|
|
630
|
+
const errorLogsResult = await client.callTool({
|
|
631
|
+
name: 'execute',
|
|
632
|
+
arguments: {
|
|
633
|
+
code: js`
|
|
341
634
|
const logs = await getLatestLogs({ search: 'error' });
|
|
342
635
|
logs.forEach(log => console.log(log));
|
|
343
636
|
`,
|
|
344
|
-
|
|
345
|
-
|
|
637
|
+
},
|
|
638
|
+
})
|
|
346
639
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
640
|
+
const errorOutput = (errorLogsResult as any).content[0].text
|
|
641
|
+
expect(errorOutput).toContain('[error] Test error 67890')
|
|
642
|
+
// With context lines (5 above/below), nearby logs are also included
|
|
643
|
+
expect(errorOutput).toContain('[log] Test log 12345')
|
|
351
644
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
645
|
+
// Test that logs are cleared on page reload
|
|
646
|
+
await client.callTool({
|
|
647
|
+
name: 'execute',
|
|
648
|
+
arguments: {
|
|
649
|
+
code: js`
|
|
357
650
|
// First add a log before reload
|
|
358
651
|
await state.testLogPage.evaluate(() => {
|
|
359
652
|
console.log('Before reload 99999');
|
|
360
653
|
});
|
|
361
654
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
362
655
|
`,
|
|
363
|
-
|
|
364
|
-
|
|
656
|
+
},
|
|
657
|
+
})
|
|
365
658
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
659
|
+
// Verify the log exists
|
|
660
|
+
const beforeReloadResult = await client.callTool({
|
|
661
|
+
name: 'execute',
|
|
662
|
+
arguments: {
|
|
663
|
+
code: js`
|
|
371
664
|
const logs = await getLatestLogs({ page: state.testLogPage });
|
|
372
665
|
console.log('Logs before reload:', logs.length);
|
|
373
666
|
logs.forEach(log => console.log(log));
|
|
374
667
|
`,
|
|
375
|
-
|
|
376
|
-
|
|
668
|
+
},
|
|
669
|
+
})
|
|
377
670
|
|
|
378
|
-
|
|
379
|
-
|
|
671
|
+
const beforeReloadOutput = (beforeReloadResult as any).content[0].text
|
|
672
|
+
expect(beforeReloadOutput).toContain('[log] Before reload 99999')
|
|
380
673
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
674
|
+
// Reload the page
|
|
675
|
+
await client.callTool({
|
|
676
|
+
name: 'execute',
|
|
677
|
+
arguments: {
|
|
678
|
+
code: js`
|
|
386
679
|
await state.testLogPage.reload();
|
|
387
680
|
await state.testLogPage.evaluate(() => {
|
|
388
681
|
console.log('After reload 88888');
|
|
389
682
|
});
|
|
390
683
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
391
684
|
`,
|
|
392
|
-
|
|
393
|
-
|
|
685
|
+
},
|
|
686
|
+
})
|
|
394
687
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
688
|
+
// Check logs after reload - old logs should be gone
|
|
689
|
+
const afterReloadResult = await client.callTool({
|
|
690
|
+
name: 'execute',
|
|
691
|
+
arguments: {
|
|
692
|
+
code: js`
|
|
400
693
|
const logs = await getLatestLogs({ page: state.testLogPage });
|
|
401
694
|
console.log('Logs after reload:', logs.length);
|
|
402
695
|
logs.forEach(log => console.log(log));
|
|
403
696
|
`,
|
|
404
|
-
|
|
405
|
-
|
|
697
|
+
},
|
|
698
|
+
})
|
|
406
699
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
700
|
+
const afterReloadOutput = (afterReloadResult as any).content[0].text
|
|
701
|
+
expect(afterReloadOutput).toContain('[log] After reload 88888')
|
|
702
|
+
expect(afterReloadOutput).not.toContain('[log] Before reload 99999')
|
|
410
703
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
704
|
+
// Clean up
|
|
705
|
+
await client.callTool({
|
|
706
|
+
name: 'execute',
|
|
707
|
+
arguments: {
|
|
708
|
+
code: js`
|
|
416
709
|
await state.testLogPage.close();
|
|
417
710
|
delete state.testLogPage;
|
|
418
711
|
`,
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
712
|
+
},
|
|
713
|
+
})
|
|
714
|
+
}, 30000)
|
|
715
|
+
|
|
716
|
+
it('should keep logs separate between different pages', async () => {
|
|
717
|
+
// Clear any existing logs from previous tests
|
|
718
|
+
await client.callTool({
|
|
719
|
+
name: 'execute',
|
|
720
|
+
arguments: {
|
|
721
|
+
code: js`
|
|
429
722
|
clearAllLogs();
|
|
430
723
|
console.log('Cleared all existing logs for second log test');
|
|
431
724
|
`,
|
|
432
|
-
|
|
433
|
-
|
|
725
|
+
},
|
|
726
|
+
})
|
|
434
727
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
728
|
+
// Create two pages
|
|
729
|
+
await client.callTool({
|
|
730
|
+
name: 'execute',
|
|
731
|
+
arguments: {
|
|
732
|
+
code: js`
|
|
440
733
|
state.pageA = await context.newPage();
|
|
441
734
|
state.pageB = await context.newPage();
|
|
442
735
|
await state.pageA.goto('about:blank');
|
|
443
736
|
await state.pageB.goto('about:blank');
|
|
444
737
|
`,
|
|
445
|
-
|
|
446
|
-
|
|
738
|
+
},
|
|
739
|
+
})
|
|
447
740
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
741
|
+
// Generate logs in page A
|
|
742
|
+
await client.callTool({
|
|
743
|
+
name: 'execute',
|
|
744
|
+
arguments: {
|
|
745
|
+
code: js`
|
|
453
746
|
await state.pageA.evaluate(() => {
|
|
454
747
|
console.log('PageA log 11111');
|
|
455
748
|
console.error('PageA error 22222');
|
|
456
749
|
});
|
|
457
750
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
458
751
|
`,
|
|
459
|
-
|
|
460
|
-
|
|
752
|
+
},
|
|
753
|
+
})
|
|
461
754
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
755
|
+
// Generate logs in page B
|
|
756
|
+
await client.callTool({
|
|
757
|
+
name: 'execute',
|
|
758
|
+
arguments: {
|
|
759
|
+
code: js`
|
|
467
760
|
await state.pageB.evaluate(() => {
|
|
468
761
|
console.log('PageB log 33333');
|
|
469
762
|
console.error('PageB error 44444');
|
|
470
763
|
});
|
|
471
764
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
472
765
|
`,
|
|
473
|
-
|
|
474
|
-
|
|
766
|
+
},
|
|
767
|
+
})
|
|
475
768
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
769
|
+
// Check logs for page A - should only have page A logs
|
|
770
|
+
const pageALogsResult = await client.callTool({
|
|
771
|
+
name: 'execute',
|
|
772
|
+
arguments: {
|
|
773
|
+
code: js`
|
|
481
774
|
const logs = await getLatestLogs({ page: state.pageA });
|
|
482
775
|
console.log('Page A logs:', logs.length);
|
|
483
776
|
logs.forEach(log => console.log(log));
|
|
484
777
|
`,
|
|
485
|
-
|
|
486
|
-
|
|
778
|
+
},
|
|
779
|
+
})
|
|
487
780
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
781
|
+
const pageAOutput = (pageALogsResult as any).content[0].text
|
|
782
|
+
expect(pageAOutput).toContain('[log] PageA log 11111')
|
|
783
|
+
expect(pageAOutput).toContain('[error] PageA error 22222')
|
|
784
|
+
expect(pageAOutput).not.toContain('PageB')
|
|
492
785
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
786
|
+
// Check logs for page B - should only have page B logs
|
|
787
|
+
const pageBLogsResult = await client.callTool({
|
|
788
|
+
name: 'execute',
|
|
789
|
+
arguments: {
|
|
790
|
+
code: js`
|
|
498
791
|
const logs = await getLatestLogs({ page: state.pageB });
|
|
499
792
|
console.log('Page B logs:', logs.length);
|
|
500
793
|
logs.forEach(log => console.log(log));
|
|
501
794
|
`,
|
|
502
|
-
|
|
503
|
-
|
|
795
|
+
},
|
|
796
|
+
})
|
|
504
797
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
798
|
+
const pageBOutput = (pageBLogsResult as any).content[0].text
|
|
799
|
+
expect(pageBOutput).toContain('[log] PageB log 33333')
|
|
800
|
+
expect(pageBOutput).toContain('[error] PageB error 44444')
|
|
801
|
+
expect(pageBOutput).not.toContain('PageA')
|
|
509
802
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
803
|
+
// Check all logs - should have logs from both pages
|
|
804
|
+
const allLogsResult = await client.callTool({
|
|
805
|
+
name: 'execute',
|
|
806
|
+
arguments: {
|
|
807
|
+
code: js`
|
|
515
808
|
const logs = await getLatestLogs();
|
|
516
809
|
console.log('All logs:', logs.length);
|
|
517
810
|
logs.forEach(log => console.log(log));
|
|
518
811
|
`,
|
|
519
|
-
|
|
520
|
-
|
|
812
|
+
},
|
|
813
|
+
})
|
|
521
814
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
815
|
+
const allOutput = (allLogsResult as any).content[0].text
|
|
816
|
+
expect(allOutput).toContain('[log] PageA log 11111')
|
|
817
|
+
expect(allOutput).toContain('[log] PageB log 33333')
|
|
525
818
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
819
|
+
// Test that reloading page A clears only page A logs
|
|
820
|
+
await client.callTool({
|
|
821
|
+
name: 'execute',
|
|
822
|
+
arguments: {
|
|
823
|
+
code: js`
|
|
531
824
|
await state.pageA.reload();
|
|
532
825
|
await state.pageA.evaluate(() => {
|
|
533
826
|
console.log('PageA after reload 55555');
|
|
534
827
|
});
|
|
535
828
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
536
829
|
`,
|
|
537
|
-
|
|
538
|
-
|
|
830
|
+
},
|
|
831
|
+
})
|
|
539
832
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
833
|
+
// Check page A logs - should only have new log
|
|
834
|
+
const pageAAfterReloadResult = await client.callTool({
|
|
835
|
+
name: 'execute',
|
|
836
|
+
arguments: {
|
|
837
|
+
code: js`
|
|
545
838
|
const logs = await getLatestLogs({ page: state.pageA });
|
|
546
839
|
console.log('Page A logs after reload:', logs.length);
|
|
547
840
|
logs.forEach(log => console.log(log));
|
|
548
841
|
`,
|
|
549
|
-
|
|
550
|
-
|
|
842
|
+
},
|
|
843
|
+
})
|
|
551
844
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
845
|
+
const pageAAfterReloadOutput = (pageAAfterReloadResult as any).content[0].text
|
|
846
|
+
expect(pageAAfterReloadOutput).toContain('[log] PageA after reload 55555')
|
|
847
|
+
expect(pageAAfterReloadOutput).not.toContain('[log] PageA log 11111')
|
|
555
848
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
849
|
+
// Check page B logs - should still have original logs
|
|
850
|
+
const pageBAfterAReloadResult = await client.callTool({
|
|
851
|
+
name: 'execute',
|
|
852
|
+
arguments: {
|
|
853
|
+
code: js`
|
|
561
854
|
const logs = await getLatestLogs({ page: state.pageB });
|
|
562
855
|
console.log('Page B logs after A reload:', logs.length);
|
|
563
856
|
logs.forEach(log => console.log(log));
|
|
564
857
|
`,
|
|
565
|
-
|
|
566
|
-
|
|
858
|
+
},
|
|
859
|
+
})
|
|
567
860
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
861
|
+
const pageBAfterAReloadOutput = (pageBAfterAReloadResult as any).content[0].text
|
|
862
|
+
expect(pageBAfterAReloadOutput).toContain('[log] PageB log 33333')
|
|
863
|
+
expect(pageBAfterAReloadOutput).toContain('[error] PageB error 44444')
|
|
571
864
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
865
|
+
// Test that logs are deleted when page is closed
|
|
866
|
+
await client.callTool({
|
|
867
|
+
name: 'execute',
|
|
868
|
+
arguments: {
|
|
869
|
+
code: js`
|
|
577
870
|
// Close page A
|
|
578
871
|
await state.pageA.close();
|
|
579
872
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
580
873
|
`,
|
|
581
|
-
|
|
582
|
-
|
|
874
|
+
},
|
|
875
|
+
})
|
|
583
876
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
877
|
+
// Check all logs - page A logs should be gone
|
|
878
|
+
const logsAfterCloseResult = await client.callTool({
|
|
879
|
+
name: 'execute',
|
|
880
|
+
arguments: {
|
|
881
|
+
code: js`
|
|
589
882
|
const logs = await getLatestLogs();
|
|
590
883
|
console.log('All logs after closing page A:', logs.length);
|
|
591
884
|
logs.forEach(log => console.log(log));
|
|
592
885
|
`,
|
|
593
|
-
|
|
594
|
-
|
|
886
|
+
},
|
|
887
|
+
})
|
|
595
888
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
889
|
+
const logsAfterCloseOutput = (logsAfterCloseResult as any).content[0].text
|
|
890
|
+
expect(logsAfterCloseOutput).not.toContain('PageA')
|
|
891
|
+
expect(logsAfterCloseOutput).toContain('[log] PageB log 33333')
|
|
599
892
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
893
|
+
// Clean up remaining page
|
|
894
|
+
await client.callTool({
|
|
895
|
+
name: 'execute',
|
|
896
|
+
arguments: {
|
|
897
|
+
code: js`
|
|
605
898
|
await state.pageB.close();
|
|
606
899
|
delete state.pageA;
|
|
607
900
|
delete state.pageB;
|
|
608
901
|
`,
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
902
|
+
},
|
|
903
|
+
})
|
|
904
|
+
}, 30000)
|
|
905
|
+
|
|
906
|
+
it('should capture console logs from cross-origin iframes', async () => {
|
|
907
|
+
// Two servers on different ports = different origins
|
|
908
|
+
const iframeServer = await createSimpleServer({
|
|
909
|
+
routes: {
|
|
910
|
+
'/iframe.html': `<!doctype html><html><body>
|
|
911
|
+
<script>
|
|
912
|
+
console.log('iframe-log-ALPHA');
|
|
913
|
+
console.error('iframe-error-BETA');
|
|
914
|
+
console.warn('iframe-warn-GAMMA');
|
|
915
|
+
</script>
|
|
916
|
+
<p>cross-origin iframe</p>
|
|
917
|
+
</body></html>`,
|
|
918
|
+
},
|
|
919
|
+
})
|
|
626
920
|
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
921
|
+
const parentServer = await createSimpleServer({
|
|
922
|
+
routes: {
|
|
923
|
+
'/': `<!doctype html><html><body>
|
|
924
|
+
<script>console.log('parent-log-DELTA');</script>
|
|
925
|
+
<iframe src="${iframeServer.baseUrl}/iframe.html"></iframe>
|
|
926
|
+
</body></html>`,
|
|
927
|
+
},
|
|
928
|
+
})
|
|
631
929
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
930
|
+
try {
|
|
931
|
+
// Clear logs and navigate to the parent page with cross-origin iframe
|
|
932
|
+
await client.callTool({
|
|
933
|
+
name: 'execute',
|
|
934
|
+
arguments: {
|
|
935
|
+
code: js`
|
|
936
|
+
clearAllLogs();
|
|
937
|
+
state.iframePage = await context.newPage();
|
|
938
|
+
await state.iframePage.goto('${parentServer.baseUrl}', { waitUntil: 'networkidle' });
|
|
939
|
+
// Wait for iframe to load and logs to be captured
|
|
940
|
+
await state.iframePage.frameLocator('iframe').locator('p').waitFor({ timeout: 5000 });
|
|
941
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
942
|
+
`,
|
|
943
|
+
},
|
|
944
|
+
})
|
|
945
|
+
|
|
946
|
+
// Retrieve logs and verify both parent and iframe logs are captured
|
|
947
|
+
const logsResult = await client.callTool({
|
|
948
|
+
name: 'execute',
|
|
949
|
+
arguments: {
|
|
950
|
+
code: js`
|
|
951
|
+
const logs = await getLatestLogs({ page: state.iframePage });
|
|
952
|
+
console.log('Cross-origin iframe logs count:', logs.length);
|
|
953
|
+
logs.forEach(log => console.log(log));
|
|
954
|
+
`,
|
|
955
|
+
},
|
|
956
|
+
})
|
|
957
|
+
|
|
958
|
+
const output = (logsResult as any).content[0].text
|
|
959
|
+
// Parent page log
|
|
960
|
+
expect(output).toContain('parent-log-DELTA')
|
|
961
|
+
// Cross-origin iframe logs
|
|
962
|
+
expect(output).toContain('iframe-log-ALPHA')
|
|
963
|
+
expect(output).toContain('iframe-error-BETA')
|
|
964
|
+
expect(output).toContain('iframe-warn-GAMMA')
|
|
965
|
+
|
|
966
|
+
// Clean up
|
|
967
|
+
await client.callTool({
|
|
968
|
+
name: 'execute',
|
|
969
|
+
arguments: {
|
|
970
|
+
code: js`
|
|
971
|
+
await state.iframePage.close();
|
|
972
|
+
delete state.iframePage;
|
|
973
|
+
`,
|
|
974
|
+
},
|
|
975
|
+
})
|
|
976
|
+
} finally {
|
|
977
|
+
await Promise.all([parentServer.close(), iframeServer.close()])
|
|
978
|
+
}
|
|
979
|
+
}, 60000)
|
|
980
|
+
|
|
981
|
+
it(
|
|
982
|
+
'should preserve system color scheme instead of forcing light mode',
|
|
983
|
+
async () => {
|
|
984
|
+
const browserContext = getBrowserContext()
|
|
985
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
986
|
+
|
|
987
|
+
const page = await browserContext.newPage()
|
|
988
|
+
await page.goto('https://example.com')
|
|
989
|
+
await page.bringToFront()
|
|
990
|
+
|
|
991
|
+
// test-utils launches with colorScheme: 'dark', so before MCP connection
|
|
992
|
+
// the browser should report dark mode
|
|
993
|
+
const colorSchemeBefore = await page.evaluate(() => {
|
|
994
|
+
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
|
995
|
+
})
|
|
996
|
+
expect(colorSchemeBefore).toBe('dark')
|
|
997
|
+
|
|
998
|
+
await serviceWorker.evaluate(async () => {
|
|
999
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
1000
|
+
})
|
|
1001
|
+
await new Promise((r) => setTimeout(r, 500))
|
|
1002
|
+
|
|
1003
|
+
const result = await client.callTool({
|
|
1004
|
+
name: 'execute',
|
|
1005
|
+
arguments: {
|
|
1006
|
+
code: js`
|
|
636
1007
|
const pages = context.pages();
|
|
637
1008
|
const urls = pages.map(p => p.url());
|
|
638
1009
|
const targetPage = pages.find(p => p.url().includes('example.com'));
|
|
@@ -643,29 +1014,34 @@ describe('Relay Core Tests', () => {
|
|
|
643
1014
|
const isLight = await targetPage.evaluate(() => window.matchMedia('(prefers-color-scheme: light)').matches);
|
|
644
1015
|
return { matchesDark: isDark, matchesLight: isLight };
|
|
645
1016
|
`,
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
1017
|
+
},
|
|
1018
|
+
})
|
|
1019
|
+
|
|
1020
|
+
console.log('Color scheme after MCP connection:', result.content)
|
|
1021
|
+
|
|
1022
|
+
// After MCP connection, color scheme should NOT be forced to light.
|
|
1023
|
+
// The page.ts default is now 'no-override', so the browser's actual
|
|
1024
|
+
// color scheme (dark, from test-utils launch config) should be preserved.
|
|
1025
|
+
expect(result.content).toMatchInlineSnapshot(`
|
|
1026
|
+
[
|
|
1027
|
+
{
|
|
1028
|
+
"text": "[return value] { matchesDark: true, matchesLight: false }",
|
|
1029
|
+
"type": "text",
|
|
1030
|
+
},
|
|
1031
|
+
]
|
|
1032
|
+
`)
|
|
1033
|
+
|
|
1034
|
+
await page.close()
|
|
1035
|
+
},
|
|
1036
|
+
60000,
|
|
1037
|
+
)
|
|
1038
|
+
|
|
1039
|
+
it('should get clean HTML with getCleanHTML', async () => {
|
|
1040
|
+
const browserContext = getBrowserContext()
|
|
1041
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
1042
|
+
|
|
1043
|
+
const page = await browserContext.newPage()
|
|
1044
|
+
await page.setContent(`
|
|
669
1045
|
<html>
|
|
670
1046
|
<head>
|
|
671
1047
|
<style>.hidden { display: none; }</style>
|
|
@@ -681,18 +1057,18 @@ describe('Relay Core Tests', () => {
|
|
|
681
1057
|
</body>
|
|
682
1058
|
</html>
|
|
683
1059
|
`)
|
|
684
|
-
|
|
1060
|
+
await page.bringToFront()
|
|
685
1061
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
1062
|
+
await serviceWorker.evaluate(async () => {
|
|
1063
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
1064
|
+
})
|
|
1065
|
+
await new Promise((r) => setTimeout(r, 400))
|
|
690
1066
|
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
1067
|
+
// Test basic getCleanHTML
|
|
1068
|
+
const result = await client.callTool({
|
|
1069
|
+
name: 'execute',
|
|
1070
|
+
arguments: {
|
|
1071
|
+
code: js`
|
|
696
1072
|
let testPage;
|
|
697
1073
|
for (const p of context.pages()) {
|
|
698
1074
|
const html = await p.content();
|
|
@@ -702,15 +1078,15 @@ describe('Relay Core Tests', () => {
|
|
|
702
1078
|
const html = await getCleanHTML({ locator: testPage.locator('body') });
|
|
703
1079
|
return html;
|
|
704
1080
|
`,
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
1081
|
+
timeout: 15000,
|
|
1082
|
+
},
|
|
1083
|
+
})
|
|
708
1084
|
|
|
709
|
-
|
|
710
|
-
|
|
1085
|
+
expect(result.isError).toBeFalsy()
|
|
1086
|
+
const text = (result.content as any)[0]?.text || ''
|
|
711
1087
|
|
|
712
|
-
|
|
713
|
-
|
|
1088
|
+
// Inline snapshot of cleaned HTML
|
|
1089
|
+
expect(text).toMatchInlineSnapshot(`
|
|
714
1090
|
"[return value] <div data-testid="main">
|
|
715
1091
|
<h1>Hello World</h1>
|
|
716
1092
|
<button aria-label="Click me">Submit</button>
|
|
@@ -719,16 +1095,16 @@ describe('Relay Core Tests', () => {
|
|
|
719
1095
|
</div>"
|
|
720
1096
|
`)
|
|
721
1097
|
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
1098
|
+
// Should NOT contain script/style tags (they're removed)
|
|
1099
|
+
expect(text).not.toContain('<script')
|
|
1100
|
+
expect(text).not.toContain('<style')
|
|
1101
|
+
expect(text).not.toContain('console.log')
|
|
726
1102
|
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
1103
|
+
// Test search functionality
|
|
1104
|
+
const searchResult = await client.callTool({
|
|
1105
|
+
name: 'execute',
|
|
1106
|
+
arguments: {
|
|
1107
|
+
code: js`
|
|
732
1108
|
let testPage;
|
|
733
1109
|
for (const p of context.pages()) {
|
|
734
1110
|
const html = await p.content();
|
|
@@ -738,24 +1114,24 @@ describe('Relay Core Tests', () => {
|
|
|
738
1114
|
const html = await getCleanHTML({ locator: testPage, search: /button/i });
|
|
739
1115
|
return html;
|
|
740
1116
|
`,
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
1117
|
+
timeout: 15000,
|
|
1118
|
+
},
|
|
1119
|
+
})
|
|
744
1120
|
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
1121
|
+
expect(searchResult.isError).toBeFalsy()
|
|
1122
|
+
const searchText = (searchResult.content as any)[0]?.text || ''
|
|
1123
|
+
expect(searchText).toContain('button')
|
|
748
1124
|
|
|
749
|
-
|
|
750
|
-
|
|
1125
|
+
await page.close()
|
|
1126
|
+
}, 60000)
|
|
751
1127
|
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
1128
|
+
it('should extract page content as markdown with getPageMarkdown', async () => {
|
|
1129
|
+
const browserContext = getBrowserContext()
|
|
1130
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
755
1131
|
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
1132
|
+
const page = await browserContext.newPage()
|
|
1133
|
+
// Create a realistic article-like page structure
|
|
1134
|
+
await page.setContent(`
|
|
759
1135
|
<html>
|
|
760
1136
|
<head>
|
|
761
1137
|
<title>Test Article Title</title>
|
|
@@ -782,18 +1158,18 @@ describe('Relay Core Tests', () => {
|
|
|
782
1158
|
</body>
|
|
783
1159
|
</html>
|
|
784
1160
|
`)
|
|
785
|
-
|
|
1161
|
+
await page.bringToFront()
|
|
786
1162
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
1163
|
+
await serviceWorker.evaluate(async () => {
|
|
1164
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
1165
|
+
})
|
|
1166
|
+
await new Promise((r) => setTimeout(r, 400))
|
|
791
1167
|
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
1168
|
+
// Test basic getPageMarkdown
|
|
1169
|
+
const result = await client.callTool({
|
|
1170
|
+
name: 'execute',
|
|
1171
|
+
arguments: {
|
|
1172
|
+
code: js`
|
|
797
1173
|
let testPage;
|
|
798
1174
|
for (const p of context.pages()) {
|
|
799
1175
|
const html = await p.content();
|
|
@@ -803,30 +1179,30 @@ describe('Relay Core Tests', () => {
|
|
|
803
1179
|
const content = await getPageMarkdown({ page: testPage });
|
|
804
1180
|
console.log(content);
|
|
805
1181
|
`,
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
1182
|
+
timeout: 15000,
|
|
1183
|
+
},
|
|
1184
|
+
})
|
|
809
1185
|
|
|
810
|
-
|
|
811
|
-
|
|
1186
|
+
expect(result.isError).toBeFalsy()
|
|
1187
|
+
const text = (result.content as any)[0]?.text || ''
|
|
812
1188
|
|
|
813
|
-
|
|
814
|
-
|
|
1189
|
+
// Snapshot the full output
|
|
1190
|
+
await expect(text).toMatchFileSnapshot('./snapshots/page-markdown-output.txt')
|
|
815
1191
|
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
1192
|
+
// Should contain article content
|
|
1193
|
+
expect(text).toContain('Test Article Title')
|
|
1194
|
+
expect(text).toContain('first paragraph')
|
|
1195
|
+
expect(text).toContain('second paragraph')
|
|
820
1196
|
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
1197
|
+
// Should NOT contain script/style content
|
|
1198
|
+
expect(text).not.toContain('analytics')
|
|
1199
|
+
expect(text).not.toContain('background: blue')
|
|
824
1200
|
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
1201
|
+
// Test search functionality
|
|
1202
|
+
const searchResult = await client.callTool({
|
|
1203
|
+
name: 'execute',
|
|
1204
|
+
arguments: {
|
|
1205
|
+
code: js`
|
|
830
1206
|
let testPage;
|
|
831
1207
|
for (const p of context.pages()) {
|
|
832
1208
|
const html = await p.content();
|
|
@@ -836,97 +1212,260 @@ describe('Relay Core Tests', () => {
|
|
|
836
1212
|
const content = await getPageMarkdown({ page: testPage, search: /important/i, showDiffSinceLastCall: false });
|
|
837
1213
|
return content;
|
|
838
1214
|
`,
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
1215
|
+
timeout: 15000,
|
|
1216
|
+
},
|
|
1217
|
+
})
|
|
842
1218
|
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
1219
|
+
expect(searchResult.isError).toBeFalsy()
|
|
1220
|
+
const searchText = (searchResult.content as any)[0]?.text || ''
|
|
1221
|
+
expect(searchText).toContain('important')
|
|
846
1222
|
|
|
847
|
-
|
|
848
|
-
|
|
1223
|
+
await page.close()
|
|
1224
|
+
}, 60000)
|
|
849
1225
|
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
1226
|
+
it('should handle default page being closed and switch to another available page', async () => {
|
|
1227
|
+
// This test verifies that when the default `page` in MCP scope is closed,
|
|
1228
|
+
// the MCP automatically switches to another available page instead of failing
|
|
1229
|
+
// with cryptic "page closed" errors.
|
|
854
1230
|
|
|
855
|
-
|
|
856
|
-
|
|
1231
|
+
const browserContext = getBrowserContext()
|
|
1232
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
857
1233
|
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
1234
|
+
// 1. Disconnect everything to start fresh
|
|
1235
|
+
await serviceWorker.evaluate(async () => {
|
|
1236
|
+
await globalThis.disconnectEverything()
|
|
1237
|
+
})
|
|
1238
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
863
1239
|
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
1240
|
+
// 2. Create first page and enable extension
|
|
1241
|
+
const page1 = await browserContext.newPage()
|
|
1242
|
+
await page1.goto('https://example.com/first-page')
|
|
1243
|
+
await page1.bringToFront()
|
|
868
1244
|
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
1245
|
+
await serviceWorker.evaluate(async () => {
|
|
1246
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
1247
|
+
})
|
|
1248
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
873
1249
|
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
1250
|
+
// 3. Reset MCP to ensure page1 becomes the default page (only page available)
|
|
1251
|
+
const resetResult = await client.callTool({
|
|
1252
|
+
name: 'reset',
|
|
1253
|
+
arguments: {},
|
|
1254
|
+
})
|
|
1255
|
+
expect((resetResult as any).content[0].text).toContain('Connection reset successfully')
|
|
880
1256
|
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
1257
|
+
// 4. Verify initial page is accessible via default `page`
|
|
1258
|
+
const initialResult = await client.callTool({
|
|
1259
|
+
name: 'execute',
|
|
1260
|
+
arguments: {
|
|
1261
|
+
code: js`
|
|
886
1262
|
const url = page.url();
|
|
887
1263
|
console.log('Initial page URL:', url);
|
|
888
1264
|
return { url };
|
|
889
1265
|
`,
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
1266
|
+
},
|
|
1267
|
+
})
|
|
1268
|
+
expect((initialResult as any).content[0].text).toContain('first-page')
|
|
893
1269
|
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
1270
|
+
// 5. Create second page and enable extension
|
|
1271
|
+
const page2 = await browserContext.newPage()
|
|
1272
|
+
await page2.goto('https://example.com/second-page')
|
|
1273
|
+
await page2.bringToFront()
|
|
898
1274
|
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
1275
|
+
await serviceWorker.evaluate(async () => {
|
|
1276
|
+
await globalThis.toggleExtensionForActiveTab()
|
|
1277
|
+
})
|
|
1278
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
1279
|
+
|
|
1280
|
+
// 6. Close the first page (which is the default `page` in MCP scope)
|
|
1281
|
+
await page1.close()
|
|
1282
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
1283
|
+
|
|
1284
|
+
// 7. Execute code via MCP - should NOT fail with "page closed" error
|
|
1285
|
+
// Instead, it should automatically switch to the second page
|
|
1286
|
+
const afterCloseResult = await client.callTool({
|
|
1287
|
+
name: 'execute',
|
|
1288
|
+
arguments: {
|
|
1289
|
+
code: js`
|
|
914
1290
|
const url = page.url();
|
|
915
1291
|
console.log('Page URL after close:', url);
|
|
916
1292
|
const title = await page.title();
|
|
917
1293
|
return { url, title };
|
|
918
1294
|
`,
|
|
919
|
-
|
|
920
|
-
|
|
1295
|
+
},
|
|
1296
|
+
})
|
|
921
1297
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
1298
|
+
// Should succeed and return the second page's info
|
|
1299
|
+
expect((afterCloseResult as any).isError).toBeFalsy()
|
|
1300
|
+
const output = (afterCloseResult as any).content[0].text
|
|
1301
|
+
expect(output).toContain('second-page')
|
|
1302
|
+
expect(output).not.toContain('page closed')
|
|
1303
|
+
expect(output).not.toContain('Target closed')
|
|
1304
|
+
|
|
1305
|
+
// Cleanup
|
|
1306
|
+
await page2.close()
|
|
1307
|
+
}, 60000)
|
|
1308
|
+
|
|
1309
|
+
it('should show descriptive error when clicking a hidden element', async () => {
|
|
1310
|
+
await ensureConnectedTabForExecute()
|
|
1311
|
+
|
|
1312
|
+
// Create a fresh page and set content with a collapsed details element
|
|
1313
|
+
await client.callTool({
|
|
1314
|
+
name: 'execute',
|
|
1315
|
+
arguments: {
|
|
1316
|
+
code: js`
|
|
1317
|
+
state.errorTestPage = await context.newPage();
|
|
1318
|
+
await state.errorTestPage.setContent(\`
|
|
1319
|
+
<details>
|
|
1320
|
+
<summary>Toggle</summary>
|
|
1321
|
+
<button id="hidden-btn">Hidden Button</button>
|
|
1322
|
+
</details>
|
|
1323
|
+
\`);
|
|
1324
|
+
`,
|
|
1325
|
+
},
|
|
1326
|
+
})
|
|
1327
|
+
const result = await client.callTool({
|
|
1328
|
+
name: 'execute',
|
|
1329
|
+
arguments: {
|
|
1330
|
+
code: js`
|
|
1331
|
+
await state.errorTestPage.click('#hidden-btn', { timeout: 100 });
|
|
1332
|
+
`,
|
|
1333
|
+
},
|
|
1334
|
+
})
|
|
1335
|
+
expect(result).toMatchInlineSnapshot(`
|
|
1336
|
+
{
|
|
1337
|
+
"content": [
|
|
1338
|
+
{
|
|
1339
|
+
"text": "
|
|
1340
|
+
Error executing code: page.click: Timeout 100ms 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
|
|
1341
|
+
Call log:
|
|
1342
|
+
[2m - waiting for locator('#hidden-btn')[22m
|
|
1343
|
+
[2m - locator resolved to <button id="hidden-btn">Hidden Button</button>[22m
|
|
1344
|
+
[2m - attempting click action[22m
|
|
1345
|
+
[2m 2 × waiting for element to be visible, enabled and stable[22m
|
|
1346
|
+
[2m - element is not visible[22m
|
|
1347
|
+
[2m - retrying click action[22m
|
|
1348
|
+
[2m - waiting 20ms[22m
|
|
1349
|
+
[2m - waiting for element to be visible, enabled and stable[22m
|
|
1350
|
+
[2m - element is not visible[22m
|
|
1351
|
+
[2m - retrying click action[22m
|
|
1352
|
+
[2m - waiting 100ms[22m
|
|
1353
|
+
",
|
|
1354
|
+
"type": "text",
|
|
1355
|
+
},
|
|
1356
|
+
],
|
|
1357
|
+
"isError": true,
|
|
1358
|
+
}
|
|
1359
|
+
`)
|
|
1360
|
+
// Cleanup
|
|
1361
|
+
await client.callTool({ name: 'execute', arguments: { code: js`await state.errorTestPage.close(); delete state.errorTestPage;` } })
|
|
1362
|
+
}, 30000)
|
|
1363
|
+
|
|
1364
|
+
it('should show descriptive error when clicking an element covered by another', async () => {
|
|
1365
|
+
await ensureConnectedTabForExecute()
|
|
1366
|
+
|
|
1367
|
+
await client.callTool({
|
|
1368
|
+
name: 'execute',
|
|
1369
|
+
arguments: {
|
|
1370
|
+
code: js`
|
|
1371
|
+
state.errorTestPage = await context.newPage();
|
|
1372
|
+
await state.errorTestPage.setContent(\`
|
|
1373
|
+
<div style="position:relative">
|
|
1374
|
+
<button id="covered-btn" style="position:absolute;top:0;left:0">Covered</button>
|
|
1375
|
+
<div id="overlay" style="position:absolute;top:0;left:0;width:200px;height:200px;background:red;z-index:10">Overlay</div>
|
|
1376
|
+
</div>
|
|
1377
|
+
\`);
|
|
1378
|
+
`,
|
|
1379
|
+
},
|
|
1380
|
+
})
|
|
1381
|
+
const result = await client.callTool({
|
|
1382
|
+
name: 'execute',
|
|
1383
|
+
arguments: {
|
|
1384
|
+
code: js`
|
|
1385
|
+
await state.errorTestPage.click('#covered-btn', { timeout: 100 });
|
|
1386
|
+
`,
|
|
1387
|
+
},
|
|
1388
|
+
})
|
|
1389
|
+
expect(result).toMatchInlineSnapshot(`
|
|
1390
|
+
{
|
|
1391
|
+
"content": [
|
|
1392
|
+
{
|
|
1393
|
+
"text": "
|
|
1394
|
+
Error executing code: page.click: Timeout 100ms exceeded. <div id="overlay">Overlay</div> intercepts pointer events
|
|
1395
|
+
Call log:
|
|
1396
|
+
[2m - waiting for locator('#covered-btn')[22m
|
|
1397
|
+
[2m - locator resolved to <button id="covered-btn">Covered</button>[22m
|
|
1398
|
+
[2m - attempting click action[22m
|
|
1399
|
+
[2m 2 × waiting for element to be visible, enabled and stable[22m
|
|
1400
|
+
[2m - element is visible, enabled and stable[22m
|
|
1401
|
+
[2m - scrolling into view if needed[22m
|
|
1402
|
+
[2m - done scrolling[22m
|
|
1403
|
+
[2m - <div id="overlay">Overlay</div> intercepts pointer events[22m
|
|
1404
|
+
[2m - retrying click action[22m
|
|
1405
|
+
[2m - waiting 20ms[22m
|
|
1406
|
+
[2m - waiting for element to be visible, enabled and stable[22m
|
|
1407
|
+
[2m - element is visible, enabled and stable[22m
|
|
1408
|
+
[2m - scrolling into view if needed[22m
|
|
1409
|
+
[2m - done scrolling[22m
|
|
1410
|
+
[2m - <div id="overlay">Overlay</div> intercepts pointer events[22m
|
|
1411
|
+
[2m - retrying click action[22m
|
|
1412
|
+
[2m - waiting 100ms[22m
|
|
1413
|
+
",
|
|
1414
|
+
"type": "text",
|
|
1415
|
+
},
|
|
1416
|
+
],
|
|
1417
|
+
"isError": true,
|
|
1418
|
+
}
|
|
1419
|
+
`)
|
|
1420
|
+
await client.callTool({ name: 'execute', arguments: { code: js`await state.errorTestPage.close(); delete state.errorTestPage;` } })
|
|
1421
|
+
}, 30000)
|
|
1422
|
+
|
|
1423
|
+
it('should show descriptive error when clicking a display:none element', async () => {
|
|
1424
|
+
await ensureConnectedTabForExecute()
|
|
1425
|
+
|
|
1426
|
+
await client.callTool({
|
|
1427
|
+
name: 'execute',
|
|
1428
|
+
arguments: {
|
|
1429
|
+
code: js`
|
|
1430
|
+
state.errorTestPage = await context.newPage();
|
|
1431
|
+
await state.errorTestPage.setContent('<button id="invisible" style="display:none">Invisible</button>');
|
|
1432
|
+
`,
|
|
1433
|
+
},
|
|
1434
|
+
})
|
|
1435
|
+
const result = await client.callTool({
|
|
1436
|
+
name: 'execute',
|
|
1437
|
+
arguments: {
|
|
1438
|
+
code: js`
|
|
1439
|
+
await state.errorTestPage.click('#invisible', { timeout: 100 });
|
|
1440
|
+
`,
|
|
1441
|
+
},
|
|
1442
|
+
})
|
|
1443
|
+
expect(result).toMatchInlineSnapshot(`
|
|
1444
|
+
{
|
|
1445
|
+
"content": [
|
|
1446
|
+
{
|
|
1447
|
+
"text": "
|
|
1448
|
+
Error executing code: page.click: Timeout 100ms 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
|
|
1449
|
+
Call log:
|
|
1450
|
+
[2m - waiting for locator('#invisible')[22m
|
|
1451
|
+
[2m - locator resolved to <button id="invisible">Invisible</button>[22m
|
|
1452
|
+
[2m - attempting click action[22m
|
|
1453
|
+
[2m 2 × waiting for element to be visible, enabled and stable[22m
|
|
1454
|
+
[2m - element is not visible[22m
|
|
1455
|
+
[2m - retrying click action[22m
|
|
1456
|
+
[2m - waiting 20ms[22m
|
|
1457
|
+
[2m - waiting for element to be visible, enabled and stable[22m
|
|
1458
|
+
[2m - element is not visible[22m
|
|
1459
|
+
[2m - retrying click action[22m
|
|
1460
|
+
[2m - waiting 100ms[22m
|
|
1461
|
+
",
|
|
1462
|
+
"type": "text",
|
|
1463
|
+
},
|
|
1464
|
+
],
|
|
1465
|
+
"isError": true,
|
|
1466
|
+
}
|
|
1467
|
+
`)
|
|
1468
|
+
await client.callTool({ name: 'execute', arguments: { code: js`await state.errorTestPage.close(); delete state.errorTestPage;` } })
|
|
1469
|
+
}, 30000)
|
|
928
1470
|
|
|
929
|
-
// Cleanup
|
|
930
|
-
await page2.close()
|
|
931
|
-
}, 60000)
|
|
932
1471
|
})
|