codeceptjs 4.2.0 → 4.2.1-beta.2

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