playwriter 0.0.16 → 0.0.21
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/cdp-session.d.ts +21 -0
- package/dist/cdp-session.d.ts.map +1 -0
- package/dist/cdp-session.js +131 -0
- package/dist/cdp-session.js.map +1 -0
- package/dist/cdp-types.d.ts +15 -0
- package/dist/cdp-types.d.ts.map +1 -1
- package/dist/cdp-types.js.map +1 -1
- package/dist/create-logger.d.ts +9 -0
- package/dist/create-logger.d.ts.map +1 -0
- package/dist/create-logger.js +43 -0
- package/dist/create-logger.js.map +1 -0
- package/dist/extension/cdp-relay.d.ts +7 -3
- package/dist/extension/cdp-relay.d.ts.map +1 -1
- package/dist/extension/cdp-relay.js +22 -12
- package/dist/extension/cdp-relay.js.map +1 -1
- package/dist/mcp.js +86 -44
- package/dist/mcp.js.map +1 -1
- package/dist/mcp.test.d.ts.map +1 -1
- package/dist/mcp.test.js +669 -183
- package/dist/mcp.test.js.map +1 -1
- package/dist/prompt.md +38 -8
- package/dist/selector-generator.js +331 -0
- package/dist/start-relay-server.d.ts +1 -3
- package/dist/start-relay-server.d.ts.map +1 -1
- package/dist/start-relay-server.js +3 -16
- package/dist/start-relay-server.js.map +1 -1
- package/dist/utils.d.ts +3 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +36 -0
- package/dist/utils.js.map +1 -1
- package/dist/wait-for-page-load.d.ts +16 -0
- package/dist/wait-for-page-load.d.ts.map +1 -0
- package/dist/wait-for-page-load.js +126 -0
- package/dist/wait-for-page-load.js.map +1 -0
- package/package.json +16 -12
- package/src/cdp-session.ts +156 -0
- package/src/cdp-types.ts +6 -0
- package/src/create-logger.ts +56 -0
- package/src/debugger.md +453 -0
- package/src/extension/cdp-relay.ts +32 -14
- package/src/mcp.test.ts +795 -189
- package/src/mcp.ts +101 -47
- package/src/prompt.md +38 -8
- package/src/snapshots/shadcn-ui-accessibility.md +94 -91
- package/src/start-relay-server.ts +3 -20
- package/src/utils.ts +45 -0
- package/src/wait-for-page-load.ts +173 -0
package/src/debugger.md
ADDED
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
# JavaScript Debugging with CDP
|
|
2
|
+
|
|
3
|
+
You can use the Chrome DevTools Protocol (CDP) Debugger domain to debug JavaScript in the browser. This allows you to set breakpoints, step through code, inspect variables, and examine stack traces.
|
|
4
|
+
|
|
5
|
+
## Getting Started
|
|
6
|
+
|
|
7
|
+
Use `getCDPSession` to access CDP commands:
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
const cdp = await getCDPSession({ page });
|
|
11
|
+
await cdp.send('Debugger.enable');
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Always enable the debugger before using other debugger commands.
|
|
15
|
+
|
|
16
|
+
## Listing Scripts on the Page
|
|
17
|
+
|
|
18
|
+
Use `Page.getResourceTree` to list all loaded resources including scripts:
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
const cdp = await getCDPSession({ page });
|
|
22
|
+
const tree = await cdp.send('Page.getResourceTree');
|
|
23
|
+
const scripts = tree.frameTree.resources.filter(r => r.type === 'Script');
|
|
24
|
+
scripts.forEach(s => { console.log(s.url); });
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The response structure:
|
|
28
|
+
|
|
29
|
+
```js
|
|
30
|
+
{
|
|
31
|
+
"frameTree": {
|
|
32
|
+
"frame": {
|
|
33
|
+
"id": "FRAME_ID",
|
|
34
|
+
"url": "https://example.com/page",
|
|
35
|
+
"securityOrigin": "https://example.com",
|
|
36
|
+
"mimeType": "text/html"
|
|
37
|
+
},
|
|
38
|
+
"resources": [
|
|
39
|
+
{
|
|
40
|
+
"url": "https://example.com/app.js",
|
|
41
|
+
"type": "Script", // Script, Image, Font, Stylesheet, etc.
|
|
42
|
+
"mimeType": "text/javascript",
|
|
43
|
+
"contentSize": 78679, // size in bytes
|
|
44
|
+
"lastModified": 1765390319 // timestamp
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Filter to find app scripts (exclude libraries):
|
|
52
|
+
|
|
53
|
+
```js
|
|
54
|
+
const cdp = await getCDPSession({ page });
|
|
55
|
+
const tree = await cdp.send('Page.getResourceTree');
|
|
56
|
+
const scripts = tree.frameTree.resources.filter(r => r.type === 'Script' && !r.url.includes('vendor') && !r.url.includes('node_modules'));
|
|
57
|
+
scripts.forEach(s => { console.log(`${(s.contentSize / 1024).toFixed(1)}KB - ${s.url.split('/').pop()}`); });
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Understanding Call Frames
|
|
61
|
+
|
|
62
|
+
When the debugger pauses, you get an array of **call frames** representing the call stack. Frame 0 is where execution paused, frame 1 is the function that called it, frame 2 called frame 1, and so on.
|
|
63
|
+
|
|
64
|
+
Example: If you have `outer()` → `middle()` → `inner()` → `debugger`, you'll see:
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
#0 inner ← where we paused
|
|
68
|
+
#1 middle ← called inner()
|
|
69
|
+
#2 outer ← called middle()
|
|
70
|
+
#3 (anonymous) ← top-level code that called outer()
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Call Frame Structure
|
|
74
|
+
|
|
75
|
+
Each call frame contains:
|
|
76
|
+
|
|
77
|
+
```js
|
|
78
|
+
{
|
|
79
|
+
"callFrameId": "-4687593592348163175.1.0", // ID for evaluating in this frame
|
|
80
|
+
"functionName": "inner", // function name (empty for anonymous)
|
|
81
|
+
"canBeRestarted": true, // can restart from this frame
|
|
82
|
+
"location": {
|
|
83
|
+
"scriptId": "1104",
|
|
84
|
+
"lineNumber": 0,
|
|
85
|
+
"columnNumber": 116
|
|
86
|
+
},
|
|
87
|
+
"functionLocation": { // where function is defined
|
|
88
|
+
"scriptId": "1104",
|
|
89
|
+
"lineNumber": 0,
|
|
90
|
+
"columnNumber": 110
|
|
91
|
+
},
|
|
92
|
+
"scopeChain": [ // variables in scope
|
|
93
|
+
{
|
|
94
|
+
"type": "local", // local, closure, global, block, catch, etc.
|
|
95
|
+
"name": "inner",
|
|
96
|
+
"object": { "objectId": "..." } // use Runtime.getProperties to inspect
|
|
97
|
+
},
|
|
98
|
+
{ "type": "closure", "object": { "objectId": "..." } },
|
|
99
|
+
{ "type": "global", "object": { "objectId": "..." } }
|
|
100
|
+
],
|
|
101
|
+
"this": { // 'this' value in this frame
|
|
102
|
+
"type": "object",
|
|
103
|
+
"className": "Window"
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Inspecting the Call Stack
|
|
109
|
+
|
|
110
|
+
Print the full call stack when paused:
|
|
111
|
+
|
|
112
|
+
```js
|
|
113
|
+
state.lastPause.callFrames.forEach((frame, i) => {
|
|
114
|
+
console.log(`#${i} ${frame.functionName || '(anonymous)'} at line ${frame.location.lineNumber}`);
|
|
115
|
+
console.log(` Scopes: ${frame.scopeChain.map(s => s.type).join(', ')}`);
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Getting Local Variables from a Frame
|
|
120
|
+
|
|
121
|
+
```js
|
|
122
|
+
const cdp = await getCDPSession({ page });
|
|
123
|
+
const frame = state.lastPause.callFrames[0];
|
|
124
|
+
const localScope = frame.scopeChain.find(s => s.type === 'local');
|
|
125
|
+
const props = await cdp.send('Runtime.getProperties', { objectId: localScope.object.objectId });
|
|
126
|
+
props.result.forEach(p => { console.log(`${p.name} = ${p.value?.value}`); });
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Evaluating in Different Frames
|
|
130
|
+
|
|
131
|
+
You can evaluate expressions in any frame, not just the current one:
|
|
132
|
+
|
|
133
|
+
```js
|
|
134
|
+
const cdp = await getCDPSession({ page });
|
|
135
|
+
// Evaluate in the parent frame (frame 1)
|
|
136
|
+
const parentFrameId = state.lastPause.callFrames[1].callFrameId;
|
|
137
|
+
const result = await cdp.send('Debugger.evaluateOnCallFrame', { callFrameId: parentFrameId, expression: 'myVar' });
|
|
138
|
+
console.log('Value in parent frame:', result.result.value);
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Example Flows
|
|
142
|
+
|
|
143
|
+
### Flow 1: Catch a `debugger` Statement in Code
|
|
144
|
+
|
|
145
|
+
If the site has `debugger;` statements or you want to pause on them:
|
|
146
|
+
|
|
147
|
+
```js
|
|
148
|
+
const cdp = await getCDPSession({ page });
|
|
149
|
+
await cdp.send('Debugger.enable');
|
|
150
|
+
state.pauseHandler = (event) => { state.lastPause = event; };
|
|
151
|
+
cdp.on('Debugger.paused', state.pauseHandler);
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Then trigger the code that contains `debugger;`. After it pauses:
|
|
155
|
+
|
|
156
|
+
```js
|
|
157
|
+
console.log('Paused at:', state.lastPause.callFrames[0].location);
|
|
158
|
+
console.log('Reason:', state.lastPause.reason);
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Resume execution:
|
|
162
|
+
|
|
163
|
+
```js
|
|
164
|
+
const cdp = await getCDPSession({ page }); await cdp.send('Debugger.resume');
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Flow 2: Set a Breakpoint by URL and Line Number
|
|
168
|
+
|
|
169
|
+
Set a breakpoint in a specific file:
|
|
170
|
+
|
|
171
|
+
```js
|
|
172
|
+
const cdp = await getCDPSession({ page });
|
|
173
|
+
await cdp.send('Debugger.enable');
|
|
174
|
+
const result = await cdp.send('Debugger.setBreakpointByUrl', { lineNumber: 42, urlRegex: '.*app\\.js$' });
|
|
175
|
+
console.log('Breakpoint ID:', result.breakpointId);
|
|
176
|
+
console.log('Resolved locations:', result.locations);
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Flow 3: Pause on Exceptions
|
|
180
|
+
|
|
181
|
+
Catch all exceptions (or only uncaught ones):
|
|
182
|
+
|
|
183
|
+
```js
|
|
184
|
+
const cdp = await getCDPSession({ page });
|
|
185
|
+
await cdp.send('Debugger.enable');
|
|
186
|
+
await cdp.send('Debugger.setPauseOnExceptions', { state: 'all' }); // 'none', 'caught', 'uncaught', 'all'
|
|
187
|
+
state.pauseHandler = (event) => { state.lastPause = event; };
|
|
188
|
+
cdp.on('Debugger.paused', state.pauseHandler);
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Now when an exception is thrown, the debugger pauses. Check what happened:
|
|
192
|
+
|
|
193
|
+
```js
|
|
194
|
+
console.log('Exception paused:', state.lastPause.reason);
|
|
195
|
+
console.log('Data:', JSON.stringify(state.lastPause.data, null, 2));
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Flow 4: Inspect the Call Stack
|
|
199
|
+
|
|
200
|
+
When paused, examine the stack trace:
|
|
201
|
+
|
|
202
|
+
```js
|
|
203
|
+
const frames = state.lastPause.callFrames;
|
|
204
|
+
frames.forEach((frame, i) => {
|
|
205
|
+
console.log(`#${i} ${frame.functionName || '(anonymous)'} at ${frame.url}:${frame.location.lineNumber}:${frame.location.columnNumber}`);
|
|
206
|
+
});
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Flow 5: Evaluate Variables in a Paused Frame
|
|
210
|
+
|
|
211
|
+
When paused, you can evaluate expressions in any call frame:
|
|
212
|
+
|
|
213
|
+
```js
|
|
214
|
+
const cdp = await getCDPSession({ page });
|
|
215
|
+
const frameId = state.lastPause.callFrames[0].callFrameId;
|
|
216
|
+
const result = await cdp.send('Debugger.evaluateOnCallFrame', { callFrameId: frameId, expression: 'myVariable' });
|
|
217
|
+
console.log('Value:', result.result.value);
|
|
218
|
+
console.log('Type:', result.result.type);
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Evaluate complex expressions:
|
|
222
|
+
|
|
223
|
+
```js
|
|
224
|
+
const cdp = await getCDPSession({ page });
|
|
225
|
+
const frameId = state.lastPause.callFrames[0].callFrameId;
|
|
226
|
+
const result = await cdp.send('Debugger.evaluateOnCallFrame', { callFrameId: frameId, expression: 'JSON.stringify({ user, config, state })' });
|
|
227
|
+
console.log('Context:', result.result.value);
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Flow 6: Inspect Scope Variables
|
|
231
|
+
|
|
232
|
+
Each call frame has a scope chain. List all variables in scope:
|
|
233
|
+
|
|
234
|
+
```js
|
|
235
|
+
const frame = state.lastPause.callFrames[0];
|
|
236
|
+
frame.scopeChain.forEach((scope, i) => {
|
|
237
|
+
console.log(`Scope ${i}: ${scope.type} ${scope.name || ''}`);
|
|
238
|
+
});
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
To get variables from a specific scope, use `Runtime.getProperties` on the scope's object:
|
|
242
|
+
|
|
243
|
+
```js
|
|
244
|
+
const cdp = await getCDPSession({ page });
|
|
245
|
+
const localScope = state.lastPause.callFrames[0].scopeChain.find(s => s.type === 'local');
|
|
246
|
+
if (localScope) {
|
|
247
|
+
const props = await cdp.send('Runtime.getProperties', { objectId: localScope.object.objectId });
|
|
248
|
+
props.result.forEach(p => { console.log(` ${p.name}: ${p.value?.value ?? p.value?.type}`); });
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Flow 7: Step Through Code
|
|
253
|
+
|
|
254
|
+
When paused, use stepping commands:
|
|
255
|
+
|
|
256
|
+
```js
|
|
257
|
+
const cdp = await getCDPSession({ page });
|
|
258
|
+
await cdp.send('Debugger.stepOver'); // Execute current line, don't enter functions
|
|
259
|
+
await cdp.send('Debugger.stepInto'); // Step into function calls
|
|
260
|
+
await cdp.send('Debugger.stepOut'); // Run until current function returns
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
After each step, the `Debugger.paused` event fires again with the new location.
|
|
264
|
+
|
|
265
|
+
### Flow 8: Get Script Source
|
|
266
|
+
|
|
267
|
+
When you see a scriptId in the pause event, get its source:
|
|
268
|
+
|
|
269
|
+
```js
|
|
270
|
+
const cdp = await getCDPSession({ page });
|
|
271
|
+
const scriptId = state.lastPause.callFrames[0].location.scriptId;
|
|
272
|
+
const source = await cdp.send('Debugger.getScriptSource', { scriptId });
|
|
273
|
+
const lines = source.scriptSource.split('\n');
|
|
274
|
+
const line = state.lastPause.callFrames[0].location.lineNumber;
|
|
275
|
+
console.log('Current line:', lines[line]);
|
|
276
|
+
console.log('Context:', lines.slice(Math.max(0, line - 3), line + 4).join('\n'));
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Flow 9: Search in Script Content
|
|
280
|
+
|
|
281
|
+
Find where a function or variable is used:
|
|
282
|
+
|
|
283
|
+
```js
|
|
284
|
+
const cdp = await getCDPSession({ page });
|
|
285
|
+
const scriptId = state.lastPause.callFrames[0].location.scriptId;
|
|
286
|
+
const matches = await cdp.send('Debugger.searchInContent', { scriptId, query: 'handleClick', isRegex: false });
|
|
287
|
+
matches.result.forEach(m => { console.log(`Line ${m.lineNumber}: ${m.lineContent}`); });
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Flow 10: Watch a Variable Across Steps
|
|
291
|
+
|
|
292
|
+
Store variable values as you step through:
|
|
293
|
+
|
|
294
|
+
```js
|
|
295
|
+
state.watchedValues = state.watchedValues || [];
|
|
296
|
+
const cdp = await getCDPSession({ page });
|
|
297
|
+
const frameId = state.lastPause.callFrames[0].callFrameId;
|
|
298
|
+
const result = await cdp.send('Debugger.evaluateOnCallFrame', { callFrameId: frameId, expression: 'counter' });
|
|
299
|
+
state.watchedValues.push({ line: state.lastPause.callFrames[0].location.lineNumber, value: result.result.value });
|
|
300
|
+
console.log('Watch history:', state.watchedValues);
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Flow 11: Modify a Variable at Runtime
|
|
304
|
+
|
|
305
|
+
Change a variable's value while paused:
|
|
306
|
+
|
|
307
|
+
```js
|
|
308
|
+
const cdp = await getCDPSession({ page });
|
|
309
|
+
const frameId = state.lastPause.callFrames[0].callFrameId;
|
|
310
|
+
await cdp.send('Debugger.setVariableValue', {
|
|
311
|
+
scopeNumber: 0, // 0 = local scope
|
|
312
|
+
variableName: 'shouldRetry',
|
|
313
|
+
newValue: { value: true },
|
|
314
|
+
callFrameId: frameId
|
|
315
|
+
});
|
|
316
|
+
console.log('Variable modified');
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### Flow 12: Skip All Pauses Temporarily
|
|
320
|
+
|
|
321
|
+
Disable breakpoints without removing them:
|
|
322
|
+
|
|
323
|
+
```js
|
|
324
|
+
const cdp = await getCDPSession({ page }); await cdp.send('Debugger.setSkipAllPauses', { skip: true });
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
Re-enable:
|
|
328
|
+
|
|
329
|
+
```js
|
|
330
|
+
const cdp = await getCDPSession({ page }); await cdp.send('Debugger.setSkipAllPauses', { skip: false });
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### Flow 13: Blackbox Third-Party Scripts
|
|
334
|
+
|
|
335
|
+
Skip stepping into library code (e.g., React, lodash):
|
|
336
|
+
|
|
337
|
+
```js
|
|
338
|
+
const cdp = await getCDPSession({ page });
|
|
339
|
+
await cdp.send('Debugger.setBlackboxPatterns', { patterns: ['.*node_modules.*', '.*vendor.*', '.*react.*'] });
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### Flow 14: Continue to a Specific Location
|
|
343
|
+
|
|
344
|
+
Instead of stepping line by line, jump to a specific location:
|
|
345
|
+
|
|
346
|
+
```js
|
|
347
|
+
const cdp = await getCDPSession({ page });
|
|
348
|
+
await cdp.send('Debugger.continueToLocation', {
|
|
349
|
+
location: { scriptId: 'your-script-id', lineNumber: 100 },
|
|
350
|
+
targetCallFrames: 'any'
|
|
351
|
+
});
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
## Complete Debugging Session Example
|
|
355
|
+
|
|
356
|
+
Here's a full example debugging a button click handler:
|
|
357
|
+
|
|
358
|
+
**Step 1: Enable debugger and set up pause listener**
|
|
359
|
+
|
|
360
|
+
```js
|
|
361
|
+
const cdp = await getCDPSession({ page });
|
|
362
|
+
await cdp.send('Debugger.enable');
|
|
363
|
+
await cdp.send('Debugger.setPauseOnExceptions', { state: 'uncaught' });
|
|
364
|
+
state.pauseHandler = (event) => { state.lastPause = event; console.log('PAUSED:', event.reason, 'at line', event.callFrames[0]?.location?.lineNumber); };
|
|
365
|
+
cdp.on('Debugger.paused', state.pauseHandler);
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
**Step 2: Set breakpoint on the handler**
|
|
369
|
+
|
|
370
|
+
```js
|
|
371
|
+
const cdp = await getCDPSession({ page });
|
|
372
|
+
const bp = await cdp.send('Debugger.setBreakpointByUrl', { lineNumber: 25, urlRegex: '.*handleSubmit.*' });
|
|
373
|
+
console.log('Breakpoint set:', bp.breakpointId);
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
**Step 3: Trigger the action (click the button)**
|
|
377
|
+
|
|
378
|
+
```js
|
|
379
|
+
await page.click('#submit-button');
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
**Step 4: When paused, inspect state**
|
|
383
|
+
|
|
384
|
+
```js
|
|
385
|
+
const cdp = await getCDPSession({ page });
|
|
386
|
+
const frameId = state.lastPause.callFrames[0].callFrameId;
|
|
387
|
+
const formData = await cdp.send('Debugger.evaluateOnCallFrame', { callFrameId: frameId, expression: 'JSON.stringify(formData)' });
|
|
388
|
+
console.log('Form data at breakpoint:', formData.result.value);
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
**Step 5: Step through and watch**
|
|
392
|
+
|
|
393
|
+
```js
|
|
394
|
+
const cdp = await getCDPSession({ page }); await cdp.send('Debugger.stepOver');
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
**Step 6: Check new state after step**
|
|
398
|
+
|
|
399
|
+
```js
|
|
400
|
+
const cdp = await getCDPSession({ page });
|
|
401
|
+
const frameId = state.lastPause.callFrames[0].callFrameId;
|
|
402
|
+
const result = await cdp.send('Debugger.evaluateOnCallFrame', { callFrameId: frameId, expression: 'validationResult' });
|
|
403
|
+
console.log('Validation result:', result.result.value);
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
**Step 7: Resume and clean up**
|
|
407
|
+
|
|
408
|
+
```js
|
|
409
|
+
const cdp = await getCDPSession({ page });
|
|
410
|
+
await cdp.send('Debugger.resume');
|
|
411
|
+
await cdp.send('Debugger.disable');
|
|
412
|
+
cdp.off('Debugger.paused', state.pauseHandler);
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
## CDP Debugger Command Reference
|
|
416
|
+
|
|
417
|
+
| Command | Description |
|
|
418
|
+
|---------|-------------|
|
|
419
|
+
| `Debugger.enable` | Enable debugging (required first) |
|
|
420
|
+
| `Debugger.disable` | Disable debugging |
|
|
421
|
+
| `Debugger.pause` | Pause execution immediately |
|
|
422
|
+
| `Debugger.resume` | Resume execution |
|
|
423
|
+
| `Debugger.stepOver` | Step over current statement |
|
|
424
|
+
| `Debugger.stepInto` | Step into function call |
|
|
425
|
+
| `Debugger.stepOut` | Step out of current function |
|
|
426
|
+
| `Debugger.setBreakpoint` | Set breakpoint at exact location |
|
|
427
|
+
| `Debugger.setBreakpointByUrl` | Set breakpoint by URL pattern |
|
|
428
|
+
| `Debugger.removeBreakpoint` | Remove a breakpoint |
|
|
429
|
+
| `Debugger.setPauseOnExceptions` | Pause on exceptions: `none`, `caught`, `uncaught`, `all` |
|
|
430
|
+
| `Debugger.evaluateOnCallFrame` | Evaluate expression in a frame |
|
|
431
|
+
| `Debugger.setVariableValue` | Modify variable value |
|
|
432
|
+
| `Debugger.getScriptSource` | Get script source code |
|
|
433
|
+
| `Debugger.searchInContent` | Search in script |
|
|
434
|
+
| `Debugger.setSkipAllPauses` | Temporarily disable all breakpoints |
|
|
435
|
+
| `Debugger.setBlackboxPatterns` | Skip stepping into matching scripts |
|
|
436
|
+
|
|
437
|
+
## Events
|
|
438
|
+
|
|
439
|
+
| Event | Description |
|
|
440
|
+
|-------|-------------|
|
|
441
|
+
| `Debugger.paused` | VM paused (breakpoint, exception, step) |
|
|
442
|
+
| `Debugger.resumed` | VM resumed execution |
|
|
443
|
+
| `Debugger.scriptParsed` | New script was parsed |
|
|
444
|
+
| `Debugger.scriptFailedToParse` | Script parsing failed |
|
|
445
|
+
|
|
446
|
+
## Tips
|
|
447
|
+
|
|
448
|
+
- Always call `Debugger.enable` before other debugger commands
|
|
449
|
+
- Store pause events in `state.lastPause` to inspect them in subsequent calls
|
|
450
|
+
- Use `Debugger.setBlackboxPatterns` to skip library code when stepping
|
|
451
|
+
- Clean up event listeners with `cdp.off()` when done debugging
|
|
452
|
+
- The `callFrameId` is only valid while paused - use it immediately
|
|
453
|
+
- Use `Debugger.evaluateOnCallFrame` with `JSON.stringify()` for complex objects
|
|
@@ -3,9 +3,10 @@ import { serve } from '@hono/node-server'
|
|
|
3
3
|
import { createNodeWebSocket } from '@hono/node-ws'
|
|
4
4
|
import type { WSContext } from 'hono/ws'
|
|
5
5
|
import type { Protocol } from '../cdp-types.js'
|
|
6
|
-
import type { CDPCommand, CDPResponseBase, CDPEventBase, CDPEventFor } from '../cdp-types.js'
|
|
6
|
+
import type { CDPCommand, CDPResponseBase, CDPEventBase, CDPEventFor, RelayServerEvents } from '../cdp-types.js'
|
|
7
7
|
import type { ExtensionMessage, ExtensionEventMessage } from './protocol.js'
|
|
8
8
|
import chalk from 'chalk'
|
|
9
|
+
import { EventEmitter } from 'node:events'
|
|
9
10
|
|
|
10
11
|
type ConnectedTarget = {
|
|
11
12
|
sessionId: string
|
|
@@ -21,7 +22,14 @@ type PlaywrightClient = {
|
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
|
|
24
|
-
export
|
|
25
|
+
export type RelayServer = {
|
|
26
|
+
close(): void
|
|
27
|
+
on<K extends keyof RelayServerEvents>(event: K, listener: RelayServerEvents[K]): void
|
|
28
|
+
off<K extends keyof RelayServerEvents>(event: K, listener: RelayServerEvents[K]): void
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.0.0.1', logger }: { port?: number; host?: string; logger?: { log(...args: any[]): void; error(...args: any[]): void } } = {}): Promise<RelayServer> {
|
|
32
|
+
const emitter = new EventEmitter()
|
|
25
33
|
const connectedTargets = new Map<string, ConnectedTarget>()
|
|
26
34
|
|
|
27
35
|
const playwrightClients = new Map<string, PlaywrightClient>()
|
|
@@ -314,6 +322,8 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
314
322
|
id
|
|
315
323
|
})
|
|
316
324
|
|
|
325
|
+
emitter.emit('cdp:command', { clientId, command: message })
|
|
326
|
+
|
|
317
327
|
if (!extensionWs) {
|
|
318
328
|
sendToPlaywright({
|
|
319
329
|
message: {
|
|
@@ -395,20 +405,18 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
395
405
|
}
|
|
396
406
|
}
|
|
397
407
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
})
|
|
408
|
+
const response: CDPResponseBase = { id, sessionId, result }
|
|
409
|
+
sendToPlaywright({ message: response, clientId })
|
|
410
|
+
emitter.emit('cdp:response', { clientId, response, command: message })
|
|
402
411
|
} catch (e) {
|
|
403
412
|
logger?.error('Error handling CDP command:', method, params, e)
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
})
|
|
413
|
+
const errorResponse: CDPResponseBase = {
|
|
414
|
+
id,
|
|
415
|
+
sessionId,
|
|
416
|
+
error: { message: (e as Error).message }
|
|
417
|
+
}
|
|
418
|
+
sendToPlaywright({ message: errorResponse, clientId })
|
|
419
|
+
emitter.emit('cdp:response', { clientId, response: errorResponse, command: message })
|
|
412
420
|
}
|
|
413
421
|
},
|
|
414
422
|
|
|
@@ -492,6 +500,9 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
492
500
|
params
|
|
493
501
|
})
|
|
494
502
|
|
|
503
|
+
const cdpEvent: CDPEventBase = { method, sessionId, params }
|
|
504
|
+
emitter.emit('cdp:event', { event: cdpEvent, sessionId })
|
|
505
|
+
|
|
495
506
|
if (method === 'Target.attachedToTarget') {
|
|
496
507
|
const targetParams = params as Protocol.Target.AttachedToTargetEvent
|
|
497
508
|
|
|
@@ -608,6 +619,13 @@ export async function startPlayWriterCDPRelayServer({ port = 19988, host = '127.
|
|
|
608
619
|
playwrightClients.clear()
|
|
609
620
|
extensionWs?.close(1000, 'Server stopped')
|
|
610
621
|
server.close()
|
|
622
|
+
emitter.removeAllListeners()
|
|
623
|
+
},
|
|
624
|
+
on<K extends keyof RelayServerEvents>(event: K, listener: RelayServerEvents[K]) {
|
|
625
|
+
emitter.on(event, listener as (...args: unknown[]) => void)
|
|
626
|
+
},
|
|
627
|
+
off<K extends keyof RelayServerEvents>(event: K, listener: RelayServerEvents[K]) {
|
|
628
|
+
emitter.off(event, listener as (...args: unknown[]) => void)
|
|
611
629
|
}
|
|
612
630
|
}
|
|
613
631
|
}
|