codeceptjs 4.0.7 → 4.0.9

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.
@@ -63,6 +63,25 @@ Make sure `Playwright` helper is enabled in `codecept.conf.js` config:
63
63
  > Turn off the `show` option if you want to run test in headless mode.
64
64
  > If you don't specify the browser here, `chromium` will be used. Possible browsers are: `chromium`, `firefox` and `webkit`
65
65
 
66
+ To point `firefox` at a custom build instead of the bundled one, pass `executablePath` under the
67
+ `firefox` key:
68
+
69
+ ```js
70
+ helpers: {
71
+ Playwright: {
72
+ url: "http://localhost",
73
+ browser: 'firefox',
74
+ firefox: {
75
+ executablePath: '/path/to/firefox'
76
+ }
77
+ }
78
+ }
79
+ ```
80
+
81
+ Useful with a build like [invisible_playwright](https://github.com/feder-cr/invisible_playwright),
82
+ patched at the source level for a realistic fingerprint, for sites that detect the default Chromium
83
+ path.
84
+
66
85
  Playwright uses different strategies to detect if a page is loaded. In configuration use `waitForNavigation` option for that:
67
86
 
68
87
  When to consider navigation succeeded, defaults to `load`. Given an array of event strings, navigation is considered to be successful after all events have been fired. Events can be either:
package/lib/codecept.js CHANGED
@@ -289,8 +289,11 @@ class Codecept {
289
289
  // Ignore if gherkin module not available
290
290
  }
291
291
 
292
- // Sort test files alphabetically for consistent execution order
293
- this.testFiles.sort()
292
+ // Sort test files alphabetically for consistent execution order,
293
+ // but skip sorting when --shuffle is active so the randomised order is preserved.
294
+ if (!this.opts.shuffle) {
295
+ this.testFiles.sort()
296
+ }
294
297
 
295
298
  return new Promise((resolve, reject) => {
296
299
  const mocha = container.mocha()
@@ -82,8 +82,13 @@ function isTableDataRow(row) {
82
82
  return has.call(row, 'data') && has.call(row, 'skip')
83
83
  }
84
84
 
85
+ function isDataTable(dataTable) {
86
+ if (dataTable instanceof DataTable) return true
87
+ return Boolean(dataTable) && Array.isArray(dataTable.array) && Array.isArray(dataTable.rows) && typeof dataTable.add === 'function'
88
+ }
89
+
85
90
  function detectDataType(dataTable) {
86
- if (dataTable instanceof DataTable) {
91
+ if (isDataTable(dataTable)) {
87
92
  return dataTable.rows
88
93
  }
89
94
 
package/lib/effects.js CHANGED
@@ -50,8 +50,10 @@ function within(context, fn) {
50
50
  return recorder.promise().then(() => res)
51
51
  })
52
52
  .catch(e => {
53
+ finishHelpers()
53
54
  finalize()
54
55
  recorder.throw(e)
56
+ return recorder.promise()
55
57
  })
56
58
  }
57
59
 
@@ -203,7 +205,7 @@ async function retryTo(callback, maxTries, pollInterval = 200) {
203
205
  const sessionName = 'retryTo'
204
206
 
205
207
  return new Promise((done, reject) => {
206
- let tries = 1
208
+ let tries = 0
207
209
 
208
210
  function handleRetryException(err) {
209
211
  recorder.throw(err)
@@ -216,7 +218,7 @@ async function retryTo(callback, maxTries, pollInterval = 200) {
216
218
  try {
217
219
  await callback(tries)
218
220
  } catch (err) {
219
- handleRetryException(err)
221
+ recorder.throw(err)
220
222
  }
221
223
 
222
224
  // Call done if no errors
@@ -228,7 +230,7 @@ async function retryTo(callback, maxTries, pollInterval = 200) {
228
230
  // Catch errors and retry
229
231
  recorder.session.catch(err => {
230
232
  recorder.session.restore(`${sessionName} ${tries}`)
231
- if (tries <= maxTries) {
233
+ if (tries < maxTries) {
232
234
  output.debug(`Error ${err}... Retrying`)
233
235
  recorder.add(`${sessionName} ${tries}`, () => setTimeout(tryBlock, pollInterval))
234
236
  } else {
package/lib/globals.js CHANGED
@@ -30,10 +30,8 @@ export async function initCodeceptGlobals(dir, config, container) {
30
30
  // pause/inject/share stay global even under noGlobals — they're the everyday
31
31
  // debugging/wiring entry points and have no useful import alternative for
32
32
  // page-object code that runs before the container is available.
33
- global.pause = async (...args) => {
34
- const pauseModule = await import('./pause.js')
35
- return (pauseModule.default || pauseModule)(...args)
36
- }
33
+ const pauseModule = await import('./pause.js')
34
+ global.pause = pauseModule.default || pauseModule
37
35
  global.inject = () => container.support()
38
36
  global.share = container.share
39
37
 
@@ -53,14 +53,6 @@ class GraphQL extends Helper {
53
53
  this.axios.defaults.headers = this.options.defaultHeaders
54
54
  }
55
55
 
56
- static _checkRequirements() {
57
- try {
58
- require('axios')
59
- } catch (e) {
60
- return ['axios']
61
- }
62
- }
63
-
64
56
  static _config() {
65
57
  return [
66
58
  {
@@ -174,15 +174,6 @@ class GraphQLDataFactory extends Helper {
174
174
  Object.keys(this.factories).forEach(f => (this.created[f] = []))
175
175
  }
176
176
 
177
- static _checkRequirements() {
178
- try {
179
- require('axios')
180
- require('rosie')
181
- } catch (e) {
182
- return ['axios', 'rosie']
183
- }
184
- }
185
-
186
177
  _after() {
187
178
  if (!this.config.cleanup) {
188
179
  return Promise.resolve()
@@ -197,11 +197,15 @@ export function setup(suite) {
197
197
  return function (done) {
198
198
  const doneFn = makeDoneCallableOnce(done)
199
199
  recorder.startUnlessRunning()
200
- import('./test.js').then(testModule => {
201
- const { enhanceMochaTest } = testModule.default || testModule
202
- event.emit(event.test.before, enhanceMochaTest(suite?.ctx?.currentTest ?? suite?.currentTest))
203
- recorder.add(() => doneFn())
204
- })
200
+ import('./test.js')
201
+ .then(testModule => {
202
+ const { enhanceMochaTest } = testModule.default || testModule
203
+ event.emit(event.test.before, enhanceMochaTest(suite?.ctx?.currentTest ?? suite?.currentTest))
204
+ recorder.add(() => doneFn())
205
+ })
206
+ .catch(err => {
207
+ doneFn(err)
208
+ })
205
209
  }
206
210
  }
207
211
 
@@ -209,11 +213,15 @@ export function teardown(suite) {
209
213
  return function (done) {
210
214
  const doneFn = makeDoneCallableOnce(done)
211
215
  recorder.startUnlessRunning()
212
- import('./test.js').then(testModule => {
213
- const { enhanceMochaTest } = testModule.default || testModule
214
- event.emit(event.test.after, enhanceMochaTest(suite?.ctx?.currentTest ?? suite?.currentTest))
215
- recorder.add(() => doneFn())
216
- })
216
+ import('./test.js')
217
+ .then(testModule => {
218
+ const { enhanceMochaTest } = testModule.default || testModule
219
+ event.emit(event.test.after, enhanceMochaTest(suite?.ctx?.currentTest ?? suite?.currentTest))
220
+ recorder.add(() => doneFn())
221
+ })
222
+ .catch(err => {
223
+ doneFn(err)
224
+ })
217
225
  }
218
226
  }
219
227
 
package/lib/pause.js CHANGED
@@ -19,6 +19,35 @@ let finish
19
19
  let next
20
20
  let registeredVariables = {}
21
21
  let externalHandler = null
22
+ let pauseSessionOpen = false
23
+
24
+ function onStepAfter() {
25
+ recorder.add('Start next pause session', () => {
26
+ // test already finished, nothing to pause
27
+ if (!store.currentTest) return
28
+ if (!next) return
29
+ return pauseSession()
30
+ })
31
+ }
32
+
33
+ function onTestFinished() {
34
+ if (typeof finish === 'function') finish()
35
+ if (pauseSessionOpen) {
36
+ recorder.session.restore('pause')
37
+ pauseSessionOpen = false
38
+ }
39
+ if (rl) rl.close()
40
+ if (!externalHandler) history.save()
41
+ event.dispatcher.removeListener(event.step.after, onStepAfter)
42
+ event.dispatcher.removeListener(event.test.finished, onTestFinished)
43
+ }
44
+
45
+ function registerPauseListeners() {
46
+ event.dispatcher.removeListener(event.step.after, onStepAfter)
47
+ event.dispatcher.removeListener(event.test.finished, onTestFinished)
48
+ event.dispatcher.on(event.step.after, onStepAfter)
49
+ event.dispatcher.on(event.test.finished, onTestFinished)
50
+ }
22
51
 
23
52
  /**
24
53
  * Pauses test execution and starts interactive shell
@@ -28,22 +57,7 @@ const pause = function (passedObject = {}) {
28
57
  if (store.dryRun) return
29
58
 
30
59
  next = false
31
- // add listener to all next steps to provide next() functionality
32
- event.dispatcher.on(event.step.after, () => {
33
- recorder.add('Start next pause session', () => {
34
- // test already finished, nothing to pause
35
- if (!store.currentTest) return
36
- if (!next) return
37
- return pauseSession()
38
- })
39
- })
40
-
41
- event.dispatcher.on(event.test.finished, () => {
42
- if (typeof finish === 'function') finish()
43
- recorder.session.restore('pause')
44
- if (rl) rl.close()
45
- if (!externalHandler) history.save()
46
- })
60
+ registerPauseListeners()
47
61
 
48
62
  recorder.add('Start new session', () => pauseSession(passedObject))
49
63
  }
@@ -51,12 +65,14 @@ const pause = function (passedObject = {}) {
51
65
  function pauseSession(passedObject = {}) {
52
66
  registeredVariables = passedObject
53
67
  recorder.session.start('pause')
68
+ pauseSessionOpen = true
54
69
 
55
70
  if (externalHandler) {
56
71
  store.onPause = true
57
72
  return externalHandler({ registeredVariables }).then(() => {
58
73
  store.onPause = false
59
74
  recorder.session.restore('pause')
75
+ pauseSessionOpen = false
60
76
  })
61
77
  }
62
78
 
@@ -107,6 +123,7 @@ async function parseInput(cmd) {
107
123
  if (!cmd || cmd === 'resume' || cmd === 'exit') {
108
124
  if (typeof finish === 'function') finish()
109
125
  recorder.session.restore('pause')
126
+ pauseSessionOpen = false
110
127
  rl.close()
111
128
  history.save()
112
129
  return nextStep()
@@ -265,6 +282,7 @@ function setPauseHandler(handler) {
265
282
  */
266
283
  function pauseNow(passedObject = {}) {
267
284
  if (store.dryRun) return
285
+ registerPauseListeners()
268
286
  recorder.add('Triggered pause', () => pauseSession(passedObject))
269
287
  }
270
288
 
@@ -66,6 +66,26 @@ export default function (config = {}) {
66
66
  config = Object.assign({}, defaultConfig, config)
67
67
 
68
68
  let written = false
69
+ const hookFailures = []
70
+
71
+ event.dispatcher.on(event.hook.failed, hook => {
72
+ if (!hook || !['BeforeSuite', 'AfterSuite'].includes(hook.hookName)) return
73
+ const err = hook.err || hook.error
74
+ if (!err) return
75
+ const runnable = hook.ctx && hook.ctx.test
76
+ const suite = runnable && runnable.parent
77
+ hookFailures.push({
78
+ title: hook.title || `${hook.hookName} hook failed`,
79
+ state: 'failed',
80
+ err,
81
+ parent: suite,
82
+ file: (runnable && runnable.file) || (suite && suite.file),
83
+ tags: (suite && suite.tags) || [],
84
+ meta: {},
85
+ steps: [],
86
+ duration: (runnable && runnable.duration) || 0,
87
+ })
88
+ })
69
89
 
70
90
  const writeReport = result => {
71
91
  if (written) return
@@ -76,7 +96,7 @@ export default function (config = {}) {
76
96
  mkdirp.sync(dir)
77
97
  const file = path.join(dir, config.outputName)
78
98
 
79
- fs.writeFileSync(file, buildXml(result, config))
99
+ fs.writeFileSync(file, buildXml(result, config, hookFailures))
80
100
  output.plugin('junitReporter', `JUnit report saved to ${file}`)
81
101
  }
82
102
 
@@ -84,17 +104,18 @@ export default function (config = {}) {
84
104
  event.dispatcher.on(event.workers.result, writeReport)
85
105
  }
86
106
 
87
- function buildXml(result, config) {
107
+ function buildXml(result, config, hookFailures = []) {
88
108
  const doc = new DOMImplementation().createDocument(null, null, null)
89
- const suites = groupBySuite(result.tests)
109
+ const allTests = result.tests.concat(hookFailures)
110
+ const suites = groupBySuite(allTests)
90
111
 
91
112
  const root = doc.createElement('testsuites')
92
113
  setAttr(root, 'name', config.testGroupName)
93
- setAttr(root, 'tests', result.tests.length)
94
- setAttr(root, 'failures', countState(result.tests, 'failed'))
95
- setAttr(root, 'skipped', countSkipped(result.tests))
114
+ setAttr(root, 'tests', allTests.length)
115
+ setAttr(root, 'failures', countState(allTests, 'failed'))
116
+ setAttr(root, 'skipped', countSkipped(allTests))
96
117
  setAttr(root, 'errors', 0)
97
- setAttr(root, 'time', toSeconds(sumDuration(result.tests)))
118
+ setAttr(root, 'time', toSeconds(sumDuration(allTests)))
98
119
  setAttr(root, 'timestamp', toIso(result.stats && result.stats.start))
99
120
  doc.appendChild(root)
100
121
 
package/lib/session.js CHANGED
@@ -109,6 +109,7 @@ function session(sessionName, config, fn) {
109
109
  output.stepShift = 0
110
110
  session.restoreVars(sessionName)
111
111
  event.dispatcher.removeListener(event.step.after, addContextToStep)
112
+ recorder.add('restore session on error', () => recorder.session.restore(`session:${sessionName}`))
112
113
  recorder.throw(e)
113
114
  return recorder.promise()
114
115
  })
@@ -124,6 +125,7 @@ function session(sessionName, config, fn) {
124
125
  session.restoreVars(sessionName)
125
126
  output.stepShift = 0
126
127
  event.dispatcher.removeListener(event.step.after, addContextToStep)
128
+ recorder.session.restore(`session:${sessionName}`)
127
129
  throw e
128
130
  })
129
131
  }
@@ -385,7 +385,9 @@ const __dirname = __dirname_fn(__filename);
385
385
  )
386
386
 
387
387
  // Write the transpiled file with updated imports
388
- const tempFile = filePath.replace(/\.ts$/, '.temp.mjs')
388
+ // Include process.pid + a random suffix so concurrent run-multiple workers
389
+ // don't write to and delete each other's temp files (see issue #5642).
390
+ const tempFile = filePath.replace(/\.ts$/, `.${process.pid}.${Math.random().toString(36).slice(2, 10)}.temp.mjs`)
389
391
  fs.writeFileSync(tempFile, jsContent)
390
392
  transpiledFiles.set(filePath, tempFile)
391
393
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeceptjs",
3
- "version": "4.0.7",
3
+ "version": "4.0.9",
4
4
  "type": "module",
5
5
  "description": "Supercharged End 2 End Testing Framework for NodeJS",
6
6
  "keywords": [