Skip to content

Commit a43b54e

Browse files
DavertMikDavertMikclaude
authored
refactor: eliminate internal globals in favor of store singleton (#5506)
* refactor: eliminate internal globals in favor of store singleton and direct imports Replace all internal global variable usage with proper ESM patterns: - Add store.initialize() method with immutable required fields (codeceptDir, outputDir, workerMode) - Replace global.codecept_dir/output_dir reads with store.codeceptDir/outputDir (~55 sites) - Replace global.container reads with direct container imports (~15 sites) - Replace global.debugMode with store.debugMode in WebDriver/Puppeteer - Replace process.env.RUNS_WITH_WORKERS with store.workerMode - Replace process.env.FEATURE_ONLY/SCENARIO_ONLY with store.featureOnly/scenarioOnly - Replace global.maskSensitiveData with store.maskSensitiveData - Convert singleton guard globals to module-level variables - Remove dead global.codecept_debug - Keep user-facing DSL globals (Feature, Scenario, etc.) and backward compat writes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: restore global.Helper and global.codecept_helper lost in ESM migration These were set in 3.x codecept.js but never ported to 4.x globals.js. Needed for CJS helpers that use `const Helper = codecept_helper`. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: deprecate globals, default noGlobals: true for new projects - noGlobals: true skips ALL globals including Mocha DSL (Feature, Scenario, etc.) - noGlobals: false (default for existing projects) prints deprecation warning - New projects generated via `codeceptjs init` get noGlobals: true by default - Store noGlobals flag in store so initMochaGlobals respects it Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve CI failures — circular deps, frozen workerMode, null paths - Move tsFileMapping from container to store to break circular dep (step/base.js -> container.js -> step.js -> step/base.js) - Use getters with global fallback for codeceptDir/outputDir so unit tests that set global.codecept_dir directly still work - Stop freezing workerMode — it's set after initialize() by run-workers Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve remaining CI failures - Fix Map type annotation in store.js (TS error) - getMaskConfig falls back to global.maskSensitiveData for unit tests - Update ui_test.js to check store.featureOnly instead of process.env - Remove Object.defineProperty freeze on paths — causes issues when unit tests share process and shard_test calls initialize() first - Restore process.env.RUNS_WITH_WORKERS for cross-process communication (env vars cross process boundaries, store doesn't) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: prefer global.codecept_dir over store._codeceptDir in getter When unit tests set global.codecept_dir directly (without calling store.initialize()), the getter must pick up the global value even if _codeceptDir was set by a prior test in the same process. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: migrate utils_test to use store instead of globals Revert getter priority to store-first (correct design). Fix the test to set store.codeceptDir/outputDir instead of globals. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: DavertMik <davert@testomat.io> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a9cb979 commit a43b54e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+287
-132
lines changed

lib/ai.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { generateText } from 'ai'
77
import { fileURLToPath } from 'url'
88
import path from 'path'
99
import { fileExists } from './utils.js'
10+
import store from './store.js'
1011

1112
const __dirname = path.dirname(fileURLToPath(import.meta.url))
1213

@@ -24,8 +25,8 @@ async function loadPrompts() {
2425
for (const name of promptNames) {
2526
let promptPath
2627

27-
if (global.codecept_dir) {
28-
promptPath = path.join(global.codecept_dir, `prompts/${name}.js`)
28+
if (store.codeceptDir) {
29+
promptPath = path.join(store.codeceptDir, `prompts/${name}.js`)
2930
}
3031

3132
if (!promptPath || !fileExists(promptPath)) {

lib/codecept.js

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { emptyFolder } from './utils.js'
2121
import { initCodeceptGlobals } from './globals.js'
2222
import { validateTypeScriptSetup, getTSNodeESMWarning } from './utils/loaderCheck.js'
2323
import recorder from './recorder.js'
24+
import store from './store.js'
2425

2526
import storeListener from './listener/store.js'
2627
import stepsListener from './listener/steps.js'
@@ -71,7 +72,7 @@ class Codecept {
7172
} else {
7273
// For npm packages, resolve from the user's directory
7374
// This ensures packages like tsx are found in user's node_modules
74-
const userDir = global.codecept_dir || process.cwd()
75+
const userDir = store.codeceptDir || process.cwd()
7576

7677
try {
7778
// Use createRequire to resolve from user's directory
@@ -102,8 +103,6 @@ class Codecept {
102103
await this.requireModules(this.requiringModules)
103104
// initializing listeners
104105
await container.create(this.config, this.opts)
105-
// Store container globally for easy access
106-
global.container = container
107106
await this.runHooks()
108107
}
109108

@@ -171,7 +170,7 @@ class Codecept {
171170
*/
172171
loadTests(pattern) {
173172
const options = {
174-
cwd: global.codecept_dir,
173+
cwd: store.codeceptDir,
175174
}
176175

177176
let patterns = [pattern]
@@ -203,7 +202,7 @@ class Codecept {
203202
globSync(pattern, options).forEach(file => {
204203
if (file.includes('node_modules')) return
205204
if (!fsPath.isAbsolute(file)) {
206-
file = fsPath.join(global.codecept_dir, file)
205+
file = fsPath.join(store.codeceptDir, file)
207206
}
208207
if (!this.testFiles.includes(fsPath.resolve(file))) {
209208
this.testFiles.push(fsPath.resolve(file))
@@ -293,7 +292,7 @@ class Codecept {
293292

294293
if (test) {
295294
if (!fsPath.isAbsolute(test)) {
296-
test = fsPath.join(global.codecept_dir, test)
295+
test = fsPath.join(store.codeceptDir, test)
297296
}
298297
const testBasename = fsPath.basename(test, '.js')
299298
const testFeatureBasename = fsPath.basename(test, '.feature')

lib/command/dryRun.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ async function printTests(files) {
5050
const { default: figures } = await import('figures')
5151
const { default: colors } = await import('chalk')
5252

53-
output.print(output.styles.debug(`Tests from ${global.codecept_dir}:`))
53+
output.print(output.styles.debug(`Tests from ${store.codeceptDir}:`))
5454
output.print()
5555

5656
const mocha = Container.mocha()

lib/command/generate.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { mkdirp } from 'mkdirp'
55
import path from 'path'
66
import { fileExists, ucfirst, lcfirst, beautify } from '../utils.js'
77
import output from '../output.js'
8+
import store from '../store.js'
89
import generateDefinitions from './definitions.js'
910
import { getConfig, getTestRoot, safeFileWrite, readConfig } from './utils.js'
1011

@@ -20,6 +21,7 @@ Scenario('test something', async ({ {{actor}} }) => {
2021
// generates empty test
2122
export async function test(genPath) {
2223
const testsPath = getTestRoot(genPath)
24+
store.codeceptDir = testsPath
2325
global.codecept_dir = testsPath
2426
const config = await getConfig(testsPath)
2527
if (!config) return

lib/command/gherkin/snippets.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import fsPath from 'path'
88
import { getConfig, getTestRoot } from '../utils.js'
99
import Codecept from '../../codecept.js'
1010
import output from '../../output.js'
11+
import store from '../../store.js'
1112
import { matchStep } from '../../mocha/bdd.js'
1213

1314
const uuidFn = IdGenerator.uuid()
@@ -43,9 +44,9 @@ export default async function (genPath, options) {
4344
}
4445

4546
const files = []
46-
globSync(options.feature || config.gherkin.features, { cwd: options.feature ? '.' : global.codecept_dir }).forEach(file => {
47+
globSync(options.feature || config.gherkin.features, { cwd: options.feature ? '.' : store.codeceptDir }).forEach(file => {
4748
if (!fsPath.isAbsolute(file)) {
48-
file = fsPath.join(global.codecept_dir, file)
49+
file = fsPath.join(store.codeceptDir, file)
4950
}
5051
files.push(fsPath.resolve(file))
5152
})
@@ -92,7 +93,7 @@ export default async function (genPath, options) {
9293
if (child.scenario.keyword === 'Scenario Outline') continue // skip scenario outline
9394
parseSteps(child.scenario.steps)
9495
.map(step => {
95-
return Object.assign(step, { file: file.replace(global.codecept_dir, '').slice(1) })
96+
return Object.assign(step, { file: file.replace(store.codeceptDir, '').slice(1) })
9697
})
9798
.map(step => newSteps.set(`${step.type}(${step})`, step))
9899
}
@@ -107,7 +108,7 @@ export default async function (genPath, options) {
107108
}
108109

109110
if (!fsPath.isAbsolute(stepFile)) {
110-
stepFile = fsPath.join(global.codecept_dir, stepFile)
111+
stepFile = fsPath.join(store.codeceptDir, stepFile)
111112
}
112113

113114
const snippets = [...newSteps.values()]

lib/command/init.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const defaultConfig = {
1919
output: '',
2020
helpers: {},
2121
include: {},
22+
noGlobals: true,
2223
plugins: {
2324
},
2425
}

lib/command/run-multiple.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import event from '../event.js'
88
import { createRuns } from './run-multiple/collection.js'
99
import { clearString, replaceValueDeep } from '../utils.js'
1010
import { getConfig, getTestRoot, fail } from './utils.js'
11+
import store from '../store.js'
1112

1213
const __filename = fileURLToPath(import.meta.url)
1314
const __dirname = path.dirname(__filename)
@@ -35,6 +36,7 @@ export default async function (selectedRuns, options) {
3536
const configFile = options.config
3637

3738
const testRoot = getTestRoot(configFile)
39+
store.codeceptDir = testRoot
3840
global.codecept_dir = testRoot
3941

4042
// copy opts to run

lib/command/run-workers.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export default async function (workerCount, selectedRuns, options) {
4141
output.print(`CodeceptJS v${Codecept.version()} ${output.standWithUkraine()}`)
4242
output.print(`Running tests in ${output.styles.bold(numberOfWorkers)} workers...`)
4343
store.hasWorkers = true
44+
store.workerMode = true
4445
process.env.RUNS_WITH_WORKERS = 'true'
4546

4647
const workers = new Workers(numberOfWorkers, config)

lib/command/run.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export default async function (test, options) {
3232
codecept.loadTests(test)
3333

3434
if (options.verbose) {
35-
global.debugMode = true
35+
store.debugMode = true
3636
const { getMachineInfo } = await import('./info.js')
3737
await getMachineInfo()
3838
}

lib/command/workers/runTests.js

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ const { options, tests, testRoot, workerIndex, poolMode } = workerData
2121

2222
// Global error handlers to catch critical errors but not test failures
2323
process.on('uncaughtException', (err) => {
24-
if (global.container?.tsFileMapping && fixErrorStack) {
25-
const fileMapping = global.container.tsFileMapping()
24+
if (container?.tsFileMapping && fixErrorStack) {
25+
const fileMapping = container.tsFileMapping()
2626
if (fileMapping) {
2727
fixErrorStack(err, fileMapping)
2828
}
@@ -40,8 +40,8 @@ process.on('uncaughtException', (err) => {
4040
})
4141

4242
process.on('unhandledRejection', (reason, promise) => {
43-
if (reason && typeof reason === 'object' && reason.stack && global.container?.tsFileMapping && fixErrorStack) {
44-
const fileMapping = global.container.tsFileMapping()
43+
if (reason && typeof reason === 'object' && reason.stack && container?.tsFileMapping && fixErrorStack) {
44+
const fileMapping = container.tsFileMapping()
4545
if (fileMapping) {
4646
fixErrorStack(reason, fileMapping)
4747
}
@@ -163,8 +163,8 @@ initPromise = (async function () {
163163
// IMPORTANT: await is required here since getConfig is async
164164
baseConfig = await getConfig(options.config || testRoot)
165165
} catch (configErr) {
166-
if (global.container?.tsFileMapping && fixErrorStack) {
167-
const fileMapping = global.container.tsFileMapping()
166+
if (container?.tsFileMapping && fixErrorStack) {
167+
const fileMapping = container.tsFileMapping()
168168
if (fileMapping) {
169169
fixErrorStack(configErr, fileMapping)
170170
}
@@ -185,8 +185,8 @@ initPromise = (async function () {
185185
try {
186186
await codecept.init(testRoot)
187187
} catch (initErr) {
188-
if (global.container?.tsFileMapping && fixErrorStack) {
189-
const fileMapping = global.container.tsFileMapping()
188+
if (container?.tsFileMapping && fixErrorStack) {
189+
const fileMapping = container.tsFileMapping()
190190
if (fileMapping) {
191191
fixErrorStack(initErr, fileMapping)
192192
}
@@ -218,8 +218,8 @@ initPromise = (async function () {
218218
parentPort?.close()
219219
}
220220
} catch (err) {
221-
if (global.container?.tsFileMapping && fixErrorStack) {
222-
const fileMapping = global.container.tsFileMapping()
221+
if (container?.tsFileMapping && fixErrorStack) {
222+
const fileMapping = container.tsFileMapping()
223223
if (fileMapping) {
224224
fixErrorStack(err, fileMapping)
225225
}

0 commit comments

Comments
 (0)