@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
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Learning system — SQLite read queries.
|
|
3
|
+
*
|
|
4
|
+
* All functions return plain objects/arrays ready for JSON serialization.
|
|
5
|
+
* Used by MCP tools, dashboard REST endpoints, CLI, and markdown generator.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { getDb } from './db.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Full learning summary for a project — reads from the cached learning_summary table.
|
|
12
|
+
* Falls back to live queries if cache is empty.
|
|
13
|
+
*/
|
|
14
|
+
export function getLearningsSummary(projectId) {
|
|
15
|
+
const d = getDb();
|
|
16
|
+
const empty = {
|
|
17
|
+
totalRuns: 0, totalTests: 0, overallPassRate: 0, avgDurationMs: 0,
|
|
18
|
+
flakyTests: [], slowTests: [], unstableSelectors: [],
|
|
19
|
+
failingPages: [], apiIssues: [], topErrors: [], updatedAt: null,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// Cross-project aggregate when projectId is null
|
|
23
|
+
if (projectId === null || projectId === undefined) {
|
|
24
|
+
const rows = d.prepare('SELECT * FROM learning_summary').all();
|
|
25
|
+
if (!rows.length) return empty;
|
|
26
|
+
let totalRuns = 0, totalTests = 0, passSumW = 0, durSumW = 0;
|
|
27
|
+
let allFlaky = [], allSlow = [], allSelectors = [], allPages = [], allApis = [], allErrors = [];
|
|
28
|
+
let latestUpdate = null;
|
|
29
|
+
for (const row of rows) {
|
|
30
|
+
totalRuns += row.total_runs;
|
|
31
|
+
totalTests += row.total_tests;
|
|
32
|
+
passSumW += row.overall_pass_rate * row.total_tests;
|
|
33
|
+
durSumW += row.avg_duration_ms * row.total_tests;
|
|
34
|
+
allFlaky = allFlaky.concat(JSON.parse(row.flaky_tests || '[]'));
|
|
35
|
+
allSlow = allSlow.concat(JSON.parse(row.slow_tests || '[]'));
|
|
36
|
+
allSelectors = allSelectors.concat(JSON.parse(row.unstable_selectors || '[]'));
|
|
37
|
+
allPages = allPages.concat(JSON.parse(row.failing_pages || '[]'));
|
|
38
|
+
allApis = allApis.concat(JSON.parse(row.api_issues || '[]'));
|
|
39
|
+
allErrors = allErrors.concat(JSON.parse(row.top_errors || '[]'));
|
|
40
|
+
if (!latestUpdate || (row.updated_at && row.updated_at > latestUpdate)) latestUpdate = row.updated_at;
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
totalRuns, totalTests,
|
|
44
|
+
overallPassRate: totalTests > 0 ? Math.round(passSumW / totalTests * 10) / 10 : 0,
|
|
45
|
+
avgDurationMs: totalTests > 0 ? Math.round(durSumW / totalTests) : 0,
|
|
46
|
+
flakyTests: allFlaky, slowTests: allSlow, unstableSelectors: allSelectors,
|
|
47
|
+
failingPages: allPages, apiIssues: allApis, topErrors: allErrors, updatedAt: latestUpdate,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const row = d.prepare('SELECT * FROM learning_summary WHERE project_id = ?').get(projectId);
|
|
52
|
+
if (!row) return empty;
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
totalRuns: row.total_runs,
|
|
56
|
+
totalTests: row.total_tests,
|
|
57
|
+
overallPassRate: Math.round(row.overall_pass_rate * 10) / 10,
|
|
58
|
+
avgDurationMs: Math.round(row.avg_duration_ms),
|
|
59
|
+
flakyTests: JSON.parse(row.flaky_tests || '[]'),
|
|
60
|
+
slowTests: JSON.parse(row.slow_tests || '[]'),
|
|
61
|
+
unstableSelectors: JSON.parse(row.unstable_selectors || '[]'),
|
|
62
|
+
failingPages: JSON.parse(row.failing_pages || '[]'),
|
|
63
|
+
apiIssues: JSON.parse(row.api_issues || '[]'),
|
|
64
|
+
topErrors: JSON.parse(row.top_errors || '[]'),
|
|
65
|
+
updatedAt: row.updated_at,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Flaky test details — tests that pass only after retries. */
|
|
70
|
+
export function getFlakySummary(projectId, days = 30) {
|
|
71
|
+
const d = getDb();
|
|
72
|
+
return d.prepare(`
|
|
73
|
+
SELECT
|
|
74
|
+
test_name,
|
|
75
|
+
COUNT(*) AS total_runs,
|
|
76
|
+
SUM(flaky) AS flaky_count,
|
|
77
|
+
ROUND(AVG(flaky) * 100, 1) AS flaky_rate,
|
|
78
|
+
ROUND(AVG(duration_ms)) AS avg_duration_ms,
|
|
79
|
+
MAX(CASE WHEN flaky = 1 THEN created_at END) AS last_flaky,
|
|
80
|
+
ROUND(AVG(attempt), 1) AS avg_attempts
|
|
81
|
+
FROM test_learnings
|
|
82
|
+
WHERE project_id = ? AND created_at >= datetime('now', '-' || ? || ' days')
|
|
83
|
+
GROUP BY test_name
|
|
84
|
+
HAVING flaky_count > 0
|
|
85
|
+
ORDER BY flaky_rate DESC
|
|
86
|
+
`).all(projectId, days);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Selector stability — selectors with failure rates. */
|
|
90
|
+
export function getSelectorStability(projectId, days = 30) {
|
|
91
|
+
const d = getDb();
|
|
92
|
+
return d.prepare(`
|
|
93
|
+
SELECT
|
|
94
|
+
selector,
|
|
95
|
+
action_type,
|
|
96
|
+
COUNT(*) AS total_uses,
|
|
97
|
+
SUM(CASE WHEN success = 0 THEN 1 ELSE 0 END) AS fail_count,
|
|
98
|
+
ROUND(AVG(CASE WHEN success = 0 THEN 100.0 ELSE 0.0 END), 1) AS fail_rate,
|
|
99
|
+
COUNT(DISTINCT test_name) AS used_by_tests,
|
|
100
|
+
page_url,
|
|
101
|
+
MAX(CASE WHEN success = 0 THEN error END) AS last_error
|
|
102
|
+
FROM selector_learnings
|
|
103
|
+
WHERE project_id = ? AND created_at >= datetime('now', '-' || ? || ' days')
|
|
104
|
+
GROUP BY selector, action_type
|
|
105
|
+
HAVING fail_rate > 0
|
|
106
|
+
ORDER BY fail_rate DESC
|
|
107
|
+
`).all(projectId, days);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Page health — pages with failure rates, console/network errors. */
|
|
111
|
+
export function getPageHealth(projectId, days = 30) {
|
|
112
|
+
const d = getDb();
|
|
113
|
+
return d.prepare(`
|
|
114
|
+
SELECT
|
|
115
|
+
url_path,
|
|
116
|
+
COUNT(*) AS total_visits,
|
|
117
|
+
SUM(CASE WHEN success = 0 THEN 1 ELSE 0 END) AS fail_count,
|
|
118
|
+
ROUND(AVG(CASE WHEN success = 0 THEN 100.0 ELSE 0.0 END), 1) AS fail_rate,
|
|
119
|
+
COUNT(DISTINCT test_name) AS tested_by,
|
|
120
|
+
SUM(console_errors) AS console_errors,
|
|
121
|
+
SUM(console_warns) AS console_warns,
|
|
122
|
+
SUM(network_errors) AS network_errors,
|
|
123
|
+
ROUND(AVG(load_time_ms)) AS avg_load_ms
|
|
124
|
+
FROM page_learnings
|
|
125
|
+
WHERE project_id = ? AND created_at >= datetime('now', '-' || ? || ' days')
|
|
126
|
+
GROUP BY url_path
|
|
127
|
+
ORDER BY fail_rate DESC
|
|
128
|
+
`).all(projectId, days);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** API health — endpoints with error rates and latency. */
|
|
132
|
+
export function getApiHealth(projectId, days = 30) {
|
|
133
|
+
const d = getDb();
|
|
134
|
+
return d.prepare(`
|
|
135
|
+
SELECT
|
|
136
|
+
endpoint,
|
|
137
|
+
COUNT(*) AS total_calls,
|
|
138
|
+
SUM(is_error) AS error_count,
|
|
139
|
+
ROUND(AVG(CASE WHEN is_error = 1 THEN 100.0 ELSE 0.0 END), 1) AS error_rate,
|
|
140
|
+
ROUND(AVG(duration_ms)) AS avg_duration_ms,
|
|
141
|
+
MAX(duration_ms) AS max_duration_ms,
|
|
142
|
+
GROUP_CONCAT(DISTINCT status) AS status_codes
|
|
143
|
+
FROM api_learnings
|
|
144
|
+
WHERE project_id = ? AND created_at >= datetime('now', '-' || ? || ' days')
|
|
145
|
+
GROUP BY endpoint
|
|
146
|
+
ORDER BY error_rate DESC, total_calls DESC
|
|
147
|
+
`).all(projectId, days);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Error patterns — most frequent errors with categories. */
|
|
151
|
+
export function getErrorPatterns(projectId) {
|
|
152
|
+
const d = getDb();
|
|
153
|
+
return d.prepare(`
|
|
154
|
+
SELECT
|
|
155
|
+
pattern,
|
|
156
|
+
category,
|
|
157
|
+
occurrence_count,
|
|
158
|
+
first_seen,
|
|
159
|
+
last_seen,
|
|
160
|
+
example_error,
|
|
161
|
+
example_test
|
|
162
|
+
FROM error_patterns
|
|
163
|
+
WHERE project_id = ?
|
|
164
|
+
ORDER BY occurrence_count DESC
|
|
165
|
+
`).all(projectId);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Test pass/fail trends over time — aggregated by day, or by hour when all data is from a single day. */
|
|
169
|
+
export function getTestTrends(projectId, days = 7) {
|
|
170
|
+
const d = getDb();
|
|
171
|
+
const projectClause = (projectId !== null && projectId !== undefined) ? 'project_id = ? AND' : '';
|
|
172
|
+
const params = (projectId !== null && projectId !== undefined) ? [projectId, days] : [days];
|
|
173
|
+
|
|
174
|
+
const daily = d.prepare(`
|
|
175
|
+
SELECT
|
|
176
|
+
DATE(created_at) AS date,
|
|
177
|
+
COUNT(*) AS total_tests,
|
|
178
|
+
SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) AS passed,
|
|
179
|
+
SUM(CASE WHEN success = 0 THEN 1 ELSE 0 END) AS failed,
|
|
180
|
+
ROUND(AVG(CASE WHEN success = 1 THEN 100.0 ELSE 0.0 END), 1) AS pass_rate,
|
|
181
|
+
ROUND(AVG(duration_ms)) AS avg_duration_ms,
|
|
182
|
+
SUM(flaky) AS flaky_count
|
|
183
|
+
FROM test_learnings
|
|
184
|
+
WHERE ${projectClause} created_at >= datetime('now', '-' || ? || ' days')
|
|
185
|
+
GROUP BY DATE(created_at)
|
|
186
|
+
ORDER BY date ASC
|
|
187
|
+
`).all(...params);
|
|
188
|
+
|
|
189
|
+
// If all data is from a single day, provide hourly breakdown instead
|
|
190
|
+
if (daily.length <= 1 && daily[0]?.total_tests > 1) {
|
|
191
|
+
const hourly = d.prepare(`
|
|
192
|
+
SELECT
|
|
193
|
+
strftime('%Y-%m-%d %H:00', created_at) AS date,
|
|
194
|
+
COUNT(*) AS total_tests,
|
|
195
|
+
SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) AS passed,
|
|
196
|
+
SUM(CASE WHEN success = 0 THEN 1 ELSE 0 END) AS failed,
|
|
197
|
+
ROUND(AVG(CASE WHEN success = 1 THEN 100.0 ELSE 0.0 END), 1) AS pass_rate,
|
|
198
|
+
ROUND(AVG(duration_ms)) AS avg_duration_ms,
|
|
199
|
+
SUM(flaky) AS flaky_count
|
|
200
|
+
FROM test_learnings
|
|
201
|
+
WHERE ${projectClause} created_at >= datetime('now', '-' || ? || ' days')
|
|
202
|
+
GROUP BY strftime('%Y-%m-%d %H', created_at)
|
|
203
|
+
ORDER BY date ASC
|
|
204
|
+
`).all(...params);
|
|
205
|
+
if (hourly.length > 1) {
|
|
206
|
+
return { granularity: 'hourly', data: hourly };
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return { granularity: 'daily', data: daily };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Contextual insights for the current run — identifies known flaky tests,
|
|
215
|
+
* new failures, recovered tests, and unstable selectors used.
|
|
216
|
+
*/
|
|
217
|
+
export function getRunInsights(projectId, report) {
|
|
218
|
+
const d = getDb();
|
|
219
|
+
const insights = [];
|
|
220
|
+
|
|
221
|
+
if (!report?.results) return insights;
|
|
222
|
+
|
|
223
|
+
for (const result of report.results) {
|
|
224
|
+
const history = d.prepare(`
|
|
225
|
+
SELECT
|
|
226
|
+
COUNT(*) AS total_runs,
|
|
227
|
+
SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) AS pass_count,
|
|
228
|
+
SUM(flaky) AS flaky_count,
|
|
229
|
+
ROUND(AVG(CASE WHEN success = 1 THEN 100.0 ELSE 0.0 END), 1) AS pass_rate
|
|
230
|
+
FROM test_learnings
|
|
231
|
+
WHERE project_id = ? AND test_name = ?
|
|
232
|
+
`).get(projectId, result.name);
|
|
233
|
+
|
|
234
|
+
if (!history || history.total_runs === 0) continue;
|
|
235
|
+
|
|
236
|
+
// Known flaky test
|
|
237
|
+
if (history.flaky_count > 0 && result.success) {
|
|
238
|
+
insights.push({
|
|
239
|
+
type: 'flaky',
|
|
240
|
+
test: result.name,
|
|
241
|
+
message: `Known flaky test (${history.flaky_count} flaky runs out of ${history.total_runs}). Passed this time.`,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// New failure (was passing, now fails)
|
|
246
|
+
if (!result.success && history.pass_rate > 80) {
|
|
247
|
+
insights.push({
|
|
248
|
+
type: 'new-failure',
|
|
249
|
+
test: result.name,
|
|
250
|
+
message: `New failure — this test had ${history.pass_rate}% pass rate over ${history.total_runs} runs.`,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Recovered (was failing, now passes)
|
|
255
|
+
if (result.success && history.pass_rate < 50 && history.total_runs >= 3) {
|
|
256
|
+
insights.push({
|
|
257
|
+
type: 'recovered',
|
|
258
|
+
test: result.name,
|
|
259
|
+
message: `Recovered — was failing (${history.pass_rate}% pass rate over ${history.total_runs} runs).`,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Check for unstable selectors used in this run
|
|
265
|
+
const selectorStats = d.prepare(`
|
|
266
|
+
SELECT selector, ROUND(AVG(CASE WHEN success = 0 THEN 100.0 ELSE 0.0 END), 1) AS fail_rate
|
|
267
|
+
FROM selector_learnings
|
|
268
|
+
WHERE project_id = ?
|
|
269
|
+
GROUP BY selector
|
|
270
|
+
HAVING fail_rate > 20
|
|
271
|
+
`).all(projectId);
|
|
272
|
+
|
|
273
|
+
if (selectorStats.length > 0) {
|
|
274
|
+
const selectorSet = new Set(selectorStats.map(s => s.selector));
|
|
275
|
+
const usedUnstable = [];
|
|
276
|
+
|
|
277
|
+
for (const result of report.results) {
|
|
278
|
+
if (!result.actions) continue;
|
|
279
|
+
for (const action of result.actions) {
|
|
280
|
+
if (action.selector && selectorSet.has(action.selector)) {
|
|
281
|
+
usedUnstable.push(action.selector);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const unique = [...new Set(usedUnstable)];
|
|
287
|
+
if (unique.length > 0) {
|
|
288
|
+
insights.push({
|
|
289
|
+
type: 'unstable-selectors',
|
|
290
|
+
selectors: unique.slice(0, 5),
|
|
291
|
+
message: `${unique.length} unstable selector(s) used in this run: ${unique.slice(0, 3).join(', ')}${unique.length > 3 ? '...' : ''}`,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return insights;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Drill-down: history for a specific test. */
|
|
300
|
+
export function getTestHistory(projectId, testName, days = 30) {
|
|
301
|
+
const d = getDb();
|
|
302
|
+
return d.prepare(`
|
|
303
|
+
SELECT
|
|
304
|
+
tl.test_name,
|
|
305
|
+
tl.success,
|
|
306
|
+
tl.duration_ms,
|
|
307
|
+
tl.flaky,
|
|
308
|
+
tl.attempt,
|
|
309
|
+
tl.error_pattern,
|
|
310
|
+
tl.created_at,
|
|
311
|
+
r.run_id
|
|
312
|
+
FROM test_learnings tl
|
|
313
|
+
LEFT JOIN runs r ON r.id = tl.run_id
|
|
314
|
+
WHERE tl.project_id = ? AND tl.test_name = ? AND tl.created_at >= datetime('now', '-' || ? || ' days')
|
|
315
|
+
ORDER BY tl.created_at DESC
|
|
316
|
+
`).all(projectId, testName, days);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** Drill-down: history for a specific page. */
|
|
320
|
+
export function getPageHistory(projectId, urlPath, days = 30) {
|
|
321
|
+
const d = getDb();
|
|
322
|
+
return d.prepare(`
|
|
323
|
+
SELECT
|
|
324
|
+
url_path,
|
|
325
|
+
success,
|
|
326
|
+
load_time_ms,
|
|
327
|
+
console_errors,
|
|
328
|
+
console_warns,
|
|
329
|
+
network_errors,
|
|
330
|
+
test_name,
|
|
331
|
+
created_at
|
|
332
|
+
FROM page_learnings
|
|
333
|
+
WHERE project_id = ? AND url_path = ? AND created_at >= datetime('now', '-' || ? || ' days')
|
|
334
|
+
ORDER BY created_at DESC
|
|
335
|
+
`).all(projectId, urlPath, days);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** Drill-down: history for a specific selector. */
|
|
339
|
+
export function getSelectorHistory(projectId, selector, days = 30) {
|
|
340
|
+
const d = getDb();
|
|
341
|
+
return d.prepare(`
|
|
342
|
+
SELECT
|
|
343
|
+
selector,
|
|
344
|
+
action_type,
|
|
345
|
+
success,
|
|
346
|
+
page_url,
|
|
347
|
+
test_name,
|
|
348
|
+
error,
|
|
349
|
+
created_at
|
|
350
|
+
FROM selector_learnings
|
|
351
|
+
WHERE project_id = ? AND selector = ? AND created_at >= datetime('now', '-' || ? || ' days')
|
|
352
|
+
ORDER BY created_at DESC
|
|
353
|
+
`).all(projectId, selector, days);
|
|
354
|
+
}
|