From bda5e5e4a21293c3fe42d80722a9f4b317058115 Mon Sep 17 00:00:00 2001 From: kobenguyent <7845001+kobenguyent@users.noreply.github.com> Date: Tue, 11 Nov 2025 15:44:14 +0000 Subject: [PATCH 01/11] fix bugs --- lib/container.js | 36 +++++++++++++++++++++------- test/rest/REST_test.js | 2 +- test/unit/bdd_test.js | 4 ++-- test/unit/container_test.js | 28 +++++++++++----------- test/unit/mocha/asyncWrapper_test.js | 4 +--- 5 files changed, 45 insertions(+), 29 deletions(-) diff --git a/lib/container.js b/lib/container.js index aaa93ee0c..83bd6e914 100644 --- a/lib/container.js +++ b/lib/container.js @@ -32,7 +32,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 } /** @@ -202,6 +202,15 @@ class Container { static append(newContainer) { container = deepMerge(container, newContainer) + // If new helpers are added, set the helpers property on them + if (newContainer.helpers) { + for (const name in newContainer.helpers) { + if (container.helpers[name] && typeof container.helpers[name] === 'object') { + container.helpers[name].helpers = container.helpers + } + } + } + // If new support objects are added, update the proxy support if (newContainer.support) { const newProxySupport = createSupportObjects(newContainer.support) @@ -250,10 +259,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) } @@ -349,6 +358,16 @@ async function createHelpers(config) { } } + // Set helpers property on each helper to allow access to other helpers + for (const name in helpers) { + if (helpers[name] && typeof helpers[name] === 'object') { + helpers[name].helpers = helpers + } + } + + // Wait for async helpers and call _init + await asyncHelperPromise + for (const name in helpers) { if (helpers[name]._init) await helpers[name]._init() } @@ -677,24 +696,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 +721,7 @@ async function loadSupportObject(modulePath, supportObjectName) { importPath = `${importPath}.js` } } - + let obj try { obj = await import(importPath) 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: {}, }, }) }) From 7acd1044288fff6963e09e8fcbd5e4edee1a28a4 Mon Sep 17 00:00:00 2001 From: kobenguyent <7845001+kobenguyent@users.noreply.github.com> Date: Tue, 11 Nov 2025 16:24:22 +0000 Subject: [PATCH 02/11] fix bug --- lib/container.js | 33 ++++++++++----------------------- lib/helper/REST.js | 3 ++- 2 files changed, 12 insertions(+), 24 deletions(-) diff --git a/lib/container.js b/lib/container.js index 83bd6e914..723a5a58a 100644 --- a/lib/container.js +++ b/lib/container.js @@ -202,15 +202,6 @@ class Container { static append(newContainer) { container = deepMerge(container, newContainer) - // If new helpers are added, set the helpers property on them - if (newContainer.helpers) { - for (const name in newContainer.helpers) { - if (container.helpers[name] && typeof container.helpers[name] === 'object') { - container.helpers[name].helpers = container.helpers - } - } - } - // If new support objects are added, update the proxy support if (newContainer.support) { const newProxySupport = createSupportObjects(newContainer.support) @@ -301,7 +292,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) @@ -320,8 +311,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 { @@ -341,9 +331,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 @@ -358,19 +347,17 @@ async function createHelpers(config) { } } - // Set helpers property on each helper to allow access to other helpers - for (const name in helpers) { - if (helpers[name] && typeof helpers[name] === 'object') { - helpers[name].helpers = helpers - } - } - - // Wait for async helpers and call _init + // Wait for all async helpers to be fully loaded await asyncHelperPromise + // Call _init on all helpers after they're all loaded for (const name in helpers) { - if (helpers[name]._init) await helpers[name]._init() + if (helpers[name]._init) { + await helpers[name]._init() + debug(`helper ${name} _init() called`) + } } + return helpers } diff --git a/lib/helper/REST.js b/lib/helper/REST.js index f398b4802..d63718306 100644 --- a/lib/helper/REST.js +++ b/lib/helper/REST.js @@ -468,7 +468,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) { From 5ad33dc85d0ebeb90b0c2a6be047b04caccccdaa Mon Sep 17 00:00:00 2001 From: kobenguyent <7845001+kobenguyent@users.noreply.github.com> Date: Tue, 11 Nov 2025 16:30:07 +0000 Subject: [PATCH 03/11] fix bug --- lib/container.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/container.js b/lib/container.js index 723a5a58a..0c020599c 100644 --- a/lib/container.js +++ b/lib/container.js @@ -110,6 +110,9 @@ class Container { } } + // Wait for all async helpers to finish loading and populate the actor with helper methods + 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 From 9b531cd2f85e951e57cb7bc5feaaba32cee01ab9 Mon Sep 17 00:00:00 2001 From: kobenguyent <7845001+kobenguyent@users.noreply.github.com> Date: Tue, 11 Nov 2025 16:50:32 +0000 Subject: [PATCH 04/11] fix: get helpers dynamically in config listener to avoid undefined errors The config listener was capturing a static snapshot of helpers at initialization time, which could contain empty placeholder objects during async helper loading. This caused 'Cannot read properties of undefined (reading name)' errors when tests tried to access helper properties. Changes: - Move helpers retrieval inside event handler to get runtime instances - Add guard clause to check helper validity before accessing properties - Ensures we always get fully initialized helper instances, not placeholders Fixes issue where actor I was empty {} and helper.constructor.name threw undefined errors during Scenario execution. --- lib/listener/config.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) 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] From cfa1bfa84ece678ea6d5f4997110f1895feaeea8 Mon Sep 17 00:00:00 2001 From: kobenguyent <7845001+kobenguyent@users.noreply.github.com> Date: Fri, 14 Nov 2025 14:22:46 +0000 Subject: [PATCH 05/11] fix: use this.options instead of this.config for helper callbacks The Helper base class from @codeceptjs/helper sets both this.config (raw user config) and this.options (validated/merged config with defaults). Helpers should consistently use this.options to access configuration values. Fixed helpers to use this.options for onRequest/onResponse callbacks: - REST helper: Fixed onRequest and onResponse to use this.options - GraphQL helper: Fixed onRequest and onResponse to use this.options - Playwright helper: Fixed onResponse to use this.options - JSONResponse helper: Fixed accessing request helper's options.onResponse Updated tests to match: - JSONResponse_test.js: Changed restHelper.config to restHelper.options - JSONResponse_test.js: Made beforeEach async and await Container.create() - Playwright_test.js: Changed I.config to I.options - JSONResponse.js: Added named export for test compatibility This fixes the error: 'Cannot read properties of undefined (reading "name")' which occurred because JSONResponse tried to access this.helpers.REST.config.onResponse but REST helper stores callbacks in this.options, not this.config. All 488 unit tests passing. --- lib/helper/GraphQL.js | 8 ++--- lib/helper/JSONResponse.js | 7 ++--- lib/helper/Playwright.js | 53 +++++++++++--------------------- lib/helper/REST.js | 8 ++--- test/helper/JSONResponse_test.js | 50 +++++++++++++++--------------- test/helper/Playwright_test.js | 2 +- 6 files changed, 56 insertions(+), 72 deletions(-) diff --git a/lib/helper/GraphQL.js b/lib/helper/GraphQL.js index 390aba4d9..e6ff68a80 100644 --- a/lib/helper/GraphQL.js +++ b/lib/helper/GraphQL.js @@ -87,8 +87,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 +102,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..024f25ccd 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 @@ -794,10 +794,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 +837,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 +948,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 +961,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 +1378,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 +1387,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 +1396,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 +1522,7 @@ class Playwright extends Helper { acceptDownloads: true, ...this.options.emulate, } - + try { this.browserContext = await this.browser.newContext(contextOptions) } catch (err) { @@ -3183,14 +3166,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 +4320,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 +4333,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 +4374,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 +4950,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 d63718306..783547b09 100644 --- a/lib/helper/REST.js +++ b/lib/helper/REST.js @@ -215,8 +215,8 @@ class REST extends Helper { } } - if (this.config.onRequest) { - await this.config.onRequest(request) + if (this.options.onRequest) { + await this.options.onRequest(request) } try { @@ -245,8 +245,8 @@ class REST extends Helper { } response = err.response } - if (this.config.onResponse) { - await this.config.onResponse(response) + if (this.options.onResponse) { + await this.options.onResponse(response) } try { this.options.prettyPrintJson ? this.debugSection('Response', beautify(JSON.stringify(response.data))) : this.debugSection('Response', JSON.stringify(response.data)) 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) From de632a32e698d83657473d6bf1f9d2584d59a9f4 Mon Sep 17 00:00:00 2001 From: kobenguyent <7845001+kobenguyent@users.noreply.github.com> Date: Fri, 14 Nov 2025 15:25:44 +0000 Subject: [PATCH 06/11] fix: ensure helper options always have default values for onRequest/onResponse The issue was that _setConfig() overwrites this.options entirely, losing any defaults that were set before it was called. This caused this.options.onResponse to be undefined instead of null when not configured by the user. Fixed by: - REST: Merge config with defaults after _setConfig() call - GraphQL: Add onRequest/onResponse to defaults (was missing) - Playwright: Add onResponse to defaults (was missing) This ensures that helpers always have these properties defined, even if set to null, preventing 'Cannot read properties of undefined' errors when JSONResponse or other code tries to access them. All 488 unit tests passing. --- lib/helper/GraphQL.js | 2 ++ lib/helper/Playwright.js | 1 + lib/helper/REST.js | 11 +++++++---- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/helper/GraphQL.js b/lib/helper/GraphQL.js index e6ff68a80..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 } diff --git a/lib/helper/Playwright.js b/lib/helper/Playwright.js index 024f25ccd..882e7c455 100644 --- a/lib/helper/Playwright.js +++ b/lib/helper/Playwright.js @@ -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' diff --git a/lib/helper/REST.js b/lib/helper/REST.js index 783547b09..ad5b09319 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 From 1911ae645660a46625e7ac7f2038bed882f3605f Mon Sep 17 00:00:00 2001 From: kobenguyent <7845001+kobenguyent@users.noreply.github.com> Date: Fri, 14 Nov 2025 16:55:08 +0000 Subject: [PATCH 07/11] fix: actor initialization and helper config issues (v4.0.1-beta.7) - Fixed actor not being populated with helper methods - Fixed circular dependency in actor.js - Moved actor creation timing to allow proper callback registration - Fixed helpers to use this.options consistently instead of this.config - Fixed helpers to merge defaults after _setConfig() - Fixed container proxy getOwnPropertyDescriptor for destructuring support - Changed createHelpers to defer await for proper callback accumulation --- lib/actor.js | 5 +++-- lib/container.js | 34 +++++++++++++++++++++------------- package.json | 2 +- 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/lib/actor.js b/lib/actor.js index ab2238c32..dd602ca3e 100644 --- a/lib/actor.js +++ b/lib/actor.js @@ -75,8 +75,9 @@ export default function (obj = {}, container) { if (!container) { container = Container } - - const actor = container.actor() || new Actor() + + // Create a new Actor instance + const actor = new Actor() // load all helpers once container initialized container.started(() => { diff --git a/lib/container.js b/lib/container.js index 0c020599c..6c49d1c66 100644 --- a/lib/container.js +++ b/lib/container.js @@ -74,7 +74,7 @@ class Container { // 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 +89,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,7 +110,7 @@ class Container { } } - // Wait for all async helpers to finish loading and populate the actor with helper methods + // 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 @@ -350,16 +350,17 @@ async function createHelpers(config) { } } - // Wait for all async helpers to be fully loaded - await asyncHelperPromise - - // 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`) + // 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 } @@ -534,10 +535,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) { diff --git a/package.json b/package.json index e61cdd069..fb7e73af9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codeceptjs", - "version": "4.0.0-beta.20", + "version": "4.0.1-beta.7", "type": "module", "description": "Supercharged End 2 End Testing Framework for NodeJS", "keywords": [ From 4e4efb3bb48c359feb7f0a2f8f3cbb8690273090 Mon Sep 17 00:00:00 2001 From: kobenguyent <7845001+kobenguyent@users.noreply.github.com> Date: Fri, 14 Nov 2025 16:58:52 +0000 Subject: [PATCH 08/11] fix: preserve actor reference and custom PageObject methods - Fixed actor to use existing instance when available - Added container.append() when actor reference changes - Ensures custom PageObject methods are properly added to actor --- lib/actor.js | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/lib/actor.js b/lib/actor.js index dd602ca3e..73911306c 100644 --- a/lib/actor.js +++ b/lib/actor.js @@ -76,8 +76,8 @@ export default function (obj = {}, container) { container = Container } - // Create a new Actor instance - const actor = new Actor() + // Get existing actor or create a new one + const actor = container.actor() || new Actor() // load all helpers once container initialized container.started(() => { @@ -112,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) From 44002f3d837a807ce1bac91ec9e47ef4acefc44c Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sun, 16 Nov 2025 07:52:33 +0100 Subject: [PATCH 09/11] Fix failed unit tests - achieve 100% pass rate (#5305) --- lib/workers.js | 75 +++++++++++++++++++++++--------------------------- 1 file changed, 35 insertions(+), 40 deletions(-) diff --git a/lib/workers.js b/lib/workers.js index 4a837eba9..aaf8f65a0 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) } @@ -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) { From 409bfec0a00c1e134204d7747f5613288306b15b Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sun, 16 Nov 2025 16:53:02 +0100 Subject: [PATCH 10/11] [WIP] Fix and push failed unit tests (#5306) --- lib/container.js | 11 ++++++++--- lib/helper/REST.js | 10 ++++++---- lib/workers.js | 2 +- test/unit/worker_test.js | 38 +++++++++++++++++++++++--------------- 4 files changed, 38 insertions(+), 23 deletions(-) diff --git a/lib/container.js b/lib/container.js index 6c49d1c66..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, /** @@ -67,7 +68,8 @@ 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() @@ -207,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)) @@ -224,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 diff --git a/lib/helper/REST.js b/lib/helper/REST.js index ad5b09319..3c272d958 100644 --- a/lib/helper/REST.js +++ b/lib/helper/REST.js @@ -218,8 +218,9 @@ class REST extends Helper { } } - if (this.options.onRequest) { - await this.options.onRequest(request) + const onRequest = this.options.onRequest || this.config.onRequest + if (onRequest) { + await onRequest(request) } try { @@ -248,8 +249,9 @@ class REST extends Helper { } response = err.response } - if (this.options.onResponse) { - await this.options.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)) diff --git a/lib/workers.js b/lib/workers.js index aaf8f65a0..80dca4e1b 100644 --- a/lib/workers.js +++ b/lib/workers.js @@ -315,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 } 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() From 3e5fa65343b3e348c8cc50500ad0ebcc39048d3f Mon Sep 17 00:00:00 2001 From: kobenguyent <7845001+kobenguyent@users.noreply.github.com> Date: Sun, 16 Nov 2025 17:25:19 +0100 Subject: [PATCH 11/11] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fb7e73af9..16b464ff5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codeceptjs", - "version": "4.0.1-beta.7", + "version": "4.0.1-beta.8", "type": "module", "description": "Supercharged End 2 End Testing Framework for NodeJS", "keywords": [