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.
Files changed (46) hide show
  1. package/bin.js +1 -1
  2. package/dist/browser-config.js +1 -3
  3. package/dist/browser-config.js.map +1 -1
  4. package/dist/cdp-types.d.ts +25 -0
  5. package/dist/cdp-types.d.ts.map +1 -0
  6. package/dist/cdp-types.js +91 -0
  7. package/dist/cdp-types.js.map +1 -0
  8. package/dist/extension/cdp-relay.d.ts +12 -0
  9. package/dist/extension/cdp-relay.d.ts.map +1 -0
  10. package/dist/extension/cdp-relay.js +378 -0
  11. package/dist/extension/cdp-relay.js.map +1 -0
  12. package/dist/extension/protocol.d.ts +29 -0
  13. package/dist/extension/protocol.d.ts.map +1 -0
  14. package/dist/extension/protocol.js +2 -0
  15. package/dist/extension/protocol.js.map +1 -0
  16. package/dist/index.d.ts +2 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +2 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/mcp-client.d.ts.map +1 -1
  21. package/dist/mcp-client.js +1 -1
  22. package/dist/mcp-client.js.map +1 -1
  23. package/dist/mcp.js +74 -464
  24. package/dist/mcp.js.map +1 -1
  25. package/dist/mcp.test.js +101 -142
  26. package/dist/mcp.test.js.map +1 -1
  27. package/dist/prompt.md +41 -487
  28. package/dist/resource.md +436 -0
  29. package/dist/start-relay-server.d.ts +8 -0
  30. package/dist/start-relay-server.d.ts.map +1 -0
  31. package/dist/start-relay-server.js +33 -0
  32. package/dist/start-relay-server.js.map +1 -0
  33. package/package.json +42 -36
  34. package/src/browser-config.ts +48 -50
  35. package/src/cdp-types.ts +124 -0
  36. package/src/extension/cdp-relay.ts +480 -0
  37. package/src/extension/protocol.ts +34 -0
  38. package/src/index.ts +1 -0
  39. package/src/mcp-client.ts +46 -46
  40. package/src/mcp.test.ts +109 -165
  41. package/src/mcp.ts +202 -694
  42. package/src/prompt.md +41 -487
  43. package/src/resource.md +436 -0
  44. package/src/snapshots/hacker-news-initial-accessibility.md +243 -127
  45. package/src/snapshots/shadcn-ui-accessibility.md +300 -510
  46. 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
- clientName?: string
11
+ clientName?: string
12
12
  }
13
13
 
14
14
  export async function createTransport(args: string[]): Promise<{
15
- transport: Transport
16
- stderr: Stream | null
15
+ transport: Transport
16
+ stderr: Stream | null
17
17
  }> {
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
- })
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
- return {
32
- transport,
33
- stderr: transport.stderr!,
34
- }
31
+ return {
32
+ transport,
33
+ stderr: transport.stderr!,
34
+ }
35
35
  }
36
36
 
37
37
  export async function createMCPClient(options?: CreateTransportOptions): Promise<{
38
- client: Client
39
- stderr: string
40
- cleanup: () => Promise<void>
38
+ client: Client
39
+ stderr: string
40
+ cleanup: () => Promise<void>
41
41
  }> {
42
- const client = new Client({
43
- name: options?.clientName ?? 'test',
44
- version: '1.0.0'
45
- })
42
+ const client = new Client({
43
+ name: options?.clientName ?? 'test',
44
+ version: '1.0.0',
45
+ })
46
46
 
47
- const { transport, stderr } = await createTransport([])
47
+ const { transport, stderr } = await createTransport([])
48
48
 
49
- let stderrBuffer = ''
50
- stderr?.on('data', (data) => {
51
- process.stderr.write(data)
49
+ let stderrBuffer = ''
50
+ stderr?.on('data', (data) => {
51
+ process.stderr.write(data)
52
52
 
53
- stderrBuffer += data.toString()
54
- })
53
+ stderrBuffer += data.toString()
54
+ })
55
55
 
56
- await client.connect(transport)
57
- await client.ping()
56
+ await client.connect(transport)
57
+ await client.ping()
58
58
 
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
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
- return {
69
- client,
70
- stderr: stderrBuffer,
71
- cleanup,
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, afterEach, beforeAll, afterAll } from 'vitest'
3
- import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'
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 logs', async () => {
23
- // Connect first (open a new page)
24
- const connectResult = await client.callTool({
25
- name: 'new_page',
26
- arguments: {},
27
- })
28
- expect(connectResult.content).toBeDefined()
29
- expect(connectResult.content).toMatchInlineSnapshot(`
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
- await page.goto('https://news.ycombinator.com');
44
- await page.evaluate(() => {
45
- console.log('Test log message');
46
- console.error('Test error message');
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": "Code executed successfully (no output)",
56
- "type": "text",
57
- },
58
- ]
59
- `)
60
-
61
- // Get console logs
62
- const logsResult = (await client.callTool({
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 capture accessibility snapshot of hacker news', async () => {
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: `await page.goto('https://news.ycombinator.com/item?id=1', { waitUntil: 'networkidle' })`,
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
- // Get initial accessibility snapshot
103
- const initialSnapshot = await client.callTool({
104
- name: 'accessibility_snapshot',
105
- arguments: {},
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 initialSnapshot === 'object' &&
112
- initialSnapshot.content?.[0]?.text
113
- ? tryJsonParse(initialSnapshot.content[0].text)
114
- : initialSnapshot
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
- // Focus on first link on the page
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
- await page.keyboard.press('Tab')
164
- console.log('Pressed Tab key')
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
- // Get snapshot after tab navigation
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: `await page.goto('https://ui.shadcn.com/', { waitUntil: 'networkidle' })`,
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
- // Close the page opened
228
- await client.callTool({
229
- name: 'close_page',
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
- }, 30000)
175
+
176
+ })
233
177
  })
234
178
  function tryJsonParse(str: string) {
235
179
  try {