From 6ac43f7cd7333e101a5c0db61fe18d8fb5d13494 Mon Sep 17 00:00:00 2001 From: David Goss Date: Tue, 29 Jul 2025 08:41:04 +0100 Subject: [PATCH 1/3] make naming more consistent with messages --- cucumber-core.api.md | 26 +++++++++++++------------- src/makeTestPlan.spec.ts | 22 +++++++++++----------- src/makeTestPlan.ts | 12 ++++++------ src/types.ts | 16 ++++++++-------- 4 files changed, 38 insertions(+), 38 deletions(-) diff --git a/cucumber-core.api.md b/cucumber-core.api.md index 41bd75c..b1f1e7f 100644 --- a/cucumber-core.api.md +++ b/cucumber-core.api.md @@ -24,23 +24,11 @@ export class AmbiguousError extends Error { constructor(text: string, references: ReadonlyArray); } -// @public -export interface AssembledStep { - always: boolean; - id: string; - name: { - prefix: string; - body: string; - }; - prepare(thisArg?: unknown): PreparedFunction; - toMessage(): TestStep; -} - // @public export interface AssembledTestCase { id: string; name: string; - steps: ReadonlyArray; + testSteps: ReadonlyArray; toMessage(): TestCase; } @@ -51,6 +39,18 @@ export interface AssembledTestPlan { toEnvelopes(): ReadonlyArray; } +// @public +export interface AssembledTestStep { + always: boolean; + id: string; + name: { + prefix: string; + body: string; + }; + prepare(thisArg?: unknown): PreparedFunction; + toMessage(): TestStep; +} + // @public export function buildSupportCode(options?: SupportCodeOptions): SupportCodeBuilder; diff --git a/src/makeTestPlan.spec.ts b/src/makeTestPlan.spec.ts index 62e8cd7..8acd6dc 100644 --- a/src/makeTestPlan.spec.ts +++ b/src/makeTestPlan.spec.ts @@ -154,7 +154,7 @@ describe('makeTestPlan', () => { } ) - expect(() => result.testCases[0].steps[0].prepare(undefined)).to.throw(AmbiguousError) + expect(() => result.testCases[0].testSteps[0].prepare(undefined)).to.throw(AmbiguousError) }) it('throws if a step is undefined', () => { @@ -168,7 +168,7 @@ describe('makeTestPlan', () => { } ) - expect(() => result.testCases[0].steps[0].prepare(undefined)).to.throw(UndefinedError) + expect(() => result.testCases[0].testSteps[0].prepare(undefined)).to.throw(UndefinedError) }) it('matches and prepares a step without parameters', () => { @@ -194,7 +194,7 @@ describe('makeTestPlan', () => { ) const fakeWorld = new FakeWorld() - const prepared = result.testCases[0].steps[0].prepare(fakeWorld) + const prepared = result.testCases[0].testSteps[0].prepare(fakeWorld) expect(prepared.args).to.deep.eq([]) prepared.fn() expect(fn).to.have.been.calledWithExactly() @@ -224,7 +224,7 @@ describe('makeTestPlan', () => { ) const fakeWorld = new FakeWorld() - const prepared = result.testCases[0].steps[0].prepare(fakeWorld) + const prepared = result.testCases[0].testSteps[0].prepare(fakeWorld) expect(prepared.args).to.deep.eq([4, 5]) prepared.fn(...prepared.args) expect(fn).to.have.been.calledWithExactly(...prepared.args) @@ -254,7 +254,7 @@ describe('makeTestPlan', () => { ) const fakeWorld = new FakeWorld() - const prepared = result.testCases[0].steps[0].prepare(fakeWorld) + const prepared = result.testCases[0].testSteps[0].prepare(fakeWorld) expect(prepared.args).to.deep.eq([ new DataTable([ ['a', 'b', 'c'], @@ -289,7 +289,7 @@ describe('makeTestPlan', () => { ) const fakeWorld = new FakeWorld() - const prepared = result.testCases[0].steps[0].prepare(fakeWorld) + const prepared = result.testCases[0].testSteps[0].prepare(fakeWorld) expect(prepared.args).to.deep.eq(['Hello world']) prepared.fn(...prepared.args) expect(fn).to.have.been.calledWithExactly(...prepared.args) @@ -335,7 +335,7 @@ describe('makeTestPlan', () => { } ) - expect(result.testCases[0].steps.map((step) => step.name)).to.deep.eq([ + expect(result.testCases[0].testSteps.map((step) => step.name)).to.deep.eq([ // Before hooks in definition order { prefix: 'Before', body: 'setup 1' }, { prefix: 'Before', body: 'setup 2' }, @@ -386,7 +386,7 @@ describe('makeTestPlan', () => { } ) - expect(result.testCases[0].steps.map((step) => step.always)).to.deep.eq([ + expect(result.testCases[0].testSteps.map((step) => step.always)).to.deep.eq([ // Before hooks false, false, @@ -452,7 +452,7 @@ describe('makeTestPlan', () => { } ) - expect(result.testCases[0].steps.map((step) => step.name)).to.deep.eq([ + expect(result.testCases[0].testSteps.map((step) => step.name)).to.deep.eq([ // Before hooks matched { prefix: 'Before', body: 'general setup' }, { prefix: 'Before', body: 'foo-only setup' }, @@ -493,7 +493,7 @@ describe('makeTestPlan', () => { ) const fakeWorld = new FakeWorld() - const prepared = result.testCases[0].steps[0].prepare(fakeWorld) + const prepared = result.testCases[0].testSteps[0].prepare(fakeWorld) expect(prepared.args).to.deep.eq([]) prepared.fn() expect(fn).to.have.been.calledWithExactly() @@ -527,7 +527,7 @@ describe('makeTestPlan', () => { ) const fakeWorld = new FakeWorld() - const prepared = result.testCases[0].steps[3].prepare(fakeWorld) + const prepared = result.testCases[0].testSteps[3].prepare(fakeWorld) expect(prepared.args).to.deep.eq([]) prepared.fn() expect(fn).to.have.been.calledWithExactly() diff --git a/src/makeTestPlan.ts b/src/makeTestPlan.ts index 7a81da2..85b3836 100644 --- a/src/makeTestPlan.ts +++ b/src/makeTestPlan.ts @@ -18,8 +18,8 @@ import { import { AmbiguousError } from './AmbiguousError' import { DataTable } from './DataTable' import { - AssembledStep, AssembledTestPlan, + AssembledTestStep, SupportCodeLibrary, TestPlanIngredients, TestPlanOptions, @@ -51,7 +51,7 @@ export function makeTestPlan( return { id: newId(), name: strategy.reduce(lineage, pickle), - steps: [ + testSteps: [ ...fromBeforeHooks(pickle, supportCodeLibrary, newId), ...fromPickleSteps(pickle, supportCodeLibrary, newId, query), ...fromAfterHooks(pickle, supportCodeLibrary, newId), @@ -60,7 +60,7 @@ export function makeTestPlan( return { id: this.id, pickleId: pickle.id, - testSteps: this.steps.map((step) => step.toMessage()), + testSteps: this.testSteps.map((step) => step.toMessage()), testRunStartedId, } }, @@ -83,7 +83,7 @@ function fromBeforeHooks( pickle: Pickle, supportCodeLibrary: SupportCodeLibrary, newId: () => string -): ReadonlyArray { +): ReadonlyArray { return supportCodeLibrary.findAllBeforeHooksBy(pickle.tags.map((tag) => tag.name)).map((def) => { return { id: newId(), @@ -112,7 +112,7 @@ function fromAfterHooks( pickle: Pickle, supportCodeLibrary: SupportCodeLibrary, newId: () => string -): ReadonlyArray { +): ReadonlyArray { return supportCodeLibrary .findAllAfterHooksBy(pickle.tags.map((tag) => tag.name)) .toReversed() @@ -145,7 +145,7 @@ function fromPickleSteps( supportCodeLibrary: SupportCodeLibrary, newId: () => string, query: Query -): ReadonlyArray { +): ReadonlyArray { return pickle.steps.map((pickleStep) => { const step = query.findStepBy(pickleStep) as Step const matched = supportCodeLibrary.findAllStepsBy(pickleStep.text) diff --git a/src/types.ts b/src/types.ts index 55f1929..5fcaca7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -350,27 +350,27 @@ export type PreparedFunction = { } /** - * A step that belongs to an {@link AssembledTestCase} + * A test step that belongs to an {@link AssembledTestCase} * @public */ -export interface AssembledStep { +export interface AssembledTestStep { /** - * A unique identifier for this step + * A unique identifier for this test step */ id: string /** - * A non-unique name for this step + * A non-unique name for this test step */ name: { prefix: string body: string } /** - * Whether this step should always be executed even if preceding steps fail + * Whether this test step should always be executed even if preceding ones fail */ always: boolean /** - * Prepare the step for execution and return the prepared function and arguments + * Prepare the test step for execution and return the prepared function and arguments * @param thisArg - the value to bound as `this` on the function * @remarks * For pickle steps, preparation includes finding matching step definitions from @@ -400,9 +400,9 @@ export interface AssembledTestCase { */ name: string /** - * An ordered array of steps to be executed for this test case + * An ordered array of test steps to be executed for this test case */ - steps: ReadonlyArray + testSteps: ReadonlyArray /** * Converts the test case to a TestCase message */ From 7a1c0e3d6e9f436f87bcb31e99b4ea92f2d86137 Mon Sep 17 00:00:00 2001 From: David Goss Date: Tue, 29 Jul 2025 08:43:06 +0100 Subject: [PATCH 2/3] add sourceReference to test cases and test steps --- cucumber-core.api.md | 2 ++ src/makeTestPlan.spec.ts | 48 ++++++++++++++++++++++++++++++++++++++++ src/makeTestPlan.ts | 24 ++++++++++++++++++-- src/types.ts | 11 +++++++++ 4 files changed, 83 insertions(+), 2 deletions(-) diff --git a/cucumber-core.api.md b/cucumber-core.api.md index b1f1e7f..caedc90 100644 --- a/cucumber-core.api.md +++ b/cucumber-core.api.md @@ -28,6 +28,7 @@ export class AmbiguousError extends Error { export interface AssembledTestCase { id: string; name: string; + sourceReference: SourceReference; testSteps: ReadonlyArray; toMessage(): TestCase; } @@ -48,6 +49,7 @@ export interface AssembledTestStep { body: string; }; prepare(thisArg?: unknown): PreparedFunction; + sourceReference: SourceReference; toMessage(): TestStep; } diff --git a/src/makeTestPlan.spec.ts b/src/makeTestPlan.spec.ts index 8acd6dc..c52751f 100644 --- a/src/makeTestPlan.spec.ts +++ b/src/makeTestPlan.spec.ts @@ -535,6 +535,54 @@ describe('makeTestPlan', () => { }) }) + describe('source references', () => { + it('includes correct source references with test cases and test steps', () => { + const { gherkinDocument, pickles } = parseGherkin('minimal.feature', newId) + const supportCodeLibrary = buildSupportCode({ newId }) + .beforeHook({ + fn: sinon.stub(), + sourceReference: { uri: 'hooks.js', location: { line: 1, column: 1 } }, + }) + .step({ + pattern: 'a step', + fn: sinon.stub(), + sourceReference: { uri: 'steps.js', location: { line: 1, column: 1 } }, + }) + .build() + + const result = makeTestPlan( + { testRunStartedId, gherkinDocument, pickles, supportCodeLibrary }, + { + newId, + } + ) + // sourceReference on test case is for scenario/example + expect(result.testCases[0].sourceReference).to.deep.eq({ + uri: 'features/minimal.feature', + location: { + line: 2, + column: 3, + }, + }) + // sourceReference on hook step is for the scenario/example + expect(result.testCases[0].testSteps[0].sourceReference).to.deep.eq({ + uri: 'features/minimal.feature', + location: { + line: 2, + column: 3, + }, + }) + // sourceReference on pickle step is for the gherkin step + expect(result.testCases[0].testSteps[1].sourceReference).to.deep.eq({ + uri: 'features/minimal.feature', + location: { + line: 3, + column: 5, + }, + }) + }) + }) + describe('messages', () => { it('produces the correct envelopes', () => { const { gherkinDocument, pickles } = parseGherkin('parameters.feature', newId) diff --git a/src/makeTestPlan.ts b/src/makeTestPlan.ts index 85b3836..afebab1 100644 --- a/src/makeTestPlan.ts +++ b/src/makeTestPlan.ts @@ -3,6 +3,7 @@ import { GherkinDocument, Group as MessagesGroup, IdGenerator, + Location as MessagesLocation, Pickle, Step, } from '@cucumber/messages' @@ -48,13 +49,18 @@ export function makeTestPlan( name: gherkinDocument.feature?.name || gherkinDocument.uri, testCases: pickles.map((pickle) => { const lineage = query.findLineageBy(pickle) as Lineage + const location = query.findLocationOf(pickle) as MessagesLocation return { id: newId(), name: strategy.reduce(lineage, pickle), + sourceReference: { + uri: pickle.uri, + location, + }, testSteps: [ - ...fromBeforeHooks(pickle, supportCodeLibrary, newId), + ...fromBeforeHooks(pickle, location, supportCodeLibrary, newId), ...fromPickleSteps(pickle, supportCodeLibrary, newId, query), - ...fromAfterHooks(pickle, supportCodeLibrary, newId), + ...fromAfterHooks(pickle, location, supportCodeLibrary, newId), ], toMessage() { return { @@ -81,6 +87,7 @@ function populateQuery(gherkinDocument: GherkinDocument, pickles: ReadonlyArray< function fromBeforeHooks( pickle: Pickle, + location: MessagesLocation, supportCodeLibrary: SupportCodeLibrary, newId: () => string ): ReadonlyArray { @@ -91,6 +98,10 @@ function fromBeforeHooks( prefix: 'Before', body: def.name ?? '', }, + sourceReference: { + uri: pickle.uri, + location, + }, always: false, prepare(thisArg) { return { @@ -110,6 +121,7 @@ function fromBeforeHooks( function fromAfterHooks( pickle: Pickle, + location: MessagesLocation, supportCodeLibrary: SupportCodeLibrary, newId: () => string ): ReadonlyArray { @@ -123,6 +135,10 @@ function fromAfterHooks( prefix: 'After', body: def.name ?? '', }, + sourceReference: { + uri: pickle.uri, + location, + }, always: true, prepare(thisArg) { return { @@ -155,6 +171,10 @@ function fromPickleSteps( prefix: step.keyword.trim(), body: pickleStep.text, }, + sourceReference: { + uri: pickle.uri, + location: step.location, + }, always: false, prepare(thisArg) { if (matched.length < 1) { diff --git a/src/types.ts b/src/types.ts index 5fcaca7..e94a847 100644 --- a/src/types.ts +++ b/src/types.ts @@ -365,6 +365,13 @@ export interface AssembledTestStep { prefix: string body: string } + /** + * A reference to the source of this test step in the Gherkin document + * @remarks + * For pickle steps, this will be the line the step is on. For hook steps, + * this will be the line the pickle is on. + */ + sourceReference: SourceReference /** * Whether this test step should always be executed even if preceding ones fail */ @@ -399,6 +406,10 @@ export interface AssembledTestCase { * This could vary depending on the supplied naming strategy. */ name: string + /** + * A reference to the source of this test case in the Gherkin document + */ + sourceReference: SourceReference /** * An ordered array of test steps to be executed for this test case */ From ef250043690d15c62b192e446be8805ba0fdeda9 Mon Sep 17 00:00:00 2001 From: David Goss Date: Wed, 30 Jul 2025 13:39:43 +0100 Subject: [PATCH 3/3] changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8b8381..bbfcb73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ 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 +- Add `sourceReference` to test cases and test steps in plan ([#5](https://github.com/cucumber/javascript-core/pull/5)) + +### Changed +- BREAKING CHANGE: Rename `AssembledStep` to `AssembledTestStep` ([#5](https://github.com/cucumber/javascript-core/pull/5)) ## [0.2.1] - 2025-07-24 ### Fixed