codeceptjs 3.7.3 → 3.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/codecept.js CHANGED
@@ -164,6 +164,7 @@ program
164
164
  .option('--tests', 'run only JS test files and skip features')
165
165
  .option('--no-timeouts', 'disable all timeouts')
166
166
  .option('-p, --plugins <k=v,k2=v2,...>', 'enable plugins, comma-separated')
167
+ .option('--shuffle', 'Shuffle the order in which test files run')
167
168
 
168
169
  // mocha options
169
170
  .option('--colors', 'force enabling of colors')
package/lib/codecept.js CHANGED
@@ -1,5 +1,6 @@
1
1
  const { existsSync, readFileSync } = require('fs')
2
2
  const { globSync } = require('glob')
3
+ const shuffle = require('lodash.shuffle')
3
4
  const fsPath = require('path')
4
5
  const { resolve } = require('path')
5
6
 
@@ -180,6 +181,10 @@ class Codecept {
180
181
  })
181
182
  }
182
183
  }
184
+
185
+ if (this.opts.shuffle) {
186
+ this.testFiles = shuffle(this.testFiles)
187
+ }
183
188
  }
184
189
 
185
190
  /**
package/lib/container.js CHANGED
@@ -28,6 +28,7 @@ let container = {
28
28
  translation: {},
29
29
  /** @type {Result | null} */
30
30
  result: null,
31
+ sharedKeys: new Set() // Track keys shared via share() function
31
32
  }
32
33
 
33
34
  /**
@@ -174,6 +175,7 @@ class Container {
174
175
  container.translation = loadTranslation()
175
176
  container.proxySupport = createSupportObjects(newSupport)
176
177
  container.plugins = newPlugins
178
+ container.sharedKeys = new Set() // Clear shared keys
177
179
  asyncHelperPromise = Promise.resolve()
178
180
  store.actor = null
179
181
  debug('container cleared')
@@ -197,7 +199,13 @@ class Container {
197
199
  * @param {Object} options - set {local: true} to not share among workers
198
200
  */
199
201
  static share(data, options = {}) {
200
- Container.append({ support: data })
202
+ // Instead of using append which replaces the entire container,
203
+ // directly update the support object to maintain proxy references
204
+ Object.assign(container.support, data)
205
+
206
+ // Track which keys were explicitly shared
207
+ Object.keys(data).forEach(key => container.sharedKeys.add(key))
208
+
201
209
  if (!options.local) {
202
210
  WorkerStorage.share(data)
203
211
  }
@@ -396,10 +404,11 @@ function createSupportObjects(config) {
396
404
  {},
397
405
  {
398
406
  has(target, key) {
399
- return keys.includes(key)
407
+ return keys.includes(key) || container.sharedKeys.has(key)
400
408
  },
401
409
  ownKeys() {
402
- return keys
410
+ // Return both original config keys and explicitly shared keys
411
+ return [...new Set([...keys, ...container.sharedKeys])]
403
412
  },
404
413
  getOwnPropertyDescriptor(target, prop) {
405
414
  return {
@@ -409,6 +418,10 @@ function createSupportObjects(config) {
409
418
  }
410
419
  },
411
420
  get(target, key) {
421
+ // First check if this is an explicitly shared property
422
+ if (container.sharedKeys.has(key) && key in container.support) {
423
+ return container.support[key]
424
+ }
412
425
  return lazyLoad(key)
413
426
  },
414
427
  },
@@ -1,4 +1,3 @@
1
- let addMochawesomeContext
2
1
  let currentTest
3
2
  let currentSuite
4
3
 
@@ -16,7 +15,8 @@ class Mochawesome extends Helper {
16
15
  disableScreenshots: false,
17
16
  }
18
17
 
19
- addMochawesomeContext = require('mochawesome/addContext')
18
+ this._addContext = require('mochawesome/addContext')
19
+
20
20
  this._createConfig(config)
21
21
  }
22
22
 
@@ -44,28 +44,27 @@ class Mochawesome extends Helper {
44
44
  if (this.options.disableScreenshots) return
45
45
  let fileName
46
46
  // Get proper name if we are fail on hook
47
- if (test.ctx.test.type === 'hook') {
47
+ if (test.ctx?.test?.type === 'hook') {
48
48
  currentTest = { test: test.ctx.test }
49
49
  // ignore retries if we are in hook
50
50
  test._retries = -1
51
51
  fileName = clearString(`${test.title}_${currentTest.test.title}`)
52
52
  } else {
53
53
  currentTest = { test }
54
- fileName = `${testToFileName(test)}`
54
+ fileName = testToFileName(test)
55
55
  }
56
56
  if (this.options.uniqueScreenshotNames) {
57
- const uuid = test.uuid || test.ctx.test.uuid
58
- fileName = `${fileName.substring(0, 10)}_${uuid}`
57
+ fileName = testToFileName(test, { unique: true })
59
58
  }
60
59
  if (test._retries < 1 || test._retries === test.retryNum) {
61
60
  fileName = `${fileName}.failed.png`
62
- return addMochawesomeContext(currentTest, fileName)
61
+ return this._addContext(currentTest, fileName)
63
62
  }
64
63
  }
65
64
 
66
65
  addMochawesomeContext(context) {
67
66
  if (currentTest === '') currentTest = { test: currentSuite.ctx.test }
68
- return addMochawesomeContext(currentTest, context)
67
+ return this._addContext(currentTest, context)
69
68
  }
70
69
  }
71
70
 
@@ -2377,15 +2377,19 @@ class Playwright extends Helper {
2377
2377
  if (this.options.recordVideo && this.page && this.page.video()) {
2378
2378
  test.artifacts.video = saveVideoForPage(this.page, `${test.title}.failed`)
2379
2379
  for (const sessionName in this.sessionPages) {
2380
- test.artifacts[`video_${sessionName}`] = saveVideoForPage(this.sessionPages[sessionName], `${test.title}_${sessionName}.failed`)
2380
+ if (sessionName === '') continue
2381
+ test.artifacts[`video_${sessionName}`] = saveVideoForPage(this.sessionPages[sessionName], `${sessionName}_${test.title}.failed`)
2381
2382
  }
2382
2383
  }
2383
2384
 
2384
2385
  if (this.options.trace) {
2385
2386
  test.artifacts.trace = await saveTraceForContext(this.browserContext, `${test.title}.failed`)
2386
2387
  for (const sessionName in this.sessionPages) {
2387
- if (!this.sessionPages[sessionName].context) continue
2388
- test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(this.sessionPages[sessionName].context, `${test.title}_${sessionName}.failed`)
2388
+ if (sessionName === '') continue
2389
+ const sessionPage = this.sessionPages[sessionName]
2390
+ const sessionContext = sessionPage.context()
2391
+ if (!sessionContext || !sessionContext.tracing) continue
2392
+ test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(sessionContext, `${sessionName}_${test.title}.failed`)
2389
2393
  }
2390
2394
  }
2391
2395
 
@@ -2399,7 +2403,8 @@ class Playwright extends Helper {
2399
2403
  if (this.options.keepVideoForPassedTests) {
2400
2404
  test.artifacts.video = saveVideoForPage(this.page, `${test.title}.passed`)
2401
2405
  for (const sessionName of Object.keys(this.sessionPages)) {
2402
- test.artifacts[`video_${sessionName}`] = saveVideoForPage(this.sessionPages[sessionName], `${test.title}_${sessionName}.passed`)
2406
+ if (sessionName === '') continue
2407
+ test.artifacts[`video_${sessionName}`] = saveVideoForPage(this.sessionPages[sessionName], `${sessionName}_${test.title}.passed`)
2403
2408
  }
2404
2409
  } else {
2405
2410
  this.page
@@ -2414,8 +2419,11 @@ class Playwright extends Helper {
2414
2419
  if (this.options.trace) {
2415
2420
  test.artifacts.trace = await saveTraceForContext(this.browserContext, `${test.title}.passed`)
2416
2421
  for (const sessionName in this.sessionPages) {
2417
- if (!this.sessionPages[sessionName].context) continue
2418
- test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(this.sessionPages[sessionName].context, `${test.title}_${sessionName}.passed`)
2422
+ if (sessionName === '') continue
2423
+ const sessionPage = this.sessionPages[sessionName]
2424
+ const sessionContext = sessionPage.context()
2425
+ if (!sessionContext || !sessionContext.tracing) continue
2426
+ test.artifacts[`trace_${sessionName}`] = await saveTraceForContext(sessionContext, `${sessionName}_${test.title}.passed`)
2419
2427
  }
2420
2428
  }
2421
2429
  } else {
@@ -3883,9 +3891,18 @@ function saveVideoForPage(page, name) {
3883
3891
  async function saveTraceForContext(context, name) {
3884
3892
  if (!context) return
3885
3893
  if (!context.tracing) return
3886
- const fileName = `${`${global.output_dir}${pathSeparator}trace${pathSeparator}${uuidv4()}_${clearString(name)}`.slice(0, 245)}.zip`
3887
- await context.tracing.stop({ path: fileName })
3888
- return fileName
3894
+ try {
3895
+ const fileName = `${`${global.output_dir}${pathSeparator}trace${pathSeparator}${uuidv4()}_${clearString(name)}`.slice(0, 245)}.zip`
3896
+ await context.tracing.stop({ path: fileName })
3897
+ return fileName
3898
+ } catch (err) {
3899
+ // Handle the case where tracing was not started or context is invalid
3900
+ if (err.message && err.message.includes('Must start tracing before stopping')) {
3901
+ // Tracing was never started on this context, silently skip
3902
+ return null
3903
+ }
3904
+ throw err
3905
+ }
3889
3906
  }
3890
3907
 
3891
3908
  async function highlightActiveElement(element) {
@@ -121,9 +121,19 @@ module.exports.injected = function (fn, suite, hookName) {
121
121
  const errHandler = err => {
122
122
  recorder.session.start('teardown')
123
123
  recorder.cleanAsyncErr()
124
- if (hookName == 'before' || hookName == 'beforeSuite') suiteTestFailedHookError(suite, err, hookName)
125
- if (hookName === 'after') suite.eachTest(test => event.emit(event.test.after, test))
126
- if (hookName === 'afterSuite') event.emit(event.suite.after, suite)
124
+ if (['before', 'beforeSuite'].includes(hookName)) {
125
+ suiteTestFailedHookError(suite, err, hookName)
126
+ }
127
+ if (hookName === 'after') {
128
+ suiteTestFailedHookError(suite, err, hookName)
129
+ suite.eachTest(test => {
130
+ event.emit(event.test.after, test)
131
+ })
132
+ }
133
+ if (hookName === 'afterSuite') {
134
+ suiteTestFailedHookError(suite, err, hookName)
135
+ event.emit(event.suite.after, suite)
136
+ }
127
137
  recorder.add(() => doneFn(err))
128
138
  }
129
139
 
@@ -107,7 +107,7 @@ module.exports = (text, file) => {
107
107
  )
108
108
  continue
109
109
  }
110
- if (child.scenario && (currentLanguage ? currentLanguage.contexts.ScenarioOutline.includes(child.scenario.keyword) : child.scenario.keyword === 'Scenario Outline')) {
110
+ if (child.scenario && (currentLanguage ? currentLanguage.contexts.ScenarioOutline === child.scenario.keyword : child.scenario.keyword === 'Scenario Outline')) {
111
111
  for (const examples of child.scenario.examples) {
112
112
  const fields = examples.tableHeader.cells.map(c => c.value)
113
113
  for (const example of examples.tableBody) {
package/lib/mocha/test.js CHANGED
@@ -135,9 +135,19 @@ function cloneTest(test) {
135
135
  return deserializeTest(serializeTest(test))
136
136
  }
137
137
 
138
- function testToFileName(test, suffix = '') {
138
+ /**
139
+ * Get a filename from the test object
140
+ * @param {CodeceptJS.Test} test
141
+ * @param {Object} options
142
+ * @param {string} options.suffix Add a suffix to the filename
143
+ * @param {boolean} options.unique Add a unique suffix to the file
144
+ *
145
+ * @returns {string} the filename
146
+ */
147
+ function testToFileName(test, { suffix = '', unique = false } = {}) {
139
148
  let fileName = test.title
140
149
 
150
+ if (unique) fileName = `${fileName}_${test?.uid || Math.floor(new Date().getTime() / 1000)}`
141
151
  if (suffix) fileName = `${fileName}_${suffix}`
142
152
  // remove tags with empty string (disable for now)
143
153
  // fileName = fileName.replace(/\@\w+/g, '')
@@ -151,6 +161,7 @@ function testToFileName(test, suffix = '') {
151
161
  // fileName = `${clearString(test.parent.title)}_${fileName}`
152
162
  // }
153
163
  fileName = clearString(fileName).slice(0, 100)
164
+
154
165
  return fileName
155
166
  }
156
167
 
package/lib/pause.js CHANGED
@@ -175,7 +175,12 @@ async function parseInput(cmd) {
175
175
  output.print(output.styles.success(' OK '), cmd)
176
176
  }
177
177
  if (cmd?.startsWith('I.grab')) {
178
- output.print(output.styles.debug(val))
178
+ try {
179
+ output.print(output.styles.debug(JSON.stringify(val, null, 2)))
180
+ } catch (err) {
181
+ output.print(output.styles.error(' ERROR '), 'Failed to stringify result:', err.message)
182
+ output.print(output.styles.error(' RAW VALUE '), String(val))
183
+ }
179
184
  }
180
185
 
181
186
  history.push(cmd) // add command to history when successful
@@ -7,7 +7,7 @@ let currentCommentStep
7
7
  const defaultGlobalName = '__'
8
8
 
9
9
  /**
10
- * @deprecated
10
+ * This plugin is **deprecated**, use `Section` instead.
11
11
  *
12
12
  * Add descriptive nested steps for your tests:
13
13
  *
@@ -86,7 +86,7 @@ module.exports = function (config) {
86
86
  let fileName
87
87
 
88
88
  if (options.uniqueScreenshotNames && test) {
89
- fileName = `${testToFileName(test, _getUUID(test))}.failed.png`
89
+ fileName = `${testToFileName(test, { unique: true })}.failed.png`
90
90
  } else {
91
91
  fileName = `${testToFileName(test)}.failed.png`
92
92
  }
@@ -137,12 +137,4 @@ module.exports = function (config) {
137
137
  true,
138
138
  )
139
139
  })
140
-
141
- function _getUUID(test) {
142
- if (test.uid) {
143
- return test.uid
144
- }
145
-
146
- return Math.floor(new Date().getTime() / 1000)
147
- }
148
140
  }
@@ -7,7 +7,8 @@ const invokeWorkerListeners = (workerObj) => {
7
7
  const { threadId } = workerObj;
8
8
  workerObj.on('message', (messageData) => {
9
9
  if (messageData.event === shareEvent) {
10
- share(messageData.data);
10
+ const Container = require('./container');
11
+ Container.share(messageData.data);
11
12
  }
12
13
  });
13
14
  workerObj.on('exit', () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeceptjs",
3
- "version": "3.7.3",
3
+ "version": "3.7.4",
4
4
  "description": "Supercharged End 2 End Testing Framework for NodeJS",
5
5
  "keywords": [
6
6
  "acceptance",
@@ -75,15 +75,15 @@
75
75
  "publish-beta": "./runok.js publish:next-beta-version"
76
76
  },
77
77
  "dependencies": {
78
- "@codeceptjs/configure": "1.0.3",
78
+ "@codeceptjs/configure": "1.0.6",
79
79
  "@codeceptjs/helper": "2.0.4",
80
80
  "@cucumber/cucumber-expressions": "18",
81
- "@cucumber/gherkin": "32",
81
+ "@cucumber/gherkin": "32.1.2",
82
82
  "@cucumber/messages": "27.2.0",
83
83
  "@xmldom/xmldom": "0.9.8",
84
84
  "acorn": "8.14.1",
85
85
  "arrify": "3.0.0",
86
- "axios": "1.8.3",
86
+ "axios": "1.11.0",
87
87
  "chalk": "4.1.2",
88
88
  "cheerio": "^1.0.0",
89
89
  "commander": "11.1.0",
@@ -95,83 +95,85 @@
95
95
  "figures": "3.2.0",
96
96
  "fn-args": "4.0.0",
97
97
  "fs-extra": "11.3.0",
98
- "glob": ">=9.0.0 <12",
99
98
  "fuse.js": "^7.0.0",
99
+ "glob": ">=9.0.0 <12",
100
100
  "html-minifier-terser": "7.2.0",
101
101
  "inquirer": "8.2.6",
102
102
  "invisi-data": "^1.0.0",
103
103
  "joi": "17.13.3",
104
104
  "js-beautify": "1.15.4",
105
105
  "lodash.clonedeep": "4.5.0",
106
+ "lodash.shuffle": "4.2.0",
106
107
  "lodash.merge": "4.6.2",
107
108
  "mkdirp": "3.0.1",
108
- "mocha": "11.1.0",
109
- "monocart-coverage-reports": "2.12.3",
109
+ "mocha": "11.6.0",
110
+ "monocart-coverage-reports": "2.12.6",
110
111
  "ms": "2.1.3",
111
112
  "ora-classic": "5.4.2",
112
113
  "parse-function": "5.6.10",
113
- "parse5": "7.2.1",
114
+ "parse5": "7.3.0",
114
115
  "promise-retry": "1.1.1",
115
116
  "resq": "1.11.0",
116
117
  "sprintf-js": "1.1.3",
117
118
  "uuid": "11.1.0"
118
119
  },
119
120
  "optionalDependencies": {
120
- "@codeceptjs/detox-helper": "1.1.7"
121
+ "@codeceptjs/detox-helper": "1.1.8"
121
122
  },
122
123
  "devDependencies": {
123
124
  "@apollo/server": "^4",
124
- "@codeceptjs/expect-helper": "^1.0.1",
125
+ "@codeceptjs/expect-helper": "^1.0.2",
125
126
  "@codeceptjs/mock-request": "0.3.1",
126
- "@eslint/eslintrc": "3.3.0",
127
- "@eslint/js": "9.22.0",
128
- "@faker-js/faker": "9.6.0",
127
+ "@eslint/eslintrc": "3.3.1",
128
+ "@eslint/js": "9.31.0",
129
+ "@faker-js/faker": "9.8.0",
129
130
  "@pollyjs/adapter-puppeteer": "6.0.6",
130
131
  "@pollyjs/core": "6.0.6",
131
- "@types/chai": "5.2.0",
132
+ "@types/chai": "5.2.2",
132
133
  "@types/inquirer": "9.0.7",
133
- "@types/node": "22.13.10",
134
- "@wdio/sauce-service": "9.12.0",
134
+ "@types/node": "24.0.10",
135
+ "@wdio/sauce-service": "9.12.5",
135
136
  "@wdio/selenium-standalone-service": "8.15.0",
136
- "@wdio/utils": "9.11.0",
137
+ "@wdio/utils": "9.15.0",
137
138
  "@xmldom/xmldom": "0.9.8",
138
139
  "chai": "^4.0.0",
139
140
  "chai-as-promised": "7.1.2",
140
141
  "chai-subset": "1.6.0",
141
142
  "documentation": "14.0.3",
142
- "electron": "35.0.1",
143
- "eslint": "^9.21.0",
144
- "eslint-plugin-import": "2.31.0",
145
- "eslint-plugin-mocha": "10.5.0",
146
- "expect": "29.7.0",
147
- "express": "4.21.2",
148
- "globals": "16.0.0",
149
- "graphql": "16.10.0",
143
+ "electron": "37.2.3",
144
+ "eslint": "^9.24.0",
145
+ "eslint-plugin-import": "2.32.0",
146
+ "eslint-plugin-mocha": "11.1.0",
147
+ "expect": "30.0.5",
148
+ "express": "5.1.0",
149
+ "globals": "16.2.0",
150
+ "graphql": "16.11.0",
150
151
  "graphql-tag": "^2.12.6",
151
152
  "husky": "9.1.7",
152
153
  "inquirer-test": "2.0.1",
153
154
  "jsdoc": "^3.6.11",
154
155
  "jsdoc-typeof-plugin": "1.0.0",
155
156
  "json-server": "0.17.4",
156
- "playwright": "1.51.0",
157
+ "mochawesome": "^7.1.3",
158
+ "playwright": "1.54.1",
157
159
  "prettier": "^3.3.2",
158
- "puppeteer": "24.4.0",
160
+ "puppeteer": "24.15.0",
159
161
  "qrcode-terminal": "0.12.0",
160
162
  "rosie": "2.1.1",
161
163
  "runok": "0.9.3",
162
- "semver": "7.7.1",
163
- "sinon": "19.0.2",
164
+ "semver": "7.7.2",
165
+ "sinon": "21.0.0",
164
166
  "sinon-chai": "3.7.0",
165
167
  "testcafe": "3.7.2",
166
- "ts-morph": "25.0.1",
168
+ "ts-morph": "26.0.0",
167
169
  "ts-node": "10.9.2",
168
- "tsd": "^0.31.0",
170
+ "tsd": "^0.33.0",
169
171
  "tsd-jsdoc": "2.5.0",
170
- "typedoc": "0.28.0",
171
- "typedoc-plugin-markdown": "4.5.0",
172
- "typescript": "5.8.2",
172
+ "typedoc": "0.28.10",
173
+ "typedoc-plugin-markdown": "4.8.1",
174
+ "typescript": "5.8.3",
173
175
  "wdio-docker-service": "3.2.1",
174
- "webdriverio": "9.12.0",
176
+ "webdriverio": "9.12.5",
175
177
  "xml2js": "0.6.2",
176
178
  "xpath": "0.0.34"
177
179
  },