@wdio/nightwatch-devtools 1.1.1 → 1.3.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 (39) hide show
  1. package/README.md +66 -1
  2. package/dist/bidi-OH4ZFDN5.js +215 -0
  3. package/dist/bidi-OH4ZFDN5.js.map +1 -0
  4. package/dist/chunk-DKSBYMPM.js +191 -0
  5. package/dist/chunk-DKSBYMPM.js.map +1 -0
  6. package/dist/index.d.ts +111 -16
  7. package/dist/index.js +6253 -770
  8. package/dist/index.js.map +1 -1
  9. package/package.json +20 -13
  10. package/dist/constants.d.ts +0 -55
  11. package/dist/constants.js +0 -95
  12. package/dist/constants.js.map +0 -1
  13. package/dist/helpers/browserProxy.d.ts +0 -46
  14. package/dist/helpers/browserProxy.js +0 -314
  15. package/dist/helpers/browserProxy.js.map +0 -1
  16. package/dist/helpers/capturePerformance.d.ts +0 -5
  17. package/dist/helpers/capturePerformance.js +0 -53
  18. package/dist/helpers/capturePerformance.js.map +0 -1
  19. package/dist/helpers/cucumberHooks.cjs +0 -54
  20. package/dist/helpers/cucumberHooks.cjs.map +0 -1
  21. package/dist/helpers/cucumberHooks.d.cts +0 -21
  22. package/dist/helpers/suiteManager.d.ts +0 -43
  23. package/dist/helpers/suiteManager.js +0 -143
  24. package/dist/helpers/suiteManager.js.map +0 -1
  25. package/dist/helpers/testManager.d.ts +0 -45
  26. package/dist/helpers/testManager.js +0 -143
  27. package/dist/helpers/testManager.js.map +0 -1
  28. package/dist/helpers/utils.d.ts +0 -83
  29. package/dist/helpers/utils.js +0 -447
  30. package/dist/helpers/utils.js.map +0 -1
  31. package/dist/reporter.d.ts +0 -44
  32. package/dist/reporter.js +0 -185
  33. package/dist/reporter.js.map +0 -1
  34. package/dist/session.d.ts +0 -78
  35. package/dist/session.js +0 -643
  36. package/dist/session.js.map +0 -1
  37. package/dist/types.d.ts +0 -190
  38. package/dist/types.js +0 -5
  39. package/dist/types.js.map +0 -1
package/README.md CHANGED
@@ -82,16 +82,54 @@ module.exports = {
82
82
  |--------|------|---------|-------------|
83
83
  | `port` | `number` | `3000` | Port for the DevTools backend server. Auto-incremented if already in use. |
84
84
  | `hostname` | `string` | `'localhost'` | Hostname the backend server binds to. |
85
+ | `screencast` | `ScreencastOptions` | `{ enabled: false }` | Session video recording — live mode only (see [Screencast](#screencast)). |
86
+ | `bidi` | `boolean` | `false` | Opt into WebDriver BiDi capture for browser console + JS exceptions + network. Requires `webSocketUrl: true` in your capabilities and a BiDi-capable chromedriver. When attached, the per-command Chrome perf-log network path is gated off so requests don't duplicate. |
87
+ | `mode` | `'live' \| 'trace'` | `'live'` | `'live'` opens the DevTools UI window; `'trace'` skips the UI and writes a `trace-<sessionId>.zip` next to your `nightwatch.conf.cjs` at run end. See [Trace mode](../../README.md#-trace-mode-tracezip). |
85
88
 
86
89
  ```javascript
87
90
  globals: nightwatchDevtools({
88
91
  port: 3000,
89
- hostname: 'localhost'
92
+ hostname: 'localhost',
93
+ screencast: { enabled: true }
90
94
  })
91
95
  ```
92
96
 
93
97
  ---
94
98
 
99
+ ## Screencast
100
+
101
+ Record a continuous `.webm` video of the browser session. The recording starts on the first session the plugin sees and is finalized in Nightwatch's `after()` hook, writing `nightwatch-video-<sessionId>.webm` to the directory of the test file that just ran. Falls back to the directory containing `nightwatch.conf.*` if the test file path isn't known, and to `process.cwd()` as a last resort. Directories under `node_modules/` are skipped.
102
+
103
+ **Polling mode only.** Nightwatch doesn't expose a stable CDP escape hatch the way WebdriverIO (`browser.getPuppeteer()`) and Selenium (`driver.createCDPConnection`) do, so the screencast captures frames by calling `browser.takeScreenshot()` at a fixed interval. This works on every browser Nightwatch supports.
104
+
105
+ ### Quick start
106
+
107
+ ```javascript
108
+ globals: nightwatchDevtools({
109
+ port: 3000,
110
+ screencast: { enabled: true, pollIntervalMs: 200 }
111
+ })
112
+ ```
113
+
114
+ ### Options
115
+
116
+ | Option | Type | Default | Notes |
117
+ |--------|------|---------|-------|
118
+ | `enabled` | `boolean` | `false` | Master switch. |
119
+ | `pollIntervalMs` | `number` | `200` | Screenshot interval (ms). Lower = smoother video, more WebDriver round-trips. 200 ms ≈ 5 fps. |
120
+ | `captureFormat` | `'jpeg' \| 'png'` | `'jpeg'` | Frame format. WebDriver screenshots are always PNG, so this only affects the encoded output. |
121
+ | `maxWidth` / `maxHeight` / `quality` | — | — | CDP-only options, ignored in polling mode. Listed for shape compatibility with the WDIO/Selenium adapters. |
122
+
123
+ ### Prerequisites
124
+
125
+ `fluent-ffmpeg` (already a runtime dep of this package) plus the `ffmpeg` binary on PATH. macOS: `brew install ffmpeg`. Linux: `apt install ffmpeg`. Without ffmpeg the recorder still runs but the encode step logs a warning and skips writing the file.
126
+
127
+ ### Output
128
+
129
+ The encoded video is sent to the DevTools dashboard via the `screencast` WS scope and shown in the **Screencast** tab. The absolute path also appears in the Nightwatch log line `📹 Screencast video: <path>`.
130
+
131
+ ---
132
+
95
133
  ## Examples
96
134
 
97
135
  Working examples are included in this package:
@@ -124,6 +162,33 @@ Nightwatch does not provide the same depth of framework hooks as WebdriverIO, so
124
162
 
125
163
  Overall feature parity with the WebdriverIO DevTools service is approximately **80–90%**.
126
164
 
165
+ ### Preserve & Rerun (Compare)
166
+
167
+ Available for Nightwatch — same dashboard UI as WebdriverIO. The "compare with rerun" flow snapshots the failing run, re-launches the test with `DEVTOOLS_RERUN_LABEL` set (the plugin filters down to just that test name on the rerun), and the dashboard shows the two runs side-by-side aligned by command.
168
+
169
+ ### BiDi capture (opt-in)
170
+
171
+ Enable WebDriver BiDi capture for browser console messages, JS exceptions, and network requests. Equivalent to the path selenium-devtools uses — both adapters call the same `attachBidiHandlers` in `@wdio/devtools-core`.
172
+
173
+ ```javascript
174
+ globals: nightwatchDevtools({
175
+ port: 3000,
176
+ bidi: true
177
+ })
178
+ ```
179
+
180
+ You also need `webSocketUrl: true` in your capabilities so chromedriver actually exposes the BiDi channel:
181
+
182
+ ```javascript
183
+ desiredCapabilities: {
184
+ browserName: 'chrome',
185
+ 'webSocketUrl': true, // ← enables BiDi
186
+ 'goog:chromeOptions': { /* ... */ }
187
+ }
188
+ ```
189
+
190
+ When attached, the per-command Chrome performance-log network capture path is gated off so requests don't appear twice in the dashboard. If `webSocketUrl` is missing or the chromedriver version doesn't expose BiDi, the attach silently fails and the perf-log fallback continues to work.
191
+
127
192
  ## :page_facing_up: License
128
193
 
129
194
  [MIT](/LICENSE)
@@ -0,0 +1,215 @@
1
+ import {
2
+ LOG_SOURCES,
3
+ chromeLogLevelToLogLevel,
4
+ errorMessage,
5
+ getRequestType
6
+ } from "./chunk-DKSBYMPM.js";
7
+
8
+ // src/bidi.ts
9
+ import logger from "@wdio/logger";
10
+
11
+ // ../core/src/bidi.ts
12
+ import { createRequire } from "module";
13
+ function loadSeleniumSubmodule(subpath) {
14
+ try {
15
+ const userRequire = createRequire(`${process.cwd()}/`);
16
+ return userRequire(`selenium-webdriver/${subpath}`);
17
+ } catch {
18
+ try {
19
+ const localRequire = createRequire(import.meta.url);
20
+ return localRequire(`selenium-webdriver/${subpath}`);
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+ }
26
+ function handleBidiConsoleEntry(rawEntry, sinks, log2) {
27
+ const entry = rawEntry;
28
+ try {
29
+ const level = (entry?.level ?? entry?.type ?? "info").toString();
30
+ const text = entry?.text ?? entry?.message ?? "";
31
+ sinks.pushConsoleLog({
32
+ timestamp: Number(entry?.timestamp) || Date.now(),
33
+ type: chromeLogLevelToLogLevel(level),
34
+ args: [text],
35
+ source: LOG_SOURCES.BROWSER
36
+ });
37
+ } catch (err) {
38
+ log2("warn", `onConsoleEntry handler threw: ${errorMessage(err)}`);
39
+ }
40
+ }
41
+ function handleBidiJsException(rawExc, sinks, log2) {
42
+ const exception = rawExc;
43
+ try {
44
+ const text = exception?.text ?? exception?.message ?? String(rawExc);
45
+ const trimmed = String(text).replace(/\s+/g, " ").slice(0, 200);
46
+ log2(
47
+ "warn",
48
+ `\u{1F41B} JS error in page: ${trimmed}${String(text).length > 200 ? "\u2026" : ""}`
49
+ );
50
+ sinks.pushConsoleLog({
51
+ timestamp: Date.now(),
52
+ type: "error",
53
+ args: [text],
54
+ source: LOG_SOURCES.BROWSER
55
+ });
56
+ } catch (err) {
57
+ log2("warn", `onJavascriptException handler threw: ${errorMessage(err)}`);
58
+ }
59
+ }
60
+ async function attachLogInspector(driver, factory, sinks, log2) {
61
+ try {
62
+ const inspector = await factory(driver);
63
+ await inspector.onConsoleEntry((e) => handleBidiConsoleEntry(e, sinks, log2));
64
+ await inspector.onJavascriptException(
65
+ (e) => handleBidiJsException(e, sinks, log2)
66
+ );
67
+ log2("info", "\u2713 BiDi LogInspector attached (console + JS exceptions)");
68
+ return true;
69
+ } catch (err) {
70
+ log2("warn", `BiDi LogInspector attach failed: ${errorMessage(err)}`);
71
+ return false;
72
+ }
73
+ }
74
+ function handleBidiRequestSent(rawEvent, pending, sinks, log2) {
75
+ const event = rawEvent;
76
+ try {
77
+ const requestId = String(event?.request?.request ?? event?.id ?? "");
78
+ if (!requestId) {
79
+ return;
80
+ }
81
+ const entry = {
82
+ id: requestId,
83
+ url: event?.request?.url ?? "",
84
+ method: event?.request?.method ?? "GET",
85
+ requestHeaders: arrayHeadersToObject(event?.request?.headers),
86
+ timestamp: Date.now(),
87
+ startTime: Number(event?.timestamp ?? Date.now()),
88
+ type: getRequestType(event?.request?.url ?? "")
89
+ };
90
+ pending.set(requestId, entry);
91
+ sinks.pushNetworkRequest(entry);
92
+ } catch (err) {
93
+ log2("warn", `beforeRequestSent threw: ${errorMessage(err)}`);
94
+ }
95
+ }
96
+ function handleBidiResponseCompleted(rawEvent, pending, sinks, log2) {
97
+ const event = rawEvent;
98
+ try {
99
+ const requestId = String(event?.request?.request ?? event?.id ?? "");
100
+ const previous = pending.get(requestId);
101
+ if (!previous) {
102
+ return;
103
+ }
104
+ const finalized = {
105
+ ...previous,
106
+ status: Number(event?.response?.status) || previous.status,
107
+ statusText: event?.response?.statusText ?? previous.statusText,
108
+ responseHeaders: arrayHeadersToObject(event?.response?.headers),
109
+ type: getRequestType(previous.url, event?.response?.mimeType),
110
+ endTime: Number(event?.timestamp ?? Date.now()),
111
+ time: Number(event?.timestamp ?? Date.now()) - previous.startTime,
112
+ size: Number(event?.response?.bytesReceived) || void 0
113
+ };
114
+ pending.delete(requestId);
115
+ sinks.replaceNetworkRequest(requestId, finalized);
116
+ } catch (err) {
117
+ log2("warn", `responseCompleted threw: ${errorMessage(err)}`);
118
+ }
119
+ }
120
+ async function attachNetworkInspector(driver, factory, sinks, log2) {
121
+ try {
122
+ const inspector = await factory(driver);
123
+ const pending = /* @__PURE__ */ new Map();
124
+ await inspector.beforeRequestSent(
125
+ (e) => handleBidiRequestSent(e, pending, sinks, log2)
126
+ );
127
+ await inspector.responseCompleted(
128
+ (e) => handleBidiResponseCompleted(e, pending, sinks, log2)
129
+ );
130
+ log2("info", "\u2713 BiDi NetworkInspector attached (request + response)");
131
+ return true;
132
+ } catch (err) {
133
+ log2("warn", `BiDi NetworkInspector attach failed: ${errorMessage(err)}`);
134
+ return false;
135
+ }
136
+ }
137
+ async function attachBidiHandlers(driver, sinks, onLog) {
138
+ const log2 = (level, message) => onLog?.(level, message);
139
+ const logFactory = loadSeleniumSubmodule("bidi/logInspector");
140
+ const networkFactory = loadSeleniumSubmodule(
141
+ "bidi/networkInspector"
142
+ );
143
+ let attached = 0;
144
+ if (typeof logFactory === "function") {
145
+ if (await attachLogInspector(driver, logFactory, sinks, log2)) {
146
+ attached++;
147
+ }
148
+ } else {
149
+ log2("info", "selenium-webdriver/bidi/logInspector not available \u2014 skipping");
150
+ }
151
+ if (typeof networkFactory === "function") {
152
+ if (await attachNetworkInspector(driver, networkFactory, sinks, log2)) {
153
+ attached++;
154
+ }
155
+ } else {
156
+ log2(
157
+ "info",
158
+ "selenium-webdriver/bidi/networkInspector not available \u2014 skipping"
159
+ );
160
+ }
161
+ return attached > 0;
162
+ }
163
+ function arrayHeadersToObject(headers) {
164
+ if (!Array.isArray(headers)) {
165
+ return void 0;
166
+ }
167
+ const out = {};
168
+ for (const h of headers) {
169
+ const name = String(h?.name ?? "").toLowerCase();
170
+ if (!name) {
171
+ continue;
172
+ }
173
+ const v = h?.value;
174
+ out[name] = typeof v === "string" ? v : typeof v?.value === "string" ? v.value : JSON.stringify(v ?? "");
175
+ }
176
+ return out;
177
+ }
178
+
179
+ // src/bidi.ts
180
+ var log = logger("@wdio/nightwatch-devtools:bidi");
181
+ async function attachBidiHandlers2(driver, sinks) {
182
+ return attachBidiHandlers(
183
+ driver,
184
+ sinks,
185
+ (level, message) => log[level](message)
186
+ );
187
+ }
188
+ function buildBidiSinks(capturer) {
189
+ return {
190
+ pushConsoleLog: (entry) => {
191
+ capturer.consoleLogs.push(entry);
192
+ capturer.sendUpstream("consoleLogs", [entry]);
193
+ },
194
+ pushNetworkRequest: (entry) => {
195
+ capturer.networkRequests.push(entry);
196
+ capturer.sendUpstream("networkRequests", [entry]);
197
+ },
198
+ replaceNetworkRequest: (id, entry) => {
199
+ const idx = capturer.networkRequests.findIndex(
200
+ (r) => r.id === id
201
+ );
202
+ if (idx !== -1) {
203
+ capturer.networkRequests[idx] = entry;
204
+ } else {
205
+ capturer.networkRequests.push(entry);
206
+ }
207
+ capturer.sendUpstream("networkRequests", [entry]);
208
+ }
209
+ };
210
+ }
211
+ export {
212
+ attachBidiHandlers2 as attachBidiHandlers,
213
+ buildBidiSinks
214
+ };
215
+ //# sourceMappingURL=bidi-OH4ZFDN5.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/bidi.ts","../../core/src/bidi.ts"],"sourcesContent":["import logger from '@wdio/logger'\nimport {\n type BidiHandlerSinks,\n attachBidiHandlers as attachBidiHandlersCore\n} from '@wdio/devtools-core'\nimport type { SessionCapturer } from './session.js'\n\nconst log = logger('@wdio/nightwatch-devtools:bidi')\n\n/**\n * Nightwatch wrapper around the core BiDi attach helper. Nightwatch ships\n * selenium-webdriver under the hood (via chromedriver), so the same\n * `selenium-webdriver/bidi` inspectors selenium-devtools uses are available\n * whenever the user has set `webSocketUrl: true` in their capabilities.\n *\n * Opt-in via the plugin's `bidi: true` option. When attached, the per-command\n * Chrome performance-log network capture is gated off to avoid duplicate\n * entries in the dashboard.\n */\nexport async function attachBidiHandlers(\n driver: unknown,\n sinks: BidiHandlerSinks\n): Promise<boolean> {\n return attachBidiHandlersCore(driver, sinks, (level, message) =>\n log[level](message)\n )\n}\n\n/**\n * Build sinks that route BiDi events into the SessionCapturer's local arrays\n * and broadcast them upstream. Mirrors selenium-devtools' `buildBidiSinks` —\n * separate per-adapter because the SessionCapturer concrete types differ.\n */\nexport function buildBidiSinks(capturer: SessionCapturer): BidiHandlerSinks {\n return {\n pushConsoleLog: (entry) => {\n capturer.consoleLogs.push(entry)\n capturer.sendUpstream('consoleLogs', [entry])\n },\n pushNetworkRequest: (entry) => {\n capturer.networkRequests.push(entry)\n capturer.sendUpstream('networkRequests', [entry])\n },\n replaceNetworkRequest: (id, entry) => {\n const idx = capturer.networkRequests.findIndex(\n (r: { id?: string }) => r.id === id\n )\n if (idx !== -1) {\n capturer.networkRequests[idx] = entry\n } else {\n capturer.networkRequests.push(entry)\n }\n capturer.sendUpstream('networkRequests', [entry])\n }\n }\n}\n","import { createRequire } from 'node:module'\nimport type {\n ConsoleLog,\n LogLevel,\n NetworkRequest\n} from '@wdio/devtools-shared'\nimport {\n LOG_SOURCES,\n chromeLogLevelToLogLevel,\n type LogSource\n} from './console.js'\nimport { errorMessage } from './error.js'\nimport { getRequestType } from './net.js'\n\n/**\n * Generic sinks the BiDi handlers push into. Each adapter wires these to its\n * own SessionCapturer state — selenium's `buildBidiSinks` is the canonical\n * example; nightwatch can mirror the pattern when it wires up BiDi.\n */\nexport interface BidiHandlerSinks {\n pushConsoleLog: (entry: ConsoleLog) => void\n pushNetworkRequest: (entry: NetworkRequest) => void\n replaceNetworkRequest: (id: string, entry: NetworkRequest) => void\n}\n\n/**\n * Resolve a `selenium-webdriver/<subpath>` module from the user's install\n * (preferred) or the package's local install (fallback). Returns `null` if\n * neither resolves — caller should treat as \"BiDi not available on this\n * runtime\" and degrade gracefully.\n *\n * Used by both selenium-devtools and (when wired up) nightwatch-devtools —\n * both ship selenium-webdriver-style drivers under the hood.\n */\nexport function loadSeleniumSubmodule<T = unknown>(subpath: string): T | null {\n try {\n const userRequire = createRequire(`${process.cwd()}/`)\n return userRequire(`selenium-webdriver/${subpath}`) as T\n } catch {\n try {\n const localRequire = createRequire(import.meta.url)\n return localRequire(`selenium-webdriver/${subpath}`) as T\n } catch {\n return null\n }\n }\n}\n\ntype BidiLogger = (level: 'info' | 'warn', message: string) => void\ntype InspectorFactory = (driver: unknown) => Promise<unknown>\n\ninterface LogInspector {\n onConsoleEntry: (cb: (entry: unknown) => void) => Promise<void>\n onJavascriptException: (cb: (exc: unknown) => void) => Promise<void>\n}\n\ninterface NetworkInspector {\n beforeRequestSent: (cb: (e: unknown) => void) => Promise<void>\n responseCompleted: (cb: (e: unknown) => void) => Promise<void>\n}\n\nexport function handleBidiConsoleEntry(\n rawEntry: unknown,\n sinks: BidiHandlerSinks,\n log: BidiLogger\n): void {\n const entry = rawEntry as {\n level?: string\n type?: string\n text?: string\n message?: string\n timestamp?: number\n }\n try {\n const level = (entry?.level ?? entry?.type ?? 'info').toString()\n const text = entry?.text ?? entry?.message ?? ''\n sinks.pushConsoleLog({\n timestamp: Number(entry?.timestamp) || Date.now(),\n type: chromeLogLevelToLogLevel(level) as LogLevel,\n args: [text],\n source: LOG_SOURCES.BROWSER as LogSource\n })\n } catch (err) {\n log('warn', `onConsoleEntry handler threw: ${errorMessage(err)}`)\n }\n}\n\nexport function handleBidiJsException(\n rawExc: unknown,\n sinks: BidiHandlerSinks,\n log: BidiLogger\n): void {\n const exception = rawExc as { text?: string; message?: string }\n try {\n const text = exception?.text ?? exception?.message ?? String(rawExc)\n const trimmed = String(text).replace(/\\s+/g, ' ').slice(0, 200)\n log(\n 'warn',\n `🐛 JS error in page: ${trimmed}${String(text).length > 200 ? '…' : ''}`\n )\n sinks.pushConsoleLog({\n timestamp: Date.now(),\n type: 'error',\n args: [text],\n source: LOG_SOURCES.BROWSER as LogSource\n })\n } catch (err) {\n log('warn', `onJavascriptException handler threw: ${errorMessage(err)}`)\n }\n}\n\nasync function attachLogInspector(\n driver: unknown,\n factory: InspectorFactory,\n sinks: BidiHandlerSinks,\n log: BidiLogger\n): Promise<boolean> {\n try {\n const inspector = (await factory(driver)) as LogInspector\n await inspector.onConsoleEntry((e) => handleBidiConsoleEntry(e, sinks, log))\n await inspector.onJavascriptException((e) =>\n handleBidiJsException(e, sinks, log)\n )\n log('info', '✓ BiDi LogInspector attached (console + JS exceptions)')\n return true\n } catch (err) {\n log('warn', `BiDi LogInspector attach failed: ${errorMessage(err)}`)\n return false\n }\n}\n\ninterface BeforeRequestSentEvent {\n request?: {\n request?: string\n url?: string\n method?: string\n headers?: unknown\n }\n id?: string\n timestamp?: number\n}\n\ninterface ResponseCompletedEvent {\n request?: { request?: string }\n id?: string\n timestamp?: number\n response?: {\n status?: number\n statusText?: string\n headers?: unknown\n mimeType?: string\n bytesReceived?: number\n }\n}\n\nexport function handleBidiRequestSent(\n rawEvent: unknown,\n pending: Map<string, NetworkRequest>,\n sinks: BidiHandlerSinks,\n log: BidiLogger\n): void {\n const event = rawEvent as BeforeRequestSentEvent\n try {\n const requestId = String(event?.request?.request ?? event?.id ?? '')\n if (!requestId) {\n return\n }\n const entry: NetworkRequest = {\n id: requestId,\n url: event?.request?.url ?? '',\n method: event?.request?.method ?? 'GET',\n requestHeaders: arrayHeadersToObject(event?.request?.headers),\n timestamp: Date.now(),\n startTime: Number(event?.timestamp ?? Date.now()),\n type: getRequestType(event?.request?.url ?? '')\n }\n pending.set(requestId, entry)\n sinks.pushNetworkRequest(entry)\n } catch (err) {\n log('warn', `beforeRequestSent threw: ${errorMessage(err)}`)\n }\n}\n\nexport function handleBidiResponseCompleted(\n rawEvent: unknown,\n pending: Map<string, NetworkRequest>,\n sinks: BidiHandlerSinks,\n log: BidiLogger\n): void {\n const event = rawEvent as ResponseCompletedEvent\n try {\n const requestId = String(event?.request?.request ?? event?.id ?? '')\n const previous = pending.get(requestId)\n if (!previous) {\n return\n }\n const finalized: NetworkRequest = {\n ...previous,\n status: Number(event?.response?.status) || previous.status,\n statusText: event?.response?.statusText ?? previous.statusText,\n responseHeaders: arrayHeadersToObject(event?.response?.headers),\n type: getRequestType(previous.url, event?.response?.mimeType),\n endTime: Number(event?.timestamp ?? Date.now()),\n time: Number(event?.timestamp ?? Date.now()) - previous.startTime,\n size: Number(event?.response?.bytesReceived) || undefined\n }\n pending.delete(requestId)\n sinks.replaceNetworkRequest(requestId, finalized)\n } catch (err) {\n log('warn', `responseCompleted threw: ${errorMessage(err)}`)\n }\n}\n\nasync function attachNetworkInspector(\n driver: unknown,\n factory: InspectorFactory,\n sinks: BidiHandlerSinks,\n log: BidiLogger\n): Promise<boolean> {\n try {\n const inspector = (await factory(driver)) as NetworkInspector\n const pending = new Map<string, NetworkRequest>()\n await inspector.beforeRequestSent((e) =>\n handleBidiRequestSent(e, pending, sinks, log)\n )\n await inspector.responseCompleted((e) =>\n handleBidiResponseCompleted(e, pending, sinks, log)\n )\n log('info', '✓ BiDi NetworkInspector attached (request + response)')\n return true\n } catch (err) {\n log('warn', `BiDi NetworkInspector attach failed: ${errorMessage(err)}`)\n return false\n }\n}\n\n/**\n * Attach the selenium-webdriver BiDi LogInspector + NetworkInspector to a\n * driver and route their events into the given sinks. Returns `true` when at\n * least one inspector connected — caller can disable the equivalent\n * script-injection collectors to avoid duplicates.\n *\n * Tolerant of older / non-BiDi runtimes: if either submodule fails to load\n * or the inspector factory throws (driver session doesn't have webSocketUrl\n * capability set, etc.), the corresponding stream is silently skipped and\n * the function returns false.\n *\n * @param onLog Optional callback for adapter-side logging. Receives ('info' |\n * 'warn', message) on lifecycle events. Default: silent — adapters wire their\n * own logger when they want visibility into BiDi attach state.\n */\nexport async function attachBidiHandlers(\n driver: unknown,\n sinks: BidiHandlerSinks,\n onLog?: (level: 'info' | 'warn', message: string) => void\n): Promise<boolean> {\n const log: BidiLogger = (level, message) => onLog?.(level, message)\n const logFactory =\n loadSeleniumSubmodule<InspectorFactory>('bidi/logInspector')\n const networkFactory = loadSeleniumSubmodule<InspectorFactory>(\n 'bidi/networkInspector'\n )\n\n let attached = 0\n if (typeof logFactory === 'function') {\n if (await attachLogInspector(driver, logFactory, sinks, log)) {\n attached++\n }\n } else {\n log('info', 'selenium-webdriver/bidi/logInspector not available — skipping')\n }\n if (typeof networkFactory === 'function') {\n if (await attachNetworkInspector(driver, networkFactory, sinks, log)) {\n attached++\n }\n } else {\n log(\n 'info',\n 'selenium-webdriver/bidi/networkInspector not available — skipping'\n )\n }\n return attached > 0\n}\n\n/**\n * Flatten BiDi's `Array<{name, value:{value|type}}>` header shape to a\n * lowercased `Record<string, string>`. Exported so adapter-side helpers can\n * reuse it for their own header normalization.\n */\nexport function arrayHeadersToObject(\n headers: unknown\n): Record<string, string> | undefined {\n if (!Array.isArray(headers)) {\n return undefined\n }\n const out: Record<string, string> = {}\n for (const h of headers as Array<{\n name?: string\n value?: string | { value?: string; type?: string }\n }>) {\n const name = String(h?.name ?? '').toLowerCase()\n if (!name) {\n continue\n }\n const v = h?.value\n out[name] =\n typeof v === 'string'\n ? v\n : typeof (v as { value?: string })?.value === 'string'\n ? (v as { value: string }).value\n : JSON.stringify(v ?? '')\n }\n return out\n}\n"],"mappings":"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","names":["log","attachBidiHandlers"]}
@@ -0,0 +1,191 @@
1
+ // ../core/src/error.ts
2
+ function toError(value) {
3
+ if (value instanceof Error) {
4
+ return value;
5
+ }
6
+ if (value !== null && typeof value === "object" && typeof value.message === "string") {
7
+ const e = new Error(value.message);
8
+ const name = value.name;
9
+ if (typeof name === "string") {
10
+ e.name = name;
11
+ }
12
+ return e;
13
+ }
14
+ return new Error(String(value));
15
+ }
16
+ function errorMessage(value) {
17
+ if (value instanceof Error) {
18
+ return value.message;
19
+ }
20
+ if (typeof value === "string") {
21
+ return value;
22
+ }
23
+ if (value !== null && typeof value === "object" && typeof value.message === "string") {
24
+ return value.message;
25
+ }
26
+ if (value === void 0 || value === null) {
27
+ return "unknown error";
28
+ }
29
+ try {
30
+ return String(value);
31
+ } catch {
32
+ return "unknown error";
33
+ }
34
+ }
35
+ function serializeError(error) {
36
+ if (!error) {
37
+ return void 0;
38
+ }
39
+ return { name: error.name, message: error.message, stack: error.stack };
40
+ }
41
+
42
+ // ../core/src/console.ts
43
+ var CONSOLE_METHODS = ["log", "info", "warn", "error"];
44
+ var ANSI_REGEX = /\x1b\[[?]?[0-9;]*[A-Za-z]/g;
45
+ function stripAnsi(text) {
46
+ return text.replace(ANSI_REGEX, "");
47
+ }
48
+ var LOG_LEVEL_PATTERNS = [
49
+ { level: "trace", pattern: /\btrace\b/i },
50
+ { level: "debug", pattern: /\bdebug\b/i },
51
+ { level: "info", pattern: /\binfo\b/i },
52
+ { level: "warn", pattern: /\bwarn(ing)?\b/i },
53
+ { level: "error", pattern: /\berror\b/i }
54
+ ];
55
+ var ERROR_INDICATORS = ["\u2717", "failed", "failure"];
56
+ var SPINNER_RE = /^[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏]/u;
57
+ function isInternalStreamLine(line) {
58
+ const t = line.trim();
59
+ if (t.startsWith('{"') || t.startsWith("[SESSION]")) {
60
+ return true;
61
+ }
62
+ if (t.includes("@wdio/devtools-backend")) {
63
+ return true;
64
+ }
65
+ if (/^console\.(log|info|warn|error|debug|trace)$/.test(t)) {
66
+ return true;
67
+ }
68
+ if (/^at\s.+:\d+:\d+\)?$/.test(t)) {
69
+ return true;
70
+ }
71
+ return false;
72
+ }
73
+ var LOG_SOURCES = {
74
+ BROWSER: "browser",
75
+ TEST: "test",
76
+ TERMINAL: "terminal"
77
+ };
78
+ function detectLogLevel(text) {
79
+ const normalised = stripAnsi(text).toLowerCase();
80
+ for (const { level, pattern } of LOG_LEVEL_PATTERNS) {
81
+ if (pattern.test(normalised)) {
82
+ return level;
83
+ }
84
+ }
85
+ if (ERROR_INDICATORS.some((i) => normalised.includes(i.toLowerCase()))) {
86
+ return "error";
87
+ }
88
+ return "log";
89
+ }
90
+ function createConsoleLogEntry(type, args, source = LOG_SOURCES.TEST) {
91
+ return { timestamp: Date.now(), type, args, source };
92
+ }
93
+ function mapChromeBrowserLogs(entries) {
94
+ return entries.map((entry) => ({
95
+ timestamp: entry.timestamp,
96
+ type: chromeLogLevelToLogLevel(
97
+ entry.level
98
+ ),
99
+ args: [entry.message],
100
+ source: LOG_SOURCES.BROWSER
101
+ }));
102
+ }
103
+ function chromeLogLevelToLogLevel(level) {
104
+ const levelName = (typeof level === "object" ? level?.name ?? "" : level ?? "").toUpperCase();
105
+ switch (levelName) {
106
+ case "SEVERE":
107
+ return "error";
108
+ case "WARNING":
109
+ return "warn";
110
+ case "INFO":
111
+ return "info";
112
+ case "DEBUG":
113
+ return "debug";
114
+ default:
115
+ return "log";
116
+ }
117
+ }
118
+
119
+ // ../core/src/net.ts
120
+ import * as net from "net";
121
+ function isPortInUse(port, hostname) {
122
+ return new Promise((resolve) => {
123
+ const server = net.createServer();
124
+ server.once("error", () => resolve(true));
125
+ server.once("listening", () => server.close(() => resolve(false)));
126
+ server.listen(port, hostname);
127
+ });
128
+ }
129
+ async function findFreePort(startPort, hostname) {
130
+ let port = startPort;
131
+ while (await isPortInUse(port, hostname)) {
132
+ port++;
133
+ }
134
+ return port;
135
+ }
136
+ function getRequestType(url, mimeType) {
137
+ const contentType = mimeType?.toLowerCase() ?? "";
138
+ const urlLower = url.toLowerCase();
139
+ if (contentType.includes("text/html")) {
140
+ return "document";
141
+ }
142
+ if (contentType.includes("text/css")) {
143
+ return "stylesheet";
144
+ }
145
+ if (contentType.includes("javascript") || contentType.includes("ecmascript")) {
146
+ return "script";
147
+ }
148
+ if (contentType.includes("image/")) {
149
+ return "image";
150
+ }
151
+ if (contentType.includes("font/") || contentType.includes("woff")) {
152
+ return "font";
153
+ }
154
+ if (contentType.includes("application/json")) {
155
+ return "fetch";
156
+ }
157
+ if (urlLower.endsWith(".html") || urlLower.endsWith(".htm")) {
158
+ return "document";
159
+ }
160
+ if (urlLower.endsWith(".css")) {
161
+ return "stylesheet";
162
+ }
163
+ if (urlLower.endsWith(".js") || urlLower.endsWith(".mjs")) {
164
+ return "script";
165
+ }
166
+ if (/\.(png|jpg|jpeg|gif|svg|webp|ico)$/.test(urlLower)) {
167
+ return "image";
168
+ }
169
+ if (/\.(woff|woff2|ttf|eot|otf)$/.test(urlLower)) {
170
+ return "font";
171
+ }
172
+ return "xhr";
173
+ }
174
+
175
+ export {
176
+ toError,
177
+ errorMessage,
178
+ serializeError,
179
+ CONSOLE_METHODS,
180
+ stripAnsi,
181
+ SPINNER_RE,
182
+ isInternalStreamLine,
183
+ LOG_SOURCES,
184
+ detectLogLevel,
185
+ createConsoleLogEntry,
186
+ mapChromeBrowserLogs,
187
+ chromeLogLevelToLogLevel,
188
+ findFreePort,
189
+ getRequestType
190
+ };
191
+ //# sourceMappingURL=chunk-DKSBYMPM.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../core/src/error.ts","../../core/src/console.ts","../../core/src/net.ts"],"sourcesContent":["/** Plain-object shape of an Error after `serializeError`. */\nexport interface SerializedError {\n name: string\n message: string\n stack?: string\n}\n\n/**\n * Coerce an unknown value (caught exception, framework-supplied error\n * object, string, etc.) into an Error instance. Used at adapter command\n * boundaries where caught values can be anything — Error subclasses,\n * thrown strings, framework objects with a `.message` — and downstream\n * code wants a stable `Error` to inspect and serialize.\n */\nexport function toError(value: unknown): Error {\n if (value instanceof Error) {\n return value\n }\n if (\n value !== null &&\n typeof value === 'object' &&\n typeof (value as { message?: unknown }).message === 'string'\n ) {\n const e = new Error((value as { message: string }).message)\n const name = (value as { name?: unknown }).name\n if (typeof name === 'string') {\n e.name = name\n }\n return e\n }\n return new Error(String(value))\n}\n\n/**\n * Extract a printable message from a caught value. Equivalent to reading\n * `.message` on an Error, but degrades cleanly when the thrown value is a\n * string, a plain object, undefined, or anything else — `(err as Error).message`\n * silently returns `undefined` in those cases and yields useless log output.\n */\nexport function errorMessage(value: unknown): string {\n if (value instanceof Error) {\n return value.message\n }\n if (typeof value === 'string') {\n return value\n }\n if (\n value !== null &&\n typeof value === 'object' &&\n typeof (value as { message?: unknown }).message === 'string'\n ) {\n return (value as { message: string }).message\n }\n if (value === undefined || value === null) {\n return 'unknown error'\n }\n try {\n return String(value)\n } catch {\n return 'unknown error'\n }\n}\n\n/**\n * Normalize an Error to a plain object so its fields survive `JSON.stringify`\n * over the WS bridge. Error instances have `message`/`name`/`stack` as\n * non-enumerable, which `JSON.stringify` would drop.\n *\n * Returns `undefined` when the input is undefined so callers can pass through\n * possibly-undefined values without an extra branch.\n */\nexport function serializeError(\n error: Error | undefined\n): SerializedError | undefined {\n if (!error) {\n return undefined\n }\n return { name: error.name, message: error.message, stack: error.stack }\n}\n","import type { ConsoleLog, LogLevel, LogSource } from '@wdio/devtools-shared'\n\n/**\n * Console methods we intercept to forward test/runner-process output into the\n * UI Console tab.\n */\nexport const CONSOLE_METHODS = ['log', 'info', 'warn', 'error'] as const\n\n/**\n * Strips ANSI escape sequences (colour codes, cursor moves, etc.) from\n * terminal output so the UI Console renders plain text. The pattern accepts\n * any trailing letter, not just `m`, so cursor/style sequences are handled\n * too.\n */\nexport const ANSI_REGEX = /\\x1b\\[[?]?[0-9;]*[A-Za-z]/g\n\nexport function stripAnsi(text: string): string {\n return text.replace(ANSI_REGEX, '')\n}\n\n/**\n * Log-level detection patterns, applied in priority order (highest to\n * lowest). The first matching pattern wins.\n */\nexport const LOG_LEVEL_PATTERNS: ReadonlyArray<{\n level: 'trace' | 'debug' | 'info' | 'warn' | 'error'\n pattern: RegExp\n}> = [\n { level: 'trace', pattern: /\\btrace\\b/i },\n { level: 'debug', pattern: /\\bdebug\\b/i },\n { level: 'info', pattern: /\\binfo\\b/i },\n { level: 'warn', pattern: /\\bwarn(ing)?\\b/i },\n { level: 'error', pattern: /\\berror\\b/i }\n] as const\n\n/** Visual indicators that suggest error-level logs in unstructured output. */\nexport const ERROR_INDICATORS = ['✗', 'failed', 'failure'] as const\n\n/**\n * Matches the leading Braille spinner glyphs that runners (Nightwatch CLI,\n * Selenium tooling) emit for in-place progress updates. Adapters skip lines\n * that match this so the dashboard's Console tab isn't flooded with frames.\n */\nexport const SPINNER_RE = /^[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏]/u\n\n/**\n * Filter out terminal/stream lines that would feed back into the WS bridge\n * and cause an infinite forwarding loop: pino JSON output, [SESSION] markers,\n * backend logger lines, Jest console framing, and bare stack-frame lines.\n *\n * Adapters call this from their stream-patch before forwarding lines to the\n * UI Console tab. Combine with SPINNER_RE for full noise filtering.\n */\nexport function isInternalStreamLine(line: string): boolean {\n const t = line.trim()\n if (t.startsWith('{\"') || t.startsWith('[SESSION]')) {\n return true\n }\n if (t.includes('@wdio/devtools-backend')) {\n return true\n }\n if (/^console\\.(log|info|warn|error|debug|trace)$/.test(t)) {\n return true\n }\n if (/^at\\s.+:\\d+:\\d+\\)?$/.test(t)) {\n return true\n }\n return false\n}\n\n/** Enum-style accessor for the canonical LogSource values from shared. */\nexport const LOG_SOURCES = {\n BROWSER: 'browser',\n TEST: 'test',\n TERMINAL: 'terminal'\n} as const satisfies Record<string, LogSource>\n\nexport type { LogSource } from '@wdio/devtools-shared'\n\n/**\n * Classify a line of unstructured terminal output by scanning for log-level\n * keywords. Falls back to `'log'` when no pattern matches.\n */\nexport function detectLogLevel(text: string): LogLevel {\n const normalised = stripAnsi(text).toLowerCase()\n for (const { level, pattern } of LOG_LEVEL_PATTERNS) {\n if (pattern.test(normalised)) {\n return level\n }\n }\n if (ERROR_INDICATORS.some((i) => normalised.includes(i.toLowerCase()))) {\n return 'error'\n }\n return 'log'\n}\n\n/** Build a ConsoleLog entry tagged with the supplied source. */\nexport function createConsoleLogEntry(\n type: LogLevel,\n args: unknown[],\n source: LogSource = LOG_SOURCES.TEST\n): ConsoleLog {\n return { timestamp: Date.now(), type, args, source }\n}\n\n/**\n * Map raw Chrome browser-log entries (the shape returned by both\n * `driver.manage().logs().get('browser')` in selenium-webdriver and\n * `browser.getLog('browser')` in nightwatch) into the dashboard's typed\n * ConsoleLog shape, tagged as source='browser'. Each entry's Chrome level\n * (`SEVERE` / `WARNING` / `INFO` / `DEBUG`) is normalised through\n * {@link chromeLogLevelToLogLevel}.\n */\nexport function mapChromeBrowserLogs(\n entries: Array<{ level: unknown; message: string; timestamp: number }>\n): ConsoleLog[] {\n return entries.map((entry) => ({\n timestamp: entry.timestamp,\n type: chromeLogLevelToLogLevel(\n entry.level as string | { value?: number; name?: string }\n ),\n args: [entry.message],\n source: LOG_SOURCES.BROWSER\n }))\n}\n\n/**\n * Map a Chrome DevTools log-level string (or `{name, value}` object) to our\n * `LogLevel` union. Used by CDP/BiDi consumers that surface browser-side\n * console output through SEVERE/WARNING/INFO/DEBUG severity names.\n */\nexport function chromeLogLevelToLogLevel(\n level: string | { value?: number; name?: string }\n): LogLevel {\n const levelName = (\n typeof level === 'object' ? (level?.name ?? '') : (level ?? '')\n ).toUpperCase()\n switch (levelName) {\n case 'SEVERE':\n return 'error'\n case 'WARNING':\n return 'warn'\n case 'INFO':\n return 'info'\n case 'DEBUG':\n return 'debug'\n default:\n return 'log'\n }\n}\n","import * as net from 'node:net'\n\n/**\n * Return true if the given TCP port on `hostname` cannot be bound for\n * listening (already in use, or otherwise unavailable).\n */\nexport function isPortInUse(port: number, hostname: string): Promise<boolean> {\n return new Promise((resolve) => {\n const server = net.createServer()\n server.once('error', () => resolve(true))\n server.once('listening', () => server.close(() => resolve(false)))\n server.listen(port, hostname)\n })\n}\n\n/**\n * Walk upward from `startPort` until a free port is found and return it.\n * Silent: callers that want to log retries should wrap this themselves.\n */\nexport async function findFreePort(\n startPort: number,\n hostname: string\n): Promise<number> {\n let port = startPort\n while (await isPortInUse(port, hostname)) {\n port++\n }\n return port\n}\n\n/**\n * Classify an HTTP request into the categories the dashboard's Network tab\n * uses, preferring the response `mimeType` and falling back to URL extension\n * heuristics. Unknown shapes return `'xhr'`.\n */\nexport function getRequestType(url: string, mimeType?: string): string {\n const contentType = mimeType?.toLowerCase() ?? ''\n const urlLower = url.toLowerCase()\n if (contentType.includes('text/html')) {\n return 'document'\n }\n if (contentType.includes('text/css')) {\n return 'stylesheet'\n }\n if (\n contentType.includes('javascript') ||\n contentType.includes('ecmascript')\n ) {\n return 'script'\n }\n if (contentType.includes('image/')) {\n return 'image'\n }\n if (contentType.includes('font/') || contentType.includes('woff')) {\n return 'font'\n }\n if (contentType.includes('application/json')) {\n return 'fetch'\n }\n if (urlLower.endsWith('.html') || urlLower.endsWith('.htm')) {\n return 'document'\n }\n if (urlLower.endsWith('.css')) {\n return 'stylesheet'\n }\n if (urlLower.endsWith('.js') || urlLower.endsWith('.mjs')) {\n return 'script'\n }\n if (/\\.(png|jpg|jpeg|gif|svg|webp|ico)$/.test(urlLower)) {\n return 'image'\n }\n if (/\\.(woff|woff2|ttf|eot|otf)$/.test(urlLower)) {\n return 'font'\n }\n return 'xhr'\n}\n"],"mappings":"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","names":[]}