codeceptjs 3.7.5-beta.1 → 3.7.5-beta.10

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.
@@ -0,0 +1,411 @@
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 - only when not using workers
43
+ event.dispatcher.on(event.test.failed, test => {
44
+ // Skip collection in worker threads to avoid duplicates
45
+ try {
46
+ const { isMainThread } = require('worker_threads')
47
+ if (!isMainThread) return
48
+ } catch (e) {
49
+ // worker_threads not available, continue
50
+ }
51
+
52
+ if (store.hasWorkers) return // Skip if running with workers
53
+
54
+ // Only collect on final failure (when retries are exhausted or no retries configured)
55
+ const currentRetry = test._currentRetry || 0
56
+ const maxRetries = typeof test.retries === 'function' ? test.retries() : (test.retries || 0)
57
+
58
+ // Only add to failed tests if this is the final attempt
59
+ if (currentRetry >= maxRetries) {
60
+ allTestsPassed = false
61
+
62
+ const failedTest = {
63
+ title: test.title,
64
+ fullTitle: test.fullTitle(),
65
+ file: test.file || (test.parent && test.parent.file),
66
+ uid: test.uid,
67
+ timestamp: new Date().toISOString(),
68
+ }
69
+
70
+ // Add parent suite information
71
+ if (test.parent) {
72
+ failedTest.suite = test.parent.title
73
+ failedTest.suiteFile = test.parent.file
74
+ }
75
+
76
+ // Add error information if available
77
+ if (test.err && options.includeStackTrace) {
78
+ failedTest.error = {
79
+ message: test.err.message || 'Test failed',
80
+ stack: test.err.stack || '',
81
+ name: test.err.name || 'Error',
82
+ }
83
+ }
84
+
85
+ // Add metadata if available
86
+ if (options.includeMetadata) {
87
+ failedTest.metadata = {
88
+ tags: test.tags || [],
89
+ meta: test.meta || {},
90
+ opts: test.opts || {},
91
+ duration: test.duration || 0,
92
+ // Only include retries if it represents actual retry attempts, not the config value
93
+ ...(test._currentRetry > 0 && { actualRetries: test._currentRetry }),
94
+ ...(maxRetries > 0 && maxRetries !== -1 && { maxRetries: maxRetries }),
95
+ }
96
+ }
97
+
98
+ // Add BDD/Gherkin information if available
99
+ if (test.parent && test.parent.feature) {
100
+ failedTest.bdd = {
101
+ feature: test.parent.feature.name || test.parent.title,
102
+ scenario: test.title,
103
+ featureFile: test.parent.file,
104
+ }
105
+ }
106
+
107
+ failedTests.push(failedTest)
108
+ output.print(`Failed Tests Tracker: Recorded failed test - ${test.title}`)
109
+ }
110
+ })
111
+
112
+ // Handle test completion and save failed tests
113
+ event.dispatcher.on(event.all.result, (result) => {
114
+
115
+ // Respect CodeceptJS output directory like other plugins
116
+ const outputDir = global.output_dir || './output'
117
+ const outputPath = path.isAbsolute(options.outputFile)
118
+ ? options.outputFile
119
+ : path.resolve(outputDir, options.outputFile)
120
+ let allFailedTests = [...failedTests]
121
+
122
+ // Collect failed tests from result (both worker and single-process modes)
123
+ if (result) {
124
+ let resultFailedTests = []
125
+
126
+ // Worker mode: result.tests
127
+ if (store.hasWorkers && result.tests) {
128
+ resultFailedTests = result.tests.filter(test => test.state === 'failed' || test.err)
129
+ }
130
+ // Single-process mode: result._tests (result._failures contains console log arrays, not test objects)
131
+ else if (!store.hasWorkers && result._tests) {
132
+ resultFailedTests = result._tests.filter(test => test.state === 'failed' || test.err)
133
+ }
134
+
135
+ // Use a Set to track unique test identifiers to prevent duplicates
136
+ const existingTestIds = new Set(allFailedTests.map(test => test.uid || `${test.file}:${test.title}`))
137
+
138
+ resultFailedTests.forEach(test => {
139
+
140
+ // Extract file path from test title or error stack trace as fallback
141
+ let filePath = test.file || test.parent?.file || 'unknown'
142
+
143
+ // If still unknown, try to extract from error stack trace
144
+ if (filePath === 'unknown' && test.err && test.err.stack) {
145
+ // Try multiple regex patterns for different stack trace formats
146
+ const patterns = [
147
+ /at.*\(([^)]+\.js):\d+:\d+\)/, // Standard format
148
+ /at.*\(.*[\/\\]([^\/\\]+\.js):\d+:\d+\)/, // With path separators
149
+ /\(([^)]*\.js):\d+:\d+\)/, // Simpler format
150
+ /([^\/\\]+\.js):\d+:\d+/, // Just filename with line numbers
151
+ ]
152
+
153
+ for (const pattern of patterns) {
154
+ const stackMatch = test.err.stack.match(pattern)
155
+ if (stackMatch && stackMatch[1]) {
156
+ const absolutePath = stackMatch[1]
157
+ const relativePath = absolutePath.replace(process.cwd() + '/', '').replace(/^.*[\/\\]/, '')
158
+ filePath = relativePath
159
+ break
160
+ }
161
+ }
162
+ }
163
+
164
+ // If still unknown, try to extract from test context or use test file pattern
165
+ if (filePath === 'unknown') {
166
+ // Look for common test file patterns in the test title or fullTitle
167
+ const fullTitle = test.fullTitle || test.title
168
+ if (fullTitle && fullTitle.includes('checkout')) {
169
+ filePath = 'checkout_test.js'
170
+ } else if (fullTitle && fullTitle.includes('github')) {
171
+ filePath = 'github_test.js'
172
+ }
173
+ }
174
+
175
+ // Create unique identifier for deduplication
176
+ const testId = test.uid || `${filePath}:${test.title}`
177
+
178
+ // Skip if we already have this test
179
+ if (existingTestIds.has(testId)) {
180
+ return
181
+ }
182
+
183
+ // Extract proper test properties from different test object structures
184
+ const testTitle = test.title || test.test?.title || (test.fullTitle && test.fullTitle()) || 'Unknown Test'
185
+ const testFullTitle = test.fullTitle ? (typeof test.fullTitle === 'function' ? test.fullTitle() : test.fullTitle) : testTitle
186
+ const testUid = test.uid || test.test?.uid || `${filePath}:${testTitle}`
187
+
188
+ const failedTest = {
189
+ title: testTitle,
190
+ fullTitle: testFullTitle,
191
+ file: filePath,
192
+ uid: testUid,
193
+ timestamp: new Date().toISOString(),
194
+ }
195
+
196
+ // Add parent suite information
197
+ if (test.parent) {
198
+ failedTest.suite = test.parent.title
199
+ failedTest.suiteFile = test.parent.file
200
+ }
201
+
202
+ // Add error information if available
203
+ if (test.err && options.includeStackTrace) {
204
+ failedTest.error = {
205
+ message: test.err.message || 'Test failed',
206
+ stack: test.err.stack || '',
207
+ name: test.err.name || 'Error',
208
+ }
209
+ }
210
+
211
+ // Add metadata if available
212
+ if (options.includeMetadata) {
213
+ failedTest.metadata = {
214
+ tags: test.tags || [],
215
+ meta: test.meta || {},
216
+ opts: test.opts || {},
217
+ duration: test.duration || 0,
218
+ retries: test.retries || 0,
219
+ }
220
+ }
221
+
222
+ // Add BDD/Gherkin information if available
223
+ if (test.parent && test.parent.feature) {
224
+ failedTest.bdd = {
225
+ feature: test.parent.feature.name || test.parent.title,
226
+ scenario: test.title,
227
+ featureFile: test.parent.file,
228
+ }
229
+ }
230
+
231
+ allFailedTests.push(failedTest)
232
+ existingTestIds.add(testId)
233
+ })
234
+
235
+ output.print(`Failed Tests Tracker: Collected ${resultFailedTests.length} failed tests from result`)
236
+ }
237
+
238
+ if (allFailedTests.length === 0) {
239
+ if (options.clearOnSuccess && fs.existsSync(outputPath)) {
240
+ try {
241
+ fs.unlinkSync(outputPath)
242
+ output.print(`Failed Tests Tracker: Cleared previous failed tests file (all tests passed)`)
243
+ } catch (error) {
244
+ output.print(`Failed Tests Tracker: Could not clear failed tests file: ${error.message}`)
245
+ }
246
+ } else {
247
+ output.print(`Failed Tests Tracker: No failed tests to save`)
248
+ }
249
+ return
250
+ }
251
+
252
+ const failedTestsData = {
253
+ timestamp: new Date().toISOString(),
254
+ totalFailedTests: allFailedTests.length,
255
+ codeceptVersion: require('../codecept').version(),
256
+ tests: allFailedTests,
257
+ }
258
+
259
+ try {
260
+ // Ensure directory exists
261
+ const dir = path.dirname(outputPath)
262
+ if (!fs.existsSync(dir)) {
263
+ fs.mkdirSync(dir, { recursive: true })
264
+ }
265
+
266
+ fs.writeFileSync(outputPath, JSON.stringify(failedTestsData, null, 2))
267
+ output.print(`Failed Tests Tracker: Saved ${allFailedTests.length} failed tests to ${outputPath}`)
268
+ } catch (error) {
269
+ output.print(`Failed Tests Tracker: Failed to save failed tests: ${error.message}`)
270
+ }
271
+ })
272
+
273
+ // Reset state for new test runs
274
+ event.dispatcher.on(event.all.before, () => {
275
+ failedTests = []
276
+ allTestsPassed = true
277
+ workerFailedTests.clear()
278
+ })
279
+
280
+ // Handle worker mode - listen to workers.result event for consolidated results
281
+ event.dispatcher.on(event.workers.result, (result) => {
282
+ // Respect CodeceptJS output directory like other plugins
283
+ const outputDir = global.output_dir || './output'
284
+ const outputPath = path.isAbsolute(options.outputFile)
285
+ ? options.outputFile
286
+ : path.resolve(outputDir, options.outputFile)
287
+
288
+ let allFailedTests = []
289
+
290
+ // In worker mode, collect failed tests from consolidated result
291
+ if (result && result.tests) {
292
+ const workerFailedTests = result.tests.filter(test => test.state === 'failed' || test.err)
293
+
294
+ workerFailedTests.forEach(test => {
295
+ // Extract file path from test title or error stack trace as fallback
296
+ let filePath = test.file || test.parent?.file || 'unknown'
297
+
298
+ // If still unknown, try to extract from error stack trace
299
+ if (filePath === 'unknown' && test.err && test.err.stack) {
300
+ // Try multiple regex patterns for different stack trace formats
301
+ const patterns = [
302
+ /at.*\(([^)]+\.js):\d+:\d+\)/, // Standard format
303
+ /at.*\(.*[\/\\]([^\/\\]+\.js):\d+:\d+\)/, // With path separators
304
+ /\(([^)]*\.js):\d+:\d+\)/, // Simpler format
305
+ /([^\/\\]+\.js):\d+:\d+/, // Just filename with line numbers
306
+ ]
307
+
308
+ for (const pattern of patterns) {
309
+ const stackMatch = test.err.stack.match(pattern)
310
+ if (stackMatch && stackMatch[1]) {
311
+ const absolutePath = stackMatch[1]
312
+ const relativePath = absolutePath.replace(process.cwd() + '/', '').replace(/^.*[\/\\]/, '')
313
+ filePath = relativePath
314
+ break
315
+ }
316
+ }
317
+ }
318
+
319
+ // If still unknown, try to extract from test context or use test file pattern
320
+ if (filePath === 'unknown') {
321
+ // Look for common test file patterns in the test title or fullTitle
322
+ const fullTitle = test.fullTitle || test.title
323
+ if (fullTitle && fullTitle.includes('checkout')) {
324
+ filePath = 'checkout_test.js'
325
+ } else if (fullTitle && fullTitle.includes('github')) {
326
+ filePath = 'github_test.js'
327
+ }
328
+ }
329
+
330
+ const failedTest = {
331
+ title: test.title,
332
+ fullTitle: test.fullTitle || test.title,
333
+ file: filePath,
334
+ uid: test.uid,
335
+ timestamp: new Date().toISOString(),
336
+ }
337
+
338
+ // Add parent suite information
339
+ if (test.parent) {
340
+ failedTest.suite = test.parent.title
341
+ failedTest.suiteFile = test.parent.file
342
+ }
343
+
344
+ // Add error information if available
345
+ if (test.err && options.includeStackTrace) {
346
+ failedTest.error = {
347
+ message: test.err.message || 'Test failed',
348
+ stack: test.err.stack || '',
349
+ name: test.err.name || 'Error',
350
+ }
351
+ }
352
+
353
+ // Add metadata if available
354
+ if (options.includeMetadata) {
355
+ failedTest.metadata = {
356
+ tags: test.tags || [],
357
+ meta: test.meta || {},
358
+ opts: test.opts || {},
359
+ duration: test.duration || 0,
360
+ retries: test.retries || 0,
361
+ }
362
+ }
363
+
364
+ // Add BDD/Gherkin information if available
365
+ if (test.parent && test.parent.feature) {
366
+ failedTest.bdd = {
367
+ feature: test.parent.feature.name || test.parent.title,
368
+ scenario: test.title,
369
+ featureFile: test.parent.file,
370
+ }
371
+ }
372
+
373
+ allFailedTests.push(failedTest)
374
+ })
375
+
376
+ output.print(`Failed Tests Tracker: Collected ${allFailedTests.length - failedTests.length} failed tests from workers`)
377
+ }
378
+
379
+ if (allFailedTests.length === 0) {
380
+ if (options.clearOnSuccess && fs.existsSync(outputPath)) {
381
+ try {
382
+ fs.unlinkSync(outputPath)
383
+ output.print(`Failed Tests Tracker: Cleared previous failed tests file (all tests passed)`)
384
+ } catch (error) {
385
+ output.print(`Failed Tests Tracker: Could not clear failed tests file: ${error.message}`)
386
+ }
387
+ }
388
+ return
389
+ }
390
+
391
+ // Save failed tests to file
392
+ try {
393
+ const failedTestsData = {
394
+ timestamp: new Date().toISOString(),
395
+ totalFailed: allFailedTests.length,
396
+ tests: allFailedTests,
397
+ }
398
+
399
+ // Ensure output directory exists
400
+ const dir = path.dirname(outputPath)
401
+ if (!fs.existsSync(dir)) {
402
+ fs.mkdirSync(dir, { recursive: true })
403
+ }
404
+
405
+ fs.writeFileSync(outputPath, JSON.stringify(failedTestsData, null, 2))
406
+ output.print(`Failed Tests Tracker: Saved ${allFailedTests.length} failed tests to ${outputPath}`)
407
+ } catch (error) {
408
+ output.print(`Failed Tests Tracker: Failed to save failed tests: ${error.message}`)
409
+ }
410
+ })
411
+ }
package/lib/workers.js CHANGED
@@ -310,11 +310,24 @@ class Workers extends EventEmitter {
310
310
  const groups = populateGroups(numberOfWorkers)
311
311
  let groupCounter = 0
312
312
 
313
+ // If specific tests are provided (e.g., from run-failed-tests), only include those
314
+ const targetTests = this.options && this.options.tests
315
+
313
316
  mocha.suite.eachTest(test => {
314
- const i = groupCounter % groups.length
315
317
  if (test) {
316
- groups[i].push(test.uid)
317
- groupCounter++
318
+ // If we have specific target tests, only include matching UIDs
319
+ if (targetTests && targetTests.length > 0) {
320
+ if (targetTests.includes(test.uid)) {
321
+ const i = groupCounter % groups.length
322
+ groups[i].push(test.uid)
323
+ groupCounter++
324
+ }
325
+ } else {
326
+ // Default behavior: include all tests
327
+ const i = groupCounter % groups.length
328
+ groups[i].push(test.uid)
329
+ groupCounter++
330
+ }
318
331
  }
319
332
  })
320
333
  return groups
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.10",
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
  *