craftdriver 1.6.0 → 1.8.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 (229) hide show
  1. package/CHANGELOG.md +35 -6
  2. package/README.md +17 -6
  3. package/dist/cli/agentSession.d.ts +62 -0
  4. package/dist/cli/agentSession.d.ts.map +1 -0
  5. package/dist/cli/agentSession.js +100 -0
  6. package/dist/cli/agentSession.js.map +1 -0
  7. package/dist/cli/artifactPaths.d.ts +42 -0
  8. package/dist/cli/artifactPaths.d.ts.map +1 -0
  9. package/dist/cli/artifactPaths.js +144 -0
  10. package/dist/cli/artifactPaths.js.map +1 -0
  11. package/dist/cli/bounds.d.ts +88 -0
  12. package/dist/cli/bounds.d.ts.map +1 -0
  13. package/dist/cli/bounds.js +128 -0
  14. package/dist/cli/bounds.js.map +1 -0
  15. package/dist/cli/client.d.ts +3 -0
  16. package/dist/cli/client.d.ts.map +1 -1
  17. package/dist/cli/client.js +24 -8
  18. package/dist/cli/client.js.map +1 -1
  19. package/dist/cli/daemon.d.ts +3 -0
  20. package/dist/cli/daemon.d.ts.map +1 -1
  21. package/dist/cli/daemon.js +152 -39
  22. package/dist/cli/daemon.js.map +1 -1
  23. package/dist/cli/defaults.d.ts +23 -3
  24. package/dist/cli/defaults.d.ts.map +1 -1
  25. package/dist/cli/defaults.js +50 -5
  26. package/dist/cli/defaults.js.map +1 -1
  27. package/dist/cli/dispatcher.d.ts +54 -11
  28. package/dist/cli/dispatcher.d.ts.map +1 -1
  29. package/dist/cli/dispatcher.js +851 -54
  30. package/dist/cli/dispatcher.js.map +1 -1
  31. package/dist/cli/index.d.ts.map +1 -1
  32. package/dist/cli/index.js +305 -56
  33. package/dist/cli/index.js.map +1 -1
  34. package/dist/cli/init.d.ts +14 -14
  35. package/dist/cli/init.d.ts.map +1 -1
  36. package/dist/cli/init.js +328 -180
  37. package/dist/cli/init.js.map +1 -1
  38. package/dist/cli/journal.d.ts +153 -0
  39. package/dist/cli/journal.d.ts.map +1 -0
  40. package/dist/cli/journal.js +252 -0
  41. package/dist/cli/journal.js.map +1 -0
  42. package/dist/cli/lineReader.d.ts +33 -0
  43. package/dist/cli/lineReader.d.ts.map +1 -0
  44. package/dist/cli/lineReader.js +66 -0
  45. package/dist/cli/lineReader.js.map +1 -0
  46. package/dist/cli/locatorCandidates.d.ts +44 -0
  47. package/dist/cli/locatorCandidates.d.ts.map +1 -0
  48. package/dist/cli/locatorCandidates.js +190 -0
  49. package/dist/cli/locatorCandidates.js.map +1 -0
  50. package/dist/cli/mcp/artifacts.d.ts +39 -1
  51. package/dist/cli/mcp/artifacts.d.ts.map +1 -1
  52. package/dist/cli/mcp/artifacts.js +104 -9
  53. package/dist/cli/mcp/artifacts.js.map +1 -1
  54. package/dist/cli/mcp/bounds.d.ts +65 -0
  55. package/dist/cli/mcp/bounds.d.ts.map +1 -0
  56. package/dist/cli/mcp/bounds.js +124 -0
  57. package/dist/cli/mcp/bounds.js.map +1 -0
  58. package/dist/cli/mcp/params.d.ts +38 -0
  59. package/dist/cli/mcp/params.d.ts.map +1 -0
  60. package/dist/cli/mcp/params.js +174 -0
  61. package/dist/cli/mcp/params.js.map +1 -0
  62. package/dist/cli/mcp/server.d.ts +94 -16
  63. package/dist/cli/mcp/server.d.ts.map +1 -1
  64. package/dist/cli/mcp/server.js +304 -150
  65. package/dist/cli/mcp/server.js.map +1 -1
  66. package/dist/cli/mcp/tools.d.ts +62 -17
  67. package/dist/cli/mcp/tools.d.ts.map +1 -1
  68. package/dist/cli/mcp/tools.js +373 -158
  69. package/dist/cli/mcp/tools.js.map +1 -1
  70. package/dist/cli/pageSemantics.d.ts +42 -0
  71. package/dist/cli/pageSemantics.d.ts.map +1 -0
  72. package/dist/cli/pageSemantics.js +194 -0
  73. package/dist/cli/pageSemantics.js.map +1 -0
  74. package/dist/cli/parseArgs.d.ts +6 -18
  75. package/dist/cli/parseArgs.d.ts.map +1 -1
  76. package/dist/cli/parseArgs.js +689 -45
  77. package/dist/cli/parseArgs.js.map +1 -1
  78. package/dist/cli/protocol.d.ts +8 -0
  79. package/dist/cli/protocol.d.ts.map +1 -1
  80. package/dist/cli/sessionRegistry.d.ts +42 -0
  81. package/dist/cli/sessionRegistry.d.ts.map +1 -0
  82. package/dist/cli/sessionRegistry.js +99 -0
  83. package/dist/cli/sessionRegistry.js.map +1 -0
  84. package/dist/cli/snapshot.d.ts +100 -18
  85. package/dist/cli/snapshot.d.ts.map +1 -1
  86. package/dist/cli/snapshot.js +248 -102
  87. package/dist/cli/snapshot.js.map +1 -1
  88. package/dist/cli/stateStore.d.ts +50 -0
  89. package/dist/cli/stateStore.d.ts.map +1 -0
  90. package/dist/cli/stateStore.js +122 -0
  91. package/dist/cli/stateStore.js.map +1 -0
  92. package/dist/index.d.ts +2 -1
  93. package/dist/index.d.ts.map +1 -1
  94. package/dist/index.js +2 -0
  95. package/dist/index.js.map +1 -1
  96. package/dist/lib/bidi/index.d.ts +1 -1
  97. package/dist/lib/bidi/index.d.ts.map +1 -1
  98. package/dist/lib/bidi/index.js +2 -2
  99. package/dist/lib/bidi/index.js.map +1 -1
  100. package/dist/lib/bidi/logs.d.ts +3 -0
  101. package/dist/lib/bidi/logs.d.ts.map +1 -1
  102. package/dist/lib/bidi/logs.js +7 -0
  103. package/dist/lib/bidi/logs.js.map +1 -1
  104. package/dist/lib/bidi/network.d.ts +14 -1
  105. package/dist/lib/bidi/network.d.ts.map +1 -1
  106. package/dist/lib/bidi/network.js +45 -7
  107. package/dist/lib/bidi/network.js.map +1 -1
  108. package/dist/lib/bidi/storage.d.ts +30 -6
  109. package/dist/lib/bidi/storage.d.ts.map +1 -1
  110. package/dist/lib/bidi/storage.js +190 -45
  111. package/dist/lib/bidi/storage.js.map +1 -1
  112. package/dist/lib/bidi/types.d.ts +7 -2
  113. package/dist/lib/bidi/types.d.ts.map +1 -1
  114. package/dist/lib/browser.d.ts +38 -3
  115. package/dist/lib/browser.d.ts.map +1 -1
  116. package/dist/lib/browser.js +196 -30
  117. package/dist/lib/browser.js.map +1 -1
  118. package/dist/lib/browserContext.d.ts +54 -18
  119. package/dist/lib/browserContext.d.ts.map +1 -1
  120. package/dist/lib/browserContext.js +295 -92
  121. package/dist/lib/browserContext.js.map +1 -1
  122. package/dist/lib/by.d.ts +3 -3
  123. package/dist/lib/by.d.ts.map +1 -1
  124. package/dist/lib/by.js +7 -3
  125. package/dist/lib/by.js.map +1 -1
  126. package/dist/lib/driver.d.ts +7 -0
  127. package/dist/lib/driver.d.ts.map +1 -1
  128. package/dist/lib/driver.js +12 -0
  129. package/dist/lib/driver.js.map +1 -1
  130. package/dist/lib/errors.d.ts +15 -0
  131. package/dist/lib/errors.d.ts.map +1 -1
  132. package/dist/lib/errors.js +15 -0
  133. package/dist/lib/errors.js.map +1 -1
  134. package/dist/lib/initScript.d.ts +9 -0
  135. package/dist/lib/initScript.d.ts.map +1 -0
  136. package/dist/lib/initScript.js +14 -0
  137. package/dist/lib/initScript.js.map +1 -0
  138. package/dist/lib/page.d.ts +22 -0
  139. package/dist/lib/page.d.ts.map +1 -1
  140. package/dist/lib/page.js +48 -0
  141. package/dist/lib/page.js.map +1 -1
  142. package/dist/lib/secureFile.d.ts +8 -0
  143. package/dist/lib/secureFile.d.ts.map +1 -0
  144. package/dist/lib/secureFile.js +67 -0
  145. package/dist/lib/secureFile.js.map +1 -0
  146. package/dist/lib/sessionStateValidation.d.ts +35 -0
  147. package/dist/lib/sessionStateValidation.d.ts.map +1 -0
  148. package/dist/lib/sessionStateValidation.js +223 -0
  149. package/dist/lib/sessionStateValidation.js.map +1 -0
  150. package/dist/lib/timing.d.ts +8 -0
  151. package/dist/lib/timing.d.ts.map +1 -1
  152. package/dist/lib/timing.js +8 -0
  153. package/dist/lib/timing.js.map +1 -1
  154. package/dist/lib/tracing.d.ts +2 -1
  155. package/dist/lib/tracing.d.ts.map +1 -1
  156. package/dist/lib/tracing.js +12 -1
  157. package/dist/lib/tracing.js.map +1 -1
  158. package/dist/lib/visual/antialias.d.ts +41 -0
  159. package/dist/lib/visual/antialias.d.ts.map +1 -0
  160. package/dist/lib/visual/antialias.js +126 -0
  161. package/dist/lib/visual/antialias.js.map +1 -0
  162. package/dist/lib/visual/codec.d.ts +24 -0
  163. package/dist/lib/visual/codec.d.ts.map +1 -0
  164. package/dist/lib/visual/codec.js +11 -0
  165. package/dist/lib/visual/codec.js.map +1 -0
  166. package/dist/lib/visual/compare.d.ts +38 -0
  167. package/dist/lib/visual/compare.d.ts.map +1 -0
  168. package/dist/lib/visual/compare.js +209 -0
  169. package/dist/lib/visual/compare.js.map +1 -0
  170. package/dist/lib/visual/diff.d.ts +18 -0
  171. package/dist/lib/visual/diff.d.ts.map +1 -0
  172. package/dist/lib/visual/diff.js +23 -0
  173. package/dist/lib/visual/diff.js.map +1 -0
  174. package/dist/lib/visual/expectScreenshot.d.ts +65 -0
  175. package/dist/lib/visual/expectScreenshot.d.ts.map +1 -0
  176. package/dist/lib/visual/expectScreenshot.js +287 -0
  177. package/dist/lib/visual/expectScreenshot.js.map +1 -0
  178. package/dist/lib/visual/index.d.ts +10 -0
  179. package/dist/lib/visual/index.d.ts.map +1 -0
  180. package/dist/lib/visual/index.js +9 -0
  181. package/dist/lib/visual/index.js.map +1 -0
  182. package/dist/lib/visual/pixelMath.d.ts +32 -0
  183. package/dist/lib/visual/pixelMath.d.ts.map +1 -0
  184. package/dist/lib/visual/pixelMath.js +70 -0
  185. package/dist/lib/visual/pixelMath.js.map +1 -0
  186. package/dist/lib/visual/pngHeader.d.ts +27 -0
  187. package/dist/lib/visual/pngHeader.d.ts.map +1 -0
  188. package/dist/lib/visual/pngHeader.js +63 -0
  189. package/dist/lib/visual/pngHeader.js.map +1 -0
  190. package/dist/lib/visual/pngjsCodec.d.ts +22 -0
  191. package/dist/lib/visual/pngjsCodec.d.ts.map +1 -0
  192. package/dist/lib/visual/pngjsCodec.js +70 -0
  193. package/dist/lib/visual/pngjsCodec.js.map +1 -0
  194. package/dist/lib/visual/types.d.ts +89 -0
  195. package/dist/lib/visual/types.d.ts.map +1 -0
  196. package/dist/lib/visual/types.js +20 -0
  197. package/dist/lib/visual/types.js.map +1 -0
  198. package/dist/lib/visual/visualError.d.ts +32 -0
  199. package/dist/lib/visual/visualError.d.ts.map +1 -0
  200. package/dist/lib/visual/visualError.js +59 -0
  201. package/dist/lib/visual/visualError.js.map +1 -0
  202. package/dist/lib/wait.d.ts.map +1 -1
  203. package/dist/lib/wait.js +7 -1
  204. package/dist/lib/wait.js.map +1 -1
  205. package/docs/agents.md +56 -61
  206. package/docs/api-reference.md +16 -1
  207. package/docs/browser-api.md +24 -4
  208. package/docs/browser-context.md +18 -12
  209. package/docs/cli.md +346 -26
  210. package/docs/error-codes.md +6 -4
  211. package/docs/getting-started.md +6 -1
  212. package/docs/index.md +1 -1
  213. package/docs/mcp.md +129 -62
  214. package/docs/public/examples/agent-actions.html +48 -0
  215. package/docs/public/examples/agent-debug.html +86 -0
  216. package/docs/public/examples/agent-late-mutation.html +18 -0
  217. package/docs/public/examples/agent-ref-shift.html +33 -0
  218. package/docs/recipes/login-once-reuse-session.md +32 -4
  219. package/docs/screenshots.md +15 -6
  220. package/docs/session-management.md +64 -3
  221. package/docs/visual-testing.md +254 -0
  222. package/docs/why-craftdriver.md +4 -3
  223. package/package.json +5 -1
  224. package/skills/craftdriver/SKILL.md +59 -75
  225. package/skills/craftdriver/agents/openai.yaml +4 -0
  226. package/skills/craftdriver/cheatsheet.md +18 -4
  227. package/skills/craftdriver/cli.md +194 -68
  228. package/skills/craftdriver/patterns.md +14 -8
  229. package/skills/craftdriver/workflow.md +149 -0
@@ -9,6 +9,10 @@ CraftDriver supports Playwright-style session persistence, allowing you to save
9
9
  Session management is the user-facing feature. Under the hood, CraftDriver uses
10
10
  the best available WebDriver transport for the browser you launched.
11
11
 
12
+ `storageState` is CraftDriver's native cookies-plus-localStorage format. It is
13
+ not a promise of Playwright JSON compatibility and does not include IndexedDB,
14
+ Cache Storage, service workers, or reusable sessionStorage.
15
+
12
16
  ## Saving Session State
13
17
 
14
18
  ```typescript
@@ -52,6 +56,9 @@ The saved file contains:
52
56
  ```typescript
53
57
  // Load state into current browser
54
58
  await browser.loadState('./session.json');
59
+
60
+ // Paths and in-memory state objects are equivalent
61
+ await browser.loadState(await browser.storage.getState());
55
62
  ```
56
63
 
57
64
  ## Working With State Objects
@@ -63,6 +70,11 @@ const state = await browser.storage.getState();
63
70
  await browser.storage.setState(state);
64
71
  ```
65
72
 
73
+ `browser.storage.setState()` is an active-page operation: navigate to the
74
+ state's sole HTTP(S) origin first. `browser.loadState()` uses the full
75
+ multi-origin hydrator on BiDi when the state has no sessionStorage; otherwise it
76
+ uses the same strict active-page rules.
77
+
66
78
  ## Launching with Pre-loaded State
67
79
 
68
80
  The most common pattern - launch a browser with existing session:
@@ -78,6 +90,55 @@ await browser.navigateTo('https://example.com/dashboard');
78
90
  // Already logged in!
79
91
  ```
80
92
 
93
+ On supported BiDi sessions, cookies and every captured localStorage origin are
94
+ ready before the first real navigation. A private intercepted document seeds
95
+ each origin once; application changes are not overwritten on reload.
96
+
97
+ ## Browser and Transport Support
98
+
99
+ | Surface | Chrome/Chromium BiDi | Firefox BiDi | Chrome/Firefox/Safari Classic |
100
+ | --- | --- | --- | --- |
101
+ | `Browser.launch({ storageState })` | Full cookies + multi-origin localStorage | Full cookies + multi-origin localStorage | Non-empty state rejected before mutation |
102
+ | `browser.newContext({ storageState })` | Full isolated restore | Full isolated restore | Unavailable (Classic has no user contexts) |
103
+ | `context.loadStorageState()` | Full multi-origin overlay | Full multi-origin overlay | Unavailable |
104
+ | `browser.loadState()` | Full multi-origin overlay without sessionStorage; strict active page with it | Same | Strict single-active-origin restore |
105
+ | Save | Cookies + current/open-page localStorage origins | Same | Cookies + current-page localStorage |
106
+
107
+ Chrome and Firefox BiDi and the Chrome/Firefox Classic fallback are covered by
108
+ the integration suite. Chromium uses the same Chrome-family BiDi implementation.
109
+ Safari is Classic-only and therefore receives the standards-only active-origin
110
+ contract, not launch-time restore.
111
+
112
+ ### WebDriver Classic
113
+
114
+ Classic cannot write arbitrary-origin localStorage at launch. Use the explicit
115
+ fallback instead:
116
+
117
+ ```typescript
118
+ const browser = await Browser.launch({ enableBiDi: false });
119
+ await browser.navigateTo('https://example.com');
120
+ await browser.loadState('./session.json');
121
+ await browser.reload(); // the app can now read the restored state
122
+ ```
123
+
124
+ CraftDriver validates the entire snapshot before mutation. `about:blank`,
125
+ multiple storage origins, a mismatched cookie domain, or a secure cookie that
126
+ cannot be set from the active page fails with `STATE_INVALID`. Non-empty
127
+ `storageState` at Classic launch fails with `UNSUPPORTED` instead of pretending
128
+ that a partial restore succeeded.
129
+
130
+ ### sessionStorage and runtime failure semantics
131
+
132
+ `sessionStorage` capture is opt-in diagnostic state. Context and launch APIs
133
+ reject a non-empty `sessionStorage` section because it cannot be transferred to
134
+ future tabs. Active-page APIs can restore one matching origin after navigation.
135
+
136
+ Fresh launch and `newContext` restoration clean up the new session/context on
137
+ failure. An overlay into an existing context is not rollback-safe across
138
+ multiple protocol calls. A runtime failure throws `DRIVER_ERROR` with
139
+ `detail.phase` and `detail.partialApplied`; use a fresh context when failure
140
+ isolation matters.
141
+
81
142
  ## Cookie Management API
82
143
 
83
144
  Direct cookie manipulation is also available:
@@ -221,7 +282,7 @@ async function testLoginPersistence() {
221
282
  | Method | Description |
222
283
  | --------------------------- | ---------------------------------- |
223
284
  | `saveState(path, options?)` | Save cookies and storage to file |
224
- | `loadState(path)` | Load cookies and storage from file |
285
+ | `loadState(source)` | Load cookies and storage from a path or object |
225
286
 
226
287
  ### StorageStateOptions
227
288
 
@@ -241,6 +302,6 @@ async function testLoginPersistence() {
241
302
  | `setCookies(cookies)` | Set multiple cookies |
242
303
  | `clearCookies(filter?)` | Clear cookies, optionally filtered |
243
304
  | `getState(options?)` | Get current state as object |
244
- | `setState(state)` | Set state from object |
305
+ | `setState(source)` | Set active-page state from path or object |
245
306
  | `saveState(path, options?)` | Save state to file |
246
- | `loadState(path)` | Load state from file |
307
+ | `loadState(source)` | Load active-page state from path or object |
@@ -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).
@@ -11,11 +11,12 @@ 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** - 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.
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
- - 🤖 **Agent-friendly** - CLI, MCP, and assistant rules when coding agents need the browser.
18
+ - ⚛️ **Electron apps** - drive packaged Electron desktop apps and mock native OS dialogs (open/save, message boxes).
19
+ - 🤖 **Agent-friendly** - CLI, a project-local skill, and optional MCP when coding agents need the browser.
19
20
 
20
21
  ## Good Fits
21
22
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "craftdriver",
3
- "version": "1.6.0",
3
+ "version": "1.8.0",
4
4
  "description": "Modern WebDriver automation library for NodeJS",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -18,6 +18,8 @@
18
18
  "test:chrome": "BROWSER_NAME=chrome vitest run",
19
19
  "test:chromium": "BROWSER_NAME=chromium vitest run",
20
20
  "test:safari": "BROWSER_NAME=safari vitest run tests/safari.test.ts --maxWorkers=1",
21
+ "test:cli": "HEADLESS=true vitest run --config vitest.cli.config.ts",
22
+ "test:release": "vitest run --config vitest.release.config.ts",
21
23
  "test:recipes": "HEADLESS=true vitest run --config vitest.recipes.config.ts",
22
24
  "test:electron": "vitest run --config vitest.electron.config.ts",
23
25
  "test:browserstack": "vitest run --config vitest.browserstack.config.ts",
@@ -88,6 +90,7 @@
88
90
  "@semantic-release/changelog": "^6.0.3",
89
91
  "@semantic-release/git": "^10.0.1",
90
92
  "@types/node": "^20.0.0",
93
+ "@types/pngjs": "6.0.5",
91
94
  "@types/ws": "^8.18.1",
92
95
  "@types/yazl": "^3.3.1",
93
96
  "@typescript-eslint/parser": "^8.1.0",
@@ -102,6 +105,7 @@
102
105
  },
103
106
  "dependencies": {
104
107
  "axe-core": "^4.12.1",
108
+ "pngjs": "7.0.0",
105
109
  "ws": "^8.21.0",
106
110
  "yazl": "^3.3.1"
107
111
  },
@@ -1,89 +1,73 @@
1
- # craftdriver — SKILL
1
+ ---
2
+ name: craftdriver
3
+ description: Explore live web applications with the CraftDriver CLI, validate durable selectors, and write, run, and debug CraftDriver tests. Use when a coding agent needs to inspect a page, drive browser flows, choose locators, author browser automation, or diagnose a failing CraftDriver test.
4
+ ---
2
5
 
3
- Modern WebDriver library for Node.js — picks the fastest correct protocol
4
- (Classic or BiDi) per command. Playwright ergonomics, WebDriver
5
- standards-compliance, no AI in the hot path.
6
+ # CraftDriver browser workflow
7
+
8
+ Use CraftDriver's CLI to inspect and exercise the running application, then use
9
+ the public TypeScript API to write and run durable tests. Chrome is the default
10
+ agent workflow.
6
11
 
7
12
  ## Core loop
8
13
 
9
- ```
10
- navigate → findactionassert
14
+ ```text
15
+ start app → navigate → snapshotinspectact → validate selector
16
+ → write test → run focused test → debug from fresh evidence
11
17
  ```
12
18
 
13
- Every action and every `expect(locator).to…()` **auto-waits** up to the
14
- default timeout (30 s in-library, 5 s on agent surfaces). Never add
15
- `sleep()` / `setTimeout()` — it's a footgun.
19
+ Actions and `expect(locator).to…()` auto-wait. Do not add sleeps or hand-written
20
+ polling loops.
16
21
 
17
- ## Selector preference order
22
+ ## Selector order
18
23
 
19
- ```
20
- By.testId > By.role({name}) > By.labelText >
21
- By.text({exact:true}) > By.css > By.xpath
24
+ Prefer evidence from the live page in this order:
25
+
26
+ ```text
27
+ By.role({ name }) / By.labelText / By.testId
28
+ → exact visible text → stable CSS → XPath only as a last resort
22
29
  ```
23
30
 
24
- `testId` and `role` are stable across DOM refactors; CSS and XPath
25
- break the moment markup shifts.
26
-
27
- ## Five rules
28
-
29
- 1. Use `expect(locator).to…()` for assertions. Never hand-roll
30
- `while (...) { ... sleep(100) }` retry loops — they auto-wait.
31
- 2. Read errors by `code`, not by prose. Every public throw is a
32
- `CraftdriverError` with a stable `code`. See
33
- [docs/error-codes.md](../../docs/error-codes.md).
34
- 3. `instanceof CraftdriverError` is true on every public throw;
35
- `instanceof Error` is also true.
36
- 4. BiDi features (`network`, `logs`, tracing, init scripts, true load
37
- events) work out of the box — `enableBiDi` defaults to `true`. They only
38
- break if you explicitly pass `enableBiDi: false`; the error code on the
39
- wrong transport is `UNSUPPORTED`. Exception: `browserName: 'safari'`
40
- defaults `enableBiDi` to `false` and rejects `true` — Safari is
41
- Classic-only (macOS, headed, one session at a time). See
42
- [docs/safari.md](../../docs/safari.md).
43
- 5. Tests fetch from the example server. Start it in a separate
44
- terminal: `npm run examples:start` before `npm test`.
45
-
46
- ## When you reach for more
47
-
48
- - **Writing tests, looking for a method** → read
49
- [skills/craftdriver/cheatsheet.md](cheatsheet.md). Also see
50
- [docs/api-reference.md](../../docs/api-reference.md) — every public
51
- export, one row each.
52
- - **Login, upload, wait-for-network, etc.** → read
53
- [skills/craftdriver/patterns.md](patterns.md).
54
- - **Driving the browser from a shell or agent loop** → read
55
- [skills/craftdriver/cli.md](cli.md). Same Browser API, exposed as a
56
- `craftdriver` binary with daemon + ephemeral modes.
57
- - **Driving from an MCP-aware AI host** (Claude Desktop / Code, Cursor,
58
- Windsurf, Zed, Goose, Gemini CLI) → `craftdriver mcp` is a stdio
59
- JSON-RPC server with 14 schema-typed tools. Mutating tools return a
60
- compact a11y snapshot **diffed from the previous turn** — you see
61
- what changed without a follow-up read. See
62
- [docs/mcp.md](../../docs/mcp.md).
63
- - **An error code you don't recognise** → read
64
- [docs/error-codes.md](../../docs/error-codes.md).
65
-
66
- ## Probing rule
67
-
68
- When unsure a selector exists, check before acting. Today: call
69
- `await locator.count()` (zero-wait, returns the current match count).
70
- Acting on a hallucinated selector wastes the whole auto-wait budget.
71
-
72
- For the **CLI and MCP**, the cheapest way to drive a page is to take a
73
- snapshot first and use refs:
31
+ Confirm a selector against the live page with `craftdriver exists` or
32
+ `craftdriver find` before putting it in a test.
74
33
 
75
- ```
76
- $ craftdriver snapshot
77
- e4: textbox "Username" #username
78
- e7: button "Sign in" #submit
79
- $ craftdriver fill ref=e4 alice
80
- $ craftdriver click ref=e7
81
- ```
34
+ ## Snapshot refs are exploration-only
82
35
 
83
- `ref=eN` resolves to a CSS attribute selector, auto-waits like any
84
- other selector, and re-allocates on every snapshot. No hallucination.
36
+ CLI snapshots show `ref=eN`. A ref binds to one element for as long as that
37
+ element lives, and is never reassigned to another one: if the element is removed
38
+ or duplicated, or the page navigates or reloads, the command fails with
39
+ `STALE_REF`. Take a fresh snapshot when that happens — CraftDriver will not
40
+ guess a replacement.
85
41
 
86
- ## Imports
42
+ A ref still means nothing outside the current session, so never copy one into
43
+ test source. Convert the element into a durable locator and let CraftDriver
44
+ check it against the live page:
45
+
46
+ ```bash
47
+ npx craftdriver locators ref=e7
48
+ ```
87
49
 
88
- Always `import { ... } from 'craftdriver'`. Never reach into
89
- `craftdriver/src/lib/...` internals are unstable.
50
+ Use a candidate reported `unique` (ordered by durability: role + accessible
51
+ name, label, test ID, unique text, minimal CSS). If none is unique, add a
52
+ `data-testid` to the application instead of committing a positional selector.
53
+
54
+ ## Test rules
55
+
56
+ 1. Inspect the repository's existing tests and package scripts before choosing
57
+ a test location or command.
58
+ 2. Import only from `craftdriver`, never from `craftdriver/src/...`.
59
+ 3. Use public locators and `expect(locator)` assertions.
60
+ 4. Read failures by stable `CraftdriverError.code`, then gather a fresh
61
+ snapshot and focused page evidence.
62
+ 5. Make ordinary reviewable source changes. Never hide a failure with runtime
63
+ locator repair.
64
+ 6. Always close the browser in `finally` or the repository's existing fixture.
65
+
66
+ ## Focused references
67
+
68
+ - Explore and write a test: [workflow.md](workflow.md)
69
+ - Shell commands: [cli.md](cli.md)
70
+ - TypeScript API cheatsheet: [cheatsheet.md](cheatsheet.md)
71
+ - Worked library recipes: [patterns.md](patterns.md)
72
+ - Full installed package docs: `node_modules/craftdriver/docs/`
73
+ - Optional MCP adapter: `node_modules/craftdriver/docs/mcp.md`
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "CraftDriver Browser Testing"
3
+ short_description: "Explore apps and author CraftDriver tests"
4
+ default_prompt: "Use $craftdriver to explore this app and write a durable browser test."
@@ -1,8 +1,7 @@
1
1
  # craftdriver — cheatsheet
2
2
 
3
- Compact reference for writing tests. Pair with
4
- [docs/api-reference.md](../../docs/api-reference.md) for the full export
5
- list.
3
+ Compact reference for writing tests. The full installed API reference is
4
+ `node_modules/craftdriver/docs/api-reference.md`.
6
5
 
7
6
  ## Launch & teardown
8
7
 
@@ -111,7 +110,7 @@ Codes: `NO_MATCH`, `TIMEOUT_WAITING_VISIBLE`, `TIMEOUT_WAITING_STATE`,
111
110
  `TIMEOUT_WAITING_DIALOG`, `TIMEOUT`, `EXPECT_MISMATCH`,
112
111
  `A11Y_VIOLATIONS`, `EVAL_THREW`, `EVAL_BAD_ARG`, `INVALID_ARGUMENT`,
113
112
  `UNSUPPORTED`, `STATE_INVALID`, `DRIVER_ERROR`. Full table:
114
- [docs/error-codes.md](../../docs/error-codes.md).
113
+ `node_modules/craftdriver/docs/error-codes.md`.
115
114
 
116
115
  ## Pages and contexts
117
116
 
@@ -124,6 +123,21 @@ const pages = browser.pages();
124
123
  const fresh = await browser.waitForPage(() => browser.click('a[target=_blank]'));
125
124
  ```
126
125
 
126
+ ## Reusable login state
127
+
128
+ ```ts
129
+ // Save after login (atomic file, mode 0600 where supported).
130
+ await browser.saveState('.auth/alice.json');
131
+
132
+ // BiDi: cookies + multi-origin localStorage are ready before navigation.
133
+ const reused = await Browser.launch({ storageState: '.auth/alice.json' });
134
+ await reused.navigateTo('https://app.example.com/dashboard');
135
+ ```
136
+
137
+ Classic launch rejects non-empty state. Use `Browser.launch()`, navigate to the
138
+ sole captured HTTP(S) origin, then `browser.loadState(...)`. State containing
139
+ sessionStorage always uses that active-origin flow.
140
+
127
141
  ## Network (BiDi)
128
142
 
129
143
  ```ts