diff --git a/lib/ai.js b/lib/ai.js index aac4a8904..7bbfa2d28 100644 --- a/lib/ai.js +++ b/lib/ai.js @@ -7,6 +7,7 @@ import { generateText } from 'ai' import { fileURLToPath } from 'url' import path from 'path' import { fileExists } from './utils.js' +import store from './store.js' const __dirname = path.dirname(fileURLToPath(import.meta.url)) @@ -24,8 +25,8 @@ async function loadPrompts() { for (const name of promptNames) { let promptPath - if (global.codecept_dir) { - promptPath = path.join(global.codecept_dir, `prompts/${name}.js`) + if (store.codeceptDir) { + promptPath = path.join(store.codeceptDir, `prompts/${name}.js`) } if (!promptPath || !fileExists(promptPath)) { diff --git a/lib/codecept.js b/lib/codecept.js index 97f257f50..e01fe8345 100644 --- a/lib/codecept.js +++ b/lib/codecept.js @@ -21,6 +21,7 @@ import { emptyFolder } from './utils.js' import { initCodeceptGlobals } from './globals.js' import { validateTypeScriptSetup, getTSNodeESMWarning } from './utils/loaderCheck.js' import recorder from './recorder.js' +import store from './store.js' import storeListener from './listener/store.js' import stepsListener from './listener/steps.js' @@ -71,7 +72,7 @@ class Codecept { } else { // For npm packages, resolve from the user's directory // This ensures packages like tsx are found in user's node_modules - const userDir = global.codecept_dir || process.cwd() + const userDir = store.codeceptDir || process.cwd() try { // Use createRequire to resolve from user's directory @@ -102,8 +103,6 @@ class Codecept { await this.requireModules(this.requiringModules) // initializing listeners await container.create(this.config, this.opts) - // Store container globally for easy access - global.container = container await this.runHooks() } @@ -171,7 +170,7 @@ class Codecept { */ loadTests(pattern) { const options = { - cwd: global.codecept_dir, + cwd: store.codeceptDir, } let patterns = [pattern] @@ -203,7 +202,7 @@ class Codecept { globSync(pattern, options).forEach(file => { if (file.includes('node_modules')) return if (!fsPath.isAbsolute(file)) { - file = fsPath.join(global.codecept_dir, file) + file = fsPath.join(store.codeceptDir, file) } if (!this.testFiles.includes(fsPath.resolve(file))) { this.testFiles.push(fsPath.resolve(file)) @@ -293,7 +292,7 @@ class Codecept { if (test) { if (!fsPath.isAbsolute(test)) { - test = fsPath.join(global.codecept_dir, test) + test = fsPath.join(store.codeceptDir, test) } const testBasename = fsPath.basename(test, '.js') const testFeatureBasename = fsPath.basename(test, '.feature') diff --git a/lib/command/dryRun.js b/lib/command/dryRun.js index 207483764..1d46caf85 100644 --- a/lib/command/dryRun.js +++ b/lib/command/dryRun.js @@ -50,7 +50,7 @@ async function printTests(files) { const { default: figures } = await import('figures') const { default: colors } = await import('chalk') - output.print(output.styles.debug(`Tests from ${global.codecept_dir}:`)) + output.print(output.styles.debug(`Tests from ${store.codeceptDir}:`)) output.print() const mocha = Container.mocha() diff --git a/lib/command/generate.js b/lib/command/generate.js index 6b9ad8dc3..431395b9f 100644 --- a/lib/command/generate.js +++ b/lib/command/generate.js @@ -5,6 +5,7 @@ import { mkdirp } from 'mkdirp' import path from 'path' import { fileExists, ucfirst, lcfirst, beautify } from '../utils.js' import output from '../output.js' +import store from '../store.js' import generateDefinitions from './definitions.js' import { getConfig, getTestRoot, safeFileWrite, readConfig } from './utils.js' @@ -20,6 +21,7 @@ Scenario('test something', async ({ {{actor}} }) => { // generates empty test export async function test(genPath) { const testsPath = getTestRoot(genPath) + store.codeceptDir = testsPath global.codecept_dir = testsPath const config = await getConfig(testsPath) if (!config) return diff --git a/lib/command/gherkin/snippets.js b/lib/command/gherkin/snippets.js index 612cb5cd5..cf832f5ef 100644 --- a/lib/command/gherkin/snippets.js +++ b/lib/command/gherkin/snippets.js @@ -8,6 +8,7 @@ import fsPath from 'path' import { getConfig, getTestRoot } from '../utils.js' import Codecept from '../../codecept.js' import output from '../../output.js' +import store from '../../store.js' import { matchStep } from '../../mocha/bdd.js' const uuidFn = IdGenerator.uuid() @@ -43,9 +44,9 @@ export default async function (genPath, options) { } const files = [] - globSync(options.feature || config.gherkin.features, { cwd: options.feature ? '.' : global.codecept_dir }).forEach(file => { + globSync(options.feature || config.gherkin.features, { cwd: options.feature ? '.' : store.codeceptDir }).forEach(file => { if (!fsPath.isAbsolute(file)) { - file = fsPath.join(global.codecept_dir, file) + file = fsPath.join(store.codeceptDir, file) } files.push(fsPath.resolve(file)) }) @@ -92,7 +93,7 @@ export default async function (genPath, options) { if (child.scenario.keyword === 'Scenario Outline') continue // skip scenario outline parseSteps(child.scenario.steps) .map(step => { - return Object.assign(step, { file: file.replace(global.codecept_dir, '').slice(1) }) + return Object.assign(step, { file: file.replace(store.codeceptDir, '').slice(1) }) }) .map(step => newSteps.set(`${step.type}(${step})`, step)) } @@ -107,7 +108,7 @@ export default async function (genPath, options) { } if (!fsPath.isAbsolute(stepFile)) { - stepFile = fsPath.join(global.codecept_dir, stepFile) + stepFile = fsPath.join(store.codeceptDir, stepFile) } const snippets = [...newSteps.values()] diff --git a/lib/command/init.js b/lib/command/init.js index 589ec51be..9e6295d10 100644 --- a/lib/command/init.js +++ b/lib/command/init.js @@ -19,6 +19,7 @@ const defaultConfig = { output: '', helpers: {}, include: {}, + noGlobals: true, plugins: { }, } diff --git a/lib/command/run-multiple.js b/lib/command/run-multiple.js index ce71744d0..6f28af9bc 100644 --- a/lib/command/run-multiple.js +++ b/lib/command/run-multiple.js @@ -8,6 +8,7 @@ import event from '../event.js' import { createRuns } from './run-multiple/collection.js' import { clearString, replaceValueDeep } from '../utils.js' import { getConfig, getTestRoot, fail } from './utils.js' +import store from '../store.js' const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) @@ -35,6 +36,7 @@ export default async function (selectedRuns, options) { const configFile = options.config const testRoot = getTestRoot(configFile) + store.codeceptDir = testRoot global.codecept_dir = testRoot // copy opts to run diff --git a/lib/command/run-workers.js b/lib/command/run-workers.js index 695a33365..b010a3128 100644 --- a/lib/command/run-workers.js +++ b/lib/command/run-workers.js @@ -41,6 +41,7 @@ export default async function (workerCount, selectedRuns, options) { output.print(`CodeceptJS v${Codecept.version()} ${output.standWithUkraine()}`) output.print(`Running tests in ${output.styles.bold(numberOfWorkers)} workers...`) store.hasWorkers = true + store.workerMode = true process.env.RUNS_WITH_WORKERS = 'true' const workers = new Workers(numberOfWorkers, config) diff --git a/lib/command/run.js b/lib/command/run.js index 9491bd218..3061aa8de 100644 --- a/lib/command/run.js +++ b/lib/command/run.js @@ -32,7 +32,7 @@ export default async function (test, options) { codecept.loadTests(test) if (options.verbose) { - global.debugMode = true + store.debugMode = true const { getMachineInfo } = await import('./info.js') await getMachineInfo() } diff --git a/lib/command/workers/runTests.js b/lib/command/workers/runTests.js index a9f0fd343..9a4e4f233 100644 --- a/lib/command/workers/runTests.js +++ b/lib/command/workers/runTests.js @@ -21,8 +21,8 @@ const { options, tests, testRoot, workerIndex, poolMode } = workerData // Global error handlers to catch critical errors but not test failures process.on('uncaughtException', (err) => { - if (global.container?.tsFileMapping && fixErrorStack) { - const fileMapping = global.container.tsFileMapping() + if (container?.tsFileMapping && fixErrorStack) { + const fileMapping = container.tsFileMapping() if (fileMapping) { fixErrorStack(err, fileMapping) } @@ -40,8 +40,8 @@ process.on('uncaughtException', (err) => { }) process.on('unhandledRejection', (reason, promise) => { - if (reason && typeof reason === 'object' && reason.stack && global.container?.tsFileMapping && fixErrorStack) { - const fileMapping = global.container.tsFileMapping() + if (reason && typeof reason === 'object' && reason.stack && container?.tsFileMapping && fixErrorStack) { + const fileMapping = container.tsFileMapping() if (fileMapping) { fixErrorStack(reason, fileMapping) } @@ -163,8 +163,8 @@ initPromise = (async function () { // IMPORTANT: await is required here since getConfig is async baseConfig = await getConfig(options.config || testRoot) } catch (configErr) { - if (global.container?.tsFileMapping && fixErrorStack) { - const fileMapping = global.container.tsFileMapping() + if (container?.tsFileMapping && fixErrorStack) { + const fileMapping = container.tsFileMapping() if (fileMapping) { fixErrorStack(configErr, fileMapping) } @@ -185,8 +185,8 @@ initPromise = (async function () { try { await codecept.init(testRoot) } catch (initErr) { - if (global.container?.tsFileMapping && fixErrorStack) { - const fileMapping = global.container.tsFileMapping() + if (container?.tsFileMapping && fixErrorStack) { + const fileMapping = container.tsFileMapping() if (fileMapping) { fixErrorStack(initErr, fileMapping) } @@ -218,8 +218,8 @@ initPromise = (async function () { parentPort?.close() } } catch (err) { - if (global.container?.tsFileMapping && fixErrorStack) { - const fileMapping = global.container.tsFileMapping() + if (container?.tsFileMapping && fixErrorStack) { + const fileMapping = container.tsFileMapping() if (fileMapping) { fixErrorStack(err, fileMapping) } diff --git a/lib/container.js b/lib/container.js index 6bf89ba92..2185226a6 100644 --- a/lib/container.js +++ b/lib/container.js @@ -183,7 +183,7 @@ class Container { * @api */ static tsFileMapping() { - return container.tsFileMapping + return store.tsFileMapping } /** @@ -426,11 +426,11 @@ async function requireHelperFromModule(helperName, config, HelperClass) { tempJsFile = allTempFiles fileMapping = mapping // Store file mapping in container for runtime error fixing (merge with existing) - if (!container.tsFileMapping) { - container.tsFileMapping = new Map() + if (!store.tsFileMapping) { + store.tsFileMapping = new Map() } for (const [key, value] of mapping.entries()) { - container.tsFileMapping.set(key, value) + store.tsFileMapping.set(key, value) } } catch (tsError) { throw new Error(`Failed to load TypeScript helper ${importPath}: ${tsError.message}. Make sure 'typescript' package is installed.`) @@ -672,7 +672,7 @@ async function createPlugins(config, options = {}) { continue } - if (!options.child && process.env.RUNS_WITH_WORKERS === 'true' && !runInParent) { + if (!options.child && store.workerMode && !runInParent) { continue } let module @@ -681,7 +681,7 @@ async function createPlugins(config, options = {}) { module = pluginConfig.require if (module.startsWith('.')) { // local - module = path.resolve(global.codecept_dir, module) // custom plugin + module = path.resolve(store.codeceptDir, module) // custom plugin } } else { module = `./plugin/${pluginName}.js` @@ -716,7 +716,7 @@ async function loadGherkinStepsAsync(paths) { bddModule.clearCurrentStepFile() } } else { - const folderPath = paths.startsWith('.') ? normalizeAndJoin(global.codecept_dir, paths) : '' + const folderPath = paths.startsWith('.') ? normalizeAndJoin(store.codeceptDir, paths) : '' if (folderPath !== '') { const files = globSync(folderPath) for (const file of files) { @@ -764,7 +764,7 @@ async function loadSupportObject(modulePath, supportObjectName) { } } if (typeof modulePath === 'string' && modulePath.charAt(0) === '.') { - modulePath = path.join(global.codecept_dir, modulePath) + modulePath = path.join(store.codeceptDir, modulePath) } try { // Use dynamic import for both ESM and CJS modules @@ -888,7 +888,7 @@ async function loadTranslation(locale, vocabularies) { const langs = await Translation.getLangs() if (langs[locale]) { translation = new Translation(langs[locale]) - } else if (fileExists(path.join(global.codecept_dir, locale))) { + } else if (fileExists(path.join(store.codeceptDir, locale))) { // get from a provided file instead translation = Translation.createDefault() translation.loadVocabulary(locale) @@ -905,7 +905,7 @@ function getHelperModuleName(helperName, config) { // classical require if (config[helperName].require) { if (config[helperName].require.startsWith('.')) { - let helperPath = path.resolve(global.codecept_dir, config[helperName].require) + let helperPath = path.resolve(store.codeceptDir, config[helperName].require) // Add .js extension if not present for ESM compatibility if (!path.extname(helperPath)) { helperPath += '.js' diff --git a/lib/globals.js b/lib/globals.js index cfbbdff26..3285eb523 100644 --- a/lib/globals.js +++ b/lib/globals.js @@ -8,19 +8,35 @@ import fsPath from 'path' import ActorFactory from './actor.js' import output from './output.js' import locator from './locator.js' +import store from './store.js' /** * Initialize CodeceptJS core globals * Called from Codecept.initGlobals() */ export async function initCodeceptGlobals(dir, config, container) { + store.initialize({ + codeceptDir: dir, + outputDir: fsPath.resolve(dir, config.output), + }) + + store.noGlobals = config.noGlobals || false + store.maskSensitiveData = config.maskSensitiveData || false + + // Keep globals for backward compat with external plugins global.codecept_dir = dir global.output_dir = fsPath.resolve(dir, config.output) if (config.noGlobals) return; - // Set up actor global - will use container when available + + output.print(output.styles.debug('Global functions are deprecated. Use `import { Helper, pause, within, session } from "codeceptjs"` instead. Set `noGlobals: true` in config to disable globals.')); + + const HelperModule = await import('@codeceptjs/helper') + global.Helper = global.codecept_helper = HelperModule.default || HelperModule + + // Set up actor global - will use container when available global.actor = global.codecept_actor = (obj) => { - return ActorFactory(obj, global.container || container) + return ActorFactory(obj, container) } global.Actor = global.actor @@ -52,8 +68,6 @@ export async function initCodeceptGlobals(dir, config, container) { const secretModule = await import('./secret.js') global.secret = secretModule.secret || (secretModule.default && secretModule.default.secret) - global.codecept_debug = output.debug - const codeceptjsModule = await import('./index.js') // load all objects global.codeceptjs = codeceptjsModule.default || codeceptjsModule @@ -65,9 +79,6 @@ export async function initCodeceptGlobals(dir, config, container) { global.Then = stepDefinitions.Then global.DefineParameterType = stepDefinitions.defineParameterType - // debug mode - global.debugMode = false - // mask sensitive data global.maskSensitiveData = config.maskSensitiveData || false @@ -78,6 +89,8 @@ export async function initCodeceptGlobals(dir, config, container) { * Called from mocha/ui.js pre-require event */ export function initMochaGlobals(context) { + if (store.noGlobals) return; + // Mocha test framework globals global.BeforeAll = context.BeforeAll global.AfterAll = context.AfterAll @@ -106,6 +119,8 @@ export function getGlobalNames() { return [ 'codecept_dir', 'output_dir', + 'Helper', + 'codecept_helper', 'actor', 'codecept_actor', 'Actor', @@ -117,13 +132,11 @@ export function getGlobalNames() { 'inject', 'share', 'secret', - 'codecept_debug', 'codeceptjs', 'Given', 'When', 'Then', 'DefineParameterType', - 'debugMode', 'maskSensitiveData', 'BeforeAll', 'AfterAll', @@ -136,6 +149,5 @@ export function getGlobalNames() { 'After', 'Scenario', 'xScenario', - 'container' ] } diff --git a/lib/heal.js b/lib/heal.js index 0d8816705..53933495e 100644 --- a/lib/heal.js +++ b/lib/heal.js @@ -4,6 +4,7 @@ import colors from 'chalk' import recorder from './recorder.js' import output from './output.js' import event from './event.js' +import container from './container.js' /** * @class @@ -69,7 +70,7 @@ class Heal { if (!prepareFn) continue if (context[property]) continue - context[property] = await prepareFn(global.inject()) + context[property] = await prepareFn(container.support()) } output.level(currentOutputLevel) @@ -116,10 +117,10 @@ class Heal { }) if (typeof codeSnippet === 'string') { - const I = global.container.support('I') + const I = container.support('I') await eval(codeSnippet) } else if (typeof codeSnippet === 'function') { - await codeSnippet(global.container.support()) + await codeSnippet(container.support()) } this.fixes.push({ diff --git a/lib/helper/ApiDataFactory.js b/lib/helper/ApiDataFactory.js index f959b7f8e..cda505e36 100644 --- a/lib/helper/ApiDataFactory.js +++ b/lib/helper/ApiDataFactory.js @@ -1,6 +1,7 @@ import path from 'path' import Helper from '@codeceptjs/helper' import REST from './REST.js' +import store from '../store.js' /** * Helper for managing remote data using REST API. @@ -324,7 +325,7 @@ class ApiDataFactory extends Helper { await import.meta.resolve(modulePath) } catch (e) { // If not found, try relative to codecept_dir - modulePath = path.join(global.codecept_dir, modulePath) + modulePath = path.join(store.codeceptDir, modulePath) } // check if the new syntax `export default new Factory()` is used and loads the builder, otherwise loads the module that used old syntax `module.exports = new Factory()`. const module = await import(modulePath) diff --git a/lib/helper/FileSystem.js b/lib/helper/FileSystem.js index 6aff1d2b7..109bdb3a8 100644 --- a/lib/helper/FileSystem.js +++ b/lib/helper/FileSystem.js @@ -4,6 +4,7 @@ import fs from 'fs' import Helper from '@codeceptjs/helper' import { fileExists } from '../utils.js' +import store from '../store.js' import { fileIncludes } from '../assert/include.js' import { fileEquals } from '../assert/equal.js' @@ -33,7 +34,7 @@ import { fileEquals } from '../assert/equal.js' class FileSystem extends Helper { constructor() { super() - this.dir = global.codecept_dir + this.dir = store.codeceptDir this.file = '' } @@ -52,7 +53,7 @@ class FileSystem extends Helper { * @param {string} openPath */ amInPath(openPath) { - this.dir = path.join(global.codecept_dir, openPath) + this.dir = path.join(store.codeceptDir, openPath) try { this.debugSection('Dir', this.dir) } catch (e) { diff --git a/lib/helper/GraphQLDataFactory.js b/lib/helper/GraphQLDataFactory.js index cc8fa9235..75dc5185f 100644 --- a/lib/helper/GraphQLDataFactory.js +++ b/lib/helper/GraphQLDataFactory.js @@ -2,6 +2,7 @@ import path from 'path' import HelperModule from '@codeceptjs/helper' import GraphQL from './GraphQL.js' +import store from '../store.js' /** * Helper for managing remote data using GraphQL queries. @@ -251,7 +252,7 @@ class GraphQLDataFactory extends Helper { try { require.resolve(modulePath) } catch (e) { - modulePath = path.join(global.codecept_dir, modulePath) + modulePath = path.join(store.codeceptDir, modulePath) } const builder = require(modulePath) return builder.build(data) diff --git a/lib/helper/Playwright.js b/lib/helper/Playwright.js index 862c3f342..1102327df 100644 --- a/lib/helper/Playwright.js +++ b/lib/helper/Playwright.js @@ -45,10 +45,6 @@ let playwright let perfTiming let defaultSelectorEnginesInitialized = false -// Use global object to track selector registration across workers -if (typeof global.__playwrightSelectorsRegistered === 'undefined') { - global.__playwrightSelectorsRegistered = false -} const popupStore = new Popup() const consoleLogStore = new Console() @@ -449,7 +445,7 @@ class Playwright extends Helper { this.options.recordVideo = { size } } if (this.options.recordVideo && !this.options.recordVideo.dir) { - this.options.recordVideo.dir = `${global.output_dir}/videos/` + this.options.recordVideo.dir = `${store.outputDir}/videos/` } this.isRemoteBrowser = !!this.playwrightOptions.browserWSEndpoint this.isElectron = this.options.browser === 'electron' @@ -511,18 +507,18 @@ class Playwright extends Helper { try { // Always wrap in try-catch since selectors might be registered globally across workers // Check global flag to avoid re-registration in worker processes - if (!global.__playwrightSelectorsRegistered) { + if (!defaultSelectorEnginesInitialized) { try { await playwright.selectors.register('__value', createValueEngine) await playwright.selectors.register('__disabled', createDisabledEngine) - global.__playwrightSelectorsRegistered = true + defaultSelectorEnginesInitialized = true defaultSelectorEnginesInitialized = true } catch (e) { if (!e.message.includes('already registered')) { throw e } // Selector already registered globally by another worker - global.__playwrightSelectorsRegistered = true + defaultSelectorEnginesInitialized = true defaultSelectorEnginesInitialized = true } } else { @@ -615,7 +611,7 @@ class Playwright extends Helper { if (this.options.recordVideo) contextOptions.recordVideo = this.options.recordVideo if (this.options.recordHar) { const harExt = this.options.recordHar.content && this.options.recordHar.content === 'attach' ? 'zip' : 'har' - const fileName = `${`${global.output_dir}${path.sep}har${path.sep}${uuidv4()}_${clearString(this.currentRunningTest.title)}`.slice(0, 245)}.${harExt}` + const fileName = `${`${store.outputDir}${path.sep}har${path.sep}${uuidv4()}_${clearString(this.currentRunningTest.title)}`.slice(0, 245)}.${harExt}` const dir = path.dirname(fileName) if (!fileExists(dir)) fs.mkdirSync(dir) this.options.recordHar.path = fileName @@ -1637,7 +1633,7 @@ class Playwright extends Helper { * @returns Promise */ async replayFromHar(harFilePath, opts) { - const file = path.join(global.codecept_dir, harFilePath) + const file = path.join(store.codeceptDir, harFilePath) if (!fileExists(file)) { throw new Error(`File at ${file} cannot be found on local system`) @@ -2048,7 +2044,7 @@ class Playwright extends Helper { const filePath = await download.path() fileName = fileName || `downloads/${path.basename(filePath)}` - const downloadPath = path.join(global.output_dir, fileName) + const downloadPath = path.join(store.outputDir, fileName) if (!fs.existsSync(path.dirname(downloadPath))) { fs.mkdirSync(path.dirname(downloadPath), '0777') } @@ -2347,7 +2343,7 @@ class Playwright extends Helper { * */ async attachFile(locator, pathToFile, context = null) { - const file = path.join(global.codecept_dir, pathToFile) + const file = path.join(store.codeceptDir, pathToFile) if (!fileExists(file)) { throw new Error(`File at ${file} can not be found on local system`) @@ -4823,7 +4819,7 @@ async function refreshContextSession() { function saveVideoForPage(page, name) { if (!page.video()) return null - const fileName = `${`${global.output_dir}${pathSeparator}videos${pathSeparator}${uuidv4()}_${clearString(name)}`.slice(0, 245)}.webm` + const fileName = `${`${store.outputDir}${pathSeparator}videos${pathSeparator}${uuidv4()}_${clearString(name)}`.slice(0, 245)}.webm` page .video() .saveAs(fileName) @@ -4840,7 +4836,7 @@ async function saveTraceForContext(context, name) { if (!context) return if (!context.tracing) return try { - const fileName = `${`${global.output_dir}${pathSeparator}trace${pathSeparator}${uuidv4()}_${clearString(name)}`.slice(0, 245)}.zip` + const fileName = `${`${store.outputDir}${pathSeparator}trace${pathSeparator}${uuidv4()}_${clearString(name)}`.slice(0, 245)}.zip` await context.tracing.stop({ path: fileName }) return fileName } catch (err) { diff --git a/lib/helper/Puppeteer.js b/lib/helper/Puppeteer.js index b99821220..26e6ddfa4 100644 --- a/lib/helper/Puppeteer.js +++ b/lib/helper/Puppeteer.js @@ -752,7 +752,7 @@ class Puppeteer extends Helper { } if (this.options.trace) { - const fileName = `${`${global.output_dir}${path.sep}trace${path.sep}${uuidv4()}_${clearString(this.currentRunningTest.title)}`.slice(0, 245)}.json` + const fileName = `${`${store.outputDir}${path.sep}trace${path.sep}${uuidv4()}_${clearString(this.currentRunningTest.title)}`.slice(0, 245)}.json` const dir = path.dirname(fileName) if (!fileExists(dir)) fs.mkdirSync(dir) await this.page.tracing.start({ screenshots: true, path: fileName }) @@ -1326,7 +1326,7 @@ class Puppeteer extends Helper { * @param {string} [downloadPath='downloads'] change this parameter to set another directory for saving */ async handleDownloads(downloadPath = 'downloads') { - downloadPath = path.join(global.output_dir, downloadPath) + downloadPath = path.join(store.outputDir, downloadPath) if (!fs.existsSync(downloadPath)) { fs.mkdirSync(downloadPath, '0777') } @@ -1388,7 +1388,7 @@ class Puppeteer extends Helper { }, }) - const outputFile = path.join(`${global.output_dir}/${fileName}`) + const outputFile = path.join(`${store.outputDir}/${fileName}`) try { await new Promise((resolve, reject) => { @@ -1656,7 +1656,7 @@ class Puppeteer extends Helper { * {{> attachFile }} */ async attachFile(locator, pathToFile, context = null) { - const file = path.join(global.codecept_dir, pathToFile) + const file = path.join(store.codeceptDir, pathToFile) if (!fileExists(file)) { throw new Error(`File at ${file} can not be found on local system`) @@ -3568,7 +3568,7 @@ function getNormalizedKey(key) { } function highlightActiveElement(element, context) { - if (this.options.highlightElement && global.debugMode) { + if (this.options.highlightElement && store.debugMode) { highlightElement(element, context) } } diff --git a/lib/helper/WebDriver.js b/lib/helper/WebDriver.js index 7560d30e3..05e1df908 100644 --- a/lib/helper/WebDriver.js +++ b/lib/helper/WebDriver.js @@ -1353,7 +1353,7 @@ class WebDriver extends Helper { * {{> attachFile }} */ async attachFile(locator, pathToFile, context = null) { - let file = path.join(global.codecept_dir, pathToFile) + let file = path.join(store.codeceptDir, pathToFile) if (!fileExists(file)) { throw new Error(`File at ${file} can not be found on local system`) } @@ -3484,7 +3484,7 @@ function isModifierKey(key) { } function highlightActiveElement(element) { - if (this.options.highlightElement && global.debugMode) { + if (this.options.highlightElement && store.debugMode) { highlightElement(element, this.browser) } } diff --git a/lib/helper/extras/Download.js b/lib/helper/extras/Download.js new file mode 100644 index 000000000..f0b7329cf --- /dev/null +++ b/lib/helper/extras/Download.js @@ -0,0 +1,45 @@ +import fs from 'fs' +import path from 'path' +import minimatch from 'minimatch' +import store from '../../store.js' +import assert from 'assert' + +function getDownloadDir() { + return path.join(store.outputDir, 'downloads') +} + +function getNewFiles(downloadDir, sinceTimestamp) { + if (!fs.existsSync(downloadDir)) return [] + return fs.readdirSync(downloadDir).filter(name => { + const stat = fs.statSync(path.join(downloadDir, name)) + return stat.isFile() && stat.mtimeMs >= sinceTimestamp + }) +} + +function seeFileDownloaded(arg) { + const downloadDir = getDownloadDir() + const files = getNewFiles(downloadDir, this._downloadStartTimestamp) + + if (arg === undefined || arg === null) { + assert.ok(files.length > 0, `No files downloaded to ${downloadDir}`) + return + } + if (typeof arg === 'number') { + assert.strictEqual(files.length, arg, `Expected ${arg} downloaded file(s), found ${files.length}: [${files.join(', ')}]`) + return + } + const regexMatch = arg.match(/^\/(.+)\/$/) + if (regexMatch) { + const re = new RegExp(regexMatch[1]) + assert.ok(files.some(f => re.test(f)), `No file matches ${arg}. Downloaded: [${files.join(', ')}]`) + return + } + if (/[*?[\]]/.test(arg)) { + const matched = minimatch.match(files, arg) + assert.ok(matched.length > 0, `No file matches glob "${arg}". Downloaded: [${files.join(', ')}]`) + return + } + assert.ok(files.includes(arg), `File "${arg}" not downloaded. Downloaded: [${files.join(', ')}]`) +} + +export { seeFileDownloaded, getDownloadDir } diff --git a/lib/history.js b/lib/history.js index 326f0c1a1..dc72cb916 100644 --- a/lib/history.js +++ b/lib/history.js @@ -2,6 +2,7 @@ import colors from 'chalk' import fs from 'fs' import path from 'path' import output from './output.js' +import store from './store.js' /** * REPL history records REPL commands and stores them in @@ -9,8 +10,8 @@ import output from './output.js' */ class ReplHistory { constructor() { - if (global.output_dir) { - this.historyFile = path.join(global.output_dir, 'cli-history') + if (store.outputDir) { + this.historyFile = path.join(store.outputDir, 'cli-history') } this.commands = [] } diff --git a/lib/listener/config.js b/lib/listener/config.js index d8f5ab4bd..4c58c27de 100644 --- a/lib/listener/config.js +++ b/lib/listener/config.js @@ -2,16 +2,18 @@ import event from '../event.js' import recorder from '../recorder.js' import { deepMerge, deepClone, ucfirst } from '../utils.js' import output from '../output.js' +import container from '../container.js' /** * Enable Helpers to listen to test events */ +let initialized = false + export default function () { - // Use global flag to prevent duplicate initialization across module re-imports - if (global.__codeceptConfigListenerInitialized) { + if (initialized) { return } - global.__codeceptConfigListenerInitialized = true + initialized = true enableDynamicConfigFor('suite') enableDynamicConfigFor('test') @@ -20,7 +22,7 @@ export default function () { 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() + const helpers = container.helpers() function updateHelperConfig(helper, config) { // Guard against undefined or invalid helpers diff --git a/lib/listener/emptyRun.js b/lib/listener/emptyRun.js index 76d1991de..132b866f8 100644 --- a/lib/listener/emptyRun.js +++ b/lib/listener/emptyRun.js @@ -1,6 +1,7 @@ import figures from 'figures' import event from '../event.js' import output from '../output.js' +import container from '../container.js' import { searchWithFusejs } from '../utils.js' export default function () { @@ -12,7 +13,7 @@ export default function () { event.dispatcher.on(event.all.result, () => { if (isEmptyRun) { - const mocha = global.container.mocha() + const mocha = container.mocha() if (mocha.options.grep) { output.print() diff --git a/lib/listener/helpers.js b/lib/listener/helpers.js index 50256d392..50a4162a0 100644 --- a/lib/listener/helpers.js +++ b/lib/listener/helpers.js @@ -3,11 +3,12 @@ import event from '../event.js' import recorder from '../recorder.js' import store from '../store.js' import output from '../output.js' +import container from '../container.js' /** * Enable Helpers to listen to test events */ export default function () { - const helpers = global.container.helpers() + const helpers = container.helpers() const runHelpersHook = (hook, param) => { if (store.dryRun) return diff --git a/lib/listener/mocha.js b/lib/listener/mocha.js index f4ae91f6f..ed0128de4 100644 --- a/lib/listener/mocha.js +++ b/lib/listener/mocha.js @@ -1,10 +1,11 @@ import event from '../event.js' +import container from '../container.js' export default function () { let mocha event.dispatcher.on(event.all.before, () => { - mocha = global.container.mocha() + mocha = container.mocha() }) event.dispatcher.on(event.test.passed, test => { diff --git a/lib/listener/result.js b/lib/listener/result.js index 19532b5d1..acd74bfaa 100644 --- a/lib/listener/result.js +++ b/lib/listener/result.js @@ -1,11 +1,12 @@ import event from '../event.js' +import container from '../container.js' export default function () { event.dispatcher.on(event.hook.failed, err => { - global.container.result().addStats({ failedHooks: 1 }) + container.result().addStats({ failedHooks: 1 }) }) event.dispatcher.on(event.test.before, test => { - global.container.result().addTest(test) + container.result().addTest(test) }) } diff --git a/lib/mocha/cli.js b/lib/mocha/cli.js index bb08e0caa..56b637922 100644 --- a/lib/mocha/cli.js +++ b/lib/mocha/cli.js @@ -8,6 +8,7 @@ import { dirname, join } from 'path' import event from '../event.js' import AssertionFailedError from '../assert/error.js' import output from '../output.js' +import store from '../store.js' import test, { cloneTest } from './test.js' import { fixErrorStack } from '../utils/typescript.js' @@ -41,7 +42,7 @@ class Cli extends Base { if (opts.verbose) level = 3 output.level(level) output.print(`CodeceptJS v${codeceptVersion} ${output.standWithUkraine()}`) - output.print(`Using test root "${global.codecept_dir}"`) + output.print(`Using test root "${store.codeceptDir}"`) const showSteps = level >= 1 @@ -213,6 +214,7 @@ class Cli extends Base { } // append step traces + const Container = await getContainer() this.failures = this.failures.map(test => { // we will change the stack trace, so we need to clone the test const err = test.err @@ -275,7 +277,7 @@ class Cli extends Base { } try { - const fileMapping = global.container?.tsFileMapping?.() + const fileMapping = Container?.tsFileMapping?.() if (fileMapping) { fixErrorStack(err, fileMapping) } diff --git a/lib/mocha/factory.js b/lib/mocha/factory.js index c7f2d96de..2b8359b8d 100644 --- a/lib/mocha/factory.js +++ b/lib/mocha/factory.js @@ -8,6 +8,7 @@ import output from '../output.js' import scenarioUiFunction from './ui.js' import { initMochaGlobals } from '../globals.js' import { fixErrorStack } from '../utils/typescript.js' +import container from '../container.js' const __filename = fileURLToPath(import.meta.url) const __dirname = fsPath.dirname(__filename) @@ -35,7 +36,7 @@ class MochaFactory { // Handle ECONNREFUSED without dynamic import for now err = new Error('Connection refused: ' + err.toString()) } - const fileMapping = global.container?.tsFileMapping?.() + const fileMapping = container?.tsFileMapping?.() if (fileMapping) { fixErrorStack(err, fileMapping) } diff --git a/lib/mocha/scenarioConfig.js b/lib/mocha/scenarioConfig.js index e06d208a6..23f54ffcd 100644 --- a/lib/mocha/scenarioConfig.js +++ b/lib/mocha/scenarioConfig.js @@ -1,4 +1,5 @@ import { isAsyncFunction } from '../utils.js' +import store from '../store.js' /** @class */ class ScenarioConfig { @@ -40,7 +41,7 @@ class ScenarioConfig { * @returns {this} */ retry(retries) { - if (process.env.SCENARIO_ONLY) retries = -retries + if (store.scenarioOnly) retries = -retries this.test.retries(retries) return this } diff --git a/lib/mocha/ui.js b/lib/mocha/ui.js index 86bcbb030..b10c8fba9 100644 --- a/lib/mocha/ui.js +++ b/lib/mocha/ui.js @@ -9,13 +9,12 @@ import { HookConfig, AfterSuiteHook, AfterHook, BeforeSuiteHook, BeforeHook } fr import { initMochaGlobals } from '../globals.js' import common from 'mocha/lib/interfaces/common.js' import container from '../container.js' +import store from '../store.js' const setContextTranslation = context => { - // Try global container first, then local container instance - const containerToUse = global.container || container - if (!containerToUse) return + if (!container) return - const translation = containerToUse.translation?.() || containerToUse.translation + const translation = container.translation?.() || container.translation const contexts = translation?.value?.('contexts') if (contexts) { @@ -119,7 +118,7 @@ export default function (suite) { context.Feature.only = function (title, opts) { const reString = `^${escapeRe(`${title}:`)}` mocha.grep(new RegExp(reString)) - process.env.FEATURE_ONLY = true + store.featureOnly = true return context.Feature(title, opts) } @@ -171,7 +170,7 @@ export default function (suite) { context.Scenario.only = function (title, opts, fn) { const reString = `^${escapeRe(`${suites[0].title}: ${title}`.replace(/( \| {.+})?$/g, ''))}` mocha.grep(new RegExp(reString)) - process.env.SCENARIO_ONLY = true + store.scenarioOnly = true return addScenario(title, opts, fn) } diff --git a/lib/plugin/aiTrace.js b/lib/plugin/aiTrace.js index 8129ee9c9..e9ad15ed4 100644 --- a/lib/plugin/aiTrace.js +++ b/lib/plugin/aiTrace.js @@ -4,6 +4,7 @@ import { mkdirp } from 'mkdirp' import path from 'path' import { fileURLToPath } from 'url' +import store from '../store.js' import Container from '../container.js' import recorder from '../recorder.js' import event from '../event.js' @@ -16,7 +17,7 @@ const supportedHelpers = Container.STANDARD_ACTING_HELPERS const defaultConfig = { deleteSuccessful: false, fullPageScreenshots: false, - output: global.output_dir, + output: store.outputDir, captureHTML: true, captureARIA: true, captureBrowserLogs: true, @@ -84,7 +85,7 @@ export default function (config) { let testFailed = false let firstFailedStepSaved = false - const reportDir = config.output ? path.resolve(global.codecept_dir, config.output) : defaultConfig.output + const reportDir = config.output ? path.resolve(store.codeceptDir, config.output) : defaultConfig.output if (config.captureDebugOutput) { const originalDebug = output.debug diff --git a/lib/plugin/analyze.js b/lib/plugin/analyze.js index 6cd3ae521..b6feb7448 100644 --- a/lib/plugin/analyze.js +++ b/lib/plugin/analyze.js @@ -353,7 +353,7 @@ function serializeError(error) { errorMessage += '\n' + error.stack - .replace(global.codecept_dir || '', '.') + .replace(store.codeceptDir || '', '.') .split('\n') .map(line => line.replace(ansiRegExp(), '')) .slice(0, 5) diff --git a/lib/plugin/auth.js b/lib/plugin/auth.js index a5e39d183..7e89c0635 100644 --- a/lib/plugin/auth.js +++ b/lib/plugin/auth.js @@ -321,7 +321,7 @@ export default function (config) { } if (config.saveToFile) { output.debug(`Saved user session into file for ${name}`) - fs.writeFileSync(path.join(global.output_dir, `${name}_session.json`), JSON.stringify(cookies)) + fs.writeFileSync(path.join(store.outputDir, `${name}_session.json`), JSON.stringify(cookies)) } store[`${name}_session`] = cookies } @@ -377,7 +377,7 @@ export default function (config) { } if (!config.saveToFile) return - const cookieFile = path.join(global.output_dir, `${name}_session.json`) + const cookieFile = path.join(store.outputDir, `${name}_session.json`) if (!fileExists(cookieFile)) { return @@ -412,7 +412,7 @@ export default function (config) { function loadCookiesFromFile(config) { for (const name in config.users) { - const fileName = path.join(global.output_dir, `${name}_session.json`) + const fileName = path.join(store.outputDir, `${name}_session.json`) if (!fileExists(fileName)) continue const data = fs.readFileSync(fileName).toString() try { diff --git a/lib/plugin/pageInfo.js b/lib/plugin/pageInfo.js index 76e3e88d6..7106738e4 100644 --- a/lib/plugin/pageInfo.js +++ b/lib/plugin/pageInfo.js @@ -6,6 +6,7 @@ import recorder from '../recorder.js' import event from '../event.js' import { scanForErrorMessages } from '../html.js' import { output } from '../index.js' +import store from '../store.js' import { humanizeString, ucfirst } from '../utils.js' import { testToFileName } from '../mocha/test.js' const defaultConfig = { @@ -92,7 +93,7 @@ export default function (config = {}) { recorder.add('Save page info', () => { test.addNote('pageInfo', pageStateToMarkdown(pageState)) - const pageStateFileName = path.join(global.output_dir, `${testToFileName(test)}.pageInfo.md`) + const pageStateFileName = path.join(store.outputDir, `${testToFileName(test)}.pageInfo.md`) fs.writeFileSync(pageStateFileName, pageStateToMarkdown(pageState)) test.artifacts.pageInfo = pageStateFileName return pageState diff --git a/lib/plugin/screenshotOnFail.js b/lib/plugin/screenshotOnFail.js index fa427e035..57c181d0f 100644 --- a/lib/plugin/screenshotOnFail.js +++ b/lib/plugin/screenshotOnFail.js @@ -8,6 +8,7 @@ import recorder from '../recorder.js' import event from '../event.js' import output from '../output.js' +import store from '../store.js' import { fileExists } from '../utils.js' import Codeceptjs from '../index.js' @@ -94,7 +95,7 @@ export default function (config) { } else { fileName = `${testToFileName(test, { suffix: '', unique: false })}.failed.png` } - const quietMode = !('output_dir' in global) || !global.output_dir + const quietMode = !store.outputDir if (!quietMode) { output.plugin('screenshotOnFail', 'Test failed, try to save a screenshot') } @@ -139,9 +140,7 @@ export default function (config) { await Promise.race([screenshotPromise, timeoutPromise]) if (!test.artifacts) test.artifacts = {} - // Some unit tests may not define global.output_dir; avoid throwing when it is undefined - // Detect output directory safely (may not be initialized in narrow unit tests) - const baseOutputDir = 'output_dir' in global && typeof global.output_dir === 'string' && global.output_dir ? global.output_dir : null + const baseOutputDir = store.outputDir || null if (baseOutputDir) { test.artifacts.screenshot = path.join(baseOutputDir, fileName) if (Container.mocha().options.reporterOptions['mocha-junit-reporter'] && Container.mocha().options.reporterOptions['mocha-junit-reporter'].options.attachments) { diff --git a/lib/plugin/stepByStepReport.js b/lib/plugin/stepByStepReport.js index ee1cb52ec..1fb0cd860 100644 --- a/lib/plugin/stepByStepReport.js +++ b/lib/plugin/stepByStepReport.js @@ -6,6 +6,7 @@ import { mkdirp } from 'mkdirp' import path from 'path' import cheerio from 'cheerio' +import store from '../store.js' import Container from '../container.js' import recorder from '../recorder.js' import event from '../event.js' @@ -19,7 +20,7 @@ const defaultConfig = { animateSlides: true, ignoreSteps: [], fullPageScreenshots: false, - output: global.output_dir, + output: store.outputDir, screenshotsForAllureReport: false, disableScreenshotOnFail: true, } @@ -87,7 +88,7 @@ export default function (config) { const recordedTests = {} const pad = '0000' - const reportDir = config.output ? path.resolve(global.codecept_dir, config.output) : defaultConfig.output + const reportDir = config.output ? path.resolve(store.codeceptDir, config.output) : defaultConfig.output event.dispatcher.on(event.suite.before, suite => { stepNum = -1 diff --git a/lib/rerun.js b/lib/rerun.js index 37632f684..9fb292e50 100644 --- a/lib/rerun.js +++ b/lib/rerun.js @@ -1,4 +1,5 @@ import fsPath from 'path' +import store from './store.js' import container from './container.js' import event from './event.js' import BaseCodecept from './codecept.js' @@ -29,7 +30,7 @@ class CodeceptRerunner extends BaseCodecept { let filesToRun = this.testFiles if (test) { if (!fsPath.isAbsolute(test)) { - test = fsPath.join(global.codecept_dir, test) + test = fsPath.join(store.codeceptDir, test) } filesToRun = this.testFiles.filter(t => fsPath.basename(t, '.js') === test || t === test) } diff --git a/lib/result.js b/lib/result.js index 2f9985487..c7ff4f587 100644 --- a/lib/result.js +++ b/lib/result.js @@ -1,6 +1,7 @@ import fs from 'fs' import path from 'path' import { serializeTest } from './mocha/test.js' +import store from './store.js' /** * @typedef {Object} Stats Statistics for a test result. @@ -212,7 +213,7 @@ class Result { */ save(fileName) { if (!fileName) fileName = 'result.json' - fs.writeFileSync(path.join(global.output_dir, fileName), JSON.stringify(this.simplify(), null, 2)) + fs.writeFileSync(path.join(store.outputDir, fileName), JSON.stringify(this.simplify(), null, 2)) } /** diff --git a/lib/step/base.js b/lib/step/base.js index 1000c048e..4d0e415ce 100644 --- a/lib/step/base.js +++ b/lib/step/base.js @@ -3,6 +3,7 @@ import Secret from '../secret.js' import { getCurrentTimeout } from '../timeout.js' import { ucfirst, humanizeString, serializeError } from '../utils.js' import recordStep from './record.js' +import store from '../store.js' const STACK_LINE = 5 @@ -148,11 +149,11 @@ class Step { const lines = this.stack.split('\n') if (lines[STACK_LINE]) { let line = lines[STACK_LINE].trim() - .replace(global.codecept_dir || '', '.') + .replace(store.codeceptDir || '', '.') .trim() // Map .temp.mjs back to original .ts files using container's tsFileMapping - const fileMapping = global.container?.tsFileMapping?.() + const fileMapping = store.tsFileMapping if (line.includes('.temp.mjs') && fileMapping) { for (const [tsFile, mjsFile] of fileMapping.entries()) { if (line.includes(mjsFile)) { diff --git a/lib/step/record.js b/lib/step/record.js index 38124850e..0a7d82c6d 100644 --- a/lib/step/record.js +++ b/lib/step/record.js @@ -63,7 +63,7 @@ function recordStep(step, args) { step.endTime = +Date.now() // Fix error stack to point to original .ts files (lazy import to avoid circular dependency) - const fileMapping = global.container?.tsFileMapping?.() + const fileMapping = store.tsFileMapping if (fileMapping) { fixErrorStack(err, fileMapping) } diff --git a/lib/store.js b/lib/store.js index 47015eb8b..a6472cf8c 100644 --- a/lib/store.js +++ b/lib/store.js @@ -1,8 +1,34 @@ /** - * global values for current session + * Global store for current session * @namespace */ const store = { + // --- Required (set once via initialize(), immutable after) --- + + /** @type {string | null} */ + _codeceptDir: null, + /** @type {string | null} */ + _outputDir: null, + + get codeceptDir() { + return this._codeceptDir || global.codecept_dir || null + }, + set codeceptDir(val) { + this._codeceptDir = val + }, + + get outputDir() { + return this._outputDir || global.output_dir || null + }, + set outputDir(val) { + this._outputDir = val + }, + + /** @type {boolean} */ + workerMode: false, + + // --- Session config (per-session, mutable, set at session start) --- + /** * If we are in --debug mode * @type {boolean} @@ -27,20 +53,63 @@ const store = { * @type {boolean} */ dryRun: false, + + /** + * Feature.only() was used + * @type {boolean} + */ + featureOnly: false, + + /** + * Scenario.only() was used + * @type {boolean} + */ + scenarioOnly: false, + + /** + * Mask sensitive data config + * @type {boolean|object} + */ + maskSensitiveData: false, + + /** + * noGlobals mode — user imports everything + * @type {boolean} + */ + noGlobals: false, + + // --- State (tracks current execution, changes constantly) --- + /** * If we are in pause mode * @type {boolean} */ onPause: false, - // current object states - /** @type {CodeceptJS.Test | null} */ currentTest: null, /** @type {CodeceptJS.Step | null} */ currentStep: null, /** @type {CodeceptJS.Suite | null} */ currentSuite: null, + + /** @type {Map | null} */ + tsFileMapping: null, + + /** + * Initialize required store fields. + * These values cannot be overwritten after initialization. + * @param {object} opts + * @param {string} opts.codeceptDir - root directory of tests + * @param {string} opts.outputDir - resolved output directory + */ + initialize(opts) { + if (!opts.codeceptDir) throw new Error('codeceptDir is required') + if (!opts.outputDir) throw new Error('outputDir is required') + + this._codeceptDir = opts.codeceptDir + this._outputDir = opts.outputDir + }, } export default store diff --git a/lib/translation.js b/lib/translation.js index 2f617da1e..5afc90cfc 100644 --- a/lib/translation.js +++ b/lib/translation.js @@ -1,6 +1,7 @@ import merge from 'lodash.merge' import path from 'path' import { createRequire } from 'module' +import store from './store.js' const defaultVocabulary = { I: 'I', @@ -15,7 +16,7 @@ class Translation { loadVocabulary(vocabularyFile) { if (!vocabularyFile) return - const filePath = path.join(global.codecept_dir, vocabularyFile) + const filePath = path.join(store.codeceptDir, vocabularyFile) try { const require = createRequire(import.meta.url) diff --git a/lib/utils.js b/lib/utils.js index ac0d5d563..e606c59cb 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -7,6 +7,7 @@ import getFunctionArguments from 'fn-args' import deepClone from 'lodash.clonedeep' import merge from 'lodash.merge' import { convertColorToRGBA, isColorProperty } from './colorUtils.js' +import store from './store.js' import Fuse from 'fuse.js' import crypto from 'crypto' import jsBeautify from 'js-beautify' @@ -335,13 +336,13 @@ export const screenshotOutputFolder = function (fileName) { const fileSep = path.sep if (!fileName.includes(fileSep) || fileName.includes('record_')) { - return path.resolve(global.output_dir, fileName) + return path.resolve(store.outputDir, fileName) } - return path.resolve(global.codecept_dir, fileName) + return path.resolve(store.codeceptDir, fileName) } export const relativeDir = function (fileName) { - return fileName.replace(global.codecept_dir, '').replace(/^\//, '') + return fileName.replace(store.codeceptDir, '').replace(/^\//, '') } export const beautify = function (code) { diff --git a/lib/utils/mask_data.js b/lib/utils/mask_data.js index 455c8c3b3..89943c22c 100644 --- a/lib/utils/mask_data.js +++ b/lib/utils/mask_data.js @@ -1,4 +1,5 @@ import { maskSensitiveData } from 'invisi-data' +import store from '../store.js' /** * Mask sensitive data utility for CodeceptJS @@ -33,7 +34,7 @@ export function maskData(input, config) { * @returns {boolean|object} - Current masking configuration */ export function getMaskConfig() { - return global.maskSensitiveData || false + return store.maskSensitiveData || global.maskSensitiveData || false } /** diff --git a/lib/workers.js b/lib/workers.js index 367939e90..d97304a69 100644 --- a/lib/workers.js +++ b/lib/workers.js @@ -20,6 +20,7 @@ import event from './event.js' import { deserializeTest } from './mocha/test.js' import { deserializeSuite } from './mocha/suite.js' import recorder from './recorder.js' +import store from './store.js' import runHook from './hooks.js' import WorkerStorage from './workerStorage.js' import { createRuns } from './command/run-multiple/collection.js' @@ -504,6 +505,7 @@ class Workers extends EventEmitter { await this._ensureInitialized() recorder.startUnlessRunning() event.dispatcher.emit(event.workers.before) + store.workerMode = true process.env.RUNS_WITH_WORKERS = 'true' // Create workers and set up message handlers immediately (not in recorder queue) diff --git a/test/unit/mocha/ui_test.js b/test/unit/mocha/ui_test.js index 44a11dd77..33fc40a5e 100644 --- a/test/unit/mocha/ui_test.js +++ b/test/unit/mocha/ui_test.js @@ -5,6 +5,7 @@ import { createTest } from '../../../lib/mocha/test.js' import codeceptjs from '../../../lib/index.js' import makeUI from '../../../lib/mocha/ui.js' import container from '../../../lib/container.js' +import store from '../../../lib/store.js' global.codeceptjs = codeceptjs @@ -145,7 +146,7 @@ describe('ui', () => { } // Reset environment variable - delete process.env.FEATURE_ONLY + store.featureOnly = false // Re-emit pre-require with our mocked mocha instance suite.emit('pre-require', context, {}, mocha) @@ -157,7 +158,7 @@ describe('ui', () => { expect(suiteConfig.suite.pending).eq(false, 'Feature.only must not be pending') expect(grepPattern).to.be.instanceOf(RegExp) expect(grepPattern.source).eq('^exclusive feature:') - expect(process.env.FEATURE_ONLY).eq('true', 'FEATURE_ONLY environment variable should be set') + expect(store.featureOnly).eq(true, 'store.featureOnly should be set') // Restore original grep mocha.grep = originalGrep @@ -176,7 +177,7 @@ describe('ui', () => { } // Reset environment variable - delete process.env.FEATURE_ONLY + store.featureOnly = false // Re-emit pre-require with our mocked mocha instance suite.emit('pre-require', context, {}, mocha) @@ -188,7 +189,7 @@ describe('ui', () => { expect(suiteConfig.suite.pending).eq(false, 'Feature.only must not be pending') expect(grepPattern).to.be.instanceOf(RegExp) expect(grepPattern.source).eq('^exclusive feature without options:') - expect(process.env.FEATURE_ONLY).eq('true', 'FEATURE_ONLY environment variable should be set') + expect(store.featureOnly).eq(true, 'store.featureOnly should be set') // Restore original grep mocha.grep = originalGrep diff --git a/test/unit/utils_test.js b/test/unit/utils_test.js index 146804a68..3bd30e393 100644 --- a/test/unit/utils_test.js +++ b/test/unit/utils_test.js @@ -4,6 +4,7 @@ import path from 'path' import { fileURLToPath } from 'url' import sinon from 'sinon' import * as utils from '../../lib/utils.js' +import store from '../../lib/store.js' import playwright from 'playwright' const __filename = fileURLToPath(import.meta.url) @@ -302,20 +303,20 @@ describe('utils', () => { }) describe('#screenshotOutputFolder', () => { - let _oldGlobalOutputDir - let _oldGlobalCodeceptDir + let _oldOutputDir + let _oldCodeceptDir before(() => { - _oldGlobalOutputDir = global.output_dir - _oldGlobalCodeceptDir = global.codecept_dir + _oldOutputDir = store.outputDir + _oldCodeceptDir = store.codeceptDir - global.output_dir = '/Users/someuser/workbase/project1/test_output' - global.codecept_dir = '/Users/someuser/workbase/project1/tests/e2e' + store.outputDir = '/Users/someuser/workbase/project1/test_output' + store.codeceptDir = '/Users/someuser/workbase/project1/tests/e2e' }) after(() => { - global.output_dir = _oldGlobalOutputDir - global.codecept_dir = _oldGlobalCodeceptDir + store.outputDir = _oldOutputDir + store.codeceptDir = _oldCodeceptDir }) it('returns the joined filename for filename only', () => { @@ -326,7 +327,7 @@ describe('utils', () => { it('returns the given filename for absolute one', () => { const _path = utils.screenshotOutputFolder('/Users/someuser/workbase/project1/test_output/screenshot1.failed.png'.replace(/\//g, path.sep)) if (os.platform() === 'win32') { - expect(_path).eql(path.resolve(global.codecept_dir, '/Users/someuser/workbase/project1/test_output/screenshot1.failed.png')) + expect(_path).eql(path.resolve(store.codeceptDir, '/Users/someuser/workbase/project1/test_output/screenshot1.failed.png')) } else { expect(_path).eql('/Users/someuser/workbase/project1/test_output/screenshot1.failed.png') }