playwriter 0.0.3 → 0.0.4
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/bin.js +1 -1
- package/dist/browser-config.js +1 -3
- package/dist/browser-config.js.map +1 -1
- package/dist/cdp-types.d.ts +25 -0
- package/dist/cdp-types.d.ts.map +1 -0
- package/dist/cdp-types.js +91 -0
- package/dist/cdp-types.js.map +1 -0
- package/dist/extension/cdp-relay.d.ts +12 -0
- package/dist/extension/cdp-relay.d.ts.map +1 -0
- package/dist/extension/cdp-relay.js +378 -0
- package/dist/extension/cdp-relay.js.map +1 -0
- package/dist/extension/protocol.d.ts +29 -0
- package/dist/extension/protocol.d.ts.map +1 -0
- package/dist/extension/protocol.js +2 -0
- package/dist/extension/protocol.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-client.d.ts.map +1 -1
- package/dist/mcp-client.js +1 -1
- package/dist/mcp-client.js.map +1 -1
- package/dist/mcp.js +74 -464
- package/dist/mcp.js.map +1 -1
- package/dist/mcp.test.js +101 -142
- package/dist/mcp.test.js.map +1 -1
- package/dist/prompt.md +41 -487
- package/dist/resource.md +436 -0
- package/dist/start-relay-server.d.ts +8 -0
- package/dist/start-relay-server.d.ts.map +1 -0
- package/dist/start-relay-server.js +33 -0
- package/dist/start-relay-server.js.map +1 -0
- package/package.json +42 -36
- package/src/browser-config.ts +48 -50
- package/src/cdp-types.ts +124 -0
- package/src/extension/cdp-relay.ts +480 -0
- package/src/extension/protocol.ts +34 -0
- package/src/index.ts +1 -0
- package/src/mcp-client.ts +46 -46
- package/src/mcp.test.ts +109 -165
- package/src/mcp.ts +202 -694
- package/src/prompt.md +41 -487
- package/src/resource.md +436 -0
- package/src/snapshots/hacker-news-initial-accessibility.md +243 -127
- package/src/snapshots/shadcn-ui-accessibility.md +300 -510
- package/src/start-relay-server.ts +43 -0
package/src/mcp-client.ts
CHANGED
|
@@ -8,66 +8,66 @@ import url from 'node:url'
|
|
|
8
8
|
const __filename = url.fileURLToPath(import.meta.url)
|
|
9
9
|
|
|
10
10
|
export interface CreateTransportOptions {
|
|
11
|
-
|
|
11
|
+
clientName?: string
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export async function createTransport(args: string[]): Promise<{
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
transport: Transport
|
|
16
|
+
stderr: Stream | null
|
|
17
17
|
}> {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
18
|
+
const transport = new StdioClientTransport({
|
|
19
|
+
command: 'pnpm',
|
|
20
|
+
args: ['vite-node', path.join(path.dirname(__filename), 'mcp.ts'), ...args],
|
|
21
|
+
cwd: path.join(path.dirname(__filename), '..'),
|
|
22
|
+
stderr: 'pipe',
|
|
23
|
+
env: {
|
|
24
|
+
...process.env,
|
|
25
|
+
DEBUG: 'playwriter:mcp:test',
|
|
26
|
+
DEBUG_COLORS: '0',
|
|
27
|
+
DEBUG_HIDE_DATE: '1',
|
|
28
|
+
},
|
|
29
|
+
})
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
return {
|
|
32
|
+
transport,
|
|
33
|
+
stderr: transport.stderr!,
|
|
34
|
+
}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
export async function createMCPClient(options?: CreateTransportOptions): Promise<{
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
client: Client
|
|
39
|
+
stderr: string
|
|
40
|
+
cleanup: () => Promise<void>
|
|
41
41
|
}> {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
const client = new Client({
|
|
43
|
+
name: options?.clientName ?? 'test',
|
|
44
|
+
version: '1.0.0',
|
|
45
|
+
})
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
const { transport, stderr } = await createTransport([])
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
let stderrBuffer = ''
|
|
50
|
+
stderr?.on('data', (data) => {
|
|
51
|
+
process.stderr.write(data)
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
stderrBuffer += data.toString()
|
|
54
|
+
})
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
await client.connect(transport)
|
|
57
|
+
await client.ping()
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
59
|
+
const cleanup = async () => {
|
|
60
|
+
try {
|
|
61
|
+
await client.close()
|
|
62
|
+
} catch (e) {
|
|
63
|
+
console.error('Error during MCP client cleanup:', e)
|
|
64
|
+
// Ignore errors during cleanup
|
|
66
65
|
}
|
|
66
|
+
}
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
68
|
+
return {
|
|
69
|
+
client,
|
|
70
|
+
stderr: stderrBuffer,
|
|
71
|
+
cleanup,
|
|
72
|
+
}
|
|
73
73
|
}
|
package/src/mcp.test.ts
CHANGED
|
@@ -1,12 +1,37 @@
|
|
|
1
1
|
import { createMCPClient } from './mcp-client.js'
|
|
2
|
-
import { describe, it, expect,
|
|
3
|
-
import
|
|
2
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
|
|
3
|
+
import { exec } from 'node:child_process'
|
|
4
|
+
import { promisify } from 'node:util'
|
|
5
|
+
|
|
6
|
+
const execAsync = promisify(exec)
|
|
7
|
+
|
|
8
|
+
function js(strings: TemplateStringsArray, ...values: any[]): string {
|
|
9
|
+
return strings.reduce(
|
|
10
|
+
(result, str, i) => result + str + (values[i] || ''),
|
|
11
|
+
'',
|
|
12
|
+
)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function killProcessOnPort(port: number): Promise<void> {
|
|
16
|
+
try {
|
|
17
|
+
const { stdout } = await execAsync(`lsof -ti:${port}`)
|
|
18
|
+
const pid = stdout.trim()
|
|
19
|
+
if (pid) {
|
|
20
|
+
await execAsync(`kill -9 ${pid}`)
|
|
21
|
+
console.log(`Killed process ${pid} on port ${port}`)
|
|
22
|
+
await new Promise((resolve) => setTimeout(resolve, 500))
|
|
23
|
+
}
|
|
24
|
+
} catch (error) {
|
|
25
|
+
// No process running on port or already killed
|
|
26
|
+
}
|
|
27
|
+
}
|
|
4
28
|
|
|
5
29
|
describe('MCP Server Tests', () => {
|
|
6
30
|
let client: Awaited<ReturnType<typeof createMCPClient>>['client']
|
|
7
31
|
let cleanup: (() => Promise<void>) | null = null
|
|
8
32
|
|
|
9
33
|
beforeAll(async () => {
|
|
34
|
+
await killProcessOnPort(19988)
|
|
10
35
|
const result = await createMCPClient()
|
|
11
36
|
client = result.client
|
|
12
37
|
cleanup = result.cleanup
|
|
@@ -19,217 +44,136 @@ describe('MCP Server Tests', () => {
|
|
|
19
44
|
}
|
|
20
45
|
})
|
|
21
46
|
|
|
22
|
-
it('should capture console
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"text": "Created new page. URL: about:blank. Total pages: 20",
|
|
33
|
-
"type": "text",
|
|
47
|
+
it('should execute code and capture console output', async () => {
|
|
48
|
+
await client.callTool({
|
|
49
|
+
name: 'execute',
|
|
50
|
+
arguments: {
|
|
51
|
+
code: js`
|
|
52
|
+
const newPage = await context.newPage();
|
|
53
|
+
state.page = newPage;
|
|
54
|
+
if (!state.pages) state.pages = [];
|
|
55
|
+
state.pages.push(newPage);
|
|
56
|
+
`,
|
|
34
57
|
},
|
|
35
|
-
|
|
36
|
-
`)
|
|
58
|
+
})
|
|
37
59
|
|
|
38
|
-
// Navigate to a page and log something
|
|
39
60
|
const result = await client.callTool({
|
|
40
61
|
name: 'execute',
|
|
41
62
|
arguments: {
|
|
42
|
-
code: `
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
`,
|
|
63
|
+
code: js`
|
|
64
|
+
await state.page.goto('https://news.ycombinator.com');
|
|
65
|
+
const title = await state.page.title();
|
|
66
|
+
console.log('Page title:', title);
|
|
67
|
+
return { url: state.page.url(), title };
|
|
68
|
+
`,
|
|
49
69
|
},
|
|
50
70
|
})
|
|
51
|
-
expect(result.content).toBeDefined()
|
|
52
71
|
expect(result.content).toMatchInlineSnapshot(`
|
|
53
72
|
[
|
|
54
73
|
{
|
|
55
|
-
"text": "
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
name: 'console_logs',
|
|
64
|
-
arguments: {
|
|
65
|
-
limit: 10,
|
|
66
|
-
},
|
|
67
|
-
})) as CallToolResult
|
|
68
|
-
|
|
69
|
-
expect(logsResult.content).toBeDefined()
|
|
70
|
-
expect(logsResult.content).toMatchInlineSnapshot(`
|
|
71
|
-
[
|
|
72
|
-
{
|
|
73
|
-
"text": "[log]: Test log message :1:32
|
|
74
|
-
[error]: Test error message :2:32",
|
|
74
|
+
"text": "Console output:
|
|
75
|
+
[log] Page title: Hacker News
|
|
76
|
+
|
|
77
|
+
Return value:
|
|
78
|
+
{
|
|
79
|
+
"url": "https://news.ycombinator.com/",
|
|
80
|
+
"title": "Hacker News"
|
|
81
|
+
}",
|
|
75
82
|
"type": "text",
|
|
76
83
|
},
|
|
77
84
|
]
|
|
78
85
|
`)
|
|
79
|
-
|
|
80
|
-
// Close the page opened
|
|
81
|
-
await client.callTool({
|
|
82
|
-
name: 'close_page',
|
|
83
|
-
arguments: {},
|
|
84
|
-
})
|
|
86
|
+
expect(result.content).toBeDefined()
|
|
85
87
|
}, 30000)
|
|
86
88
|
|
|
87
|
-
it('should
|
|
88
|
-
// Create new page
|
|
89
|
-
await client.callTool({
|
|
90
|
-
name: 'new_page',
|
|
91
|
-
arguments: {},
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
// Navigate to a specific old Hacker News story that won't change
|
|
89
|
+
it('should get accessibility snapshot of hacker news', async () => {
|
|
95
90
|
await client.callTool({
|
|
96
91
|
name: 'execute',
|
|
97
92
|
arguments: {
|
|
98
|
-
code: `
|
|
93
|
+
code: js`
|
|
94
|
+
const newPage = await context.newPage();
|
|
95
|
+
state.page = newPage;
|
|
96
|
+
if (!state.pages) state.pages = [];
|
|
97
|
+
state.pages.push(newPage);
|
|
98
|
+
`,
|
|
99
99
|
},
|
|
100
100
|
})
|
|
101
101
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
102
|
+
const result = await client.callTool({
|
|
103
|
+
name: 'execute',
|
|
104
|
+
arguments: {
|
|
105
|
+
code: js`
|
|
106
|
+
await state.page.goto('https://news.ycombinator.com/item?id=1', { waitUntil: 'networkidle' });
|
|
107
|
+
const snapshot = await state.page._snapshotForAI();
|
|
108
|
+
return snapshot;
|
|
109
|
+
`,
|
|
110
|
+
},
|
|
106
111
|
})
|
|
107
|
-
expect(initialSnapshot.content).toBeDefined()
|
|
108
112
|
|
|
109
|
-
// Save initial snapshot
|
|
110
113
|
const initialData =
|
|
111
|
-
typeof
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
expect(initialData).toMatchFileSnapshot(
|
|
114
|
+
typeof result === 'object' && result.content?.[0]?.text
|
|
115
|
+
? tryJsonParse(result.content[0].text)
|
|
116
|
+
: result
|
|
117
|
+
await expect(initialData).toMatchFileSnapshot(
|
|
116
118
|
'snapshots/hacker-news-initial-accessibility.md',
|
|
117
119
|
)
|
|
120
|
+
expect(result.content).toBeDefined()
|
|
118
121
|
expect(initialData).toContain('table')
|
|
119
122
|
expect(initialData).toContain('Hacker News')
|
|
123
|
+
}, 30000)
|
|
120
124
|
|
|
121
|
-
|
|
122
|
-
await client.callTool({
|
|
123
|
-
name: 'execute',
|
|
124
|
-
arguments: {
|
|
125
|
-
code: `
|
|
126
|
-
// Find and focus the first link
|
|
127
|
-
const firstLink = await page.$('a')
|
|
128
|
-
if (firstLink) {
|
|
129
|
-
await firstLink.focus()
|
|
130
|
-
const linkText = await firstLink.textContent()
|
|
131
|
-
console.log('Focused on first link:', linkText)
|
|
132
|
-
}
|
|
133
|
-
`,
|
|
134
|
-
},
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
// Get snapshot after focusing
|
|
138
|
-
const focusedSnapshot = await client.callTool({
|
|
139
|
-
name: 'accessibility_snapshot',
|
|
140
|
-
arguments: {},
|
|
141
|
-
})
|
|
142
|
-
expect(focusedSnapshot.content).toBeDefined()
|
|
143
|
-
|
|
144
|
-
// Save focused snapshot
|
|
145
|
-
const focusedData =
|
|
146
|
-
typeof focusedSnapshot === 'object' &&
|
|
147
|
-
focusedSnapshot.content?.[0]?.text
|
|
148
|
-
? tryJsonParse(focusedSnapshot.content[0].text)
|
|
149
|
-
: focusedSnapshot
|
|
150
|
-
expect(focusedData).toMatchFileSnapshot(
|
|
151
|
-
'snapshots/hacker-news-focused-accessibility.md',
|
|
152
|
-
)
|
|
153
|
-
|
|
154
|
-
// Verify the snapshot contains expected content
|
|
155
|
-
expect(focusedData).toBeDefined()
|
|
156
|
-
expect(focusedData).toContain('link')
|
|
157
|
-
|
|
158
|
-
// Press Tab to go to next item
|
|
125
|
+
it('should get accessibility snapshot of shadcn UI', async () => {
|
|
159
126
|
await client.callTool({
|
|
160
127
|
name: 'execute',
|
|
161
128
|
arguments: {
|
|
162
|
-
code: `
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
129
|
+
code: js`
|
|
130
|
+
const newPage = await context.newPage();
|
|
131
|
+
state.page = newPage;
|
|
132
|
+
if (!state.pages) state.pages = [];
|
|
133
|
+
state.pages.push(newPage);
|
|
134
|
+
`,
|
|
166
135
|
},
|
|
167
136
|
})
|
|
168
137
|
|
|
169
|
-
|
|
170
|
-
const tabbedSnapshot = await client.callTool({
|
|
171
|
-
name: 'accessibility_snapshot',
|
|
172
|
-
arguments: {},
|
|
173
|
-
})
|
|
174
|
-
expect(tabbedSnapshot.content).toBeDefined()
|
|
175
|
-
|
|
176
|
-
// Save tabbed snapshot
|
|
177
|
-
const tabbedData =
|
|
178
|
-
typeof tabbedSnapshot === 'object' &&
|
|
179
|
-
tabbedSnapshot.content?.[0]?.text
|
|
180
|
-
? tryJsonParse(tabbedSnapshot.content[0].text)
|
|
181
|
-
: tabbedSnapshot
|
|
182
|
-
expect(tabbedData).toMatchFileSnapshot(
|
|
183
|
-
'snapshots/hacker-news-tabbed-accessibility.md',
|
|
184
|
-
)
|
|
185
|
-
|
|
186
|
-
// Verify the snapshot is different
|
|
187
|
-
expect(tabbedData).toBeDefined()
|
|
188
|
-
expect(tabbedData).toContain('Hacker News')
|
|
189
|
-
|
|
190
|
-
// Close the page opened
|
|
191
|
-
await client.callTool({
|
|
192
|
-
name: 'close_page',
|
|
193
|
-
arguments: {},
|
|
194
|
-
})
|
|
195
|
-
}, 30000)
|
|
196
|
-
|
|
197
|
-
it('should capture accessibility snapshot of shadcn UI', async () => {
|
|
198
|
-
// Create new page
|
|
199
|
-
await client.callTool({
|
|
200
|
-
name: 'new_page',
|
|
201
|
-
arguments: {},
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
// Navigate to shadcn UI
|
|
205
|
-
await client.callTool({
|
|
138
|
+
const snapshot = await client.callTool({
|
|
206
139
|
name: 'execute',
|
|
207
140
|
arguments: {
|
|
208
|
-
code: `
|
|
141
|
+
code: js`
|
|
142
|
+
await state.page.goto('https://ui.shadcn.com/', { waitUntil: 'networkidle' });
|
|
143
|
+
const snapshot = await state.page._snapshotForAI();
|
|
144
|
+
return snapshot;
|
|
145
|
+
`,
|
|
209
146
|
},
|
|
210
147
|
})
|
|
211
148
|
|
|
212
|
-
// Get accessibility snapshot
|
|
213
|
-
const snapshot = await client.callTool({
|
|
214
|
-
name: 'accessibility_snapshot',
|
|
215
|
-
arguments: {},
|
|
216
|
-
})
|
|
217
|
-
expect(snapshot.content).toBeDefined()
|
|
218
|
-
|
|
219
|
-
// Save snapshot
|
|
220
149
|
const data =
|
|
221
150
|
typeof snapshot === 'object' && snapshot.content?.[0]?.text
|
|
222
151
|
? tryJsonParse(snapshot.content[0].text)
|
|
223
152
|
: snapshot
|
|
224
|
-
expect(data).toMatchFileSnapshot('snapshots/shadcn-ui-accessibility.md')
|
|
153
|
+
await expect(data).toMatchFileSnapshot('snapshots/shadcn-ui-accessibility.md')
|
|
154
|
+
expect(snapshot.content).toBeDefined()
|
|
225
155
|
expect(data).toContain('shadcn')
|
|
156
|
+
}, 30000)
|
|
226
157
|
|
|
227
|
-
|
|
228
|
-
await client.callTool({
|
|
229
|
-
name: '
|
|
230
|
-
arguments: {
|
|
158
|
+
it('should close all created pages', async () => {
|
|
159
|
+
const result = await client.callTool({
|
|
160
|
+
name: 'execute',
|
|
161
|
+
arguments: {
|
|
162
|
+
code: js`
|
|
163
|
+
if (state.pages && state.pages.length > 0) {
|
|
164
|
+
for (const page of state.pages) {
|
|
165
|
+
await page.close();
|
|
166
|
+
}
|
|
167
|
+
const closedCount = state.pages.length;
|
|
168
|
+
state.pages = [];
|
|
169
|
+
return { closedCount };
|
|
170
|
+
}
|
|
171
|
+
return { closedCount: 0 };
|
|
172
|
+
`,
|
|
173
|
+
},
|
|
231
174
|
})
|
|
232
|
-
|
|
175
|
+
|
|
176
|
+
})
|
|
233
177
|
})
|
|
234
178
|
function tryJsonParse(str: string) {
|
|
235
179
|
try {
|