codeceptjs 4.2.1-beta.1 → 4.2.1-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/helpers/Lightpanda.md +1 -1
- package/lib/aria.js +36 -1
- package/lib/container.js +1 -1
- package/lib/helper/CDPBrowser.js +66 -13
- package/lib/helper/Decision.js +2 -2
- package/lib/helper/Lightpanda.js +1 -1
- package/lib/helper/Playwright.js +2 -0
- package/lib/helper/Puppeteer.js +25 -0
- package/lib/helper/WebDriver.js +2 -0
- package/lib/helper/clientscripts/cdpBrowserClient.js +2 -1
- package/lib/helper/clientscripts/layoutProbe.js +12 -0
- package/lib/helper/extras/CDPConnection.js +10 -2
- package/lib/plugin/aiTrace.js +1 -1
- package/lib/plugin/pause.js +1 -1
- package/lib/plugin/screencast.js +3 -6
- package/lib/plugin/screenshot.js +5 -3
- package/lib/utils/trace.js +4 -2
- package/package.json +1 -1
- package/typings/index.d.ts +14 -0
- package/typings/promiseBasedTypes.d.ts +30 -2
- package/typings/types.d.ts +30 -2
|
@@ -62,7 +62,7 @@ These are set automatically and only need overriding for unusual setups:
|
|
|
62
62
|
|
|
63
63
|
## Limitations
|
|
64
64
|
|
|
65
|
-
* No screenshots and no screencast: `saveScreenshot`
|
|
65
|
+
* No screenshots and no screencast: `saveScreenshot` and `saveElementScreenshot` do nothing (a debug message is printed).
|
|
66
66
|
* No frames or popups.
|
|
67
67
|
* No scrolling: scroll positions always stay at 0.
|
|
68
68
|
* Computed styles cover what visibility checks need (`display`, `visibility`, `opacity`,
|
package/lib/aria.js
CHANGED
|
@@ -257,4 +257,39 @@ function diffAriaSnapshots(previous, current) {
|
|
|
257
257
|
return formatDiff(added, removed)
|
|
258
258
|
}
|
|
259
259
|
|
|
260
|
-
|
|
260
|
+
// ─────────────────────────────────────────────────────────────────
|
|
261
|
+
// Build: accessibility tree → YAML snapshot
|
|
262
|
+
// ─────────────────────────────────────────────────────────────────
|
|
263
|
+
|
|
264
|
+
const HIDDEN_ROLES = new Set(['RootWebArea', 'generic', 'none', 'LabelText', 'InlineTextBox', 'LineBreak', 'ListMarker'])
|
|
265
|
+
|
|
266
|
+
function renderAXTree(node, depth = 0, parentName = '') {
|
|
267
|
+
if (!node) return []
|
|
268
|
+
const indent = ' '.repeat(depth)
|
|
269
|
+
const name = (node.name || '').trim()
|
|
270
|
+
if (node.role === 'StaticText') return name && name !== parentName ? [`${indent}- text: ${JSON.stringify(name)}`] : []
|
|
271
|
+
const hidden = HIDDEN_ROLES.has(node.role)
|
|
272
|
+
const children = (node.children || []).flatMap(child => renderAXTree(child, hidden ? depth : depth + 1, hidden ? parentName : name))
|
|
273
|
+
if (hidden) return children
|
|
274
|
+
const line = `${indent}- ${node.role}${name ? ` ${JSON.stringify(name)}` : ''}`
|
|
275
|
+
return [children.length ? `${line}:` : line, ...children]
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Nested tree shaped like Puppeteer's SerializedAXNode: { role, name, children }
|
|
279
|
+
function ariaSnapshotFromTree(root) {
|
|
280
|
+
return renderAXTree(root).join('\n')
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Flat `nodes` list from CDP `Accessibility.getFullAXTree`, rooted at a DOM node
|
|
284
|
+
function ariaSnapshotFromAXNodes(axNodes, backendNodeId) {
|
|
285
|
+
const byId = new Map(axNodes.map(node => [node.nodeId, node]))
|
|
286
|
+
const toTree = node => ({
|
|
287
|
+
role: node.ignored ? 'none' : node.role?.value,
|
|
288
|
+
name: node.name?.value,
|
|
289
|
+
children: (node.childIds || []).map(id => byId.get(id)).filter(Boolean).map(toTree),
|
|
290
|
+
})
|
|
291
|
+
const root = axNodes.find(node => node.backendDOMNodeId === backendNodeId)
|
|
292
|
+
return root ? ariaSnapshotFromTree(toTree(root)) : ''
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export { diffAriaSnapshots, compactAriaSnapshot, ariaSnapshotFromTree, ariaSnapshotFromAXNodes }
|
package/lib/container.js
CHANGED
|
@@ -61,7 +61,7 @@ class Container {
|
|
|
61
61
|
*
|
|
62
62
|
*/
|
|
63
63
|
static get STANDARD_ACTING_HELPERS() {
|
|
64
|
-
return ['Playwright', 'WebDriver', 'Puppeteer', 'Appium']
|
|
64
|
+
return ['Playwright', 'WebDriver', 'Puppeteer', 'Appium', 'Obscura', 'Lightpanda']
|
|
65
65
|
}
|
|
66
66
|
/**
|
|
67
67
|
* Create container with all required helpers and support objects
|
package/lib/helper/CDPBrowser.js
CHANGED
|
@@ -7,9 +7,12 @@ import Helper from '@codeceptjs/helper'
|
|
|
7
7
|
import CDPConnection from './extras/CDPConnection.js'
|
|
8
8
|
import installCodeceptClient from './clientscripts/cdpBrowserClient.js'
|
|
9
9
|
import xpathPolyfillSource from './clientscripts/xpathPolyfill.js'
|
|
10
|
+
import layoutProbe from './clientscripts/layoutProbe.js'
|
|
11
|
+
import { ariaSnapshotFromAXNodes } from '../aria.js'
|
|
10
12
|
import Locator from '../locator.js'
|
|
13
|
+
import output from '../output.js'
|
|
11
14
|
import store from '../store.js'
|
|
12
|
-
import { xpathLocator, normalizePath, resolveUrl, toCamelCase, convertCssPropertiesToCamelCase, normalizeSpacesInString, fileExists, base64EncodeFile, getMimeType, isFile, isWindows } from '../utils.js'
|
|
15
|
+
import { xpathLocator, normalizePath, resolveUrl, toCamelCase, convertCssPropertiesToCamelCase, normalizeSpacesInString, fileExists, base64EncodeFile, getMimeType, isFile, isWindows, screenshotOutputFolder } from '../utils.js'
|
|
13
16
|
import ElementNotFound from './errors/ElementNotFound.js'
|
|
14
17
|
import MultipleElementsFound from './errors/MultipleElementsFound.js'
|
|
15
18
|
import { includes as stringIncludes } from '../assert/include.js'
|
|
@@ -36,7 +39,7 @@ import { assembleApng, isPng } from './extras/apngAssembler.js'
|
|
|
36
39
|
* @prop {object} [headers={}] - headers sent when connecting to the endpoint, e.g. for authenticated remote browsers.
|
|
37
40
|
* @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
41
|
* @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.
|
|
42
|
+
* @prop {object} [capabilities={}] - browser capabilities (`layout`, `xpath`, `screenshot`, `video`, `innerText`, `aria`) to use instead of detecting them at runtime.
|
|
40
43
|
* @prop {number} [waitForTimeout=5] - default timeout for wait* actions, in seconds.
|
|
41
44
|
* @prop {number} [waitForAction] - fixed delay after each action, in milliseconds. When unset, actions wait only for the navigation they trigger.
|
|
42
45
|
* @prop {number} [pollInterval=25] - interval between checks while waiting, in milliseconds.
|
|
@@ -135,7 +138,8 @@ class CDPBrowser extends Helper {
|
|
|
135
138
|
this.serverError = null
|
|
136
139
|
this.binaryPath = null
|
|
137
140
|
this._selfManagedResolved = false
|
|
138
|
-
|
|
141
|
+
/** @type {CodeceptJS.HelperCapabilities} */
|
|
142
|
+
this.capabilities = { screenshot: null, video: null, trace: false, aria: true, layout: null, xpath: null, innerText: null, ...this.options.capabilities }
|
|
139
143
|
this.withinCandidates = null
|
|
140
144
|
this.requests = []
|
|
141
145
|
this.recording = false
|
|
@@ -357,8 +361,20 @@ class CDPBrowser extends Helper {
|
|
|
357
361
|
await this.cdp.connect()
|
|
358
362
|
}
|
|
359
363
|
|
|
364
|
+
_beforeStep() {
|
|
365
|
+
if (this._connectError) throw new Error(this._connectError.message)
|
|
366
|
+
}
|
|
367
|
+
|
|
360
368
|
async _before() {
|
|
361
|
-
|
|
369
|
+
this._connectError = null
|
|
370
|
+
if (!this.cdp || !this.cdp.isConnected) {
|
|
371
|
+
try {
|
|
372
|
+
await this._connect()
|
|
373
|
+
} catch (err) {
|
|
374
|
+
this._connectError = err
|
|
375
|
+
throw err
|
|
376
|
+
}
|
|
377
|
+
}
|
|
362
378
|
this._navStartWaiters = []
|
|
363
379
|
this._lastMainFrameNav = { loaderId: null, events: {} }
|
|
364
380
|
const { targetId } = await this.cdp.send('Target.createTarget', { url: 'about:blank' })
|
|
@@ -595,12 +611,15 @@ class CDPBrowser extends Helper {
|
|
|
595
611
|
|
|
596
612
|
async _probeCapabilities() {
|
|
597
613
|
if (this.capabilities.layout === null) {
|
|
598
|
-
const display = await this._evaluate(`
|
|
599
|
-
this.capabilities.layout = display === '' ? '
|
|
614
|
+
const display = await this._evaluate(`(${layoutProbe.toString()})()`)
|
|
615
|
+
this.capabilities.layout = display === 'none' ? 'real' : 'none'
|
|
600
616
|
}
|
|
601
617
|
if (this.capabilities.screenshot === null) {
|
|
602
618
|
this.capabilities.screenshot = this.capabilities.layout === 'real'
|
|
603
619
|
}
|
|
620
|
+
if (this.capabilities.video === null) {
|
|
621
|
+
this.capabilities.video = this.capabilities.screenshot
|
|
622
|
+
}
|
|
604
623
|
if (this.options.input === 'auto') {
|
|
605
624
|
this.options.input = this.capabilities.layout === 'real' ? 'cdp' : 'synthetic'
|
|
606
625
|
}
|
|
@@ -610,6 +629,13 @@ class CDPBrowser extends Helper {
|
|
|
610
629
|
if (this.capabilities.innerText === null) {
|
|
611
630
|
await this._needsVisibleTextFallback()
|
|
612
631
|
}
|
|
632
|
+
if (!this._engineReported) {
|
|
633
|
+
this._engineReported = true
|
|
634
|
+
let engine = 'render (layout and screenshots)'
|
|
635
|
+
if (this.capabilities.layout === 'none') engine = 'no-render (DOM only: no layout, visibility checks or screenshots)'
|
|
636
|
+
else if (!this.capabilities.screenshot) engine = 'layout without screenshots'
|
|
637
|
+
output.print(`${this.constructor.name} engine: ${engine}`)
|
|
638
|
+
}
|
|
613
639
|
}
|
|
614
640
|
|
|
615
641
|
async _run(candidates, action, payload, context = null) {
|
|
@@ -733,6 +759,32 @@ class CDPBrowser extends Helper {
|
|
|
733
759
|
return this._evaluate('document.documentElement.outerHTML')
|
|
734
760
|
}
|
|
735
761
|
|
|
762
|
+
/**
|
|
763
|
+
* Retrieves the ARIA snapshot of the page, or of an element, built from the browser accessibility tree.
|
|
764
|
+
* The format matches Playwright's [ARIA snapshots](https://playwright.dev/docs/aria-snapshots).
|
|
765
|
+
*
|
|
766
|
+
* ```js
|
|
767
|
+
* const snapshot = await I.grabAriaSnapshot();
|
|
768
|
+
* const formSnapshot = await I.grabAriaSnapshot('#login-form');
|
|
769
|
+
* ```
|
|
770
|
+
*
|
|
771
|
+
* @param {string|object} [locator='//body'] element located by CSS|XPath|strict locator. Defaults to body element.
|
|
772
|
+
* @return {Promise<string>} YAML representation of the accessibility tree
|
|
773
|
+
*/
|
|
774
|
+
async grabAriaSnapshot(locator = '//body') {
|
|
775
|
+
const candidates = this._candidates(locator)
|
|
776
|
+
const { found } = await this._run(candidates, 'count')
|
|
777
|
+
if (!found) throw new ElementNotFound(locator)
|
|
778
|
+
const scope = this.withinCandidates ? [this.withinCandidates] : null
|
|
779
|
+
const expression = `window.__codecept.run(${JSON.stringify(candidates)}, 'element', null, ${JSON.stringify(scope)}, ${JSON.stringify(this._selectionDescriptor())}).result`
|
|
780
|
+
const { result } = await this.cdp.send('Runtime.evaluate', { expression }, this.sessionId)
|
|
781
|
+
const { node } = await this.cdp.send('DOM.describeNode', { objectId: result.objectId }, this.sessionId)
|
|
782
|
+
const { nodes } = await this.cdp.send('Accessibility.getFullAXTree', {}, this.sessionId)
|
|
783
|
+
const snapshot = ariaSnapshotFromAXNodes(nodes, node.backendNodeId)
|
|
784
|
+
this.debugSection('Aria Snapshot', `${snapshot.split('\n').length} lines`)
|
|
785
|
+
return snapshot
|
|
786
|
+
}
|
|
787
|
+
|
|
736
788
|
_candidates(locator, kind = 'element') {
|
|
737
789
|
locator = new Locator(locator)
|
|
738
790
|
if (locator.isShadow()) {
|
|
@@ -807,7 +859,8 @@ class CDPBrowser extends Helper {
|
|
|
807
859
|
const useWalker = await this._needsVisibleTextFallback()
|
|
808
860
|
const res = await this._runTextCheck(text, { ignoreCase, useWalker })
|
|
809
861
|
if (negate ? !res.found : res.found) return
|
|
810
|
-
let source =
|
|
862
|
+
let source = res.text
|
|
863
|
+
if (source === undefined) source = await this._textSource(null)
|
|
811
864
|
let needle = text
|
|
812
865
|
if (ignoreCase) {
|
|
813
866
|
needle = needle.toLowerCase()
|
|
@@ -1343,11 +1396,11 @@ class CDPBrowser extends Helper {
|
|
|
1343
1396
|
*/
|
|
1344
1397
|
async saveScreenshot(fileName) {
|
|
1345
1398
|
if (this.capabilities.screenshot === false) {
|
|
1346
|
-
|
|
1399
|
+
this.debug('saveScreenshot skipped: this browser has no rendering engine')
|
|
1400
|
+
return
|
|
1347
1401
|
}
|
|
1348
|
-
const outputDir = global.output_dir || '.'
|
|
1349
1402
|
const res = await this.cdp.send('Page.captureScreenshot', { format: 'png' }, this.sessionId)
|
|
1350
|
-
fs.writeFileSync(
|
|
1403
|
+
fs.writeFileSync(screenshotOutputFolder(fileName), Buffer.from(res.data, 'base64'))
|
|
1351
1404
|
}
|
|
1352
1405
|
|
|
1353
1406
|
/**
|
|
@@ -1355,14 +1408,14 @@ class CDPBrowser extends Helper {
|
|
|
1355
1408
|
*/
|
|
1356
1409
|
async saveElementScreenshot(locator, fileName) {
|
|
1357
1410
|
if (this.capabilities.screenshot === false) {
|
|
1358
|
-
|
|
1411
|
+
this.debug('saveElementScreenshot skipped: this browser has no rendering engine')
|
|
1412
|
+
return
|
|
1359
1413
|
}
|
|
1360
1414
|
const res = await this._run(this._candidates(locator), 'rect')
|
|
1361
1415
|
if (!res.found) throw new ElementNotFound(locator)
|
|
1362
|
-
const outputDir = global.output_dir || '.'
|
|
1363
1416
|
const { x, y, width, height } = res.result
|
|
1364
1417
|
const shot = await this.cdp.send('Page.captureScreenshot', { format: 'png', clip: { x, y, width, height, scale: 1 } }, this.sessionId)
|
|
1365
|
-
fs.writeFileSync(
|
|
1418
|
+
fs.writeFileSync(screenshotOutputFolder(fileName), Buffer.from(shot.data, 'base64'))
|
|
1366
1419
|
}
|
|
1367
1420
|
|
|
1368
1421
|
/**
|
package/lib/helper/Decision.js
CHANGED
|
@@ -221,7 +221,7 @@ class Decision extends Helper {
|
|
|
221
221
|
title: await helper.grabTitle(),
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
-
if (helper.grabAriaSnapshot) {
|
|
224
|
+
if (helper.grabAriaSnapshot && !(helper.capabilities && helper.capabilities.aria === false)) {
|
|
225
225
|
state.aria = compactAriaSnapshot(await helper.grabAriaSnapshot()).slice(0, this.options.maxLength)
|
|
226
226
|
return state
|
|
227
227
|
}
|
|
@@ -244,7 +244,7 @@ class Decision extends Helper {
|
|
|
244
244
|
|
|
245
245
|
_actingHelper() {
|
|
246
246
|
const helper = pickActingHelper(Container.helpers())
|
|
247
|
-
if (!helper) throw new Error('Decision helper requires a web helper: Playwright, Puppeteer or
|
|
247
|
+
if (!helper) throw new Error('Decision helper requires a web helper: Playwright, Puppeteer, WebDriver, Obscura or Lightpanda')
|
|
248
248
|
return helper
|
|
249
249
|
}
|
|
250
250
|
}
|
package/lib/helper/Lightpanda.js
CHANGED
|
@@ -72,7 +72,7 @@ const config = {}
|
|
|
72
72
|
*
|
|
73
73
|
* ## Limitations
|
|
74
74
|
*
|
|
75
|
-
* - No screenshots and no screencast: `saveScreenshot`
|
|
75
|
+
* - No screenshots and no screencast: `saveScreenshot` and `saveElementScreenshot` do nothing (a debug message is printed).
|
|
76
76
|
* - No frames or popups.
|
|
77
77
|
* - No scrolling: scroll positions always stay at 0.
|
|
78
78
|
* - Computed styles cover what visibility checks need (`display`, `visibility`, `opacity`,
|
package/lib/helper/Playwright.js
CHANGED
|
@@ -340,6 +340,8 @@ class Playwright extends Helper {
|
|
|
340
340
|
super(config)
|
|
341
341
|
|
|
342
342
|
// playwright will be loaded dynamically in _init method
|
|
343
|
+
/** @type {CodeceptJS.HelperCapabilities} */
|
|
344
|
+
this.capabilities = { screenshot: true, video: true, trace: true, aria: true }
|
|
343
345
|
|
|
344
346
|
// set defaults
|
|
345
347
|
this.isRemoteBrowser = false
|
package/lib/helper/Puppeteer.js
CHANGED
|
@@ -6,6 +6,7 @@ import Helper from '@codeceptjs/helper'
|
|
|
6
6
|
import { v4 as uuidv4 } from 'uuid'
|
|
7
7
|
import promiseRetry from 'promise-retry'
|
|
8
8
|
import Locator from '../locator.js'
|
|
9
|
+
import { ariaSnapshotFromTree } from '../aria.js'
|
|
9
10
|
import recorder from '../recorder.js'
|
|
10
11
|
import store from '../store.js'
|
|
11
12
|
import { checkFocusBeforeType, checkFocusBeforePressKey } from './extras/focusCheck.js'
|
|
@@ -237,6 +238,8 @@ class Puppeteer extends Helper {
|
|
|
237
238
|
super(config)
|
|
238
239
|
|
|
239
240
|
// puppeteer will be loaded dynamically in _init method
|
|
241
|
+
/** @type {CodeceptJS.HelperCapabilities} */
|
|
242
|
+
this.capabilities = { screenshot: true, video: false, trace: true, aria: true }
|
|
240
243
|
// set defaults
|
|
241
244
|
this.isRemoteBrowser = false
|
|
242
245
|
this.isRunning = false
|
|
@@ -289,6 +292,7 @@ class Puppeteer extends Helper {
|
|
|
289
292
|
|
|
290
293
|
_setConfig(config) {
|
|
291
294
|
this.options = this._validateConfig(config)
|
|
295
|
+
this.capabilities.aria = this.options.browser !== 'firefox'
|
|
292
296
|
this.puppeteerOptions = {
|
|
293
297
|
headless: !this.options.show,
|
|
294
298
|
...this._getOptions(config),
|
|
@@ -1827,6 +1831,27 @@ class Puppeteer extends Helper {
|
|
|
1827
1831
|
return this.page.content()
|
|
1828
1832
|
}
|
|
1829
1833
|
|
|
1834
|
+
/**
|
|
1835
|
+
* Retrieves the ARIA snapshot of the page, or of an element, built from the browser accessibility tree.
|
|
1836
|
+
* The format matches Playwright's [ARIA snapshots](https://playwright.dev/docs/aria-snapshots).
|
|
1837
|
+
*
|
|
1838
|
+
* ```js
|
|
1839
|
+
* const snapshot = await I.grabAriaSnapshot();
|
|
1840
|
+
* const formSnapshot = await I.grabAriaSnapshot('#login-form');
|
|
1841
|
+
* ```
|
|
1842
|
+
*
|
|
1843
|
+
* @param {string|object} [locator='//body'] element located by CSS|XPath|strict locator. Defaults to body element.
|
|
1844
|
+
* @return {Promise<string>} YAML representation of the accessibility tree
|
|
1845
|
+
*/
|
|
1846
|
+
async grabAriaSnapshot(locator = '//body') {
|
|
1847
|
+
const els = await this._locate(locator)
|
|
1848
|
+
assertElementExists(els, locator)
|
|
1849
|
+
const tree = await this.page.accessibility.snapshot({ root: els[0], interestingOnly: false })
|
|
1850
|
+
const snapshot = ariaSnapshotFromTree(tree)
|
|
1851
|
+
this.debugSection('Aria Snapshot', `${snapshot.split('\n').length} lines`)
|
|
1852
|
+
return snapshot
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1830
1855
|
/**
|
|
1831
1856
|
* Get JS log from browser.
|
|
1832
1857
|
*
|
package/lib/helper/WebDriver.js
CHANGED
|
@@ -461,6 +461,8 @@ class WebDriver extends Helper {
|
|
|
461
461
|
constructor(config) {
|
|
462
462
|
super(config)
|
|
463
463
|
// webdriverio will be loaded dynamically in _init method
|
|
464
|
+
/** @type {CodeceptJS.HelperCapabilities} */
|
|
465
|
+
this.capabilities = { screenshot: true, video: false, trace: false, aria: false }
|
|
464
466
|
|
|
465
467
|
// set defaults
|
|
466
468
|
this.root = webRoot
|
|
@@ -76,7 +76,7 @@ export default function installCodeceptClient(xpathNeedsPolyfill) {
|
|
|
76
76
|
const hayCmp = opts.ignoreCase ? hay.toLowerCase() : hay
|
|
77
77
|
const needleCmp = opts.ignoreCase ? needleNorm.toLowerCase() : needleNorm
|
|
78
78
|
const idx = hayCmp.indexOf(needleCmp)
|
|
79
|
-
if (idx === -1) return { found: false, snippet: null }
|
|
79
|
+
if (idx === -1) return { found: false, snippet: null, text: hay }
|
|
80
80
|
const from = Math.max(0, idx - 60)
|
|
81
81
|
return { found: true, snippet: hay.slice(from, idx + needleNorm.length + 60) }
|
|
82
82
|
}
|
|
@@ -319,6 +319,7 @@ export default function installCodeceptClient(xpathNeedsPolyfill) {
|
|
|
319
319
|
els[0].blur()
|
|
320
320
|
return true
|
|
321
321
|
},
|
|
322
|
+
element: els => els[0],
|
|
322
323
|
rect: els => {
|
|
323
324
|
if (els[0].scrollIntoView) els[0].scrollIntoView({ block: 'center', inline: 'center' })
|
|
324
325
|
const r = els[0].getBoundingClientRect()
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export default function layoutProbe() {
|
|
2
|
+
const style = document.createElement('style')
|
|
3
|
+
style.textContent = '.__codecept-layout-probe{display:none}'
|
|
4
|
+
document.documentElement.appendChild(style)
|
|
5
|
+
const probe = document.createElement('div')
|
|
6
|
+
probe.className = '__codecept-layout-probe'
|
|
7
|
+
;(document.body || document.documentElement).appendChild(probe)
|
|
8
|
+
const display = getComputedStyle(probe).display
|
|
9
|
+
style.remove()
|
|
10
|
+
probe.remove()
|
|
11
|
+
return display
|
|
12
|
+
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { WebSocket } from 'ws'
|
|
2
|
+
import debugModule from 'debug'
|
|
3
|
+
|
|
4
|
+
const debug = debugModule('codeceptjs:cdp')
|
|
2
5
|
|
|
3
6
|
class CDPConnection {
|
|
4
7
|
constructor(endpoint, options = {}) {
|
|
@@ -9,6 +12,7 @@ class CDPConnection {
|
|
|
9
12
|
this.lastId = 0
|
|
10
13
|
this.pending = new Map()
|
|
11
14
|
this.listeners = new Map()
|
|
15
|
+
this.totals = {}
|
|
12
16
|
}
|
|
13
17
|
|
|
14
18
|
async connect() {
|
|
@@ -49,7 +53,7 @@ class CDPConnection {
|
|
|
49
53
|
this.pending.delete(id)
|
|
50
54
|
reject(new Error(`CDP command ${method} timed out after ${this.timeout}ms`))
|
|
51
55
|
}, this.timeout)
|
|
52
|
-
this.pending.set(id, { resolve, reject, timer })
|
|
56
|
+
this.pending.set(id, { resolve, reject, timer, method, started: Date.now() })
|
|
53
57
|
this.ws.send(JSON.stringify(message))
|
|
54
58
|
})
|
|
55
59
|
}
|
|
@@ -61,9 +65,12 @@ class CDPConnection {
|
|
|
61
65
|
|
|
62
66
|
_onMessage(msg) {
|
|
63
67
|
if (msg.id && this.pending.has(msg.id)) {
|
|
64
|
-
const { resolve, reject, timer } = this.pending.get(msg.id)
|
|
68
|
+
const { resolve, reject, timer, method, started } = this.pending.get(msg.id)
|
|
65
69
|
clearTimeout(timer)
|
|
66
70
|
this.pending.delete(msg.id)
|
|
71
|
+
const ms = Date.now() - started
|
|
72
|
+
this.totals[method] = (this.totals[method] || 0) + ms
|
|
73
|
+
debug(`${method} ${ms}ms`)
|
|
67
74
|
if (msg.error) reject(new Error(msg.error.message))
|
|
68
75
|
else resolve(msg.result)
|
|
69
76
|
return
|
|
@@ -80,6 +87,7 @@ class CDPConnection {
|
|
|
80
87
|
|
|
81
88
|
async close() {
|
|
82
89
|
if (!this.ws) return
|
|
90
|
+
debug('total time per method', this.totals)
|
|
83
91
|
await new Promise(resolve => {
|
|
84
92
|
if (this.ws.readyState === WebSocket.CLOSED) return resolve()
|
|
85
93
|
this.ws.once('close', resolve)
|
package/lib/plugin/aiTrace.js
CHANGED
|
@@ -78,7 +78,7 @@ export default function (config = {}) {
|
|
|
78
78
|
const helper = pickActingHelper(Container.helpers())
|
|
79
79
|
|
|
80
80
|
if (!helper) {
|
|
81
|
-
output.
|
|
81
|
+
output.print('aiTrace plugin: No supported helper found. Plugin disabled.')
|
|
82
82
|
return
|
|
83
83
|
}
|
|
84
84
|
|
package/lib/plugin/pause.js
CHANGED
|
@@ -109,7 +109,7 @@ function initUrlMode(pattern) {
|
|
|
109
109
|
const helper = getBrowserHelper()
|
|
110
110
|
|
|
111
111
|
if (!helper) {
|
|
112
|
-
output.error('pause:on=url requires a browser helper (Playwright, WebDriver, Puppeteer, Appium)')
|
|
112
|
+
output.error('pause:on=url requires a browser helper (Playwright, WebDriver, Puppeteer, Appium, Obscura, Lightpanda)')
|
|
113
113
|
return
|
|
114
114
|
}
|
|
115
115
|
|
package/lib/plugin/screencast.js
CHANGED
|
@@ -350,12 +350,9 @@ function buildSrt(steps) {
|
|
|
350
350
|
return out
|
|
351
351
|
}
|
|
352
352
|
|
|
353
|
-
// `getBrowserHelper` (from `pluginParser.js`) only recognizes `Container.STANDARD_ACTING_HELPERS
|
|
354
|
-
// (`
|
|
355
|
-
//
|
|
356
|
-
// Playwright/WebDriver-specific assumptions — this plugin does its own duck-typed fallback lookup,
|
|
357
|
-
// scoped to itself: any active helper exposing a `startScreencast` function qualifies, with no
|
|
358
|
-
// hardcoded class names, so any future CDP-family helper picks this up automatically too.
|
|
353
|
+
// `getBrowserHelper` (from `pluginParser.js`) only recognizes `Container.STANDARD_ACTING_HELPERS`,
|
|
354
|
+
// so helpers outside that list (`CDPBrowser`, `Kitesurf`) are found by a duck-typed fallback:
|
|
355
|
+
// any active helper exposing a `startScreencast` function qualifies.
|
|
359
356
|
function getScreencastHelper() {
|
|
360
357
|
const standard = getBrowserHelper()
|
|
361
358
|
if (standard) return standard
|
package/lib/plugin/screenshot.js
CHANGED
|
@@ -189,14 +189,15 @@ function scheduleScreenshot(test, fileName, options) {
|
|
|
189
189
|
}
|
|
190
190
|
|
|
191
191
|
async function takeScreenshot(test, fileName, options) {
|
|
192
|
+
const helper = getBrowserHelper()
|
|
193
|
+
if (!helper || typeof helper.saveScreenshot !== 'function') return
|
|
194
|
+
if (helper.capabilities && helper.capabilities.screenshot === false) return
|
|
195
|
+
|
|
192
196
|
const quietMode = !store.outputDir
|
|
193
197
|
if (!quietMode) {
|
|
194
198
|
output.plugin('screenshot', `Saving screenshot ${fileName}`)
|
|
195
199
|
}
|
|
196
200
|
|
|
197
|
-
const helper = getBrowserHelper()
|
|
198
|
-
if (!helper || typeof helper.saveScreenshot !== 'function') return
|
|
199
|
-
|
|
200
201
|
try {
|
|
201
202
|
if (options.reportDir) {
|
|
202
203
|
fileName = path.join(options.reportDir, fileName)
|
|
@@ -332,6 +333,7 @@ function wireSlides(options, trigger) {
|
|
|
332
333
|
|
|
333
334
|
const helper = getBrowserHelper()
|
|
334
335
|
if (!helper || typeof helper.saveScreenshot !== 'function') return
|
|
336
|
+
if (helper.capabilities && helper.capabilities.screenshot === false) return
|
|
335
337
|
|
|
336
338
|
try {
|
|
337
339
|
const screenshotPath = path.join(dir, fileName)
|
package/lib/utils/trace.js
CHANGED
|
@@ -176,7 +176,8 @@ export async function captureSnapshot(helper, {
|
|
|
176
176
|
} catch {}
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
|
|
179
|
+
const canScreenshot = !(helper.capabilities && helper.capabilities.screenshot === false)
|
|
180
|
+
if (captureScreenshot && canScreenshot && helper.saveScreenshot) {
|
|
180
181
|
try {
|
|
181
182
|
const file = `${prefix}_screenshot.png`
|
|
182
183
|
await helper.saveScreenshot(path.join(dir, file), fullPage)
|
|
@@ -201,7 +202,8 @@ export async function captureSnapshot(helper, {
|
|
|
201
202
|
} catch {}
|
|
202
203
|
}
|
|
203
204
|
|
|
204
|
-
|
|
205
|
+
const canAria = !(helper.capabilities && helper.capabilities.aria === false)
|
|
206
|
+
if (captureARIA && canAria && helper.grabAriaSnapshot) {
|
|
205
207
|
try {
|
|
206
208
|
const aria = await helper.grabAriaSnapshot()
|
|
207
209
|
const file = `${prefix}_aria.txt`
|
package/package.json
CHANGED
package/typings/index.d.ts
CHANGED
|
@@ -465,6 +465,20 @@ declare namespace CodeceptJS {
|
|
|
465
465
|
y: number
|
|
466
466
|
}
|
|
467
467
|
|
|
468
|
+
/**
|
|
469
|
+
* Features an acting helper supports. Plugins read them to decide what to capture.
|
|
470
|
+
* `null` means the value is detected at runtime.
|
|
471
|
+
*/
|
|
472
|
+
interface HelperCapabilities {
|
|
473
|
+
screenshot: boolean | null
|
|
474
|
+
video: boolean | null
|
|
475
|
+
trace: boolean
|
|
476
|
+
aria: boolean
|
|
477
|
+
layout?: 'real' | 'none' | null
|
|
478
|
+
xpath?: 'native' | 'polyfill' | null
|
|
479
|
+
innerText?: 'native' | 'computed' | null
|
|
480
|
+
}
|
|
481
|
+
|
|
468
482
|
// Could get extended by user generated typings
|
|
469
483
|
interface Methods extends ActorStatic {}
|
|
470
484
|
interface I {}
|
|
@@ -1197,7 +1197,7 @@ declare namespace CodeceptJS {
|
|
|
1197
1197
|
* @property [headers = {}] - headers sent when connecting to the endpoint, e.g. for authenticated remote browsers.
|
|
1198
1198
|
* @property [input = auto] - how actions are dispatched: `cdp` (real mouse and keyboard events) or `synthetic` (DOM events). `auto` picks `cdp` when the browser renders layout.
|
|
1199
1199
|
* @property [xpathPolyfill = auto] - inject an XPath polyfill for browsers with incomplete XPath support. `auto` detects when it is needed.
|
|
1200
|
-
* @property [capabilities = {}] - browser capabilities (`layout`, `xpath`, `screenshot`, `innerText`) to use instead of detecting them at runtime.
|
|
1200
|
+
* @property [capabilities = {}] - browser capabilities (`layout`, `xpath`, `screenshot`, `video`, `innerText`, `aria`) to use instead of detecting them at runtime.
|
|
1201
1201
|
* @property [waitForTimeout = 5] - default timeout for wait* actions, in seconds.
|
|
1202
1202
|
* @property [waitForAction] - fixed delay after each action, in milliseconds. When unset, actions wait only for the navigation they trigger.
|
|
1203
1203
|
* @property [pollInterval = 25] - interval between checks while waiting, in milliseconds.
|
|
@@ -1249,6 +1249,7 @@ declare namespace CodeceptJS {
|
|
|
1249
1249
|
*/
|
|
1250
1250
|
class CDPBrowserTs {
|
|
1251
1251
|
constructor(config: CDPBrowserConfig);
|
|
1252
|
+
capabilities: CodeceptJS.HelperCapabilities;
|
|
1252
1253
|
/**
|
|
1253
1254
|
* Opts a subclass into managing its own browser server process. Called from a subclass
|
|
1254
1255
|
* constructor with the raw user config and a server descriptor:
|
|
@@ -1404,6 +1405,18 @@ declare namespace CodeceptJS {
|
|
|
1404
1405
|
* @returns source code
|
|
1405
1406
|
*/
|
|
1406
1407
|
grabSource(): Promise<string>;
|
|
1408
|
+
/**
|
|
1409
|
+
* Retrieves the ARIA snapshot of the page, or of an element, built from the browser accessibility tree.
|
|
1410
|
+
* The format matches Playwright's [ARIA snapshots](https://playwright.dev/docs/aria-snapshots).
|
|
1411
|
+
*
|
|
1412
|
+
* ```js
|
|
1413
|
+
* const snapshot = await I.grabAriaSnapshot();
|
|
1414
|
+
* const formSnapshot = await I.grabAriaSnapshot('#login-form');
|
|
1415
|
+
* ```
|
|
1416
|
+
* @param [locator = '//body'] - element located by CSS|XPath|strict locator. Defaults to body element.
|
|
1417
|
+
* @returns YAML representation of the accessibility tree
|
|
1418
|
+
*/
|
|
1419
|
+
grabAriaSnapshot(locator?: string | any): Promise<string>;
|
|
1407
1420
|
/**
|
|
1408
1421
|
* Checks that a page contains a visible text.
|
|
1409
1422
|
* Use context parameter to narrow down the search.
|
|
@@ -3438,7 +3451,7 @@ declare namespace CodeceptJS {
|
|
|
3438
3451
|
*
|
|
3439
3452
|
* ## Limitations
|
|
3440
3453
|
*
|
|
3441
|
-
* - No screenshots and no screencast: `saveScreenshot`
|
|
3454
|
+
* - No screenshots and no screencast: `saveScreenshot` and `saveElementScreenshot` do nothing (a debug message is printed).
|
|
3442
3455
|
* - No frames or popups.
|
|
3443
3456
|
* - No scrolling: scroll positions always stay at 0.
|
|
3444
3457
|
* - Computed styles cover what visibility checks need (`display`, `visibility`, `opacity`,
|
|
@@ -3924,6 +3937,7 @@ declare namespace CodeceptJS {
|
|
|
3924
3937
|
* ```
|
|
3925
3938
|
*/
|
|
3926
3939
|
class PlaywrightTs {
|
|
3940
|
+
capabilities: CodeceptJS.HelperCapabilities;
|
|
3927
3941
|
/**
|
|
3928
3942
|
* Use Playwright API inside a test.
|
|
3929
3943
|
*
|
|
@@ -6135,6 +6149,7 @@ declare namespace CodeceptJS {
|
|
|
6135
6149
|
* ## Methods
|
|
6136
6150
|
*/
|
|
6137
6151
|
class PuppeteerTs {
|
|
6152
|
+
capabilities: CodeceptJS.HelperCapabilities;
|
|
6138
6153
|
/**
|
|
6139
6154
|
* Use Puppeteer API inside a test.
|
|
6140
6155
|
*
|
|
@@ -7132,6 +7147,18 @@ declare namespace CodeceptJS {
|
|
|
7132
7147
|
* @returns source code
|
|
7133
7148
|
*/
|
|
7134
7149
|
grabSource(): Promise<string>;
|
|
7150
|
+
/**
|
|
7151
|
+
* Retrieves the ARIA snapshot of the page, or of an element, built from the browser accessibility tree.
|
|
7152
|
+
* The format matches Playwright's [ARIA snapshots](https://playwright.dev/docs/aria-snapshots).
|
|
7153
|
+
*
|
|
7154
|
+
* ```js
|
|
7155
|
+
* const snapshot = await I.grabAriaSnapshot();
|
|
7156
|
+
* const formSnapshot = await I.grabAriaSnapshot('#login-form');
|
|
7157
|
+
* ```
|
|
7158
|
+
* @param [locator = '//body'] - element located by CSS|XPath|strict locator. Defaults to body element.
|
|
7159
|
+
* @returns YAML representation of the accessibility tree
|
|
7160
|
+
*/
|
|
7161
|
+
grabAriaSnapshot(locator?: string | any): Promise<string>;
|
|
7135
7162
|
/**
|
|
7136
7163
|
* Get JS log from browser.
|
|
7137
7164
|
*
|
|
@@ -8587,6 +8614,7 @@ declare namespace CodeceptJS {
|
|
|
8587
8614
|
* ## Methods
|
|
8588
8615
|
*/
|
|
8589
8616
|
class WebDriverTs {
|
|
8617
|
+
capabilities: CodeceptJS.HelperCapabilities;
|
|
8590
8618
|
/**
|
|
8591
8619
|
* Use [webdriverio](https://webdriver.io/docs/api.html) API inside a test.
|
|
8592
8620
|
*
|
package/typings/types.d.ts
CHANGED
|
@@ -1225,7 +1225,7 @@ declare namespace CodeceptJS {
|
|
|
1225
1225
|
* @property [headers = {}] - headers sent when connecting to the endpoint, e.g. for authenticated remote browsers.
|
|
1226
1226
|
* @property [input = auto] - how actions are dispatched: `cdp` (real mouse and keyboard events) or `synthetic` (DOM events). `auto` picks `cdp` when the browser renders layout.
|
|
1227
1227
|
* @property [xpathPolyfill = auto] - inject an XPath polyfill for browsers with incomplete XPath support. `auto` detects when it is needed.
|
|
1228
|
-
* @property [capabilities = {}] - browser capabilities (`layout`, `xpath`, `screenshot`, `innerText`) to use instead of detecting them at runtime.
|
|
1228
|
+
* @property [capabilities = {}] - browser capabilities (`layout`, `xpath`, `screenshot`, `video`, `innerText`, `aria`) to use instead of detecting them at runtime.
|
|
1229
1229
|
* @property [waitForTimeout = 5] - default timeout for wait* actions, in seconds.
|
|
1230
1230
|
* @property [waitForAction] - fixed delay after each action, in milliseconds. When unset, actions wait only for the navigation they trigger.
|
|
1231
1231
|
* @property [pollInterval = 25] - interval between checks while waiting, in milliseconds.
|
|
@@ -1277,6 +1277,7 @@ declare namespace CodeceptJS {
|
|
|
1277
1277
|
*/
|
|
1278
1278
|
class CDPBrowser {
|
|
1279
1279
|
constructor(config: CDPBrowserConfig);
|
|
1280
|
+
capabilities: CodeceptJS.HelperCapabilities;
|
|
1280
1281
|
/**
|
|
1281
1282
|
* Opts a subclass into managing its own browser server process. Called from a subclass
|
|
1282
1283
|
* constructor with the raw user config and a server descriptor:
|
|
@@ -1435,6 +1436,18 @@ declare namespace CodeceptJS {
|
|
|
1435
1436
|
* @returns source code
|
|
1436
1437
|
*/
|
|
1437
1438
|
grabSource(): Promise<string>;
|
|
1439
|
+
/**
|
|
1440
|
+
* Retrieves the ARIA snapshot of the page, or of an element, built from the browser accessibility tree.
|
|
1441
|
+
* The format matches Playwright's [ARIA snapshots](https://playwright.dev/docs/aria-snapshots).
|
|
1442
|
+
*
|
|
1443
|
+
* ```js
|
|
1444
|
+
* const snapshot = await I.grabAriaSnapshot();
|
|
1445
|
+
* const formSnapshot = await I.grabAriaSnapshot('#login-form');
|
|
1446
|
+
* ```
|
|
1447
|
+
* @param [locator = '//body'] - element located by CSS|XPath|strict locator. Defaults to body element.
|
|
1448
|
+
* @returns YAML representation of the accessibility tree
|
|
1449
|
+
*/
|
|
1450
|
+
grabAriaSnapshot(locator?: string | any): Promise<string>;
|
|
1438
1451
|
/**
|
|
1439
1452
|
* Checks that a page contains a visible text.
|
|
1440
1453
|
* Use context parameter to narrow down the search.
|
|
@@ -3541,7 +3554,7 @@ declare namespace CodeceptJS {
|
|
|
3541
3554
|
*
|
|
3542
3555
|
* ## Limitations
|
|
3543
3556
|
*
|
|
3544
|
-
* - No screenshots and no screencast: `saveScreenshot`
|
|
3557
|
+
* - No screenshots and no screencast: `saveScreenshot` and `saveElementScreenshot` do nothing (a debug message is printed).
|
|
3545
3558
|
* - No frames or popups.
|
|
3546
3559
|
* - No scrolling: scroll positions always stay at 0.
|
|
3547
3560
|
* - Computed styles cover what visibility checks need (`display`, `visibility`, `opacity`,
|
|
@@ -4027,6 +4040,7 @@ declare namespace CodeceptJS {
|
|
|
4027
4040
|
* ```
|
|
4028
4041
|
*/
|
|
4029
4042
|
class Playwright {
|
|
4043
|
+
capabilities: CodeceptJS.HelperCapabilities;
|
|
4030
4044
|
/**
|
|
4031
4045
|
* Use Playwright API inside a test.
|
|
4032
4046
|
*
|
|
@@ -6326,6 +6340,7 @@ declare namespace CodeceptJS {
|
|
|
6326
6340
|
* ## Methods
|
|
6327
6341
|
*/
|
|
6328
6342
|
class Puppeteer {
|
|
6343
|
+
capabilities: CodeceptJS.HelperCapabilities;
|
|
6329
6344
|
/**
|
|
6330
6345
|
* Use Puppeteer API inside a test.
|
|
6331
6346
|
*
|
|
@@ -7370,6 +7385,18 @@ declare namespace CodeceptJS {
|
|
|
7370
7385
|
* @returns source code
|
|
7371
7386
|
*/
|
|
7372
7387
|
grabSource(): Promise<string>;
|
|
7388
|
+
/**
|
|
7389
|
+
* Retrieves the ARIA snapshot of the page, or of an element, built from the browser accessibility tree.
|
|
7390
|
+
* The format matches Playwright's [ARIA snapshots](https://playwright.dev/docs/aria-snapshots).
|
|
7391
|
+
*
|
|
7392
|
+
* ```js
|
|
7393
|
+
* const snapshot = await I.grabAriaSnapshot();
|
|
7394
|
+
* const formSnapshot = await I.grabAriaSnapshot('#login-form');
|
|
7395
|
+
* ```
|
|
7396
|
+
* @param [locator = '//body'] - element located by CSS|XPath|strict locator. Defaults to body element.
|
|
7397
|
+
* @returns YAML representation of the accessibility tree
|
|
7398
|
+
*/
|
|
7399
|
+
grabAriaSnapshot(locator?: string | any): Promise<string>;
|
|
7373
7400
|
/**
|
|
7374
7401
|
* Get JS log from browser.
|
|
7375
7402
|
*
|
|
@@ -8868,6 +8895,7 @@ declare namespace CodeceptJS {
|
|
|
8868
8895
|
* ## Methods
|
|
8869
8896
|
*/
|
|
8870
8897
|
class WebDriver {
|
|
8898
|
+
capabilities: CodeceptJS.HelperCapabilities;
|
|
8871
8899
|
/**
|
|
8872
8900
|
* Use [webdriverio](https://webdriver.io/docs/api.html) API inside a test.
|
|
8873
8901
|
*
|