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