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/kill-port.ts
CHANGED
|
@@ -148,9 +148,7 @@ export async function getListeningPidsForPort({ port }: { port: number }): Promi
|
|
|
148
148
|
throw new Error(`Invalid port: ${port}`)
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
return os.platform() === 'win32'
|
|
152
|
-
? await getPidsForPortWindows(port)
|
|
153
|
-
: await getPidsForPortUnix(port)
|
|
151
|
+
return os.platform() === 'win32' ? await getPidsForPortWindows(port) : await getPidsForPortUnix(port)
|
|
154
152
|
}
|
|
155
153
|
|
|
156
154
|
function toError(value: unknown): Error {
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Tests for Locator.selector() — verifies the raw selector strings returned
|
|
2
|
+
// by various locator creation methods (getByRole, locator, getByText, etc.)
|
|
3
|
+
|
|
4
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
|
|
5
|
+
import { chromium, type Page, type Browser } from '@xmorse/playwright-core'
|
|
6
|
+
|
|
7
|
+
const HTML = `<!DOCTYPE html>
|
|
8
|
+
<html>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="main">
|
|
11
|
+
<button>Submit</button>
|
|
12
|
+
<button>Cancel</button>
|
|
13
|
+
<button class="primary">Save</button>
|
|
14
|
+
<input type="text" placeholder="Enter name" />
|
|
15
|
+
<input type="email" placeholder="Enter email" />
|
|
16
|
+
<a href="/about">About Us</a>
|
|
17
|
+
<h1>Page Title</h1>
|
|
18
|
+
<p>Some paragraph text</p>
|
|
19
|
+
<div class="card">
|
|
20
|
+
<span>Card content</span>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="card">
|
|
23
|
+
<span>Another card</span>
|
|
24
|
+
</div>
|
|
25
|
+
<div class="card">
|
|
26
|
+
<span>Third card</span>
|
|
27
|
+
</div>
|
|
28
|
+
<label for="age">Age</label>
|
|
29
|
+
<input id="age" type="number" />
|
|
30
|
+
<img alt="Logo" src="/logo.png" />
|
|
31
|
+
<div data-testid="dashboard">Dashboard</div>
|
|
32
|
+
</div>
|
|
33
|
+
</body>
|
|
34
|
+
</html>`
|
|
35
|
+
|
|
36
|
+
describe('Locator.selector()', () => {
|
|
37
|
+
let browser: Browser
|
|
38
|
+
let page: Page
|
|
39
|
+
|
|
40
|
+
beforeAll(async () => {
|
|
41
|
+
browser = await chromium.launch({ headless: true })
|
|
42
|
+
const context = await browser.newContext()
|
|
43
|
+
page = await context.newPage()
|
|
44
|
+
await page.setContent(HTML)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
afterAll(async () => {
|
|
48
|
+
await browser.close()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('CSS selectors', () => {
|
|
52
|
+
expect(page.locator('#main').selector()).toMatchInlineSnapshot(`"#main"`)
|
|
53
|
+
expect(page.locator('.card').selector()).toMatchInlineSnapshot(`".card"`)
|
|
54
|
+
expect(page.locator('button').selector()).toMatchInlineSnapshot(`"button"`)
|
|
55
|
+
expect(page.locator('div.card > span').selector()).toMatchInlineSnapshot(`"div.card > span"`)
|
|
56
|
+
expect(page.locator('#main .card:first-child').selector()).toMatchInlineSnapshot(`"#main .card:first-child"`)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('getByRole', () => {
|
|
60
|
+
expect(page.getByRole('button').selector()).toMatchInlineSnapshot(`"internal:role=button"`)
|
|
61
|
+
expect(page.getByRole('button', { name: 'Submit' }).selector()).toMatchInlineSnapshot(`"internal:role=button[name="Submit"i]"`)
|
|
62
|
+
expect(page.getByRole('link').selector()).toMatchInlineSnapshot(`"internal:role=link"`)
|
|
63
|
+
expect(page.getByRole('heading').selector()).toMatchInlineSnapshot(`"internal:role=heading"`)
|
|
64
|
+
expect(page.getByRole('textbox').selector()).toMatchInlineSnapshot(`"internal:role=textbox"`)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('getByText', () => {
|
|
68
|
+
expect(page.getByText('Submit').selector()).toMatchInlineSnapshot(`"internal:text="Submit"i"`)
|
|
69
|
+
expect(page.getByText('Some paragraph').selector()).toMatchInlineSnapshot(`"internal:text="Some paragraph"i"`)
|
|
70
|
+
expect(page.getByText(/card/i).selector()).toMatchInlineSnapshot(`"internal:text=/card/i"`)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('getByPlaceholder', () => {
|
|
74
|
+
expect(page.getByPlaceholder('Enter name').selector()).toMatchInlineSnapshot(`"internal:attr=[placeholder="Enter name"i]"`)
|
|
75
|
+
expect(page.getByPlaceholder('Enter email').selector()).toMatchInlineSnapshot(`"internal:attr=[placeholder="Enter email"i]"`)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('getByLabel', () => {
|
|
79
|
+
expect(page.getByLabel('Age').selector()).toMatchInlineSnapshot(`"internal:label="Age"i"`)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('getByAltText', () => {
|
|
83
|
+
expect(page.getByAltText('Logo').selector()).toMatchInlineSnapshot(`"internal:attr=[alt="Logo"i]"`)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('getByTestId', () => {
|
|
87
|
+
expect(page.getByTestId('dashboard').selector()).toMatchInlineSnapshot(`"internal:testid=[data-testid="dashboard"s]"`)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('chained locators', () => {
|
|
91
|
+
expect(page.locator('#main').locator('.card').selector()).toMatchInlineSnapshot(`"#main >> .card"`)
|
|
92
|
+
expect(page.locator('.card').first().selector()).toMatchInlineSnapshot(`".card >> nth=0"`)
|
|
93
|
+
expect(page.locator('.card').last().selector()).toMatchInlineSnapshot(`".card >> nth=-1"`)
|
|
94
|
+
expect(page.locator('.card').nth(1).selector()).toMatchInlineSnapshot(`".card >> nth=1"`)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('filtered locators', () => {
|
|
98
|
+
expect(page.locator('button').filter({ hasText: 'Save' }).selector()).toMatchInlineSnapshot(`"button >> internal:has-text="Save"i"`)
|
|
99
|
+
expect(page.locator('div').filter({ has: page.locator('span') }).selector()).toMatchInlineSnapshot(`"div >> internal:has="span""`)
|
|
100
|
+
expect(page.locator('button').filter({ hasNotText: 'Cancel' }).selector()).toMatchInlineSnapshot(`"button >> internal:has-not-text="Cancel"i"`)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('described locators', () => {
|
|
104
|
+
expect(page.locator('button').describe('main action button').selector()).toMatchInlineSnapshot(`"button >> internal:describe="main action button""`)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('combined with and/or', () => {
|
|
108
|
+
expect(
|
|
109
|
+
page.locator('button').and(page.locator('.primary')).selector(),
|
|
110
|
+
).toMatchInlineSnapshot(`"button >> internal:and=".primary""`)
|
|
111
|
+
expect(
|
|
112
|
+
page.locator('button').or(page.locator('a')).selector(),
|
|
113
|
+
).toMatchInlineSnapshot(`"button >> internal:or="a""`)
|
|
114
|
+
})
|
|
115
|
+
})
|
package/src/mcp-client.ts
CHANGED
|
@@ -17,7 +17,7 @@ export async function createTransport({ args = [], port }: { args?: string[]; po
|
|
|
17
17
|
stderr: Stream | null
|
|
18
18
|
}> {
|
|
19
19
|
const env: Record<string, string> = {
|
|
20
|
-
...process.env as Record<string, string
|
|
20
|
+
...(process.env as Record<string, string>),
|
|
21
21
|
DEBUG: 'playwriter:mcp:test',
|
|
22
22
|
DEBUG_COLORS: '0',
|
|
23
23
|
DEBUG_HIDE_DATE: '1',
|
package/src/mcp.ts
CHANGED
|
@@ -91,12 +91,12 @@ async function getOrCreateExecutor(): Promise<PlaywrightExecutor> {
|
|
|
91
91
|
if (executor) {
|
|
92
92
|
return executor
|
|
93
93
|
}
|
|
94
|
-
|
|
94
|
+
|
|
95
95
|
const remote = getRemoteConfig()
|
|
96
96
|
if (!remote) {
|
|
97
97
|
await ensureRelayServerForMcp()
|
|
98
98
|
}
|
|
99
|
-
|
|
99
|
+
|
|
100
100
|
// Pass config instead of pre-generated URL so executor can generate unique URLs for each connection
|
|
101
101
|
const cdpConfig = remote || { port: RELAY_PORT }
|
|
102
102
|
executor = new PlaywrightExecutor({
|
|
@@ -104,7 +104,7 @@ async function getOrCreateExecutor(): Promise<PlaywrightExecutor> {
|
|
|
104
104
|
logger: mcpLogger,
|
|
105
105
|
cwd: process.cwd(),
|
|
106
106
|
})
|
|
107
|
-
|
|
107
|
+
|
|
108
108
|
return executor
|
|
109
109
|
}
|
|
110
110
|
|
|
@@ -198,39 +198,42 @@ server.tool(
|
|
|
198
198
|
if (!remote) {
|
|
199
199
|
await ensureRelayServerForMcp()
|
|
200
200
|
}
|
|
201
|
-
|
|
201
|
+
|
|
202
202
|
const exec = await getOrCreateExecutor()
|
|
203
203
|
const result = await exec.execute(code, timeout)
|
|
204
|
-
|
|
204
|
+
|
|
205
205
|
// Transform executor result to MCP format
|
|
206
206
|
const content: Array<{ type: 'text'; text: string } | { type: 'image'; data: string; mimeType: string }> = [
|
|
207
207
|
{ type: 'text', text: result.text },
|
|
208
208
|
]
|
|
209
|
-
|
|
209
|
+
|
|
210
210
|
for (const image of result.images) {
|
|
211
211
|
content.push({ type: 'image', data: image.data, mimeType: image.mimeType })
|
|
212
212
|
}
|
|
213
|
-
|
|
213
|
+
|
|
214
214
|
if (result.isError) {
|
|
215
215
|
return { content, isError: true }
|
|
216
216
|
}
|
|
217
|
-
|
|
217
|
+
|
|
218
218
|
return { content }
|
|
219
219
|
} catch (error: any) {
|
|
220
220
|
const errorStack = error.stack || error.message
|
|
221
|
-
const isTimeoutError =
|
|
222
|
-
|
|
221
|
+
const isTimeoutError =
|
|
222
|
+
error instanceof CodeExecutionTimeoutError || error?.name === 'TimeoutError' || error?.name === 'AbortError'
|
|
223
|
+
|
|
223
224
|
console.error('Error in execute tool:', errorStack)
|
|
224
225
|
if (!isTimeoutError) {
|
|
225
226
|
sendLogToRelayServer('error', 'Error in execute tool:', errorStack)
|
|
226
227
|
}
|
|
227
|
-
|
|
228
|
+
|
|
228
229
|
const resetHint = isTimeoutError
|
|
229
230
|
? ''
|
|
230
231
|
: '\n\n[HINT: If this is an internal Playwright error, page/browser closed, or connection issue, call the `reset` tool to reconnect. Do NOT reset for other non-connection non-internal errors.]'
|
|
231
|
-
|
|
232
|
+
|
|
233
|
+
// timeout stacks are internal noise (Promise.race / setTimeout); only show the message
|
|
234
|
+
const errorText = isTimeoutError ? error.message : errorStack
|
|
232
235
|
return {
|
|
233
|
-
content: [{ type: 'text', text: `Error executing code: ${
|
|
236
|
+
content: [{ type: 'text', text: `Error executing code: ${errorText}${resetHint}` }],
|
|
234
237
|
isError: true,
|
|
235
238
|
}
|
|
236
239
|
}
|
|
@@ -256,13 +259,16 @@ server.tool(
|
|
|
256
259
|
if (!remote) {
|
|
257
260
|
await ensureRelayServerForMcp()
|
|
258
261
|
}
|
|
259
|
-
|
|
262
|
+
|
|
260
263
|
const exec = await getOrCreateExecutor()
|
|
261
264
|
const { page, context } = await exec.reset()
|
|
262
265
|
const pagesCount = context.pages().length
|
|
263
266
|
return {
|
|
264
267
|
content: [
|
|
265
|
-
{
|
|
268
|
+
{
|
|
269
|
+
type: 'text',
|
|
270
|
+
text: `Connection reset successfully. ${pagesCount} page(s) available. Current page URL: ${page.url()}`,
|
|
271
|
+
},
|
|
266
272
|
],
|
|
267
273
|
}
|
|
268
274
|
} catch (error: any) {
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test for the page.onMouseAction callback added to the playwright fork.
|
|
3
|
+
* Verifies the callback fires for both explicit page.mouse.* calls
|
|
4
|
+
* and locator-initiated actions like page.locator().click().
|
|
5
|
+
*/
|
|
6
|
+
import { chromium } from '@xmorse/playwright-core'
|
|
7
|
+
import type { MouseActionEvent, Page } from '@xmorse/playwright-core'
|
|
8
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
|
|
9
|
+
import { getCdpUrl } from './utils.js'
|
|
10
|
+
import { enableGhostCursor, applyGhostCursorMouseAction, disableGhostCursor } from './ghost-cursor.js'
|
|
11
|
+
import {
|
|
12
|
+
setupTestContext,
|
|
13
|
+
cleanupTestContext,
|
|
14
|
+
getExtensionServiceWorker,
|
|
15
|
+
type TestContext,
|
|
16
|
+
safeCloseCDPBrowser,
|
|
17
|
+
} from './test-utils.js'
|
|
18
|
+
import './test-declarations.js'
|
|
19
|
+
|
|
20
|
+
const TEST_PORT = 19994
|
|
21
|
+
|
|
22
|
+
describe('onMouseAction callback', () => {
|
|
23
|
+
let cleanup: (() => Promise<void>) | null = null
|
|
24
|
+
let testCtx: TestContext | null = null
|
|
25
|
+
|
|
26
|
+
beforeAll(async () => {
|
|
27
|
+
testCtx = await setupTestContext({
|
|
28
|
+
port: TEST_PORT,
|
|
29
|
+
tempDirPrefix: 'pw-mouse-action-test-',
|
|
30
|
+
toggleExtension: true,
|
|
31
|
+
})
|
|
32
|
+
}, 600000)
|
|
33
|
+
|
|
34
|
+
afterAll(async () => {
|
|
35
|
+
await cleanupTestContext(testCtx, cleanup)
|
|
36
|
+
cleanup = null
|
|
37
|
+
testCtx = null
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('should fire onMouseAction for page.mouse.click()', async () => {
|
|
41
|
+
const browserContext = testCtx!.browserContext
|
|
42
|
+
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
|
43
|
+
|
|
44
|
+
const page = await browserContext.newPage()
|
|
45
|
+
await page.goto('data:text/html,<html><body><button id="btn">Click me</button></body></html>')
|
|
46
|
+
await page.bringToFront()
|
|
47
|
+
|
|
48
|
+
await serviceWorker.evaluate(async () => {
|
|
49
|
+
await (globalThis as any).toggleExtensionForActiveTab()
|
|
50
|
+
})
|
|
51
|
+
await new Promise((r) => { setTimeout(r, 200) })
|
|
52
|
+
|
|
53
|
+
const directBrowser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
54
|
+
const contexts = directBrowser.contexts()
|
|
55
|
+
const pages = contexts[0].pages()
|
|
56
|
+
const targetPage = pages.find((p) => p.url().startsWith('data:'))
|
|
57
|
+
expect(targetPage).toBeDefined()
|
|
58
|
+
|
|
59
|
+
const events: MouseActionEvent[] = []
|
|
60
|
+
targetPage!.onMouseAction = async (event) => {
|
|
61
|
+
events.push({ ...event })
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
await targetPage!.mouse.click(100, 100)
|
|
65
|
+
|
|
66
|
+
// click dispatches: move → down → up
|
|
67
|
+
const types = events.map((e) => e.type)
|
|
68
|
+
expect(types).toContain('move')
|
|
69
|
+
expect(types).toContain('down')
|
|
70
|
+
expect(types).toContain('up')
|
|
71
|
+
|
|
72
|
+
// All events should have coordinates
|
|
73
|
+
for (const event of events) {
|
|
74
|
+
expect(typeof event.x).toBe('number')
|
|
75
|
+
expect(typeof event.y).toBe('number')
|
|
76
|
+
expect(typeof event.button).toBe('string')
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// The move event should have the target coordinates
|
|
80
|
+
const moveEvent = events.find((e) => e.type === 'move')!
|
|
81
|
+
expect(moveEvent.x).toBe(100)
|
|
82
|
+
expect(moveEvent.y).toBe(100)
|
|
83
|
+
|
|
84
|
+
await safeCloseCDPBrowser(directBrowser)
|
|
85
|
+
}, 30000)
|
|
86
|
+
|
|
87
|
+
it('should fire onMouseAction for locator.click()', async () => {
|
|
88
|
+
const browserContext = testCtx!.browserContext
|
|
89
|
+
|
|
90
|
+
const directBrowser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
91
|
+
const contexts = directBrowser.contexts()
|
|
92
|
+
const pages = contexts[0].pages()
|
|
93
|
+
const targetPage = pages.find((p) => p.url().startsWith('data:'))
|
|
94
|
+
expect(targetPage).toBeDefined()
|
|
95
|
+
|
|
96
|
+
const events: MouseActionEvent[] = []
|
|
97
|
+
targetPage!.onMouseAction = async (event) => {
|
|
98
|
+
events.push({ ...event })
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// locator.click() resolves coordinates server-side, then calls server Mouse
|
|
102
|
+
await targetPage!.locator('#btn').click()
|
|
103
|
+
|
|
104
|
+
const types = events.map((e) => e.type)
|
|
105
|
+
expect(types).toContain('move')
|
|
106
|
+
expect(types).toContain('down')
|
|
107
|
+
expect(types).toContain('up')
|
|
108
|
+
|
|
109
|
+
// The button center should be somewhere reasonable (not 0,0)
|
|
110
|
+
const moveEvent = events.find((e) => e.type === 'move')!
|
|
111
|
+
expect(moveEvent.x).toBeGreaterThan(0)
|
|
112
|
+
expect(moveEvent.y).toBeGreaterThan(0)
|
|
113
|
+
|
|
114
|
+
await safeCloseCDPBrowser(directBrowser)
|
|
115
|
+
}, 30000)
|
|
116
|
+
|
|
117
|
+
it('should animate ghost cursor from onMouseAction callback', async () => {
|
|
118
|
+
const browserContext = testCtx!.browserContext
|
|
119
|
+
|
|
120
|
+
const directBrowser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
121
|
+
let targetPage: Page | null = null
|
|
122
|
+
try {
|
|
123
|
+
const contexts = directBrowser.contexts()
|
|
124
|
+
const pages = contexts[0].pages()
|
|
125
|
+
targetPage = pages.find((p) => p.url().startsWith('data:'))!
|
|
126
|
+
expect(targetPage).toBeDefined()
|
|
127
|
+
const pageForTest = targetPage
|
|
128
|
+
|
|
129
|
+
await enableGhostCursor({ page: pageForTest })
|
|
130
|
+
pageForTest.onMouseAction = async (event) => {
|
|
131
|
+
await applyGhostCursorMouseAction({ page: pageForTest, event })
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
await pageForTest.mouse.click(140, 120)
|
|
135
|
+
|
|
136
|
+
const cursorState = await pageForTest.evaluate(() => {
|
|
137
|
+
const cursorElement = document.getElementById('__playwriter_ghost_cursor__')
|
|
138
|
+
if (!cursorElement) {
|
|
139
|
+
return { exists: false, transform: '' }
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
exists: true,
|
|
143
|
+
transform: cursorElement.getAttribute('style') || '',
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
expect(cursorState.exists).toBe(true)
|
|
148
|
+
const translateMatch = cursorState.transform.match(/translate3d\(([-\d.]+)px, ([-\d.]+)px, 0(?:px)?\)/)
|
|
149
|
+
expect(translateMatch).toBeTruthy()
|
|
150
|
+
const translateX = Number(translateMatch![1])
|
|
151
|
+
const translateY = Number(translateMatch![2])
|
|
152
|
+
// Screen Studio cursor uses a hotspot offset, so CSS position is slightly above/left of click target.
|
|
153
|
+
expect(translateX).toBeLessThanOrEqual(140)
|
|
154
|
+
expect(translateY).toBeLessThanOrEqual(120)
|
|
155
|
+
expect(translateX).toBeGreaterThan(120)
|
|
156
|
+
expect(translateY).toBeGreaterThan(100)
|
|
157
|
+
|
|
158
|
+
await disableGhostCursor({ page: pageForTest })
|
|
159
|
+
|
|
160
|
+
const hasGhostCursor = await pageForTest.evaluate(() => {
|
|
161
|
+
return Boolean(document.getElementById('__playwriter_ghost_cursor__'))
|
|
162
|
+
})
|
|
163
|
+
expect(hasGhostCursor).toBe(false)
|
|
164
|
+
} finally {
|
|
165
|
+
if (targetPage) {
|
|
166
|
+
targetPage.onMouseAction = null
|
|
167
|
+
await disableGhostCursor({ page: targetPage })
|
|
168
|
+
}
|
|
169
|
+
await safeCloseCDPBrowser(directBrowser)
|
|
170
|
+
}
|
|
171
|
+
}, 30000)
|
|
172
|
+
|
|
173
|
+
it('should not fire when onMouseAction is set to null', async () => {
|
|
174
|
+
const browserContext = testCtx!.browserContext
|
|
175
|
+
|
|
176
|
+
const directBrowser = await chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT }))
|
|
177
|
+
const contexts = directBrowser.contexts()
|
|
178
|
+
const pages = contexts[0].pages()
|
|
179
|
+
const targetPage = pages.find((p) => p.url().startsWith('data:'))
|
|
180
|
+
expect(targetPage).toBeDefined()
|
|
181
|
+
|
|
182
|
+
const events: MouseActionEvent[] = []
|
|
183
|
+
targetPage!.onMouseAction = async (event) => {
|
|
184
|
+
events.push({ ...event })
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Disable callback
|
|
188
|
+
targetPage!.onMouseAction = null
|
|
189
|
+
|
|
190
|
+
await targetPage!.mouse.click(50, 50)
|
|
191
|
+
|
|
192
|
+
expect(events).toHaveLength(0)
|
|
193
|
+
|
|
194
|
+
await safeCloseCDPBrowser(directBrowser)
|
|
195
|
+
}, 30000)
|
|
196
|
+
})
|
package/src/page-markdown.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Extract page content as markdown using Mozilla Readability.
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* This utility injects the Readability library into the page and extracts
|
|
5
5
|
* the main content, similar to Firefox Reader View.
|
|
6
6
|
*/
|
|
@@ -65,12 +65,12 @@ function isRegExp(value: unknown): value is RegExp {
|
|
|
65
65
|
|
|
66
66
|
/**
|
|
67
67
|
* Extract page content as markdown using Mozilla Readability.
|
|
68
|
-
*
|
|
68
|
+
*
|
|
69
69
|
* Injects Readability into the page if not already present, then extracts
|
|
70
70
|
* the main content. Returns plain text content (no HTML).
|
|
71
71
|
*/
|
|
72
72
|
export async function getPageMarkdown(options: GetPageMarkdownOptions): Promise<string> {
|
|
73
|
-
const { page, search, showDiffSinceLastCall =
|
|
73
|
+
const { page, search, showDiffSinceLastCall = !search } = options
|
|
74
74
|
|
|
75
75
|
// Check if readability is already injected
|
|
76
76
|
const hasReadability = await page.evaluate(() => !!(globalThis as any).__readability)
|
|
@@ -81,7 +81,7 @@ export async function getPageMarkdown(options: GetPageMarkdownOptions): Promise<
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
// Extract content using Readability
|
|
84
|
-
const result = await page.evaluate(() => {
|
|
84
|
+
const result = (await page.evaluate(() => {
|
|
85
85
|
const readability = (globalThis as any).__readability
|
|
86
86
|
if (!readability) {
|
|
87
87
|
throw new Error('Readability not loaded')
|
|
@@ -131,11 +131,11 @@ export async function getPageMarkdown(options: GetPageMarkdownOptions): Promise<
|
|
|
131
131
|
publishedTime: article.publishedTime || null,
|
|
132
132
|
wordCount: (article.textContent || '').split(/\s+/).filter(Boolean).length,
|
|
133
133
|
}
|
|
134
|
-
}) as PageMarkdownResult & { _notReadable?: boolean }
|
|
134
|
+
})) as PageMarkdownResult & { _notReadable?: boolean }
|
|
135
135
|
|
|
136
136
|
// Format output
|
|
137
137
|
const lines: string[] = []
|
|
138
|
-
|
|
138
|
+
|
|
139
139
|
if (result.title) {
|
|
140
140
|
lines.push(`# ${result.title}`)
|
|
141
141
|
lines.push('')
|
|
@@ -172,7 +172,7 @@ export async function getPageMarkdown(options: GetPageMarkdownOptions): Promise<
|
|
|
172
172
|
const previousSnapshot = lastMarkdownSnapshots.get(page)
|
|
173
173
|
lastMarkdownSnapshots.set(page, markdown)
|
|
174
174
|
|
|
175
|
-
//
|
|
175
|
+
// Diff defaults off when search is provided, but agent can explicitly enable both
|
|
176
176
|
if (showDiffSinceLastCall && previousSnapshot) {
|
|
177
177
|
const diffResult = createSmartDiff({
|
|
178
178
|
oldContent: previousSnapshot,
|
package/src/protocol.ts
CHANGED
|
@@ -2,19 +2,18 @@ import { CDPEventFor, ProtocolMapping } from './cdp-types.js'
|
|
|
2
2
|
|
|
3
3
|
export const VERSION = 1
|
|
4
4
|
|
|
5
|
-
type ForwardCDPCommand =
|
|
6
|
-
{
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
source?: 'playwriter'
|
|
15
|
-
}
|
|
5
|
+
type ForwardCDPCommand = {
|
|
6
|
+
[K in keyof ProtocolMapping.Commands]: {
|
|
7
|
+
id: number
|
|
8
|
+
method: 'forwardCDPCommand'
|
|
9
|
+
params: {
|
|
10
|
+
method: K
|
|
11
|
+
sessionId?: string
|
|
12
|
+
params?: ProtocolMapping.Commands[K]['paramsType'][0]
|
|
13
|
+
source?: 'playwriter'
|
|
16
14
|
}
|
|
17
|
-
}
|
|
15
|
+
}
|
|
16
|
+
}[keyof ProtocolMapping.Commands]
|
|
18
17
|
|
|
19
18
|
export type ExtensionCommandMessage = ForwardCDPCommand
|
|
20
19
|
|
|
@@ -29,18 +28,17 @@ export type ExtensionResponseMessage = {
|
|
|
29
28
|
* This produces a discriminated union for narrowing, similar to ForwardCDPCommand,
|
|
30
29
|
* but for forwarded CDP events. Uses CDPEvent to maintain proper type extraction.
|
|
31
30
|
*/
|
|
32
|
-
export type ExtensionEventMessage =
|
|
33
|
-
{
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
params?: CDPEventFor<K>['params']
|
|
41
|
-
}
|
|
31
|
+
export type ExtensionEventMessage = {
|
|
32
|
+
[K in keyof ProtocolMapping.Events]: {
|
|
33
|
+
id?: undefined
|
|
34
|
+
method: 'forwardCDPEvent'
|
|
35
|
+
params: {
|
|
36
|
+
method: CDPEventFor<K>['method']
|
|
37
|
+
sessionId?: string
|
|
38
|
+
params?: CDPEventFor<K>['params']
|
|
42
39
|
}
|
|
43
|
-
}
|
|
40
|
+
}
|
|
41
|
+
}[keyof ProtocolMapping.Events]
|
|
44
42
|
|
|
45
43
|
export type ExtensionLogMessage = {
|
|
46
44
|
id?: undefined
|
|
@@ -78,10 +76,17 @@ export type RecordingCancelledMessage = {
|
|
|
78
76
|
}
|
|
79
77
|
}
|
|
80
78
|
|
|
81
|
-
export type ExtensionMessage =
|
|
79
|
+
export type ExtensionMessage =
|
|
80
|
+
| ExtensionResponseMessage
|
|
81
|
+
| ExtensionEventMessage
|
|
82
|
+
| ExtensionLogMessage
|
|
83
|
+
| ExtensionPongMessage
|
|
84
|
+
| RecordingDataMessage
|
|
85
|
+
| RecordingCancelledMessage
|
|
82
86
|
|
|
83
87
|
// Recording command messages (MCP -> Extension via relay)
|
|
84
88
|
export type StartRecordingParams = {
|
|
89
|
+
/** CDP tab session ID (pw-tab-*) to identify which tab to record. */
|
|
85
90
|
sessionId?: string
|
|
86
91
|
frameRate?: number
|
|
87
92
|
audio?: boolean
|
|
@@ -95,14 +100,17 @@ export type StartRecordingBody = StartRecordingParams & {
|
|
|
95
100
|
}
|
|
96
101
|
|
|
97
102
|
export type StopRecordingParams = {
|
|
103
|
+
/** CDP tab session ID (pw-tab-*) to identify which tab to stop recording. */
|
|
98
104
|
sessionId?: string
|
|
99
105
|
}
|
|
100
106
|
|
|
101
107
|
export type IsRecordingParams = {
|
|
108
|
+
/** CDP tab session ID (pw-tab-*) to identify which tab to check. */
|
|
102
109
|
sessionId?: string
|
|
103
110
|
}
|
|
104
111
|
|
|
105
112
|
export type CancelRecordingParams = {
|
|
113
|
+
/** CDP tab session ID (pw-tab-*) to identify which tab to cancel. */
|
|
106
114
|
sessionId?: string
|
|
107
115
|
}
|
|
108
116
|
|
|
@@ -137,36 +145,42 @@ export type RecordingCommandMessage =
|
|
|
137
145
|
| CancelRecordingMessage
|
|
138
146
|
|
|
139
147
|
// Recording result types
|
|
140
|
-
export type StartRecordingResult =
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
+
export type StartRecordingResult =
|
|
149
|
+
| {
|
|
150
|
+
success: true
|
|
151
|
+
tabId: number
|
|
152
|
+
startedAt: number
|
|
153
|
+
}
|
|
154
|
+
| {
|
|
155
|
+
success: false
|
|
156
|
+
error: string
|
|
157
|
+
}
|
|
148
158
|
|
|
149
159
|
/** Result from extension - doesn't include path/size since relay writes the file */
|
|
150
|
-
export type ExtensionStopRecordingResult =
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
160
|
+
export type ExtensionStopRecordingResult =
|
|
161
|
+
| {
|
|
162
|
+
success: true
|
|
163
|
+
tabId: number
|
|
164
|
+
duration: number
|
|
165
|
+
}
|
|
166
|
+
| {
|
|
167
|
+
success: false
|
|
168
|
+
error: string
|
|
169
|
+
}
|
|
158
170
|
|
|
159
171
|
/** Final result from relay - includes path/size after file is written */
|
|
160
|
-
export type StopRecordingResult =
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
172
|
+
export type StopRecordingResult =
|
|
173
|
+
| {
|
|
174
|
+
success: true
|
|
175
|
+
tabId: number
|
|
176
|
+
duration: number
|
|
177
|
+
path: string
|
|
178
|
+
size: number
|
|
179
|
+
}
|
|
180
|
+
| {
|
|
181
|
+
success: false
|
|
182
|
+
error: string
|
|
183
|
+
}
|
|
170
184
|
|
|
171
185
|
export type IsRecordingResult = {
|
|
172
186
|
isRecording: boolean
|
|
@@ -193,10 +207,12 @@ export type GhostBrowserCommandMessage = {
|
|
|
193
207
|
}
|
|
194
208
|
}
|
|
195
209
|
|
|
196
|
-
export type GhostBrowserCommandResult =
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
210
|
+
export type GhostBrowserCommandResult =
|
|
211
|
+
| {
|
|
212
|
+
success: true
|
|
213
|
+
result: unknown
|
|
214
|
+
}
|
|
215
|
+
| {
|
|
216
|
+
success: false
|
|
217
|
+
error: string
|
|
218
|
+
}
|