craftdriver 1.5.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.
Files changed (120) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +7 -3
  3. package/dist/cli/daemon.d.ts.map +1 -1
  4. package/dist/cli/daemon.js +4 -0
  5. package/dist/cli/daemon.js.map +1 -1
  6. package/dist/cli/mcp/server.d.ts.map +1 -1
  7. package/dist/cli/mcp/server.js +4 -0
  8. package/dist/cli/mcp/server.js.map +1 -1
  9. package/dist/index.d.ts +2 -0
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +2 -0
  12. package/dist/index.js.map +1 -1
  13. package/dist/lib/browser.d.ts +73 -22
  14. package/dist/lib/browser.d.ts.map +1 -1
  15. package/dist/lib/browser.js +155 -41
  16. package/dist/lib/browser.js.map +1 -1
  17. package/dist/lib/browserContext.d.ts +1 -1
  18. package/dist/lib/browserContext.d.ts.map +1 -1
  19. package/dist/lib/builder.d.ts +12 -1
  20. package/dist/lib/builder.d.ts.map +1 -1
  21. package/dist/lib/builder.js +26 -0
  22. package/dist/lib/builder.js.map +1 -1
  23. package/dist/lib/capabilities.d.ts +8 -0
  24. package/dist/lib/capabilities.d.ts.map +1 -1
  25. package/dist/lib/capabilities.js +26 -10
  26. package/dist/lib/capabilities.js.map +1 -1
  27. package/dist/lib/driver.d.ts +17 -1
  28. package/dist/lib/driver.d.ts.map +1 -1
  29. package/dist/lib/driver.js +27 -2
  30. package/dist/lib/driver.js.map +1 -1
  31. package/dist/lib/elementHandle.d.ts.map +1 -1
  32. package/dist/lib/elementHandle.js +34 -0
  33. package/dist/lib/elementHandle.js.map +1 -1
  34. package/dist/lib/errors.d.ts +7 -0
  35. package/dist/lib/errors.d.ts.map +1 -1
  36. package/dist/lib/errors.js +7 -0
  37. package/dist/lib/errors.js.map +1 -1
  38. package/dist/lib/http.d.ts.map +1 -1
  39. package/dist/lib/http.js +31 -10
  40. package/dist/lib/http.js.map +1 -1
  41. package/dist/lib/launchTarget.d.ts +21 -2
  42. package/dist/lib/launchTarget.d.ts.map +1 -1
  43. package/dist/lib/launchTarget.js +105 -17
  44. package/dist/lib/launchTarget.js.map +1 -1
  45. package/dist/lib/remote.d.ts +55 -0
  46. package/dist/lib/remote.d.ts.map +1 -0
  47. package/dist/lib/remote.js +127 -0
  48. package/dist/lib/remote.js.map +1 -0
  49. package/dist/lib/timing.d.ts +8 -0
  50. package/dist/lib/timing.d.ts.map +1 -1
  51. package/dist/lib/timing.js +8 -0
  52. package/dist/lib/timing.js.map +1 -1
  53. package/dist/lib/tracing.d.ts +1 -1
  54. package/dist/lib/tracing.d.ts.map +1 -1
  55. package/dist/lib/tracing.js.map +1 -1
  56. package/dist/lib/types.d.ts +22 -0
  57. package/dist/lib/types.d.ts.map +1 -1
  58. package/dist/lib/vibiumTrace.d.ts +1 -1
  59. package/dist/lib/vibiumTrace.d.ts.map +1 -1
  60. package/dist/lib/visual/antialias.d.ts +41 -0
  61. package/dist/lib/visual/antialias.d.ts.map +1 -0
  62. package/dist/lib/visual/antialias.js +126 -0
  63. package/dist/lib/visual/antialias.js.map +1 -0
  64. package/dist/lib/visual/codec.d.ts +24 -0
  65. package/dist/lib/visual/codec.d.ts.map +1 -0
  66. package/dist/lib/visual/codec.js +11 -0
  67. package/dist/lib/visual/codec.js.map +1 -0
  68. package/dist/lib/visual/compare.d.ts +38 -0
  69. package/dist/lib/visual/compare.d.ts.map +1 -0
  70. package/dist/lib/visual/compare.js +209 -0
  71. package/dist/lib/visual/compare.js.map +1 -0
  72. package/dist/lib/visual/diff.d.ts +18 -0
  73. package/dist/lib/visual/diff.d.ts.map +1 -0
  74. package/dist/lib/visual/diff.js +23 -0
  75. package/dist/lib/visual/diff.js.map +1 -0
  76. package/dist/lib/visual/expectScreenshot.d.ts +65 -0
  77. package/dist/lib/visual/expectScreenshot.d.ts.map +1 -0
  78. package/dist/lib/visual/expectScreenshot.js +287 -0
  79. package/dist/lib/visual/expectScreenshot.js.map +1 -0
  80. package/dist/lib/visual/index.d.ts +10 -0
  81. package/dist/lib/visual/index.d.ts.map +1 -0
  82. package/dist/lib/visual/index.js +9 -0
  83. package/dist/lib/visual/index.js.map +1 -0
  84. package/dist/lib/visual/pixelMath.d.ts +32 -0
  85. package/dist/lib/visual/pixelMath.d.ts.map +1 -0
  86. package/dist/lib/visual/pixelMath.js +70 -0
  87. package/dist/lib/visual/pixelMath.js.map +1 -0
  88. package/dist/lib/visual/pngHeader.d.ts +27 -0
  89. package/dist/lib/visual/pngHeader.d.ts.map +1 -0
  90. package/dist/lib/visual/pngHeader.js +63 -0
  91. package/dist/lib/visual/pngHeader.js.map +1 -0
  92. package/dist/lib/visual/pngjsCodec.d.ts +22 -0
  93. package/dist/lib/visual/pngjsCodec.d.ts.map +1 -0
  94. package/dist/lib/visual/pngjsCodec.js +70 -0
  95. package/dist/lib/visual/pngjsCodec.js.map +1 -0
  96. package/dist/lib/visual/types.d.ts +89 -0
  97. package/dist/lib/visual/types.d.ts.map +1 -0
  98. package/dist/lib/visual/types.js +20 -0
  99. package/dist/lib/visual/types.js.map +1 -0
  100. package/dist/lib/visual/visualError.d.ts +32 -0
  101. package/dist/lib/visual/visualError.d.ts.map +1 -0
  102. package/dist/lib/visual/visualError.js +59 -0
  103. package/dist/lib/visual/visualError.js.map +1 -0
  104. package/dist/lib/wait.d.ts.map +1 -1
  105. package/dist/lib/wait.js +7 -1
  106. package/dist/lib/wait.js.map +1 -1
  107. package/docs/api-reference.md +18 -2
  108. package/docs/browser-api.md +5 -4
  109. package/docs/driver-configuration.md +6 -0
  110. package/docs/error-codes.md +1 -0
  111. package/docs/index.md +1 -0
  112. package/docs/keyboard-mouse.md +7 -5
  113. package/docs/recipes/run-on-browserstack.md +265 -0
  114. package/docs/recipes.md +15 -1
  115. package/docs/remote-webdriver.md +308 -0
  116. package/docs/screenshots.md +15 -6
  117. package/docs/standards.md +15 -10
  118. package/docs/visual-testing.md +254 -0
  119. package/docs/why-craftdriver.md +5 -2
  120. package/package.json +5 -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).
@@ -5,14 +5,17 @@ CraftDriver is for writing boringly reliable automation against real browsers, w
5
5
  ## What It Cares About
6
6
 
7
7
  - 🍺 **Focused Node.js API** - browser automation without a giant framework around it.
8
- - 🧭 **Real browsers** - drives installed Chrome, Chromium, and Firefox instead of patched browser engine builds, plus real ([Safari on macOS](./safari.md)).
8
+ - 🧭 **Real browsers** - drives installed Chrome, Chromium, and Firefox instead of patched browser engine builds, plus real [Safari on macOS](./safari.md).
9
9
  - 🌐 **Standards that age well** - W3C WebDriver standards stay stable while browser-private protocols change.
10
10
  - 🚦 **Readable, auto-waited flows** - role, label, text, test id, CSS, XPath, click, fill, and expect.
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** - run axe-core checks on pages, elements, and locators.
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.
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).
16
19
  - 🤖 **Agent-friendly** - CLI, MCP, and assistant rules when coding agents need the browser.
17
20
 
18
21
  ## Good Fits
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "craftdriver",
3
- "version": "1.5.0",
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",
@@ -20,6 +20,8 @@
20
20
  "test:safari": "BROWSER_NAME=safari vitest run tests/safari.test.ts --maxWorkers=1",
21
21
  "test:recipes": "HEADLESS=true vitest run --config vitest.recipes.config.ts",
22
22
  "test:electron": "vitest run --config vitest.electron.config.ts",
23
+ "test:browserstack": "vitest run --config vitest.browserstack.config.ts",
24
+ "test:grid": "vitest run --config vitest.grid.config.ts",
23
25
  "bench": "HEADLESS=true vitest run --config vitest.perf.config.ts",
24
26
  "clean": "rimraf dist",
25
27
  "serve": "http-server ./examples -a 127.0.0.1 -p 8080 -c-1 --cors",
@@ -86,6 +88,7 @@
86
88
  "@semantic-release/changelog": "^6.0.3",
87
89
  "@semantic-release/git": "^10.0.1",
88
90
  "@types/node": "^20.0.0",
91
+ "@types/pngjs": "6.0.5",
89
92
  "@types/ws": "^8.18.1",
90
93
  "@types/yazl": "^3.3.1",
91
94
  "@typescript-eslint/parser": "^8.1.0",
@@ -100,6 +103,7 @@
100
103
  },
101
104
  "dependencies": {
102
105
  "axe-core": "^4.12.1",
106
+ "pngjs": "7.0.0",
103
107
  "ws": "^8.21.0",
104
108
  "yazl": "^3.3.1"
105
109
  },