@wdio/nightwatch-devtools 1.5.1 → 1.7.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/README.md +131 -35
- package/dist/bidi-4GYMJUJN.js +42 -0
- package/dist/bidi-4GYMJUJN.js.map +1 -0
- package/dist/chunk-QYGJTYKJ.js +380 -0
- package/dist/chunk-QYGJTYKJ.js.map +1 -0
- package/dist/helpers/cucumberHooks.cjs +68 -0
- package/dist/index.d.ts +43 -44
- package/dist/index.js +6970 -3684
- package/dist/index.js.map +1 -1
- package/package.json +10 -7
- package/dist/bidi-FIHBBGT4.js +0 -229
- package/dist/bidi-FIHBBGT4.js.map +0 -1
- package/dist/chunk-DKSBYMPM.js +0 -191
- package/dist/chunk-DKSBYMPM.js.map +0 -1
package/README.md
CHANGED
|
@@ -8,6 +8,46 @@ npm install @wdio/nightwatch-devtools
|
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
**1. Install the package** in your Nightwatch project:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @wdio/nightwatch-devtools
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
**2. Wire the plugin into your Nightwatch config** via the `globals` field:
|
|
20
|
+
|
|
21
|
+
```javascript
|
|
22
|
+
// nightwatch.conf.cjs
|
|
23
|
+
const nightwatchDevtools = require('@wdio/nightwatch-devtools').default
|
|
24
|
+
|
|
25
|
+
module.exports = {
|
|
26
|
+
src_folders: ['tests'],
|
|
27
|
+
|
|
28
|
+
test_settings: {
|
|
29
|
+
default: {
|
|
30
|
+
desiredCapabilities: {
|
|
31
|
+
browserName: 'chrome',
|
|
32
|
+
// Required for network request capture
|
|
33
|
+
'goog:loggingPrefs': { performance: 'ALL' }
|
|
34
|
+
},
|
|
35
|
+
globals: nightwatchDevtools({ port: 3000 })
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**3. Run your tests as normal** — the DevTools UI opens automatically in a new browser window:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
nightwatch
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
No changes to your test files are needed.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
11
51
|
## Setup
|
|
12
52
|
|
|
13
53
|
### Standard Nightwatch (mocha-style)
|
|
@@ -76,15 +116,9 @@ module.exports = {
|
|
|
76
116
|
|
|
77
117
|
---
|
|
78
118
|
|
|
79
|
-
## Configuration
|
|
119
|
+
## Configuration
|
|
80
120
|
|
|
81
|
-
|
|
82
|
-
|--------|------|---------|-------------|
|
|
83
|
-
| `port` | `number` | `3000` | Port for the DevTools backend server. Auto-incremented if already in use. |
|
|
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). |
|
|
121
|
+
Options are passed to the plugin through the `globals` field — `globals: nightwatchDevtools({ ... })`. **All options are optional**; the plugin runs fine with `nightwatchDevtools({})`. See [Reference](#reference) for the full option table.
|
|
88
122
|
|
|
89
123
|
```javascript
|
|
90
124
|
globals: nightwatchDevtools({
|
|
@@ -96,6 +130,71 @@ globals: nightwatchDevtools({
|
|
|
96
130
|
|
|
97
131
|
---
|
|
98
132
|
|
|
133
|
+
## Live Mode
|
|
134
|
+
|
|
135
|
+
In the default `mode: 'live'`, the plugin launches the DevTools dashboard in a new browser window when your tests run, streaming each test's commands, screenshots, console logs, and network activity in real time. See the [dashboard docs](https://webdriver.io/docs/devtools/dashboard) for a walkthrough of the UI.
|
|
136
|
+
|
|
137
|
+
### Preserve & Rerun (Compare)
|
|
138
|
+
|
|
139
|
+
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.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Trace Mode
|
|
144
|
+
|
|
145
|
+
Set `mode: 'trace'` to skip the DevTools UI window and instead write a portable, self-contained trace artifact at run end — for offline replay, CI artifact collection, or AI-agent diffing. The backend port-bind, the UI window, and the live-only `screencast` option are all skipped in trace mode.
|
|
146
|
+
|
|
147
|
+
```javascript
|
|
148
|
+
globals: nightwatchDevtools({
|
|
149
|
+
mode: 'trace',
|
|
150
|
+
traceFormat: 'ndjson-directory' // optional; default 'zip'
|
|
151
|
+
})
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Nightwatch emits the **same normalized trace** as the WebdriverIO and Selenium adapters (all three share `@wdio/devtools-core`), so the archive format and the `show-trace` player are identical no matter which adapter produced it. A Nightwatch trace carries the full per-action capture — a screenshot, the depth-indented accessibility-tree snapshot, the interactable-element list, and the Markdown transcript — so it opens in the player with DOM/snapshot time-travel, the **A11y** and **Transcript** tabs, an **Errors** tab surfacing expect/step failures with jump-to-source, the pick-locator element overlay, and (for Cucumber) **Feature → Scenario → Step** nesting.
|
|
155
|
+
|
|
156
|
+
Open a trace with the `show-trace` bin, shipped with this package (no extra dependency):
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
npx show-trace test-results/trace-<sessionId>.zip # in a project that installs this adapter
|
|
160
|
+
pnpm show-trace test-results/trace-<sessionId>.zip # from the devtools monorepo
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
For the full artifact contents, the `traceGranularity` / `tracePolicy` reference, and the player walkthrough, see [Trace mode](../../README.md#-trace-mode-tracezip) and [Viewing traces](../../README.md#viewing-traces) in the root README, and the [Trace Mode docs](https://webdriver.io/docs/devtools/wdio/trace-mode).
|
|
164
|
+
|
|
165
|
+
### Per-test slicing & the BDD `describe/it` caveat
|
|
166
|
+
|
|
167
|
+
The per-test options — `traceGranularity: 'test'`, and the `tracePolicy`, `screenshot`, and `video` options that pair with it — need a per-test hook to cut each test's slice. The **exports-object (mocha-style)** interface and **Cucumber** (per-scenario hooks) expose one, so they get real per-test slicing. The **BDD `describe/it`** interface — the style used by this package's bundled example — is the exception: Nightwatch runs each `it()` internally and fires the plugin's per-test hook only once per module, so `traceGranularity: 'test'` collapses to a single **session-scoped** slice keyed to the first test. The artifacts manifest still lists every testcase with its correct state; only the per-test slice/artifact keying collapses. Session- and spec-granularity traces are unaffected.
|
|
168
|
+
|
|
169
|
+
**Cucumber and `traceGranularity`.** Nightwatch quits the browser after every scenario. A `'session'` trace now spans that — one zip covering the whole run, every scenario, with each scenario nested under its feature. (Before, it captured only the *last* scenario, silently.) `'test'` remains the recommendation for Cucumber: one zip per scenario, in its own folder, keyed for retention and smaller to open.
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## BiDi capture
|
|
174
|
+
|
|
175
|
+
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`.
|
|
176
|
+
|
|
177
|
+
```javascript
|
|
178
|
+
globals: nightwatchDevtools({
|
|
179
|
+
port: 3000,
|
|
180
|
+
bidi: true
|
|
181
|
+
})
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
You also need `webSocketUrl: true` in your capabilities so chromedriver actually exposes the BiDi channel:
|
|
185
|
+
|
|
186
|
+
```javascript
|
|
187
|
+
desiredCapabilities: {
|
|
188
|
+
browserName: 'chrome',
|
|
189
|
+
'webSocketUrl': true, // ← enables BiDi
|
|
190
|
+
'goog:chromeOptions': { /* ... */ }
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
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.
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
99
198
|
## Screencast
|
|
100
199
|
|
|
101
200
|
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.
|
|
@@ -130,6 +229,28 @@ The encoded video is sent to the DevTools dashboard via the `screencast` WS scop
|
|
|
130
229
|
|
|
131
230
|
---
|
|
132
231
|
|
|
232
|
+
## Reference
|
|
233
|
+
|
|
234
|
+
| Option | Type | Default | Description |
|
|
235
|
+
|--------|------|---------|-------------|
|
|
236
|
+
| `port` | `number` | `3000` | Port for the DevTools backend server. Auto-incremented if already in use. |
|
|
237
|
+
| `hostname` | `string` | `'localhost'` | Hostname the backend server binds to. |
|
|
238
|
+
| `screencast` | `ScreencastOptions` | `{ enabled: false }` | Session video recording — live mode only (see [Screencast](#screencast)). |
|
|
239
|
+
| `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. |
|
|
240
|
+
| `mode` | `'live' \| 'trace'` | `'live'` | `'live'` opens the DevTools UI window; `'trace'` skips the UI and writes a `trace-<sessionId>.zip` (or directory) under a `test-results/` directory at run end (base dir resolved from the test file dir → config dir → cwd). See [Trace mode](#trace-mode). |
|
|
241
|
+
| `traceFormat` | `'zip' \| 'ndjson-directory'` | `'zip'` | Trace artifact layout — `zip` writes a single archive, `ndjson-directory` unpacks the same files into `trace-<sessionId>/`. Only applies when `mode: 'trace'`. |
|
|
242
|
+
| `traceGranularity` | `'session' \| 'spec' \| 'test'` | `'session'` | One trace per session / spec file / test. `'test'` is what the per-test `screenshot` / `video` artifacts attach to. Only applies when `mode: 'trace'`. **Caveat:** the BDD `describe/it` interface collapses to a single session-scoped slice (see [Per-test slicing](#per-test-slicing--the-bdd-describeit-caveat)). For Cucumber, `'session'` spans every scenario in one zip; `'test'` gives one per scenario and is the recommendation. |
|
|
243
|
+
| `tracePolicy` | `'on' \| 'retain-on-failure' \| 'retain-on-first-failure' \| 'on-first-retry' \| 'on-all-retries' \| 'retain-on-failure-and-retries'` | `'on'` | Which traces to keep. Pairs with `traceGranularity: 'test'`. Only applies when `mode: 'trace'`. |
|
|
244
|
+
| `filmstrip` | `boolean` | `true` | Record a dense, continuous screencast filmstrip into the trace for scrubbable playback in the trace player — not just one frame per action. Runs the screencast recorder (polling mode on Nightwatch) for the session. Only applies when `mode: 'trace'`. |
|
|
245
|
+
| `screenshot` | `'off' \| 'on' \| 'only-on-failure'` | `'off'` | Per-test screenshot. Trace mode + `traceGranularity: 'test'` only. **Produce-only** — the PNG is written to the trace output dir (and listed in the artifacts manifest when `emitArtifactsManifest: true`); it is NOT attached inline to Allure (see note below). |
|
|
246
|
+
| `video` | `'off' \| TraceRetentionPolicy` | `'off'` | Per-test video slice, retained per the given policy (e.g. `'retain-on-failure'`). Trace mode + `traceGranularity: 'test'` only. Setting a non-`off` policy starts the screencast recorder itself — you do **not** also need `filmstrip` or `screencast.enabled`; the recorder runs continuously for the session and each test's slice is cut from it by wall-clock window. **Produce-only** — the `.webm` is written to the trace output dir (and listed in the manifest when `emitArtifactsManifest: true`); NOT attached inline to Allure. |
|
|
247
|
+
| `emitArtifactsManifest` | `boolean` | `false` | Write the `devtools-artifacts-<sessionId>.json` manifest — the generic index reporters/CI consume to discover produced artifacts — next to the trace. **Opt-in for Nightwatch**: it has no live Allure signal to auto-detect against (`nightwatch-allure` is post-hoc), so unlike WDIO/Selenium it never auto-enables. Trace mode only. |
|
|
248
|
+
| `captureAssertions` | `boolean` | `true` | Capture assertions as trace action rows — `node:assert` plus native `browser.assert.*` / `browser.verify.*`, including negated `.not.*` matchers. Passing assertions render green, failing ones red with the error. Set `false` to opt out. |
|
|
249
|
+
|
|
250
|
+
> **Inline Allure attachment is not supported for Nightwatch.** Its official `nightwatch-allure` reporter is post-hoc (no live attach API), and `allure-js-commons`' `attachment()` no-ops in a Nightwatch run. So `screenshot`/`video` artifacts are *produced* (files, plus the artifacts manifest when `emitArtifactsManifest: true`) in the trace output dir but not attached to an Allure test. Per-test slicing (and therefore these artifacts) is meaningful for the cucumber and exports-object interfaces; the BDD `describe/it` interface collapses to session granularity, so the gate no-ops there.
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
133
254
|
## Examples
|
|
134
255
|
|
|
135
256
|
Working examples are included in this package:
|
|
@@ -159,36 +280,11 @@ Nightwatch does not provide the same depth of framework hooks as WebdriverIO, so
|
|
|
159
280
|
| Limited test context | `browser.currentTest` provides less metadata than the WDIO runner context; test names and file paths require additional heuristics. |
|
|
160
281
|
| Flat suite nesting | Nightwatch does not natively support multiply-nested `describe` blocks; the plugin reports a maximum of two levels. |
|
|
161
282
|
| Delayed result availability | Test results are only finalised in `afterEach`, not available mid-test. |
|
|
283
|
+
| Per-test trace slicing (BDD `describe/it`) | The BDD interface fires the plugin's per-test hook once per module, so `traceGranularity: 'test'` collapses to one session-scoped slice. The exports-object (mocha-style) and Cucumber interfaces get real per-test slicing. See [Per-test slicing](#per-test-slicing--the-bdd-describeit-caveat). |
|
|
284
|
+
| Produce-only trace artifacts | Per-test `screenshot` / `video` files are written to the trace output dir (and the manifest when `emitArtifactsManifest: true`) but not attached inline to Allure — Nightwatch has no live Allure attach API. |
|
|
162
285
|
|
|
163
286
|
Overall feature parity with the WebdriverIO DevTools service is approximately **80–90%**.
|
|
164
287
|
|
|
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
|
-
|
|
192
288
|
## :page_facing_up: License
|
|
193
289
|
|
|
194
290
|
[MIT](/LICENSE)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import {
|
|
2
|
+
attachBidiHandlers
|
|
3
|
+
} from "./chunk-QYGJTYKJ.js";
|
|
4
|
+
|
|
5
|
+
// src/bidi.ts
|
|
6
|
+
import logger from "@wdio/logger";
|
|
7
|
+
var log = logger("@wdio/nightwatch-devtools:bidi");
|
|
8
|
+
async function attachBidiHandlers2(driver, sinks) {
|
|
9
|
+
return attachBidiHandlers(
|
|
10
|
+
driver,
|
|
11
|
+
sinks,
|
|
12
|
+
(level, message) => log[level](message)
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
function buildBidiSinks(capturer) {
|
|
16
|
+
return {
|
|
17
|
+
pushConsoleLog: (entry) => {
|
|
18
|
+
capturer.consoleLogs.push(entry);
|
|
19
|
+
capturer.sendUpstream("consoleLogs", [entry]);
|
|
20
|
+
},
|
|
21
|
+
pushNetworkRequest: (entry) => {
|
|
22
|
+
capturer.networkRequests.push(entry);
|
|
23
|
+
capturer.sendUpstream("networkRequests", [entry]);
|
|
24
|
+
},
|
|
25
|
+
replaceNetworkRequest: (id, entry) => {
|
|
26
|
+
const idx = capturer.networkRequests.findIndex(
|
|
27
|
+
(r) => r.id === id
|
|
28
|
+
);
|
|
29
|
+
if (idx !== -1) {
|
|
30
|
+
capturer.networkRequests[idx] = entry;
|
|
31
|
+
} else {
|
|
32
|
+
capturer.networkRequests.push(entry);
|
|
33
|
+
}
|
|
34
|
+
capturer.sendUpstream("networkRequests", [entry]);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export {
|
|
39
|
+
attachBidiHandlers2 as attachBidiHandlers,
|
|
40
|
+
buildBidiSinks
|
|
41
|
+
};
|
|
42
|
+
//# sourceMappingURL=bidi-4GYMJUJN.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../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"],"mappings":";;;;;AAAA,OAAO,YAAY;AAOnB,IAAM,MAAM,OAAO,gCAAgC;AAYnD,eAAsBA,oBACpB,QACA,OACkB;AAClB,SAAO;AAAA,IAAuB;AAAA,IAAQ;AAAA,IAAO,CAAC,OAAO,YACnD,IAAI,KAAK,EAAE,OAAO;AAAA,EACpB;AACF;AAOO,SAAS,eAAe,UAA6C;AAC1E,SAAO;AAAA,IACL,gBAAgB,CAAC,UAAU;AACzB,eAAS,YAAY,KAAK,KAAK;AAC/B,eAAS,aAAa,eAAe,CAAC,KAAK,CAAC;AAAA,IAC9C;AAAA,IACA,oBAAoB,CAAC,UAAU;AAC7B,eAAS,gBAAgB,KAAK,KAAK;AACnC,eAAS,aAAa,mBAAmB,CAAC,KAAK,CAAC;AAAA,IAClD;AAAA,IACA,uBAAuB,CAAC,IAAI,UAAU;AACpC,YAAM,MAAM,SAAS,gBAAgB;AAAA,QACnC,CAAC,MAAuB,EAAE,OAAO;AAAA,MACnC;AACA,UAAI,QAAQ,IAAI;AACd,iBAAS,gBAAgB,GAAG,IAAI;AAAA,MAClC,OAAO;AACL,iBAAS,gBAAgB,KAAK,KAAK;AAAA,MACrC;AACA,eAAS,aAAa,mBAAmB,CAAC,KAAK,CAAC;AAAA,IAClD;AAAA,EACF;AACF;","names":["attachBidiHandlers"]}
|
|
@@ -0,0 +1,380 @@
|
|
|
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/bidi.ts
|
|
43
|
+
import { createRequire } from "module";
|
|
44
|
+
|
|
45
|
+
// ../core/src/console.ts
|
|
46
|
+
var CONSOLE_METHODS = ["log", "info", "warn", "error"];
|
|
47
|
+
var ANSI_REGEX = /\x1b\[[?]?[0-9;]*[A-Za-z]/g;
|
|
48
|
+
function stripAnsi(text) {
|
|
49
|
+
return text.replace(ANSI_REGEX, "");
|
|
50
|
+
}
|
|
51
|
+
var LOG_LEVEL_PATTERNS = [
|
|
52
|
+
{ level: "trace", pattern: /\btrace\b/i },
|
|
53
|
+
{ level: "debug", pattern: /\bdebug\b/i },
|
|
54
|
+
{ level: "info", pattern: /\binfo\b/i },
|
|
55
|
+
{ level: "warn", pattern: /\bwarn(ing)?\b/i },
|
|
56
|
+
{ level: "error", pattern: /\berror\b/i }
|
|
57
|
+
];
|
|
58
|
+
var ERROR_INDICATORS = ["\u2717", "failed", "failure"];
|
|
59
|
+
var SPINNER_RE = /^[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏]/u;
|
|
60
|
+
function isInternalStreamLine(line) {
|
|
61
|
+
const t = line.trim();
|
|
62
|
+
if (t.startsWith('{"') || t.startsWith("[SESSION]")) {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
if (t.includes("@wdio/devtools-backend")) {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
if (/^console\.(log|info|warn|error|debug|trace)$/.test(t)) {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
if (/^at\s.+:\d+:\d+\)?$/.test(t)) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
var LOG_SOURCES = {
|
|
77
|
+
BROWSER: "browser",
|
|
78
|
+
TEST: "test",
|
|
79
|
+
TERMINAL: "terminal"
|
|
80
|
+
};
|
|
81
|
+
function detectLogLevel(text) {
|
|
82
|
+
const normalised = stripAnsi(text).toLowerCase();
|
|
83
|
+
for (const { level, pattern } of LOG_LEVEL_PATTERNS) {
|
|
84
|
+
if (pattern.test(normalised)) {
|
|
85
|
+
return level;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (ERROR_INDICATORS.some((i) => normalised.includes(i.toLowerCase()))) {
|
|
89
|
+
return "error";
|
|
90
|
+
}
|
|
91
|
+
return "log";
|
|
92
|
+
}
|
|
93
|
+
function createConsoleLogEntry(type, args, source = LOG_SOURCES.TEST) {
|
|
94
|
+
return { timestamp: Date.now(), type, args, source };
|
|
95
|
+
}
|
|
96
|
+
function mapChromeBrowserLogs(entries) {
|
|
97
|
+
return entries.map((entry) => ({
|
|
98
|
+
timestamp: entry.timestamp,
|
|
99
|
+
type: chromeLogLevelToLogLevel(
|
|
100
|
+
entry.level
|
|
101
|
+
),
|
|
102
|
+
args: [entry.message],
|
|
103
|
+
source: LOG_SOURCES.BROWSER
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
106
|
+
function chromeLogLevelToLogLevel(level) {
|
|
107
|
+
const levelName = (typeof level === "object" ? level?.name ?? "" : level ?? "").toUpperCase();
|
|
108
|
+
switch (levelName) {
|
|
109
|
+
case "SEVERE":
|
|
110
|
+
return "error";
|
|
111
|
+
case "WARNING":
|
|
112
|
+
return "warn";
|
|
113
|
+
case "INFO":
|
|
114
|
+
return "info";
|
|
115
|
+
case "DEBUG":
|
|
116
|
+
return "debug";
|
|
117
|
+
default:
|
|
118
|
+
return "log";
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ../core/src/net.ts
|
|
123
|
+
import * as net from "net";
|
|
124
|
+
|
|
125
|
+
// ../core/src/request-type.ts
|
|
126
|
+
function getRequestType(url, mimeType) {
|
|
127
|
+
const contentType = mimeType?.toLowerCase() ?? "";
|
|
128
|
+
const urlLower = url.toLowerCase();
|
|
129
|
+
if (contentType.includes("text/html")) {
|
|
130
|
+
return "document";
|
|
131
|
+
}
|
|
132
|
+
if (contentType.includes("text/css")) {
|
|
133
|
+
return "stylesheet";
|
|
134
|
+
}
|
|
135
|
+
if (contentType.includes("javascript") || contentType.includes("ecmascript")) {
|
|
136
|
+
return "script";
|
|
137
|
+
}
|
|
138
|
+
if (contentType.includes("image/")) {
|
|
139
|
+
return "image";
|
|
140
|
+
}
|
|
141
|
+
if (contentType.includes("font/") || contentType.includes("woff")) {
|
|
142
|
+
return "font";
|
|
143
|
+
}
|
|
144
|
+
if (contentType.includes("application/json")) {
|
|
145
|
+
return "fetch";
|
|
146
|
+
}
|
|
147
|
+
if (urlLower.endsWith(".html") || urlLower.endsWith(".htm")) {
|
|
148
|
+
return "document";
|
|
149
|
+
}
|
|
150
|
+
if (urlLower.endsWith(".css")) {
|
|
151
|
+
return "stylesheet";
|
|
152
|
+
}
|
|
153
|
+
if (urlLower.endsWith(".js") || urlLower.endsWith(".mjs")) {
|
|
154
|
+
return "script";
|
|
155
|
+
}
|
|
156
|
+
if (/\.(png|jpg|jpeg|gif|svg|webp|ico)$/.test(urlLower)) {
|
|
157
|
+
return "image";
|
|
158
|
+
}
|
|
159
|
+
if (/\.(woff|woff2|ttf|eot|otf)$/.test(urlLower)) {
|
|
160
|
+
return "font";
|
|
161
|
+
}
|
|
162
|
+
return "xhr";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ../core/src/net.ts
|
|
166
|
+
function isPortInUse(port, hostname) {
|
|
167
|
+
return new Promise((resolve) => {
|
|
168
|
+
const server = net.createServer();
|
|
169
|
+
server.once("error", () => resolve(true));
|
|
170
|
+
server.once("listening", () => server.close(() => resolve(false)));
|
|
171
|
+
server.listen(port, hostname);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
async function findFreePort(startPort, hostname) {
|
|
175
|
+
let port = startPort;
|
|
176
|
+
while (await isPortInUse(port, hostname)) {
|
|
177
|
+
port++;
|
|
178
|
+
}
|
|
179
|
+
return port;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ../core/src/bidi.ts
|
|
183
|
+
function loadSeleniumSubmodule(subpath) {
|
|
184
|
+
try {
|
|
185
|
+
const userRequire = createRequire(`${process.cwd()}/`);
|
|
186
|
+
return userRequire(`selenium-webdriver/${subpath}`);
|
|
187
|
+
} catch {
|
|
188
|
+
try {
|
|
189
|
+
const localRequire = createRequire(import.meta.url);
|
|
190
|
+
return localRequire(`selenium-webdriver/${subpath}`);
|
|
191
|
+
} catch {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function handleBidiConsoleEntry(rawEntry, sinks, log) {
|
|
197
|
+
const entry = rawEntry;
|
|
198
|
+
try {
|
|
199
|
+
const level = (entry?.level ?? entry?.type ?? "info").toString();
|
|
200
|
+
const text = entry?.text ?? entry?.message ?? "";
|
|
201
|
+
sinks.pushConsoleLog({
|
|
202
|
+
timestamp: Number(entry?.timestamp) || Date.now(),
|
|
203
|
+
type: chromeLogLevelToLogLevel(level),
|
|
204
|
+
args: [text],
|
|
205
|
+
source: LOG_SOURCES.BROWSER
|
|
206
|
+
});
|
|
207
|
+
} catch (err) {
|
|
208
|
+
log("warn", `onConsoleEntry handler threw: ${errorMessage(err)}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
function handleBidiJsException(rawExc, sinks, log) {
|
|
212
|
+
const exception = rawExc;
|
|
213
|
+
try {
|
|
214
|
+
const text = exception?.text ?? exception?.message ?? String(rawExc);
|
|
215
|
+
const trimmed = String(text).replace(/\s+/g, " ").slice(0, 200);
|
|
216
|
+
log(
|
|
217
|
+
"warn",
|
|
218
|
+
`\u{1F41B} JS error in page: ${trimmed}${String(text).length > 200 ? "\u2026" : ""}`
|
|
219
|
+
);
|
|
220
|
+
sinks.pushConsoleLog({
|
|
221
|
+
timestamp: Date.now(),
|
|
222
|
+
type: "error",
|
|
223
|
+
args: [text],
|
|
224
|
+
source: LOG_SOURCES.BROWSER
|
|
225
|
+
});
|
|
226
|
+
} catch (err) {
|
|
227
|
+
log("warn", `onJavascriptException handler threw: ${errorMessage(err)}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
async function attachLogInspector(driver, factory, sinks, log) {
|
|
231
|
+
try {
|
|
232
|
+
const inspector = await factory(driver);
|
|
233
|
+
await inspector.onConsoleEntry((e) => handleBidiConsoleEntry(e, sinks, log));
|
|
234
|
+
await inspector.onJavascriptException(
|
|
235
|
+
(e) => handleBidiJsException(e, sinks, log)
|
|
236
|
+
);
|
|
237
|
+
log("info", "\u2713 BiDi LogInspector attached (console + JS exceptions)");
|
|
238
|
+
return true;
|
|
239
|
+
} catch (err) {
|
|
240
|
+
log("warn", `BiDi LogInspector attach failed: ${errorMessage(err)}`);
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
var isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
|
|
245
|
+
function bidiResponseTiming(timings, startTime, timestamp) {
|
|
246
|
+
if (timings && isFiniteNumber(timings.requestTime) && isFiniteNumber(timings.responseEnd) && timings.responseEnd > timings.requestTime) {
|
|
247
|
+
const time = Math.round(timings.responseEnd - timings.requestTime);
|
|
248
|
+
return { endTime: startTime + time, time };
|
|
249
|
+
}
|
|
250
|
+
const endTime = Number(timestamp ?? Date.now());
|
|
251
|
+
return { endTime, time: Math.max(0, endTime - startTime) };
|
|
252
|
+
}
|
|
253
|
+
function handleBidiRequestSent(rawEvent, pending, sinks, log) {
|
|
254
|
+
const event = rawEvent;
|
|
255
|
+
try {
|
|
256
|
+
const requestId = String(event?.request?.request ?? event?.id ?? "");
|
|
257
|
+
if (!requestId) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const entry = {
|
|
261
|
+
id: requestId,
|
|
262
|
+
url: event?.request?.url ?? "",
|
|
263
|
+
method: event?.request?.method ?? "GET",
|
|
264
|
+
requestHeaders: arrayHeadersToObject(event?.request?.headers),
|
|
265
|
+
timestamp: Date.now(),
|
|
266
|
+
startTime: Number(event?.timestamp ?? Date.now()),
|
|
267
|
+
type: getRequestType(event?.request?.url ?? "")
|
|
268
|
+
};
|
|
269
|
+
pending.set(requestId, entry);
|
|
270
|
+
sinks.pushNetworkRequest(entry);
|
|
271
|
+
} catch (err) {
|
|
272
|
+
log("warn", `beforeRequestSent threw: ${errorMessage(err)}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
function handleBidiResponseCompleted(rawEvent, pending, sinks, log) {
|
|
276
|
+
const event = rawEvent;
|
|
277
|
+
try {
|
|
278
|
+
const requestId = String(event?.request?.request ?? event?.id ?? "");
|
|
279
|
+
const previous = pending.get(requestId);
|
|
280
|
+
if (!previous) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const { endTime, time } = bidiResponseTiming(
|
|
284
|
+
event?.request?.timings,
|
|
285
|
+
previous.startTime,
|
|
286
|
+
event?.timestamp
|
|
287
|
+
);
|
|
288
|
+
const finalized = {
|
|
289
|
+
...previous,
|
|
290
|
+
status: Number(event?.response?.status) || previous.status,
|
|
291
|
+
statusText: event?.response?.statusText ?? previous.statusText,
|
|
292
|
+
responseHeaders: arrayHeadersToObject(event?.response?.headers),
|
|
293
|
+
type: getRequestType(previous.url, event?.response?.mimeType),
|
|
294
|
+
endTime,
|
|
295
|
+
time,
|
|
296
|
+
size: Number(event?.response?.bytesReceived) || void 0
|
|
297
|
+
};
|
|
298
|
+
pending.delete(requestId);
|
|
299
|
+
sinks.replaceNetworkRequest(requestId, finalized);
|
|
300
|
+
} catch (err) {
|
|
301
|
+
log("warn", `responseCompleted threw: ${errorMessage(err)}`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
async function attachNetworkInspector(driver, factory, sinks, log) {
|
|
305
|
+
try {
|
|
306
|
+
const inspector = await factory(driver);
|
|
307
|
+
const pending = /* @__PURE__ */ new Map();
|
|
308
|
+
await inspector.beforeRequestSent(
|
|
309
|
+
(e) => handleBidiRequestSent(e, pending, sinks, log)
|
|
310
|
+
);
|
|
311
|
+
await inspector.responseCompleted(
|
|
312
|
+
(e) => handleBidiResponseCompleted(e, pending, sinks, log)
|
|
313
|
+
);
|
|
314
|
+
log("info", "\u2713 BiDi NetworkInspector attached (request + response)");
|
|
315
|
+
return true;
|
|
316
|
+
} catch (err) {
|
|
317
|
+
log("warn", `BiDi NetworkInspector attach failed: ${errorMessage(err)}`);
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
async function attachBidiHandlers(driver, sinks, onLog) {
|
|
322
|
+
const log = (level, message) => onLog?.(level, message);
|
|
323
|
+
const logFactory = loadSeleniumSubmodule("bidi/logInspector");
|
|
324
|
+
const networkFactory = loadSeleniumSubmodule(
|
|
325
|
+
"bidi/networkInspector"
|
|
326
|
+
);
|
|
327
|
+
let attached = 0;
|
|
328
|
+
if (typeof logFactory === "function") {
|
|
329
|
+
if (await attachLogInspector(driver, logFactory, sinks, log)) {
|
|
330
|
+
attached++;
|
|
331
|
+
}
|
|
332
|
+
} else {
|
|
333
|
+
log("info", "selenium-webdriver/bidi/logInspector not available \u2014 skipping");
|
|
334
|
+
}
|
|
335
|
+
if (typeof networkFactory === "function") {
|
|
336
|
+
if (await attachNetworkInspector(driver, networkFactory, sinks, log)) {
|
|
337
|
+
attached++;
|
|
338
|
+
}
|
|
339
|
+
} else {
|
|
340
|
+
log(
|
|
341
|
+
"info",
|
|
342
|
+
"selenium-webdriver/bidi/networkInspector not available \u2014 skipping"
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
return attached > 0;
|
|
346
|
+
}
|
|
347
|
+
function arrayHeadersToObject(headers) {
|
|
348
|
+
if (!Array.isArray(headers)) {
|
|
349
|
+
return void 0;
|
|
350
|
+
}
|
|
351
|
+
const out = {};
|
|
352
|
+
for (const h of headers) {
|
|
353
|
+
const name = String(h?.name ?? "").toLowerCase();
|
|
354
|
+
if (!name) {
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
const v = h?.value;
|
|
358
|
+
out[name] = typeof v === "string" ? v : typeof v?.value === "string" ? v.value : JSON.stringify(v ?? "");
|
|
359
|
+
}
|
|
360
|
+
return out;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export {
|
|
364
|
+
toError,
|
|
365
|
+
errorMessage,
|
|
366
|
+
serializeError,
|
|
367
|
+
CONSOLE_METHODS,
|
|
368
|
+
stripAnsi,
|
|
369
|
+
SPINNER_RE,
|
|
370
|
+
isInternalStreamLine,
|
|
371
|
+
LOG_SOURCES,
|
|
372
|
+
detectLogLevel,
|
|
373
|
+
createConsoleLogEntry,
|
|
374
|
+
mapChromeBrowserLogs,
|
|
375
|
+
getRequestType,
|
|
376
|
+
findFreePort,
|
|
377
|
+
loadSeleniumSubmodule,
|
|
378
|
+
attachBidiHandlers
|
|
379
|
+
};
|
|
380
|
+
//# sourceMappingURL=chunk-QYGJTYKJ.js.map
|