codeceptjs 4.1.0 → 4.2.0-beta.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 (41) hide show
  1. package/docs/alternative-browsers.md +153 -0
  2. package/docs/basics.md +9 -1
  3. package/docs/configuration.md +2 -0
  4. package/docs/helpers/CDPBrowser.md +2138 -0
  5. package/docs/helpers/Kitesurf.md +118 -0
  6. package/docs/helpers/Obscura.md +210 -0
  7. package/docs/migration-4.md +3 -1
  8. package/docs/parallel.md +10 -0
  9. package/docs/plugins/screencast.md +18 -13
  10. package/docs/plugins.md +1 -1
  11. package/lib/command/info.js +11 -3
  12. package/lib/command/workers/runTests.js +14 -20
  13. package/lib/container.js +6 -0
  14. package/lib/data/context.js +4 -0
  15. package/lib/element/WebElement.js +5 -0
  16. package/lib/helper/Appium.js +14 -2
  17. package/lib/helper/CDPBrowser.js +3004 -0
  18. package/lib/helper/Kitesurf.js +139 -0
  19. package/lib/helper/Obscura.js +344 -0
  20. package/lib/helper/Playwright.js +30 -3
  21. package/lib/helper/Puppeteer.js +43 -16
  22. package/lib/helper/WebDriver.js +43 -8
  23. package/lib/helper/clientscripts/cdpBrowserClient.js +486 -0
  24. package/lib/helper/clientscripts/xpathPolyfill.js +31 -0
  25. package/lib/helper/extras/CDPConnection.js +92 -0
  26. package/lib/helper/extras/CDPElementHandle.js +27 -0
  27. package/lib/helper/extras/apngAssembler.js +156 -0
  28. package/lib/html.js +9 -2
  29. package/lib/listener/retryEnhancer.js +2 -1
  30. package/lib/listener/steps.js +8 -0
  31. package/lib/mocha/hooks.js +10 -0
  32. package/lib/parser.js +14 -2
  33. package/lib/plugin/junitReporter.js +17 -1
  34. package/lib/plugin/screencast.js +116 -24
  35. package/lib/step/base.js +15 -3
  36. package/lib/utils/loaderCheck.js +6 -0
  37. package/lib/utils.js +1 -1
  38. package/lib/workers.js +17 -0
  39. package/package.json +4 -1
  40. package/typings/promiseBasedTypes.d.ts +1833 -0
  41. package/typings/types.d.ts +1840 -0
@@ -0,0 +1,118 @@
1
+ ---
2
+ permalink: /helpers/Kitesurf
3
+ editLink: false
4
+ sidebar: auto
5
+ title: Kitesurf
6
+ ---
7
+
8
+ <!-- Generated by documentation.js. Update this documentation by updating the source code. -->
9
+
10
+ ## Kitesurf
11
+
12
+ **Extends CDPBrowser**
13
+
14
+ Kitesurf is a cloud browser helper that extends CDPBrowser to run tests against
15
+ Cloudflare's Browser Run service (Kitesurf browser). It automates real browser
16
+ sessions in Cloudflare's cloud infrastructure, eliminating the need to manage
17
+ local browser instances.
18
+
19
+ **Status:** Beta
20
+
21
+ ## Requirements
22
+
23
+ * Cloudflare account with Browser Run enabled
24
+ * API token with **Browser Rendering Edit** permission
25
+
26
+ ## Setup
27
+
28
+ To create an API token with Browser Rendering Edit permission:
29
+
30
+ 1. Log in to your Cloudflare dashboard
31
+ 2. Go to My Profile > API Tokens
32
+ 3. Click "Create Token"
33
+ 4. Use the "Custom token" template
34
+ 5. Under "Permissions", select "Browser Rendering" > "Edit"
35
+ 6. Set the account scope to your target account
36
+ 7. Copy the token and set it as `CF_API_TOKEN` environment variable
37
+
38
+ For more details, see:
39
+
40
+ * [Cloudflare Browser Run Developers Docs][1]
41
+ * [Cloudflare Blog - Kitesurf Announcement][2]
42
+
43
+ ## Example
44
+
45
+ ```js
46
+ // codecept.conf.js
47
+ {
48
+ helpers: {
49
+ Kitesurf: {
50
+ url: 'https://example.com',
51
+ accountId: process.env.CF_ACCOUNT_ID,
52
+ apiToken: process.env.CF_API_TOKEN,
53
+ }
54
+ }
55
+ }
56
+ ```
57
+
58
+ Or set environment variables and rely on defaults:
59
+
60
+ ```bash
61
+ export CF_ACCOUNT_ID="your-account-id"
62
+ export CF_API_TOKEN="your-api-token"
63
+ ```
64
+
65
+
66
+
67
+ ## Configuration
68
+
69
+ This helper should be configured in codecept.conf.js. It accepts everything `CDPBrowser`
70
+ accepts (see its config table), plus:
71
+
72
+ Type: [object][5]
73
+
74
+ ### Properties
75
+
76
+ * `url` **[string][4]?** base URL for tests
77
+ * `accountId` **[string][4]?** Cloudflare account ID; defaults to CF_ACCOUNT_ID env var
78
+ * `apiToken` **[string][4]?** Cloudflare API token; defaults to CF_API_TOKEN env var
79
+ * `keepAlive` **[number][6]?** session keep-alive time in milliseconds
80
+ * `apiBase` **[string][4]?** Cloudflare API base URL
81
+ * `input` **[string][4]?** input method for user actions; defaults to 'cdp' for Kitesurf's real layout engine, but can be overridden
82
+ * `capabilities` **[object][5]?** pre-configured capabilities; Kitesurf uses { layout: 'real', screenshot: true }
83
+
84
+
85
+
86
+ ## Methods
87
+
88
+ ### Parameters
89
+
90
+ * `config` **KitesurfConfig**&#x20;
91
+
92
+ ### _finishTest
93
+
94
+ Closes the target as `CDPBrowser._finishTest` does, then releases the cloud session acquired
95
+ in `_resolveEndpoint` via the Cloudflare API so it does not linger for the full `keepAlive`
96
+ window. The release runs in a `finally` so a rejection while closing the CDP connection still
97
+ frees the cloud session instead of leaving the browser alive until `keepAlive` expires; the
98
+ session id is cleared before the request, so a repeated call never releases it twice.
99
+
100
+ ### _resolveEndpoint
101
+
102
+ Acquires a Kitesurf browser session from the Cloudflare Browser Run API and resolves it to
103
+ the `wss://` debugger URL `CDPConnection` connects to. Overrides `CDPBrowser._resolveEndpoint`,
104
+ which resolves a fixed local endpoint instead of provisioning a cloud session per test.
105
+
106
+ Returns **[Promise][3]<[string][4]>** a `wss://` debugger URL ready to be passed to `CDPConnection`.
107
+
108
+ [1]: https://developers.cloudflare.com/browser-run/
109
+
110
+ [2]: https://blog.cloudflare.com/kitesurf/
111
+
112
+ [3]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise
113
+
114
+ [4]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
115
+
116
+ [5]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
117
+
118
+ [6]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number
@@ -0,0 +1,210 @@
1
+ ---
2
+ permalink: /helpers/Obscura
3
+ editLink: false
4
+ sidebar: auto
5
+ title: Obscura
6
+ ---
7
+
8
+ <!-- Generated by documentation.js. Update this documentation by updating the source code. -->
9
+
10
+ ## Obscura
11
+
12
+ **Extends CDPBrowser**
13
+
14
+ Obscura drives [Obscura][1], a minimal headless
15
+ browser exposed over the Chrome DevTools Protocol. From v0.2.0, default release builds ship a
16
+ real rendering engine (layout, paint, screenshots); `-no-render` variants and v0.1.x builds keep
17
+ the original single-V8-isolate, nothing-rendered mode. This helper does not hardcode which mode a
18
+ given binary is in — `CDPBrowser._probeCapabilities` detects `layout`/`screenshot` per binary at
19
+ runtime, so the same helper works against either.
20
+
21
+ This helper is a thin `CDPBrowser` subclass: it changes nothing about how locating or acting on
22
+ elements works, it only pins the config presets Obscura requires and manages the `obscura serve`
23
+ process lifecycle, the same way Playwright manages its own browser process.
24
+
25
+ ## Modes
26
+
27
+ * **ATTACH** — `endpoint` is set explicitly in the config. The helper only connects to it; it
28
+ never spawns or kills anything, no matter what `binaryPath`/`port` are set to.
29
+ * **SELF-LAUNCH** — `endpoint` is unset and a binary can be resolved, in order: `binaryPath` in
30
+ the config, then the `OBSCURA_PATH` environment variable, then `obscura` on `PATH`. The helper
31
+ spawns `obscura serve --port <port> --allow-private-network` (`port` from the config, or a
32
+ free port picked automatically), waits for it to answer, connects, and kills it in
33
+ `_finishTest`.
34
+ * **COURTESY-ATTACH** — `endpoint` is unset and no binary can be resolved, but something already
35
+ answers `http://127.0.0.1:9222/json/version` (e.g. `obscura serve` started by hand, or by CI
36
+ before this process ever ran). The helper attaches to it and never kills it — it isn't the
37
+ helper's process to kill. If neither a binary nor a running server on :9222 can be found, the
38
+ helper throws a loud, actionable error.
39
+
40
+ ## Install
41
+
42
+ Download a release binary and put it on your `PATH` (or point `binaryPath`/`OBSCURA_PATH` at
43
+ it directly) and the helper launches and tears it down for you automatically:
44
+
45
+ ```sh
46
+ curl -sL https://github.com/h4ckf0r0day/obscura/releases/download/v0.2.0/obscura-x86_64-linux.tar.gz | tar xz
47
+ ```
48
+
49
+ `--allow-private-network` is always passed by this helper (it's required to reach apps running
50
+ on `localhost`/private IPs, e.g. a dev server on `127.0.0.1:8000` — Obscura blocks
51
+ private-network requests by default).
52
+
53
+ ## Config presets
54
+
55
+ These are set automatically and only need overriding for unusual setups:
56
+
57
+ | option | value | why |
58
+ | --------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
59
+ | `input` | `synthetic` | coordinate-click navigation is unreliable over CDP on Obscura even on rendering builds (no `frameNavigated` event, stale `page.url()`); `click` always takes the `forceClick` path — on Obscura, `click` and `forceClick` are the same thing |
60
+ | `xpathPolyfill` | `auto` | probed per binary/page: Obscura's native `document.evaluate` still doesn't support attribute selection or `not()`, so the polyfill is used until that lands |
61
+
62
+ `capabilities.layout`/`capabilities.screenshot`/`capabilities.xpath` are intentionally left
63
+ unset here — `CDPBrowser._probeCapabilities` detects them at runtime from the actual binary
64
+ (`'real'`/`true` on v0.2.0+ default builds, `'none'`/`false` on `-no-render` builds and v0.1.x).
65
+ Set them explicitly in your own config to skip probing or to force a mode.
66
+
67
+ ## Limitations
68
+
69
+ * `input` is always `synthetic`, even on rendering builds — see `input` above.
70
+ * No frames, popups, or file uploads.
71
+ * On `-no-render` builds and v0.1.x: no screenshots, no visibility assertions
72
+ (`seeElement`/`dontSeeElement` always throw) — only DOM presence
73
+ (`seeElementInDOM`/`dontSeeElementInDOM`) is meaningful without a layout engine.
74
+ * On v0.2.0+ default (rendering) builds: layout, screenshots, and CSS work, but it's a new,
75
+ independently implemented rendering/CSS engine — expect edge cases and gaps versus a real browser.
76
+ * Single V8 isolate: heavy or long-running pages, or many pages in parallel against one
77
+ `obscura serve` process, compete for the same isolate.
78
+
79
+
80
+
81
+ ## Configuration
82
+
83
+ This helper should be configured in codecept.conf.js. It accepts everything `CDPBrowser`
84
+ accepts (see its config table), plus:
85
+
86
+ Type: [object][6]
87
+
88
+ ### Properties
89
+
90
+ * `endpoint` **[string][4]?** explicit CDP endpoint. Setting this switches the helper to ATTACH
91
+ mode: it only connects, and never spawns or kills a process, no matter what else is configured.
92
+ Leave it unset for SELF-MANAGED mode (see below).
93
+ * `binaryPath` **[string][4]?** path to the `obscura` executable, used in SELF-MANAGED mode
94
+ (`endpoint` unset). Checked before `OBSCURA_PATH` and `PATH`.
95
+ * `port` **[number][3]?** port `obscura serve` listens on, in SELF-MANAGED mode. When unset, a
96
+ free port is picked automatically, which is what makes `run-workers` collision-free — every
97
+ worker gets its own instance on its own port with zero config.
98
+ * `serverStartTimeout` **[number][3]?** milliseconds to wait for a spawned `obscura serve`
99
+ to answer `/json/version` before `_connect` gives up.
100
+
101
+
102
+
103
+ ## Example
104
+
105
+ ```js
106
+ // inside codecept.conf.js — SELF-LAUNCH mode (recommended): the helper finds/starts/stops
107
+ // obscura serve on its own, on a free port. Ideal for run-workers: every worker gets its own
108
+ // instance with no config.
109
+ {
110
+ helpers: {
111
+ Obscura: {
112
+ url: 'http://localhost',
113
+ }
114
+ }
115
+ }
116
+ ```
117
+
118
+ ```js
119
+ // ATTACH mode — connect to an Obscura instance you manage yourself (remote host, container, etc.)
120
+ {
121
+ helpers: {
122
+ Obscura: {
123
+ url: 'http://localhost',
124
+ endpoint: 'http://127.0.0.1:9222',
125
+ }
126
+ }
127
+ }
128
+ ```
129
+
130
+ ## Methods
131
+
132
+ ### Parameters
133
+
134
+ * `config` **ObscuraConfig**&#x20;
135
+
136
+ ### _connect
137
+
138
+ In ATTACH mode, connects exactly as `CDPBrowser._connect` would. In SELF-MANAGED mode,
139
+ resolves and spawns `obscura serve` (or courtesy-attaches to an already-running one on
140
+ :9222) exactly once via `_resolveSelfManaged`, then connects.
141
+
142
+ A spawn failure (e.g. a bad binary) is delivered asynchronously by Node as an `error`
143
+ event; it is recorded on `this.serverError` and surfaced as a rejection from `_waitForServer`
144
+ instead of crashing the process as an uncaught exception.
145
+
146
+ ### _findFreePort
147
+
148
+ Picks a free TCP port on 127.0.0.1 by briefly listening on port 0 and reading back the OS-assigned
149
+ port. Used as the SELF-LAUNCH default when `options.port` isn't explicitly set, so multiple
150
+ `run-workers` workers never collide on the same port.
151
+
152
+ Returns **[Promise][2]<[number][3]>** a free port.
153
+
154
+ ### _finishTest
155
+
156
+ Closes the CDP connection (via `CDPBrowser._finishTest`), then kills the `obscura serve`
157
+ process spawned by `_connect`, if any (never runs in ATTACH or COURTESY-ATTACH mode, since
158
+ `this.serverProcess` is only ever set in SELF-LAUNCH mode). Runs in a `finally` so the process
159
+ is always reaped even if closing the CDP connection throws. Sends `SIGTERM` first and waits for
160
+ the process to exit; a process that ignores `SIGTERM` is escalated to `SIGKILL` after 5s. The
161
+ promise only resolves once the child has actually exited (confirmed via the `exit` event, not
162
+ merely once `SIGKILL` was sent — the kernel needs a moment to reap it), with a final safety-net
163
+ timeout so a stuck child can never keep the event loop alive even if that confirmation is
164
+ somehow lost.
165
+
166
+ ### _probeUp
167
+
168
+ Probes a `/json/version`-style URL with a short timeout, used for the COURTESY-ATTACH check.
169
+
170
+ #### Parameters
171
+
172
+ * `url` **[string][4]**&#x20;
173
+
174
+ Returns **[Promise][2]<[boolean][5]>** true if the URL answered.
175
+
176
+ ### _resolveBinary
177
+
178
+ Resolves the `obscura` binary to spawn, in priority order: `options.binaryPath`, then the
179
+ `OBSCURA_PATH` environment variable, then `obscura` on `PATH`. The `PATH` lookup walks the
180
+ directories itself instead of shelling out to `which`, which does not exist on Windows: on
181
+ Windows every `PATHEXT` suffix is tried, so an `obscura.exe` on `PATH` is found too.
182
+
183
+ Returns **([string][4] | null)** an absolute or relative path to the binary, or null if none resolved.
184
+
185
+ ### _resolveSelfManaged
186
+
187
+ Resolves how to reach Obscura when no explicit `endpoint` was configured, trying, in order:
188
+ spawn a binary (`binaryPath` config, then `OBSCURA_PATH` env, then `obscura` on `PATH`),
189
+ courtesy-attach to `http://127.0.0.1:9222` if something already answers there, or throw a
190
+ loud, actionable error. Sets `this.options.endpoint` as a side effect.
191
+
192
+ ### _waitForServer
193
+
194
+ Polls `http://127.0.0.1:<port>/json/version` until `obscura serve` responds, `this.serverError`
195
+ is set by the spawned process' `error` event, or `options.serverStartTimeout` elapses. The
196
+ process typically comes up within tens of milliseconds — a 20ms retry interval (down from a
197
+ previous 200ms) keeps the wasted tail after the server is actually ready small, since this cost
198
+ is paid once per run and counts directly toward real-world startup latency.
199
+
200
+ [1]: https://github.com/h4ckf0r0day/obscura
201
+
202
+ [2]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise
203
+
204
+ [3]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number
205
+
206
+ [4]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
207
+
208
+ [5]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
209
+
210
+ [6]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
@@ -614,7 +614,7 @@ Test files written for 3.x keep working until you flip the flag.
614
614
 
615
615
  ### `wait*` Methods Resolve Relative URLs
616
616
 
617
- `waitInUrl`, `waitUrlEquals`, and `waitCurrentPathEquals` now resolve a relative path against the helper's configured `url` before comparing. In 3.x a literal substring match against `window.location.href` would fail for relative paths.
617
+ `waitUrlEquals` and `waitCurrentPathEquals` now resolve a relative path against the helper's configured `url` before comparing. In 3.x a literal comparison against `window.location.href` would fail for relative paths.
618
618
 
619
619
  ```js
620
620
  // helpers: { Playwright: { url: 'https://app.example.com' } }
@@ -623,6 +623,8 @@ I.waitUrlEquals('/dashboard') // matches https://app.example.com/dashboard
623
623
  I.waitInUrl('/users') // matches any URL containing /users
624
624
  ```
625
625
 
626
+ `waitInUrl` is unchanged from 3.x — it stays a plain substring match against the current URL and never resolves its argument.
627
+
626
628
  `waitUrlEquals` error messages now include the actual URL the page was on when the wait timed out — easier to diagnose `/dashboard` vs `/dashboard?session=expired`.
627
629
 
628
630
  ## 6. Adopt New Behaviors
package/docs/parallel.md CHANGED
@@ -22,6 +22,8 @@ npx codeceptjs run-workers 4
22
22
 
23
23
  Steps are not streamed to the console in this mode — output from separate threads can't be interleaved cleanly. While workers run, CodeceptJS sets `process.env.RUNS_WITH_WORKERS=true`, so plugins and helpers can branch on it. All `run` options work here too: `--grep "@smoke"`, `-c codecept.conf.js`, `--debug`, and the rest.
24
24
 
25
+ By default, workers are created with a staggered delay of 200ms to prevent CPU spikes and stagger browser initializations. You can adjust this via `workerInitializationDelay` in your configuration.
26
+
25
27
  ### Distribution strategies
26
28
 
27
29
  `--by` controls how tests spread across workers:
@@ -185,3 +187,11 @@ Shared data is a Proxy. Don't reassign the injected object itself (`let d = inje
185
187
  ```js
186
188
  share({ tmpFile: '/tmp/run-1' }, { local: true })
187
189
  ```
190
+
191
+ ## Scaling beyond one machine
192
+
193
+ Worker threads split the suite, but every Playwright worker still runs a full local browser.
194
+ With the [Kitesurf helper](/helpers/Kitesurf) each worker drives a cloud browser on Cloudflare
195
+ instead — `run-workers 16` means sixteen browsers spawned in about a second, none of them
196
+ competing for your runner's CPU. See [Alternative Browser Engines](/alternative-browsers) for
197
+ the full setup.
@@ -9,16 +9,21 @@ title: screencast
9
9
 
10
10
  ## screencast
11
11
 
12
- Records WebM video of tests using Playwright's screencast API.
13
-
14
- When `captions` is enabled, action annotations are burned into the video;
15
- when `subtitles` is enabled, a standalone `.srt` is also produced. Default
16
- `on=fail` keeps videos for failed tests only; `on=test` keeps every test's
17
- video.
18
-
19
- Note: enabling Playwright's helper-level `video: true` together with this
20
- plugin produces two independent recordings (`output/videos/*.webm` from the
21
- helper, `output/screencast/*.webm` from this plugin).
12
+ Records a video of tests. Uses Playwright's `page.screencast` API (WebM) when the active
13
+ helper is Playwright, or raw CDP `Page.startScreencast` (APNG, assembled in-process) when the
14
+ active helper is `CDPBrowser` or a subclass (`Obscura`, `Kitesurf`, ...). Which path is used is
15
+ detected automatically per test run; nothing in the config changes between them.
16
+
17
+ When `captions` is enabled, action annotations are burned into the video — Playwright only,
18
+ via `page.screencast.showActions()`/`showChapter()`; silently absent on the CDP path, since CDP
19
+ screencast frames are raw, uncomposited page captures with no overlay mechanism. `subtitles`
20
+ (a standalone `.srt`) works identically on both paths, since it's driven by step events, not by
21
+ the video API. Default `on=fail` keeps videos for failed tests only; `on=test` keeps every
22
+ test's video.
23
+
24
+ Note: enabling Playwright's helper-level `video: true` together with this plugin produces two
25
+ independent recordings (`output/videos/*.webm` from the helper, `output/screencast/*.webm` from
26
+ this plugin).
22
27
 
23
28
  #### Configuration
24
29
 
@@ -38,11 +43,11 @@ plugins: {
38
43
 
39
44
  Other config options:
40
45
 
41
- * `captions`: burn-in action overlays via `page.screencast.showActions()`. Default: true.
46
+ * `captions`: burn-in action overlays via `page.screencast.showActions()`. Playwright only. Default: true.
42
47
  * `subtitles`: also write a standalone `.srt` file alongside the video. Default: false.
43
48
  * `video`: record a video. With `video=false, subtitles=true`, only the `.srt` is produced. Default: true.
44
- * `size`: pass-through `{ width, height }` for `screencast.start`.
45
- * `quality`: pass-through 0–100 for `screencast.start`.
49
+ * `size`: pass-through `{ width, height }` `screencast.start`'s `size` on Playwright, `maxWidth`/`maxHeight` on the CDP path.
50
+ * `quality`: pass-through 0–100 for `screencast.start` (Playwright) or CDP `Page.startScreencast` (CDPBrowser family).
46
51
 
47
52
  CLI examples:
48
53
 
package/docs/plugins.md CHANGED
@@ -71,7 +71,7 @@ Retries each failed step in a test.
71
71
 
72
72
  ## [screencast](/plugins/screencast)
73
73
 
74
- Records WebM video of tests using Playwright's screencast API.
74
+ Records a video of tests. Uses Playwright's `page.screencast` API (WebM) when the active helper is Playwright, or raw CDP `Page.startScreencast` (APNG, assembled in-process) when the active helper is `CDPBrowser` or a subclass (`Obscura`, `Kitesurf`, ...). Which path is used is detected automatically per test run; nothing in the config changes between them.
75
75
 
76
76
  ## [screenshot](/plugins/screenshot)
77
77
 
@@ -44,6 +44,14 @@ async function getOsBrowsers() {
44
44
  ].join(', ')
45
45
  }
46
46
 
47
+ // Which runtime executes CodeceptJS can only be told from `process.versions`; envinfo probes
48
+ // PATH, where under `bunx --bun` the first `node` is a Bun shim with no --version, so
49
+ // getNodeInfo() returns "Not Found" on a working install.
50
+ async function getRuntimeInfo() {
51
+ if (process.versions.bun) return { bunInfo: await envinfo.helpers.getbunInfo() }
52
+ return { nodeInfo: await envinfo.helpers.getNodeInfo() }
53
+ }
54
+
47
55
  export default async function (path) {
48
56
  const testsPath = getTestRoot(path)
49
57
  const config = await getConfig(testsPath)
@@ -53,7 +61,7 @@ export default async function (path) {
53
61
  output.print('\n Environment information: \n')
54
62
  const info = {}
55
63
  info.codeceptVersion = Codecept.version()
56
- info.nodeInfo = await envinfo.helpers.getNodeInfo()
64
+ Object.assign(info, await getRuntimeInfo())
57
65
  info.osInfo = await envinfo.helpers.getOSInfo()
58
66
  info.cpuInfo = await envinfo.helpers.getCPUInfo()
59
67
  info.osBrowsers = await getOsBrowsers()
@@ -77,11 +85,11 @@ export default async function (path) {
77
85
  output.print('***************************************')
78
86
  }
79
87
 
80
- export { parsePlaywrightBrowsers }
88
+ export { parsePlaywrightBrowsers, getRuntimeInfo }
81
89
 
82
90
  export const getMachineInfo = async () => {
83
91
  const info = {
84
- nodeInfo: await envinfo.helpers.getNodeInfo(),
92
+ ...(await getRuntimeInfo()),
85
93
  osInfo: await envinfo.helpers.getOSInfo(),
86
94
  cpuInfo: await envinfo.helpers.getCPUInfo(),
87
95
  chromeInfo: await envinfo.helpers.getChromeInfo(),
@@ -129,13 +129,7 @@ let config
129
129
  // Load test and run
130
130
  initPromise = (async function () {
131
131
  try {
132
- // Add staggered delay at the very start to prevent resource conflicts
133
- // Longer delay for browser initialization conflicts
134
- const delay = (workerIndex - 1) * 2000 // 0ms, 2s, 4s, etc.
135
- if (delay > 0) {
136
- await new Promise(resolve => setTimeout(resolve, delay))
137
- }
138
-
132
+
139
133
  // Import modules dynamically to avoid ES Module loader race conditions in Node 22.x
140
134
  const eventModule = await import('../../event.js')
141
135
  const containerModule = await import('../../container.js')
@@ -153,9 +147,9 @@ initPromise = (async function () {
153
147
  Codecept = CodeceptModule.default
154
148
  fixErrorStack = typescriptModule.fixErrorStack
155
149
  loadTests = loadTestsModule.default
156
-
150
+
157
151
  const overrideConfigs = tryOrDefault(() => JSON.parse(options.override), {})
158
-
152
+
159
153
  let baseConfig
160
154
  try {
161
155
  // IMPORTANT: await is required here since getConfig is async
@@ -172,14 +166,14 @@ initPromise = (async function () {
172
166
  await new Promise(resolve => setTimeout(resolve, 100))
173
167
  process.exit(1)
174
168
  }
175
-
169
+
176
170
  // important deep merge so dynamic things e.g. functions on config are not overridden
177
171
  config = deepMerge(baseConfig, overrideConfigs)
178
-
172
+
179
173
  // Pass workerIndex as child option for output.process() to display worker prefix
180
174
  const optsWithChild = { ...options, child: workerIndex }
181
175
  codecept = new Codecept(config, optsWithChild)
182
-
176
+
183
177
  try {
184
178
  await codecept.init(testRoot)
185
179
  } catch (initErr) {
@@ -193,7 +187,7 @@ initPromise = (async function () {
193
187
  process.stderr.write(`${initErr.stack}\n`)
194
188
  process.exit(1)
195
189
  }
196
-
190
+
197
191
  codecept.loadTests()
198
192
  mocha = container.mocha()
199
193
 
@@ -279,7 +273,7 @@ async function runPoolTests() {
279
273
  const messageHandler = async eventData => {
280
274
  // Remove handler immediately to prevent duplicate processing
281
275
  parentPort?.off('message', messageHandler)
282
-
276
+
283
277
  if (eventData.type === 'TEST_ASSIGNED') {
284
278
  // In pool mode with ESM, we receive test FILE paths instead of UIDs
285
279
  // because UIDs are not stable across different mocha instances
@@ -289,7 +283,7 @@ async function runPoolTests() {
289
283
  // Create a fresh Mocha instance for each test file
290
284
  container.createMocha()
291
285
  const mocha = container.mocha()
292
-
286
+
293
287
  // Load only the assigned test file
294
288
  mocha.files = [testIdentifier]
295
289
  await loadTests(mocha)
@@ -348,7 +342,7 @@ async function runPoolTests() {
348
342
 
349
343
  // Set up handler BEFORE sending request to avoid race condition
350
344
  parentPort?.on('message', messageHandler)
351
-
345
+
352
346
  // Now send the request
353
347
  sendToParentThread({ type: 'REQUEST_TEST', workerIndex })
354
348
  })
@@ -391,13 +385,13 @@ async function runPoolTests() {
391
385
  function filterTestById(testUid) {
392
386
  // In pool mode with ESM, test files are already loaded once at initialization
393
387
  // We just need to filter the existing mocha suite to only include the target test
394
-
388
+
395
389
  // Get the existing mocha instance
396
390
  const mocha = container.mocha()
397
391
 
398
392
  // Save reference to all suites before clearing
399
393
  const allSuites = [...mocha.suite.suites]
400
-
394
+
401
395
  // Clear suites and tests but preserve other mocha settings
402
396
  mocha.suite.suites = []
403
397
  mocha.suite.tests = []
@@ -406,10 +400,10 @@ function filterTestById(testUid) {
406
400
  let foundTest = false
407
401
  for (const suite of allSuites) {
408
402
  const originalTests = [...suite.tests]
409
-
403
+
410
404
  // Check if this suite has our target test
411
405
  const targetTest = originalTests.find(test => test.uid === testUid)
412
-
406
+
413
407
  if (targetTest) {
414
408
  // Create a filtered suite with only the target test
415
409
  suite.tests = [targetTest]
package/lib/container.js CHANGED
@@ -892,6 +892,12 @@ async function loadSupportObject(modulePath, supportObjectName) {
892
892
  for (const [key, value] of mapping.entries()) {
893
893
  container.tsFileMapping.set(key, value)
894
894
  }
895
+ if (!store.tsFileMapping) {
896
+ store.tsFileMapping = new Map()
897
+ }
898
+ for (const [key, value] of mapping.entries()) {
899
+ store.tsFileMapping.set(key, value)
900
+ }
895
901
  } catch (tsError) {
896
902
  throw new Error(`Failed to load TypeScript file ${importPath}: ${tsError.message}. Make sure 'typescript' package is installed.`)
897
903
  }
@@ -68,6 +68,10 @@ function replaceTitle(title, dataRow) {
68
68
  return `${title} | {${JSON.stringify(dataRow.data)}}`
69
69
  }
70
70
 
71
+ if (typeof dataRow.data.getMasked === 'function') {
72
+ return `${title} | ${dataRow.data.getMasked()}`
73
+ }
74
+
71
75
  // if `dataRow` is object and has own `toString()` method,
72
76
  // it should be printed
73
77
  if (Object.prototype.toString.call(dataRow.data) === Object().toString() && dataRow.data.toString() !== Object().toString()) {
@@ -20,6 +20,7 @@ class WebElement {
20
20
  if (ctor.name === 'Playwright') return 'playwright'
21
21
  if (ctor.name === 'WebDriver') return 'webdriver'
22
22
  if (ctor.name === 'Puppeteer') return 'puppeteer'
23
+ if (ctor.name === 'CDPBrowser') return 'cdpbrowser'
23
24
  ctor = Object.getPrototypeOf(ctor)
24
25
  }
25
26
 
@@ -493,6 +494,8 @@ class WebElement {
493
494
  return this.element.evaluate(xpathFn)
494
495
  case 'webdriver':
495
496
  return this.helper.browser.execute(xpathFn, this.element)
497
+ case 'cdpbrowser':
498
+ return this.element.absoluteXPath()
496
499
  default:
497
500
  throw new Error(`Unsupported helper type: ${this.helperType}`)
498
501
  }
@@ -506,6 +509,8 @@ class WebElement {
506
509
  return this.element.evaluate(el => el.outerHTML)
507
510
  case 'webdriver':
508
511
  return this.helper.browser.execute(el => el.outerHTML, this.element)
512
+ case 'cdpbrowser':
513
+ return this.element.outerHTML()
509
514
  default:
510
515
  throw new Error(`Unsupported helper type: ${this.helperType}`)
511
516
  }
@@ -978,11 +978,23 @@ class Appium extends Webdriver {
978
978
  */
979
979
  async setNetworkConnection(value) {
980
980
  onlyForApps.call(this, supportedPlatform.android)
981
- return this.browser.execute('mobile: setConnectivity', {
981
+ const connectivity = {
982
982
  airplaneMode: !!(value & 1),
983
983
  wifi: !!(value & 2),
984
984
  data: !!(value & 4),
985
- })
985
+ }
986
+ // `mobile: setConnectivity` runs `adb shell svc data <state>` for every field it receives,
987
+ // which fails with "Can't find service: phone" on images without telephony (e.g. tablets).
988
+ // Only a positive result is cached: a freshly booted device may not have registered a carrier
989
+ // yet, and caching that would strip `data` for the whole session.
990
+ if (!this._hasTelephony) {
991
+ const deviceInfo = await this.browser.execute('mobile: deviceInfo')
992
+ this._hasTelephony = !!deviceInfo?.carrierName
993
+ }
994
+ // Keep `data` on telephony-capable devices, otherwise the device stays online over cellular
995
+ // and going offline silently does nothing.
996
+ if (!this._hasTelephony) delete connectivity.data
997
+ return this.browser.execute('mobile: setConnectivity', connectivity)
986
998
  }
987
999
 
988
1000
  /**