codeceptjs 3.7.5-beta.1 → 3.7.5-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/bin/codecept.js +23 -0
- package/lib/command/run-failed-tests.js +216 -0
- package/lib/helper/Playwright.js +348 -28
- package/lib/plugin/failedTestsTracker.js +197 -0
- package/package.json +1 -1
- package/typings/promiseBasedTypes.d.ts +10 -49
- package/typings/types.d.ts +10 -60
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,216 @@
|
|
|
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
|
|
29
|
+
const failedTestsFile = options.file || './failed-tests.json'
|
|
30
|
+
const failedTestsPath = path.resolve(process.cwd(), failedTestsFile)
|
|
31
|
+
|
|
32
|
+
// Check if failed tests file exists
|
|
33
|
+
if (!fs.existsSync(failedTestsPath)) {
|
|
34
|
+
output.error(`Failed tests file not found: ${failedTestsPath}`)
|
|
35
|
+
output.print('Run tests first to generate a failed tests file, or specify a different file with --file option')
|
|
36
|
+
process.exitCode = 1
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let failedTestsData
|
|
41
|
+
try {
|
|
42
|
+
const fileContent = fs.readFileSync(failedTestsPath, 'utf8')
|
|
43
|
+
failedTestsData = JSON.parse(fileContent)
|
|
44
|
+
} catch (error) {
|
|
45
|
+
output.error(`Failed to read or parse failed tests file: ${error.message}`)
|
|
46
|
+
process.exitCode = 1
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!failedTestsData.tests || failedTestsData.tests.length === 0) {
|
|
51
|
+
output.print('No failed tests found in the file')
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
output.print(`Found ${failedTestsData.tests.length} failed tests from ${failedTestsData.timestamp}`)
|
|
56
|
+
|
|
57
|
+
// Build test patterns from failed tests
|
|
58
|
+
const testPatterns = []
|
|
59
|
+
const testsByFile = new Map()
|
|
60
|
+
|
|
61
|
+
// Group tests by file for more efficient execution
|
|
62
|
+
failedTestsData.tests.forEach(test => {
|
|
63
|
+
if (test.file) {
|
|
64
|
+
if (!testsByFile.has(test.file)) {
|
|
65
|
+
testsByFile.set(test.file, [])
|
|
66
|
+
}
|
|
67
|
+
testsByFile.get(test.file).push(test)
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
// If we have specific test files, use them
|
|
72
|
+
if (testsByFile.size > 0) {
|
|
73
|
+
for (const [file, tests] of testsByFile) {
|
|
74
|
+
if (options.grep) {
|
|
75
|
+
// If grep is specified, combine with file pattern
|
|
76
|
+
testPatterns.push(file)
|
|
77
|
+
} else {
|
|
78
|
+
// Try to be more specific with test titles if possible
|
|
79
|
+
testPatterns.push(file)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
// Fallback: use test titles with grep
|
|
84
|
+
const testTitles = failedTestsData.tests.map(test => test.title).filter(Boolean)
|
|
85
|
+
if (testTitles.length > 0) {
|
|
86
|
+
// Create a regex pattern to match any of the failed test titles
|
|
87
|
+
const grepPattern = testTitles.map(title => title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')
|
|
88
|
+
options.grep = grepPattern
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Check if user wants to run with workers
|
|
93
|
+
if (options.workers) {
|
|
94
|
+
await runWithWorkers(config, options, testPatterns, failedTestsData)
|
|
95
|
+
} else {
|
|
96
|
+
await runWithoutWorkers(config, options, testPatterns, failedTestsData, testRoot)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function runWithWorkers(config, options, testPatterns, failedTestsData) {
|
|
101
|
+
const numberOfWorkers = parseInt(options.workers, 10)
|
|
102
|
+
const overrideConfigs = tryOrDefault(() => JSON.parse(options.override || '{}'), {})
|
|
103
|
+
|
|
104
|
+
// Determine test split strategy
|
|
105
|
+
let by = 'test' // default for failed tests
|
|
106
|
+
if (options.by) {
|
|
107
|
+
by = options.by
|
|
108
|
+
} else if (options.suites) {
|
|
109
|
+
by = 'suite'
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Validate the by option
|
|
113
|
+
const validStrategies = ['test', 'suite', 'pool']
|
|
114
|
+
if (!validStrategies.includes(by)) {
|
|
115
|
+
throw new Error(`Invalid --by strategy: ${by}. Valid options are: ${validStrategies.join(', ')}`)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const workerConfig = {
|
|
119
|
+
by,
|
|
120
|
+
testConfig: options.config,
|
|
121
|
+
options,
|
|
122
|
+
selectedRuns: undefined,
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
output.print(`CodeceptJS v${require('../codecept').version()}`)
|
|
126
|
+
output.print(`Re-running ${failedTestsData.tests.length} failed tests in ${output.styles.bold(numberOfWorkers)} workers...`)
|
|
127
|
+
output.print()
|
|
128
|
+
store.hasWorkers = true
|
|
129
|
+
|
|
130
|
+
const workers = new Workers(numberOfWorkers, workerConfig)
|
|
131
|
+
workers.overrideConfig(overrideConfigs)
|
|
132
|
+
|
|
133
|
+
// Set up event listeners for worker output
|
|
134
|
+
workers.on('test.failed', test => {
|
|
135
|
+
output.test.failed(test)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
workers.on('test.passed', test => {
|
|
139
|
+
output.test.passed(test)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
workers.on('test.skipped', test => {
|
|
143
|
+
output.test.skipped(test)
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
workers.on('all.result', result => {
|
|
147
|
+
workers.printResults()
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
if (options.verbose || options.debug) store.debugMode = true
|
|
152
|
+
|
|
153
|
+
if (options.verbose) {
|
|
154
|
+
output.print('\nFailed tests to re-run with workers:')
|
|
155
|
+
failedTestsData.tests.forEach((test, index) => {
|
|
156
|
+
output.print(` ${index + 1}. ${test.fullTitle || test.title} (${test.file || 'unknown file'})`)
|
|
157
|
+
if (test.error && test.error.message) {
|
|
158
|
+
output.print(` Error: ${test.error.message}`)
|
|
159
|
+
}
|
|
160
|
+
})
|
|
161
|
+
output.print('')
|
|
162
|
+
|
|
163
|
+
const { getMachineInfo } = require('./info')
|
|
164
|
+
await getMachineInfo()
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
await workers.bootstrapAll()
|
|
168
|
+
await workers.run()
|
|
169
|
+
} catch (err) {
|
|
170
|
+
printError(err)
|
|
171
|
+
process.exitCode = 1
|
|
172
|
+
} finally {
|
|
173
|
+
await workers.teardownAll()
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function runWithoutWorkers(config, options, testPatterns, failedTestsData, testRoot) {
|
|
178
|
+
const codecept = new Codecept(config, options)
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
codecept.init(testRoot)
|
|
182
|
+
await codecept.bootstrap()
|
|
183
|
+
|
|
184
|
+
// Load tests - if we have specific patterns, use them, otherwise load all and filter with grep
|
|
185
|
+
if (testPatterns.length > 0) {
|
|
186
|
+
codecept.loadTests(testPatterns.join(' '))
|
|
187
|
+
} else {
|
|
188
|
+
codecept.loadTests()
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (options.verbose) {
|
|
192
|
+
global.debugMode = true
|
|
193
|
+
const { getMachineInfo } = require('./info')
|
|
194
|
+
await getMachineInfo()
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Display information about what we're running
|
|
198
|
+
if (options.verbose) {
|
|
199
|
+
output.print('\nFailed tests to re-run:')
|
|
200
|
+
failedTestsData.tests.forEach((test, index) => {
|
|
201
|
+
output.print(` ${index + 1}. ${test.fullTitle || test.title} (${test.file || 'unknown file'})`)
|
|
202
|
+
if (test.error && test.error.message) {
|
|
203
|
+
output.print(` Error: ${test.error.message}`)
|
|
204
|
+
}
|
|
205
|
+
})
|
|
206
|
+
output.print('')
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
await codecept.run()
|
|
210
|
+
} catch (err) {
|
|
211
|
+
printError(err)
|
|
212
|
+
process.exitCode = 1
|
|
213
|
+
} finally {
|
|
214
|
+
await codecept.teardown()
|
|
215
|
+
}
|
|
216
|
+
}
|
package/lib/helper/Playwright.js
CHANGED
|
@@ -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
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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
|
-
|
|
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
|
|
2070
|
-
|
|
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
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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,197 @@
|
|
|
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
|
+
const outputPath = path.resolve(process.cwd(), options.outputFile)
|
|
96
|
+
let allFailedTests = [...failedTests]
|
|
97
|
+
|
|
98
|
+
// In worker mode, collect failed tests from consolidated result
|
|
99
|
+
if (store.hasWorkers && result && result.tests) {
|
|
100
|
+
const workerFailedTests = result.tests.filter(test => test.state === 'failed' || test.err)
|
|
101
|
+
|
|
102
|
+
workerFailedTests.forEach(test => {
|
|
103
|
+
const failedTest = {
|
|
104
|
+
title: test.title,
|
|
105
|
+
fullTitle: test.fullTitle || test.title,
|
|
106
|
+
file: test.file || (test.parent && test.parent.file),
|
|
107
|
+
uid: test.uid,
|
|
108
|
+
timestamp: new Date().toISOString(),
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Add parent suite information
|
|
112
|
+
if (test.parent) {
|
|
113
|
+
failedTest.suite = test.parent.title
|
|
114
|
+
failedTest.suiteFile = test.parent.file
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Add error information if available
|
|
118
|
+
if (test.err && options.includeStackTrace) {
|
|
119
|
+
failedTest.error = {
|
|
120
|
+
message: test.err.message || 'Test failed',
|
|
121
|
+
stack: test.err.stack || '',
|
|
122
|
+
name: test.err.name || 'Error',
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Add metadata if available
|
|
127
|
+
if (options.includeMetadata) {
|
|
128
|
+
failedTest.metadata = {
|
|
129
|
+
tags: test.tags || [],
|
|
130
|
+
meta: test.meta || {},
|
|
131
|
+
opts: test.opts || {},
|
|
132
|
+
duration: test.duration || 0,
|
|
133
|
+
retries: test.retries || 0,
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Add BDD/Gherkin information if available
|
|
138
|
+
if (test.parent && test.parent.feature) {
|
|
139
|
+
failedTest.bdd = {
|
|
140
|
+
feature: test.parent.feature.name || test.parent.title,
|
|
141
|
+
scenario: test.title,
|
|
142
|
+
featureFile: test.parent.file,
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
allFailedTests.push(failedTest)
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (allFailedTests.length === 0) {
|
|
151
|
+
if (options.clearOnSuccess && fs.existsSync(outputPath)) {
|
|
152
|
+
try {
|
|
153
|
+
fs.unlinkSync(outputPath)
|
|
154
|
+
output.print(`Failed Tests Tracker: Cleared previous failed tests file (all tests passed)`)
|
|
155
|
+
} catch (error) {
|
|
156
|
+
output.print(`Failed Tests Tracker: Could not clear failed tests file: ${error.message}`)
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
output.print(`Failed Tests Tracker: No failed tests to save`)
|
|
160
|
+
}
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const failedTestsData = {
|
|
165
|
+
timestamp: new Date().toISOString(),
|
|
166
|
+
totalFailedTests: allFailedTests.length,
|
|
167
|
+
codeceptVersion: require('../codecept').version(),
|
|
168
|
+
tests: allFailedTests,
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
// Ensure directory exists
|
|
173
|
+
const dir = path.dirname(outputPath)
|
|
174
|
+
if (!fs.existsSync(dir)) {
|
|
175
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
fs.writeFileSync(outputPath, JSON.stringify(failedTestsData, null, 2))
|
|
179
|
+
output.print(`Failed Tests Tracker: Saved ${allFailedTests.length} failed tests to ${outputPath}`)
|
|
180
|
+
} catch (error) {
|
|
181
|
+
output.print(`Failed Tests Tracker: Failed to save failed tests: ${error.message}`)
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
// Reset state for new test runs
|
|
186
|
+
event.dispatcher.on(event.all.before, () => {
|
|
187
|
+
failedTests = []
|
|
188
|
+
allTestsPassed = true
|
|
189
|
+
workerFailedTests.clear()
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
// Handle worker mode - listen to workers.result event for consolidated results
|
|
193
|
+
event.dispatcher.on(event.workers.result, (result) => {
|
|
194
|
+
// This event is fired in worker mode with consolidated results
|
|
195
|
+
// The event.all.result handler above will process the consolidated data
|
|
196
|
+
})
|
|
197
|
+
}
|
package/package.json
CHANGED
|
@@ -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
|
*
|
package/typings/types.d.ts
CHANGED
|
@@ -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;
|