browsecraft-runner 0.4.0 → 0.5.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/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # browsecraft-runner
2
2
 
3
- Test runner and CLI for [Browsecraft](https://github.com/rik9564/browsecraft).
3
+ Test runner, parallel scheduler, and multi-browser worker pool for [Browsecraft](https://github.com/rik9564/browsecraft).
4
4
 
5
- Discovers test files, coordinates execution, and reports results. Used internally by the `browsecraft` CLI.
5
+ Provides an event-driven architecture for distributing test scenarios across browser instances with work-stealing scheduling, failure classification, smart retry, and rich result aggregation.
6
6
 
7
7
  Most users should install [`browsecraft`](https://www.npmjs.com/package/browsecraft) instead — it includes the runner automatically.
8
8
 
@@ -12,53 +12,113 @@ Most users should install [`browsecraft`](https://www.npmjs.com/package/browsecr
12
12
  npm install browsecraft-runner
13
13
  ```
14
14
 
15
- ## Usage
15
+ ## Core Components
16
+
17
+ ### EventBus
18
+
19
+ Decouples execution from reporting. Subscribe to lifecycle events:
20
+
21
+ ```js
22
+ import { EventBus } from 'browsecraft-runner';
23
+
24
+ const bus = new EventBus();
25
+
26
+ bus.on('item:pass', ({ item, worker, duration }) => {
27
+ console.log(`✓ ${item.title} on ${worker.browser} (${duration}ms)`);
28
+ });
29
+
30
+ bus.on('item:fail', ({ item, error }) => {
31
+ console.log(`✗ ${item.title}: ${error.message}`);
32
+ });
33
+ ```
34
+
35
+ Events: `run:start/end`, `worker:spawn/ready/busy/idle/error/terminate`, `item:enqueue/start/pass/fail/skip/retry/end`, `browser:start/end`, `progress`.
36
+
37
+ ### WorkerPool
38
+
39
+ Manages browser instances across multiple browsers:
40
+
41
+ ```js
42
+ import { WorkerPool } from 'browsecraft-runner';
43
+
44
+ const pool = new WorkerPool(bus, {
45
+ browsers: { chrome: 2, firefox: 1, edge: 1 },
46
+ maxRetries: 1,
47
+ bail: false,
48
+ });
49
+
50
+ await pool.spawn(async (worker) => {
51
+ const session = await launchBrowser(worker.browser);
52
+ return { close: () => session.close() };
53
+ });
54
+ ```
55
+
56
+ ### Scheduler
57
+
58
+ Three execution strategies:
59
+
60
+ | Strategy | How it works |
61
+ |----------|-------------|
62
+ | `parallel` | Distribute scenarios across all browsers simultaneously |
63
+ | `sequential` | One browser at a time |
64
+ | `matrix` | Every scenario × every browser |
65
+
66
+ ```js
67
+ import { Scheduler } from 'browsecraft-runner';
68
+
69
+ const scheduler = new Scheduler(bus, pool, { strategy: 'matrix' });
70
+ const result = await scheduler.run(scenarios, executor);
71
+ ```
72
+
73
+ ### ResultAggregator
74
+
75
+ Produces scenario × browser matrices with analytics:
76
+
77
+ ```js
78
+ import { ResultAggregator } from 'browsecraft-runner';
79
+
80
+ const aggregator = new ResultAggregator();
81
+ const summary = aggregator.aggregate(result);
82
+
83
+ console.log(aggregator.formatMatrix(summary));
84
+ console.log(aggregator.formatSummary(summary));
85
+ ```
86
+
87
+ Includes flaky test detection, cross-browser inconsistency analysis, and timing statistics (min, max, avg, median, p95).
88
+
89
+ ### Failure Classification & Smart Retry
90
+
91
+ ```js
92
+ import { classifyFailure } from 'browsecraft-runner';
93
+
94
+ const classification = classifyFailure(error);
95
+ // { category: 'network', retryable: true, name: 'ECONNRESET' }
96
+ ```
97
+
98
+ | Category | Retryable | Examples |
99
+ |----------|-----------|----------|
100
+ | `network` | Yes | `ECONNRESET`, `ECONNREFUSED`, socket timeouts |
101
+ | `timeout` | Yes | Navigation timeouts, page load timeouts |
102
+ | `element` | Conditional | Not found (retryable), disabled (not retryable) |
103
+ | `assertion` | No | `Expected "foo" but got "bar"` |
104
+ | `script` | No | `TypeError`, `ReferenceError` |
105
+
106
+ ### TestRunner
107
+
108
+ Simple test file runner with grep, bail, and retry support:
16
109
 
17
110
  ```js
18
111
  import { TestRunner } from 'browsecraft-runner';
19
112
 
20
113
  const runner = new TestRunner({
21
- config: {
22
- browser: 'chrome',
23
- headless: true,
24
- timeout: 30000,
25
- retries: 0,
26
- testMatch: ['**/*.test.mjs'],
27
- outputDir: 'test-results',
28
- },
29
- grep: 'login', // optional: filter tests by name
30
- bail: false, // optional: stop on first failure
114
+ config: { browser: 'chrome', headless: true, timeout: 30000 },
115
+ grep: 'login',
116
+ bail: false,
31
117
  });
32
118
 
33
119
  const exitCode = await runner.run(loadFile, executeTest);
34
120
  ```
35
121
 
36
- ## Features
37
-
38
- - Test file discovery by glob patterns
39
- - Grep filtering by test name
40
- - `.only` support for focused tests
41
- - Retry on failure
42
- - Bail on first failure
43
- - Color-coded console reporter
44
-
45
- ## Configuration
46
-
47
- The runner accepts a `BrowsecraftConfig` object:
48
-
49
- | Option | Type | Default | Description |
50
- |--------|------|---------|-------------|
51
- | `browser` | `string` | `'chrome'` | Browser to use (`chrome`, `firefox`, `edge`) |
52
- | `headless` | `boolean` | `true` | Run in headless mode |
53
- | `timeout` | `number` | `30000` | Test timeout in ms |
54
- | `retries` | `number` | `0` | Retry failed tests |
55
- | `testMatch` | `string[]` | `['**/*.test.*']` | Glob patterns for test files |
56
- | `outputDir` | `string` | `'test-results'` | Output directory for artifacts |
57
- | `viewport` | `object` | `{ width: 1280, height: 720 }` | Browser viewport size |
58
- | `maximized` | `boolean` | `false` | Maximize the browser window |
59
- | `baseURL` | `string` | — | Base URL for `page.goto()` |
60
- | `workers` | `number` | `1` | Number of parallel workers |
61
-
62
122
  ## License
63
123
 
64
124
  [MIT](LICENSE)
package/dist/index.cjs CHANGED
@@ -3,6 +3,81 @@
3
3
  var fs = require('fs');
4
4
  var path = require('path');
5
5
 
6
+ // src/runner.ts
7
+
8
+ // src/smart-retry.ts
9
+ function classifyFailure(error) {
10
+ if (!(error instanceof Error)) {
11
+ return { category: "unknown", retryable: true, description: "Non-Error thrown" };
12
+ }
13
+ const name = error.name ?? "";
14
+ const msg = (error.message ?? "").toLowerCase();
15
+ if (name === "AssertionError" || name === "AssertionError [ERR_ASSERTION]" || name === "ERR_ASSERTION" || error.constructor?.name === "AssertionError") {
16
+ return {
17
+ category: "assertion",
18
+ retryable: false,
19
+ description: "Assertion failed \u2014 retrying won't help"
20
+ };
21
+ }
22
+ if (msg.includes("expected") && (msg.includes("to equal") || msg.includes("to be") || msg.includes("to have") || msg.includes("to match") || msg.includes("to contain") || msg.includes("but got") || msg.includes("but received"))) {
23
+ return {
24
+ category: "assertion",
25
+ retryable: false,
26
+ description: "Assertion failed \u2014 retrying won't help"
27
+ };
28
+ }
29
+ if (name === "SyntaxError" || name === "ReferenceError" || name === "TypeError" || name === "RangeError") {
30
+ return {
31
+ category: "script",
32
+ retryable: false,
33
+ description: `${name} \u2014 code bug, retrying won't help`
34
+ };
35
+ }
36
+ if (name === "ElementNotFoundError") {
37
+ return {
38
+ category: "element",
39
+ retryable: true,
40
+ description: "Element not found \u2014 page may still be loading"
41
+ };
42
+ }
43
+ if (name === "ElementNotActionableError") {
44
+ return {
45
+ category: "actionability",
46
+ retryable: true,
47
+ description: "Element not actionable \u2014 may become ready"
48
+ };
49
+ }
50
+ if (name === "NetworkError") {
51
+ return {
52
+ category: "network",
53
+ retryable: true,
54
+ description: "Network failure \u2014 may be transient"
55
+ };
56
+ }
57
+ if (name === "TimeoutError") {
58
+ return {
59
+ category: "timeout",
60
+ retryable: true,
61
+ description: "Timed out \u2014 environment may be slow"
62
+ };
63
+ }
64
+ if (msg.includes("timed out") || msg.includes("timeout")) {
65
+ return {
66
+ category: "timeout",
67
+ retryable: true,
68
+ description: "Timed out \u2014 environment may be slow"
69
+ };
70
+ }
71
+ if (msg.includes("econnrefused") || msg.includes("econnreset") || msg.includes("enotfound") || msg.includes("fetch failed") || msg.includes("network")) {
72
+ return {
73
+ category: "network",
74
+ retryable: true,
75
+ description: "Network failure \u2014 may be transient"
76
+ };
77
+ }
78
+ return { category: "unknown", retryable: true, description: "Unknown error \u2014 will retry" };
79
+ }
80
+
6
81
  // src/runner.ts
7
82
  var TestRunner = class {
8
83
  options;
@@ -45,6 +120,10 @@ var TestRunner = class {
45
120
  const maxRetries = test.options.retries ?? this.options.config.retries;
46
121
  let retryCount = 0;
47
122
  while (result.status === "failed" && retryCount < maxRetries) {
123
+ const classification = classifyFailure(result.error);
124
+ if (!classification.retryable) {
125
+ break;
126
+ }
48
127
  retryCount++;
49
128
  result = await executeTest(test);
50
129
  }
@@ -467,24 +546,27 @@ var WorkerPool = class {
467
546
  error: execResult.error
468
547
  };
469
548
  if (execResult.status === "failed" && this.config.maxRetries > 0) {
470
- let attempt = 1;
471
- while (attempt <= this.config.maxRetries && finalResult.status === "failed") {
472
- this.bus.emit("item:retry", {
473
- item,
474
- worker: worker.info,
475
- attempt,
476
- maxRetries: this.config.maxRetries
477
- });
478
- const retryResult = await executor(item, worker.info);
479
- finalResult = {
480
- item,
481
- worker: worker.info,
482
- status: retryResult.status,
483
- duration: retryResult.duration,
484
- error: retryResult.error,
485
- retries: attempt
486
- };
487
- attempt++;
549
+ const classification = classifyFailure(execResult.error);
550
+ if (classification.retryable) {
551
+ let attempt = 1;
552
+ while (attempt <= this.config.maxRetries && finalResult.status === "failed") {
553
+ this.bus.emit("item:retry", {
554
+ item,
555
+ worker: worker.info,
556
+ attempt,
557
+ maxRetries: this.config.maxRetries
558
+ });
559
+ const retryResult = await executor(item, worker.info);
560
+ finalResult = {
561
+ item,
562
+ worker: worker.info,
563
+ status: retryResult.status,
564
+ duration: retryResult.duration,
565
+ error: retryResult.error,
566
+ retries: attempt
567
+ };
568
+ attempt++;
569
+ }
488
570
  }
489
571
  }
490
572
  } catch (err) {
@@ -985,5 +1067,6 @@ exports.ResultAggregator = ResultAggregator;
985
1067
  exports.Scheduler = Scheduler;
986
1068
  exports.TestRunner = TestRunner;
987
1069
  exports.WorkerPool = WorkerPool;
1070
+ exports.classifyFailure = classifyFailure;
988
1071
  //# sourceMappingURL=index.cjs.map
989
1072
  //# sourceMappingURL=index.cjs.map