@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
@@ -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
+ }