playwriter 0.0.25 → 0.0.29

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 (105) hide show
  1. package/bin.js +1 -1
  2. package/dist/bippy.js +966 -0
  3. package/dist/{extension/cdp-relay.d.ts → cdp-relay.d.ts} +3 -2
  4. package/dist/cdp-relay.d.ts.map +1 -0
  5. package/dist/{extension/cdp-relay.js → cdp-relay.js} +101 -3
  6. package/dist/cdp-relay.js.map +1 -0
  7. package/dist/cdp-session.d.ts +1 -1
  8. package/dist/cdp-session.d.ts.map +1 -1
  9. package/dist/cdp-session.js +4 -4
  10. package/dist/cdp-session.js.map +1 -1
  11. package/dist/cli.d.ts +3 -0
  12. package/dist/cli.d.ts.map +1 -0
  13. package/dist/cli.js +71 -0
  14. package/dist/cli.js.map +1 -0
  15. package/dist/create-logger.d.ts.map +1 -1
  16. package/dist/create-logger.js +2 -1
  17. package/dist/create-logger.js.map +1 -1
  18. package/dist/debugger-examples-types.d.ts +18 -0
  19. package/dist/debugger-examples-types.d.ts.map +1 -0
  20. package/dist/debugger-examples-types.js +2 -0
  21. package/dist/debugger-examples-types.js.map +1 -0
  22. package/dist/debugger-examples.d.ts +6 -0
  23. package/dist/debugger-examples.d.ts.map +1 -0
  24. package/dist/debugger-examples.js +53 -0
  25. package/dist/debugger-examples.js.map +1 -0
  26. package/dist/debugger-examples.ts +66 -0
  27. package/dist/debugger.d.ts +380 -0
  28. package/dist/debugger.d.ts.map +1 -0
  29. package/dist/debugger.js +631 -0
  30. package/dist/debugger.js.map +1 -0
  31. package/dist/editor-examples.d.ts +11 -0
  32. package/dist/editor-examples.d.ts.map +1 -0
  33. package/dist/editor-examples.js +124 -0
  34. package/dist/editor-examples.js.map +1 -0
  35. package/dist/editor.d.ts +203 -0
  36. package/dist/editor.d.ts.map +1 -0
  37. package/dist/editor.js +335 -0
  38. package/dist/editor.js.map +1 -0
  39. package/dist/index.d.ts +1 -1
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +1 -1
  42. package/dist/index.js.map +1 -1
  43. package/dist/mcp-client.d.ts +5 -1
  44. package/dist/mcp-client.d.ts.map +1 -1
  45. package/dist/mcp-client.js +13 -9
  46. package/dist/mcp-client.js.map +1 -1
  47. package/dist/mcp.d.ts +4 -1
  48. package/dist/mcp.d.ts.map +1 -1
  49. package/dist/mcp.js +170 -27
  50. package/dist/mcp.js.map +1 -1
  51. package/dist/mcp.test.d.ts.map +1 -1
  52. package/dist/mcp.test.js +886 -182
  53. package/dist/mcp.test.js.map +1 -1
  54. package/dist/prompt.md +86 -6
  55. package/dist/{extension/protocol.d.ts → protocol.d.ts} +1 -1
  56. package/dist/protocol.d.ts.map +1 -0
  57. package/dist/protocol.js.map +1 -0
  58. package/dist/react-source.d.ts +13 -0
  59. package/dist/react-source.d.ts.map +1 -0
  60. package/dist/react-source.js +66 -0
  61. package/dist/react-source.js.map +1 -0
  62. package/dist/selector-generator.js +7065 -18
  63. package/dist/start-relay-server.d.ts +4 -2
  64. package/dist/start-relay-server.d.ts.map +1 -1
  65. package/dist/start-relay-server.js +3 -3
  66. package/dist/start-relay-server.js.map +1 -1
  67. package/dist/styles.d.ts +27 -0
  68. package/dist/styles.d.ts.map +1 -0
  69. package/dist/styles.js +232 -0
  70. package/dist/styles.js.map +1 -0
  71. package/dist/utils.d.ts +3 -1
  72. package/dist/utils.d.ts.map +1 -1
  73. package/dist/utils.js +7 -3
  74. package/dist/utils.js.map +1 -1
  75. package/dist/wait-for-page-load.d.ts.map +1 -1
  76. package/dist/wait-for-page-load.js +3 -2
  77. package/dist/wait-for-page-load.js.map +1 -1
  78. package/package.json +5 -2
  79. package/src/{extension/cdp-relay.ts → cdp-relay.ts} +109 -5
  80. package/src/cdp-session.ts +4 -4
  81. package/src/cdp-timing.md +128 -0
  82. package/src/cli.ts +85 -0
  83. package/src/create-logger.ts +2 -1
  84. package/src/debugger-examples-types.ts +10 -0
  85. package/src/debugger-examples.ts +66 -0
  86. package/src/debugger.ts +711 -0
  87. package/src/editor-examples.ts +148 -0
  88. package/src/editor.ts +389 -0
  89. package/src/index.ts +1 -1
  90. package/src/mcp-client.ts +14 -9
  91. package/src/mcp.test.ts +1053 -196
  92. package/src/mcp.ts +195 -30
  93. package/src/prompt.md +86 -6
  94. package/src/{extension/protocol.ts → protocol.ts} +1 -1
  95. package/src/react-source.ts +92 -0
  96. package/src/snapshots/shadcn-ui-accessibility.md +57 -57
  97. package/src/start-relay-server.ts +3 -3
  98. package/src/styles.ts +343 -0
  99. package/src/utils.ts +8 -3
  100. package/src/wait-for-page-load.ts +3 -2
  101. package/dist/extension/cdp-relay.d.ts.map +0 -1
  102. package/dist/extension/cdp-relay.js.map +0 -1
  103. package/dist/extension/protocol.d.ts.map +0 -1
  104. package/dist/extension/protocol.js.map +0 -1
  105. /package/dist/{extension/protocol.js → protocol.js} +0 -0
@@ -101,7 +101,7 @@ export class CDPSession {
101
101
  this.eventListeners.get(event)?.delete(callback as (params: unknown) => void)
102
102
  }
103
103
 
104
- detach() {
104
+ close() {
105
105
  try {
106
106
  for (const pending of this.pendingRequests.values()) {
107
107
  pending.reject(new Error('CDPSession detached'))
@@ -128,7 +128,7 @@ export async function getCDPSessionForPage({ page, wsUrl }: { page: Page; wsUrl:
128
128
  const pages = page.context().pages()
129
129
  const pageIndex = pages.indexOf(page)
130
130
  if (pageIndex === -1) {
131
- cdp.detach()
131
+ cdp.close()
132
132
  throw new Error('Page not found in context')
133
133
  }
134
134
 
@@ -136,13 +136,13 @@ export async function getCDPSessionForPage({ page, wsUrl }: { page: Page; wsUrl:
136
136
  const pageTargets = targetInfos.filter((t) => t.type === 'page')
137
137
 
138
138
  if (pageIndex >= pageTargets.length) {
139
- cdp.detach()
139
+ cdp.close()
140
140
  throw new Error(`Page index ${pageIndex} out of bounds (${pageTargets.length} targets)`)
141
141
  }
142
142
 
143
143
  const target = pageTargets[pageIndex]
144
144
  if (target.url !== page.url()) {
145
- cdp.detach()
145
+ cdp.close()
146
146
  throw new Error(`URL mismatch: page has "${page.url()}" but target has "${target.url}"`)
147
147
  }
148
148
 
@@ -0,0 +1,128 @@
1
+ # CDP Event Timing and Synchronization
2
+
3
+ This document describes the timing issues we discovered and fixed in the CDP relay server, and outlines potential future improvements.
4
+
5
+ ## The Problem
6
+
7
+ When Playwright connects to Chrome via our CDP relay, there's a race condition between:
8
+ 1. **Target attachment** - Extension attaches to a tab and sends `Target.attachedToTarget`
9
+ 2. **Runtime initialization** - Playwright calls `Runtime.enable` to set up JavaScript execution contexts
10
+ 3. **Page visibility** - `context.pages()` returns pages that are fully ready
11
+
12
+ ### Symptom
13
+ Tests or MCP calls that run immediately after toggling the extension would fail with "page not found" because `context.pages()` didn't include the newly attached page yet.
14
+
15
+ ### Root Cause
16
+ The `Runtime.enable` CDP command triggers `Runtime.executionContextCreated` events that tell Playwright the page's JavaScript context is ready. Without these events, pages aren't fully visible to `context.pages()`.
17
+
18
+ Previously, the extension had an arbitrary `sleep(200ms)` in the `Runtime.enable` handler to work around this. When this was increased to 400ms, tests started failing due to timing mismatches.
19
+
20
+ ## The Fix
21
+
22
+ We replaced the arbitrary sleep with **event-based synchronization** in the relay server:
23
+
24
+ ```typescript
25
+ case 'Runtime.enable': {
26
+ // Set up listener for executionContextCreated
27
+ const contextCreatedPromise = new Promise<void>((resolve) => {
28
+ const handler = ({ event }) => {
29
+ if (event.method === 'Runtime.executionContextCreated' &&
30
+ event.sessionId === sessionId) {
31
+ clearTimeout(timeout)
32
+ emitter.off('cdp:event', handler)
33
+ resolve()
34
+ }
35
+ }
36
+ const timeout = setTimeout(() => {
37
+ emitter.off('cdp:event', handler)
38
+ logger?.log('IMPORTANT: Runtime.enable timed out...')
39
+ resolve()
40
+ }, 3000)
41
+ emitter.on('cdp:event', handler)
42
+ })
43
+
44
+ // Forward command to extension
45
+ const result = await sendToExtension(...)
46
+
47
+ // Wait for the event before returning
48
+ await contextCreatedPromise
49
+
50
+ return result
51
+ }
52
+ ```
53
+
54
+ ### Why This Works
55
+ - When Playwright calls `Runtime.enable`, we forward it to the extension
56
+ - The extension enables Runtime on the Chrome tab
57
+ - Chrome sends `Runtime.executionContextCreated` events
58
+ - We wait for at least one such event before returning
59
+ - By the time `Runtime.enable` returns, the page's context is ready
60
+
61
+ ## Event Flow
62
+
63
+ ```
64
+ Test toggles extension
65
+
66
+ Extension attaches to tab
67
+
68
+ Extension sends Target.attachedToTarget to relay
69
+
70
+ Relay broadcasts to Playwright clients
71
+
72
+ Playwright calls Runtime.enable ──────────────────┐
73
+ ↓ │
74
+ Relay forwards to extension │
75
+ ↓ │
76
+ Extension enables Runtime on Chrome tab │
77
+ ↓ │
78
+ Chrome sends Runtime.executionContextCreated │
79
+ ↓ │
80
+ Relay receives event, resolves promise ───────────┘
81
+
82
+ Runtime.enable returns to Playwright
83
+
84
+ Page is now visible in context.pages()
85
+ ```
86
+
87
+ ## Future Improvements
88
+
89
+ ### 1. Wait for Target Attachment
90
+
91
+ Currently, tests still need small waits after `toggleExtensionForActiveTab()` because the MCP's Playwright browser needs time to process `Target.attachedToTarget`.
92
+
93
+ A potential improvement: Track "pending" vs "ready" targets in the relay server:
94
+ - When `Target.attachedToTarget` arrives → mark as pending
95
+ - When `Runtime.executionContextCreated` arrives → mark as ready
96
+ - Expose an endpoint or mechanism for MCP to wait for all targets to be ready
97
+
98
+ ### 2. Extension-Level Confirmation
99
+
100
+ The extension could wait for confirmation from the relay server before returning from `attachTab()`:
101
+ - Extension sends `Target.attachedToTarget`
102
+ - Relay waits for `Runtime.executionContextCreated`
103
+ - Relay sends acknowledgment back to extension
104
+ - Extension's `attachTab()` returns
105
+ - `toggleExtensionForActiveTab()` returns with page fully ready
106
+
107
+ This would eliminate the need for any waits in test code.
108
+
109
+ ### 3. MCP-Level Page Readiness Check
110
+
111
+ The MCP could check page readiness before executing code:
112
+ - Before running user code, verify each page in `context.pages()` has a ready execution context
113
+ - Use Playwright's `page.evaluate()` with a simple expression to confirm the page is responsive
114
+
115
+ ## Test Implications
116
+
117
+ The current test waits (100ms after toggle) exist for multiple reasons:
118
+ 1. **Target attachment** - Waiting for `Target.attachedToTarget` to be processed
119
+ 2. **Navigation completion** - Waiting for page loads/navigations
120
+ 3. **State cleanup** - Ensuring previous test state is cleared
121
+ 4. **Debugger synchronization** - Waiting for breakpoints/pause states
122
+
123
+ The `Runtime.enable` fix addresses reason #1 at the CDP level, but test waits are still needed for the other reasons.
124
+
125
+ ## Files Changed
126
+
127
+ - `playwriter/src/cdp-relay.ts` - Added event-based wait in `Runtime.enable` handler
128
+ - `extension/src/background.ts` - Removed arbitrary `sleep(200)` from `Runtime.enable` handler
package/src/cli.ts ADDED
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { cac } from 'cac'
4
+ import { startPlayWriterCDPRelayServer } from './cdp-relay.js'
5
+ import { createFileLogger } from './create-logger.js'
6
+ import { VERSION } from './utils.js'
7
+
8
+ const RELAY_PORT = 19988
9
+
10
+ const cli = cac('playwriter')
11
+
12
+ cli
13
+ .command('', 'Start the MCP server (default)')
14
+ .option('--host <host>', 'Remote relay server host to connect to (or use PLAYWRITER_HOST env var)')
15
+ .option('--token <token>', 'Authentication token (or use PLAYWRITER_TOKEN env var)')
16
+ .action(async (options: { host?: string; token?: string }) => {
17
+ const { startMcp } = await import('./mcp.js')
18
+ await startMcp({
19
+ host: options.host,
20
+ token: options.token,
21
+ })
22
+ })
23
+
24
+ cli
25
+ .command('serve', 'Start the CDP relay server for remote MCP connections')
26
+ .option('--host <host>', 'Host to bind to', { default: '0.0.0.0' })
27
+ .option('--token <token>', 'Authentication token (or use PLAYWRITER_TOKEN env var)')
28
+ .action(async (options: { host: string; token?: string }) => {
29
+ const token = options.token || process.env.PLAYWRITER_TOKEN
30
+ if (!token) {
31
+ console.error('Error: Authentication token is required.')
32
+ console.error('Provide --token <token> or set PLAYWRITER_TOKEN environment variable.')
33
+ process.exit(1)
34
+ }
35
+
36
+ const logger = createFileLogger()
37
+
38
+ process.title = 'playwriter-serve'
39
+
40
+ process.on('uncaughtException', async (err) => {
41
+ await logger.error('Uncaught Exception:', err)
42
+ process.exit(1)
43
+ })
44
+
45
+ process.on('unhandledRejection', async (reason) => {
46
+ await logger.error('Unhandled Rejection:', reason)
47
+ process.exit(1)
48
+ })
49
+
50
+ const server = await startPlayWriterCDPRelayServer({
51
+ port: RELAY_PORT,
52
+ host: options.host,
53
+ token,
54
+ logger,
55
+ })
56
+
57
+ console.log('Playwriter CDP relay server started')
58
+ console.log(` Host: ${options.host}`)
59
+ console.log(` Port: ${RELAY_PORT}`)
60
+ console.log(` Token: (configured)`)
61
+ console.log(` Logs: ${logger.logFilePath}`)
62
+ console.log('')
63
+ console.log('Endpoints:')
64
+ console.log(` Extension: ws://${options.host}:${RELAY_PORT}/extension`)
65
+ console.log(` CDP: ws://${options.host}:${RELAY_PORT}/cdp/<client-id>?token=<token>`)
66
+ console.log('')
67
+ console.log('Press Ctrl+C to stop.')
68
+
69
+ process.on('SIGINT', () => {
70
+ console.log('\nShutting down...')
71
+ server.close()
72
+ process.exit(0)
73
+ })
74
+
75
+ process.on('SIGTERM', () => {
76
+ console.log('\nShutting down...')
77
+ server.close()
78
+ process.exit(0)
79
+ })
80
+ })
81
+
82
+ cli.help()
83
+ cli.version(VERSION)
84
+
85
+ cli.parse()
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import util from 'node:util'
4
+ import stripAnsi from 'strip-ansi'
4
5
  import { LOG_FILE_PATH } from './utils.js'
5
6
 
6
7
  export type Logger = {
@@ -23,7 +24,7 @@ export function createFileLogger({ logFilePath }: { logFilePath?: string } = {})
23
24
  const message = args.map(arg =>
24
25
  typeof arg === 'string' ? arg : util.inspect(arg, { depth: null, colors: false })
25
26
  ).join(' ')
26
- queue = queue.then(() => fs.promises.appendFile(resolvedLogFilePath, message + '\n'))
27
+ queue = queue.then(() => fs.promises.appendFile(resolvedLogFilePath, stripAnsi(message) + '\n'))
27
28
  return queue
28
29
  }
29
30
 
@@ -0,0 +1,10 @@
1
+ import type { Page } from 'playwright-core'
2
+ import type { CDPSession } from './cdp-session.js'
3
+ import type { Debugger } from './debugger.js'
4
+ import type { Editor } from './editor.js'
5
+
6
+ export declare const page: Page
7
+ export declare const getCDPSession: (options: { page: Page }) => Promise<CDPSession>
8
+ export declare const createDebugger: (options: { cdp: CDPSession }) => Debugger
9
+ export declare const createEditor: (options: { cdp: CDPSession }) => Editor
10
+ export declare const console: { log: (...args: unknown[]) => void }
@@ -0,0 +1,66 @@
1
+ import { page, getCDPSession, createDebugger, console } from './debugger-examples-types.js'
2
+
3
+ // Example: List available scripts and set a breakpoint
4
+ async function listScriptsAndSetBreakpoint() {
5
+ const cdp = await getCDPSession({ page })
6
+ const dbg = createDebugger({ cdp })
7
+ await dbg.enable()
8
+
9
+ const scripts = await dbg.listScripts({ search: 'app' })
10
+ console.log(scripts)
11
+
12
+ if (scripts.length > 0) {
13
+ const bpId = await dbg.setBreakpoint({ file: scripts[0].url, line: 100 })
14
+ console.log('Breakpoint set:', bpId)
15
+ }
16
+ }
17
+
18
+ // Example: Inspect state when paused at a breakpoint
19
+ async function inspectWhenPaused() {
20
+ const cdp = await getCDPSession({ page })
21
+ const dbg = createDebugger({ cdp })
22
+ await dbg.enable()
23
+
24
+ if (dbg.isPaused()) {
25
+ const loc = await dbg.getLocation()
26
+ console.log('Paused at:', loc.url, 'line', loc.lineNumber)
27
+ console.log('Source:', loc.sourceContext)
28
+
29
+ const vars = await dbg.inspectLocalVariables()
30
+ console.log('Variables:', vars)
31
+
32
+ const result = await dbg.evaluate({ expression: 'myVar.length' })
33
+ console.log('myVar.length =', result.value)
34
+
35
+ await dbg.stepOver()
36
+ }
37
+ }
38
+
39
+ // Example: Step through code
40
+ async function stepThroughCode() {
41
+ const cdp = await getCDPSession({ page })
42
+ const dbg = createDebugger({ cdp })
43
+ await dbg.enable()
44
+
45
+ await dbg.setBreakpoint({ file: 'https://example.com/app.js', line: 42 })
46
+
47
+ if (dbg.isPaused()) {
48
+ await dbg.stepOver()
49
+ await dbg.stepInto()
50
+ await dbg.stepOut()
51
+ await dbg.resume()
52
+ }
53
+ }
54
+
55
+ // Example: Cleanup all breakpoints
56
+ async function cleanupBreakpoints() {
57
+ const cdp = await getCDPSession({ page })
58
+ const dbg = createDebugger({ cdp })
59
+
60
+ const breakpoints = dbg.listBreakpoints()
61
+ for (const bp of breakpoints) {
62
+ await dbg.deleteBreakpoint({ breakpointId: bp.id })
63
+ }
64
+ }
65
+
66
+ export { listScriptsAndSetBreakpoint, inspectWhenPaused, stepThroughCode, cleanupBreakpoints }