Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:

strategy:
matrix:
node-version: [20.x]
node-version: [26.x]

steps:
- uses: actions/checkout@v6
Expand Down Expand Up @@ -53,6 +53,27 @@ jobs:
- name: Stop mock server
run: npm run mock-server:stop

unit-tests-windows:
name: Unit tests
runs-on: windows-latest
timeout-minutes: 15

strategy:
matrix:
node-version: [ 20.x ]

steps:
- uses: actions/checkout@v6
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
- run: npm i
env:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: true
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true
- run: npm run test:unit:windows

runner-tests:
name: Runner tests
runs-on: ubuntu-22.04
Expand Down
4 changes: 3 additions & 1 deletion bin/codecept.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Codecept from '../lib/codecept.js'
import output from '../lib/output.js'
const { print, error } = output
import { printError } from '../lib/command/utils.js'
import { resolveImportModulePath } from '../lib/utils.js'

const commandFlags = {
ai: {
Expand Down Expand Up @@ -45,7 +46,8 @@ const errorHandler =
}

const dynamicImport = async modulePath => {
const module = await import(modulePath)
const resolvedPath = resolveImportModulePath(modulePath)
const module = await import(resolvedPath)
return module.default || module
}

Expand Down
5 changes: 3 additions & 2 deletions lib/ai.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { removeNonInteractiveElements, minifyHtml, splitByChunks } from './html.
import { generateText } from 'ai'
import { fileURLToPath } from 'url'
import path from 'path'
import { fileExists } from './utils.js'
import { fileExists, resolveImportModulePath } from './utils.js'
import store from './store.js'

const __dirname = path.dirname(fileURLToPath(import.meta.url))
Expand Down Expand Up @@ -34,7 +34,8 @@ async function loadPrompts() {
}

try {
const module = await import(promptPath)
const resolvedPath = resolveImportModulePath(promptPath)
const module = await import(resolvedPath)
prompts[name] = module.default || module
debug(`Loaded prompt ${name} from ${promptPath}`)
} catch (err) {
Expand Down
10 changes: 6 additions & 4 deletions lib/codecept.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import event from './event.js'
import runHook from './hooks.js'
import ActorFactory from './actor.js'
import output from './output.js'
import { emptyFolder } from './utils.js'
import { emptyFolder, resolveImportModulePath } from './utils.js'
import { initCodeceptGlobals } from './globals.js'
import { validateTypeScriptSetup, getTSNodeESMWarning } from './utils/loaderCheck.js'
import recorder from './recorder.js'
Expand Down Expand Up @@ -73,7 +73,7 @@ class Codecept {
// For npm packages, resolve from the user's directory
// This ensures packages like tsx are found in user's node_modules
const userDir = store.codeceptDir || process.cwd()

try {
// Use createRequire to resolve from user's directory
const userRequire = createRequire(pathToFileURL(resolve(userDir, 'package.json')).href)
Expand All @@ -86,7 +86,8 @@ class Codecept {
}
}
// Use dynamic import for ESM
await import(modulePath)
const resolvedPath = resolveImportModulePath(modulePath)
await import(resolvedPath)
}
}
}
Expand Down Expand Up @@ -137,7 +138,8 @@ class Codecept {
]

for (const modulePath of listenerModules) {
const module = await import(modulePath)
const resolvedPath = resolveImportModulePath(modulePath)
const module = await import(resolvedPath)
runHook(module.default || module)
}
}
Expand Down
1 change: 1 addition & 0 deletions lib/command/run-rerun.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Codecept from '../rerun.js'
export default async function (test, options) {
// registering options globally to use in config
// Backward compatibility for --profile

process.profile = options.profile
process.env.profile = options.profile
const configFile = options.config
Expand Down
12 changes: 7 additions & 5 deletions lib/config.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import fs from 'fs'
import path from 'path'
import { createRequire } from 'module'
import { fileExists, isFile, deepMerge, deepClone } from './utils.js'
import { fileExists, isFile, deepMerge, deepClone, resolveImportModulePath } from './utils.js'
import { transpileTypeScript, cleanupTempFiles, fixErrorStack } from './utils/typescript.js'

const defaultConfig = {
Expand Down Expand Up @@ -96,7 +96,7 @@ class Config {
// Try different extensions if the file doesn't exist
const extensions = ['.ts', '.cjs', '.mjs']
let found = false

for (const ext of extensions) {
const altConfig = configFile.replace(/\.js$/, ext)
if (fileExists(altConfig)) {
Expand All @@ -105,7 +105,7 @@ class Config {
break
}
}

if (!found) {
throw new Error(`Config file ${configFile} does not exist. Execute 'codeceptjs init' to create config`)
}
Expand Down Expand Up @@ -242,7 +242,8 @@ async function loadConfigFile(configFile) {
allTempFiles = result.allTempFiles
fileMapping = result.fileMapping

configModule = await import(tempFile)
const resolvedPath = resolveImportModulePath(tempFile)
configModule = await import(resolvedPath)
cleanupTempFiles(allTempFiles)
} catch (err) {
transpileError = err
Expand All @@ -258,7 +259,8 @@ async function loadConfigFile(configFile) {
}
} else {
// Try ESM import first for JS files
configModule = await import(configFile)
const resolvedPath = resolveImportModulePath(configFile)
configModule = await import(resolvedPath)
}
} catch (importError) {
try {
Expand Down
31 changes: 21 additions & 10 deletions lib/container.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,15 @@ import { isMainThread } from 'worker_threads'
import debugModule from 'debug'
const debug = debugModule('codeceptjs:container')
import { MetaStep } from './step.js'
import { methodsOfObject, fileExists, isFunction, isAsyncFunction, installedLocally, deepMerge } from './utils.js'
import {
methodsOfObject,
fileExists,
isFunction,
isAsyncFunction,
installedLocally,
deepMerge,
resolveImportModulePath,
} from './utils.js'
import { transpileTypeScript, cleanupTempFiles, fixErrorStack } from './utils/typescript.js'
import Translation from './translation.js'
import MochaFactory from './mocha/factory.js'
Expand Down Expand Up @@ -434,7 +442,9 @@ async function requireHelperFromModule(helperName, config, HelperClass) {
try {
// For built-in helpers, use direct relative import with .js extension
const helperPath = `${moduleName}.js`
const mod = await import(helperPath)

const resolvedPath = resolveImportModulePath(helperPath)
const mod = await import(resolvedPath)
HelperClass = mod.default || mod
} catch (err) {
throw err
Expand Down Expand Up @@ -472,7 +482,9 @@ async function requireHelperFromModule(helperName, config, HelperClass) {
// check if the new syntax export default HelperName is used and loads the Helper, otherwise loads the module that used old syntax export = HelperName.
try {
// Try dynamic import for both CommonJS and ESM modules
const mod = await import(importPath)
const resolvedPath = resolveImportModulePath(importPath)
const mod = await import(resolvedPath)

if (!mod && !mod.default) {
throw new Error(`Helper module '${moduleName}' was not found. Make sure you have installed the package correctly.`)
}
Expand All @@ -488,7 +500,7 @@ async function requireHelperFromModule(helperName, config, HelperClass) {
if (fileMapping) {
fixErrorStack(err, fileMapping)
}

// Clean up temp files before rethrowing
if (tempJsFile) {
const filesToClean = Array.isArray(tempJsFile) ? tempJsFile : [tempJsFile]
Expand Down Expand Up @@ -683,7 +695,8 @@ async function loadPluginAsync(modulePath, config) {
let pluginMod
try {
// Try dynamic import first (works for both ESM and CJS)
pluginMod = await import(modulePath)
const resolvedPath = resolveImportModulePath(modulePath)
pluginMod = await import(resolvedPath)
} catch (err) {
throw new Error(`Could not load plugin from '${modulePath}': ${err.message}`)
}
Expand Down Expand Up @@ -890,21 +903,19 @@ async function loadSupportObject(modulePath, supportObjectName) {

let obj
try {
obj = await import(importPath)
const resolvedPath = resolveImportModulePath(importPath)
obj = await import(resolvedPath)
} catch (importError) {
// Fix error stack to point to original .ts files
if (fileMapping) {
fixErrorStack(importError, fileMapping)
}

// Clean up temp files if created before rethrowing

if (tempJsFile) {
const filesToClean = Array.isArray(tempJsFile) ? tempJsFile : [tempJsFile]
cleanupTempFiles(filesToClean)
}
throw importError
} finally {
// Clean up temp files if created
if (tempJsFile) {
const filesToClean = Array.isArray(tempJsFile) ? tempJsFile : [tempJsFile]
cleanupTempFiles(filesToClean)
Expand Down
4 changes: 3 additions & 1 deletion lib/helper/ApiDataFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import path from 'path'
import Helper from '@codeceptjs/helper'
import REST from './REST.js'
import store from '../store.js'
import { resolveImportModulePath } from '../utils.js'

/**
* Helper for managing remote data using REST API.
Expand Down Expand Up @@ -328,7 +329,8 @@ class ApiDataFactory extends Helper {
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)
const resolvedPath = resolveImportModulePath(modulePath)
const module = await import(resolvedPath)
const builder = module.default || module
return builder.build(data, options)
} catch (err) {
Expand Down
6 changes: 4 additions & 2 deletions lib/rerun.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import event from './event.js'
import BaseCodecept from './codecept.js'
import output from './output.js'
import { createRequire } from 'module'
import { resolveImportModulePath } from './utils.js'

const require = createRequire(import.meta.url)

Expand Down Expand Up @@ -50,8 +51,9 @@ class CodeceptRerunner extends BaseCodecept {
}

// Force reload the module by using a cache-busting query parameter
const fileUrl = `${fsPath.resolve(file)}?t=${Date.now()}`
await import(fileUrl)
const fileUrl = `${fsPath.resolve(file)}`
const resolvedPath = resolveImportModulePath(fileUrl)
await import(resolvedPath)
} catch (e) {
console.error(`Error loading test file ${file}:`, e)
}
Expand Down
8 changes: 7 additions & 1 deletion lib/translation.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,13 @@ class Translation {

loadVocabulary(vocabularyFile) {
if (!vocabularyFile) return
const filePath = path.join(store.codeceptDir, vocabularyFile)

let filePath;
if (path.isAbsolute(vocabularyFile)) {
filePath = vocabularyFile;
} else {
filePath = path.join(store.codeceptDir, vocabularyFile);
}

try {
const require = createRequire(import.meta.url)
Expand Down
23 changes: 22 additions & 1 deletion lib/utils.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import fs from 'fs'
import os from 'os'
import path from 'path'
import { pathToFileURL } from 'url'
import { createRequire } from 'module'
import chalk from 'chalk'
import getFunctionArguments from 'fn-args'
Expand Down Expand Up @@ -38,6 +39,22 @@ export const isAsyncFunction = function (fn) {
return fn[Symbol.toStringTag] === 'AsyncFunction'
}

export const resolveImportModulePath = function (modulePath) {
// 1. If it's an absolute path, convert to a file:// URL
if (path.isAbsolute(modulePath)) {
return pathToFileURL(modulePath).href;
}

// 2. If it's a relative path (starts with ./ or ../), resolve it fully
if (modulePath.startsWith('./') || modulePath.startsWith('../')) {
return modulePath
}

// 3. Otherwise, it's likely a bare NPM module (e.g., 'chai', 'codeceptjs')
// Let Node.js resolve it natively from node_modules
return modulePath;
}

export const fileExists = function (filePath) {
return fs.existsSync(filePath)
}
Expand Down Expand Up @@ -229,7 +246,7 @@ export const test = {
// Use Node.js child_process.spawnSync with platform-specific sleep commands
// This avoids busy waiting and allows other processes to run
try {
if (os.platform() === 'win32') {
if (isWindows()) {
// Windows: use ping with precise timing (ping waits exactly the specified ms)
spawnSync('ping', ['-n', '1', '-w', pollInterval.toString(), '127.0.0.1'], { stdio: 'ignore' })
} else {
Expand Down Expand Up @@ -735,3 +752,7 @@ export const markdownToAnsi = function (markdown) {
})
)
}

export function isWindows() {
return os.platform() === 'win32'
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"prettier": "prettier --config prettier.config.js --write bin/**/*.js lib/**/*.js test/**/*.js translations/**/*.js runok.cjs",
"docs": "./runok.cjs docs",
"test:unit": "mocha test/unit --recursive --timeout 10000 --reporter @testomatio/reporter/mocha",
"test:unit:windows": "mocha test/unit/container_test.js --recursive --timeout 10000 --reporter @testomatio/reporter/mocha",
"test:rest": "mocha test/rest --recursive --timeout 20000 --reporter @testomatio/reporter/mocha",
"test:runner": "mocha test/runner --recursive --timeout 10000 --reporter @testomatio/reporter/mocha",
"test": "npm run test:unit && npm run test:rest && npm run test:runner",
Expand Down
3 changes: 3 additions & 0 deletions test/data/sandbox/data/fs_sample.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
A simple file
for FileSystem helper
test
Loading
Loading