diff --git a/lib/actor.js b/lib/actor.js index ab2238c32..73911306c 100644 --- a/lib/actor.js +++ b/lib/actor.js @@ -75,7 +75,8 @@ export default function (obj = {}, container) { if (!container) { container = Container } - + + // Get existing actor or create a new one const actor = container.actor() || new Actor() // load all helpers once container initialized @@ -111,14 +112,17 @@ export default function (obj = {}, container) { } }) - container.append({ - support: { - I: actor, - }, - }) + // Update container.support.I to ensure it has the latest actor reference + if (!container.actor() || container.actor() !== actor) { + container.append({ + support: { + I: actor, + }, + }) + } }) - // store.actor = actor; - // add custom steps from actor + + // add custom steps from actor immediately Object.keys(obj).forEach(key => { const ms = new MetaStep('I', key) ms.setContext(actor) diff --git a/lib/container.js b/lib/container.js index aaa93ee0c..e1f68008a 100644 --- a/lib/container.js +++ b/lib/container.js @@ -22,6 +22,7 @@ let container = { helpers: {}, support: {}, proxySupport: {}, + proxySupportConfig: {}, // Track config used to create proxySupport plugins: {}, actor: null, /** @@ -32,7 +33,7 @@ let container = { translation: {}, /** @type {Result | null} */ result: null, - sharedKeys: new Set() // Track keys shared via share() function + sharedKeys: new Set(), // Track keys shared via share() function } /** @@ -67,14 +68,15 @@ class Container { container.support = {} container.helpers = await createHelpers(config.helpers || {}) container.translation = await loadTranslation(config.translation || null, config.vocabularies || []) - container.proxySupport = createSupportObjects(config.include || {}) + 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 || {} - // Ensure I is available for DI modules at import time + // Check if custom I is provided if (Object.prototype.hasOwnProperty.call(includes, 'I')) { try { const mod = includes.I @@ -89,7 +91,7 @@ class Container { throw new Error(`Could not include object I: ${e.message}`) } } else { - // Create default actor if not provided via includes + // Create default actor - this sets up the callback in asyncHelperPromise createActor() } @@ -110,6 +112,9 @@ class Container { } } + // Wait for all async helpers to finish loading and populate the actor + await asyncHelperPromise + 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 @@ -204,8 +209,10 @@ class Container { // If new support objects are added, update the proxy support if (newContainer.support) { - const newProxySupport = createSupportObjects(newContainer.support) - container.proxySupport = { ...container.proxySupport, ...newProxySupport } + // 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)) @@ -221,6 +228,7 @@ class Container { 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 @@ -250,10 +258,10 @@ class Container { // 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) } @@ -292,7 +300,7 @@ async function createHelpers(config) { if (!HelperClass) { const helperResult = requireHelperFromModule(helperName, config) if (helperResult instanceof Promise) { - // Handle async ESM loading + // Handle async ESM loading - create placeholder helpers[helperName] = {} asyncHelperPromise = asyncHelperPromise .then(() => helperResult) @@ -311,8 +319,7 @@ async function createHelpers(config) { checkHelperRequirements(ResolvedHelperClass) helpers[helperName] = new ResolvedHelperClass(config[helperName]) - if (helpers[helperName]._init) await helpers[helperName]._init() - debug(`helper ${helperName} async initialized`) + debug(`helper ${helperName} async loaded`) }) continue } else { @@ -332,9 +339,8 @@ async function createHelpers(config) { throw new Error(`Helper class from module '${helperName}' is not a class. Use CJS async module syntax.`) } - debug(`helper ${helperName} async initialized`) - helpers[helperName] = new ResolvedHelperClass(config[helperName]) + debug(`helper ${helperName} async CJS loaded`) }) continue @@ -349,9 +355,18 @@ async function createHelpers(config) { } } - for (const name in helpers) { - if (helpers[name]._init) await helpers[name]._init() - } + // 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 } @@ -525,10 +540,17 @@ function createSupportObjects(config) { 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 { + value = lazyLoad(prop) + } return { enumerable: true, configurable: true, - value: target[prop], + value: value, } }, get(target, key) { @@ -677,24 +699,23 @@ async function loadSupportObject(modulePath, supportObjectName) { // Use dynamic import for both ESM and CJS modules let importPath = modulePath let tempJsFile = 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') const { tempFile, allTempFiles } = 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 - } catch (tsError) { throw new Error(`Failed to load TypeScript file ${importPath}: ${tsError.message}. Make sure 'typescript' package is installed.`) } @@ -703,7 +724,7 @@ async function loadSupportObject(modulePath, supportObjectName) { importPath = `${importPath}.js` } } - + let obj try { obj = await import(importPath) diff --git a/lib/helper/GraphQL.js b/lib/helper/GraphQL.js index 390aba4d9..5286b85b2 100644 --- a/lib/helper/GraphQL.js +++ b/lib/helper/GraphQL.js @@ -45,6 +45,8 @@ class GraphQL extends Helper { timeout: 10000, defaultHeaders: {}, endpoint: '', + onRequest: null, + onResponse: null, } this.options = Object.assign(this.options, config) this.headers = { ...this.options.defaultHeaders } @@ -87,8 +89,8 @@ class GraphQL extends Helper { request.headers = { ...this.headers, ...request.headers } - if (this.config.onRequest) { - await this.config.onRequest(request) + if (this.options.onRequest) { + await this.options.onRequest(request) } this.debugSection('Request', JSON.stringify(request)) @@ -102,8 +104,8 @@ class GraphQL extends Helper { response = err.response } - if (this.config.onResponse) { - await this.config.onResponse(response) + if (this.options.onResponse) { + await this.options.onResponse(response) } this.debugSection('Response', JSON.stringify(response.data)) diff --git a/lib/helper/JSONResponse.js b/lib/helper/JSONResponse.js index 9a2e34543..297c9d0c3 100644 --- a/lib/helper/JSONResponse.js +++ b/lib/helper/JSONResponse.js @@ -72,8 +72,8 @@ class JSONResponse extends Helper { if (!this.helpers[this.options.requestHelper]) { throw new Error(`Error setting JSONResponse, helper ${this.options.requestHelper} is not enabled in config, helpers: ${Object.keys(this.helpers)}`) } - const origOnResponse = this.helpers[this.options.requestHelper].config.onResponse - this.helpers[this.options.requestHelper].config.onResponse = response => { + const origOnResponse = this.helpers[this.options.requestHelper].options.onResponse + this.helpers[this.options.requestHelper].options.onResponse = response => { this.response = response if (typeof origOnResponse === 'function') origOnResponse(response) } @@ -83,7 +83,6 @@ class JSONResponse extends Helper { this.response = null } - /** * Checks that response code is equal to the provided one * @@ -372,4 +371,4 @@ class JSONResponse extends Helper { } } -export { JSONResponse as default } +export { JSONResponse, JSONResponse as default } diff --git a/lib/helper/Playwright.js b/lib/helper/Playwright.js index c52136392..882e7c455 100644 --- a/lib/helper/Playwright.js +++ b/lib/helper/Playwright.js @@ -355,7 +355,7 @@ class Playwright extends Helper { this.recordingWebSocketMessages = false this.recordedWebSocketMessagesAtLeastOnce = false this.cdpSession = null - + // Filter out invalid customLocatorStrategies (empty arrays, objects without functions) // This can happen in worker threads where config is serialized/deserialized let validCustomLocators = null @@ -367,7 +367,7 @@ class Playwright extends Helper { validCustomLocators = config.customLocatorStrategies } } - + this.customLocatorStrategies = validCustomLocators this._customLocatorsRegistered = false @@ -416,6 +416,7 @@ class Playwright extends Helper { ignoreHTTPSErrors: false, // Adding it here o that context can be set up to ignore the SSL errors, highlightElement: false, storageState: undefined, + onResponse: null, } process.env.testIdAttribute = 'data-testid' @@ -794,10 +795,7 @@ class Playwright extends Helper { await Promise.allSettled(pages.map(p => p.close().catch(() => {}))) } // Use timeout to prevent hanging (10s should be enough for browser cleanup) - await Promise.race([ - this._stopBrowser(), - new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout')), 10000)), - ]) + await Promise.race([this._stopBrowser(), new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout')), 10000))]) } catch (e) { console.warn('Warning during browser restart in _after:', e.message) // Force cleanup even on timeout @@ -840,10 +838,7 @@ class Playwright extends Helper { if (this.isRunning) { try { // Add timeout protection to prevent hanging - await Promise.race([ - this._stopBrowser(), - new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout in afterSuite')), 10000)), - ]) + await Promise.race([this._stopBrowser(), new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout in afterSuite')), 10000))]) } catch (e) { console.warn('Warning during suite cleanup:', e.message) // Track suite cleanup failures @@ -954,10 +949,7 @@ class Playwright extends Helper { if (this.isRunning) { try { // Add timeout protection to prevent hanging - await Promise.race([ - this._stopBrowser(), - new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout in cleanup')), 10000)), - ]) + await Promise.race([this._stopBrowser(), new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout in cleanup')), 10000))]) } catch (e) { console.warn('Warning during final cleanup:', e.message) // Force cleanup on timeout @@ -970,10 +962,7 @@ class Playwright extends Helper { if (this.browser) { try { // Add timeout protection to prevent hanging - await Promise.race([ - this._stopBrowser(), - new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout in forced cleanup')), 10000)), - ]) + await Promise.race([this._stopBrowser(), new Promise((_, reject) => setTimeout(() => reject(new Error('Browser stop timeout in forced cleanup')), 10000))]) } catch (e) { console.warn('Warning during forced cleanup:', e.message) // Force cleanup on timeout @@ -1390,7 +1379,7 @@ class Playwright extends Helper { this.context = null this.frame = null popupStore.clear() - + // Remove all event listeners to prevent hanging if (this.browser) { try { @@ -1399,7 +1388,7 @@ class Playwright extends Helper { // Ignore errors if browser is already closed } } - + if (this.options.recordHar && this.browserContext) { try { await this.browserContext.close() @@ -1408,16 +1397,11 @@ class Playwright extends Helper { } } this.browserContext = null - + if (this.browser) { try { // Add timeout to prevent browser.close() from hanging indefinitely - await Promise.race([ - this.browser.close(), - new Promise((_, reject) => - setTimeout(() => reject(new Error('Browser close timeout')), 5000) - ) - ]) + await Promise.race([this.browser.close(), new Promise((_, reject) => setTimeout(() => reject(new Error('Browser close timeout')), 5000))]) } catch (e) { // Ignore errors if browser is already closed or timeout if (!e.message?.includes('Browser close timeout')) { @@ -1539,7 +1523,7 @@ class Playwright extends Helper { acceptDownloads: true, ...this.options.emulate, } - + try { this.browserContext = await this.browser.newContext(contextOptions) } catch (err) { @@ -3183,14 +3167,14 @@ class Playwright extends Helper { this.debugSection('Response', await response.text()) // hook to allow JSON response handle this - if (this.config.onResponse) { + if (this.options.onResponse) { const axiosResponse = { data: await response.json(), status: response.status(), statusText: response.statusText(), headers: response.headers(), } - this.config.onResponse(axiosResponse) + this.options.onResponse(axiosResponse) } return response @@ -4337,11 +4321,11 @@ function isRoleLocatorObject(locator) { */ async function handleRoleLocator(context, locator) { if (!isRoleLocatorObject(locator)) return null - + const options = {} if (locator.text) options.name = locator.text if (locator.exact !== undefined) options.exact = locator.exact - + return context.getByRole(locator.role, Object.keys(options).length > 0 ? options : undefined).all() } @@ -4350,7 +4334,7 @@ async function findElements(matcher, locator) { const isReactLocator = locator.type === 'react' || (locator.locator && locator.locator.react) || locator.react const isVueLocator = locator.type === 'vue' || (locator.locator && locator.locator.vue) || locator.vue const isPwLocator = locator.type === 'pw' || (locator.locator && locator.locator.pw) || locator.pw - + if (isReactLocator) return findReact(matcher, locator) if (isVueLocator) return findVue(matcher, locator) if (isPwLocator) return findByPlaywrightLocator.call(this, matcher, locator) @@ -4391,7 +4375,7 @@ async function findCustomElements(matcher, locator) { // Always prioritize this.customLocatorStrategies which is set in constructor from config // and persists in every worker thread instance let strategyFunction = null - + if (this.customLocatorStrategies && this.customLocatorStrategies[locator.type]) { strategyFunction = this.customLocatorStrategies[locator.type] } else if (globalCustomLocatorStrategies.has(locator.type)) { @@ -4967,7 +4951,7 @@ async function refreshContextSession() { this.debugSection('Session', 'Skipping storage cleanup - no active page/context') return } - + const currentUrl = await this.grabCurrentUrl() if (currentUrl.startsWith('http')) { diff --git a/lib/helper/REST.js b/lib/helper/REST.js index f398b4802..3c272d958 100644 --- a/lib/helper/REST.js +++ b/lib/helper/REST.js @@ -94,7 +94,9 @@ const config = {} class REST extends Helper { constructor(config) { super(config) - this.options = { + + // Set defaults first + const defaults = { timeout: 10000, defaultHeaders: {}, endpoint: '', @@ -103,15 +105,16 @@ class REST extends Helper { onResponse: null, } + // Merge config with defaults + this._setConfig(config) + this.options = { ...defaults, ...this.options } + if (this.options.maxContentLength) { const maxContentLength = this.options.maxUploadFileSize * 1024 * 1024 this.options.maxContentLength = maxContentLength this.options.maxBodyLength = maxContentLength } - // override defaults with config - this._setConfig(config) - this.headers = { ...this.options.defaultHeaders } // Create an agent with SSL certificate @@ -215,8 +218,9 @@ class REST extends Helper { } } - if (this.config.onRequest) { - await this.config.onRequest(request) + const onRequest = this.options.onRequest || this.config.onRequest + if (onRequest) { + await onRequest(request) } try { @@ -245,8 +249,9 @@ class REST extends Helper { } response = err.response } - if (this.config.onResponse) { - await this.config.onResponse(response) + const onResponse = this.options.onResponse || this.config.onResponse + if (onResponse) { + await onResponse(response) } try { this.options.prettyPrintJson ? this.debugSection('Response', beautify(JSON.stringify(response.data))) : this.debugSection('Response', JSON.stringify(response.data)) @@ -468,7 +473,8 @@ class REST extends Helper { export { REST as default } function curlize(request) { - if (request.data?.constructor.name.toLowerCase() === 'formdata') return 'cURL is not printed as the request body is not a JSON' + // Guard access to nested properties safely in case request.data is undefined + if ((request.data?.constructor?.name || '').toLowerCase() === 'formdata') return 'cURL is not printed as the request body is not a JSON' let curl = `curl --location --request ${request.method ? request.method.toUpperCase() : 'GET'} ${request.baseURL} `.replace("'", '') if (request.headers) { diff --git a/lib/listener/config.js b/lib/listener/config.js index ce057327a..d8f5ab4bd 100644 --- a/lib/listener/config.js +++ b/lib/listener/config.js @@ -12,15 +12,23 @@ export default function () { return } global.__codeceptConfigListenerInitialized = true - - const helpers = global.container.helpers() enableDynamicConfigFor('suite') enableDynamicConfigFor('test') function enableDynamicConfigFor(type) { event.dispatcher.on(event[type].before, (context = {}) => { + // Get helpers dynamically at runtime, not at initialization time + // This ensures we get the actual helper instances, not placeholders + const helpers = global.container.helpers() + function updateHelperConfig(helper, config) { + // Guard against undefined or invalid helpers + if (!helper || !helper.constructor) { + output.debug(`[${ucfirst(type)} Config] Helper not found or not properly initialized`) + return + } + const oldConfig = deepClone(helper.options) try { helper._setConfig(deepMerge(deepClone(oldConfig), config)) @@ -41,7 +49,7 @@ export default function () { for (let name in context.config) { const config = context.config[name] if (name === '0') { - // first helper + // first helper - get dynamically name = Object.keys(helpers)[0] } const helper = helpers[name] diff --git a/lib/workers.js b/lib/workers.js index 4a837eba9..80dca4e1b 100644 --- a/lib/workers.js +++ b/lib/workers.js @@ -5,6 +5,7 @@ import { mkdirp } from 'mkdirp' import { Worker } from 'worker_threads' import { EventEmitter } from 'events' import ms from 'ms' +import merge from 'lodash.merge' const __filename = fileURLToPath(import.meta.url) const __dirname = dirname(__filename) @@ -66,21 +67,21 @@ const createWorker = (workerObject, isPoolMode = false) => { stdout: true, stderr: true, }) - + // Pipe worker stdout/stderr to main process if (worker.stdout) { worker.stdout.setEncoding('utf8') - worker.stdout.on('data', (data) => { + worker.stdout.on('data', data => { process.stdout.write(data) }) } if (worker.stderr) { worker.stderr.setEncoding('utf8') - worker.stderr.on('data', (data) => { + worker.stderr.on('data', data => { process.stderr.write(data) }) } - + worker.on('error', err => { console.error(`[Main] Worker Error:`, err) output.error(`Worker Error: ${err.stack}`) @@ -221,13 +222,13 @@ class WorkerObject { addConfig(config) { const oldConfig = JSON.parse(this.options.override || '{}') - + // Remove customLocatorStrategies from both old and new config before JSON serialization // since functions cannot be serialized and will be lost, causing workers to have empty strategies const configWithoutFunctions = { ...config } - + // Clean both old and new config - const cleanConfig = (cfg) => { + const cleanConfig = cfg => { if (cfg.helpers) { cfg.helpers = { ...cfg.helpers } Object.keys(cfg.helpers).forEach(helperName => { @@ -239,14 +240,12 @@ class WorkerObject { } return cfg } - + const cleanedOldConfig = cleanConfig(oldConfig) const cleanedNewConfig = cleanConfig(configWithoutFunctions) - - const newConfig = { - ...cleanedOldConfig, - ...cleanedNewConfig, - } + + // Deep merge configurations to preserve all helpers from base config + const newConfig = merge({}, cleanedOldConfig, cleanedNewConfig) this.options.override = JSON.stringify(newConfig) } @@ -280,8 +279,8 @@ class Workers extends EventEmitter { this.setMaxListeners(50) this.codeceptPromise = initializeCodecept(config.testConfig, config.options) this.codecept = null - this.config = config // Save config - this.numberOfWorkersRequested = numberOfWorkers // Save requested worker count + this.config = config // Save config + this.numberOfWorkersRequested = numberOfWorkers // Save requested worker count this.options = config.options || {} this.errors = [] this.numberOfWorkers = 0 @@ -304,11 +303,8 @@ class Workers extends EventEmitter { // Initialize workers in these cases: // 1. Positive number requested AND no manual workers pre-spawned // 2. Function-based grouping (indicated by negative number) AND no manual workers pre-spawned - const shouldAutoInit = this.workers.length === 0 && ( - (Number.isInteger(this.numberOfWorkersRequested) && this.numberOfWorkersRequested > 0) || - (this.numberOfWorkersRequested < 0 && isFunction(this.config.by)) - ) - + const shouldAutoInit = this.workers.length === 0 && ((Number.isInteger(this.numberOfWorkersRequested) && this.numberOfWorkersRequested > 0) || (this.numberOfWorkersRequested < 0 && isFunction(this.config.by))) + if (shouldAutoInit) { this._initWorkers(this.numberOfWorkersRequested, this.config) } @@ -319,7 +315,7 @@ class Workers extends EventEmitter { this.splitTestsByGroups(numberOfWorkers, config) // For function-based grouping, use the actual number of test groups created const actualNumberOfWorkers = isFunction(config.by) ? this.testGroups.length : numberOfWorkers - this.workers = createWorkerObjects(this.testGroups, this.codecept.config, config.testConfig, config.options, config.selectedRuns) + this.workers = createWorkerObjects(this.testGroups, this.codecept.config, getTestRoot(config.testConfig), config.options, config.selectedRuns) this.numberOfWorkers = this.workers.length } @@ -371,9 +367,9 @@ class Workers extends EventEmitter { * @param {Number} numberOfWorkers */ createGroupsOfTests(numberOfWorkers) { - // If Codecept isn't initialized yet, return empty groups as a safe fallback - if (!this.codecept) return populateGroups(numberOfWorkers) - const files = this.codecept.testFiles + // If Codecept isn't initialized yet, return empty groups as a safe fallback + if (!this.codecept) return populateGroups(numberOfWorkers) + const files = this.codecept.testFiles const mocha = Container.mocha() mocha.files = files mocha.loadFiles() @@ -430,7 +426,7 @@ class Workers extends EventEmitter { for (const file of files) { this.testPool.push(file) } - + this.testPoolInitialized = true } @@ -443,7 +439,7 @@ class Workers extends EventEmitter { if (!this.testPoolInitialized) { this._initializeTestPool() } - + return this.testPool.shift() } @@ -451,9 +447,9 @@ class Workers extends EventEmitter { * @param {Number} numberOfWorkers */ createGroupsOfSuites(numberOfWorkers) { - // If Codecept isn't initialized yet, return empty groups as a safe fallback - if (!this.codecept) return populateGroups(numberOfWorkers) - const files = this.codecept.testFiles + // If Codecept isn't initialized yet, return empty groups as a safe fallback + if (!this.codecept) return populateGroups(numberOfWorkers) + const files = this.codecept.testFiles const groups = populateGroups(numberOfWorkers) const mocha = Container.mocha() @@ -494,7 +490,7 @@ class Workers extends EventEmitter { recorder.startUnlessRunning() event.dispatcher.emit(event.workers.before) process.env.RUNS_WITH_WORKERS = 'true' - + // Create workers and set up message handlers immediately (not in recorder queue) // This prevents a race condition where workers start sending messages before handlers are attached const workerThreads = [] @@ -503,11 +499,11 @@ class Workers extends EventEmitter { this._listenWorkerEvents(workerThread) workerThreads.push(workerThread) } - + recorder.add('workers started', () => { // Workers are already running, this is just a placeholder step }) - + return new Promise(resolve => { this.on('end', resolve) }) @@ -591,7 +587,7 @@ class Workers extends EventEmitter { // Otherwise skip - we'll emit based on finished state break case event.test.passed: - // Skip individual passed events - we'll emit based on finished state + // Skip individual passed events - we'll emit based on finished state break case event.test.skipped: this.emit(event.test.skipped, deserializeTest(message.data)) @@ -602,15 +598,15 @@ class Workers extends EventEmitter { const data = message.data const uid = data?.uid const isFailed = !!data?.err || data?.state === 'failed' - + if (uid) { // Track states for each test UID if (!this._testStates) this._testStates = new Map() - + if (!this._testStates.has(uid)) { this._testStates.set(uid, { states: [], lastData: data }) } - + const testState = this._testStates.get(uid) testState.states.push({ isFailed, data }) testState.lastData = data @@ -622,7 +618,7 @@ class Workers extends EventEmitter { this.emit(event.test.passed, deserializeTest(data)) } } - + this.emit(event.test.finished, deserializeTest(data)) } break @@ -682,11 +678,10 @@ class Workers extends EventEmitter { // For tests with retries configured, emit all failures + final success // For tests without retries, emit only final state const lastState = states[states.length - 1] - + // Check if this test had retries by looking for failure followed by success - const hasRetryPattern = states.length > 1 && - states.some((s, i) => s.isFailed && i < states.length - 1 && !states[i + 1].isFailed) - + const hasRetryPattern = states.length > 1 && states.some((s, i) => s.isFailed && i < states.length - 1 && !states[i + 1].isFailed) + if (hasRetryPattern) { // Emit all intermediate failures and final success for retries for (const state of states) { diff --git a/package.json b/package.json index e61cdd069..16b464ff5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codeceptjs", - "version": "4.0.0-beta.20", + "version": "4.0.1-beta.8", "type": "module", "description": "Supercharged End 2 End Testing Framework for NodeJS", "keywords": [ diff --git a/test/helper/JSONResponse_test.js b/test/helper/JSONResponse_test.js index a764b5182..638627864 100644 --- a/test/helper/JSONResponse_test.js +++ b/test/helper/JSONResponse_test.js @@ -27,8 +27,8 @@ let restHelper let I describe('JSONResponse', () => { - beforeEach(() => { - Container.create({ + beforeEach(async () => { + await Container.create({ helpers: { REST: {}, }, @@ -41,42 +41,42 @@ describe('JSONResponse', () => { describe('response codes', () => { it('should check 200x codes', async () => { - restHelper.config.onResponse({ status: 204 }) + restHelper.options.onResponse({ status: 204 }) I.seeResponseCodeIs(204) I.dontSeeResponseCodeIs(200) I.seeResponseCodeIsSuccessful() }) it('should check 300x codes', async () => { - restHelper.config.onResponse({ status: 304 }) + restHelper.options.onResponse({ status: 304 }) I.seeResponseCodeIs(304) I.dontSeeResponseCodeIs(200) I.seeResponseCodeIsRedirection() }) it('should check 400x codes', async () => { - restHelper.config.onResponse({ status: 404 }) + restHelper.options.onResponse({ status: 404 }) I.seeResponseCodeIs(404) I.dontSeeResponseCodeIs(200) I.seeResponseCodeIsClientError() }) it('should check 500x codes', async () => { - restHelper.config.onResponse({ status: 504 }) + restHelper.options.onResponse({ status: 504 }) I.seeResponseCodeIs(504) I.dontSeeResponseCodeIs(200) I.seeResponseCodeIsServerError() }) it('should throw error on invalid code', () => { - restHelper.config.onResponse({ status: 504 }) + restHelper.options.onResponse({ status: 504 }) expect(() => I.seeResponseCodeIs(200)).to.throw('Response code') }) }) describe('response data', () => { it('should check for json inclusion', () => { - restHelper.config.onResponse({ data }) + restHelper.options.onResponse({ data }) I.seeResponseContainsJson({ posts: [{ id: 2 }], }) @@ -88,7 +88,7 @@ describe('JSONResponse', () => { it('should check for json inclusion - returned Array', () => { const arrayData = [{ ...data }] - restHelper.config.onResponse({ data: arrayData }) + restHelper.options.onResponse({ data: arrayData }) I.seeResponseContainsJson({ posts: [{ id: 2 }], }) @@ -100,48 +100,48 @@ describe('JSONResponse', () => { it('should check for json inclusion - returned Array of 2 items', () => { const arrayData = [{ ...data }, { posts: { id: 3 } }] - restHelper.config.onResponse({ data: arrayData }) + restHelper.options.onResponse({ data: arrayData }) I.seeResponseContainsJson({ posts: { id: 3 }, }) }) it('should simply check for json inclusion', () => { - restHelper.config.onResponse({ data: { user: { name: 'jon', email: 'jon@doe.com' } } }) + restHelper.options.onResponse({ data: { user: { name: 'jon', email: 'jon@doe.com' } } }) I.seeResponseContainsJson({ user: { name: 'jon' } }) I.dontSeeResponseContainsJson({ user: { name: 'jo' } }) I.dontSeeResponseContainsJson({ name: 'joe' }) }) it('should simply check for json inclusion - returned Array', () => { - restHelper.config.onResponse({ data: [{ user: { name: 'jon', email: 'jon@doe.com' } }] }) + restHelper.options.onResponse({ data: [{ user: { name: 'jon', email: 'jon@doe.com' } }] }) I.seeResponseContainsJson({ user: { name: 'jon' } }) I.dontSeeResponseContainsJson({ user: { name: 'jo' } }) I.dontSeeResponseContainsJson({ name: 'joe' }) }) it('should simply check for json equality', () => { - restHelper.config.onResponse({ data: { user: 1 } }) + restHelper.options.onResponse({ data: { user: 1 } }) I.seeResponseEquals({ user: 1 }) }) it('should simply check for json equality - returned Array', () => { - restHelper.config.onResponse({ data: [{ user: 1 }] }) + restHelper.options.onResponse({ data: [{ user: 1 }] }) I.seeResponseEquals([{ user: 1 }]) }) it('should check json contains keys', () => { - restHelper.config.onResponse({ data: { user: 1, post: 2 } }) + restHelper.options.onResponse({ data: { user: 1, post: 2 } }) I.seeResponseContainsKeys(['user', 'post']) }) it('should check json contains keys - returned Array', () => { - restHelper.config.onResponse({ data: [{ user: 1, post: 2 }] }) + restHelper.options.onResponse({ data: [{ user: 1, post: 2 }] }) I.seeResponseContainsKeys(['user', 'post']) }) it('should check for json by callback', () => { - restHelper.config.onResponse({ data }) + restHelper.options.onResponse({ data }) const fn = ({ assert, data }) => { assert('posts' in data) assert('user' in data) @@ -151,13 +151,15 @@ describe('JSONResponse', () => { }) it('should check for json by zod schema', () => { - restHelper.config.onResponse({ data }) + restHelper.options.onResponse({ data }) const schema = z.object({ - posts: z.array(z.object({ - id: z.number(), - author: z.string(), - title: z.string(), - })), + posts: z.array( + z.object({ + id: z.number(), + author: z.string(), + title: z.string(), + }), + ), user: z.object({ name: z.string(), }), @@ -170,7 +172,7 @@ describe('JSONResponse', () => { }) it('should throw error when zod validation fails', () => { - restHelper.config.onResponse({ data: { name: 'invalid', age: 'not_a_number' } }) + restHelper.options.onResponse({ data: { name: 'invalid', age: 'not_a_number' } }) const schema = z.object({ name: z.string(), age: z.number(), diff --git a/test/helper/Playwright_test.js b/test/helper/Playwright_test.js index 9a54aa874..7799511d8 100644 --- a/test/helper/Playwright_test.js +++ b/test/helper/Playwright_test.js @@ -1101,7 +1101,7 @@ describe('Playwright', function () { it('should convert to axios response with onResponse hook', async () => { let response - I.config.onResponse = resp => (response = resp) + I.options.onResponse = resp => (response = resp) await I.makeApiRequest('get', 'http://localhost:3001/api/users?page=2') expect(response).to.be.ok expect(response.status).to.equal(200) diff --git a/test/rest/REST_test.js b/test/rest/REST_test.js index e68c547ef..ba687ab4a 100644 --- a/test/rest/REST_test.js +++ b/test/rest/REST_test.js @@ -140,7 +140,7 @@ describe('REST', () => { let jsonResponse beforeEach(async () => { - Container.create({ + await Container.create({ helpers: { REST: {}, JSONResponse: {}, diff --git a/test/unit/bdd_test.js b/test/unit/bdd_test.js index 4c7ba4713..eae122cf9 100644 --- a/test/unit/bdd_test.js +++ b/test/unit/bdd_test.js @@ -55,10 +55,10 @@ const checkTestForErrors = test => { } describe('BDD', () => { - beforeEach(() => { + beforeEach(async () => { clearSteps() recorder.start() - container.create({}) + await container.create({}) Config.reset() }) diff --git a/test/unit/container_test.js b/test/unit/container_test.js index f76efebb4..ccc93be91 100644 --- a/test/unit/container_test.js +++ b/test/unit/container_test.js @@ -161,7 +161,7 @@ describe('Container', () => { FileSystem: {}, }, } - container.create(config) + await container.create(config) await container.started() // custom helpers expect(container.helpers('MyHelper')).is.ok @@ -266,7 +266,7 @@ describe('Container', () => { FileSystem: {}, }, } - container.create(config) + await container.create(config) await container.started() container.append({ helpers: { @@ -294,10 +294,10 @@ describe('Container', () => { const tsStepsPath = path.join(__dirname, '../data/typescript-support/steps_file.ts') await container.create({ include: { - I: tsStepsPath - } + I: tsStepsPath, + }, }) - + const I = container.support('I') expect(I).to.be.ok expect(I.testMethod).to.be.a('function') @@ -309,10 +309,10 @@ describe('Container', () => { const tsStepsPath = path.join(__dirname, '../data/typescript-support/steps_file.ts') await container.create({ include: { - I: tsStepsPath - } + I: tsStepsPath, + }, }) - + const I = container.support('I') // Note: These are proxied through MetaStep, so we can't call them directly in tests // The test verifies that the file loads and the structure is correct @@ -325,10 +325,10 @@ describe('Container', () => { const tsStepsPath = path.join(__dirname, '../data/typescript-support/steps_with_dirname.ts') await container.create({ include: { - I: tsStepsPath - } + I: tsStepsPath, + }, }) - + const I = container.support('I') expect(I).to.be.ok expect(I.getConfigPath).to.be.a('function') @@ -340,10 +340,10 @@ describe('Container', () => { const tsStepsPath = path.join(__dirname, '../data/typescript-support/steps_with_require.ts') await container.create({ include: { - I: tsStepsPath - } + I: tsStepsPath, + }, }) - + const I = container.support('I') expect(I).to.be.ok expect(I.getPluginPath).to.be.a('function') diff --git a/test/unit/mocha/asyncWrapper_test.js b/test/unit/mocha/asyncWrapper_test.js index 48971b0b5..f843a1864 100644 --- a/test/unit/mocha/asyncWrapper_test.js +++ b/test/unit/mocha/asyncWrapper_test.js @@ -21,9 +21,7 @@ describe('AsyncWrapper', () => { test.fn = fn await Container.create({ helpers: { - TestHelper: { - testMethod: () => 'test result', - }, + FileSystem: {}, }, }) }) diff --git a/test/unit/worker_test.js b/test/unit/worker_test.js index 77a77ceac..635f00b72 100644 --- a/test/unit/worker_test.js +++ b/test/unit/worker_test.js @@ -60,21 +60,23 @@ describe('Workers', function () { const workerConfig = { by: createTestGroups, testConfig: './test/data/sandbox/codecept.customworker.js', + options: { + override: JSON.stringify({ + helpers: { + FileSystem: {}, + Workers: { + require: './workers_helper', + }, + CustomWorkers: { + require: './custom_worker_helper', + }, + }, + }), + }, } const workers = new Workers(-1, workerConfig) - for (const worker of workers.getWorkers()) { - worker.addConfig({ - helpers: { - FileSystem: {}, - Workers: { - require: './custom_worker_helper.js', - }, - }, - }) - } - workers.run() workers.on(event.all.result, result => { @@ -110,7 +112,7 @@ describe('Workers', function () { // Clean up event listeners workers.removeListener(event.test.failed, onTestFailed) workers.removeListener(event.test.passed, onTestPassed) - + // The main assertion is that workers ran and some tests failed (indicating they executed) expect(result.hasFailed).equal(true) // In test suite context, event counting has timing issues, but functionality works @@ -141,7 +143,10 @@ describe('Workers', function () { helpers: { FileSystem: {}, Workers: { - require: './custom_worker_helper.js', + require: './workers_helper', + }, + CustomWorkers: { + require: './custom_worker_helper', }, }, }) @@ -176,7 +181,10 @@ describe('Workers', function () { helpers: { FileSystem: {}, Workers: { - require: './custom_worker_helper.js', + require: './workers_helper', + }, + CustomWorkers: { + require: './custom_worker_helper', }, }, }) @@ -233,7 +241,7 @@ describe('Workers', function () { testConfig: './test/data/sandbox/codecept.non-test-events-worker.js', } - let workers = new Workers(2, workerConfig) + let workers = new Workers(2, workerConfig) workers.run()