craftdriver 0.2.2 → 1.0.1

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 (94) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/dist/cli/mcp/server.js +16 -3
  3. package/dist/cli/mcp/server.js.map +1 -1
  4. package/dist/lib/bidi/connection.d.ts +1 -1
  5. package/dist/lib/bidi/connection.d.ts.map +1 -1
  6. package/dist/lib/bidi/connection.js +3 -2
  7. package/dist/lib/bidi/connection.js.map +1 -1
  8. package/dist/lib/bidi/index.d.ts +40 -3
  9. package/dist/lib/bidi/index.d.ts.map +1 -1
  10. package/dist/lib/bidi/index.js +55 -18
  11. package/dist/lib/bidi/index.js.map +1 -1
  12. package/dist/lib/bidi/logs.d.ts.map +1 -1
  13. package/dist/lib/bidi/logs.js +7 -2
  14. package/dist/lib/bidi/logs.js.map +1 -1
  15. package/dist/lib/bidi/network.d.ts +9 -0
  16. package/dist/lib/bidi/network.d.ts.map +1 -1
  17. package/dist/lib/bidi/network.js +26 -11
  18. package/dist/lib/bidi/network.js.map +1 -1
  19. package/dist/lib/browser.d.ts +42 -4
  20. package/dist/lib/browser.d.ts.map +1 -1
  21. package/dist/lib/browser.js +140 -62
  22. package/dist/lib/browser.js.map +1 -1
  23. package/dist/lib/browserContext.d.ts +7 -2
  24. package/dist/lib/browserContext.d.ts.map +1 -1
  25. package/dist/lib/browserContext.js +15 -6
  26. package/dist/lib/browserContext.js.map +1 -1
  27. package/dist/lib/builder.d.ts +2 -1
  28. package/dist/lib/builder.d.ts.map +1 -1
  29. package/dist/lib/builder.js +35 -5
  30. package/dist/lib/builder.js.map +1 -1
  31. package/dist/lib/clickFastPath.d.ts +7 -0
  32. package/dist/lib/clickFastPath.d.ts.map +1 -0
  33. package/dist/lib/clickFastPath.js +39 -0
  34. package/dist/lib/clickFastPath.js.map +1 -0
  35. package/dist/lib/driver.d.ts.map +1 -1
  36. package/dist/lib/driver.js +21 -6
  37. package/dist/lib/driver.js.map +1 -1
  38. package/dist/lib/driverManager.d.ts +6 -0
  39. package/dist/lib/driverManager.d.ts.map +1 -1
  40. package/dist/lib/driverManager.js +97 -22
  41. package/dist/lib/driverManager.js.map +1 -1
  42. package/dist/lib/elementHandle.d.ts.map +1 -1
  43. package/dist/lib/elementHandle.js +7 -2
  44. package/dist/lib/elementHandle.js.map +1 -1
  45. package/dist/lib/expect.d.ts.map +1 -1
  46. package/dist/lib/expect.js +91 -229
  47. package/dist/lib/expect.js.map +1 -1
  48. package/dist/lib/firefox.d.ts.map +1 -1
  49. package/dist/lib/firefox.js +2 -1
  50. package/dist/lib/firefox.js.map +1 -1
  51. package/dist/lib/frame.d.ts.map +1 -1
  52. package/dist/lib/frame.js +7 -7
  53. package/dist/lib/frame.js.map +1 -1
  54. package/dist/lib/http.d.ts +7 -0
  55. package/dist/lib/http.d.ts.map +1 -1
  56. package/dist/lib/http.js +112 -3
  57. package/dist/lib/http.js.map +1 -1
  58. package/dist/lib/loadState.d.ts +23 -0
  59. package/dist/lib/loadState.d.ts.map +1 -0
  60. package/dist/lib/loadState.js +25 -0
  61. package/dist/lib/loadState.js.map +1 -0
  62. package/dist/lib/locator.d.ts +2 -0
  63. package/dist/lib/locator.d.ts.map +1 -1
  64. package/dist/lib/locator.js +19 -6
  65. package/dist/lib/locator.js.map +1 -1
  66. package/dist/lib/mouse.d.ts.map +1 -1
  67. package/dist/lib/mouse.js +2 -1
  68. package/dist/lib/mouse.js.map +1 -1
  69. package/dist/lib/page.d.ts +14 -1
  70. package/dist/lib/page.d.ts.map +1 -1
  71. package/dist/lib/page.js +66 -15
  72. package/dist/lib/page.js.map +1 -1
  73. package/dist/lib/service.d.ts +1 -0
  74. package/dist/lib/service.d.ts.map +1 -1
  75. package/dist/lib/service.js +21 -3
  76. package/dist/lib/service.js.map +1 -1
  77. package/dist/lib/timing.d.ts +104 -0
  78. package/dist/lib/timing.d.ts.map +1 -0
  79. package/dist/lib/timing.js +108 -0
  80. package/dist/lib/timing.js.map +1 -0
  81. package/dist/lib/wait.d.ts.map +1 -1
  82. package/dist/lib/wait.js +13 -6
  83. package/dist/lib/wait.js.map +1 -1
  84. package/docs/bidi-features.md +10 -2
  85. package/docs/browser-api.md +2 -1
  86. package/docs/browser-context.md +11 -14
  87. package/docs/dialogs.md +14 -3
  88. package/docs/driver-configuration.md +126 -8
  89. package/docs/emulation.md +8 -4
  90. package/docs/error-codes.md +9 -4
  91. package/docs/keyboard-mouse.md +20 -7
  92. package/docs/session-management.md +1 -1
  93. package/package.json +4 -1
  94. package/skills/craftdriver/SKILL.md +3 -2
@@ -7,13 +7,25 @@ Craftdriver resolves the WebDriver binary through a chain — first match wins:
7
7
  | 1 | `driverPath` option in `Browser.launch()` |
8
8
  | 2 | `CRAFTDRIVER_CHROMEDRIVER_PATH` / `CRAFTDRIVER_GECKODRIVER_PATH` env var |
9
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 |
10
+ | 4 | Legacy/Selenium-compatible env vars: `CHROMEDRIVER_PATH`, `SE_CHROMEDRIVER` (chromedriver) or `GECKODRIVER_PATH`, `GECKODRIVER_FILEPATH`, `SE_GECKODRIVER` (geckodriver) |
11
+ | 5 | **Cached auto-resolution** the path a previous auto-resolve settled on, reused within the `CRAFTDRIVER_DRIVER_TTL` window |
12
+ | 6 | `chromedriver` / `geckodriver` in `node_modules/.bin` |
13
+ | 7 | `chromedriver` / `geckodriver` on `PATH` |
14
+ | 8 | **Auto-download from Chrome for Testing / GitHub** ← the zero-config default |
13
15
 
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.
16
+ Downloaded drivers are cached in `~/.cache/craftdriver`, and so is the
17
+ *resolution itself* which driver path to use. Within the
18
+ `CRAFTDRIVER_DRIVER_TTL` window (default 24 h), craftdriver reuses the
19
+ resolved path directly and skips the system-browser probes it would
20
+ otherwise run on **every** launch: launching the browser binary just to read
21
+ its version string, and a `PATH` lookup. Both are blocking calls, so caching
22
+ the resolution measurably speeds up launch — most noticeably when several
23
+ browsers start in parallel (see [Performance](#performance)). After the TTL
24
+ expires, or if Chrome reports that the cached chromedriver is for the wrong
25
+ major version after a browser upgrade, the driver is re-resolved and
26
+ re-downloaded if needed. Only the driver binary is ever downloaded, never the
27
+ browser itself. Explicit configuration (steps 1–4) always takes precedence over
28
+ the cache.
17
29
 
18
30
  ## Environment variables
19
31
 
@@ -24,7 +36,7 @@ never the browser itself.
24
36
  | `CRAFTDRIVER_DRIVER_PATH` | Generic fallback path (either browser) | — |
25
37
  | `CRAFTDRIVER_CACHE_DIR` | Directory for cached driver downloads | `~/.cache/craftdriver` |
26
38
  | `CRAFTDRIVER_OFFLINE` | Set to `1` to disable all network calls | — |
27
- | `CRAFTDRIVER_DRIVER_TTL` | Geckodriver freshness check interval (seconds) | `86400` (24 h) |
39
+ | `CRAFTDRIVER_DRIVER_TTL` | Driver-resolution cache lifetime, in seconds (both browsers). `0` disables the cache | `86400` (24 h) |
28
40
 
29
41
  ## Examples
30
42
 
@@ -35,7 +47,7 @@ CRAFTDRIVER_CHROMEDRIVER_PATH=/usr/bin/chromedriver npm test
35
47
  # Pin a specific geckodriver
36
48
  CRAFTDRIVER_GECKODRIVER_PATH=/usr/local/bin/geckodriver npm test
37
49
 
38
- # Never make a network call (requires a local driver to exist in steps 1–5)
50
+ # Never make a network call (requires a local driver to exist in steps 1–7)
39
51
  CRAFTDRIVER_OFFLINE=1 npm test
40
52
 
41
53
  # Change the cache location
@@ -45,6 +57,112 @@ CRAFTDRIVER_CACHE_DIR=/tmp/my-driver-cache npm test
45
57
  CRAFTDRIVER_DRIVER_TTL=3600 npm test
46
58
  ```
47
59
 
60
+ ## Performance
61
+
62
+ Most of the time `Browser.launch()` spends is the browser process starting up
63
+ (that's the same for WebDriver Classic and BiDi and there's little a client
64
+ library can do about it). The part craftdriver *does* control is resolving and
65
+ starting the driver, and it's tuned to stay out of the way:
66
+
67
+ - **Driver resolution is cached** (see above). Without a cache, resolving a
68
+ chromedriver means launching your Chrome binary just to read its version
69
+ string — a blocking call of a few hundred milliseconds on *every* launch.
70
+ The TTL cache skips that after the first launch.
71
+ - **Point at a driver explicitly to skip resolution entirely.** If you set
72
+ `CRAFTDRIVER_CHROMEDRIVER_PATH` / `CRAFTDRIVER_GECKODRIVER_PATH` (or pass
73
+ `chromeService: new ChromeService({ binaryPath })`), craftdriver uses it
74
+ directly — no version detection, no `PATH` lookup, no cache read. This is
75
+ the fastest and most deterministic option and is recommended for CI:
76
+
77
+ ```bash
78
+ CRAFTDRIVER_CHROMEDRIVER_PATH=/opt/chromedriver/chromedriver npm test
79
+ ```
80
+
81
+ - **Parallel runs benefit the most.** The resolution work that the cache (or an
82
+ explicit path) removes was synchronous and blocked the event loop, so it
83
+ serialized when several browsers were launched at once. Removing it lets
84
+ concurrent `Browser.launch()` calls overlap their startup (measured ~17–20%
85
+ faster wall time for a batch of concurrent launches).
86
+
87
+ ### Concurrency and oversubscription
88
+
89
+ Once resolution is out of the way, the rest of launch time is the browser
90
+ process starting — and that is **CPU-bound**. Starting many browsers at once
91
+ on a machine with fewer CPU cores oversubscribes the CPU, and each browser's
92
+ startup slows down roughly in proportion. On an 8-core machine, for example, a
93
+ single launch is ~2s but 20 simultaneous launches take ~25s *each* (they still
94
+ finish sooner in aggregate than launching serially, just with diminishing
95
+ returns). This is not a craftdriver limitation — it's the browser competing for
96
+ CPU — and no client-side change removes it.
97
+
98
+ Practical guidance for parallel test suites (e.g. Vitest / Jest / Playwright
99
+ Test): **cap worker concurrency at roughly the number of CPU cores.** More
100
+ workers than cores mostly adds launch latency, and a launch that is merely slow
101
+ under heavy load can trip a short per-test/hook timeout and look like a hang.
102
+ With Vitest, set `maxWorkers` (or `poolOptions`) accordingly; a generous
103
+ `hookTimeout` for the `beforeAll` that launches the browser also helps on
104
+ loaded CI runners.
105
+
106
+ Indicative numbers from the `tests/perf/launch-critical-path.perf.ts`
107
+ benchmark (macOS, Chrome, headless — absolute values are machine-dependent,
108
+ but the direction holds):
109
+
110
+ | Scenario | Before | After |
111
+ |---|---|---|
112
+ | `Browser.launch()` (BiDi) | ~2760ms | ~2240ms |
113
+ | `Browser.launch()` (Classic) | ~2400ms | ~1860ms |
114
+ | 4 browsers launched concurrently | ~6450ms | ~5130ms |
115
+
116
+ Run it yourself with `npm run bench -- launch-critical-path`.
117
+
118
+ ### Browser startup flags (advanced, opt-in)
119
+
120
+ craftdriver launches the browser with **no performance flags of its own** — it
121
+ stays unopinionated so it never silently changes browser behavior underneath
122
+ you. If you want to experiment, you can pass extra browser command-line flags
123
+ via the `args` launch option (appended to `goog:chromeOptions.args` for
124
+ Chrome/Chromium, `moz:firefoxOptions.args` for Firefox):
125
+
126
+ ```typescript
127
+ const browser = await Browser.launch({
128
+ browserName: 'chrome',
129
+ args: [
130
+ '--no-first-run',
131
+ '--no-default-browser-check',
132
+ '--disable-background-networking',
133
+ '--disable-component-update',
134
+ '--disable-default-apps',
135
+ '--disable-extensions',
136
+ '--disable-sync',
137
+ '--metrics-recording-only',
138
+ '--disable-background-timer-throttling',
139
+ '--disable-backgrounding-occluded-windows',
140
+ '--disable-renderer-backgrounding',
141
+ '--mute-audio',
142
+ '--no-service-autorun',
143
+ '--password-store=basic',
144
+ '--use-mock-keychain', // macOS: skip keychain access
145
+ '--disable-features=Translate,BackForwardCache,AcceptCHFrame,MediaRouter,OptimizationHints',
146
+ ],
147
+ });
148
+ ```
149
+
150
+ > `args` are **browser** flags. They are distinct from the **driver**
151
+ > (chromedriver/geckodriver) args you'd pass via `chromeService` /
152
+ > `firefoxService` below.
153
+
154
+ **Set expectations honestly:** on a normal local machine this set moved
155
+ `Browser.launch()` wall time by **~0.4% (≈8ms — noise)** in our measurements.
156
+ Cold browser startup is dominated by unavoidable process/engine init; these
157
+ flags mostly suppress *background* work (auto-updates, telemetry, background
158
+ networking, sync) that happens after startup rather than on the launch critical
159
+ path. So their real value is **determinism and avoiding intermittent stalls in
160
+ CI / constrained environments** (no update popups, no background network
161
+ calls), not raw local launch speed. If you adopt them, **measure on your own
162
+ environment** — the payoff is environment-dependent, and some flags can change
163
+ behavior (e.g. `--no-sandbox`, or `--disable-features=...` entries a page or
164
+ fixture relies on).
165
+
48
166
  ## Pinning via code
49
167
 
50
168
  For tighter control (custom port, extra driver flags), pass a `ChromeService`
package/docs/emulation.md CHANGED
@@ -25,12 +25,16 @@ await browser.emulate({
25
25
 
26
26
  | Option | What the page sees | Transport | Cross-browser |
27
27
  |---|---|---|---|
28
- | `colorScheme` | `prefers-color-scheme` media query | BiDi + CDP `Emulation.setEmulatedMedia` | Chromium only |
29
- | `reducedMotion` | `prefers-reduced-motion` media query | BiDi + CDP `Emulation.setEmulatedMedia` | Chromium only |
30
- | `forcedColors` | `forced-colors` media query | BiDi + CDP `Emulation.setEmulatedMedia` | Chromium only |
28
+ | `colorScheme` | `prefers-color-scheme` media query | CDP `Emulation.setEmulatedMedia` (over the BiDi+CDP bridge) | Chromium only |
29
+ | `reducedMotion` | `prefers-reduced-motion` media query | CDP `Emulation.setEmulatedMedia` (over the BiDi+CDP bridge) | Chromium only |
30
+ | `forcedColors` | `forced-colors` media query | CDP `Emulation.setEmulatedMedia` (over the BiDi+CDP bridge) | Chromium only |
31
31
  | `locale` | `navigator.language`, `Intl.*` formatting | BiDi `emulation.setLocaleOverride` | yes |
32
32
  | `timezoneId` | `Intl.DateTimeFormat`, `Date` offsets | BiDi `emulation.setTimezoneOverride` | yes |
33
- | `offline` | `navigator.onLine`, network stack | BiDi + CDP `Network.emulateNetworkConditions` | Chromium only |
33
+ | `offline` | `navigator.onLine`, network stack | CDP `Network.emulateNetworkConditions` (over the BiDi+CDP bridge) | Chromium only |
34
+
35
+ All options still require an active BiDi session (`enableBiDi: true`, the
36
+ default) — the four CDP-backed options ride over a CDP bridge that BiDi
37
+ opens, they don't work with BiDi disabled.
34
38
 
35
39
  - Pass `null` for any field to clear that override.
36
40
  - Calling `emulate({})` is a no-op.
@@ -1,6 +1,6 @@
1
1
  # Error codes
2
2
 
3
- Every error thrown from the public craftdriver API is a
3
+ Nearly every error thrown from the public craftdriver API is a
4
4
  [`CraftdriverError`](../src/lib/errors.ts), carrying:
5
5
 
6
6
  - `code` — a stable, machine-readable identifier from the table below.
@@ -12,6 +12,11 @@ Every error thrown from the public craftdriver API is a
12
12
  `CraftdriverError extends Error`, so `instanceof Error` keeps working
13
13
  and stack traces are preserved.
14
14
 
15
+ > A few edge cases still throw a plain `Error` instead — e.g. calling
16
+ > `startTrace()` again while a trace is already running, or without
17
+ > `outDir`. Guard against these with normal usage (don't call `startTrace()`
18
+ > twice without `stopTrace()`) rather than a `code` check.
19
+
15
20
  ```ts
16
21
  import { CraftdriverError, ErrorCode } from 'craftdriver';
17
22
 
@@ -38,12 +43,12 @@ try {
38
43
  | `TIMEOUT` | Generic `WebDriverWait.until(...)` timeout with no more specific code. | Same as above — check the condition. |
39
44
  | `EXPECT_MISMATCH` | An `expect(locator).to…()` assertion failed after auto-waiting. | Inspect `error.detail` for the selector and observed value. |
40
45
  | `A11Y_VIOLATIONS` | `browser.a11y.check()` (or scoped variants) found axe-core violations. | Iterate over `error.violations` — each has an `id`, `impact`, and `helpUrl`. |
41
- | `EVAL_THREW` | The function passed to `evaluate()` threw inside the page. | The page-side exception text is in `error.detail.exception`. |
46
+ | `EVAL_THREW` | The function passed to `evaluate()` threw inside the page. Also fires for `browser.clock` methods (`tick()`, `setSystemTime()`, `runFor()`) called before `install()` — they run as in-page scripts under the hood. | The page-side exception text is in `error.detail.exception`. |
42
47
  | `EVAL_BAD_ARG` | `evaluate()` / `addInitScript()` received a non-JSON-serializable argument (function, Symbol, DOM node…). | Pass plain JSON values. |
43
48
  | `INVALID_ARGUMENT` | Caller passed an invalid value (bad enum, wrong shape, unparseable duration…). | Read the message; it lists the accepted forms. |
44
49
  | `UNSUPPORTED` | Feature exists but is unavailable on this browser/transport (e.g. Chromium-only over Firefox, or a BiDi-only feature with BiDi disabled). | Enable BiDi (`enableBiDi: true`) or switch browser. |
45
- | `STATE_INVALID` | Method called in the wrong state (e.g. `stopTrace()` without `startTrace()`, `clock.tick()` before `install()`). | Call the prerequisite first. |
46
- | `DRIVER_ERROR` | Driver-level / transport-level failure surfaced to the caller. | Inspect `error.cause`. |
50
+ | `STATE_INVALID` | Method called in the wrong state (e.g. `stopTrace()` without `startTrace()`). | Call the prerequisite first. |
51
+ | `DRIVER_ERROR` | A WebDriver command returned a protocol error (non-200 response) — e.g. `stale element reference`, `element click intercepted`, `invalid selector` — or a transport-level failure. | `error.detail.webDriverError` carries the W3C JSON error code and `error.detail.webDriverMessage` the driver's message; recovery loops match on `webDriverError`. Also inspect `error.cause`. |
47
52
 
48
53
  ## Stability
49
54
 
@@ -98,22 +98,35 @@ await browser.keyboard.press('Backspace');
98
98
 
99
99
  ## Mouse
100
100
 
101
- Access the mouse API via `browser.mouse`.
101
+ Access the mouse API via `browser.mouse`. `click()`, `move()`, and
102
+ `dragAndDrop()` all accept a `Target`: a CSS selector string, a `By`
103
+ locator, or `{ x, y }` coordinates.
102
104
 
103
- ### click(x, y)
105
+ ### click(target, options?)
104
106
 
105
- Click at specific coordinates.
107
+ Click at coordinates or on an element.
106
108
 
107
109
  ```typescript
108
- await browser.mouse.click(100, 200);
110
+ await browser.mouse.click({ x: 100, y: 200 });
111
+ await browser.mouse.click('#submit');
112
+ await browser.mouse.click('#submit', { button: 'right', clickCount: 2 });
109
113
  ```
110
114
 
111
- ### move(x, y)
115
+ ### dblclick(target, options?)
112
116
 
113
- Move the mouse to coordinates.
117
+ Double-click at coordinates or on an element (shorthand for `click(target, { clickCount: 2 })`).
114
118
 
115
119
  ```typescript
116
- await browser.mouse.move(150, 300);
120
+ await browser.mouse.dblclick('#item');
121
+ ```
122
+
123
+ ### move(target, options?)
124
+
125
+ Move the mouse to coordinates or over an element.
126
+
127
+ ```typescript
128
+ await browser.mouse.move({ x: 150, y: 300 });
129
+ await browser.mouse.move('#target');
117
130
  ```
118
131
 
119
132
  ### down()
@@ -213,7 +213,7 @@ async function testLoginPersistence() {
213
213
  | `includeCookies` | `boolean` | `true` | Include cookies in saved state |
214
214
  | `includeLocalStorage` | `boolean` | `true` | Include localStorage |
215
215
  | `includeSessionStorage` | `boolean` | `false` | Include sessionStorage |
216
- | `origins` | `string[]` | all | Limit to specific origins |
216
+ | `origins` | `string[]` | all | Filter localStorage/sessionStorage capture by origin. Capture is always scoped to the page's *current* origin — this only decides whether that one origin is included, it does not collect storage from other origins the page hasn't visited. Cookies are unaffected by this option. |
217
217
 
218
218
  ### SessionStateManager (browser.storage)
219
219
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "craftdriver",
3
- "version": "0.2.2",
3
+ "version": "1.0.1",
4
4
  "description": "Modern WebDriver automation library for NodeJS",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -17,6 +17,8 @@
17
17
  "test:firefox": "BROWSER_NAME=firefox HEADLESS=true vitest run --maxWorkers=2",
18
18
  "test:chrome": "BROWSER_NAME=chrome vitest run",
19
19
  "test:chromium": "BROWSER_NAME=chromium vitest run",
20
+ "bench": "HEADLESS=true vitest run --config vitest.perf.config.ts",
21
+ "bench:realapp": "HEADLESS=true vitest run --config vitest.perf.realapp.config.ts",
20
22
  "clean": "rimraf dist",
21
23
  "serve": "http-server ./examples -a 127.0.0.1 -p 8080 -c-1 --cors",
22
24
  "examples:start": "npm run serve",
@@ -87,6 +89,7 @@
87
89
  "overrides": {
88
90
  "brace-expansion": "5.0.6",
89
91
  "ip-address": "10.1.1",
92
+ "npm": "^11.18.0",
90
93
  "undici": "7.28.0"
91
94
  }
92
95
  }
@@ -1,7 +1,8 @@
1
1
  # craftdriver — SKILL
2
2
 
3
- Modern BiDi-first WebDriver library for Node.js. Playwright ergonomics,
4
- WebDriver standards-compliance, no AI in the hot path.
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.
5
6
 
6
7
  ## Core loop
7
8