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.
- package/docs/alternative-browsers.md +153 -0
- package/docs/basics.md +9 -1
- package/docs/configuration.md +2 -0
- package/docs/helpers/CDPBrowser.md +2138 -0
- package/docs/helpers/Kitesurf.md +118 -0
- package/docs/helpers/Obscura.md +210 -0
- package/docs/migration-4.md +3 -1
- package/docs/parallel.md +10 -0
- package/docs/plugins/screencast.md +18 -13
- package/docs/plugins.md +1 -1
- package/lib/command/info.js +11 -3
- package/lib/command/workers/runTests.js +14 -20
- package/lib/container.js +6 -0
- package/lib/data/context.js +4 -0
- package/lib/element/WebElement.js +5 -0
- package/lib/helper/Appium.js +14 -2
- package/lib/helper/CDPBrowser.js +3004 -0
- package/lib/helper/Kitesurf.js +139 -0
- package/lib/helper/Obscura.js +344 -0
- package/lib/helper/Playwright.js +30 -3
- package/lib/helper/Puppeteer.js +43 -16
- package/lib/helper/WebDriver.js +43 -8
- package/lib/helper/clientscripts/cdpBrowserClient.js +486 -0
- package/lib/helper/clientscripts/xpathPolyfill.js +31 -0
- package/lib/helper/extras/CDPConnection.js +92 -0
- package/lib/helper/extras/CDPElementHandle.js +27 -0
- package/lib/helper/extras/apngAssembler.js +156 -0
- package/lib/html.js +9 -2
- package/lib/listener/retryEnhancer.js +2 -1
- package/lib/listener/steps.js +8 -0
- package/lib/mocha/hooks.js +10 -0
- package/lib/parser.js +14 -2
- package/lib/plugin/junitReporter.js +17 -1
- package/lib/plugin/screencast.js +116 -24
- package/lib/step/base.js +15 -3
- package/lib/utils/loaderCheck.js +6 -0
- package/lib/utils.js +1 -1
- package/lib/workers.js +17 -0
- package/package.json +4 -1
- package/typings/promiseBasedTypes.d.ts +1833 -0
- 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** 
|
|
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** 
|
|
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]** 
|
|
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
|
package/docs/migration-4.md
CHANGED
|
@@ -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
|
-
`
|
|
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
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
video
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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 }`
|
|
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
|
|
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
|
|
package/lib/command/info.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/lib/data/context.js
CHANGED
|
@@ -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
|
}
|
package/lib/helper/Appium.js
CHANGED
|
@@ -978,11 +978,23 @@ class Appium extends Webdriver {
|
|
|
978
978
|
*/
|
|
979
979
|
async setNetworkConnection(value) {
|
|
980
980
|
onlyForApps.call(this, supportedPlatform.android)
|
|
981
|
-
|
|
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
|
/**
|