codeceptjs 4.2.0-beta.4 → 4.2.1-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 (65) hide show
  1. package/README.md +2 -0
  2. package/bin/codecept.js +11 -0
  3. package/docs/agents.md +26 -1
  4. package/docs/ai.md +57 -0
  5. package/docs/alternative-browsers.md +175 -148
  6. package/docs/assertions.md +74 -0
  7. package/docs/bootstrap.md +2 -0
  8. package/docs/commands.md +45 -0
  9. package/docs/configuration.md +37 -0
  10. package/docs/debugging.md +1 -1
  11. package/docs/helpers/ApiDataFactory.md +20 -20
  12. package/docs/helpers/Appium.md +1615 -670
  13. package/docs/helpers/Decision.md +146 -0
  14. package/docs/helpers/GraphQL.md +31 -31
  15. package/docs/helpers/GraphQLDataFactory.md +20 -20
  16. package/docs/helpers/Kitesurf.md +1808 -25
  17. package/docs/helpers/Lightpanda.md +139 -0
  18. package/docs/helpers/Obscura.md +1799 -74
  19. package/docs/helpers/Playwright.md +490 -428
  20. package/docs/helpers/Puppeteer.md +317 -255
  21. package/docs/helpers/REST.md +43 -43
  22. package/docs/helpers/WebDriver.md +237 -175
  23. package/docs/installation.md +16 -0
  24. package/docs/mcp.md +7 -1
  25. package/docs/parallel.md +35 -1
  26. package/docs/plugins/pause.md +2 -1
  27. package/docs/plugins/sitemap.md +48 -0
  28. package/docs/plugins.md +4 -0
  29. package/lib/ai.js +81 -2
  30. package/lib/codecept.js +32 -2
  31. package/lib/command/init.js +22 -0
  32. package/lib/command/initAgent.js +78 -0
  33. package/lib/command/run-multiple.js +3 -0
  34. package/lib/command/run-workers.js +10 -3
  35. package/lib/command/run.js +3 -2
  36. package/lib/command/utils.js +15 -0
  37. package/lib/command/workers/runTests.js +10 -10
  38. package/lib/container.js +4 -2
  39. package/lib/effects.js +19 -1
  40. package/lib/helper/Appium.js +23 -0
  41. package/lib/helper/CDPBrowser.js +387 -1338
  42. package/lib/helper/Decision.js +256 -0
  43. package/lib/helper/Kitesurf.js +3 -21
  44. package/lib/helper/Lightpanda.js +137 -0
  45. package/lib/helper/Obscura.js +21 -238
  46. package/lib/helper/Playwright.js +50 -4
  47. package/lib/helper/Puppeteer.js +62 -8
  48. package/lib/helper/WebDriver.js +45 -1
  49. package/lib/helper/extras/clipboard.js +31 -0
  50. package/lib/listener/result.js +4 -0
  51. package/lib/mocha/bdd.js +19 -1
  52. package/lib/plugin/junitReporter.js +4 -1
  53. package/lib/plugin/pause.js +4 -1
  54. package/lib/plugin/sitemap.js +228 -0
  55. package/lib/recorder.js +19 -18
  56. package/lib/result.js +16 -0
  57. package/lib/step/base.js +12 -0
  58. package/lib/utils.js +2 -10
  59. package/lib/webServer.js +193 -0
  60. package/lib/workers.js +118 -31
  61. package/package.json +4 -2
  62. package/typings/index.d.ts +60 -7
  63. package/typings/promiseBasedTypes.d.ts +985 -814
  64. package/typings/types.d.ts +1084 -815
  65. package/docs/helpers/CDPBrowser.md +0 -2138
@@ -1,3 +1,5 @@
1
+ import { spawn } from 'child_process'
2
+ import net from 'net'
1
3
  import path from 'path'
2
4
  import fs from 'fs'
3
5
  import axios from 'axios'
@@ -7,7 +9,7 @@ import installCodeceptClient from './clientscripts/cdpBrowserClient.js'
7
9
  import xpathPolyfillSource from './clientscripts/xpathPolyfill.js'
8
10
  import Locator from '../locator.js'
9
11
  import store from '../store.js'
10
- import { xpathLocator, normalizePath, resolveUrl, toCamelCase, convertCssPropertiesToCamelCase, normalizeSpacesInString, fileExists, base64EncodeFile, getMimeType } from '../utils.js'
12
+ import { xpathLocator, normalizePath, resolveUrl, toCamelCase, convertCssPropertiesToCamelCase, normalizeSpacesInString, fileExists, base64EncodeFile, getMimeType, isFile, isWindows } from '../utils.js'
11
13
  import ElementNotFound from './errors/ElementNotFound.js'
12
14
  import MultipleElementsFound from './errors/MultipleElementsFound.js'
13
15
  import { includes as stringIncludes } from '../assert/include.js'
@@ -18,6 +20,7 @@ import { isColorProperty, convertColorToRGBA } from '../colorUtils.js'
18
20
  import WebElement from '../element/WebElement.js'
19
21
  import CDPElementHandle from './extras/CDPElementHandle.js'
20
22
  import { checkFocusBeforeType, checkFocusBeforePressKey } from './extras/focusCheck.js'
23
+ import { CLIPBOARD_READ_TIMEOUT_MS, readClipboardScript, writeClipboardScript, clipboardExpression } from './extras/clipboard.js'
21
24
  import { dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, flushNetworkTraffics } from './network/actions.js'
22
25
  import { assembleApng, isPng } from './extras/apngAssembler.js'
23
26
 
@@ -29,16 +32,16 @@ import { assembleApng, isPng } from './extras/apngAssembler.js'
29
32
  * @typedef CDPBrowserConfig
30
33
  * @type {object}
31
34
  * @prop {string} [url=http://localhost] - base url of website to be tested.
32
- * @prop {string} [endpoint=http://127.0.0.1:9222] - Chrome DevTools Protocol endpoint. Either an `http(s)://` address exposing `/json/version` (from which the `webSocketDebuggerUrl` is resolved) or a raw `ws(s)://` debugger URL.
33
- * @prop {object} [headers={}] - headers sent with the endpoint resolution request and the WebSocket handshake. Useful for authenticated remote browser providers.
34
- * @prop {string} [input=auto] - how synthetic user actions (click, fill, etc.) are dispatched by helpers built on top of this class. `auto` picks `cdp` when a real layout engine is detected and `synthetic` otherwise; can be pinned to `cdp` or `synthetic`.
35
- * @prop {string|boolean} [xpathPolyfill=auto] - whether to inject the bundled XPath polyfill before installing the in-page client. `auto` probes the page and only injects when `document.evaluate` is unavailable or broken; `true`/`false` force the behavior.
36
- * @prop {object} [capabilities={}] - pre-seed detected browser capabilities (`layout`, `xpath`, `screenshot`, `innerText`) to skip runtime probing. Values set here are never overwritten by `_probeCapabilities`/`_ensureClient`.
37
- * @prop {number} [waitForTimeout=5] - default wait* timeout in seconds, used by helpers built on top of this class.
38
- * @prop {number} [waitForAction=100] - only takes effect when set explicitly: a literal fixed pacing sleep (in milliseconds) after click, type, or other interactions, mirroring other browser helpers. Left unset, actions settle in an event-aware way instead — near-instant when nothing navigates, waiting for the navigation to actually finish (not a guessed fixed delay) when one does.
39
- * @prop {number} [pollInterval=25] - interval in milliseconds between retries while polling for a condition (e.g. page ready state, `waitFor*`). Distinct from `waitForAction`.
40
- * @prop {number} [getPageTimeout=30] - maximum time in seconds to wait for a page to finish loading after navigation or reload; also used as the CDP command timeout (in ms, x1000).
41
- * @prop {string} [waitForNavigation=load] - when to consider a navigation finished: `load`, `domcontentloaded`, or `networkidle`. Mirrors the Puppeteer helper's option name. `networkidle` waits for the CDP `networkIdle` lifecycle event, which on a busy page can lag `load` by a second or more — only opt in if the extra wait is actually needed.
35
+ * @prop {string} [endpoint=http://127.0.0.1:9222] - Chrome DevTools Protocol endpoint: an `http(s)://` address or a `ws(s)://` debugger URL.
36
+ * @prop {object} [headers={}] - headers sent when connecting to the endpoint, e.g. for authenticated remote browsers.
37
+ * @prop {string} [input=auto] - how actions are dispatched: `cdp` (real mouse and keyboard events) or `synthetic` (DOM events). `auto` picks `cdp` when the browser renders layout.
38
+ * @prop {string|boolean} [xpathPolyfill=auto] - inject an XPath polyfill for browsers with incomplete XPath support. `auto` detects when it is needed.
39
+ * @prop {object} [capabilities={}] - browser capabilities (`layout`, `xpath`, `screenshot`, `innerText`) to use instead of detecting them at runtime.
40
+ * @prop {number} [waitForTimeout=5] - default timeout for wait* actions, in seconds.
41
+ * @prop {number} [waitForAction] - fixed delay after each action, in milliseconds. When unset, actions wait only for the navigation they trigger.
42
+ * @prop {number} [pollInterval=25] - interval between checks while waiting, in milliseconds.
43
+ * @prop {number} [getPageTimeout=30] - maximum time to wait for a page to load, in seconds.
44
+ * @prop {string} [waitForNavigation=load] - when a navigation is considered finished: `load`, `domcontentloaded`, or `networkidle`.
42
45
  */
43
46
  const config = {}
44
47
 
@@ -82,6 +85,8 @@ const ACTION_SETTLE_GRACE_MS = 20
82
85
  * script evaluation, and simple in-page element interaction (installed lazily through the
83
86
  * `window.__codecept` client script). It does not launch a browser itself — point `endpoint` at an
84
87
  * already-running Chrome (or any CDP-compatible browser) started with `--remote-debugging-port`.
88
+ * Subclasses for browsers shipped as a standalone binary opt into launching and reaping their own
89
+ * server process through `_manageServer`.
85
90
  *
86
91
  * ## Example
87
92
  *
@@ -124,6 +129,12 @@ class CDPBrowser extends Helper {
124
129
  this.cdp = null
125
130
  this.sessionId = null
126
131
  this.targetId = null
132
+ this.mode = 'attach'
133
+ this.server = null
134
+ this.serverProcess = null
135
+ this.serverError = null
136
+ this.binaryPath = null
137
+ this._selfManagedResolved = false
127
138
  this.capabilities = { layout: null, xpath: null, screenshot: null, innerText: null, ...this.options.capabilities }
128
139
  this.withinCandidates = null
129
140
  this.requests = []
@@ -142,24 +153,8 @@ class CDPBrowser extends Helper {
142
153
  this._waitForActionExplicit = config?.waitForAction !== undefined
143
154
  }
144
155
 
145
- /**
146
- * No-op hook kept for interface parity with other browser helpers. Connecting to the CDP
147
- * endpoint is deferred to `_before`, since a fresh target/session is opened per test.
148
- */
149
156
  _init() {}
150
157
 
151
- /**
152
- * Resolves `options.endpoint` to a raw WebSocket debugger URL. If the configured endpoint is
153
- * an `http(s)://` address, this fetches `/json/version` from it and reads `webSocketDebuggerUrl`
154
- * from the response, matching the discovery flow exposed by Chrome's `--remote-debugging-port`.
155
- * A `ws(s)://` endpoint is returned unchanged.
156
- *
157
- * This is the subclass override point for helpers that connect through a different discovery
158
- * mechanism (e.g. a cloud browser provider with its own session-creation API).
159
- *
160
- * @returns {Promise<string>} a `ws(s)://` debugger URL ready to be passed to `CDPConnection`.
161
- * @protected
162
- */
163
158
  async _resolveEndpoint() {
164
159
  let endpoint = this.options.endpoint
165
160
  if (endpoint.startsWith('http')) {
@@ -170,32 +165,198 @@ class CDPBrowser extends Helper {
170
165
  }
171
166
 
172
167
  /**
173
- * Resolves the CDP endpoint and opens the underlying `CDPConnection`, storing it on `this.cdp`.
168
+ * Opts a subclass into managing its own browser server process. Called from a subclass
169
+ * constructor with the raw user config and a server descriptor:
170
+ *
171
+ * - `name` — binary name looked up on `PATH`, also used in messages
172
+ * - `envVar` — environment variable that may point at the binary
173
+ * - `args(port)` — arguments the binary is spawned with to serve CDP on `port`
174
+ * - `env` — extra environment variables for the spawned process
175
+ * - `releases` — URL shown when no binary can be resolved
174
176
  *
177
+ * An explicit `endpoint` in the user config always wins: the helper stays in ATTACH mode and
178
+ * never spawns or kills anything. Otherwise the helper becomes SELF-MANAGED and the default
179
+ * `endpoint` is cleared until `_resolveSelfManaged` sets the real one.
180
+ *
181
+ * @param {object} config - the config object the helper was constructed with.
182
+ * @param {object} server - the server descriptor described above.
175
183
  * @protected
176
184
  */
177
- async _connect() {
178
- const endpoint = await this._resolveEndpoint()
179
- this.cdp = new CDPConnection(endpoint, { headers: this.options.headers, timeout: this.options.getPageTimeout * 1000 })
180
- await this.cdp.connect()
185
+ _manageServer(config, server) {
186
+ if (config && config.endpoint) return
187
+ this.mode = 'self-managed'
188
+ this.options.endpoint = null
189
+ this.server = server
190
+ }
191
+
192
+ /**
193
+ * Resolves how to reach a SELF-MANAGED browser, trying, in order: spawn a binary
194
+ * (`binaryPath` config, then the descriptor's `envVar`, then its `name` on `PATH`) on
195
+ * `options.port` or a free port, courtesy-attach to `http://127.0.0.1:9222` if something already
196
+ * answers there (never killed by this helper), or throw a loud, actionable error. Sets
197
+ * `this.options.endpoint` as a side effect.
198
+ *
199
+ * A spawn failure (e.g. a bad binary) is delivered asynchronously by Node as an `error` event;
200
+ * it is recorded on `this.serverError` and surfaced as a rejection from `_waitForServer` instead
201
+ * of crashing the process as an uncaught exception.
202
+ *
203
+ * @protected
204
+ */
205
+ async _resolveSelfManaged() {
206
+ const { name, envVar, args, env, releases } = this.server
207
+ const helperName = this.constructor.name
208
+ const binaryPath = this._resolveBinary()
209
+ if (binaryPath) {
210
+ this.binaryPath = binaryPath
211
+ const port = this.options.port || (await this._findFreePort())
212
+ this.options.port = port
213
+ this.serverError = null
214
+ this.serverProcess = spawn(binaryPath, args(port), { stdio: 'ignore', env: { ...process.env, ...env } })
215
+ this.serverProcess.on('error', err => {
216
+ this.serverError = err
217
+ })
218
+ await this._waitForServer()
219
+ this.options.endpoint = `http://127.0.0.1:${port}`
220
+ return
221
+ }
222
+ if (await this._probeUp('http://127.0.0.1:9222/json/version')) {
223
+ this.debugSection(helperName, `no binaryPath/${envVar}/${name}-on-PATH found — courtesy-attaching to an already-running ${name} on 127.0.0.1:9222 (this helper will not manage or kill it)`)
224
+ this.options.endpoint = 'http://127.0.0.1:9222'
225
+ return
226
+ }
227
+ throw new Error(
228
+ [
229
+ `${helperName} has no endpoint configured, no binary could be resolved (checked config.binaryPath, ${envVar}, and PATH), and nothing answered http://127.0.0.1:9222/json/version.`,
230
+ `Start it yourself: ${name} ${args(9222).join(' ')}`,
231
+ `Or let this helper launch it: put a release binary on PATH, set ${envVar}=/path/to/${name}, or pass binaryPath in the config.`,
232
+ `Releases: ${releases}`,
233
+ ].join('\n '),
234
+ )
235
+ }
236
+
237
+ /**
238
+ * Resolves the binary to spawn, in priority order: `options.binaryPath`, then the server
239
+ * descriptor's environment variable, then its name on `PATH`. The `PATH` lookup walks the
240
+ * directories itself instead of shelling out to `which`, which does not exist on Windows: on
241
+ * Windows every `PATHEXT` suffix is tried, so a `.exe` on `PATH` is found too.
242
+ *
243
+ * @returns {string|null} an absolute or relative path to the binary, or null if none resolved.
244
+ * @protected
245
+ */
246
+ _resolveBinary() {
247
+ const { name, envVar } = this.server
248
+ if (this.options.binaryPath) return this.options.binaryPath
249
+ if (process.env[envVar]) return process.env[envVar]
250
+ const windows = isWindows()
251
+ const extensions = windows ? (process.env.PATHEXT || '.EXE;.CMD;.BAT;.COM').split(';') : ['']
252
+ for (const entry of (process.env.PATH || '').split(path.delimiter)) {
253
+ const dir = windows ? entry.replace(/^"|"$/g, '') : entry
254
+ if (!dir) continue
255
+ for (const extension of extensions) {
256
+ const candidate = path.join(dir, `${name}${extension}`)
257
+ if (!isFile(candidate)) continue
258
+ try {
259
+ fs.accessSync(candidate, fs.constants.X_OK)
260
+ return candidate
261
+ } catch (e) {
262
+ continue
263
+ }
264
+ }
265
+ }
266
+ return null
267
+ }
268
+
269
+ /**
270
+ * Picks a free TCP port on 127.0.0.1 by briefly listening on port 0 and reading back the
271
+ * OS-assigned port. Used when `options.port` isn't explicitly set, so multiple `run-workers`
272
+ * workers never collide on the same port.
273
+ *
274
+ * @returns {Promise<number>} a free port.
275
+ * @protected
276
+ */
277
+ async _findFreePort() {
278
+ return new Promise((resolve, reject) => {
279
+ const srv = net.createServer()
280
+ srv.unref()
281
+ srv.on('error', reject)
282
+ srv.listen(0, '127.0.0.1', () => {
283
+ const { port } = srv.address()
284
+ srv.close(() => resolve(port))
285
+ })
286
+ })
181
287
  }
182
288
 
183
289
  /**
184
- * Hook executed before each test. Ensures a live `CDPConnection` exists (connecting lazily on
185
- * first use, and reconnecting if a previous connection was closed), then creates a fresh
186
- * `about:blank` target and attaches to it with `Target.attachToTarget`, storing `this.targetId`
187
- * and `this.sessionId`. `Page` and `Runtime` domains are enabled on the new session, and engine
188
- * capabilities are probed here (against `about:blank`, uncontended) rather than only lazily on
189
- * the first real page — see `_probeCapabilities`.
290
+ * Probes a `/json/version`-style URL with a short timeout, used for the courtesy-attach check.
190
291
  *
191
- * Also resets `_navStartWaiters` and `_lastMainFrameNav`: both are scoped to a single
192
- * `sessionId`/`targetId`, which are about to change, so anything left over from the previous test
193
- * (e.g. an action-settle waiter still armed because its test threw between arming and settling)
194
- * can never legitimately resolve against the new session — better to drop it here than leave it
195
- * waiting for the rest of the run.
292
+ * @param {string} url
293
+ * @returns {Promise<boolean>} true if the URL answered.
294
+ * @protected
295
+ */
296
+ async _probeUp(url) {
297
+ try {
298
+ await axios.get(url, { timeout: 1000 })
299
+ return true
300
+ } catch (e) {
301
+ return false
302
+ }
303
+ }
304
+
305
+ /**
306
+ * Polls `http://127.0.0.1:<port>/json/version` until the spawned server responds,
307
+ * `this.serverError` is set by the process' `error` event, or `options.serverStartTimeout`
308
+ * elapses. The process typically comes up within tens of milliseconds — a 20ms retry interval
309
+ * keeps the wasted tail after the server is actually ready small, since this cost is paid once
310
+ * per run and counts directly toward real-world startup latency.
196
311
  *
197
312
  * @protected
198
313
  */
314
+ async _waitForServer() {
315
+ const { name } = this.server
316
+ const timeout = this.options.serverStartTimeout || 15000
317
+ const deadline = Date.now() + timeout
318
+ while (Date.now() < deadline) {
319
+ if (this.serverError) {
320
+ throw new Error(`Failed to start ${name} at ${this.binaryPath}: ${this.serverError.message}`)
321
+ }
322
+ try {
323
+ await axios.get(`http://127.0.0.1:${this.options.port}/json/version`, { timeout: Math.min(1000, deadline - Date.now()) })
324
+ return
325
+ } catch (e) {
326
+ await new Promise(r => setTimeout(r, 20))
327
+ }
328
+ }
329
+ if (this.serverError) {
330
+ throw new Error(`Failed to start ${name} at ${this.binaryPath}: ${this.serverError.message}`)
331
+ }
332
+ throw new Error(`${name} did not start on port ${this.options.port} within ${timeout}ms`)
333
+ }
334
+
335
+ /**
336
+ * In SELF-MANAGED mode, resolves and spawns the browser server (or courtesy-attaches to an
337
+ * already-running one on :9222) exactly once via `_resolveSelfManaged`. Then resolves the CDP
338
+ * endpoint and opens the underlying `CDPConnection`, storing it on `this.cdp`.
339
+ *
340
+ * @protected
341
+ */
342
+ async _connect() {
343
+ if (this.server) {
344
+ if (this._selfManagedError) throw this._selfManagedError
345
+ if (!this._selfManagedResolved) {
346
+ this._selfManagedResolved = true
347
+ try {
348
+ await this._resolveSelfManaged()
349
+ } catch (err) {
350
+ this._selfManagedError = err
351
+ throw err
352
+ }
353
+ }
354
+ }
355
+ const endpoint = await this._resolveEndpoint()
356
+ this.cdp = new CDPConnection(endpoint, { headers: this.options.headers, timeout: this.options.getPageTimeout * 1000 })
357
+ await this.cdp.connect()
358
+ }
359
+
199
360
  async _before() {
200
361
  if (!this.cdp || !this.cdp.isConnected) await this._connect()
201
362
  this._navStartWaiters = []
@@ -210,33 +371,6 @@ class CDPBrowser extends Helper {
210
371
  await this._probeCapabilities()
211
372
  }
212
373
 
213
- /**
214
- * Lazily installs a single, persistent `Page.lifecycleEvent` listener on the underlying
215
- * `CDPConnection` and drains it into whichever `_waitForLoadEvent` calls are currently pending,
216
- * matched by `loaderId`. Installed once per helper instance (the connection outlives individual
217
- * tests), never removed — `CDPConnection` has no listener-removal API, so a single persistent
218
- * dispatcher (rather than one listener per navigation) is what keeps this leak-free.
219
- *
220
- * Also, for the main frame (`params.frameId === this.targetId`, which holds for a page target's
221
- * own top-level frame) only:
222
- * - Drains `_navStartWaiters` (armed by `_armActionSettle`, before an action, for `_waitForAction`'s
223
- * event-aware settle) on an `'init'` event — confirmed via a raw probe (against both Obscura and
224
- * Chrome, through the actual CLI path) to be the earliest signal CDP emits when a new top-level
225
- * navigation begins. Arming happens *before* the action is dispatched, not after: the same probe
226
- * found `'init'` can arrive while the action's own CDP round trip is still in flight, sometimes
227
- * only a millisecond or two after it started — a listener installed only once the action's
228
- * promise resolves can already be too late, not merely unlucky.
229
- * - Maintains `_lastMainFrameNav`, a rolling `{loaderId, events}` record of every lifecycle event
230
- * name seen for the current main-frame navigation (reset whenever `loaderId` changes). On a
231
- * fast/local navigation, the same raw probe found the *entire* sequence — `init` through
232
- * `networkIdle` — arriving as one batch while the triggering action's own round trip was still
233
- * in flight. Without this cache, `_waitForAction` would correctly detect that a navigation
234
- * started, then arm a *fresh* wait for the `load` event specifically — which, in that common
235
- * case, had already fired and will never fire again, paying the full grace-window-plus-poll cost
236
- * of `_waitForPageLoad` on every single navigating action instead of settling immediately.
237
- *
238
- * @protected
239
- */
240
374
  _ensureLifecycleListener() {
241
375
  if (this._lifecycleListenerInstalled) return
242
376
  this._lifecycleListenerInstalled = true
@@ -260,24 +394,6 @@ class CDPBrowser extends Helper {
260
394
  })
261
395
  }
262
396
 
263
- /**
264
- * Starts waiting for a `Page.lifecycleEvent` named `eventName` for the given `loaderId` on the
265
- * current session. `loaderId` (from the `Page.navigate` response) discriminates the awaited
266
- * navigation from any other in-flight or stale lifecycle events (e.g. the `about:blank` target
267
- * created in `_before`), which is essential since Chrome emits the target's initial `about:blank`
268
- * lifecycle sequence asynchronously, sometimes after this listener is already installed.
269
- *
270
- * Returns a `{promise, cancel}` pair rather than a bare promise: `_waitForPageLoad` races this
271
- * against a readyState poll, and whichever side loses must be actively torn down (not just have
272
- * its rejection swallowed) — an abandoned-but-still-pending wait would sit in `_pageLoadWaiters`
273
- * for the full timeout on every single navigation, for no purpose.
274
- *
275
- * @param {string} loaderId - the loader id of the navigation to wait for, from `Page.navigate`'s response.
276
- * @param {string} eventName - the `Page.lifecycleEvent` name to wait for (e.g. `load`, `DOMContentLoaded`, `networkIdle`).
277
- * @param {number} timeoutSec - maximum time to wait, in seconds.
278
- * @returns {{promise: Promise<void>, cancel: function}}
279
- * @protected
280
- */
281
397
  _waitForLoadEvent(loaderId, eventName, timeoutSec) {
282
398
  this._ensureLifecycleListener()
283
399
  let waiter
@@ -303,19 +419,6 @@ class CDPBrowser extends Helper {
303
419
  return { promise, cancel: () => waiter.cancel() }
304
420
  }
305
421
 
306
- /**
307
- * Arms the event-aware settle's navigation-start listener. Must be called *before* the action
308
- * that might trigger a navigation is dispatched, not after — see `_ensureLifecycleListener` for
309
- * why. Returns `null` when `options.waitForAction` was set explicitly, since `_waitForAction`
310
- * ignores the armed listener entirely in that case (a literal fixed sleep, as before this round).
311
- *
312
- * No timeout here: `_waitForAction` applies the grace window itself, starting from when *it*
313
- * runs (after the action's own dispatch already resolved), racing this already-armed listener
314
- * against a fresh timer instead of one that started ticking before the action even began.
315
- *
316
- * @returns {({promise: Promise<string|null>, cancel: function}|null)}
317
- * @protected
318
- */
319
422
  _armActionSettle() {
320
423
  if (this._waitForActionExplicit) return null
321
424
  this._ensureLifecycleListener()
@@ -327,30 +430,6 @@ class CDPBrowser extends Helper {
327
430
  return { promise, cancel: () => { this._navStartWaiters = this._navStartWaiters.filter(w => w !== waiter) } }
328
431
  }
329
432
 
330
- /**
331
- * Waits for a page to finish loading after `Page.navigate`/`Page.reload`, per `options.waitForNavigation`.
332
- *
333
- * Purely event-driven for the first `PAGE_LOAD_GRACE_MS`: only the push-based
334
- * `Page.lifecycleEvent` signal (matched by `loaderId`) is awaited, issuing zero `_evaluate` calls
335
- * — this matters because an `_evaluate` sent while the page's own JavaScript is still busy (e.g.
336
- * a real-world page doing post-load hydration/analytics work) can queue behind it for hundreds of
337
- * ms to multiple seconds, measured directly against a JS-heavy page. Only if the grace window
338
- * elapses without the event (an engine that doesn't emit it, or a genuinely slow navigation) does
339
- * the `document.readyState` poll (via `_poll`) start, racing the still-pending lifecycle wait —
340
- * both bounded by the same `options.getPageTimeout`, so a lifecycle-less engine costs at most
341
- * `PAGE_LOAD_GRACE_MS` more than the poll alone would have, never double the timeout. No
342
- * `loaderId` (e.g. from `Page.reload`, which returns none) skips straight to the poll.
343
- *
344
- * Whichever side ultimately loses is actively cancelled, not merely abandoned — an abandoned poll
345
- * or lifecycle wait would otherwise keep running (issuing readyState `_evaluate` calls every
346
- * `pollInterval`, or holding a `_pageLoadWaiters` entry) for up to the full timeout on every
347
- * navigation, competing for the same CDP connection with real work.
348
- *
349
- * @param {string|null} loaderId - loader id from the triggering `Page.navigate` response, if any.
350
- * @param {string} timeoutMessage - error message used if the readyState poll times out.
351
- * @returns {Promise<void>}
352
- * @protected
353
- */
354
433
  async _waitForPageLoad(loaderId, timeoutMessage) {
355
434
  const waitUntil = READY_STATE_EXPR_BY_WAIT_UNTIL[this.options.waitForNavigation] ? this.options.waitForNavigation : 'load'
356
435
  const readyStateCheck = () => this._evaluate(READY_STATE_EXPR_BY_WAIT_UNTIL[waitUntil]).catch(() => false)
@@ -386,13 +465,6 @@ class CDPBrowser extends Helper {
386
465
  }
387
466
  }
388
467
 
389
- /**
390
- * Hook executed after each test. Closes the target opened in `_before` via `Target.closeTarget`
391
- * and clears `this.targetId`/`this.sessionId`. The underlying `CDPConnection` is left open so it
392
- * can be reused by the next test.
393
- *
394
- * @protected
395
- */
396
468
  async _after() {
397
469
  if (!this.targetId) return
398
470
  await this.cdp.send('Target.closeTarget', { targetId: this.targetId }).catch(() => null)
@@ -402,26 +474,49 @@ class CDPBrowser extends Helper {
402
474
 
403
475
  /**
404
476
  * Hook executed after all tests are run. Closes the underlying `CDPConnection` (and its
405
- * WebSocket) and clears `this.cdp`. Must leave no open sockets or pending timers behind, so the
406
- * process can exit on its own.
477
+ * WebSocket) and clears `this.cdp`, then kills the server process spawned by
478
+ * `_resolveSelfManaged`, if any (`this.serverProcess` is never set when attaching). Must leave no
479
+ * open sockets, pending timers or child processes behind, so the process can exit on its own.
480
+ *
481
+ * The kill runs in a `finally` so the process is always reaped even if closing the CDP
482
+ * connection throws. It sends `SIGTERM` first and waits for the process to exit; a process that
483
+ * ignores `SIGTERM` is escalated to `SIGKILL` after 5s. The promise only resolves once the child
484
+ * has actually exited (confirmed via the `exit` event, not merely once `SIGKILL` was sent — the
485
+ * kernel needs a moment to reap it), with a final safety-net timeout so a stuck child can never
486
+ * keep the event loop alive even if that confirmation is somehow lost.
407
487
  *
408
488
  * @protected
409
489
  */
410
490
  async _finishTest() {
411
- if (this.cdp) await this.cdp.close()
412
- this.cdp = null
491
+ try {
492
+ if (this.cdp) await this.cdp.close()
493
+ this.cdp = null
494
+ } finally {
495
+ if (this.serverProcess) {
496
+ const proc = this.serverProcess
497
+ this.serverProcess = null
498
+ await new Promise(resolve => {
499
+ if (proc.exitCode !== null || proc.signalCode !== null) {
500
+ resolve()
501
+ return
502
+ }
503
+ let settled = false
504
+ const finish = () => {
505
+ if (settled) return
506
+ settled = true
507
+ clearTimeout(killTimer)
508
+ clearTimeout(safetyTimer)
509
+ resolve()
510
+ }
511
+ proc.once('exit', finish)
512
+ const killTimer = setTimeout(() => proc.kill('SIGKILL'), 5000)
513
+ const safetyTimer = setTimeout(finish, 5500)
514
+ proc.kill()
515
+ })
516
+ }
517
+ }
413
518
  }
414
519
 
415
- /**
416
- * Evaluates a JavaScript expression in the page attached to the current session via
417
- * `Runtime.evaluate`, awaiting any returned promise and returning the value by reference
418
- * (`returnByValue: true`). If the expression throws, the browser-side exception description
419
- * (or fallback text) is re-thrown as a JS `Error`.
420
- *
421
- * @param {string} expression - a JavaScript expression (or IIFE) to run in the page context.
422
- * @returns {Promise<any>} the evaluated value, or `undefined` if the expression has no result.
423
- * @protected
424
- */
425
520
  async _evaluate(expression) {
426
521
  const res = await this.cdp.send('Runtime.evaluate', { expression, returnByValue: true, awaitPromise: true }, this.sessionId)
427
522
  if (res.exceptionDetails) {
@@ -431,39 +526,12 @@ class CDPBrowser extends Helper {
431
526
  return res.result ? res.result.value : undefined
432
527
  }
433
528
 
434
- /**
435
- * Ensures the in-page client (`window.__codecept`, installed from `cdpBrowserClient.js`) is
436
- * present on the current page, installing it (and the XPath polyfill, if needed) exactly once.
437
- * Safe to call repeatedly; it is a no-op once the client is detected.
438
- *
439
- * @protected
440
- */
441
529
  async _ensureClient() {
442
530
  const installed = await this._evaluate(`typeof window.__codecept !== 'undefined'`)
443
531
  if (installed) return
444
532
  await this._installClient()
445
533
  }
446
534
 
447
- /**
448
- * Installs the in-page client unconditionally — no `typeof window.__codecept` presence check.
449
- * Used by callers that already know, from a sentinel value returned alongside a failed action,
450
- * that the client is missing on the current page, so re-checking would just be a redundant
451
- * contended round trip.
452
- *
453
- * The 171KB XPath polyfill is only injected alongside the client when `needsXPath` is true (the
454
- * default, for callers without candidate information) *and* the engine actually needs it
455
- * (`capabilities.xpath === 'polyfill'`, cached from `_before`'s probe). Callers that know their
456
- * candidates never resolve to an `xpath` strategy (e.g. `_runSelected`, once it has inspected
457
- * `candidates`/`within`) can pass `false` to skip that inject — the client is still told, via the
458
- * `xpathNeedsPolyfill` flag baked in at install time, that the *engine* will eventually need it,
459
- * so a later call that does hit an xpath candidate gets a clean `'__NO_XPATH__'` miss signal
460
- * (from `window.__codecept.run`) instead of silently falling through to a broken native
461
- * `document.evaluate` — `_runSelected` reacts to that sentinel by injecting the polyfill and
462
- * retrying once, mirroring the `'__NO_CLIENT__'` handling right next to it.
463
- *
464
- * @param {boolean} [needsXPath=true]
465
- * @protected
466
- */
467
535
  async _installClient(needsXPath = true) {
468
536
  const enginePolyfillNeeded = await this._needsXPathPolyfill()
469
537
  if (needsXPath && enginePolyfillNeeded) {
@@ -472,36 +540,12 @@ class CDPBrowser extends Helper {
472
540
  await this._evaluate(`(${installCodeceptClient.toString()})(${JSON.stringify(enginePolyfillNeeded)})`)
473
541
  }
474
542
 
475
- /**
476
- * Whether any candidate strategy — in `candidates` itself, or in any of the `within` scoping
477
- * `layers` searched before it — is an `xpath` locator. Used to decide, before the client is even
478
- * installed, whether the XPath polyfill needs to be bundled into that install or can be deferred.
479
- *
480
- * @param {?Array<{type: string, value: string}>} candidates
481
- * @param {Array<Array<{type: string, value: string}>>} layers
482
- * @returns {boolean}
483
- * @protected
484
- */
485
543
  _candidatesNeedXPath(candidates, layers) {
486
544
  const hasXPath = arr => Array.isArray(arr) && arr.some(c => c && c.type === 'xpath')
487
545
  if (hasXPath(candidates)) return true
488
546
  return layers.some(hasXPath)
489
547
  }
490
548
 
491
- /**
492
- * Determines whether the bundled XPath polyfill must be injected before the in-page client is
493
- * installed. Honors an explicit `options.xpathPolyfill` boolean; otherwise reuses a previously
494
- * probed `capabilities.xpath`, or probes the page's native `document.evaluate`. The probe appends
495
- * two throwaway elements distinguished only by text content and asserts that a text-value XPath
496
- * predicate (`normalize-space(string(.))=...`, the basis of every fuzzy/clickable locator) resolves
497
- * to exactly the matching one — merely checking that `document.evaluate` runs without throwing is
498
- * not enough, since some engines execute a text-value predicate without actually filtering by it,
499
- * silently returning every candidate node instead of none or one. The result is cached on
500
- * `capabilities.xpath` (`'native'` or `'polyfill'`).
501
- *
502
- * @returns {Promise<boolean>} `true` if the polyfill should be injected.
503
- * @protected
504
- */
505
549
  async _needsXPathPolyfill() {
506
550
  if (this.options.xpathPolyfill === true) return true
507
551
  if (this.options.xpathPolyfill === false) return false
@@ -526,18 +570,6 @@ class CDPBrowser extends Helper {
526
570
  return !ok
527
571
  }
528
572
 
529
- /**
530
- * Determines whether `see`/`dontSee`/`waitForText` should read whole-page text through the
531
- * client's own visibility-aware `visibleText()` walker instead of the native
532
- * `document.body.innerText`. Probes once per page by appending a `display:none` element and a
533
- * `<script>` element, each with distinguishing text, and checking that native `innerText`
534
- * excludes both — some engines return an `innerText` that does not honor computed visibility or
535
- * exclude script/style content, even when `getComputedStyle`/layout are otherwise reliable. The
536
- * result is cached on `capabilities.innerText` (`'native'` or `'computed'`).
537
- *
538
- * @returns {Promise<boolean>} `true` if the `visibleText()` fallback should be used.
539
- * @protected
540
- */
541
573
  async _needsVisibleTextFallback() {
542
574
  if (this.capabilities.innerText) return this.capabilities.innerText === 'computed'
543
575
  const ok = await this._evaluate(`(function(){
@@ -561,27 +593,6 @@ class CDPBrowser extends Helper {
561
593
  return !ok
562
594
  }
563
595
 
564
- /**
565
- * Probes and caches capabilities that depend on the actual browser *engine* rather than any
566
- * particular page's content: `capabilities.layout` (via `getComputedStyle`), `capabilities.screenshot`
567
- * (inferred from `layout`), `capabilities.xpath` (via `_needsXPathPolyfill`), and
568
- * `capabilities.innerText` (via `_needsVisibleTextFallback`). Already-known capabilities
569
- * (pre-seeded through `options.capabilities`, or probed earlier) are never re-probed — so, across
570
- * a whole run, this issues a handful of `_evaluate` calls exactly once and is a no-op afterward.
571
- *
572
- * Called from `_before`, against the fresh `about:blank` target created there, specifically so
573
- * these probes run before any real navigation — measured directly (a full stall ledger against
574
- * `github.com`) that running them on the first *real* page instead can cost seconds each, since
575
- * every one is an `_evaluate` competing with that page's own JavaScript for the V8 isolate.
576
- * `about:blank` has no such competition. Also called (cheaply, already cached by then) from
577
- * `amOnPage`, so a helper that skips `_before` for some reason still probes correctly.
578
- *
579
- * The `xpath`/`innerText` probes determine *whether* their respective fallback is needed; they
580
- * do not install anything — injection stays deferred to `_ensureClient`'s reactive install and
581
- * `_textSource`'s own read, matching `amOnPage` no longer eagerly installing the client.
582
- *
583
- * @protected
584
- */
585
596
  async _probeCapabilities() {
586
597
  if (this.capabilities.layout === null) {
587
598
  const display = await this._evaluate(`getComputedStyle(document.documentElement).display`)
@@ -601,47 +612,10 @@ class CDPBrowser extends Helper {
601
612
  }
602
613
  }
603
614
 
604
- /**
605
- * Delegates a find-and-act call to `window.__codecept.run(candidates, action, payload)`. This is
606
- * the primary extension point used by helpers built on top of this class for element queries and
607
- * interactions.
608
- *
609
- * A per-call `context` locator, when given, is resolved and layered on top of any active
610
- * `within` block (searched inside it, not instead of it), so `context` narrows the search
611
- * without breaking out of a surrounding `within`.
612
- *
613
- * @param {Array<{type: 'css'|'xpath', value: string}>} candidates - locator strategies to try, in order, until one matches at least one element.
614
- * @param {string} action - name of the action to run against the matched elements (e.g. `count`, `click`, `fill`).
615
- * @param {object} [payload] - extra data the action needs (e.g. `{ value }` for `fill`).
616
- * @param {?CodeceptJS.LocatorOrString} [context=null] - element to search in, narrowing the candidates below it.
617
- * @returns {Promise<{found: number, result: any}>} number of matched elements and the action's result.
618
- * @protected
619
- */
620
615
  async _run(candidates, action, payload, context = null) {
621
616
  return this._runSelected(candidates, action, payload, this._selectionDescriptor(), context)
622
617
  }
623
618
 
624
- /**
625
- * Same as `_run`, but takes an explicit selection descriptor instead of reading one from
626
- * `store.currentStep`/`options.strict`. Used internally by `CDPElementHandle` to address one
627
- * specific element out of a candidate set by its 1-based index.
628
- *
629
- * The in-page client's presence is checked in the same round-trip as the action itself: the
630
- * evaluated expression resolves to a sentinel string when `window.__codecept` is missing (e.g.
631
- * right after a navigation the registered script didn't reach), in which case the client is
632
- * installed and the call is retried exactly once. Separately, if the client is already present
633
- * but reports (via its own `'__NO_XPATH__'` sentinel) that this call needs the XPath polyfill and
634
- * it was not bundled into that earlier install, the polyfill is injected and the call is retried
635
- * once more — see `_installClient`.
636
- *
637
- * @param {Array<{type: 'css'|'xpath', value: string}>} candidates
638
- * @param {string} action
639
- * @param {object|null} payload
640
- * @param {object|null} selection - `{index}` or `{strict: true}`, mirroring `_selectionDescriptor`.
641
- * @param {?CodeceptJS.LocatorOrString} [context=null]
642
- * @returns {Promise<{found: number, result: any}>}
643
- * @protected
644
- */
645
619
  async _runSelected(candidates, action, payload, selection, context = null) {
646
620
  const layers = []
647
621
  if (this.withinCandidates) layers.push(this.withinCandidates)
@@ -669,17 +643,6 @@ class CDPBrowser extends Helper {
669
643
  return res
670
644
  }
671
645
 
672
- /**
673
- * Builds the `{index, strict}` element-selection descriptor from the current step's options
674
- * (`store.currentStep.opts`) and `options.strict`, mirroring the semantics of
675
- * `lib/helper/extras/elementSelection.js` (used by Puppeteer/WebDriver): a per-step
676
- * `elementIndex` (numeric, or the `'first'`/`'last'` aliases) always takes precedence and
677
- * disables strict mode for that step; otherwise `exact`/`strictMode` per-step options
678
- * override `options.strict` to enable or cancel strict mode.
679
- *
680
- * @returns {object|null} descriptor with optional `index` and `strict` keys, or `null` when neither applies.
681
- * @protected
682
- */
683
646
  _selectionDescriptor() {
684
647
  const opts = store.currentStep?.opts
685
648
  let index = opts?.elementIndex
@@ -692,27 +655,10 @@ class CDPBrowser extends Helper {
692
655
  return strict ? { strict: true } : null
693
656
  }
694
657
 
695
- /**
696
- * A short, human-readable label built from `candidates`, used in `_run`'s elementIndex/strict
697
- * error messages when no locator string is otherwise available.
698
- *
699
- * @param {?Array<{type: 'css'|'xpath', value: string}>} candidates
700
- * @returns {string}
701
- * @protected
702
- */
703
658
  _candidatesLabel(candidates) {
704
659
  return (candidates || []).map(c => c.value).join(' | ')
705
660
  }
706
661
 
707
- /**
708
- * Starts a `within` block, scoping every subsequent `_run` call (and therefore every element
709
- * lookup performed by this helper) to the descendants of the element matched by `locator`.
710
- * Verifies the element exists (against the full document, i.e. unscoped) before narrowing.
711
- *
712
- * @param {CodeceptJS.LocatorOrString} locator - element located by CSS|XPath|strict locator.
713
- * @returns {Promise<void>}
714
- * @throws {ElementNotFound} if no element matches `locator`.
715
- */
716
662
  async _withinBegin(locator) {
717
663
  const candidates = this._candidates(locator)
718
664
  const { found } = await this._run(candidates, 'count')
@@ -720,27 +666,10 @@ class CDPBrowser extends Helper {
720
666
  this.withinCandidates = candidates
721
667
  }
722
668
 
723
- /**
724
- * Ends the current `within` block, restoring unscoped element lookups.
725
- *
726
- * @returns {Promise<void>}
727
- */
728
669
  async _withinEnd() {
729
670
  this.withinCandidates = null
730
671
  }
731
672
 
732
- /**
733
- * Repeatedly calls `fn` until it returns a truthy value or `timeoutSec` elapses, checking
734
- * immediately and waiting `options.pollInterval` milliseconds between subsequent attempts.
735
- *
736
- * @param {function} fn - the condition to poll; should resolve to a truthy value once satisfied.
737
- * @param {number} timeoutSec - maximum time to poll, in seconds.
738
- * @param {string} message - error message used when the timeout is reached.
739
- * @param {?{cancelled: boolean}} [cancelToken] - when `cancelled` becomes `true` (set by the caller from outside), polling stops early with an error instead of continuing to `timeoutSec`. Used by `_waitForPageLoad` to tear down the losing side of a race instead of leaving it running.
740
- * @returns {Promise<any>} the truthy value returned by `fn`.
741
- * @throws {Error} with `message` if `timeoutSec` elapses without `fn` returning a truthy value, or a cancellation error if `cancelToken.cancelled` is set first.
742
- * @protected
743
- */
744
673
  async _poll(fn, timeoutSec, message, cancelToken) {
745
674
  const deadline = Date.now() + timeoutSec * 1000
746
675
  while (Date.now() < deadline) {
@@ -753,43 +682,13 @@ class CDPBrowser extends Helper {
753
682
  throw new Error(message)
754
683
  }
755
684
 
756
- /**
757
- * Resolves a path against `options.url`. Absolute URLs (matching `scheme://`) are returned
758
- * unchanged; anything else is appended to `options.url` with its trailing slash stripped.
759
- *
760
- * @param {string} path - an absolute URL or a path relative to `options.url`.
761
- * @returns {string} the resolved, absolute URL.
762
- * @protected
763
- */
764
685
  _url(path) {
765
686
  if (/^\w+:\/\//.test(path)) return path
766
687
  return this.options.url.replace(/\/$/, '') + path
767
688
  }
768
689
 
769
690
  /**
770
- * Opens a web page in the current session.
771
- *
772
- * ```js
773
- * I.amOnPage('/'); // opens main page of website
774
- * I.amOnPage('https://github.com'); // opens github
775
- * I.amOnPage('/login'); // opens a login page
776
- * ```
777
- *
778
- * Navigates via `Page.navigate`, then waits (up to `options.getPageTimeout` seconds) for the page
779
- * to finish loading, preferring the push-based `Page.lifecycleEvent` signal (per
780
- * `options.waitForNavigation`) over polling `document.readyState`. Capabilities are (re-)probed
781
- * (a no-op after the first page, since they're cached for the helper's lifetime).
782
- *
783
- * The in-page client is deliberately *not* eagerly (re-)installed here — navigation discards any
784
- * previously injected script, but installing it is deferred to the first actual action after
785
- * this call, via `_runSelected`'s sentinel-and-retry. This keeps `amOnPage` itself down to the
786
- * navigate command plus the push-based wait: no `_evaluate` call is issued on this hot path,
787
- * which matters most right when the page's own JavaScript may still be busy (measured directly:
788
- * an `_evaluate` sent in that window can queue behind it for hundreds of ms to multiple seconds
789
- * on a JS-heavy real-world page, regardless of how small the evaluated expression is).
790
- *
791
- * @param {string} url - url path or global url.
792
- * @returns {Promise<void>}
691
+ * {{> amOnPage }}
793
692
  */
794
693
  async amOnPage(url) {
795
694
  const navRes = await this.cdp.send('Page.navigate', { url: this._url(url) }, this.sessionId)
@@ -798,16 +697,7 @@ class CDPBrowser extends Helper {
798
697
  }
799
698
 
800
699
  /**
801
- * Reloads the current page.
802
- *
803
- * ```js
804
- * I.refreshPage();
805
- * ```
806
- *
807
- * Triggers `Page.reload` and waits (up to `options.getPageTimeout` seconds) for
808
- * `document.readyState` to reach `'complete'`.
809
- *
810
- * @returns {Promise<void>}
700
+ * {{> refreshPage }}
811
701
  */
812
702
  async refreshPage() {
813
703
  await this.cdp.send('Page.reload', {}, this.sessionId)
@@ -815,22 +705,7 @@ class CDPBrowser extends Helper {
815
705
  }
816
706
 
817
707
  /**
818
- * Executes a JavaScript function in the browser context and returns its result.
819
- *
820
- * If a function is passed, it is serialized with `Function.prototype.toString()`, so it must
821
- * not reference variables from the outer (Node.js) scope — pass any needed data as arguments
822
- * instead. A string is evaluated as-is.
823
- *
824
- * ```js
825
- * let title = await I.executeScript(() => document.title);
826
- * let sum = await I.executeScript((a, b) => a + b, 2, 3);
827
- * ```
828
- *
829
- * If the function returns a promise, `executeScript` waits for it to resolve.
830
- *
831
- * @param {(string|function)} fn - a JavaScript function to be executed in the browser context, or a string expression.
832
- * @param {...any} args - arguments to pass into the function.
833
- * @returns {Promise<any>} the value returned (or resolved) by the function.
708
+ * {{> executeScript }}
834
709
  */
835
710
  async executeScript(fn, ...args) {
836
711
  const body = typeof fn === 'function' ? `(${fn.toString()})(...${JSON.stringify(args)})` : fn
@@ -838,62 +713,26 @@ class CDPBrowser extends Helper {
838
713
  }
839
714
 
840
715
  /**
841
- * Retrieves the page URL of the current page.
842
- *
843
- * ```js
844
- * let url = await I.grabCurrentUrl();
845
- * console.log(`Current URL is [${url}]`);
846
- * ```
847
- *
848
- * @returns {Promise<string>} current URL.
716
+ * {{> grabCurrentUrl }}
849
717
  */
850
718
  async grabCurrentUrl() {
851
719
  return this._evaluate('window.location.href')
852
720
  }
853
721
 
854
722
  /**
855
- * Retrieves a page title.
856
- *
857
- * ```js
858
- * let title = await I.grabTitle();
859
- * ```
860
- *
861
- * @returns {Promise<string>} title of the page.
723
+ * {{> grabTitle }}
862
724
  */
863
725
  async grabTitle() {
864
726
  return this._evaluate('document.title')
865
727
  }
866
728
 
867
729
  /**
868
- * Retrieves the source code of the current page.
869
- *
870
- * ```js
871
- * let pageSource = await I.grabSource();
872
- * ```
873
- *
874
- * @returns {Promise<string>} source code of the current page (the outer HTML of `<html>`).
730
+ * {{> grabSource }}
875
731
  */
876
732
  async grabSource() {
877
733
  return this._evaluate('document.documentElement.outerHTML')
878
734
  }
879
735
 
880
- /**
881
- * Builds the list of `{type, value}` candidates `_run` should try, in order, for a given
882
- * locator and `kind`. A strict locator (CSS/XPath/object form) resolves to a single candidate.
883
- * A fuzzy (plain-text) locator is expanded into a strategy-specific list of XPath expressions
884
- * mirroring the click/field/checkbox matching used by other browser helpers (matching by
885
- * visible text, label, name, placeholder, ARIA attributes, etc.), falling back to treating the
886
- * raw text as a CSS selector.
887
- *
888
- * A role locator (`{role, text, exact}`) resolves to a single `role`-type candidate, resolved
889
- * in-page by the client's implicit ARIA role mapping (native elements) plus explicit `role`
890
- * attributes, filtered by accessible name/text when `text` is given.
891
- *
892
- * @param {CodeceptJS.LocatorOrString} locator - element located by CSS|XPath|strict locator, or plain fuzzy text.
893
- * @param {'element'|'clickable'|'field'|'checkable'} [kind='element'] - matching strategy to use when `locator` is fuzzy.
894
- * @returns {Array<{type: 'css'|'xpath'|'role', value: (string|object)}>} candidates to pass to `_run`.
895
- * @protected
896
- */
897
736
  _candidates(locator, kind = 'element') {
898
737
  locator = new Locator(locator)
899
738
  if (locator.isShadow()) {
@@ -933,18 +772,6 @@ class CDPBrowser extends Helper {
933
772
  return [{ type: 'css', value: locator.value }]
934
773
  }
935
774
 
936
- /**
937
- * Resolves the text to search `see`/`dontSee`/`waitForText` against, when no explicit `context`
938
- * locator is given. An explicit `context` is always resolved through `_run`, so it is implicitly
939
- * scoped to the active `within` block, if any. Without a `context`, this reads the `within` root's
940
- * text when a `within` block is active, or the whole page's text otherwise — via native
941
- * `document.body.innerText`, or the client's `visibleText()` walker when
942
- * `_needsVisibleTextFallback` determines native `innerText` is not trustworthy.
943
- *
944
- * @param {?CodeceptJS.LocatorOrString} context
945
- * @returns {Promise<string>}
946
- * @protected
947
- */
948
775
  async _textSource(context) {
949
776
  if (context) return (await this._texts(this._candidates(context))).result?.join(' | ') || ''
950
777
  if (this.withinCandidates) return (await this._texts(null)).result?.join(' | ') || ''
@@ -955,32 +782,6 @@ class CDPBrowser extends Helper {
955
782
  return this._evaluate('document.body ? document.body.innerText : ""')
956
783
  }
957
784
 
958
- /**
959
- * Runs the in-page `containsText` check against the whole page (no `context`/`within` scoping —
960
- * those stay on `_textSource`'s per-element path, already small). Returns only `{found, snippet}`
961
- * instead of the full haystack: on a page with a large body, serializing that whole string across
962
- * the CDP wire (and, on engines where `capabilities.innerText` requires the `visibleText()`
963
- * walker, holding it in memory) is avoidable work `see`/`dontSee`/`waitForText` don't actually
964
- * need on their common, non-throwing path.
965
- *
966
- * Also folds the client's install into the very same round trip when it is still missing, instead
967
- * of a separate presence-check evaluate followed by an install evaluate before the check itself
968
- * can even run — mirroring `_runSelected`'s sentinel-retry design, but collapsed into one evaluate
969
- * since the install source itself is known and cacheable up front. That install is client-only,
970
- * never the 171KB XPath polyfill: `containsText` never calls `document.evaluate`, so bundling it
971
- * here would be pure waste for a scenario that never resolves an xpath locator. The client is
972
- * still told, via `installCodeceptClient`'s `xpathNeedsPolyfill` flag, whether the *engine*
973
- * (`capabilities.xpath`, cached by `_before`'s `about:blank` probe) will eventually need it, so a
974
- * later xpath-resolving action on the same page gets a clean `'__NO_XPATH__'` miss signal from
975
- * `window.__codecept.run` instead of silently hitting a broken native `document.evaluate` —
976
- * `_runSelected` reacts to that sentinel already. The bootstrap source is built once and reused
977
- * for the life of the instance, since whether the engine needs the polyfill never changes.
978
- *
979
- * @param {string} text
980
- * @param {{ignoreCase: boolean, useWalker: boolean}} opts
981
- * @returns {Promise<{found: boolean, snippet: (string|null)}>}
982
- * @protected
983
- */
984
785
  async _runTextCheck(text, opts) {
985
786
  if (!this._textCheckBootstrap) {
986
787
  const needsPolyfill = await this._needsXPathPolyfill()
@@ -990,19 +791,6 @@ class CDPBrowser extends Helper {
990
791
  return this._evaluate(expression)
991
792
  }
992
793
 
993
- /**
994
- * Shared implementation for `see`/`dontSee`. Without a `context` locator and outside any `within`
995
- * block, checks presence via `_runTextCheck`'s fast, boolean-only round trip; the full haystack is
996
- * only fetched (one extra, rare evaluate) when the assertion is about to fail, to build the same
997
- * `stringIncludes` error as before this optimization. With a `context` or inside `within`, this is
998
- * unchanged from before — already a small, per-element read, not the identified cost.
999
- *
1000
- * @param {string} text
1001
- * @param {?CodeceptJS.LocatorOrString} context
1002
- * @param {boolean} negate
1003
- * @returns {Promise<void>}
1004
- * @protected
1005
- */
1006
794
  async _checkText(text, context, negate) {
1007
795
  const label = context ? `element ${new Locator(context).toString()}` : 'web page'
1008
796
  const ignoreCase = store.currentStep?.opts?.ignoreCase === true
@@ -1030,112 +818,56 @@ class CDPBrowser extends Helper {
1030
818
  }
1031
819
 
1032
820
  /**
1033
- * Checks that a page contains a visible text.
1034
- * Use context parameter to narrow down the search.
1035
- *
1036
- * ```js
1037
- * I.see('Welcome'); // text welcome on a page
1038
- * I.see('Welcome', '.content'); // text inside .content div
1039
- * I.see('Register', {css: 'form.register'}); // use strict locator
1040
- * ```
1041
- *
1042
- * @param {string} text expected on page.
1043
- * @param {?CodeceptJS.LocatorOrString} [context=null] (optional, `null` by default) element located by CSS|Xpath|strict locator in which to search for text.
1044
- * @returns {Promise<void>}
821
+ * {{> see }}
1045
822
  */
1046
823
  async see(text, context = null) {
1047
824
  return this._checkText(text, context, false)
1048
825
  }
1049
826
 
1050
827
  /**
1051
- * Opposite to `see`. Checks that a text is not present on a page.
1052
- * Use context parameter to narrow down the search.
1053
- *
1054
- * ```js
1055
- * I.dontSee('Login'); // assume we are already logged in.
1056
- * I.dontSee('Login', '.nav'); // no login inside .nav element
1057
- * ```
1058
- *
1059
- * @param {string} text which is not present.
1060
- * @param {?CodeceptJS.LocatorOrString} [context=null] (optional) element located by CSS|XPath|strict locator in which to perform search.
1061
- * @returns {Promise<void>}
828
+ * {{> dontSee }}
1062
829
  */
1063
830
  async dontSee(text, context = null) {
1064
831
  return this._checkText(text, context, true)
1065
832
  }
1066
833
 
1067
834
  /**
1068
- * Checks that the current page contains the given string in its raw source code.
1069
- *
1070
- * ```js
1071
- * I.seeInSource('<h1>Green eggs &amp; ham</h1>');
1072
- * ```
1073
- *
1074
- * @param {string} text value to check.
1075
- * @returns {Promise<void>}
835
+ * {{> seeInSource }}
1076
836
  */
1077
837
  async seeInSource(text) {
1078
838
  return stringIncludes('HTML source of a page').assert(text, await this.grabSource())
1079
839
  }
1080
840
 
1081
841
  /**
1082
- * Checks that the current page does not contain the given string in its raw source code.
1083
- *
1084
- * @param {string} text value to check.
1085
- * @returns {Promise<void>}
842
+ * {{> dontSeeInSource }}
1086
843
  */
1087
844
  async dontSeeInSource(text) {
1088
845
  return stringIncludes('HTML source of a page').negate(text, await this.grabSource())
1089
846
  }
1090
847
 
1091
848
  /**
1092
- * Checks that current url contains a provided fragment.
1093
- *
1094
- * ```js
1095
- * I.seeInCurrentUrl('/register'); // we are on registration page
1096
- * ```
1097
- *
1098
- * @param {string} url a fragment to check
1099
- * @returns {Promise<void>}
849
+ * {{> seeInCurrentUrl }}
1100
850
  */
1101
851
  async seeInCurrentUrl(url) {
1102
852
  return stringIncludes('url').assert(url, await this.grabCurrentUrl())
1103
853
  }
1104
854
 
1105
855
  /**
1106
- * Checks that current url does not contain a provided fragment.
1107
- *
1108
- * @param {string} url value to check.
1109
- * @returns {Promise<void>}
856
+ * {{> dontSeeInCurrentUrl }}
1110
857
  */
1111
858
  async dontSeeInCurrentUrl(url) {
1112
859
  return stringIncludes('url').negate(url, await this.grabCurrentUrl())
1113
860
  }
1114
861
 
1115
862
  /**
1116
- * Checks that title contains text.
1117
- *
1118
- * ```js
1119
- * I.seeInTitle('Home Page');
1120
- * ```
1121
- *
1122
- * @param {string} text text value to check.
1123
- * @returns {Promise<void>}
863
+ * {{> seeInTitle }}
1124
864
  */
1125
865
  async seeInTitle(text) {
1126
866
  return stringIncludes('web page title').assert(text, await this.grabTitle())
1127
867
  }
1128
868
 
1129
869
  /**
1130
- * Checks that a given Element is present in the DOM.
1131
- * Element is located by CSS or XPath.
1132
- *
1133
- * ```js
1134
- * I.seeElementInDOM('#modal');
1135
- * ```
1136
- *
1137
- * @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
1138
- * @returns {Promise<void>}
870
+ * {{> seeElementInDOM }}
1139
871
  */
1140
872
  async seeElementInDOM(locator) {
1141
873
  const { found } = await this._run(this._candidates(locator), 'count')
@@ -1143,28 +875,13 @@ class CDPBrowser extends Helper {
1143
875
  }
1144
876
 
1145
877
  /**
1146
- * Opposite to `seeElementInDOM`. Checks that element is not on page.
1147
- *
1148
- * ```js
1149
- * I.dontSeeElementInDOM('.nav'); // checks that element is not on page visible or not
1150
- * ```
1151
- *
1152
- * @param {CodeceptJS.LocatorOrString} locator located by CSS|XPath|strict locator.
1153
- * @returns {Promise<void>}
878
+ * {{> dontSeeElementInDOM }}
1154
879
  */
1155
880
  async dontSeeElementInDOM(locator) {
1156
881
  const { found } = await this._run(this._candidates(locator), 'count')
1157
882
  return empty(`elements of ${new Locator(locator).toString()}`).assert(found === 0 ? null : found)
1158
883
  }
1159
884
 
1160
- /**
1161
- * Throws if the current page has no real layout engine (`capabilities.layout === 'none'`),
1162
- * used to guard visibility-dependent assertions that cannot be evaluated without one.
1163
- *
1164
- * @param {string} action - name of the calling assertion, used in the error message.
1165
- * @throws {Error} if the page has no layout engine.
1166
- * @protected
1167
- */
1168
885
  _assertLayoutSupported(action) {
1169
886
  if (this.capabilities.layout === 'none') {
1170
887
  throw new Error(`${action} requires a layout engine which this browser does not provide. Use ${action}InDOM instead.`)
@@ -1172,16 +889,7 @@ class CDPBrowser extends Helper {
1172
889
  }
1173
890
 
1174
891
  /**
1175
- * Checks that a given Element is visible.
1176
- * Element is located by CSS or XPath.
1177
- *
1178
- * ```js
1179
- * I.seeElement('#modal');
1180
- * ```
1181
- *
1182
- * @param {CodeceptJS.LocatorOrString} locator located by CSS|XPath|strict locator.
1183
- * @param {?CodeceptJS.LocatorOrString} [context=null] (optional, `null` by default) element to search in CSS|XPath|Strict locator.
1184
- * @returns {Promise<void>}
892
+ * {{> seeElement }}
1185
893
  */
1186
894
  async seeElement(locator, context = null) {
1187
895
  this._assertLayoutSupported('seeElement')
@@ -1190,15 +898,7 @@ class CDPBrowser extends Helper {
1190
898
  }
1191
899
 
1192
900
  /**
1193
- * Opposite to `seeElement`. Checks that element is not visible.
1194
- *
1195
- * ```js
1196
- * I.dontSeeElement('.modal'); // modal is not shown
1197
- * ```
1198
- *
1199
- * @param {CodeceptJS.LocatorOrString} locator located by CSS|XPath|strict locator.
1200
- * @param {?CodeceptJS.LocatorOrString} [context=null] (optional, `null` by default) element to search in CSS|XPath|Strict locator.
1201
- * @returns {Promise<void>}
901
+ * {{> dontSeeElement }}
1202
902
  */
1203
903
  async dontSeeElement(locator, context = null) {
1204
904
  this._assertLayoutSupported('dontSeeElement')
@@ -1207,16 +907,7 @@ class CDPBrowser extends Helper {
1207
907
  }
1208
908
 
1209
909
  /**
1210
- * Verifies that the specified checkbox is checked.
1211
- *
1212
- * ```js
1213
- * I.seeCheckboxIsChecked('Agree');
1214
- * I.seeCheckboxIsChecked('#agree'); // I suppose user agreed to terms
1215
- * I.seeCheckboxIsChecked({css: '#signup_form input[type=checkbox]'});
1216
- * ```
1217
- *
1218
- * @param {CodeceptJS.LocatorOrString} locator located by label|name|CSS|XPath|strict locator.
1219
- * @returns {Promise<void>}
910
+ * {{> seeCheckboxIsChecked }}
1220
911
  */
1221
912
  async seeCheckboxIsChecked(locator) {
1222
913
  const res = await this._run(this._candidates(locator, 'checkable'), 'checked')
@@ -1225,15 +916,7 @@ class CDPBrowser extends Helper {
1225
916
  }
1226
917
 
1227
918
  /**
1228
- * Verifies that the specified checkbox is not checked.
1229
- *
1230
- * ```js
1231
- * I.dontSeeCheckboxIsChecked('#agree'); // located by ID
1232
- * I.dontSeeCheckboxIsChecked('I agree to terms'); // located by label
1233
- * ```
1234
- *
1235
- * @param {CodeceptJS.LocatorOrString} locator located by label|name|CSS|XPath|strict locator.
1236
- * @returns {Promise<void>}
919
+ * {{> dontSeeCheckboxIsChecked }}
1237
920
  */
1238
921
  async dontSeeCheckboxIsChecked(locator) {
1239
922
  const res = await this._run(this._candidates(locator, 'checkable'), 'checked')
@@ -1242,16 +925,7 @@ class CDPBrowser extends Helper {
1242
925
  }
1243
926
 
1244
927
  /**
1245
- * Retrieves a text from an element located by CSS or XPath and returns it to test.
1246
- * Resumes test execution, so **should be used inside async with `await`** operator.
1247
- *
1248
- * ```js
1249
- * let pin = await I.grabTextFrom('#pin');
1250
- * ```
1251
- * If multiple elements found returns first element.
1252
- *
1253
- * @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
1254
- * @returns {Promise<string>} text value
928
+ * {{> grabTextFrom }}
1255
929
  */
1256
930
  async grabTextFrom(locator) {
1257
931
  const res = await this._texts(this._candidates(locator))
@@ -1260,15 +934,7 @@ class CDPBrowser extends Helper {
1260
934
  }
1261
935
 
1262
936
  /**
1263
- * Retrieves all texts from elements located by CSS or XPath and returns it to test.
1264
- * Resumes test execution, so **should be used inside async with `await`** operator.
1265
- *
1266
- * ```js
1267
- * let pins = await I.grabTextFromAll('#pin li');
1268
- * ```
1269
- *
1270
- * @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
1271
- * @returns {Promise<string[]>} array of text values
937
+ * {{> grabTextFromAll }}
1272
938
  */
1273
939
  async grabTextFromAll(locator) {
1274
940
  const res = await this._texts(this._candidates(locator))
@@ -1276,17 +942,7 @@ class CDPBrowser extends Helper {
1276
942
  }
1277
943
 
1278
944
  /**
1279
- * Retrieves an array of `WebElement`s matching a locator (`lib/element/WebElement.js`,
1280
- * wrapping a `CDPElementHandle`). Element handles are re-resolved on demand by re-running
1281
- * `candidates` and picking the matching index, since `CDPBrowser` never keeps a persistent
1282
- * handle to a DOM node on the Node side.
1283
- *
1284
- * ```js
1285
- * const buttons = await I.grabWebElements({ role: 'button' });
1286
- * ```
1287
- *
1288
- * @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
1289
- * @returns {Promise<object[]>} array of WebElement instances.
945
+ * {{> grabWebElements }}
1290
946
  */
1291
947
  async grabWebElements(locator) {
1292
948
  const candidates = this._candidates(locator)
@@ -1295,15 +951,7 @@ class CDPBrowser extends Helper {
1295
951
  }
1296
952
 
1297
953
  /**
1298
- * Retrieves the first `WebElement` matching a locator.
1299
- *
1300
- * ```js
1301
- * const button = await I.grabWebElement({ role: 'button', text: 'Submit' });
1302
- * ```
1303
- *
1304
- * @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
1305
- * @returns {Promise<object>} a WebElement instance.
1306
- * @throws {ElementNotFound} if no element matches `locator`.
954
+ * {{> grabWebElement }}
1307
955
  */
1308
956
  async grabWebElement(locator) {
1309
957
  const elements = await this.grabWebElements(locator)
@@ -1311,31 +959,13 @@ class CDPBrowser extends Helper {
1311
959
  return elements[0]
1312
960
  }
1313
961
 
1314
- /**
1315
- * Resolves whether the `texts` action should read via the client's `visibleText()` walker
1316
- * (probed once via `_needsVisibleTextFallback` and cached on `capabilities.innerText`) instead
1317
- * of each element's native `innerText`, then runs it.
1318
- *
1319
- * @param {Array<{type: string, value: (string|object)}>} candidates
1320
- * @returns {Promise<{found: number, result: any}>}
1321
- * @protected
1322
- */
1323
962
  async _texts(candidates) {
1324
963
  const visible = await this._needsVisibleTextFallback()
1325
964
  return this._run(candidates, 'texts', { visible })
1326
965
  }
1327
966
 
1328
967
  /**
1329
- * Retrieves a value from a form element located by CSS or XPath and returns it to test.
1330
- * Resumes test execution, so **should be used inside async function with `await`** operator.
1331
- * If more than one element is found - value of first element is returned.
1332
- *
1333
- * ```js
1334
- * let email = await I.grabValueFrom('input[name=email]');
1335
- * ```
1336
- *
1337
- * @param {CodeceptJS.LocatorOrString} locator field located by label|name|CSS|XPath|strict locator.
1338
- * @returns {Promise<string>} attribute value
968
+ * {{> grabValueFrom }}
1339
969
  */
1340
970
  async grabValueFrom(locator) {
1341
971
  const res = await this._run(this._candidates(locator, 'field'), 'values')
@@ -1344,15 +974,7 @@ class CDPBrowser extends Helper {
1344
974
  }
1345
975
 
1346
976
  /**
1347
- * Retrieves an array of values from fields located by CSS or XPath and returns it to test.
1348
- * Resumes test execution, so **should be used inside async function with `await`** operator.
1349
- *
1350
- * ```js
1351
- * let inputs = await I.grabValueFromAll('//form/input');
1352
- * ```
1353
- *
1354
- * @param {CodeceptJS.LocatorOrString} locator field located by label|name|CSS|XPath|strict locator.
1355
- * @returns {Promise<string[]>} array of attribute values
977
+ * {{> grabValueFromAll }}
1356
978
  */
1357
979
  async grabValueFromAll(locator) {
1358
980
  const res = await this._run(this._candidates(locator, 'field'), 'values')
@@ -1360,17 +982,7 @@ class CDPBrowser extends Helper {
1360
982
  }
1361
983
 
1362
984
  /**
1363
- * Retrieves an attribute from an element located by CSS or XPath and returns it to test.
1364
- * Resumes test execution, so **should be used inside async with `await`** operator.
1365
- * If more than one element is found - attribute of first element is returned.
1366
- *
1367
- * ```js
1368
- * let hint = await I.grabAttributeFrom('#tooltip', 'title');
1369
- * ```
1370
- *
1371
- * @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
1372
- * @param {string} attr attribute name.
1373
- * @returns {Promise<string>} attribute value
985
+ * {{> grabAttributeFrom }}
1374
986
  */
1375
987
  async grabAttributeFrom(locator, attr) {
1376
988
  const res = await this._run(this._candidates(locator), 'attrs', { name: attr })
@@ -1378,17 +990,8 @@ class CDPBrowser extends Helper {
1378
990
  return res.result[0]
1379
991
  }
1380
992
 
1381
- /**
1382
- * Retrieves an array of attributes from elements located by CSS or XPath and returns it to test.
1383
- * Resumes test execution, so **should be used inside async with `await`** operator.
1384
- *
1385
- * ```js
1386
- * let hints = await I.grabAttributeFromAll('.tooltip', 'title');
1387
- * ```
1388
- *
1389
- * @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
1390
- * @param {string} attr attribute name.
1391
- * @returns {Promise<string[]>} array of attribute values
993
+ /**
994
+ * {{> grabAttributeFromAll }}
1392
995
  */
1393
996
  async grabAttributeFromAll(locator, attr) {
1394
997
  const res = await this._run(this._candidates(locator), 'attrs', { name: attr })
@@ -1412,34 +1015,7 @@ class CDPBrowser extends Helper {
1412
1015
  }
1413
1016
 
1414
1017
  /**
1415
- * Perform a click on a link or a button, given by a locator.
1416
- * If a fuzzy locator is given, the page will be searched for a button, link, or image matching the locator string.
1417
- * For buttons, the "value" attribute, "name" attribute, and inner text are searched. For links, the link text is searched.
1418
- * For images, the "alt" attribute and inner text of any parent links are searched.
1419
- *
1420
- * When `options.input` is `'cdp'`, the click is dispatched as real `Input.dispatchMouseEvent` mouse events
1421
- * (`mouseMoved` / `mousePressed` / `mouseReleased`) at the center of the element's bounding box, so it
1422
- * exercises the same input pipeline a real user would. Otherwise it delegates to `forceClick`. A matched
1423
- * element with a zero-size bounding box (e.g. `display: none`) has no valid coordinate to click and throws;
1424
- * use `forceClick` to dispatch a synthetic click on such elements instead.
1425
- *
1426
- * ```js
1427
- * // simple link
1428
- * I.click('Logout');
1429
- * // button of form
1430
- * I.click('Submit');
1431
- * // CSS button
1432
- * I.click('#form input[type=submit]');
1433
- * // XPath
1434
- * I.click('//form/*[@type=submit]');
1435
- * // using strict locator
1436
- * I.click({css: 'nav a.login'});
1437
- * ```
1438
- *
1439
- * @param {CodeceptJS.LocatorOrString} locator clickable link or button located by text, or any element located by CSS|XPath|strict locator.
1440
- * @param {?CodeceptJS.LocatorOrString} [context=null] (optional, `null` by default) element to search in CSS|XPath|Strict locator.
1441
- * @returns {Promise<void>}
1442
- * @throws {Error} if the matched element has a zero-size bounding box.
1018
+ * {{> click }}
1443
1019
  */
1444
1020
  async click(locator, context = null) {
1445
1021
  if (this.options.input !== 'cdp') return this.forceClick(locator, context)
@@ -1459,30 +1035,7 @@ class CDPBrowser extends Helper {
1459
1035
  }
1460
1036
 
1461
1037
  /**
1462
- * Perform an emulated click on a link or a button, given by a locator.
1463
- * Unlike `click`, this always dispatches a synthetic in-page `el.click()` instead of sending native
1464
- * CDP input events. This works on hidden, animated or inactive elements as well.
1465
- *
1466
- * If a fuzzy locator is given, the page will be searched for a button, link, or image matching the locator string.
1467
- * For buttons, the "value" attribute, "name" attribute, and inner text are searched. For links, the link text is searched.
1468
- * For images, the "alt" attribute and inner text of any parent links are searched.
1469
- *
1470
- * ```js
1471
- * // simple link
1472
- * I.forceClick('Logout');
1473
- * // button of form
1474
- * I.forceClick('Submit');
1475
- * // CSS button
1476
- * I.forceClick('#form input[type=submit]');
1477
- * // XPath
1478
- * I.forceClick('//form/*[@type=submit]');
1479
- * // using strict locator
1480
- * I.forceClick({css: 'nav a.login'});
1481
- * ```
1482
- *
1483
- * @param {CodeceptJS.LocatorOrString} locator clickable link or button located by text, or any element located by CSS|XPath|strict locator.
1484
- * @param {?CodeceptJS.LocatorOrString} [context=null] (optional, `null` by default) element to search in CSS|XPath|Strict locator.
1485
- * @returns {Promise<void>}
1038
+ * {{> forceClick }}
1486
1039
  */
1487
1040
  async forceClick(locator, context = null) {
1488
1041
  const armed = this._armActionSettle()
@@ -1491,37 +1044,6 @@ class CDPBrowser extends Helper {
1491
1044
  return this._waitForAction(armed)
1492
1045
  }
1493
1046
 
1494
- /**
1495
- * Settles after an interaction (click, key press, etc.) before the next step runs, using the
1496
- * listener `_armActionSettle` started *before* the interaction was dispatched (`armed`; a fresh
1497
- * one is armed here too, as a safety net, if a call site forgot to — but arming this late can
1498
- * only miss a navigation that already started during the action's own dispatch, exactly the race
1499
- * this design exists to avoid, so every call site should pass its own pre-armed `armed`, not rely
1500
- * on this fallback).
1501
- *
1502
- * If `options.waitForAction` was set explicitly in the config, honors it literally as a fixed
1503
- * pacing sleep, exactly as before this round — an explicit value is a deliberate choice
1504
- * (slow-motion debugging, a known-slow app) this never second-guesses.
1505
- *
1506
- * Otherwise, event-aware: races the armed listener against a *fresh* `ACTION_SETTLE_GRACE_MS`
1507
- * window (started now, not when it was armed — the action's own dispatch already ran concurrently
1508
- * with the arm, so this is genuinely bounded extra time, not a guess). If nothing declares a
1509
- * navigation, returns immediately once the window elapses — the common case for most actions
1510
- * (typing, toggling a checkbox, focusing a field) — instead of a fixed `options.waitForAction`
1511
- * (100ms by default) sleep on every single action regardless of whether anything is happening.
1512
- *
1513
- * If a navigation *did* start, `_lastMainFrameNav` (see `_ensureLifecycleListener`) is checked
1514
- * first: on a fast/local page, the entire lifecycle sequence through the target event has
1515
- * typically already arrived in the same batch that announced the navigation started, in which
1516
- * case this returns immediately. Only a navigation still genuinely in flight falls through to
1517
- * `_waitForPageLoad` (the same mechanism `amOnPage`/`refreshPage` use) — which waits for it to
1518
- * actually finish, rather than a fixed sleep that has no relationship to how long the navigation
1519
- * actually takes: strictly more correct for a slow navigation, not just faster for a fast one.
1520
- *
1521
- * @param {({promise: Promise<string|null>, cancel: function}|null)} [armed] - from `_armActionSettle`, called before the action.
1522
- * @returns {Promise<void>}
1523
- * @protected
1524
- */
1525
1047
  async _waitForAction(armed) {
1526
1048
  if (this._waitForActionExplicit) {
1527
1049
  return new Promise(r => setTimeout(r, this.options.waitForAction))
@@ -1546,24 +1068,7 @@ class CDPBrowser extends Helper {
1546
1068
  }
1547
1069
 
1548
1070
  /**
1549
- * Fills a text field or textarea, after clearing its value, with the given string.
1550
- * Field is located by name, label, CSS, or XPath.
1551
- *
1552
- * ```js
1553
- * // by label
1554
- * I.fillField('Email', 'hello@world.com');
1555
- * // by name
1556
- * I.fillField('password', secret('123456'));
1557
- * // by CSS
1558
- * I.fillField('form#login input[name=username]', 'John');
1559
- * // or by strict locator
1560
- * I.fillField({css: 'form#login input[name=username]'}, 'John');
1561
- * ```
1562
- *
1563
- * @param {CodeceptJS.LocatorOrString} field located by label|name|CSS|XPath|strict locator.
1564
- * @param {CodeceptJS.StringOrSecret} value text value to fill.
1565
- * @param {?CodeceptJS.LocatorOrString} [context=null] (optional, `null` by default) element to search in CSS|XPath|Strict locator.
1566
- * @returns {Promise<void>}
1071
+ * {{> fillField }}
1567
1072
  */
1568
1073
  async fillField(field, value, context = null) {
1569
1074
  const res = await this._run(this._candidates(field, 'field'), 'fill', { value: String(value) }, context)
@@ -1571,19 +1076,7 @@ class CDPBrowser extends Helper {
1571
1076
  }
1572
1077
 
1573
1078
  /**
1574
- * Appends text to a input field or textarea.
1575
- * Field is located by name, label, CSS or XPath
1576
- *
1577
- * ```js
1578
- * I.appendField('#myTextField', 'appended');
1579
- * // typing secret
1580
- * I.appendField('password', secret('123456'));
1581
- * ```
1582
- *
1583
- * @param {CodeceptJS.LocatorOrString} field located by label|name|CSS|XPath|strict locator
1584
- * @param {string} value text value to append.
1585
- * @param {?CodeceptJS.LocatorOrString} [context=null] (optional, `null` by default) element to search in CSS|XPath|Strict locator.
1586
- * @returns {Promise<void>}
1079
+ * {{> appendField }}
1587
1080
  */
1588
1081
  async appendField(field, value, context = null) {
1589
1082
  const res = await this._run(this._candidates(field, 'field'), 'append', { value: String(value) }, context)
@@ -1591,17 +1084,7 @@ class CDPBrowser extends Helper {
1591
1084
  }
1592
1085
 
1593
1086
  /**
1594
- * Clears a `<textarea>` or text `<input>` element's value.
1595
- *
1596
- * ```js
1597
- * I.clearField('Email');
1598
- * I.clearField('user[email]');
1599
- * I.clearField('#email');
1600
- * ```
1601
- *
1602
- * @param {CodeceptJS.LocatorOrString} field editable field located by label|name|CSS|XPath|strict locator.
1603
- * @param {?CodeceptJS.LocatorOrString} [context=null] (optional, `null` by default) element to search in CSS|XPath|Strict locator.
1604
- * @returns {Promise<void>}
1087
+ * {{> clearField }}
1605
1088
  */
1606
1089
  async clearField(field, context = null) {
1607
1090
  const res = await this._run(this._candidates(field, 'field'), 'clear', null, context)
@@ -1609,23 +1092,7 @@ class CDPBrowser extends Helper {
1609
1092
  }
1610
1093
 
1611
1094
  /**
1612
- * Selects an option in a drop-down select.
1613
- * Field is searched by label | name | CSS | XPath.
1614
- * Option is selected by visible text or by value.
1615
- *
1616
- * ```js
1617
- * I.selectOption('Choose Plan', 'Monthly'); // select by label
1618
- * I.selectOption('subscription', 'Monthly'); // match option by text
1619
- * I.selectOption('subscription', '0'); // or by value
1620
- * I.selectOption('//form/select[@name=account]','Premium');
1621
- * I.selectOption('form select[name=account]', 'Premium');
1622
- * I.selectOption({css: 'form select[name=account]'}, 'Premium');
1623
- * ```
1624
- *
1625
- * @param {CodeceptJS.LocatorOrString} select field located by label|name|CSS|XPath|strict locator.
1626
- * @param {string|string[]} option visible text or value of option, or an array of them for a multi-select.
1627
- * @param {?CodeceptJS.LocatorOrString} [context=null] (optional, `null` by default) element to search in CSS|XPath|Strict locator.
1628
- * @returns {Promise<void>}
1095
+ * {{> selectOption }}
1629
1096
  */
1630
1097
  async selectOption(select, option, context = null) {
1631
1098
  const value = Array.isArray(option) ? option.map(String) : String(option)
@@ -1638,18 +1105,7 @@ class CDPBrowser extends Helper {
1638
1105
  }
1639
1106
 
1640
1107
  /**
1641
- * Selects a checkbox or radio button.
1642
- * Element is located by label or name or CSS or XPath.
1643
- *
1644
- * ```js
1645
- * I.checkOption('#agree');
1646
- * I.checkOption('I Agree to Terms and Conditions');
1647
- * I.checkOption('agree', '//form');
1648
- * ```
1649
- *
1650
- * @param {CodeceptJS.LocatorOrString} field checkbox located by label | name | CSS | XPath | strict locator.
1651
- * @param {?CodeceptJS.LocatorOrString} [context=null] (optional, `null` by default) element located by CSS | XPath | strict locator.
1652
- * @returns {Promise<void>}
1108
+ * {{> checkOption }}
1653
1109
  */
1654
1110
  async checkOption(field, context = null) {
1655
1111
  const res = await this._run(this._candidates(field, 'checkable'), 'check', null, context)
@@ -1657,18 +1113,7 @@ class CDPBrowser extends Helper {
1657
1113
  }
1658
1114
 
1659
1115
  /**
1660
- * Unselects a checkbox or radio button.
1661
- * Element is located by label or name or CSS or XPath.
1662
- *
1663
- * ```js
1664
- * I.uncheckOption('#agree');
1665
- * I.uncheckOption('I Agree to Terms and Conditions');
1666
- * I.uncheckOption('agree', '//form');
1667
- * ```
1668
- *
1669
- * @param {CodeceptJS.LocatorOrString} field checkbox located by label | name | CSS | XPath | strict locator.
1670
- * @param {?CodeceptJS.LocatorOrString} [context=null] (optional, `null` by default) element located by CSS | XPath | strict locator.
1671
- * @returns {Promise<void>}
1116
+ * {{> uncheckOption }}
1672
1117
  */
1673
1118
  async uncheckOption(field, context = null) {
1674
1119
  const res = await this._run(this._candidates(field, 'checkable'), 'uncheck', null, context)
@@ -1676,27 +1121,7 @@ class CDPBrowser extends Helper {
1676
1121
  }
1677
1122
 
1678
1123
  /**
1679
- * Attaches a file to a file input field, or drops it onto a drag-and-drop dropzone element,
1680
- * resolved by label|name|CSS|XPath|strict locator. `pathToFile` is resolved relative to
1681
- * `codecept_dir` (matching Puppeteer/WebDriver). Since `CDPBrowser` never brings element handles
1682
- * back to Node, the resolved element is marked with a throwaway `data-codecept-upload` attribute
1683
- * in-page (respecting `context`/`within`/elementIndex exactly like every other action). A real
1684
- * `<input type="file">` is then addressed by that attribute through the CDP `DOM` domain, which
1685
- * `CDPBrowser` otherwise never uses, to call `DOM.setFileInputFiles`; any other element (a
1686
- * drag-and-drop dropzone) instead gets a synthetic `dragenter`/`dragover`/`drop` sequence with a
1687
- * `DataTransfer` built from the file's contents, entirely in-page. The marker is removed again in
1688
- * a `finally`.
1689
- *
1690
- * ```js
1691
- * I.attachFile('Avatar', 'data/avatar.jpg');
1692
- * I.attachFile('#file', 'data/avatar.jpg');
1693
- * I.attachFile('#dropzone', 'data/avatar.jpg');
1694
- * ```
1695
- *
1696
- * @param {CodeceptJS.LocatorOrString} field located by label|name|CSS|XPath|strict locator.
1697
- * @param {string} pathToFile path to file, relative to `codecept_dir`.
1698
- * @param {?CodeceptJS.LocatorOrString} [context=null] (optional, `null` by default) element to search in CSS|XPath|Strict locator.
1699
- * @returns {Promise<void>}
1124
+ * {{> attachFile }}
1700
1125
  */
1701
1126
  async attachFile(field, pathToFile, context = null) {
1702
1127
  const file = path.join(store.codeceptDir, pathToFile)
@@ -1730,17 +1155,7 @@ class CDPBrowser extends Helper {
1730
1155
  }
1731
1156
 
1732
1157
  /**
1733
- * Waits for element to be present on page (by default waits for `options.waitForTimeout` seconds).
1734
- * Element can be located by CSS or XPath.
1735
- *
1736
- * ```js
1737
- * I.waitForElement('.btn.continue');
1738
- * I.waitForElement('.btn.continue', 5); // wait for 5 secs
1739
- * ```
1740
- *
1741
- * @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
1742
- * @param {number} [sec] (optional, `options.waitForTimeout` by default) time in seconds to wait
1743
- * @returns {Promise<void>}
1158
+ * {{> waitForElement }}
1744
1159
  */
1745
1160
  async waitForElement(locator, sec = null) {
1746
1161
  const timeout = sec || this.options.waitForTimeout
@@ -1753,19 +1168,7 @@ class CDPBrowser extends Helper {
1753
1168
  }
1754
1169
 
1755
1170
  /**
1756
- * Waits for a text to appear (by default waits for `options.waitForTimeout` seconds).
1757
- * Element can be located by CSS or XPath.
1758
- * Narrow down search results by providing context.
1759
- *
1760
- * ```js
1761
- * I.waitForText('Thank you, form has been submitted');
1762
- * I.waitForText('Thank you, form has been submitted', 5, '#modal');
1763
- * ```
1764
- *
1765
- * @param {string} text to wait for.
1766
- * @param {number} [sec] (optional, `options.waitForTimeout` by default) time in seconds to wait
1767
- * @param {?CodeceptJS.LocatorOrString} [context=null] (optional) element located by CSS|XPath|strict locator.
1768
- * @returns {Promise<void>}
1171
+ * {{> waitForText }}
1769
1172
  */
1770
1173
  async waitForText(text, sec = null, context = null) {
1771
1174
  const timeout = sec || this.options.waitForTimeout
@@ -1801,15 +1204,7 @@ class CDPBrowser extends Helper {
1801
1204
  }
1802
1205
 
1803
1206
  /**
1804
- * Waiting for the part of the URL to match the expected. Useful for SPA to understand that page was changed.
1805
- *
1806
- * ```js
1807
- * I.waitInUrl('/info', 2);
1808
- * ```
1809
- *
1810
- * @param {string} urlPart value to check.
1811
- * @param {number} [sec] (optional, `options.waitForTimeout` by default) time in seconds to wait
1812
- * @returns {Promise<void>}
1207
+ * {{> waitInUrl }}
1813
1208
  */
1814
1209
  async waitInUrl(urlPart, sec = null) {
1815
1210
  const timeout = sec || this.options.waitForTimeout
@@ -1829,19 +1224,7 @@ class CDPBrowser extends Helper {
1829
1224
  }
1830
1225
 
1831
1226
  /**
1832
- * Waits for a function to return true (waits for `options.waitForTimeout` seconds by default).
1833
- * Running in browser context.
1834
- *
1835
- * ```js
1836
- * I.waitForFunction(() => window.requests == 0);
1837
- * I.waitForFunction(() => window.requests == 0, 5); // waits for 5 sec
1838
- * I.waitForFunction((count) => window.requests == count, [3], 5) // pass args and wait for 5 sec
1839
- * ```
1840
- *
1841
- * @param {(string|function)} fn to be executed in browser context.
1842
- * @param {any[]|number} [argsOrSec] (optional) arguments for function or, if a number, seconds to wait.
1843
- * @param {number} [sec] (optional, `options.waitForTimeout` by default) time in seconds to wait
1844
- * @returns {Promise<void>}
1227
+ * {{> waitForFunction }}
1845
1228
  */
1846
1229
  async waitForFunction(fn, argsOrSec = null, sec = null) {
1847
1230
  let args = []
@@ -1857,22 +1240,7 @@ class CDPBrowser extends Helper {
1857
1240
  }
1858
1241
 
1859
1242
  /**
1860
- * Sets cookie(s).
1861
- *
1862
- * Can be a single cookie object or an array of cookies:
1863
- *
1864
- * ```js
1865
- * I.setCookie({name: 'auth', value: true});
1866
- *
1867
- * // as array
1868
- * I.setCookie([
1869
- * {name: 'auth', value: true},
1870
- * {name: 'agree', value: true}
1871
- * ]);
1872
- * ```
1873
- *
1874
- * @param {CodeceptJS.Cookie|Array<CodeceptJS.Cookie>} cookie a cookie object or array of cookie objects.
1875
- * @returns {Promise<void>}
1243
+ * {{> setCookie }}
1876
1244
  */
1877
1245
  async setCookie(cookie) {
1878
1246
  const cookies = Array.isArray(cookie) ? cookie : [cookie]
@@ -1900,17 +1268,7 @@ class CDPBrowser extends Helper {
1900
1268
  }
1901
1269
 
1902
1270
  /**
1903
- * Gets a cookie object by name.
1904
- * If none provided gets all cookies.
1905
- * Resumes test execution, so **should be used inside async function with `await`** operator.
1906
- *
1907
- * ```js
1908
- * let cookie = await I.grabCookie('auth');
1909
- * assert(cookie.value, '123456');
1910
- * ```
1911
- *
1912
- * @param {string|null} [name=null] cookie name.
1913
- * @returns {Promise<CodeceptJS.Cookie|Array<CodeceptJS.Cookie>>} a cookie object, or an array of all cookies when `name` is not provided.
1271
+ * {{> grabCookie }}
1914
1272
  */
1915
1273
  async grabCookie(name) {
1916
1274
  const cookies = await this.grabCookies()
@@ -1919,16 +1277,7 @@ class CDPBrowser extends Helper {
1919
1277
  }
1920
1278
 
1921
1279
  /**
1922
- * Clears a cookie by name,
1923
- * if none provided clears all cookies.
1924
- *
1925
- * ```js
1926
- * I.clearCookie();
1927
- * I.clearCookie('test');
1928
- * ```
1929
- *
1930
- * @param {string|null} [name=null] (optional, `null` by default) cookie name
1931
- * @returns {Promise<void>}
1280
+ * {{> clearCookie }}
1932
1281
  */
1933
1282
  async clearCookie(name) {
1934
1283
  const cookies = await this.grabCookies()
@@ -1940,6 +1289,47 @@ class CDPBrowser extends Helper {
1940
1289
  }
1941
1290
  }
1942
1291
 
1292
+ /**
1293
+ * {{> seeInClipboard }}
1294
+ */
1295
+ async seeInClipboard(text) {
1296
+ const clipboard = await this.grabFromClipboard()
1297
+ return stringIncludes('clipboard').assert(text, clipboard)
1298
+ }
1299
+
1300
+ /**
1301
+ * {{> seeClipboardEquals }}
1302
+ */
1303
+ async seeClipboardEquals(text) {
1304
+ const clipboard = await this.grabFromClipboard()
1305
+ return equals('clipboard').assert(clipboard, text)
1306
+ }
1307
+
1308
+ /**
1309
+ * {{> clearClipboard }}
1310
+ */
1311
+ async clearClipboard() {
1312
+ await this._grantClipboardAccess()
1313
+ await this._evaluate(clipboardExpression(writeClipboardScript, ''))
1314
+ }
1315
+
1316
+ /**
1317
+ * {{> grabFromClipboard }}
1318
+ */
1319
+ async grabFromClipboard() {
1320
+ await this._grantClipboardAccess()
1321
+ const clipboard = await this._evaluate(clipboardExpression(readClipboardScript, CLIPBOARD_READ_TIMEOUT_MS))
1322
+ this.debugSection('Clipboard', clipboard)
1323
+ return clipboard
1324
+ }
1325
+
1326
+ async _grantClipboardAccess() {
1327
+ await this.cdp.send('Page.bringToFront', {}, this.sessionId).catch(() => null)
1328
+ const origin = await this._evaluate('window.location.origin').catch(() => null)
1329
+ if (!origin || !origin.startsWith('http')) return
1330
+ await this.cdp.send('Browser.grantPermissions', { origin, permissions: ['clipboardReadWrite', 'clipboardSanitizedWrite'] }).catch(() => null)
1331
+ }
1332
+
1943
1333
  /**
1944
1334
  * Saves a screenshot to the output folder (set in codecept.conf.ts or codecept.conf.js).
1945
1335
  * Filename is relative to the output folder.
@@ -1949,7 +1339,7 @@ class CDPBrowser extends Helper {
1949
1339
  * ```
1950
1340
  *
1951
1341
  * @param {string} fileName file name to save.
1952
- * @returns {Promise<void>}
1342
+ * @returns {void} automatically synchronized promise through #recorder
1953
1343
  */
1954
1344
  async saveScreenshot(fileName) {
1955
1345
  if (this.capabilities.screenshot === false) {
@@ -1961,15 +1351,7 @@ class CDPBrowser extends Helper {
1961
1351
  }
1962
1352
 
1963
1353
  /**
1964
- * Saves a screenshot of a single element to the output folder.
1965
- *
1966
- * ```js
1967
- * I.saveElementScreenshot('#logo', 'logo.png');
1968
- * ```
1969
- *
1970
- * @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
1971
- * @param {string} fileName file name to save.
1972
- * @returns {Promise<void>}
1354
+ * {{> saveElementScreenshot }}
1973
1355
  */
1974
1356
  async saveElementScreenshot(locator, fileName) {
1975
1357
  if (this.capabilities.screenshot === false) {
@@ -1984,61 +1366,33 @@ class CDPBrowser extends Helper {
1984
1366
  }
1985
1367
 
1986
1368
  /**
1987
- * Pauses execution for a number of seconds.
1988
- *
1989
- * ```js
1990
- * I.wait(2); // waits 2 secs
1991
- * ```
1992
- *
1993
- * @param {number} sec number of seconds to wait.
1994
- * @returns {Promise<void>}
1369
+ * {{> wait }}
1995
1370
  */
1996
1371
  async wait(sec) {
1997
1372
  return new Promise(r => setTimeout(r, sec * 1000))
1998
1373
  }
1999
1374
 
2000
1375
  /**
2001
- * Checks that title does not contain text.
2002
- *
2003
- * @param {string} text value to check.
2004
- * @returns {Promise<void>}
1376
+ * {{> dontSeeInTitle }}
2005
1377
  */
2006
1378
  async dontSeeInTitle(text) {
2007
1379
  return stringIncludes('web page title').negate(text, await this.grabTitle())
2008
1380
  }
2009
1381
 
2010
1382
  /**
2011
- * Checks that current url is equal to provided one.
2012
- * Unlike `seeInCurrentUrl` performs a strict comparison.
2013
- *
2014
- * ```js
2015
- * I.seeCurrentUrlEquals('/register');
2016
- * ```
2017
- *
2018
- * @param {string} url value to check.
2019
- * @returns {Promise<void>}
1383
+ * {{> seeCurrentUrlEquals }}
2020
1384
  */
2021
1385
  async seeCurrentUrlEquals(url) {
2022
1386
  return urlEquals(this.options.url).assert(url, await this.grabCurrentUrl())
2023
1387
  }
2024
1388
 
2025
1389
  /**
2026
- * Checks that current url is not equal to provided one.
2027
- * Unlike `dontSeeInCurrentUrl` performs a strict comparison.
2028
- *
2029
- * @param {string} url value to check.
2030
- * @returns {Promise<void>}
1390
+ * {{> dontSeeCurrentUrlEquals }}
2031
1391
  */
2032
1392
  async dontSeeCurrentUrlEquals(url) {
2033
1393
  return urlEquals(this.options.url).negate(url, await this.grabCurrentUrl())
2034
1394
  }
2035
1395
 
2036
- /**
2037
- * Resolves the current page URL to a `pathname`, ignoring the origin, query string, and hash.
2038
- *
2039
- * @returns {Promise<string>} the pathname of the current page.
2040
- * @protected
2041
- */
2042
1396
  async _grabCurrentPath() {
2043
1397
  const currentUrl = await this.grabCurrentUrl()
2044
1398
  const baseUrl = this.options.url || 'http://localhost'
@@ -2046,39 +1400,21 @@ class CDPBrowser extends Helper {
2046
1400
  }
2047
1401
 
2048
1402
  /**
2049
- * Checks that current url path (ignoring query string and hash) equals to provided one.
2050
- *
2051
- * ```js
2052
- * I.seeCurrentPathEquals('/info');
2053
- * ```
2054
- *
2055
- * @param {string} path value to check.
2056
- * @returns {Promise<void>}
1403
+ * {{> seeCurrentPathEquals }}
2057
1404
  */
2058
1405
  async seeCurrentPathEquals(path) {
2059
1406
  return equals('url path').assert(normalizePath(path), normalizePath(await this._grabCurrentPath()))
2060
1407
  }
2061
1408
 
2062
1409
  /**
2063
- * Opposite to `seeCurrentPathEquals`.
2064
- *
2065
- * @param {string} path value to check.
2066
- * @returns {Promise<void>}
1410
+ * {{> dontSeeCurrentPathEquals }}
2067
1411
  */
2068
1412
  async dontSeeCurrentPathEquals(path) {
2069
1413
  return equals('url path').negate(normalizePath(path), normalizePath(await this._grabCurrentPath()))
2070
1414
  }
2071
1415
 
2072
1416
  /**
2073
- * Waits for the entire URL to match the expected (by default waits for `options.waitForTimeout` seconds).
2074
- *
2075
- * ```js
2076
- * I.waitUrlEquals('/info', 2);
2077
- * ```
2078
- *
2079
- * @param {string} urlPart value to check.
2080
- * @param {number} [sec] (optional, `options.waitForTimeout` by default) time in seconds to wait
2081
- * @returns {Promise<void>}
1417
+ * {{> waitUrlEquals }}
2082
1418
  */
2083
1419
  async waitUrlEquals(urlPart, sec = null) {
2084
1420
  const timeout = sec || this.options.waitForTimeout
@@ -2099,15 +1435,16 @@ class CDPBrowser extends Helper {
2099
1435
  }
2100
1436
 
2101
1437
  /**
2102
- * Waits for current url path (ignoring query string and hash) to equal to the expected.
1438
+ * Waits for the current URL path to match the expected path.
1439
+ * Query strings and URL fragments are ignored.
2103
1440
  *
2104
1441
  * ```js
2105
- * I.waitCurrentPathEquals('/info', 2);
1442
+ * I.waitCurrentPathEquals('/info', 5);
2106
1443
  * ```
2107
1444
  *
2108
1445
  * @param {string} path value to check.
2109
- * @param {number} [sec] (optional, `options.waitForTimeout` by default) time in seconds to wait
2110
- * @returns {Promise<void>}
1446
+ * @param {number} [sec] (optional, `waitForTimeout` by default) time in seconds to wait
1447
+ * @returns {void} automatically synchronized promise through #recorder
2111
1448
  */
2112
1449
  async waitCurrentPathEquals(path, sec = null) {
2113
1450
  const timeout = sec || this.options.waitForTimeout
@@ -2128,44 +1465,19 @@ class CDPBrowser extends Helper {
2128
1465
  }
2129
1466
 
2130
1467
  /**
2131
- * Checks that the given input field or textarea equals (contains) the given value.
2132
- * For fuzzy locators, the field is searched by label|name|CSS|XPath|strict locator.
2133
- *
2134
- * ```js
2135
- * I.seeInField('Username', 'davert');
2136
- * ```
2137
- *
2138
- * @param {CodeceptJS.LocatorOrString} field located by label|name|CSS|XPath|strict locator.
2139
- * @param {CodeceptJS.StringOrSecret} value value to check.
2140
- * @param {?CodeceptJS.LocatorOrString} [context=null] (optional, `null` by default) element to search in CSS|XPath|Strict locator.
2141
- * @returns {Promise<void>}
1468
+ * {{> seeInField }}
2142
1469
  */
2143
1470
  async seeInField(field, value, context = null) {
2144
1471
  return this._seeInField('assert', field, value, context)
2145
1472
  }
2146
1473
 
2147
1474
  /**
2148
- * Opposite to `seeInField`.
2149
- *
2150
- * @param {CodeceptJS.LocatorOrString} field located by label|name|CSS|XPath|strict locator.
2151
- * @param {CodeceptJS.StringOrSecret} value value to check.
2152
- * @param {?CodeceptJS.LocatorOrString} [context=null] (optional, `null` by default) element to search in CSS|XPath|Strict locator.
2153
- * @returns {Promise<void>}
1475
+ * {{> dontSeeInField }}
2154
1476
  */
2155
1477
  async dontSeeInField(field, value, context = null) {
2156
1478
  return this._seeInField('negate', field, value, context)
2157
1479
  }
2158
1480
 
2159
- /**
2160
- * Shared implementation for `seeInField`/`dontSeeInField`.
2161
- *
2162
- * @param {'assert'|'negate'} assertType
2163
- * @param {CodeceptJS.LocatorOrString} field
2164
- * @param {CodeceptJS.StringOrSecret} value
2165
- * @param {?CodeceptJS.LocatorOrString} [context=null]
2166
- * @returns {Promise<void>}
2167
- * @protected
2168
- */
2169
1481
  async _seeInField(assertType, field, value, context = null) {
2170
1482
  const locatorText = new Locator(field).toString()
2171
1483
  if (typeof value === 'boolean') {
@@ -2181,14 +1493,7 @@ class CDPBrowser extends Helper {
2181
1493
  }
2182
1494
 
2183
1495
  /**
2184
- * Grab number of visible elements by locator.
2185
- *
2186
- * ```js
2187
- * let numOfVisibleElements = await I.grabNumberOfVisibleElements('p');
2188
- * ```
2189
- *
2190
- * @param {CodeceptJS.LocatorOrString} locator located by CSS|XPath|strict locator.
2191
- * @returns {Promise<number>} number of visible matched elements.
1496
+ * {{> grabNumberOfVisibleElements }}
2192
1497
  */
2193
1498
  async grabNumberOfVisibleElements(locator) {
2194
1499
  this._assertLayoutSupported('grabNumberOfVisibleElements')
@@ -2197,15 +1502,7 @@ class CDPBrowser extends Helper {
2197
1502
  }
2198
1503
 
2199
1504
  /**
2200
- * Asserts that an element appears a given number of times on the page, and that all matching elements are visible.
2201
- *
2202
- * ```js
2203
- * I.seeNumberOfVisibleElements('.buttons', 3);
2204
- * ```
2205
- *
2206
- * @param {CodeceptJS.LocatorOrString} locator located by CSS|XPath|strict locator.
2207
- * @param {number} num expected number of elements.
2208
- * @returns {Promise<void>}
1505
+ * {{> seeNumberOfVisibleElements }}
2209
1506
  */
2210
1507
  async seeNumberOfVisibleElements(locator, num) {
2211
1508
  const found = await this.grabNumberOfVisibleElements(locator)
@@ -2213,39 +1510,21 @@ class CDPBrowser extends Helper {
2213
1510
  }
2214
1511
 
2215
1512
  /**
2216
- * Retrieves the current page scroll position.
2217
- *
2218
- * ```js
2219
- * let { x, y } = await I.grabPageScrollPosition();
2220
- * ```
2221
- *
2222
- * @returns {Promise<{x: number, y: number}>} scroll position.
1513
+ * {{> grabPageScrollPosition }}
2223
1514
  */
2224
1515
  async grabPageScrollPosition() {
2225
1516
  return this.executeScript(() => ({ x: window.pageXOffset, y: window.pageYOffset }))
2226
1517
  }
2227
1518
 
2228
1519
  /**
2229
- * Scrolls to the top of the page.
2230
- *
2231
- * ```js
2232
- * I.scrollPageToTop();
2233
- * ```
2234
- *
2235
- * @returns {Promise<void>}
1520
+ * {{> scrollPageToTop }}
2236
1521
  */
2237
1522
  scrollPageToTop() {
2238
1523
  return this.executeScript(() => window.scrollTo(0, 0))
2239
1524
  }
2240
1525
 
2241
1526
  /**
2242
- * Scrolls to the bottom of the page.
2243
- *
2244
- * ```js
2245
- * I.scrollPageToBottom();
2246
- * ```
2247
- *
2248
- * @returns {Promise<void>}
1527
+ * {{> scrollPageToBottom }}
2249
1528
  */
2250
1529
  scrollPageToBottom() {
2251
1530
  return this.executeScript(() => {
@@ -2256,17 +1535,7 @@ class CDPBrowser extends Helper {
2256
1535
  }
2257
1536
 
2258
1537
  /**
2259
- * Scrolls to the element matched by locator, or to given coordinates.
2260
- *
2261
- * ```js
2262
- * I.scrollTo('#submit');
2263
- * I.scrollTo(100, 200);
2264
- * ```
2265
- *
2266
- * @param {CodeceptJS.LocatorOrString|number} locator element to scroll to, or an X coordinate if no element.
2267
- * @param {number} [offsetX=0] X offset, or Y coordinate if `locator` is a number.
2268
- * @param {number} [offsetY=0] Y offset applied when scrolling to an element.
2269
- * @returns {Promise<void>}
1538
+ * {{> scrollTo }}
2270
1539
  */
2271
1540
  async scrollTo(locator, offsetX = 0, offsetY = 0) {
2272
1541
  if (typeof locator === 'number' && typeof offsetX === 'number') {
@@ -2286,16 +1555,7 @@ class CDPBrowser extends Helper {
2286
1555
  }
2287
1556
 
2288
1557
  /**
2289
- * Retrieves a CSS property from an element located by CSS or XPath.
2290
- * If more than one element is found - value of first element is returned.
2291
- *
2292
- * ```js
2293
- * const value = await I.grabCssPropertyFrom('h3', 'font-weight');
2294
- * ```
2295
- *
2296
- * @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
2297
- * @param {string} cssProperty CSS property name.
2298
- * @returns {Promise<string>} CSS value
1558
+ * {{> grabCssPropertyFrom }}
2299
1559
  */
2300
1560
  async grabCssPropertyFrom(locator, cssProperty) {
2301
1561
  const values = await this.grabCssPropertyFromAll(locator, cssProperty)
@@ -2304,15 +1564,7 @@ class CDPBrowser extends Helper {
2304
1564
  }
2305
1565
 
2306
1566
  /**
2307
- * Retrieves an array of CSS properties from elements located by CSS or XPath.
2308
- *
2309
- * ```js
2310
- * const values = await I.grabCssPropertyFromAll('h3', 'font-weight');
2311
- * ```
2312
- *
2313
- * @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
2314
- * @param {string} cssProperty CSS property name.
2315
- * @returns {Promise<string[]>} array of CSS values
1567
+ * {{> grabCssPropertyFromAll }}
2316
1568
  */
2317
1569
  async grabCssPropertyFromAll(locator, cssProperty) {
2318
1570
  const camelProperty = toCamelCase(cssProperty)
@@ -2322,15 +1574,7 @@ class CDPBrowser extends Helper {
2322
1574
  }
2323
1575
 
2324
1576
  /**
2325
- * Checks that all elements matched by locator have the given CSS properties.
2326
- *
2327
- * ```js
2328
- * I.seeCssPropertiesOnElements('h3', { 'font-weight': 'bold', display: 'block' });
2329
- * ```
2330
- *
2331
- * @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
2332
- * @param {object} cssProperties object with CSS properties and their values to check.
2333
- * @returns {Promise<void>}
1577
+ * {{> seeCssPropertiesOnElements }}
2334
1578
  */
2335
1579
  async seeCssPropertiesOnElements(locator, cssProperties) {
2336
1580
  const cssPropertiesCamelCase = convertCssPropertiesToCamelCase(cssProperties)
@@ -2348,16 +1592,7 @@ class CDPBrowser extends Helper {
2348
1592
  }
2349
1593
 
2350
1594
  /**
2351
- * Checks that all elements matched by locator have the given attribute values.
2352
- * An expected value is matched either as an exact match or as a regular expression against the actual value.
2353
- *
2354
- * ```js
2355
- * I.seeAttributesOnElements('//form', { method: 'post' });
2356
- * ```
2357
- *
2358
- * @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
2359
- * @param {object} attributes object with attribute names and expected values.
2360
- * @returns {Promise<void>}
1595
+ * {{> seeAttributesOnElements }}
2361
1596
  */
2362
1597
  async seeAttributesOnElements(locator, attributes) {
2363
1598
  const attrs = Object.keys(attributes)
@@ -2376,14 +1611,7 @@ class CDPBrowser extends Helper {
2376
1611
  }
2377
1612
 
2378
1613
  /**
2379
- * Focuses a given element.
2380
- *
2381
- * ```js
2382
- * I.focus('#name');
2383
- * ```
2384
- *
2385
- * @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
2386
- * @returns {Promise<void>}
1614
+ * {{> focus }}
2387
1615
  */
2388
1616
  async focus(locator) {
2389
1617
  const armed = this._armActionSettle()
@@ -2393,14 +1621,7 @@ class CDPBrowser extends Helper {
2393
1621
  }
2394
1622
 
2395
1623
  /**
2396
- * Removes focus from a given element.
2397
- *
2398
- * ```js
2399
- * I.blur('#name');
2400
- * ```
2401
- *
2402
- * @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
2403
- * @returns {Promise<void>}
1624
+ * {{> blur }}
2404
1625
  */
2405
1626
  async blur(locator) {
2406
1627
  const armed = this._armActionSettle()
@@ -2410,25 +1631,7 @@ class CDPBrowser extends Helper {
2410
1631
  }
2411
1632
 
2412
1633
  /**
2413
- * Types characters into the currently focused element (as set by `click`, `focus`, etc). Each
2414
- * character dispatches a real `keydown` → `keypress` → (value mutated) → `input` → `keyup`
2415
- * sequence, and mutates a `contenteditable` host's `textContent` instead of `.value`, so this
2416
- * works on rich-text/contenteditable targets as well as `input`/`textarea`. Mirrors Puppeteer's
2417
- * `type(text, options)` semantics.
2418
- *
2419
- * Without a `delay`, every character is dispatched in a single round-trip to the page. With a
2420
- * `delay`, characters are dispatched one round-trip at a time so the requested pause actually
2421
- * elapses between key presses.
2422
- *
2423
- * ```js
2424
- * I.click('Name');
2425
- * I.type('CodeceptJS');
2426
- * I.type(['C', 'o', 'd', 'e']);
2427
- * ```
2428
- *
2429
- * @param {string|string[]} keys characters to type, either as a string or an array of characters.
2430
- * @param {number} [delay] (optional) delay in milliseconds between key presses.
2431
- * @returns {Promise<void>}
1634
+ * {{> type }}
2432
1635
  */
2433
1636
  async type(keys, delay = null) {
2434
1637
  await checkFocusBeforeType(this)
@@ -2477,9 +1680,7 @@ class CDPBrowser extends Helper {
2477
1680
  }
2478
1681
 
2479
1682
  /**
2480
- * Presses a key or key combination on the currently focused element.
2481
- * Under `options.strict`, a modifier+editing-key combination (e.g. `Ctrl+A`) dispatched with no
2482
- * element focused throws `NonFocusedType`, mirroring `focusCheck.js`'s behavior on other helpers.
1683
+ * Presses a key or a key combination on the focused element.
2483
1684
  *
2484
1685
  * ```js
2485
1686
  * I.pressKey('Enter');
@@ -2487,7 +1688,7 @@ class CDPBrowser extends Helper {
2487
1688
  * ```
2488
1689
  *
2489
1690
  * @param {string|string[]} key a key or an array of keys to combine (modifiers first).
2490
- * @returns {Promise<void>}
1691
+ * @returns {void} automatically synchronized promise through #recorder
2491
1692
  */
2492
1693
  async pressKey(key) {
2493
1694
  const originalKey = Array.isArray(key) ? key : [key]
@@ -2513,15 +1714,7 @@ class CDPBrowser extends Helper {
2513
1714
  }
2514
1715
 
2515
1716
  /**
2516
- * Resizes the browser viewport.
2517
- *
2518
- * ```js
2519
- * I.resizeWindow(1024, 768);
2520
- * ```
2521
- *
2522
- * @param {number|'maximize'} width window width, or `'maximize'`.
2523
- * @param {number} [height] window height.
2524
- * @returns {Promise<void>}
1717
+ * {{> resizeWindow }}
2525
1718
  */
2526
1719
  async resizeWindow(width, height) {
2527
1720
  if (width === 'maximize') {
@@ -2533,15 +1726,7 @@ class CDPBrowser extends Helper {
2533
1726
  }
2534
1727
 
2535
1728
  /**
2536
- * Performs a double-click on an element matched by locator.
2537
- *
2538
- * ```js
2539
- * I.doubleClick('Edit');
2540
- * ```
2541
- *
2542
- * @param {CodeceptJS.LocatorOrString} locator clickable element located by text, or any element located by CSS|XPath|strict locator.
2543
- * @param {?CodeceptJS.LocatorOrString} [context=null] (optional, `null` by default, currently ignored by this helper).
2544
- * @returns {Promise<void>}
1729
+ * {{> doubleClick }}
2545
1730
  */
2546
1731
  async doubleClick(locator, context = null) {
2547
1732
  const armed = this._armActionSettle()
@@ -2551,15 +1736,7 @@ class CDPBrowser extends Helper {
2551
1736
  }
2552
1737
 
2553
1738
  /**
2554
- * Performs a right-click on an element matched by locator.
2555
- *
2556
- * ```js
2557
- * I.rightClick('Menu');
2558
- * ```
2559
- *
2560
- * @param {CodeceptJS.LocatorOrString} locator clickable element located by text, or any element located by CSS|XPath|strict locator.
2561
- * @param {?CodeceptJS.LocatorOrString} [context=null] (optional, `null` by default, currently ignored by this helper).
2562
- * @returns {Promise<void>}
1739
+ * {{> rightClick }}
2563
1740
  */
2564
1741
  async rightClick(locator, context = null) {
2565
1742
  const armed = this._armActionSettle()
@@ -2569,18 +1746,18 @@ class CDPBrowser extends Helper {
2569
1746
  }
2570
1747
 
2571
1748
  /**
2572
- * Clicks at global page coordinates, or at coordinates relative to an element.
2573
- * Dispatches a real CDP mouse click and therefore requires a real layout engine.
1749
+ * Clicks at page coordinates, or at coordinates relative to an element.
1750
+ * Requires a browser that renders layout.
2574
1751
  *
2575
1752
  * ```js
2576
- * I.clickXY(100, 200); // global coordinates
1753
+ * I.clickXY(100, 200); // page coordinates
2577
1754
  * I.clickXY('#area', 50, 30); // relative to #area
2578
1755
  * ```
2579
1756
  *
2580
- * @param {CodeceptJS.LocatorOrString|number} locator element to click relative to, or a global X coordinate.
2581
- * @param {number} [x] X coordinate relative to element, or global Y coordinate if `locator` is a number.
2582
- * @param {number} [y] Y coordinate relative to element.
2583
- * @returns {Promise<void>}
1757
+ * @param {CodeceptJS.LocatorOrString|number} locator element to click relative to, or the X page coordinate.
1758
+ * @param {number} [x] X coordinate relative to the element, or the Y page coordinate if `locator` is a number.
1759
+ * @param {number} [y] Y coordinate relative to the element.
1760
+ * @returns {void} automatically synchronized promise through #recorder
2584
1761
  */
2585
1762
  async clickXY(locator, x, y) {
2586
1763
  this._assertLayoutSupported('clickXY')
@@ -2605,15 +1782,7 @@ class CDPBrowser extends Helper {
2605
1782
  }
2606
1783
 
2607
1784
  /**
2608
- * Waits for an element to become visible (by default waits for `options.waitForTimeout` seconds).
2609
- *
2610
- * ```js
2611
- * I.waitForVisible('#popup', 5);
2612
- * ```
2613
- *
2614
- * @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
2615
- * @param {number} [sec] (optional, `options.waitForTimeout` by default) time in seconds to wait
2616
- * @returns {Promise<void>}
1785
+ * {{> waitForVisible }}
2617
1786
  */
2618
1787
  async waitForVisible(locator, sec = null) {
2619
1788
  this._assertLayoutSupported('waitForVisible')
@@ -2627,15 +1796,7 @@ class CDPBrowser extends Helper {
2627
1796
  }
2628
1797
 
2629
1798
  /**
2630
- * Waits for an element to become invisible (by default waits for `options.waitForTimeout` seconds).
2631
- *
2632
- * ```js
2633
- * I.waitForInvisible('#popup', 5);
2634
- * ```
2635
- *
2636
- * @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
2637
- * @param {number} [sec] (optional, `options.waitForTimeout` by default) time in seconds to wait
2638
- * @returns {Promise<void>}
1799
+ * {{> waitForInvisible }}
2639
1800
  */
2640
1801
  async waitForInvisible(locator, sec = null) {
2641
1802
  this._assertLayoutSupported('waitForInvisible')
@@ -2649,26 +1810,14 @@ class CDPBrowser extends Helper {
2649
1810
  }
2650
1811
 
2651
1812
  /**
2652
- * Waits for an element to be hidden. Alias of `waitForInvisible`.
2653
- *
2654
- * @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
2655
- * @param {number} [sec] (optional, `options.waitForTimeout` by default) time in seconds to wait
2656
- * @returns {Promise<void>}
1813
+ * {{> waitToHide }}
2657
1814
  */
2658
1815
  async waitToHide(locator, sec = null) {
2659
1816
  return this.waitForInvisible(locator, sec)
2660
1817
  }
2661
1818
 
2662
1819
  /**
2663
- * Waits for an element to be removed from the DOM (by default waits for `options.waitForTimeout` seconds).
2664
- *
2665
- * ```js
2666
- * I.waitForDetached('#popup', 5);
2667
- * ```
2668
- *
2669
- * @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
2670
- * @param {number} [sec] (optional, `options.waitForTimeout` by default) time in seconds to wait
2671
- * @returns {Promise<void>}
1820
+ * {{> waitForDetached }}
2672
1821
  */
2673
1822
  async waitForDetached(locator, sec = null) {
2674
1823
  const timeout = sec || this.options.waitForTimeout
@@ -2681,10 +1830,7 @@ class CDPBrowser extends Helper {
2681
1830
  }
2682
1831
 
2683
1832
  /**
2684
- * Checks that a cookie with the given name is set.
2685
- *
2686
- * @param {string} name cookie name.
2687
- * @returns {Promise<void>}
1833
+ * {{> seeCookie }}
2688
1834
  */
2689
1835
  async seeCookie(name) {
2690
1836
  const cookies = await this.grabCookies()
@@ -2692,10 +1838,7 @@ class CDPBrowser extends Helper {
2692
1838
  }
2693
1839
 
2694
1840
  /**
2695
- * Checks that a cookie with the given name is not set.
2696
- *
2697
- * @param {string} name cookie name.
2698
- * @returns {Promise<void>}
1841
+ * {{> dontSeeCookie }}
2699
1842
  */
2700
1843
  async dontSeeCookie(name) {
2701
1844
  const cookies = await this.grabCookies()
@@ -2703,15 +1846,7 @@ class CDPBrowser extends Helper {
2703
1846
  }
2704
1847
 
2705
1848
  /**
2706
- * Waits for a cookie with the given name to be set (by default waits for `options.waitForTimeout` seconds).
2707
- *
2708
- * ```js
2709
- * I.waitForCookie('auth', 5);
2710
- * ```
2711
- *
2712
- * @param {string} name cookie name.
2713
- * @param {number} [sec] (optional, `options.waitForTimeout` by default) time in seconds to wait
2714
- * @returns {Promise<void>}
1849
+ * {{> waitForCookie }}
2715
1850
  */
2716
1851
  async waitForCookie(name, sec = null) {
2717
1852
  const timeout = sec || this.options.waitForTimeout
@@ -2719,15 +1854,7 @@ class CDPBrowser extends Helper {
2719
1854
  }
2720
1855
 
2721
1856
  /**
2722
- * Retrieves the inner HTML from an element located by CSS or XPath.
2723
- * If more than one element is found - HTML of first element is returned.
2724
- *
2725
- * ```js
2726
- * let postHTML = await I.grabHTMLFrom('#post');
2727
- * ```
2728
- *
2729
- * @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
2730
- * @returns {Promise<string>} HTML code for an element
1857
+ * {{> grabHTMLFrom }}
2731
1858
  */
2732
1859
  async grabHTMLFrom(locator) {
2733
1860
  const html = await this.grabHTMLFromAll(locator)
@@ -2736,14 +1863,7 @@ class CDPBrowser extends Helper {
2736
1863
  }
2737
1864
 
2738
1865
  /**
2739
- * Retrieves the inner HTML from elements located by CSS or XPath.
2740
- *
2741
- * ```js
2742
- * let postHTMLs = await I.grabHTMLFromAll('.post');
2743
- * ```
2744
- *
2745
- * @param {CodeceptJS.LocatorOrString} locator element located by CSS|XPath|strict locator.
2746
- * @returns {Promise<string[]>} HTML code for matched elements
1866
+ * {{> grabHTMLFromAll }}
2747
1867
  */
2748
1868
  async grabHTMLFromAll(locator) {
2749
1869
  const res = await this._run(this._candidates(locator), 'innerHtml')
@@ -2751,18 +1871,7 @@ class CDPBrowser extends Helper {
2751
1871
  }
2752
1872
 
2753
1873
  /**
2754
- * Executes an asynchronous script (callback-style, in the same way `window.setTimeout` works)
2755
- * in the browser context and returns the value passed to `done`.
2756
- *
2757
- * ```js
2758
- * const val = await I.executeAsyncScript(function(val, done) {
2759
- * setTimeout(() => done(val + 1), 100)
2760
- * }, 5)
2761
- * ```
2762
- *
2763
- * @param {function} fn - an asynchronous function to be executed in the browser context; its last argument is a `done` callback.
2764
- * @param {...any} args - arguments to pass into the function (before `done`).
2765
- * @returns {Promise<any>} the value passed to `done`.
1874
+ * {{> executeAsyncScript }}
2766
1875
  */
2767
1876
  async executeAsyncScript(fn, ...args) {
2768
1877
  const fnBody = typeof fn === 'function' ? fn.toString() : fn
@@ -2770,19 +1879,7 @@ class CDPBrowser extends Helper {
2770
1879
  }
2771
1880
 
2772
1881
  /**
2773
- * Starts recording network traffic via CDP's `Network.requestWillBeSent`/`responseReceived`
2774
- * events, in the same shape (`{url, method, requestHeaders, requestPostData, response}` per
2775
- * request, `response` a promise of `{url(), status(), statusText(), body()}`) the shared
2776
- * `lib/helper/network` actions expect from Puppeteer/Playwright. The CDP listeners are
2777
- * installed once (lazily) and left in place afterwards, since `CDPConnection` has no listener
2778
- * removal and `this.cdp` is reused across tests; they filter by `this.sessionId`, so only the
2779
- * currently active test/page's requests are recorded.
2780
- *
2781
- * ```js
2782
- * I.startRecordingTraffic();
2783
- * ```
2784
- *
2785
- * @returns {Promise<void>}
1882
+ * {{> startRecordingTraffic }}
2786
1883
  */
2787
1884
  async startRecordingTraffic() {
2788
1885
  this.flushNetworkTraffics()
@@ -2797,12 +1894,6 @@ class CDPBrowser extends Helper {
2797
1894
  this.cdp.on('Network.loadingFailed', (params, sessionId) => this._onTrafficLoadingFailed(params, sessionId))
2798
1895
  }
2799
1896
 
2800
- /**
2801
- * `Network.requestWillBeSent` handler, pushed into `this.requests` when it belongs to the
2802
- * currently active session and recording is on.
2803
- *
2804
- * @protected
2805
- */
2806
1897
  _onTrafficRequest(params, sessionId) {
2807
1898
  if (!this.recording || sessionId !== this.sessionId) return
2808
1899
  let resolveResponse
@@ -2827,12 +1918,6 @@ class CDPBrowser extends Helper {
2827
1918
  })
2828
1919
  }
2829
1920
 
2830
- /**
2831
- * `Network.responseReceived` handler, resolving the matching pending `response` promise pushed
2832
- * by `_onTrafficRequest` with a Puppeteer-`HTTPResponse`-like object.
2833
- *
2834
- * @protected
2835
- */
2836
1921
  _onTrafficResponse(params, sessionId) {
2837
1922
  if (sessionId !== this.sessionId) return
2838
1923
  const resolve = this._pendingTrafficResponses.get(params.requestId)
@@ -2854,13 +1939,6 @@ class CDPBrowser extends Helper {
2854
1939
  })
2855
1940
  }
2856
1941
 
2857
- /**
2858
- * `Network.loadingFailed` handler, resolving a still-pending `response` promise to `null`
2859
- * (matching Puppeteer's `request.response()` for a failed request) so `grabRecordedNetworkTraffics`
2860
- * never awaits a promise that would otherwise never settle.
2861
- *
2862
- * @protected
2863
- */
2864
1942
  _onTrafficLoadingFailed(params, sessionId) {
2865
1943
  if (sessionId !== this.sessionId) return
2866
1944
  const resolve = this._pendingTrafficResponses.get(params.requestId)
@@ -2891,14 +1969,7 @@ class CDPBrowser extends Helper {
2891
1969
  }
2892
1970
 
2893
1971
  /**
2894
- * Stops recording network traffic started by `startRecordingTraffic`. Already-recorded requests
2895
- * in `this.requests` are kept; only new requests stop being appended.
2896
- *
2897
- * ```js
2898
- * I.stopRecordingTraffic();
2899
- * ```
2900
- *
2901
- * @returns {void}
1972
+ * {{> stopRecordingTraffic }}
2902
1973
  */
2903
1974
  stopRecordingTraffic() {
2904
1975
  this.recording = false
@@ -2912,20 +1983,15 @@ class CDPBrowser extends Helper {
2912
1983
  }
2913
1984
 
2914
1985
  /**
2915
- * Starts recording a CDP `Page.startScreencast` session for the current test's target: frames
2916
- * arrive as `Page.screencastFrame` events, are acknowledged immediately (`Page.screencastFrameAck`,
2917
- * required or the browser stops sending more), and buffered in `this._screencastFrames`. The
2918
- * underlying `Page.screencastFrame` listener is installed once (lazily) and left in place, like
2919
- * `startRecordingTraffic`'s listeners, since `CDPConnection` has no listener-removal API; it
2920
- * filters by `this.sessionId` so only the currently active test's frames are buffered. Call
2921
- * `stopScreencast` to end the capture and assemble the buffered frames into an APNG.
1986
+ * Starts recording a video of the page. Stop it with `stopScreencast`.
1987
+ * Usually enabled through the `screencast` plugin rather than called directly.
2922
1988
  *
2923
1989
  * ```js
2924
1990
  * I.startScreencast();
2925
1991
  * ```
2926
1992
  *
2927
- * @param {object} [options] {maxWidth: number, maxHeight: number, quality: number, everyNthFrame: number} — CDP `Page.startScreencast` pass-throughs. `format` is always `'png'`.
2928
- * @returns {Promise<void>}
1993
+ * @param {object} [options] `{ maxWidth, maxHeight, quality, everyNthFrame }`
1994
+ * @returns {void} automatically synchronized promise through #recorder
2929
1995
  */
2930
1996
  async startScreencast(options = {}) {
2931
1997
  if (!this._screencastListenerInstalled) {
@@ -2942,14 +2008,6 @@ class CDPBrowser extends Helper {
2942
2008
  await this.cdp.send('Page.startScreencast', params, this.sessionId)
2943
2009
  }
2944
2010
 
2945
- /**
2946
- * `Page.screencastFrame` handler: ignores frames from a session other than the currently active
2947
- * one (stale frames from a previous test, since the listener is never removed), acknowledges the
2948
- * frame so the browser keeps sending more, and buffers `{data, timestamp}` for `stopScreencast`
2949
- * to assemble.
2950
- *
2951
- * @protected
2952
- */
2953
2011
  _onScreencastFrame(params, sessionId) {
2954
2012
  if (!this._screencastActive || sessionId !== this.sessionId) return
2955
2013
  this._screencastFrames.push({ data: params.data, timestamp: params.metadata && params.metadata.timestamp })
@@ -2957,23 +2015,14 @@ class CDPBrowser extends Helper {
2957
2015
  }
2958
2016
 
2959
2017
  /**
2960
- * Stops the screencast started by `startScreencast` and assembles the buffered frames into a
2961
- * single APNG (Animated PNG) file, returned as a Buffer. Frame delays are derived from the CDP
2962
- * frame metadata's `timestamp` deltas (frame arrival is activity-driven — Obscura and Chrome both
2963
- * only emit a frame on damage — so this reproduces the actual pacing of what happened, not a
2964
- * fixed frame rate); the last frame is held for `options.lastFrameDelayMs` (default 1000ms) since
2965
- * it has no "next" frame to derive a delay from. Every frame is checked for the PNG signature
2966
- * before assembly — CDP's `format: 'png'` is honored by both Chrome and Obscura (verified
2967
- * directly), but if some other engine ever sends a different format regardless, this reports it
2968
- * via `debugSection` and returns `null` instead of muxing a broken file. Returns `null` if no
2969
- * frames were captured (screencast never started, or stopped immediately after starting).
2018
+ * Stops the recording started by `startScreencast` and returns it as an animated PNG.
2970
2019
  *
2971
2020
  * ```js
2972
- * const apngBuffer = await I.stopScreencast();
2021
+ * const video = await I.stopScreencast();
2973
2022
  * ```
2974
2023
  *
2975
- * @param {object} [options] {lastFrameDelayMs: number} — hold time in milliseconds for the final frame (default 1000).
2976
- * @returns {Promise<object>} a Buffer with the assembled APNG, or null if there was nothing to assemble.
2024
+ * @param {object} [options] `{ lastFrameDelayMs }`: how long the last frame is shown, in milliseconds (1000 by default).
2025
+ * @returns {Promise<Buffer|null>} APNG data, or `null` if no frames were recorded.
2977
2026
  */
2978
2027
  async stopScreencast(options = {}) {
2979
2028
  this._screencastActive = false