@matware/e2e-runner 1.0.3 → 1.1.1
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/README.md +150 -8
- package/bin/cli.js +242 -2
- package/package.json +6 -3
- package/src/actions.js +28 -4
- package/src/ai-generate.js +216 -0
- package/src/config.js +44 -0
- package/src/dashboard.js +559 -0
- package/src/db.js +387 -0
- package/src/index.js +5 -1
- package/src/issues.js +152 -0
- package/src/mcp-server.js +8 -337
- package/src/mcp-tools.js +656 -0
- package/src/reporter.js +85 -2
- package/src/runner.js +119 -9
- package/src/verify.js +65 -0
- package/src/websocket.js +177 -0
- package/templates/dashboard.html +1281 -0
- package/templates/e2e.config.js +3 -0
package/README.md
CHANGED
|
@@ -37,18 +37,38 @@ JSON-driven E2E test runner. Define browser tests as simple JSON action arrays,
|
|
|
37
37
|
|
|
38
38
|
## Quick Start
|
|
39
39
|
|
|
40
|
+
**One-liner** (requires Node.js >= 20 and Docker):
|
|
41
|
+
|
|
40
42
|
```bash
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
curl -fsSL https://raw.githubusercontent.com/fastslack/mtw-e2e-runner/main/scripts/quickstart.sh | bash
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
This checks prerequisites, installs the package, scaffolds the project, starts the Chrome pool, and runs the sample tests.
|
|
43
47
|
|
|
44
|
-
|
|
48
|
+
**Step by step:**
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# 1. Install
|
|
52
|
+
npm install --save-dev @matware/e2e-runner
|
|
53
|
+
|
|
54
|
+
# 2. Scaffold project structure
|
|
45
55
|
npx e2e-runner init
|
|
46
56
|
|
|
47
|
-
# Start Chrome pool (requires Docker)
|
|
57
|
+
# 3. Start Chrome pool (requires Docker)
|
|
48
58
|
npx e2e-runner pool start
|
|
49
59
|
|
|
50
|
-
# Run all tests
|
|
60
|
+
# 4. Run all tests
|
|
51
61
|
npx e2e-runner run --all
|
|
62
|
+
|
|
63
|
+
# 5. Open the dashboard
|
|
64
|
+
npx e2e-runner dashboard
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Add to Claude Code** (once, available in all projects):
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
claude mcp add --transport stdio --scope user e2e-runner \
|
|
71
|
+
-- npx -y -p @matware/e2e-runner e2e-runner-mcp
|
|
52
72
|
```
|
|
53
73
|
|
|
54
74
|
The `init` command creates:
|
|
@@ -128,6 +148,14 @@ npx e2e-runner pool start # Start Chrome container
|
|
|
128
148
|
npx e2e-runner pool stop # Stop Chrome container
|
|
129
149
|
npx e2e-runner pool status # Check pool health
|
|
130
150
|
|
|
151
|
+
# Issue-to-test
|
|
152
|
+
npx e2e-runner issue <url> # Fetch issue details
|
|
153
|
+
npx e2e-runner issue <url> --generate # Generate test file via AI
|
|
154
|
+
npx e2e-runner issue <url> --verify # Generate + run + report
|
|
155
|
+
|
|
156
|
+
# Dashboard
|
|
157
|
+
npx e2e-runner dashboard # Start web dashboard
|
|
158
|
+
|
|
131
159
|
# Other
|
|
132
160
|
npx e2e-runner list # List available suites
|
|
133
161
|
npx e2e-runner init # Scaffold project
|
|
@@ -150,6 +178,7 @@ npx e2e-runner init # Scaffold project
|
|
|
150
178
|
| `--env <name>` | `default` | Environment profile |
|
|
151
179
|
| `--pool-port <port>` | `3333` | Chrome pool port |
|
|
152
180
|
| `--max-sessions <n>` | `10` | Max concurrent Chrome sessions |
|
|
181
|
+
| `--project-name <name>` | dir name | Project display name for dashboard |
|
|
153
182
|
|
|
154
183
|
## Configuration
|
|
155
184
|
|
|
@@ -201,6 +230,9 @@ When `--env <name>` is set, the matching profile from `environments` overrides e
|
|
|
201
230
|
| `TEST_TIMEOUT` | `testTimeout` |
|
|
202
231
|
| `OUTPUT_FORMAT` | `outputFormat` |
|
|
203
232
|
| `E2E_ENV` | `env` |
|
|
233
|
+
| `PROJECT_NAME` | `projectName` |
|
|
234
|
+
| `ANTHROPIC_API_KEY` | `anthropicApiKey` |
|
|
235
|
+
| `ANTHROPIC_MODEL` | `anthropicModel` |
|
|
204
236
|
|
|
205
237
|
## Hooks
|
|
206
238
|
|
|
@@ -342,12 +374,14 @@ claude mcp add --transport stdio --scope user e2e-runner \
|
|
|
342
374
|
| `e2e_list` | List available test suites with test names and counts |
|
|
343
375
|
| `e2e_create_test` | Create a new test JSON file |
|
|
344
376
|
| `e2e_pool_status` | Check Chrome pool availability and capacity |
|
|
345
|
-
| `
|
|
346
|
-
| `
|
|
377
|
+
| `e2e_screenshot` | Retrieve a screenshot by its hash (e.g. `ss:a3f2b1c9`) |
|
|
378
|
+
| `e2e_issue` | Fetch a GitHub/GitLab issue and generate E2E tests |
|
|
379
|
+
|
|
380
|
+
> **Note:** Pool start/stop are only available via CLI (`e2e-runner pool start|stop`), not via MCP — restarting the pool kills all active sessions from other clients.
|
|
347
381
|
|
|
348
382
|
All tools accept an optional `cwd` parameter (absolute path to the project root). Claude Code passes its current working directory so the MCP server resolves `e2e/tests/`, `e2e.config.js`, and `.e2e-pool/` relative to the correct project — even when switching between multiple projects in the same session.
|
|
349
383
|
|
|
350
|
-
Once installed, Claude Code can run tests, analyze failures, create new test files
|
|
384
|
+
Once installed, Claude Code can run tests, analyze failures, and create new test files as part of its normal workflow. Just ask:
|
|
351
385
|
|
|
352
386
|
> "Run all E2E tests"
|
|
353
387
|
> "Create a test that verifies the checkout flow"
|
|
@@ -360,6 +394,108 @@ claude mcp list
|
|
|
360
394
|
# e2e-runner: ... - Connected
|
|
361
395
|
```
|
|
362
396
|
|
|
397
|
+
## Issue-to-Test
|
|
398
|
+
|
|
399
|
+
Turn GitHub and GitLab issues into executable E2E tests. Paste an issue URL and get runnable tests -- automatically.
|
|
400
|
+
|
|
401
|
+
### How It Works
|
|
402
|
+
|
|
403
|
+
1. **Fetch** -- Pulls issue details (title, body, labels) via `gh` or `glab` CLI
|
|
404
|
+
2. **Generate** -- AI creates JSON test actions based on the issue description
|
|
405
|
+
3. **Run** -- Optionally executes the tests immediately to verify if a bug is reproducible
|
|
406
|
+
|
|
407
|
+
### Two Modes
|
|
408
|
+
|
|
409
|
+
**Prompt mode** (default, no API key): Returns issue data + a structured prompt. Claude Code uses its own intelligence to create tests via `e2e_create_test` and run them.
|
|
410
|
+
|
|
411
|
+
**Verify mode** (requires `ANTHROPIC_API_KEY`): Calls Claude API directly, generates tests, runs them, and reports whether the bug is confirmed or not reproducible.
|
|
412
|
+
|
|
413
|
+
### CLI
|
|
414
|
+
|
|
415
|
+
```bash
|
|
416
|
+
# Fetch and display issue details
|
|
417
|
+
e2e-runner issue https://github.com/owner/repo/issues/42
|
|
418
|
+
|
|
419
|
+
# Generate a test file via Claude API
|
|
420
|
+
e2e-runner issue https://github.com/owner/repo/issues/42 --generate
|
|
421
|
+
# -> Creates e2e/tests/issue-42.json
|
|
422
|
+
|
|
423
|
+
# Generate + run + report bug status
|
|
424
|
+
e2e-runner issue https://github.com/owner/repo/issues/42 --verify
|
|
425
|
+
# -> "BUG CONFIRMED" or "NOT REPRODUCIBLE"
|
|
426
|
+
|
|
427
|
+
# Output AI prompt as JSON (for piping)
|
|
428
|
+
e2e-runner issue https://github.com/owner/repo/issues/42 --prompt
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
### MCP
|
|
432
|
+
|
|
433
|
+
In Claude Code, the `e2e_issue` tool handles everything:
|
|
434
|
+
|
|
435
|
+
> "Fetch issue https://github.com/owner/repo/issues/42 and create E2E tests for it"
|
|
436
|
+
|
|
437
|
+
Claude Code receives the issue data, generates appropriate test actions, saves them via `e2e_create_test`, and runs them with `e2e_run`.
|
|
438
|
+
|
|
439
|
+
### Auth Requirements
|
|
440
|
+
|
|
441
|
+
- **GitHub**: `gh` CLI authenticated (`gh auth login`)
|
|
442
|
+
- **GitLab**: `glab` CLI authenticated (`glab auth login`)
|
|
443
|
+
|
|
444
|
+
Provider is auto-detected from the URL. Self-hosted GitLab is supported via `glab` config.
|
|
445
|
+
|
|
446
|
+
### Bug Verification Logic
|
|
447
|
+
|
|
448
|
+
Generated tests assert the **correct** behavior. If the tests fail, the correct behavior doesn't work -- bug confirmed. If all tests pass, the bug is not reproducible.
|
|
449
|
+
|
|
450
|
+
## Web Dashboard
|
|
451
|
+
|
|
452
|
+
Real-time UI for running tests, viewing results, screenshots, and run history.
|
|
453
|
+
|
|
454
|
+
```bash
|
|
455
|
+
e2e-runner dashboard # Start on default port 8484
|
|
456
|
+
e2e-runner dashboard --port 9090 # Custom port
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
### Live Execution
|
|
460
|
+
|
|
461
|
+
Monitor tests in real-time as they run. Each test shows its steps with individual durations, pass/fail status, and active connection count.
|
|
462
|
+
|
|
463
|
+
<p align="center">
|
|
464
|
+
<img src="https://raw.githubusercontent.com/fastslack/mtw-e2e-runner/main/docs/screenshots/blog-dashboard-live-running.png" alt="Dashboard - Live test execution" width="900" />
|
|
465
|
+
</p>
|
|
466
|
+
|
|
467
|
+
### Test Suites
|
|
468
|
+
|
|
469
|
+
Browse all test suites across multiple projects. Run a single suite or all tests with one click.
|
|
470
|
+
|
|
471
|
+
<p align="center">
|
|
472
|
+
<img src="https://raw.githubusercontent.com/fastslack/mtw-e2e-runner/main/docs/screenshots/blog-dashboard-suites.png" alt="Dashboard - Test suites grid" width="900" />
|
|
473
|
+
</p>
|
|
474
|
+
|
|
475
|
+
### Run History
|
|
476
|
+
|
|
477
|
+
Track pass rate trends over time with the bar chart. Click any row to expand the full run detail with per-test results, screenshots, and console errors.
|
|
478
|
+
|
|
479
|
+
<p align="center">
|
|
480
|
+
<img src="https://raw.githubusercontent.com/fastslack/mtw-e2e-runner/main/docs/screenshots/blog-dashboard-runs.png" alt="Dashboard - Run history with trend chart" width="900" />
|
|
481
|
+
</p>
|
|
482
|
+
|
|
483
|
+
### Run Detail
|
|
484
|
+
|
|
485
|
+
Expanded view shows each test with PASS/FAIL badge, screenshot thumbnails with copyable hashes (`ss:77c28b5a`), and formatted console errors.
|
|
486
|
+
|
|
487
|
+
<p align="center">
|
|
488
|
+
<img src="https://raw.githubusercontent.com/fastslack/mtw-e2e-runner/main/docs/screenshots/blog-dashboard-run-detail.png" alt="Dashboard - Run detail with screenshot hashes" width="900" />
|
|
489
|
+
</p>
|
|
490
|
+
|
|
491
|
+
### Screenshot Gallery
|
|
492
|
+
|
|
493
|
+
Browse all captured screenshots per project. Includes both manual captures and error screenshots.
|
|
494
|
+
|
|
495
|
+
<p align="center">
|
|
496
|
+
<img src="https://raw.githubusercontent.com/fastslack/mtw-e2e-runner/main/docs/screenshots/blog-dashboard-screenshots-gallery.png" alt="Dashboard - Screenshot gallery" width="900" />
|
|
497
|
+
</p>
|
|
498
|
+
|
|
363
499
|
## Architecture
|
|
364
500
|
|
|
365
501
|
```
|
|
@@ -371,6 +507,12 @@ src/runner.js Parallel test executor with retries and timeouts
|
|
|
371
507
|
src/actions.js Action engine: maps JSON actions to Puppeteer calls
|
|
372
508
|
src/reporter.js JSON reports, JUnit XML, console output
|
|
373
509
|
src/mcp-server.js MCP server: exposes tools for Claude Code
|
|
510
|
+
src/mcp-tools.js Shared MCP tool definitions and handlers
|
|
511
|
+
src/dashboard.js Web dashboard: HTTP server, REST API, WebSocket
|
|
512
|
+
src/db.js SQLite multi-project database
|
|
513
|
+
src/issues.js GitHub/GitLab issue fetching (gh/glab CLI)
|
|
514
|
+
src/ai-generate.js AI test generation (prompt builder + Claude API)
|
|
515
|
+
src/verify.js Bug verification orchestrator
|
|
374
516
|
src/logger.js ANSI colored logger
|
|
375
517
|
src/index.js Programmatic API (createRunner)
|
|
376
518
|
templates/ Scaffolding templates for init command
|
package/bin/cli.js
CHANGED
|
@@ -13,6 +13,12 @@
|
|
|
13
13
|
* e2e-runner pool stop Stop the Chrome Pool
|
|
14
14
|
* e2e-runner pool status Show pool status
|
|
15
15
|
* e2e-runner pool restart Restart the pool
|
|
16
|
+
* e2e-runner dashboard Start the web dashboard
|
|
17
|
+
* e2e-runner capture <url> Capture a screenshot of any URL
|
|
18
|
+
* e2e-runner issue <url> Fetch issue and show details
|
|
19
|
+
* e2e-runner issue <url> --generate Generate test file via Claude API
|
|
20
|
+
* e2e-runner issue <url> --verify Generate + run + report bug status
|
|
21
|
+
* e2e-runner issue <url> --prompt Output the AI prompt (for piping)
|
|
16
22
|
* e2e-runner init Scaffold e2e/ in the current project
|
|
17
23
|
* e2e-runner --help Show help
|
|
18
24
|
* e2e-runner --version Show version
|
|
@@ -20,11 +26,17 @@
|
|
|
20
26
|
|
|
21
27
|
import fs from 'fs';
|
|
22
28
|
import path from 'path';
|
|
29
|
+
import http from 'http';
|
|
23
30
|
import { fileURLToPath } from 'url';
|
|
24
31
|
import { loadConfig } from '../src/config.js';
|
|
25
|
-
import { startPool, stopPool, restartPool, getPoolStatus, waitForPool } from '../src/pool.js';
|
|
32
|
+
import { startPool, stopPool, restartPool, getPoolStatus, waitForPool, connectToPool } from '../src/pool.js';
|
|
26
33
|
import { runTestsParallel, loadTestFile, loadTestSuite, loadAllSuites, listSuites } from '../src/runner.js';
|
|
27
|
-
import { generateReport, saveReport, printReport } from '../src/reporter.js';
|
|
34
|
+
import { generateReport, saveReport, printReport, persistRun } from '../src/reporter.js';
|
|
35
|
+
import { startDashboard } from '../src/dashboard.js';
|
|
36
|
+
import { fetchIssue } from '../src/issues.js';
|
|
37
|
+
import { buildPrompt, generateTests, hasApiKey } from '../src/ai-generate.js';
|
|
38
|
+
import { verifyIssue } from '../src/verify.js';
|
|
39
|
+
import { ensureProject, computeScreenshotHash, registerScreenshotHash } from '../src/db.js';
|
|
28
40
|
import { log, colors as C } from '../src/logger.js';
|
|
29
41
|
|
|
30
42
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -59,6 +71,12 @@ function parseCLIConfig() {
|
|
|
59
71
|
if (getFlag('--test-timeout')) cliArgs.testTimeout = parseInt(getFlag('--test-timeout'));
|
|
60
72
|
if (getFlag('--output')) cliArgs.outputFormat = getFlag('--output');
|
|
61
73
|
if (getFlag('--env')) cliArgs.env = getFlag('--env');
|
|
74
|
+
if (getFlag('--port')) cliArgs.dashboardPort = parseInt(getFlag('--port'));
|
|
75
|
+
if (getFlag('--dashboard-port')) cliArgs.dashboardPort = parseInt(getFlag('--dashboard-port'));
|
|
76
|
+
if (getFlag('--project-name')) cliArgs.projectName = getFlag('--project-name');
|
|
77
|
+
if (hasFlag('--fail-on-network-error')) cliArgs.failOnNetworkError = true;
|
|
78
|
+
if (getFlag('--auth-token')) cliArgs.authToken = getFlag('--auth-token');
|
|
79
|
+
if (getFlag('--auth-storage-key')) cliArgs.authStorageKey = getFlag('--auth-storage-key');
|
|
62
80
|
return cliArgs;
|
|
63
81
|
}
|
|
64
82
|
|
|
@@ -75,6 +93,20 @@ ${C.bold}Usage:${C.reset}
|
|
|
75
93
|
|
|
76
94
|
e2e-runner list List available suites
|
|
77
95
|
|
|
96
|
+
e2e-runner dashboard Start the web dashboard
|
|
97
|
+
e2e-runner dashboard --port <port> Custom port (default: 8484)
|
|
98
|
+
|
|
99
|
+
e2e-runner capture <url> Capture a screenshot of any URL
|
|
100
|
+
e2e-runner capture <url> --full-page Capture full scrollable page
|
|
101
|
+
e2e-runner capture <url> --selector <sel> Wait for selector before capture
|
|
102
|
+
e2e-runner capture <url> --delay <ms> Wait before capturing
|
|
103
|
+
e2e-runner capture <url> --filename <name> Custom filename
|
|
104
|
+
|
|
105
|
+
e2e-runner issue <url> Fetch issue and show details
|
|
106
|
+
e2e-runner issue <url> --generate Generate test file via Claude API
|
|
107
|
+
e2e-runner issue <url> --verify Generate + run + report bug status
|
|
108
|
+
e2e-runner issue <url> --prompt Output the AI prompt (for piping)
|
|
109
|
+
|
|
78
110
|
e2e-runner pool start Start the Chrome Pool
|
|
79
111
|
e2e-runner pool stop Stop the Chrome Pool
|
|
80
112
|
e2e-runner pool status Show pool status
|
|
@@ -96,6 +128,8 @@ ${C.bold}Options:${C.reset}
|
|
|
96
128
|
--test-timeout <ms> Per-test timeout (default: 60000)
|
|
97
129
|
--output <format> Report format: json, junit, both (default: json)
|
|
98
130
|
--env <name> Environment profile from config (default: default)
|
|
131
|
+
--project-name <name> Project display name for dashboard (default: directory name)
|
|
132
|
+
--fail-on-network-error Fail tests when network requests fail (e.g. ERR_CONNECTION_REFUSED)
|
|
99
133
|
|
|
100
134
|
${C.bold}Config:${C.reset}
|
|
101
135
|
Looks for e2e.config.js or e2e.config.json in the current directory.
|
|
@@ -106,6 +140,7 @@ ${C.bold}Config:${C.reset}
|
|
|
106
140
|
async function cmdRun() {
|
|
107
141
|
const cliArgs = parseCLIConfig();
|
|
108
142
|
const config = await loadConfig(cliArgs);
|
|
143
|
+
config.triggeredBy = 'cli';
|
|
109
144
|
let tests = [];
|
|
110
145
|
let hooks = {};
|
|
111
146
|
|
|
@@ -151,13 +186,35 @@ async function cmdRun() {
|
|
|
151
186
|
const pressure = await waitForPool(config.poolUrl);
|
|
152
187
|
log('✅', `Pool ready (${pressure.running}/${pressure.maxConcurrent} sessions, queued: ${pressure.queued})`);
|
|
153
188
|
|
|
189
|
+
// Wire up live progress to dashboard if running
|
|
190
|
+
let _lastBroadcast = null;
|
|
191
|
+
try {
|
|
192
|
+
const res = await fetch('http://127.0.0.1:' + (config.dashboardPort || 8484) + '/api/status');
|
|
193
|
+
if (res.ok) {
|
|
194
|
+
const dp = config.dashboardPort || 8484;
|
|
195
|
+
config.onProgress = (data) => {
|
|
196
|
+
const body = JSON.stringify(data);
|
|
197
|
+
_lastBroadcast = new Promise((resolve) => {
|
|
198
|
+
const req = http.request({ hostname: '127.0.0.1', port: dp, path: '/api/broadcast', method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) }, timeout: 1000 });
|
|
199
|
+
req.on('error', () => resolve());
|
|
200
|
+
req.on('close', () => resolve());
|
|
201
|
+
req.end(body);
|
|
202
|
+
});
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
} catch { /* dashboard not running */ }
|
|
206
|
+
|
|
154
207
|
// Execute tests
|
|
155
208
|
console.log('');
|
|
209
|
+
const suiteName = getFlag('--suite') || (hasFlag('--all') ? null : null);
|
|
156
210
|
const results = await runTestsParallel(tests, config, hooks);
|
|
157
211
|
const report = generateReport(results);
|
|
158
212
|
saveReport(report, config.screenshotsDir, config);
|
|
213
|
+
persistRun(report, config, suiteName);
|
|
159
214
|
printReport(report, config.screenshotsDir);
|
|
160
215
|
|
|
216
|
+
// Wait for the last dashboard broadcast (run:complete) to flush before exiting
|
|
217
|
+
if (_lastBroadcast) await _lastBroadcast;
|
|
161
218
|
process.exit(report.summary.failed > 0 ? 1 : 0);
|
|
162
219
|
}
|
|
163
220
|
|
|
@@ -288,6 +345,177 @@ ${C.bold}Next steps:${C.reset}
|
|
|
288
345
|
`);
|
|
289
346
|
}
|
|
290
347
|
|
|
348
|
+
async function cmdDashboard() {
|
|
349
|
+
const cliArgs = parseCLIConfig();
|
|
350
|
+
const config = await loadConfig(cliArgs);
|
|
351
|
+
|
|
352
|
+
console.log(`\n${C.bold}${C.cyan}@matware/e2e-runner${C.reset} v${pkg.version}`);
|
|
353
|
+
console.log(`${C.dim}Starting dashboard on port ${config.dashboardPort}...${C.reset}\n`);
|
|
354
|
+
|
|
355
|
+
const handle = await startDashboard(config);
|
|
356
|
+
|
|
357
|
+
// Keep process alive until SIGINT/SIGTERM
|
|
358
|
+
const shutdown = () => {
|
|
359
|
+
console.log(`\n${C.dim}Shutting down dashboard...${C.reset}`);
|
|
360
|
+
handle.close();
|
|
361
|
+
process.exit(0);
|
|
362
|
+
};
|
|
363
|
+
process.on('SIGINT', shutdown);
|
|
364
|
+
process.on('SIGTERM', shutdown);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async function cmdCapture() {
|
|
368
|
+
const url = args[1];
|
|
369
|
+
if (!url || url.startsWith('--')) {
|
|
370
|
+
console.error(`${C.red}Usage: e2e-runner capture <url> [--filename <name>] [--full-page] [--selector <sel>] [--delay <ms>]${C.reset}`);
|
|
371
|
+
process.exit(1);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const cliArgs = parseCLIConfig();
|
|
375
|
+
const config = await loadConfig(cliArgs);
|
|
376
|
+
|
|
377
|
+
console.log(`\n${C.bold}${C.cyan}@matware/e2e-runner${C.reset} v${pkg.version}`);
|
|
378
|
+
|
|
379
|
+
log('🔌', 'Checking Chrome Pool...');
|
|
380
|
+
await waitForPool(config.poolUrl);
|
|
381
|
+
|
|
382
|
+
let browser;
|
|
383
|
+
try {
|
|
384
|
+
browser = await connectToPool(config.poolUrl);
|
|
385
|
+
const page = await browser.newPage();
|
|
386
|
+
await page.setViewport(config.viewport);
|
|
387
|
+
|
|
388
|
+
log('📸', `Navigating to ${C.cyan}${url}${C.reset}`);
|
|
389
|
+
await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 });
|
|
390
|
+
|
|
391
|
+
const selector = getFlag('--selector');
|
|
392
|
+
if (selector) {
|
|
393
|
+
log('⏳', `Waiting for selector: ${C.dim}${selector}${C.reset}`);
|
|
394
|
+
await page.waitForSelector(selector, { timeout: 10000 });
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const delay = getFlag('--delay');
|
|
398
|
+
if (delay) {
|
|
399
|
+
await new Promise(r => setTimeout(r, parseInt(delay)));
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Build filename
|
|
403
|
+
let filename = getFlag('--filename') || `capture-${Date.now()}.png`;
|
|
404
|
+
filename = path.basename(filename);
|
|
405
|
+
if (!filename.endsWith('.png')) filename += '.png';
|
|
406
|
+
|
|
407
|
+
if (!fs.existsSync(config.screenshotsDir)) {
|
|
408
|
+
fs.mkdirSync(config.screenshotsDir, { recursive: true });
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const screenshotPath = path.join(config.screenshotsDir, filename);
|
|
412
|
+
const fullPage = hasFlag('--full-page');
|
|
413
|
+
await page.screenshot({ path: screenshotPath, fullPage });
|
|
414
|
+
|
|
415
|
+
// Register hash in SQLite
|
|
416
|
+
const cwd = process.cwd();
|
|
417
|
+
const projectName = config.projectName || path.basename(cwd);
|
|
418
|
+
const projectId = ensureProject(cwd, projectName, config.screenshotsDir, config.testsDir);
|
|
419
|
+
const hash = computeScreenshotHash(screenshotPath);
|
|
420
|
+
registerScreenshotHash(hash, screenshotPath, projectId, null);
|
|
421
|
+
|
|
422
|
+
log('✅', `Saved: ${C.cyan}${screenshotPath}${C.reset}`);
|
|
423
|
+
log('🏷️', `Hash: ${C.bold}ss:${hash}${C.reset}`);
|
|
424
|
+
console.log('');
|
|
425
|
+
} finally {
|
|
426
|
+
if (browser) browser.disconnect();
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async function cmdIssue() {
|
|
431
|
+
const url = args[1];
|
|
432
|
+
if (!url || url.startsWith('--')) {
|
|
433
|
+
console.error(`${C.red}Usage: e2e-runner issue <url> [--generate|--verify|--prompt]${C.reset}`);
|
|
434
|
+
process.exit(1);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const cliArgs = parseCLIConfig();
|
|
438
|
+
const config = await loadConfig(cliArgs);
|
|
439
|
+
|
|
440
|
+
if (hasFlag('--prompt')) {
|
|
441
|
+
// Output AI prompt as JSON to stdout
|
|
442
|
+
const issue = fetchIssue(url);
|
|
443
|
+
const promptData = buildPrompt(issue, config);
|
|
444
|
+
console.log(JSON.stringify(promptData, null, 2));
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (hasFlag('--verify')) {
|
|
449
|
+
// Generate + run + report
|
|
450
|
+
if (!hasApiKey(config)) {
|
|
451
|
+
console.error(`${C.red}ANTHROPIC_API_KEY is required for --verify mode.${C.reset}`);
|
|
452
|
+
process.exit(1);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
console.log(`\n${C.bold}${C.cyan}@matware/e2e-runner${C.reset} v${pkg.version}`);
|
|
456
|
+
log('🔍', 'Fetching issue...');
|
|
457
|
+
|
|
458
|
+
const result = await verifyIssue(url, config);
|
|
459
|
+
const { issue, report, bugConfirmed } = result;
|
|
460
|
+
|
|
461
|
+
console.log('');
|
|
462
|
+
if (bugConfirmed) {
|
|
463
|
+
log('🐛', `${C.red}${C.bold}BUG CONFIRMED${C.reset} — ${issue.title}`);
|
|
464
|
+
log('', `${C.dim}${report.summary.failed} of ${report.summary.total} tests failed${C.reset}`);
|
|
465
|
+
} else {
|
|
466
|
+
log('✅', `${C.green}${C.bold}NOT REPRODUCIBLE${C.reset} — ${issue.title}`);
|
|
467
|
+
log('', `${C.dim}All ${report.summary.total} tests passed${C.reset}`);
|
|
468
|
+
}
|
|
469
|
+
console.log(`${C.dim}Issue: ${issue.url}${C.reset}\n`);
|
|
470
|
+
|
|
471
|
+
process.exit(bugConfirmed ? 1 : 0);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (hasFlag('--generate')) {
|
|
475
|
+
// Generate test file via Claude API
|
|
476
|
+
if (!hasApiKey(config)) {
|
|
477
|
+
console.error(`${C.red}ANTHROPIC_API_KEY is required for --generate mode.${C.reset}`);
|
|
478
|
+
process.exit(1);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
console.log(`\n${C.bold}${C.cyan}@matware/e2e-runner${C.reset} v${pkg.version}`);
|
|
482
|
+
log('🔍', 'Fetching issue...');
|
|
483
|
+
|
|
484
|
+
const issue = fetchIssue(url);
|
|
485
|
+
log('📋', `${C.cyan}${issue.title}${C.reset}`);
|
|
486
|
+
log('🤖', 'Generating tests via Claude API...');
|
|
487
|
+
|
|
488
|
+
const { tests, suiteName } = await generateTests(issue, config);
|
|
489
|
+
|
|
490
|
+
if (!fs.existsSync(config.testsDir)) {
|
|
491
|
+
fs.mkdirSync(config.testsDir, { recursive: true });
|
|
492
|
+
}
|
|
493
|
+
const filePath = path.join(config.testsDir, `${suiteName}.json`);
|
|
494
|
+
fs.writeFileSync(filePath, JSON.stringify(tests, null, 2) + '\n');
|
|
495
|
+
|
|
496
|
+
log('✅', `Created ${C.cyan}${filePath}${C.reset} (${tests.length} tests)`);
|
|
497
|
+
console.log(`${C.dim}Run with: e2e-runner run --suite ${suiteName}${C.reset}\n`);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Default: fetch and display issue
|
|
502
|
+
log('🔍', 'Fetching issue...');
|
|
503
|
+
const issue = fetchIssue(url);
|
|
504
|
+
|
|
505
|
+
console.log(`\n${C.bold}${issue.title}${C.reset}`);
|
|
506
|
+
console.log(`${C.dim}${'─'.repeat(50)}${C.reset}`);
|
|
507
|
+
console.log(` Repo: ${C.cyan}${issue.repo}${C.reset}`);
|
|
508
|
+
console.log(` Number: #${issue.number}`);
|
|
509
|
+
console.log(` State: ${issue.state === 'open' ? C.green : C.red}${issue.state}${C.reset}`);
|
|
510
|
+
console.log(` Labels: ${issue.labels.length ? issue.labels.join(', ') : C.dim + 'none' + C.reset}`);
|
|
511
|
+
console.log(` URL: ${C.dim}${issue.url}${C.reset}`);
|
|
512
|
+
if (issue.body) {
|
|
513
|
+
console.log(`\n${C.bold}Description:${C.reset}`);
|
|
514
|
+
console.log(issue.body.length > 500 ? issue.body.substring(0, 500) + '...' : issue.body);
|
|
515
|
+
}
|
|
516
|
+
console.log('');
|
|
517
|
+
}
|
|
518
|
+
|
|
291
519
|
// ==================== Main ====================
|
|
292
520
|
|
|
293
521
|
async function main() {
|
|
@@ -316,6 +544,18 @@ async function main() {
|
|
|
316
544
|
await cmdPool();
|
|
317
545
|
break;
|
|
318
546
|
|
|
547
|
+
case 'dashboard':
|
|
548
|
+
await cmdDashboard();
|
|
549
|
+
break;
|
|
550
|
+
|
|
551
|
+
case 'capture':
|
|
552
|
+
await cmdCapture();
|
|
553
|
+
break;
|
|
554
|
+
|
|
555
|
+
case 'issue':
|
|
556
|
+
await cmdIssue();
|
|
557
|
+
break;
|
|
558
|
+
|
|
319
559
|
case 'init':
|
|
320
560
|
cmdInit();
|
|
321
561
|
break;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@matware/e2e-runner",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"mcpName": "io.github.fastslack/e2e-runner",
|
|
5
5
|
"description": "E2E test runner using Chrome Pool (browserless/chrome) with parallel execution",
|
|
6
6
|
"type": "module",
|
|
@@ -25,17 +25,20 @@
|
|
|
25
25
|
"browserless",
|
|
26
26
|
"parallel",
|
|
27
27
|
"mcp",
|
|
28
|
-
"claude-code"
|
|
28
|
+
"claude-code",
|
|
29
|
+
"github-issues",
|
|
30
|
+
"ai-testing"
|
|
29
31
|
],
|
|
30
32
|
"author": "Matware",
|
|
31
33
|
"license": "Apache-2.0",
|
|
32
34
|
"repository": {
|
|
33
35
|
"type": "git",
|
|
34
|
-
"url": "https://github.com/fastslack/mtw-e2e-runner.git"
|
|
36
|
+
"url": "git+https://github.com/fastslack/mtw-e2e-runner.git"
|
|
35
37
|
},
|
|
36
38
|
"homepage": "https://github.com/fastslack/mtw-e2e-runner#readme",
|
|
37
39
|
"dependencies": {
|
|
38
40
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
41
|
+
"better-sqlite3": "^11.0.0",
|
|
39
42
|
"puppeteer-core": "^24.0.0"
|
|
40
43
|
},
|
|
41
44
|
"engines": {
|
package/src/actions.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* The JS comes from team-authored JSON test files.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import path from 'path';
|
|
10
11
|
import { log } from './logger.js';
|
|
11
12
|
|
|
12
13
|
function sleep(ms) {
|
|
@@ -70,7 +71,9 @@ export async function executeAction(page, action, config) {
|
|
|
70
71
|
if (!/\.(png|jpg|jpeg|webp)$/i.test(filename)) {
|
|
71
72
|
filename += '.png';
|
|
72
73
|
}
|
|
73
|
-
|
|
74
|
+
// Sanitize: use only the basename to prevent path traversal
|
|
75
|
+
filename = path.basename(filename);
|
|
76
|
+
const filepath = path.join(screenshotsDir, filename);
|
|
74
77
|
await page.screenshot({ path: filepath, fullPage: action.fullPage || false });
|
|
75
78
|
return { screenshot: filepath };
|
|
76
79
|
}
|
|
@@ -146,10 +149,31 @@ export async function executeAction(page, action, config) {
|
|
|
146
149
|
await page.hover(selector);
|
|
147
150
|
break;
|
|
148
151
|
|
|
149
|
-
case '
|
|
150
|
-
|
|
151
|
-
|
|
152
|
+
case 'navigate': {
|
|
153
|
+
const navUrl = value.startsWith('http') ? value : `${baseUrl}${value}`;
|
|
154
|
+
// Navigate with a race: try page.goto but don't block more than 5s
|
|
155
|
+
// This handles SPAs where domcontentloaded may not fire on client-side routing
|
|
156
|
+
try {
|
|
157
|
+
await Promise.race([
|
|
158
|
+
page.goto(navUrl, { waitUntil: 'load', timeout: 30000 }),
|
|
159
|
+
sleep(5000),
|
|
160
|
+
]);
|
|
161
|
+
} catch { /* navigation may still be loading */ }
|
|
152
162
|
break;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
case 'evaluate': {
|
|
166
|
+
// Intentional: runs JS in browser page context (from test JSON files)
|
|
167
|
+
const evalResult = await page.evaluate(value);
|
|
168
|
+
// Check return value for failure signals
|
|
169
|
+
if (typeof evalResult === 'string' && /^(FAIL|ERROR|FAILED)[\s:]/i.test(evalResult)) {
|
|
170
|
+
throw new Error(`evaluate failed: ${evalResult}`);
|
|
171
|
+
}
|
|
172
|
+
if (evalResult === false) {
|
|
173
|
+
throw new Error('evaluate returned false');
|
|
174
|
+
}
|
|
175
|
+
return evalResult !== undefined && evalResult !== null ? { value: evalResult } : null;
|
|
176
|
+
}
|
|
153
177
|
|
|
154
178
|
default:
|
|
155
179
|
log('⚠️', `Unknown action: ${type}`);
|