codeceptjs 4.0.0-beta.2 → 4.0.0-beta.21
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 +73 -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 +765 -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 +54 -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/container.js
CHANGED
|
@@ -1,34 +1,96 @@
|
|
|
1
|
-
import
|
|
2
|
-
import path
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
} from './utils.js'
|
|
9
|
-
import Translation from './translation.js'
|
|
10
|
-
import
|
|
11
|
-
|
|
12
|
-
import
|
|
13
|
-
import
|
|
14
|
-
import
|
|
15
|
-
import
|
|
16
|
-
import
|
|
17
|
-
|
|
18
|
-
|
|
1
|
+
import { globSync } from 'glob'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import fs from 'fs'
|
|
4
|
+
import debugModule from 'debug'
|
|
5
|
+
const debug = debugModule('codeceptjs:container')
|
|
6
|
+
import { MetaStep } from './step.js'
|
|
7
|
+
import { methodsOfObject, fileExists, isFunction, isAsyncFunction, installedLocally, deepMerge } from './utils.js'
|
|
8
|
+
import { transpileTypeScript, cleanupTempFiles } from './utils/typescript.js'
|
|
9
|
+
import Translation from './translation.js'
|
|
10
|
+
import MochaFactory from './mocha/factory.js'
|
|
11
|
+
import recorder from './recorder.js'
|
|
12
|
+
import event from './event.js'
|
|
13
|
+
import WorkerStorage from './workerStorage.js'
|
|
14
|
+
import store from './store.js'
|
|
15
|
+
import Result from './result.js'
|
|
16
|
+
import ai from './ai.js'
|
|
17
|
+
import actorFactory from './actor.js'
|
|
18
|
+
|
|
19
|
+
let asyncHelperPromise
|
|
20
|
+
let tsxLoaderRegistered = false
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Automatically register tsx ESM loader for TypeScript imports
|
|
24
|
+
* This allows loading .ts files without NODE_OPTIONS
|
|
25
|
+
*/
|
|
26
|
+
async function ensureTsxLoader() {
|
|
27
|
+
if (tsxLoaderRegistered) return true
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
// Check if tsx is available
|
|
31
|
+
const { createRequire } = await import('module')
|
|
32
|
+
const { pathToFileURL } = await import('url')
|
|
33
|
+
const userRequire = createRequire(pathToFileURL(path.join(global.codecept_dir || process.cwd(), 'package.json')).href)
|
|
34
|
+
|
|
35
|
+
// Try to resolve tsx from user's project
|
|
36
|
+
try {
|
|
37
|
+
userRequire.resolve('tsx')
|
|
38
|
+
} catch {
|
|
39
|
+
debug('tsx not found in user project')
|
|
40
|
+
return false
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Register tsx/esm loader dynamically
|
|
44
|
+
// Use Node.js register() API from node:module (Node 18.19+, 20.6+)
|
|
45
|
+
try {
|
|
46
|
+
const { register } = await import('node:module')
|
|
47
|
+
if (typeof register === 'function') {
|
|
48
|
+
debug('Registering tsx ESM loader via node:module register()')
|
|
49
|
+
const tsxPath = userRequire.resolve('tsx/esm')
|
|
50
|
+
register(tsxPath, import.meta.url)
|
|
51
|
+
tsxLoaderRegistered = true
|
|
52
|
+
return true
|
|
53
|
+
}
|
|
54
|
+
} catch (registerErr) {
|
|
55
|
+
debug('node:module register() not available:', registerErr.message)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
debug('module.register not available, tsx loader must be set via NODE_OPTIONS')
|
|
59
|
+
return false
|
|
60
|
+
} catch (err) {
|
|
61
|
+
debug('Failed to register tsx loader:', err.message)
|
|
62
|
+
return false
|
|
63
|
+
}
|
|
64
|
+
}
|
|
19
65
|
|
|
20
66
|
let container = {
|
|
21
67
|
helpers: {},
|
|
22
68
|
support: {},
|
|
69
|
+
proxySupport: {},
|
|
23
70
|
plugins: {},
|
|
71
|
+
actor: null,
|
|
72
|
+
/**
|
|
73
|
+
* @type {Mocha | {}}
|
|
74
|
+
* @ignore
|
|
75
|
+
*/
|
|
24
76
|
mocha: {},
|
|
25
77
|
translation: {},
|
|
26
|
-
}
|
|
78
|
+
/** @type {Result | null} */
|
|
79
|
+
result: null,
|
|
80
|
+
sharedKeys: new Set() // Track keys shared via share() function
|
|
81
|
+
}
|
|
27
82
|
|
|
28
83
|
/**
|
|
29
84
|
* Dependency Injection Container
|
|
30
85
|
*/
|
|
31
|
-
|
|
86
|
+
class Container {
|
|
87
|
+
/**
|
|
88
|
+
* Get the standard acting helpers of CodeceptJS Container
|
|
89
|
+
*
|
|
90
|
+
*/
|
|
91
|
+
static get STANDARD_ACTING_HELPERS() {
|
|
92
|
+
return ['Playwright', 'WebDriver', 'Puppeteer', 'Appium']
|
|
93
|
+
}
|
|
32
94
|
/**
|
|
33
95
|
* Create container with all required helpers and support objects
|
|
34
96
|
*
|
|
@@ -36,22 +98,70 @@ export default class Container {
|
|
|
36
98
|
* @param {*} config
|
|
37
99
|
* @param {*} opts
|
|
38
100
|
*/
|
|
39
|
-
static create(config, opts) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
101
|
+
static async create(config, opts) {
|
|
102
|
+
debug('creating container')
|
|
103
|
+
asyncHelperPromise = Promise.resolve()
|
|
104
|
+
|
|
105
|
+
// dynamically create mocha instance
|
|
106
|
+
const mochaConfig = config.mocha || {}
|
|
107
|
+
if (config.grep && !opts.grep) mochaConfig.grep = config.grep
|
|
108
|
+
this.createMocha = () => (container.mocha = MochaFactory.create(mochaConfig, opts || {}))
|
|
109
|
+
this.createMocha()
|
|
110
|
+
|
|
111
|
+
// create support objects
|
|
112
|
+
container.support = {}
|
|
113
|
+
container.helpers = await createHelpers(config.helpers || {})
|
|
114
|
+
container.translation = await loadTranslation(config.translation || null, config.vocabularies || [])
|
|
115
|
+
container.proxySupport = createSupportObjects(config.include || {})
|
|
116
|
+
container.plugins = await createPlugins(config.plugins || {}, opts)
|
|
117
|
+
container.result = new Result()
|
|
118
|
+
|
|
119
|
+
// Preload includes (so proxies can expose real objects synchronously)
|
|
120
|
+
const includes = config.include || {}
|
|
121
|
+
|
|
122
|
+
// Ensure I is available for DI modules at import time
|
|
123
|
+
if (Object.prototype.hasOwnProperty.call(includes, 'I')) {
|
|
124
|
+
try {
|
|
125
|
+
const mod = includes.I
|
|
126
|
+
if (typeof mod === 'string') {
|
|
127
|
+
container.support.I = await loadSupportObject(mod, 'I')
|
|
128
|
+
} else if (typeof mod === 'function') {
|
|
129
|
+
container.support.I = await loadSupportObject(mod, 'I')
|
|
130
|
+
} else if (mod && typeof mod === 'object') {
|
|
131
|
+
container.support.I = mod
|
|
132
|
+
}
|
|
133
|
+
} catch (e) {
|
|
134
|
+
throw new Error(`Could not include object I: ${e.message}`)
|
|
135
|
+
}
|
|
136
|
+
} else {
|
|
137
|
+
// Create default actor if not provided via includes
|
|
138
|
+
createActor()
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Load remaining includes except I
|
|
142
|
+
for (const [name, mod] of Object.entries(includes)) {
|
|
143
|
+
if (name === 'I') continue
|
|
144
|
+
try {
|
|
145
|
+
if (typeof mod === 'string') {
|
|
146
|
+
container.support[name] = await loadSupportObject(mod, name)
|
|
147
|
+
} else if (typeof mod === 'function') {
|
|
148
|
+
// function or class
|
|
149
|
+
container.support[name] = await loadSupportObject(mod, name)
|
|
150
|
+
} else if (mod && typeof mod === 'object') {
|
|
151
|
+
container.support[name] = mod
|
|
152
|
+
}
|
|
153
|
+
} catch (e) {
|
|
154
|
+
throw new Error(`Could not include object ${name}: ${e.message}`)
|
|
155
|
+
}
|
|
43
156
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
container.
|
|
52
|
-
if (opts && opts.ai) ai.enable(config.ai); // enable AI Assistant
|
|
53
|
-
if (config.gherkin) loadGherkinSteps(config.gherkin.steps || []);
|
|
54
|
-
if (opts && typeof opts.timeouts === 'boolean') store.timeouts = opts.timeouts;
|
|
157
|
+
|
|
158
|
+
if (opts && opts.ai) ai.enable(config.ai) // enable AI Assistant
|
|
159
|
+
if (config.gherkin) await loadGherkinStepsAsync(config.gherkin.steps || [])
|
|
160
|
+
if (opts && typeof opts.timeouts === 'boolean') store.timeouts = opts.timeouts
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
static actor() {
|
|
164
|
+
return container.support.I
|
|
55
165
|
}
|
|
56
166
|
|
|
57
167
|
/**
|
|
@@ -62,7 +172,10 @@ export default class Container {
|
|
|
62
172
|
* @returns { * }
|
|
63
173
|
*/
|
|
64
174
|
static plugins(name) {
|
|
65
|
-
|
|
175
|
+
if (!name) {
|
|
176
|
+
return container.plugins
|
|
177
|
+
}
|
|
178
|
+
return container.plugins[name]
|
|
66
179
|
}
|
|
67
180
|
|
|
68
181
|
/**
|
|
@@ -74,9 +187,10 @@ export default class Container {
|
|
|
74
187
|
*/
|
|
75
188
|
static support(name) {
|
|
76
189
|
if (!name) {
|
|
77
|
-
return container.
|
|
190
|
+
return container.proxySupport
|
|
78
191
|
}
|
|
79
|
-
return
|
|
192
|
+
// Always return the proxy to ensure MetaStep creation works
|
|
193
|
+
return container.proxySupport[name]
|
|
80
194
|
}
|
|
81
195
|
|
|
82
196
|
/**
|
|
@@ -88,9 +202,9 @@ export default class Container {
|
|
|
88
202
|
*/
|
|
89
203
|
static helpers(name) {
|
|
90
204
|
if (!name) {
|
|
91
|
-
return container.helpers
|
|
205
|
+
return container.helpers
|
|
92
206
|
}
|
|
93
|
-
return container.helpers[name]
|
|
207
|
+
return container.helpers[name]
|
|
94
208
|
}
|
|
95
209
|
|
|
96
210
|
/**
|
|
@@ -99,7 +213,7 @@ export default class Container {
|
|
|
99
213
|
* @api
|
|
100
214
|
*/
|
|
101
215
|
static translation() {
|
|
102
|
-
return container.translation
|
|
216
|
+
return container.translation
|
|
103
217
|
}
|
|
104
218
|
|
|
105
219
|
/**
|
|
@@ -109,7 +223,19 @@ export default class Container {
|
|
|
109
223
|
* @returns { * }
|
|
110
224
|
*/
|
|
111
225
|
static mocha() {
|
|
112
|
-
return container.mocha
|
|
226
|
+
return container.mocha
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Get result
|
|
231
|
+
*
|
|
232
|
+
* @returns {Result}
|
|
233
|
+
*/
|
|
234
|
+
static result() {
|
|
235
|
+
if (!container.result) {
|
|
236
|
+
container.result = new Result()
|
|
237
|
+
}
|
|
238
|
+
return container.result
|
|
113
239
|
}
|
|
114
240
|
|
|
115
241
|
/**
|
|
@@ -119,7 +245,15 @@ export default class Container {
|
|
|
119
245
|
* @param {Object<string, *>} newContainer
|
|
120
246
|
*/
|
|
121
247
|
static append(newContainer) {
|
|
122
|
-
container = deepMerge(container, newContainer)
|
|
248
|
+
container = deepMerge(container, newContainer)
|
|
249
|
+
|
|
250
|
+
// If new support objects are added, update the proxy support
|
|
251
|
+
if (newContainer.support) {
|
|
252
|
+
const newProxySupport = createSupportObjects(newContainer.support)
|
|
253
|
+
container.proxySupport = { ...container.proxySupport, ...newProxySupport }
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
debug('appended', JSON.stringify(newContainer).slice(0, 300))
|
|
123
257
|
}
|
|
124
258
|
|
|
125
259
|
/**
|
|
@@ -129,11 +263,26 @@ export default class Container {
|
|
|
129
263
|
* @param {Object<string, *>} newSupport
|
|
130
264
|
* @param {Object<string, *>} newPlugins
|
|
131
265
|
*/
|
|
132
|
-
static clear(newHelpers, newSupport, newPlugins) {
|
|
133
|
-
container.helpers = newHelpers
|
|
134
|
-
container.
|
|
135
|
-
container.
|
|
136
|
-
container.
|
|
266
|
+
static async clear(newHelpers = {}, newSupport = {}, newPlugins = {}) {
|
|
267
|
+
container.helpers = newHelpers
|
|
268
|
+
container.translation = await loadTranslation()
|
|
269
|
+
container.proxySupport = createSupportObjects(newSupport)
|
|
270
|
+
container.plugins = newPlugins
|
|
271
|
+
container.sharedKeys = new Set() // Clear shared keys
|
|
272
|
+
asyncHelperPromise = Promise.resolve()
|
|
273
|
+
store.actor = null
|
|
274
|
+
debug('container cleared')
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* @param {Function|null} fn
|
|
279
|
+
* @returns {Promise<void>}
|
|
280
|
+
*/
|
|
281
|
+
static async started(fn = null) {
|
|
282
|
+
if (fn) {
|
|
283
|
+
asyncHelperPromise = asyncHelperPromise.then(fn)
|
|
284
|
+
}
|
|
285
|
+
return asyncHelperPromise
|
|
137
286
|
}
|
|
138
287
|
|
|
139
288
|
/**
|
|
@@ -143,308 +292,663 @@ export default class Container {
|
|
|
143
292
|
* @param {Object} options - set {local: true} to not share among workers
|
|
144
293
|
*/
|
|
145
294
|
static share(data, options = {}) {
|
|
146
|
-
|
|
295
|
+
// Instead of using append which replaces the entire container,
|
|
296
|
+
// directly update the support object to maintain proxy references
|
|
297
|
+
Object.assign(container.support, data)
|
|
298
|
+
|
|
299
|
+
// Track which keys were explicitly shared
|
|
300
|
+
Object.keys(data).forEach(key => container.sharedKeys.add(key))
|
|
301
|
+
|
|
147
302
|
if (!options.local) {
|
|
148
|
-
WorkerStorage.share(data)
|
|
303
|
+
WorkerStorage.share(data)
|
|
149
304
|
}
|
|
150
305
|
}
|
|
306
|
+
|
|
307
|
+
static createMocha(config = {}, opts = {}) {
|
|
308
|
+
const mochaConfig = config?.mocha || {}
|
|
309
|
+
if (config?.grep && !opts?.grep) {
|
|
310
|
+
mochaConfig.grep = config.grep
|
|
311
|
+
}
|
|
312
|
+
container.mocha = MochaFactory.create(mochaConfig, opts || {})
|
|
313
|
+
}
|
|
151
314
|
}
|
|
152
315
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
316
|
+
export default Container
|
|
317
|
+
|
|
318
|
+
async function createHelpers(config) {
|
|
319
|
+
const helpers = {}
|
|
320
|
+
for (let helperName in config) {
|
|
157
321
|
try {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
} else {
|
|
166
|
-
moduleName = `./helper/${helperName}.js`; // built-in helper
|
|
322
|
+
let HelperClass
|
|
323
|
+
|
|
324
|
+
// Check if helper class was stored in config during ESM import processing
|
|
325
|
+
if (config[helperName]._helperClass) {
|
|
326
|
+
HelperClass = config[helperName]._helperClass
|
|
327
|
+
debug(`helper ${helperName} loaded from ESM import`)
|
|
167
328
|
}
|
|
168
329
|
|
|
169
|
-
//
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
174
|
-
HelperClass = importSync(path.resolve(__dirname, moduleName)).default;
|
|
175
|
-
} else {
|
|
176
|
-
// check if the new syntax export default HelperName is used and loads the Helper, otherwise loads the module that used old syntax export = HelperName.
|
|
177
|
-
HelperClass = importSync(path.resolve(moduleName)).default;
|
|
330
|
+
// ESM import (legacy check)
|
|
331
|
+
if (!HelperClass && typeof helperName === 'function' && helperName.prototype) {
|
|
332
|
+
HelperClass = helperName
|
|
333
|
+
helperName = HelperClass.constructor.name
|
|
178
334
|
}
|
|
179
335
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
336
|
+
// classical require - may be async for ESM modules
|
|
337
|
+
if (!HelperClass) {
|
|
338
|
+
const helperResult = requireHelperFromModule(helperName, config)
|
|
339
|
+
if (helperResult instanceof Promise) {
|
|
340
|
+
// Handle async ESM loading
|
|
341
|
+
helpers[helperName] = {}
|
|
342
|
+
asyncHelperPromise = asyncHelperPromise
|
|
343
|
+
.then(() => helperResult)
|
|
344
|
+
.then(async ResolvedHelperClass => {
|
|
345
|
+
debug(`helper ${helperName} resolved type: ${typeof ResolvedHelperClass}`, ResolvedHelperClass)
|
|
346
|
+
|
|
347
|
+
// Extract default export from ESM module wrapper if needed
|
|
348
|
+
if (ResolvedHelperClass && ResolvedHelperClass.__esModule && ResolvedHelperClass.default) {
|
|
349
|
+
ResolvedHelperClass = ResolvedHelperClass.default
|
|
350
|
+
debug(`extracted default export for ${helperName}, new type: ${typeof ResolvedHelperClass}`)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (typeof ResolvedHelperClass !== 'function') {
|
|
354
|
+
throw new Error(`Helper '${helperName}' is not a class. Got: ${typeof ResolvedHelperClass}`)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
checkHelperRequirements(ResolvedHelperClass)
|
|
358
|
+
helpers[helperName] = new ResolvedHelperClass(config[helperName])
|
|
359
|
+
if (helpers[helperName]._init) await helpers[helperName]._init()
|
|
360
|
+
debug(`helper ${helperName} async initialized`)
|
|
361
|
+
})
|
|
362
|
+
continue
|
|
363
|
+
} else {
|
|
364
|
+
HelperClass = helperResult
|
|
191
365
|
}
|
|
192
366
|
}
|
|
193
|
-
|
|
367
|
+
|
|
368
|
+
// handle async CJS modules that use dynamic import
|
|
369
|
+
if (isAsyncFunction(HelperClass)) {
|
|
370
|
+
helpers[helperName] = {}
|
|
371
|
+
|
|
372
|
+
asyncHelperPromise = asyncHelperPromise
|
|
373
|
+
.then(() => HelperClass())
|
|
374
|
+
.then(ResolvedHelperClass => {
|
|
375
|
+
// Check if ResolvedHelperClass is a constructor function
|
|
376
|
+
if (typeof ResolvedHelperClass?.constructor !== 'function') {
|
|
377
|
+
throw new Error(`Helper class from module '${helperName}' is not a class. Use CJS async module syntax.`)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
debug(`helper ${helperName} async initialized`)
|
|
381
|
+
|
|
382
|
+
helpers[helperName] = new ResolvedHelperClass(config[helperName])
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
continue
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
checkHelperRequirements(HelperClass)
|
|
389
|
+
|
|
390
|
+
helpers[helperName] = new HelperClass(config[helperName])
|
|
391
|
+
debug(`helper ${helperName} initialized`)
|
|
194
392
|
} catch (err) {
|
|
195
|
-
throw new Error(`Could not load helper ${helperName}
|
|
393
|
+
throw new Error(`Could not load helper ${helperName} (${err.message})`)
|
|
196
394
|
}
|
|
197
395
|
}
|
|
198
396
|
|
|
397
|
+
// Wait for all async helpers to be resolved before calling _init
|
|
398
|
+
await asyncHelperPromise
|
|
399
|
+
|
|
199
400
|
for (const name in helpers) {
|
|
200
|
-
if (helpers[name]._init) helpers[name]._init()
|
|
401
|
+
if (helpers[name]._init) await helpers[name]._init()
|
|
201
402
|
}
|
|
202
|
-
return helpers
|
|
403
|
+
return helpers
|
|
203
404
|
}
|
|
204
405
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
406
|
+
function checkHelperRequirements(HelperClass) {
|
|
407
|
+
if (HelperClass._checkRequirements) {
|
|
408
|
+
const requirements = HelperClass._checkRequirements()
|
|
409
|
+
if (requirements) {
|
|
410
|
+
let install
|
|
411
|
+
if (installedLocally()) {
|
|
412
|
+
install = `npm install --save-dev ${requirements.join(' ')}`
|
|
413
|
+
} else {
|
|
414
|
+
console.log('WARNING: CodeceptJS is not installed locally. It is recommended to switch to local installation')
|
|
415
|
+
install = `[sudo] npm install -g ${requirements.join(' ')}`
|
|
416
|
+
}
|
|
417
|
+
throw new Error(`Required modules are not installed.\n\nRUN: ${install}`)
|
|
217
418
|
}
|
|
218
419
|
}
|
|
420
|
+
}
|
|
219
421
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
let newObj = getSupportObject(config, name);
|
|
422
|
+
async function requireHelperFromModule(helperName, config, HelperClass) {
|
|
423
|
+
const moduleName = getHelperModuleName(helperName, config)
|
|
424
|
+
if (moduleName.startsWith('./helper/')) {
|
|
224
425
|
try {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
426
|
+
// For built-in helpers, use direct relative import with .js extension
|
|
427
|
+
const helperPath = `${moduleName}.js`
|
|
428
|
+
const mod = await import(helperPath)
|
|
429
|
+
HelperClass = mod.default || mod
|
|
430
|
+
} catch (err) {
|
|
431
|
+
throw err
|
|
432
|
+
}
|
|
433
|
+
} else {
|
|
434
|
+
// check if the new syntax export default HelperName is used and loads the Helper, otherwise loads the module that used old syntax export = HelperName.
|
|
435
|
+
try {
|
|
436
|
+
// Try dynamic import for both CommonJS and ESM modules
|
|
437
|
+
const mod = await import(moduleName)
|
|
438
|
+
if (!mod && !mod.default) {
|
|
439
|
+
throw new Error(`Helper module '${moduleName}' was not found. Make sure you have installed the package correctly.`)
|
|
229
440
|
}
|
|
441
|
+
HelperClass = mod.default || mod
|
|
230
442
|
} catch (err) {
|
|
231
|
-
|
|
443
|
+
if (err.code === 'ERR_UNKNOWN_FILE_EXTENSION' || (err.message && err.message.includes('Unknown file extension'))) {
|
|
444
|
+
// This is likely a TypeScript helper file. Transpile it to a temporary JS file
|
|
445
|
+
// and import that as a reliable fallback (no NODE_OPTIONS required).
|
|
446
|
+
try {
|
|
447
|
+
const pathModule = await import('path')
|
|
448
|
+
const absolutePath = pathModule.default.resolve(moduleName)
|
|
449
|
+
|
|
450
|
+
// Attempt to load local 'typescript' to transpile the helper
|
|
451
|
+
let typescript
|
|
452
|
+
try {
|
|
453
|
+
typescript = await import('typescript')
|
|
454
|
+
} catch (tsImportErr) {
|
|
455
|
+
throw new Error(`TypeScript helper detected (${moduleName}). Please install 'typescript' in your project: npm install --save-dev typescript`)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const { tempFile, allTempFiles } = await transpileTypeScript(absolutePath, typescript)
|
|
459
|
+
debug(`Transpiled TypeScript helper: ${moduleName} -> ${tempFile}`)
|
|
460
|
+
|
|
461
|
+
try {
|
|
462
|
+
try {
|
|
463
|
+
const mod = await import(tempFile)
|
|
464
|
+
HelperClass = mod.default || mod
|
|
465
|
+
debug(`helper ${helperName} loaded from transpiled JS: ${tempFile}`)
|
|
466
|
+
} catch (importTempErr) {
|
|
467
|
+
// If import fails due to CommonJS named export incompatibility,
|
|
468
|
+
// try a quick transform: convert named imports from CommonJS packages
|
|
469
|
+
// into default import + destructuring. This resolves cases like
|
|
470
|
+
// "Named export 'Helper' not found. The requested module '@codeceptjs/helper' is a CommonJS module"
|
|
471
|
+
const msg = importTempErr && importTempErr.message || ''
|
|
472
|
+
const commonJsMatch = msg.match(/The requested module '(.+?)' is a CommonJS module/)
|
|
473
|
+
if (commonJsMatch) {
|
|
474
|
+
// Read the transpiled file, perform heuristic replacement, and import again
|
|
475
|
+
const fs = await import('fs')
|
|
476
|
+
let content = fs.readFileSync(tempFile, 'utf8')
|
|
477
|
+
// Heuristic: replace "import { X, Y } from 'mod'" with default import + destructure
|
|
478
|
+
content = content.replace(/import\s+\{([^}]+)\}\s+from\s+(['"])([^'"\)]+)\2/gm, (m, names, q, modName) => {
|
|
479
|
+
// Only adjust imports for the module reported in the error or for local modules
|
|
480
|
+
if (modName === commonJsMatch[1] || modName.startsWith('.') || !modName.includes('/')) {
|
|
481
|
+
const cleanedNames = names.trim()
|
|
482
|
+
return `import pkg__interop from ${q}${modName}${q};\nconst { ${cleanedNames} } = pkg__interop`
|
|
483
|
+
}
|
|
484
|
+
return m
|
|
485
|
+
})
|
|
486
|
+
|
|
487
|
+
// Write to a secondary temp file
|
|
488
|
+
const os = await import('os')
|
|
489
|
+
const path = await import('path')
|
|
490
|
+
const fallbackTemp = path.default.join(os.default.tmpdir(), `helper-fallback-${Date.now()}.mjs`)
|
|
491
|
+
fs.writeFileSync(fallbackTemp, content, 'utf8')
|
|
492
|
+
try {
|
|
493
|
+
const mod = await import(fallbackTemp)
|
|
494
|
+
HelperClass = mod.default || mod
|
|
495
|
+
debug(`helper ${helperName} loaded from transpiled JS after CommonJS interop fix: ${fallbackTemp}`)
|
|
496
|
+
} finally {
|
|
497
|
+
try { fs.unlinkSync(fallbackTemp) } catch (e) {}
|
|
498
|
+
}
|
|
499
|
+
} else {
|
|
500
|
+
throw importTempErr
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
} finally {
|
|
504
|
+
// Cleanup transpiled temporary files
|
|
505
|
+
const filesToClean = Array.isArray(allTempFiles) ? allTempFiles : [allTempFiles]
|
|
506
|
+
cleanupTempFiles(filesToClean)
|
|
507
|
+
}
|
|
508
|
+
} catch (importErr) {
|
|
509
|
+
throw new Error(
|
|
510
|
+
`Helper '${helperName}' is a TypeScript file but could not be loaded.\n` +
|
|
511
|
+
`Path: ${moduleName}\n` +
|
|
512
|
+
`Error: ${importErr.message}\n\n` +
|
|
513
|
+
`To load TypeScript helpers, install 'typescript' in your project or use an ESM loader (e.g. tsx):\n` +
|
|
514
|
+
` npm install --save-dev typescript\n` +
|
|
515
|
+
` OR run CodeceptJS with an ESM loader: NODE_OPTIONS='--import tsx' npx codeceptjs run\n\n` +
|
|
516
|
+
`CodeceptJS will transpile TypeScript helpers automatically at runtime if 'typescript' is available.`
|
|
517
|
+
)
|
|
518
|
+
}
|
|
519
|
+
} else if (err.code === 'ERR_REQUIRE_ESM' || (err.message && err.message.includes('ES module'))) {
|
|
520
|
+
// This is an ESM module, use dynamic import
|
|
521
|
+
try {
|
|
522
|
+
const pathModule = await import('path')
|
|
523
|
+
const absolutePath = pathModule.default.resolve(moduleName)
|
|
524
|
+
const mod = await import(absolutePath)
|
|
525
|
+
HelperClass = mod.default || mod
|
|
526
|
+
debug(`helper ${helperName} loaded via ESM import`)
|
|
527
|
+
} catch (importErr) {
|
|
528
|
+
throw new Error(`Helper module '${moduleName}' could not be imported as ESM: ${importErr.message}`)
|
|
529
|
+
}
|
|
530
|
+
} else if (err.code === 'MODULE_NOT_FOUND') {
|
|
531
|
+
throw new Error(`Helper module '${moduleName}' was not found. Make sure you have installed the package correctly.`)
|
|
532
|
+
} else {
|
|
533
|
+
throw err
|
|
534
|
+
}
|
|
232
535
|
}
|
|
233
|
-
return newObj;
|
|
234
536
|
}
|
|
537
|
+
return HelperClass
|
|
538
|
+
}
|
|
235
539
|
|
|
540
|
+
function createSupportObjects(config) {
|
|
236
541
|
const asyncWrapper = function (f) {
|
|
237
542
|
return function () {
|
|
238
|
-
return f.apply(this, arguments).catch(
|
|
239
|
-
recorder.saveFirstAsyncError(e)
|
|
240
|
-
throw e
|
|
241
|
-
})
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
Object.keys(objects).forEach((object) => {
|
|
246
|
-
const currentObject = objects[object];
|
|
247
|
-
Object.keys(currentObject).forEach((method) => {
|
|
248
|
-
const currentMethod = currentObject[method];
|
|
249
|
-
if (currentMethod && currentMethod[Symbol.toStringTag] === 'AsyncFunction') {
|
|
250
|
-
objects[object][method] = asyncWrapper(currentMethod);
|
|
251
|
-
}
|
|
252
|
-
});
|
|
253
|
-
});
|
|
543
|
+
return f.apply(this, arguments).catch(e => {
|
|
544
|
+
recorder.saveFirstAsyncError(e)
|
|
545
|
+
throw e
|
|
546
|
+
})
|
|
547
|
+
}
|
|
548
|
+
}
|
|
254
549
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
550
|
+
function lazyLoad(name) {
|
|
551
|
+
return new Proxy(
|
|
552
|
+
{},
|
|
553
|
+
{
|
|
554
|
+
get(target, prop) {
|
|
555
|
+
// behavr like array or
|
|
556
|
+
if (prop === 'length') return Object.keys(config).length
|
|
557
|
+
if (prop === Symbol.iterator) {
|
|
558
|
+
return function* () {
|
|
559
|
+
for (let i = 0; i < Object.keys(config).length; i++) {
|
|
560
|
+
yield target[i]
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// load actual name from vocabulary
|
|
566
|
+
if (container.translation && container.translation.I && name === 'I') {
|
|
567
|
+
// Use translated name for I
|
|
568
|
+
const actualName = container.translation.I
|
|
569
|
+
if (actualName !== 'I') {
|
|
570
|
+
name = actualName
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if (name === 'I') {
|
|
575
|
+
if (!container.support.I) {
|
|
576
|
+
// Actor will be created during container.create()
|
|
577
|
+
return undefined
|
|
578
|
+
}
|
|
579
|
+
methodsOfObject(container.support.I)
|
|
580
|
+
return container.support.I[prop]
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (!container.support[name] && typeof config[name] === 'object') {
|
|
584
|
+
container.support[name] = config[name]
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (!container.support[name]) {
|
|
588
|
+
// Cannot load object synchronously in proxy getter
|
|
589
|
+
// Return undefined and log warning - object should be pre-loaded during container creation
|
|
590
|
+
debug(`Support object ${name} not pre-loaded, returning undefined`)
|
|
591
|
+
return undefined
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const currentObject = container.support[name]
|
|
595
|
+
let currentValue = currentObject[prop]
|
|
596
|
+
|
|
597
|
+
if (isFunction(currentValue) || isAsyncFunction(currentValue)) {
|
|
598
|
+
const ms = new MetaStep(name, prop)
|
|
599
|
+
ms.setContext(currentObject)
|
|
600
|
+
if (isAsyncFunction(currentValue)) currentValue = asyncWrapper(currentValue)
|
|
601
|
+
debug(`metastep is created for ${name}.${prop.toString()}()`)
|
|
602
|
+
return ms.run.bind(ms, currentValue)
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
return currentValue
|
|
606
|
+
},
|
|
607
|
+
has(target, prop) {
|
|
608
|
+
if (!container.support[name]) {
|
|
609
|
+
// Note: This is sync, so we can't use async loadSupportObject here
|
|
610
|
+
// The object will be loaded lazily on first property access
|
|
611
|
+
return false
|
|
612
|
+
}
|
|
613
|
+
return prop in container.support[name]
|
|
614
|
+
},
|
|
615
|
+
getOwnPropertyDescriptor(target, prop) {
|
|
616
|
+
if (!container.support[name]) {
|
|
617
|
+
// Object will be loaded on property access
|
|
618
|
+
return {
|
|
619
|
+
enumerable: true,
|
|
620
|
+
configurable: true,
|
|
621
|
+
value: undefined,
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
return {
|
|
625
|
+
enumerable: true,
|
|
626
|
+
configurable: true,
|
|
627
|
+
value: container.support[name][prop],
|
|
628
|
+
}
|
|
629
|
+
},
|
|
630
|
+
ownKeys() {
|
|
631
|
+
if (!container.support[name]) {
|
|
632
|
+
return []
|
|
633
|
+
}
|
|
634
|
+
return Reflect.ownKeys(container.support[name])
|
|
635
|
+
},
|
|
636
|
+
},
|
|
637
|
+
)
|
|
638
|
+
}
|
|
269
639
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
640
|
+
const keys = Reflect.ownKeys(config)
|
|
641
|
+
return new Proxy(
|
|
642
|
+
{},
|
|
643
|
+
{
|
|
644
|
+
has(target, key) {
|
|
645
|
+
return keys.includes(key) || container.sharedKeys.has(key)
|
|
646
|
+
},
|
|
647
|
+
ownKeys() {
|
|
648
|
+
// Return both original config keys and explicitly shared keys
|
|
649
|
+
return [...new Set([...keys, ...container.sharedKeys])]
|
|
650
|
+
},
|
|
651
|
+
getOwnPropertyDescriptor(target, prop) {
|
|
652
|
+
return {
|
|
653
|
+
enumerable: true,
|
|
654
|
+
configurable: true,
|
|
655
|
+
value: target[prop],
|
|
275
656
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
657
|
+
},
|
|
658
|
+
get(target, key) {
|
|
659
|
+
// First check if this is an explicitly shared property
|
|
660
|
+
if (container.sharedKeys.has(key) && key in container.support) {
|
|
661
|
+
return container.support[key]
|
|
662
|
+
}
|
|
663
|
+
return lazyLoad(key)
|
|
664
|
+
},
|
|
279
665
|
},
|
|
280
|
-
|
|
666
|
+
)
|
|
281
667
|
}
|
|
282
668
|
|
|
283
|
-
function
|
|
284
|
-
|
|
669
|
+
function createActor(actorPath) {
|
|
670
|
+
if (container.support.I) return container.support.I
|
|
671
|
+
|
|
672
|
+
// Default actor
|
|
673
|
+
container.support.I = actorFactory({}, Container)
|
|
285
674
|
|
|
286
|
-
|
|
675
|
+
return container.support.I
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
async function loadPluginAsync(modulePath, config) {
|
|
679
|
+
let pluginMod
|
|
680
|
+
try {
|
|
681
|
+
// Try dynamic import first (works for both ESM and CJS)
|
|
682
|
+
pluginMod = await import(modulePath)
|
|
683
|
+
} catch (err) {
|
|
684
|
+
throw new Error(`Could not load plugin from '${modulePath}': ${err.message}`)
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const pluginFactory = pluginMod.default || pluginMod
|
|
688
|
+
if (typeof pluginFactory !== 'function') {
|
|
689
|
+
throw new Error(`Plugin '${modulePath}' is not a function. Expected a plugin factory function.`)
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
return pluginFactory(config)
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
async function loadPluginFallback(modulePath, config) {
|
|
696
|
+
// This function is kept for backwards compatibility but now uses dynamic import
|
|
697
|
+
return await loadPluginAsync(modulePath, config)
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
async function createPlugins(config, options = {}) {
|
|
701
|
+
const plugins = {}
|
|
702
|
+
|
|
703
|
+
const enabledPluginsByOptions = (options.plugins || '').split(',')
|
|
287
704
|
for (const pluginName in config) {
|
|
288
|
-
if (!config[pluginName]) config[pluginName] = {}
|
|
289
|
-
if (!config[pluginName].enabled &&
|
|
290
|
-
continue
|
|
705
|
+
if (!config[pluginName]) config[pluginName] = {}
|
|
706
|
+
if (!config[pluginName].enabled && enabledPluginsByOptions.indexOf(pluginName) < 0) {
|
|
707
|
+
continue // plugin is disabled
|
|
291
708
|
}
|
|
292
|
-
let module
|
|
709
|
+
let module
|
|
293
710
|
try {
|
|
294
711
|
if (config[pluginName].require) {
|
|
295
|
-
module = config[pluginName].require
|
|
296
|
-
if (module.startsWith('.')) {
|
|
297
|
-
//
|
|
298
|
-
module = path.resolve(global.codecept_dir, module)
|
|
712
|
+
module = config[pluginName].require
|
|
713
|
+
if (module.startsWith('.')) {
|
|
714
|
+
// local
|
|
715
|
+
module = path.resolve(global.codecept_dir, module) // custom plugin
|
|
299
716
|
}
|
|
300
717
|
} else {
|
|
301
|
-
module = `./plugin/${pluginName}.js
|
|
718
|
+
module = `./plugin/${pluginName}.js`
|
|
302
719
|
}
|
|
303
|
-
|
|
720
|
+
|
|
721
|
+
// Use async loading for all plugins (ESM and CJS)
|
|
722
|
+
plugins[pluginName] = await loadPluginAsync(module, config[pluginName])
|
|
723
|
+
debug(`plugin ${pluginName} loaded via async import`)
|
|
304
724
|
} catch (err) {
|
|
305
|
-
throw new Error(`Could not load plugin ${pluginName} from module '${module}':\n${err.message}\n${err.stack}`)
|
|
725
|
+
throw new Error(`Could not load plugin ${pluginName} from module '${module}':\n${err.message}\n${err.stack}`)
|
|
306
726
|
}
|
|
307
727
|
}
|
|
308
|
-
return plugins
|
|
728
|
+
return plugins
|
|
309
729
|
}
|
|
310
730
|
|
|
311
|
-
function
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
}
|
|
316
|
-
return module;
|
|
317
|
-
}
|
|
731
|
+
async function loadGherkinStepsAsync(paths) {
|
|
732
|
+
global.Before = fn => event.dispatcher.on(event.test.started, fn)
|
|
733
|
+
global.After = fn => event.dispatcher.on(event.test.finished, fn)
|
|
734
|
+
global.Fail = fn => event.dispatcher.on(event.test.failed, fn)
|
|
318
735
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
global.Before = fn => event.dispatcher.on(event.test.started, fn);
|
|
322
|
-
// @ts-ignore
|
|
323
|
-
global.After = fn => event.dispatcher.on(event.test.finished, fn);
|
|
324
|
-
global.Fail = fn => event.dispatcher.on(event.test.failed, fn);
|
|
736
|
+
// Import BDD module to access step file tracking functions
|
|
737
|
+
const bddModule = await import('./mocha/bdd.js')
|
|
325
738
|
|
|
326
739
|
// If gherkin.steps is string, then this will iterate through that folder and send all step def js files to loadSupportObject
|
|
327
740
|
// If gherkin.steps is Array, it will go the old way
|
|
328
741
|
// This is done so that we need not enter all Step Definition files under config.gherkin.steps
|
|
329
742
|
if (Array.isArray(paths)) {
|
|
330
743
|
for (const path of paths) {
|
|
331
|
-
|
|
744
|
+
// Set context for step definition file location tracking
|
|
745
|
+
bddModule.setCurrentStepFile(path)
|
|
746
|
+
await loadSupportObject(path, `Step Definition from ${path}`)
|
|
747
|
+
bddModule.clearCurrentStepFile()
|
|
332
748
|
}
|
|
333
749
|
} else {
|
|
334
|
-
|
|
335
|
-
const folderPath = paths.startsWith('.') ? path.join(global.codecept_dir, paths) : '';
|
|
750
|
+
const folderPath = paths.startsWith('.') ? normalizeAndJoin(global.codecept_dir, paths) : ''
|
|
336
751
|
if (folderPath !== '') {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
752
|
+
const files = globSync(folderPath)
|
|
753
|
+
for (const file of files) {
|
|
754
|
+
// Set context for step definition file location tracking
|
|
755
|
+
bddModule.setCurrentStepFile(file)
|
|
756
|
+
await loadSupportObject(file, `Step Definition from ${file}`)
|
|
757
|
+
bddModule.clearCurrentStepFile()
|
|
758
|
+
}
|
|
340
759
|
}
|
|
341
760
|
}
|
|
342
761
|
|
|
343
|
-
|
|
344
|
-
delete global.
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
762
|
+
delete global.Before
|
|
763
|
+
delete global.After
|
|
764
|
+
delete global.Fail
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function loadGherkinSteps(paths) {
|
|
768
|
+
global.Before = fn => event.dispatcher.on(event.test.started, fn)
|
|
769
|
+
global.After = fn => event.dispatcher.on(event.test.finished, fn)
|
|
770
|
+
global.Fail = fn => event.dispatcher.on(event.test.failed, fn)
|
|
771
|
+
|
|
772
|
+
// Gherkin step loading must be handled asynchronously
|
|
773
|
+
throw new Error('Gherkin step loading must be converted to async. Use loadGherkinStepsAsync() instead.')
|
|
774
|
+
|
|
775
|
+
delete global.Before
|
|
776
|
+
delete global.After
|
|
777
|
+
delete global.Fail
|
|
348
778
|
}
|
|
349
779
|
|
|
350
|
-
function loadSupportObject(modulePath, supportObjectName) {
|
|
351
|
-
if (modulePath
|
|
352
|
-
|
|
353
|
-
|
|
780
|
+
async function loadSupportObject(modulePath, supportObjectName) {
|
|
781
|
+
if (!modulePath) {
|
|
782
|
+
throw new Error(`Support object "${supportObjectName}" is not defined`)
|
|
783
|
+
}
|
|
784
|
+
// If function/class provided directly
|
|
785
|
+
if (typeof modulePath === 'function') {
|
|
786
|
+
try {
|
|
787
|
+
// class constructor
|
|
788
|
+
if (modulePath.prototype && modulePath.prototype.constructor === modulePath) {
|
|
789
|
+
return new modulePath()
|
|
790
|
+
}
|
|
791
|
+
// plain function factory
|
|
792
|
+
return modulePath()
|
|
793
|
+
} catch (err) {
|
|
794
|
+
throw new Error(`Could not include object ${supportObjectName} from function: ${err.message}`)
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
if (typeof modulePath === 'string' && modulePath.charAt(0) === '.') {
|
|
798
|
+
modulePath = path.join(global.codecept_dir, modulePath)
|
|
354
799
|
}
|
|
355
800
|
try {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
801
|
+
// Use dynamic import for both ESM and CJS modules
|
|
802
|
+
let importPath = modulePath
|
|
803
|
+
let tempJsFile = null
|
|
804
|
+
|
|
805
|
+
if (typeof importPath === 'string') {
|
|
806
|
+
const ext = path.extname(importPath)
|
|
807
|
+
|
|
808
|
+
// Handle TypeScript files
|
|
809
|
+
if (ext === '.ts') {
|
|
810
|
+
try {
|
|
811
|
+
// Use the TypeScript transpilation utility
|
|
812
|
+
const typescript = await import('typescript')
|
|
813
|
+
const { tempFile, allTempFiles } = await transpileTypeScript(importPath, typescript)
|
|
814
|
+
|
|
815
|
+
debug(`Transpiled TypeScript file: ${importPath} -> ${tempFile}`)
|
|
816
|
+
|
|
817
|
+
// Attach cleanup handler
|
|
818
|
+
importPath = tempFile
|
|
819
|
+
// Store temp files list in a way that cleanup can access them
|
|
820
|
+
tempJsFile = allTempFiles
|
|
821
|
+
|
|
822
|
+
} catch (tsError) {
|
|
823
|
+
throw new Error(`Failed to load TypeScript file ${importPath}: ${tsError.message}. Make sure 'typescript' package is installed.`)
|
|
824
|
+
}
|
|
825
|
+
} else if (!ext) {
|
|
826
|
+
// Append .js if no extension provided (ESM resolution requires it)
|
|
827
|
+
importPath = `${importPath}.js`
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
let obj
|
|
832
|
+
try {
|
|
833
|
+
obj = await import(importPath)
|
|
834
|
+
} catch (importError) {
|
|
835
|
+
// Clean up temp files if created before rethrowing
|
|
836
|
+
if (tempJsFile) {
|
|
837
|
+
const filesToClean = Array.isArray(tempJsFile) ? tempJsFile : [tempJsFile]
|
|
838
|
+
cleanupTempFiles(filesToClean)
|
|
839
|
+
}
|
|
840
|
+
throw importError
|
|
841
|
+
} finally {
|
|
842
|
+
// Clean up temp files if created
|
|
843
|
+
if (tempJsFile) {
|
|
844
|
+
const filesToClean = Array.isArray(tempJsFile) ? tempJsFile : [tempJsFile]
|
|
845
|
+
cleanupTempFiles(filesToClean)
|
|
846
|
+
}
|
|
847
|
+
}
|
|
360
848
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
849
|
+
// Handle ESM module wrapper
|
|
850
|
+
let actualObj = obj
|
|
851
|
+
if (obj && obj.__esModule && obj.default) {
|
|
852
|
+
actualObj = obj.default
|
|
853
|
+
} else if (obj.default) {
|
|
854
|
+
actualObj = obj.default
|
|
855
|
+
}
|
|
367
856
|
|
|
368
|
-
|
|
857
|
+
// Handle different types of imports
|
|
858
|
+
if (typeof actualObj === 'function') {
|
|
859
|
+
// If it's a class (constructor function)
|
|
860
|
+
if (actualObj.prototype && actualObj.prototype.constructor === actualObj) {
|
|
861
|
+
const ClassName = actualObj
|
|
862
|
+
return new ClassName()
|
|
369
863
|
}
|
|
864
|
+
// If it's a regular function
|
|
865
|
+
return actualObj()
|
|
370
866
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
) {
|
|
375
|
-
const methods = getObjectMethods(obj);
|
|
376
|
-
Object.keys(methods)
|
|
377
|
-
.filter(key => !key.startsWith('_'))
|
|
378
|
-
.forEach(key => {
|
|
379
|
-
const currentMethod = methods[key];
|
|
380
|
-
if (isFunction(currentMethod) || isAsyncFunction(currentMethod)) {
|
|
381
|
-
const ms = new MetaStep(supportObjectName, key);
|
|
382
|
-
ms.setContext(methods);
|
|
383
|
-
methods[key] = ms.run.bind(ms, currentMethod);
|
|
384
|
-
}
|
|
385
|
-
});
|
|
386
|
-
return methods;
|
|
867
|
+
|
|
868
|
+
if (actualObj && Array.isArray(actualObj)) {
|
|
869
|
+
return actualObj
|
|
387
870
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
obj[key] = ms.run.bind(ms, currentMethod);
|
|
397
|
-
}
|
|
398
|
-
});
|
|
871
|
+
|
|
872
|
+
// If it's a plain object
|
|
873
|
+
if (actualObj && typeof actualObj === 'object') {
|
|
874
|
+
// Call _init if it exists (for page objects)
|
|
875
|
+
if (actualObj._init && typeof actualObj._init === 'function') {
|
|
876
|
+
actualObj._init()
|
|
877
|
+
}
|
|
878
|
+
return actualObj
|
|
399
879
|
}
|
|
400
880
|
|
|
401
|
-
|
|
881
|
+
throw new Error(`Support object "${supportObjectName}" should be an object, class, or function, but got ${typeof actualObj}`)
|
|
402
882
|
} catch (err) {
|
|
403
|
-
throw new Error(`Could not include object ${supportObjectName} from module '${modulePath}'\n${err.message}\n${err.stack}`)
|
|
883
|
+
throw new Error(`Could not include object ${supportObjectName} from module '${modulePath}'\n${err.message}\n${err.stack}`)
|
|
404
884
|
}
|
|
405
885
|
}
|
|
406
886
|
|
|
887
|
+
// Backwards compatibility function that throws an error for sync usage
|
|
888
|
+
function loadSupportObjectSync(modulePath, supportObjectName) {
|
|
889
|
+
throw new Error(`loadSupportObjectSync is deprecated. Support object "${supportObjectName || 'undefined'}" from '${modulePath}' must be loaded asynchronously. Use loadSupportObject() instead.`)
|
|
890
|
+
}
|
|
891
|
+
|
|
407
892
|
/**
|
|
408
893
|
* Method collect own property and prototype
|
|
409
894
|
*/
|
|
410
|
-
function getObjectMethods(obj) {
|
|
411
|
-
const methodsSet = new Set();
|
|
412
|
-
let protoObj = Reflect.getPrototypeOf(obj);
|
|
413
|
-
do {
|
|
414
|
-
if (protoObj?.constructor.prototype !== Object.prototype) {
|
|
415
|
-
const keys = Reflect.ownKeys(protoObj);
|
|
416
|
-
keys.forEach(k => methodsSet.add(k));
|
|
417
|
-
}
|
|
418
|
-
} while (protoObj = Reflect.getPrototypeOf(protoObj));
|
|
419
|
-
Reflect.ownKeys(obj).forEach(k => methodsSet.add(k));
|
|
420
|
-
const methods = {};
|
|
421
|
-
for (const key of methodsSet.keys()) {
|
|
422
|
-
if (key !== 'constructor') methods[key] = obj[key];
|
|
423
|
-
}
|
|
424
|
-
return methods;
|
|
425
|
-
}
|
|
426
895
|
|
|
427
|
-
function loadTranslation(locale, vocabularies) {
|
|
896
|
+
async function loadTranslation(locale, vocabularies) {
|
|
428
897
|
if (!locale) {
|
|
429
|
-
return Translation.createEmpty()
|
|
898
|
+
return Translation.createEmpty()
|
|
430
899
|
}
|
|
431
900
|
|
|
432
|
-
let translation
|
|
433
|
-
locale = locale.replace('-', '_');
|
|
901
|
+
let translation
|
|
434
902
|
|
|
435
903
|
// check if it is a known translation
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
//
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
904
|
+
const langs = await Translation.getLangs()
|
|
905
|
+
if (langs[locale]) {
|
|
906
|
+
translation = new Translation(langs[locale])
|
|
907
|
+
} else if (fileExists(path.join(global.codecept_dir, locale))) {
|
|
908
|
+
// get from a provided file instead
|
|
909
|
+
translation = Translation.createDefault()
|
|
910
|
+
translation.loadVocabulary(locale)
|
|
911
|
+
} else {
|
|
912
|
+
translation = Translation.createDefault()
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
vocabularies.forEach(v => translation.loadVocabulary(v))
|
|
916
|
+
|
|
917
|
+
return translation
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
function getHelperModuleName(helperName, config) {
|
|
921
|
+
// classical require
|
|
922
|
+
if (config[helperName].require) {
|
|
923
|
+
if (config[helperName].require.startsWith('.')) {
|
|
924
|
+
let helperPath = path.resolve(global.codecept_dir, config[helperName].require)
|
|
925
|
+
// Add .js extension if not present for ESM compatibility
|
|
926
|
+
if (!path.extname(helperPath)) {
|
|
927
|
+
helperPath += '.js'
|
|
928
|
+
}
|
|
929
|
+
return helperPath // custom helper
|
|
444
930
|
}
|
|
931
|
+
return config[helperName].require // plugin helper
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// built-in helpers
|
|
935
|
+
if (helperName.startsWith('@codeceptjs/')) {
|
|
936
|
+
return helperName
|
|
445
937
|
}
|
|
446
938
|
|
|
447
|
-
|
|
939
|
+
// built-in helpers
|
|
940
|
+
return `./helper/${helperName}`
|
|
941
|
+
}
|
|
942
|
+
function normalizeAndJoin(basePath, subPath) {
|
|
943
|
+
// Normalize and convert slashes to forward slashes in one step
|
|
944
|
+
const normalizedBase = path.posix.normalize(basePath.replace(/\\/g, '/'))
|
|
945
|
+
const normalizedSub = path.posix.normalize(subPath.replace(/\\/g, '/'))
|
|
946
|
+
|
|
947
|
+
// If subPath is absolute (starts with "/"), return it as the final path
|
|
948
|
+
if (normalizedSub.startsWith('/')) {
|
|
949
|
+
return normalizedSub
|
|
950
|
+
}
|
|
448
951
|
|
|
449
|
-
|
|
952
|
+
// Join the paths using POSIX-style
|
|
953
|
+
return path.posix.join(normalizedBase, normalizedSub)
|
|
450
954
|
}
|