diff --git a/lib/container.js b/lib/container.js index 89e80f3d8..ca0ffcac2 100644 --- a/lib/container.js +++ b/lib/container.js @@ -892,6 +892,12 @@ async function loadSupportObject(modulePath, supportObjectName) { for (const [key, value] of mapping.entries()) { container.tsFileMapping.set(key, value) } + 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.`) } 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/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/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..e829df901 --- /dev/null +++ b/test/runner/typescript_step_paths_test.js @@ -0,0 +1,33 @@ +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', () => { + 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) + } + }, + ) + }) + } +})