craftdriver 0.1.0 → 0.2.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 (186) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/README.md +157 -45
  3. package/bin/craftdriver.mjs +26 -0
  4. package/dist/cli/client.d.ts +17 -0
  5. package/dist/cli/client.d.ts.map +1 -0
  6. package/dist/cli/client.js +88 -0
  7. package/dist/cli/client.js.map +1 -0
  8. package/dist/cli/daemon.d.ts +15 -0
  9. package/dist/cli/daemon.d.ts.map +1 -0
  10. package/dist/cli/daemon.js +145 -0
  11. package/dist/cli/daemon.js.map +1 -0
  12. package/dist/cli/defaults.d.ts +10 -0
  13. package/dist/cli/defaults.d.ts.map +1 -0
  14. package/dist/cli/defaults.js +42 -0
  15. package/dist/cli/defaults.js.map +1 -0
  16. package/dist/cli/dispatcher.d.ts +28 -0
  17. package/dist/cli/dispatcher.d.ts.map +1 -0
  18. package/dist/cli/dispatcher.js +319 -0
  19. package/dist/cli/dispatcher.js.map +1 -0
  20. package/dist/cli/index.d.ts +2 -0
  21. package/dist/cli/index.d.ts.map +1 -0
  22. package/dist/cli/index.js +404 -0
  23. package/dist/cli/index.js.map +1 -0
  24. package/dist/cli/init.d.ts +24 -0
  25. package/dist/cli/init.d.ts.map +1 -0
  26. package/dist/cli/init.js +192 -0
  27. package/dist/cli/init.js.map +1 -0
  28. package/dist/cli/mcp/artifacts.d.ts +43 -0
  29. package/dist/cli/mcp/artifacts.d.ts.map +1 -0
  30. package/dist/cli/mcp/artifacts.js +104 -0
  31. package/dist/cli/mcp/artifacts.js.map +1 -0
  32. package/dist/cli/mcp/server.d.ts +34 -0
  33. package/dist/cli/mcp/server.d.ts.map +1 -0
  34. package/dist/cli/mcp/server.js +214 -0
  35. package/dist/cli/mcp/server.js.map +1 -0
  36. package/dist/cli/mcp/tools.d.ts +34 -0
  37. package/dist/cli/mcp/tools.d.ts.map +1 -0
  38. package/dist/cli/mcp/tools.js +236 -0
  39. package/dist/cli/mcp/tools.js.map +1 -0
  40. package/dist/cli/parseArgs.d.ts +37 -0
  41. package/dist/cli/parseArgs.d.ts.map +1 -0
  42. package/dist/cli/parseArgs.js +215 -0
  43. package/dist/cli/parseArgs.js.map +1 -0
  44. package/dist/cli/protocol.d.ts +34 -0
  45. package/dist/cli/protocol.d.ts.map +1 -0
  46. package/dist/cli/protocol.js +2 -0
  47. package/dist/cli/protocol.js.map +1 -0
  48. package/dist/cli/selector.d.ts +29 -0
  49. package/dist/cli/selector.d.ts.map +1 -0
  50. package/dist/cli/selector.js +100 -0
  51. package/dist/cli/selector.js.map +1 -0
  52. package/dist/cli/snapshot.d.ts +48 -0
  53. package/dist/cli/snapshot.d.ts.map +1 -0
  54. package/dist/cli/snapshot.js +179 -0
  55. package/dist/cli/snapshot.js.map +1 -0
  56. package/dist/index.d.ts +14 -2
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +15 -1
  59. package/dist/index.js.map +1 -1
  60. package/dist/lib/a11y.d.ts +81 -0
  61. package/dist/lib/a11y.d.ts.map +1 -0
  62. package/dist/lib/a11y.js +171 -0
  63. package/dist/lib/a11y.js.map +1 -0
  64. package/dist/lib/bidi/index.d.ts +26 -1
  65. package/dist/lib/bidi/index.d.ts.map +1 -1
  66. package/dist/lib/bidi/index.js +49 -0
  67. package/dist/lib/bidi/index.js.map +1 -1
  68. package/dist/lib/bidi/logs.d.ts +2 -2
  69. package/dist/lib/bidi/logs.d.ts.map +1 -1
  70. package/dist/lib/bidi/logs.js +2 -2
  71. package/dist/lib/bidi/logs.js.map +1 -1
  72. package/dist/lib/bidi/network.d.ts +74 -1
  73. package/dist/lib/bidi/network.d.ts.map +1 -1
  74. package/dist/lib/bidi/network.js +222 -4
  75. package/dist/lib/bidi/network.js.map +1 -1
  76. package/dist/lib/bidi/storage.d.ts +5 -3
  77. package/dist/lib/bidi/storage.d.ts.map +1 -1
  78. package/dist/lib/bidi/storage.js +14 -7
  79. package/dist/lib/bidi/storage.js.map +1 -1
  80. package/dist/lib/bidi/types.d.ts +27 -6
  81. package/dist/lib/bidi/types.d.ts.map +1 -1
  82. package/dist/lib/browser.d.ts +637 -16
  83. package/dist/lib/browser.d.ts.map +1 -1
  84. package/dist/lib/browser.js +1428 -102
  85. package/dist/lib/browser.js.map +1 -1
  86. package/dist/lib/browserContext.d.ts +486 -0
  87. package/dist/lib/browserContext.d.ts.map +1 -0
  88. package/dist/lib/browserContext.js +937 -0
  89. package/dist/lib/browserContext.js.map +1 -0
  90. package/dist/lib/builder.d.ts +3 -0
  91. package/dist/lib/builder.d.ts.map +1 -1
  92. package/dist/lib/builder.js +31 -4
  93. package/dist/lib/builder.js.map +1 -1
  94. package/dist/lib/by.d.ts +18 -2
  95. package/dist/lib/by.d.ts.map +1 -1
  96. package/dist/lib/by.js +20 -2
  97. package/dist/lib/by.js.map +1 -1
  98. package/dist/lib/chrome.d.ts +13 -1
  99. package/dist/lib/chrome.d.ts.map +1 -1
  100. package/dist/lib/chrome.js +13 -17
  101. package/dist/lib/chrome.js.map +1 -1
  102. package/dist/lib/clock.d.ts +115 -0
  103. package/dist/lib/clock.d.ts.map +1 -0
  104. package/dist/lib/clock.js +407 -0
  105. package/dist/lib/clock.js.map +1 -0
  106. package/dist/lib/driver.d.ts +43 -0
  107. package/dist/lib/driver.d.ts.map +1 -1
  108. package/dist/lib/driver.js +120 -0
  109. package/dist/lib/driver.js.map +1 -1
  110. package/dist/lib/driverManager.d.ts +25 -0
  111. package/dist/lib/driverManager.d.ts.map +1 -0
  112. package/dist/lib/driverManager.js +399 -0
  113. package/dist/lib/driverManager.js.map +1 -0
  114. package/dist/lib/elementHandle.d.ts +67 -3
  115. package/dist/lib/elementHandle.d.ts.map +1 -1
  116. package/dist/lib/elementHandle.js +209 -67
  117. package/dist/lib/elementHandle.js.map +1 -1
  118. package/dist/lib/errors.d.ts +71 -0
  119. package/dist/lib/errors.d.ts.map +1 -0
  120. package/dist/lib/errors.js +73 -0
  121. package/dist/lib/errors.js.map +1 -0
  122. package/dist/lib/expect.d.ts +6 -1
  123. package/dist/lib/expect.d.ts.map +1 -1
  124. package/dist/lib/expect.js +55 -37
  125. package/dist/lib/expect.js.map +1 -1
  126. package/dist/lib/firefox.d.ts +18 -0
  127. package/dist/lib/firefox.d.ts.map +1 -0
  128. package/dist/lib/firefox.js +33 -0
  129. package/dist/lib/firefox.js.map +1 -0
  130. package/dist/lib/frame.d.ts +57 -0
  131. package/dist/lib/frame.d.ts.map +1 -0
  132. package/dist/lib/frame.js +277 -0
  133. package/dist/lib/frame.js.map +1 -0
  134. package/dist/lib/keyboard.d.ts +1 -1
  135. package/dist/lib/keyboard.d.ts.map +1 -1
  136. package/dist/lib/keyboard.js +1 -1
  137. package/dist/lib/keyboard.js.map +1 -1
  138. package/dist/lib/locator.d.ts +71 -2
  139. package/dist/lib/locator.d.ts.map +1 -1
  140. package/dist/lib/locator.js +289 -31
  141. package/dist/lib/locator.js.map +1 -1
  142. package/dist/lib/mouse.d.ts +1 -1
  143. package/dist/lib/mouse.d.ts.map +1 -1
  144. package/dist/lib/mouse.js +1 -1
  145. package/dist/lib/mouse.js.map +1 -1
  146. package/dist/lib/page.d.ts +107 -0
  147. package/dist/lib/page.d.ts.map +1 -0
  148. package/dist/lib/page.js +388 -0
  149. package/dist/lib/page.js.map +1 -0
  150. package/dist/lib/tracing.d.ts +128 -0
  151. package/dist/lib/tracing.d.ts.map +1 -0
  152. package/dist/lib/tracing.js +272 -0
  153. package/dist/lib/tracing.js.map +1 -0
  154. package/dist/lib/wait.d.ts.map +1 -1
  155. package/dist/lib/wait.js +14 -1
  156. package/dist/lib/wait.js.map +1 -1
  157. package/dist/lib/webelement.d.ts +2 -0
  158. package/dist/lib/webelement.d.ts.map +1 -1
  159. package/dist/lib/webelement.js +18 -0
  160. package/dist/lib/webelement.js.map +1 -1
  161. package/docs/accessibility.md +177 -0
  162. package/docs/api-reference.md +71 -0
  163. package/docs/assertions.md +182 -0
  164. package/docs/bidi-features.md +462 -0
  165. package/docs/browser-api.md +673 -0
  166. package/docs/browser-context.md +522 -0
  167. package/docs/cli.md +233 -0
  168. package/docs/clock.md +245 -0
  169. package/docs/dialogs.md +143 -0
  170. package/docs/driver-configuration.md +64 -0
  171. package/docs/element-api.md +219 -0
  172. package/docs/emulation.md +155 -0
  173. package/docs/error-codes.md +67 -0
  174. package/docs/getting-started.md +168 -0
  175. package/docs/keyboard-mouse.md +250 -0
  176. package/docs/mcp.md +250 -0
  177. package/docs/mobile-emulation.md +194 -0
  178. package/docs/screenshots.md +146 -0
  179. package/docs/selectors.md +382 -0
  180. package/docs/session-management.md +229 -0
  181. package/docs/tracing.md +282 -0
  182. package/package.json +23 -6
  183. package/skills/craftdriver/SKILL.md +84 -0
  184. package/skills/craftdriver/cheatsheet.md +194 -0
  185. package/skills/craftdriver/cli.md +104 -0
  186. package/skills/craftdriver/patterns.md +139 -0
package/docs/cli.md ADDED
@@ -0,0 +1,233 @@
1
+ # Command-line interface
2
+
3
+ `craftdriver` ships with a CLI binary that wraps the library for
4
+ **shell scripts, ad-hoc exploration, and AI agents** (Copilot, Claude
5
+ Code, Cursor, Codex, Gemini CLI, Goose, OpenCode, …).
6
+
7
+ ```bash
8
+ npm install craftdriver
9
+ npx craftdriver --help
10
+ ```
11
+
12
+ The CLI uses the same Browser API as the library, so anything you can
13
+ script in TypeScript you can also drive from `bash` — but the CLI tunes
14
+ defaults for **fast, fail-fast probing** instead of stable test runs.
15
+
16
+ ## Quick start
17
+
18
+ Two-terminal workflow:
19
+
20
+ ```bash
21
+ # terminal 1 — keep a long-lived browser around
22
+ npx craftdriver daemon start
23
+
24
+ # terminal 2 — drive it
25
+ npx craftdriver go http://127.0.0.1:8080/login.html
26
+ npx craftdriver fill '#username' alice
27
+ npx craftdriver fill '#password' hunter2
28
+ npx craftdriver click 'button[type=submit]'
29
+ npx craftdriver wait '#result' --state visible
30
+ npx craftdriver text '#result'
31
+
32
+ npx craftdriver daemon stop
33
+ ```
34
+
35
+ Each call opens a fresh connection to the daemon and exits — but the
36
+ **browser, page, and cookies all persist** between calls. That makes
37
+ the CLI feel like a REPL for the browser.
38
+
39
+ If you don't want a daemon (sandboxed cloud agents, one-shot scripts),
40
+ pipe a script through `--ephemeral` and the CLI launches a short-lived
41
+ browser for the whole script:
42
+
43
+ ```bash
44
+ printf 'go http://127.0.0.1:8080/login.html
45
+ fill "#username" alice
46
+ fill "#password" hunter2
47
+ click "button[type=submit]"
48
+ text "#result"
49
+ ' | npx craftdriver --ephemeral
50
+ ```
51
+
52
+ ## Commands
53
+
54
+ ```
55
+ craftdriver go <url>
56
+ craftdriver find <selector> [--all] [--limit N] [--offset M]
57
+ craftdriver click <selector>
58
+ craftdriver fill <selector> <value>
59
+ craftdriver press <key> [selector]
60
+ craftdriver hover <selector>
61
+
62
+ craftdriver text [selector] [--limit N]
63
+ craftdriver attr <selector> <name>
64
+ craftdriver value <selector>
65
+ craftdriver is visible|enabled|checked <selector>
66
+
67
+ craftdriver wait <selector> [--state visible|hidden|attached|detached] [--timeout ms]
68
+ craftdriver wait load [--state load|domcontentloaded|networkidle]
69
+ craftdriver exists <selector> # 0-wait probe; exit 0 if any match
70
+
71
+ craftdriver pages
72
+ craftdriver snapshot # sanitized DOM summary with refs
73
+ craftdriver screenshot [-o file.png] [--full-page] [--selector S]
74
+ craftdriver eval <js> # last resort
75
+ craftdriver back | forward | reload | status | quit
76
+
77
+ craftdriver daemon start|status|stop
78
+ ```
79
+
80
+ Run `craftdriver --help` for the full list.
81
+
82
+ ## Selector syntax
83
+
84
+ CSS is the default. Switch kind with a `prefix=value` form:
85
+
86
+ | Prefix | Maps to | Example |
87
+ | --------------- | ---------------------------------------- | ------------------------------------ |
88
+ | _none_, `css=` | `By.css` | `'.product-list li'` |
89
+ | `xpath=` | `By.xpath` | `'xpath=//button[1]'` |
90
+ | `role=` | `By.role` (+ `[name=...]` for the name) | `'role=button[name=Submit]'` |
91
+ | `text=` | `By.text` (exact) | `'text=Sign In'` |
92
+ | `text*=` | `By.partialText` | `'text*=Sign'` |
93
+ | `label=` | `By.labelText` | `'label=Email'` |
94
+ | `placeholder=` | `By.placeholder` | `'placeholder=name@example.com'` |
95
+ | `alt=` | `By.altText` | `'alt=Logo'` |
96
+ | `title=` | `By.title` | `'title=Help'` |
97
+ | `testid=` | `By.testId` | `'testid=login-btn'` |
98
+ | `id=` | `By.id` | `'id=submit'` |
99
+ | `name=` | `By.name` | `'name=email'` |
100
+ | `ref=` | snapshot ref (`craftdriver snapshot`) | `'ref=e5'` |
101
+
102
+ Anything else is treated as a CSS selector, so attribute selectors with
103
+ `=` inside (e.g. `'button[type=submit]'`) work as expected.
104
+
105
+ ## Snapshot — sanitized DOM with refs
106
+
107
+ `craftdriver snapshot` returns one line per visible interactive element
108
+ on the active page, with a stable ref (`e1`, `e2`, …) that you can
109
+ use as a selector for the next command:
110
+
111
+ ```bash
112
+ $ craftdriver snapshot
113
+ page: Login — http://127.0.0.1:8080/login.html
114
+ e1: heading "Login"
115
+ e2: form "Username Password Sign in" #login-form
116
+ e3: label "Username"
117
+ e4: textbox "Username" #username
118
+ e5: label "Password"
119
+ e6: textbox "Password" #password
120
+ e7: button "Sign in" #submit
121
+
122
+ $ craftdriver fill ref=e4 alice
123
+ $ craftdriver fill ref=e6 hunter2
124
+ $ craftdriver click ref=e7
125
+ ```
126
+
127
+ Refs are recomputed on every `snapshot` call and invalidated on
128
+ navigation. A stale ref just returns `NO_MATCH` — take a fresh
129
+ snapshot.
130
+
131
+ Internally `ref=eN` resolves to a CSS attribute selector
132
+ (`[data-craftdriver-ref="eN"]`); auto-waiting works unchanged.
133
+
134
+ ## Output: pretty on a TTY, JSON when piped
135
+
136
+ - TTY: human-readable text, one line per result for `find` / `pages`.
137
+ - Piped or redirected: `{ "ok": true, "result": … }` per line.
138
+ - Force either with `--json` or `--pretty`.
139
+
140
+ Errors carry the same machine-readable `code` field as the library, plus
141
+ an optional one-line `hint:`. See [error-codes.md](./error-codes.md).
142
+
143
+ ```bash
144
+ $ npx craftdriver find '#nope'
145
+ error: find: no element matches css selector=#nope
146
+ code: NO_MATCH
147
+ ```
148
+
149
+ ## Exit codes
150
+
151
+ | Code | Meaning |
152
+ | ---- | ----------------------------------------------------------------------- |
153
+ | `0` | success (or `exists` matched at least one element) |
154
+ | `1` | assertion / timeout / `NO_MATCH` / `exists` matched zero elements |
155
+ | `2` | usage error (missing argument, unknown command) |
156
+
157
+ ## Fail-fast defaults
158
+
159
+ The library auto-waits up to **30 s** because tests want stability. The
160
+ CLI lowers that to **5 s** because agents probe with guesses and should
161
+ learn from failures fast.
162
+
163
+ - Override per call with `--timeout <ms>`.
164
+ - Override globally with `CRAFTDRIVER_AGENT_TIMEOUT=2000`.
165
+ - Use `exists` as a 0-wait probe before `click` / `wait` when you're not
166
+ sure a selector matches.
167
+
168
+ ## Daemon details
169
+
170
+ - Socket: `~/.craftdriver/sock` (override with `CRAFTDRIVER_SOCKET`).
171
+ - PID file: `~/.craftdriver/pid` (override with `CRAFTDRIVER_PID`).
172
+ - Wire: line-delimited JSON, one request per connection.
173
+ - First request after `daemon start` triggers the browser launch; later
174
+ requests reuse the same browser, page and cookies.
175
+ - `craftdriver daemon status` reports PID + active page URL.
176
+ - `craftdriver daemon stop` cleans up the socket and PID file.
177
+
178
+ The CLI also auto-starts a daemon for you on the first command if none
179
+ is running — `daemon start` is only required when you want to control
180
+ the timing (or to choose a non-default browser):
181
+
182
+ ```bash
183
+ npx craftdriver daemon start --browser firefox
184
+ ```
185
+
186
+ ## When to use the CLI vs. the library
187
+
188
+ - **Library** — write a test suite. Stable, 30 s auto-waits, full TS
189
+ types, runs under vitest / jest / playwright-test.
190
+ - **CLI** — exploration, debugging, agent-driven loops, REPL-style
191
+ poking at a real page from your shell.
192
+
193
+ Both share the same underlying Browser API and the same error codes, so
194
+ findings transfer directly between the two.
195
+
196
+ ## Teach your AI assistant (`craftdriver init`)
197
+
198
+ For every supported assistant, the CLI can drop an opinionated rules
199
+ file into the **current project** so the assistant picks up
200
+ craftdriver conventions (selector preference, auto-waiting, error
201
+ codes, CLI usage) on every turn:
202
+
203
+ ```bash
204
+ npx craftdriver init copilot # .github/copilot-instructions.md
205
+ npx craftdriver init claude # CLAUDE.md
206
+ npx craftdriver init cursor # .cursor/rules/craftdriver.mdc
207
+ npx craftdriver init gemini # GEMINI.md
208
+ npx craftdriver init agents # AGENTS.md (Codex, OpenCode, Aider, Amp, Cursor)
209
+ npx craftdriver init all # every file above
210
+ ```
211
+
212
+ Files are per-project (commit them to git so the team's agents share
213
+ the rules). Pre-existing files are skipped unless you pass `--force`;
214
+ use `--dry-run` to see what would be written. The body is identical
215
+ across flavors, only the file name and any tool-specific header
216
+ (e.g. Cursor's `.mdc` frontmatter) differ.
217
+
218
+ ## Skill pack
219
+
220
+ For agents that load skills explicitly (Claude Code's Skills system,
221
+ Copilot agent customization, custom orchestrators), the npm tarball
222
+ ships a tiered skill pack under `skills/craftdriver/`:
223
+
224
+ | File | Purpose |
225
+ | ------------------------------------------------------------- | ------------------------------------------------------------------------ |
226
+ | [`SKILL.md`](../skills/craftdriver/SKILL.md) | Always-on, ≤ 500 tokens. Selector order, error-code-first, auto-wait. |
227
+ | [`cheatsheet.md`](../skills/craftdriver/cheatsheet.md) | Command-by-command reference for writing tests. |
228
+ | [`patterns.md`](../skills/craftdriver/patterns.md) | Worked recipes (login, upload, network-wait, a11y, tracing, clock). |
229
+ | [`cli.md`](../skills/craftdriver/cli.md) | Agent-facing CLI reference. |
230
+
231
+ Point your agent at `node_modules/craftdriver/skills/craftdriver/SKILL.md`
232
+ or copy it into your project. The other files are referenced from
233
+ `SKILL.md` and loaded on demand.
package/docs/clock.md ADDED
@@ -0,0 +1,245 @@
1
+ # Virtual Clock
2
+
3
+ Test idle timeouts, trial expirations, and debounced inputs without `sleep()`.
4
+
5
+ ## Why you need this
6
+
7
+ Three tests that are impossible — or flaky at best — without clock control:
8
+
9
+ 1. **Auto-logout after 15 minutes idle.** Without clock control you either
10
+ wait 15 real minutes in CI, or stub `Date` inside the app code (the test no
11
+ longer reflects production). With it: jump 15 min, assert the login modal.
12
+
13
+ 2. **Trial banner that flips at midnight.** Assert both "expires today" before
14
+ midnight and "expired" right after, in the same CI run, deterministically.
15
+
16
+ 3. **Debounced search input.** Advance exactly 299 ms (no request), then 2 ms
17
+ more (exactly one request). No `setTimeout(400)` flake.
18
+
19
+ ---
20
+
21
+ ## Quick-start examples
22
+
23
+ ### 1 — Idle logout after 15 minutes
24
+
25
+ ```ts
26
+ import { Browser } from 'craftdriver';
27
+
28
+ const browser = await Browser.launch();
29
+ await browser.clock.install({ time: '2026-01-01T09:00:00Z' });
30
+ await browser.navigateTo('http://localhost:3000/dashboard');
31
+ await browser.clock.fastForward('15:01');
32
+ await browser.expect('#login-modal').toBeVisible();
33
+ await browser.quit();
34
+ ```
35
+
36
+ ### 2 — Trial banner flipping at midnight
37
+
38
+ ```ts
39
+ import { Browser } from 'craftdriver';
40
+
41
+ const browser = await Browser.launch();
42
+
43
+ // Before midnight — banner says "expires today"
44
+ await browser.clock.setFixedTime('2026-06-15T23:59:00Z');
45
+ await browser.navigateTo('http://localhost:3000/billing');
46
+ await browser.expect('#trial-banner').toContainText('expires today');
47
+
48
+ // After midnight — banner says "expired"
49
+ await browser.clock.setFixedTime('2026-06-16T00:00:01Z');
50
+ await browser.reload();
51
+ await browser.expect('#trial-banner').toContainText('expired');
52
+
53
+ await browser.quit();
54
+ ```
55
+
56
+ ### 3 — Debounced search input
57
+
58
+ ```ts
59
+ import { Browser } from 'craftdriver';
60
+
61
+ const browser = await Browser.launch();
62
+ await browser.clock.install();
63
+ await browser.navigateTo('http://localhost:3000/search');
64
+ await browser.fill('#q', 'lap');
65
+
66
+ await browser.clock.tick(299); // debounce hasn't fired yet
67
+ // assert: no /search network request
68
+
69
+ await browser.clock.tick(2); // total 301 ms — debounce fires
70
+ // assert: exactly one /search?q=lap request
71
+
72
+ await browser.quit();
73
+ ```
74
+
75
+ ---
76
+
77
+ ## Method reference
78
+
79
+ | Method | When to use |
80
+ |--------|-------------|
81
+ | `clock.install(opts?)` | Full fake-timer suite: `Date`, `performance.now`, `setTimeout`, `setInterval`, `rAF`. Use when you need `tick()`. |
82
+ | `clock.uninstall()` | Restore real globals and remove the preload script. |
83
+ | `clock.tick(ms)` | Advance virtual time by `ms` milliseconds and fire all due timers. |
84
+ | `clock.fastForward(duration)` | Like `tick()` but also accepts `"MM:SS"` / `"HH:MM:SS"` strings. |
85
+ | `clock.setFixedTime(time)` | Freeze `Date.now()` at a point in time. Does **not** fake timers. |
86
+ | `clock.setSystemTime(time)` | Move the virtual clock to `time` without firing timers. Requires `install()`. |
87
+ | `clock.runFor(ms)` | Like `tick()` but yields between frames so async/microtask callbacks resolve. |
88
+
89
+ ### `clock.install(options?)`
90
+
91
+ ```ts
92
+ await browser.clock.install();
93
+ await browser.clock.install({ time: '2026-01-01T09:00:00Z' });
94
+ await browser.clock.install({ time: 1_735_689_600_000 }); // ms since epoch
95
+ await browser.clock.install({ time: new Date('2026-01-01') });
96
+ ```
97
+
98
+ Installs fake implementations of:
99
+ - `Date` constructor and `Date.now()`
100
+ - `performance.now()`
101
+ - `setTimeout` / `clearTimeout`
102
+ - `setInterval` / `clearInterval`
103
+ - `requestAnimationFrame` / `cancelAnimationFrame`
104
+
105
+ Timers only fire when you explicitly call `tick()`, `fastForward()`, or
106
+ `runFor()`. Also registers a **preload script** so the fake clock is
107
+ automatically re-installed on every subsequent navigation for the duration of
108
+ the browser session.
109
+
110
+ **`time`** defaults to the real current time (`Date.now()`) if omitted.
111
+
112
+ ### `clock.uninstall()`
113
+
114
+ ```ts
115
+ await browser.clock.uninstall();
116
+ ```
117
+
118
+ Restores `Date`, `setTimeout`, `setInterval`, and all other faked globals to
119
+ their originals. Removes the preload script so future navigations use the real
120
+ clock again.
121
+
122
+ ### `clock.tick(ms)`
123
+
124
+ ```ts
125
+ await browser.clock.tick(500); // advance 500 ms
126
+ await browser.clock.tick(0); // flush zero-delay timers
127
+ ```
128
+
129
+ Advances the virtual clock by `ms` milliseconds. Every timer whose deadline
130
+ falls within the window is fired in order. `setInterval` timers reschedule
131
+ themselves automatically.
132
+
133
+ Requires `install()`.
134
+
135
+ ### `clock.fastForward(duration)`
136
+
137
+ ```ts
138
+ await browser.clock.fastForward(60_000); // 60 seconds
139
+ await browser.clock.fastForward('15:00'); // 15 minutes
140
+ await browser.clock.fastForward('01:30:00'); // 1 hour 30 minutes
141
+ ```
142
+
143
+ Same as `tick()` but accepts a human-readable duration string.
144
+
145
+ - **`"MM:SS"`** — minutes and seconds (numbers can exceed 59: `"90:00"` = 90 min)
146
+ - **`"HH:MM:SS"`** — hours, minutes, seconds
147
+
148
+ Requires `install()`.
149
+
150
+ ### `clock.setFixedTime(time)`
151
+
152
+ ```ts
153
+ await browser.clock.setFixedTime('2026-06-15T23:59:00Z');
154
+ await browser.clock.setFixedTime(1_750_032_540_000);
155
+ await browser.clock.setFixedTime(new Date('2026-06-15T23:59:00Z'));
156
+ ```
157
+
158
+ Freezes `Date.now()` and `new Date()` at the given instant. Does **not**
159
+ install fake `setTimeout` or `setInterval` — real timers continue to fire.
160
+
161
+ Use this for simple date-dependent rendering where you don't need to control
162
+ timer execution.
163
+
164
+ Also registers a preload script so the fixed date persists across navigations.
165
+
166
+ ### `clock.setSystemTime(time)`
167
+
168
+ ```ts
169
+ await browser.clock.install({ time: 0 });
170
+ await browser.clock.setSystemTime(Date.UTC(2026, 5, 16)); // jump to 2026-06-16
171
+ await browser.clock.tick(1000); // continue from here
172
+ ```
173
+
174
+ Moves the virtual clock to `time` without advancing the timer queue or firing
175
+ any callbacks. Subsequent `tick()` calls continue from the new position.
176
+
177
+ Requires `install()`.
178
+
179
+ ### `clock.runFor(ms)`
180
+
181
+ ```ts
182
+ await browser.clock.runFor(1000);
183
+ ```
184
+
185
+ Like `tick()` but yields between each ~16 ms frame so that microtask callbacks
186
+ (e.g. `Promise.then()` chained to a timer) resolve in the correct order before
187
+ the next frame is processed.
188
+
189
+ Requires `install()`.
190
+
191
+ ---
192
+
193
+ ## Accepted time formats
194
+
195
+ All methods that take a `time` argument accept:
196
+
197
+ | Type | Example | Notes |
198
+ |------|---------|-------|
199
+ | `number` | `1_750_032_540_000` | Milliseconds since Unix epoch |
200
+ | `string` | `'2026-06-15T23:59:00Z'` | Anything `new Date(string)` accepts |
201
+ | `Date` | `new Date('2026-01-01')` | Used as-is |
202
+
203
+ `fastForward` additionally accepts a **duration** string (`"MM:SS"` or
204
+ `"HH:MM:SS"`), not an absolute point in time.
205
+
206
+ ---
207
+
208
+ ## Gotchas
209
+
210
+ ### What is and isn't faked
211
+
212
+ After `install()`:
213
+
214
+ | Faked ✓ | Not faked ✗ |
215
+ |---------|------------|
216
+ | `Date.now()` | `Date.prototype.toLocaleString` formatting |
217
+ | `new Date()` with no args | `Intl.DateTimeFormat` time zone offsets |
218
+ | `performance.now()` | Real network request timing |
219
+ | `setTimeout` / `clearTimeout` | `Worker` timers |
220
+ | `setInterval` / `clearInterval` | `SharedWorker` / `ServiceWorker` timers |
221
+ | `requestAnimationFrame` | CSS animation timing |
222
+
223
+ After `setFixedTime()` only `Date.now()`, `new Date()`, and `performance.now()`
224
+ are affected. All timers use real wall time.
225
+
226
+ ### Navigation
227
+
228
+ Both `install()` and `setFixedTime()` register a BiDi preload script that
229
+ re-applies the fake clock automatically on every navigation. You don't need to
230
+ call them again after `navigateTo()` or `reload()`.
231
+
232
+ `uninstall()` removes the preload script. After uninstalling, future
233
+ navigations use the real clock.
234
+
235
+ ### Interaction with real network requests
236
+
237
+ Clock control affects only in-page JavaScript. Real HTTP requests still run at
238
+ wall-clock speed. If your test asserts on a network response, make sure the
239
+ request is made (i.e. a timer fires and calls `fetch()`) before asserting on the
240
+ response.
241
+
242
+ ### install() is idempotent
243
+
244
+ Calling `install()` a second time resets the virtual time and the timer queue.
245
+ Any timers registered before the second `install()` are discarded.
@@ -0,0 +1,143 @@
1
+ # Dialogs
2
+
3
+ JavaScript dialogs (`alert`, `confirm`, `prompt`, `beforeunload`)
4
+ appear as native browser modals — they block the page until the user
5
+ or the automation handles them.
6
+
7
+ > **craftdriver's stance: dialogs block until handled.**
8
+ > If a dialog opens and nothing handles it, the action that triggered
9
+ > it will time out. This is intentional. Playwright silently
10
+ > auto-dismisses unhandled dialogs; craftdriver promotes them to loud
11
+ > failures so unexpected dialogs surface as broken tests rather than
12
+ > passing tests against a stuck browser.
13
+
14
+ You handle a dialog one of three ways, depending on the shape of the
15
+ test:
16
+
17
+ | API | When to use |
18
+ | ------------------------- | -------------------------------------------------------- |
19
+ | `browser.waitForDialog()` | One-shot — you know exactly which action triggers it. |
20
+ | `browser.onDialog(...)` | Persistent — multiple dialogs in a workflow, or "accept all". |
21
+ | `browser.acceptDialog()` / `browser.dismissDialog()` | You already raced past the dialog and want to handle whatever is open. |
22
+
23
+ ## `waitForDialog(opts?)` — one-shot wait
24
+
25
+ Returns a `Promise<Dialog>` that resolves the next time a dialog opens.
26
+ Pair it with the triggering action in `Promise.all` so both run
27
+ concurrently:
28
+
29
+ ```typescript
30
+ // confirm()
31
+ const [, dialog] = await Promise.all([
32
+ browser.click('#confirm-btn'),
33
+ browser.waitForDialog(),
34
+ ]);
35
+
36
+ console.log(dialog.type()); // 'confirm'
37
+ console.log(dialog.message()); // 'Are you sure?'
38
+ await dialog.accept(); // continue
39
+
40
+ // prompt() — pass text to fill the input first
41
+ const [, prompt] = await Promise.all([
42
+ browser.click('#prompt-btn'),
43
+ browser.waitForDialog(),
44
+ ]);
45
+ console.log(prompt.defaultValue()); // pre-filled default
46
+ await prompt.accept('my answer');
47
+
48
+ // alert()
49
+ const [, alert] = await Promise.all([
50
+ browser.click('#alert-btn'),
51
+ browser.waitForDialog(),
52
+ ]);
53
+ await alert.dismiss();
54
+ ```
55
+
56
+ `waitForDialog` rejects with a timeout error if no dialog appears
57
+ within `opts.timeout` (default: the browser's default timeout).
58
+
59
+ ## `onDialog(handler)` — persistent handler
60
+
61
+ Register a handler that fires for **every** dialog until you call the
62
+ returned `off()` function. Use it when a workflow triggers multiple
63
+ dialogs, or when you want a blanket "accept everything" during a noisy
64
+ sequence:
65
+
66
+ ```typescript
67
+ const off = browser.onDialog(async (dialog) => {
68
+ await dialog.accept();
69
+ });
70
+
71
+ await runNoisyWorkflow();
72
+ off(); // stop listening — subsequent dialogs are loud again
73
+ ```
74
+
75
+ `onDialog` does not retroactively handle dialogs that opened before
76
+ registration; pair it with `acceptDialog` / `dismissDialog` for those.
77
+
78
+ ## Imperative `acceptDialog` / `dismissDialog`
79
+
80
+ When you cannot wrap the triggering action in `Promise.all` (e.g.
81
+ multiple actions race the dialog, or you handle a dialog inside a
82
+ catch block), use the imperative API to act on whichever dialog is
83
+ currently open:
84
+
85
+ ```typescript
86
+ await browser.acceptDialog(); // click OK
87
+ await browser.acceptDialog('value'); // fill a prompt, then accept
88
+ await browser.dismissDialog(); // click Cancel / close
89
+ const text = await browser.getDialogMessage();
90
+ ```
91
+
92
+ These map directly onto W3C Classic alert endpoints
93
+ (`/alert/accept`, `/alert/dismiss`, `/alert/text`) and throw if no
94
+ dialog is currently open.
95
+
96
+ ## `Dialog` interface
97
+
98
+ | Member | Description |
99
+ | ---------------------------- | -------------------------------------------------------------------------- |
100
+ | `dialog.type()` | `'alert'` \| `'confirm'` \| `'prompt'` \| `'beforeunload'` |
101
+ | `dialog.message()` | Text shown in the dialog. |
102
+ | `dialog.defaultValue()` | Pre-filled value for prompts; empty string for other types. |
103
+ | `await dialog.accept(text?)` | Click OK. For prompts, optionally provide the input text first. |
104
+ | `await dialog.dismiss()` | Click Cancel / close. |
105
+
106
+ ## Recipes
107
+
108
+ ### Accept everything during a flow
109
+
110
+ ```typescript
111
+ const off = browser.onDialog((d) => d.accept());
112
+ try {
113
+ await runFlow();
114
+ } finally {
115
+ off();
116
+ }
117
+ ```
118
+
119
+ ### Assert on a dialog and dismiss
120
+
121
+ ```typescript
122
+ const [, dialog] = await Promise.all([
123
+ browser.click('#delete'),
124
+ browser.waitForDialog(),
125
+ ]);
126
+ expect(dialog.type()).toBe('confirm');
127
+ expect(dialog.message()).toContain('cannot be undone');
128
+ await dialog.dismiss();
129
+ ```
130
+
131
+ ### `beforeunload` confirmation
132
+
133
+ `beforeunload` arrives like any other dialog. Accept it to allow the
134
+ navigation, dismiss it to keep the page:
135
+
136
+ ```typescript
137
+ const [, dialog] = await Promise.all([
138
+ browser.navigateTo('/somewhere-else'),
139
+ browser.waitForDialog(),
140
+ ]);
141
+ expect(dialog.type()).toBe('beforeunload');
142
+ await dialog.accept();
143
+ ```
@@ -0,0 +1,64 @@
1
+ # Driver Configuration
2
+
3
+ Craftdriver resolves the WebDriver binary through a chain — first match wins:
4
+
5
+ | Step | Source |
6
+ |---|---|
7
+ | 1 | `driverPath` option in `Browser.launch()` |
8
+ | 2 | `CRAFTDRIVER_CHROMEDRIVER_PATH` / `CRAFTDRIVER_GECKODRIVER_PATH` env var |
9
+ | 3 | `CRAFTDRIVER_DRIVER_PATH` env var (generic fallback for either browser) |
10
+ | 4 | `chromedriver` / `geckodriver` in `node_modules/.bin` |
11
+ | 5 | `chromedriver` / `geckodriver` on `PATH` |
12
+ | 6 | **Auto-download from Chrome for Testing / GitHub** ← the zero-config default |
13
+
14
+ Downloaded drivers are cached in `~/.cache/craftdriver` and reused as long as
15
+ your browser version doesn't change. Only the driver binary is downloaded,
16
+ never the browser itself.
17
+
18
+ ## Environment variables
19
+
20
+ | Variable | Description | Default |
21
+ |---|---|---|
22
+ | `CRAFTDRIVER_CHROMEDRIVER_PATH` | Absolute path to a chromedriver binary | — |
23
+ | `CRAFTDRIVER_GECKODRIVER_PATH` | Absolute path to a geckodriver binary | — |
24
+ | `CRAFTDRIVER_DRIVER_PATH` | Generic fallback path (either browser) | — |
25
+ | `CRAFTDRIVER_CACHE_DIR` | Directory for cached driver downloads | `~/.cache/craftdriver` |
26
+ | `CRAFTDRIVER_OFFLINE` | Set to `1` to disable all network calls | — |
27
+ | `CRAFTDRIVER_DRIVER_TTL` | Geckodriver freshness check interval (seconds) | `86400` (24 h) |
28
+
29
+ ## Examples
30
+
31
+ ```bash
32
+ # Pin a specific chromedriver
33
+ CRAFTDRIVER_CHROMEDRIVER_PATH=/usr/bin/chromedriver npm test
34
+
35
+ # Pin a specific geckodriver
36
+ CRAFTDRIVER_GECKODRIVER_PATH=/usr/local/bin/geckodriver npm test
37
+
38
+ # Never make a network call (requires a local driver to exist in steps 1–5)
39
+ CRAFTDRIVER_OFFLINE=1 npm test
40
+
41
+ # Change the cache location
42
+ CRAFTDRIVER_CACHE_DIR=/tmp/my-driver-cache npm test
43
+
44
+ # Re-check geckodriver more frequently (every hour instead of 24 h)
45
+ CRAFTDRIVER_DRIVER_TTL=3600 npm test
46
+ ```
47
+
48
+ ## Pinning via code
49
+
50
+ For tighter control (custom port, extra driver flags), pass a `ChromeService`
51
+ or `FirefoxService` directly to `Browser.launch()`:
52
+
53
+ ```typescript
54
+ import { Browser, ChromeService, FirefoxService } from 'craftdriver';
55
+
56
+ const browser = await Browser.launch({
57
+ browserName: 'chrome',
58
+ chromeService: new ChromeService({
59
+ binaryPath: '/opt/chromedriver/chromedriver',
60
+ port: 9515,
61
+ args: ['--log-level=ALL'],
62
+ }),
63
+ });
64
+ ```