codeceptjs 4.0.0-beta.7.esm-aria → 4.0.0-beta.8.esm-aria

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.
Files changed (68) hide show
  1. package/README.md +46 -3
  2. package/bin/codecept.js +9 -0
  3. package/bin/test-server.js +64 -0
  4. package/docs/webapi/click.mustache +5 -1
  5. package/lib/ai.js +66 -102
  6. package/lib/codecept.js +99 -24
  7. package/lib/command/generate.js +33 -1
  8. package/lib/command/init.js +7 -3
  9. package/lib/command/run-workers.js +31 -2
  10. package/lib/command/run.js +15 -0
  11. package/lib/command/workers/runTests.js +331 -58
  12. package/lib/config.js +16 -5
  13. package/lib/container.js +15 -13
  14. package/lib/effects.js +1 -1
  15. package/lib/element/WebElement.js +327 -0
  16. package/lib/event.js +10 -1
  17. package/lib/helper/AI.js +11 -11
  18. package/lib/helper/ApiDataFactory.js +34 -6
  19. package/lib/helper/Appium.js +156 -42
  20. package/lib/helper/GraphQL.js +3 -3
  21. package/lib/helper/GraphQLDataFactory.js +4 -4
  22. package/lib/helper/JSONResponse.js +48 -40
  23. package/lib/helper/Mochawesome.js +24 -2
  24. package/lib/helper/Playwright.js +841 -153
  25. package/lib/helper/Puppeteer.js +263 -67
  26. package/lib/helper/REST.js +21 -0
  27. package/lib/helper/WebDriver.js +105 -16
  28. package/lib/helper/clientscripts/PollyWebDriverExt.js +1 -1
  29. package/lib/helper/extras/PlaywrightReactVueLocator.js +52 -0
  30. package/lib/helper/extras/PlaywrightRestartOpts.js +12 -1
  31. package/lib/helper/network/actions.js +8 -6
  32. package/lib/listener/config.js +11 -3
  33. package/lib/listener/enhancedGlobalRetry.js +110 -0
  34. package/lib/listener/globalTimeout.js +19 -4
  35. package/lib/listener/helpers.js +8 -2
  36. package/lib/listener/retryEnhancer.js +85 -0
  37. package/lib/listener/steps.js +12 -0
  38. package/lib/mocha/asyncWrapper.js +13 -3
  39. package/lib/mocha/cli.js +1 -1
  40. package/lib/mocha/factory.js +3 -0
  41. package/lib/mocha/gherkin.js +1 -1
  42. package/lib/mocha/test.js +6 -0
  43. package/lib/mocha/ui.js +13 -0
  44. package/lib/output.js +62 -18
  45. package/lib/plugin/coverage.js +16 -3
  46. package/lib/plugin/enhancedRetryFailedStep.js +99 -0
  47. package/lib/plugin/htmlReporter.js +3648 -0
  48. package/lib/plugin/retryFailedStep.js +1 -0
  49. package/lib/plugin/stepByStepReport.js +1 -1
  50. package/lib/recorder.js +28 -3
  51. package/lib/result.js +100 -23
  52. package/lib/retryCoordinator.js +207 -0
  53. package/lib/step/base.js +1 -1
  54. package/lib/step/comment.js +2 -2
  55. package/lib/step/meta.js +1 -1
  56. package/lib/template/heal.js +1 -1
  57. package/lib/template/prompts/generatePageObject.js +31 -0
  58. package/lib/template/prompts/healStep.js +13 -0
  59. package/lib/template/prompts/writeStep.js +9 -0
  60. package/lib/test-server.js +334 -0
  61. package/lib/utils/mask_data.js +47 -0
  62. package/lib/utils.js +87 -6
  63. package/lib/workerStorage.js +2 -1
  64. package/lib/workers.js +179 -23
  65. package/package.json +58 -47
  66. package/typings/index.d.ts +19 -7
  67. package/typings/promiseBasedTypes.d.ts +5525 -3759
  68. package/typings/types.d.ts +5791 -3781
@@ -3,6 +3,7 @@ let webdriverio
3
3
  import assert from 'assert'
4
4
  import path from 'path'
5
5
  import crypto from 'crypto'
6
+
6
7
  import Helper from '@codeceptjs/helper'
7
8
  import promiseRetry from 'promise-retry'
8
9
  import { includes as stringIncludes } from '../assert/include.js'
@@ -22,6 +23,7 @@ import { focusElement } from './scripts/focusElement.js'
22
23
  import { blurElement } from './scripts/blurElement.js'
23
24
  import { dontSeeElementError, seeElementError, seeElementInDOMError, dontSeeElementInDOMError } from './errors/ElementAssertion.js'
24
25
  import { dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics } from './network/actions.js'
26
+ import WebElement from '../element/WebElement.js'
25
27
 
26
28
  const SHADOW = 'shadow'
27
29
  const webRoot = 'body'
@@ -504,7 +506,7 @@ class WebDriver extends Helper {
504
506
  }
505
507
  config.capabilities.browserName = config.browser || config.capabilities.browserName
506
508
 
507
- // WebDriver Bidi Protocol. Default: false
509
+ // WebDriver Bidi Protocol. Default: true
508
510
  config.capabilities.webSocketUrl = config.bidiProtocol ?? config.capabilities.webSocketUrl ?? true
509
511
 
510
512
  config.capabilities.browserVersion = config.browserVersion || config.capabilities.browserVersion
@@ -656,8 +658,11 @@ class WebDriver extends Helper {
656
658
 
657
659
  this.browser.on('dialog', () => {})
658
660
 
659
- await this.browser.sessionSubscribe({ events: ['log.entryAdded'] })
660
- this.browser.on('log.entryAdded', logEvents)
661
+ // Check for Bidi, because "sessionSubscribe" is an exclusive Bidi protocol feature. Otherwise, error will be thrown.
662
+ if (this.browser.capabilities && this.browser.capabilities.webSocketUrl) {
663
+ await this.browser.sessionSubscribe({ events: ['log.entryAdded'] })
664
+ this.browser.on('log.entryAdded', logEvents)
665
+ }
661
666
 
662
667
  return this.browser
663
668
  }
@@ -1004,7 +1009,20 @@ class WebDriver extends Helper {
1004
1009
  *
1005
1010
  */
1006
1011
  async grabWebElements(locator) {
1007
- return this._locate(locator)
1012
+ const elements = await this._locate(locator)
1013
+ return elements.map(element => new WebElement(element, this))
1014
+ }
1015
+
1016
+ /**
1017
+ * {{> grabWebElement }}
1018
+ *
1019
+ */
1020
+ async grabWebElement(locator) {
1021
+ const elements = await this._locate(locator)
1022
+ if (elements.length === 0) {
1023
+ throw new ElementNotFound(locator, 'Element')
1024
+ }
1025
+ return new WebElement(elements[0], this)
1008
1026
  }
1009
1027
 
1010
1028
  /**
@@ -1049,7 +1067,7 @@ class WebDriver extends Helper {
1049
1067
  * {{ react }}
1050
1068
  */
1051
1069
  async click(locator, context = null) {
1052
- const clickMethod = this.browser.isMobile && !this.browser.isW3C ? 'touchClick' : 'elementClick'
1070
+ const clickMethod = this.browser.isMobile && this.browser.capabilities.platformName !== 'android' ? 'touchClick' : 'elementClick'
1053
1071
  const locateFn = prepareLocateFn.call(this, context)
1054
1072
 
1055
1073
  const res = await findClickable.call(this, locator, locateFn)
@@ -1136,6 +1154,75 @@ class WebDriver extends Helper {
1136
1154
  await this.browser.buttonDown(2)
1137
1155
  }
1138
1156
 
1157
+ /**
1158
+ * Performs click at specific coordinates.
1159
+ * If locator is provided, the coordinates are relative to the element's top-left corner.
1160
+ * If locator is not provided, the coordinates are relative to the body element.
1161
+ *
1162
+ * ```js
1163
+ * // Click at coordinates (100, 200) relative to body
1164
+ * I.clickXY(100, 200);
1165
+ *
1166
+ * // Click at coordinates (50, 30) relative to element's top-left corner
1167
+ * I.clickXY('#someElement', 50, 30);
1168
+ * ```
1169
+ *
1170
+ * @param {CodeceptJS.LocatorOrString|number} locator Element to click on or X coordinate if no element.
1171
+ * @param {number} [x] X coordinate relative to element's top-left, or Y coordinate if locator is a number.
1172
+ * @param {number} [y] Y coordinate relative to element's top-left.
1173
+ * @returns {Promise<void>}
1174
+ */
1175
+ async clickXY(locator, x, y) {
1176
+ // If locator is a number, treat it as X coordinate and use body as base
1177
+ if (typeof locator === 'number') {
1178
+ const globalX = locator
1179
+ const globalY = x
1180
+ locator = '//body'
1181
+ x = globalX
1182
+ y = globalY
1183
+ }
1184
+
1185
+ // Locate the base element
1186
+ const res = await this._locate(withStrictLocator(locator), true)
1187
+ assertElementExists(res, locator, 'Element to click')
1188
+ const el = usingFirstElement(res)
1189
+
1190
+ // Get element position and size to calculate top-left corner
1191
+ const location = await el.getLocation()
1192
+ const size = await el.getSize()
1193
+
1194
+ // WebDriver clicks at center by default, so we need to offset from center to top-left
1195
+ // then add our desired x, y coordinates
1196
+ const offsetX = -(size.width / 2) + x
1197
+ const offsetY = -(size.height / 2) + y
1198
+
1199
+ if (this.browser.isW3C) {
1200
+ // Use performActions for W3C WebDriver
1201
+ return this.browser.performActions([
1202
+ {
1203
+ type: 'pointer',
1204
+ id: 'pointer1',
1205
+ parameters: { pointerType: 'mouse' },
1206
+ actions: [
1207
+ {
1208
+ type: 'pointerMove',
1209
+ origin: el,
1210
+ duration: 0,
1211
+ x: Math.round(offsetX),
1212
+ y: Math.round(offsetY),
1213
+ },
1214
+ { type: 'pointerDown', button: 0 },
1215
+ { type: 'pointerUp', button: 0 },
1216
+ ],
1217
+ },
1218
+ ])
1219
+ }
1220
+
1221
+ // Fallback for non-W3C browsers
1222
+ await el.moveTo({ xOffset: Math.round(offsetX), yOffset: Math.round(offsetY) })
1223
+ return el.click()
1224
+ }
1225
+
1139
1226
  /**
1140
1227
  * {{> forceRightClick }}
1141
1228
  *
@@ -1173,7 +1260,17 @@ class WebDriver extends Helper {
1173
1260
  assertElementExists(res, field, 'Field')
1174
1261
  const elem = usingFirstElement(res)
1175
1262
  highlightActiveElement.call(this, elem)
1176
- await elem.clearValue()
1263
+ try {
1264
+ await elem.clearValue()
1265
+ } catch (err) {
1266
+ if (err.message && err.message.includes('invalid element state')) {
1267
+ await this.executeScript(el => {
1268
+ el.value = ''
1269
+ }, elem)
1270
+ } else {
1271
+ throw err
1272
+ }
1273
+ }
1177
1274
  await elem.setValue(value.toString())
1178
1275
  }
1179
1276
 
@@ -1268,7 +1365,7 @@ class WebDriver extends Helper {
1268
1365
  * {{> checkOption }}
1269
1366
  */
1270
1367
  async checkOption(field, context = null) {
1271
- const clickMethod = this.browser.isMobile && !this.browser.isW3C ? 'touchClick' : 'elementClick'
1368
+ const clickMethod = this.browser.isMobile && this.browser.capabilities.platformName !== 'android' ? 'touchClick' : 'elementClick'
1272
1369
  const locateFn = prepareLocateFn.call(this, context)
1273
1370
 
1274
1371
  const res = await findCheckable.call(this, field, locateFn)
@@ -1289,7 +1386,7 @@ class WebDriver extends Helper {
1289
1386
  * {{> uncheckOption }}
1290
1387
  */
1291
1388
  async uncheckOption(field, context = null) {
1292
- const clickMethod = this.browser.isMobile && !this.browser.isW3C ? 'touchClick' : 'elementClick'
1389
+ const clickMethod = this.browser.isMobile && this.browser.capabilities.platformName !== 'android' ? 'touchClick' : 'elementClick'
1293
1390
  const locateFn = prepareLocateFn.call(this, context)
1294
1391
 
1295
1392
  const res = await findCheckable.call(this, field, locateFn)
@@ -2912,14 +3009,6 @@ async function findFields(locator) {
2912
3009
  let els = await this._locate(Locator.field.labelEquals(literal))
2913
3010
  if (els.length) return els
2914
3011
 
2915
- // Try ARIA selector for accessible name
2916
- try {
2917
- els = await this._locate(`aria/${locator.value}`)
2918
- if (els.length) return els
2919
- } catch (e) {
2920
- // ARIA selector not supported or failed
2921
- }
2922
-
2923
3012
  els = await this._locate(Locator.field.labelContains(literal))
2924
3013
  if (els.length) return els
2925
3014
 
@@ -108,4 +108,4 @@ const pollyWebDriver = {
108
108
  },
109
109
  };
110
110
 
111
- module.exports = pollyWebDriver;
111
+ export default pollyWebDriver;
@@ -0,0 +1,52 @@
1
+ async function findReact(matcher, locator) {
2
+ // Handle both Locator objects and raw locator objects
3
+ const reactLocator = locator.locator || locator
4
+ let _locator = `_react=${reactLocator.react}`;
5
+ let props = '';
6
+
7
+ if (reactLocator.props) {
8
+ props += propBuilder(reactLocator.props);
9
+ _locator += props;
10
+ }
11
+ return matcher.locator(_locator).all();
12
+ }
13
+
14
+ async function findVue(matcher, locator) {
15
+ // Handle both Locator objects and raw locator objects
16
+ const vueLocator = locator.locator || locator
17
+ let _locator = `_vue=${vueLocator.vue}`;
18
+ let props = '';
19
+
20
+ if (vueLocator.props) {
21
+ props += propBuilder(vueLocator.props);
22
+ _locator += props;
23
+ }
24
+ return matcher.locator(_locator).all();
25
+ }
26
+
27
+ async function findByPlaywrightLocator(matcher, locator) {
28
+ // Handle both Locator objects and raw locator objects
29
+ const pwLocator = locator.locator || locator
30
+ if (pwLocator && pwLocator.toString && pwLocator.toString().includes(process.env.testIdAttribute)) {
31
+ return matcher.getByTestId(pwLocator.pw.value.split('=')[1]);
32
+ }
33
+ const pwValue = typeof pwLocator.pw === 'string' ? pwLocator.pw : pwLocator.pw
34
+ return matcher.locator(pwValue).all();
35
+ }
36
+
37
+ function propBuilder(props) {
38
+ let _props = '';
39
+
40
+ for (const [key, value] of Object.entries(props)) {
41
+ if (typeof value === 'object') {
42
+ for (const [k, v] of Object.entries(value)) {
43
+ _props += `[${key}.${k} = "${v}"]`;
44
+ }
45
+ } else {
46
+ _props += `[${key} = "${value}"]`;
47
+ }
48
+ }
49
+ return _props;
50
+ }
51
+
52
+ export { findReact, findVue, findByPlaywrightLocator };
@@ -1,6 +1,7 @@
1
1
  const RESTART_OPTS = {
2
2
  session: 'keep',
3
3
  context: false,
4
+ browser: true,
4
5
  }
5
6
 
6
7
  let restarts = null
@@ -19,9 +20,15 @@ export function setRestartStrategy(options) {
19
20
  return
20
21
  }
21
22
 
23
+ // When restart is true, map to 'browser' restart
24
+ if (restart === true) {
25
+ restarts = 'browser'
26
+ return
27
+ }
28
+
22
29
  restarts = Object.keys(RESTART_OPTS).find(key => RESTART_OPTS[key] === restart)
23
30
 
24
- if (restarts === null || restarts === undefined) throw new Error('No restart strategy set, use the following values for restart: session, context')
31
+ if (restarts === null || restarts === undefined) throw new Error('No restart strategy set, use the following values for restart: session, context, browser')
25
32
  }
26
33
 
27
34
  export function restartsSession() {
@@ -31,3 +38,7 @@ export function restartsSession() {
31
38
  export function restartsContext() {
32
39
  return restarts === 'context'
33
40
  }
41
+
42
+ export function restartsBrowser() {
43
+ return restarts === 'browser'
44
+ }
@@ -28,8 +28,8 @@ async function seeTraffic({ name, url, parameters, requestPostData, timeout = 10
28
28
  throw new Error('Missing required key "url" in object given to "I.seeTraffic".')
29
29
  }
30
30
 
31
- if (!this.recording || !this.recordedAtLeastOnce) {
32
- throw new Error('Failure in test automation. You use "I.seeTraffic", but "I.startRecordingTraffic" was never called before.')
31
+ if (!this.recordedAtLeastOnce) {
32
+ throw new Error('Failure in test automation. You use "I.seeTraffic", but "I.startRecordingTraffic" was never called before.');
33
33
  }
34
34
 
35
35
  for (let i = 0; i <= timeout * 2; i++) {
@@ -57,8 +57,8 @@ async function seeTraffic({ name, url, parameters, requestPostData, timeout = 10
57
57
  }
58
58
 
59
59
  async function grabRecordedNetworkTraffics() {
60
- if (!this.recording || !this.recordedAtLeastOnce) {
61
- throw new Error('Failure in test automation. You use "I.grabRecordedNetworkTraffics", but "I.startRecordingTraffic" was never called before.')
60
+ if (!this.recordedAtLeastOnce) {
61
+ throw new Error('Failure in test automation. You use "I.grabRecordedNetworkTraffics", but "I.startRecordingTraffic" was never called before.');
62
62
  }
63
63
 
64
64
  const promises = this.requests.map(async request => {
@@ -97,8 +97,10 @@ async function grabRecordedNetworkTraffics() {
97
97
 
98
98
  function stopRecordingTraffic() {
99
99
  // @ts-ignore
100
- this.page.removeAllListeners('request')
101
- this.recording = false
100
+ this.page.removeAllListeners('request');
101
+ // @ts-ignore
102
+ this.page.removeAllListeners('requestfinished');
103
+ this.recording = false;
102
104
  }
103
105
 
104
106
  function flushNetworkTraffics() {
@@ -2,10 +2,17 @@ import event from '../event.js'
2
2
  import recorder from '../recorder.js'
3
3
  import { deepMerge, deepClone, ucfirst } from '../utils.js'
4
4
  import output from '../output.js'
5
+
5
6
  /**
6
7
  * Enable Helpers to listen to test events
7
8
  */
8
9
  export default function () {
10
+ // Use global flag to prevent duplicate initialization across module re-imports
11
+ if (global.__codeceptConfigListenerInitialized) {
12
+ return
13
+ }
14
+ global.__codeceptConfigListenerInitialized = true
15
+
9
16
  const helpers = global.container.helpers()
10
17
 
11
18
  enableDynamicConfigFor('suite')
@@ -14,7 +21,7 @@ export default function () {
14
21
  function enableDynamicConfigFor(type) {
15
22
  event.dispatcher.on(event[type].before, (context = {}) => {
16
23
  function updateHelperConfig(helper, config) {
17
- const oldConfig = { ...helper.options }
24
+ const oldConfig = deepClone(helper.options)
18
25
  try {
19
26
  helper._setConfig(deepMerge(deepClone(oldConfig), config))
20
27
  output.debug(`[${ucfirst(type)} Config] ${helper.constructor.name} ${JSON.stringify(config)}`)
@@ -22,10 +29,11 @@ export default function () {
22
29
  recorder.throw(err)
23
30
  return
24
31
  }
25
- event.dispatcher.once(event[type].after, () => {
32
+ const restoreCallback = () => {
26
33
  helper._setConfig(oldConfig)
27
34
  output.debug(`[${ucfirst(type)} Config] Reverted for ${helper.constructor.name}`)
28
- })
35
+ }
36
+ event.dispatcher.once(event[type].after, restoreCallback)
29
37
  }
30
38
 
31
39
  // change config
@@ -0,0 +1,110 @@
1
+ import event from '../event.js'
2
+ import output from '../output.js'
3
+ import Config from '../config.js'
4
+ import { isNotSet } from '../utils.js'
5
+
6
+ const hooks = ['Before', 'After', 'BeforeSuite', 'AfterSuite']
7
+
8
+ /**
9
+ * Priority levels for retry mechanisms (higher number = higher priority)
10
+ * This ensures consistent behavior when multiple retry mechanisms are active
11
+ */
12
+ const RETRY_PRIORITIES = {
13
+ MANUAL_STEP: 100, // I.retry() or step.retry() - highest priority
14
+ STEP_PLUGIN: 50, // retryFailedStep plugin
15
+ SCENARIO_CONFIG: 30, // Global scenario retry config
16
+ FEATURE_CONFIG: 20, // Global feature retry config
17
+ HOOK_CONFIG: 10, // Hook retry config - lowest priority
18
+ }
19
+
20
+ /**
21
+ * Enhanced global retry mechanism that coordinates with other retry types
22
+ */
23
+ export default function () {
24
+ event.dispatcher.on(event.suite.before, suite => {
25
+ let retryConfig = Config.get('retry')
26
+ if (!retryConfig) return
27
+
28
+ if (Number.isInteger(+retryConfig)) {
29
+ // is number - apply as feature-level retry
30
+ const retryNum = +retryConfig
31
+ output.log(`[Global Retry] Feature retries: ${retryNum}`)
32
+
33
+ // Only set if not already set by higher priority mechanism
34
+ if (isNotSet(suite.retries())) {
35
+ suite.retries(retryNum)
36
+ suite.opts.retryPriority = RETRY_PRIORITIES.FEATURE_CONFIG
37
+ }
38
+ return
39
+ }
40
+
41
+ if (!Array.isArray(retryConfig)) {
42
+ retryConfig = [retryConfig]
43
+ }
44
+
45
+ for (const config of retryConfig) {
46
+ if (config.grep) {
47
+ if (!suite.title.includes(config.grep)) continue
48
+ }
49
+
50
+ // Handle hook retries with priority awareness
51
+ hooks
52
+ .filter(hook => !!config[hook])
53
+ .forEach(hook => {
54
+ const retryKey = `retry${hook}`
55
+ if (isNotSet(suite.opts[retryKey])) {
56
+ suite.opts[retryKey] = config[hook]
57
+ suite.opts[`${retryKey}Priority`] = RETRY_PRIORITIES.HOOK_CONFIG
58
+ }
59
+ })
60
+
61
+ // Handle feature-level retries
62
+ if (config.Feature) {
63
+ if (isNotSet(suite.retries()) || (suite.opts.retryPriority || 0) <= RETRY_PRIORITIES.FEATURE_CONFIG) {
64
+ suite.retries(config.Feature)
65
+ suite.opts.retryPriority = RETRY_PRIORITIES.FEATURE_CONFIG
66
+ output.log(`[Global Retry] Feature retries: ${config.Feature}`)
67
+ }
68
+ }
69
+ }
70
+ })
71
+
72
+ event.dispatcher.on(event.test.before, test => {
73
+ let retryConfig = Config.get('retry')
74
+ if (!retryConfig) return
75
+
76
+ if (Number.isInteger(+retryConfig)) {
77
+ // Only set if not already set by higher priority mechanism
78
+ if (test.retries() === -1) {
79
+ test.retries(retryConfig)
80
+ test.opts.retryPriority = RETRY_PRIORITIES.SCENARIO_CONFIG
81
+ output.log(`[Global Retry] Scenario retries: ${retryConfig}`)
82
+ }
83
+ return
84
+ }
85
+
86
+ if (!Array.isArray(retryConfig)) {
87
+ retryConfig = [retryConfig]
88
+ }
89
+
90
+ retryConfig = retryConfig.filter(config => !!config.Scenario)
91
+
92
+ for (const config of retryConfig) {
93
+ if (config.grep) {
94
+ if (!test.fullTitle().includes(config.grep)) continue
95
+ }
96
+
97
+ if (config.Scenario) {
98
+ // Respect priority system
99
+ if (test.retries() === -1 || (test.opts.retryPriority || 0) <= RETRY_PRIORITIES.SCENARIO_CONFIG) {
100
+ test.retries(config.Scenario)
101
+ test.opts.retryPriority = RETRY_PRIORITIES.SCENARIO_CONFIG
102
+ output.log(`[Global Retry] Scenario retries: ${config.Scenario}`)
103
+ }
104
+ }
105
+ }
106
+ })
107
+ }
108
+
109
+ // Export priority constants for use by other retry mechanisms
110
+ export { RETRY_PRIORITIES }
@@ -21,14 +21,29 @@ export default function () {
21
21
 
22
22
  // disable timeout for BeforeSuite/AfterSuite hooks
23
23
  // add separate configs to them?
24
+ // When a BeforeSuite/AfterSuite hook starts we want to disable the
25
+ // per-test timeout during that hook execution only. Previously the
26
+ // code cleared `suiteTimeout` permanently which caused the suite
27
+ // level timeout to be lost for subsequent tests. Save previous
28
+ // values and restore them when the hook finishes.
29
+ let __prevTimeout = undefined
30
+ let __prevSuiteTimeout = undefined
31
+
24
32
  event.dispatcher.on(event.hook.started, hook => {
25
- if (hook instanceof BeforeSuiteHook) {
33
+ if (hook instanceof BeforeSuiteHook || hook instanceof AfterSuiteHook) {
34
+ __prevTimeout = timeout
35
+ // copy array to preserve original values
36
+ __prevSuiteTimeout = suiteTimeout.slice()
26
37
  timeout = null
27
38
  suiteTimeout = []
28
39
  }
29
- if (hook instanceof AfterSuiteHook) {
30
- timeout = null
31
- suiteTimeout = []
40
+ })
41
+
42
+ event.dispatcher.on(event.hook.finished, hook => {
43
+ if (hook instanceof BeforeSuiteHook || hook instanceof AfterSuiteHook) {
44
+ // restore previously stored values
45
+ timeout = __prevTimeout
46
+ suiteTimeout = __prevSuiteTimeout.slice()
32
47
  }
33
48
  })
34
49
 
@@ -74,7 +74,10 @@ export default function () {
74
74
 
75
75
  event.dispatcher.on(event.all.result, () => {
76
76
  // Skip _finishTest for all helpers if any browser helper restarts to avoid double cleanup
77
- const hasBrowserRestart = Object.values(helpers).some(helper => (helper.config && helper.config.restart) || (helper.options && helper.options.restart === 'context'))
77
+ const hasBrowserRestart = Object.values(helpers).some(helper =>
78
+ (helper.config && (helper.config.restart === 'browser' || helper.config.restart === 'context' || helper.config.restart === true)) ||
79
+ (helper.options && (helper.options.restart === 'browser' || helper.options.restart === 'context' || helper.options.restart === true))
80
+ )
78
81
 
79
82
  Object.keys(helpers).forEach(key => {
80
83
  const helper = helpers[key]
@@ -86,7 +89,10 @@ export default function () {
86
89
 
87
90
  event.dispatcher.on(event.all.after, () => {
88
91
  // Skip _cleanup for all helpers if any browser helper restarts to avoid double cleanup
89
- const hasBrowserRestart = Object.values(helpers).some(helper => (helper.config && helper.config.restart) || (helper.options && helper.options.restart === 'context'))
92
+ const hasBrowserRestart = Object.values(helpers).some(helper =>
93
+ (helper.config && (helper.config.restart === 'browser' || helper.config.restart === 'context' || helper.config.restart === true)) ||
94
+ (helper.options && (helper.options.restart === 'browser' || helper.options.restart === 'context' || helper.options.restart === true))
95
+ )
90
96
 
91
97
  Object.keys(helpers).forEach(key => {
92
98
  const helper = helpers[key]
@@ -0,0 +1,85 @@
1
+ import event from '../event.js'
2
+ import { enhanceMochaTest } from '../mocha/test.js'
3
+
4
+ /**
5
+ * Enhance retried tests by copying CodeceptJS-specific properties from the original test
6
+ * This fixes the issue where Mocha's shallow clone during retries loses CodeceptJS properties
7
+ */
8
+ export default function () {
9
+ event.dispatcher.on(event.test.before, test => {
10
+ // Check if this test is a retry (has a reference to the original test)
11
+ const originalTest = test.retriedTest && test.retriedTest()
12
+
13
+ if (originalTest) {
14
+ // This is a retried test - copy CodeceptJS-specific properties from the original
15
+ copyCodeceptJSProperties(originalTest, test)
16
+
17
+ // Ensure the test is enhanced with CodeceptJS functionality
18
+ enhanceMochaTest(test)
19
+ }
20
+ })
21
+ }
22
+
23
+ /**
24
+ * Copy CodeceptJS-specific properties from the original test to the retried test
25
+ * @param {CodeceptJS.Test} originalTest - The original test object
26
+ * @param {CodeceptJS.Test} retriedTest - The retried test object
27
+ */
28
+ function copyCodeceptJSProperties(originalTest, retriedTest) {
29
+ // Copy CodeceptJS-specific properties
30
+ if (originalTest.opts !== undefined) {
31
+ retriedTest.opts = originalTest.opts ? { ...originalTest.opts } : {}
32
+ }
33
+
34
+ if (originalTest.tags !== undefined) {
35
+ retriedTest.tags = originalTest.tags ? [...originalTest.tags] : []
36
+ }
37
+
38
+ if (originalTest.notes !== undefined) {
39
+ retriedTest.notes = originalTest.notes ? [...originalTest.notes] : []
40
+ }
41
+
42
+ if (originalTest.meta !== undefined) {
43
+ retriedTest.meta = originalTest.meta ? { ...originalTest.meta } : {}
44
+ }
45
+
46
+ if (originalTest.artifacts !== undefined) {
47
+ retriedTest.artifacts = originalTest.artifacts ? [...originalTest.artifacts] : []
48
+ }
49
+
50
+ if (originalTest.steps !== undefined) {
51
+ retriedTest.steps = originalTest.steps ? [...originalTest.steps] : []
52
+ }
53
+
54
+ if (originalTest.config !== undefined) {
55
+ retriedTest.config = originalTest.config ? { ...originalTest.config } : {}
56
+ }
57
+
58
+ if (originalTest.inject !== undefined) {
59
+ retriedTest.inject = originalTest.inject ? { ...originalTest.inject } : {}
60
+ }
61
+
62
+ // Copy methods that might be missing
63
+ if (originalTest.addNote && !retriedTest.addNote) {
64
+ retriedTest.addNote = function (type, note) {
65
+ this.notes = this.notes || []
66
+ this.notes.push({ type, text: note })
67
+ }
68
+ }
69
+
70
+ if (originalTest.applyOptions && !retriedTest.applyOptions) {
71
+ retriedTest.applyOptions = originalTest.applyOptions.bind(retriedTest)
72
+ }
73
+
74
+ if (originalTest.simplify && !retriedTest.simplify) {
75
+ retriedTest.simplify = originalTest.simplify.bind(retriedTest)
76
+ }
77
+
78
+ // Preserve the uid if it exists
79
+ if (originalTest.uid !== undefined) {
80
+ retriedTest.uid = originalTest.uid
81
+ }
82
+
83
+ // Mark as enhanced
84
+ retriedTest.codeceptjs = true
85
+ }
@@ -4,10 +4,14 @@ import event from '../event.js'
4
4
  import store from '../store.js'
5
5
  import output from '../output.js'
6
6
  import { BeforeHook, AfterHook, BeforeSuiteHook, AfterSuiteHook } from '../mocha/hooks.js'
7
+ import recorder from '../recorder.js'
7
8
 
8
9
  let currentTest
9
10
  let currentHook
10
11
 
12
+ // Session names that should not contribute steps to the main test trace
13
+ const EXCLUDED_SESSIONS = ['tryTo', 'hopeThat']
14
+
11
15
  /**
12
16
  * Register steps inside tests
13
17
  */
@@ -76,6 +80,14 @@ export default function () {
76
80
  return currentHook.steps.push(step)
77
81
  }
78
82
  if (!currentTest || !currentTest.steps) return
83
+
84
+ // Check if we're in a session that should be excluded from main test steps
85
+ const currentSessionId = recorder.getCurrentSessionId()
86
+ if (currentSessionId && EXCLUDED_SESSIONS.includes(currentSessionId)) {
87
+ // Skip adding this step to the main test steps
88
+ return
89
+ }
90
+
79
91
  currentTest.steps.push(step)
80
92
  })
81
93
 
@@ -117,9 +117,19 @@ export function injected(fn, suite, hookName) {
117
117
  const errHandler = err => {
118
118
  recorder.session.start('teardown')
119
119
  recorder.cleanAsyncErr()
120
- if (hookName == 'before' || hookName == 'beforeSuite') suiteTestFailedHookError(suite, err, hookName)
121
- if (hookName === 'after') suite.eachTest(test => event.emit(event.test.after, test))
122
- if (hookName === 'afterSuite') event.emit(event.suite.after, suite)
120
+ if (['before', 'beforeSuite'].includes(hookName)) {
121
+ suiteTestFailedHookError(suite, err, hookName)
122
+ }
123
+ if (hookName === 'after') {
124
+ suiteTestFailedHookError(suite, err, hookName)
125
+ suite.eachTest(test => {
126
+ event.emit(event.test.after, test)
127
+ })
128
+ }
129
+ if (hookName === 'afterSuite') {
130
+ suiteTestFailedHookError(suite, err, hookName)
131
+ event.emit(event.suite.after, suite)
132
+ }
123
133
  recorder.add(() => doneFn(err))
124
134
  }
125
135
 
package/lib/mocha/cli.js CHANGED
@@ -228,7 +228,7 @@ class Cli extends Base {
228
228
 
229
229
  // explicitly show file with error
230
230
  if (test.file) {
231
- log += `\n${output.styles.basic(figures.circle)} ${output.styles.section('File:')} ${output.styles.basic(test.file)}\n`
231
+ log += `\n${output.styles.basic(figures.circle)} ${output.styles.section('File:')} file://${test.file}\n`
232
232
  }
233
233
 
234
234
  const steps = test.steps || (test.ctx && test.ctx.test.steps)