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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
### Changed
- BREAKING CHANGE: Make `prepare()` less prescriptive ([#32](https://github.com/cucumber/javascript-core/pull/32))

## [0.7.0] - 2025-11-19
### Changed
Expand Down
11 changes: 8 additions & 3 deletions cucumber-core.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ import { IdGenerator } from '@cucumber/messages';
import { NamingStrategy } from '@cucumber/query';
import parse from '@cucumber/tag-expressions';
import { Pickle } from '@cucumber/messages';
import { PickleDocString } from '@cucumber/messages';
import { PickleStep } from '@cucumber/messages';
import { PickleTable } from '@cucumber/messages';
import { RegularExpression } from '@cucumber/cucumber-expressions';
import { SourceReference } from '@cucumber/messages';
import { StepDefinition } from '@cucumber/messages';
Expand Down Expand Up @@ -51,7 +53,7 @@ export interface AssembledTestStep {
prefix: string;
body: string;
};
prepare(thisArg?: unknown): PreparedFunction;
prepare(): PreparedStep;
sourceReference: SourceReference;
toMessage(): TestStep;
}
Expand All @@ -62,6 +64,7 @@ export function buildSupportCode(options?: SupportCodeOptions): SupportCodeBuild
// @public
export class DataTable {
constructor(cells: ReadonlyArray<ReadonlyArray<string>>);
static from(pickleTable: PickleTable): DataTable;
hashes(): ReadonlyArray<Record<string, string>>;
list(): ReadonlyArray<string>;
raw(): ReadonlyArray<ReadonlyArray<string>>;
Expand Down Expand Up @@ -156,9 +159,11 @@ export interface NewTestRunHook {
}

// @public
export type PreparedFunction = {
export type PreparedStep = {
fn: SupportCodeFunction;
args: ReadonlyArray<unknown>;
args: ReadonlyArray<Argument>;
dataTable?: PickleTable;
docString?: PickleDocString;
};

// @public
Expand Down
14 changes: 14 additions & 0 deletions src/DataTable.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { PickleTable } from '@cucumber/messages'
import { expect } from 'chai'
import { describe, it } from 'mocha'

import { parseGherkin } from '../test/parseGherkin'
import { DataTable } from './DataTable'

describe('DataTable', () => {
Expand Down Expand Up @@ -103,4 +105,16 @@ describe('DataTable', () => {
])
})
})

describe('from', () => {
it('should construct directly from a PickleTable', () => {
const { pickles } = parseGherkin('datatable.feature')

const dataTable = DataTable.from(pickles[0].steps[0].argument?.dataTable as PickleTable)
expect(dataTable.raw()).to.deep.eq([
['a', 'b', 'c'],
['1', '2', '3'],
])
})
})
})
20 changes: 18 additions & 2 deletions src/DataTable.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { PickleTable } from '@cucumber/messages'

/**
* Represents the cells of a Gherkin data table associated with a test step.
* @public
* @remarks
* For steps that include a data table, an instance of this will be injected as the last
* argument to your step function.
*/
export class DataTable {
constructor(private readonly cells: ReadonlyArray<ReadonlyArray<string>>) {}
Expand Down Expand Up @@ -151,4 +151,20 @@ export class DataTable {
transpose(): DataTable {
return new DataTable(this.cells[0].map((x, i) => this.cells.map((y) => y[i])))
}

/**
* Constructs a DataTable directly from a PickleTable
*
* @example
* ```typescript
* const dataTable = DataTable.from(pickleStep.argument.dataTable)
* ```
*/
static from(pickleTable: PickleTable): DataTable {
return new DataTable(
pickleTable.rows.map((row) => {
return row.cells.map((cell) => cell.value)
})
)
}
}
119 changes: 27 additions & 92 deletions src/makeTestPlan.spec.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,17 @@
import * as fs from 'node:fs'
import * as path from 'node:path'

import { AstBuilder, compile, GherkinClassicTokenMatcher, Parser } from '@cucumber/gherkin'
import { GherkinDocument, IdGenerator, Pickle } from '@cucumber/messages'
import { IdGenerator } from '@cucumber/messages'
import { expect, use } from 'chai'
import sinon from 'sinon'
import sinonChai from 'sinon-chai'

import { parseGherkin } from '../test/parseGherkin'
import { AmbiguousError } from './AmbiguousError'
import { buildSupportCode } from './buildSupportCode'
import { DataTable } from './DataTable'
import { makeTestPlan } from './makeTestPlan'
import { UndefinedError } from './UndefinedError'

use(sinonChai)

function parseGherkin(
file: string,
newId: () => string
): { gherkinDocument: GherkinDocument; pickles: ReadonlyArray<Pickle> } {
const data = fs.readFileSync(path.join(__dirname, '..', 'testdata', file), { encoding: 'utf-8' })
const builder = new AstBuilder(newId)
const matcher = new GherkinClassicTokenMatcher()
const parser = new Parser(builder, matcher)
const uri = 'features/' + file
const gherkinDocument = {
uri,
...parser.parse(data),
}
const pickles = compile(gherkinDocument, uri, newId)
return {
gherkinDocument,
pickles,
}
}

describe('makeTestPlan', () => {
class FakeWorld {}
const testRunStartedId = 'run-id'
let newId: () => string

Expand Down Expand Up @@ -154,7 +129,7 @@ describe('makeTestPlan', () => {
}
)

expect(() => result.testCases[0].testSteps[0].prepare(undefined)).to.throw(AmbiguousError)
expect(() => result.testCases[0].testSteps[0].prepare()).to.throw(AmbiguousError)
})

it('throws if a step is undefined', () => {
Expand All @@ -169,18 +144,15 @@ describe('makeTestPlan', () => {
)

try {
result.testCases[0].testSteps[0].prepare(undefined)
result.testCases[0].testSteps[0].prepare()
} catch (err: any) {
expect(err).to.be.instanceOf(UndefinedError)
expect(err.pickleStep).to.eq(pickles[0].steps[0])
}
})

it('matches and prepares a step without parameters', () => {
let capturedThis: any
const fn = sinon.spy(function (this: any) {
capturedThis = this
})
const fn = sinon.stub()

const { gherkinDocument, pickles } = parseGherkin('minimal.feature', newId)
const supportCodeLibrary = buildSupportCode({ newId })
Expand All @@ -198,19 +170,13 @@ describe('makeTestPlan', () => {
}
)

const fakeWorld = new FakeWorld()
const prepared = result.testCases[0].testSteps[0].prepare(fakeWorld)
const prepared = result.testCases[0].testSteps[0].prepare()
expect(prepared.fn).to.eq(fn)
expect(prepared.args).to.deep.eq([])
prepared.fn()
expect(fn).to.have.been.calledWithExactly()
expect(capturedThis).to.eq(fakeWorld)
})

it('matches and prepares a step with parameters', () => {
let capturedThis: any
const fn = sinon.spy(function (this: any) {
capturedThis = this
})
const fn = sinon.stub()

const { gherkinDocument, pickles } = parseGherkin('parameters.feature', newId)
const supportCodeLibrary = buildSupportCode({ newId })
Expand All @@ -228,19 +194,13 @@ describe('makeTestPlan', () => {
}
)

const fakeWorld = new 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)
expect(capturedThis).to.eq(fakeWorld)
const prepared = result.testCases[0].testSteps[0].prepare()
expect(prepared.fn).to.eq(fn)
expect(prepared.args.map((arg) => arg.getValue(undefined))).to.deep.eq([4, 5])
})

it('matches and prepares a step with a data table', () => {
let capturedThis: any
const fn = sinon.spy(function (this: any) {
capturedThis = this
})
const fn = sinon.stub()

const { gherkinDocument, pickles } = parseGherkin('datatable.feature', newId)
const supportCodeLibrary = buildSupportCode({ newId })
Expand All @@ -258,24 +218,14 @@ describe('makeTestPlan', () => {
}
)

const fakeWorld = new FakeWorld()
const prepared = result.testCases[0].testSteps[0].prepare(fakeWorld)
expect(prepared.args).to.deep.eq([
new DataTable([
['a', 'b', 'c'],
['1', '2', '3'],
]),
])
prepared.fn(...prepared.args)
expect(fn).to.have.been.calledWithExactly(...prepared.args)
expect(capturedThis).to.eq(fakeWorld)
const prepared = result.testCases[0].testSteps[0].prepare()
expect(prepared.fn).to.eq(fn)
expect(prepared.args).to.deep.eq([])
expect(prepared.dataTable).to.eq(pickles[0].steps[0].argument?.dataTable)
})

it('matches and prepares a step with a doc string', () => {
let capturedThis: any
const fn = sinon.spy(function (this: any) {
capturedThis = this
})
const fn = sinon.stub()

const { gherkinDocument, pickles } = parseGherkin('docstring.feature', newId)
const supportCodeLibrary = buildSupportCode({ newId })
Expand All @@ -293,12 +243,10 @@ describe('makeTestPlan', () => {
}
)

const fakeWorld = new 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)
expect(capturedThis).to.eq(fakeWorld)
const prepared = result.testCases[0].testSteps[0].prepare()
expect(prepared.fn).to.eq(fn)
expect(prepared.args).to.deep.eq([])
expect(prepared.docString).to.eq(pickles[0].steps[0].argument?.docString)
})
})

Expand Down Expand Up @@ -472,11 +420,7 @@ describe('makeTestPlan', () => {
})

it('prepares Before hooks for execution', () => {
let capturedThis: any
const fn = sinon.spy(function (this: any) {
capturedThis = this
})

const fn = sinon.stub()
const { gherkinDocument, pickles } = parseGherkin('minimal.feature', newId)
const supportCodeLibrary = buildSupportCode({ newId })
.beforeHook({
Expand All @@ -497,19 +441,13 @@ describe('makeTestPlan', () => {
}
)

const fakeWorld = new FakeWorld()
const prepared = result.testCases[0].testSteps[0].prepare(fakeWorld)
const prepared = result.testCases[0].testSteps[0].prepare()
expect(prepared.fn).to.eq(fn)
expect(prepared.args).to.deep.eq([])
prepared.fn()
expect(fn).to.have.been.calledWithExactly()
expect(capturedThis).to.eq(fakeWorld)
})

it('prepares After hooks for execution', () => {
let capturedThis: any
const fn = sinon.spy(function (this: any) {
capturedThis = this
})
const fn = sinon.stub()

const { gherkinDocument, pickles } = parseGherkin('minimal.feature', newId)
const supportCodeLibrary = buildSupportCode({ newId })
Expand All @@ -531,12 +469,9 @@ describe('makeTestPlan', () => {
}
)

const fakeWorld = new FakeWorld()
const prepared = result.testCases[0].testSteps[3].prepare(fakeWorld)
const prepared = result.testCases[0].testSteps[3].prepare()
expect(prepared.fn).to.eq(fn)
expect(prepared.args).to.deep.eq([])
prepared.fn()
expect(fn).to.have.been.calledWithExactly()
expect(capturedThis).to.eq(fakeWorld)
})
})

Expand Down
30 changes: 9 additions & 21 deletions src/makeTestPlan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import {
} from '@cucumber/query'

import { AmbiguousError } from './AmbiguousError'
import { DataTable } from './DataTable'
import {
AssembledTestPlan,
AssembledTestStep,
Expand Down Expand Up @@ -104,9 +103,9 @@ function fromBeforeHooks(
location,
},
always: false,
prepare(thisArg) {
prepare() {
return {
fn: def.fn.bind(thisArg),
fn: def.fn,
args: [],
}
},
Expand Down Expand Up @@ -141,9 +140,9 @@ function fromAfterHooks(
location,
},
always: true,
prepare(thisArg) {
prepare() {
return {
fn: def.fn.bind(thisArg),
fn: def.fn,
args: [],
}
},
Expand Down Expand Up @@ -177,7 +176,7 @@ function fromPickleSteps(
location: step.location,
},
always: false,
prepare(thisArg) {
prepare() {
if (matched.length < 1) {
throw new UndefinedError(pickleStep)
} else if (matched.length > 1) {
Expand All @@ -187,22 +186,11 @@ function fromPickleSteps(
)
} else {
const { def, args } = matched[0]
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const allArgs: Array<any> = args.map((arg) => arg.getValue(thisArg))
if (pickleStep.argument?.dataTable) {
allArgs.push(
new DataTable(
pickleStep.argument.dataTable.rows.map((row) => {
return row.cells.map((cell) => cell.value)
})
)
)
} else if (pickleStep.argument?.docString) {
allArgs.push(pickleStep.argument.docString.content)
}
return {
fn: def.fn.bind(thisArg),
args: allArgs,
fn: def.fn,
args,
dataTable: pickleStep.argument?.dataTable,
docString: pickleStep.argument?.docString,
}
}
},
Expand Down
Loading