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.
- package/docs/playwright.md +19 -0
- package/lib/codecept.js +5 -2
- package/lib/data/context.js +6 -1
- package/lib/effects.js +5 -3
- package/lib/globals.js +2 -4
- package/lib/helper/GraphQL.js +0 -8
- package/lib/helper/GraphQLDataFactory.js +0 -9
- package/lib/mocha/asyncWrapper.js +18 -10
- package/lib/pause.js +34 -16
- package/lib/plugin/junitReporter.js +28 -7
- package/lib/session.js +2 -0
- package/lib/utils/typescript.js +3 -1
- package/package.json +1 -1
package/docs/playwright.md
CHANGED
|
@@ -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
|
-
|
|
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()
|
package/lib/data/context.js
CHANGED
|
@@ -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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
34
|
-
|
|
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
|
|
package/lib/helper/GraphQL.js
CHANGED
|
@@ -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')
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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')
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
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
|
|
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',
|
|
94
|
-
setAttr(root, 'failures', countState(
|
|
95
|
-
setAttr(root, 'skipped', countSkipped(
|
|
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(
|
|
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
|
}
|
package/lib/utils/typescript.js
CHANGED
|
@@ -385,7 +385,9 @@ const __dirname = __dirname_fn(__filename);
|
|
|
385
385
|
)
|
|
386
386
|
|
|
387
387
|
// Write the transpiled file with updated imports
|
|
388
|
-
|
|
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
|
}
|