codeceptjs 4.0.2-beta.9 → 4.0.2
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 +39 -28
- package/bin/codecept.js +15 -2
- package/bin/codeceptq.js +49 -0
- package/bin/mcp-server.js +1189 -0
- package/docs/advanced.md +201 -0
- package/docs/agents.md +181 -0
- package/docs/ai.md +489 -0
- package/docs/aitrace.md +266 -0
- package/docs/api.md +332 -0
- package/docs/architecture.md +235 -0
- package/docs/assertions.md +415 -0
- package/docs/auth.md +318 -0
- package/docs/basics.md +424 -0
- package/docs/bdd.md +539 -0
- package/docs/best.md +240 -0
- package/docs/bootstrap.md +132 -0
- package/docs/commands.md +352 -0
- package/docs/community-helpers.md +63 -0
- package/docs/configuration.md +185 -0
- package/docs/continuous-integration.md +431 -0
- package/docs/custom-helpers.md +297 -0
- package/docs/data.md +448 -0
- package/docs/debugging.md +332 -0
- package/docs/detox.md +235 -0
- package/docs/docker.md +107 -0
- package/docs/effects.md +179 -0
- package/docs/element-based-testing.md +295 -0
- package/docs/element-selection.md +125 -0
- package/docs/els.md +328 -0
- package/docs/environment-variables.md +131 -0
- package/docs/examples.md +160 -0
- package/docs/heal.md +213 -0
- package/docs/helpers/ApiDataFactory.md +267 -0
- package/docs/helpers/Appium.md +1419 -0
- package/docs/helpers/Detox.md +665 -0
- package/docs/helpers/ExpectHelper.md +275 -0
- package/docs/helpers/FileSystem.md +152 -0
- package/docs/helpers/GraphQL.md +152 -0
- package/docs/helpers/GraphQLDataFactory.md +226 -0
- package/docs/helpers/JSONResponse.md +255 -0
- package/docs/helpers/MockRequest.md +377 -0
- package/docs/helpers/Playwright.md +2970 -0
- package/docs/helpers/Puppeteer-firefox.md +86 -0
- package/docs/helpers/Puppeteer.md +2583 -0
- package/docs/helpers/REST.md +289 -0
- package/docs/helpers/WebDriver.md +2639 -0
- package/docs/hooks.md +148 -0
- package/docs/index.md +111 -0
- package/docs/installation.md +121 -0
- package/docs/internal-test-server.md +89 -0
- package/docs/locators.md +355 -0
- package/docs/mcp.md +485 -0
- package/docs/migrate-from-cypress.md +98 -0
- package/docs/migrate-from-java.md +108 -0
- package/docs/migrate-from-protractor.md +101 -0
- package/docs/migrate-from-testcafe.md +99 -0
- package/docs/migration-4.md +745 -0
- package/docs/mobile.md +338 -0
- package/docs/pageobjects.md +399 -0
- package/docs/parallel.md +187 -0
- package/docs/playwright.md +714 -0
- package/docs/plugins/aiTrace.md +49 -0
- package/docs/plugins/analyze.md +66 -0
- package/docs/plugins/auth.md +241 -0
- package/docs/plugins/autoDelay.md +48 -0
- package/docs/plugins/browser.md +41 -0
- package/docs/plugins/coverage.md +39 -0
- package/docs/plugins/customLocator.md +119 -0
- package/docs/plugins/customReporter.md +16 -0
- package/docs/plugins/expose.md +75 -0
- package/docs/plugins/heal.md +44 -0
- package/docs/plugins/junitReporter.md +51 -0
- package/docs/plugins/pageInfo.md +34 -0
- package/docs/plugins/pause.md +43 -0
- package/docs/plugins/pauseOnFail.md +18 -0
- package/docs/plugins/retryFailedStep.md +75 -0
- package/docs/plugins/screencast.md +55 -0
- package/docs/plugins/screenshot.md +58 -0
- package/docs/plugins/screenshotOnFail.md +18 -0
- package/docs/plugins/stepTimeout.md +65 -0
- package/docs/plugins.md +87 -0
- package/docs/puppeteer.md +314 -0
- package/docs/quickstart.md +120 -0
- package/docs/reports.md +195 -0
- package/docs/retry.md +311 -0
- package/docs/secrets.md +150 -0
- package/docs/sessions.md +80 -0
- package/docs/shadow.md +68 -0
- package/docs/store.md +94 -0
- package/docs/test-structure.md +275 -0
- package/docs/timeouts.md +183 -0
- package/docs/translation.md +247 -0
- package/docs/tutorial.md +323 -0
- package/docs/typescript.md +159 -0
- package/docs/web-element.md +251 -0
- package/docs/webdriver.md +641 -0
- package/docs/within.md +55 -0
- package/lib/actor.js +1 -36
- package/lib/ai.js +3 -2
- package/lib/aria.js +260 -0
- package/lib/assertions.js +18 -0
- package/lib/codecept.js +34 -25
- package/lib/command/check.js +2 -1
- package/lib/command/definitions.js +6 -7
- package/lib/command/dryRun.js +24 -5
- package/lib/command/generate.js +3 -1
- package/lib/command/gherkin/snippets.js +5 -4
- package/lib/command/init.js +249 -270
- package/lib/command/list.js +150 -10
- package/lib/command/query.js +218 -0
- package/lib/command/run-multiple.js +3 -1
- package/lib/command/run-workers.js +2 -14
- package/lib/command/run.js +3 -17
- package/lib/command/utils.js +14 -0
- package/lib/command/workers/runTests.js +84 -41
- package/lib/config.js +96 -18
- package/lib/container.js +115 -17
- package/lib/effects.js +17 -0
- package/lib/element/WebElement.js +246 -2
- package/lib/els.js +12 -6
- package/lib/globals.js +32 -19
- package/lib/heal.js +7 -4
- package/lib/helper/ApiDataFactory.js +2 -1
- package/lib/helper/Appium.js +8 -8
- package/lib/helper/FileSystem.js +3 -2
- package/lib/helper/GraphQLDataFactory.js +2 -1
- package/lib/helper/Playwright.js +358 -467
- package/lib/helper/Puppeteer.js +335 -192
- package/lib/helper/WebDriver.js +324 -111
- package/lib/helper/errors/ElementNotFound.js +5 -2
- package/lib/helper/errors/MultipleElementsFound.js +52 -0
- package/lib/helper/errors/NonFocusedType.js +8 -0
- package/lib/helper/extras/Download.js +45 -0
- package/lib/helper/extras/PlaywrightLocator.js +7 -107
- package/lib/helper/extras/elementSelection.js +58 -0
- package/lib/helper/extras/focusCheck.js +43 -0
- package/lib/helper/extras/richTextEditor.js +178 -0
- package/lib/helper/scripts/dropFile.js +11 -0
- package/lib/history.js +3 -2
- package/lib/html.js +103 -16
- package/lib/index.js +9 -1
- package/lib/listener/config.js +6 -4
- package/lib/listener/emptyRun.js +2 -1
- package/lib/listener/globalRetry.js +32 -6
- package/lib/listener/helpers.js +4 -1
- package/lib/listener/mocha.js +2 -1
- package/lib/listener/pageobjects.js +43 -0
- package/lib/listener/result.js +3 -2
- package/lib/locator.js +158 -16
- package/lib/mocha/cli.js +19 -1
- package/lib/mocha/factory.js +11 -1
- package/lib/mocha/inject.js +1 -1
- package/lib/mocha/scenarioConfig.js +2 -1
- package/lib/mocha/ui.js +5 -6
- package/lib/parser.js +2 -2
- package/lib/pause.js +38 -4
- package/lib/plugin/aiTrace.js +457 -0
- package/lib/plugin/analyze.js +9 -9
- package/lib/plugin/auth.js +5 -4
- package/lib/plugin/browser.js +77 -0
- package/lib/plugin/expose.js +159 -0
- package/lib/plugin/heal.js +47 -3
- package/lib/plugin/junitReporter.js +303 -0
- package/lib/plugin/pageInfo.js +54 -52
- package/lib/plugin/pause.js +131 -0
- package/lib/plugin/pauseOnFail.js +11 -33
- package/lib/plugin/retryFailedStep.js +43 -32
- package/lib/plugin/screencast.js +289 -0
- package/lib/plugin/screenshot.js +558 -0
- package/lib/plugin/screenshotOnFail.js +9 -170
- package/lib/plugin/stepTimeout.js +3 -2
- package/lib/recorder.js +1 -1
- package/lib/rerun.js +2 -1
- package/lib/result.js +2 -1
- package/lib/step/base.js +10 -9
- package/lib/step/comment.js +2 -2
- package/lib/step/config.js +15 -2
- package/lib/step/helper.js +4 -4
- package/lib/step/meta.js +3 -3
- package/lib/step/record.js +5 -5
- package/lib/store.js +72 -3
- package/lib/translation.js +2 -1
- package/lib/utils/loaderCheck.js +28 -0
- package/lib/utils/mask_data.js +2 -1
- package/lib/utils/pluginParser.js +151 -0
- package/lib/utils/trace.js +297 -0
- package/lib/utils/typescript.js +188 -23
- package/lib/utils.js +77 -3
- package/lib/workers.js +65 -40
- package/package.json +35 -30
- package/typings/index.d.ts +119 -8
- package/typings/promiseBasedTypes.d.ts +3158 -6065
- package/typings/types.d.ts +3453 -6494
- package/docs/webapi/amOnPage.mustache +0 -11
- package/docs/webapi/appendField.mustache +0 -11
- package/docs/webapi/attachFile.mustache +0 -12
- package/docs/webapi/blur.mustache +0 -18
- package/docs/webapi/checkOption.mustache +0 -13
- package/docs/webapi/clearCookie.mustache +0 -9
- package/docs/webapi/clearField.mustache +0 -9
- package/docs/webapi/click.mustache +0 -29
- package/docs/webapi/clickLink.mustache +0 -8
- package/docs/webapi/closeCurrentTab.mustache +0 -7
- package/docs/webapi/closeOtherTabs.mustache +0 -8
- package/docs/webapi/dontSee.mustache +0 -11
- package/docs/webapi/dontSeeCheckboxIsChecked.mustache +0 -10
- package/docs/webapi/dontSeeCookie.mustache +0 -8
- package/docs/webapi/dontSeeCurrentUrlEquals.mustache +0 -10
- package/docs/webapi/dontSeeElement.mustache +0 -8
- package/docs/webapi/dontSeeElementInDOM.mustache +0 -8
- package/docs/webapi/dontSeeInCurrentUrl.mustache +0 -4
- package/docs/webapi/dontSeeInField.mustache +0 -11
- package/docs/webapi/dontSeeInSource.mustache +0 -8
- package/docs/webapi/dontSeeInTitle.mustache +0 -8
- package/docs/webapi/dontSeeTraffic.mustache +0 -13
- package/docs/webapi/doubleClick.mustache +0 -13
- package/docs/webapi/downloadFile.mustache +0 -12
- package/docs/webapi/dragAndDrop.mustache +0 -9
- package/docs/webapi/dragSlider.mustache +0 -11
- package/docs/webapi/executeAsyncScript.mustache +0 -24
- package/docs/webapi/executeScript.mustache +0 -26
- package/docs/webapi/fillField.mustache +0 -16
- package/docs/webapi/flushNetworkTraffics.mustache +0 -5
- package/docs/webapi/focus.mustache +0 -13
- package/docs/webapi/forceClick.mustache +0 -28
- package/docs/webapi/forceRightClick.mustache +0 -18
- package/docs/webapi/grabAllWindowHandles.mustache +0 -7
- package/docs/webapi/grabAttributeFrom.mustache +0 -10
- package/docs/webapi/grabAttributeFromAll.mustache +0 -9
- package/docs/webapi/grabBrowserLogs.mustache +0 -9
- package/docs/webapi/grabCookie.mustache +0 -11
- package/docs/webapi/grabCssPropertyFrom.mustache +0 -11
- package/docs/webapi/grabCssPropertyFromAll.mustache +0 -10
- package/docs/webapi/grabCurrentUrl.mustache +0 -9
- package/docs/webapi/grabCurrentWindowHandle.mustache +0 -6
- package/docs/webapi/grabDataFromPerformanceTiming.mustache +0 -20
- package/docs/webapi/grabElementBoundingRect.mustache +0 -20
- package/docs/webapi/grabGeoLocation.mustache +0 -8
- package/docs/webapi/grabHTMLFrom.mustache +0 -10
- package/docs/webapi/grabHTMLFromAll.mustache +0 -9
- package/docs/webapi/grabNumberOfOpenTabs.mustache +0 -8
- package/docs/webapi/grabNumberOfVisibleElements.mustache +0 -9
- package/docs/webapi/grabPageScrollPosition.mustache +0 -8
- package/docs/webapi/grabPopupText.mustache +0 -5
- package/docs/webapi/grabRecordedNetworkTraffics.mustache +0 -10
- package/docs/webapi/grabSource.mustache +0 -8
- package/docs/webapi/grabTextFrom.mustache +0 -10
- package/docs/webapi/grabTextFromAll.mustache +0 -9
- package/docs/webapi/grabTitle.mustache +0 -8
- package/docs/webapi/grabValueFrom.mustache +0 -9
- package/docs/webapi/grabValueFromAll.mustache +0 -8
- package/docs/webapi/grabWebElement.mustache +0 -9
- package/docs/webapi/grabWebElements.mustache +0 -9
- package/docs/webapi/moveCursorTo.mustache +0 -12
- package/docs/webapi/openNewTab.mustache +0 -7
- package/docs/webapi/pressKey.mustache +0 -12
- package/docs/webapi/pressKeyDown.mustache +0 -12
- package/docs/webapi/pressKeyUp.mustache +0 -12
- package/docs/webapi/pressKeyWithKeyNormalization.mustache +0 -60
- package/docs/webapi/refreshPage.mustache +0 -6
- package/docs/webapi/resizeWindow.mustache +0 -6
- package/docs/webapi/rightClick.mustache +0 -14
- package/docs/webapi/saveElementScreenshot.mustache +0 -10
- package/docs/webapi/saveScreenshot.mustache +0 -12
- package/docs/webapi/say.mustache +0 -10
- package/docs/webapi/scrollIntoView.mustache +0 -11
- package/docs/webapi/scrollPageToBottom.mustache +0 -6
- package/docs/webapi/scrollPageToTop.mustache +0 -6
- package/docs/webapi/scrollTo.mustache +0 -12
- package/docs/webapi/see.mustache +0 -11
- package/docs/webapi/seeAttributesOnElements.mustache +0 -9
- package/docs/webapi/seeCheckboxIsChecked.mustache +0 -10
- package/docs/webapi/seeCookie.mustache +0 -8
- package/docs/webapi/seeCssPropertiesOnElements.mustache +0 -9
- package/docs/webapi/seeCurrentUrlEquals.mustache +0 -11
- package/docs/webapi/seeElement.mustache +0 -8
- package/docs/webapi/seeElementInDOM.mustache +0 -8
- package/docs/webapi/seeInCurrentUrl.mustache +0 -8
- package/docs/webapi/seeInField.mustache +0 -12
- package/docs/webapi/seeInPopup.mustache +0 -8
- package/docs/webapi/seeInSource.mustache +0 -7
- package/docs/webapi/seeInTitle.mustache +0 -8
- package/docs/webapi/seeNumberOfElements.mustache +0 -11
- package/docs/webapi/seeNumberOfVisibleElements.mustache +0 -10
- package/docs/webapi/seeTextEquals.mustache +0 -9
- package/docs/webapi/seeTitleEquals.mustache +0 -8
- package/docs/webapi/seeTraffic.mustache +0 -36
- package/docs/webapi/selectOption.mustache +0 -21
- package/docs/webapi/setCookie.mustache +0 -16
- package/docs/webapi/setGeoLocation.mustache +0 -12
- package/docs/webapi/startRecordingTraffic.mustache +0 -8
- package/docs/webapi/startRecordingWebSocketMessages.mustache +0 -8
- package/docs/webapi/stopRecordingTraffic.mustache +0 -5
- package/docs/webapi/stopRecordingWebSocketMessages.mustache +0 -7
- package/docs/webapi/switchTo.mustache +0 -9
- package/docs/webapi/switchToNextTab.mustache +0 -10
- package/docs/webapi/switchToPreviousTab.mustache +0 -10
- package/docs/webapi/type.mustache +0 -21
- package/docs/webapi/uncheckOption.mustache +0 -13
- package/docs/webapi/wait.mustache +0 -8
- package/docs/webapi/waitForClickable.mustache +0 -11
- package/docs/webapi/waitForCookie.mustache +0 -9
- package/docs/webapi/waitForDetached.mustache +0 -10
- package/docs/webapi/waitForDisabled.mustache +0 -6
- package/docs/webapi/waitForElement.mustache +0 -11
- package/docs/webapi/waitForEnabled.mustache +0 -6
- package/docs/webapi/waitForFunction.mustache +0 -17
- package/docs/webapi/waitForInvisible.mustache +0 -10
- package/docs/webapi/waitForNumberOfTabs.mustache +0 -9
- package/docs/webapi/waitForText.mustache +0 -13
- package/docs/webapi/waitForValue.mustache +0 -10
- package/docs/webapi/waitForVisible.mustache +0 -10
- package/docs/webapi/waitInUrl.mustache +0 -9
- package/docs/webapi/waitNumberOfVisibleElements.mustache +0 -10
- package/docs/webapi/waitToHide.mustache +0 -10
- package/docs/webapi/waitUrlEquals.mustache +0 -10
- package/lib/helper/AI.js +0 -214
- package/lib/helper/Mochawesome.js +0 -96
- package/lib/helper/extras/PlaywrightReactVueLocator.js +0 -52
- package/lib/helper/extras/React.js +0 -65
- package/lib/listener/enhancedGlobalRetry.js +0 -110
- package/lib/plugin/enhancedRetryFailedStep.js +0 -99
- package/lib/plugin/htmlReporter.js +0 -3648
- package/lib/plugin/stepByStepReport.js +0 -427
- package/lib/plugin/subtitles.js +0 -89
- package/lib/retryCoordinator.js +0 -207
|
@@ -11,7 +11,7 @@ import { parentPort, workerData } from 'worker_threads'
|
|
|
11
11
|
|
|
12
12
|
// Delay imports to avoid ES Module loader race conditions in Node 22.x worker threads
|
|
13
13
|
// These will be imported dynamically when needed
|
|
14
|
-
let event, container, Codecept, getConfig, tryOrDefault, deepMerge
|
|
14
|
+
let event, container, Codecept, getConfig, tryOrDefault, deepMerge, fixErrorStack
|
|
15
15
|
|
|
16
16
|
let stdout = ''
|
|
17
17
|
|
|
@@ -21,25 +21,40 @@ const { options, tests, testRoot, workerIndex, poolMode } = workerData
|
|
|
21
21
|
|
|
22
22
|
// Global error handlers to catch critical errors but not test failures
|
|
23
23
|
process.on('uncaughtException', (err) => {
|
|
24
|
+
if (container?.tsFileMapping && fixErrorStack) {
|
|
25
|
+
const fileMapping = container.tsFileMapping()
|
|
26
|
+
if (fileMapping) {
|
|
27
|
+
fixErrorStack(err, fileMapping)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Log to stderr to bypass stdout suppression
|
|
32
|
+
process.stderr.write(`[Worker ${workerIndex}] UNCAUGHT EXCEPTION: ${err.message}\n`)
|
|
33
|
+
process.stderr.write(`${err.stack}\n`)
|
|
34
|
+
|
|
24
35
|
// Don't exit on test assertion errors - those are handled by mocha
|
|
25
36
|
if (err.name === 'AssertionError' || err.message?.includes('expected')) {
|
|
26
|
-
console.error(`[Worker ${workerIndex}] Test assertion error (handled by mocha):`, err.message)
|
|
27
37
|
return
|
|
28
38
|
}
|
|
29
|
-
console.error(`[Worker ${workerIndex}] Uncaught exception:`, err.message)
|
|
30
|
-
console.error(err.stack)
|
|
31
39
|
process.exit(1)
|
|
32
40
|
})
|
|
33
41
|
|
|
34
42
|
process.on('unhandledRejection', (reason, promise) => {
|
|
35
|
-
|
|
43
|
+
if (reason && typeof reason === 'object' && reason.stack && container?.tsFileMapping && fixErrorStack) {
|
|
44
|
+
const fileMapping = container.tsFileMapping()
|
|
45
|
+
if (fileMapping) {
|
|
46
|
+
fixErrorStack(reason, fileMapping)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Log to stderr to bypass stdout suppression
|
|
36
51
|
const msg = reason?.message || String(reason)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
52
|
+
process.stderr.write(`[Worker ${workerIndex}] UNHANDLED REJECTION: ${msg}\n`)
|
|
53
|
+
if (reason?.stack) {
|
|
54
|
+
process.stderr.write(`${reason.stack}\n`)
|
|
40
55
|
}
|
|
41
|
-
|
|
42
|
-
|
|
56
|
+
|
|
57
|
+
// Do not exit — killing the worker silently drops every remaining test from the report.
|
|
43
58
|
})
|
|
44
59
|
|
|
45
60
|
// hide worker output
|
|
@@ -49,6 +64,10 @@ if (poolMode && !options.debug) {
|
|
|
49
64
|
// In pool mode without debug, allow test names and important output but suppress verbose details
|
|
50
65
|
const originalWrite = process.stdout.write
|
|
51
66
|
process.stdout.write = string => {
|
|
67
|
+
// Always allow Worker logs
|
|
68
|
+
if (string.includes('[Worker')) {
|
|
69
|
+
return originalWrite.call(process.stdout, string)
|
|
70
|
+
}
|
|
52
71
|
// Allow test names (✔ or ✖), Scenario Steps, failures, and important markers
|
|
53
72
|
if (
|
|
54
73
|
string.includes('✔') ||
|
|
@@ -68,7 +87,12 @@ if (poolMode && !options.debug) {
|
|
|
68
87
|
return originalWrite.call(process.stdout, string)
|
|
69
88
|
}
|
|
70
89
|
} else if (!poolMode && !options.debug && !options.verbose) {
|
|
90
|
+
const originalWrite = process.stdout.write
|
|
71
91
|
process.stdout.write = string => {
|
|
92
|
+
// Always allow Worker logs
|
|
93
|
+
if (string.includes('[Worker')) {
|
|
94
|
+
return originalWrite.call(process.stdout, string)
|
|
95
|
+
}
|
|
72
96
|
stdout += string
|
|
73
97
|
return true
|
|
74
98
|
}
|
|
@@ -105,24 +129,47 @@ let config
|
|
|
105
129
|
// Load test and run
|
|
106
130
|
initPromise = (async function () {
|
|
107
131
|
try {
|
|
132
|
+
// Add staggered delay at the very start to prevent resource conflicts
|
|
133
|
+
// Longer delay for browser initialization conflicts
|
|
134
|
+
const delay = (workerIndex - 1) * 2000 // 0ms, 2s, 4s, etc.
|
|
135
|
+
if (delay > 0) {
|
|
136
|
+
await new Promise(resolve => setTimeout(resolve, delay))
|
|
137
|
+
}
|
|
138
|
+
|
|
108
139
|
// Import modules dynamically to avoid ES Module loader race conditions in Node 22.x
|
|
109
140
|
const eventModule = await import('../../event.js')
|
|
110
141
|
const containerModule = await import('../../container.js')
|
|
111
142
|
const utilsModule = await import('../utils.js')
|
|
112
143
|
const coreUtilsModule = await import('../../utils.js')
|
|
113
144
|
const CodeceptModule = await import('../../codecept.js')
|
|
114
|
-
|
|
145
|
+
const typescriptModule = await import('../../utils/typescript.js')
|
|
146
|
+
|
|
115
147
|
event = eventModule.default
|
|
116
148
|
container = containerModule.default
|
|
117
149
|
getConfig = utilsModule.getConfig
|
|
118
150
|
tryOrDefault = coreUtilsModule.tryOrDefault
|
|
119
151
|
deepMerge = coreUtilsModule.deepMerge
|
|
120
152
|
Codecept = CodeceptModule.default
|
|
153
|
+
fixErrorStack = typescriptModule.fixErrorStack
|
|
121
154
|
|
|
122
155
|
const overrideConfigs = tryOrDefault(() => JSON.parse(options.override), {})
|
|
123
156
|
|
|
124
|
-
|
|
125
|
-
|
|
157
|
+
let baseConfig
|
|
158
|
+
try {
|
|
159
|
+
// IMPORTANT: await is required here since getConfig is async
|
|
160
|
+
baseConfig = await getConfig(options.config || testRoot)
|
|
161
|
+
} catch (configErr) {
|
|
162
|
+
if (container?.tsFileMapping && fixErrorStack) {
|
|
163
|
+
const fileMapping = container.tsFileMapping()
|
|
164
|
+
if (fileMapping) {
|
|
165
|
+
fixErrorStack(configErr, fileMapping)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
process.stderr.write(`[Worker ${workerIndex}] FAILED loading config: ${configErr.message}\n`)
|
|
169
|
+
process.stderr.write(`${configErr.stack}\n`)
|
|
170
|
+
await new Promise(resolve => setTimeout(resolve, 100))
|
|
171
|
+
process.exit(1)
|
|
172
|
+
}
|
|
126
173
|
|
|
127
174
|
// important deep merge so dynamic things e.g. functions on config are not overridden
|
|
128
175
|
config = deepMerge(baseConfig, overrideConfigs)
|
|
@@ -130,7 +177,21 @@ initPromise = (async function () {
|
|
|
130
177
|
// Pass workerIndex as child option for output.process() to display worker prefix
|
|
131
178
|
const optsWithChild = { ...options, child: workerIndex }
|
|
132
179
|
codecept = new Codecept(config, optsWithChild)
|
|
133
|
-
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
await codecept.init(testRoot)
|
|
183
|
+
} catch (initErr) {
|
|
184
|
+
if (container?.tsFileMapping && fixErrorStack) {
|
|
185
|
+
const fileMapping = container.tsFileMapping()
|
|
186
|
+
if (fileMapping) {
|
|
187
|
+
fixErrorStack(initErr, fileMapping)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
process.stderr.write(`[Worker ${workerIndex}] FAILED during codecept.init(): ${initErr.message}\n`)
|
|
191
|
+
process.stderr.write(`${initErr.stack}\n`)
|
|
192
|
+
process.exit(1)
|
|
193
|
+
}
|
|
194
|
+
|
|
134
195
|
codecept.loadTests()
|
|
135
196
|
mocha = container.mocha()
|
|
136
197
|
|
|
@@ -139,10 +200,7 @@ initPromise = (async function () {
|
|
|
139
200
|
// We'll reload test files fresh for each test request
|
|
140
201
|
} else {
|
|
141
202
|
// Legacy mode - filter tests upfront
|
|
142
|
-
console.log(`[Worker ${workerIndex}] Starting test filtering. Assigned ${tests.length} test UIDs`)
|
|
143
203
|
filterTests()
|
|
144
|
-
const finalCount = mocha.suite.total()
|
|
145
|
-
console.log(`[Worker ${workerIndex}] After filtering: ${finalCount} tests to run`)
|
|
146
204
|
}
|
|
147
205
|
|
|
148
206
|
// run tests
|
|
@@ -156,7 +214,14 @@ initPromise = (async function () {
|
|
|
156
214
|
parentPort?.close()
|
|
157
215
|
}
|
|
158
216
|
} catch (err) {
|
|
159
|
-
|
|
217
|
+
if (container?.tsFileMapping && fixErrorStack) {
|
|
218
|
+
const fileMapping = container.tsFileMapping()
|
|
219
|
+
if (fileMapping) {
|
|
220
|
+
fixErrorStack(err, fileMapping)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
process.stderr.write(`[Worker ${workerIndex}] FATAL ERROR: ${err.message}\n`)
|
|
224
|
+
process.stderr.write(`${err.stack}\n`)
|
|
160
225
|
process.exit(1)
|
|
161
226
|
}
|
|
162
227
|
})()
|
|
@@ -167,7 +232,6 @@ async function runTests() {
|
|
|
167
232
|
try {
|
|
168
233
|
await codecept.bootstrap()
|
|
169
234
|
} catch (err) {
|
|
170
|
-
console.error(`[Worker ${workerIndex}] Bootstrap error:`, err.message)
|
|
171
235
|
throw new Error(`Error while running bootstrap file :${err}`)
|
|
172
236
|
}
|
|
173
237
|
listenToParentThread()
|
|
@@ -176,13 +240,12 @@ async function runTests() {
|
|
|
176
240
|
try {
|
|
177
241
|
await codecept.run()
|
|
178
242
|
} catch (err) {
|
|
179
|
-
console.error(`[Worker ${workerIndex}] Runtime error:`, err.message)
|
|
180
243
|
throw err
|
|
181
244
|
} finally {
|
|
182
245
|
try {
|
|
183
246
|
await codecept.teardown()
|
|
184
247
|
} catch (err) {
|
|
185
|
-
|
|
248
|
+
// Ignore teardown errors
|
|
186
249
|
}
|
|
187
250
|
}
|
|
188
251
|
}
|
|
@@ -371,26 +434,6 @@ function filterTests() {
|
|
|
371
434
|
mocha.files = files
|
|
372
435
|
mocha.loadFiles()
|
|
373
436
|
|
|
374
|
-
// Collect all loaded tests for debugging
|
|
375
|
-
const allLoadedTests = [];
|
|
376
|
-
mocha.suite.eachTest(test => {
|
|
377
|
-
if (test) {
|
|
378
|
-
allLoadedTests.push({ uid: test.uid, title: test.fullTitle() });
|
|
379
|
-
}
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
console.log(`[Worker ${workerIndex}] Loaded ${allLoadedTests.length} tests from ${files.length} files`);
|
|
383
|
-
console.log(`[Worker ${workerIndex}] Expecting ${tests.length} test UIDs`);
|
|
384
|
-
|
|
385
|
-
const loadedUids = new Set(allLoadedTests.map(t => t.uid));
|
|
386
|
-
const missingTests = tests.filter(uid => !loadedUids.has(uid));
|
|
387
|
-
|
|
388
|
-
if (missingTests.length > 0) {
|
|
389
|
-
console.error(`[Worker ${workerIndex}] ERROR: ${missingTests.length} assigned tests NOT FOUND in loaded files!`);
|
|
390
|
-
console.error(`[Worker ${workerIndex}] Missing UIDs:`, missingTests);
|
|
391
|
-
console.error(`[Worker ${workerIndex}] Available UIDs:`, Array.from(loadedUids).slice(0, 5), '...');
|
|
392
|
-
}
|
|
393
|
-
|
|
394
437
|
// Recursively filter tests in all suites (including nested ones)
|
|
395
438
|
const filterSuiteTests = (suite) => {
|
|
396
439
|
suite.tests = suite.tests.filter(test => tests.indexOf(test.uid) >= 0)
|
package/lib/config.js
CHANGED
|
@@ -15,8 +15,9 @@ const defaultConfig = {
|
|
|
15
15
|
hooks: [],
|
|
16
16
|
gherkin: {},
|
|
17
17
|
plugins: {
|
|
18
|
-
|
|
19
|
-
enabled: true,
|
|
18
|
+
screenshot: {
|
|
19
|
+
enabled: true,
|
|
20
|
+
on: 'fail',
|
|
20
21
|
},
|
|
21
22
|
},
|
|
22
23
|
stepTimeout: 0,
|
|
@@ -32,9 +33,27 @@ const defaultConfig = {
|
|
|
32
33
|
],
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
// Array<{ fn: (cfg) => void, ran: boolean, error?: Error }>
|
|
35
37
|
let hooks = []
|
|
36
38
|
let config = {}
|
|
37
39
|
|
|
40
|
+
// Apply a single hook against `cfg`, swallowing errors so one broken hook
|
|
41
|
+
// can't take down the whole run. The failure is logged through the
|
|
42
|
+
// framework's own output module (when available) so it shows up in test
|
|
43
|
+
// reports; the hook is still marked ran so it doesn't get retried.
|
|
44
|
+
function applyHook(hook, cfg) {
|
|
45
|
+
try {
|
|
46
|
+
hook.fn(cfg)
|
|
47
|
+
} catch (err) {
|
|
48
|
+
hook.error = err
|
|
49
|
+
const out = globalThis.codeceptjs?.output
|
|
50
|
+
if (out && typeof out.error === 'function') out.error(`config hook failed: ${err.message}`)
|
|
51
|
+
else console.error('config hook failed:', err)
|
|
52
|
+
} finally {
|
|
53
|
+
hook.ran = true
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
38
57
|
const configFileNames = ['codecept.config.js', 'codecept.conf.js', 'codecept.js', 'codecept.config.cjs', 'codecept.conf.cjs', 'codecept.config.ts', 'codecept.conf.ts']
|
|
39
58
|
|
|
40
59
|
/**
|
|
@@ -49,7 +68,11 @@ class Config {
|
|
|
49
68
|
*/
|
|
50
69
|
static create(newConfig) {
|
|
51
70
|
config = deepMerge(deepClone(defaultConfig), newConfig)
|
|
52
|
-
hooks
|
|
71
|
+
// Re-apply every hook against the freshly built config; hooks added later
|
|
72
|
+
// (e.g. from plugin boot) stay pending until runPendingHooks. Array
|
|
73
|
+
// iterators re-check length on each step, so hooks pushed during a hook
|
|
74
|
+
// execution are visited in this same pass.
|
|
75
|
+
for (const hook of hooks) applyHook(hook, config)
|
|
53
76
|
return config
|
|
54
77
|
}
|
|
55
78
|
|
|
@@ -121,7 +144,48 @@ class Config {
|
|
|
121
144
|
}
|
|
122
145
|
|
|
123
146
|
static addHook(fn) {
|
|
124
|
-
hooks.push(fn)
|
|
147
|
+
hooks.push({ fn, ran: false })
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Run every hook that hasn't been applied to the current config yet.
|
|
152
|
+
* Hooks added after `Config.create()` (e.g. from plugin boot code) stay
|
|
153
|
+
* pending until this is called; once it runs, they're marked applied so
|
|
154
|
+
* subsequent calls are no-ops. Hooks added while pending hooks are running
|
|
155
|
+
* are picked up in the same pass (the array iterator re-checks length).
|
|
156
|
+
*
|
|
157
|
+
* Failures are logged through `output.error` and don't abort the loop —
|
|
158
|
+
* a broken hook can't poison the run, but its error is visible.
|
|
159
|
+
*
|
|
160
|
+
* @param {Object<string, *>} [cfg] target config (defaults to the live singleton)
|
|
161
|
+
* @return {boolean} true if any hook ran
|
|
162
|
+
*/
|
|
163
|
+
static runPendingHooks(cfg = config) {
|
|
164
|
+
let ran = false
|
|
165
|
+
for (const hook of hooks) {
|
|
166
|
+
if (hook.ran) continue
|
|
167
|
+
applyHook(hook, cfg)
|
|
168
|
+
ran = true
|
|
169
|
+
}
|
|
170
|
+
return ran
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Number of registered config hooks. Useful for snapshotting before a phase
|
|
175
|
+
* (e.g. plugin loading) and re-running only the hooks added during it.
|
|
176
|
+
* @return {number}
|
|
177
|
+
*/
|
|
178
|
+
static hooksCount() {
|
|
179
|
+
return hooks.length
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Run hooks in `[fromIndex, end)` against the given config object, mutating it.
|
|
184
|
+
* @param {number} fromIndex
|
|
185
|
+
* @param {Object<string, *>} cfg
|
|
186
|
+
*/
|
|
187
|
+
static runHooksFrom(fromIndex, cfg) {
|
|
188
|
+
for (let i = fromIndex; i < hooks.length; i++) hooks[i](cfg)
|
|
125
189
|
}
|
|
126
190
|
|
|
127
191
|
/**
|
|
@@ -150,33 +214,47 @@ async function loadConfigFile(configFile) {
|
|
|
150
214
|
const require = createRequire(import.meta.url)
|
|
151
215
|
const extensionName = path.extname(configFile)
|
|
152
216
|
|
|
217
|
+
// Populate the in-process registry that packages like @codeceptjs/configure
|
|
218
|
+
// look up at config-import time (their proxies throw if `globalThis.codeceptjs`
|
|
219
|
+
// is missing). initCodeceptGlobals sets this too, but only later during
|
|
220
|
+
// bootstrap — config files are imported here first.
|
|
221
|
+
if (!globalThis.codeceptjs) {
|
|
222
|
+
const indexModule = await import('./index.js')
|
|
223
|
+
globalThis.codeceptjs = indexModule.default || indexModule
|
|
224
|
+
}
|
|
225
|
+
|
|
153
226
|
// .conf.js config file
|
|
154
227
|
if (extensionName === '.js' || extensionName === '.ts' || extensionName === '.cjs') {
|
|
155
228
|
let configModule
|
|
156
229
|
try {
|
|
157
230
|
// For .ts files, try to compile and load as JavaScript
|
|
158
231
|
if (extensionName === '.ts') {
|
|
232
|
+
let transpileError = null
|
|
233
|
+
let tempFile = null
|
|
234
|
+
let allTempFiles = null
|
|
235
|
+
let fileMapping = null
|
|
236
|
+
|
|
159
237
|
try {
|
|
160
238
|
// Use the TypeScript transpilation utility
|
|
161
239
|
const typescript = require('typescript')
|
|
162
|
-
const
|
|
240
|
+
const result = await transpileTypeScript(configFile, typescript)
|
|
241
|
+
tempFile = result.tempFile
|
|
242
|
+
allTempFiles = result.allTempFiles
|
|
243
|
+
fileMapping = result.fileMapping
|
|
163
244
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
245
|
+
configModule = await import(tempFile)
|
|
246
|
+
cleanupTempFiles(allTempFiles)
|
|
247
|
+
} catch (err) {
|
|
248
|
+
transpileError = err
|
|
249
|
+
if (fileMapping) {
|
|
168
250
|
fixErrorStack(err, fileMapping)
|
|
169
|
-
cleanupTempFiles(allTempFiles)
|
|
170
|
-
throw err
|
|
171
251
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
try {
|
|
175
|
-
require('ts-node/register')
|
|
176
|
-
configModule = require(configFile)
|
|
177
|
-
} catch (tsNodeError) {
|
|
178
|
-
throw new Error(`Failed to load TypeScript config: ${tsError.message}`)
|
|
252
|
+
if (allTempFiles) {
|
|
253
|
+
cleanupTempFiles(allTempFiles)
|
|
179
254
|
}
|
|
255
|
+
// Throw immediately with the actual error - don't fall back to ts-node
|
|
256
|
+
// as it will mask the real error with "Unexpected token 'export'"
|
|
257
|
+
throw err
|
|
180
258
|
}
|
|
181
259
|
} else {
|
|
182
260
|
// Try ESM import first for JS files
|
package/lib/container.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { globSync } from 'glob'
|
|
2
2
|
import path from 'path'
|
|
3
3
|
import fs from 'fs'
|
|
4
|
+
import { isMainThread } from 'worker_threads'
|
|
4
5
|
import debugModule from 'debug'
|
|
5
6
|
const debug = debugModule('codeceptjs:container')
|
|
6
7
|
import { MetaStep } from './step.js'
|
|
@@ -15,9 +16,15 @@ import store from './store.js'
|
|
|
15
16
|
import Result from './result.js'
|
|
16
17
|
import ai from './ai.js'
|
|
17
18
|
import actorFactory from './actor.js'
|
|
19
|
+
import Config from './config.js'
|
|
18
20
|
|
|
19
21
|
let asyncHelperPromise
|
|
20
22
|
|
|
23
|
+
let beforeCalledSet = new Set()
|
|
24
|
+
|
|
25
|
+
export function getBeforeCalledSet() { return beforeCalledSet }
|
|
26
|
+
export function resetBeforeCalledSet() { beforeCalledSet = new Set() }
|
|
27
|
+
|
|
21
28
|
let container = {
|
|
22
29
|
helpers: {},
|
|
23
30
|
support: {},
|
|
@@ -116,6 +123,18 @@ class Container {
|
|
|
116
123
|
// Wait for all async helpers to finish loading and populate the actor
|
|
117
124
|
await asyncHelperPromise
|
|
118
125
|
|
|
126
|
+
// Plugins may have registered Config hooks during their boot. Run anything
|
|
127
|
+
// that hasn't been applied yet and re-feed the mutated helper config to the
|
|
128
|
+
// already-instantiated helpers.
|
|
129
|
+
if (Config.runPendingHooks(config)) {
|
|
130
|
+
for (const name of Object.keys(container.helpers)) {
|
|
131
|
+
const helper = container.helpers[name]
|
|
132
|
+
if (helper && typeof helper._setConfig === 'function' && config.helpers && config.helpers[name]) {
|
|
133
|
+
helper._setConfig(config.helpers[name])
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
119
138
|
if (opts && opts.ai) ai.enable(config.ai) // enable AI Assistant
|
|
120
139
|
if (config.gherkin) await loadGherkinStepsAsync(config.gherkin.steps || [])
|
|
121
140
|
if (opts && typeof opts.timeouts === 'boolean') store.timeouts = opts.timeouts
|
|
@@ -150,10 +169,23 @@ class Container {
|
|
|
150
169
|
if (!name) {
|
|
151
170
|
return container.proxySupport
|
|
152
171
|
}
|
|
153
|
-
|
|
172
|
+
if (typeof container.support[name] === 'function') {
|
|
173
|
+
return container.support[name]
|
|
174
|
+
}
|
|
154
175
|
return container.proxySupport[name]
|
|
155
176
|
}
|
|
156
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Get raw (non-proxied) support objects for direct access.
|
|
180
|
+
* Used by listeners to call lifecycle hooks without MetaStep wrapping.
|
|
181
|
+
*
|
|
182
|
+
* @api
|
|
183
|
+
* @returns {object}
|
|
184
|
+
*/
|
|
185
|
+
static supportObjects() {
|
|
186
|
+
return container.support
|
|
187
|
+
}
|
|
188
|
+
|
|
157
189
|
/**
|
|
158
190
|
* Get all helpers or get a helper by name
|
|
159
191
|
*
|
|
@@ -183,7 +215,7 @@ class Container {
|
|
|
183
215
|
* @api
|
|
184
216
|
*/
|
|
185
217
|
static tsFileMapping() {
|
|
186
|
-
return
|
|
218
|
+
return store.tsFileMapping
|
|
187
219
|
}
|
|
188
220
|
|
|
189
221
|
/**
|
|
@@ -426,11 +458,11 @@ async function requireHelperFromModule(helperName, config, HelperClass) {
|
|
|
426
458
|
tempJsFile = allTempFiles
|
|
427
459
|
fileMapping = mapping
|
|
428
460
|
// Store file mapping in container for runtime error fixing (merge with existing)
|
|
429
|
-
if (!
|
|
430
|
-
|
|
461
|
+
if (!store.tsFileMapping) {
|
|
462
|
+
store.tsFileMapping = new Map()
|
|
431
463
|
}
|
|
432
464
|
for (const [key, value] of mapping.entries()) {
|
|
433
|
-
|
|
465
|
+
store.tsFileMapping.set(key, value)
|
|
434
466
|
}
|
|
435
467
|
} catch (tsError) {
|
|
436
468
|
throw new Error(`Failed to load TypeScript helper ${importPath}: ${tsError.message}. Make sure 'typescript' package is installed.`)
|
|
@@ -542,6 +574,19 @@ function createSupportObjects(config) {
|
|
|
542
574
|
let currentValue = currentObject[prop]
|
|
543
575
|
|
|
544
576
|
if (isFunction(currentValue) || isAsyncFunction(currentValue)) {
|
|
577
|
+
if (prop.toString().charAt(0) !== '_' && currentObject._before && !beforeCalledSet.has(name)) {
|
|
578
|
+
beforeCalledSet.add(name)
|
|
579
|
+
const originalValue = currentValue
|
|
580
|
+
const wrappedValue = async function (...args) {
|
|
581
|
+
await currentObject._before()
|
|
582
|
+
return originalValue.apply(currentObject, args)
|
|
583
|
+
}
|
|
584
|
+
const ms = new MetaStep(name, prop)
|
|
585
|
+
ms.setContext(currentObject)
|
|
586
|
+
debug(`metastep is created for ${name}.${prop.toString()}() (with _before)`)
|
|
587
|
+
return ms.run.bind(ms, asyncWrapper(wrappedValue))
|
|
588
|
+
}
|
|
589
|
+
|
|
545
590
|
const ms = new MetaStep(name, prop)
|
|
546
591
|
ms.setContext(currentObject)
|
|
547
592
|
if (isAsyncFunction(currentValue)) currentValue = asyncWrapper(currentValue)
|
|
@@ -600,6 +645,8 @@ function createSupportObjects(config) {
|
|
|
600
645
|
let value
|
|
601
646
|
if (container.sharedKeys.has(prop) && prop in container.support) {
|
|
602
647
|
value = container.support[prop]
|
|
648
|
+
} else if (prop in container.support && typeof container.support[prop] === 'function') {
|
|
649
|
+
value = container.support[prop]
|
|
603
650
|
} else {
|
|
604
651
|
value = lazyLoad(prop)
|
|
605
652
|
}
|
|
@@ -614,6 +661,9 @@ function createSupportObjects(config) {
|
|
|
614
661
|
if (container.sharedKeys.has(key) && key in container.support) {
|
|
615
662
|
return container.support[key]
|
|
616
663
|
}
|
|
664
|
+
if (key in container.support && typeof container.support[key] === 'function') {
|
|
665
|
+
return container.support[key]
|
|
666
|
+
}
|
|
617
667
|
return lazyLoad(key)
|
|
618
668
|
},
|
|
619
669
|
},
|
|
@@ -654,26 +704,55 @@ async function loadPluginFallback(modulePath, config) {
|
|
|
654
704
|
async function createPlugins(config, options = {}) {
|
|
655
705
|
const plugins = {}
|
|
656
706
|
|
|
657
|
-
const
|
|
707
|
+
const pluginOptionMap = new Map()
|
|
708
|
+
for (const token of (options.plugins || '').split(',').filter(Boolean)) {
|
|
709
|
+
const parts = token.split(':')
|
|
710
|
+
pluginOptionMap.set(parts[0], parts.slice(1))
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
for (const [name] of pluginOptionMap) {
|
|
714
|
+
if (!config[name]) config[name] = {}
|
|
715
|
+
}
|
|
716
|
+
|
|
658
717
|
for (const pluginName in config) {
|
|
659
718
|
if (!config[pluginName]) config[pluginName] = {}
|
|
660
|
-
|
|
719
|
+
const pluginConfig = config[pluginName]
|
|
720
|
+
const enabledByCli = pluginOptionMap.has(pluginName)
|
|
721
|
+
if (!pluginConfig.enabled && !enabledByCli) {
|
|
661
722
|
continue // plugin is disabled
|
|
662
723
|
}
|
|
724
|
+
|
|
725
|
+
if (enabledByCli && pluginOptionMap.get(pluginName).length > 0) {
|
|
726
|
+
pluginConfig._args = pluginOptionMap.get(pluginName)
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// Generic workers gate:
|
|
730
|
+
// - runInWorker / runInWorkers controls plugin execution inside worker threads.
|
|
731
|
+
// - runInParent / runInMain can disable plugin in workers parent process.
|
|
732
|
+
const runInWorker = pluginConfig.runInWorker ?? pluginConfig.runInWorkers ?? (pluginName === 'testomatio' ? false : true)
|
|
733
|
+
const runInParent = pluginConfig.runInParent ?? pluginConfig.runInMain ?? true
|
|
734
|
+
|
|
735
|
+
if (!isMainThread && !runInWorker) {
|
|
736
|
+
continue
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
if (isMainThread && store.workerMode && !runInParent) {
|
|
740
|
+
continue
|
|
741
|
+
}
|
|
663
742
|
let module
|
|
664
743
|
try {
|
|
665
|
-
if (
|
|
666
|
-
module =
|
|
744
|
+
if (pluginConfig.require) {
|
|
745
|
+
module = pluginConfig.require
|
|
667
746
|
if (module.startsWith('.')) {
|
|
668
747
|
// local
|
|
669
|
-
module = path.resolve(
|
|
748
|
+
module = path.resolve(store.codeceptDir, module) // custom plugin
|
|
670
749
|
}
|
|
671
750
|
} else {
|
|
672
751
|
module = `./plugin/${pluginName}.js`
|
|
673
752
|
}
|
|
674
753
|
|
|
675
754
|
// Use async loading for all plugins (ESM and CJS)
|
|
676
|
-
plugins[pluginName] = await loadPluginAsync(module,
|
|
755
|
+
plugins[pluginName] = await loadPluginAsync(module, pluginConfig)
|
|
677
756
|
debug(`plugin ${pluginName} loaded via async import`)
|
|
678
757
|
} catch (err) {
|
|
679
758
|
throw new Error(`Could not load plugin ${pluginName} from module '${module}':\n${err.message}\n${err.stack}`)
|
|
@@ -683,12 +762,24 @@ async function createPlugins(config, options = {}) {
|
|
|
683
762
|
}
|
|
684
763
|
|
|
685
764
|
async function loadGherkinStepsAsync(paths) {
|
|
765
|
+
// Import BDD module to access step file tracking functions and step DSL
|
|
766
|
+
const bddModule = await import('./mocha/bdd.js')
|
|
767
|
+
|
|
686
768
|
global.Before = fn => event.dispatcher.on(event.test.started, fn)
|
|
687
769
|
global.After = fn => event.dispatcher.on(event.test.finished, fn)
|
|
688
770
|
global.Fail = fn => event.dispatcher.on(event.test.failed, fn)
|
|
689
771
|
|
|
690
|
-
//
|
|
691
|
-
|
|
772
|
+
// Scope-inject Given/When/Then/And while loading step files so they work
|
|
773
|
+
// with noGlobals: true. When noGlobals: false, globals.js has already set
|
|
774
|
+
// them as permanent globals — skip to avoid deleting them at the end.
|
|
775
|
+
const injectStepDsl = !!store.noGlobals
|
|
776
|
+
if (injectStepDsl) {
|
|
777
|
+
global.Given = bddModule.Given
|
|
778
|
+
global.When = bddModule.When
|
|
779
|
+
global.Then = bddModule.Then
|
|
780
|
+
global.And = bddModule.And
|
|
781
|
+
global.DefineParameterType = bddModule.defineParameterType
|
|
782
|
+
}
|
|
692
783
|
|
|
693
784
|
// If gherkin.steps is string, then this will iterate through that folder and send all step def js files to loadSupportObject
|
|
694
785
|
// If gherkin.steps is Array, it will go the old way
|
|
@@ -701,7 +792,7 @@ async function loadGherkinStepsAsync(paths) {
|
|
|
701
792
|
bddModule.clearCurrentStepFile()
|
|
702
793
|
}
|
|
703
794
|
} else {
|
|
704
|
-
const folderPath = paths.startsWith('.') ? normalizeAndJoin(
|
|
795
|
+
const folderPath = paths.startsWith('.') ? normalizeAndJoin(store.codeceptDir, paths) : ''
|
|
705
796
|
if (folderPath !== '') {
|
|
706
797
|
const files = globSync(folderPath)
|
|
707
798
|
for (const file of files) {
|
|
@@ -716,6 +807,13 @@ async function loadGherkinStepsAsync(paths) {
|
|
|
716
807
|
delete global.Before
|
|
717
808
|
delete global.After
|
|
718
809
|
delete global.Fail
|
|
810
|
+
if (injectStepDsl) {
|
|
811
|
+
delete global.Given
|
|
812
|
+
delete global.When
|
|
813
|
+
delete global.Then
|
|
814
|
+
delete global.And
|
|
815
|
+
delete global.DefineParameterType
|
|
816
|
+
}
|
|
719
817
|
}
|
|
720
818
|
|
|
721
819
|
function loadGherkinSteps(paths) {
|
|
@@ -749,7 +847,7 @@ async function loadSupportObject(modulePath, supportObjectName) {
|
|
|
749
847
|
}
|
|
750
848
|
}
|
|
751
849
|
if (typeof modulePath === 'string' && modulePath.charAt(0) === '.') {
|
|
752
|
-
modulePath = path.join(
|
|
850
|
+
modulePath = path.join(store.codeceptDir, modulePath)
|
|
753
851
|
}
|
|
754
852
|
try {
|
|
755
853
|
// Use dynamic import for both ESM and CJS modules
|
|
@@ -873,7 +971,7 @@ async function loadTranslation(locale, vocabularies) {
|
|
|
873
971
|
const langs = await Translation.getLangs()
|
|
874
972
|
if (langs[locale]) {
|
|
875
973
|
translation = new Translation(langs[locale])
|
|
876
|
-
} else if (fileExists(path.join(
|
|
974
|
+
} else if (fileExists(path.join(store.codeceptDir, locale))) {
|
|
877
975
|
// get from a provided file instead
|
|
878
976
|
translation = Translation.createDefault()
|
|
879
977
|
translation.loadVocabulary(locale)
|
|
@@ -890,7 +988,7 @@ function getHelperModuleName(helperName, config) {
|
|
|
890
988
|
// classical require
|
|
891
989
|
if (config[helperName].require) {
|
|
892
990
|
if (config[helperName].require.startsWith('.')) {
|
|
893
|
-
let helperPath = path.resolve(
|
|
991
|
+
let helperPath = path.resolve(store.codeceptDir, config[helperName].require)
|
|
894
992
|
// Add .js extension if not present for ESM compatibility
|
|
895
993
|
if (!path.extname(helperPath)) {
|
|
896
994
|
helperPath += '.js'
|