@rettangoli/vt 0.0.14 → 1.0.0-rc12
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 +164 -64
- package/package.json +9 -2
- package/src/capture/capture-scheduler.js +206 -0
- package/src/capture/playwright-runner.js +405 -0
- package/src/capture/result-collector.js +234 -0
- package/src/capture/screenshot-naming.js +13 -0
- package/src/capture/spec-loader.js +119 -0
- package/src/capture/worker-plan.js +66 -0
- package/src/cli/generate-options.js +94 -0
- package/src/cli/generate.js +118 -28
- package/src/cli/index.js +3 -1
- package/src/cli/report-options.js +43 -0
- package/src/cli/report.js +88 -151
- package/src/cli/screenshot.js +8 -0
- package/src/cli/service-runtime.js +116 -0
- package/src/cli/templates/default.html +5 -3
- package/src/cli/templates/index.html +9 -7
- package/src/cli/templates/report.html +8 -6
- package/src/common.js +124 -185
- package/src/createSteps.js +810 -44
- package/src/report/report-model.js +76 -0
- package/src/report/report-render.js +22 -0
- package/src/section-page-key.js +14 -0
- package/src/selector-filter.js +140 -0
- package/src/step-commands.js +37 -0
- package/src/validation.js +636 -0
- package/src/viewport.js +99 -0
- package/docker/Dockerfile +0 -21
- package/docker/build-and-push.sh +0 -16
package/README.md
CHANGED
|
@@ -1,98 +1,198 @@
|
|
|
1
|
-
|
|
2
1
|
# Rettangoli Visual Testing
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
Visual regression testing for Rettangoli specs using Playwright screenshots.
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
## Commands
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
- `rtgl vt generate`
|
|
8
|
+
- `rtgl vt screenshot`
|
|
9
|
+
- `rtgl vt report`
|
|
10
|
+
- `rtgl vt accept`
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
- **Visual Comparison** - Compare screenshots to detect visual changes
|
|
12
|
-
- **Test Reports** - Generate detailed reports with diff highlights
|
|
13
|
-
- **Playwright Integration** - Uses Playwright for reliable cross-browser testing
|
|
14
|
-
- **Template System** - Liquid templates for flexible HTML generation
|
|
15
|
-
- **Configuration** - YAML-based configuration for easy customization
|
|
12
|
+
Behavior split:
|
|
16
13
|
|
|
17
|
-
|
|
14
|
+
- `generate` builds candidate HTML only (no Playwright capture)
|
|
15
|
+
- `screenshot` runs generate flow and captures candidate screenshots
|
|
16
|
+
- `report` compares existing artifacts only (does not run generate/screenshot)
|
|
18
17
|
|
|
19
|
-
|
|
18
|
+
## Public Screenshot Options
|
|
20
19
|
|
|
21
|
-
-
|
|
22
|
-
-
|
|
20
|
+
- `--headed`
|
|
21
|
+
- `--concurrency <number>`
|
|
22
|
+
- `--timeout <ms>`
|
|
23
|
+
- `--wait-event <name>`
|
|
24
|
+
- `--folder <path>` (repeatable)
|
|
25
|
+
- `--group <section-key>` (repeatable)
|
|
26
|
+
- `--item <spec-path>` (repeatable)
|
|
23
27
|
|
|
24
|
-
|
|
28
|
+
## Public Report Options
|
|
25
29
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
- `--compare-method <method>`
|
|
31
|
+
- `--color-threshold <number>`
|
|
32
|
+
- `--diff-threshold <number>`
|
|
33
|
+
- `--folder <path>` (repeatable)
|
|
34
|
+
- `--group <section-key>` (repeatable)
|
|
35
|
+
- `--item <spec-path>` (repeatable)
|
|
36
|
+
|
|
37
|
+
## Scoped Runs
|
|
38
|
+
|
|
39
|
+
Use selectors to run only part of VT in both `screenshot` and `report`:
|
|
40
|
+
|
|
41
|
+
- `folder`: matches specs by folder prefix under `vt/specs` (example: `components/forms`)
|
|
42
|
+
- `group`: matches derived section page key from `vt.sections` titles (`kebab-case(title)`)
|
|
43
|
+
- `item`: matches a single spec path relative to `vt/specs` (with or without extension)
|
|
44
|
+
|
|
45
|
+
Selector rules:
|
|
46
|
+
|
|
47
|
+
- selectors are unioned (OR); any matched item is included
|
|
48
|
+
- if no selector is provided, all items are included
|
|
49
|
+
|
|
50
|
+
Examples:
|
|
30
51
|
|
|
31
|
-
2. **Install Playwright browsers** (if not already installed):
|
|
32
52
|
```bash
|
|
33
|
-
|
|
53
|
+
# Only specs under a folder
|
|
54
|
+
rtgl vt screenshot --folder components/forms
|
|
55
|
+
|
|
56
|
+
# Only one section/group key from vt.sections
|
|
57
|
+
rtgl vt screenshot --group components-basic
|
|
58
|
+
|
|
59
|
+
# Only one spec item (extension optional)
|
|
60
|
+
rtgl vt screenshot --item components/forms/login
|
|
61
|
+
rtgl vt screenshot --item components/forms/login.html
|
|
62
|
+
|
|
63
|
+
# Combine selectors (union)
|
|
64
|
+
rtgl vt screenshot --group components-basic --item pages/home
|
|
65
|
+
|
|
66
|
+
# Same selectors for report
|
|
67
|
+
rtgl vt report --folder components/forms
|
|
68
|
+
rtgl vt report --group components-basic
|
|
69
|
+
rtgl vt report --item components/forms/login
|
|
34
70
|
```
|
|
35
71
|
|
|
36
|
-
|
|
72
|
+
Everything else in capture is internal and intentionally not user-configurable.
|
|
73
|
+
|
|
74
|
+
## Config
|
|
37
75
|
|
|
76
|
+
`rettangoli.config.yaml`:
|
|
77
|
+
|
|
78
|
+
```yaml
|
|
79
|
+
vt:
|
|
80
|
+
path: ./vt
|
|
81
|
+
port: 3001
|
|
82
|
+
url: http://127.0.0.1:4173
|
|
83
|
+
service:
|
|
84
|
+
start: bun run preview
|
|
85
|
+
concurrency: 4
|
|
86
|
+
timeout: 30000
|
|
87
|
+
waitEvent: vt:ready
|
|
88
|
+
viewport:
|
|
89
|
+
id: desktop
|
|
90
|
+
width: 1280
|
|
91
|
+
height: 720
|
|
92
|
+
sections:
|
|
93
|
+
- title: Components Basic
|
|
94
|
+
files: components
|
|
38
95
|
```
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
96
|
+
|
|
97
|
+
Notes:
|
|
98
|
+
|
|
99
|
+
- `vt.sections` is required.
|
|
100
|
+
- `vt.service` is optional. When set, VT starts the command before capture, waits for `vt.url`, then stops it after capture.
|
|
101
|
+
- when `vt.service` is omitted and `vt.url` is set, VT expects that URL to already be running.
|
|
102
|
+
- Section page keys are derived as `kebab-case(title)` for flat sections and group `items[].title`.
|
|
103
|
+
- Derived section page keys must be unique case-insensitively.
|
|
104
|
+
- `vt.viewport` supports object or array; each viewport requires `id`, `width`, `height`.
|
|
105
|
+
- `vt.capture` is internal and must be omitted.
|
|
106
|
+
- Viewport contract details: `docs/viewport-contract.md`.
|
|
107
|
+
|
|
108
|
+
## Spec Frontmatter
|
|
109
|
+
|
|
110
|
+
Supported frontmatter keys per spec file:
|
|
111
|
+
|
|
112
|
+
- `title`
|
|
113
|
+
- `description`
|
|
114
|
+
- `template`
|
|
115
|
+
- `url`
|
|
116
|
+
- `waitEvent`
|
|
117
|
+
- `waitSelector`
|
|
118
|
+
- `waitStrategy` (`networkidle` | `load` | `event` | `selector`)
|
|
119
|
+
- `viewport` (object or array of viewport objects)
|
|
120
|
+
- `skipScreenshot`
|
|
121
|
+
- `skipInitialScreenshot`
|
|
122
|
+
- `specs`
|
|
123
|
+
- `steps`
|
|
124
|
+
|
|
125
|
+
Step action reference:
|
|
126
|
+
|
|
127
|
+
- `docs/step-actions.md`
|
|
128
|
+
- canonical format is structured action objects (`- action: ...`); legacy one-line string steps are not supported.
|
|
129
|
+
- `assert` supports `js` deep-equal checks for object/array values.
|
|
130
|
+
|
|
131
|
+
Screenshot naming:
|
|
132
|
+
|
|
133
|
+
- By default, VT takes an immediate first screenshot before running `steps`.
|
|
134
|
+
- Set `skipInitialScreenshot: true` in frontmatter to skip that immediate first screenshot.
|
|
135
|
+
- First captured screenshot is `-01`.
|
|
136
|
+
- Then `-02`, `-03`, up to `-99`.
|
|
137
|
+
- When viewport id is configured, filenames include `--<viewportId>` before ordinal (for example `pages/home--mobile-01.webp`).
|
|
138
|
+
|
|
139
|
+
## Docker
|
|
140
|
+
|
|
141
|
+
A pre-built Docker image with `rtgl` and Playwright browsers is available:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
docker pull han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.0.0-rc12
|
|
48
145
|
```
|
|
49
146
|
|
|
50
|
-
|
|
147
|
+
Run commands against a local project:
|
|
51
148
|
|
|
52
|
-
|
|
149
|
+
```bash
|
|
150
|
+
docker run --rm -v "$(pwd):/workspace" han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.0.0-rc12 rtgl vt screenshot
|
|
151
|
+
docker run --rm -v "$(pwd):/workspace" han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.0.0-rc12 rtgl vt report
|
|
152
|
+
docker run --rm -v "$(pwd):/workspace" han4wluc/rtgl:playwright-v1.57.0-rtgl-v1.0.0-rc12 rtgl vt accept
|
|
153
|
+
```
|
|
53
154
|
|
|
54
|
-
|
|
55
|
-
- Reads HTML specifications from `vt/specs/` directory
|
|
56
|
-
- Generates screenshots using Playwright
|
|
57
|
-
- Saves candidate screenshots for comparison
|
|
58
|
-
- Creates a static site for viewing results
|
|
155
|
+
Note:
|
|
59
156
|
|
|
60
|
-
|
|
61
|
-
-
|
|
62
|
-
- Generates visual diff reports highlighting changes
|
|
63
|
-
- Creates an HTML report with before/after comparisons
|
|
64
|
-
- Uses `looks-same` library for pixel-perfect comparison
|
|
157
|
+
- Image default working directory is `/workspace`.
|
|
158
|
+
- Use `-w /workspace/<subdir>` only when running commands from a subfolder within the mounted project.
|
|
65
159
|
|
|
66
|
-
|
|
67
|
-
- Accepts candidate screenshots as new reference images
|
|
68
|
-
- Updates the golden/reference screenshot directory
|
|
69
|
-
- Used when visual changes are intentional
|
|
160
|
+
Supports `linux/amd64` and `linux/arm64`.
|
|
70
161
|
|
|
71
|
-
|
|
162
|
+
## Development
|
|
72
163
|
|
|
73
|
-
|
|
164
|
+
Run unit tests:
|
|
74
165
|
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
port: 3001
|
|
78
|
-
screenshotWaitTime: 500
|
|
79
|
-
skipScreenshots: false
|
|
166
|
+
```bash
|
|
167
|
+
bun test
|
|
80
168
|
```
|
|
81
169
|
|
|
82
|
-
|
|
170
|
+
Default unit run behavior:
|
|
171
|
+
|
|
172
|
+
- `bun test` skips the real-browser smoke tests in `spec/e2e-smoke.spec.js` unless `VT_E2E=1`.
|
|
173
|
+
- Skipped smoke tests are:
|
|
174
|
+
- `runs generate, accept, and report with real screenshots`
|
|
175
|
+
- `supports waitEvent readiness with real browser screenshots`
|
|
176
|
+
- `supports managed service lifecycle with vt.service.start and vt.url`
|
|
83
177
|
|
|
84
|
-
|
|
178
|
+
Run real-browser smoke:
|
|
85
179
|
|
|
86
180
|
```bash
|
|
87
|
-
|
|
88
|
-
node ../rettangoli-cli/cli.js vt generate
|
|
89
|
-
node ../rettangoli-cli/cli.js vt report
|
|
90
|
-
node ../rettangoli-cli/cli.js vt accept
|
|
181
|
+
VT_E2E=1 bun test spec/e2e-smoke.spec.js
|
|
91
182
|
```
|
|
92
183
|
|
|
93
|
-
|
|
184
|
+
Run Docker E2E tests (requires Docker daemon running):
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
# Full pipeline: build test image → run all E2E scenarios
|
|
188
|
+
bun run test:e2e:full
|
|
189
|
+
|
|
190
|
+
# Scenarios only (skip image build, assumes image already exists)
|
|
191
|
+
bun run test:e2e
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Optional benchmark fixture:
|
|
195
|
+
|
|
94
196
|
```bash
|
|
95
|
-
|
|
96
|
-
rtgl vt report
|
|
97
|
-
rtgl vt accept
|
|
197
|
+
bun run bench:capture
|
|
98
198
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rettangoli/vt",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "1.0.0-rc12",
|
|
4
4
|
"description": "Rettangoli Visual Testing",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
"./cli": "./src/cli/index.js"
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
|
-
"test": "
|
|
12
|
+
"test": "vitest run --reporter=verbose",
|
|
13
|
+
"test:e2e": "node e2e/run.js",
|
|
14
|
+
"test:e2e:full": "bash scripts/docker-e2e-test.sh",
|
|
15
|
+
"bench:capture": "node ./scripts/benchmark-capture.js"
|
|
13
16
|
},
|
|
14
17
|
"dependencies": {
|
|
15
18
|
"commander": "^13.1.0",
|
|
@@ -19,5 +22,9 @@
|
|
|
19
22
|
"playwright": "1.57.0",
|
|
20
23
|
"sharp": "^0.34.5",
|
|
21
24
|
"shiki": "^3.3.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"puty": "^0.1.1",
|
|
28
|
+
"vitest": "^4.0.15"
|
|
22
29
|
}
|
|
23
30
|
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { chromium } from "playwright";
|
|
2
|
+
import { PlaywrightRunner } from "./playwright-runner.js";
|
|
3
|
+
import { ResultCollector } from "./result-collector.js";
|
|
4
|
+
import { resolveWorkerPlan } from "./worker-plan.js";
|
|
5
|
+
|
|
6
|
+
function nowMs() {
|
|
7
|
+
return performance.now();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function createTaskQueues(tasks) {
|
|
11
|
+
const freshQueue = tasks
|
|
12
|
+
.map((task, sourceOrder) => ({
|
|
13
|
+
task,
|
|
14
|
+
sourceOrder,
|
|
15
|
+
attempt: 1,
|
|
16
|
+
queueType: "fresh",
|
|
17
|
+
enqueuedAtMs: nowMs(),
|
|
18
|
+
}))
|
|
19
|
+
.sort((left, right) => {
|
|
20
|
+
const leftCost = left.task.estimatedCost ?? 0;
|
|
21
|
+
const rightCost = right.task.estimatedCost ?? 0;
|
|
22
|
+
if (leftCost !== rightCost) {
|
|
23
|
+
return leftCost - rightCost;
|
|
24
|
+
}
|
|
25
|
+
return right.sourceOrder - left.sourceOrder;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const retryQueue = [];
|
|
29
|
+
let dispatchCount = 0;
|
|
30
|
+
const fairRetryInterval = 4; // 3 fresh dispatches then 1 retry when available
|
|
31
|
+
|
|
32
|
+
const getNextTask = () => {
|
|
33
|
+
const shouldDispatchRetry = retryQueue.length > 0
|
|
34
|
+
&& (freshQueue.length === 0 || (dispatchCount % fairRetryInterval) === (fairRetryInterval - 1));
|
|
35
|
+
|
|
36
|
+
if (shouldDispatchRetry) {
|
|
37
|
+
dispatchCount += 1;
|
|
38
|
+
return retryQueue.shift();
|
|
39
|
+
}
|
|
40
|
+
if (freshQueue.length > 0) {
|
|
41
|
+
dispatchCount += 1;
|
|
42
|
+
return freshQueue.pop();
|
|
43
|
+
}
|
|
44
|
+
if (retryQueue.length > 0) {
|
|
45
|
+
dispatchCount += 1;
|
|
46
|
+
return retryQueue.shift();
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const enqueueRetry = (item) => {
|
|
52
|
+
retryQueue.push({
|
|
53
|
+
task: item.task,
|
|
54
|
+
attempt: item.attempt + 1,
|
|
55
|
+
queueType: "retry",
|
|
56
|
+
enqueuedAtMs: nowMs(),
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
getNextTask,
|
|
62
|
+
enqueueRetry,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function runCaptureScheduler(options) {
|
|
67
|
+
const {
|
|
68
|
+
tasks,
|
|
69
|
+
screenshotsDir,
|
|
70
|
+
workerCount: requestedWorkers,
|
|
71
|
+
isolationMode,
|
|
72
|
+
screenshotWaitTime,
|
|
73
|
+
waitEvent,
|
|
74
|
+
waitSelector,
|
|
75
|
+
waitStrategy,
|
|
76
|
+
navigationTimeout,
|
|
77
|
+
readyTimeout,
|
|
78
|
+
screenshotTimeout,
|
|
79
|
+
maxRetries,
|
|
80
|
+
recycleEvery,
|
|
81
|
+
metricsPath,
|
|
82
|
+
headless,
|
|
83
|
+
} = options;
|
|
84
|
+
|
|
85
|
+
const { workerCount, adaptivePolicy } = resolveWorkerPlan(requestedWorkers);
|
|
86
|
+
console.log(
|
|
87
|
+
`Capture scheduler: mode=${adaptivePolicy.mode}, workers=${workerCount}, isolation=${isolationMode}`,
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const collector = new ResultCollector({
|
|
91
|
+
totalTasks: tasks.length,
|
|
92
|
+
metricsPath,
|
|
93
|
+
workerCount,
|
|
94
|
+
isolationMode,
|
|
95
|
+
maxRetries,
|
|
96
|
+
adaptivePolicy,
|
|
97
|
+
schedulingPolicy: {
|
|
98
|
+
type: "duration-aware-fair-retry",
|
|
99
|
+
freshBeforeRetry: 3,
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
if (!tasks.length) {
|
|
104
|
+
const { summary } = collector.finalize();
|
|
105
|
+
return {
|
|
106
|
+
summary,
|
|
107
|
+
failures: [],
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const queue = createTaskQueues(tasks);
|
|
112
|
+
const getNextTask = queue.getNextTask;
|
|
113
|
+
|
|
114
|
+
const browser = await chromium.launch({ headless });
|
|
115
|
+
const runners = [];
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
for (let index = 0; index < workerCount; index += 1) {
|
|
119
|
+
const runner = new PlaywrightRunner({
|
|
120
|
+
workerId: index + 1,
|
|
121
|
+
browser,
|
|
122
|
+
screenshotsDir,
|
|
123
|
+
isolationMode,
|
|
124
|
+
screenshotWaitTime,
|
|
125
|
+
waitEvent,
|
|
126
|
+
waitSelector,
|
|
127
|
+
waitStrategy,
|
|
128
|
+
navigationTimeout,
|
|
129
|
+
readyTimeout,
|
|
130
|
+
screenshotTimeout,
|
|
131
|
+
});
|
|
132
|
+
await runner.initialize();
|
|
133
|
+
runners.push(runner);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const workerLoops = runners.map(async (runner) => {
|
|
137
|
+
let processedSinceRecycle = 0;
|
|
138
|
+
|
|
139
|
+
while (true) {
|
|
140
|
+
const item = getNextTask();
|
|
141
|
+
if (!item) {
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const queueWaitMs = Math.max(0, nowMs() - item.enqueuedAtMs);
|
|
146
|
+
const attemptStartMs = nowMs();
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const result = await runner.runTask(item.task, item.attempt);
|
|
150
|
+
const attemptMs = nowMs() - attemptStartMs;
|
|
151
|
+
collector.recordSuccess(item.task, result, {
|
|
152
|
+
workerId: runner.workerId,
|
|
153
|
+
queueType: item.queueType,
|
|
154
|
+
queueWaitMs,
|
|
155
|
+
attemptMs,
|
|
156
|
+
});
|
|
157
|
+
processedSinceRecycle += 1;
|
|
158
|
+
|
|
159
|
+
if (
|
|
160
|
+
isolationMode === "fast"
|
|
161
|
+
&& recycleEvery > 0
|
|
162
|
+
&& processedSinceRecycle >= recycleEvery
|
|
163
|
+
) {
|
|
164
|
+
await runner.recycleSharedContext();
|
|
165
|
+
collector.recordRecycle(runner.workerId, `recycleEvery=${recycleEvery}`);
|
|
166
|
+
processedSinceRecycle = 0;
|
|
167
|
+
}
|
|
168
|
+
} catch (error) {
|
|
169
|
+
const attemptMs = nowMs() - attemptStartMs;
|
|
170
|
+
if (item.attempt <= maxRetries) {
|
|
171
|
+
collector.recordRetry(item.task, item.attempt, error.message, {
|
|
172
|
+
workerId: runner.workerId,
|
|
173
|
+
queueType: item.queueType,
|
|
174
|
+
queueWaitMs,
|
|
175
|
+
attemptMs,
|
|
176
|
+
});
|
|
177
|
+
queue.enqueueRetry(item);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
collector.recordFailure(item.task, item.attempt, error.message, {
|
|
182
|
+
workerId: runner.workerId,
|
|
183
|
+
queueType: item.queueType,
|
|
184
|
+
queueWaitMs,
|
|
185
|
+
attemptMs,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
await Promise.all(workerLoops);
|
|
192
|
+
} finally {
|
|
193
|
+
await Promise.all(
|
|
194
|
+
runners.map(async (runner) => {
|
|
195
|
+
await runner.dispose();
|
|
196
|
+
}),
|
|
197
|
+
);
|
|
198
|
+
await browser.close();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const { summary, failures } = collector.finalize();
|
|
202
|
+
return {
|
|
203
|
+
summary,
|
|
204
|
+
failures,
|
|
205
|
+
};
|
|
206
|
+
}
|