codeceptjs 4.0.0-beta.9.esm-aria → 4.0.1-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/codecept.js +25 -1
- package/lib/command/init.js +2 -1
- package/lib/config.js +7 -22
- package/lib/container.js +65 -6
- package/lib/mocha/gherkin.js +4 -4
- package/lib/plugin/htmlReporter.js +4 -4
- package/lib/utils/loaderCheck.js +124 -0
- package/lib/utils/typescript.js +237 -0
- package/package.json +10 -1
- package/typings/promiseBasedTypes.d.ts +4 -0
- package/typings/types.d.ts +4 -0
package/lib/codecept.js
CHANGED
|
@@ -3,8 +3,9 @@ import { globSync } from 'glob'
|
|
|
3
3
|
import shuffle from 'lodash.shuffle'
|
|
4
4
|
import fsPath from 'path'
|
|
5
5
|
import { resolve } from 'path'
|
|
6
|
-
import { fileURLToPath } from 'url'
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from 'url'
|
|
7
7
|
import { dirname } from 'path'
|
|
8
|
+
import { createRequire } from 'module'
|
|
8
9
|
|
|
9
10
|
const __filename = fileURLToPath(import.meta.url)
|
|
10
11
|
const __dirname = dirname(__filename)
|
|
@@ -18,6 +19,7 @@ import ActorFactory from './actor.js'
|
|
|
18
19
|
import output from './output.js'
|
|
19
20
|
import { emptyFolder } from './utils.js'
|
|
20
21
|
import { initCodeceptGlobals } from './globals.js'
|
|
22
|
+
import { validateTypeScriptSetup } from './utils/loaderCheck.js'
|
|
21
23
|
import recorder from './recorder.js'
|
|
22
24
|
|
|
23
25
|
import storeListener from './listener/store.js'
|
|
@@ -66,6 +68,21 @@ class Codecept {
|
|
|
66
68
|
modulePath = `${modulePath}.js`
|
|
67
69
|
}
|
|
68
70
|
}
|
|
71
|
+
} else {
|
|
72
|
+
// For npm packages, resolve from the user's directory
|
|
73
|
+
// This ensures packages like tsx are found in user's node_modules
|
|
74
|
+
const userDir = global.codecept_dir || process.cwd()
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
// Use createRequire to resolve from user's directory
|
|
78
|
+
const userRequire = createRequire(pathToFileURL(resolve(userDir, 'package.json')).href)
|
|
79
|
+
const resolvedPath = userRequire.resolve(requiredModule)
|
|
80
|
+
modulePath = pathToFileURL(resolvedPath).href
|
|
81
|
+
} catch (resolveError) {
|
|
82
|
+
// If resolution fails, try direct import (will check from CodeceptJS node_modules)
|
|
83
|
+
// This is the fallback for globally installed packages
|
|
84
|
+
modulePath = requiredModule
|
|
85
|
+
}
|
|
69
86
|
}
|
|
70
87
|
// Use dynamic import for ESM
|
|
71
88
|
await import(modulePath)
|
|
@@ -246,6 +263,13 @@ class Codecept {
|
|
|
246
263
|
async run(test) {
|
|
247
264
|
await container.started()
|
|
248
265
|
|
|
266
|
+
// Check TypeScript loader configuration before running tests
|
|
267
|
+
const tsValidation = validateTypeScriptSetup(this.testFiles, this.requiringModules || [])
|
|
268
|
+
if (tsValidation.hasError) {
|
|
269
|
+
output.error(tsValidation.message)
|
|
270
|
+
process.exit(1)
|
|
271
|
+
}
|
|
272
|
+
|
|
249
273
|
// Ensure translations are loaded for Gherkin features
|
|
250
274
|
try {
|
|
251
275
|
const { loadTranslations } = await import('./mocha/gherkin.js')
|
package/lib/command/init.js
CHANGED
|
@@ -161,7 +161,7 @@ export default async function (initPath) {
|
|
|
161
161
|
isTypeScript = true
|
|
162
162
|
extension = isTypeScript === true ? 'ts' : 'js'
|
|
163
163
|
packages.push('typescript')
|
|
164
|
-
packages.push('
|
|
164
|
+
packages.push('tsx') // Add tsx for TypeScript support
|
|
165
165
|
packages.push('@types/node')
|
|
166
166
|
}
|
|
167
167
|
|
|
@@ -172,6 +172,7 @@ export default async function (initPath) {
|
|
|
172
172
|
config.tests = result.tests
|
|
173
173
|
if (isTypeScript) {
|
|
174
174
|
config.tests = `${config.tests.replace(/\.js$/, `.${extension}`)}`
|
|
175
|
+
config.require = ['tsx/cjs'] // Add tsx/cjs loader for TypeScript tests
|
|
175
176
|
}
|
|
176
177
|
|
|
177
178
|
// create a directory tests if it is included in tests path
|
package/lib/config.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from 'fs'
|
|
|
2
2
|
import path from 'path'
|
|
3
3
|
import { createRequire } from 'module'
|
|
4
4
|
import { fileExists, isFile, deepMerge, deepClone } from './utils.js'
|
|
5
|
+
import { transpileTypeScript, cleanupTempFiles } from './utils/typescript.js'
|
|
5
6
|
|
|
6
7
|
const defaultConfig = {
|
|
7
8
|
output: './_output',
|
|
@@ -156,31 +157,15 @@ async function loadConfigFile(configFile) {
|
|
|
156
157
|
// For .ts files, try to compile and load as JavaScript
|
|
157
158
|
if (extensionName === '.ts') {
|
|
158
159
|
try {
|
|
159
|
-
//
|
|
160
|
-
const
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
// Transpile TypeScript to JavaScript with ES module output
|
|
164
|
-
const jsContent = transpile(tsContent, {
|
|
165
|
-
module: 99, // ModuleKind.ESNext
|
|
166
|
-
target: 99, // ScriptTarget.ESNext
|
|
167
|
-
esModuleInterop: true,
|
|
168
|
-
allowSyntheticDefaultImports: true,
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
// Create a temporary JS file with .mjs extension to force ES module treatment
|
|
172
|
-
const tempJsFile = configFile.replace('.ts', '.temp.mjs')
|
|
173
|
-
fs.writeFileSync(tempJsFile, jsContent)
|
|
160
|
+
// Use the TypeScript transpilation utility
|
|
161
|
+
const typescript = require('typescript')
|
|
162
|
+
const { tempFile, allTempFiles } = await transpileTypeScript(configFile, typescript)
|
|
174
163
|
|
|
175
164
|
try {
|
|
176
|
-
configModule = await import(
|
|
177
|
-
|
|
178
|
-
fs.unlinkSync(tempJsFile)
|
|
165
|
+
configModule = await import(tempFile)
|
|
166
|
+
cleanupTempFiles(allTempFiles)
|
|
179
167
|
} catch (err) {
|
|
180
|
-
|
|
181
|
-
if (fs.existsSync(tempJsFile)) {
|
|
182
|
-
fs.unlinkSync(tempJsFile)
|
|
183
|
-
}
|
|
168
|
+
cleanupTempFiles(allTempFiles)
|
|
184
169
|
throw err
|
|
185
170
|
}
|
|
186
171
|
} catch (tsError) {
|
package/lib/container.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { globSync } from 'glob'
|
|
2
2
|
import path from 'path'
|
|
3
|
+
import fs from 'fs'
|
|
3
4
|
import debugModule from 'debug'
|
|
4
5
|
const debug = debugModule('codeceptjs:container')
|
|
5
6
|
import { MetaStep } from './step.js'
|
|
6
7
|
import { methodsOfObject, fileExists, isFunction, isAsyncFunction, installedLocally, deepMerge } from './utils.js'
|
|
8
|
+
import { transpileTypeScript, cleanupTempFiles } from './utils/typescript.js'
|
|
7
9
|
import Translation from './translation.js'
|
|
8
10
|
import MochaFactory from './mocha/factory.js'
|
|
9
11
|
import recorder from './recorder.js'
|
|
@@ -30,7 +32,7 @@ let container = {
|
|
|
30
32
|
translation: {},
|
|
31
33
|
/** @type {Result | null} */
|
|
32
34
|
result: null,
|
|
33
|
-
sharedKeys: new Set() // Track keys shared via share() function
|
|
35
|
+
sharedKeys: new Set(), // Track keys shared via share() function
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
/**
|
|
@@ -200,6 +202,15 @@ class Container {
|
|
|
200
202
|
static append(newContainer) {
|
|
201
203
|
container = deepMerge(container, newContainer)
|
|
202
204
|
|
|
205
|
+
// If new helpers are added, set the helpers property on them
|
|
206
|
+
if (newContainer.helpers) {
|
|
207
|
+
for (const name in newContainer.helpers) {
|
|
208
|
+
if (container.helpers[name] && typeof container.helpers[name] === 'object') {
|
|
209
|
+
container.helpers[name].helpers = container.helpers
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
203
214
|
// If new support objects are added, update the proxy support
|
|
204
215
|
if (newContainer.support) {
|
|
205
216
|
const newProxySupport = createSupportObjects(newContainer.support)
|
|
@@ -248,10 +259,10 @@ class Container {
|
|
|
248
259
|
// Instead of using append which replaces the entire container,
|
|
249
260
|
// directly update the support object to maintain proxy references
|
|
250
261
|
Object.assign(container.support, data)
|
|
251
|
-
|
|
262
|
+
|
|
252
263
|
// Track which keys were explicitly shared
|
|
253
264
|
Object.keys(data).forEach(key => container.sharedKeys.add(key))
|
|
254
|
-
|
|
265
|
+
|
|
255
266
|
if (!options.local) {
|
|
256
267
|
WorkerStorage.share(data)
|
|
257
268
|
}
|
|
@@ -347,6 +358,16 @@ async function createHelpers(config) {
|
|
|
347
358
|
}
|
|
348
359
|
}
|
|
349
360
|
|
|
361
|
+
// Set helpers property on each helper to allow access to other helpers
|
|
362
|
+
for (const name in helpers) {
|
|
363
|
+
if (helpers[name] && typeof helpers[name] === 'object') {
|
|
364
|
+
helpers[name].helpers = helpers
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Wait for async helpers and call _init
|
|
369
|
+
await asyncHelperPromise
|
|
370
|
+
|
|
350
371
|
for (const name in helpers) {
|
|
351
372
|
if (helpers[name]._init) await helpers[name]._init()
|
|
352
373
|
}
|
|
@@ -674,12 +695,50 @@ async function loadSupportObject(modulePath, supportObjectName) {
|
|
|
674
695
|
try {
|
|
675
696
|
// Use dynamic import for both ESM and CJS modules
|
|
676
697
|
let importPath = modulePath
|
|
677
|
-
|
|
698
|
+
let tempJsFile = null
|
|
699
|
+
|
|
678
700
|
if (typeof importPath === 'string') {
|
|
679
701
|
const ext = path.extname(importPath)
|
|
680
|
-
|
|
702
|
+
|
|
703
|
+
// Handle TypeScript files
|
|
704
|
+
if (ext === '.ts') {
|
|
705
|
+
try {
|
|
706
|
+
// Use the TypeScript transpilation utility
|
|
707
|
+
const typescript = await import('typescript')
|
|
708
|
+
const { tempFile, allTempFiles } = await transpileTypeScript(importPath, typescript)
|
|
709
|
+
|
|
710
|
+
debug(`Transpiled TypeScript file: ${importPath} -> ${tempFile}`)
|
|
711
|
+
|
|
712
|
+
// Attach cleanup handler
|
|
713
|
+
importPath = tempFile
|
|
714
|
+
// Store temp files list in a way that cleanup can access them
|
|
715
|
+
tempJsFile = allTempFiles
|
|
716
|
+
} catch (tsError) {
|
|
717
|
+
throw new Error(`Failed to load TypeScript file ${importPath}: ${tsError.message}. Make sure 'typescript' package is installed.`)
|
|
718
|
+
}
|
|
719
|
+
} else if (!ext) {
|
|
720
|
+
// Append .js if no extension provided (ESM resolution requires it)
|
|
721
|
+
importPath = `${importPath}.js`
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
let obj
|
|
726
|
+
try {
|
|
727
|
+
obj = await import(importPath)
|
|
728
|
+
} catch (importError) {
|
|
729
|
+
// Clean up temp files if created before rethrowing
|
|
730
|
+
if (tempJsFile) {
|
|
731
|
+
const filesToClean = Array.isArray(tempJsFile) ? tempJsFile : [tempJsFile]
|
|
732
|
+
cleanupTempFiles(filesToClean)
|
|
733
|
+
}
|
|
734
|
+
throw importError
|
|
735
|
+
} finally {
|
|
736
|
+
// Clean up temp files if created
|
|
737
|
+
if (tempJsFile) {
|
|
738
|
+
const filesToClean = Array.isArray(tempJsFile) ? tempJsFile : [tempJsFile]
|
|
739
|
+
cleanupTempFiles(filesToClean)
|
|
740
|
+
}
|
|
681
741
|
}
|
|
682
|
-
const obj = await import(importPath)
|
|
683
742
|
|
|
684
743
|
// Handle ESM module wrapper
|
|
685
744
|
let actualObj = obj
|
package/lib/mocha/gherkin.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { AstBuilder, GherkinClassicTokenMatcher, Parser } from '@cucumber/gherkin'
|
|
2
2
|
import { IdGenerator } from '@cucumber/messages'
|
|
3
3
|
import { Context, Suite } from 'mocha'
|
|
4
4
|
import debug from 'debug'
|
|
@@ -15,9 +15,9 @@ import { createTest } from './test.js'
|
|
|
15
15
|
import { matchStep } from './bdd.js'
|
|
16
16
|
|
|
17
17
|
const uuidFn = IdGenerator.uuid()
|
|
18
|
-
const builder = new
|
|
19
|
-
const matcher = new
|
|
20
|
-
const parser = new
|
|
18
|
+
const builder = new AstBuilder(uuidFn)
|
|
19
|
+
const matcher = new GherkinClassicTokenMatcher()
|
|
20
|
+
const parser = new Parser(builder, matcher)
|
|
21
21
|
parser.stopAtFirstError = false
|
|
22
22
|
|
|
23
23
|
const gherkinParser = (text, file) => {
|
|
@@ -143,7 +143,7 @@ export default function (config) {
|
|
|
143
143
|
|
|
144
144
|
event.dispatcher.on(event.step.finished, step => {
|
|
145
145
|
if (step.htmlReporterStartTime) {
|
|
146
|
-
step.
|
|
146
|
+
step.htmlReporterDuration = Date.now() - step.htmlReporterStartTime
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
// Serialize args immediately to preserve them through worker serialization
|
|
@@ -170,7 +170,7 @@ export default function (config) {
|
|
|
170
170
|
actor: step.actor,
|
|
171
171
|
args: serializedArgs,
|
|
172
172
|
status: step.failed ? 'failed' : 'success',
|
|
173
|
-
duration: step.duration || 0,
|
|
173
|
+
duration: step.htmlReporterDuration || step.duration || 0,
|
|
174
174
|
})
|
|
175
175
|
})
|
|
176
176
|
|
|
@@ -210,13 +210,13 @@ export default function (config) {
|
|
|
210
210
|
|
|
211
211
|
event.dispatcher.on(event.bddStep.finished, step => {
|
|
212
212
|
if (step.htmlReporterStartTime) {
|
|
213
|
-
step.
|
|
213
|
+
step.htmlReporterDuration = Date.now() - step.htmlReporterStartTime
|
|
214
214
|
}
|
|
215
215
|
currentBddSteps.push({
|
|
216
216
|
keyword: step.actor || 'Given',
|
|
217
217
|
text: step.name,
|
|
218
218
|
status: step.failed ? 'failed' : 'success',
|
|
219
|
-
duration: step.duration || 0,
|
|
219
|
+
duration: step.htmlReporterDuration || step.duration || 0,
|
|
220
220
|
comment: step.comment,
|
|
221
221
|
})
|
|
222
222
|
})
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for checking TypeScript loader availability
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Check if a TypeScript loader is available for test files
|
|
7
|
+
* Note: This checks if loaders are in the require array, not if packages are installed
|
|
8
|
+
* Package installation is checked when actually requiring modules
|
|
9
|
+
* @param {string[]} requiredModules - Array of required modules from config
|
|
10
|
+
* @returns {boolean}
|
|
11
|
+
*/
|
|
12
|
+
export function checkTypeScriptLoader(requiredModules = []) {
|
|
13
|
+
// Check if a loader is configured in the require array
|
|
14
|
+
return (
|
|
15
|
+
requiredModules.includes('tsx/esm') ||
|
|
16
|
+
requiredModules.includes('tsx/cjs') ||
|
|
17
|
+
requiredModules.includes('tsx') ||
|
|
18
|
+
requiredModules.includes('ts-node/esm') ||
|
|
19
|
+
requiredModules.includes('ts-node/register') ||
|
|
20
|
+
requiredModules.includes('ts-node')
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Generate helpful error message if .ts tests found but no loader configured
|
|
26
|
+
* @param {string[]} testFiles - Array of test file paths
|
|
27
|
+
* @returns {string|null} Error message or null if no TypeScript files
|
|
28
|
+
*/
|
|
29
|
+
export function getTypeScriptLoaderError(testFiles) {
|
|
30
|
+
const tsFiles = testFiles.filter(f => f.endsWith('.ts'))
|
|
31
|
+
|
|
32
|
+
if (tsFiles.length === 0) return null
|
|
33
|
+
|
|
34
|
+
return `
|
|
35
|
+
╔═════════════════════════════════════════════════════════════════════════════╗
|
|
36
|
+
║ ║
|
|
37
|
+
║ ⚠️ TypeScript Test Files Detected but No Loader Configured ║
|
|
38
|
+
║ ║
|
|
39
|
+
╚═════════════════════════════════════════════════════════════════════════════╝
|
|
40
|
+
|
|
41
|
+
Found ${tsFiles.length} TypeScript test file(s) but no TypeScript loader is configured.
|
|
42
|
+
|
|
43
|
+
CodeceptJS 4.x uses ES Modules (ESM) and requires a loader to run TypeScript tests.
|
|
44
|
+
|
|
45
|
+
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
46
|
+
│ Option 1: tsx (Recommended - Fast, Zero Config) │
|
|
47
|
+
└─────────────────────────────────────────────────────────────────────────────┘
|
|
48
|
+
|
|
49
|
+
Installation:
|
|
50
|
+
npm install --save-dev tsx
|
|
51
|
+
|
|
52
|
+
Configuration:
|
|
53
|
+
Add to your codecept.conf.ts or codecept.conf.js:
|
|
54
|
+
|
|
55
|
+
export const config = {
|
|
56
|
+
tests: './**/*_test.ts',
|
|
57
|
+
require: ['tsx/cjs'], // ← Add this line
|
|
58
|
+
helpers: { /* ... */ }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
Why tsx?
|
|
62
|
+
⚡ Fast: Built on esbuild
|
|
63
|
+
🎯 Zero config: No tsconfig.json required
|
|
64
|
+
✅ Works with Mocha: Uses CommonJS hooks
|
|
65
|
+
✅ Complete: Handles all TypeScript features
|
|
66
|
+
|
|
67
|
+
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
68
|
+
│ Option 2: ts-node/esm (Alternative - Established, Requires Config) │
|
|
69
|
+
└─────────────────────────────────────────────────────────────────────────────┘
|
|
70
|
+
|
|
71
|
+
Installation:
|
|
72
|
+
npm install --save-dev ts-node
|
|
73
|
+
|
|
74
|
+
Configuration:
|
|
75
|
+
1. Add to your codecept.conf.ts:
|
|
76
|
+
require: ['ts-node/esm']
|
|
77
|
+
|
|
78
|
+
2. Create tsconfig.json:
|
|
79
|
+
{
|
|
80
|
+
"compilerOptions": {
|
|
81
|
+
"module": "ESNext",
|
|
82
|
+
"target": "ES2022",
|
|
83
|
+
"moduleResolution": "node",
|
|
84
|
+
"esModuleInterop": true
|
|
85
|
+
},
|
|
86
|
+
"ts-node": {
|
|
87
|
+
"esm": true,
|
|
88
|
+
"experimentalSpecifierResolution": "node"
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
📚 Documentation: https://codecept.io/typescript
|
|
93
|
+
|
|
94
|
+
Note: TypeScript config files (codecept.conf.ts) and helpers are automatically
|
|
95
|
+
transpiled. Only test files require a loader to be configured.
|
|
96
|
+
`
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Check if user is trying to run TypeScript tests without proper loader
|
|
101
|
+
* @param {string[]} testFiles - Array of test file paths
|
|
102
|
+
* @param {string[]} requiredModules - Array of required modules from config
|
|
103
|
+
* @returns {{hasError: boolean, message: string|null}}
|
|
104
|
+
*/
|
|
105
|
+
export function validateTypeScriptSetup(testFiles, requiredModules = []) {
|
|
106
|
+
const tsFiles = testFiles.filter(f => f.endsWith('.ts'))
|
|
107
|
+
|
|
108
|
+
if (tsFiles.length === 0) {
|
|
109
|
+
// No TypeScript test files, all good
|
|
110
|
+
return { hasError: false, message: null }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Check if a loader is configured in the require array
|
|
114
|
+
const hasLoader = checkTypeScriptLoader(requiredModules)
|
|
115
|
+
|
|
116
|
+
if (hasLoader) {
|
|
117
|
+
// Loader configured, all good (package will be checked when requireModules runs)
|
|
118
|
+
return { hasError: false, message: null }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// No loader configured and TypeScript tests exist
|
|
122
|
+
const message = getTypeScriptLoaderError(testFiles)
|
|
123
|
+
return { hasError: true, message }
|
|
124
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Transpile TypeScript files to ES modules with CommonJS shim support
|
|
6
|
+
* Handles recursive transpilation of imported TypeScript files
|
|
7
|
+
*
|
|
8
|
+
* @param {string} mainFilePath - Path to the main TypeScript file to transpile
|
|
9
|
+
* @param {object} typescript - TypeScript compiler instance
|
|
10
|
+
* @returns {Promise<{tempFile: string, allTempFiles: string[]}>} - Main temp file and all temp files created
|
|
11
|
+
*/
|
|
12
|
+
export async function transpileTypeScript(mainFilePath, typescript) {
|
|
13
|
+
const { transpile } = typescript
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Transpile a single TypeScript file to JavaScript
|
|
17
|
+
* Injects CommonJS shims (require, module, exports, __dirname, __filename) as needed
|
|
18
|
+
*/
|
|
19
|
+
const transpileTS = (filePath) => {
|
|
20
|
+
const tsContent = fs.readFileSync(filePath, 'utf8')
|
|
21
|
+
|
|
22
|
+
// Transpile TypeScript to JavaScript with ES module output
|
|
23
|
+
let jsContent = transpile(tsContent, {
|
|
24
|
+
module: 99, // ModuleKind.ESNext
|
|
25
|
+
target: 99, // ScriptTarget.ESNext
|
|
26
|
+
esModuleInterop: true,
|
|
27
|
+
allowSyntheticDefaultImports: true,
|
|
28
|
+
lib: ['lib.esnext.d.ts'], // Enable latest features including top-level await
|
|
29
|
+
suppressOutputPathCheck: true,
|
|
30
|
+
skipLibCheck: true,
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
// Check if the code uses CommonJS globals
|
|
34
|
+
const usesCommonJSGlobals = /__dirname|__filename/.test(jsContent)
|
|
35
|
+
const usesRequire = /\brequire\s*\(/.test(jsContent)
|
|
36
|
+
const usesModuleExports = /\b(module\.exports|exports\.)/.test(jsContent)
|
|
37
|
+
|
|
38
|
+
if (usesCommonJSGlobals || usesRequire || usesModuleExports) {
|
|
39
|
+
// Inject ESM equivalents at the top of the file
|
|
40
|
+
let esmGlobals = ''
|
|
41
|
+
|
|
42
|
+
if (usesRequire || usesModuleExports) {
|
|
43
|
+
// IMPORTANT: Use the original .ts file path as the base for require()
|
|
44
|
+
// This ensures dynamic require() calls work with relative paths from the original file location
|
|
45
|
+
const originalFileUrl = `file://${filePath.replace(/\\/g, '/')}`
|
|
46
|
+
esmGlobals += `import { createRequire } from 'module';
|
|
47
|
+
import { extname as __extname } from 'path';
|
|
48
|
+
const __baseRequire = createRequire('${originalFileUrl}');
|
|
49
|
+
|
|
50
|
+
// Wrap require to auto-resolve extensions (mimics CommonJS behavior)
|
|
51
|
+
const require = (id) => {
|
|
52
|
+
try {
|
|
53
|
+
return __baseRequire(id);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
// If module not found and it's a relative/absolute path without extension, try common extensions
|
|
56
|
+
if (err.code === 'MODULE_NOT_FOUND' && (id.startsWith('./') || id.startsWith('../') || id.startsWith('/'))) {
|
|
57
|
+
const ext = __extname(id);
|
|
58
|
+
// Only treat known file extensions as real extensions (so names like .TEST don't block probing)
|
|
59
|
+
const __knownExts = ['.js', '.cjs', '.mjs', '.json', '.node'];
|
|
60
|
+
const hasKnownExt = ext && __knownExts.includes(ext.toLowerCase());
|
|
61
|
+
if (!hasKnownExt) {
|
|
62
|
+
// Try common extensions in order: .js, .cjs, .json, .node
|
|
63
|
+
// Note: .ts files cannot be required - they need transpilation first
|
|
64
|
+
const extensions = ['.js', '.cjs', '.json', '.node'];
|
|
65
|
+
for (const testExt of extensions) {
|
|
66
|
+
try {
|
|
67
|
+
return __baseRequire(id + testExt);
|
|
68
|
+
} catch (e) {
|
|
69
|
+
// Continue to next extension
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Re-throw original error if all attempts failed
|
|
75
|
+
throw err;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const module = { exports: {} };
|
|
80
|
+
const exports = module.exports;
|
|
81
|
+
|
|
82
|
+
`
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (usesCommonJSGlobals) {
|
|
86
|
+
// For __dirname and __filename, also use the original file path
|
|
87
|
+
const originalFileUrl = `file://${filePath.replace(/\\/g, '/')}`
|
|
88
|
+
esmGlobals += `import { fileURLToPath as __fileURLToPath } from 'url';
|
|
89
|
+
import { dirname as __dirname_fn } from 'path';
|
|
90
|
+
const __filename = '${filePath.replace(/\\/g, '/')}';
|
|
91
|
+
const __dirname = __dirname_fn(__filename);
|
|
92
|
+
|
|
93
|
+
`
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
jsContent = esmGlobals + jsContent
|
|
97
|
+
|
|
98
|
+
// If module.exports is used, we need to export it as default
|
|
99
|
+
if (usesModuleExports) {
|
|
100
|
+
jsContent += `\nexport default module.exports;\n`
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return jsContent
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Create a map to track transpiled files
|
|
108
|
+
const transpiledFiles = new Map()
|
|
109
|
+
const baseDir = path.dirname(mainFilePath)
|
|
110
|
+
|
|
111
|
+
// Recursive function to transpile a file and all its TypeScript dependencies
|
|
112
|
+
const transpileFileAndDeps = (filePath) => {
|
|
113
|
+
// Already transpiled, skip
|
|
114
|
+
if (transpiledFiles.has(filePath)) {
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Transpile this file
|
|
119
|
+
let jsContent = transpileTS(filePath)
|
|
120
|
+
|
|
121
|
+
// Find all relative TypeScript imports in this file
|
|
122
|
+
const importRegex = /from\s+['"](\..+?)(?:\.ts)?['"]/g
|
|
123
|
+
let match
|
|
124
|
+
const imports = []
|
|
125
|
+
|
|
126
|
+
while ((match = importRegex.exec(jsContent)) !== null) {
|
|
127
|
+
imports.push(match[1])
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Get the base directory for this file
|
|
131
|
+
const fileBaseDir = path.dirname(filePath)
|
|
132
|
+
|
|
133
|
+
// Recursively transpile each imported TypeScript file
|
|
134
|
+
for (const relativeImport of imports) {
|
|
135
|
+
let importedPath = path.resolve(fileBaseDir, relativeImport)
|
|
136
|
+
|
|
137
|
+
// Handle .js extensions that might actually be .ts files
|
|
138
|
+
if (importedPath.endsWith('.js')) {
|
|
139
|
+
const tsVersion = importedPath.replace(/\.js$/, '.ts')
|
|
140
|
+
if (fs.existsSync(tsVersion)) {
|
|
141
|
+
importedPath = tsVersion
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Try adding .ts extension if file doesn't exist and no extension provided
|
|
146
|
+
if (!path.extname(importedPath)) {
|
|
147
|
+
const tsPath = importedPath + '.ts'
|
|
148
|
+
if (fs.existsSync(tsPath)) {
|
|
149
|
+
importedPath = tsPath
|
|
150
|
+
} else {
|
|
151
|
+
// Try .js extension as well
|
|
152
|
+
const jsPath = importedPath + '.js'
|
|
153
|
+
if (fs.existsSync(jsPath)) {
|
|
154
|
+
// Skip .js files, they don't need transpilation
|
|
155
|
+
continue
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// If it's a TypeScript file, recursively transpile it and its dependencies
|
|
161
|
+
if (importedPath.endsWith('.ts') && fs.existsSync(importedPath)) {
|
|
162
|
+
transpileFileAndDeps(importedPath)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// After all dependencies are transpiled, rewrite imports in this file
|
|
167
|
+
jsContent = jsContent.replace(
|
|
168
|
+
/from\s+['"](\..+?)(?:\.ts)?['"]/g,
|
|
169
|
+
(match, importPath) => {
|
|
170
|
+
let resolvedPath = path.resolve(fileBaseDir, importPath)
|
|
171
|
+
|
|
172
|
+
// Handle .js extension that might be .ts
|
|
173
|
+
if (resolvedPath.endsWith('.js')) {
|
|
174
|
+
const tsVersion = resolvedPath.replace(/\.js$/, '.ts')
|
|
175
|
+
if (transpiledFiles.has(tsVersion)) {
|
|
176
|
+
const tempFile = transpiledFiles.get(tsVersion)
|
|
177
|
+
const relPath = path.relative(fileBaseDir, tempFile).replace(/\\/g, '/')
|
|
178
|
+
// Ensure the path starts with ./
|
|
179
|
+
if (!relPath.startsWith('.')) {
|
|
180
|
+
return `from './${relPath}'`
|
|
181
|
+
}
|
|
182
|
+
return `from '${relPath}'`
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Try with .ts extension
|
|
187
|
+
const tsPath = resolvedPath.endsWith('.ts') ? resolvedPath : resolvedPath + '.ts'
|
|
188
|
+
|
|
189
|
+
// If we transpiled this file, use the temp file
|
|
190
|
+
if (transpiledFiles.has(tsPath)) {
|
|
191
|
+
const tempFile = transpiledFiles.get(tsPath)
|
|
192
|
+
const relPath = path.relative(fileBaseDir, tempFile).replace(/\\/g, '/')
|
|
193
|
+
// Ensure the path starts with ./
|
|
194
|
+
if (!relPath.startsWith('.')) {
|
|
195
|
+
return `from './${relPath}'`
|
|
196
|
+
}
|
|
197
|
+
return `from '${relPath}'`
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Otherwise, keep the import as-is
|
|
201
|
+
return match
|
|
202
|
+
}
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
// Write the transpiled file with updated imports
|
|
206
|
+
const tempFile = filePath.replace(/\.ts$/, '.temp.mjs')
|
|
207
|
+
fs.writeFileSync(tempFile, jsContent)
|
|
208
|
+
transpiledFiles.set(filePath, tempFile)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Start recursive transpilation from the main file
|
|
212
|
+
transpileFileAndDeps(mainFilePath)
|
|
213
|
+
|
|
214
|
+
// Get the main transpiled file
|
|
215
|
+
const tempJsFile = transpiledFiles.get(mainFilePath)
|
|
216
|
+
|
|
217
|
+
// Store all temp files for cleanup
|
|
218
|
+
const allTempFiles = Array.from(transpiledFiles.values())
|
|
219
|
+
|
|
220
|
+
return { tempFile: tempJsFile, allTempFiles }
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Clean up temporary transpiled files
|
|
225
|
+
* @param {string[]} tempFiles - Array of temp file paths to delete
|
|
226
|
+
*/
|
|
227
|
+
export function cleanupTempFiles(tempFiles) {
|
|
228
|
+
for (const file of tempFiles) {
|
|
229
|
+
if (fs.existsSync(file)) {
|
|
230
|
+
try {
|
|
231
|
+
fs.unlinkSync(file)
|
|
232
|
+
} catch (err) {
|
|
233
|
+
// Ignore cleanup errors
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codeceptjs",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.1-beta.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Supercharged End 2 End Testing Framework for NodeJS",
|
|
6
6
|
"keywords": [
|
|
@@ -183,6 +183,7 @@
|
|
|
183
183
|
"ts-node": "10.9.2",
|
|
184
184
|
"tsd": "^0.33.0",
|
|
185
185
|
"tsd-jsdoc": "2.5.0",
|
|
186
|
+
"tsx": "^4.19.2",
|
|
186
187
|
"typedoc": "0.28.13",
|
|
187
188
|
"typedoc-plugin-markdown": "4.9.0",
|
|
188
189
|
"typescript": "5.8.3",
|
|
@@ -191,6 +192,14 @@
|
|
|
191
192
|
"xml2js": "0.6.2",
|
|
192
193
|
"xpath": "0.0.34"
|
|
193
194
|
},
|
|
195
|
+
"peerDependencies": {
|
|
196
|
+
"tsx": "^4.0.0"
|
|
197
|
+
},
|
|
198
|
+
"peerDependenciesMeta": {
|
|
199
|
+
"tsx": {
|
|
200
|
+
"optional": true
|
|
201
|
+
}
|
|
202
|
+
},
|
|
194
203
|
"engines": {
|
|
195
204
|
"node": ">=16.0",
|
|
196
205
|
"npm": ">=5.6.0"
|
|
@@ -2742,6 +2742,7 @@ declare namespace CodeceptJS {
|
|
|
2742
2742
|
* `grabStorageState({ indexedDB: true })`) IndexedDB data; treat as sensitive and do not commit.
|
|
2743
2743
|
*/
|
|
2744
2744
|
// @ts-ignore
|
|
2745
|
+
// @ts-ignore
|
|
2745
2746
|
type PlaywrightConfig = {
|
|
2746
2747
|
url?: string;
|
|
2747
2748
|
browser?: 'chromium' | 'firefox' | 'webkit' | 'electron';
|
|
@@ -6142,6 +6143,7 @@ declare namespace CodeceptJS {
|
|
|
6142
6143
|
* @property [highlightElement] - highlight the interacting elements. Default: false. Note: only activate under verbose mode (--verbose).
|
|
6143
6144
|
*/
|
|
6144
6145
|
// @ts-ignore
|
|
6146
|
+
// @ts-ignore
|
|
6145
6147
|
type PuppeteerConfig = {
|
|
6146
6148
|
url: string;
|
|
6147
6149
|
basicAuth?: any;
|
|
@@ -7987,6 +7989,7 @@ declare namespace CodeceptJS {
|
|
|
7987
7989
|
* @property [maxUploadFileSize] - set the max content file size in MB when performing api calls.
|
|
7988
7990
|
*/
|
|
7989
7991
|
// @ts-ignore
|
|
7992
|
+
// @ts-ignore
|
|
7990
7993
|
type RESTConfig = {
|
|
7991
7994
|
endpoint?: string;
|
|
7992
7995
|
prettyPrintJson?: boolean;
|
|
@@ -9143,6 +9146,7 @@ declare namespace CodeceptJS {
|
|
|
9143
9146
|
* @property [logLevel = silent] - level of logging verbosity. Default: silent. Options: trace | debug | info | warn | error | silent. More info: https://webdriver.io/docs/configuration/#loglevel
|
|
9144
9147
|
*/
|
|
9145
9148
|
// @ts-ignore
|
|
9149
|
+
// @ts-ignore
|
|
9146
9150
|
type WebDriverConfig = {
|
|
9147
9151
|
url: string;
|
|
9148
9152
|
browser: string;
|
package/typings/types.d.ts
CHANGED
|
@@ -2832,6 +2832,7 @@ declare namespace CodeceptJS {
|
|
|
2832
2832
|
* `grabStorageState({ indexedDB: true })`) IndexedDB data; treat as sensitive and do not commit.
|
|
2833
2833
|
*/
|
|
2834
2834
|
// @ts-ignore
|
|
2835
|
+
// @ts-ignore
|
|
2835
2836
|
type PlaywrightConfig = {
|
|
2836
2837
|
url?: string;
|
|
2837
2838
|
browser?: 'chromium' | 'firefox' | 'webkit' | 'electron';
|
|
@@ -6383,6 +6384,7 @@ declare namespace CodeceptJS {
|
|
|
6383
6384
|
* @property [highlightElement] - highlight the interacting elements. Default: false. Note: only activate under verbose mode (--verbose).
|
|
6384
6385
|
*/
|
|
6385
6386
|
// @ts-ignore
|
|
6387
|
+
// @ts-ignore
|
|
6386
6388
|
type PuppeteerConfig = {
|
|
6387
6389
|
url: string;
|
|
6388
6390
|
basicAuth?: any;
|
|
@@ -8364,6 +8366,7 @@ declare namespace CodeceptJS {
|
|
|
8364
8366
|
* @property [maxUploadFileSize] - set the max content file size in MB when performing api calls.
|
|
8365
8367
|
*/
|
|
8366
8368
|
// @ts-ignore
|
|
8369
|
+
// @ts-ignore
|
|
8367
8370
|
type RESTConfig = {
|
|
8368
8371
|
endpoint?: string;
|
|
8369
8372
|
prettyPrintJson?: boolean;
|
|
@@ -9580,6 +9583,7 @@ declare namespace CodeceptJS {
|
|
|
9580
9583
|
* @property [logLevel = silent] - level of logging verbosity. Default: silent. Options: trace | debug | info | warn | error | silent. More info: https://webdriver.io/docs/configuration/#loglevel
|
|
9581
9584
|
*/
|
|
9582
9585
|
// @ts-ignore
|
|
9586
|
+
// @ts-ignore
|
|
9583
9587
|
type WebDriverConfig = {
|
|
9584
9588
|
url: string;
|
|
9585
9589
|
browser: string;
|