agentic-loop 3.1.6 → 3.2.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/package.json +1 -1
- package/ralph/setup.sh +76 -11
- package/ralph/verify.sh +12 -143
- package/templates/PROMPT.md +26 -43
- package/ralph/api.sh +0 -216
- package/ralph/browser-verify/README.md +0 -135
- package/ralph/browser-verify/verify.ts +0 -450
- package/ralph/playwright.sh +0 -238
- package/ralph/verify/browser.sh +0 -324
- package/ralph/verify/review.sh +0 -152
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
# Browser Verify Skill
|
|
2
|
-
|
|
3
|
-
Verify that a web page loads correctly using real browser automation (Playwright).
|
|
4
|
-
|
|
5
|
-
## Purpose
|
|
6
|
-
|
|
7
|
-
This skill launches a real Chromium browser to verify pages work correctly. Unlike asking Claude to "imagine" what a page looks like, this actually:
|
|
8
|
-
|
|
9
|
-
- Loads the page in a real browser
|
|
10
|
-
- Detects JavaScript console errors
|
|
11
|
-
- Detects failed network requests
|
|
12
|
-
- Checks for error messages on the page ("Oops!", "Something went wrong")
|
|
13
|
-
- Verifies required elements exist
|
|
14
|
-
- Takes screenshots for evidence
|
|
15
|
-
- Tests mobile viewport
|
|
16
|
-
|
|
17
|
-
## When to Use
|
|
18
|
-
|
|
19
|
-
Use this skill for **frontend story verification**:
|
|
20
|
-
- After implementing a UI feature
|
|
21
|
-
- To verify a page loads without errors
|
|
22
|
-
- To check that required elements render
|
|
23
|
-
- To test mobile responsiveness
|
|
24
|
-
|
|
25
|
-
## Usage
|
|
26
|
-
|
|
27
|
-
```bash
|
|
28
|
-
npx tsx ralph/browser-verify/verify.ts <url> [options]
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
### Options
|
|
32
|
-
|
|
33
|
-
| Option | Description | Default |
|
|
34
|
-
|--------|-------------|---------|
|
|
35
|
-
| `--selectors '["#app", ".btn"]'` | Required elements (JSON array) | `[]` |
|
|
36
|
-
| `--screenshot <path>` | Save screenshot to path | none |
|
|
37
|
-
| `--timeout <ms>` | Page load timeout | 30000 |
|
|
38
|
-
| `--headless` | Run without visible browser | true |
|
|
39
|
-
| `--no-headless` | Show browser window | false |
|
|
40
|
-
| `--check-console` | Fail on console errors | true |
|
|
41
|
-
| `--no-check-console` | Ignore console errors | false |
|
|
42
|
-
| `--mobile` | Use mobile viewport (375x667) | false |
|
|
43
|
-
| `--viewport <W>x<H>` | Custom viewport size | 1280x720 |
|
|
44
|
-
|
|
45
|
-
### Examples
|
|
46
|
-
|
|
47
|
-
**Basic page check:**
|
|
48
|
-
```bash
|
|
49
|
-
npx tsx ralph/browser-verify/verify.ts http://localhost:3000/dashboard
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
**Check specific elements exist:**
|
|
53
|
-
```bash
|
|
54
|
-
npx tsx ralph/browser-verify/verify.ts http://localhost:3000/login \
|
|
55
|
-
--selectors '["#email", "#password", "button[type=submit]"]'
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
**Take screenshot and check mobile:**
|
|
59
|
-
```bash
|
|
60
|
-
npx tsx ralph/browser-verify/verify.ts http://localhost:3000/dashboard \
|
|
61
|
-
--screenshot .ralph/screenshots/dashboard.png \
|
|
62
|
-
--mobile
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
**Debug mode (visible browser):**
|
|
66
|
-
```bash
|
|
67
|
-
npx tsx ralph/browser-verify/verify.ts http://localhost:3000/dashboard \
|
|
68
|
-
--no-headless
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
## Output
|
|
72
|
-
|
|
73
|
-
Returns JSON to stdout:
|
|
74
|
-
|
|
75
|
-
```json
|
|
76
|
-
{
|
|
77
|
-
"pass": true,
|
|
78
|
-
"errors": [],
|
|
79
|
-
"warnings": ["Page loaded slowly (3500ms)"],
|
|
80
|
-
"screenshot": ".ralph/screenshots/dashboard.png",
|
|
81
|
-
"title": "Dashboard - MyApp",
|
|
82
|
-
"elementsFound": ["#app", ".header", ".sidebar"],
|
|
83
|
-
"elementsMissing": [],
|
|
84
|
-
"consoleErrors": [],
|
|
85
|
-
"networkErrors": [],
|
|
86
|
-
"loadTime": 1234
|
|
87
|
-
}
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
### Exit Codes
|
|
91
|
-
|
|
92
|
-
- `0` - Verification passed
|
|
93
|
-
- `1` - Verification failed (check `errors` array)
|
|
94
|
-
|
|
95
|
-
## What It Catches
|
|
96
|
-
|
|
97
|
-
| Issue | How It's Detected |
|
|
98
|
-
|-------|-------------------|
|
|
99
|
-
| Page won't load | Connection refused, timeout |
|
|
100
|
-
| JavaScript errors | Console error messages |
|
|
101
|
-
| Failed API calls | Network request failures, 4xx/5xx responses |
|
|
102
|
-
| Error pages | Text matching "Oops!", "Something went wrong", etc. |
|
|
103
|
-
| Missing elements | Selector not found in DOM |
|
|
104
|
-
| Slow pages | Load time > 5 seconds (warning) |
|
|
105
|
-
|
|
106
|
-
## Integration with Ralph
|
|
107
|
-
|
|
108
|
-
Ralph automatically uses this skill for frontend story verification:
|
|
109
|
-
|
|
110
|
-
1. Story has `testUrl` defined
|
|
111
|
-
2. Ralph calls `verify.ts` with the URL
|
|
112
|
-
3. Optionally checks `selectors` from story config
|
|
113
|
-
4. Takes screenshot for evidence
|
|
114
|
-
5. Fails verification if errors detected
|
|
115
|
-
|
|
116
|
-
## Requirements
|
|
117
|
-
|
|
118
|
-
- Node.js 18+
|
|
119
|
-
- Playwright (`npx playwright install chromium`)
|
|
120
|
-
|
|
121
|
-
## Troubleshooting
|
|
122
|
-
|
|
123
|
-
**"Cannot find module 'playwright'"**
|
|
124
|
-
```bash
|
|
125
|
-
npm install playwright
|
|
126
|
-
npx playwright install chromium
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
**"Browser closed unexpectedly"**
|
|
130
|
-
- Try with `--no-headless` to see what's happening
|
|
131
|
-
- Check if the URL is accessible
|
|
132
|
-
|
|
133
|
-
**"Timeout waiting for page"**
|
|
134
|
-
- Increase timeout: `--timeout 60000`
|
|
135
|
-
- Check if dev server is running
|
|
@@ -1,450 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env npx tsx
|
|
2
|
-
/**
|
|
3
|
-
* browser-verify.ts - Real browser verification for Ralph
|
|
4
|
-
*
|
|
5
|
-
* Uses Playwright to actually load pages and verify they work.
|
|
6
|
-
* Much more reliable than asking Claude to "imagine" what a page looks like.
|
|
7
|
-
*
|
|
8
|
-
* Usage:
|
|
9
|
-
* npx tsx browser-verify.ts <url> [options]
|
|
10
|
-
*
|
|
11
|
-
* Options:
|
|
12
|
-
* --selectors '["#app", ".header"]' - Required elements (CSS selectors)
|
|
13
|
-
* --screenshot <path> - Save screenshot to path
|
|
14
|
-
* --timeout <ms> - Page load timeout (default: 30000)
|
|
15
|
-
* --headless - Run headless (default: true)
|
|
16
|
-
* --check-console - Fail on console errors (default: true)
|
|
17
|
-
* --check-network - Fail on failed network requests (default: true)
|
|
18
|
-
* --viewport <width>x<height> - Viewport size (default: 1280x720)
|
|
19
|
-
* --mobile - Use mobile viewport (375x667)
|
|
20
|
-
* --auth-cookie <json> - Set auth cookie before loading
|
|
21
|
-
*
|
|
22
|
-
* Output: JSON to stdout
|
|
23
|
-
* { "pass": true/false, "errors": [], "warnings": [], "screenshot": "path" }
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
import { chromium, Browser, Page, ConsoleMessage } from 'playwright';
|
|
27
|
-
|
|
28
|
-
interface AuthLogin {
|
|
29
|
-
loginUrl: string;
|
|
30
|
-
usernameSelector: string;
|
|
31
|
-
passwordSelector: string;
|
|
32
|
-
submitSelector: string;
|
|
33
|
-
username: string;
|
|
34
|
-
password: string;
|
|
35
|
-
successIndicator?: string; // Selector or URL pattern to confirm login worked
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
interface VerifyOptions {
|
|
39
|
-
url: string;
|
|
40
|
-
selectors: string[];
|
|
41
|
-
screenshot?: string;
|
|
42
|
-
timeout: number;
|
|
43
|
-
headless: boolean;
|
|
44
|
-
checkConsole: boolean;
|
|
45
|
-
checkNetwork: boolean;
|
|
46
|
-
viewport: { width: number; height: number };
|
|
47
|
-
authCookie?: { name: string; value: string; domain: string };
|
|
48
|
-
authLogin?: AuthLogin;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
interface VerifyResult {
|
|
52
|
-
pass: boolean;
|
|
53
|
-
errors: string[];
|
|
54
|
-
warnings: string[];
|
|
55
|
-
screenshot?: string;
|
|
56
|
-
title?: string;
|
|
57
|
-
elementsFound: string[];
|
|
58
|
-
elementsMissing: string[];
|
|
59
|
-
consoleErrors: string[];
|
|
60
|
-
networkErrors: string[];
|
|
61
|
-
loadTime: number;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
async function parseArgs(): Promise<VerifyOptions> {
|
|
65
|
-
const args = process.argv.slice(2);
|
|
66
|
-
|
|
67
|
-
if (args.length === 0 || args[0] === '--help') {
|
|
68
|
-
console.error(`
|
|
69
|
-
Usage: npx tsx browser-verify.ts <url> [options]
|
|
70
|
-
|
|
71
|
-
Options:
|
|
72
|
-
--selectors '["#app", ".btn"]' Required elements (JSON array)
|
|
73
|
-
--screenshot <path> Save screenshot
|
|
74
|
-
--timeout <ms> Load timeout (default: 30000)
|
|
75
|
-
--headless Run headless (default: true)
|
|
76
|
-
--no-headless Show browser window
|
|
77
|
-
--check-console Fail on console errors (default: true)
|
|
78
|
-
--no-check-console Ignore console errors
|
|
79
|
-
--check-network Fail on network errors (default: true)
|
|
80
|
-
--no-check-network Ignore network errors
|
|
81
|
-
--viewport <W>x<H> Viewport size (default: 1280x720)
|
|
82
|
-
--mobile Mobile viewport (375x667)
|
|
83
|
-
--auth-cookie '<json>' Set auth cookie
|
|
84
|
-
--auth-login '<json>' Login before verifying (see below)
|
|
85
|
-
|
|
86
|
-
Auth Login JSON format:
|
|
87
|
-
{
|
|
88
|
-
"loginUrl": "http://localhost:3000/login",
|
|
89
|
-
"usernameSelector": "input[name='email']",
|
|
90
|
-
"passwordSelector": "input[name='password']",
|
|
91
|
-
"submitSelector": "button[type='submit']",
|
|
92
|
-
"username": "test@test.local",
|
|
93
|
-
"password": "testpass123",
|
|
94
|
-
"successIndicator": "/dashboard" // URL pattern or selector
|
|
95
|
-
}
|
|
96
|
-
`);
|
|
97
|
-
process.exit(1);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const url = args[0];
|
|
101
|
-
const options: VerifyOptions = {
|
|
102
|
-
url,
|
|
103
|
-
selectors: [],
|
|
104
|
-
timeout: 30000,
|
|
105
|
-
headless: true,
|
|
106
|
-
checkConsole: true,
|
|
107
|
-
checkNetwork: true,
|
|
108
|
-
viewport: { width: 1280, height: 720 },
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
for (let i = 1; i < args.length; i++) {
|
|
112
|
-
const arg = args[i];
|
|
113
|
-
|
|
114
|
-
switch (arg) {
|
|
115
|
-
case '--selectors':
|
|
116
|
-
try {
|
|
117
|
-
options.selectors = JSON.parse(args[++i]);
|
|
118
|
-
} catch {
|
|
119
|
-
console.error('Invalid --selectors JSON');
|
|
120
|
-
process.exit(1);
|
|
121
|
-
}
|
|
122
|
-
break;
|
|
123
|
-
case '--screenshot':
|
|
124
|
-
options.screenshot = args[++i];
|
|
125
|
-
break;
|
|
126
|
-
case '--timeout':
|
|
127
|
-
options.timeout = parseInt(args[++i], 10);
|
|
128
|
-
break;
|
|
129
|
-
case '--headless':
|
|
130
|
-
options.headless = true;
|
|
131
|
-
break;
|
|
132
|
-
case '--no-headless':
|
|
133
|
-
options.headless = false;
|
|
134
|
-
break;
|
|
135
|
-
case '--check-console':
|
|
136
|
-
options.checkConsole = true;
|
|
137
|
-
break;
|
|
138
|
-
case '--no-check-console':
|
|
139
|
-
options.checkConsole = false;
|
|
140
|
-
break;
|
|
141
|
-
case '--check-network':
|
|
142
|
-
options.checkNetwork = true;
|
|
143
|
-
break;
|
|
144
|
-
case '--no-check-network':
|
|
145
|
-
options.checkNetwork = false;
|
|
146
|
-
break;
|
|
147
|
-
case '--viewport':
|
|
148
|
-
const [w, h] = args[++i].split('x').map(Number);
|
|
149
|
-
options.viewport = { width: w, height: h };
|
|
150
|
-
break;
|
|
151
|
-
case '--mobile':
|
|
152
|
-
options.viewport = { width: 375, height: 667 };
|
|
153
|
-
break;
|
|
154
|
-
case '--auth-cookie':
|
|
155
|
-
try {
|
|
156
|
-
options.authCookie = JSON.parse(args[++i]);
|
|
157
|
-
} catch {
|
|
158
|
-
console.error('Invalid --auth-cookie JSON');
|
|
159
|
-
process.exit(1);
|
|
160
|
-
}
|
|
161
|
-
break;
|
|
162
|
-
case '--auth-login':
|
|
163
|
-
try {
|
|
164
|
-
options.authLogin = JSON.parse(args[++i]);
|
|
165
|
-
} catch {
|
|
166
|
-
console.error('Invalid --auth-login JSON');
|
|
167
|
-
process.exit(1);
|
|
168
|
-
}
|
|
169
|
-
break;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
return options;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
async function performLogin(page: Page, auth: AuthLogin, timeout: number): Promise<{ success: boolean; error?: string }> {
|
|
177
|
-
try {
|
|
178
|
-
// Navigate to login page
|
|
179
|
-
await page.goto(auth.loginUrl, {
|
|
180
|
-
timeout,
|
|
181
|
-
waitUntil: 'networkidle',
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
// Wait for and fill username
|
|
185
|
-
await page.waitForSelector(auth.usernameSelector, { timeout: 10000 });
|
|
186
|
-
await page.fill(auth.usernameSelector, auth.username);
|
|
187
|
-
|
|
188
|
-
// Fill password
|
|
189
|
-
await page.waitForSelector(auth.passwordSelector, { timeout: 5000 });
|
|
190
|
-
await page.fill(auth.passwordSelector, auth.password);
|
|
191
|
-
|
|
192
|
-
// Click submit
|
|
193
|
-
await page.waitForSelector(auth.submitSelector, { timeout: 5000 });
|
|
194
|
-
await page.click(auth.submitSelector);
|
|
195
|
-
|
|
196
|
-
// Wait for login to complete
|
|
197
|
-
if (auth.successIndicator) {
|
|
198
|
-
if (auth.successIndicator.startsWith('/') || auth.successIndicator.startsWith('http')) {
|
|
199
|
-
// It's a URL pattern - wait for navigation
|
|
200
|
-
await page.waitForURL(`**${auth.successIndicator}*`, { timeout: 15000 });
|
|
201
|
-
} else {
|
|
202
|
-
// It's a selector - wait for element
|
|
203
|
-
await page.waitForSelector(auth.successIndicator, { timeout: 15000 });
|
|
204
|
-
}
|
|
205
|
-
} else {
|
|
206
|
-
// No indicator - just wait for navigation away from login page
|
|
207
|
-
await page.waitForFunction(
|
|
208
|
-
(loginUrl) => !window.location.href.includes(loginUrl),
|
|
209
|
-
auth.loginUrl,
|
|
210
|
-
{ timeout: 15000 }
|
|
211
|
-
);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
return { success: true };
|
|
215
|
-
} catch (err: any) {
|
|
216
|
-
return { success: false, error: `Login failed: ${err.message}` };
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
async function verify(options: VerifyOptions): Promise<VerifyResult> {
|
|
221
|
-
const result: VerifyResult = {
|
|
222
|
-
pass: true,
|
|
223
|
-
errors: [],
|
|
224
|
-
warnings: [],
|
|
225
|
-
elementsFound: [],
|
|
226
|
-
elementsMissing: [],
|
|
227
|
-
consoleErrors: [],
|
|
228
|
-
networkErrors: [],
|
|
229
|
-
loadTime: 0,
|
|
230
|
-
};
|
|
231
|
-
|
|
232
|
-
let browser: Browser | null = null;
|
|
233
|
-
let page: Page | null = null;
|
|
234
|
-
|
|
235
|
-
try {
|
|
236
|
-
// Launch browser
|
|
237
|
-
browser = await chromium.launch({
|
|
238
|
-
headless: options.headless,
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
const context = await browser.newContext({
|
|
242
|
-
viewport: options.viewport,
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
// Set auth cookie if provided
|
|
246
|
-
if (options.authCookie) {
|
|
247
|
-
await context.addCookies([{
|
|
248
|
-
name: options.authCookie.name,
|
|
249
|
-
value: options.authCookie.value,
|
|
250
|
-
domain: options.authCookie.domain,
|
|
251
|
-
path: '/',
|
|
252
|
-
}]);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
page = await context.newPage();
|
|
256
|
-
|
|
257
|
-
// Perform login if auth credentials provided
|
|
258
|
-
if (options.authLogin) {
|
|
259
|
-
const loginResult = await performLogin(page, options.authLogin, options.timeout);
|
|
260
|
-
if (!loginResult.success) {
|
|
261
|
-
result.errors.push(loginResult.error || 'Login failed');
|
|
262
|
-
result.pass = false;
|
|
263
|
-
return result;
|
|
264
|
-
}
|
|
265
|
-
result.warnings.push('Logged in as ' + options.authLogin.username);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// Collect console errors
|
|
269
|
-
const consoleErrors: string[] = [];
|
|
270
|
-
page.on('console', (msg: ConsoleMessage) => {
|
|
271
|
-
const type = msg.type();
|
|
272
|
-
const text = msg.text();
|
|
273
|
-
|
|
274
|
-
// Capture actual console.error calls
|
|
275
|
-
if (type === 'error') {
|
|
276
|
-
// Only ignore truly benign noise (favicons, devtools)
|
|
277
|
-
if (!text.includes('favicon') && !text.includes('DevTools')) {
|
|
278
|
-
consoleErrors.push(text);
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// Also capture logs/warnings that indicate errors (e.g., custom loggers)
|
|
283
|
-
if ((type === 'log' || type === 'warning') &&
|
|
284
|
-
(text.includes('_error') || text.includes('Error:') || text.includes('ERROR'))) {
|
|
285
|
-
// Ignore known non-critical patterns
|
|
286
|
-
if (!text.includes('ResizeObserver')) {
|
|
287
|
-
consoleErrors.push(`[${type}] ${text}`);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
// Collect network errors
|
|
293
|
-
const networkErrors: string[] = [];
|
|
294
|
-
page.on('requestfailed', (request) => {
|
|
295
|
-
const failure = request.failure();
|
|
296
|
-
if (failure) {
|
|
297
|
-
networkErrors.push(`${request.method()} ${request.url()}: ${failure.errorText}`);
|
|
298
|
-
}
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
// Track failed responses (4xx, 5xx)
|
|
302
|
-
page.on('response', (response) => {
|
|
303
|
-
const status = response.status();
|
|
304
|
-
if (status >= 400) {
|
|
305
|
-
const url = response.url();
|
|
306
|
-
// Ignore favicon and some common 404s
|
|
307
|
-
if (!url.includes('favicon') && !url.includes('hot-update')) {
|
|
308
|
-
networkErrors.push(`${response.request().method()} ${url}: ${status}`);
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
// Navigate to page
|
|
314
|
-
const startTime = Date.now();
|
|
315
|
-
|
|
316
|
-
try {
|
|
317
|
-
await page.goto(options.url, {
|
|
318
|
-
timeout: options.timeout,
|
|
319
|
-
waitUntil: 'networkidle',
|
|
320
|
-
});
|
|
321
|
-
} catch (err: any) {
|
|
322
|
-
if (err.message.includes('net::ERR_CONNECTION_REFUSED')) {
|
|
323
|
-
result.errors.push(`Cannot connect to ${options.url} - is the server running?`);
|
|
324
|
-
result.pass = false;
|
|
325
|
-
return result;
|
|
326
|
-
}
|
|
327
|
-
throw err;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
result.loadTime = Date.now() - startTime;
|
|
331
|
-
|
|
332
|
-
// Get page title
|
|
333
|
-
result.title = await page.title();
|
|
334
|
-
if (!result.title) {
|
|
335
|
-
result.warnings.push('Page has no title');
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// Check for error page indicators
|
|
339
|
-
const bodyText = await page.textContent('body') || '';
|
|
340
|
-
const errorPatterns = [
|
|
341
|
-
/something went wrong/i,
|
|
342
|
-
/error occurred/i,
|
|
343
|
-
/500 internal server/i,
|
|
344
|
-
/503 service unavailable/i,
|
|
345
|
-
/404 not found/i,
|
|
346
|
-
/oops!/i,
|
|
347
|
-
/application error/i,
|
|
348
|
-
/unhandled runtime error/i,
|
|
349
|
-
];
|
|
350
|
-
|
|
351
|
-
for (const pattern of errorPatterns) {
|
|
352
|
-
if (pattern.test(bodyText)) {
|
|
353
|
-
result.errors.push(`Page contains error text: "${bodyText.match(pattern)?.[0]}"`);
|
|
354
|
-
result.pass = false;
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// Check required selectors
|
|
359
|
-
for (const selector of options.selectors) {
|
|
360
|
-
try {
|
|
361
|
-
const element = await page.$(selector);
|
|
362
|
-
if (element) {
|
|
363
|
-
result.elementsFound.push(selector);
|
|
364
|
-
} else {
|
|
365
|
-
result.elementsMissing.push(selector);
|
|
366
|
-
result.errors.push(`Required element not found: ${selector}`);
|
|
367
|
-
result.pass = false;
|
|
368
|
-
}
|
|
369
|
-
} catch (err: any) {
|
|
370
|
-
result.elementsMissing.push(selector);
|
|
371
|
-
result.errors.push(`Error checking selector "${selector}": ${err.message}`);
|
|
372
|
-
result.pass = false;
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
// Check console errors
|
|
377
|
-
result.consoleErrors = consoleErrors;
|
|
378
|
-
if (options.checkConsole && consoleErrors.length > 0) {
|
|
379
|
-
result.errors.push(`${consoleErrors.length} console error(s) detected`);
|
|
380
|
-
result.pass = false;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// Check network errors
|
|
384
|
-
result.networkErrors = networkErrors;
|
|
385
|
-
if (options.checkNetwork && networkErrors.length > 0) {
|
|
386
|
-
// Filter out truly benign 404s (static resources)
|
|
387
|
-
const criticalErrors = networkErrors.filter(e =>
|
|
388
|
-
!e.includes('favicon') &&
|
|
389
|
-
!e.includes('.map') &&
|
|
390
|
-
!e.includes('hot-update') &&
|
|
391
|
-
!e.includes('.woff') &&
|
|
392
|
-
!e.includes('.ttf') &&
|
|
393
|
-
!e.includes('.png') &&
|
|
394
|
-
!e.includes('.jpg') &&
|
|
395
|
-
!e.includes('.svg') &&
|
|
396
|
-
!e.includes('.ico')
|
|
397
|
-
);
|
|
398
|
-
if (criticalErrors.length > 0) {
|
|
399
|
-
// Show actual URLs so Claude knows what to fix
|
|
400
|
-
result.errors.push(`Network errors (fix these):`);
|
|
401
|
-
criticalErrors.slice(0, 5).forEach(e => result.errors.push(` ${e}`));
|
|
402
|
-
result.pass = false;
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
// Take screenshot
|
|
407
|
-
if (options.screenshot) {
|
|
408
|
-
await page.screenshot({
|
|
409
|
-
path: options.screenshot,
|
|
410
|
-
fullPage: true,
|
|
411
|
-
});
|
|
412
|
-
result.screenshot = options.screenshot;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
} catch (err: any) {
|
|
416
|
-
result.errors.push(`Browser error: ${err.message}`);
|
|
417
|
-
result.pass = false;
|
|
418
|
-
} finally {
|
|
419
|
-
if (browser) {
|
|
420
|
-
await browser.close();
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
return result;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
async function main() {
|
|
428
|
-
const options = await parseArgs();
|
|
429
|
-
const result = await verify(options);
|
|
430
|
-
|
|
431
|
-
// Output JSON result
|
|
432
|
-
console.log(JSON.stringify(result, null, 2));
|
|
433
|
-
|
|
434
|
-
// Exit with appropriate code
|
|
435
|
-
process.exit(result.pass ? 0 : 1);
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
main().catch((err) => {
|
|
439
|
-
console.error(JSON.stringify({
|
|
440
|
-
pass: false,
|
|
441
|
-
errors: [`Fatal error: ${err.message}`],
|
|
442
|
-
warnings: [],
|
|
443
|
-
elementsFound: [],
|
|
444
|
-
elementsMissing: [],
|
|
445
|
-
consoleErrors: [],
|
|
446
|
-
networkErrors: [],
|
|
447
|
-
loadTime: 0,
|
|
448
|
-
}, null, 2));
|
|
449
|
-
process.exit(1);
|
|
450
|
-
});
|