Skip to content

Commit f2d832c

Browse files
miraoclaude
andcommitted
fix(config): make config.get() work from tests/page objects under tsx/cjs (#5635)
The internal API `config` imported via `import { config } from "codeceptjs"` returned an empty object `{}` from tests, page objects and fragments, while it worked from helpers. Test files are loaded through Mocha's synchronous require() (the CommonJS realm) under a CJS loader such as tsx/cjs, while the framework runs as native ESM. So a test importing `config` gets a second, disconnected CJS copy of config.js whose module-scoped `config` is an empty `{}` that the runner never populates. Helpers don't hit this because they are loaded via import() (the ESM realm) and share the live singleton. Bridge the live config through globalThis, the same way recorder.js and container.js were fixed in #5634: setConfig() mirrors the config onto globalThis.__codeceptjs_config on every create/append/reset, and Config.get() prefers it, falling back to the local module copy. Under pure ESM both point at the same object, so there is no behavior change on the ESM path. Adds a runner regression test driving a real tsx/cjs project that asserts a test (CJS realm) and a helper (ESM realm) both read the live config. It fails without the fix and passes with it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent a615e9f commit f2d832c

7 files changed

Lines changed: 119 additions & 5 deletions

File tree

lib/config.js

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,20 @@ const defaultConfig = {
3737
let hooks = []
3838
let config = {}
3939

40+
// Expose the live config object globally so a duplicate copy of the framework
41+
// (loaded as CommonJS when a test, page object or fragment does
42+
// `import { config } from 'codeceptjs'` through a CJS loader such as tsx/cjs)
43+
// reads the same config the runner actually loaded. Test files are loaded via
44+
// Mocha's synchronous require() (CJS realm) while the framework runs as ESM, so
45+
// the CJS copy would otherwise see its own empty module-scoped `config = {}`.
46+
// Helpers don't hit this because they're loaded via import() (the ESM realm).
47+
// Mirrors the globalThis bridges in recorder.js and container.js.
48+
function setConfig(value) {
49+
config = value
50+
if (typeof globalThis !== 'undefined') globalThis.__codeceptjs_config = config
51+
return config
52+
}
53+
4054
// Apply a single hook against `cfg`, swallowing errors so one broken hook
4155
// can't take down the whole run. The failure is logged through the
4256
// framework's own output module (when available) so it shows up in test
@@ -67,7 +81,7 @@ class Config {
6781
* @return {Object<string, *>}
6882
*/
6983
static create(newConfig) {
70-
config = deepMerge(deepClone(defaultConfig), newConfig)
84+
setConfig(deepMerge(deepClone(defaultConfig), newConfig))
7185
// Re-apply every hook against the freshly built config; hooks added later
7286
// (e.g. from plugin boot) stay pending until runPendingHooks. Array
7387
// iterators re-check length on each step, so hooks pushed during a hook
@@ -137,10 +151,11 @@ class Config {
137151
* @return {*}
138152
*/
139153
static get(key, val) {
154+
const cfg = (typeof globalThis !== 'undefined' && globalThis.__codeceptjs_config) || config
140155
if (key) {
141-
return config[key] || val
156+
return cfg[key] || val
142157
}
143-
return config
158+
return cfg
144159
}
145160

146161
static addHook(fn) {
@@ -195,7 +210,7 @@ class Config {
195210
* @return {Object<string, *>}
196211
*/
197212
static append(additionalConfig) {
198-
return (config = deepMerge(config, additionalConfig))
213+
return setConfig(deepMerge(config, additionalConfig))
199214
}
200215

201216
/**
@@ -204,7 +219,7 @@ class Config {
204219
*/
205220
static reset() {
206221
hooks = []
207-
return (config = { ...defaultConfig })
222+
return setConfig({ ...defaultConfig })
208223
}
209224
}
210225

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export const config: CodeceptJS.MainConfig = {
2+
tests: "./*_test.ts",
3+
output: "./output",
4+
helpers: {
5+
ConfigHelper: {
6+
require: "./config_helper.js",
7+
marker: "config-marker-123"
8+
}
9+
},
10+
name: "config-tsx-cjs-test",
11+
require: ["tsx/cjs"]
12+
};
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import HelperModule from '../../../lib/helper.js'
2+
import ConfigModule from '../../../lib/config.js'
3+
4+
const Helper = HelperModule.default || HelperModule
5+
const Config = ConfigModule.default || ConfigModule
6+
7+
class ConfigHelper extends Helper {
8+
reportConfig() {
9+
// Helper is loaded via import() (ESM realm), so it has always shared the live config.
10+
console.log(`CONFIG_FROM_HELPER marker=${Config.get().helpers.ConfigHelper.marker}`)
11+
}
12+
}
13+
14+
export default ConfigHelper
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Regression test for https://github.com/codeceptjs/CodeceptJS/issues/5635
2+
// Importing the internal API (config) through a CommonJS loader (tsx/cjs) used to load
3+
// a second, disconnected copy of config.js whose module-scoped `config` was an empty
4+
// object, so `config.get()` returned {} from tests/page objects/fragments while helpers
5+
// (loaded via import(), the ESM realm) saw the real config. The relative path is loaded
6+
// as CJS here while the runner loads the same module as ESM, reproducing the split that
7+
// the globalThis bridge fixes.
8+
import ConfigModule from "../../../lib/config.js";
9+
10+
const Config = (ConfigModule as any).default || ConfigModule;
11+
12+
Feature("config under tsx/cjs");
13+
14+
Scenario("config.get() reads the live config from a test", ({ I }) => {
15+
console.log(`CONFIG_FROM_TEST name=${Config.get("name")}`);
16+
console.log(`CONFIG_FROM_TEST marker=${Config.get().helpers.ConfigHelper.marker}`);
17+
I.reportConfig();
18+
});
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"name": "config-tsx-cjs",
3+
"version": "1.0.0",
4+
"type": "module",
5+
"devDependencies": {
6+
"tsx": "^4.20.6"
7+
}
8+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"compilerOptions": {
3+
"target": "es2022",
4+
"lib": ["es2022", "DOM"],
5+
"esModuleInterop": true,
6+
"module": "esnext",
7+
"moduleResolution": "node",
8+
"strictNullChecks": false,
9+
"types": ["codeceptjs", "node"],
10+
"declaration": true,
11+
"skipLibCheck": true
12+
},
13+
"exclude": ["node_modules"]
14+
}

test/runner/config_tsx_test.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import * as chai from 'chai'
2+
chai.should()
3+
import path from 'path'
4+
import { exec } from 'child_process'
5+
import { fileURLToPath } from 'url'
6+
7+
const __filename = fileURLToPath(import.meta.url)
8+
const __dirname = path.dirname(__filename)
9+
10+
const runner = path.join(__dirname, '/../../bin/codecept.js')
11+
const codecept_dir = path.join(__dirname, '/../data/config-tsx-cjs')
12+
const codecept_run = `${runner} run --config ${codecept_dir}/codecept.conf.ts`
13+
14+
// Regression test for https://github.com/codeceptjs/CodeceptJS/issues/5635
15+
// When the internal API (config) is imported through a CommonJS loader (tsx/cjs) a second,
16+
// disconnected copy of config.js was loaded, so config.get() returned {} from tests/page
17+
// objects while helpers (loaded via import()) saw the real config.
18+
describe('CodeceptJS config under tsx/cjs', function () {
19+
this.timeout(40000)
20+
21+
it('config.get() reads the live config from a test imported via tsx/cjs', done => {
22+
exec(`${codecept_run}`, { timeout: 30000 }, (err, stdout) => {
23+
stdout.should.include('1 passed')
24+
// the test (CJS realm) reads the real config the runner loaded, not an empty {}
25+
stdout.should.include('CONFIG_FROM_TEST name=config-tsx-cjs-test')
26+
stdout.should.include('CONFIG_FROM_TEST marker=config-marker-123')
27+
// the helper (ESM realm) reads the same live config
28+
stdout.should.include('CONFIG_FROM_HELPER marker=config-marker-123')
29+
chai.expect(err).to.be.null
30+
done()
31+
})
32+
})
33+
})

0 commit comments

Comments
 (0)