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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion lib/command/workers/runTests.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,9 @@ initPromise = (async function () {
// important deep merge so dynamic things e.g. functions on config are not overridden
config = deepMerge(baseConfig, overrideConfigs)

codecept = new Codecept(config, options)
// Pass workerIndex as child option for output.process() to display worker prefix
const optsWithChild = { ...options, child: workerIndex }
codecept = new Codecept(config, optsWithChild)
await codecept.init(testRoot)
codecept.loadTests()
mocha = container.mocha()
Expand Down
5 changes: 3 additions & 2 deletions lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import fs from 'fs'
import path from 'path'
import { createRequire } from 'module'
import { fileExists, isFile, deepMerge, deepClone } from './utils.js'
import { transpileTypeScript, cleanupTempFiles } from './utils/typescript.js'
import { transpileTypeScript, cleanupTempFiles, fixErrorStack } from './utils/typescript.js'

const defaultConfig = {
output: './_output',
Expand Down Expand Up @@ -159,12 +159,13 @@ async function loadConfigFile(configFile) {
try {
// Use the TypeScript transpilation utility
const typescript = require('typescript')
const { tempFile, allTempFiles } = await transpileTypeScript(configFile, typescript)
const { tempFile, allTempFiles, fileMapping } = await transpileTypeScript(configFile, typescript)

try {
configModule = await import(tempFile)
cleanupTempFiles(allTempFiles)
} catch (err) {
fixErrorStack(err, fileMapping)
cleanupTempFiles(allTempFiles)
throw err
}
Expand Down
50 changes: 45 additions & 5 deletions lib/container.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ 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 { transpileTypeScript, cleanupTempFiles } from './utils/typescript.js'
import { transpileTypeScript, cleanupTempFiles, fixErrorStack } from './utils/typescript.js'
import Translation from './translation.js'
import MochaFactory from './mocha/factory.js'
import recorder from './recorder.js'
Expand Down Expand Up @@ -34,6 +34,7 @@ let container = {
/** @type {Result | null} */
result: null,
sharedKeys: new Set(), // Track keys shared via share() function
tsFileMapping: null, // TypeScript file mapping for error stack fixing
}

/**
Expand Down Expand Up @@ -88,7 +89,7 @@ class Container {
container.support.I = mod
}
} catch (e) {
throw new Error(`Could not include object I: ${e.message}`)
throw e
}
} else {
// Create default actor - this sets up the callback in asyncHelperPromise
Expand Down Expand Up @@ -176,6 +177,15 @@ class Container {
return container.translation
}

/**
* Get TypeScript file mapping for error stack fixing
*
* @api
*/
static tsFileMapping() {
return container.tsFileMapping
}

/**
* Get Mocha instance
*
Expand Down Expand Up @@ -401,18 +411,27 @@ async function requireHelperFromModule(helperName, config, HelperClass) {
// Handle TypeScript files
let importPath = moduleName
let tempJsFile = null
let fileMapping = null
const ext = path.extname(moduleName)

if (ext === '.ts') {
try {
// Use the TypeScript transpilation utility
const typescript = await import('typescript')
const { tempFile, allTempFiles } = await transpileTypeScript(importPath, typescript)
const { tempFile, allTempFiles, fileMapping: mapping } = await transpileTypeScript(importPath, typescript)

debug(`Transpiled TypeScript helper: ${importPath} -> ${tempFile}`)

importPath = tempFile
tempJsFile = allTempFiles
fileMapping = mapping
// Store file mapping in container for runtime error fixing (merge with existing)
if (!container.tsFileMapping) {
container.tsFileMapping = new Map()
}
for (const [key, value] of mapping.entries()) {
container.tsFileMapping.set(key, value)
}
} catch (tsError) {
throw new Error(`Failed to load TypeScript helper ${importPath}: ${tsError.message}. Make sure 'typescript' package is installed.`)
}
Expand All @@ -433,6 +452,11 @@ async function requireHelperFromModule(helperName, config, HelperClass) {
cleanupTempFiles(filesToClean)
}
} catch (err) {
// Fix error stack to point to original .ts files
if (fileMapping) {
fixErrorStack(err, fileMapping)
}

// Clean up temp files before rethrowing
if (tempJsFile) {
const filesToClean = Array.isArray(tempJsFile) ? tempJsFile : [tempJsFile]
Expand Down Expand Up @@ -731,6 +755,7 @@ async function loadSupportObject(modulePath, supportObjectName) {
// Use dynamic import for both ESM and CJS modules
let importPath = modulePath
let tempJsFile = null
let fileMapping = null

if (typeof importPath === 'string') {
const ext = path.extname(importPath)
Expand All @@ -740,14 +765,22 @@ async function loadSupportObject(modulePath, supportObjectName) {
try {
// Use the TypeScript transpilation utility
const typescript = await import('typescript')
const { tempFile, allTempFiles } = await transpileTypeScript(importPath, typescript)
const { tempFile, allTempFiles, fileMapping: mapping } = await transpileTypeScript(importPath, typescript)

debug(`Transpiled TypeScript file: ${importPath} -> ${tempFile}`)

// Attach cleanup handler
importPath = tempFile
// Store temp files list in a way that cleanup can access them
tempJsFile = allTempFiles
fileMapping = mapping
// Store file mapping in container for runtime error fixing (merge with existing)
if (!container.tsFileMapping) {
container.tsFileMapping = new Map()
}
for (const [key, value] of mapping.entries()) {
container.tsFileMapping.set(key, value)
}
} catch (tsError) {
throw new Error(`Failed to load TypeScript file ${importPath}: ${tsError.message}. Make sure 'typescript' package is installed.`)
}
Expand All @@ -761,6 +794,11 @@ async function loadSupportObject(modulePath, supportObjectName) {
try {
obj = await import(importPath)
} 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]
Expand Down Expand Up @@ -809,7 +847,9 @@ async function loadSupportObject(modulePath, supportObjectName) {

throw new Error(`Support object "${supportObjectName}" should be an object, class, or function, but got ${typeof actualObj}`)
} catch (err) {
throw new Error(`Could not include object ${supportObjectName} from module '${modulePath}'\n${err.message}\n${err.stack}`)
const newErr = new Error(`Could not include object ${supportObjectName} from module '${modulePath}': ${err.message}`)
newErr.stack = err.stack
throw newErr
}
}

Expand Down
15 changes: 14 additions & 1 deletion lib/step/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,9 +147,22 @@ class Step {
line() {
const lines = this.stack.split('\n')
if (lines[STACK_LINE]) {
return lines[STACK_LINE].trim()
let line = lines[STACK_LINE].trim()
.replace(global.codecept_dir || '', '.')
.trim()

// Map .temp.mjs back to original .ts files using container's tsFileMapping
const fileMapping = global.container?.tsFileMapping?.()
if (line.includes('.temp.mjs') && fileMapping) {
for (const [tsFile, mjsFile] of fileMapping.entries()) {
if (line.includes(mjsFile)) {
line = line.replace(mjsFile, tsFile)
break
}
}
}

return line
}
return ''
}
Expand Down
8 changes: 8 additions & 0 deletions lib/step/record.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import output from '../output.js'
import store from '../store.js'
import { TIMEOUT_ORDER } from '../timeout.js'
import retryStep from './retry.js'
import { fixErrorStack } from '../utils/typescript.js'
function recordStep(step, args) {
step.status = 'queued'

Expand Down Expand Up @@ -60,6 +61,13 @@ function recordStep(step, args) {
recorder.catch(err => {
step.status = 'failed'
step.endTime = +Date.now()

// Fix error stack to point to original .ts files (lazy import to avoid circular dependency)
const fileMapping = global.container?.tsFileMapping?.()
if (fileMapping) {
fixErrorStack(err, fileMapping)
}

event.emit(event.step.failed, step, err)
event.emit(event.step.finished, step)
throw err
Expand Down
Loading