Skip to content
Open
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
21 changes: 21 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
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,8 @@ import Codecept from '../lib/codecept.js'
import output from '../lib/output.js'
const { print, error } = output
import { printError } from '../lib/command/utils.js'
import { isWindows } from '../lib/utils.js'
import { pathToFileURL } from 'url'

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

const dynamicImport = async modulePath => {
const module = await import(modulePath)
const module = isWindows() ? await import(pathToFileURL(modulePath).href) : await import(modulePath)
return module.default || module
}

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

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

try {
const module = await import(promptPath)
const module = isWindows() ? await import(pathToFileURL(promptPath).href) : await import(promptPath)
prompts[name] = module.default || module
debug(`Loaded prompt ${name} from ${promptPath}`)
} catch (err) {
Expand Down
8 changes: 4 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, isWindows } 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,7 @@ class Codecept {
}
}
// Use dynamic import for ESM
await import(modulePath)
isWindows() ? await import(pathToFileURL(modulePath).href) : await import(modulePath)
}
}
}
Expand Down Expand Up @@ -137,7 +137,7 @@ class Codecept {
]

for (const modulePath of listenerModules) {
const module = await import(modulePath)
const module = isWindows() ? await import(pathToFileURL(modulePath).href) : await import(modulePath)
runHook(module.default || module)
}
}
Expand Down
9 changes: 5 additions & 4 deletions lib/config.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
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, isWindows } from './utils.js'
import { transpileTypeScript, cleanupTempFiles, fixErrorStack } from './utils/typescript.js'
import { pathToFileURL } from 'url'

const defaultConfig = {
output: './_output',
Expand Down Expand Up @@ -96,7 +97,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 +106,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 +243,7 @@ async function loadConfigFile(configFile) {
allTempFiles = result.allTempFiles
fileMapping = result.fileMapping

configModule = await import(tempFile)
configModule = isWindows() ? await import(pathToFileURL(tempFile).href) : await import(tempFile)
cleanupTempFiles(allTempFiles)
} catch (err) {
transpileError = err
Expand Down
43 changes: 33 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,
isWindows,
} from './utils.js'
import { transpileTypeScript, cleanupTempFiles, fixErrorStack } from './utils/typescript.js'
import Translation from './translation.js'
import MochaFactory from './mocha/factory.js'
Expand All @@ -17,6 +25,7 @@ import Result from './result.js'
import ai from './ai.js'
import actorFactory from './actor.js'
import Config from './config.js'
import { pathToFileURL } from 'url'

let asyncHelperPromise

Expand Down Expand Up @@ -434,7 +443,13 @@ 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 resolvedImportPath =
isWindows() && typeof helperPath === 'string' && path.isAbsolute(helperPath)
? pathToFileURL(helperPath).href
: helperPath

const mod = await import(resolvedImportPath)
HelperClass = mod.default || mod
} catch (err) {
throw err
Expand Down Expand Up @@ -472,7 +487,13 @@ 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 resolvedImportPath =
isWindows() && typeof importPath === 'string' && path.isAbsolute(importPath)
? pathToFileURL(importPath).href
: importPath

const mod = await import(resolvedImportPath)

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 +509,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 +704,7 @@ async function loadPluginAsync(modulePath, config) {
let pluginMod
try {
// Try dynamic import first (works for both ESM and CJS)
pluginMod = await import(modulePath)
pluginMod = isWindows() ? await import(pathToFileURL(modulePath).href) : await import(modulePath)
} catch (err) {
throw new Error(`Could not load plugin from '${modulePath}': ${err.message}`)
}
Expand Down Expand Up @@ -890,21 +911,23 @@ async function loadSupportObject(modulePath, supportObjectName) {

let obj
try {
obj = await import(importPath)
const resolvedImportPath =
isWindows() && typeof importPath === 'string' && path.isAbsolute(importPath)
? pathToFileURL(importPath).href
: importPath

obj = await import(resolvedImportPath)
} 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,8 @@ import path from 'path'
import Helper from '@codeceptjs/helper'
import REST from './REST.js'
import store from '../store.js'
import { isWindows } from '../utils.js'
import { pathToFileURL } from 'url'

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

const require = createRequire(import.meta.url)

Expand Down Expand Up @@ -51,7 +53,7 @@ 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)
isWindows() ? await import(pathToFileURL(fileUrl).href) : await import(fileUrl)
} catch (e) {
console.error(`Error loading test file ${file}:`, e)
}
Expand Down
6 changes: 5 additions & 1 deletion lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,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 +735,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
11 changes: 9 additions & 2 deletions test/unit/container_test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { expect } from 'chai'
import path from 'path'
import { fileURLToPath } from 'url'
import { fileURLToPath, pathToFileURL } from 'url'

const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
Expand All @@ -9,6 +9,7 @@ import FileSystem from '../../lib/helper/FileSystem.js'
import actor from '../../lib/actor.js'
import container from '../../lib/container.js'
import Translation from '../../lib/translation.js'
import { isWindows } from '../../lib/utils.js'

describe('Container', () => {
before(() => {
Expand Down Expand Up @@ -183,7 +184,13 @@ describe('Container', () => {
dummyPage: './data/dummy_page.js',
},
})
const dummyPage = await import('../data/dummy_page.js')

const resolvedImportPath =
isWindows() && typeof '../data/dummy_page.js' === 'string' && path.isAbsolute('../data/dummy_page.js')
? pathToFileURL('../data/dummy_page.js').href
: '../data/dummy_page.js'

const dummyPage = await import(resolvedImportPath)
expect(container.support('dummyPage').toString()).is.eql((dummyPage.default || dummyPage).toString())
})

Expand Down
Loading
Loading