@matware/e2e-runner 1.2.1 → 1.3.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 (88) hide show
  1. package/.claude-plugin/marketplace.json +52 -0
  2. package/.claude-plugin/plugin.json +17 -3
  3. package/.mcp.json +2 -2
  4. package/.opencode/commands/create-test.md +63 -0
  5. package/.opencode/commands/run.md +50 -0
  6. package/.opencode/commands/verify-issue.md +62 -0
  7. package/.opencode/skills/e2e-testing/SKILL.md +181 -0
  8. package/.opencode/skills/e2e-testing/references/action-types.md +143 -0
  9. package/.opencode/skills/e2e-testing/references/auth-strategies.md +91 -0
  10. package/.opencode/skills/e2e-testing/references/graphql.md +59 -0
  11. package/.opencode/skills/e2e-testing/references/issue-verification.md +59 -0
  12. package/.opencode/skills/e2e-testing/references/multi-pool.md +60 -0
  13. package/.opencode/skills/e2e-testing/references/network-debugging.md +62 -0
  14. package/.opencode/skills/e2e-testing/references/test-json-format.md +163 -0
  15. package/.opencode/skills/e2e-testing/references/troubleshooting.md +224 -0
  16. package/.opencode/skills/e2e-testing/references/variables.md +41 -0
  17. package/.opencode/skills/e2e-testing/references/visual-verification.md +89 -0
  18. package/LICENSE +190 -0
  19. package/OPENCODE.md +166 -0
  20. package/README.md +165 -104
  21. package/agents/test-creator.md +54 -1
  22. package/agents/test-improver.md +37 -0
  23. package/bin/cli.js +409 -16
  24. package/commands/capture.md +45 -0
  25. package/commands/create-test.md +16 -1
  26. package/opencode.json +11 -0
  27. package/package.json +7 -2
  28. package/scripts/setup-opencode.sh +113 -0
  29. package/skills/e2e-testing/SKILL.md +10 -3
  30. package/skills/e2e-testing/references/action-types.md +48 -5
  31. package/skills/e2e-testing/references/auth-strategies.md +91 -0
  32. package/skills/e2e-testing/references/graphql.md +59 -0
  33. package/skills/e2e-testing/references/issue-verification.md +59 -0
  34. package/skills/e2e-testing/references/multi-pool.md +60 -0
  35. package/skills/e2e-testing/references/network-debugging.md +62 -0
  36. package/skills/e2e-testing/references/test-json-format.md +4 -0
  37. package/skills/e2e-testing/references/troubleshooting.md +44 -2
  38. package/skills/e2e-testing/references/variables.md +41 -0
  39. package/skills/e2e-testing/references/visual-verification.md +89 -0
  40. package/src/actions.js +475 -2
  41. package/src/ai-generate.js +139 -8
  42. package/src/app-pool.js +339 -0
  43. package/src/config.js +266 -5
  44. package/src/dashboard.js +216 -17
  45. package/src/db.js +191 -7
  46. package/src/index.js +12 -9
  47. package/src/learner-sqlite.js +458 -0
  48. package/src/learner.js +78 -6
  49. package/src/mcp-tools.js +1348 -51
  50. package/src/module-resolver.js +37 -0
  51. package/src/narrate.js +65 -0
  52. package/src/pool-manager.js +229 -0
  53. package/src/pool.js +301 -31
  54. package/src/reporter.js +86 -2
  55. package/src/runner.js +480 -71
  56. package/src/sync/auth.js +354 -0
  57. package/src/sync/client.js +572 -0
  58. package/src/sync/hub-routes.js +816 -0
  59. package/src/sync/index.js +68 -0
  60. package/src/sync/middleware.js +347 -0
  61. package/src/sync/queue.js +209 -0
  62. package/src/sync/schema.js +540 -0
  63. package/src/verify.js +10 -7
  64. package/src/visual-diff.js +446 -0
  65. package/src/watch.js +384 -0
  66. package/templates/build-dashboard.js +47 -6
  67. package/templates/dashboard/js/api.js +62 -0
  68. package/templates/dashboard/js/init.js +13 -0
  69. package/templates/dashboard/js/keyboard.js +46 -0
  70. package/templates/dashboard/js/state.js +40 -0
  71. package/templates/dashboard/js/toast.js +41 -0
  72. package/templates/dashboard/js/utils.js +216 -0
  73. package/templates/dashboard/js/view-live.js +181 -0
  74. package/templates/dashboard/js/view-runs.js +676 -0
  75. package/templates/dashboard/js/view-tests.js +294 -0
  76. package/templates/dashboard/js/view-watch.js +242 -0
  77. package/templates/dashboard/js/websocket.js +116 -0
  78. package/templates/dashboard/styles/base.css +69 -0
  79. package/templates/dashboard/styles/components.css +117 -0
  80. package/templates/dashboard/styles/view-live.css +97 -0
  81. package/templates/dashboard/styles/view-runs.css +243 -0
  82. package/templates/dashboard/styles/view-tests.css +96 -0
  83. package/templates/dashboard/styles/view-watch.css +53 -0
  84. package/templates/dashboard/template.html +181 -100
  85. package/templates/dashboard.html +1614 -547
  86. package/templates/sample-test.json +0 -8
  87. package/templates/dashboard/app.js +0 -1152
  88. package/templates/dashboard/styles.css +0 -413
package/src/learner.js CHANGED
@@ -17,8 +17,12 @@ const ERROR_CATEGORIES = [
17
17
  { pattern: /waitForSelector/i, category: 'selector-not-found' },
18
18
  { pattern: /not visible/i, category: 'selector-not-found' },
19
19
  { pattern: /navigation/i, category: 'navigation-error' },
20
- { pattern: /net::ERR_/i, category: 'connection-refused' },
20
+ { pattern: /ERR_NAME_NOT_RESOLVED/i, category: 'dns-resolution' },
21
21
  { pattern: /ERR_CONNECTION_REFUSED/i, category: 'connection-refused' },
22
+ { pattern: /ECONNREFUSED/i, category: 'connection-refused' },
23
+ { pattern: /Chrome Pool unavailable/i, category: 'pool-unavailable' },
24
+ { pattern: /Failed to connect to pool/i, category: 'pool-connect-failed' },
25
+ { pattern: /net::ERR_/i, category: 'network-error' },
22
26
  { pattern: /assert_text/i, category: 'assert-text-failed' },
23
27
  { pattern: /assert_url/i, category: 'assert-url-failed' },
24
28
  { pattern: /assert_visible/i, category: 'assert-visible-failed' },
@@ -35,6 +39,18 @@ const ERROR_CATEGORIES = [
35
39
  { pattern: /evaluate.*ERROR/i, category: 'evaluate-error' },
36
40
  ];
37
41
 
42
+ /** Categories that indicate infrastructure failures — not test/app issues. */
43
+ export const INFRA_CATEGORIES = new Set([
44
+ 'connection-refused', 'dns-resolution', 'pool-unavailable', 'pool-connect-failed', 'network-error',
45
+ ]);
46
+
47
+ /** Returns true if the error is an infrastructure issue (pool down, DNS, connection refused). */
48
+ export function isInfraError(errorMsg) {
49
+ if (!errorMsg) return false;
50
+ const { category } = categorizeError(errorMsg);
51
+ return INFRA_CATEGORIES.has(category);
52
+ }
53
+
38
54
  export function categorizeError(errorMsg) {
39
55
  if (!errorMsg) return { category: 'unknown', pattern: 'unknown' };
40
56
 
@@ -204,6 +220,11 @@ export function learnFromRun(projectId, runDbId, report, config, suiteName) {
204
220
  VALUES (?, ?, ?, ?, ?, ?, ?, ?)
205
221
  `);
206
222
 
223
+ const insertActionHealth = d.prepare(`
224
+ INSERT INTO action_health (project_id, run_id, test_name, action_index, action_type, selector, success, duration_ms, console_errors_after, network_errors_after, page_url)
225
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
226
+ `);
227
+
207
228
  const upsertErrorPattern = d.prepare(`
208
229
  INSERT INTO error_patterns (project_id, pattern, category, occurrence_count, first_seen, last_seen, example_error, example_test)
209
230
  VALUES (?, ?, ?, 1, datetime('now'), datetime('now'), ?, ?)
@@ -214,23 +235,40 @@ export function learnFromRun(projectId, runDbId, report, config, suiteName) {
214
235
  example_test = excluded.example_test
215
236
  `);
216
237
 
238
+ let infraCount = 0;
239
+
217
240
  const tx = d.transaction(() => {
218
241
  for (const result of results) {
219
242
  const durationMs = (result.endTime && result.startTime)
220
243
  ? new Date(result.endTime) - new Date(result.startTime)
221
244
  : null;
222
- const isFlaky = result.success && (result.attempt || 1) > 1 ? 1 : 0;
245
+ const isFlaky = result.flaky ? 1 : (result.success && (result.attempt || 1) > 1 ? 1 : 0);
223
246
 
224
247
  // Categorize error
225
248
  let errorPattern = null;
249
+ let infraFailure = false;
226
250
  if (result.error) {
227
251
  const { category, pattern } = categorizeError(result.error);
228
252
  errorPattern = category;
253
+ infraFailure = INFRA_CATEGORIES.has(category);
229
254
 
230
- // Track error pattern
255
+ // Always track error patterns (even infra) for awareness
231
256
  upsertErrorPattern.run(projectId, pattern, category, result.error, result.name);
232
257
  }
233
258
 
259
+ if (infraFailure) {
260
+ infraCount++;
261
+ // Still write test_learnings so run counts are accurate,
262
+ // but skip selector/page/api learnings to avoid polluting metrics
263
+ insertTestLearning.run(
264
+ projectId, runDbId, result.name,
265
+ result.success ? 1 : 0, durationMs, isFlaky,
266
+ result.attempt || 1, result.maxAttempts || 1,
267
+ errorPattern
268
+ );
269
+ continue;
270
+ }
271
+
234
272
  // Test-level learning
235
273
  insertTestLearning.run(
236
274
  projectId, runDbId, result.name,
@@ -275,6 +313,33 @@ export function learnFromRun(projectId, runDbId, report, config, suiteName) {
275
313
  api.isError, result.name
276
314
  );
277
315
  }
316
+
317
+ // Action health — per-action metrics with collateral error estimation
318
+ if (result.actions?.length) {
319
+ const totalConsoleErrors = (result.consoleLogs || []).filter(l => l.type === 'error').length;
320
+ const totalNetworkErrors = (result.networkErrors || []).length;
321
+ const actionCount = result.actions.length;
322
+ let currentPage = '/';
323
+
324
+ for (let i = 0; i < actionCount; i++) {
325
+ const action = result.actions[i];
326
+ if (action.type === 'goto' || action.type === 'navigate') {
327
+ try { currentPage = new URL(action.value, 'http://placeholder').pathname; } catch { currentPage = action.value || '/'; }
328
+ }
329
+ // Estimate collateral errors: later actions inherit more errors (weighted distribution)
330
+ const weight = (i + 1) / actionCount;
331
+ const consoleAfter = action.success === false ? Math.round(totalConsoleErrors * weight) : 0;
332
+ const networkAfter = action.success === false ? Math.round(totalNetworkErrors * weight) : 0;
333
+
334
+ insertActionHealth.run(
335
+ projectId, runDbId, result.name, i,
336
+ action.type || 'unknown', action.selector || null,
337
+ action.success === false ? 0 : 1,
338
+ action.duration || null,
339
+ consoleAfter, networkAfter, currentPage
340
+ );
341
+ }
342
+ }
278
343
  }
279
344
  });
280
345
 
@@ -287,6 +352,8 @@ export function learnFromRun(projectId, runDbId, report, config, suiteName) {
287
352
  if (config?.learningsNeo4j) {
288
353
  writeToGraph(projectId, runDbId, report, config, suiteName).catch(() => {});
289
354
  }
355
+
356
+ return { infraCount };
290
357
  }
291
358
 
292
359
  // ── Summary cache ─────────────────────────────────────────────────────────────
@@ -335,8 +402,11 @@ function updateLearningSummary(projectId, config) {
335
402
  // Unstable selectors
336
403
  const unstableSelectors = d.prepare(`
337
404
  SELECT selector,
405
+ MAX(action_type) AS action_type,
338
406
  ROUND(AVG(CASE WHEN success = 0 THEN 100.0 ELSE 0.0 END), 1) AS fail_rate,
339
- COUNT(*) AS total_uses
407
+ COUNT(*) AS total_uses,
408
+ COUNT(DISTINCT test_name) AS used_by_tests,
409
+ MAX(page_url) AS page_url
340
410
  FROM selector_learnings
341
411
  WHERE project_id = ? AND created_at >= ${cutoff}
342
412
  GROUP BY selector
@@ -349,6 +419,7 @@ function updateLearningSummary(projectId, config) {
349
419
  const failingPages = d.prepare(`
350
420
  SELECT url_path,
351
421
  ROUND(AVG(CASE WHEN success = 0 THEN 100.0 ELSE 0.0 END), 1) AS fail_rate,
422
+ COUNT(*) AS total_visits,
352
423
  SUM(console_errors) AS console_errors,
353
424
  SUM(network_errors) AS network_errors
354
425
  FROM page_learnings
@@ -364,7 +435,8 @@ function updateLearningSummary(projectId, config) {
364
435
  SELECT endpoint,
365
436
  ROUND(AVG(CASE WHEN is_error = 1 THEN 100.0 ELSE 0.0 END), 1) AS error_rate,
366
437
  ROUND(AVG(duration_ms)) AS avg_duration_ms,
367
- COUNT(*) AS total_calls
438
+ COUNT(*) AS total_calls,
439
+ GROUP_CONCAT(DISTINCT status) AS status_codes
368
440
  FROM api_learnings
369
441
  WHERE project_id = ? AND created_at >= ${cutoff}
370
442
  GROUP BY endpoint
@@ -375,7 +447,7 @@ function updateLearningSummary(projectId, config) {
375
447
 
376
448
  // Top errors
377
449
  const topErrors = d.prepare(`
378
- SELECT pattern, category, occurrence_count, last_seen, example_error
450
+ SELECT pattern, category, occurrence_count, first_seen, last_seen, example_error AS example_test
379
451
  FROM error_patterns
380
452
  WHERE project_id = ?
381
453
  ORDER BY occurrence_count DESC