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 CHANGED
@@ -79,7 +79,7 @@ Data(function*() {
79
79
  }).Scenario() // ...
80
80
  ```
81
81
 
82
- *HINT: If you don't use DataTable. add `toString()` method to each object added to data set, so the data could be pretty printed in a test name*
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][49].
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/locators#locate-by-test-id
2971
+ [49]: https://playwright.dev/docs/api/class-locator#locator-visible
2972
+
2973
+ [50]: https://playwright.dev/docs/locators#locate-by-test-id
@@ -72,13 +72,11 @@ function replaceTitle(title, dataRow) {
72
72
  return `${title} | ${dataRow.data.getMasked()}`
73
73
  }
74
74
 
75
- // if `dataRow` is object and has own `toString()` method,
76
- // it should be printed
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
- return `${title} | ${JSON.stringify(dataRow.data)}`
78
+ function maskSecret(key, value) {
79
+ return typeof value?.getMasked === 'function' ? value.getMasked() : value
82
80
  }
83
81
 
84
82
  function isTableDataRow(row) {
@@ -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) {
@@ -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
 
@@ -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 items = []
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
- items.push(` ${i + 1}. > ${xpath}\n ${html}`)
83
+ entries.push({ index: i + 1, xpath, html })
29
84
  } catch (err) {
30
- items.push(` ${i + 1}. [Unable to get element info: ${err.message}]`)
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
- async function findByPlaywrightLocator(matcher, locator) {
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).all()
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
- `.//input[@type = 'checkbox' or @type = 'radio'][(@id = //label[@for][contains(normalize-space(string(.)), ${literal})]/@for) or @placeholder = ${literal}]`,
660
- `.//label[contains(normalize-space(string(.)), ${literal})]//input[@type = 'radio' or @type = 'checkbox']`,
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
 
@@ -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.1",
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.59.0",
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;
@@ -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.