codeceptjs 4.0.0-beta.9.esm-aria → 4.0.1-beta.2
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 +58 -12
- package/lib/helper/REST.js +2 -1
- 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
|
/**
|
|
@@ -248,10 +250,10 @@ class Container {
|
|
|
248
250
|
// Instead of using append which replaces the entire container,
|
|
249
251
|
// directly update the support object to maintain proxy references
|
|
250
252
|
Object.assign(container.support, data)
|
|
251
|
-
|
|
253
|
+
|
|
252
254
|
// Track which keys were explicitly shared
|
|
253
255
|
Object.keys(data).forEach(key => container.sharedKeys.add(key))
|
|
254
|
-
|
|
256
|
+
|
|
255
257
|
if (!options.local) {
|
|
256
258
|
WorkerStorage.share(data)
|
|
257
259
|
}
|
|
@@ -290,7 +292,7 @@ async function createHelpers(config) {
|
|
|
290
292
|
if (!HelperClass) {
|
|
291
293
|
const helperResult = requireHelperFromModule(helperName, config)
|
|
292
294
|
if (helperResult instanceof Promise) {
|
|
293
|
-
// Handle async ESM loading
|
|
295
|
+
// Handle async ESM loading - create placeholder
|
|
294
296
|
helpers[helperName] = {}
|
|
295
297
|
asyncHelperPromise = asyncHelperPromise
|
|
296
298
|
.then(() => helperResult)
|
|
@@ -309,8 +311,7 @@ async function createHelpers(config) {
|
|
|
309
311
|
|
|
310
312
|
checkHelperRequirements(ResolvedHelperClass)
|
|
311
313
|
helpers[helperName] = new ResolvedHelperClass(config[helperName])
|
|
312
|
-
|
|
313
|
-
debug(`helper ${helperName} async initialized`)
|
|
314
|
+
debug(`helper ${helperName} async loaded`)
|
|
314
315
|
})
|
|
315
316
|
continue
|
|
316
317
|
} else {
|
|
@@ -330,9 +331,8 @@ async function createHelpers(config) {
|
|
|
330
331
|
throw new Error(`Helper class from module '${helperName}' is not a class. Use CJS async module syntax.`)
|
|
331
332
|
}
|
|
332
333
|
|
|
333
|
-
debug(`helper ${helperName} async initialized`)
|
|
334
|
-
|
|
335
334
|
helpers[helperName] = new ResolvedHelperClass(config[helperName])
|
|
335
|
+
debug(`helper ${helperName} async CJS loaded`)
|
|
336
336
|
})
|
|
337
337
|
|
|
338
338
|
continue
|
|
@@ -347,9 +347,17 @@ async function createHelpers(config) {
|
|
|
347
347
|
}
|
|
348
348
|
}
|
|
349
349
|
|
|
350
|
+
// Wait for all async helpers to be fully loaded
|
|
351
|
+
await asyncHelperPromise
|
|
352
|
+
|
|
353
|
+
// Call _init on all helpers after they're all loaded
|
|
350
354
|
for (const name in helpers) {
|
|
351
|
-
if (helpers[name]._init)
|
|
355
|
+
if (helpers[name]._init) {
|
|
356
|
+
await helpers[name]._init()
|
|
357
|
+
debug(`helper ${name} _init() called`)
|
|
358
|
+
}
|
|
352
359
|
}
|
|
360
|
+
|
|
353
361
|
return helpers
|
|
354
362
|
}
|
|
355
363
|
|
|
@@ -674,12 +682,50 @@ async function loadSupportObject(modulePath, supportObjectName) {
|
|
|
674
682
|
try {
|
|
675
683
|
// Use dynamic import for both ESM and CJS modules
|
|
676
684
|
let importPath = modulePath
|
|
677
|
-
|
|
685
|
+
let tempJsFile = null
|
|
686
|
+
|
|
678
687
|
if (typeof importPath === 'string') {
|
|
679
688
|
const ext = path.extname(importPath)
|
|
680
|
-
|
|
689
|
+
|
|
690
|
+
// Handle TypeScript files
|
|
691
|
+
if (ext === '.ts') {
|
|
692
|
+
try {
|
|
693
|
+
// Use the TypeScript transpilation utility
|
|
694
|
+
const typescript = await import('typescript')
|
|
695
|
+
const { tempFile, allTempFiles } = await transpileTypeScript(importPath, typescript)
|
|
696
|
+
|
|
697
|
+
debug(`Transpiled TypeScript file: ${importPath} -> ${tempFile}`)
|
|
698
|
+
|
|
699
|
+
// Attach cleanup handler
|
|
700
|
+
importPath = tempFile
|
|
701
|
+
// Store temp files list in a way that cleanup can access them
|
|
702
|
+
tempJsFile = allTempFiles
|
|
703
|
+
} catch (tsError) {
|
|
704
|
+
throw new Error(`Failed to load TypeScript file ${importPath}: ${tsError.message}. Make sure 'typescript' package is installed.`)
|
|
705
|
+
}
|
|
706
|
+
} else if (!ext) {
|
|
707
|
+
// Append .js if no extension provided (ESM resolution requires it)
|
|
708
|
+
importPath = `${importPath}.js`
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
let obj
|
|
713
|
+
try {
|
|
714
|
+
obj = await import(importPath)
|
|
715
|
+
} catch (importError) {
|
|
716
|
+
// Clean up temp files if created before rethrowing
|
|
717
|
+
if (tempJsFile) {
|
|
718
|
+
const filesToClean = Array.isArray(tempJsFile) ? tempJsFile : [tempJsFile]
|
|
719
|
+
cleanupTempFiles(filesToClean)
|
|
720
|
+
}
|
|
721
|
+
throw importError
|
|
722
|
+
} finally {
|
|
723
|
+
// Clean up temp files if created
|
|
724
|
+
if (tempJsFile) {
|
|
725
|
+
const filesToClean = Array.isArray(tempJsFile) ? tempJsFile : [tempJsFile]
|
|
726
|
+
cleanupTempFiles(filesToClean)
|
|
727
|
+
}
|
|
681
728
|
}
|
|
682
|
-
const obj = await import(importPath)
|
|
683
729
|
|
|
684
730
|
// Handle ESM module wrapper
|
|
685
731
|
let actualObj = obj
|
package/lib/helper/REST.js
CHANGED
|
@@ -468,7 +468,8 @@ class REST extends Helper {
|
|
|
468
468
|
export { REST as default }
|
|
469
469
|
|
|
470
470
|
function curlize(request) {
|
|
471
|
-
|
|
471
|
+
// Guard access to nested properties safely in case request.data is undefined
|
|
472
|
+
if ((request.data?.constructor?.name || '').toLowerCase() === 'formdata') return 'cURL is not printed as the request body is not a JSON'
|
|
472
473
|
let curl = `curl --location --request ${request.method ? request.method.toUpperCase() : 'GET'} ${request.baseURL} `.replace("'", '')
|
|
473
474
|
|
|
474
475
|
if (request.headers) {
|
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.2",
|
|
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;
|