diff --git a/CHANGELOG.md b/CHANGELOG.md index 8508b72..b1d7d39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added +- Provide source maps for pickles ([#149](https://github.com/cucumber/cucumber-node/pull/149)) ## [0.5.0] - 2025-11-17 ### Added diff --git a/README.md b/README.md index 443f6d2..5a339a6 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ Then('I should have heard {string}', (t, expectedResponse) => { Finally, run `node --test` with some special arguments: ```shell -node --import @cucumber/node/bootstrap --test "features/**/*.feature" +node --enable-source-maps --import @cucumber/node/bootstrap --test "features/**/*.feature" ``` ## Running tests @@ -132,7 +132,7 @@ npm install --save-dev tsx @types/node Then, add `tsx` as another import when you run: ```shell -node --import @cucumber/node/bootstrap --import tsx --test "features/**/*.feature" +node --enable-source-maps --import @cucumber/node/bootstrap --import tsx --test "features/**/*.feature" ``` Remember to add a [`tsconfig.json`](https://www.typescriptlang.org/tsconfig/) to your project. If you're not sure what you need, [`@tsconfig/node22`](https://www.npmjs.com/package/@tsconfig/node22) is a good place to start. diff --git a/package-lock.json b/package-lock.json index dd99fef..f2d98e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,13 +9,15 @@ "version": "0.5.0", "license": "MIT", "dependencies": { + "@babel/generator": "^7.28.5", + "@babel/types": "^7.28.5", "@cucumber/ci-environment": "12.0.0", - "@cucumber/core": "0.6.0", + "@cucumber/core": "0.7.0", "@cucumber/cucumber-expressions": "18.0.1", - "@cucumber/gherkin": "36.1.0", + "@cucumber/gherkin": "37.0.0", "@cucumber/html-formatter": "22.1.0", "@cucumber/junit-xml-formatter": "0.9.0", - "@cucumber/messages": "30.1.0", + "@cucumber/messages": "31.0.0", "@cucumber/query": "^14.0.0", "@cucumber/tag-expressions": "8.0.0", "globby": "^16.0.0", @@ -23,15 +25,16 @@ "type-fest": "^5.0.0" }, "devDependencies": { - "@cucumber/compatibility-kit": "^25.0.0", + "@cucumber/compatibility-kit": "^26.0.0", "@eslint/compat": "^2.0.0", "@eslint/eslintrc": "^3.2.0", "@eslint/js": "^9.18.0", "@microsoft/api-extractor": "7.55.1", "@tsconfig/node20": "^20.1.4", + "@types/babel__generator": "^7.27.0", "@types/chai": "^5.0.1", "@types/mocha": "^10.0.10", - "@types/node": "^22.10.5", + "@types/node": "^22.13.14", "@types/stack-utils": "^2.0.3", "@typescript-eslint/eslint-plugin": "^8.25.0", "@typescript-eslint/parser": "^8.25.0", @@ -50,6 +53,68 @@ "typescript": "^5.7.3" } }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@bcoe/v8-coverage": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", @@ -67,20 +132,20 @@ "license": "MIT" }, "node_modules/@cucumber/compatibility-kit": { - "version": "25.0.0", - "resolved": "https://registry.npmjs.org/@cucumber/compatibility-kit/-/compatibility-kit-25.0.0.tgz", - "integrity": "sha512-DmB6oOWVh+0L7VRa4kq/xHIccrBezdibSHt7RAVdVX/hv1grf5+yzt44a+JTliPg5IKVEqxusmQ556sB8Tlabg==", + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/@cucumber/compatibility-kit/-/compatibility-kit-26.0.0.tgz", + "integrity": "sha512-Ts3srZcsbr0KIegDGOLITJOFzD+9qAvHkNnFcRsSa18AtrsAs9VPm7Z1ssc92nIIZWHQB+f4R7c1lVlPVbuQXA==", "dev": true, "license": "MIT" }, "node_modules/@cucumber/core": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@cucumber/core/-/core-0.6.0.tgz", - "integrity": "sha512-QnkN7f3gfnDNSQ+et9BVYCrYxmPdYoMZyQudYxQLLP/cZwsQap/a5UBuDYiD20/SVjYvxy2wCn+NKXSExUoQng==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@cucumber/core/-/core-0.7.0.tgz", + "integrity": "sha512-dEjZuQj6LmXkOvn7fphxf+t9nUpNT+KNLsmXnmmC4L/emonniNhb59FpuYEwNOhUZhJn7Irha2t4WSKATKGDyw==", "license": "MIT", "peerDependencies": { "@cucumber/cucumber-expressions": "*", - "@cucumber/messages": "*", + "@cucumber/messages": ">=31.0.0", "@cucumber/query": "*", "@cucumber/tag-expressions": "*" } @@ -96,12 +161,12 @@ } }, "node_modules/@cucumber/gherkin": { - "version": "36.1.0", - "resolved": "https://registry.npmjs.org/@cucumber/gherkin/-/gherkin-36.1.0.tgz", - "integrity": "sha512-g95sQtfr2JcHZDYYryaX3x2IOiNui34/K98VFlXXPT7EKvCg5+AJOZ90hfjVUCjA9zNFSXjMPzSLtS2Q4XnKZg==", + "version": "37.0.0", + "resolved": "https://registry.npmjs.org/@cucumber/gherkin/-/gherkin-37.0.0.tgz", + "integrity": "sha512-vKJVJ6h4HCktG870wgYUUskNpFxbFI0WmAkVLPTz1LlLwJX7/KOBqFcr2/L3u0pPoHjbLRW+IpbiXLT2T13/wg==", "license": "MIT", "dependencies": { - "@cucumber/messages": ">=19.1.4 <31" + "@cucumber/messages": ">=31.0.0 <32" } }, "node_modules/@cucumber/html-formatter": { @@ -129,9 +194,9 @@ } }, "node_modules/@cucumber/messages": { - "version": "30.1.0", - "resolved": "https://registry.npmjs.org/@cucumber/messages/-/messages-30.1.0.tgz", - "integrity": "sha512-KxnsSjHz9EGF23GeZc3BRMK2+bagt2p87mwwNfisBK7BfuyvnXJumyBQJJN4xv5SLSzBKxH3FsZnuOf8LwsHhg==", + "version": "31.0.0", + "resolved": "https://registry.npmjs.org/@cucumber/messages/-/messages-31.0.0.tgz", + "integrity": "sha512-Dqhatp4AjMsH9SREfWz3Q8nlGuwJMTW7YAW5L3OzRId86ZUEu/a8vIL1RO2c0agQefuBS2SVH9fEZ66ovrMYRA==", "license": "MIT", "peer": true, "dependencies": { @@ -925,11 +990,20 @@ "node": ">=8" } }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -939,14 +1013,12 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dev": true, + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -1420,6 +1492,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, "node_modules/@types/chai": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.0.1.tgz", @@ -1483,14 +1565,14 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.10.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz", - "integrity": "sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==", + "version": "22.19.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz", + "integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "undici-types": "~6.20.0" + "undici-types": "~6.21.0" } }, "node_modules/@types/stack-utils": { @@ -4467,6 +4549,18 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -6173,9 +6267,9 @@ } }, "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index a43f221..ec236f4 100644 --- a/package.json +++ b/package.json @@ -29,13 +29,15 @@ "test:unit": "mocha 'src/**/*.spec.ts'" }, "dependencies": { + "@babel/generator": "^7.28.5", + "@babel/types": "^7.28.5", "@cucumber/ci-environment": "12.0.0", - "@cucumber/core": "0.6.0", + "@cucumber/core": "0.7.0", "@cucumber/cucumber-expressions": "18.0.1", - "@cucumber/gherkin": "36.1.0", + "@cucumber/gherkin": "37.0.0", "@cucumber/html-formatter": "22.1.0", "@cucumber/junit-xml-formatter": "0.9.0", - "@cucumber/messages": "30.1.0", + "@cucumber/messages": "31.0.0", "@cucumber/query": "^14.0.0", "@cucumber/tag-expressions": "8.0.0", "globby": "^16.0.0", @@ -43,15 +45,16 @@ "type-fest": "^5.0.0" }, "devDependencies": { - "@cucumber/compatibility-kit": "^25.0.0", + "@cucumber/compatibility-kit": "^26.0.0", "@eslint/compat": "^2.0.0", "@eslint/eslintrc": "^3.2.0", "@eslint/js": "^9.18.0", "@microsoft/api-extractor": "7.55.1", "@tsconfig/node20": "^20.1.4", + "@types/babel__generator": "^7.27.0", "@types/chai": "^5.0.1", "@types/mocha": "^10.0.10", - "@types/node": "^22.10.5", + "@types/node": "^22.13.14", "@types/stack-utils": "^2.0.3", "@typescript-eslint/eslint-plugin": "^8.25.0", "@typescript-eslint/parser": "^8.25.0", diff --git a/src/bootstrap/generateCode.ts b/src/bootstrap/generateCode.ts new file mode 100644 index 0000000..fb01dca --- /dev/null +++ b/src/bootstrap/generateCode.ts @@ -0,0 +1,199 @@ +import { generate } from '@babel/generator' +import * as t from '@babel/types' +import { Pickle } from '@cucumber/messages' + +import { CompiledGherkin } from '../runner/index.js' +import { mapLocation } from './mapLocation.js' + +export function generateCode(filename: string, gherkin: CompiledGherkin): string { + const program = t.program([...makeImports(), makeSuite(filename, gherkin)]) + + const output = generate( + program, + { + retainLines: false, + compact: false, + sourceMaps: true, + sourceFileName: filename, + }, + gherkin.source.data + ) + + const sourceMapComment = `//# sourceMappingURL=data:application/json;base64,${Buffer.from(JSON.stringify(output.map)).toString('base64')}` + + return `${output.code}\n${sourceMapComment}` +} + +function makeImports() { + return [ + t.importDeclaration( + [ + t.importSpecifier(t.identifier('suite'), t.identifier('suite')), + t.importSpecifier(t.identifier('test'), t.identifier('test')), + ], + t.stringLiteral('node:test') + ), + t.importDeclaration( + [t.importSpecifier(t.identifier('prepare'), t.identifier('prepare'))], + t.stringLiteral('@cucumber/node/runner') + ), + ] +} + +function makeSuite(filename: string, gherkin: CompiledGherkin) { + const suiteName = gherkin.gherkinDocument.feature?.name || gherkin.gherkinDocument.uri + return t.expressionStatement( + // suite(suiteName, async () => { ... }) + t.callExpression(t.identifier('suite'), [ + t.valueToNode(suiteName), + t.arrowFunctionExpression( + [], + t.blockStatement([ + // const plan = await prepare(gherkin) + t.variableDeclaration('const', [ + t.variableDeclarator( + t.identifier('plan'), + t.awaitExpression(t.callExpression(t.identifier('prepare'), [t.valueToNode(gherkin)])) + ), + ]), + ...gherkin.pickles.flatMap((pickle, index) => makeTestCase(filename, pickle, index)), + ]), + true + ), + ]) + ) +} + +function makeTestCase(filename: string, pickle: Pickle, index: number) { + const testCaseVar = `testCase${index}` + const location = mapLocation(filename, pickle.location) + + return [ + // const testCaseN = plan.select(pickleId) + t.variableDeclaration('const', [ + t.variableDeclarator( + t.identifier(testCaseVar), + withLoc( + t.callExpression(t.memberExpression(t.identifier('plan'), t.identifier('select')), [ + t.stringLiteral(pickle.id), + ]), + location + ) + ), + ]), + // await test(testCaseN.name, async (ctx1) => { ... }) + t.expressionStatement( + t.awaitExpression( + withLoc( + t.callExpression(t.identifier('test'), [ + t.memberExpression(t.identifier(testCaseVar), t.identifier('name')), + t.arrowFunctionExpression( + [t.identifier('ctx1')], + t.blockStatement([ + // await testCaseN.setup(ctx1) + t.expressionStatement( + t.awaitExpression( + withLoc( + t.callExpression( + t.memberExpression(t.identifier(testCaseVar), t.identifier('setup')), + [t.identifier('ctx1')] + ), + location + ) + ) + ), + // for await (const testStep of testCaseN.testSteps) { ... } + t.forOfStatement( + t.variableDeclaration('const', [t.variableDeclarator(t.identifier('testStep'))]), + t.memberExpression(t.identifier(testCaseVar), t.identifier('testSteps')), + t.blockStatement([ + // await testStep.setup() + t.expressionStatement( + t.awaitExpression( + withLoc( + t.callExpression( + t.memberExpression(t.identifier('testStep'), t.identifier('setup')), + [] + ), + location + ) + ) + ), + // await ctx1.test(testStep.name, testStep.options, async (ctx2) => { ... }) + t.expressionStatement( + t.awaitExpression( + withLoc( + t.callExpression( + t.memberExpression(t.identifier('ctx1'), t.identifier('test')), + [ + t.memberExpression(t.identifier('testStep'), t.identifier('name')), + t.memberExpression(t.identifier('testStep'), t.identifier('options')), + t.arrowFunctionExpression( + [t.identifier('ctx2')], + t.blockStatement([ + // await testStep.execute(ctx2) + t.expressionStatement( + t.awaitExpression( + withLoc( + t.callExpression( + t.memberExpression( + t.identifier('testStep'), + t.identifier('execute') + ), + [t.identifier('ctx2')] + ), + location + ) + ) + ), + ]), + true + ), + ] + ), + location + ) + ) + ), + // await testStep.teardown() + t.expressionStatement( + t.awaitExpression( + withLoc( + t.callExpression( + t.memberExpression(t.identifier('testStep'), t.identifier('teardown')), + [] + ), + location + ) + ) + ), + ]), + true + ), + // await testCaseN.teardown() + t.expressionStatement( + t.awaitExpression( + withLoc( + t.callExpression( + t.memberExpression(t.identifier(testCaseVar), t.identifier('teardown')), + [] + ), + location + ) + ) + ), + ]), + true + ), + ]), + location + ) + ) + ), + ] +} + +function withLoc(node: T, loc: t.SourceLocation): T { + node.loc = loc + return node +} diff --git a/src/bootstrap/loader.ts b/src/bootstrap/loader.ts index 5885e28..d6cefb6 100644 --- a/src/bootstrap/loader.ts +++ b/src/bootstrap/loader.ts @@ -13,7 +13,7 @@ import { import { Source, SourceMediaType } from '@cucumber/messages' import { newId } from '../newId.js' -import { CompiledGherkin } from '../runner/index.js' +import { generateCode } from './generateCode.js' export const load: LoadHook = async (url, context, nextLoad) => { if (url.endsWith('.feature.md') || url.endsWith('.feature')) { @@ -39,37 +39,12 @@ export const load: LoadHook = async (url, context, nextLoad) => { ...parser.parse(data), } const pickles = compile(gherkinDocument, uri, newId) + const filename = path.basename(uri) return { format: 'module', shortCircuit: true, - source: generateCode({ source, gherkinDocument, pickles }), + source: generateCode(filename, { source, gherkinDocument, pickles }), } } return nextLoad(url) } - -function generateCode(gherkin: CompiledGherkin) { - return `import { suite, test } from 'node:test' -import { prepare } from '@cucumber/node/runner' - -suite(${JSON.stringify(gherkin.gherkinDocument.feature?.name || gherkin.gherkinDocument.uri)}, async () => { - const plan = await prepare(${JSON.stringify(gherkin)}) - ${gherkin.pickles - .map((pickle, index) => { - return `const testCase${index} = plan.select(${JSON.stringify(pickle.id)}) - await test(testCase${index}.name, async (ctx1) => { - await testCase${index}.setup(ctx1) - for (const testStep of testCase${index}.testSteps) { - await testStep.setup() - await ctx1.test(testStep.name, testStep.options, async (ctx2) => { - await testStep.execute(ctx2) - }) - await testStep.teardown() - } - await testCase${index}.teardown() - })` - }) - .join('\n')} -}) -` -} diff --git a/src/bootstrap/mapLocation.spec.ts b/src/bootstrap/mapLocation.spec.ts new file mode 100644 index 0000000..bf4a429 --- /dev/null +++ b/src/bootstrap/mapLocation.spec.ts @@ -0,0 +1,43 @@ +import { Location } from '@cucumber/messages' +import { expect } from 'chai' + +import { mapLocation } from './mapLocation.js' + +describe('mapSourceLocation', () => { + const filename = 'example.feature' + + it('maps location with both line and column', () => { + const location: Location = { line: 10, column: 5 } + const result = mapLocation(filename, location) + + expect(result).to.deep.eq({ + start: { line: 10, column: 4, index: 0 }, + end: { line: 10, column: 4, index: 0 }, + filename: 'example.feature', + identifierName: undefined, + }) + }) + + it('maps location with line but missing column', () => { + const location: Location = { line: 15 } + const result = mapLocation(filename, location) + + expect(result).to.deep.eq({ + start: { line: 15, column: 0, index: 0 }, + end: { line: 15, column: 0, index: 0 }, + filename: 'example.feature', + identifierName: undefined, + }) + }) + + it('maps with default position when location is missing', () => { + const result = mapLocation(filename) + + expect(result).to.deep.eq({ + start: { line: 1, column: 0, index: 0 }, + end: { line: 1, column: 0, index: 0 }, + filename: 'example.feature', + identifierName: undefined, + }) + }) +}) diff --git a/src/bootstrap/mapLocation.ts b/src/bootstrap/mapLocation.ts new file mode 100644 index 0000000..cf916d8 --- /dev/null +++ b/src/bootstrap/mapLocation.ts @@ -0,0 +1,23 @@ +import * as t from '@babel/types' +import { Location } from '@cucumber/messages' + +export function mapLocation(filename: string, location?: Location): t.SourceLocation { + const position = mapPosition(location) + return { + start: position, + end: position, + filename, + identifierName: undefined, + } +} + +function mapPosition(location: Location | undefined) { + if (location) { + return { line: location.line, column: location.column ? location.column - 1 : 0, index: 0 } + } + return { + line: 1, + column: 0, + index: 0, + } +} diff --git a/src/reporters/generateEnvelopes.ts b/src/reporters/generateEnvelopes.ts index 2da12a8..df52aed 100644 --- a/src/reporters/generateEnvelopes.ts +++ b/src/reporters/generateEnvelopes.ts @@ -1,3 +1,4 @@ +import { EventData } from 'node:test' import { TestEvent } from 'node:test/reporters' import { @@ -19,7 +20,7 @@ export async function* generateEnvelopes( ): AsyncGenerator { yield { meta } - const nodeFailOrPassEvents: Array = [] + const nodeFailOrPassEvents: Array = [] const testStepFinishedMessages: Array = [] const nonSuccessTestCaseStartedIds: Array = [] const testRunEnvelopes: Array = [] @@ -90,7 +91,7 @@ export async function* generateEnvelopes( yield { testRunFinished } } -function isFromHere(testLocationInfo: TestLocationInfo) { +function isFromHere(testLocationInfo: EventData.LocationInfo) { return ( testLocationInfo.file?.endsWith('.feature') || testLocationInfo.file?.endsWith('.feature.md') ) diff --git a/src/reporters/mapTestStepResult.ts b/src/reporters/mapTestStepResult.ts index c13ba0b..3d300fc 100644 --- a/src/reporters/mapTestStepResult.ts +++ b/src/reporters/mapTestStepResult.ts @@ -1,6 +1,10 @@ +import { EventData } from 'node:test' + import { TestStepResult, TestStepResultStatus, TimeConversion } from '@cucumber/messages' -export function mapTestStepResult(testEvent: TestFail | TestPass | undefined): TestStepResult { +export function mapTestStepResult( + testEvent: EventData.TestFail | EventData.TestPass | undefined +): TestStepResult { if (!testEvent) { return { duration: { diff --git a/test/integration/reporters.spec.ts b/test/integration/reporters.spec.ts index 758c921..fb2dd7b 100644 --- a/test/integration/reporters.spec.ts +++ b/test/integration/reporters.spec.ts @@ -6,6 +6,30 @@ import { TestStepResultStatus } from '@cucumber/messages' describe('Reporters', () => { describe('spec', () => { + it('correctly references pickle location when reporting errors', async () => { + const harness = await makeTestHarness() + await harness.writeFile( + 'features/foo.feature', + `Feature: a feature + Scenario: a scenario + Given a passing step + And a failing step + ` + ) + await harness.writeFile( + 'features/steps.js', + `import { Given } from '@cucumber/node' + Given('a passing step', () => {}) + Given('a failing step', () => { + throw new Error('whoops') + }) + ` + ) + const [output] = await harness.run('spec') + const sanitised = stripVTControlCharacters(output.trim()) + expect(sanitised).to.include(`test at ${path.join('features', 'foo.feature')}:2:3`) + }) + it('does not emit messages as diagnostics if no cucumber reporters', async () => { const harness = await makeTestHarness() await harness.writeFile( diff --git a/test/utils.ts b/test/utils.ts index c7e4e82..65a7bb4 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -30,6 +30,7 @@ class TestHarness { exec( [ 'node', + `--enable-source-maps`, `--import`, `@cucumber/node/bootstrap`, `--test-reporter=${reporter}`,