Skip to content

Commit aaac0f1

Browse files
authored
Make prepare() less prescriptive (#32)
1 parent 3a0eee8 commit aaac0f1

File tree

8 files changed

+127
-130
lines changed

8 files changed

+127
-130
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

88
## [Unreleased]
9+
### Changed
10+
- BREAKING CHANGE: Make `prepare()` less prescriptive ([#32](https://github.com/cucumber/javascript-core/pull/32))
911

1012
## [0.7.0] - 2025-11-19
1113
### Changed

cucumber-core.api.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ import { IdGenerator } from '@cucumber/messages';
1414
import { NamingStrategy } from '@cucumber/query';
1515
import parse from '@cucumber/tag-expressions';
1616
import { Pickle } from '@cucumber/messages';
17+
import { PickleDocString } from '@cucumber/messages';
1718
import { PickleStep } from '@cucumber/messages';
19+
import { PickleTable } from '@cucumber/messages';
1820
import { RegularExpression } from '@cucumber/cucumber-expressions';
1921
import { SourceReference } from '@cucumber/messages';
2022
import { StepDefinition } from '@cucumber/messages';
@@ -51,7 +53,7 @@ export interface AssembledTestStep {
5153
prefix: string;
5254
body: string;
5355
};
54-
prepare(thisArg?: unknown): PreparedFunction;
56+
prepare(): PreparedStep;
5557
sourceReference: SourceReference;
5658
toMessage(): TestStep;
5759
}
@@ -62,6 +64,7 @@ export function buildSupportCode(options?: SupportCodeOptions): SupportCodeBuild
6264
// @public
6365
export class DataTable {
6466
constructor(cells: ReadonlyArray<ReadonlyArray<string>>);
67+
static from(pickleTable: PickleTable): DataTable;
6568
hashes(): ReadonlyArray<Record<string, string>>;
6669
list(): ReadonlyArray<string>;
6770
raw(): ReadonlyArray<ReadonlyArray<string>>;
@@ -156,9 +159,11 @@ export interface NewTestRunHook {
156159
}
157160

158161
// @public
159-
export type PreparedFunction = {
162+
export type PreparedStep = {
160163
fn: SupportCodeFunction;
161-
args: ReadonlyArray<unknown>;
164+
args: ReadonlyArray<Argument>;
165+
dataTable?: PickleTable;
166+
docString?: PickleDocString;
162167
};
163168

164169
// @public

src/DataTable.spec.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import { PickleTable } from '@cucumber/messages'
12
import { expect } from 'chai'
23
import { describe, it } from 'mocha'
34

5+
import { parseGherkin } from '../test/parseGherkin'
46
import { DataTable } from './DataTable'
57

68
describe('DataTable', () => {
@@ -103,4 +105,16 @@ describe('DataTable', () => {
103105
])
104106
})
105107
})
108+
109+
describe('from', () => {
110+
it('should construct directly from a PickleTable', () => {
111+
const { pickles } = parseGherkin('datatable.feature')
112+
113+
const dataTable = DataTable.from(pickles[0].steps[0].argument?.dataTable as PickleTable)
114+
expect(dataTable.raw()).to.deep.eq([
115+
['a', 'b', 'c'],
116+
['1', '2', '3'],
117+
])
118+
})
119+
})
106120
})

src/DataTable.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1+
import { PickleTable } from '@cucumber/messages'
2+
13
/**
24
* Represents the cells of a Gherkin data table associated with a test step.
35
* @public
46
* @remarks
5-
* For steps that include a data table, an instance of this will be injected as the last
6-
* argument to your step function.
77
*/
88
export class DataTable {
99
constructor(private readonly cells: ReadonlyArray<ReadonlyArray<string>>) {}
@@ -151,4 +151,20 @@ export class DataTable {
151151
transpose(): DataTable {
152152
return new DataTable(this.cells[0].map((x, i) => this.cells.map((y) => y[i])))
153153
}
154+
155+
/**
156+
* Constructs a DataTable directly from a PickleTable
157+
*
158+
* @example
159+
* ```typescript
160+
* const dataTable = DataTable.from(pickleStep.argument.dataTable)
161+
* ```
162+
*/
163+
static from(pickleTable: PickleTable): DataTable {
164+
return new DataTable(
165+
pickleTable.rows.map((row) => {
166+
return row.cells.map((cell) => cell.value)
167+
})
168+
)
169+
}
154170
}

src/makeTestPlan.spec.ts

Lines changed: 27 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,17 @@
1-
import * as fs from 'node:fs'
2-
import * as path from 'node:path'
3-
4-
import { AstBuilder, compile, GherkinClassicTokenMatcher, Parser } from '@cucumber/gherkin'
5-
import { GherkinDocument, IdGenerator, Pickle } from '@cucumber/messages'
1+
import { IdGenerator } from '@cucumber/messages'
62
import { expect, use } from 'chai'
73
import sinon from 'sinon'
84
import sinonChai from 'sinon-chai'
95

6+
import { parseGherkin } from '../test/parseGherkin'
107
import { AmbiguousError } from './AmbiguousError'
118
import { buildSupportCode } from './buildSupportCode'
12-
import { DataTable } from './DataTable'
139
import { makeTestPlan } from './makeTestPlan'
1410
import { UndefinedError } from './UndefinedError'
1511

1612
use(sinonChai)
1713

18-
function parseGherkin(
19-
file: string,
20-
newId: () => string
21-
): { gherkinDocument: GherkinDocument; pickles: ReadonlyArray<Pickle> } {
22-
const data = fs.readFileSync(path.join(__dirname, '..', 'testdata', file), { encoding: 'utf-8' })
23-
const builder = new AstBuilder(newId)
24-
const matcher = new GherkinClassicTokenMatcher()
25-
const parser = new Parser(builder, matcher)
26-
const uri = 'features/' + file
27-
const gherkinDocument = {
28-
uri,
29-
...parser.parse(data),
30-
}
31-
const pickles = compile(gherkinDocument, uri, newId)
32-
return {
33-
gherkinDocument,
34-
pickles,
35-
}
36-
}
37-
3814
describe('makeTestPlan', () => {
39-
class FakeWorld {}
4015
const testRunStartedId = 'run-id'
4116
let newId: () => string
4217

@@ -154,7 +129,7 @@ describe('makeTestPlan', () => {
154129
}
155130
)
156131

157-
expect(() => result.testCases[0].testSteps[0].prepare(undefined)).to.throw(AmbiguousError)
132+
expect(() => result.testCases[0].testSteps[0].prepare()).to.throw(AmbiguousError)
158133
})
159134

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

171146
try {
172-
result.testCases[0].testSteps[0].prepare(undefined)
147+
result.testCases[0].testSteps[0].prepare()
173148
} catch (err: any) {
174149
expect(err).to.be.instanceOf(UndefinedError)
175150
expect(err.pickleStep).to.eq(pickles[0].steps[0])
176151
}
177152
})
178153

179154
it('matches and prepares a step without parameters', () => {
180-
let capturedThis: any
181-
const fn = sinon.spy(function (this: any) {
182-
capturedThis = this
183-
})
155+
const fn = sinon.stub()
184156

185157
const { gherkinDocument, pickles } = parseGherkin('minimal.feature', newId)
186158
const supportCodeLibrary = buildSupportCode({ newId })
@@ -198,19 +170,13 @@ describe('makeTestPlan', () => {
198170
}
199171
)
200172

201-
const fakeWorld = new FakeWorld()
202-
const prepared = result.testCases[0].testSteps[0].prepare(fakeWorld)
173+
const prepared = result.testCases[0].testSteps[0].prepare()
174+
expect(prepared.fn).to.eq(fn)
203175
expect(prepared.args).to.deep.eq([])
204-
prepared.fn()
205-
expect(fn).to.have.been.calledWithExactly()
206-
expect(capturedThis).to.eq(fakeWorld)
207176
})
208177

209178
it('matches and prepares a step with parameters', () => {
210-
let capturedThis: any
211-
const fn = sinon.spy(function (this: any) {
212-
capturedThis = this
213-
})
179+
const fn = sinon.stub()
214180

215181
const { gherkinDocument, pickles } = parseGherkin('parameters.feature', newId)
216182
const supportCodeLibrary = buildSupportCode({ newId })
@@ -228,19 +194,13 @@ describe('makeTestPlan', () => {
228194
}
229195
)
230196

231-
const fakeWorld = new FakeWorld()
232-
const prepared = result.testCases[0].testSteps[0].prepare(fakeWorld)
233-
expect(prepared.args).to.deep.eq([4, 5])
234-
prepared.fn(...prepared.args)
235-
expect(fn).to.have.been.calledWithExactly(...prepared.args)
236-
expect(capturedThis).to.eq(fakeWorld)
197+
const prepared = result.testCases[0].testSteps[0].prepare()
198+
expect(prepared.fn).to.eq(fn)
199+
expect(prepared.args.map((arg) => arg.getValue(undefined))).to.deep.eq([4, 5])
237200
})
238201

239202
it('matches and prepares a step with a data table', () => {
240-
let capturedThis: any
241-
const fn = sinon.spy(function (this: any) {
242-
capturedThis = this
243-
})
203+
const fn = sinon.stub()
244204

245205
const { gherkinDocument, pickles } = parseGherkin('datatable.feature', newId)
246206
const supportCodeLibrary = buildSupportCode({ newId })
@@ -258,24 +218,14 @@ describe('makeTestPlan', () => {
258218
}
259219
)
260220

261-
const fakeWorld = new FakeWorld()
262-
const prepared = result.testCases[0].testSteps[0].prepare(fakeWorld)
263-
expect(prepared.args).to.deep.eq([
264-
new DataTable([
265-
['a', 'b', 'c'],
266-
['1', '2', '3'],
267-
]),
268-
])
269-
prepared.fn(...prepared.args)
270-
expect(fn).to.have.been.calledWithExactly(...prepared.args)
271-
expect(capturedThis).to.eq(fakeWorld)
221+
const prepared = result.testCases[0].testSteps[0].prepare()
222+
expect(prepared.fn).to.eq(fn)
223+
expect(prepared.args).to.deep.eq([])
224+
expect(prepared.dataTable).to.eq(pickles[0].steps[0].argument?.dataTable)
272225
})
273226

274227
it('matches and prepares a step with a doc string', () => {
275-
let capturedThis: any
276-
const fn = sinon.spy(function (this: any) {
277-
capturedThis = this
278-
})
228+
const fn = sinon.stub()
279229

280230
const { gherkinDocument, pickles } = parseGherkin('docstring.feature', newId)
281231
const supportCodeLibrary = buildSupportCode({ newId })
@@ -293,12 +243,10 @@ describe('makeTestPlan', () => {
293243
}
294244
)
295245

296-
const fakeWorld = new FakeWorld()
297-
const prepared = result.testCases[0].testSteps[0].prepare(fakeWorld)
298-
expect(prepared.args).to.deep.eq(['Hello world'])
299-
prepared.fn(...prepared.args)
300-
expect(fn).to.have.been.calledWithExactly(...prepared.args)
301-
expect(capturedThis).to.eq(fakeWorld)
246+
const prepared = result.testCases[0].testSteps[0].prepare()
247+
expect(prepared.fn).to.eq(fn)
248+
expect(prepared.args).to.deep.eq([])
249+
expect(prepared.docString).to.eq(pickles[0].steps[0].argument?.docString)
302250
})
303251
})
304252

@@ -472,11 +420,7 @@ describe('makeTestPlan', () => {
472420
})
473421

474422
it('prepares Before hooks for execution', () => {
475-
let capturedThis: any
476-
const fn = sinon.spy(function (this: any) {
477-
capturedThis = this
478-
})
479-
423+
const fn = sinon.stub()
480424
const { gherkinDocument, pickles } = parseGherkin('minimal.feature', newId)
481425
const supportCodeLibrary = buildSupportCode({ newId })
482426
.beforeHook({
@@ -497,19 +441,13 @@ describe('makeTestPlan', () => {
497441
}
498442
)
499443

500-
const fakeWorld = new FakeWorld()
501-
const prepared = result.testCases[0].testSteps[0].prepare(fakeWorld)
444+
const prepared = result.testCases[0].testSteps[0].prepare()
445+
expect(prepared.fn).to.eq(fn)
502446
expect(prepared.args).to.deep.eq([])
503-
prepared.fn()
504-
expect(fn).to.have.been.calledWithExactly()
505-
expect(capturedThis).to.eq(fakeWorld)
506447
})
507448

508449
it('prepares After hooks for execution', () => {
509-
let capturedThis: any
510-
const fn = sinon.spy(function (this: any) {
511-
capturedThis = this
512-
})
450+
const fn = sinon.stub()
513451

514452
const { gherkinDocument, pickles } = parseGherkin('minimal.feature', newId)
515453
const supportCodeLibrary = buildSupportCode({ newId })
@@ -531,12 +469,9 @@ describe('makeTestPlan', () => {
531469
}
532470
)
533471

534-
const fakeWorld = new FakeWorld()
535-
const prepared = result.testCases[0].testSteps[3].prepare(fakeWorld)
472+
const prepared = result.testCases[0].testSteps[3].prepare()
473+
expect(prepared.fn).to.eq(fn)
536474
expect(prepared.args).to.deep.eq([])
537-
prepared.fn()
538-
expect(fn).to.have.been.calledWithExactly()
539-
expect(capturedThis).to.eq(fakeWorld)
540475
})
541476
})
542477

src/makeTestPlan.ts

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import {
1717
} from '@cucumber/query'
1818

1919
import { AmbiguousError } from './AmbiguousError'
20-
import { DataTable } from './DataTable'
2120
import {
2221
AssembledTestPlan,
2322
AssembledTestStep,
@@ -104,9 +103,9 @@ function fromBeforeHooks(
104103
location,
105104
},
106105
always: false,
107-
prepare(thisArg) {
106+
prepare() {
108107
return {
109-
fn: def.fn.bind(thisArg),
108+
fn: def.fn,
110109
args: [],
111110
}
112111
},
@@ -141,9 +140,9 @@ function fromAfterHooks(
141140
location,
142141
},
143142
always: true,
144-
prepare(thisArg) {
143+
prepare() {
145144
return {
146-
fn: def.fn.bind(thisArg),
145+
fn: def.fn,
147146
args: [],
148147
}
149148
},
@@ -177,7 +176,7 @@ function fromPickleSteps(
177176
location: step.location,
178177
},
179178
always: false,
180-
prepare(thisArg) {
179+
prepare() {
181180
if (matched.length < 1) {
182181
throw new UndefinedError(pickleStep)
183182
} else if (matched.length > 1) {
@@ -187,22 +186,11 @@ function fromPickleSteps(
187186
)
188187
} else {
189188
const { def, args } = matched[0]
190-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
191-
const allArgs: Array<any> = args.map((arg) => arg.getValue(thisArg))
192-
if (pickleStep.argument?.dataTable) {
193-
allArgs.push(
194-
new DataTable(
195-
pickleStep.argument.dataTable.rows.map((row) => {
196-
return row.cells.map((cell) => cell.value)
197-
})
198-
)
199-
)
200-
} else if (pickleStep.argument?.docString) {
201-
allArgs.push(pickleStep.argument.docString.content)
202-
}
203189
return {
204-
fn: def.fn.bind(thisArg),
205-
args: allArgs,
190+
fn: def.fn,
191+
args,
192+
dataTable: pickleStep.argument?.dataTable,
193+
docString: pickleStep.argument?.docString,
206194
}
207195
}
208196
},

0 commit comments

Comments
 (0)