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 +99 -39
- package/dist/index.cjs +101 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +17 -2
- package/dist/index.d.ts +17 -2
- package/dist/index.js +101 -19
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# browsecraft-runner
|
|
2
2
|
|
|
3
|
-
Test runner and
|
|
3
|
+
Test runner, parallel scheduler, and multi-browser worker pool for [Browsecraft](https://github.com/rik9564/browsecraft).
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
23
|
-
|
|
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
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
item,
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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
|