codeceptjs
Version:
Supercharged End 2 End Testing Framework for NodeJS
1,033 lines (915 loc) • 33.5 kB
JavaScript
import { globSync } from 'glob'
import path from 'path'
import fs from 'fs'
import { isMainThread } from 'worker_threads'
import debugModule from 'debug'
const debug = debugModule('codeceptjs:container')
import { MetaStep } from './step.js'
import {
methodsOfObject,
fileExists,
isFunction,
isAsyncFunction,
installedLocally,
deepMerge,
resolveImportModulePath,
} from './utils.js'
import { transpileTypeScript, cleanupTempFiles, fixErrorStack } from './utils/typescript.js'
import Translation from './translation.js'
import MochaFactory from './mocha/factory.js'
import recorder from './recorder.js'
import event from './event.js'
import WorkerStorage from './workerStorage.js'
import store from './store.js'
import Result from './result.js'
import ai from './ai.js'
import actorFactory from './actor.js'
import Config from './config.js'
let asyncHelperPromise
let beforeCalledSet = new Set()
export function getBeforeCalledSet() { return beforeCalledSet }
export function resetBeforeCalledSet() { beforeCalledSet = new Set() }
let container = {
helpers: {},
support: {},
proxySupport: {},
proxySupportConfig: {}, // Track config used to create proxySupport
plugins: {},
actor: null,
/**
* @type {Mocha | {}}
* @ignore
*/
mocha: {},
translation: {},
/** @type {Result | null} */
result: null,
sharedKeys: new Set(), // Track keys shared via share() function
tsFileMapping: null, // TypeScript file mapping for error stack fixing
}
/**
* Dependency Injection Container
*/
class Container {
/**
* Get the standard acting helpers of CodeceptJS Container
*
*/
static get STANDARD_ACTING_HELPERS() {
return ['Playwright', 'WebDriver', 'Puppeteer', 'Appium']
}
/**
* Create container with all required helpers and support objects
*
* @api
* @param {*} config
* @param {*} opts
*/
static async create(config, opts) {
debug('creating container')
asyncHelperPromise = Promise.resolve()
// dynamically create mocha instance
const mochaConfig = config.mocha || {}
if (config.grep && !opts.grep) mochaConfig.grep = config.grep
this.createMocha = () => (container.mocha = MochaFactory.create(mochaConfig, opts || {}))
this.createMocha()
// create support objects
container.support = {}
container.helpers = await createHelpers(config.helpers || {})
container.translation = await loadTranslation(config.translation || null, config.vocabularies || [])
container.proxySupportConfig = config.include || {}
container.proxySupport = createSupportObjects(container.proxySupportConfig)
container.plugins = await createPlugins(config.plugins || {}, opts)
container.result = new Result()
// Preload includes (so proxies can expose real objects synchronously)
const includes = config.include || {}
// Check if custom I is provided
if (Object.prototype.hasOwnProperty.call(includes, 'I')) {
try {
const mod = includes.I
if (typeof mod === 'string') {
container.support.I = await loadSupportObject(mod, 'I')
} else if (typeof mod === 'function') {
container.support.I = await loadSupportObject(mod, 'I')
} else if (mod && typeof mod === 'object') {
container.support.I = mod
}
} catch (e) {
throw e
}
} else {
// Create default actor - this sets up the callback in asyncHelperPromise
createActor()
}
// Load remaining includes except I
for (const [name, mod] of Object.entries(includes)) {
if (name === 'I') continue
try {
if (typeof mod === 'string') {
container.support[name] = await loadSupportObject(mod, name)
} else if (typeof mod === 'function') {
// function or class
container.support[name] = await loadSupportObject(mod, name)
} else if (mod && typeof mod === 'object') {
container.support[name] = mod
}
} catch (e) {
throw new Error(`Could not include object ${name}: ${e.message}`)
}
}
// Wait for all async helpers to finish loading and populate the actor
await asyncHelperPromise
// Plugins may have registered Config hooks during their boot. Run anything
// that hasn't been applied yet and re-feed the mutated helper config to the
// already-instantiated helpers.
if (Config.runPendingHooks(config)) {
for (const name of Object.keys(container.helpers)) {
const helper = container.helpers[name]
if (helper && typeof helper._setConfig === 'function' && config.helpers && config.helpers[name]) {
helper._setConfig(config.helpers[name])
}
}
}
if (opts && opts.ai) ai.enable(config.ai) // enable AI Assistant
if (config.gherkin) await loadGherkinStepsAsync(config.gherkin.steps || [])
if (opts && typeof opts.timeouts === 'boolean') store.timeouts = opts.timeouts
}
static actor() {
return container.support.I
}
/**
* Get all plugins
*
* @api
* @param {string} [name]
* @returns { * }
*/
static plugins(name) {
if (!name) {
return container.plugins
}
return container.plugins[name]
}
/**
* Get all support objects or get support object by name
*
* @api
* @param {string} [name]
* @returns { * }
*/
static support(name) {
if (!name) {
return container.proxySupport
}
if (typeof container.support[name] === 'function') {
return container.support[name]
}
return container.proxySupport[name]
}
/**
* Get raw (non-proxied) support objects for direct access.
* Used by listeners to call lifecycle hooks without MetaStep wrapping.
*
* @api
* @returns {object}
*/
static supportObjects() {
return container.support
}
/**
* Get all helpers or get a helper by name
*
* @api
* @param {string} [name]
* @returns { * }
*/
static helpers(name) {
if (!name) {
return container.helpers
}
return container.helpers[name]
}
/**
* Get translation
*
* @api
*/
static translation() {
return container.translation
}
/**
* Get TypeScript file mapping for error stack fixing
*
* @api
*/
static tsFileMapping() {
return store.tsFileMapping
}
/**
* Get Mocha instance
*
* @api
* @returns { * }
*/
static mocha() {
return container.mocha
}
/**
* Get result
*
* @returns {Result}
*/
static result() {
if (!container.result) {
container.result = new Result()
}
return container.result
}
/**
* Append new services to container
*
* @api
* @param {Object<string, *>} newContainer
*/
static append(newContainer) {
container = deepMerge(container, newContainer)
// If new support objects are added, update the proxy support
if (newContainer.support) {
// Merge the new support config with existing config
container.proxySupportConfig = { ...container.proxySupportConfig, ...newContainer.support }
// Recreate the proxy with merged config
container.proxySupport = createSupportObjects(container.proxySupportConfig)
}
debug('appended', JSON.stringify(newContainer).slice(0, 300))
}
/**
* Clear container
*
* @param {Object<string, *>} newHelpers
* @param {Object<string, *>} newSupport
* @param {Object<string, *>} newPlugins
*/
static async clear(newHelpers = {}, newSupport = {}, newPlugins = {}) {
container.helpers = newHelpers
container.translation = await loadTranslation()
container.proxySupportConfig = newSupport
container.proxySupport = createSupportObjects(newSupport)
container.plugins = newPlugins
container.sharedKeys = new Set() // Clear shared keys
asyncHelperPromise = Promise.resolve()
store.actor = null
debug('container cleared')
}
/**
* @param {Function|null} fn
* @returns {Promise<void>}
*/
static async started(fn = null) {
if (fn) {
asyncHelperPromise = asyncHelperPromise.then(fn)
}
return asyncHelperPromise
}
/**
* Share data across worker threads
*
* @param {Object} data
* @param {Object} options - set {local: true} to not share among workers
*/
static share(data, options = {}) {
// Instead of using append which replaces the entire container,
// directly update the support object to maintain proxy references
Object.assign(container.support, data)
// Track which keys were explicitly shared
Object.keys(data).forEach(key => container.sharedKeys.add(key))
if (!options.local) {
WorkerStorage.share(data)
}
}
static createMocha(config = {}, opts = {}) {
const mochaConfig = config?.mocha || {}
if (config?.grep && !opts?.grep) {
mochaConfig.grep = config.grep
}
container.mocha = MochaFactory.create(mochaConfig, opts || {})
}
}
export default Container
async function createHelpers(config) {
const helpers = {}
for (let helperName in config) {
try {
let HelperClass
// Check if helper class was stored in config during ESM import processing
if (config[helperName]._helperClass) {
HelperClass = config[helperName]._helperClass
debug(`helper ${helperName} loaded from ESM import`)
}
// ESM import (legacy check)
if (!HelperClass && typeof helperName === 'function' && helperName.prototype) {
HelperClass = helperName
helperName = HelperClass.constructor.name
}
// classical require - may be async for ESM modules
if (!HelperClass) {
const helperResult = requireHelperFromModule(helperName, config)
if (helperResult instanceof Promise) {
// Handle async ESM loading - create placeholder
helpers[helperName] = {}
asyncHelperPromise = asyncHelperPromise
.then(() => helperResult)
.then(async ResolvedHelperClass => {
debug(`helper ${helperName} resolved type: ${typeof ResolvedHelperClass}`, ResolvedHelperClass)
// Extract default export from ESM module wrapper if needed
if (ResolvedHelperClass && ResolvedHelperClass.__esModule && ResolvedHelperClass.default) {
ResolvedHelperClass = ResolvedHelperClass.default
debug(`extracted default export for ${helperName}, new type: ${typeof ResolvedHelperClass}`)
}
if (typeof ResolvedHelperClass !== 'function') {
throw new Error(`Helper '${helperName}' is not a class. Got: ${typeof ResolvedHelperClass}`)
}
checkHelperRequirements(ResolvedHelperClass)
helpers[helperName] = new ResolvedHelperClass(config[helperName])
debug(`helper ${helperName} async loaded`)
})
continue
} else {
HelperClass = helperResult
}
}
// handle async CJS modules that use dynamic import
if (isAsyncFunction(HelperClass)) {
helpers[helperName] = {}
asyncHelperPromise = asyncHelperPromise
.then(() => HelperClass())
.then(ResolvedHelperClass => {
// Check if ResolvedHelperClass is a constructor function
if (typeof ResolvedHelperClass?.constructor !== 'function') {
throw new Error(`Helper class from module '${helperName}' is not a class. Use CJS async module syntax.`)
}
helpers[helperName] = new ResolvedHelperClass(config[helperName])
debug(`helper ${helperName} async CJS loaded`)
})
continue
}
checkHelperRequirements(HelperClass)
helpers[helperName] = new HelperClass(config[helperName])
debug(`helper ${helperName} initialized`)
} catch (err) {
throw new Error(`Could not load helper ${helperName} (${err.message})`)
}
}
// Don't await here - let Container.create() handle the await
// This allows actor callbacks to be registered before resolution
asyncHelperPromise = asyncHelperPromise.then(async () => {
// Call _init on all helpers after they're all loaded
for (const name in helpers) {
if (helpers[name]._init) {
await helpers[name]._init()
debug(`helper ${name} _init() called`)
}
}
})
return helpers
}
function checkHelperRequirements(HelperClass) {
if (HelperClass._checkRequirements) {
const requirements = HelperClass._checkRequirements()
if (requirements) {
let install
if (installedLocally()) {
install = `npm install --save-dev ${requirements.join(' ')}`
} else {
console.log('WARNING: CodeceptJS is not installed locally. It is recommended to switch to local installation')
install = `[sudo] npm install -g ${requirements.join(' ')}`
}
throw new Error(`Required modules are not installed.\n\nRUN: ${install}`)
}
}
}
async function requireHelperFromModule(helperName, config, HelperClass) {
const moduleName = getHelperModuleName(helperName, config)
if (moduleName.startsWith('./helper/')) {
try {
// For built-in helpers, use direct relative import with .js extension
const helperPath = `${moduleName}.js`
const resolvedPath = resolveImportModulePath(helperPath)
const mod = await import(resolvedPath)
HelperClass = mod.default || mod
} catch (err) {
throw err
}
} else {
// Handle TypeScript files
let importPath = moduleName
let tempJsFile = null
let fileMapping = null
const ext = path.extname(moduleName)
if (ext === '.ts') {
try {
// Use the TypeScript transpilation utility
const typescript = ((await import('typescript')).default || (await import('typescript')))
const { tempFile, allTempFiles, fileMapping: mapping } = await transpileTypeScript(importPath, typescript)
debug(`Transpiled TypeScript helper: ${importPath} -> ${tempFile}`)
importPath = tempFile
tempJsFile = allTempFiles
fileMapping = mapping
// Store file mapping in container for runtime error fixing (merge with existing)
if (!store.tsFileMapping) {
store.tsFileMapping = new Map()
}
for (const [key, value] of mapping.entries()) {
store.tsFileMapping.set(key, value)
}
} catch (tsError) {
throw new Error(`Failed to load TypeScript helper ${importPath}: ${tsError.message}. Make sure 'typescript' package is installed.`)
}
}
// check if the new syntax export default HelperName is used and loads the Helper, otherwise loads the module that used old syntax export = HelperName.
try {
// Try dynamic import for both CommonJS and ESM modules
const resolvedPath = resolveImportModulePath(importPath)
const mod = await import(resolvedPath)
if (!mod && !mod.default) {
throw new Error(`Helper module '${moduleName}' was not found. Make sure you have installed the package correctly.`)
}
HelperClass = mod.default || mod
// Clean up temp files if created
if (tempJsFile) {
const filesToClean = Array.isArray(tempJsFile) ? tempJsFile : [tempJsFile]
cleanupTempFiles(filesToClean)
}
} catch (err) {
// Fix error stack to point to original .ts files
if (fileMapping) {
fixErrorStack(err, fileMapping)
}
// Clean up temp files before rethrowing
if (tempJsFile) {
const filesToClean = Array.isArray(tempJsFile) ? tempJsFile : [tempJsFile]
cleanupTempFiles(filesToClean)
}
if (err.code === 'ERR_REQUIRE_ESM' || (err.message && err.message.includes('ES module'))) {
// This is an ESM module, use dynamic import
try {
const pathModule = await import('path')
const absolutePath = pathModule.default.resolve(importPath)
const mod = await import(absolutePath)
HelperClass = mod.default || mod
debug(`helper ${helperName} loaded via ESM import`)
} catch (importErr) {
throw new Error(`Helper module '${moduleName}' could not be imported as ESM: ${importErr.message}`)
}
} else if (err.code === 'MODULE_NOT_FOUND') {
throw new Error(`Helper module '${moduleName}' was not found. Make sure you have installed the package correctly.`)
} else {
throw err
}
}
}
return HelperClass
}
function createSupportObjects(config) {
const asyncWrapper = function (f) {
return function () {
return f.apply(this, arguments).catch(e => {
recorder.saveFirstAsyncError(e)
throw e
})
}
}
function lazyLoad(name) {
return new Proxy(
{},
{
get(target, prop) {
// behavr like array or
if (prop === 'length') return Object.keys(config).length
if (prop === Symbol.iterator) {
return function* () {
for (let i = 0; i < Object.keys(config).length; i++) {
yield target[i]
}
}
}
// load actual name from vocabulary
if (container.translation && container.translation.I && name === 'I') {
// Use translated name for I
const actualName = container.translation.I
if (actualName !== 'I') {
name = actualName
}
}
if (name === 'I') {
if (!container.support.I) {
// Actor will be created during container.create()
return undefined
}
methodsOfObject(container.support.I)
return container.support.I[prop]
}
if (!container.support[name] && typeof config[name] === 'object') {
container.support[name] = config[name]
}
if (!container.support[name]) {
// Cannot load object synchronously in proxy getter
// Return undefined and log warning - object should be pre-loaded during container creation
debug(`Support object ${name} not pre-loaded, returning undefined`)
return undefined
}
const currentObject = container.support[name]
let currentValue = currentObject[prop]
if (isFunction(currentValue) || isAsyncFunction(currentValue)) {
if (prop.toString().charAt(0) !== '_' && currentObject._before && !beforeCalledSet.has(name)) {
beforeCalledSet.add(name)
const originalValue = currentValue
const wrappedValue = async function (...args) {
await currentObject._before()
return originalValue.apply(currentObject, args)
}
const ms = new MetaStep(name, prop)
ms.setContext(currentObject)
debug(`metastep is created for ${name}.${prop.toString()}() (with _before)`)
return ms.run.bind(ms, asyncWrapper(wrappedValue))
}
const ms = new MetaStep(name, prop)
ms.setContext(currentObject)
if (isAsyncFunction(currentValue)) currentValue = asyncWrapper(currentValue)
debug(`metastep is created for ${name}.${prop.toString()}()`)
return ms.run.bind(ms, currentValue)
}
return currentValue
},
has(target, prop) {
if (!container.support[name]) {
// Note: This is sync, so we can't use async loadSupportObject here
// The object will be loaded lazily on first property access
return false
}
return prop in container.support[name]
},
getOwnPropertyDescriptor(target, prop) {
if (!container.support[name]) {
// Object will be loaded on property access
return {
enumerable: true,
configurable: true,
value: undefined,
}
}
return {
enumerable: true,
configurable: true,
value: container.support[name][prop],
}
},
ownKeys() {
if (!container.support[name]) {
return []
}
return Reflect.ownKeys(container.support[name])
},
},
)
}
const keys = Reflect.ownKeys(config)
return new Proxy(
{},
{
has(target, key) {
return keys.includes(key) || container.sharedKeys.has(key)
},
ownKeys() {
// Return both original config keys and explicitly shared keys
return [...new Set([...keys, ...container.sharedKeys])]
},
getOwnPropertyDescriptor(target, prop) {
// For destructuring to work, we need to return the actual value from the getter
let value
if (container.sharedKeys.has(prop) && prop in container.support) {
value = container.support[prop]
} else if (prop in container.support && typeof container.support[prop] === 'function') {
value = container.support[prop]
} else {
value = lazyLoad(prop)
}
return {
enumerable: true,
configurable: true,
value: value,
}
},
get(target, key) {
// First check if this is an explicitly shared property
if (container.sharedKeys.has(key) && key in container.support) {
return container.support[key]
}
if (key in container.support && typeof container.support[key] === 'function') {
return container.support[key]
}
return lazyLoad(key)
},
},
)
}
function createActor(actorPath) {
if (container.support.I) return container.support.I
// Default actor
container.support.I = actorFactory({}, Container)
return container.support.I
}
async function loadPluginAsync(modulePath, config) {
let pluginMod
try {
// Try dynamic import first (works for both ESM and CJS)
const resolvedPath = resolveImportModulePath(modulePath)
pluginMod = await import(resolvedPath)
} catch (err) {
throw new Error(`Could not load plugin from '${modulePath}': ${err.message}`)
}
const pluginFactory = pluginMod.default || pluginMod
if (typeof pluginFactory !== 'function') {
throw new Error(`Plugin '${modulePath}' is not a function. Expected a plugin factory function.`)
}
return pluginFactory(config)
}
async function loadPluginFallback(modulePath, config) {
// This function is kept for backwards compatibility but now uses dynamic import
return await loadPluginAsync(modulePath, config)
}
async function createPlugins(config, options = {}) {
const plugins = {}
const pluginOptionMap = new Map()
for (const token of (options.plugins || '').split(',').filter(Boolean)) {
const parts = token.split(':')
pluginOptionMap.set(parts[0], parts.slice(1))
}
for (const [name] of pluginOptionMap) {
if (!config[name]) config[name] = {}
}
for (const pluginName in config) {
if (!config[pluginName]) config[pluginName] = {}
const pluginConfig = config[pluginName]
const enabledByCli = pluginOptionMap.has(pluginName)
if (!pluginConfig.enabled && !enabledByCli) {
continue // plugin is disabled
}
if (enabledByCli && pluginOptionMap.get(pluginName).length > 0) {
pluginConfig._args = pluginOptionMap.get(pluginName)
}
// Generic workers gate:
// - runInWorker / runInWorkers controls plugin execution inside worker threads.
// - runInParent / runInMain can disable plugin in workers parent process.
const runInWorker = pluginConfig.runInWorker ?? pluginConfig.runInWorkers ?? (pluginName === 'testomatio' ? false : true)
const runInParent = pluginConfig.runInParent ?? pluginConfig.runInMain ?? true
if (!isMainThread && !runInWorker) {
continue
}
if (isMainThread && store.workerMode && !runInParent) {
continue
}
let module
try {
if (pluginConfig.require) {
module = pluginConfig.require
if (module.startsWith('.')) {
// local
module = path.resolve(store.codeceptDir, module) // custom plugin
}
} else {
module = `./plugin/${pluginName}.js`
}
// Use async loading for all plugins (ESM and CJS)
plugins[pluginName] = await loadPluginAsync(module, pluginConfig)
debug(`plugin ${pluginName} loaded via async import`)
} catch (err) {
throw new Error(`Could not load plugin ${pluginName} from module '${module}':\n${err.message}\n${err.stack}`)
}
}
return plugins
}
async function loadGherkinStepsAsync(paths) {
// Import BDD module to access step file tracking functions and step DSL
const bddModule = await import('./mocha/bdd.js')
global.Before = fn => event.dispatcher.on(event.test.started, fn)
global.After = fn => event.dispatcher.on(event.test.finished, fn)
global.Fail = fn => event.dispatcher.on(event.test.failed, fn)
// Scope-inject Given/When/Then/And while loading step files so they work
// with noGlobals: true. When noGlobals: false, globals.js has already set
// them as permanent globals — skip to avoid deleting them at the end.
const injectStepDsl = !!store.noGlobals
if (injectStepDsl) {
global.Given = bddModule.Given
global.When = bddModule.When
global.Then = bddModule.Then
global.And = bddModule.And
global.DefineParameterType = bddModule.defineParameterType
}
// If gherkin.steps is string, then this will iterate through that folder and send all step def js files to loadSupportObject
// If gherkin.steps is Array, it will go the old way
// This is done so that we need not enter all Step Definition files under config.gherkin.steps
if (Array.isArray(paths)) {
for (const path of paths) {
// Set context for step definition file location tracking
bddModule.setCurrentStepFile(path)
await loadSupportObject(path, `Step Definition from ${path}`)
bddModule.clearCurrentStepFile()
}
} else {
const folderPath = paths.startsWith('.') ? normalizeAndJoin(store.codeceptDir, paths) : ''
if (folderPath !== '') {
const files = globSync(folderPath)
for (const file of files) {
// Set context for step definition file location tracking
bddModule.setCurrentStepFile(file)
await loadSupportObject(file, `Step Definition from ${file}`)
bddModule.clearCurrentStepFile()
}
}
}
delete global.Before
delete global.After
delete global.Fail
if (injectStepDsl) {
delete global.Given
delete global.When
delete global.Then
delete global.And
delete global.DefineParameterType
}
}
function loadGherkinSteps(paths) {
global.Before = fn => event.dispatcher.on(event.test.started, fn)
global.After = fn => event.dispatcher.on(event.test.finished, fn)
global.Fail = fn => event.dispatcher.on(event.test.failed, fn)
// Gherkin step loading must be handled asynchronously
throw new Error('Gherkin step loading must be converted to async. Use loadGherkinStepsAsync() instead.')
delete global.Before
delete global.After
delete global.Fail
}
async function loadSupportObject(modulePath, supportObjectName) {
if (!modulePath) {
throw new Error(`Support object "${supportObjectName}" is not defined`)
}
// If function/class provided directly
if (typeof modulePath === 'function') {
try {
// class constructor
if (modulePath.prototype && modulePath.prototype.constructor === modulePath) {
return new modulePath()
}
// plain function factory
return modulePath()
} catch (err) {
throw new Error(`Could not include object ${supportObjectName} from function: ${err.message}`)
}
}
if (typeof modulePath === 'string' && modulePath.charAt(0) === '.') {
modulePath = path.join(store.codeceptDir, modulePath)
}
try {
// Use dynamic import for both ESM and CJS modules
let importPath = modulePath
let tempJsFile = null
let fileMapping = null
if (typeof importPath === 'string') {
const ext = path.extname(importPath)
// Handle TypeScript files
if (ext === '.ts') {
try {
// Use the TypeScript transpilation utility
const typescript = ((await import('typescript')).default || (await import('typescript')))
const { tempFile, allTempFiles, fileMapping: mapping } = await transpileTypeScript(importPath, typescript)
debug(`Transpiled TypeScript file: ${importPath} -> ${tempFile}`)
// Attach cleanup handler
importPath = tempFile
// Store temp files list in a way that cleanup can access them
tempJsFile = allTempFiles
fileMapping = mapping
// Store file mapping in container for runtime error fixing (merge with existing)
if (!container.tsFileMapping) {
container.tsFileMapping = new Map()
}
for (const [key, value] of mapping.entries()) {
container.tsFileMapping.set(key, value)
}
} catch (tsError) {
throw new Error(`Failed to load TypeScript file ${importPath}: ${tsError.message}. Make sure 'typescript' package is installed.`)
}
} else if (!ext) {
// Append .js if no extension provided (ESM resolution requires it)
importPath = `${importPath}.js`
}
}
let obj
try {
const resolvedPath = resolveImportModulePath(importPath)
obj = await import(resolvedPath)
} catch (importError) {
if (fileMapping) {
fixErrorStack(importError, fileMapping)
}
if (tempJsFile) {
const filesToClean = Array.isArray(tempJsFile) ? tempJsFile : [tempJsFile]
cleanupTempFiles(filesToClean)
}
throw importError
} finally {
if (tempJsFile) {
const filesToClean = Array.isArray(tempJsFile) ? tempJsFile : [tempJsFile]
cleanupTempFiles(filesToClean)
}
}
// Handle ESM module wrapper
let actualObj = obj
if (obj && obj.__esModule && obj.default) {
actualObj = obj.default
} else if (obj.default) {
actualObj = obj.default
}
// Handle different types of imports
if (typeof actualObj === 'function') {
// If it's a class (constructor function)
if (actualObj.prototype && actualObj.prototype.constructor === actualObj) {
const ClassName = actualObj
return new ClassName()
}
// If it's a regular function
return actualObj()
}
if (actualObj && Array.isArray(actualObj)) {
return actualObj
}
// If it's a plain object
if (actualObj && typeof actualObj === 'object') {
// Call _init if it exists (for page objects)
if (actualObj._init && typeof actualObj._init === 'function') {
actualObj._init()
}
return actualObj
}
throw new Error(`Support object "${supportObjectName}" should be an object, class, or function, but got ${typeof actualObj}`)
} catch (err) {
const newErr = new Error(`Could not include object ${supportObjectName} from module '${modulePath}': ${err.message}`)
newErr.stack = err.stack
throw newErr
}
}
// Backwards compatibility function that throws an error for sync usage
function loadSupportObjectSync(modulePath, supportObjectName) {
throw new Error(`loadSupportObjectSync is deprecated. Support object "${supportObjectName || 'undefined'}" from '${modulePath}' must be loaded asynchronously. Use loadSupportObject() instead.`)
}
/**
* Method collect own property and prototype
*/
async function loadTranslation(locale, vocabularies) {
if (!locale) {
return Translation.createEmpty()
}
let translation
// check if it is a known translation
const langs = await Translation.getLangs()
if (langs[locale]) {
translation = new Translation(langs[locale])
} else if (fileExists(path.join(store.codeceptDir, locale))) {
// get from a provided file instead
translation = Translation.createDefault()
translation.loadVocabulary(locale)
} else {
translation = Translation.createDefault()
}
vocabularies.forEach(v => translation.loadVocabulary(v))
return translation
}
function getHelperModuleName(helperName, config) {
// classical require
if (config[helperName].require) {
if (config[helperName].require.startsWith('.')) {
let helperPath = path.resolve(store.codeceptDir, config[helperName].require)
// Add .js extension if not present for ESM compatibility
if (!path.extname(helperPath)) {
helperPath += '.js'
}
return helperPath // custom helper
}
return config[helperName].require // plugin helper
}
// built-in helpers
if (helperName.startsWith('@codeceptjs/')) {
return helperName
}
// built-in helpers
return `./helper/${helperName}`
}
function normalizeAndJoin(basePath, subPath) {
// Normalize and convert slashes to forward slashes in one step
const normalizedBase = path.posix.normalize(basePath.replace(/\\/g, '/'))
const normalizedSub = path.posix.normalize(subPath.replace(/\\/g, '/'))
// If subPath is absolute (starts with "/"), return it as the final path
if (normalizedSub.startsWith('/')) {
return normalizedSub
}
// Join the paths using POSIX-style
return path.posix.join(normalizedBase, normalizedSub)
}