codeceptjs 4.0.0-beta.2 → 4.0.0-beta.20
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/README.md +133 -120
- package/bin/codecept.js +107 -96
- package/bin/test-server.js +64 -0
- package/docs/webapi/clearCookie.mustache +1 -1
- package/docs/webapi/click.mustache +5 -1
- package/lib/actor.js +71 -103
- package/lib/ai.js +159 -188
- package/lib/assert/empty.js +22 -24
- package/lib/assert/equal.js +30 -37
- package/lib/assert/error.js +14 -14
- package/lib/assert/include.js +43 -48
- package/lib/assert/throws.js +11 -11
- package/lib/assert/truth.js +22 -22
- package/lib/assert.js +20 -18
- package/lib/codecept.js +262 -162
- package/lib/colorUtils.js +50 -52
- package/lib/command/check.js +206 -0
- package/lib/command/configMigrate.js +56 -51
- package/lib/command/definitions.js +96 -109
- package/lib/command/dryRun.js +77 -79
- package/lib/command/generate.js +234 -194
- package/lib/command/gherkin/init.js +42 -33
- package/lib/command/gherkin/snippets.js +76 -74
- package/lib/command/gherkin/steps.js +20 -17
- package/lib/command/info.js +74 -38
- package/lib/command/init.js +301 -290
- package/lib/command/interactive.js +41 -32
- package/lib/command/list.js +28 -27
- package/lib/command/run-multiple/chunk.js +51 -48
- package/lib/command/run-multiple/collection.js +5 -5
- package/lib/command/run-multiple/run.js +5 -1
- package/lib/command/run-multiple.js +97 -97
- package/lib/command/run-rerun.js +19 -25
- package/lib/command/run-workers.js +68 -92
- package/lib/command/run.js +39 -27
- package/lib/command/utils.js +80 -64
- package/lib/command/workers/runTests.js +388 -226
- package/lib/config.js +109 -50
- package/lib/container.js +641 -261
- package/lib/data/context.js +60 -61
- package/lib/data/dataScenarioConfig.js +47 -47
- package/lib/data/dataTableArgument.js +32 -32
- package/lib/data/table.js +22 -22
- package/lib/effects.js +307 -0
- package/lib/element/WebElement.js +327 -0
- package/lib/els.js +160 -0
- package/lib/event.js +173 -163
- package/lib/globals.js +141 -0
- package/lib/heal.js +89 -85
- package/lib/helper/AI.js +131 -41
- package/lib/helper/ApiDataFactory.js +107 -75
- package/lib/helper/Appium.js +542 -404
- package/lib/helper/FileSystem.js +100 -79
- package/lib/helper/GraphQL.js +44 -43
- package/lib/helper/GraphQLDataFactory.js +52 -52
- package/lib/helper/JSONResponse.js +126 -88
- package/lib/helper/Mochawesome.js +54 -29
- package/lib/helper/Playwright.js +2547 -1316
- package/lib/helper/Puppeteer.js +1578 -1181
- package/lib/helper/REST.js +209 -68
- package/lib/helper/WebDriver.js +1482 -1342
- package/lib/helper/errors/ConnectionRefused.js +6 -6
- package/lib/helper/errors/ElementAssertion.js +11 -16
- package/lib/helper/errors/ElementNotFound.js +5 -9
- package/lib/helper/errors/RemoteBrowserConnectionRefused.js +5 -5
- package/lib/helper/extras/Console.js +11 -11
- package/lib/helper/extras/PlaywrightLocator.js +110 -0
- package/lib/helper/extras/PlaywrightPropEngine.js +18 -18
- package/lib/helper/extras/PlaywrightReactVueLocator.js +17 -8
- package/lib/helper/extras/PlaywrightRestartOpts.js +25 -11
- package/lib/helper/extras/Popup.js +22 -22
- package/lib/helper/extras/React.js +27 -28
- package/lib/helper/network/actions.js +36 -42
- package/lib/helper/network/utils.js +78 -84
- package/lib/helper/scripts/blurElement.js +5 -5
- package/lib/helper/scripts/focusElement.js +5 -5
- package/lib/helper/scripts/highlightElement.js +8 -8
- package/lib/helper/scripts/isElementClickable.js +34 -34
- package/lib/helper.js +2 -3
- package/lib/history.js +23 -19
- package/lib/hooks.js +8 -8
- package/lib/html.js +94 -104
- package/lib/index.js +38 -27
- package/lib/listener/config.js +30 -23
- package/lib/listener/emptyRun.js +54 -0
- package/lib/listener/enhancedGlobalRetry.js +110 -0
- package/lib/listener/exit.js +16 -18
- package/lib/listener/globalRetry.js +70 -0
- package/lib/listener/globalTimeout.js +181 -0
- package/lib/listener/helpers.js +76 -51
- package/lib/listener/mocha.js +10 -11
- package/lib/listener/result.js +11 -0
- package/lib/listener/retryEnhancer.js +85 -0
- package/lib/listener/steps.js +71 -59
- package/lib/listener/store.js +20 -0
- package/lib/locator.js +214 -197
- package/lib/mocha/asyncWrapper.js +274 -0
- package/lib/mocha/bdd.js +167 -0
- package/lib/mocha/cli.js +341 -0
- package/lib/mocha/factory.js +163 -0
- package/lib/mocha/featureConfig.js +89 -0
- package/lib/mocha/gherkin.js +231 -0
- package/lib/mocha/hooks.js +121 -0
- package/lib/mocha/index.js +21 -0
- package/lib/mocha/inject.js +46 -0
- package/lib/{interfaces → mocha}/scenarioConfig.js +58 -34
- package/lib/mocha/suite.js +89 -0
- package/lib/mocha/test.js +184 -0
- package/lib/mocha/types.d.ts +42 -0
- package/lib/mocha/ui.js +242 -0
- package/lib/output.js +141 -71
- package/lib/parser.js +47 -44
- package/lib/pause.js +173 -145
- package/lib/plugin/analyze.js +403 -0
- package/lib/plugin/{autoLogin.js → auth.js} +178 -79
- package/lib/plugin/autoDelay.js +36 -40
- package/lib/plugin/coverage.js +131 -78
- package/lib/plugin/customLocator.js +22 -21
- package/lib/plugin/customReporter.js +53 -0
- package/lib/plugin/enhancedRetryFailedStep.js +99 -0
- package/lib/plugin/heal.js +101 -110
- package/lib/plugin/htmlReporter.js +3648 -0
- package/lib/plugin/pageInfo.js +140 -0
- package/lib/plugin/pauseOnFail.js +12 -11
- package/lib/plugin/retryFailedStep.js +82 -47
- package/lib/plugin/screenshotOnFail.js +111 -92
- package/lib/plugin/stepByStepReport.js +159 -101
- package/lib/plugin/stepTimeout.js +20 -25
- package/lib/plugin/subtitles.js +38 -38
- package/lib/recorder.js +193 -130
- package/lib/rerun.js +94 -49
- package/lib/result.js +238 -0
- package/lib/retryCoordinator.js +207 -0
- package/lib/secret.js +20 -18
- package/lib/session.js +95 -89
- package/lib/step/base.js +239 -0
- package/lib/step/comment.js +10 -0
- package/lib/step/config.js +50 -0
- package/lib/step/func.js +46 -0
- package/lib/step/helper.js +50 -0
- package/lib/step/meta.js +99 -0
- package/lib/step/record.js +74 -0
- package/lib/step/retry.js +11 -0
- package/lib/step/section.js +55 -0
- package/lib/step.js +18 -329
- package/lib/steps.js +54 -0
- package/lib/store.js +38 -7
- package/lib/template/heal.js +3 -12
- package/lib/template/prompts/generatePageObject.js +31 -0
- package/lib/template/prompts/healStep.js +13 -0
- package/lib/template/prompts/writeStep.js +9 -0
- package/lib/test-server.js +334 -0
- package/lib/timeout.js +60 -0
- package/lib/transform.js +8 -8
- package/lib/translation.js +34 -21
- package/lib/utils/loaderCheck.js +124 -0
- package/lib/utils/mask_data.js +47 -0
- package/lib/utils/typescript.js +237 -0
- package/lib/utils.js +411 -228
- package/lib/workerStorage.js +37 -34
- package/lib/workers.js +532 -296
- package/package.json +124 -95
- package/translations/de-DE.js +5 -3
- package/translations/fr-FR.js +5 -4
- package/translations/index.js +22 -12
- package/translations/it-IT.js +4 -3
- package/translations/ja-JP.js +4 -3
- package/translations/nl-NL.js +76 -0
- package/translations/pl-PL.js +4 -3
- package/translations/pt-BR.js +4 -3
- package/translations/ru-RU.js +4 -3
- package/translations/utils.js +10 -0
- package/translations/zh-CN.js +4 -3
- package/translations/zh-TW.js +4 -3
- package/typings/index.d.ts +546 -185
- package/typings/promiseBasedTypes.d.ts +150 -875
- package/typings/types.d.ts +547 -992
- package/lib/cli.js +0 -249
- package/lib/dirname.js +0 -5
- package/lib/helper/Expect.js +0 -425
- package/lib/helper/ExpectHelper.js +0 -399
- package/lib/helper/MockServer.js +0 -223
- package/lib/helper/Nightmare.js +0 -1411
- package/lib/helper/Protractor.js +0 -1835
- package/lib/helper/SoftExpectHelper.js +0 -381
- package/lib/helper/TestCafe.js +0 -1410
- package/lib/helper/clientscripts/nightmare.js +0 -213
- package/lib/helper/testcafe/testControllerHolder.js +0 -42
- package/lib/helper/testcafe/testcafe-utils.js +0 -63
- package/lib/interfaces/bdd.js +0 -98
- package/lib/interfaces/featureConfig.js +0 -69
- package/lib/interfaces/gherkin.js +0 -195
- package/lib/listener/artifacts.js +0 -19
- package/lib/listener/retry.js +0 -68
- package/lib/listener/timeout.js +0 -109
- package/lib/mochaFactory.js +0 -110
- package/lib/plugin/allure.js +0 -15
- package/lib/plugin/commentStep.js +0 -136
- package/lib/plugin/debugErrors.js +0 -67
- package/lib/plugin/eachElement.js +0 -127
- package/lib/plugin/fakerTransform.js +0 -49
- package/lib/plugin/retryTo.js +0 -121
- package/lib/plugin/selenoid.js +0 -371
- package/lib/plugin/standardActingHelpers.js +0 -9
- package/lib/plugin/tryTo.js +0 -105
- package/lib/plugin/wdio.js +0 -246
- package/lib/scenario.js +0 -222
- package/lib/ui.js +0 -238
- package/lib/within.js +0 -70
package/lib/mocha/cli.js
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import mocha from 'mocha'
|
|
2
|
+
const { reporters: { Base } } = mocha
|
|
3
|
+
import ms from 'ms'
|
|
4
|
+
import figures from 'figures'
|
|
5
|
+
import { readFileSync } from 'fs'
|
|
6
|
+
import { fileURLToPath } from 'url'
|
|
7
|
+
import { dirname, join } from 'path'
|
|
8
|
+
import event from '../event.js'
|
|
9
|
+
import AssertionFailedError from '../assert/error.js'
|
|
10
|
+
import output from '../output.js'
|
|
11
|
+
import test, { cloneTest } from './test.js'
|
|
12
|
+
|
|
13
|
+
// Get version from package.json to avoid circular dependency
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
15
|
+
const __dirname = dirname(__filename)
|
|
16
|
+
const packagePath = join(__dirname, '../../package.json')
|
|
17
|
+
const packageJson = JSON.parse(readFileSync(packagePath, 'utf8'))
|
|
18
|
+
const codeceptVersion = packageJson.version
|
|
19
|
+
const cursor = Base.cursor
|
|
20
|
+
let currentMetaStep = []
|
|
21
|
+
let codeceptjsEventDispatchersRegistered = false
|
|
22
|
+
|
|
23
|
+
// Lazy container loading to avoid circular dependencies
|
|
24
|
+
let containerPromise = null
|
|
25
|
+
const getContainer = () => {
|
|
26
|
+
if (!containerPromise) {
|
|
27
|
+
containerPromise = import('../container.js').then(module => module.default || module)
|
|
28
|
+
}
|
|
29
|
+
return containerPromise
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
class Cli extends Base {
|
|
33
|
+
constructor(runner, opts) {
|
|
34
|
+
super(runner)
|
|
35
|
+
let level = 0
|
|
36
|
+
this.loadedTests = []
|
|
37
|
+
opts = opts.reporterOptions || opts
|
|
38
|
+
if (opts.steps) level = 1
|
|
39
|
+
if (opts.debug) level = 2
|
|
40
|
+
if (opts.verbose) level = 3
|
|
41
|
+
output.level(level)
|
|
42
|
+
output.print(`CodeceptJS v${codeceptVersion} ${output.standWithUkraine()}`)
|
|
43
|
+
output.print(`Using test root "${global.codecept_dir}"`)
|
|
44
|
+
|
|
45
|
+
const showSteps = level >= 1
|
|
46
|
+
|
|
47
|
+
if (level >= 2) {
|
|
48
|
+
// Load container asynchronously to avoid circular dependency
|
|
49
|
+
getContainer().then(Container => {
|
|
50
|
+
output.print(output.styles.debug(`Helpers: ${Object.keys(Container.helpers()).join(', ')}`))
|
|
51
|
+
output.print(output.styles.debug(`Plugins: ${Object.keys(Container.plugins()).join(', ')}`))
|
|
52
|
+
}).catch(() => {
|
|
53
|
+
// Silently fail if container can't be loaded
|
|
54
|
+
output.print(output.styles.debug('Helpers: [loading...]'))
|
|
55
|
+
output.print(output.styles.debug('Plugins: [loading...]'))
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (level >= 3) {
|
|
60
|
+
process.on('warning', warning => {
|
|
61
|
+
console.log('\nWarning Details:')
|
|
62
|
+
console.log('Name:', warning.name)
|
|
63
|
+
console.log('Message:', warning.message)
|
|
64
|
+
console.log('Stack:', warning.stack)
|
|
65
|
+
console.log('-------------------')
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
runner.on('start', () => {
|
|
70
|
+
console.log()
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
runner.on('suite', suite => {
|
|
74
|
+
output.suite.started(suite)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
runner.on('fail', test => {
|
|
78
|
+
if (test.ctx.currentTest) {
|
|
79
|
+
this.loadedTests.push(test.ctx.currentTest.uid)
|
|
80
|
+
}
|
|
81
|
+
if (showSteps && test.steps) {
|
|
82
|
+
return output.scenario.failed(test)
|
|
83
|
+
}
|
|
84
|
+
cursor.CR()
|
|
85
|
+
output.test.failed(test)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
runner.on('pending', test => {
|
|
89
|
+
if (test.parent && test.parent.pending) {
|
|
90
|
+
const suite = test.parent
|
|
91
|
+
const skipInfo = suite.opts.skipInfo || {}
|
|
92
|
+
skipTestConfig(test, skipInfo.message)
|
|
93
|
+
} else {
|
|
94
|
+
skipTestConfig(test, null)
|
|
95
|
+
}
|
|
96
|
+
this.loadedTests.push(test.uid)
|
|
97
|
+
cursor.CR()
|
|
98
|
+
output.test.skipped(test)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
runner.on('pass', test => {
|
|
102
|
+
if (showSteps && test.steps) {
|
|
103
|
+
return output.scenario.passed(test)
|
|
104
|
+
}
|
|
105
|
+
cursor.CR()
|
|
106
|
+
output.test.passed(test)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
if (showSteps) {
|
|
110
|
+
runner.on('test', test => {
|
|
111
|
+
currentMetaStep = []
|
|
112
|
+
if (test.steps) {
|
|
113
|
+
output.test.started(test)
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
if (!codeceptjsEventDispatchersRegistered) {
|
|
118
|
+
codeceptjsEventDispatchersRegistered = true
|
|
119
|
+
|
|
120
|
+
event.dispatcher.on(event.bddStep.started, step => {
|
|
121
|
+
output.stepShift = 2
|
|
122
|
+
output.step(step)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
event.dispatcher.on(event.step.started, step => {
|
|
126
|
+
let processingStep = step
|
|
127
|
+
const metaSteps = []
|
|
128
|
+
let isHidden = false
|
|
129
|
+
while (processingStep.metaStep) {
|
|
130
|
+
metaSteps.unshift(processingStep.metaStep)
|
|
131
|
+
processingStep = processingStep.metaStep
|
|
132
|
+
if (processingStep.collapsed) isHidden = true
|
|
133
|
+
}
|
|
134
|
+
const shift = metaSteps.length
|
|
135
|
+
|
|
136
|
+
for (let i = 0; i < Math.max(currentMetaStep.length, metaSteps.length); i++) {
|
|
137
|
+
if (currentMetaStep[i] !== metaSteps[i]) {
|
|
138
|
+
output.stepShift = 3 + 2 * i
|
|
139
|
+
if (!metaSteps[i]) continue
|
|
140
|
+
// bdd steps are handled by bddStep.started
|
|
141
|
+
if (metaSteps[i].isBDD()) continue
|
|
142
|
+
output.step(metaSteps[i])
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
currentMetaStep = metaSteps
|
|
146
|
+
if (isHidden) return
|
|
147
|
+
output.stepShift = 3 + 2 * shift
|
|
148
|
+
output.step(step)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
event.dispatcher.on(event.step.finished, () => {
|
|
152
|
+
output.stepShift = 0
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
runner.on('suite end', suite => {
|
|
158
|
+
let skippedCount = 0
|
|
159
|
+
const grep = runner._grep
|
|
160
|
+
for (const test of suite.tests) {
|
|
161
|
+
if (!test.state && !this.loadedTests.includes(test.uid)) {
|
|
162
|
+
if (matchTest(grep, test.title)) {
|
|
163
|
+
if (!test.opts) {
|
|
164
|
+
test.opts = {}
|
|
165
|
+
}
|
|
166
|
+
if (!test.opts.skipInfo) {
|
|
167
|
+
test.opts.skipInfo = {}
|
|
168
|
+
}
|
|
169
|
+
skipTestConfig(test, "Skipped due to failure in 'before' hook")
|
|
170
|
+
output.test.skipped(test)
|
|
171
|
+
skippedCount += 1
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
getContainer().then(Container => {
|
|
177
|
+
Container.result().addStats({ pending: skippedCount, tests: skippedCount })
|
|
178
|
+
}).catch(() => {
|
|
179
|
+
// Silently fail if container can't be loaded
|
|
180
|
+
})
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
runner.on('end', this.result.bind(this))
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async result() {
|
|
187
|
+
try {
|
|
188
|
+
const Container = await getContainer()
|
|
189
|
+
Container.result().addStats(this.stats)
|
|
190
|
+
Container.result().finish()
|
|
191
|
+
|
|
192
|
+
const stats = Container.result().stats
|
|
193
|
+
console.log()
|
|
194
|
+
|
|
195
|
+
// passes
|
|
196
|
+
if (stats.failures) {
|
|
197
|
+
output.print(output.styles.bold('-- FAILURES:'))
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const failuresLog = []
|
|
201
|
+
|
|
202
|
+
// failures
|
|
203
|
+
if (stats.failures) {
|
|
204
|
+
// append step traces
|
|
205
|
+
this.failures = this.failures.map(test => {
|
|
206
|
+
// we will change the stack trace, so we need to clone the test
|
|
207
|
+
const err = test.err
|
|
208
|
+
|
|
209
|
+
let log = ''
|
|
210
|
+
let originalMessage = err.message
|
|
211
|
+
|
|
212
|
+
if (err instanceof AssertionFailedError) {
|
|
213
|
+
err.message = err.inspect()
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// multi-line error messages (for Playwright)
|
|
217
|
+
if (err.message && err.message.includes('\n')) {
|
|
218
|
+
const lines = err.message.split('\n')
|
|
219
|
+
const truncatedLines = lines.slice(0, 5)
|
|
220
|
+
if (lines.length > 5) {
|
|
221
|
+
truncatedLines.push('...')
|
|
222
|
+
}
|
|
223
|
+
err.message = truncatedLines.join('\n').replace(/^/gm, ' ').trim()
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// add new line before the message
|
|
227
|
+
err.message = '\n ' + err.message
|
|
228
|
+
|
|
229
|
+
// explicitly show file with error
|
|
230
|
+
if (test.file) {
|
|
231
|
+
log += `\n${output.styles.basic(figures.circle)} ${output.styles.section('File:')} file://${test.file}\n`
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const steps = test.steps || (test.ctx && test.ctx.test.steps)
|
|
235
|
+
|
|
236
|
+
if (steps && steps.length) {
|
|
237
|
+
let scenarioTrace = ''
|
|
238
|
+
steps
|
|
239
|
+
.reverse()
|
|
240
|
+
.slice(0, 10)
|
|
241
|
+
.forEach(step => {
|
|
242
|
+
const hasFailed = step.status === 'failed'
|
|
243
|
+
let line = `${hasFailed ? output.styles.bold(figures.cross) : figures.tick} ${step.toCode()} ${step.line()}`
|
|
244
|
+
if (hasFailed) line = output.styles.bold(line)
|
|
245
|
+
scenarioTrace += `\n${line}`
|
|
246
|
+
})
|
|
247
|
+
log += `\n${output.styles.basic(figures.circle)} ${output.styles.section('Scenario Steps')}:${scenarioTrace}\n`
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// display artifacts in debug mode
|
|
251
|
+
if (test?.artifacts && Object.keys(test.artifacts).length) {
|
|
252
|
+
log += `\n${output.styles.basic(figures.circle)} ${output.styles.section('Artifacts:')}`
|
|
253
|
+
for (const artifact of Object.keys(test.artifacts)) {
|
|
254
|
+
log += `\n- ${artifact}: ${test.artifacts[artifact]}`
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// display metadata
|
|
259
|
+
if (test.meta && Object.keys(test.meta).length) {
|
|
260
|
+
log += `\n\n${output.styles.basic(figures.circle)} ${output.styles.section('Metadata:')}`
|
|
261
|
+
for (const [key, value] of Object.entries(test.meta)) {
|
|
262
|
+
log += `\n- ${key}: ${value}`
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
let stack = err.stack
|
|
268
|
+
stack = (stack || '').replace(originalMessage, '')
|
|
269
|
+
stack = stack ? stack.split('\n') : []
|
|
270
|
+
|
|
271
|
+
if (stack[0] && stack[0].includes(err.message)) {
|
|
272
|
+
stack.shift()
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (stack[0] && stack[0].trim() == 'Error:') {
|
|
276
|
+
stack.shift()
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (output.level() < 3) {
|
|
280
|
+
stack = stack.slice(0, 3)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
err.stack = `${stack.join('\n')}\n\n${output.colors.blue(log)}`
|
|
284
|
+
} catch (e) {
|
|
285
|
+
console.error(e)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// we will change the stack trace, so we need to clone the test
|
|
289
|
+
test = cloneTest(test)
|
|
290
|
+
test.err = err
|
|
291
|
+
return test
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
const originalLog = Base.consoleLog
|
|
295
|
+
Base.consoleLog = (...data) => {
|
|
296
|
+
failuresLog.push([...data])
|
|
297
|
+
originalLog(...data)
|
|
298
|
+
}
|
|
299
|
+
Base.list(this.failures)
|
|
300
|
+
Base.consoleLog = originalLog
|
|
301
|
+
console.log()
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
Container.result().addFailures(failuresLog)
|
|
305
|
+
|
|
306
|
+
output.result(stats.passes, stats.failures, stats.pending, ms(stats.duration), stats.failedHooks)
|
|
307
|
+
|
|
308
|
+
if (stats.failures && output.level() < 3) {
|
|
309
|
+
output.print(output.styles.debug('Run with --verbose flag to see complete NodeJS stacktrace'))
|
|
310
|
+
}
|
|
311
|
+
} catch (error) {
|
|
312
|
+
// Fallback behavior if container can't be loaded
|
|
313
|
+
console.log('Error loading container:', error.message)
|
|
314
|
+
output.result(0, 1, 0, 0, 0)
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function matchTest(grep, test) {
|
|
320
|
+
if (grep) {
|
|
321
|
+
return grep.test(test)
|
|
322
|
+
}
|
|
323
|
+
return true
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function skipTestConfig(test, message) {
|
|
327
|
+
if (!test.opts) {
|
|
328
|
+
test.opts = {}
|
|
329
|
+
}
|
|
330
|
+
if (!test.opts.skipInfo) {
|
|
331
|
+
test.opts.skipInfo = {}
|
|
332
|
+
}
|
|
333
|
+
test.opts.skipInfo.message = test.opts.skipInfo.message || message
|
|
334
|
+
test.opts.skipInfo.isFastSkipped = true
|
|
335
|
+
event.emit(event.test.skipped, test)
|
|
336
|
+
test.state = 'skipped'
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export default function (runner, opts) {
|
|
340
|
+
return new Cli(runner, opts)
|
|
341
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import Mocha from 'mocha'
|
|
2
|
+
import fsPath from 'path'
|
|
3
|
+
import fs from 'fs'
|
|
4
|
+
import { fileURLToPath } from 'url'
|
|
5
|
+
import reporter from './cli.js'
|
|
6
|
+
import gherkinParser, { loadTranslations } from './gherkin.js'
|
|
7
|
+
import output from '../output.js'
|
|
8
|
+
import scenarioUiFunction from './ui.js'
|
|
9
|
+
import { initMochaGlobals } from '../globals.js'
|
|
10
|
+
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
12
|
+
const __dirname = fsPath.dirname(__filename)
|
|
13
|
+
|
|
14
|
+
let mocha
|
|
15
|
+
|
|
16
|
+
class MochaFactory {
|
|
17
|
+
static create(config, opts) {
|
|
18
|
+
mocha = new Mocha(Object.assign(config, opts))
|
|
19
|
+
output.process(opts.child)
|
|
20
|
+
mocha.ui(scenarioUiFunction)
|
|
21
|
+
|
|
22
|
+
// Manually trigger UI setup for globals to be available in ESM context
|
|
23
|
+
// This ensures Feature, Scenario, Before, etc. are available immediately
|
|
24
|
+
if (mocha.suite && mocha.suite.emit) {
|
|
25
|
+
const context = {}
|
|
26
|
+
mocha.suite.emit('pre-require', context, '', mocha)
|
|
27
|
+
// Also set globals immediately so they're available when ESM modules load
|
|
28
|
+
initMochaGlobals(context)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
Mocha.Runner.prototype.uncaught = function (err) {
|
|
32
|
+
if (err) {
|
|
33
|
+
if (err.toString().indexOf('ECONNREFUSED') >= 0) {
|
|
34
|
+
// Handle ECONNREFUSED without dynamic import for now
|
|
35
|
+
err = new Error('Connection refused: ' + err.toString())
|
|
36
|
+
}
|
|
37
|
+
output.error(err)
|
|
38
|
+
output.print(err.stack)
|
|
39
|
+
process.exit(1)
|
|
40
|
+
}
|
|
41
|
+
output.error('Uncaught undefined exception')
|
|
42
|
+
process.exit(1)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Override loadFiles to handle feature files
|
|
46
|
+
const originalLoadFiles = Mocha.prototype.loadFiles
|
|
47
|
+
mocha.loadFiles = function (fn) {
|
|
48
|
+
// load features
|
|
49
|
+
const featureFiles = this.files.filter(file => file.match(/\.feature$/))
|
|
50
|
+
if (featureFiles.length > 0) {
|
|
51
|
+
// Load translations for Gherkin features
|
|
52
|
+
loadTranslations().catch(() => {
|
|
53
|
+
// Ignore if translations can't be loaded
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
for (const file of featureFiles) {
|
|
57
|
+
const suite = gherkinParser(fs.readFileSync(file, 'utf8'), file)
|
|
58
|
+
this.suite.addSuite(suite)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// remove feature files
|
|
62
|
+
const jsFiles = this.files.filter(file => !file.match(/\.feature$/))
|
|
63
|
+
this.files = this.files.filter(file => !file.match(/\.feature$/))
|
|
64
|
+
|
|
65
|
+
// Load JavaScript test files using ESM imports
|
|
66
|
+
if (jsFiles.length > 0) {
|
|
67
|
+
try {
|
|
68
|
+
// Try original loadFiles first for compatibility
|
|
69
|
+
originalLoadFiles.call(this, fn)
|
|
70
|
+
} catch (e) {
|
|
71
|
+
// If original loadFiles fails, load ESM files manually
|
|
72
|
+
if (e.message.includes('not in cache') || e.message.includes('ESM') || e.message.includes('getStatus')) {
|
|
73
|
+
// Load ESM files by importing them synchronously using top-level await workaround
|
|
74
|
+
for (const file of jsFiles) {
|
|
75
|
+
try {
|
|
76
|
+
// Convert file path to file:// URL for dynamic import
|
|
77
|
+
const fileUrl = `file://${file}`
|
|
78
|
+
// Use import() but don't await it - let it load in the background
|
|
79
|
+
import(fileUrl).catch(importErr => {
|
|
80
|
+
// If dynamic import fails, the file may have syntax errors or other issues
|
|
81
|
+
console.error(`Failed to load test file ${file}:`, importErr.message)
|
|
82
|
+
})
|
|
83
|
+
if (fn) fn()
|
|
84
|
+
} catch (fileErr) {
|
|
85
|
+
console.error(`Error processing test file ${file}:`, fileErr.message)
|
|
86
|
+
if (fn) fn(fileErr)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
throw e
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// add ids for each test and check uniqueness
|
|
96
|
+
const dupes = []
|
|
97
|
+
let missingFeatureInFile = []
|
|
98
|
+
const seenTests = []
|
|
99
|
+
this.suite.eachTest(test => {
|
|
100
|
+
if (!test) {
|
|
101
|
+
return // Skip undefined tests
|
|
102
|
+
}
|
|
103
|
+
const name = test.fullTitle()
|
|
104
|
+
if (seenTests.includes(test.uid)) {
|
|
105
|
+
dupes.push(name)
|
|
106
|
+
}
|
|
107
|
+
seenTests.push(test.uid)
|
|
108
|
+
|
|
109
|
+
if (name.slice(0, name.indexOf(':')) === '') {
|
|
110
|
+
missingFeatureInFile.push(test.file)
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
if (dupes.length) {
|
|
114
|
+
// ideally this should be no-op and throw (breaking change)...
|
|
115
|
+
output.error(`Duplicate test names detected - Feature + Scenario name should be unique:\n${dupes.join('\n')}`)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (missingFeatureInFile.length) {
|
|
119
|
+
missingFeatureInFile = [...new Set(missingFeatureInFile)]
|
|
120
|
+
output.error(`Missing Feature section in:\n${missingFeatureInFile.join('\n')}`)
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
// Use original for non-feature files
|
|
124
|
+
originalLoadFiles.call(this, fn)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const presetReporter = opts.reporter || config.reporter
|
|
129
|
+
// use standard reporter
|
|
130
|
+
if (!presetReporter) {
|
|
131
|
+
mocha.reporter(reporter, opts)
|
|
132
|
+
return mocha
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// load custom reporter with options
|
|
136
|
+
const reporterOptions = Object.assign(config.reporterOptions || {})
|
|
137
|
+
|
|
138
|
+
if (opts.reporterOptions !== undefined) {
|
|
139
|
+
opts.reporterOptions.split(',').forEach(opt => {
|
|
140
|
+
const L = opt.split('=')
|
|
141
|
+
if (L.length > 2 || L.length === 0) {
|
|
142
|
+
throw new Error(`invalid reporter option '${opt}'`)
|
|
143
|
+
} else if (L.length === 2) {
|
|
144
|
+
reporterOptions[L[0]] = L[1]
|
|
145
|
+
} else {
|
|
146
|
+
reporterOptions[L[0]] = true
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const attributes = Object.getOwnPropertyDescriptor(reporterOptions, 'codeceptjs-cli-reporter')
|
|
152
|
+
if (reporterOptions['codeceptjs-cli-reporter'] && attributes) {
|
|
153
|
+
Object.defineProperty(reporterOptions, 'codeceptjs/lib/mocha/cli', attributes)
|
|
154
|
+
delete reporterOptions['codeceptjs-cli-reporter']
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// custom reporters
|
|
158
|
+
mocha.reporter(presetReporter, reporterOptions)
|
|
159
|
+
return mocha
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export default MochaFactory
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for a Feature.
|
|
3
|
+
* Can inject values and add custom configuration.
|
|
4
|
+
*/
|
|
5
|
+
class FeatureConfig {
|
|
6
|
+
/**
|
|
7
|
+
* @param {CodeceptJS.Suite} suite
|
|
8
|
+
*/
|
|
9
|
+
constructor(suite) {
|
|
10
|
+
this.suite = suite
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Set metadata for this suite
|
|
15
|
+
* @param {string} key
|
|
16
|
+
* @param {string} value
|
|
17
|
+
* @returns {this}
|
|
18
|
+
*/
|
|
19
|
+
meta(key, value) {
|
|
20
|
+
this.suite.tests.forEach(test => {
|
|
21
|
+
test.meta[key] = value
|
|
22
|
+
})
|
|
23
|
+
return this
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Retry this test for number of times
|
|
28
|
+
*
|
|
29
|
+
* @param {number} retries
|
|
30
|
+
* @returns {this}
|
|
31
|
+
*/
|
|
32
|
+
retry(retries) {
|
|
33
|
+
this.suite.retries(retries)
|
|
34
|
+
return this
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Set timeout for this test
|
|
39
|
+
* @param {number} timeout
|
|
40
|
+
* @returns {this}
|
|
41
|
+
*/
|
|
42
|
+
timeout(timeout) {
|
|
43
|
+
this.suite.timeout(timeout)
|
|
44
|
+
return this
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @callback FeatureConfigCallback
|
|
49
|
+
* @param {CodeceptJS.Suite} suite
|
|
50
|
+
* @returns {Object<string, any>}
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Configures a helper.
|
|
55
|
+
* Helper name can be omitted and values will be applied to first helper.
|
|
56
|
+
* @param {string | Object<string, any> | FeatureConfigCallback} helper
|
|
57
|
+
* @param {Object<string, any>} [obj]
|
|
58
|
+
* @returns {this}
|
|
59
|
+
*/
|
|
60
|
+
config(helper, obj) {
|
|
61
|
+
if (!obj) {
|
|
62
|
+
obj = helper
|
|
63
|
+
helper = 0
|
|
64
|
+
}
|
|
65
|
+
if (typeof obj === 'function') {
|
|
66
|
+
obj = obj(this.suite)
|
|
67
|
+
}
|
|
68
|
+
if (!this.suite.config) {
|
|
69
|
+
this.suite.config = {}
|
|
70
|
+
}
|
|
71
|
+
this.suite.config[helper] = obj
|
|
72
|
+
return this
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Append a tag name to scenario title
|
|
77
|
+
* @param {string} tagName
|
|
78
|
+
* @returns {this}
|
|
79
|
+
*/
|
|
80
|
+
tag(tagName) {
|
|
81
|
+
if (tagName[0] !== '@') tagName = `@${tagName}`
|
|
82
|
+
if (!this.suite.tags) this.suite.tags = []
|
|
83
|
+
this.suite.tags.push(tagName)
|
|
84
|
+
this.suite.title = `${this.suite.title.trim()} ${tagName}`
|
|
85
|
+
return this
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export default FeatureConfig
|