@matware/e2e-runner 1.1.0 → 1.2.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.
Files changed (39) hide show
  1. package/.claude-plugin/plugin.json +9 -0
  2. package/.mcp.json +9 -0
  3. package/README.md +505 -279
  4. package/agents/test-analyzer.md +81 -0
  5. package/agents/test-creator.md +102 -0
  6. package/agents/test-improver.md +140 -0
  7. package/bin/cli.js +275 -7
  8. package/commands/create-test.md +50 -0
  9. package/commands/run.md +49 -0
  10. package/commands/verify-issue.md +63 -0
  11. package/package.json +11 -3
  12. package/skills/e2e-testing/SKILL.md +166 -0
  13. package/skills/e2e-testing/references/action-types.md +100 -0
  14. package/skills/e2e-testing/references/test-json-format.md +159 -0
  15. package/skills/e2e-testing/references/troubleshooting.md +182 -0
  16. package/src/actions.js +280 -17
  17. package/src/ai-generate.js +122 -11
  18. package/src/config.js +58 -0
  19. package/src/dashboard.js +173 -10
  20. package/src/db.js +232 -17
  21. package/src/index.js +9 -3
  22. package/src/learner-markdown.js +177 -0
  23. package/src/learner-neo4j.js +255 -0
  24. package/src/learner-sqlite.js +354 -0
  25. package/src/learner.js +413 -0
  26. package/src/mcp-tools.js +575 -16
  27. package/src/module-resolver.js +273 -0
  28. package/src/narrate.js +225 -0
  29. package/src/neo4j-pool.js +124 -0
  30. package/src/reporter.js +47 -2
  31. package/src/runner.js +180 -40
  32. package/src/verify.js +19 -5
  33. package/templates/build-dashboard.js +28 -0
  34. package/templates/dashboard/app.js +1152 -0
  35. package/templates/dashboard/styles.css +413 -0
  36. package/templates/dashboard/template.html +201 -0
  37. package/templates/dashboard.html +1091 -268
  38. package/templates/docker-compose-neo4j.yml +19 -0
  39. package/templates/e2e.config.js +3 -0
package/bin/cli.js CHANGED
@@ -14,6 +14,7 @@
14
14
  * e2e-runner pool status Show pool status
15
15
  * e2e-runner pool restart Restart the pool
16
16
  * e2e-runner dashboard Start the web dashboard
17
+ * e2e-runner capture <url> Capture a screenshot of any URL
17
18
  * e2e-runner issue <url> Fetch issue and show details
18
19
  * e2e-runner issue <url> --generate Generate test file via Claude API
19
20
  * e2e-runner issue <url> --verify Generate + run + report bug status
@@ -28,14 +29,18 @@ import path from 'path';
28
29
  import http from 'http';
29
30
  import { fileURLToPath } from 'url';
30
31
  import { loadConfig } from '../src/config.js';
31
- import { startPool, stopPool, restartPool, getPoolStatus, waitForPool } from '../src/pool.js';
32
+ import { startPool, stopPool, restartPool, getPoolStatus, waitForPool, connectToPool } from '../src/pool.js';
32
33
  import { runTestsParallel, loadTestFile, loadTestSuite, loadAllSuites, listSuites } from '../src/runner.js';
33
34
  import { generateReport, saveReport, printReport, persistRun } from '../src/reporter.js';
34
35
  import { startDashboard } from '../src/dashboard.js';
35
36
  import { fetchIssue } from '../src/issues.js';
36
37
  import { buildPrompt, generateTests, hasApiKey } from '../src/ai-generate.js';
37
38
  import { verifyIssue } from '../src/verify.js';
39
+ import { ensureProject, computeScreenshotHash, registerScreenshotHash } from '../src/db.js';
38
40
  import { log, colors as C } from '../src/logger.js';
41
+ import { listModules } from '../src/module-resolver.js';
42
+ import { getLearningsSummary, getFlakySummary, getSelectorStability, getPageHealth, getApiHealth, getErrorPatterns, getTestTrends } from '../src/learner-sqlite.js';
43
+ import { startNeo4j, stopNeo4j, getNeo4jStatus } from '../src/neo4j-pool.js';
39
44
 
40
45
  const __filename = fileURLToPath(import.meta.url);
41
46
  const __dirname = path.dirname(__filename);
@@ -59,6 +64,7 @@ function parseCLIConfig() {
59
64
  if (getFlag('--base-url')) cliArgs.baseUrl = getFlag('--base-url');
60
65
  if (getFlag('--pool-url')) cliArgs.poolUrl = getFlag('--pool-url');
61
66
  if (getFlag('--tests-dir')) cliArgs.testsDir = getFlag('--tests-dir');
67
+ if (getFlag('--modules-dir')) cliArgs.modulesDir = getFlag('--modules-dir');
62
68
  if (getFlag('--screenshots-dir')) cliArgs.screenshotsDir = getFlag('--screenshots-dir');
63
69
  if (getFlag('--concurrency')) cliArgs.concurrency = parseInt(getFlag('--concurrency'));
64
70
  if (getFlag('--pool-port')) cliArgs.poolPort = parseInt(getFlag('--pool-port'));
@@ -72,6 +78,12 @@ function parseCLIConfig() {
72
78
  if (getFlag('--port')) cliArgs.dashboardPort = parseInt(getFlag('--port'));
73
79
  if (getFlag('--dashboard-port')) cliArgs.dashboardPort = parseInt(getFlag('--dashboard-port'));
74
80
  if (getFlag('--project-name')) cliArgs.projectName = getFlag('--project-name');
81
+ if (hasFlag('--fail-on-network-error')) cliArgs.failOnNetworkError = true;
82
+ if (getFlag('--action-retries')) cliArgs.actionRetries = parseInt(getFlag('--action-retries'));
83
+ if (getFlag('--action-retry-delay')) cliArgs.actionRetryDelay = parseInt(getFlag('--action-retry-delay'));
84
+ if (getFlag('--auth-token')) cliArgs.authToken = getFlag('--auth-token');
85
+ if (getFlag('--auth-storage-key')) cliArgs.authStorageKey = getFlag('--auth-storage-key');
86
+ if (getFlag('--test-type')) cliArgs.testType = getFlag('--test-type');
75
87
  return cliArgs;
76
88
  }
77
89
 
@@ -91,22 +103,37 @@ ${C.bold}Usage:${C.reset}
91
103
  e2e-runner dashboard Start the web dashboard
92
104
  e2e-runner dashboard --port <port> Custom port (default: 8484)
93
105
 
106
+ e2e-runner capture <url> Capture a screenshot of any URL
107
+ e2e-runner capture <url> --full-page Capture full scrollable page
108
+ e2e-runner capture <url> --selector <sel> Wait for selector before capture
109
+ e2e-runner capture <url> --delay <ms> Wait before capturing
110
+ e2e-runner capture <url> --filename <name> Custom filename
111
+
94
112
  e2e-runner issue <url> Fetch issue and show details
95
113
  e2e-runner issue <url> --generate Generate test file via Claude API
96
114
  e2e-runner issue <url> --verify Generate + run + report bug status
97
115
  e2e-runner issue <url> --prompt Output the AI prompt (for piping)
116
+ e2e-runner issue <url> --test-type e2e|api Test category (default: e2e)
98
117
 
99
118
  e2e-runner pool start Start the Chrome Pool
100
119
  e2e-runner pool stop Stop the Chrome Pool
101
120
  e2e-runner pool status Show pool status
102
121
  e2e-runner pool restart Restart the Chrome Pool
103
122
 
123
+ e2e-runner learnings Show test learnings summary
124
+ e2e-runner learnings --query <q> Query: flaky, selectors, pages, apis, errors, trends
125
+
126
+ e2e-runner neo4j start Start the Neo4j knowledge graph
127
+ e2e-runner neo4j stop Stop the Neo4j container
128
+ e2e-runner neo4j status Show Neo4j status
129
+
104
130
  e2e-runner init Scaffold e2e/ in the current project
105
131
 
106
132
  ${C.bold}Options:${C.reset}
107
133
  --base-url <url> App base URL (default: http://host.docker.internal:3000)
108
134
  --pool-url <ws-url> Chrome Pool URL (default: ws://localhost:3333)
109
135
  --tests-dir <dir> Tests directory (default: e2e/tests)
136
+ --modules-dir <dir> Reusable modules directory (default: e2e/modules)
110
137
  --screenshots-dir <dir> Screenshots directory (default: e2e/screenshots)
111
138
  --concurrency <n> Parallel test workers (default: 3)
112
139
  --pool-port <port> Chrome Pool port (default: 3333)
@@ -118,6 +145,7 @@ ${C.bold}Options:${C.reset}
118
145
  --output <format> Report format: json, junit, both (default: json)
119
146
  --env <name> Environment profile from config (default: default)
120
147
  --project-name <name> Project display name for dashboard (default: directory name)
148
+ --fail-on-network-error Fail tests when network requests fail (e.g. ERR_CONNECTION_REFUSED)
121
149
 
122
150
  ${C.bold}Config:${C.reset}
123
151
  Looks for e2e.config.js or e2e.config.json in the current directory.
@@ -128,6 +156,7 @@ ${C.bold}Config:${C.reset}
128
156
  async function cmdRun() {
129
157
  const cliArgs = parseCLIConfig();
130
158
  const config = await loadConfig(cliArgs);
159
+ config.triggeredBy = 'cli';
131
160
  let tests = [];
132
161
  let hooks = {};
133
162
 
@@ -135,18 +164,18 @@ async function cmdRun() {
135
164
  console.log(`${C.dim}Pool: ${config.poolUrl} | Base: ${config.baseUrl} | Concurrency: ${config.concurrency}${C.reset}\n`);
136
165
 
137
166
  if (hasFlag('--all')) {
138
- const loaded = loadAllSuites(config.testsDir);
167
+ const loaded = loadAllSuites(config.testsDir, config.modulesDir, config.exclude);
139
168
  tests = loaded.tests;
140
169
  hooks = loaded.hooks;
141
170
  } else if (getFlag('--suite')) {
142
171
  const name = getFlag('--suite');
143
- const loaded = loadTestSuite(name, config.testsDir);
172
+ const loaded = loadTestSuite(name, config.testsDir, config.modulesDir);
144
173
  tests = loaded.tests;
145
174
  hooks = loaded.hooks;
146
175
  log('📋', `${C.cyan}${name}${C.reset} (${tests.length} tests)`);
147
176
  } else if (getFlag('--tests')) {
148
177
  const file = getFlag('--tests');
149
- const loaded = loadTestFile(path.resolve(file));
178
+ const loaded = loadTestFile(path.resolve(file), config.modulesDir);
150
179
  tests = loaded.tests;
151
180
  hooks = loaded.hooks;
152
181
  log('📋', `${C.cyan}${file}${C.reset} (${tests.length} tests)`);
@@ -217,6 +246,18 @@ async function cmdList() {
217
246
  console.log(` ${C.dim}- ${test}${C.reset}`);
218
247
  }
219
248
  }
249
+
250
+ const modules = listModules(config.modulesDir);
251
+ if (modules.length > 0) {
252
+ console.log(`${C.bold}Available modules:${C.reset}\n`);
253
+ for (const mod of modules) {
254
+ const paramNames = mod.params.map(p => p.required ? p.name : `${C.dim}${p.name}?${C.reset}`).join(', ');
255
+ console.log(` ${C.cyan}${mod.name}${C.reset} (${paramNames})`);
256
+ if (mod.description) {
257
+ console.log(` ${C.dim}${mod.description}${C.reset}`);
258
+ }
259
+ }
260
+ }
220
261
  console.log('');
221
262
  }
222
263
 
@@ -266,6 +307,7 @@ function cmdInit() {
266
307
  // Create directory structure
267
308
  const dirs = [
268
309
  path.join(cwd, 'e2e', 'tests'),
310
+ path.join(cwd, 'e2e', 'modules'),
269
311
  path.join(cwd, 'e2e', 'screenshots'),
270
312
  ];
271
313
 
@@ -351,6 +393,69 @@ async function cmdDashboard() {
351
393
  process.on('SIGTERM', shutdown);
352
394
  }
353
395
 
396
+ async function cmdCapture() {
397
+ const url = args[1];
398
+ if (!url || url.startsWith('--')) {
399
+ console.error(`${C.red}Usage: e2e-runner capture <url> [--filename <name>] [--full-page] [--selector <sel>] [--delay <ms>]${C.reset}`);
400
+ process.exit(1);
401
+ }
402
+
403
+ const cliArgs = parseCLIConfig();
404
+ const config = await loadConfig(cliArgs);
405
+
406
+ console.log(`\n${C.bold}${C.cyan}@matware/e2e-runner${C.reset} v${pkg.version}`);
407
+
408
+ log('🔌', 'Checking Chrome Pool...');
409
+ await waitForPool(config.poolUrl);
410
+
411
+ let browser;
412
+ try {
413
+ browser = await connectToPool(config.poolUrl);
414
+ const page = await browser.newPage();
415
+ await page.setViewport(config.viewport);
416
+
417
+ log('📸', `Navigating to ${C.cyan}${url}${C.reset}`);
418
+ await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 });
419
+
420
+ const selector = getFlag('--selector');
421
+ if (selector) {
422
+ log('⏳', `Waiting for selector: ${C.dim}${selector}${C.reset}`);
423
+ await page.waitForSelector(selector, { timeout: 10000 });
424
+ }
425
+
426
+ const delay = getFlag('--delay');
427
+ if (delay) {
428
+ await new Promise(r => setTimeout(r, parseInt(delay)));
429
+ }
430
+
431
+ // Build filename
432
+ let filename = getFlag('--filename') || `capture-${Date.now()}.png`;
433
+ filename = path.basename(filename);
434
+ if (!filename.endsWith('.png')) filename += '.png';
435
+
436
+ if (!fs.existsSync(config.screenshotsDir)) {
437
+ fs.mkdirSync(config.screenshotsDir, { recursive: true });
438
+ }
439
+
440
+ const screenshotPath = path.join(config.screenshotsDir, filename);
441
+ const fullPage = hasFlag('--full-page');
442
+ await page.screenshot({ path: screenshotPath, fullPage });
443
+
444
+ // Register hash in SQLite
445
+ const cwd = process.cwd();
446
+ const projectName = config.projectName || path.basename(cwd);
447
+ const projectId = ensureProject(cwd, projectName, config.screenshotsDir, config.testsDir);
448
+ const hash = computeScreenshotHash(screenshotPath);
449
+ registerScreenshotHash(hash, screenshotPath, projectId, null);
450
+
451
+ log('✅', `Saved: ${C.cyan}${screenshotPath}${C.reset}`);
452
+ log('🏷️', `Hash: ${C.bold}ss:${hash}${C.reset}`);
453
+ console.log('');
454
+ } finally {
455
+ if (browser) browser.disconnect();
456
+ }
457
+ }
458
+
354
459
  async function cmdIssue() {
355
460
  const url = args[1];
356
461
  if (!url || url.startsWith('--')) {
@@ -360,11 +465,12 @@ async function cmdIssue() {
360
465
 
361
466
  const cliArgs = parseCLIConfig();
362
467
  const config = await loadConfig(cliArgs);
468
+ const testType = cliArgs.testType || 'e2e';
363
469
 
364
470
  if (hasFlag('--prompt')) {
365
471
  // Output AI prompt as JSON to stdout
366
472
  const issue = fetchIssue(url);
367
- const promptData = buildPrompt(issue, config);
473
+ const promptData = buildPrompt(issue, config, testType);
368
474
  console.log(JSON.stringify(promptData, null, 2));
369
475
  return;
370
476
  }
@@ -379,6 +485,7 @@ async function cmdIssue() {
379
485
  console.log(`\n${C.bold}${C.cyan}@matware/e2e-runner${C.reset} v${pkg.version}`);
380
486
  log('🔍', 'Fetching issue...');
381
487
 
488
+ config.testType = testType;
382
489
  const result = await verifyIssue(url, config);
383
490
  const { issue, report, bugConfirmed } = result;
384
491
 
@@ -407,9 +514,9 @@ async function cmdIssue() {
407
514
 
408
515
  const issue = fetchIssue(url);
409
516
  log('📋', `${C.cyan}${issue.title}${C.reset}`);
410
- log('🤖', 'Generating tests via Claude API...');
517
+ log('🤖', `Generating ${testType} tests via Claude API...`);
411
518
 
412
- const { tests, suiteName } = await generateTests(issue, config);
519
+ const { tests, suiteName } = await generateTests(issue, config, testType);
413
520
 
414
521
  if (!fs.existsSync(config.testsDir)) {
415
522
  fs.mkdirSync(config.testsDir, { recursive: true });
@@ -440,6 +547,155 @@ async function cmdIssue() {
440
547
  console.log('');
441
548
  }
442
549
 
550
+ async function cmdLearnings() {
551
+ const cliArgs = parseCLIConfig();
552
+ const config = await loadConfig(cliArgs);
553
+ const projectId = ensureProject(config._cwd, config.projectName, config.screenshotsDir, config.testsDir);
554
+ const days = config.learningsDays || 30;
555
+ const query = getFlag('--query') || 'summary';
556
+
557
+ console.log(`\n${C.bold}${C.cyan}@matware/e2e-runner${C.reset} v${pkg.version}`);
558
+ console.log(`${C.dim}Project: ${config.projectName} | Analysis window: ${days} days${C.reset}\n`);
559
+
560
+ switch (query) {
561
+ case 'summary': {
562
+ const summary = getLearningsSummary(projectId);
563
+ if (summary.totalRuns === 0) {
564
+ console.log(`${C.dim}No learnings data yet. Run some tests to start building knowledge.${C.reset}\n`);
565
+ return;
566
+ }
567
+ console.log(`${C.bold}Health Overview${C.reset}`);
568
+ console.log(`${'─'.repeat(50)}`);
569
+ console.log(` Total Runs: ${C.bold}${summary.totalRuns}${C.reset}`);
570
+ console.log(` Total Tests: ${C.bold}${summary.totalTests}${C.reset}`);
571
+ console.log(` Pass Rate: ${summary.overallPassRate >= 90 ? C.green : summary.overallPassRate >= 70 ? '' : C.red}${summary.overallPassRate}%${C.reset}`);
572
+ console.log(` Avg Duration: ${summary.avgDurationMs < 1000 ? summary.avgDurationMs + 'ms' : (summary.avgDurationMs / 1000).toFixed(1) + 's'}`);
573
+ console.log(` Flaky Tests: ${summary.flakyTests.length > 0 ? C.red : C.green}${summary.flakyTests.length}${C.reset}`);
574
+ console.log(` Unstable Selectors: ${summary.unstableSelectors.length > 0 ? C.red : C.green}${summary.unstableSelectors.length}${C.reset}`);
575
+
576
+ if (summary.flakyTests.length > 0) {
577
+ console.log(`\n${C.bold}Top Flaky Tests${C.reset}`);
578
+ summary.flakyTests.slice(0, 5).forEach(f => {
579
+ console.log(` ${C.yellow}⚠${C.reset} ${f.test_name} — ${f.flaky_rate}% flaky`);
580
+ });
581
+ }
582
+ if (summary.topErrors.length > 0) {
583
+ console.log(`\n${C.bold}Top Errors${C.reset}`);
584
+ summary.topErrors.slice(0, 5).forEach(e => {
585
+ console.log(` ${C.red}✗${C.reset} [${e.category}] ${e.pattern.slice(0, 60)}${e.pattern.length > 60 ? '...' : ''} (${e.occurrence_count}x)`);
586
+ });
587
+ }
588
+ console.log('');
589
+ break;
590
+ }
591
+ case 'flaky': {
592
+ const flaky = getFlakySummary(projectId, days);
593
+ if (flaky.length === 0) { console.log(`${C.green}No flaky tests found.${C.reset}\n`); return; }
594
+ console.log(`${C.bold}Flaky Tests${C.reset}\n`);
595
+ flaky.forEach(f => {
596
+ console.log(` ${C.yellow}⚠${C.reset} ${C.bold}${f.test_name}${C.reset}`);
597
+ console.log(` Rate: ${f.flaky_rate}% | Occurrences: ${f.flaky_count}/${f.total_runs} | Avg attempts: ${f.avg_attempts}`);
598
+ });
599
+ console.log('');
600
+ break;
601
+ }
602
+ case 'selectors': {
603
+ const sels = getSelectorStability(projectId, days);
604
+ if (sels.length === 0) { console.log(`${C.green}All selectors are stable.${C.reset}\n`); return; }
605
+ console.log(`${C.bold}Unstable Selectors${C.reset}\n`);
606
+ sels.forEach(s => {
607
+ console.log(` ${C.red}✗${C.reset} ${C.dim}${s.selector}${C.reset}`);
608
+ console.log(` Action: ${s.action_type} | Fail: ${s.fail_rate}% | Uses: ${s.total_uses} | Tests: ${s.used_by_tests}`);
609
+ });
610
+ console.log('');
611
+ break;
612
+ }
613
+ case 'pages': {
614
+ const pages = getPageHealth(projectId, days);
615
+ const failing = pages.filter(p => p.fail_rate > 0);
616
+ if (failing.length === 0) { console.log(`${C.green}All pages are healthy.${C.reset}\n`); return; }
617
+ console.log(`${C.bold}Failing Pages${C.reset}\n`);
618
+ failing.forEach(p => {
619
+ console.log(` ${C.red}✗${C.reset} ${C.bold}${p.url_path}${C.reset}`);
620
+ console.log(` Fail: ${p.fail_rate}% | Visits: ${p.total_visits} | Console errors: ${p.console_errors} | Network errors: ${p.network_errors}`);
621
+ });
622
+ console.log('');
623
+ break;
624
+ }
625
+ case 'apis': {
626
+ const apis = getApiHealth(projectId, days);
627
+ const issues = apis.filter(a => a.error_rate > 0);
628
+ if (issues.length === 0) { console.log(`${C.green}All API endpoints are healthy.${C.reset}\n`); return; }
629
+ console.log(`${C.bold}API Issues${C.reset}\n`);
630
+ issues.forEach(a => {
631
+ console.log(` ${C.red}✗${C.reset} ${C.bold}${a.endpoint}${C.reset}`);
632
+ console.log(` Error: ${a.error_rate}% | Calls: ${a.total_calls} | Avg: ${Math.round(a.avg_duration_ms)}ms | Status: ${a.status_codes}`);
633
+ });
634
+ console.log('');
635
+ break;
636
+ }
637
+ case 'errors': {
638
+ const errors = getErrorPatterns(projectId);
639
+ if (errors.length === 0) { console.log(`${C.green}No error patterns recorded.${C.reset}\n`); return; }
640
+ console.log(`${C.bold}Error Patterns${C.reset}\n`);
641
+ errors.forEach(e => {
642
+ console.log(` ${C.red}✗${C.reset} [${e.category}] ${e.pattern.slice(0, 70)}${e.pattern.length > 70 ? '...' : ''}`);
643
+ console.log(` Count: ${e.occurrence_count} | Last: ${(e.last_seen || '').split('T')[0]} | Test: ${e.example_test || '-'}`);
644
+ });
645
+ console.log('');
646
+ break;
647
+ }
648
+ case 'trends': {
649
+ const trends = getTestTrends(projectId, days);
650
+ if (trends.length === 0) { console.log(`${C.dim}No trend data available.${C.reset}\n`); return; }
651
+ console.log(`${C.bold}Test Trends (${days} days)${C.reset}\n`);
652
+ console.log(` ${'Date'.padEnd(12)} ${'Pass Rate'.padEnd(11)} ${'Tests'.padEnd(7)} ${'Pass'.padEnd(6)} ${'Fail'.padEnd(6)} Flaky`);
653
+ console.log(` ${'─'.repeat(55)}`);
654
+ trends.forEach(t => {
655
+ const rateColor = t.pass_rate >= 90 ? C.green : t.pass_rate >= 70 ? '' : C.red;
656
+ console.log(` ${t.date.padEnd(12)} ${rateColor}${(t.pass_rate + '%').padEnd(11)}${C.reset} ${String(t.total_tests).padEnd(7)} ${C.green}${String(t.passed).padEnd(6)}${C.reset} ${t.failed > 0 ? C.red : ''}${String(t.failed).padEnd(6)}${C.reset} ${t.flaky_count}`);
657
+ });
658
+ console.log('');
659
+ break;
660
+ }
661
+ default:
662
+ console.error(`${C.red}Unknown query: ${query}. Available: summary, flaky, selectors, pages, apis, errors, trends${C.reset}`);
663
+ process.exit(1);
664
+ }
665
+ }
666
+
667
+ async function cmdNeo4j() {
668
+ const subCmd = args[1];
669
+ const cliArgs = parseCLIConfig();
670
+ const config = await loadConfig(cliArgs);
671
+
672
+ switch (subCmd) {
673
+ case 'start':
674
+ startNeo4j(config);
675
+ break;
676
+ case 'stop':
677
+ stopNeo4j(config);
678
+ break;
679
+ case 'status': {
680
+ const status = getNeo4jStatus(config);
681
+ console.log(`\n${C.bold}Neo4j Status:${C.reset}\n`);
682
+ if (status.running) {
683
+ console.log(` Status: ${C.green}Running${C.reset}`);
684
+ console.log(` Bolt: ${C.cyan}bolt://localhost:${status.boltPort}${C.reset}`);
685
+ console.log(` Browser: ${C.cyan}http://localhost:${status.httpPort}${C.reset}`);
686
+ } else {
687
+ console.log(` Status: ${C.red}Stopped${C.reset}`);
688
+ if (status.error) console.log(` ${C.dim}${status.error}${C.reset}`);
689
+ }
690
+ console.log('');
691
+ break;
692
+ }
693
+ default:
694
+ console.error(`${C.red}Unknown subcommand: ${subCmd}. Available: start, stop, status${C.reset}`);
695
+ process.exit(1);
696
+ }
697
+ }
698
+
443
699
  // ==================== Main ====================
444
700
 
445
701
  async function main() {
@@ -472,10 +728,22 @@ async function main() {
472
728
  await cmdDashboard();
473
729
  break;
474
730
 
731
+ case 'capture':
732
+ await cmdCapture();
733
+ break;
734
+
475
735
  case 'issue':
476
736
  await cmdIssue();
477
737
  break;
478
738
 
739
+ case 'learnings':
740
+ await cmdLearnings();
741
+ break;
742
+
743
+ case 'neo4j':
744
+ await cmdNeo4j();
745
+ break;
746
+
479
747
  case 'init':
480
748
  cmdInit();
481
749
  break;
@@ -0,0 +1,50 @@
1
+ ---
2
+ description: Create a new E2E test by exploring the UI and designing test actions
3
+ user_invocable: true
4
+ allowed_tools:
5
+ - mcp__e2e-runner__e2e_pool_status
6
+ - mcp__e2e-runner__e2e_capture
7
+ - mcp__e2e-runner__e2e_list
8
+ - mcp__e2e-runner__e2e_create_test
9
+ - mcp__e2e-runner__e2e_create_module
10
+ - mcp__e2e-runner__e2e_run
11
+ - mcp__e2e-runner__e2e_screenshot
12
+ - Read
13
+ - Grep
14
+ - Glob
15
+ ---
16
+
17
+ # Create E2E Test
18
+
19
+ Help the user create a new E2E test file by exploring the application and designing appropriate test actions.
20
+
21
+ ## Workflow
22
+
23
+ 1. **Understand the goal** — Ask the user what they want to test if not already specified. Identify the page(s), user flow, and expected outcomes.
24
+
25
+ 2. **Check pool** — Call `e2e_pool_status` to ensure the Chrome pool is available.
26
+
27
+ 3. **Explore the UI** — Use `e2e_capture` to screenshot the target page(s). This helps understand the current state of the UI, available elements, and layout.
28
+
29
+ 4. **Check existing tests** — Call `e2e_list` to see what test suites already exist. Read relevant existing test files with `Read` to follow conventions and avoid duplication.
30
+
31
+ 5. **Explore source code** (optional) — If needed, use `Grep` and `Read` to find selectors, form field IDs, API endpoints, or component structure in the application source code.
32
+
33
+ 6. **Design the test** — Based on UI exploration and source code analysis, design the test actions:
34
+ - Use the most specific selectors available (data-testid > id > class > text)
35
+ - Prefer granular assertion actions over `evaluate`
36
+ - Use framework-aware actions for React/MUI (`type_react`, `click_option`, `focus_autocomplete`)
37
+ - Add `wait` actions before assertions on dynamic content
38
+ - Add `assert_no_network_errors` after critical page loads
39
+ - Consider adding an `expect` field for visual verification
40
+
41
+ 7. **Create the test** — Call `e2e_create_test` with the designed test structure. Consider creating reusable modules with `e2e_create_module` for repeated sequences (auth, navigation).
42
+
43
+ 8. **Validate** — Run the newly created test with `e2e_run` using the `suite` parameter. Analyze results and iterate if needed.
44
+
45
+ ## Arguments
46
+
47
+ The user may provide:
48
+ - A test name: `/e2e-runner:create-test login-flow`
49
+ - A description of what to test: `/e2e-runner:create-test test the checkout process`
50
+ - A URL to start from: `/e2e-runner:create-test http://localhost:3000/checkout`
@@ -0,0 +1,49 @@
1
+ ---
2
+ description: Run E2E tests and analyze results with screenshots and network drill-down
3
+ user_invocable: true
4
+ allowed_tools:
5
+ - mcp__e2e-runner__e2e_pool_status
6
+ - mcp__e2e-runner__e2e_list
7
+ - mcp__e2e-runner__e2e_run
8
+ - mcp__e2e-runner__e2e_screenshot
9
+ - mcp__e2e-runner__e2e_network_logs
10
+ - mcp__e2e-runner__e2e_learnings
11
+ ---
12
+
13
+ # Run E2E Tests
14
+
15
+ Execute E2E tests and provide a complete analysis of results.
16
+
17
+ ## Workflow
18
+
19
+ 1. **Check pool availability** — Call `e2e_pool_status` to confirm the Chrome pool is running. If not available, tell the user to run `npx e2e-runner pool start` via CLI.
20
+
21
+ 2. **List available suites** — Call `e2e_list` to show the user what test suites are available.
22
+
23
+ 3. **Run tests** — Call `e2e_run` based on user input:
24
+ - If user specified a suite name: use `suite` parameter
25
+ - If user specified a file: use `file` parameter
26
+ - If user said "all" or didn't specify: use `all: true`
27
+ - Always pass `cwd` with the current working directory
28
+ - Pass any user-specified overrides: `baseUrl`, `concurrency`, `retries`, `failOnNetworkError`
29
+
30
+ 4. **Analyze results** — Parse the run response:
31
+ - Report pass/fail summary and duration
32
+ - For failures: show error messages and retrieve error screenshots with `e2e_screenshot`
33
+ - For verifications (tests with `expect`): retrieve verification screenshots and judge against descriptions
34
+ - Highlight flaky tests if any
35
+ - Summarize network activity (failed requests, slow requests)
36
+
37
+ 5. **Drill down if needed** — For failed tests:
38
+ - Use `e2e_network_logs` with `runDbId` to investigate network failures
39
+ - Use `e2e_learnings` to check if this is a known pattern or new failure
40
+
41
+ 6. **Report** — Provide a clear summary to the user with actionable next steps.
42
+
43
+ ## Arguments
44
+
45
+ The user may pass arguments after the command:
46
+ - Suite name: `/e2e-runner:run auth` → run the auth suite
47
+ - `--all`: run all suites
48
+ - `--base-url <url>`: override base URL
49
+ - `--retries <n>`: set retry count
@@ -0,0 +1,63 @@
1
+ ---
2
+ description: Verify a GitHub/GitLab issue by creating and running E2E tests
3
+ user_invocable: true
4
+ allowed_tools:
5
+ - mcp__e2e-runner__e2e_pool_status
6
+ - mcp__e2e-runner__e2e_issue
7
+ - mcp__e2e-runner__e2e_create_test
8
+ - mcp__e2e-runner__e2e_run
9
+ - mcp__e2e-runner__e2e_screenshot
10
+ - mcp__e2e-runner__e2e_network_logs
11
+ - mcp__e2e-runner__e2e_capture
12
+ - Read
13
+ - Grep
14
+ ---
15
+
16
+ # Verify Issue
17
+
18
+ Turn a GitHub or GitLab bug report into executable E2E tests to confirm or dismiss the bug.
19
+
20
+ ## Workflow
21
+
22
+ 1. **Check pool** — Call `e2e_pool_status` to ensure the Chrome pool is available.
23
+
24
+ 2. **Fetch the issue** — Call `e2e_issue` with the issue URL. Default `mode: "prompt"` returns issue details + a structured prompt for test creation.
25
+
26
+ 3. **Analyze the issue** — Parse the issue details:
27
+ - Understand the reported bug or expected behavior
28
+ - Identify affected pages/flows
29
+ - Note any reproduction steps provided
30
+
31
+ 4. **Explore the app** — Use `e2e_capture` to screenshot relevant pages. Use `Read` and `Grep` to check source code for related components, API endpoints, or selectors.
32
+
33
+ 5. **Design tests** — Create tests that assert the **correct behavior**:
34
+ - If tests **fail** → bug is confirmed (correct behavior is not working)
35
+ - If tests **pass** → bug is not reproducible
36
+
37
+ 6. **Create and run** — Use `e2e_create_test` to write the test file, then `e2e_run` to execute it.
38
+
39
+ 7. **Analyze results** — For failures:
40
+ - Retrieve error screenshots with `e2e_screenshot`
41
+ - Check network logs with `e2e_network_logs` for API-related issues
42
+ - Determine if the failure confirms the bug
43
+
44
+ 8. **Report verdict** — Clearly state:
45
+ - **BUG CONFIRMED**: tests failed, reproducing the issue
46
+ - **NOT REPRODUCIBLE**: tests passed, correct behavior works as expected
47
+ - Include evidence (screenshots, error messages, network details)
48
+
49
+ ## Alternative: Verify Mode
50
+
51
+ If `ANTHROPIC_API_KEY` is set, use `e2e_issue` with `mode: "verify"` for a fully automated flow — it generates tests via Claude API, runs them, and reports the result.
52
+
53
+ ## Arguments
54
+
55
+ **Required**: GitHub or GitLab issue URL
56
+
57
+ ```
58
+ /e2e-runner:verify-issue https://github.com/org/repo/issues/123
59
+ ```
60
+
61
+ Optional flags:
62
+ - `--test-type api` — generate API tests instead of UI tests
63
+ - `--verify` — use verify mode (requires ANTHROPIC_API_KEY)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matware/e2e-runner",
3
- "version": "1.1.0",
3
+ "version": "1.2.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",
@@ -15,7 +15,12 @@
15
15
  "files": [
16
16
  "bin/",
17
17
  "src/",
18
- "templates/"
18
+ "templates/",
19
+ ".claude-plugin/",
20
+ ".mcp.json",
21
+ "skills/",
22
+ "commands/",
23
+ "agents/"
19
24
  ],
20
25
  "keywords": [
21
26
  "e2e",
@@ -33,7 +38,7 @@
33
38
  "license": "Apache-2.0",
34
39
  "repository": {
35
40
  "type": "git",
36
- "url": "https://github.com/fastslack/mtw-e2e-runner.git"
41
+ "url": "git+https://github.com/fastslack/mtw-e2e-runner.git"
37
42
  },
38
43
  "homepage": "https://github.com/fastslack/mtw-e2e-runner#readme",
39
44
  "dependencies": {
@@ -41,6 +46,9 @@
41
46
  "better-sqlite3": "^11.0.0",
42
47
  "puppeteer-core": "^24.0.0"
43
48
  },
49
+ "scripts": {
50
+ "build:dashboard": "node templates/build-dashboard.js"
51
+ },
44
52
  "engines": {
45
53
  "node": ">=20.0.0"
46
54
  }