codeceptjs 3.7.5-beta.1 → 3.7.5-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/bin/codecept.js CHANGED
@@ -296,6 +296,29 @@ program
296
296
 
297
297
  .action(require('../lib/command/run-rerun'))
298
298
 
299
+ program
300
+ .command('run-failed-tests')
301
+ .description('Re-run tests that failed in the previous test run')
302
+ .option(commandFlags.config.flag, commandFlags.config.description)
303
+ .option(commandFlags.profile.flag, commandFlags.profile.description)
304
+ .option(commandFlags.verbose.flag, commandFlags.verbose.description)
305
+ .option(commandFlags.debug.flag, commandFlags.debug.description)
306
+ .option(commandFlags.steps.flag, commandFlags.steps.description)
307
+ .option('-o, --override [value]', 'override current config options')
308
+ .option('-f, --file [path]', 'path to failed tests file (default: ./failed-tests.json)')
309
+ .option('-g, --grep <pattern>', 'only run failed tests matching <pattern>')
310
+ .option('-p, --plugins <k=v,k2=v2,...>', 'enable plugins, comma-separated')
311
+ .option('--features', 'run only *.feature files and skip tests')
312
+ .option('--tests', 'run only JS test files and skip features')
313
+ .option('--colors', 'force enabling of colors')
314
+ .option('--no-colors', 'force disabling of colors')
315
+ .option('-R, --reporter <name>', 'specify the reporter to use')
316
+ .option('-O, --reporter-options <k=v,k2=v2,...>', 'reporter-specific options')
317
+ .option('--workers <number>', 'run failed tests in parallel using specified number of workers')
318
+ .option('--suites', 'parallel execution of suites not single tests (when using --workers)')
319
+ .option('--by <strategy>', 'test distribution strategy when using --workers: "test", "suite", or "pool"')
320
+ .action(errorHandler(require('../lib/command/run-failed-tests')))
321
+
299
322
  program.on('command:*', cmd => {
300
323
  console.log(`\nUnknown command ${cmd}\n`)
301
324
  program.outputHelp()
@@ -0,0 +1,218 @@
1
+ const fs = require('fs')
2
+ const path = require('path')
3
+ const { getConfig, printError, getTestRoot, createOutputDir } = require('./utils')
4
+ const Config = require('../config')
5
+ const store = require('../store')
6
+ const Codecept = require('../codecept')
7
+ const output = require('../output')
8
+ const Workers = require('../workers')
9
+ const { tryOrDefault } = require('../utils')
10
+
11
+ module.exports = async function (options) {
12
+ // registering options globally to use in config
13
+ if (options.profile) {
14
+ process.env.profile = options.profile
15
+ }
16
+ if (options.verbose || options.debug) store.debugMode = true
17
+
18
+ const configFile = options.config
19
+ let config = getConfig(configFile)
20
+
21
+ if (options.override) {
22
+ config = Config.append(JSON.parse(options.override))
23
+ }
24
+
25
+ const testRoot = getTestRoot(configFile)
26
+ createOutputDir(config, testRoot)
27
+
28
+ // Determine failed tests file path - respect CodeceptJS output directory
29
+ const failedTestsFile = options.file || 'failed-tests.json'
30
+ const failedTestsPath = path.isAbsolute(failedTestsFile)
31
+ ? failedTestsFile
32
+ : path.resolve(global.output_dir || './output', failedTestsFile)
33
+
34
+ // Check if failed tests file exists
35
+ if (!fs.existsSync(failedTestsPath)) {
36
+ output.error(`Failed tests file not found: ${failedTestsPath}`)
37
+ output.print('Run tests first to generate a failed tests file, or specify a different file with --file option')
38
+ process.exitCode = 1
39
+ return
40
+ }
41
+
42
+ let failedTestsData
43
+ try {
44
+ const fileContent = fs.readFileSync(failedTestsPath, 'utf8')
45
+ failedTestsData = JSON.parse(fileContent)
46
+ } catch (error) {
47
+ output.error(`Failed to read or parse failed tests file: ${error.message}`)
48
+ process.exitCode = 1
49
+ return
50
+ }
51
+
52
+ if (!failedTestsData.tests || failedTestsData.tests.length === 0) {
53
+ output.print('No failed tests found in the file')
54
+ return
55
+ }
56
+
57
+ output.print(`Found ${failedTestsData.tests.length} failed tests from ${failedTestsData.timestamp}`)
58
+
59
+ // Build test patterns from failed tests
60
+ const testPatterns = []
61
+ const testsByFile = new Map()
62
+
63
+ // Group tests by file for more efficient execution
64
+ failedTestsData.tests.forEach(test => {
65
+ if (test.file) {
66
+ if (!testsByFile.has(test.file)) {
67
+ testsByFile.set(test.file, [])
68
+ }
69
+ testsByFile.get(test.file).push(test)
70
+ }
71
+ })
72
+
73
+ // If we have specific test files, use them
74
+ if (testsByFile.size > 0) {
75
+ for (const [file, tests] of testsByFile) {
76
+ if (options.grep) {
77
+ // If grep is specified, combine with file pattern
78
+ testPatterns.push(file)
79
+ } else {
80
+ // Try to be more specific with test titles if possible
81
+ testPatterns.push(file)
82
+ }
83
+ }
84
+ } else {
85
+ // Fallback: use test titles with grep
86
+ const testTitles = failedTestsData.tests.map(test => test.title).filter(Boolean)
87
+ if (testTitles.length > 0) {
88
+ // Create a regex pattern to match any of the failed test titles
89
+ const grepPattern = testTitles.map(title => title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')
90
+ options.grep = grepPattern
91
+ }
92
+ }
93
+
94
+ // Check if user wants to run with workers
95
+ if (options.workers) {
96
+ await runWithWorkers(config, options, testPatterns, failedTestsData)
97
+ } else {
98
+ await runWithoutWorkers(config, options, testPatterns, failedTestsData, testRoot)
99
+ }
100
+ }
101
+
102
+ async function runWithWorkers(config, options, testPatterns, failedTestsData) {
103
+ const numberOfWorkers = parseInt(options.workers, 10)
104
+ const overrideConfigs = tryOrDefault(() => JSON.parse(options.override || '{}'), {})
105
+
106
+ // Determine test split strategy
107
+ let by = 'test' // default for failed tests
108
+ if (options.by) {
109
+ by = options.by
110
+ } else if (options.suites) {
111
+ by = 'suite'
112
+ }
113
+
114
+ // Validate the by option
115
+ const validStrategies = ['test', 'suite', 'pool']
116
+ if (!validStrategies.includes(by)) {
117
+ throw new Error(`Invalid --by strategy: ${by}. Valid options are: ${validStrategies.join(', ')}`)
118
+ }
119
+
120
+ const workerConfig = {
121
+ by,
122
+ testConfig: options.config,
123
+ options,
124
+ selectedRuns: undefined,
125
+ }
126
+
127
+ output.print(`CodeceptJS v${require('../codecept').version()}`)
128
+ output.print(`Re-running ${failedTestsData.tests.length} failed tests in ${output.styles.bold(numberOfWorkers)} workers...`)
129
+ output.print()
130
+ store.hasWorkers = true
131
+
132
+ const workers = new Workers(numberOfWorkers, workerConfig)
133
+ workers.overrideConfig(overrideConfigs)
134
+
135
+ // Set up event listeners for worker output
136
+ workers.on('test.failed', test => {
137
+ output.test.failed(test)
138
+ })
139
+
140
+ workers.on('test.passed', test => {
141
+ output.test.passed(test)
142
+ })
143
+
144
+ workers.on('test.skipped', test => {
145
+ output.test.skipped(test)
146
+ })
147
+
148
+ workers.on('all.result', result => {
149
+ workers.printResults()
150
+ })
151
+
152
+ try {
153
+ if (options.verbose || options.debug) store.debugMode = true
154
+
155
+ if (options.verbose) {
156
+ output.print('\nFailed tests to re-run with workers:')
157
+ failedTestsData.tests.forEach((test, index) => {
158
+ output.print(` ${index + 1}. ${test.fullTitle || test.title} (${test.file || 'unknown file'})`)
159
+ if (test.error && test.error.message) {
160
+ output.print(` Error: ${test.error.message}`)
161
+ }
162
+ })
163
+ output.print('')
164
+
165
+ const { getMachineInfo } = require('./info')
166
+ await getMachineInfo()
167
+ }
168
+
169
+ await workers.bootstrapAll()
170
+ await workers.run()
171
+ } catch (err) {
172
+ printError(err)
173
+ process.exitCode = 1
174
+ } finally {
175
+ await workers.teardownAll()
176
+ }
177
+ }
178
+
179
+ async function runWithoutWorkers(config, options, testPatterns, failedTestsData, testRoot) {
180
+ const codecept = new Codecept(config, options)
181
+
182
+ try {
183
+ codecept.init(testRoot)
184
+ await codecept.bootstrap()
185
+
186
+ // Load tests - if we have specific patterns, use them, otherwise load all and filter with grep
187
+ if (testPatterns.length > 0) {
188
+ codecept.loadTests(testPatterns.join(' '))
189
+ } else {
190
+ codecept.loadTests()
191
+ }
192
+
193
+ if (options.verbose) {
194
+ global.debugMode = true
195
+ const { getMachineInfo } = require('./info')
196
+ await getMachineInfo()
197
+ }
198
+
199
+ // Display information about what we're running
200
+ if (options.verbose) {
201
+ output.print('\nFailed tests to re-run:')
202
+ failedTestsData.tests.forEach((test, index) => {
203
+ output.print(` ${index + 1}. ${test.fullTitle || test.title} (${test.file || 'unknown file'})`)
204
+ if (test.error && test.error.message) {
205
+ output.print(` Error: ${test.error.message}`)
206
+ }
207
+ })
208
+ output.print('')
209
+ }
210
+
211
+ await codecept.run()
212
+ } catch (err) {
213
+ printError(err)
214
+ process.exitCode = 1
215
+ } finally {
216
+ await codecept.teardown()
217
+ }
218
+ }
@@ -38,6 +38,8 @@ const WebElement = require('../element/WebElement')
38
38
  let playwright
39
39
  let perfTiming
40
40
  let defaultSelectorEnginesInitialized = false
41
+ let registeredCustomLocatorStrategies = new Set()
42
+ let globalCustomLocatorStrategies = new Map()
41
43
 
42
44
  const popupStore = new Popup()
43
45
  const consoleLogStore = new Console()
@@ -96,6 +98,7 @@ const pathSeparator = path.sep
96
98
  * @prop {boolean} [highlightElement] - highlight the interacting elements. Default: false. Note: only activate under verbose mode (--verbose).
97
99
  * @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).
98
100
  * @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).
101
+ * @prop {object} [customLocatorStrategies] - custom locator strategies. An object with keys as strategy names and values as JavaScript functions. Example: `{ byRole: (selector, root) => { return root.querySelector(\`[role="\${selector}\"]\`) } }`
99
102
  */
100
103
  const config = {}
101
104
 
@@ -344,9 +347,23 @@ class Playwright extends Helper {
344
347
  this.recordingWebSocketMessages = false
345
348
  this.recordedWebSocketMessagesAtLeastOnce = false
346
349
  this.cdpSession = null
350
+ this.customLocatorStrategies = typeof config.customLocatorStrategies === 'object' && config.customLocatorStrategies !== null ? config.customLocatorStrategies : null
351
+ this._customLocatorsRegistered = false
352
+
353
+ // Add custom locator strategies to global registry for early registration
354
+ if (this.customLocatorStrategies) {
355
+ for (const [strategyName, strategyFunction] of Object.entries(this.customLocatorStrategies)) {
356
+ globalCustomLocatorStrategies.set(strategyName, strategyFunction)
357
+ }
358
+ }
347
359
 
348
360
  // override defaults with config
349
361
  this._setConfig(config)
362
+
363
+ // Call _init() to register selector engines - use setTimeout to avoid blocking constructor
364
+ setTimeout(() => {
365
+ this._init().catch(console.error)
366
+ }, 0)
350
367
  }
351
368
 
352
369
  _validateConfig(config) {
@@ -463,12 +480,61 @@ class Playwright extends Helper {
463
480
 
464
481
  async _init() {
465
482
  // register an internal selector engine for reading value property of elements in a selector
466
- if (defaultSelectorEnginesInitialized) return
467
- defaultSelectorEnginesInitialized = true
468
483
  try {
469
- await playwright.selectors.register('__value', createValueEngine)
470
- await playwright.selectors.register('__disabled', createDisabledEngine)
471
- if (process.env.testIdAttribute) await playwright.selectors.setTestIdAttribute(process.env.testIdAttribute)
484
+ if (!defaultSelectorEnginesInitialized) {
485
+ await playwright.selectors.register('__value', createValueEngine)
486
+ await playwright.selectors.register('__disabled', createDisabledEngine)
487
+ if (process.env.testIdAttribute) await playwright.selectors.setTestIdAttribute(process.env.testIdAttribute)
488
+ defaultSelectorEnginesInitialized = true
489
+ }
490
+
491
+ // Register all custom locator strategies from the global registry
492
+ for (const [strategyName, strategyFunction] of globalCustomLocatorStrategies.entries()) {
493
+ if (!registeredCustomLocatorStrategies.has(strategyName)) {
494
+ try {
495
+ // Create a selector engine factory function exactly like createValueEngine pattern
496
+ // Capture variables in closure to avoid reference issues
497
+ const createCustomEngine = ((name, func) => {
498
+ return () => {
499
+ return {
500
+ create() {
501
+ return null
502
+ },
503
+ query(root, selector) {
504
+ try {
505
+ if (!root) return null
506
+ const result = func(selector, root)
507
+ return Array.isArray(result) ? result[0] : result
508
+ } catch (error) {
509
+ console.warn(`Error in custom locator "${name}":`, error)
510
+ return null
511
+ }
512
+ },
513
+ queryAll(root, selector) {
514
+ try {
515
+ if (!root) return []
516
+ const result = func(selector, root)
517
+ return Array.isArray(result) ? result : result ? [result] : []
518
+ } catch (error) {
519
+ console.warn(`Error in custom locator "${name}":`, error)
520
+ return []
521
+ }
522
+ },
523
+ }
524
+ }
525
+ })(strategyName, strategyFunction)
526
+
527
+ await playwright.selectors.register(strategyName, createCustomEngine)
528
+ registeredCustomLocatorStrategies.add(strategyName)
529
+ } catch (error) {
530
+ if (!error.message.includes('already registered')) {
531
+ console.warn(`Failed to register custom locator strategy '${strategyName}':`, error)
532
+ } else {
533
+ console.log(`Custom locator strategy '${strategyName}' already registered`)
534
+ }
535
+ }
536
+ }
537
+ }
472
538
  } catch (e) {
473
539
  console.warn(e)
474
540
  }
@@ -827,6 +893,9 @@ class Playwright extends Helper {
827
893
  }
828
894
 
829
895
  async _startBrowser() {
896
+ // Ensure custom locator strategies are registered before browser launch
897
+ await this._init()
898
+
830
899
  if (this.isElectron) {
831
900
  this.browser = await playwright._electron.launch(this.playwrightOptions)
832
901
  } else if (this.isRemoteBrowser && this.isCDPConnection) {
@@ -862,6 +931,30 @@ class Playwright extends Helper {
862
931
  return this.browser
863
932
  }
864
933
 
934
+ _lookupCustomLocator(customStrategy) {
935
+ if (typeof this.customLocatorStrategies !== 'object' || this.customLocatorStrategies === null) {
936
+ return null
937
+ }
938
+ const strategy = this.customLocatorStrategies[customStrategy]
939
+ return typeof strategy === 'function' ? strategy : null
940
+ }
941
+
942
+ _isCustomLocator(locator) {
943
+ const locatorObj = new Locator(locator)
944
+ if (locatorObj.isCustom()) {
945
+ const customLocator = this._lookupCustomLocator(locatorObj.type)
946
+ if (customLocator) {
947
+ return true
948
+ }
949
+ throw new Error('Please define "customLocatorStrategies" as an Object and the Locator Strategy as a "function".')
950
+ }
951
+ return false
952
+ }
953
+
954
+ _isCustomLocatorStrategyDefined() {
955
+ return !!(this.customLocatorStrategies && Object.keys(this.customLocatorStrategies).length > 0)
956
+ }
957
+
865
958
  /**
866
959
  * Create a new browser context with a page. \
867
960
  * Usually it should be run from a custom helper after call of `_startBrowser()`
@@ -869,11 +962,64 @@ class Playwright extends Helper {
869
962
  */
870
963
  async _createContextPage(contextOptions) {
871
964
  this.browserContext = await this.browser.newContext(contextOptions)
965
+
966
+ // Register custom locator strategies for this context
967
+ await this._registerCustomLocatorStrategies()
968
+
872
969
  const page = await this.browserContext.newPage()
873
970
  targetCreatedHandler.call(this, page)
874
971
  await this._setPage(page)
875
972
  }
876
973
 
974
+ async _registerCustomLocatorStrategies() {
975
+ if (!this.customLocatorStrategies) return
976
+
977
+ for (const [strategyName, strategyFunction] of Object.entries(this.customLocatorStrategies)) {
978
+ if (!registeredCustomLocatorStrategies.has(strategyName)) {
979
+ try {
980
+ const createCustomEngine = ((name, func) => {
981
+ return () => {
982
+ return {
983
+ create(root, target) {
984
+ return null
985
+ },
986
+ query(root, selector) {
987
+ try {
988
+ if (!root) return null
989
+ const result = func(selector, root)
990
+ return Array.isArray(result) ? result[0] : result
991
+ } catch (error) {
992
+ console.warn(`Error in custom locator "${name}":`, error)
993
+ return null
994
+ }
995
+ },
996
+ queryAll(root, selector) {
997
+ try {
998
+ if (!root) return []
999
+ const result = func(selector, root)
1000
+ return Array.isArray(result) ? result : result ? [result] : []
1001
+ } catch (error) {
1002
+ console.warn(`Error in custom locator "${name}":`, error)
1003
+ return []
1004
+ }
1005
+ },
1006
+ }
1007
+ }
1008
+ })(strategyName, strategyFunction)
1009
+
1010
+ await playwright.selectors.register(strategyName, createCustomEngine)
1011
+ registeredCustomLocatorStrategies.add(strategyName)
1012
+ } catch (error) {
1013
+ if (!error.message.includes('already registered')) {
1014
+ console.warn(`Failed to register custom locator strategy '${strategyName}':`, error)
1015
+ } else {
1016
+ console.log(`Custom locator strategy '${strategyName}' already registered`)
1017
+ }
1018
+ }
1019
+ }
1020
+ }
1021
+ }
1022
+
877
1023
  _getType() {
878
1024
  return this.browser._type
879
1025
  }
@@ -885,7 +1031,10 @@ class Playwright extends Helper {
885
1031
  this.frame = null
886
1032
  popupStore.clear()
887
1033
  if (this.options.recordHar) await this.browserContext.close()
1034
+ this.browserContext = null
888
1035
  await this.browser.close()
1036
+ this.browser = null
1037
+ this.isRunning = false
889
1038
  }
890
1039
 
891
1040
  async _evaluateHandeInContext(...args) {
@@ -1266,9 +1415,9 @@ class Playwright extends Helper {
1266
1415
  async _locate(locator) {
1267
1416
  const context = await this._getContext()
1268
1417
 
1269
- if (this.frame) return findElements(this.frame, locator)
1418
+ if (this.frame) return findElements.call(this, this.frame, locator)
1270
1419
 
1271
- const els = await findElements(context, locator)
1420
+ const els = await findElements.call(this, context, locator)
1272
1421
 
1273
1422
  if (store.debugMode) {
1274
1423
  const previewElements = els.slice(0, 3)
@@ -2063,11 +2212,25 @@ class Playwright extends Helper {
2063
2212
  * @param {*} locator
2064
2213
  */
2065
2214
  _contextLocator(locator) {
2066
- locator = buildLocatorString(new Locator(locator, 'css'))
2215
+ const locatorObj = new Locator(locator, 'css')
2216
+
2217
+ // Handle custom locators differently
2218
+ if (locatorObj.isCustom()) {
2219
+ return buildCustomLocatorString(locatorObj)
2220
+ }
2221
+
2222
+ locator = buildLocatorString(locatorObj)
2067
2223
 
2068
2224
  if (this.contextLocator) {
2069
- const contextLocator = buildLocatorString(new Locator(this.contextLocator, 'css'))
2070
- locator = `${contextLocator} >> ${locator}`
2225
+ const contextLocatorObj = new Locator(this.contextLocator, 'css')
2226
+ if (contextLocatorObj.isCustom()) {
2227
+ // For custom context locators, we can't use the >> syntax
2228
+ // Instead, we'll need to handle this differently in the calling methods
2229
+ return locator
2230
+ } else {
2231
+ const contextLocator = buildLocatorString(contextLocatorObj)
2232
+ locator = `${contextLocator} >> ${locator}`
2233
+ }
2071
2234
  }
2072
2235
 
2073
2236
  return locator
@@ -2078,11 +2241,25 @@ class Playwright extends Helper {
2078
2241
  *
2079
2242
  */
2080
2243
  async grabTextFrom(locator) {
2081
- locator = this._contextLocator(locator)
2082
- const text = await this.page.textContent(locator)
2083
- assertElementExists(text, locator)
2084
- this.debugSection('Text', text)
2085
- return text
2244
+ const locatorObj = new Locator(locator, 'css')
2245
+
2246
+ if (locatorObj.isCustom()) {
2247
+ // For custom locators, find the element first
2248
+ const elements = await findCustomElements.call(this, this.page, locatorObj)
2249
+ if (elements.length === 0) {
2250
+ throw new Error(`Element not found: ${locatorObj.toString()}`)
2251
+ }
2252
+ const text = await elements[0].textContent()
2253
+ assertElementExists(text, locatorObj.toString())
2254
+ this.debugSection('Text', text)
2255
+ return text
2256
+ } else {
2257
+ locator = this._contextLocator(locator)
2258
+ const text = await this.page.textContent(locator)
2259
+ assertElementExists(text, locator)
2260
+ this.debugSection('Text', text)
2261
+ return text
2262
+ }
2086
2263
  }
2087
2264
 
2088
2265
  /**
@@ -2095,7 +2272,6 @@ class Playwright extends Helper {
2095
2272
  for (const el of els) {
2096
2273
  texts.push(await el.innerText())
2097
2274
  }
2098
- this.debug(`Matched ${els.length} elements`)
2099
2275
  return texts
2100
2276
  }
2101
2277
 
@@ -2114,7 +2290,6 @@ class Playwright extends Helper {
2114
2290
  */
2115
2291
  async grabValueFromAll(locator) {
2116
2292
  const els = await findFields.call(this, locator)
2117
- this.debug(`Matched ${els.length} elements`)
2118
2293
  return Promise.all(els.map(el => el.inputValue()))
2119
2294
  }
2120
2295
 
@@ -2133,7 +2308,6 @@ class Playwright extends Helper {
2133
2308
  */
2134
2309
  async grabHTMLFromAll(locator) {
2135
2310
  const els = await this._locate(locator)
2136
- this.debug(`Matched ${els.length} elements`)
2137
2311
  return Promise.all(els.map(el => el.innerHTML()))
2138
2312
  }
2139
2313
 
@@ -2154,7 +2328,6 @@ class Playwright extends Helper {
2154
2328
  */
2155
2329
  async grabCssPropertyFromAll(locator, cssProperty) {
2156
2330
  const els = await this._locate(locator)
2157
- this.debug(`Matched ${els.length} elements`)
2158
2331
  const cssValues = await Promise.all(els.map(el => el.evaluate((el, cssProperty) => getComputedStyle(el).getPropertyValue(cssProperty), cssProperty)))
2159
2332
 
2160
2333
  return cssValues
@@ -2265,7 +2438,6 @@ class Playwright extends Helper {
2265
2438
  */
2266
2439
  async grabAttributeFromAll(locator, attr) {
2267
2440
  const els = await this._locate(locator)
2268
- this.debug(`Matched ${els.length} elements`)
2269
2441
  const array = []
2270
2442
 
2271
2443
  for (let index = 0; index < els.length; index++) {
@@ -2285,7 +2457,6 @@ class Playwright extends Helper {
2285
2457
  const res = await this._locateElement(locator)
2286
2458
  assertElementExists(res, locator)
2287
2459
  const elem = res
2288
- this.debug(`Screenshot of ${new Locator(locator)} element has been saved to ${outputFile}`)
2289
2460
  return elem.screenshot({ path: outputFile, type: 'png' })
2290
2461
  }
2291
2462
 
@@ -2580,7 +2751,16 @@ class Playwright extends Helper {
2580
2751
 
2581
2752
  const context = await this._getContext()
2582
2753
  try {
2583
- await context.locator(buildLocatorString(locator)).first().waitFor({ timeout: waitTimeout, state: 'attached' })
2754
+ if (locator.isCustom()) {
2755
+ // For custom locators, we need to use our custom element finding logic
2756
+ const elements = await findCustomElements.call(this, context, locator)
2757
+ if (elements.length === 0) {
2758
+ throw new Error(`Custom locator ${locator.type}=${locator.value} not found`)
2759
+ }
2760
+ await elements[0].waitFor({ timeout: waitTimeout, state: 'attached' })
2761
+ } else {
2762
+ await context.locator(buildLocatorString(locator)).first().waitFor({ timeout: waitTimeout, state: 'attached' })
2763
+ }
2584
2764
  } catch (e) {
2585
2765
  throw new Error(`element (${locator.toString()}) still not present on page after ${waitTimeout / 1000} sec\n${e.message}`)
2586
2766
  }
@@ -2594,9 +2774,30 @@ class Playwright extends Helper {
2594
2774
  async waitForVisible(locator, sec) {
2595
2775
  const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
2596
2776
  locator = new Locator(locator, 'css')
2777
+
2597
2778
  const context = await this._getContext()
2598
2779
  let count = 0
2599
2780
 
2781
+ // Handle custom locators
2782
+ if (locator.isCustom()) {
2783
+ let waiter
2784
+ do {
2785
+ const elements = await findCustomElements.call(this, context, locator)
2786
+ if (elements.length > 0) {
2787
+ waiter = await elements[0].isVisible()
2788
+ } else {
2789
+ waiter = false
2790
+ }
2791
+ if (!waiter) {
2792
+ await this.wait(1)
2793
+ count += 1000
2794
+ }
2795
+ } while (!waiter && count <= waitTimeout)
2796
+
2797
+ if (!waiter) throw new Error(`element (${locator.toString()}) still not visible after ${waitTimeout / 1000} sec.`)
2798
+ return
2799
+ }
2800
+
2600
2801
  // we have this as https://github.com/microsoft/playwright/issues/26829 is not yet implemented
2601
2802
  let waiter
2602
2803
  if (this.frame) {
@@ -2623,6 +2824,7 @@ class Playwright extends Helper {
2623
2824
  async waitForInvisible(locator, sec) {
2624
2825
  const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
2625
2826
  locator = new Locator(locator, 'css')
2827
+
2626
2828
  const context = await this._getContext()
2627
2829
  let waiter
2628
2830
  let count = 0
@@ -2653,6 +2855,7 @@ class Playwright extends Helper {
2653
2855
  async waitToHide(locator, sec) {
2654
2856
  const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout
2655
2857
  locator = new Locator(locator, 'css')
2858
+
2656
2859
  const context = await this._getContext()
2657
2860
  let waiter
2658
2861
  let count = 0
@@ -2774,9 +2977,18 @@ class Playwright extends Helper {
2774
2977
  if (context) {
2775
2978
  const locator = new Locator(context, 'css')
2776
2979
  try {
2980
+ if (locator.isCustom()) {
2981
+ // For custom locators, find the elements first then check for text within them
2982
+ const elements = await findCustomElements.call(this, contextObject, locator)
2983
+ if (elements.length === 0) {
2984
+ throw new Error(`Context element not found: ${locator.toString()}`)
2985
+ }
2986
+ return elements[0].locator(`text=${text}`).first().waitFor({ timeout: waitTimeout, state: 'visible' })
2987
+ }
2988
+
2777
2989
  if (!locator.isXPath()) {
2778
2990
  return contextObject
2779
- .locator(`${locator.isCustom() ? `${locator.type}=${locator.value}` : locator.simplify()} >> text=${text}`)
2991
+ .locator(`${locator.simplify()} >> text=${text}`)
2780
2992
  .first()
2781
2993
  .waitFor({ timeout: waitTimeout, state: 'visible' })
2782
2994
  .catch(e => {
@@ -3421,9 +3633,15 @@ class Playwright extends Helper {
3421
3633
 
3422
3634
  module.exports = Playwright
3423
3635
 
3636
+ function buildCustomLocatorString(locator) {
3637
+ // Note: this.debug not available in standalone function, using console.log
3638
+ console.log(`Building custom locator string: ${locator.type}=${locator.value}`)
3639
+ return `${locator.type}=${locator.value}`
3640
+ }
3641
+
3424
3642
  function buildLocatorString(locator) {
3425
3643
  if (locator.isCustom()) {
3426
- return `${locator.type}=${locator.value}`
3644
+ return buildCustomLocatorString(locator)
3427
3645
  }
3428
3646
  if (locator.isXPath()) {
3429
3647
  return `xpath=${locator.value}`
@@ -3435,15 +3653,119 @@ async function findElements(matcher, locator) {
3435
3653
  if (locator.react) return findReact(matcher, locator)
3436
3654
  if (locator.vue) return findVue(matcher, locator)
3437
3655
  if (locator.pw) return findByPlaywrightLocator.call(this, matcher, locator)
3656
+
3438
3657
  locator = new Locator(locator, 'css')
3439
3658
 
3440
- return matcher.locator(buildLocatorString(locator)).all()
3659
+ // Handle custom locators directly instead of relying on Playwright selector engines
3660
+ if (locator.isCustom()) {
3661
+ return findCustomElements.call(this, matcher, locator)
3662
+ }
3663
+
3664
+ // Check if we have a custom context locator and need to search within it
3665
+ if (this.contextLocator) {
3666
+ const contextLocatorObj = new Locator(this.contextLocator, 'css')
3667
+ if (contextLocatorObj.isCustom()) {
3668
+ // Find the context elements first
3669
+ const contextElements = await findCustomElements.call(this, matcher, contextLocatorObj)
3670
+ if (contextElements.length === 0) {
3671
+ return []
3672
+ }
3673
+
3674
+ // Search within the first context element
3675
+ const locatorString = buildLocatorString(locator)
3676
+ return contextElements[0].locator(locatorString).all()
3677
+ }
3678
+ }
3679
+
3680
+ const locatorString = buildLocatorString(locator)
3681
+
3682
+ return matcher.locator(locatorString).all()
3683
+ }
3684
+
3685
+ async function findCustomElements(matcher, locator) {
3686
+ const customLocatorStrategies = this.customLocatorStrategies || globalCustomLocatorStrategies
3687
+ const strategyFunction = customLocatorStrategies.get ? customLocatorStrategies.get(locator.type) : customLocatorStrategies[locator.type]
3688
+
3689
+ if (!strategyFunction) {
3690
+ throw new Error(`Custom locator strategy "${locator.type}" is not defined. Please define "customLocatorStrategies" in your configuration.`)
3691
+ }
3692
+
3693
+ // Execute the custom locator function in the browser context using page.evaluate
3694
+ const page = matcher.constructor.name === 'Page' ? matcher : await matcher.page()
3695
+
3696
+ const elements = await page.evaluate(
3697
+ ({ strategyCode, selector }) => {
3698
+ const strategy = new Function('return ' + strategyCode)()
3699
+ const result = strategy(selector, document)
3700
+
3701
+ // Convert NodeList or single element to array
3702
+ if (result && result.nodeType) {
3703
+ return [result]
3704
+ } else if (result && result.length !== undefined) {
3705
+ return Array.from(result)
3706
+ } else if (Array.isArray(result)) {
3707
+ return result
3708
+ }
3709
+
3710
+ return []
3711
+ },
3712
+ {
3713
+ strategyCode: strategyFunction.toString(),
3714
+ selector: locator.value,
3715
+ },
3716
+ )
3717
+
3718
+ // Convert the found elements back to Playwright locators
3719
+ if (elements.length === 0) {
3720
+ return []
3721
+ }
3722
+
3723
+ // Create CSS selectors for the found elements and return as locators
3724
+ const locators = []
3725
+ const timestamp = Date.now()
3726
+
3727
+ for (let i = 0; i < elements.length; i++) {
3728
+ // Use a unique attribute approach to target specific elements
3729
+ const uniqueAttr = `data-codecept-custom-${timestamp}-${i}`
3730
+
3731
+ await page.evaluate(
3732
+ ({ index, uniqueAttr, strategyCode, selector }) => {
3733
+ // Re-execute the strategy to find elements and mark the specific one
3734
+ const strategy = new Function('return ' + strategyCode)()
3735
+ const result = strategy(selector, document)
3736
+
3737
+ let elementsArray = []
3738
+ if (result && result.nodeType) {
3739
+ elementsArray = [result]
3740
+ } else if (result && result.length !== undefined) {
3741
+ elementsArray = Array.from(result)
3742
+ } else if (Array.isArray(result)) {
3743
+ elementsArray = result
3744
+ }
3745
+
3746
+ if (elementsArray[index]) {
3747
+ elementsArray[index].setAttribute(uniqueAttr, 'true')
3748
+ }
3749
+ },
3750
+ {
3751
+ index: i,
3752
+ uniqueAttr,
3753
+ strategyCode: strategyFunction.toString(),
3754
+ selector: locator.value,
3755
+ },
3756
+ )
3757
+
3758
+ locators.push(page.locator(`[${uniqueAttr}="true"]`))
3759
+ }
3760
+
3761
+ return locators
3441
3762
  }
3442
3763
 
3443
3764
  async function findElement(matcher, locator) {
3444
3765
  if (locator.react) return findReact(matcher, locator)
3445
3766
  if (locator.vue) return findVue(matcher, locator)
3446
3767
  if (locator.pw) return findByPlaywrightLocator.call(this, matcher, locator)
3768
+
3447
3769
  locator = new Locator(locator, 'css')
3448
3770
 
3449
3771
  return matcher.locator(buildLocatorString(locator)).first()
@@ -3764,9 +4086,7 @@ async function targetCreatedHandler(page) {
3764
4086
  if (this.options.windowSize && this.options.windowSize.indexOf('x') > 0 && this._getType() === 'Browser') {
3765
4087
  try {
3766
4088
  await page.setViewportSize(parseWindowSize(this.options.windowSize))
3767
- } catch (err) {
3768
- this.debug('Target can be already closed, ignoring...')
3769
- }
4089
+ } catch (err) {}
3770
4090
  }
3771
4091
  }
3772
4092
 
@@ -0,0 +1,213 @@
1
+ const fs = require('fs')
2
+ const path = require('path')
3
+ const event = require('../event')
4
+ const output = require('../output')
5
+ const store = require('../store')
6
+
7
+ const defaultConfig = {
8
+ enabled: true,
9
+ outputFile: 'failed-tests.json',
10
+ clearOnSuccess: true,
11
+ includeStackTrace: true,
12
+ includeMetadata: true,
13
+ }
14
+
15
+ /**
16
+ * Failed Tests Tracker Plugin for CodeceptJS
17
+ *
18
+ * Tracks failed tests and saves them to a file for later re-execution.
19
+ *
20
+ * ## Configuration
21
+ *
22
+ * ```js
23
+ * "plugins": {
24
+ * "failedTestsTracker": {
25
+ * "enabled": true,
26
+ * "outputFile": "./failed-tests.json",
27
+ * "clearOnSuccess": true,
28
+ * "includeStackTrace": true,
29
+ * "includeMetadata": true
30
+ * }
31
+ * }
32
+ * ```
33
+ *
34
+ * @param {object} config plugin configuration
35
+ */
36
+ module.exports = function (config) {
37
+ const options = { ...defaultConfig, ...config }
38
+ let failedTests = []
39
+ let allTestsPassed = true
40
+ let workerFailedTests = new Map() // Track failed tests from workers
41
+
42
+ // Track test failures
43
+ event.dispatcher.on(event.test.failed, test => {
44
+ allTestsPassed = false
45
+
46
+ const failedTest = {
47
+ title: test.title,
48
+ fullTitle: test.fullTitle(),
49
+ file: test.file || (test.parent && test.parent.file),
50
+ uid: test.uid,
51
+ timestamp: new Date().toISOString(),
52
+ }
53
+
54
+ // Add parent suite information
55
+ if (test.parent) {
56
+ failedTest.suite = test.parent.title
57
+ failedTest.suiteFile = test.parent.file
58
+ }
59
+
60
+ // Add error information if available
61
+ if (test.err && options.includeStackTrace) {
62
+ failedTest.error = {
63
+ message: test.err.message || 'Test failed',
64
+ stack: test.err.stack || '',
65
+ name: test.err.name || 'Error',
66
+ }
67
+ }
68
+
69
+ // Add metadata if available
70
+ if (options.includeMetadata) {
71
+ failedTest.metadata = {
72
+ tags: test.tags || [],
73
+ meta: test.meta || {},
74
+ opts: test.opts || {},
75
+ duration: test.duration || 0,
76
+ retries: test.retries || 0,
77
+ }
78
+ }
79
+
80
+ // Add BDD/Gherkin information if available
81
+ if (test.parent && test.parent.feature) {
82
+ failedTest.bdd = {
83
+ feature: test.parent.feature.name || test.parent.title,
84
+ scenario: test.title,
85
+ featureFile: test.parent.file,
86
+ }
87
+ }
88
+
89
+ failedTests.push(failedTest)
90
+ output.print(`Failed Tests Tracker: Recorded failed test - ${test.title}`)
91
+ })
92
+
93
+ // Save failed tests to file after all tests complete
94
+ event.dispatcher.on(event.all.result, (result) => {
95
+ // Respect CodeceptJS output directory like other plugins
96
+ const outputDir = global.output_dir || './output'
97
+ const outputPath = path.isAbsolute(options.outputFile)
98
+ ? options.outputFile
99
+ : path.resolve(outputDir, options.outputFile)
100
+ let allFailedTests = [...failedTests]
101
+
102
+ // In worker mode, collect failed tests from consolidated result
103
+ if (store.hasWorkers && result && result.tests) {
104
+ const workerFailedTests = result.tests.filter(test => test.state === 'failed' || test.err)
105
+
106
+ // Use a Set to track unique test identifiers to prevent duplicates
107
+ const existingTestIds = new Set(allFailedTests.map(test => test.uid || `${test.file}:${test.title}`))
108
+
109
+ workerFailedTests.forEach(test => {
110
+ // Create unique identifier for deduplication
111
+ const testId = test.uid || `${test.file || 'unknown'}:${test.title}`
112
+
113
+ // Skip if we already have this test
114
+ if (existingTestIds.has(testId)) {
115
+ return
116
+ }
117
+
118
+ const failedTest = {
119
+ title: test.title,
120
+ fullTitle: test.fullTitle || test.title,
121
+ file: test.file || (test.parent && test.parent.file),
122
+ uid: test.uid,
123
+ timestamp: new Date().toISOString(),
124
+ }
125
+
126
+ // Add parent suite information
127
+ if (test.parent) {
128
+ failedTest.suite = test.parent.title
129
+ failedTest.suiteFile = test.parent.file
130
+ }
131
+
132
+ // Add error information if available
133
+ if (test.err && options.includeStackTrace) {
134
+ failedTest.error = {
135
+ message: test.err.message || 'Test failed',
136
+ stack: test.err.stack || '',
137
+ name: test.err.name || 'Error',
138
+ }
139
+ }
140
+
141
+ // Add metadata if available
142
+ if (options.includeMetadata) {
143
+ failedTest.metadata = {
144
+ tags: test.tags || [],
145
+ meta: test.meta || {},
146
+ opts: test.opts || {},
147
+ duration: test.duration || 0,
148
+ retries: test.retries || 0,
149
+ }
150
+ }
151
+
152
+ // Add BDD/Gherkin information if available
153
+ if (test.parent && test.parent.feature) {
154
+ failedTest.bdd = {
155
+ feature: test.parent.feature.name || test.parent.title,
156
+ scenario: test.title,
157
+ featureFile: test.parent.file,
158
+ }
159
+ }
160
+
161
+ allFailedTests.push(failedTest)
162
+ existingTestIds.add(testId)
163
+ })
164
+ }
165
+
166
+ if (allFailedTests.length === 0) {
167
+ if (options.clearOnSuccess && fs.existsSync(outputPath)) {
168
+ try {
169
+ fs.unlinkSync(outputPath)
170
+ output.print(`Failed Tests Tracker: Cleared previous failed tests file (all tests passed)`)
171
+ } catch (error) {
172
+ output.print(`Failed Tests Tracker: Could not clear failed tests file: ${error.message}`)
173
+ }
174
+ } else {
175
+ output.print(`Failed Tests Tracker: No failed tests to save`)
176
+ }
177
+ return
178
+ }
179
+
180
+ const failedTestsData = {
181
+ timestamp: new Date().toISOString(),
182
+ totalFailedTests: allFailedTests.length,
183
+ codeceptVersion: require('../codecept').version(),
184
+ tests: allFailedTests,
185
+ }
186
+
187
+ try {
188
+ // Ensure directory exists
189
+ const dir = path.dirname(outputPath)
190
+ if (!fs.existsSync(dir)) {
191
+ fs.mkdirSync(dir, { recursive: true })
192
+ }
193
+
194
+ fs.writeFileSync(outputPath, JSON.stringify(failedTestsData, null, 2))
195
+ output.print(`Failed Tests Tracker: Saved ${allFailedTests.length} failed tests to ${outputPath}`)
196
+ } catch (error) {
197
+ output.print(`Failed Tests Tracker: Failed to save failed tests: ${error.message}`)
198
+ }
199
+ })
200
+
201
+ // Reset state for new test runs
202
+ event.dispatcher.on(event.all.before, () => {
203
+ failedTests = []
204
+ allTestsPassed = true
205
+ workerFailedTests.clear()
206
+ })
207
+
208
+ // Handle worker mode - listen to workers.result event for consolidated results
209
+ event.dispatcher.on(event.workers.result, (result) => {
210
+ // This event is fired in worker mode with consolidated results
211
+ // The event.all.result handler above will process the consolidated data
212
+ })
213
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeceptjs",
3
- "version": "3.7.5-beta.1",
3
+ "version": "3.7.5-beta.3",
4
4
  "description": "Supercharged End 2 End Testing Framework for NodeJS",
5
5
  "keywords": [
6
6
  "acceptance",
@@ -2733,8 +2733,11 @@ declare namespace CodeceptJS {
2733
2733
  * @property [highlightElement] - highlight the interacting elements. Default: false. Note: only activate under verbose mode (--verbose).
2734
2734
  * @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).
2735
2735
  * @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).
2736
+ * @property [customLocatorStrategies] - custom locator strategies. An object with keys as strategy names and values as JavaScript functions. Example: `{ byRole: (selector, root) => { return root.querySelector(\`[role="\${selector}\"]\`) } }`
2736
2737
  */
2737
2738
  // @ts-ignore
2739
+ // @ts-ignore
2740
+ // @ts-ignore
2738
2741
  type PlaywrightConfig = {
2739
2742
  url?: string;
2740
2743
  browser?: 'chromium' | 'firefox' | 'webkit' | 'electron';
@@ -2772,6 +2775,7 @@ declare namespace CodeceptJS {
2772
2775
  highlightElement?: boolean;
2773
2776
  recordHar?: any;
2774
2777
  testIdAttribute?: string;
2778
+ customLocatorStrategies?: any;
2775
2779
  };
2776
2780
  /**
2777
2781
  * Uses [Playwright](https://github.com/microsoft/playwright) library to run tests inside:
@@ -6112,6 +6116,8 @@ declare namespace CodeceptJS {
6112
6116
  * @property [highlightElement] - highlight the interacting elements. Default: false. Note: only activate under verbose mode (--verbose).
6113
6117
  */
6114
6118
  // @ts-ignore
6119
+ // @ts-ignore
6120
+ // @ts-ignore
6115
6121
  type PuppeteerConfig = {
6116
6122
  url: string;
6117
6123
  basicAuth?: any;
@@ -6535,17 +6541,6 @@ declare namespace CodeceptJS {
6535
6541
  * {{ react }}
6536
6542
  */
6537
6543
  _locate(): Promise<any>;
6538
- /**
6539
- * Get single element by different locator types, including strict locator
6540
- * Should be used in custom helpers:
6541
- *
6542
- * ```js
6543
- * const element = await this.helpers['Puppeteer']._locateElement({name: 'password'});
6544
- * ```
6545
- *
6546
- * {{ react }}
6547
- */
6548
- _locateElement(): Promise<any>;
6549
6544
  /**
6550
6545
  * Find a checkbox by providing human-readable text:
6551
6546
  * NOTE: Assumes the checkable element exists
@@ -6582,17 +6577,6 @@ declare namespace CodeceptJS {
6582
6577
  * @returns WebElement of being used Web helper
6583
6578
  */
6584
6579
  grabWebElements(locator: CodeceptJS.LocatorOrString): Promise<any>;
6585
- /**
6586
- * Grab WebElement for given locator
6587
- * Resumes test execution, so **should be used inside an async function with `await`** operator.
6588
- *
6589
- * ```js
6590
- * const webElement = await I.grabWebElement('#button');
6591
- * ```
6592
- * @param locator - element located by CSS|XPath|strict locator.
6593
- * @returns WebElement of being used Web helper
6594
- */
6595
- grabWebElement(locator: CodeceptJS.LocatorOrString): Promise<any>;
6596
6580
  /**
6597
6581
  * Switch focus to a particular tab by its number. It waits tabs loading and then switch tab
6598
6582
  *
@@ -7928,22 +7912,6 @@ declare namespace CodeceptJS {
7928
7912
  */
7929
7913
  flushWebSocketMessages(): Promise<any>;
7930
7914
  }
7931
- /**
7932
- * Find elements using Puppeteer's native element discovery methods
7933
- * Note: Unlike Playwright, Puppeteer's Locator API doesn't have .all() method for multiple elements
7934
- * @param matcher - Puppeteer context to search within
7935
- * @param locator - Locator specification
7936
- * @returns Array of ElementHandle objects
7937
- */
7938
- function findElements(matcher: any, locator: any | string): Promise<any[]>;
7939
- /**
7940
- * Find a single element using Puppeteer's native element discovery methods
7941
- * Note: Puppeteer Locator API doesn't have .first() method like Playwright
7942
- * @param matcher - Puppeteer context to search within
7943
- * @param locator - Locator specification
7944
- * @returns Single ElementHandle object
7945
- */
7946
- function findElement(matcher: any, locator: any | string): Promise<object>;
7947
7915
  /**
7948
7916
  * ## Configuration
7949
7917
  * @property [endpoint] - API base URL
@@ -7957,6 +7925,8 @@ declare namespace CodeceptJS {
7957
7925
  * @property [maxUploadFileSize] - set the max content file size in MB when performing api calls.
7958
7926
  */
7959
7927
  // @ts-ignore
7928
+ // @ts-ignore
7929
+ // @ts-ignore
7960
7930
  type RESTConfig = {
7961
7931
  endpoint?: string;
7962
7932
  prettyPrintJson?: boolean;
@@ -9103,6 +9073,8 @@ declare namespace CodeceptJS {
9103
9073
  * @property [logLevel = silent] - level of logging verbosity. Default: silent. Options: trace | debug | info | warn | error | silent. More info: https://webdriver.io/docs/configuration/#loglevel
9104
9074
  */
9105
9075
  // @ts-ignore
9076
+ // @ts-ignore
9077
+ // @ts-ignore
9106
9078
  type WebDriverConfig = {
9107
9079
  url: string;
9108
9080
  browser: string;
@@ -9573,17 +9545,6 @@ declare namespace CodeceptJS {
9573
9545
  * @returns WebElement of being used Web helper
9574
9546
  */
9575
9547
  grabWebElements(locator: CodeceptJS.LocatorOrString): Promise<any>;
9576
- /**
9577
- * Grab WebElement for given locator
9578
- * Resumes test execution, so **should be used inside an async function with `await`** operator.
9579
- *
9580
- * ```js
9581
- * const webElement = await I.grabWebElement('#button');
9582
- * ```
9583
- * @param locator - element located by CSS|XPath|strict locator.
9584
- * @returns WebElement of being used Web helper
9585
- */
9586
- grabWebElement(locator: CodeceptJS.LocatorOrString): Promise<any>;
9587
9548
  /**
9588
9549
  * Set [WebDriver timeouts](https://webdriver.io/docs/timeouts.html) in realtime.
9589
9550
  *
@@ -2823,8 +2823,11 @@ declare namespace CodeceptJS {
2823
2823
  * @property [highlightElement] - highlight the interacting elements. Default: false. Note: only activate under verbose mode (--verbose).
2824
2824
  * @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).
2825
2825
  * @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).
2826
+ * @property [customLocatorStrategies] - custom locator strategies. An object with keys as strategy names and values as JavaScript functions. Example: `{ byRole: (selector, root) => { return root.querySelector(\`[role="\${selector}\"]\`) } }`
2826
2827
  */
2827
2828
  // @ts-ignore
2829
+ // @ts-ignore
2830
+ // @ts-ignore
2828
2831
  type PlaywrightConfig = {
2829
2832
  url?: string;
2830
2833
  browser?: 'chromium' | 'firefox' | 'webkit' | 'electron';
@@ -2862,6 +2865,7 @@ declare namespace CodeceptJS {
2862
2865
  highlightElement?: boolean;
2863
2866
  recordHar?: any;
2864
2867
  testIdAttribute?: string;
2868
+ customLocatorStrategies?: any;
2865
2869
  };
2866
2870
  /**
2867
2871
  * Uses [Playwright](https://github.com/microsoft/playwright) library to run tests inside:
@@ -6353,6 +6357,8 @@ declare namespace CodeceptJS {
6353
6357
  * @property [highlightElement] - highlight the interacting elements. Default: false. Note: only activate under verbose mode (--verbose).
6354
6358
  */
6355
6359
  // @ts-ignore
6360
+ // @ts-ignore
6361
+ // @ts-ignore
6356
6362
  type PuppeteerConfig = {
6357
6363
  url: string;
6358
6364
  basicAuth?: any;
@@ -6792,17 +6798,6 @@ declare namespace CodeceptJS {
6792
6798
  * {{ react }}
6793
6799
  */
6794
6800
  _locate(): void;
6795
- /**
6796
- * Get single element by different locator types, including strict locator
6797
- * Should be used in custom helpers:
6798
- *
6799
- * ```js
6800
- * const element = await this.helpers['Puppeteer']._locateElement({name: 'password'});
6801
- * ```
6802
- *
6803
- * {{ react }}
6804
- */
6805
- _locateElement(): void;
6806
6801
  /**
6807
6802
  * Find a checkbox by providing human-readable text:
6808
6803
  * NOTE: Assumes the checkable element exists
@@ -6839,17 +6834,6 @@ declare namespace CodeceptJS {
6839
6834
  * @returns WebElement of being used Web helper
6840
6835
  */
6841
6836
  grabWebElements(locator: CodeceptJS.LocatorOrString): Promise<any>;
6842
- /**
6843
- * Grab WebElement for given locator
6844
- * Resumes test execution, so **should be used inside an async function with `await`** operator.
6845
- *
6846
- * ```js
6847
- * const webElement = await I.grabWebElement('#button');
6848
- * ```
6849
- * @param locator - element located by CSS|XPath|strict locator.
6850
- * @returns WebElement of being used Web helper
6851
- */
6852
- grabWebElement(locator: CodeceptJS.LocatorOrString): Promise<any>;
6853
6837
  /**
6854
6838
  * Switch focus to a particular tab by its number. It waits tabs loading and then switch tab
6855
6839
  *
@@ -8305,22 +8289,6 @@ declare namespace CodeceptJS {
8305
8289
  */
8306
8290
  flushWebSocketMessages(): void;
8307
8291
  }
8308
- /**
8309
- * Find elements using Puppeteer's native element discovery methods
8310
- * Note: Unlike Playwright, Puppeteer's Locator API doesn't have .all() method for multiple elements
8311
- * @param matcher - Puppeteer context to search within
8312
- * @param locator - Locator specification
8313
- * @returns Array of ElementHandle objects
8314
- */
8315
- function findElements(matcher: any, locator: any | string): Promise<any[]>;
8316
- /**
8317
- * Find a single element using Puppeteer's native element discovery methods
8318
- * Note: Puppeteer Locator API doesn't have .first() method like Playwright
8319
- * @param matcher - Puppeteer context to search within
8320
- * @param locator - Locator specification
8321
- * @returns Single ElementHandle object
8322
- */
8323
- function findElement(matcher: any, locator: any | string): Promise<object>;
8324
8292
  /**
8325
8293
  * ## Configuration
8326
8294
  * @property [endpoint] - API base URL
@@ -8334,6 +8302,8 @@ declare namespace CodeceptJS {
8334
8302
  * @property [maxUploadFileSize] - set the max content file size in MB when performing api calls.
8335
8303
  */
8336
8304
  // @ts-ignore
8305
+ // @ts-ignore
8306
+ // @ts-ignore
8337
8307
  type RESTConfig = {
8338
8308
  endpoint?: string;
8339
8309
  prettyPrintJson?: boolean;
@@ -9540,6 +9510,8 @@ declare namespace CodeceptJS {
9540
9510
  * @property [logLevel = silent] - level of logging verbosity. Default: silent. Options: trace | debug | info | warn | error | silent. More info: https://webdriver.io/docs/configuration/#loglevel
9541
9511
  */
9542
9512
  // @ts-ignore
9513
+ // @ts-ignore
9514
+ // @ts-ignore
9543
9515
  type WebDriverConfig = {
9544
9516
  url: string;
9545
9517
  browser: string;
@@ -10010,17 +9982,6 @@ declare namespace CodeceptJS {
10010
9982
  * @returns WebElement of being used Web helper
10011
9983
  */
10012
9984
  grabWebElements(locator: CodeceptJS.LocatorOrString): Promise<any>;
10013
- /**
10014
- * Grab WebElement for given locator
10015
- * Resumes test execution, so **should be used inside an async function with `await`** operator.
10016
- *
10017
- * ```js
10018
- * const webElement = await I.grabWebElement('#button');
10019
- * ```
10020
- * @param locator - element located by CSS|XPath|strict locator.
10021
- * @returns WebElement of being used Web helper
10022
- */
10023
- grabWebElement(locator: CodeceptJS.LocatorOrString): Promise<any>;
10024
9985
  /**
10025
9986
  * Set [WebDriver timeouts](https://webdriver.io/docs/timeouts.html) in realtime.
10026
9987
  *
@@ -11565,13 +11526,6 @@ declare namespace CodeceptJS {
11565
11526
  * Loads tests by pattern or by config.tests
11566
11527
  */
11567
11528
  loadTests(pattern?: string): void;
11568
- /**
11569
- * Apply sharding to test files based on shard configuration
11570
- * @param testFiles - Array of test file paths
11571
- * @param shardConfig - Shard configuration in format "index/total" (e.g., "1/4")
11572
- * @returns - Filtered array of test files for this shard
11573
- */
11574
- _applySharding(testFiles: string[], shardConfig: string): string[];
11575
11529
  /**
11576
11530
  * Run a specific test or all loaded tests.
11577
11531
  */
@@ -12065,10 +12019,6 @@ declare namespace CodeceptJS {
12065
12019
  * Get a state of current queue and tasks
12066
12020
  */
12067
12021
  toString(): string;
12068
- /**
12069
- * Get current session ID
12070
- */
12071
- getCurrentSessionId(): string | null;
12072
12022
  }
12073
12023
  interface RecorderSession {
12074
12024
  running: boolean;