From 44cb8b703150d8228465d3880d84323f3b250e87 Mon Sep 17 00:00:00 2001 From: Luan Taraschi <130802253+luantaraschi@users.noreply.github.com> Date: Fri, 7 Aug 2026 09:16:51 -0300 Subject: [PATCH 1/3] fix(typescript): map included .ts files in step output Two adjacent blocks in container.js merge a transpile mapping after compiling TypeScript. The helper block merges into store.tsFileMapping; the include/support block merges only into container.tsFileMapping. Step.line() reads store.tsFileMapping, so a step whose stack frame points into an included page object had no entry to match and was printed with the deleted .temp.mjs sibling instead of the .ts source. Error stacks were unaffected because fixErrorStack() is handed the mapping directly. The include block now merges into store as well, mirroring the helper block two hundred lines above it. Closes #5675 --- lib/container.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/container.js b/lib/container.js index 89e80f3d8..193a42e09 100644 --- a/lib/container.js +++ b/lib/container.js @@ -892,6 +892,16 @@ async function loadSupportObject(modulePath, supportObjectName) { for (const [key, value] of mapping.entries()) { container.tsFileMapping.set(key, value) } + // Step.line() maps temp paths back through store, not through the + // container, so an include has to land there too. Without this a step + // that originates in an included .ts page object is printed with the + // deleted .temp.mjs sibling. (#5675) + if (!store.tsFileMapping) { + store.tsFileMapping = new Map() + } + for (const [key, value] of mapping.entries()) { + store.tsFileMapping.set(key, value) + } } catch (tsError) { throw new Error(`Failed to load TypeScript file ${importPath}: ${tsError.message}. Make sure 'typescript' package is installed.`) } From 59279275f354080322a86afa0f7d0071dc1e767d Mon Sep 17 00:00:00 2001 From: Luan Taraschi <130802253+luantaraschi@users.noreply.github.com> Date: Tue, 11 Aug 2026 17:23:07 -0300 Subject: [PATCH 2/3] fix(typescript): normalize included step source paths --- lib/container.js | 4 --- lib/step/base.js | 18 +++++++++-- .../typescript-step-paths/codecept.conf.js | 13 ++++++++ .../typescript-step-paths/fakeHelper.js | 11 +++++++ .../typescript-step-paths/pages/fooPage.ts | 9 ++++++ .../typescript-step-paths/tests/fooTest.ts | 6 ++++ test/runner/typescript_step_paths_test.js | 31 +++++++++++++++++++ 7 files changed, 85 insertions(+), 7 deletions(-) create mode 100644 test/data/sandbox/typescript-step-paths/codecept.conf.js create mode 100644 test/data/sandbox/typescript-step-paths/fakeHelper.js create mode 100644 test/data/sandbox/typescript-step-paths/pages/fooPage.ts create mode 100644 test/data/sandbox/typescript-step-paths/tests/fooTest.ts create mode 100644 test/runner/typescript_step_paths_test.js diff --git a/lib/container.js b/lib/container.js index 193a42e09..ca0ffcac2 100644 --- a/lib/container.js +++ b/lib/container.js @@ -892,10 +892,6 @@ async function loadSupportObject(modulePath, supportObjectName) { for (const [key, value] of mapping.entries()) { container.tsFileMapping.set(key, value) } - // Step.line() maps temp paths back through store, not through the - // container, so an include has to land there too. Without this a step - // that originates in an included .ts page object is printed with the - // deleted .temp.mjs sibling. (#5675) if (!store.tsFileMapping) { store.tsFileMapping = new Map() } diff --git a/lib/step/base.js b/lib/step/base.js index b8254ed5d..c35c53d51 100644 --- a/lib/step/base.js +++ b/lib/step/base.js @@ -1,4 +1,5 @@ import color from 'chalk' +import { pathToFileURL } from 'url' import Secret from '../secret.js' import { getCurrentTimeout } from '../timeout.js' import { ucfirst, humanizeString, serializeError } from '../utils.js' @@ -149,8 +150,6 @@ class Step { const lines = this.stack.split('\n') if (lines[STACK_LINE]) { let line = lines[STACK_LINE].trim() - .replace(store.codeceptDir || '', '.') - .trim() // Map .temp.mjs back to original .ts files using container's tsFileMapping const fileMapping = store.tsFileMapping @@ -160,10 +159,23 @@ class Step { line = line.replace(mjsFile, tsFile) break } + + const mjsFileUrl = pathToFileURL(mjsFile).href + if (line.includes(mjsFileUrl)) { + line = line.replace(mjsFileUrl, pathToFileURL(tsFile).href) + break + } } } - return line + const codeceptDir = store.codeceptDir || '' + if (codeceptDir) { + line = line + .replace(pathToFileURL(codeceptDir).href, '.') + .replace(codeceptDir, '.') + } + + return line.trim() } return '' } diff --git a/test/data/sandbox/typescript-step-paths/codecept.conf.js b/test/data/sandbox/typescript-step-paths/codecept.conf.js new file mode 100644 index 000000000..b97fafa5c --- /dev/null +++ b/test/data/sandbox/typescript-step-paths/codecept.conf.js @@ -0,0 +1,13 @@ +export const config = { + tests: './tests/*Test.ts', + helpers: { + FakeHelper: { + require: './fakeHelper.js', + }, + }, + include: { + fooPage: './pages/fooPage.ts', + }, + require: ['tsx/cjs'], + name: 'typescript-step-paths', +} diff --git a/test/data/sandbox/typescript-step-paths/fakeHelper.js b/test/data/sandbox/typescript-step-paths/fakeHelper.js new file mode 100644 index 000000000..7b6d0dd47 --- /dev/null +++ b/test/data/sandbox/typescript-step-paths/fakeHelper.js @@ -0,0 +1,11 @@ +import Helper from 'codeceptjs/lib/helper' + +export default class FakeHelper extends Helper { + doThing(label) { + return label + } + + failNow(message) { + throw new Error(message) + } +} diff --git a/test/data/sandbox/typescript-step-paths/pages/fooPage.ts b/test/data/sandbox/typescript-step-paths/pages/fooPage.ts new file mode 100644 index 000000000..b85ce8b2b --- /dev/null +++ b/test/data/sandbox/typescript-step-paths/pages/fooPage.ts @@ -0,0 +1,9 @@ +export {} + +const { I } = inject() + +export default { + open() { + I.doThing('from page') + }, +} diff --git a/test/data/sandbox/typescript-step-paths/tests/fooTest.ts b/test/data/sandbox/typescript-step-paths/tests/fooTest.ts new file mode 100644 index 000000000..063a9b279 --- /dev/null +++ b/test/data/sandbox/typescript-step-paths/tests/fooTest.ts @@ -0,0 +1,6 @@ +Feature('TypeScript step paths') + +Scenario('shows original paths', ({ I, fooPage }) => { + fooPage.open() + I.failNow('boom') +}) diff --git a/test/runner/typescript_step_paths_test.js b/test/runner/typescript_step_paths_test.js new file mode 100644 index 000000000..363eed2e0 --- /dev/null +++ b/test/runner/typescript_step_paths_test.js @@ -0,0 +1,31 @@ +import { execFile } from 'child_process' +import { expect } from 'expect' +import path from 'path' +import { fileURLToPath } from 'url' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) +const runner = path.join(__dirname, '../../bin/codecept.js') +const codeceptDir = path.join(__dirname, '../data/sandbox/typescript-step-paths') + +describe('TypeScript step paths', () => { + it('maps included page object steps back to their source file', done => { + execFile( + process.execPath, + [runner, 'run', '--config', path.join(codeceptDir, 'codecept.conf.js')], + { cwd: codeceptDir, env: { ...process.env, FORCE_COLOR: '0' } }, + (err, stdout) => { + try { + expect(err).toBeTruthy() + expect(stdout).toContain('Scenario Steps:') + expect(stdout).toMatch(/at Object\.open \(\.\/pages\/fooPage\.ts:\d+:\d+\)/) + expect(stdout).not.toContain('.temp.mjs') + expect(stdout).not.toContain('file://./pages/fooPage.ts') + done() + } catch (error) { + done(error) + } + }, + ) + }) +}) From bca1d6b1f9ca04ccda6b92ffcd6af8c1982ff63d Mon Sep 17 00:00:00 2001 From: Luan Taraschi <130802253+luantaraschi@users.noreply.github.com> Date: Wed, 12 Aug 2026 02:05:45 -0300 Subject: [PATCH 3/3] test(typescript): cover explicit tsx esm step paths --- .../codecept.esm.conf.js | 7 ++++ test/runner/typescript_step_paths_test.js | 40 ++++++++++--------- 2 files changed, 28 insertions(+), 19 deletions(-) create mode 100644 test/data/sandbox/typescript-step-paths/codecept.esm.conf.js diff --git a/test/data/sandbox/typescript-step-paths/codecept.esm.conf.js b/test/data/sandbox/typescript-step-paths/codecept.esm.conf.js new file mode 100644 index 000000000..ecc38d08a --- /dev/null +++ b/test/data/sandbox/typescript-step-paths/codecept.esm.conf.js @@ -0,0 +1,7 @@ +import { config as baseConfig } from './codecept.conf.js' + +export const config = { + ...baseConfig, + require: ['tsx/esm'], + name: 'typescript-step-paths-esm', +} diff --git a/test/runner/typescript_step_paths_test.js b/test/runner/typescript_step_paths_test.js index 363eed2e0..e829df901 100644 --- a/test/runner/typescript_step_paths_test.js +++ b/test/runner/typescript_step_paths_test.js @@ -9,23 +9,25 @@ const runner = path.join(__dirname, '../../bin/codecept.js') const codeceptDir = path.join(__dirname, '../data/sandbox/typescript-step-paths') describe('TypeScript step paths', () => { - it('maps included page object steps back to their source file', done => { - execFile( - process.execPath, - [runner, 'run', '--config', path.join(codeceptDir, 'codecept.conf.js')], - { cwd: codeceptDir, env: { ...process.env, FORCE_COLOR: '0' } }, - (err, stdout) => { - try { - expect(err).toBeTruthy() - expect(stdout).toContain('Scenario Steps:') - expect(stdout).toMatch(/at Object\.open \(\.\/pages\/fooPage\.ts:\d+:\d+\)/) - expect(stdout).not.toContain('.temp.mjs') - expect(stdout).not.toContain('file://./pages/fooPage.ts') - done() - } catch (error) { - done(error) - } - }, - ) - }) + for (const configFile of ['codecept.conf.js', 'codecept.esm.conf.js']) { + it(`maps included page object steps back to their source file with ${configFile}`, done => { + execFile( + process.execPath, + [runner, 'run', '--config', path.join(codeceptDir, configFile)], + { cwd: codeceptDir, env: { ...process.env, FORCE_COLOR: '0' } }, + (err, stdout) => { + try { + expect(err).toBeTruthy() + expect(stdout).toContain('Scenario Steps:') + expect(stdout).toMatch(/at Object\.open \(\.\/pages\/fooPage\.ts:\d+:\d+\)/) + expect(stdout).not.toContain('.temp.mjs') + expect(stdout).not.toContain('file://./pages/fooPage.ts') + done() + } catch (error) { + done(error) + } + }, + ) + }) + } })