codeceptjs 4.2.0-beta.1 → 4.2.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/advanced.md +1 -1
- package/docs/helpers/Playwright.md +5 -2
- package/lib/data/context.js +4 -6
- package/lib/helper/Playwright.js +22 -18
- package/lib/helper/Puppeteer.js +9 -13
- package/lib/helper/WebDriver.js +8 -22
- package/lib/helper/errors/MultipleElementsFound.js +60 -3
- package/lib/helper/extras/PlaywrightLocator.js +2 -2
- package/lib/locator.js +7 -2
- package/lib/mocha/asyncWrapper.js +7 -1
- package/lib/step/config.js +1 -0
- package/lib/store.js +6 -0
- package/package.json +2 -2
- package/typings/promiseBasedTypes.d.ts +2 -0
- package/typings/types.d.ts +8 -0
package/docs/advanced.md
CHANGED
|
@@ -79,7 +79,7 @@ Data(function*() {
|
|
|
79
79
|
}).Scenario() // ...
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
Objects in a data set are serialized as JSON in the test name.
|
|
83
83
|
|
|
84
84
|
|
|
85
85
|
## Debug
|
|
@@ -78,8 +78,9 @@ Type: [object][6]
|
|
|
78
78
|
* `ignoreHTTPSErrors` **[boolean][27]?** Allows access to untrustworthy pages, e.g. to a page with an expired certificate. Default value is `false`
|
|
79
79
|
* `bypassCSP` **[boolean][27]?** bypass Content Security Policy or CSP
|
|
80
80
|
* `highlightElement` **[boolean][27]?** highlight the interacting elements. Default: false. Note: only activate under verbose mode (--verbose).
|
|
81
|
+
* `visibleLocator` **[boolean][27]?** append [`visible()`][49] to locators, so only visible elements are matched. Requires Playwright 1.63 or newer. Switch it off for a single step with `stepOpts({ visibleLocator: false })`. Not applied to `dragAndDrop`, which passes selectors to Playwright directly, nor to `seeElementInDOM`, `dontSeeElementInDOM` and `seeNumberOfElements`, which check the DOM regardless of visibility. When enabled, a locator matching only hidden elements fails as "element not found" instead of timing out on actionability, `strict` mode ignores hidden duplicates, and elements hidden by CSS (like a custom checkbox built on a visually hidden `input`) are no longer found.
|
|
81
82
|
* `recordHar` **[object][6]?** record HAR and will be saved to `output/har`. See more of [HAR options][3].
|
|
82
|
-
* `testIdAttribute` **[string][9]?** locate elements based on the testIdAttribute. See more of [locate by test id][
|
|
83
|
+
* `testIdAttribute` **[string][9]?** locate elements based on the testIdAttribute. See more of [locate by test id][50].
|
|
83
84
|
* `storageState` **([string][9] | [object][6])?** Playwright storage state (path to JSON file or object)
|
|
84
85
|
passed directly to `browser.newContext`.
|
|
85
86
|
If a Scenario is declared with a `cookies` option (e.g. `Scenario('name', { cookies: [...] }, fn)`),
|
|
@@ -2967,4 +2968,6 @@ Returns **void** automatically synchronized promise through #recorder
|
|
|
2967
2968
|
|
|
2968
2969
|
[48]: https://playwright.dev/docs/api/class-consolemessage#console-message-type
|
|
2969
2970
|
|
|
2970
|
-
[49]: https://playwright.dev/docs/
|
|
2971
|
+
[49]: https://playwright.dev/docs/api/class-locator#locator-visible
|
|
2972
|
+
|
|
2973
|
+
[50]: https://playwright.dev/docs/locators#locate-by-test-id
|
package/lib/data/context.js
CHANGED
|
@@ -72,13 +72,11 @@ function replaceTitle(title, dataRow) {
|
|
|
72
72
|
return `${title} | ${dataRow.data.getMasked()}`
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (Object.prototype.toString.call(dataRow.data) === Object().toString() && dataRow.data.toString() !== Object().toString()) {
|
|
78
|
-
return `${title} | ${dataRow.data}`
|
|
79
|
-
}
|
|
75
|
+
return `${title} | ${JSON.stringify(dataRow.data, maskSecret)}`
|
|
76
|
+
}
|
|
80
77
|
|
|
81
|
-
|
|
78
|
+
function maskSecret(key, value) {
|
|
79
|
+
return typeof value?.getMasked === 'function' ? value.getMasked() : value
|
|
82
80
|
}
|
|
83
81
|
|
|
84
82
|
function isTableDataRow(row) {
|
package/lib/helper/Playwright.js
CHANGED
|
@@ -50,6 +50,7 @@ let defaultSelectorEnginesInitialized = false
|
|
|
50
50
|
const popupStore = new Popup()
|
|
51
51
|
const consoleLogStore = new Console()
|
|
52
52
|
const availableBrowsers = ['chromium', 'webkit', 'firefox', 'electron']
|
|
53
|
+
const domPresenceSteps = ['seeElementInDOM', 'dontSeeElementInDOM', 'seeNumberOfElements']
|
|
53
54
|
const checkableRoles = ['checkbox', 'radio', 'switch']
|
|
54
55
|
|
|
55
56
|
import { setRestartStrategy, restartsSession, restartsContext, restartsBrowser } from './extras/PlaywrightRestartOpts.js'
|
|
@@ -102,6 +103,7 @@ const pathSeparator = path.sep
|
|
|
102
103
|
* @prop {boolean} [ignoreHTTPSErrors] - Allows access to untrustworthy pages, e.g. to a page with an expired certificate. Default value is `false`
|
|
103
104
|
* @prop {boolean} [bypassCSP] - bypass Content Security Policy or CSP
|
|
104
105
|
* @prop {boolean} [highlightElement] - highlight the interacting elements. Default: false. Note: only activate under verbose mode (--verbose).
|
|
106
|
+
* @prop {boolean} [visibleLocator=false] - append [`visible()`](https://playwright.dev/docs/api/class-locator#locator-visible) to locators, so only visible elements are matched. Requires Playwright 1.63 or newer. Switch it off for a single step with `stepOpts({ visibleLocator: false })`. Not applied to `dragAndDrop`, which passes selectors to Playwright directly, nor to `seeElementInDOM`, `dontSeeElementInDOM` and `seeNumberOfElements`, which check the DOM regardless of visibility. When enabled, a locator matching only hidden elements fails as "element not found" instead of timing out on actionability, `strict` mode ignores hidden duplicates, and elements hidden by CSS (like a custom checkbox built on a visually hidden `input`) are no longer found.
|
|
105
107
|
* @prop {object} [recordHar] - record HAR and will be saved to `output/har`. See more of [HAR options](https://playwright.dev/docs/api/class-browser#browser-new-context-option-record-har).
|
|
106
108
|
* @prop {string} [testIdAttribute=data-testid] - locate elements based on the testIdAttribute. See more of [locate by test id](https://playwright.dev/docs/locators#locate-by-test-id).
|
|
107
109
|
* @prop {string|object} [storageState] - Playwright storage state (path to JSON file or object)
|
|
@@ -399,6 +401,7 @@ class Playwright extends Helper {
|
|
|
399
401
|
storageState: undefined,
|
|
400
402
|
onResponse: null,
|
|
401
403
|
strict: false,
|
|
404
|
+
visibleLocator: false,
|
|
402
405
|
}
|
|
403
406
|
|
|
404
407
|
process.env.testIdAttribute = 'data-testid'
|
|
@@ -555,6 +558,10 @@ class Playwright extends Helper {
|
|
|
555
558
|
}
|
|
556
559
|
}
|
|
557
560
|
|
|
561
|
+
_beforeStep(step) {
|
|
562
|
+
store.visibleLocator = step.opts?.visibleLocator ?? (this.options.visibleLocator && !domPresenceSteps.includes(step.helperMethod))
|
|
563
|
+
}
|
|
564
|
+
|
|
558
565
|
async _before(test) {
|
|
559
566
|
// Skip browser operations in dry-run mode (used by check command)
|
|
560
567
|
if (store.dryRun) {
|
|
@@ -4197,6 +4204,14 @@ export function buildLocatorString(locator) {
|
|
|
4197
4204
|
return locator.simplify()
|
|
4198
4205
|
}
|
|
4199
4206
|
|
|
4207
|
+
function withVisibleLocator(locator) {
|
|
4208
|
+
if (!store.visibleLocator) return locator
|
|
4209
|
+
if (typeof locator.visible !== 'function') {
|
|
4210
|
+
throw new Error('visibleLocator option requires Playwright 1.63 or newer. Upgrade the playwright package or disable visibleLocator in helper config')
|
|
4211
|
+
}
|
|
4212
|
+
return locator.visible()
|
|
4213
|
+
}
|
|
4214
|
+
|
|
4200
4215
|
/**
|
|
4201
4216
|
* Handles role locator objects by converting them to Playwright's getByRole() API
|
|
4202
4217
|
* Accepts both raw objects ({role: 'button', text: 'Submit'}) and Locator-wrapped role objects.
|
|
@@ -4212,7 +4227,7 @@ async function handleRoleLocator(context, locator) {
|
|
|
4212
4227
|
if (roleObj.name) options.name = roleObj.name
|
|
4213
4228
|
if (roleObj.exact !== undefined) options.exact = roleObj.exact
|
|
4214
4229
|
|
|
4215
|
-
return context.getByRole(roleObj.role, Object.keys(options).length > 0 ? options : undefined).all()
|
|
4230
|
+
return withVisibleLocator(context.getByRole(roleObj.role, Object.keys(options).length > 0 ? options : undefined)).all()
|
|
4216
4231
|
}
|
|
4217
4232
|
|
|
4218
4233
|
async function findByRole(context, locator) {
|
|
@@ -4220,13 +4235,13 @@ async function findByRole(context, locator) {
|
|
|
4220
4235
|
const options = {}
|
|
4221
4236
|
if (locator.name) options.name = locator.name
|
|
4222
4237
|
if (locator.exact !== undefined) options.exact = locator.exact
|
|
4223
|
-
return context.getByRole(locator.role, Object.keys(options).length > 0 ? options : undefined).all()
|
|
4238
|
+
return withVisibleLocator(context.getByRole(locator.role, Object.keys(options).length > 0 ? options : undefined)).all()
|
|
4224
4239
|
}
|
|
4225
4240
|
|
|
4226
4241
|
async function findElements(matcher, locator) {
|
|
4227
4242
|
const isPwLocator = locator.type === 'pw' || (locator.locator && locator.locator.pw) || locator.pw
|
|
4228
4243
|
|
|
4229
|
-
if (isPwLocator) return findByPlaywrightLocator.call(this, matcher, locator)
|
|
4244
|
+
if (isPwLocator) return withVisibleLocator(findByPlaywrightLocator.call(this, matcher, locator)).all()
|
|
4230
4245
|
|
|
4231
4246
|
// Handle role locators with text/exact options (e.g., {role: 'button', text: 'Submit', exact: true})
|
|
4232
4247
|
const roleElements = await handleRoleLocator(matcher, locator)
|
|
@@ -4236,11 +4251,11 @@ async function findElements(matcher, locator) {
|
|
|
4236
4251
|
|
|
4237
4252
|
const locatorString = buildLocatorString(locator)
|
|
4238
4253
|
|
|
4239
|
-
return matcher.locator(locatorString).all()
|
|
4254
|
+
return withVisibleLocator(matcher.locator(locatorString)).all()
|
|
4240
4255
|
}
|
|
4241
4256
|
|
|
4242
4257
|
async function findElement(matcher, locator) {
|
|
4243
|
-
if (locator.pw) return findByPlaywrightLocator.call(this, matcher, locator)
|
|
4258
|
+
if (locator.pw) return findByPlaywrightLocator.call(this, matcher, locator).first()
|
|
4244
4259
|
|
|
4245
4260
|
locator = new Locator(locator, 'css')
|
|
4246
4261
|
|
|
@@ -4313,14 +4328,14 @@ async function findClickable(matcher, locator) {
|
|
|
4313
4328
|
const literal = xpathLocator.literal(matchedLocator.value)
|
|
4314
4329
|
|
|
4315
4330
|
try {
|
|
4316
|
-
els = await matcher.getByRole('button', { name: matchedLocator.value }).all()
|
|
4331
|
+
els = await withVisibleLocator(matcher.getByRole('button', { name: matchedLocator.value })).all()
|
|
4317
4332
|
if (els.length) return els
|
|
4318
4333
|
} catch (err) {
|
|
4319
4334
|
// getByRole not supported or failed
|
|
4320
4335
|
}
|
|
4321
4336
|
|
|
4322
4337
|
try {
|
|
4323
|
-
els = await matcher.getByRole('link', { name: matchedLocator.value }).all()
|
|
4338
|
+
els = await withVisibleLocator(matcher.getByRole('link', { name: matchedLocator.value })).all()
|
|
4324
4339
|
if (els.length) return els
|
|
4325
4340
|
} catch (err) {
|
|
4326
4341
|
// getByRole not supported or failed
|
|
@@ -4389,17 +4404,6 @@ async function findCheckable(locator, context) {
|
|
|
4389
4404
|
return findElements.call(this, contextEl, matchedLocator)
|
|
4390
4405
|
}
|
|
4391
4406
|
|
|
4392
|
-
for (const exact of [true, false]) {
|
|
4393
|
-
for (const role of checkableRoles) {
|
|
4394
|
-
try {
|
|
4395
|
-
const roleEls = await contextEl.getByRole(role, { name: matchedLocator.value, exact }).all()
|
|
4396
|
-
if (roleEls.length) return roleEls
|
|
4397
|
-
} catch (err) {
|
|
4398
|
-
// getByRole not supported or failed
|
|
4399
|
-
}
|
|
4400
|
-
}
|
|
4401
|
-
}
|
|
4402
|
-
|
|
4403
4407
|
const literal = xpathLocator.literal(matchedLocator.value)
|
|
4404
4408
|
let els = await findElements.call(this, contextEl, Locator.checkable.byText(literal))
|
|
4405
4409
|
if (els.length) {
|
package/lib/helper/Puppeteer.js
CHANGED
|
@@ -64,7 +64,6 @@ function wrapError(e) {
|
|
|
64
64
|
let perfTiming
|
|
65
65
|
const popupStore = new Popup()
|
|
66
66
|
const consoleLogStore = new Console()
|
|
67
|
-
const checkableRoles = ['checkbox', 'radio', 'switch']
|
|
68
67
|
|
|
69
68
|
/**
|
|
70
69
|
* ## Configuration
|
|
@@ -3196,19 +3195,8 @@ async function findCheckable(locator, context) {
|
|
|
3196
3195
|
return findElements.call(this, contextEl, matchedLocator)
|
|
3197
3196
|
}
|
|
3198
3197
|
|
|
3199
|
-
// Try ARIA selector for accessible name
|
|
3200
|
-
let els
|
|
3201
|
-
for (const role of checkableRoles) {
|
|
3202
|
-
try {
|
|
3203
|
-
els = await contextEl.$$(`::-p-aria([name="${matchedLocator.value}"][role="${role}"])`)
|
|
3204
|
-
if (els.length) return els
|
|
3205
|
-
} catch (err) {
|
|
3206
|
-
// ARIA selector not supported or failed
|
|
3207
|
-
}
|
|
3208
|
-
}
|
|
3209
|
-
|
|
3210
3198
|
const literal = xpathLocator.literal(matchedLocator.value)
|
|
3211
|
-
els = await findElements.call(this, contextEl, Locator.checkable.byText(literal))
|
|
3199
|
+
let els = await findElements.call(this, contextEl, Locator.checkable.byText(literal))
|
|
3212
3200
|
if (els.length) {
|
|
3213
3201
|
return els
|
|
3214
3202
|
}
|
|
@@ -3217,6 +3205,14 @@ async function findCheckable(locator, context) {
|
|
|
3217
3205
|
return els
|
|
3218
3206
|
}
|
|
3219
3207
|
|
|
3208
|
+
// Try ARIA selector for accessible name
|
|
3209
|
+
try {
|
|
3210
|
+
els = await contextEl.$$(`::-p-aria(${matchedLocator.value})`)
|
|
3211
|
+
if (els.length) return els
|
|
3212
|
+
} catch (err) {
|
|
3213
|
+
// ARIA selector not supported or failed
|
|
3214
|
+
}
|
|
3215
|
+
|
|
3220
3216
|
return findElements.call(this, contextEl, matchedLocator.value)
|
|
3221
3217
|
}
|
|
3222
3218
|
|
package/lib/helper/WebDriver.js
CHANGED
|
@@ -3248,14 +3248,6 @@ async function findCheckable(locator, locateFn) {
|
|
|
3248
3248
|
if (locator.isRole()) return locateFn(locator, true)
|
|
3249
3249
|
if (!locator.isFuzzy()) return locateFn(locator, true)
|
|
3250
3250
|
|
|
3251
|
-
// Try ARIA selector for accessible name
|
|
3252
|
-
try {
|
|
3253
|
-
els = await keepCheckable.call(this, await locateFn(`aria/${locator.value}`))
|
|
3254
|
-
if (els.length) return els
|
|
3255
|
-
} catch (e) {
|
|
3256
|
-
// ARIA selector not supported or failed
|
|
3257
|
-
}
|
|
3258
|
-
|
|
3259
3251
|
const literal = xpathLocator.literal(locator.value)
|
|
3260
3252
|
els = await locateFn(Locator.checkable.byText(literal))
|
|
3261
3253
|
if (els.length) return els
|
|
@@ -3263,23 +3255,17 @@ async function findCheckable(locator, locateFn) {
|
|
|
3263
3255
|
els = await locateFn(Locator.checkable.byName(literal))
|
|
3264
3256
|
if (els.length) return els
|
|
3265
3257
|
|
|
3258
|
+
// Try ARIA selector for accessible name
|
|
3259
|
+
try {
|
|
3260
|
+
els = await locateFn(`aria/${locator.value}`)
|
|
3261
|
+
if (els.length) return els
|
|
3262
|
+
} catch (e) {
|
|
3263
|
+
// ARIA selector not supported or failed
|
|
3264
|
+
}
|
|
3265
|
+
|
|
3266
3266
|
return await locateFn(locator.value) // by css or xpath
|
|
3267
3267
|
}
|
|
3268
3268
|
|
|
3269
|
-
async function keepCheckable(els) {
|
|
3270
|
-
if (!els || !els.length) return []
|
|
3271
|
-
|
|
3272
|
-
const checkable = await this.browser.execute(function () {
|
|
3273
|
-
return Array.prototype.slice.call(arguments).map(function (el) {
|
|
3274
|
-
if (!el) return false
|
|
3275
|
-
const role = el.getAttribute('role')
|
|
3276
|
-
if (role) return ['checkbox', 'radio', 'switch'].indexOf(role) > -1
|
|
3277
|
-
return el.tagName === 'INPUT' && (el.type === 'checkbox' || el.type === 'radio')
|
|
3278
|
-
})
|
|
3279
|
-
}, ...els)
|
|
3280
|
-
|
|
3281
|
-
return els.filter((el, index) => checkable[index])
|
|
3282
|
-
}
|
|
3283
3269
|
|
|
3284
3270
|
function withStrictLocator(locator) {
|
|
3285
3271
|
locator = new Locator(locator)
|
|
@@ -1,5 +1,60 @@
|
|
|
1
1
|
import Locator from '../../locator.js'
|
|
2
2
|
|
|
3
|
+
export function splitXPath(xpath) {
|
|
4
|
+
if (typeof xpath !== 'string' || xpath.length === 0) return []
|
|
5
|
+
const withoutRoot = xpath.startsWith('//') ? xpath.slice(1) : xpath
|
|
6
|
+
return withoutRoot.split('/').filter(Boolean)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function isAncestorXPath(ancestor, descendant) {
|
|
10
|
+
if (!ancestor || !descendant || ancestor === descendant) return false
|
|
11
|
+
const ancestorSegments = splitXPath(ancestor)
|
|
12
|
+
const descendantSegments = splitXPath(descendant)
|
|
13
|
+
if (ancestorSegments.length === 0 || ancestorSegments.length >= descendantSegments.length) return false
|
|
14
|
+
return ancestorSegments.every((segment, index) => segment === descendantSegments[index])
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function computeParents(entries) {
|
|
18
|
+
const parents = new Array(entries.length).fill(-1)
|
|
19
|
+
const stack = []
|
|
20
|
+
for (let i = 0; i < entries.length; i++) {
|
|
21
|
+
const xpath = entries[i].xpath
|
|
22
|
+
if (!xpath) continue
|
|
23
|
+
while (stack.length > 0 && !isAncestorXPath(entries[stack[stack.length - 1]].xpath, xpath)) {
|
|
24
|
+
stack.pop()
|
|
25
|
+
}
|
|
26
|
+
parents[i] = stack.length > 0 ? stack[stack.length - 1] : -1
|
|
27
|
+
stack.push(i)
|
|
28
|
+
}
|
|
29
|
+
return parents
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function computeDepths(entries) {
|
|
33
|
+
const parents = computeParents(entries)
|
|
34
|
+
return parents.map((parent, i) => {
|
|
35
|
+
if (!entries[i].xpath) return 0
|
|
36
|
+
let depth = 0
|
|
37
|
+
let current = parent
|
|
38
|
+
while (current !== -1) {
|
|
39
|
+
depth++
|
|
40
|
+
current = parents[current]
|
|
41
|
+
}
|
|
42
|
+
return depth
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function formatTree(entries, depths, parents) {
|
|
47
|
+
return entries.map((entry, i) => {
|
|
48
|
+
const pad = ' '.repeat(depths[i] || 0)
|
|
49
|
+
if (entry.error) {
|
|
50
|
+
return `${pad} ${entry.index}. [Unable to get element info: ${entry.error}]`
|
|
51
|
+
}
|
|
52
|
+
const parentPos = parents ? parents[i] : -1
|
|
53
|
+
const nesting = parentPos !== undefined && parentPos !== -1 ? ` (inside ${entries[parentPos].index}.)` : ''
|
|
54
|
+
return `${pad} ${entry.index}.${nesting} > ${entry.xpath}\n${pad} ${entry.html}`
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
3
58
|
class MultipleElementsFound extends Error {
|
|
4
59
|
constructor(locator, webElements) {
|
|
5
60
|
const locatorStr = (typeof locator === 'object' && !(locator instanceof Locator))
|
|
@@ -17,7 +72,7 @@ class MultipleElementsFound extends Error {
|
|
|
17
72
|
if (this._detailsFetched) return
|
|
18
73
|
|
|
19
74
|
try {
|
|
20
|
-
const
|
|
75
|
+
const entries = []
|
|
21
76
|
const maxToShow = Math.min(this.count, 10)
|
|
22
77
|
|
|
23
78
|
for (let i = 0; i < maxToShow; i++) {
|
|
@@ -25,12 +80,14 @@ class MultipleElementsFound extends Error {
|
|
|
25
80
|
try {
|
|
26
81
|
const xpath = await webEl.toAbsoluteXPath()
|
|
27
82
|
const html = await webEl.toSimplifiedHTML()
|
|
28
|
-
|
|
83
|
+
entries.push({ index: i + 1, xpath, html })
|
|
29
84
|
} catch (err) {
|
|
30
|
-
|
|
85
|
+
entries.push({ index: i + 1, error: err.message })
|
|
31
86
|
}
|
|
32
87
|
}
|
|
33
88
|
|
|
89
|
+
const items = formatTree(entries, computeDepths(entries), computeParents(entries))
|
|
90
|
+
|
|
34
91
|
if (this.count > 10) {
|
|
35
92
|
items.push(` ... and ${this.count - 10} more`)
|
|
36
93
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
function findByPlaywrightLocator(matcher, locator) {
|
|
2
2
|
const pwLocator = locator.locator || locator
|
|
3
3
|
if (pwLocator && pwLocator.toString && pwLocator.toString().includes(process.env.testIdAttribute)) {
|
|
4
4
|
return matcher.getByTestId(pwLocator.pw.value.split('=')[1])
|
|
5
5
|
}
|
|
6
6
|
const pwValue = typeof pwLocator.pw === 'string' ? pwLocator.pw : pwLocator.pw
|
|
7
|
-
return matcher.locator(pwValue)
|
|
7
|
+
return matcher.locator(pwValue)
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export { findByPlaywrightLocator }
|
package/lib/locator.js
CHANGED
|
@@ -649,6 +649,9 @@ Locator.field = {
|
|
|
649
649
|
]),
|
|
650
650
|
}
|
|
651
651
|
|
|
652
|
+
const checkable = `self::input[@type = 'checkbox' or @type = 'radio'] or @role = 'checkbox' or @role = 'radio' or @role = 'switch'`
|
|
653
|
+
const visibleCheckable = `.//*[${checkable}][not(@aria-hidden = 'true')]`
|
|
654
|
+
|
|
652
655
|
Locator.checkable = {
|
|
653
656
|
/**
|
|
654
657
|
* @param {string} literal
|
|
@@ -656,8 +659,10 @@ Locator.checkable = {
|
|
|
656
659
|
*/
|
|
657
660
|
byText: literal =>
|
|
658
661
|
xpathLocator.combine([
|
|
659
|
-
|
|
660
|
-
`.//label[contains(normalize-space(string(.)), ${literal})]
|
|
662
|
+
`${visibleCheckable}[(@id = //label[@for][contains(normalize-space(string(.)), ${literal})]/@for) or @placeholder = ${literal}]`,
|
|
663
|
+
`.//label[contains(normalize-space(string(.)), ${literal})]//*[${checkable}][not(@aria-hidden = 'true')]`,
|
|
664
|
+
`${visibleCheckable}[@aria-labelledby = //*[@id][contains(normalize-space(string(.)), ${literal})]/@id]`,
|
|
665
|
+
`${visibleCheckable}[@aria-label = ${literal}]`,
|
|
661
666
|
]),
|
|
662
667
|
|
|
663
668
|
/**
|
|
@@ -4,7 +4,7 @@ import recorder from '../recorder.js'
|
|
|
4
4
|
import assertThrown from '../assert/throws.js'
|
|
5
5
|
import { ucfirst, isAsyncFunction } from '../utils.js'
|
|
6
6
|
import { getInjectedArguments } from './inject.js'
|
|
7
|
-
import { fireHook } from './hooks.js'
|
|
7
|
+
import { fireHook, BeforeSuiteHook, AfterSuiteHook } from './hooks.js'
|
|
8
8
|
|
|
9
9
|
const injectHook = function (inject, suite) {
|
|
10
10
|
try {
|
|
@@ -232,6 +232,10 @@ export function suiteSetup(suite) {
|
|
|
232
232
|
|
|
233
233
|
// Set up error handler for suite setup
|
|
234
234
|
recorder.errHandler(err => {
|
|
235
|
+
// A helper's `_beforeSuite()` runs through this hook, not through the
|
|
236
|
+
// `injected()` wrapper, so nothing here used to emit `hook.failed` and
|
|
237
|
+
// reporters listening for it never saw the failure. (#5660)
|
|
238
|
+
event.emit(event.hook.failed, new BeforeSuiteHook(suite, err))
|
|
235
239
|
doneFn(err)
|
|
236
240
|
})
|
|
237
241
|
|
|
@@ -254,6 +258,8 @@ export function suiteTeardown(suite) {
|
|
|
254
258
|
|
|
255
259
|
// Set up error handler for suite teardown
|
|
256
260
|
recorder.errHandler(err => {
|
|
261
|
+
// Same for a helper's `_afterSuite()`. (#5660)
|
|
262
|
+
event.emit(event.hook.failed, new AfterSuiteHook(suite, err))
|
|
257
263
|
doneFn(err)
|
|
258
264
|
})
|
|
259
265
|
|
package/lib/step/config.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* @property {boolean} [exact] - Enable strict mode for this step. Throws if multiple elements match.
|
|
5
5
|
* @property {boolean} [strictMode] - Alias for exact.
|
|
6
6
|
* @property {boolean} [ignoreCase] - Perform case-insensitive text matching.
|
|
7
|
+
* @property {boolean} [visibleLocator] - Match only visible elements. Overrides the Playwright helper `visibleLocator` config option for this step.
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
/**
|
package/lib/store.js
CHANGED
|
@@ -93,6 +93,12 @@ const store = {
|
|
|
93
93
|
/** @type {CodeceptJS.Suite | null} */
|
|
94
94
|
currentSuite: null,
|
|
95
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Locators match only visible elements, resolved per step
|
|
98
|
+
* @type {boolean}
|
|
99
|
+
*/
|
|
100
|
+
visibleLocator: false,
|
|
101
|
+
|
|
96
102
|
/** @type {Map<string, string> | null} */
|
|
97
103
|
tsFileMapping: null,
|
|
98
104
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codeceptjs",
|
|
3
|
-
"version": "4.2.0-beta.
|
|
3
|
+
"version": "4.2.0-beta.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Supercharged End 2 End Testing Framework for NodeJS",
|
|
6
6
|
"keywords": [
|
|
@@ -177,7 +177,7 @@
|
|
|
177
177
|
"jsdoc": "^3.6.11",
|
|
178
178
|
"jsdoc-typeof-plugin": "1.0.0",
|
|
179
179
|
"json-server": "0.17.4",
|
|
180
|
-
"playwright": "^1.
|
|
180
|
+
"playwright": "^1.63.0",
|
|
181
181
|
"prettier": "^3.3.2",
|
|
182
182
|
"puppeteer": "24.36.0",
|
|
183
183
|
"qrcode-terminal": "0.12.0",
|
|
@@ -3607,6 +3607,7 @@ declare namespace CodeceptJS {
|
|
|
3607
3607
|
* @property [ignoreHTTPSErrors] - Allows access to untrustworthy pages, e.g. to a page with an expired certificate. Default value is `false`
|
|
3608
3608
|
* @property [bypassCSP] - bypass Content Security Policy or CSP
|
|
3609
3609
|
* @property [highlightElement] - highlight the interacting elements. Default: false. Note: only activate under verbose mode (--verbose).
|
|
3610
|
+
* @property [visibleLocator = false] - append [`visible()`](https://playwright.dev/docs/api/class-locator#locator-visible) to locators, so only visible elements are matched. Requires Playwright 1.63 or newer. Switch it off for a single step with `stepOpts({ visibleLocator: false })`. Not applied to `dragAndDrop`, which passes selectors to Playwright directly, nor to `seeElementInDOM`, `dontSeeElementInDOM` and `seeNumberOfElements`, which check the DOM regardless of visibility. When enabled, a locator matching only hidden elements fails as "element not found" instead of timing out on actionability, `strict` mode ignores hidden duplicates, and elements hidden by CSS (like a custom checkbox built on a visually hidden `input`) are no longer found.
|
|
3610
3611
|
* @property [recordHar] - record HAR and will be saved to `output/har`. See more of [HAR options](https://playwright.dev/docs/api/class-browser#browser-new-context-option-record-har).
|
|
3611
3612
|
* @property [testIdAttribute = data-testid] - locate elements based on the testIdAttribute. See more of [locate by test id](https://playwright.dev/docs/locators#locate-by-test-id).
|
|
3612
3613
|
* @property [storageState] - Playwright storage state (path to JSON file or object)
|
|
@@ -3651,6 +3652,7 @@ declare namespace CodeceptJS {
|
|
|
3651
3652
|
ignoreHTTPSErrors?: boolean;
|
|
3652
3653
|
bypassCSP?: boolean;
|
|
3653
3654
|
highlightElement?: boolean;
|
|
3655
|
+
visibleLocator?: boolean;
|
|
3654
3656
|
recordHar?: any;
|
|
3655
3657
|
testIdAttribute?: string;
|
|
3656
3658
|
storageState?: string | any;
|
package/typings/types.d.ts
CHANGED
|
@@ -3641,6 +3641,7 @@ declare namespace CodeceptJS {
|
|
|
3641
3641
|
* @property [ignoreHTTPSErrors] - Allows access to untrustworthy pages, e.g. to a page with an expired certificate. Default value is `false`
|
|
3642
3642
|
* @property [bypassCSP] - bypass Content Security Policy or CSP
|
|
3643
3643
|
* @property [highlightElement] - highlight the interacting elements. Default: false. Note: only activate under verbose mode (--verbose).
|
|
3644
|
+
* @property [visibleLocator = false] - append [`visible()`](https://playwright.dev/docs/api/class-locator#locator-visible) to locators, so only visible elements are matched. Requires Playwright 1.63 or newer. Switch it off for a single step with `stepOpts({ visibleLocator: false })`. Not applied to `dragAndDrop`, which passes selectors to Playwright directly, nor to `seeElementInDOM`, `dontSeeElementInDOM` and `seeNumberOfElements`, which check the DOM regardless of visibility. When enabled, a locator matching only hidden elements fails as "element not found" instead of timing out on actionability, `strict` mode ignores hidden duplicates, and elements hidden by CSS (like a custom checkbox built on a visually hidden `input`) are no longer found.
|
|
3644
3645
|
* @property [recordHar] - record HAR and will be saved to `output/har`. See more of [HAR options](https://playwright.dev/docs/api/class-browser#browser-new-context-option-record-har).
|
|
3645
3646
|
* @property [testIdAttribute = data-testid] - locate elements based on the testIdAttribute. See more of [locate by test id](https://playwright.dev/docs/locators#locate-by-test-id).
|
|
3646
3647
|
* @property [storageState] - Playwright storage state (path to JSON file or object)
|
|
@@ -3685,6 +3686,7 @@ declare namespace CodeceptJS {
|
|
|
3685
3686
|
ignoreHTTPSErrors?: boolean;
|
|
3686
3687
|
bypassCSP?: boolean;
|
|
3687
3688
|
highlightElement?: boolean;
|
|
3689
|
+
visibleLocator?: boolean;
|
|
3688
3690
|
recordHar?: any;
|
|
3689
3691
|
testIdAttribute?: string;
|
|
3690
3692
|
storageState?: string | any;
|
|
@@ -11054,12 +11056,14 @@ declare namespace CodeceptJS {
|
|
|
11054
11056
|
* @property [exact] - Enable strict mode for this step. Throws if multiple elements match.
|
|
11055
11057
|
* @property [strictMode] - Alias for exact.
|
|
11056
11058
|
* @property [ignoreCase] - Perform case-insensitive text matching.
|
|
11059
|
+
* @property [visibleLocator] - Match only visible elements. Overrides the Playwright helper `visibleLocator` config option for this step.
|
|
11057
11060
|
*/
|
|
11058
11061
|
type StepOptions = {
|
|
11059
11062
|
elementIndex?: number | 'first' | 'last';
|
|
11060
11063
|
exact?: boolean;
|
|
11061
11064
|
strictMode?: boolean;
|
|
11062
11065
|
ignoreCase?: boolean;
|
|
11066
|
+
visibleLocator?: boolean;
|
|
11063
11067
|
};
|
|
11064
11068
|
/**
|
|
11065
11069
|
* StepConfig is a configuration object for a step.
|
|
@@ -11169,6 +11173,10 @@ declare namespace CodeceptJS {
|
|
|
11169
11173
|
var currentTest: CodeceptJS.Test | null;
|
|
11170
11174
|
var currentStep: CodeceptJS.Step | null;
|
|
11171
11175
|
var currentSuite: CodeceptJS.Suite | null;
|
|
11176
|
+
/**
|
|
11177
|
+
* Locators match only visible elements, resolved per step
|
|
11178
|
+
*/
|
|
11179
|
+
var visibleLocator: boolean;
|
|
11172
11180
|
var tsFileMapping: Map<string, string> | null;
|
|
11173
11181
|
/**
|
|
11174
11182
|
* Initialize required store fields.
|