craftdriver 1.6.0 → 1.7.0
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/CHANGELOG.md +17 -0
- package/README.md +4 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/browser.d.ts +32 -0
- package/dist/lib/browser.d.ts.map +1 -1
- package/dist/lib/browser.js +56 -0
- package/dist/lib/browser.js.map +1 -1
- package/dist/lib/errors.d.ts +7 -0
- package/dist/lib/errors.d.ts.map +1 -1
- package/dist/lib/errors.js +7 -0
- package/dist/lib/errors.js.map +1 -1
- package/dist/lib/timing.d.ts +8 -0
- package/dist/lib/timing.d.ts.map +1 -1
- package/dist/lib/timing.js +8 -0
- package/dist/lib/timing.js.map +1 -1
- package/dist/lib/visual/antialias.d.ts +41 -0
- package/dist/lib/visual/antialias.d.ts.map +1 -0
- package/dist/lib/visual/antialias.js +126 -0
- package/dist/lib/visual/antialias.js.map +1 -0
- package/dist/lib/visual/codec.d.ts +24 -0
- package/dist/lib/visual/codec.d.ts.map +1 -0
- package/dist/lib/visual/codec.js +11 -0
- package/dist/lib/visual/codec.js.map +1 -0
- package/dist/lib/visual/compare.d.ts +38 -0
- package/dist/lib/visual/compare.d.ts.map +1 -0
- package/dist/lib/visual/compare.js +209 -0
- package/dist/lib/visual/compare.js.map +1 -0
- package/dist/lib/visual/diff.d.ts +18 -0
- package/dist/lib/visual/diff.d.ts.map +1 -0
- package/dist/lib/visual/diff.js +23 -0
- package/dist/lib/visual/diff.js.map +1 -0
- package/dist/lib/visual/expectScreenshot.d.ts +65 -0
- package/dist/lib/visual/expectScreenshot.d.ts.map +1 -0
- package/dist/lib/visual/expectScreenshot.js +287 -0
- package/dist/lib/visual/expectScreenshot.js.map +1 -0
- package/dist/lib/visual/index.d.ts +10 -0
- package/dist/lib/visual/index.d.ts.map +1 -0
- package/dist/lib/visual/index.js +9 -0
- package/dist/lib/visual/index.js.map +1 -0
- package/dist/lib/visual/pixelMath.d.ts +32 -0
- package/dist/lib/visual/pixelMath.d.ts.map +1 -0
- package/dist/lib/visual/pixelMath.js +70 -0
- package/dist/lib/visual/pixelMath.js.map +1 -0
- package/dist/lib/visual/pngHeader.d.ts +27 -0
- package/dist/lib/visual/pngHeader.d.ts.map +1 -0
- package/dist/lib/visual/pngHeader.js +63 -0
- package/dist/lib/visual/pngHeader.js.map +1 -0
- package/dist/lib/visual/pngjsCodec.d.ts +22 -0
- package/dist/lib/visual/pngjsCodec.d.ts.map +1 -0
- package/dist/lib/visual/pngjsCodec.js +70 -0
- package/dist/lib/visual/pngjsCodec.js.map +1 -0
- package/dist/lib/visual/types.d.ts +89 -0
- package/dist/lib/visual/types.d.ts.map +1 -0
- package/dist/lib/visual/types.js +20 -0
- package/dist/lib/visual/types.js.map +1 -0
- package/dist/lib/visual/visualError.d.ts +32 -0
- package/dist/lib/visual/visualError.d.ts.map +1 -0
- package/dist/lib/visual/visualError.js +59 -0
- package/dist/lib/visual/visualError.js.map +1 -0
- package/dist/lib/wait.d.ts.map +1 -1
- package/dist/lib/wait.js +7 -1
- package/dist/lib/wait.js.map +1 -1
- package/docs/api-reference.md +15 -1
- package/docs/error-codes.md +1 -0
- package/docs/screenshots.md +15 -6
- package/docs/visual-testing.md +254 -0
- package/docs/why-craftdriver.md +3 -2
- package/package.json +3 -1
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# Visual testing
|
|
2
|
+
|
|
3
|
+
`browser.expectScreenshot()` compares a fresh browser screenshot against a PNG
|
|
4
|
+
baseline in your repository, retrying until it matches or a timeout elapses. On
|
|
5
|
+
a mismatch it throws a typed error carrying the final screenshot and a generated
|
|
6
|
+
diff image so you can see exactly what changed.
|
|
7
|
+
|
|
8
|
+
Baselines are managed for you, so the first run of a new test just works:
|
|
9
|
+
|
|
10
|
+
- **No baseline yet** — the screenshot is captured until two consecutive frames
|
|
11
|
+
are identical (so a mid-animation frame isn't enshrined) or `timeout` elapses,
|
|
12
|
+
then written to the path you gave, and the assertion passes. Commit that PNG.
|
|
13
|
+
If the page never settled before the timeout, the write is an arbitrary frame
|
|
14
|
+
and the stderr line says so (`… page did not settle …`) — review it first.
|
|
15
|
+
- **Baseline matches** — passes.
|
|
16
|
+
- **Baseline differs** — throws `VisualMismatchError`. It is **never** rewritten
|
|
17
|
+
unless you explicitly ask (see [Updating baselines](#updating-baselines)).
|
|
18
|
+
|
|
19
|
+
Behaviour is identical on your laptop and in CI — there is no "CI mode" that
|
|
20
|
+
does something different. One tradeoff comes with the convenience: if a
|
|
21
|
+
committed baseline is *deleted*, the next run recreates it and passes. Every
|
|
22
|
+
create and update is printed to stderr (`[craftdriver] Created visual
|
|
23
|
+
baseline: …`) so it shows up in your logs, but the real guard is committing your
|
|
24
|
+
baselines and reviewing the diff whenever one changes.
|
|
25
|
+
|
|
26
|
+
## The shortest thing that works
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
// First run creates baselines/home.png; commit it. Every run after asserts
|
|
30
|
+
// against it — same call, no separate bootstrap step.
|
|
31
|
+
await browser.expectScreenshot('baselines/home.png', {
|
|
32
|
+
screenshot: { fullPage: true },
|
|
33
|
+
});
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`expectScreenshot()` returns a result object on success (`baseline`, `matches`,
|
|
37
|
+
`diffPixels`, `diffPercentage`, `attempts`, `elapsedMs`, and both images'
|
|
38
|
+
dimensions) and throws [`VisualMismatchError`](./error-codes.md) on failure.
|
|
39
|
+
`result.baseline` is `'created'`, `'matched'`, or `'updated'` — what happened to
|
|
40
|
+
the file on disk this run.
|
|
41
|
+
|
|
42
|
+
## Capture scope
|
|
43
|
+
|
|
44
|
+
`screenshot` chooses what to capture and is passed straight through to
|
|
45
|
+
`browser.screenshot()`. The three forms mirror that method and are mutually
|
|
46
|
+
exclusive:
|
|
47
|
+
|
|
48
|
+
| `screenshot` | Captures | Notes |
|
|
49
|
+
| ---------------------------- | ------------------------------- | -------------------------------------- |
|
|
50
|
+
| omitted | the visible viewport | Classic or BiDi. |
|
|
51
|
+
| `{ fullPage: true }` | the full scrollable document | Requires BiDi (`enableBiDi: true`, the default). |
|
|
52
|
+
| `{ selector: '#chart' }` | one element | Auto-waits for the element like `find()`. |
|
|
53
|
+
|
|
54
|
+
`fullPage` and `selector` together are rejected — by the TypeScript types and,
|
|
55
|
+
for JavaScript callers, at runtime with `INVALID_ARGUMENT`. Always baseline and
|
|
56
|
+
assert with the same scope; a full-page baseline compared against a viewport
|
|
57
|
+
capture is just a dimension mismatch.
|
|
58
|
+
|
|
59
|
+
## Thresholds
|
|
60
|
+
|
|
61
|
+
By default the comparison is exact: any differing pixel fails. Loosen it with
|
|
62
|
+
three independent policies.
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
await browser.expectScreenshot('baselines/home.png', {
|
|
66
|
+
pixelTolerance: 5, // per-channel RGB slack, 0..255 (default 0)
|
|
67
|
+
maxDiffPixels: 100, // allow up to 100 differing pixels
|
|
68
|
+
maxDiffPercentage: 0.1, // …and no more than 0.1% of all pixels
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
- **`pixelTolerance`** — a pixel counts as equal when the absolute difference of
|
|
73
|
+
every visible R, G and B channel is `<= pixelTolerance`. Use it to absorb tiny
|
|
74
|
+
codec/renderer noise.
|
|
75
|
+
- **`maxDiffPixels`** / **`maxDiffPercentage`** — budgets for how many pixels may
|
|
76
|
+
still differ after `pixelTolerance` is applied. Each boundary is **inclusive**.
|
|
77
|
+
- If you set **neither** budget, the image must have zero differing pixels.
|
|
78
|
+
- If you set **both**, **both** must pass.
|
|
79
|
+
- Dimensions are never resized to match. Different dimensions always fail,
|
|
80
|
+
regardless of tolerance — resizing would hide the layout regressions you are
|
|
81
|
+
trying to catch.
|
|
82
|
+
|
|
83
|
+
All values are validated, never clamped: a non-integer `pixelTolerance`, a
|
|
84
|
+
percentage above 100, an `interval` below 10 ms, and similar out-of-range inputs
|
|
85
|
+
throw `INVALID_ARGUMENT`.
|
|
86
|
+
|
|
87
|
+
### Ignoring anti-aliasing
|
|
88
|
+
|
|
89
|
+
Anti-aliasing along text and edges is the most common source of
|
|
90
|
+
platform-to-platform noise. `ignoreAntialiasing` classifies edge-smoothing
|
|
91
|
+
pixels and excludes them from the difference count (reporting them separately as
|
|
92
|
+
`ignoredAntialiasPixels`):
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
await browser.expectScreenshot('baselines/home.png', {
|
|
96
|
+
ignoreAntialiasing: true,
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
It is **off by default on purpose**. Silently ignoring pixels is a policy
|
|
101
|
+
decision that can hide thin, genuine regressions, so you opt in explicitly. It
|
|
102
|
+
ignores edge smoothing but still counts moved edges, missing glyphs, and
|
|
103
|
+
one-pixel layout shifts.
|
|
104
|
+
|
|
105
|
+
## Handling a mismatch
|
|
106
|
+
|
|
107
|
+
`VisualMismatchError` carries the two things you actually want when a visual
|
|
108
|
+
test fails: the final screenshot and a diff image. Persist them as CI artifacts:
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
import { writeFile } from 'node:fs/promises';
|
|
112
|
+
import { VisualMismatchError } from 'craftdriver';
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
await browser.expectScreenshot('baselines/home.png', { timeout: 3_000 });
|
|
116
|
+
} catch (error) {
|
|
117
|
+
if (error instanceof VisualMismatchError) {
|
|
118
|
+
await writeFile('artifacts/home.actual.png', error.actual);
|
|
119
|
+
await writeFile('artifacts/home.diff.png', error.diff);
|
|
120
|
+
}
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
In the diff image, unchanged areas appear as a dimmed grayscale copy of the
|
|
126
|
+
actual screenshot, counted differences are red, ignored anti-aliased pixels are
|
|
127
|
+
yellow, and regions present in only one image (a dimension mismatch) are
|
|
128
|
+
magenta. `error.detail` holds the JSON-safe summary (path, dimensions, counts,
|
|
129
|
+
percentage, attempts, elapsed time) for logs and agents; the two `Buffer`s live
|
|
130
|
+
on `error.actual` / `error.diff`.
|
|
131
|
+
|
|
132
|
+
Baseline read/decode failures are treated as configuration errors, not visual
|
|
133
|
+
mismatches — they are not retried, and their cause is preserved.
|
|
134
|
+
|
|
135
|
+
## Updating baselines
|
|
136
|
+
|
|
137
|
+
When a change is intentional, re-run with the update flag set and craftdriver
|
|
138
|
+
overwrites the differing baselines with the new screenshots instead of failing:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
CRAFTDRIVER_UPDATE_VISUAL_BASELINES=true npm test
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
There is no separate "save" API and no CLI flag — craftdriver has no test runner
|
|
145
|
+
of its own, so the switch is one environment variable you set for a single run.
|
|
146
|
+
While it is set:
|
|
147
|
+
|
|
148
|
+
- A **missing** baseline is still created and passes (that never needed the flag).
|
|
149
|
+
- A **matching** baseline passes untouched.
|
|
150
|
+
- A **differing** baseline is captured until the deadline, then overwritten with
|
|
151
|
+
the final screenshot; the assertion passes with `result.baseline === 'updated'`.
|
|
152
|
+
A page that settles back into a match passes *without* rewriting, so you don't
|
|
153
|
+
get spurious baseline churn.
|
|
154
|
+
|
|
155
|
+
Every rewrite is printed — `[craftdriver] Updated visual baseline: <path> (<n>
|
|
156
|
+
px, <p>% changed)`. Review the resulting diff in version control before you
|
|
157
|
+
commit it; that review is the safety net this flag deliberately leaves to you.
|
|
158
|
+
|
|
159
|
+
Rewrites are atomic: the new baseline is written to a temporary file and renamed
|
|
160
|
+
into place, so an interrupted run or disk error can never leave a truncated or
|
|
161
|
+
half-updated committed baseline — the existing one survives intact.
|
|
162
|
+
|
|
163
|
+
Parsing is strict, because the flag mutates files on disk: `true` turns it on,
|
|
164
|
+
`false` or unset leaves it off, and any other value (`1`, `yes`, …) throws
|
|
165
|
+
`INVALID_ARGUMENT` rather than being quietly ignored. A baseline that exists but
|
|
166
|
+
can't be decoded stays a hard error even here — it is never overwritten. To
|
|
167
|
+
replace a corrupt baseline, delete it and let the next run recreate it.
|
|
168
|
+
|
|
169
|
+
## Retries and timing
|
|
170
|
+
|
|
171
|
+
Each assertion reads the baseline once, then screenshots and compares on a loop
|
|
172
|
+
until it matches or `timeout` (default: the browser's default timeout, 5000 ms)
|
|
173
|
+
expires, waiting `interval` ms (default 50) between attempts. The first
|
|
174
|
+
screenshot is always taken immediately, even with `timeout: 0`. This absorbs a
|
|
175
|
+
page that is still settling — a chart finishing its layout, a font swapping in.
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
await browser.expectScreenshot('baselines/chart.png', {
|
|
179
|
+
screenshot: { selector: '#chart' },
|
|
180
|
+
timeout: 3_000,
|
|
181
|
+
interval: 100,
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
The happy path is cheap: an identical screenshot matches on a byte comparison
|
|
186
|
+
without ever decoding a PNG. But **byte equality is only a fast _positive_
|
|
187
|
+
path** — two genuinely-equivalent renders can still produce different bytes, so
|
|
188
|
+
a mismatch always falls back to a real pixel comparison.
|
|
189
|
+
|
|
190
|
+
## Making CI deterministic
|
|
191
|
+
|
|
192
|
+
Most visual-test pain is non-determinism, not craftdriver. The same page renders
|
|
193
|
+
differently across machines — **macOS and Linux genuinely rasterize different
|
|
194
|
+
pixels**, so a baseline captured on your Mac will not match a Linux CI runner.
|
|
195
|
+
Pin the environment:
|
|
196
|
+
|
|
197
|
+
- Capture baselines in the **same OS image** you run CI on (or keep per-platform
|
|
198
|
+
baselines). This is the big one.
|
|
199
|
+
- Fix the **browser and version**, the **viewport size**, and the **device pixel
|
|
200
|
+
ratio** (a HiDPI runner captures twice the pixels of a 1× one).
|
|
201
|
+
- Freeze **fonts** (ship them with the app/test image so font substitution can't
|
|
202
|
+
shift text), **locale**, and **timezone**.
|
|
203
|
+
- Disable **animations** and honor **reduced motion**; freeze time with the
|
|
204
|
+
[virtual clock](./clock.md) when content is time-dependent.
|
|
205
|
+
- Prefer a headless, consistent rendering path over whatever is on a developer
|
|
206
|
+
laptop.
|
|
207
|
+
|
|
208
|
+
Start strict (`pixelTolerance: 0`) and only add tolerance once you understand
|
|
209
|
+
which pixels move and why. A large blanket tolerance turns a visual test into a
|
|
210
|
+
test that passes no matter what.
|
|
211
|
+
|
|
212
|
+
## Comparing buffers directly
|
|
213
|
+
|
|
214
|
+
If you already have two PNG buffers and just want the comparison — no disk, no
|
|
215
|
+
browser, no retries — use `compareScreenshots(actual, expected, options)`. It
|
|
216
|
+
applies the same threshold semantics and input limits and returns the result
|
|
217
|
+
instead of throwing on a normal mismatch:
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
import { compareScreenshots } from 'craftdriver';
|
|
221
|
+
|
|
222
|
+
const result = await compareScreenshots(actualPng, expectedPng, {
|
|
223
|
+
pixelTolerance: 3,
|
|
224
|
+
maxDiffPixels: 50,
|
|
225
|
+
});
|
|
226
|
+
console.log(result.matches, result.diffPixels, result.diffPercentage);
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
The argument order is always `(actual, expected)` — everywhere in the API.
|
|
230
|
+
|
|
231
|
+
## Input limits and scope
|
|
232
|
+
|
|
233
|
+
Before decoding, both images are checked against `maxImagePixels` (default 20
|
|
234
|
+
million pixels) and `maxInputBytes` (default 50 MiB), with the size validated
|
|
235
|
+
from the PNG header before any large allocation. Decoded RGBA is ~4 bytes per
|
|
236
|
+
pixel, and an assertion may hold the expected image, the latest actual image, and
|
|
237
|
+
a diff at once, so raise `maxImagePixels` deliberately for unusually long
|
|
238
|
+
full-page screenshots — and consider lowering it when many test workers run in
|
|
239
|
+
parallel.
|
|
240
|
+
|
|
241
|
+
This feature is built for a developer-controlled baseline and a screenshot
|
|
242
|
+
produced by the browser under test. The low-level comparator is **not** a
|
|
243
|
+
hardened service for arbitrary user-uploaded images; if that ever becomes a
|
|
244
|
+
requirement, decoding needs to move to a terminable worker with a hard CPU
|
|
245
|
+
deadline (an in-process timeout cannot interrupt a synchronous parser).
|
|
246
|
+
|
|
247
|
+
## Under the hood
|
|
248
|
+
|
|
249
|
+
PNG decoding and encoding go through a single pinned dependency, `pngjs`, behind
|
|
250
|
+
an internal codec seam. It is a pure-JavaScript package with no native addon,
|
|
251
|
+
WASM, install script, or transitive runtime dependencies — chosen so the
|
|
252
|
+
security and packaging story stays simple. The optional anti-alias detector is
|
|
253
|
+
adapted from [Pixelmatch](https://github.com/mapbox/pixelmatch) (ISC licensed;
|
|
254
|
+
attribution is preserved in the source).
|
package/docs/why-craftdriver.md
CHANGED
|
@@ -11,10 +11,11 @@ CraftDriver is for writing boringly reliable automation against real browsers, w
|
|
|
11
11
|
- 📡 **Network control** - mock, block, intercept, and wait for browser requests and responses.
|
|
12
12
|
- 🔐 **Reusable sessions** - save cookies and localStorage, then launch already signed in.
|
|
13
13
|
- ⏱️ **Virtual clock** - freeze or fast-forward `Date`, timers, and time-sensitive UI.
|
|
14
|
-
- ♿ **Accessibility audits** -
|
|
14
|
+
- ♿ **Accessibility audits** - find WCAG 2.2 and Section 508 violations with built-in axe-core checks.
|
|
15
|
+
- 🖼️ **Visual testing** - catch visual regressions against baselines, without fighting anti-aliasing flakiness.
|
|
15
16
|
- 🧾 **Trace evidence** - capture actions, console output, errors, network events, and screenshots.
|
|
16
|
-
- ⚛️ **Electron apps** - drive packaged Electron desktop app renderers, matched to the app's bundled Chromium.
|
|
17
17
|
- ☁️ **Remote WebDriver** - use the Browser API with a self-hosted Selenium Grid or a cloud provider like [BrowserStack](https://www.browserstack.com/).
|
|
18
|
+
- ⚛️ **Electron apps** - drive packaged Electron desktop apps and mock native OS dialogs (open/save, message boxes).
|
|
18
19
|
- 🤖 **Agent-friendly** - CLI, MCP, and assistant rules when coding agents need the browser.
|
|
19
20
|
|
|
20
21
|
## Good Fits
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "craftdriver",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "Modern WebDriver automation library for NodeJS",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -88,6 +88,7 @@
|
|
|
88
88
|
"@semantic-release/changelog": "^6.0.3",
|
|
89
89
|
"@semantic-release/git": "^10.0.1",
|
|
90
90
|
"@types/node": "^20.0.0",
|
|
91
|
+
"@types/pngjs": "6.0.5",
|
|
91
92
|
"@types/ws": "^8.18.1",
|
|
92
93
|
"@types/yazl": "^3.3.1",
|
|
93
94
|
"@typescript-eslint/parser": "^8.1.0",
|
|
@@ -102,6 +103,7 @@
|
|
|
102
103
|
},
|
|
103
104
|
"dependencies": {
|
|
104
105
|
"axe-core": "^4.12.1",
|
|
106
|
+
"pngjs": "7.0.0",
|
|
105
107
|
"ws": "^8.21.0",
|
|
106
108
|
"yazl": "^3.3.1"
|
|
107
109
|
},
|