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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 15 additions & 13 deletions cucumber-core.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,23 +24,12 @@ export class AmbiguousError extends Error {
constructor(text: string, references: ReadonlyArray<SourceReference>);
}

// @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<AssembledStep>;
sourceReference: SourceReference;
testSteps: ReadonlyArray<AssembledTestStep>;
toMessage(): TestCase;
}

Expand All @@ -51,6 +40,19 @@ export interface AssembledTestPlan {
toEnvelopes(): ReadonlyArray<Envelope>;
}

// @public
export interface AssembledTestStep {
always: boolean;
id: string;
name: {
prefix: string;
body: string;
};
prepare(thisArg?: unknown): PreparedFunction;
sourceReference: SourceReference;
toMessage(): TestStep;
}

// @public
export function buildSupportCode(options?: SupportCodeOptions): SupportCodeBuilder;

Expand Down
70 changes: 59 additions & 11 deletions src/makeTestPlan.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -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', () => {
Expand All @@ -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()
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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'],
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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' },
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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' },
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -527,14 +527,62 @@ 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()
expect(capturedThis).to.eq(fakeWorld)
})
})

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)
Expand Down
36 changes: 28 additions & 8 deletions src/makeTestPlan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
GherkinDocument,
Group as MessagesGroup,
IdGenerator,
Location as MessagesLocation,
Pickle,
Step,
} from '@cucumber/messages'
Expand All @@ -18,8 +19,8 @@ import {
import { AmbiguousError } from './AmbiguousError'
import { DataTable } from './DataTable'
import {
AssembledStep,
AssembledTestPlan,
AssembledTestStep,
SupportCodeLibrary,
TestPlanIngredients,
TestPlanOptions,
Expand Down Expand Up @@ -48,19 +49,24 @@ 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),
steps: [
...fromBeforeHooks(pickle, supportCodeLibrary, newId),
sourceReference: {
uri: pickle.uri,
location,
},
testSteps: [
...fromBeforeHooks(pickle, location, supportCodeLibrary, newId),
...fromPickleSteps(pickle, supportCodeLibrary, newId, query),
...fromAfterHooks(pickle, supportCodeLibrary, newId),
...fromAfterHooks(pickle, location, supportCodeLibrary, newId),
],
toMessage() {
return {
id: this.id,
pickleId: pickle.id,
testSteps: this.steps.map((step) => step.toMessage()),
testSteps: this.testSteps.map((step) => step.toMessage()),
testRunStartedId,
}
},
Expand All @@ -81,16 +87,21 @@ function populateQuery(gherkinDocument: GherkinDocument, pickles: ReadonlyArray<

function fromBeforeHooks(
pickle: Pickle,
location: MessagesLocation,
supportCodeLibrary: SupportCodeLibrary,
newId: () => string
): ReadonlyArray<AssembledStep> {
): ReadonlyArray<AssembledTestStep> {
return supportCodeLibrary.findAllBeforeHooksBy(pickle.tags.map((tag) => tag.name)).map((def) => {
return {
id: newId(),
name: {
prefix: 'Before',
body: def.name ?? '',
},
sourceReference: {
uri: pickle.uri,
location,
},
always: false,
prepare(thisArg) {
return {
Expand All @@ -110,9 +121,10 @@ function fromBeforeHooks(

function fromAfterHooks(
pickle: Pickle,
location: MessagesLocation,
supportCodeLibrary: SupportCodeLibrary,
newId: () => string
): ReadonlyArray<AssembledStep> {
): ReadonlyArray<AssembledTestStep> {
return supportCodeLibrary
.findAllAfterHooksBy(pickle.tags.map((tag) => tag.name))
.toReversed()
Expand All @@ -123,6 +135,10 @@ function fromAfterHooks(
prefix: 'After',
body: def.name ?? '',
},
sourceReference: {
uri: pickle.uri,
location,
},
always: true,
prepare(thisArg) {
return {
Expand All @@ -145,7 +161,7 @@ function fromPickleSteps(
supportCodeLibrary: SupportCodeLibrary,
newId: () => string,
query: Query
): ReadonlyArray<AssembledStep> {
): ReadonlyArray<AssembledTestStep> {
return pickle.steps.map((pickleStep) => {
const step = query.findStepBy(pickleStep) as Step
const matched = supportCodeLibrary.findAllStepsBy(pickleStep.text)
Expand All @@ -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) {
Expand Down
27 changes: 19 additions & 8 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,27 +350,34 @@ 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
* 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
*/
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
Expand Down Expand Up @@ -400,9 +407,13 @@ export interface AssembledTestCase {
*/
name: string
/**
* An ordered array of steps to be executed for this test case
* 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
*/
steps: ReadonlyArray<AssembledStep>
testSteps: ReadonlyArray<AssembledTestStep>
/**
* Converts the test case to a TestCase message
*/
Expand Down
Loading