@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.
- package/.claude-plugin/plugin.json +9 -0
- package/.mcp.json +9 -0
- package/README.md +505 -279
- package/agents/test-analyzer.md +81 -0
- package/agents/test-creator.md +102 -0
- package/agents/test-improver.md +140 -0
- package/bin/cli.js +275 -7
- package/commands/create-test.md +50 -0
- package/commands/run.md +49 -0
- package/commands/verify-issue.md +63 -0
- package/package.json +11 -3
- package/skills/e2e-testing/SKILL.md +166 -0
- package/skills/e2e-testing/references/action-types.md +100 -0
- package/skills/e2e-testing/references/test-json-format.md +159 -0
- package/skills/e2e-testing/references/troubleshooting.md +182 -0
- package/src/actions.js +280 -17
- package/src/ai-generate.js +122 -11
- package/src/config.js +58 -0
- package/src/dashboard.js +173 -10
- package/src/db.js +232 -17
- package/src/index.js +9 -3
- package/src/learner-markdown.js +177 -0
- package/src/learner-neo4j.js +255 -0
- package/src/learner-sqlite.js +354 -0
- package/src/learner.js +413 -0
- package/src/mcp-tools.js +575 -16
- package/src/module-resolver.js +273 -0
- package/src/narrate.js +225 -0
- package/src/neo4j-pool.js +124 -0
- package/src/reporter.js +47 -2
- package/src/runner.js +180 -40
- package/src/verify.js +19 -5
- package/templates/build-dashboard.js +28 -0
- package/templates/dashboard/app.js +1152 -0
- package/templates/dashboard/styles.css +413 -0
- package/templates/dashboard/template.html +201 -0
- package/templates/dashboard.html +1091 -268
- package/templates/docker-compose-neo4j.yml +19 -0
- 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('🤖',
|
|
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`
|
package/commands/run.md
ADDED
|
@@ -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
|
|
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
|
}
|