@matware/e2e-runner 1.0.3 → 1.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/README.md +87 -3
- package/bin/cli.js +161 -1
- package/package.json +5 -2
- package/src/actions.js +17 -1
- package/src/ai-generate.js +185 -0
- package/src/config.js +14 -0
- package/src/dashboard.js +546 -0
- package/src/db.js +366 -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 +527 -0
- package/src/reporter.js +73 -2
- package/src/runner.js +52 -8
- package/src/verify.js +53 -0
- package/src/websocket.js +177 -0
- package/templates/dashboard.html +1044 -0
- package/templates/e2e.config.js +3 -0
package/README.md
CHANGED
|
@@ -128,6 +128,14 @@ npx e2e-runner pool start # Start Chrome container
|
|
|
128
128
|
npx e2e-runner pool stop # Stop Chrome container
|
|
129
129
|
npx e2e-runner pool status # Check pool health
|
|
130
130
|
|
|
131
|
+
# Issue-to-test
|
|
132
|
+
npx e2e-runner issue <url> # Fetch issue details
|
|
133
|
+
npx e2e-runner issue <url> --generate # Generate test file via AI
|
|
134
|
+
npx e2e-runner issue <url> --verify # Generate + run + report
|
|
135
|
+
|
|
136
|
+
# Dashboard
|
|
137
|
+
npx e2e-runner dashboard # Start web dashboard
|
|
138
|
+
|
|
131
139
|
# Other
|
|
132
140
|
npx e2e-runner list # List available suites
|
|
133
141
|
npx e2e-runner init # Scaffold project
|
|
@@ -150,6 +158,7 @@ npx e2e-runner init # Scaffold project
|
|
|
150
158
|
| `--env <name>` | `default` | Environment profile |
|
|
151
159
|
| `--pool-port <port>` | `3333` | Chrome pool port |
|
|
152
160
|
| `--max-sessions <n>` | `10` | Max concurrent Chrome sessions |
|
|
161
|
+
| `--project-name <name>` | dir name | Project display name for dashboard |
|
|
153
162
|
|
|
154
163
|
## Configuration
|
|
155
164
|
|
|
@@ -201,6 +210,9 @@ When `--env <name>` is set, the matching profile from `environments` overrides e
|
|
|
201
210
|
| `TEST_TIMEOUT` | `testTimeout` |
|
|
202
211
|
| `OUTPUT_FORMAT` | `outputFormat` |
|
|
203
212
|
| `E2E_ENV` | `env` |
|
|
213
|
+
| `PROJECT_NAME` | `projectName` |
|
|
214
|
+
| `ANTHROPIC_API_KEY` | `anthropicApiKey` |
|
|
215
|
+
| `ANTHROPIC_MODEL` | `anthropicModel` |
|
|
204
216
|
|
|
205
217
|
## Hooks
|
|
206
218
|
|
|
@@ -342,12 +354,14 @@ claude mcp add --transport stdio --scope user e2e-runner \
|
|
|
342
354
|
| `e2e_list` | List available test suites with test names and counts |
|
|
343
355
|
| `e2e_create_test` | Create a new test JSON file |
|
|
344
356
|
| `e2e_pool_status` | Check Chrome pool availability and capacity |
|
|
345
|
-
| `
|
|
346
|
-
| `
|
|
357
|
+
| `e2e_screenshot` | Retrieve a screenshot by its hash (e.g. `ss:a3f2b1c9`) |
|
|
358
|
+
| `e2e_issue` | Fetch a GitHub/GitLab issue and generate E2E tests |
|
|
359
|
+
|
|
360
|
+
> **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
361
|
|
|
348
362
|
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
363
|
|
|
350
|
-
Once installed, Claude Code can run tests, analyze failures, create new test files
|
|
364
|
+
Once installed, Claude Code can run tests, analyze failures, and create new test files as part of its normal workflow. Just ask:
|
|
351
365
|
|
|
352
366
|
> "Run all E2E tests"
|
|
353
367
|
> "Create a test that verifies the checkout flow"
|
|
@@ -360,6 +374,70 @@ claude mcp list
|
|
|
360
374
|
# e2e-runner: ... - Connected
|
|
361
375
|
```
|
|
362
376
|
|
|
377
|
+
## Issue-to-Test
|
|
378
|
+
|
|
379
|
+
Turn GitHub and GitLab issues into executable E2E tests. Paste an issue URL and get runnable tests -- automatically.
|
|
380
|
+
|
|
381
|
+
### How It Works
|
|
382
|
+
|
|
383
|
+
1. **Fetch** -- Pulls issue details (title, body, labels) via `gh` or `glab` CLI
|
|
384
|
+
2. **Generate** -- AI creates JSON test actions based on the issue description
|
|
385
|
+
3. **Run** -- Optionally executes the tests immediately to verify if a bug is reproducible
|
|
386
|
+
|
|
387
|
+
### Two Modes
|
|
388
|
+
|
|
389
|
+
**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.
|
|
390
|
+
|
|
391
|
+
**Verify mode** (requires `ANTHROPIC_API_KEY`): Calls Claude API directly, generates tests, runs them, and reports whether the bug is confirmed or not reproducible.
|
|
392
|
+
|
|
393
|
+
### CLI
|
|
394
|
+
|
|
395
|
+
```bash
|
|
396
|
+
# Fetch and display issue details
|
|
397
|
+
e2e-runner issue https://github.com/owner/repo/issues/42
|
|
398
|
+
|
|
399
|
+
# Generate a test file via Claude API
|
|
400
|
+
e2e-runner issue https://github.com/owner/repo/issues/42 --generate
|
|
401
|
+
# -> Creates e2e/tests/issue-42.json
|
|
402
|
+
|
|
403
|
+
# Generate + run + report bug status
|
|
404
|
+
e2e-runner issue https://github.com/owner/repo/issues/42 --verify
|
|
405
|
+
# -> "BUG CONFIRMED" or "NOT REPRODUCIBLE"
|
|
406
|
+
|
|
407
|
+
# Output AI prompt as JSON (for piping)
|
|
408
|
+
e2e-runner issue https://github.com/owner/repo/issues/42 --prompt
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
### MCP
|
|
412
|
+
|
|
413
|
+
In Claude Code, the `e2e_issue` tool handles everything:
|
|
414
|
+
|
|
415
|
+
> "Fetch issue https://github.com/owner/repo/issues/42 and create E2E tests for it"
|
|
416
|
+
|
|
417
|
+
Claude Code receives the issue data, generates appropriate test actions, saves them via `e2e_create_test`, and runs them with `e2e_run`.
|
|
418
|
+
|
|
419
|
+
### Auth Requirements
|
|
420
|
+
|
|
421
|
+
- **GitHub**: `gh` CLI authenticated (`gh auth login`)
|
|
422
|
+
- **GitLab**: `glab` CLI authenticated (`glab auth login`)
|
|
423
|
+
|
|
424
|
+
Provider is auto-detected from the URL. Self-hosted GitLab is supported via `glab` config.
|
|
425
|
+
|
|
426
|
+
### Bug Verification Logic
|
|
427
|
+
|
|
428
|
+
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.
|
|
429
|
+
|
|
430
|
+
## Web Dashboard
|
|
431
|
+
|
|
432
|
+
Real-time UI for running tests, viewing results, screenshots, and run history.
|
|
433
|
+
|
|
434
|
+
```bash
|
|
435
|
+
e2e-runner dashboard # Start on default port 8484
|
|
436
|
+
e2e-runner dashboard --port 9090 # Custom port
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
Features: live test execution, screenshot viewer with copy-to-clipboard hashes (`ss:a3f2b1c9`), multi-project support via SQLite, run history with auto-pruning.
|
|
440
|
+
|
|
363
441
|
## Architecture
|
|
364
442
|
|
|
365
443
|
```
|
|
@@ -371,6 +449,12 @@ src/runner.js Parallel test executor with retries and timeouts
|
|
|
371
449
|
src/actions.js Action engine: maps JSON actions to Puppeteer calls
|
|
372
450
|
src/reporter.js JSON reports, JUnit XML, console output
|
|
373
451
|
src/mcp-server.js MCP server: exposes tools for Claude Code
|
|
452
|
+
src/mcp-tools.js Shared MCP tool definitions and handlers
|
|
453
|
+
src/dashboard.js Web dashboard: HTTP server, REST API, WebSocket
|
|
454
|
+
src/db.js SQLite multi-project database
|
|
455
|
+
src/issues.js GitHub/GitLab issue fetching (gh/glab CLI)
|
|
456
|
+
src/ai-generate.js AI test generation (prompt builder + Claude API)
|
|
457
|
+
src/verify.js Bug verification orchestrator
|
|
374
458
|
src/logger.js ANSI colored logger
|
|
375
459
|
src/index.js Programmatic API (createRunner)
|
|
376
460
|
templates/ Scaffolding templates for init command
|
package/bin/cli.js
CHANGED
|
@@ -13,6 +13,11 @@
|
|
|
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 issue <url> Fetch issue and show details
|
|
18
|
+
* e2e-runner issue <url> --generate Generate test file via Claude API
|
|
19
|
+
* e2e-runner issue <url> --verify Generate + run + report bug status
|
|
20
|
+
* e2e-runner issue <url> --prompt Output the AI prompt (for piping)
|
|
16
21
|
* e2e-runner init Scaffold e2e/ in the current project
|
|
17
22
|
* e2e-runner --help Show help
|
|
18
23
|
* e2e-runner --version Show version
|
|
@@ -20,11 +25,16 @@
|
|
|
20
25
|
|
|
21
26
|
import fs from 'fs';
|
|
22
27
|
import path from 'path';
|
|
28
|
+
import http from 'http';
|
|
23
29
|
import { fileURLToPath } from 'url';
|
|
24
30
|
import { loadConfig } from '../src/config.js';
|
|
25
31
|
import { startPool, stopPool, restartPool, getPoolStatus, waitForPool } from '../src/pool.js';
|
|
26
32
|
import { runTestsParallel, loadTestFile, loadTestSuite, loadAllSuites, listSuites } from '../src/runner.js';
|
|
27
|
-
import { generateReport, saveReport, printReport } from '../src/reporter.js';
|
|
33
|
+
import { generateReport, saveReport, printReport, persistRun } from '../src/reporter.js';
|
|
34
|
+
import { startDashboard } from '../src/dashboard.js';
|
|
35
|
+
import { fetchIssue } from '../src/issues.js';
|
|
36
|
+
import { buildPrompt, generateTests, hasApiKey } from '../src/ai-generate.js';
|
|
37
|
+
import { verifyIssue } from '../src/verify.js';
|
|
28
38
|
import { log, colors as C } from '../src/logger.js';
|
|
29
39
|
|
|
30
40
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -59,6 +69,9 @@ function parseCLIConfig() {
|
|
|
59
69
|
if (getFlag('--test-timeout')) cliArgs.testTimeout = parseInt(getFlag('--test-timeout'));
|
|
60
70
|
if (getFlag('--output')) cliArgs.outputFormat = getFlag('--output');
|
|
61
71
|
if (getFlag('--env')) cliArgs.env = getFlag('--env');
|
|
72
|
+
if (getFlag('--port')) cliArgs.dashboardPort = parseInt(getFlag('--port'));
|
|
73
|
+
if (getFlag('--dashboard-port')) cliArgs.dashboardPort = parseInt(getFlag('--dashboard-port'));
|
|
74
|
+
if (getFlag('--project-name')) cliArgs.projectName = getFlag('--project-name');
|
|
62
75
|
return cliArgs;
|
|
63
76
|
}
|
|
64
77
|
|
|
@@ -75,6 +88,14 @@ ${C.bold}Usage:${C.reset}
|
|
|
75
88
|
|
|
76
89
|
e2e-runner list List available suites
|
|
77
90
|
|
|
91
|
+
e2e-runner dashboard Start the web dashboard
|
|
92
|
+
e2e-runner dashboard --port <port> Custom port (default: 8484)
|
|
93
|
+
|
|
94
|
+
e2e-runner issue <url> Fetch issue and show details
|
|
95
|
+
e2e-runner issue <url> --generate Generate test file via Claude API
|
|
96
|
+
e2e-runner issue <url> --verify Generate + run + report bug status
|
|
97
|
+
e2e-runner issue <url> --prompt Output the AI prompt (for piping)
|
|
98
|
+
|
|
78
99
|
e2e-runner pool start Start the Chrome Pool
|
|
79
100
|
e2e-runner pool stop Stop the Chrome Pool
|
|
80
101
|
e2e-runner pool status Show pool status
|
|
@@ -96,6 +117,7 @@ ${C.bold}Options:${C.reset}
|
|
|
96
117
|
--test-timeout <ms> Per-test timeout (default: 60000)
|
|
97
118
|
--output <format> Report format: json, junit, both (default: json)
|
|
98
119
|
--env <name> Environment profile from config (default: default)
|
|
120
|
+
--project-name <name> Project display name for dashboard (default: directory name)
|
|
99
121
|
|
|
100
122
|
${C.bold}Config:${C.reset}
|
|
101
123
|
Looks for e2e.config.js or e2e.config.json in the current directory.
|
|
@@ -151,13 +173,35 @@ async function cmdRun() {
|
|
|
151
173
|
const pressure = await waitForPool(config.poolUrl);
|
|
152
174
|
log('✅', `Pool ready (${pressure.running}/${pressure.maxConcurrent} sessions, queued: ${pressure.queued})`);
|
|
153
175
|
|
|
176
|
+
// Wire up live progress to dashboard if running
|
|
177
|
+
let _lastBroadcast = null;
|
|
178
|
+
try {
|
|
179
|
+
const res = await fetch('http://127.0.0.1:' + (config.dashboardPort || 8484) + '/api/status');
|
|
180
|
+
if (res.ok) {
|
|
181
|
+
const dp = config.dashboardPort || 8484;
|
|
182
|
+
config.onProgress = (data) => {
|
|
183
|
+
const body = JSON.stringify(data);
|
|
184
|
+
_lastBroadcast = new Promise((resolve) => {
|
|
185
|
+
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 });
|
|
186
|
+
req.on('error', () => resolve());
|
|
187
|
+
req.on('close', () => resolve());
|
|
188
|
+
req.end(body);
|
|
189
|
+
});
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
} catch { /* dashboard not running */ }
|
|
193
|
+
|
|
154
194
|
// Execute tests
|
|
155
195
|
console.log('');
|
|
196
|
+
const suiteName = getFlag('--suite') || (hasFlag('--all') ? null : null);
|
|
156
197
|
const results = await runTestsParallel(tests, config, hooks);
|
|
157
198
|
const report = generateReport(results);
|
|
158
199
|
saveReport(report, config.screenshotsDir, config);
|
|
200
|
+
persistRun(report, config, suiteName);
|
|
159
201
|
printReport(report, config.screenshotsDir);
|
|
160
202
|
|
|
203
|
+
// Wait for the last dashboard broadcast (run:complete) to flush before exiting
|
|
204
|
+
if (_lastBroadcast) await _lastBroadcast;
|
|
161
205
|
process.exit(report.summary.failed > 0 ? 1 : 0);
|
|
162
206
|
}
|
|
163
207
|
|
|
@@ -288,6 +332,114 @@ ${C.bold}Next steps:${C.reset}
|
|
|
288
332
|
`);
|
|
289
333
|
}
|
|
290
334
|
|
|
335
|
+
async function cmdDashboard() {
|
|
336
|
+
const cliArgs = parseCLIConfig();
|
|
337
|
+
const config = await loadConfig(cliArgs);
|
|
338
|
+
|
|
339
|
+
console.log(`\n${C.bold}${C.cyan}@matware/e2e-runner${C.reset} v${pkg.version}`);
|
|
340
|
+
console.log(`${C.dim}Starting dashboard on port ${config.dashboardPort}...${C.reset}\n`);
|
|
341
|
+
|
|
342
|
+
const handle = await startDashboard(config);
|
|
343
|
+
|
|
344
|
+
// Keep process alive until SIGINT/SIGTERM
|
|
345
|
+
const shutdown = () => {
|
|
346
|
+
console.log(`\n${C.dim}Shutting down dashboard...${C.reset}`);
|
|
347
|
+
handle.close();
|
|
348
|
+
process.exit(0);
|
|
349
|
+
};
|
|
350
|
+
process.on('SIGINT', shutdown);
|
|
351
|
+
process.on('SIGTERM', shutdown);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async function cmdIssue() {
|
|
355
|
+
const url = args[1];
|
|
356
|
+
if (!url || url.startsWith('--')) {
|
|
357
|
+
console.error(`${C.red}Usage: e2e-runner issue <url> [--generate|--verify|--prompt]${C.reset}`);
|
|
358
|
+
process.exit(1);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const cliArgs = parseCLIConfig();
|
|
362
|
+
const config = await loadConfig(cliArgs);
|
|
363
|
+
|
|
364
|
+
if (hasFlag('--prompt')) {
|
|
365
|
+
// Output AI prompt as JSON to stdout
|
|
366
|
+
const issue = fetchIssue(url);
|
|
367
|
+
const promptData = buildPrompt(issue, config);
|
|
368
|
+
console.log(JSON.stringify(promptData, null, 2));
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (hasFlag('--verify')) {
|
|
373
|
+
// Generate + run + report
|
|
374
|
+
if (!hasApiKey(config)) {
|
|
375
|
+
console.error(`${C.red}ANTHROPIC_API_KEY is required for --verify mode.${C.reset}`);
|
|
376
|
+
process.exit(1);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
console.log(`\n${C.bold}${C.cyan}@matware/e2e-runner${C.reset} v${pkg.version}`);
|
|
380
|
+
log('🔍', 'Fetching issue...');
|
|
381
|
+
|
|
382
|
+
const result = await verifyIssue(url, config);
|
|
383
|
+
const { issue, report, bugConfirmed } = result;
|
|
384
|
+
|
|
385
|
+
console.log('');
|
|
386
|
+
if (bugConfirmed) {
|
|
387
|
+
log('🐛', `${C.red}${C.bold}BUG CONFIRMED${C.reset} — ${issue.title}`);
|
|
388
|
+
log('', `${C.dim}${report.summary.failed} of ${report.summary.total} tests failed${C.reset}`);
|
|
389
|
+
} else {
|
|
390
|
+
log('✅', `${C.green}${C.bold}NOT REPRODUCIBLE${C.reset} — ${issue.title}`);
|
|
391
|
+
log('', `${C.dim}All ${report.summary.total} tests passed${C.reset}`);
|
|
392
|
+
}
|
|
393
|
+
console.log(`${C.dim}Issue: ${issue.url}${C.reset}\n`);
|
|
394
|
+
|
|
395
|
+
process.exit(bugConfirmed ? 1 : 0);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (hasFlag('--generate')) {
|
|
399
|
+
// Generate test file via Claude API
|
|
400
|
+
if (!hasApiKey(config)) {
|
|
401
|
+
console.error(`${C.red}ANTHROPIC_API_KEY is required for --generate mode.${C.reset}`);
|
|
402
|
+
process.exit(1);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
console.log(`\n${C.bold}${C.cyan}@matware/e2e-runner${C.reset} v${pkg.version}`);
|
|
406
|
+
log('🔍', 'Fetching issue...');
|
|
407
|
+
|
|
408
|
+
const issue = fetchIssue(url);
|
|
409
|
+
log('📋', `${C.cyan}${issue.title}${C.reset}`);
|
|
410
|
+
log('🤖', 'Generating tests via Claude API...');
|
|
411
|
+
|
|
412
|
+
const { tests, suiteName } = await generateTests(issue, config);
|
|
413
|
+
|
|
414
|
+
if (!fs.existsSync(config.testsDir)) {
|
|
415
|
+
fs.mkdirSync(config.testsDir, { recursive: true });
|
|
416
|
+
}
|
|
417
|
+
const filePath = path.join(config.testsDir, `${suiteName}.json`);
|
|
418
|
+
fs.writeFileSync(filePath, JSON.stringify(tests, null, 2) + '\n');
|
|
419
|
+
|
|
420
|
+
log('✅', `Created ${C.cyan}${filePath}${C.reset} (${tests.length} tests)`);
|
|
421
|
+
console.log(`${C.dim}Run with: e2e-runner run --suite ${suiteName}${C.reset}\n`);
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Default: fetch and display issue
|
|
426
|
+
log('🔍', 'Fetching issue...');
|
|
427
|
+
const issue = fetchIssue(url);
|
|
428
|
+
|
|
429
|
+
console.log(`\n${C.bold}${issue.title}${C.reset}`);
|
|
430
|
+
console.log(`${C.dim}${'─'.repeat(50)}${C.reset}`);
|
|
431
|
+
console.log(` Repo: ${C.cyan}${issue.repo}${C.reset}`);
|
|
432
|
+
console.log(` Number: #${issue.number}`);
|
|
433
|
+
console.log(` State: ${issue.state === 'open' ? C.green : C.red}${issue.state}${C.reset}`);
|
|
434
|
+
console.log(` Labels: ${issue.labels.length ? issue.labels.join(', ') : C.dim + 'none' + C.reset}`);
|
|
435
|
+
console.log(` URL: ${C.dim}${issue.url}${C.reset}`);
|
|
436
|
+
if (issue.body) {
|
|
437
|
+
console.log(`\n${C.bold}Description:${C.reset}`);
|
|
438
|
+
console.log(issue.body.length > 500 ? issue.body.substring(0, 500) + '...' : issue.body);
|
|
439
|
+
}
|
|
440
|
+
console.log('');
|
|
441
|
+
}
|
|
442
|
+
|
|
291
443
|
// ==================== Main ====================
|
|
292
444
|
|
|
293
445
|
async function main() {
|
|
@@ -316,6 +468,14 @@ async function main() {
|
|
|
316
468
|
await cmdPool();
|
|
317
469
|
break;
|
|
318
470
|
|
|
471
|
+
case 'dashboard':
|
|
472
|
+
await cmdDashboard();
|
|
473
|
+
break;
|
|
474
|
+
|
|
475
|
+
case 'issue':
|
|
476
|
+
await cmdIssue();
|
|
477
|
+
break;
|
|
478
|
+
|
|
319
479
|
case 'init':
|
|
320
480
|
cmdInit();
|
|
321
481
|
break;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@matware/e2e-runner",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
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,7 +25,9 @@
|
|
|
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",
|
|
@@ -36,6 +38,7 @@
|
|
|
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,6 +149,19 @@ export async function executeAction(page, action, config) {
|
|
|
146
149
|
await page.hover(selector);
|
|
147
150
|
break;
|
|
148
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 */ }
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
|
|
149
165
|
case 'evaluate':
|
|
150
166
|
// Intentional: runs JS in browser page context (from test JSON files)
|
|
151
167
|
await page.evaluate(value);
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Test Generation — builds prompts and optionally calls Claude API
|
|
3
|
+
*
|
|
4
|
+
* Two modes:
|
|
5
|
+
* 1. buildPrompt() — Returns issue data + prompt for Claude Code (MCP mode, no API key)
|
|
6
|
+
* 2. generateTests() — Calls Claude API directly (CLI automation, requires ANTHROPIC_API_KEY)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
import { listSuites } from './runner.js';
|
|
11
|
+
|
|
12
|
+
const SYSTEM_PROMPT = `You are an E2E test generator for a JSON-driven browser test runner.
|
|
13
|
+
|
|
14
|
+
You output ONLY valid JSON — no markdown fences, no explanation, no comments.
|
|
15
|
+
|
|
16
|
+
The test format is:
|
|
17
|
+
[
|
|
18
|
+
{
|
|
19
|
+
"name": "descriptive-test-name",
|
|
20
|
+
"actions": [
|
|
21
|
+
{ "type": "goto", "value": "/path" },
|
|
22
|
+
{ "type": "click", "selector": "#btn" },
|
|
23
|
+
{ "type": "click", "text": "Button Label" },
|
|
24
|
+
{ "type": "type", "selector": "input[name=email]", "value": "user@example.com" },
|
|
25
|
+
{ "type": "wait", "selector": ".loaded" },
|
|
26
|
+
{ "type": "wait", "text": "Expected text" },
|
|
27
|
+
{ "type": "wait", "value": "2000" },
|
|
28
|
+
{ "type": "assert_text", "text": "Expected text on page" },
|
|
29
|
+
{ "type": "assert_url", "value": "/expected-path" },
|
|
30
|
+
{ "type": "assert_visible", "selector": ".element" },
|
|
31
|
+
{ "type": "assert_count", "selector": ".items", "value": "5" },
|
|
32
|
+
{ "type": "screenshot", "value": "step-name.png" },
|
|
33
|
+
{ "type": "select", "selector": "select#role", "value": "admin" },
|
|
34
|
+
{ "type": "clear", "selector": "input" },
|
|
35
|
+
{ "type": "press", "value": "Enter" },
|
|
36
|
+
{ "type": "scroll", "selector": ".target" },
|
|
37
|
+
{ "type": "hover", "selector": ".menu" },
|
|
38
|
+
{ "type": "evaluate", "value": "document.title" }
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
Rules:
|
|
44
|
+
- Output a JSON array of test objects
|
|
45
|
+
- Use only the action types listed above
|
|
46
|
+
- "click" with "text" (no selector) finds buttons/links by visible text
|
|
47
|
+
- "goto" values starting with "/" are relative to the app's base URL
|
|
48
|
+
- Include a screenshot action before key assertions for debugging
|
|
49
|
+
- For bug reports: write tests that assert the CORRECT behavior. If the test fails, the bug is confirmed
|
|
50
|
+
- Keep test names descriptive and kebab-case
|
|
51
|
+
- Prefer CSS selectors that are stable (data-testid, name, role) over fragile ones (nth-child, classes)
|
|
52
|
+
- If the issue description is vague, create a reasonable test that covers the described scenario`;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Returns a structured prompt + issue data for Claude Code to consume.
|
|
56
|
+
* Claude Code uses its own intelligence to create tests via e2e_create_test.
|
|
57
|
+
* No API key needed.
|
|
58
|
+
*
|
|
59
|
+
* @param {object} issue - Normalized issue from fetchIssue()
|
|
60
|
+
* @param {object} config - Loaded config
|
|
61
|
+
* @returns {object}
|
|
62
|
+
*/
|
|
63
|
+
export function buildPrompt(issue, config) {
|
|
64
|
+
let existingSuites = [];
|
|
65
|
+
try {
|
|
66
|
+
existingSuites = listSuites(config.testsDir).map(s => s.name);
|
|
67
|
+
} catch { /* no suites yet */ }
|
|
68
|
+
|
|
69
|
+
const prompt = `Based on the following issue, generate E2E test actions using the e2e_create_test tool.
|
|
70
|
+
|
|
71
|
+
## Issue: ${issue.title}
|
|
72
|
+
**Repo:** ${issue.repo}
|
|
73
|
+
**Labels:** ${issue.labels.join(', ') || 'none'}
|
|
74
|
+
**State:** ${issue.state}
|
|
75
|
+
**URL:** ${issue.url}
|
|
76
|
+
|
|
77
|
+
### Description
|
|
78
|
+
${issue.body || 'No description provided.'}
|
|
79
|
+
|
|
80
|
+
## Instructions
|
|
81
|
+
1. Analyze the issue and determine what user flows to test
|
|
82
|
+
2. Create one or more tests that verify the expected behavior
|
|
83
|
+
3. For bug reports: assert the CORRECT behavior (test failure = bug confirmed)
|
|
84
|
+
4. Use the \`e2e_create_test\` tool with suite name \`issue-${issue.number}\`
|
|
85
|
+
5. After creating the test, use \`e2e_run\` with suite \`issue-${issue.number}\` to execute it
|
|
86
|
+
|
|
87
|
+
Base URL: ${config.baseUrl}
|
|
88
|
+
Existing suites: ${existingSuites.join(', ') || 'none'}`;
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
issue: {
|
|
92
|
+
title: issue.title,
|
|
93
|
+
body: issue.body,
|
|
94
|
+
labels: issue.labels,
|
|
95
|
+
url: issue.url,
|
|
96
|
+
number: issue.number,
|
|
97
|
+
repo: issue.repo,
|
|
98
|
+
state: issue.state,
|
|
99
|
+
},
|
|
100
|
+
baseUrl: config.baseUrl,
|
|
101
|
+
prompt,
|
|
102
|
+
existingSuites,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Checks if the Anthropic API key is available.
|
|
108
|
+
* @returns {boolean}
|
|
109
|
+
*/
|
|
110
|
+
export function hasApiKey(config = {}) {
|
|
111
|
+
return !!(config.anthropicApiKey || process.env.ANTHROPIC_API_KEY);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Calls Claude API directly to generate E2E tests from an issue.
|
|
116
|
+
* Requires ANTHROPIC_API_KEY env var or config.anthropicApiKey.
|
|
117
|
+
*
|
|
118
|
+
* @param {object} issue - Normalized issue from fetchIssue()
|
|
119
|
+
* @param {object} config - Loaded config
|
|
120
|
+
* @returns {Promise<{ tests: object[], suiteName: string }>}
|
|
121
|
+
*/
|
|
122
|
+
export async function generateTests(issue, config) {
|
|
123
|
+
const apiKey = config.anthropicApiKey || process.env.ANTHROPIC_API_KEY;
|
|
124
|
+
if (!apiKey) {
|
|
125
|
+
throw new Error('ANTHROPIC_API_KEY is required for test generation. Set it as an environment variable or in config.');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const model = config.anthropicModel || 'claude-sonnet-4-5-20250929';
|
|
129
|
+
const suiteName = `issue-${issue.number}`;
|
|
130
|
+
|
|
131
|
+
const userMessage = `Generate E2E tests for this issue:
|
|
132
|
+
|
|
133
|
+
Title: ${issue.title}
|
|
134
|
+
Repo: ${issue.repo}
|
|
135
|
+
Labels: ${issue.labels.join(', ') || 'none'}
|
|
136
|
+
State: ${issue.state}
|
|
137
|
+
|
|
138
|
+
Description:
|
|
139
|
+
${issue.body || 'No description provided.'}
|
|
140
|
+
|
|
141
|
+
Base URL: ${config.baseUrl}
|
|
142
|
+
|
|
143
|
+
Output a JSON array of test objects. Nothing else.`;
|
|
144
|
+
|
|
145
|
+
const response = await fetch('https://api.anthropic.com/v1/messages', {
|
|
146
|
+
method: 'POST',
|
|
147
|
+
headers: {
|
|
148
|
+
'Content-Type': 'application/json',
|
|
149
|
+
'x-api-key': apiKey,
|
|
150
|
+
'anthropic-version': '2023-06-01',
|
|
151
|
+
},
|
|
152
|
+
body: JSON.stringify({
|
|
153
|
+
model,
|
|
154
|
+
max_tokens: 4096,
|
|
155
|
+
system: SYSTEM_PROMPT,
|
|
156
|
+
messages: [{ role: 'user', content: userMessage }],
|
|
157
|
+
}),
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
if (!response.ok) {
|
|
161
|
+
const body = await response.text();
|
|
162
|
+
throw new Error(`Claude API error (${response.status}): ${body}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const result = await response.json();
|
|
166
|
+
const text = result.content?.[0]?.text;
|
|
167
|
+
if (!text) {
|
|
168
|
+
throw new Error('Claude API returned empty response');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Parse JSON — strip markdown fences if present
|
|
172
|
+
const cleaned = text.replace(/^```(?:json)?\s*\n?/m, '').replace(/\n?```\s*$/m, '').trim();
|
|
173
|
+
let tests;
|
|
174
|
+
try {
|
|
175
|
+
tests = JSON.parse(cleaned);
|
|
176
|
+
} catch (err) {
|
|
177
|
+
throw new Error(`Failed to parse generated tests as JSON: ${err.message}\n\nRaw output:\n${text}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (!Array.isArray(tests)) {
|
|
181
|
+
throw new Error('Generated tests must be a JSON array');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return { tests, suiteName };
|
|
185
|
+
}
|
package/src/config.js
CHANGED
|
@@ -30,6 +30,11 @@ const DEFAULTS = {
|
|
|
30
30
|
outputFormat: 'json',
|
|
31
31
|
env: 'default',
|
|
32
32
|
hooks: { beforeAll: [], afterAll: [], beforeEach: [], afterEach: [] },
|
|
33
|
+
dashboardPort: 8484,
|
|
34
|
+
maxHistoryRuns: 100,
|
|
35
|
+
projectName: null,
|
|
36
|
+
anthropicApiKey: null,
|
|
37
|
+
anthropicModel: 'claude-sonnet-4-5-20250929',
|
|
33
38
|
};
|
|
34
39
|
|
|
35
40
|
function loadEnvVars() {
|
|
@@ -47,6 +52,9 @@ function loadEnvVars() {
|
|
|
47
52
|
if (process.env.TEST_TIMEOUT) env.testTimeout = parseInt(process.env.TEST_TIMEOUT);
|
|
48
53
|
if (process.env.OUTPUT_FORMAT) env.outputFormat = process.env.OUTPUT_FORMAT;
|
|
49
54
|
if (process.env.E2E_ENV) env.env = process.env.E2E_ENV;
|
|
55
|
+
if (process.env.PROJECT_NAME) env.projectName = process.env.PROJECT_NAME;
|
|
56
|
+
if (process.env.ANTHROPIC_API_KEY) env.anthropicApiKey = process.env.ANTHROPIC_API_KEY;
|
|
57
|
+
if (process.env.ANTHROPIC_MODEL) env.anthropicModel = process.env.ANTHROPIC_MODEL;
|
|
50
58
|
return env;
|
|
51
59
|
}
|
|
52
60
|
|
|
@@ -100,5 +108,11 @@ export async function loadConfig(cliArgs = {}, cwd = null) {
|
|
|
100
108
|
fs.mkdirSync(config.screenshotsDir, { recursive: true });
|
|
101
109
|
}
|
|
102
110
|
|
|
111
|
+
// Stash cwd for project identity (used by db.js)
|
|
112
|
+
config._cwd = cwd;
|
|
113
|
+
if (!config.projectName) {
|
|
114
|
+
config.projectName = path.basename(cwd);
|
|
115
|
+
}
|
|
116
|
+
|
|
103
117
|
return config;
|
|
104
118
|
}
|