uisnap 0.1.0
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/.claude/settings.local.json +26 -0
- package/.claude/skills/uisnap/README.md +48 -0
- package/.claude/skills/uisnap/REFERENCE.md +1261 -0
- package/.claude/skills/uisnap/SETUP.md +75 -0
- package/.claude/skills/uisnap/SKILL.md +130 -0
- package/.claude/skills/uisnap/snapshot-capture-and-analysis.md +452 -0
- package/.claude/skills/uisnap/trace-capture-and-analysis.md +472 -0
- package/CHANGELOG.md +96 -0
- package/LICENSE +21 -0
- package/README.md +394 -0
- package/SKILL-INSTALLATION.md +103 -0
- package/dist/analyze-console.d.ts +3 -0
- package/dist/analyze-console.d.ts.map +1 -0
- package/dist/analyze-console.js +153 -0
- package/dist/analyze-console.js.map +1 -0
- package/dist/analyze-network.d.ts +3 -0
- package/dist/analyze-network.d.ts.map +1 -0
- package/dist/analyze-network.js +156 -0
- package/dist/analyze-network.js.map +1 -0
- package/dist/chrome-trace-analyze.d.ts +3 -0
- package/dist/chrome-trace-analyze.d.ts.map +1 -0
- package/dist/chrome-trace-analyze.js +119 -0
- package/dist/chrome-trace-analyze.js.map +1 -0
- package/dist/chrome-trace-import.d.ts +3 -0
- package/dist/chrome-trace-import.d.ts.map +1 -0
- package/dist/chrome-trace-import.js +90 -0
- package/dist/chrome-trace-import.js.map +1 -0
- package/dist/commands/snapshot.d.ts +4 -0
- package/dist/commands/snapshot.d.ts.map +1 -0
- package/dist/commands/snapshot.js +154 -0
- package/dist/commands/snapshot.js.map +1 -0
- package/dist/diagnose.d.ts +3 -0
- package/dist/diagnose.d.ts.map +1 -0
- package/dist/diagnose.js +244 -0
- package/dist/diagnose.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/pw.d.ts +3 -0
- package/dist/pw.d.ts.map +1 -0
- package/dist/pw.js +289 -0
- package/dist/pw.js.map +1 -0
- package/dist/query-a11y.d.ts +3 -0
- package/dist/query-a11y.d.ts.map +1 -0
- package/dist/query-a11y.js +208 -0
- package/dist/query-a11y.js.map +1 -0
- package/dist/trace-import.d.ts +3 -0
- package/dist/trace-import.d.ts.map +1 -0
- package/dist/trace-import.js +93 -0
- package/dist/trace-import.js.map +1 -0
- package/dist/types.d.ts +70 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/chromeTraceAnalyze.d.ts +40 -0
- package/dist/utils/chromeTraceAnalyze.d.ts.map +1 -0
- package/dist/utils/chromeTraceAnalyze.js +113 -0
- package/dist/utils/chromeTraceAnalyze.js.map +1 -0
- package/dist/utils/chromeTraceHelpers.d.ts +4 -0
- package/dist/utils/chromeTraceHelpers.d.ts.map +1 -0
- package/dist/utils/chromeTraceHelpers.js +68 -0
- package/dist/utils/chromeTraceHelpers.js.map +1 -0
- package/dist/utils/chromeTraceImport.d.ts +14 -0
- package/dist/utils/chromeTraceImport.d.ts.map +1 -0
- package/dist/utils/chromeTraceImport.js +77 -0
- package/dist/utils/chromeTraceImport.js.map +1 -0
- package/dist/utils/helpers.d.ts +3 -0
- package/dist/utils/helpers.d.ts.map +1 -0
- package/dist/utils/helpers.js +88 -0
- package/dist/utils/helpers.js.map +1 -0
- package/dist/utils/projectRoot.d.ts +2 -0
- package/dist/utils/projectRoot.d.ts.map +1 -0
- package/dist/utils/projectRoot.js +56 -0
- package/dist/utils/projectRoot.js.map +1 -0
- package/dist/utils/traceHelpers.d.ts +4 -0
- package/dist/utils/traceHelpers.d.ts.map +1 -0
- package/dist/utils/traceHelpers.js +67 -0
- package/dist/utils/traceHelpers.js.map +1 -0
- package/dist/utils/traceImport.d.ts +20 -0
- package/dist/utils/traceImport.d.ts.map +1 -0
- package/dist/utils/traceImport.js +124 -0
- package/dist/utils/traceImport.js.map +1 -0
- package/dist/utils/webVitals.d.ts +9 -0
- package/dist/utils/webVitals.d.ts.map +1 -0
- package/dist/utils/webVitals.js +54 -0
- package/dist/utils/webVitals.js.map +1 -0
- package/examples/login-flow.js +37 -0
- package/examples/scroll-perf-trace.js +43 -0
- package/examples/simple-capture.js +28 -0
- package/package.json +74 -0
|
@@ -0,0 +1,1261 @@
|
|
|
1
|
+
# uisnap - Specification
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
A Playwright-based debugging toolkit for Claude Code that provides efficient frontend debugging through disk-based state capture and analysis. The toolkit follows a "capture once, query many" philosophy to minimize token costs and enable systematic debugging workflows.
|
|
6
|
+
|
|
7
|
+
## Architecture
|
|
8
|
+
|
|
9
|
+
The system consists of four layers:
|
|
10
|
+
|
|
11
|
+
1. **Core Runtime** - Playwright environment manager
|
|
12
|
+
2. **Capture Commands** - Built-in data extractors that write to disk
|
|
13
|
+
3. **Analysis Commands** - Query tools for captured data
|
|
14
|
+
4. **Code Mode** - User script execution within Playwright context
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
┌─────────────────────────────────────────────┐
|
|
18
|
+
│ Agent (Claude Code) │
|
|
19
|
+
└─────────────────┬───────────────────────────┘
|
|
20
|
+
│
|
|
21
|
+
┌───────────┴───────────┐
|
|
22
|
+
│ │
|
|
23
|
+
▼ ▼
|
|
24
|
+
┌──────────┐ ┌─────────────────┐
|
|
25
|
+
│ Built-in │ │ Custom Scripts │
|
|
26
|
+
│ Commands │ │ (Code Mode) │
|
|
27
|
+
└─────┬────┘ └────────┬────────┘
|
|
28
|
+
│ │
|
|
29
|
+
└───────────┬───────────┘
|
|
30
|
+
│
|
|
31
|
+
▼
|
|
32
|
+
┌──────────────────┐
|
|
33
|
+
│ Core Runtime │
|
|
34
|
+
│ (pw.js) │
|
|
35
|
+
└────────┬─────────┘
|
|
36
|
+
│
|
|
37
|
+
▼
|
|
38
|
+
┌──────────────────┐
|
|
39
|
+
│ Playwright │
|
|
40
|
+
│ (Browser) │
|
|
41
|
+
└──────────────────┘
|
|
42
|
+
│
|
|
43
|
+
▼
|
|
44
|
+
┌──────────────────┐
|
|
45
|
+
│ Disk Output │
|
|
46
|
+
│ (JSON/JSONL) │
|
|
47
|
+
└──────────────────┘
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## 1. Core Runtime
|
|
51
|
+
|
|
52
|
+
### File: `scripts/pw.js`
|
|
53
|
+
|
|
54
|
+
The core runtime is a single entry point that manages Playwright's lifecycle and routes commands.
|
|
55
|
+
|
|
56
|
+
**Responsibilities:**
|
|
57
|
+
- Launch and manage Playwright browser instance
|
|
58
|
+
- Route to built-in commands or user scripts
|
|
59
|
+
- Provide execution context for scripts
|
|
60
|
+
- Handle cleanup on exit
|
|
61
|
+
|
|
62
|
+
**Interface:**
|
|
63
|
+
```bash
|
|
64
|
+
node scripts/pw.js <command> [args...]
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Commands:**
|
|
68
|
+
- `snapshot <url> <output-dir>` - Capture full page state
|
|
69
|
+
- `interact <scenario.yml> <output-dir>` - Run interaction scenario
|
|
70
|
+
- `exec <script.js> [args...]` - Execute user script in Playwright context
|
|
71
|
+
|
|
72
|
+
**Implementation Structure:**
|
|
73
|
+
```javascript
|
|
74
|
+
#!/usr/bin/env node
|
|
75
|
+
const { chromium } = require('playwright');
|
|
76
|
+
const fs = require('fs');
|
|
77
|
+
const path = require('path');
|
|
78
|
+
|
|
79
|
+
// Built-in command modules
|
|
80
|
+
const builtins = {
|
|
81
|
+
snapshot: require('./commands/snapshot'),
|
|
82
|
+
interact: require('./commands/interact'),
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
async function main() {
|
|
86
|
+
const command = process.argv[2];
|
|
87
|
+
const args = process.argv.slice(3);
|
|
88
|
+
|
|
89
|
+
// Launch browser
|
|
90
|
+
const browser = await chromium.launch({
|
|
91
|
+
headless: true,
|
|
92
|
+
args: ['--disable-dev-shm-usage']
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const context = await browser.newContext({
|
|
96
|
+
viewport: { width: 1280, height: 720 }
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const page = await context.newPage();
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
if (builtins[command]) {
|
|
103
|
+
// Execute built-in command
|
|
104
|
+
await builtins[command](page, context, browser, args);
|
|
105
|
+
} else if (command === 'exec') {
|
|
106
|
+
// Execute user script
|
|
107
|
+
await executeUserScript(page, context, browser, args);
|
|
108
|
+
} else {
|
|
109
|
+
console.error(`Unknown command: ${command}`);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
} finally {
|
|
113
|
+
await browser.close();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function executeUserScript(page, context, browser, args) {
|
|
118
|
+
const scriptPath = args[0];
|
|
119
|
+
const scriptArgs = args.slice(1);
|
|
120
|
+
const scriptCode = fs.readFileSync(scriptPath, 'utf8');
|
|
121
|
+
|
|
122
|
+
// Create execution environment for user script
|
|
123
|
+
const env = {
|
|
124
|
+
page,
|
|
125
|
+
context,
|
|
126
|
+
browser,
|
|
127
|
+
args: scriptArgs,
|
|
128
|
+
fs,
|
|
129
|
+
path,
|
|
130
|
+
utils: {
|
|
131
|
+
writeJson: (filepath, data) => {
|
|
132
|
+
fs.mkdirSync(path.dirname(filepath), { recursive: true });
|
|
133
|
+
fs.writeFileSync(filepath, JSON.stringify(data, null, 2));
|
|
134
|
+
},
|
|
135
|
+
writeJsonl: (filepath, items) => {
|
|
136
|
+
fs.mkdirSync(path.dirname(filepath), { recursive: true });
|
|
137
|
+
fs.writeFileSync(filepath, items.map(i => JSON.stringify(i)).join('\n'));
|
|
138
|
+
},
|
|
139
|
+
appendJsonl: (filepath, item) => {
|
|
140
|
+
fs.appendFileSync(filepath, JSON.stringify(item) + '\n');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// Execute user script in async context
|
|
146
|
+
const wrappedScript = `
|
|
147
|
+
(async ({ page, context, browser, args, fs, path, utils }) => {
|
|
148
|
+
${scriptCode}
|
|
149
|
+
})
|
|
150
|
+
`;
|
|
151
|
+
|
|
152
|
+
const userFunction = eval(wrappedScript);
|
|
153
|
+
await userFunction(env);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
main().catch(err => {
|
|
157
|
+
console.error('Fatal error:', err);
|
|
158
|
+
process.exit(1);
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## 2. Capture Commands
|
|
163
|
+
|
|
164
|
+
Built-in commands that extract browser state and write to disk. All commands produce structured JSON/JSONL output.
|
|
165
|
+
|
|
166
|
+
### 2.1 Snapshot Command
|
|
167
|
+
|
|
168
|
+
**Purpose:** Capture complete page state in a single operation
|
|
169
|
+
|
|
170
|
+
**Usage:**
|
|
171
|
+
```bash
|
|
172
|
+
node scripts/pw.js snapshot <url> <output-dir>
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**Output Structure:**
|
|
176
|
+
```
|
|
177
|
+
<output-dir>/
|
|
178
|
+
├── a11y.json # Accessibility tree
|
|
179
|
+
├── console.jsonl # Console messages (one per line)
|
|
180
|
+
├── network.jsonl # Network requests (one per line)
|
|
181
|
+
└── metadata.json # Page metadata (title, URL, timestamp, viewport)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**File: `scripts/commands/snapshot.js`**
|
|
185
|
+
```javascript
|
|
186
|
+
module.exports = async (page, context, browser, args) => {
|
|
187
|
+
const [url, outputDir] = args;
|
|
188
|
+
const fs = require('fs');
|
|
189
|
+
const path = require('path');
|
|
190
|
+
|
|
191
|
+
if (!url || !outputDir) {
|
|
192
|
+
throw new Error('Usage: snapshot <url> <output-dir>');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
196
|
+
|
|
197
|
+
// Set up event listeners before navigation
|
|
198
|
+
const consoleLogs = [];
|
|
199
|
+
page.on('console', msg => {
|
|
200
|
+
consoleLogs.push({
|
|
201
|
+
timestamp: new Date().toISOString(),
|
|
202
|
+
type: msg.type(),
|
|
203
|
+
text: msg.text(),
|
|
204
|
+
location: msg.location()
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const networkLogs = [];
|
|
209
|
+
page.on('response', response => {
|
|
210
|
+
networkLogs.push({
|
|
211
|
+
url: response.url(),
|
|
212
|
+
status: response.status(),
|
|
213
|
+
statusText: response.statusText(),
|
|
214
|
+
method: response.request().method(),
|
|
215
|
+
resourceType: response.request().resourceType(),
|
|
216
|
+
timing: response.request().timing()
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// Navigate to page
|
|
221
|
+
await page.goto(url, { waitUntil: 'networkidle' });
|
|
222
|
+
|
|
223
|
+
// Extract accessibility tree
|
|
224
|
+
const a11yTree = await page.accessibility.snapshot();
|
|
225
|
+
|
|
226
|
+
// Extract page metadata
|
|
227
|
+
const metadata = await page.evaluate(() => ({
|
|
228
|
+
title: document.title,
|
|
229
|
+
url: window.location.href,
|
|
230
|
+
viewport: {
|
|
231
|
+
width: window.innerWidth,
|
|
232
|
+
height: window.innerHeight
|
|
233
|
+
},
|
|
234
|
+
timestamp: new Date().toISOString()
|
|
235
|
+
}));
|
|
236
|
+
|
|
237
|
+
// Write all data to disk
|
|
238
|
+
fs.writeFileSync(
|
|
239
|
+
path.join(outputDir, 'a11y.json'),
|
|
240
|
+
JSON.stringify(a11yTree, null, 2)
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
fs.writeFileSync(
|
|
244
|
+
path.join(outputDir, 'console.jsonl'),
|
|
245
|
+
consoleLogs.map(l => JSON.stringify(l)).join('\n')
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
fs.writeFileSync(
|
|
249
|
+
path.join(outputDir, 'network.jsonl'),
|
|
250
|
+
networkLogs.map(l => JSON.stringify(l)).join('\n')
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
fs.writeFileSync(
|
|
254
|
+
path.join(outputDir, 'metadata.json'),
|
|
255
|
+
JSON.stringify(metadata, null, 2)
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
// Summary output
|
|
259
|
+
console.log(`✓ Snapshot saved to ${outputDir}/`);
|
|
260
|
+
console.log(` - Accessibility tree: ${JSON.stringify(a11yTree).length} bytes`);
|
|
261
|
+
console.log(` - Console messages: ${consoleLogs.length}`);
|
|
262
|
+
console.log(` - Network requests: ${networkLogs.length}`);
|
|
263
|
+
};
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
**Data Formats:**
|
|
267
|
+
|
|
268
|
+
*a11y.json:*
|
|
269
|
+
```json
|
|
270
|
+
{
|
|
271
|
+
"role": "WebArea",
|
|
272
|
+
"name": "Page Title",
|
|
273
|
+
"children": [
|
|
274
|
+
{
|
|
275
|
+
"role": "button",
|
|
276
|
+
"name": "Submit",
|
|
277
|
+
"disabled": false,
|
|
278
|
+
"focused": false
|
|
279
|
+
}
|
|
280
|
+
]
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
*console.jsonl:*
|
|
285
|
+
```json
|
|
286
|
+
{"timestamp":"2024-12-25T10:30:00.000Z","type":"error","text":"Uncaught TypeError: Cannot read property 'x' of null","location":{"url":"https://example.com/app.js","lineNumber":42}}
|
|
287
|
+
{"timestamp":"2024-12-25T10:30:01.000Z","type":"log","text":"User clicked submit","location":{"url":"https://example.com/app.js","lineNumber":100}}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
*network.jsonl:*
|
|
291
|
+
```json
|
|
292
|
+
{"url":"https://api.example.com/users","status":200,"statusText":"OK","method":"GET","resourceType":"fetch","timing":{"startTime":1234.5,"responseEnd":1456.7}}
|
|
293
|
+
{"url":"https://api.example.com/submit","status":500,"statusText":"Internal Server Error","method":"POST","resourceType":"fetch","timing":{"startTime":2345.6,"responseEnd":2567.8}}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### 2.2 Interact Command
|
|
297
|
+
|
|
298
|
+
**Purpose:** Run a sequence of actions and capture state after each step
|
|
299
|
+
|
|
300
|
+
**Usage:**
|
|
301
|
+
```bash
|
|
302
|
+
node scripts/pw.js interact <scenario.yml> <output-dir>
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
**Scenario Format (YAML):**
|
|
306
|
+
```yaml
|
|
307
|
+
steps:
|
|
308
|
+
- action: goto
|
|
309
|
+
url: https://example.com/login
|
|
310
|
+
|
|
311
|
+
- action: fill
|
|
312
|
+
selector: input[name="email"]
|
|
313
|
+
value: test@example.com
|
|
314
|
+
|
|
315
|
+
- action: fill
|
|
316
|
+
selector: input[name="password"]
|
|
317
|
+
value: password123
|
|
318
|
+
|
|
319
|
+
- action: click
|
|
320
|
+
selector: button[type="submit"]
|
|
321
|
+
wait: 2000 # Optional wait after action (ms)
|
|
322
|
+
|
|
323
|
+
- action: wait
|
|
324
|
+
duration: 1000
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
**Output Structure:**
|
|
328
|
+
```
|
|
329
|
+
<output-dir>/
|
|
330
|
+
├── step-1-goto/
|
|
331
|
+
│ ├── a11y.json
|
|
332
|
+
│ ├── console.jsonl
|
|
333
|
+
│ └── network.jsonl
|
|
334
|
+
├── step-2-fill/
|
|
335
|
+
│ ├── a11y.json
|
|
336
|
+
│ ├── console.jsonl
|
|
337
|
+
│ └── network.jsonl
|
|
338
|
+
└── step-3-click/
|
|
339
|
+
├── a11y.json
|
|
340
|
+
├── console.jsonl
|
|
341
|
+
└── network.jsonl
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
**File: `scripts/commands/interact.js`**
|
|
345
|
+
```javascript
|
|
346
|
+
module.exports = async (page, context, browser, args) => {
|
|
347
|
+
const [scenarioFile, outputDir] = args;
|
|
348
|
+
const fs = require('fs');
|
|
349
|
+
const path = require('path');
|
|
350
|
+
const yaml = require('yaml');
|
|
351
|
+
|
|
352
|
+
if (!scenarioFile || !outputDir) {
|
|
353
|
+
throw new Error('Usage: interact <scenario.yml> <output-dir>');
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const scenario = yaml.parse(fs.readFileSync(scenarioFile, 'utf8'));
|
|
357
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
358
|
+
|
|
359
|
+
// Set up logging
|
|
360
|
+
const consoleLogs = [];
|
|
361
|
+
page.on('console', msg => {
|
|
362
|
+
consoleLogs.push({
|
|
363
|
+
timestamp: new Date().toISOString(),
|
|
364
|
+
type: msg.type(),
|
|
365
|
+
text: msg.text()
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
const networkLogs = [];
|
|
370
|
+
page.on('response', response => {
|
|
371
|
+
networkLogs.push({
|
|
372
|
+
url: response.url(),
|
|
373
|
+
status: response.status(),
|
|
374
|
+
method: response.request().method()
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
// Execute each step
|
|
379
|
+
for (let i = 0; i < scenario.steps.length; i++) {
|
|
380
|
+
const step = scenario.steps[i];
|
|
381
|
+
console.log(`Step ${i + 1}/${scenario.steps.length}: ${step.action}`);
|
|
382
|
+
|
|
383
|
+
// Execute action
|
|
384
|
+
switch (step.action) {
|
|
385
|
+
case 'goto':
|
|
386
|
+
await page.goto(step.url, { waitUntil: 'networkidle' });
|
|
387
|
+
break;
|
|
388
|
+
|
|
389
|
+
case 'click':
|
|
390
|
+
await page.click(step.selector);
|
|
391
|
+
if (step.wait) await page.waitForTimeout(step.wait);
|
|
392
|
+
break;
|
|
393
|
+
|
|
394
|
+
case 'fill':
|
|
395
|
+
await page.fill(step.selector, step.value);
|
|
396
|
+
break;
|
|
397
|
+
|
|
398
|
+
case 'wait':
|
|
399
|
+
await page.waitForTimeout(step.duration || 1000);
|
|
400
|
+
break;
|
|
401
|
+
|
|
402
|
+
default:
|
|
403
|
+
throw new Error(`Unknown action: ${step.action}`);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Capture state after this step
|
|
407
|
+
const stepDir = path.join(outputDir, `step-${i + 1}-${step.action}`);
|
|
408
|
+
fs.mkdirSync(stepDir, { recursive: true });
|
|
409
|
+
|
|
410
|
+
const a11y = await page.accessibility.snapshot();
|
|
411
|
+
fs.writeFileSync(
|
|
412
|
+
path.join(stepDir, 'a11y.json'),
|
|
413
|
+
JSON.stringify(a11y, null, 2)
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
// Save new console logs since last step
|
|
417
|
+
const newConsoleLogs = consoleLogs.splice(0);
|
|
418
|
+
if (newConsoleLogs.length > 0) {
|
|
419
|
+
fs.writeFileSync(
|
|
420
|
+
path.join(stepDir, 'console.jsonl'),
|
|
421
|
+
newConsoleLogs.map(l => JSON.stringify(l)).join('\n')
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Save new network logs since last step
|
|
426
|
+
const newNetworkLogs = networkLogs.splice(0);
|
|
427
|
+
if (newNetworkLogs.length > 0) {
|
|
428
|
+
fs.writeFileSync(
|
|
429
|
+
path.join(stepDir, 'network.jsonl'),
|
|
430
|
+
newNetworkLogs.map(l => JSON.stringify(l)).join('\n')
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
console.log(`✓ Interaction complete: ${scenario.steps.length} steps captured`);
|
|
436
|
+
};
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
## 3. Analysis Commands
|
|
440
|
+
|
|
441
|
+
Query tools that operate on captured data without requiring browser access. These are standalone scripts (not Playwright commands).
|
|
442
|
+
|
|
443
|
+
### 3.1 Query Accessibility Tree
|
|
444
|
+
|
|
445
|
+
**Purpose:** Extract specific information from accessibility tree
|
|
446
|
+
|
|
447
|
+
**Usage:**
|
|
448
|
+
```bash
|
|
449
|
+
node scripts/query-a11y.js <snapshot-dir/a11y.json> <query>
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
**Queries:**
|
|
453
|
+
- `--buttons` - List all buttons
|
|
454
|
+
- `--links` - List all links
|
|
455
|
+
- `--inputs` - List all input fields
|
|
456
|
+
- `--interactive` - List all interactive elements
|
|
457
|
+
- `--disabled` - List disabled elements
|
|
458
|
+
- `<text>` - Search by text content
|
|
459
|
+
|
|
460
|
+
**File: `scripts/query-a11y.js`**
|
|
461
|
+
```javascript
|
|
462
|
+
#!/usr/bin/env node
|
|
463
|
+
const fs = require('fs');
|
|
464
|
+
|
|
465
|
+
const snapshotFile = process.argv[2];
|
|
466
|
+
const query = process.argv[3];
|
|
467
|
+
|
|
468
|
+
if (!snapshotFile || !query) {
|
|
469
|
+
console.error('Usage: query-a11y.js <a11y.json> <query>');
|
|
470
|
+
process.exit(1);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const tree = JSON.parse(fs.readFileSync(snapshotFile, 'utf8'));
|
|
474
|
+
|
|
475
|
+
function traverse(node, matcher, results = []) {
|
|
476
|
+
if (!node) return results;
|
|
477
|
+
|
|
478
|
+
if (matcher(node)) {
|
|
479
|
+
results.push(node);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (node.children) {
|
|
483
|
+
node.children.forEach(child => traverse(child, matcher, results));
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return results;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Define matchers for different queries
|
|
490
|
+
let matcher;
|
|
491
|
+
switch (query) {
|
|
492
|
+
case '--buttons':
|
|
493
|
+
matcher = n => n.role === 'button';
|
|
494
|
+
break;
|
|
495
|
+
case '--links':
|
|
496
|
+
matcher = n => n.role === 'link';
|
|
497
|
+
break;
|
|
498
|
+
case '--inputs':
|
|
499
|
+
matcher = n => ['textbox', 'searchbox', 'combobox'].includes(n.role);
|
|
500
|
+
break;
|
|
501
|
+
case '--interactive':
|
|
502
|
+
matcher = n => ['button', 'link', 'textbox', 'checkbox', 'radio', 'searchbox'].includes(n.role);
|
|
503
|
+
break;
|
|
504
|
+
case '--disabled':
|
|
505
|
+
matcher = n => n.disabled === true;
|
|
506
|
+
break;
|
|
507
|
+
default:
|
|
508
|
+
// Text search
|
|
509
|
+
matcher = n => n.name && n.name.toLowerCase().includes(query.toLowerCase());
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const results = traverse(tree, matcher);
|
|
513
|
+
|
|
514
|
+
// Output compact JSON for each result
|
|
515
|
+
results.forEach(r => {
|
|
516
|
+
console.log(JSON.stringify({
|
|
517
|
+
role: r.role,
|
|
518
|
+
name: r.name,
|
|
519
|
+
disabled: r.disabled,
|
|
520
|
+
focused: r.focused
|
|
521
|
+
}));
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
console.error(`\n✓ Found ${results.length} matches`);
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
**Example Usage:**
|
|
528
|
+
```bash
|
|
529
|
+
# Find all buttons
|
|
530
|
+
node scripts/query-a11y.js snapshots/current/a11y.json --buttons
|
|
531
|
+
|
|
532
|
+
# Find submit button
|
|
533
|
+
node scripts/query-a11y.js snapshots/current/a11y.json "Submit"
|
|
534
|
+
|
|
535
|
+
# Find disabled elements
|
|
536
|
+
node scripts/query-a11y.js snapshots/current/a11y.json --disabled
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
### 3.2 Analyze Console Logs
|
|
540
|
+
|
|
541
|
+
**Purpose:** Summarize and filter console messages
|
|
542
|
+
|
|
543
|
+
**Usage:**
|
|
544
|
+
```bash
|
|
545
|
+
node scripts/analyze-console.js <snapshot-dir/console.jsonl>
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
**File: `scripts/analyze-console.js`**
|
|
549
|
+
```javascript
|
|
550
|
+
#!/usr/bin/env node
|
|
551
|
+
const fs = require('fs');
|
|
552
|
+
|
|
553
|
+
const logFile = process.argv[2];
|
|
554
|
+
|
|
555
|
+
if (!logFile) {
|
|
556
|
+
console.error('Usage: analyze-console.js <console.jsonl>');
|
|
557
|
+
process.exit(1);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const logs = fs.readFileSync(logFile, 'utf8')
|
|
561
|
+
.split('\n')
|
|
562
|
+
.filter(Boolean)
|
|
563
|
+
.map(JSON.parse);
|
|
564
|
+
|
|
565
|
+
// Count by type
|
|
566
|
+
const counts = {};
|
|
567
|
+
logs.forEach(log => {
|
|
568
|
+
counts[log.type] = (counts[log.type] || 0) + 1;
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
console.log('=== Console Summary ===');
|
|
572
|
+
console.log(`Total messages: ${logs.length}`);
|
|
573
|
+
Object.entries(counts).forEach(([type, count]) => {
|
|
574
|
+
console.log(` ${type}: ${count}`);
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
// Show unique errors
|
|
578
|
+
const errors = logs.filter(l => l.type === 'error');
|
|
579
|
+
if (errors.length > 0) {
|
|
580
|
+
console.log('\n=== Errors ===');
|
|
581
|
+
const uniqueErrors = [...new Set(errors.map(e => e.text))];
|
|
582
|
+
uniqueErrors.forEach((err, i) => {
|
|
583
|
+
const count = errors.filter(e => e.text === err).length;
|
|
584
|
+
console.log(`${i + 1}. [${count}x] ${err}`);
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Show unique warnings
|
|
589
|
+
const warnings = logs.filter(l => l.type === 'warning');
|
|
590
|
+
if (warnings.length > 0) {
|
|
591
|
+
console.log('\n=== Warnings ===');
|
|
592
|
+
const uniqueWarnings = [...new Set(warnings.map(w => w.text))];
|
|
593
|
+
uniqueWarnings.forEach((warn, i) => {
|
|
594
|
+
const count = warnings.filter(w => w.text === warn).length;
|
|
595
|
+
console.log(`${i + 1}. [${count}x] ${warn}`);
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
### 3.3 Analyze Network Requests
|
|
601
|
+
|
|
602
|
+
**Purpose:** Summarize network activity and identify failures
|
|
603
|
+
|
|
604
|
+
**Usage:**
|
|
605
|
+
```bash
|
|
606
|
+
node scripts/analyze-network.js <snapshot-dir/network.jsonl>
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
**File: `scripts/analyze-network.js`**
|
|
610
|
+
```javascript
|
|
611
|
+
#!/usr/bin/env node
|
|
612
|
+
const fs = require('fs');
|
|
613
|
+
|
|
614
|
+
const logFile = process.argv[2];
|
|
615
|
+
|
|
616
|
+
if (!logFile) {
|
|
617
|
+
console.error('Usage: analyze-network.js <network.jsonl>');
|
|
618
|
+
process.exit(1);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const requests = fs.readFileSync(logFile, 'utf8')
|
|
622
|
+
.split('\n')
|
|
623
|
+
.filter(Boolean)
|
|
624
|
+
.map(JSON.parse);
|
|
625
|
+
|
|
626
|
+
console.log('=== Network Summary ===');
|
|
627
|
+
console.log(`Total requests: ${requests.length}`);
|
|
628
|
+
|
|
629
|
+
// Count by resource type
|
|
630
|
+
const byType = {};
|
|
631
|
+
requests.forEach(req => {
|
|
632
|
+
byType[req.resourceType] = (byType[req.resourceType] || 0) + 1;
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
console.log('\nBy resource type:');
|
|
636
|
+
Object.entries(byType).forEach(([type, count]) => {
|
|
637
|
+
console.log(` ${type}: ${count}`);
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
// Failed requests
|
|
641
|
+
const failed = requests.filter(r => r.status >= 400);
|
|
642
|
+
if (failed.length > 0) {
|
|
643
|
+
console.log(`\n=== Failed Requests (${failed.length}) ===`);
|
|
644
|
+
failed.forEach(req => {
|
|
645
|
+
console.log(` ${req.status} ${req.method} ${req.url}`);
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// Slow requests (if timing available)
|
|
650
|
+
const slow = requests.filter(r =>
|
|
651
|
+
r.timing && (r.timing.responseEnd - r.timing.startTime) > 1000
|
|
652
|
+
);
|
|
653
|
+
if (slow.length > 0) {
|
|
654
|
+
console.log(`\n=== Slow Requests (>1s, ${slow.length}) ===`);
|
|
655
|
+
slow.forEach(req => {
|
|
656
|
+
const duration = Math.round(r.timing.responseEnd - r.timing.startTime);
|
|
657
|
+
console.log(` ${duration}ms ${req.method} ${req.url}`);
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
### 3.4 Compare Snapshots
|
|
663
|
+
|
|
664
|
+
**Purpose:** Diff two snapshots to identify changes
|
|
665
|
+
|
|
666
|
+
**Usage:**
|
|
667
|
+
```bash
|
|
668
|
+
node scripts/compare.js <snapshot-dir-1> <snapshot-dir-2>
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
**File: `scripts/compare.js`**
|
|
672
|
+
```javascript
|
|
673
|
+
#!/usr/bin/env node
|
|
674
|
+
const fs = require('fs');
|
|
675
|
+
const path = require('path');
|
|
676
|
+
|
|
677
|
+
const dir1 = process.argv[2];
|
|
678
|
+
const dir2 = process.argv[3];
|
|
679
|
+
|
|
680
|
+
if (!dir1 || !dir2) {
|
|
681
|
+
console.error('Usage: compare.js <snapshot-1> <snapshot-2>');
|
|
682
|
+
process.exit(1);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function loadSnapshot(dir) {
|
|
686
|
+
const loadJsonl = (file) => {
|
|
687
|
+
const filepath = path.join(dir, file);
|
|
688
|
+
if (!fs.existsSync(filepath)) return [];
|
|
689
|
+
return fs.readFileSync(filepath, 'utf8')
|
|
690
|
+
.split('\n')
|
|
691
|
+
.filter(Boolean)
|
|
692
|
+
.map(JSON.parse);
|
|
693
|
+
};
|
|
694
|
+
|
|
695
|
+
return {
|
|
696
|
+
a11y: JSON.parse(fs.readFileSync(path.join(dir, 'a11y.json'), 'utf8')),
|
|
697
|
+
console: loadJsonl('console.jsonl'),
|
|
698
|
+
network: loadJsonl('network.jsonl')
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
const snap1 = loadSnapshot(dir1);
|
|
703
|
+
const snap2 = loadSnapshot(dir2);
|
|
704
|
+
|
|
705
|
+
console.log('=== Snapshot Comparison ===\n');
|
|
706
|
+
|
|
707
|
+
// Compare console logs
|
|
708
|
+
const errors1 = snap1.console.filter(l => l.type === 'error').map(l => l.text);
|
|
709
|
+
const errors2 = snap2.console.filter(l => l.type === 'error').map(l => l.text);
|
|
710
|
+
|
|
711
|
+
const newErrors = errors2.filter(e => !errors1.includes(e));
|
|
712
|
+
const fixedErrors = errors1.filter(e => !errors2.includes(e));
|
|
713
|
+
|
|
714
|
+
console.log('Console Errors:');
|
|
715
|
+
if (newErrors.length > 0) {
|
|
716
|
+
console.log(` New (${newErrors.length}):`);
|
|
717
|
+
newErrors.forEach(e => console.log(` + ${e}`));
|
|
718
|
+
}
|
|
719
|
+
if (fixedErrors.length > 0) {
|
|
720
|
+
console.log(` Fixed (${fixedErrors.length}):`);
|
|
721
|
+
fixedErrors.forEach(e => console.log(` - ${e}`));
|
|
722
|
+
}
|
|
723
|
+
if (newErrors.length === 0 && fixedErrors.length === 0) {
|
|
724
|
+
console.log(' No changes');
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// Compare network failures
|
|
728
|
+
const failed1 = snap1.network.filter(r => r.status >= 400).map(r => r.url);
|
|
729
|
+
const failed2 = snap2.network.filter(r => r.status >= 400).map(r => r.url);
|
|
730
|
+
|
|
731
|
+
const newFailures = failed2.filter(u => !failed1.includes(u));
|
|
732
|
+
const fixedFailures = failed1.filter(u => !failed2.includes(u));
|
|
733
|
+
|
|
734
|
+
console.log('\nNetwork Failures:');
|
|
735
|
+
if (newFailures.length > 0) {
|
|
736
|
+
console.log(` New (${newFailures.length}):`);
|
|
737
|
+
newFailures.forEach(u => console.log(` + ${u}`));
|
|
738
|
+
}
|
|
739
|
+
if (fixedFailures.length > 0) {
|
|
740
|
+
console.log(` Fixed (${fixedFailures.length}):`);
|
|
741
|
+
fixedFailures.forEach(u => console.log(` - ${u}`));
|
|
742
|
+
}
|
|
743
|
+
if (newFailures.length === 0 && fixedFailures.length === 0) {
|
|
744
|
+
console.log(' No changes');
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// Compare a11y tree size (rough indicator of structural changes)
|
|
748
|
+
const a11ySize1 = JSON.stringify(snap1.a11y).length;
|
|
749
|
+
const a11ySize2 = JSON.stringify(snap2.a11y).length;
|
|
750
|
+
const sizeDiff = a11ySize2 - a11ySize1;
|
|
751
|
+
|
|
752
|
+
console.log('\nAccessibility Tree:');
|
|
753
|
+
console.log(` Before: ${a11ySize1} bytes`);
|
|
754
|
+
console.log(` After: ${a11ySize2} bytes`);
|
|
755
|
+
if (sizeDiff !== 0) {
|
|
756
|
+
console.log(` Change: ${sizeDiff > 0 ? '+' : ''}${sizeDiff} bytes`);
|
|
757
|
+
}
|
|
758
|
+
```
|
|
759
|
+
|
|
760
|
+
## 4. Code Mode (User Scripts)
|
|
761
|
+
|
|
762
|
+
The core runtime provides an execution environment for user-written scripts. This enables the agent to write custom debugging scripts for novel scenarios.
|
|
763
|
+
|
|
764
|
+
### 4.1 Execution Model
|
|
765
|
+
|
|
766
|
+
**Command:**
|
|
767
|
+
```bash
|
|
768
|
+
node scripts/pw.js exec <script.js> [args...]
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
**Script Context:**
|
|
772
|
+
User scripts execute as async functions with access to:
|
|
773
|
+
- `page` - Playwright Page instance
|
|
774
|
+
- `context` - Playwright BrowserContext instance
|
|
775
|
+
- `browser` - Playwright Browser instance
|
|
776
|
+
- `args` - Array of command-line arguments
|
|
777
|
+
- `fs` - Node.js filesystem module
|
|
778
|
+
- `path` - Node.js path module
|
|
779
|
+
- `utils` - Helper utilities for writing data
|
|
780
|
+
|
|
781
|
+
**Utils Object:**
|
|
782
|
+
```javascript
|
|
783
|
+
utils = {
|
|
784
|
+
writeJson: (filepath, data) => {
|
|
785
|
+
// Creates directories if needed, writes JSON with formatting
|
|
786
|
+
},
|
|
787
|
+
writeJsonl: (filepath, items) => {
|
|
788
|
+
// Writes array of objects as JSONL (one JSON object per line)
|
|
789
|
+
},
|
|
790
|
+
appendJsonl: (filepath, item) => {
|
|
791
|
+
// Appends single object to JSONL file
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
### 4.2 Script Template
|
|
797
|
+
|
|
798
|
+
```javascript
|
|
799
|
+
// user-script.js
|
|
800
|
+
// Available context: page, context, browser, args, fs, path, utils
|
|
801
|
+
|
|
802
|
+
const url = args[0];
|
|
803
|
+
const output = args[1];
|
|
804
|
+
|
|
805
|
+
// Navigate to page
|
|
806
|
+
await page.goto(url);
|
|
807
|
+
|
|
808
|
+
// Extract data using Playwright API
|
|
809
|
+
const data = await page.evaluate(() => {
|
|
810
|
+
// Browser context - extract data from DOM
|
|
811
|
+
return {
|
|
812
|
+
// ... extracted data
|
|
813
|
+
};
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
// Write to disk
|
|
817
|
+
utils.writeJson(output, data);
|
|
818
|
+
|
|
819
|
+
console.log(`Results saved to ${output}`);
|
|
820
|
+
```
|
|
821
|
+
|
|
822
|
+
### 4.3 Example User Scripts
|
|
823
|
+
|
|
824
|
+
**Extract Form Data:**
|
|
825
|
+
```javascript
|
|
826
|
+
// extract-forms.js
|
|
827
|
+
// Usage: node pw.js exec extract-forms.js <url> <output>
|
|
828
|
+
|
|
829
|
+
const url = args[0];
|
|
830
|
+
const output = args[1];
|
|
831
|
+
|
|
832
|
+
await page.goto(url, { waitUntil: 'networkidle' });
|
|
833
|
+
|
|
834
|
+
const forms = await page.evaluate(() => {
|
|
835
|
+
return Array.from(document.querySelectorAll('form')).map(form => ({
|
|
836
|
+
action: form.action,
|
|
837
|
+
method: form.method,
|
|
838
|
+
fields: Array.from(form.elements).map(el => ({
|
|
839
|
+
name: el.name,
|
|
840
|
+
type: el.type,
|
|
841
|
+
required: el.required,
|
|
842
|
+
value: el.type === 'password' ? '[REDACTED]' : el.value,
|
|
843
|
+
disabled: el.disabled
|
|
844
|
+
}))
|
|
845
|
+
}));
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
utils.writeJson(output, {
|
|
849
|
+
url,
|
|
850
|
+
timestamp: new Date().toISOString(),
|
|
851
|
+
forms
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
console.log(`Found ${forms.length} forms, saved to ${output}`);
|
|
855
|
+
```
|
|
856
|
+
|
|
857
|
+
**Monitor Memory Over Time:**
|
|
858
|
+
```javascript
|
|
859
|
+
// monitor-memory.js
|
|
860
|
+
// Usage: node pw.js exec monitor-memory.js <url> <duration-ms> <interval-ms> <output>
|
|
861
|
+
|
|
862
|
+
const url = args[0];
|
|
863
|
+
const duration = parseInt(args[1]) || 30000; // 30 seconds default
|
|
864
|
+
const interval = parseInt(args[2]) || 1000; // 1 second default
|
|
865
|
+
const output = args[3];
|
|
866
|
+
|
|
867
|
+
await page.goto(url);
|
|
868
|
+
|
|
869
|
+
const samples = [];
|
|
870
|
+
const startTime = Date.now();
|
|
871
|
+
|
|
872
|
+
console.log(`Monitoring memory for ${duration}ms...`);
|
|
873
|
+
|
|
874
|
+
while (Date.now() - startTime < duration) {
|
|
875
|
+
const metrics = await page.metrics();
|
|
876
|
+
samples.push({
|
|
877
|
+
timestamp: Date.now() - startTime,
|
|
878
|
+
jsHeapUsed: metrics.JSHeapUsedSize,
|
|
879
|
+
jsHeapTotal: metrics.JSHeapTotalSize,
|
|
880
|
+
documents: metrics.Documents,
|
|
881
|
+
frames: metrics.Frames
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
await page.waitForTimeout(interval);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
utils.writeJsonl(output, samples);
|
|
888
|
+
|
|
889
|
+
console.log(`Collected ${samples.length} memory samples`);
|
|
890
|
+
console.log(`Heap growth: ${samples[samples.length-1].jsHeapUsed - samples[0].jsHeapUsed} bytes`);
|
|
891
|
+
```
|
|
892
|
+
|
|
893
|
+
**Test Interaction Flow:**
|
|
894
|
+
```javascript
|
|
895
|
+
// test-checkout.js
|
|
896
|
+
// Usage: node pw.js exec test-checkout.js <base-url> <output-dir>
|
|
897
|
+
|
|
898
|
+
const baseUrl = args[0];
|
|
899
|
+
const outputDir = args[1];
|
|
900
|
+
|
|
901
|
+
// Track errors during flow
|
|
902
|
+
const errors = [];
|
|
903
|
+
page.on('console', msg => {
|
|
904
|
+
if (msg.type() === 'error') {
|
|
905
|
+
errors.push({
|
|
906
|
+
timestamp: new Date().toISOString(),
|
|
907
|
+
text: msg.text()
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
// Step 1: Add item to cart
|
|
913
|
+
await page.goto(`${baseUrl}/products/123`);
|
|
914
|
+
const beforeCart = await page.accessibility.snapshot();
|
|
915
|
+
utils.writeJson(`${outputDir}/1-before-add.json`, beforeCart);
|
|
916
|
+
|
|
917
|
+
await page.click('button.add-to-cart');
|
|
918
|
+
await page.waitForTimeout(1000);
|
|
919
|
+
|
|
920
|
+
const afterCart = await page.accessibility.snapshot();
|
|
921
|
+
utils.writeJson(`${outputDir}/2-after-add.json`, afterCart);
|
|
922
|
+
|
|
923
|
+
// Step 2: Go to checkout
|
|
924
|
+
await page.click('a[href="/cart"]');
|
|
925
|
+
await page.waitForNavigation();
|
|
926
|
+
|
|
927
|
+
const cartPage = await page.accessibility.snapshot();
|
|
928
|
+
utils.writeJson(`${outputDir}/3-cart-page.json`, cartPage);
|
|
929
|
+
|
|
930
|
+
await page.click('button.checkout');
|
|
931
|
+
await page.waitForNavigation();
|
|
932
|
+
|
|
933
|
+
const checkoutPage = await page.accessibility.snapshot();
|
|
934
|
+
utils.writeJson(`${outputDir}/4-checkout-page.json`, checkoutPage);
|
|
935
|
+
|
|
936
|
+
// Save errors if any occurred
|
|
937
|
+
if (errors.length > 0) {
|
|
938
|
+
utils.writeJsonl(`${outputDir}/errors.jsonl`, errors);
|
|
939
|
+
console.log(`⚠️ ${errors.length} console errors during checkout flow`);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
console.log('Checkout flow captured successfully');
|
|
943
|
+
```
|
|
944
|
+
|
|
945
|
+
**Extract All Links:**
|
|
946
|
+
```javascript
|
|
947
|
+
// extract-links.js
|
|
948
|
+
// Usage: node pw.js exec extract-links.js <url> <output>
|
|
949
|
+
|
|
950
|
+
const url = args[0];
|
|
951
|
+
const output = args[1];
|
|
952
|
+
|
|
953
|
+
await page.goto(url, { waitUntil: 'networkidle' });
|
|
954
|
+
|
|
955
|
+
const links = await page.evaluate(() => {
|
|
956
|
+
return Array.from(document.links).map(link => ({
|
|
957
|
+
href: link.href,
|
|
958
|
+
text: link.textContent.trim(),
|
|
959
|
+
target: link.target,
|
|
960
|
+
rel: link.rel
|
|
961
|
+
}));
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
// Test each link
|
|
965
|
+
const results = [];
|
|
966
|
+
for (const link of links) {
|
|
967
|
+
try {
|
|
968
|
+
const response = await page.request.head(link.href);
|
|
969
|
+
results.push({
|
|
970
|
+
...link,
|
|
971
|
+
status: response.status(),
|
|
972
|
+
ok: response.ok()
|
|
973
|
+
});
|
|
974
|
+
} catch (err) {
|
|
975
|
+
results.push({
|
|
976
|
+
...link,
|
|
977
|
+
error: err.message
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
utils.writeJson(output, {
|
|
983
|
+
url,
|
|
984
|
+
totalLinks: links.length,
|
|
985
|
+
brokenLinks: results.filter(r => !r.ok && !r.error).length,
|
|
986
|
+
errors: results.filter(r => r.error).length,
|
|
987
|
+
links: results
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
console.log(`Checked ${links.length} links`);
|
|
991
|
+
```
|
|
992
|
+
|
|
993
|
+
## 5. Project Structure
|
|
994
|
+
|
|
995
|
+
```
|
|
996
|
+
uisnap/
|
|
997
|
+
├── scripts/
|
|
998
|
+
│ ├── pw.js # Core runtime
|
|
999
|
+
│ ├── commands/ # Built-in commands
|
|
1000
|
+
│ │ ├── snapshot.js
|
|
1001
|
+
│ │ └── interact.js
|
|
1002
|
+
│ ├── query-a11y.js # Analysis: query a11y tree
|
|
1003
|
+
│ ├── analyze-console.js # Analysis: summarize console logs
|
|
1004
|
+
│ ├── analyze-network.js # Analysis: summarize network
|
|
1005
|
+
│ ├── compare.js # Analysis: diff snapshots
|
|
1006
|
+
│ └── examples/ # Example user scripts
|
|
1007
|
+
│ ├── extract-forms.js
|
|
1008
|
+
│ ├── monitor-memory.js
|
|
1009
|
+
│ ├── test-checkout.js
|
|
1010
|
+
│ └── extract-links.js
|
|
1011
|
+
├── snapshots/ # Default output directory
|
|
1012
|
+
│ └── .gitkeep
|
|
1013
|
+
├── scenarios/ # Interaction scenarios
|
|
1014
|
+
│ └── example-login.yml
|
|
1015
|
+
├── package.json
|
|
1016
|
+
└── README.md
|
|
1017
|
+
```
|
|
1018
|
+
|
|
1019
|
+
## 6. Installation & Setup
|
|
1020
|
+
|
|
1021
|
+
**package.json:**
|
|
1022
|
+
```json
|
|
1023
|
+
{
|
|
1024
|
+
"name": "uisnap",
|
|
1025
|
+
"version": "1.0.0",
|
|
1026
|
+
"description": "Efficient frontend debugging toolkit for Claude Code",
|
|
1027
|
+
"scripts": {
|
|
1028
|
+
"snapshot": "node scripts/pw.js snapshot",
|
|
1029
|
+
"interact": "node scripts/pw.js interact"
|
|
1030
|
+
},
|
|
1031
|
+
"dependencies": {
|
|
1032
|
+
"playwright": "^1.40.0",
|
|
1033
|
+
"yaml": "^2.3.4"
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
```
|
|
1037
|
+
|
|
1038
|
+
**Installation:**
|
|
1039
|
+
```bash
|
|
1040
|
+
npm install
|
|
1041
|
+
npx playwright install chromium
|
|
1042
|
+
```
|
|
1043
|
+
|
|
1044
|
+
## 7. Usage Workflows
|
|
1045
|
+
|
|
1046
|
+
### 7.1 Basic Debugging Workflow
|
|
1047
|
+
|
|
1048
|
+
```bash
|
|
1049
|
+
# 1. Capture page state
|
|
1050
|
+
node scripts/pw.js snapshot "https://app.example.com" snapshots/current/
|
|
1051
|
+
|
|
1052
|
+
# 2. Analyze what was captured
|
|
1053
|
+
node scripts/analyze-console.js snapshots/current/console.jsonl
|
|
1054
|
+
node scripts/analyze-network.js snapshots/current/network.jsonl
|
|
1055
|
+
|
|
1056
|
+
# 3. Query accessibility tree for specific elements
|
|
1057
|
+
node scripts/query-a11y.js snapshots/current/a11y.json --buttons
|
|
1058
|
+
node scripts/query-a11y.js snapshots/current/a11y.json "Submit"
|
|
1059
|
+
|
|
1060
|
+
# 4. If needed, write custom script for deeper investigation
|
|
1061
|
+
node scripts/pw.js exec scripts/examples/extract-forms.js \
|
|
1062
|
+
"https://app.example.com" snapshots/forms.json
|
|
1063
|
+
```
|
|
1064
|
+
|
|
1065
|
+
### 7.2 Interaction Testing Workflow
|
|
1066
|
+
|
|
1067
|
+
```bash
|
|
1068
|
+
# 1. Create scenario
|
|
1069
|
+
cat > scenarios/login-test.yml << EOF
|
|
1070
|
+
steps:
|
|
1071
|
+
- action: goto
|
|
1072
|
+
url: https://app.example.com/login
|
|
1073
|
+
- action: fill
|
|
1074
|
+
selector: input[name="email"]
|
|
1075
|
+
value: test@example.com
|
|
1076
|
+
- action: fill
|
|
1077
|
+
selector: input[name="password"]
|
|
1078
|
+
value: password123
|
|
1079
|
+
- action: click
|
|
1080
|
+
selector: button[type="submit"]
|
|
1081
|
+
wait: 2000
|
|
1082
|
+
EOF
|
|
1083
|
+
|
|
1084
|
+
# 2. Run interaction and capture state at each step
|
|
1085
|
+
node scripts/pw.js interact scenarios/login-test.yml snapshots/login-flow/
|
|
1086
|
+
|
|
1087
|
+
# 3. Analyze each step
|
|
1088
|
+
node scripts/analyze-console.js snapshots/login-flow/step-1-goto/console.jsonl
|
|
1089
|
+
node scripts/analyze-console.js snapshots/login-flow/step-4-click/console.jsonl
|
|
1090
|
+
|
|
1091
|
+
# 4. Compare before/after
|
|
1092
|
+
node scripts/compare.js \
|
|
1093
|
+
snapshots/login-flow/step-1-goto/ \
|
|
1094
|
+
snapshots/login-flow/step-4-click/
|
|
1095
|
+
```
|
|
1096
|
+
|
|
1097
|
+
### 7.3 Regression Testing Workflow
|
|
1098
|
+
|
|
1099
|
+
```bash
|
|
1100
|
+
# 1. Capture baseline
|
|
1101
|
+
node scripts/pw.js snapshot "https://app.example.com" snapshots/baseline/
|
|
1102
|
+
|
|
1103
|
+
# 2. Make code changes
|
|
1104
|
+
# ... deploy new version ...
|
|
1105
|
+
|
|
1106
|
+
# 3. Capture after changes
|
|
1107
|
+
node scripts/pw.js snapshot "https://app.example.com" snapshots/after-deploy/
|
|
1108
|
+
|
|
1109
|
+
# 4. Compare
|
|
1110
|
+
node scripts/compare.js snapshots/baseline/ snapshots/after-deploy/
|
|
1111
|
+
```
|
|
1112
|
+
|
|
1113
|
+
### 7.4 Custom Investigation Workflow
|
|
1114
|
+
|
|
1115
|
+
```bash
|
|
1116
|
+
# Agent identifies need for custom data extraction
|
|
1117
|
+
# Writes a script:
|
|
1118
|
+
|
|
1119
|
+
cat > scripts/check-memory-leak.js << 'EOF'
|
|
1120
|
+
const url = args[0];
|
|
1121
|
+
const output = args[1];
|
|
1122
|
+
|
|
1123
|
+
await page.goto(url);
|
|
1124
|
+
|
|
1125
|
+
const samples = [];
|
|
1126
|
+
for (let i = 0; i < 20; i++) {
|
|
1127
|
+
await page.click('.trigger-action');
|
|
1128
|
+
await page.waitForTimeout(500);
|
|
1129
|
+
|
|
1130
|
+
const metrics = await page.metrics();
|
|
1131
|
+
samples.push({
|
|
1132
|
+
iteration: i,
|
|
1133
|
+
heapUsed: metrics.JSHeapUsedSize
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
utils.writeJsonl(output, samples);
|
|
1138
|
+
|
|
1139
|
+
const growth = samples[19].heapUsed - samples[0].heapUsed;
|
|
1140
|
+
console.log(`Heap grew by ${growth} bytes over 20 iterations`);
|
|
1141
|
+
EOF
|
|
1142
|
+
|
|
1143
|
+
# Executes it:
|
|
1144
|
+
node scripts/pw.js exec scripts/check-memory-leak.js \
|
|
1145
|
+
"https://app.example.com" snapshots/memory-test.jsonl
|
|
1146
|
+
|
|
1147
|
+
# Analyzes results:
|
|
1148
|
+
jq -r '[.iteration, .heapUsed] | @tsv' snapshots/memory-test.jsonl
|
|
1149
|
+
```
|
|
1150
|
+
|
|
1151
|
+
## 8. Design Principles
|
|
1152
|
+
|
|
1153
|
+
### 8.1 Capture Once, Query Many
|
|
1154
|
+
- Expensive browser operations write to disk once
|
|
1155
|
+
- Cheap local queries can analyze data repeatedly
|
|
1156
|
+
- Agent avoids re-running browser for each question
|
|
1157
|
+
|
|
1158
|
+
### 8.2 Structured Output
|
|
1159
|
+
- All data is JSON or JSONL
|
|
1160
|
+
- Can be queried with standard tools (jq, grep, awk)
|
|
1161
|
+
- Can be imported into analysis tools (DuckDB, Python, etc.)
|
|
1162
|
+
|
|
1163
|
+
### 8.3 Composability
|
|
1164
|
+
- Built-in commands handle common cases
|
|
1165
|
+
- User scripts handle novel scenarios
|
|
1166
|
+
- Analysis tools work on any captured data
|
|
1167
|
+
- Scripts can be chained together
|
|
1168
|
+
|
|
1169
|
+
### 8.4 Disk-First
|
|
1170
|
+
- Everything goes to files, not stdout (unless it's a summary)
|
|
1171
|
+
- Enables post-hoc analysis
|
|
1172
|
+
- Files can be version controlled
|
|
1173
|
+
- Results persist across sessions
|
|
1174
|
+
|
|
1175
|
+
### 8.5 Progressive Disclosure
|
|
1176
|
+
- Start with summaries (analyze-*.js scripts)
|
|
1177
|
+
- Drill into details only when needed
|
|
1178
|
+
- Query locally before running browser again
|
|
1179
|
+
|
|
1180
|
+
## 9. Token Efficiency
|
|
1181
|
+
|
|
1182
|
+
**Problem:** Browser debugging traditionally expensive
|
|
1183
|
+
- Full DOM: ~15,000 tokens
|
|
1184
|
+
- All console logs: ~8,000 tokens
|
|
1185
|
+
- Screenshots: ~10,000 tokens
|
|
1186
|
+
- Total for basic debugging: ~40,000 tokens
|
|
1187
|
+
|
|
1188
|
+
**Solution:** This toolkit's approach
|
|
1189
|
+
- Capture to disk: ~2,000 tokens (one-time)
|
|
1190
|
+
- Analyze console summary: ~100 tokens
|
|
1191
|
+
- Query a11y tree: ~50 tokens
|
|
1192
|
+
- Read filtered results: ~200 tokens
|
|
1193
|
+
- Total for same debugging: ~2,350 tokens
|
|
1194
|
+
|
|
1195
|
+
**~17x reduction in token usage**
|
|
1196
|
+
|
|
1197
|
+
## 10. Extension Points
|
|
1198
|
+
|
|
1199
|
+
The toolkit is designed to be extended:
|
|
1200
|
+
|
|
1201
|
+
### 10.1 New Capture Commands
|
|
1202
|
+
Add to `scripts/commands/`:
|
|
1203
|
+
- `performance.js` - Capture Core Web Vitals
|
|
1204
|
+
- `lighthouse.js` - Run Lighthouse audit
|
|
1205
|
+
- `coverage.js` - Capture code coverage
|
|
1206
|
+
|
|
1207
|
+
### 10.2 New Analysis Scripts
|
|
1208
|
+
Add analysis tools:
|
|
1209
|
+
- `analyze-performance.js` - Analyze perf metrics
|
|
1210
|
+
- `find-unused-css.js` - Analyze coverage data
|
|
1211
|
+
- `extract-schema.js` - Extract structured data markup
|
|
1212
|
+
|
|
1213
|
+
### 10.3 Integration Hooks
|
|
1214
|
+
Scripts can integrate with external tools:
|
|
1215
|
+
- Post results to monitoring systems
|
|
1216
|
+
- Generate reports
|
|
1217
|
+
- Trigger alerts
|
|
1218
|
+
- Update dashboards
|
|
1219
|
+
|
|
1220
|
+
## 11. Skill Documentation
|
|
1221
|
+
|
|
1222
|
+
The toolkit should be accompanied by a Claude Code skill that teaches the agent:
|
|
1223
|
+
- When to use built-in commands vs. custom scripts
|
|
1224
|
+
- How to structure user scripts
|
|
1225
|
+
- Common debugging patterns
|
|
1226
|
+
- Token-efficient workflows
|
|
1227
|
+
|
|
1228
|
+
**Skill structure:**
|
|
1229
|
+
```markdown
|
|
1230
|
+
---
|
|
1231
|
+
name: uisnap
|
|
1232
|
+
description: Efficient frontend debugging with Playwright. Captures browser state to disk, enables local querying. Use when debugging web apps, investigating errors, or analyzing interactions.
|
|
1233
|
+
---
|
|
1234
|
+
|
|
1235
|
+
# uisnap
|
|
1236
|
+
|
|
1237
|
+
[Documentation of commands, workflows, examples]
|
|
1238
|
+
```
|
|
1239
|
+
|
|
1240
|
+
## 12. Success Criteria
|
|
1241
|
+
|
|
1242
|
+
The toolkit is successful if:
|
|
1243
|
+
|
|
1244
|
+
1. **Token Efficient**: >10x reduction in tokens for common debugging tasks
|
|
1245
|
+
2. **Flexible**: Agent can write custom scripts for novel scenarios
|
|
1246
|
+
3. **Composable**: Scripts and commands work together naturally
|
|
1247
|
+
4. **Discoverable**: Built-in commands cover 80% of use cases
|
|
1248
|
+
5. **Extensible**: Easy to add new commands and analysis tools
|
|
1249
|
+
|
|
1250
|
+
## 13. Future Enhancements
|
|
1251
|
+
|
|
1252
|
+
Potential future additions:
|
|
1253
|
+
|
|
1254
|
+
- **Visual regression**: Screenshot comparison with diff highlighting
|
|
1255
|
+
- **Performance tracking**: Time-series metrics capture
|
|
1256
|
+
- **Batch testing**: Run multiple scenarios in parallel
|
|
1257
|
+
- **Report generation**: HTML reports from captured data
|
|
1258
|
+
- **CI/CD integration**: Run in automated pipelines
|
|
1259
|
+
- **Real-device testing**: Mobile device emulation
|
|
1260
|
+
- **Network throttling**: Simulate slow connections
|
|
1261
|
+
- **Accessibility auditing**: Run aXe or similar tools
|