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.
- package/CHANGELOG.md +104 -0
- package/README.md +157 -45
- package/bin/craftdriver.mjs +26 -0
- package/dist/cli/client.d.ts +17 -0
- package/dist/cli/client.d.ts.map +1 -0
- package/dist/cli/client.js +88 -0
- package/dist/cli/client.js.map +1 -0
- package/dist/cli/daemon.d.ts +15 -0
- package/dist/cli/daemon.d.ts.map +1 -0
- package/dist/cli/daemon.js +145 -0
- package/dist/cli/daemon.js.map +1 -0
- package/dist/cli/defaults.d.ts +10 -0
- package/dist/cli/defaults.d.ts.map +1 -0
- package/dist/cli/defaults.js +42 -0
- package/dist/cli/defaults.js.map +1 -0
- package/dist/cli/dispatcher.d.ts +28 -0
- package/dist/cli/dispatcher.d.ts.map +1 -0
- package/dist/cli/dispatcher.js +319 -0
- package/dist/cli/dispatcher.js.map +1 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +404 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init.d.ts +24 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +192 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/mcp/artifacts.d.ts +43 -0
- package/dist/cli/mcp/artifacts.d.ts.map +1 -0
- package/dist/cli/mcp/artifacts.js +104 -0
- package/dist/cli/mcp/artifacts.js.map +1 -0
- package/dist/cli/mcp/server.d.ts +34 -0
- package/dist/cli/mcp/server.d.ts.map +1 -0
- package/dist/cli/mcp/server.js +214 -0
- package/dist/cli/mcp/server.js.map +1 -0
- package/dist/cli/mcp/tools.d.ts +34 -0
- package/dist/cli/mcp/tools.d.ts.map +1 -0
- package/dist/cli/mcp/tools.js +236 -0
- package/dist/cli/mcp/tools.js.map +1 -0
- package/dist/cli/parseArgs.d.ts +37 -0
- package/dist/cli/parseArgs.d.ts.map +1 -0
- package/dist/cli/parseArgs.js +215 -0
- package/dist/cli/parseArgs.js.map +1 -0
- package/dist/cli/protocol.d.ts +34 -0
- package/dist/cli/protocol.d.ts.map +1 -0
- package/dist/cli/protocol.js +2 -0
- package/dist/cli/protocol.js.map +1 -0
- package/dist/cli/selector.d.ts +29 -0
- package/dist/cli/selector.d.ts.map +1 -0
- package/dist/cli/selector.js +100 -0
- package/dist/cli/selector.js.map +1 -0
- package/dist/cli/snapshot.d.ts +48 -0
- package/dist/cli/snapshot.d.ts.map +1 -0
- package/dist/cli/snapshot.js +179 -0
- package/dist/cli/snapshot.js.map +1 -0
- package/dist/index.d.ts +14 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/a11y.d.ts +81 -0
- package/dist/lib/a11y.d.ts.map +1 -0
- package/dist/lib/a11y.js +171 -0
- package/dist/lib/a11y.js.map +1 -0
- package/dist/lib/bidi/index.d.ts +26 -1
- package/dist/lib/bidi/index.d.ts.map +1 -1
- package/dist/lib/bidi/index.js +49 -0
- package/dist/lib/bidi/index.js.map +1 -1
- package/dist/lib/bidi/logs.d.ts +2 -2
- package/dist/lib/bidi/logs.d.ts.map +1 -1
- package/dist/lib/bidi/logs.js +2 -2
- package/dist/lib/bidi/logs.js.map +1 -1
- package/dist/lib/bidi/network.d.ts +74 -1
- package/dist/lib/bidi/network.d.ts.map +1 -1
- package/dist/lib/bidi/network.js +222 -4
- package/dist/lib/bidi/network.js.map +1 -1
- package/dist/lib/bidi/storage.d.ts +5 -3
- package/dist/lib/bidi/storage.d.ts.map +1 -1
- package/dist/lib/bidi/storage.js +14 -7
- package/dist/lib/bidi/storage.js.map +1 -1
- package/dist/lib/bidi/types.d.ts +27 -6
- package/dist/lib/bidi/types.d.ts.map +1 -1
- package/dist/lib/browser.d.ts +637 -16
- package/dist/lib/browser.d.ts.map +1 -1
- package/dist/lib/browser.js +1428 -102
- package/dist/lib/browser.js.map +1 -1
- package/dist/lib/browserContext.d.ts +486 -0
- package/dist/lib/browserContext.d.ts.map +1 -0
- package/dist/lib/browserContext.js +937 -0
- package/dist/lib/browserContext.js.map +1 -0
- package/dist/lib/builder.d.ts +3 -0
- package/dist/lib/builder.d.ts.map +1 -1
- package/dist/lib/builder.js +31 -4
- package/dist/lib/builder.js.map +1 -1
- package/dist/lib/by.d.ts +18 -2
- package/dist/lib/by.d.ts.map +1 -1
- package/dist/lib/by.js +20 -2
- package/dist/lib/by.js.map +1 -1
- package/dist/lib/chrome.d.ts +13 -1
- package/dist/lib/chrome.d.ts.map +1 -1
- package/dist/lib/chrome.js +13 -17
- package/dist/lib/chrome.js.map +1 -1
- package/dist/lib/clock.d.ts +115 -0
- package/dist/lib/clock.d.ts.map +1 -0
- package/dist/lib/clock.js +407 -0
- package/dist/lib/clock.js.map +1 -0
- package/dist/lib/driver.d.ts +43 -0
- package/dist/lib/driver.d.ts.map +1 -1
- package/dist/lib/driver.js +120 -0
- package/dist/lib/driver.js.map +1 -1
- package/dist/lib/driverManager.d.ts +25 -0
- package/dist/lib/driverManager.d.ts.map +1 -0
- package/dist/lib/driverManager.js +399 -0
- package/dist/lib/driverManager.js.map +1 -0
- package/dist/lib/elementHandle.d.ts +67 -3
- package/dist/lib/elementHandle.d.ts.map +1 -1
- package/dist/lib/elementHandle.js +209 -67
- package/dist/lib/elementHandle.js.map +1 -1
- package/dist/lib/errors.d.ts +71 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +73 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/expect.d.ts +6 -1
- package/dist/lib/expect.d.ts.map +1 -1
- package/dist/lib/expect.js +55 -37
- package/dist/lib/expect.js.map +1 -1
- package/dist/lib/firefox.d.ts +18 -0
- package/dist/lib/firefox.d.ts.map +1 -0
- package/dist/lib/firefox.js +33 -0
- package/dist/lib/firefox.js.map +1 -0
- package/dist/lib/frame.d.ts +57 -0
- package/dist/lib/frame.d.ts.map +1 -0
- package/dist/lib/frame.js +277 -0
- package/dist/lib/frame.js.map +1 -0
- package/dist/lib/keyboard.d.ts +1 -1
- package/dist/lib/keyboard.d.ts.map +1 -1
- package/dist/lib/keyboard.js +1 -1
- package/dist/lib/keyboard.js.map +1 -1
- package/dist/lib/locator.d.ts +71 -2
- package/dist/lib/locator.d.ts.map +1 -1
- package/dist/lib/locator.js +289 -31
- package/dist/lib/locator.js.map +1 -1
- package/dist/lib/mouse.d.ts +1 -1
- package/dist/lib/mouse.d.ts.map +1 -1
- package/dist/lib/mouse.js +1 -1
- package/dist/lib/mouse.js.map +1 -1
- package/dist/lib/page.d.ts +107 -0
- package/dist/lib/page.d.ts.map +1 -0
- package/dist/lib/page.js +388 -0
- package/dist/lib/page.js.map +1 -0
- package/dist/lib/tracing.d.ts +128 -0
- package/dist/lib/tracing.d.ts.map +1 -0
- package/dist/lib/tracing.js +272 -0
- package/dist/lib/tracing.js.map +1 -0
- package/dist/lib/wait.d.ts.map +1 -1
- package/dist/lib/wait.js +14 -1
- package/dist/lib/wait.js.map +1 -1
- package/dist/lib/webelement.d.ts +2 -0
- package/dist/lib/webelement.d.ts.map +1 -1
- package/dist/lib/webelement.js +18 -0
- package/dist/lib/webelement.js.map +1 -1
- package/docs/accessibility.md +177 -0
- package/docs/api-reference.md +71 -0
- package/docs/assertions.md +182 -0
- package/docs/bidi-features.md +462 -0
- package/docs/browser-api.md +673 -0
- package/docs/browser-context.md +522 -0
- package/docs/cli.md +233 -0
- package/docs/clock.md +245 -0
- package/docs/dialogs.md +143 -0
- package/docs/driver-configuration.md +64 -0
- package/docs/element-api.md +219 -0
- package/docs/emulation.md +155 -0
- package/docs/error-codes.md +67 -0
- package/docs/getting-started.md +168 -0
- package/docs/keyboard-mouse.md +250 -0
- package/docs/mcp.md +250 -0
- package/docs/mobile-emulation.md +194 -0
- package/docs/screenshots.md +146 -0
- package/docs/selectors.md +382 -0
- package/docs/session-management.md +229 -0
- package/docs/tracing.md +282 -0
- package/package.json +23 -6
- package/skills/craftdriver/SKILL.md +84 -0
- package/skills/craftdriver/cheatsheet.md +194 -0
- package/skills/craftdriver/cli.md +104 -0
- 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.
|
package/docs/dialogs.md
ADDED
|
@@ -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
|
+
```
|