Skip to content

Commit 51a2971

Browse files
committed
support windows
1 parent 06cdb0c commit 51a2971

10 files changed

Lines changed: 64 additions & 22 deletions

File tree

.github/workflows/test.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,27 @@ jobs:
5353
- name: Stop mock server
5454
run: npm run mock-server:stop
5555

56+
unit-tests-windows:
57+
name: Unit tests
58+
runs-on: windows-latest
59+
timeout-minutes: 15
60+
61+
strategy:
62+
matrix:
63+
node-version: [ 20.x ]
64+
65+
steps:
66+
- uses: actions/checkout@v6
67+
- name: Use Node.js ${{ matrix.node-version }}
68+
uses: actions/setup-node@v6
69+
with:
70+
node-version: ${{ matrix.node-version }}
71+
- run: npm i
72+
env:
73+
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: true
74+
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true
75+
- run: npm run test:unit
76+
5677
runner-tests:
5778
name: Runner tests
5879
runs-on: ubuntu-22.04

bin/codecept.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import Codecept from '../lib/codecept.js'
55
import output from '../lib/output.js'
66
const { print, error } = output
77
import { printError } from '../lib/command/utils.js'
8+
import { isWindows } from '../lib/utils.js'
9+
import { pathToFileURL } from 'url'
810

911
const commandFlags = {
1012
ai: {
@@ -45,7 +47,7 @@ const errorHandler =
4547
}
4648

4749
const dynamicImport = async modulePath => {
48-
const module = await import(modulePath)
50+
const module = isWindows() ? await import(pathToFileURL(modulePath).href) : await import(modulePath)
4951
return module.default || module
5052
}
5153

lib/ai.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import output from './output.js'
44
import event from './event.js'
55
import { removeNonInteractiveElements, minifyHtml, splitByChunks } from './html.js'
66
import { generateText } from 'ai'
7-
import { fileURLToPath } from 'url'
7+
import { fileURLToPath, pathToFileURL } from 'url'
88
import path from 'path'
9-
import { fileExists } from './utils.js'
9+
import { fileExists, isWindows } from './utils.js'
1010
import store from './store.js'
1111

1212
const __dirname = path.dirname(fileURLToPath(import.meta.url))
@@ -34,7 +34,7 @@ async function loadPrompts() {
3434
}
3535

3636
try {
37-
const module = await import(promptPath)
37+
const module = isWindows() ? await import(pathToFileURL(promptPath).href) : await import(promptPath)
3838
prompts[name] = module.default || module
3939
debug(`Loaded prompt ${name} from ${promptPath}`)
4040
} catch (err) {

lib/codecept.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import event from './event.js'
1717
import runHook from './hooks.js'
1818
import ActorFactory from './actor.js'
1919
import output from './output.js'
20-
import { emptyFolder } from './utils.js'
20+
import { emptyFolder, isWindows } from './utils.js'
2121
import { initCodeceptGlobals } from './globals.js'
2222
import { validateTypeScriptSetup, getTSNodeESMWarning } from './utils/loaderCheck.js'
2323
import recorder from './recorder.js'
@@ -73,7 +73,7 @@ class Codecept {
7373
// For npm packages, resolve from the user's directory
7474
// This ensures packages like tsx are found in user's node_modules
7575
const userDir = store.codeceptDir || process.cwd()
76-
76+
7777
try {
7878
// Use createRequire to resolve from user's directory
7979
const userRequire = createRequire(pathToFileURL(resolve(userDir, 'package.json')).href)
@@ -86,7 +86,7 @@ class Codecept {
8686
}
8787
}
8888
// Use dynamic import for ESM
89-
await import(modulePath)
89+
isWindows() ? await import(pathToFileURL(modulePath).href) : await import(modulePath)
9090
}
9191
}
9292
}
@@ -137,7 +137,7 @@ class Codecept {
137137
]
138138

139139
for (const modulePath of listenerModules) {
140-
const module = await import(modulePath)
140+
const module = isWindows() ? await import(pathToFileURL(modulePath).href) : await import(modulePath)
141141
runHook(module.default || module)
142142
}
143143
}

lib/config.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import fs from 'fs'
22
import path from 'path'
33
import { createRequire } from 'module'
4-
import { fileExists, isFile, deepMerge, deepClone } from './utils.js'
4+
import { fileExists, isFile, deepMerge, deepClone, isWindows } from './utils.js'
55
import { transpileTypeScript, cleanupTempFiles, fixErrorStack } from './utils/typescript.js'
6+
import { pathToFileURL } from 'url'
67

78
const defaultConfig = {
89
output: './_output',
@@ -96,7 +97,7 @@ class Config {
9697
// Try different extensions if the file doesn't exist
9798
const extensions = ['.ts', '.cjs', '.mjs']
9899
let found = false
99-
100+
100101
for (const ext of extensions) {
101102
const altConfig = configFile.replace(/\.js$/, ext)
102103
if (fileExists(altConfig)) {
@@ -105,7 +106,7 @@ class Config {
105106
break
106107
}
107108
}
108-
109+
109110
if (!found) {
110111
throw new Error(`Config file ${configFile} does not exist. Execute 'codeceptjs init' to create config`)
111112
}
@@ -242,7 +243,7 @@ async function loadConfigFile(configFile) {
242243
allTempFiles = result.allTempFiles
243244
fileMapping = result.fileMapping
244245

245-
configModule = await import(tempFile)
246+
configModule = isWindows() ? await import(pathToFileURL(tempFile).href) : await import(tempFile)
246247
cleanupTempFiles(allTempFiles)
247248
} catch (err) {
248249
transpileError = err

lib/container.js

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,15 @@ import { isMainThread } from 'worker_threads'
55
import debugModule from 'debug'
66
const debug = debugModule('codeceptjs:container')
77
import { MetaStep } from './step.js'
8-
import { methodsOfObject, fileExists, isFunction, isAsyncFunction, installedLocally, deepMerge } from './utils.js'
8+
import {
9+
methodsOfObject,
10+
fileExists,
11+
isFunction,
12+
isAsyncFunction,
13+
installedLocally,
14+
deepMerge,
15+
isWindows,
16+
} from './utils.js'
917
import { transpileTypeScript, cleanupTempFiles, fixErrorStack } from './utils/typescript.js'
1018
import Translation from './translation.js'
1119
import MochaFactory from './mocha/factory.js'
@@ -17,6 +25,7 @@ import Result from './result.js'
1725
import ai from './ai.js'
1826
import actorFactory from './actor.js'
1927
import Config from './config.js'
28+
import { pathToFileURL } from 'url'
2029

2130
let asyncHelperPromise
2231

@@ -434,7 +443,7 @@ async function requireHelperFromModule(helperName, config, HelperClass) {
434443
try {
435444
// For built-in helpers, use direct relative import with .js extension
436445
const helperPath = `${moduleName}.js`
437-
const mod = await import(helperPath)
446+
const mod = isWindows() ? await import(pathToFileURL(helperPath).href) : await import(helperPath)
438447
HelperClass = mod.default || mod
439448
} catch (err) {
440449
throw err
@@ -472,7 +481,7 @@ async function requireHelperFromModule(helperName, config, HelperClass) {
472481
// check if the new syntax export default HelperName is used and loads the Helper, otherwise loads the module that used old syntax export = HelperName.
473482
try {
474483
// Try dynamic import for both CommonJS and ESM modules
475-
const mod = await import(importPath)
484+
const mod = isWindows() ? await import(pathToFileURL(importPath).href) : await import(importPath)
476485
if (!mod && !mod.default) {
477486
throw new Error(`Helper module '${moduleName}' was not found. Make sure you have installed the package correctly.`)
478487
}
@@ -488,7 +497,7 @@ async function requireHelperFromModule(helperName, config, HelperClass) {
488497
if (fileMapping) {
489498
fixErrorStack(err, fileMapping)
490499
}
491-
500+
492501
// Clean up temp files before rethrowing
493502
if (tempJsFile) {
494503
const filesToClean = Array.isArray(tempJsFile) ? tempJsFile : [tempJsFile]
@@ -683,7 +692,7 @@ async function loadPluginAsync(modulePath, config) {
683692
let pluginMod
684693
try {
685694
// Try dynamic import first (works for both ESM and CJS)
686-
pluginMod = await import(modulePath)
695+
pluginMod = isWindows() ? await import(pathToFileURL(modulePath).href) : await import(modulePath)
687696
} catch (err) {
688697
throw new Error(`Could not load plugin from '${modulePath}': ${err.message}`)
689698
}
@@ -896,7 +905,7 @@ async function loadSupportObject(modulePath, supportObjectName) {
896905
if (fileMapping) {
897906
fixErrorStack(importError, fileMapping)
898907
}
899-
908+
900909
// Clean up temp files if created before rethrowing
901910
if (tempJsFile) {
902911
const filesToClean = Array.isArray(tempJsFile) ? tempJsFile : [tempJsFile]

lib/helper/ApiDataFactory.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import path from 'path'
22
import Helper from '@codeceptjs/helper'
33
import REST from './REST.js'
44
import store from '../store.js'
5+
import { isWindows } from '../utils.js'
6+
import { pathToFileURL } from 'url'
57

68
/**
79
* Helper for managing remote data using REST API.
@@ -328,7 +330,7 @@ class ApiDataFactory extends Helper {
328330
modulePath = path.join(store.codeceptDir, modulePath)
329331
}
330332
// 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()`.
331-
const module = await import(modulePath)
333+
const module = isWindows() ? await import(pathToFileURL(modulePath).href) : await import(modulePath)
332334
const builder = module.default || module
333335
return builder.build(data, options)
334336
} catch (err) {

lib/rerun.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import event from './event.js'
55
import BaseCodecept from './codecept.js'
66
import output from './output.js'
77
import { createRequire } from 'module'
8+
import { isWindows } from './utils.js'
9+
import { pathToFileURL } from 'url'
810

911
const require = createRequire(import.meta.url)
1012

@@ -51,7 +53,7 @@ class CodeceptRerunner extends BaseCodecept {
5153

5254
// Force reload the module by using a cache-busting query parameter
5355
const fileUrl = `${fsPath.resolve(file)}?t=${Date.now()}`
54-
await import(fileUrl)
56+
isWindows() ? await import(pathToFileURL(fileUrl).href) : await import(fileUrl)
5557
} catch (e) {
5658
console.error(`Error loading test file ${file}:`, e)
5759
}

lib/utils.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ export const test = {
229229
// Use Node.js child_process.spawnSync with platform-specific sleep commands
230230
// This avoids busy waiting and allows other processes to run
231231
try {
232-
if (os.platform() === 'win32') {
232+
if (isWindows()) {
233233
// Windows: use ping with precise timing (ping waits exactly the specified ms)
234234
spawnSync('ping', ['-n', '1', '-w', pollInterval.toString(), '127.0.0.1'], { stdio: 'ignore' })
235235
} else {
@@ -735,3 +735,7 @@ export const markdownToAnsi = function (markdown) {
735735
})
736736
)
737737
}
738+
739+
export function isWindows() {
740+
return os.platform() === 'win32'
741+
}

test/unit/utils_test.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import sinon from 'sinon'
66
import * as utils from '../../lib/utils.js'
77
import store from '../../lib/store.js'
88
import playwright from 'playwright'
9+
import { isWindows } from '../../lib/utils.js'
910

1011
const __filename = fileURLToPath(import.meta.url)
1112
const __dirname = path.dirname(__filename)
@@ -326,7 +327,7 @@ describe('utils', () => {
326327

327328
it('returns the given filename for absolute one', () => {
328329
const _path = utils.screenshotOutputFolder('/Users/someuser/workbase/project1/test_output/screenshot1.failed.png'.replace(/\//g, path.sep))
329-
if (os.platform() === 'win32') {
330+
if (isWindows()) {
330331
expect(_path).eql(path.resolve(store.codeceptDir, '/Users/someuser/workbase/project1/test_output/screenshot1.failed.png'))
331332
} else {
332333
expect(_path).eql('/Users/someuser/workbase/project1/test_output/screenshot1.failed.png')

0 commit comments

Comments
 (0)