@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 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
- # Install
42
- npm install @matware/e2e-runner
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
- # Scaffold project structure
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
- | `e2e_pool_start` | Start the Chrome pool Docker container |
346
- | `e2e_pool_stop` | Stop the Chrome pool |
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, and manage the Chrome pool as part of its normal workflow. Just ask:
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.0.3",
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
- const filepath = `${screenshotsDir}/${filename}`;
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 'evaluate':
150
- // Intentional: runs JS in browser page context (from test JSON files)
151
- await page.evaluate(value);
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}`);