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.
- package/README.md +2 -0
- package/bin/codecept.js +11 -0
- package/docs/agents.md +26 -1
- package/docs/ai.md +57 -0
- package/docs/alternative-browsers.md +175 -148
- package/docs/assertions.md +74 -0
- package/docs/bootstrap.md +2 -0
- package/docs/commands.md +45 -0
- package/docs/configuration.md +37 -0
- package/docs/debugging.md +1 -1
- package/docs/helpers/ApiDataFactory.md +20 -20
- package/docs/helpers/Appium.md +1615 -670
- package/docs/helpers/Decision.md +146 -0
- package/docs/helpers/GraphQL.md +31 -31
- package/docs/helpers/GraphQLDataFactory.md +20 -20
- package/docs/helpers/Kitesurf.md +1808 -25
- package/docs/helpers/Lightpanda.md +139 -0
- package/docs/helpers/Obscura.md +1799 -74
- package/docs/helpers/Playwright.md +490 -428
- package/docs/helpers/Puppeteer.md +317 -255
- package/docs/helpers/REST.md +43 -43
- package/docs/helpers/WebDriver.md +237 -175
- package/docs/installation.md +16 -0
- package/docs/mcp.md +7 -1
- package/docs/parallel.md +35 -1
- package/docs/plugins/pause.md +2 -1
- package/docs/plugins/sitemap.md +48 -0
- package/docs/plugins.md +4 -0
- package/lib/ai.js +81 -2
- package/lib/codecept.js +32 -2
- package/lib/command/init.js +22 -0
- package/lib/command/initAgent.js +78 -0
- package/lib/command/run-multiple.js +3 -0
- package/lib/command/run-workers.js +10 -3
- package/lib/command/run.js +3 -2
- package/lib/command/utils.js +15 -0
- package/lib/command/workers/runTests.js +10 -10
- package/lib/container.js +4 -2
- package/lib/effects.js +19 -1
- package/lib/helper/CDPBrowser.js +382 -1393
- package/lib/helper/Decision.js +256 -0
- package/lib/helper/Kitesurf.js +3 -21
- package/lib/helper/Lightpanda.js +137 -0
- package/lib/helper/Obscura.js +21 -238
- package/lib/helper/Playwright.js +6 -4
- package/lib/helper/Puppeteer.js +12 -7
- package/lib/helper/WebDriver.js +2 -1
- package/lib/helper/clientscripts/cdpBrowserClient.js +1 -1
- package/lib/helper/clientscripts/layoutProbe.js +12 -0
- package/lib/helper/extras/CDPConnection.js +10 -2
- package/lib/listener/result.js +4 -0
- package/lib/mocha/bdd.js +19 -1
- package/lib/plugin/junitReporter.js +4 -1
- package/lib/plugin/pause.js +4 -1
- package/lib/plugin/sitemap.js +228 -0
- package/lib/recorder.js +19 -18
- package/lib/result.js +16 -0
- package/lib/step/base.js +12 -0
- package/lib/utils.js +2 -10
- package/lib/webServer.js +193 -0
- package/lib/workers.js +118 -31
- package/package.json +4 -2
- package/typings/index.d.ts +60 -7
- package/typings/promiseBasedTypes.d.ts +772 -824
- package/typings/types.d.ts +866 -833
- package/docs/helpers/CDPBrowser.md +0 -2138
package/lib/helper/CDPBrowser.js
CHANGED
|
@@ -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
|
|
34
|
-
* @prop {object} [headers={}] - headers sent
|
|
35
|
-
* @prop {string} [input=auto] - how
|
|
36
|
-
* @prop {string|boolean} [xpathPolyfill=auto] -
|
|
37
|
-
* @prop {object} [capabilities={}] -
|
|
38
|
-
* @prop {number} [waitForTimeout=5] - default wait*
|
|
39
|
-
* @prop {number} [waitForAction
|
|
40
|
-
* @prop {number} [pollInterval=25] - interval
|
|
41
|
-
* @prop {number} [getPageTimeout=30] - maximum time
|
|
42
|
-
* @prop {string} [waitForNavigation=load] - when
|
|
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
|
-
*
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
this.
|
|
181
|
-
|
|
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
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
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
|
-
*
|
|
193
|
-
*
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
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
|
|
407
|
-
*
|
|
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
|
-
|
|
413
|
-
|
|
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(`
|
|
589
|
-
this.capabilities.layout = display === '' ? '
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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 =
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
1070
|
-
*
|
|
1071
|
-
* ```js
|
|
1072
|
-
* I.seeInSource('<h1>Green eggs & 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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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 {
|
|
1364
|
+
* @returns {void} automatically synchronized promise through #recorder
|
|
2037
1365
|
*/
|
|
2038
1366
|
async saveScreenshot(fileName) {
|
|
2039
1367
|
if (this.capabilities.screenshot === false) {
|
|
2040
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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',
|
|
1466
|
+
* I.waitCurrentPathEquals('/info', 5);
|
|
2190
1467
|
* ```
|
|
2191
1468
|
*
|
|
2192
1469
|
* @param {string} path value to check.
|
|
2193
|
-
* @param {number} [sec] (optional, `
|
|
2194
|
-
* @returns {
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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 {
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
2657
|
-
*
|
|
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); //
|
|
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
|
|
2665
|
-
* @param {number} [x] X coordinate relative to element, or
|
|
2666
|
-
* @param {number} [y] Y coordinate relative to element.
|
|
2667
|
-
* @returns {
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
3000
|
-
*
|
|
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
|
|
3012
|
-
* @returns {
|
|
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
|
|
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
|
|
2045
|
+
* const video = await I.stopScreencast();
|
|
3057
2046
|
* ```
|
|
3058
2047
|
*
|
|
3059
|
-
* @param {object} [options] {lastFrameDelayMs
|
|
3060
|
-
* @returns {Promise<
|
|
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
|