playwriter 0.0.0 → 0.0.2

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.
@@ -0,0 +1,73 @@
1
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js'
2
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
3
+ import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'
4
+ import { Stream } from 'node:stream'
5
+ import path from 'node:path'
6
+ import url from 'node:url'
7
+
8
+ const __filename = url.fileURLToPath(import.meta.url)
9
+
10
+ export interface CreateTransportOptions {
11
+ clientName?: string
12
+ }
13
+
14
+ export async function createTransport(args: string[]): Promise<{
15
+ transport: Transport
16
+ stderr: Stream | null
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
+ })
30
+
31
+ return {
32
+ transport,
33
+ stderr: transport.stderr!,
34
+ }
35
+ }
36
+
37
+ export async function createMCPClient(options?: CreateTransportOptions): Promise<{
38
+ client: Client
39
+ stderr: string
40
+ cleanup: () => Promise<void>
41
+ }> {
42
+ const client = new Client({
43
+ name: options?.clientName ?? 'test',
44
+ version: '1.0.0'
45
+ })
46
+
47
+ const { transport, stderr } = await createTransport([])
48
+
49
+ let stderrBuffer = ''
50
+ stderr?.on('data', (data) => {
51
+ process.stderr.write(data)
52
+
53
+ stderrBuffer += data.toString()
54
+ })
55
+
56
+ await client.connect(transport)
57
+ await client.ping()
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
+ }
66
+ }
67
+
68
+ return {
69
+ client,
70
+ stderr: stderrBuffer,
71
+ cleanup,
72
+ }
73
+ }
@@ -0,0 +1,240 @@
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'
4
+
5
+ describe('MCP Server Tests', () => {
6
+ let client: Awaited<ReturnType<typeof createMCPClient>>['client']
7
+ let cleanup: (() => Promise<void>) | null = null
8
+
9
+ beforeAll(async () => {
10
+ const result = await createMCPClient()
11
+ client = result.client
12
+ cleanup = result.cleanup
13
+ })
14
+
15
+ afterAll(async () => {
16
+ if (cleanup) {
17
+ await cleanup()
18
+ cleanup = null
19
+ }
20
+ })
21
+
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",
34
+ },
35
+ ]
36
+ `)
37
+
38
+ // Navigate to a page and log something
39
+ const result = await client.callTool({
40
+ name: 'execute',
41
+ 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
+ `,
49
+ },
50
+ })
51
+ expect(result.content).toBeDefined()
52
+ expect(result.content).toMatchInlineSnapshot(`
53
+ [
54
+ {
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",
75
+ "type": "text",
76
+ },
77
+ ]
78
+ `)
79
+
80
+ // Close the page opened
81
+ await client.callTool({
82
+ name: 'close_page',
83
+ arguments: {},
84
+ })
85
+ }, 30000)
86
+
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
95
+ await client.callTool({
96
+ name: 'execute',
97
+ arguments: {
98
+ code: `await page.goto('https://news.ycombinator.com/item?id=1', { waitUntil: 'networkidle' })`,
99
+ },
100
+ })
101
+
102
+ // Get initial accessibility snapshot
103
+ const initialSnapshot = await client.callTool({
104
+ name: 'accessibility_snapshot',
105
+ arguments: {},
106
+ })
107
+ expect(initialSnapshot.content).toBeDefined()
108
+
109
+ // Save initial snapshot
110
+ const initialData =
111
+ typeof initialSnapshot === 'object' &&
112
+ initialSnapshot.content?.[0]?.text
113
+ ? tryJsonParse(initialSnapshot.content[0].text)
114
+ : initialSnapshot
115
+ expect(initialData).toMatchFileSnapshot(
116
+ 'snapshots/hacker-news-initial-accessibility.md',
117
+ )
118
+ expect(initialData).toContain('table')
119
+ expect(initialData).toContain('Hacker News')
120
+
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
159
+ await client.callTool({
160
+ name: 'execute',
161
+ arguments: {
162
+ code: `
163
+ await page.keyboard.press('Tab')
164
+ console.log('Pressed Tab key')
165
+ `,
166
+ },
167
+ })
168
+
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({
206
+ name: 'execute',
207
+ arguments: {
208
+ code: `await page.goto('https://ui.shadcn.com/', { waitUntil: 'networkidle' })`,
209
+ },
210
+ })
211
+
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
+ const data =
221
+ typeof snapshot === 'object' && snapshot.content?.[0]?.text
222
+ ? tryJsonParse(snapshot.content[0].text)
223
+ : snapshot
224
+ expect(data).toMatchFileSnapshot('snapshots/shadcn-ui-accessibility.md')
225
+ expect(data).toContain('shadcn')
226
+
227
+ // Close the page opened
228
+ await client.callTool({
229
+ name: 'close_page',
230
+ arguments: {},
231
+ })
232
+ }, 30000)
233
+ })
234
+ function tryJsonParse(str: string) {
235
+ try {
236
+ return JSON.parse(str)
237
+ } catch {
238
+ return str
239
+ }
240
+ }