From b6971720b6ff5a526bb02155a3a96cb14ed26e9e Mon Sep 17 00:00:00 2001 From: David Goss Date: Thu, 24 Sep 2026 17:06:05 +0100 Subject: [PATCH 1/3] Support steps with both a data table and a doc string --- cucumber-core.api.md | 4 ++ package-lock.json | 2 +- package.json | 2 +- src/makeTestPlan.spec.ts | 72 ++++++++++++++++++++++++++++ src/makeTestPlan.ts | 19 ++++++++ src/types.ts | 19 ++++++++ testdata/datatable-docstring.feature | 14 ++++++ 7 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 testdata/datatable-docstring.feature diff --git a/cucumber-core.api.md b/cucumber-core.api.md index 0b259a0..e03d7d3 100644 --- a/cucumber-core.api.md +++ b/cucumber-core.api.md @@ -179,8 +179,12 @@ export type PreparedStep = { args: ReadonlyArray; dataTable?: PickleTable; docString?: PickleDocString; + stepArguments: ReadonlyArray; }; +// @public +export type StepArgument = PickleTable | PickleDocString; + // @public export interface SupportCodeBuilder { afterAllHook(options: NewTestRunHook): SupportCodeBuilder; diff --git a/package-lock.json b/package-lock.json index deb80d2..d0e4ac6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@cucumber/biome-config": "github:cucumber/biome-config#v0.6.0", "@cucumber/cucumber-expressions": "^20.0.0", "@cucumber/gherkin": "^42.0.0", - "@cucumber/messages": "^34.0.0", + "@cucumber/messages": "^34.1.0", "@cucumber/query": "^16.0.0", "@cucumber/tag-expressions": "^11.0.0", "@microsoft/api-extractor": "^7.52.8", diff --git a/package.json b/package.json index d743e18..ea98e59 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "@cucumber/biome-config": "github:cucumber/biome-config#v0.6.0", "@cucumber/cucumber-expressions": "^20.0.0", "@cucumber/gherkin": "^42.0.0", - "@cucumber/messages": "^34.0.0", + "@cucumber/messages": "^34.1.0", "@cucumber/query": "^16.0.0", "@cucumber/tag-expressions": "^11.0.0", "@microsoft/api-extractor": "^7.52.8", diff --git a/src/makeTestPlan.spec.ts b/src/makeTestPlan.spec.ts index 91f8758..41537ce 100644 --- a/src/makeTestPlan.spec.ts +++ b/src/makeTestPlan.spec.ts @@ -232,6 +232,7 @@ describe('makeTestPlan', () => { expect(prepared.fn).to.eq(fn) expect(prepared.args).to.deep.eq([]) expect(prepared.dataTable).to.eq(pickles[0].steps[0].argument?.dataTable) + expect(prepared.stepArguments).to.deep.eq([pickles[0].steps[0].argument?.dataTable]) } }) @@ -260,6 +261,77 @@ describe('makeTestPlan', () => { expect(prepared.fn).to.eq(fn) expect(prepared.args).to.deep.eq([]) expect(prepared.docString).to.eq(pickles[0].steps[0].argument?.docString) + expect(prepared.stepArguments).to.deep.eq([pickles[0].steps[0].argument?.docString]) + } + }) + + it('matches and prepares a step with a data table and a doc string in source order', () => { + const fn = sinon.stub() + + const { gherkinDocument, pickles } = parseGherkin('datatable-docstring.feature', newId) + const supportCodeLibrary = buildSupportCode({ newId }) + .step({ + pattern: 'a step with both:', + fn, + sourceReference: { uri: 'steps.js', location: { line: 1, column: 1 } }, + }) + .build() + + const result = makeTestPlan( + { testRunStartedId, gherkinDocument, pickles, supportCodeLibrary }, + { + newId, + } + ) + + const tableFirst = result.testCases[0].testSteps[0].prepare() + expect(tableFirst.type).to.eq('prepared') + if (tableFirst.type === 'prepared') { + const dataTable = pickles[0].steps[0].argument?.dataTable + const docString = pickles[0].steps[0].argument?.docString + expect(tableFirst.dataTable).to.eq(dataTable) + expect(tableFirst.docString).to.eq(docString) + expect(tableFirst.stepArguments).to.deep.eq([dataTable, docString]) + } + + const docStringFirst = result.testCases[1].testSteps[0].prepare() + expect(docStringFirst.type).to.eq('prepared') + if (docStringFirst.type === 'prepared') { + const dataTable = pickles[1].steps[0].argument?.dataTable + const docString = pickles[1].steps[0].argument?.docString + expect(docStringFirst.dataTable).to.eq(dataTable) + expect(docStringFirst.docString).to.eq(docString) + expect(docStringFirst.stepArguments).to.deep.eq([docString, dataTable]) + } + }) + + it('prepares a data table before a doc string when no argument index is present', () => { + const fn = sinon.stub() + + const { gherkinDocument, pickles } = parseGherkin('datatable-docstring.feature', newId) + const dataTable = pickles[1].steps[0].argument?.dataTable + const docString = pickles[1].steps[0].argument?.docString + delete dataTable?.argumentIndex + delete docString?.argumentIndex + const supportCodeLibrary = buildSupportCode({ newId }) + .step({ + pattern: 'a step with both:', + fn, + sourceReference: { uri: 'steps.js', location: { line: 1, column: 1 } }, + }) + .build() + + const result = makeTestPlan( + { testRunStartedId, gherkinDocument, pickles, supportCodeLibrary }, + { + newId, + } + ) + + const prepared = result.testCases[1].testSteps[0].prepare() + expect(prepared.type).to.eq('prepared') + if (prepared.type === 'prepared') { + expect(prepared.stepArguments).to.deep.eq([dataTable, docString]) } }) }) diff --git a/src/makeTestPlan.ts b/src/makeTestPlan.ts index 62633e9..ca9bac7 100644 --- a/src/makeTestPlan.ts +++ b/src/makeTestPlan.ts @@ -5,6 +5,7 @@ import { type Group as MessagesGroup, type Location as MessagesLocation, type Pickle, + type PickleStepArgument, type Step, } from '@cucumber/messages' import { @@ -19,6 +20,7 @@ import { import type { AssembledTestPlan, AssembledTestStep, + StepArgument, SupportCodeLibrary, TestPlanIngredients, TestPlanOptions, @@ -108,6 +110,7 @@ function fromBeforeHooks( type: 'prepared' as const, fn: def.fn, args: [], + stepArguments: [], } }, toMessage() { @@ -146,6 +149,7 @@ function fromAfterHooks( type: 'prepared' as const, fn: def.fn, args: [], + stepArguments: [], } }, toMessage() { @@ -198,6 +202,7 @@ function fromPickleSteps( args, dataTable: pickleStep.argument?.dataTable, docString: pickleStep.argument?.docString, + stepArguments: orderStepArguments(pickleStep.argument), } } }, @@ -222,6 +227,20 @@ function fromPickleSteps( }) } +function orderStepArguments(argument?: PickleStepArgument): ReadonlyArray { + const stepArguments: Array = [] + if (argument?.dataTable) { + stepArguments.push(argument.dataTable) + } + if (argument?.docString) { + stepArguments.push(argument.docString) + } + return stepArguments.sort( + (a, b) => + (a.argumentIndex ?? Number.MAX_SAFE_INTEGER) - (b.argumentIndex ?? Number.MAX_SAFE_INTEGER) + ) +} + function mapArgumentGroup(group: ExpressionsGroup): MessagesGroup { if (!group.children) { return { diff --git a/src/types.ts b/src/types.ts index 1d245b0..4a1f0bc 100644 --- a/src/types.ts +++ b/src/types.ts @@ -462,8 +462,27 @@ export type PreparedStep = { * The doc string to pass to the step, if there is one */ docString?: PickleDocString + /** + * The data table and/or doc string to pass to the step, in the order they + * should be passed + * @remarks + * This follows the order they were declared in the source, per their + * `argumentIndex`. Where that is not present, a data table comes before a + * doc string. + */ + stepArguments: ReadonlyArray } +/** + * A data table or doc string argument for a step + * @public + * @remarks + * Narrow with `"rows" in argument` for a data table, or `"content" in argument` + * for a doc string. Use {@link DataTable.from} to turn a data table into a + * user-friendly object. + */ +export type StepArgument = PickleTable | PickleDocString + /** * A step that could not be matched to any step definitions * @public diff --git a/testdata/datatable-docstring.feature b/testdata/datatable-docstring.feature new file mode 100644 index 0000000..39f30fd --- /dev/null +++ b/testdata/datatable-docstring.feature @@ -0,0 +1,14 @@ +Feature: a feature + Scenario: data table followed by doc string + Given a step with both: + | hello | + """ + world + """ + + Scenario: doc string followed by data table + Given a step with both: + """ + hello + """ + | world | From 1f26d33c75b817876a912f7dfbbf3d527f70a917 Mon Sep 17 00:00:00 2001 From: David Goss Date: Thu, 24 Sep 2026 17:09:47 +0100 Subject: [PATCH 2/3] Remove dataTable and docString from PreparedStep --- cucumber-core.api.md | 2 -- src/makeTestPlan.spec.ts | 6 ------ src/makeTestPlan.ts | 2 -- src/types.ts | 10 ---------- 4 files changed, 20 deletions(-) diff --git a/cucumber-core.api.md b/cucumber-core.api.md index e03d7d3..ec98578 100644 --- a/cucumber-core.api.md +++ b/cucumber-core.api.md @@ -177,8 +177,6 @@ export type PreparedStep = { type: 'prepared'; fn: SupportCodeFunction; args: ReadonlyArray; - dataTable?: PickleTable; - docString?: PickleDocString; stepArguments: ReadonlyArray; }; diff --git a/src/makeTestPlan.spec.ts b/src/makeTestPlan.spec.ts index 41537ce..600ce3d 100644 --- a/src/makeTestPlan.spec.ts +++ b/src/makeTestPlan.spec.ts @@ -231,7 +231,6 @@ describe('makeTestPlan', () => { if (prepared.type === 'prepared') { expect(prepared.fn).to.eq(fn) expect(prepared.args).to.deep.eq([]) - expect(prepared.dataTable).to.eq(pickles[0].steps[0].argument?.dataTable) expect(prepared.stepArguments).to.deep.eq([pickles[0].steps[0].argument?.dataTable]) } }) @@ -260,7 +259,6 @@ describe('makeTestPlan', () => { if (prepared.type === 'prepared') { expect(prepared.fn).to.eq(fn) expect(prepared.args).to.deep.eq([]) - expect(prepared.docString).to.eq(pickles[0].steps[0].argument?.docString) expect(prepared.stepArguments).to.deep.eq([pickles[0].steps[0].argument?.docString]) } }) @@ -289,8 +287,6 @@ describe('makeTestPlan', () => { if (tableFirst.type === 'prepared') { const dataTable = pickles[0].steps[0].argument?.dataTable const docString = pickles[0].steps[0].argument?.docString - expect(tableFirst.dataTable).to.eq(dataTable) - expect(tableFirst.docString).to.eq(docString) expect(tableFirst.stepArguments).to.deep.eq([dataTable, docString]) } @@ -299,8 +295,6 @@ describe('makeTestPlan', () => { if (docStringFirst.type === 'prepared') { const dataTable = pickles[1].steps[0].argument?.dataTable const docString = pickles[1].steps[0].argument?.docString - expect(docStringFirst.dataTable).to.eq(dataTable) - expect(docStringFirst.docString).to.eq(docString) expect(docStringFirst.stepArguments).to.deep.eq([docString, dataTable]) } }) diff --git a/src/makeTestPlan.ts b/src/makeTestPlan.ts index ca9bac7..5393563 100644 --- a/src/makeTestPlan.ts +++ b/src/makeTestPlan.ts @@ -200,8 +200,6 @@ function fromPickleSteps( type: 'prepared', fn: def.fn, args, - dataTable: pickleStep.argument?.dataTable, - docString: pickleStep.argument?.docString, stepArguments: orderStepArguments(pickleStep.argument), } } diff --git a/src/types.ts b/src/types.ts index 4a1f0bc..f4f2c66 100644 --- a/src/types.ts +++ b/src/types.ts @@ -452,16 +452,6 @@ export type PreparedStep = { * The arguments to pass to the step, as cucumber-expressions argument objects */ args: ReadonlyArray - /** - * The data table to pass to the step, if there is one - * @remarks - * Use {@link DataTable.from} to turn this into a user-friendly object - */ - dataTable?: PickleTable - /** - * The doc string to pass to the step, if there is one - */ - docString?: PickleDocString /** * The data table and/or doc string to pass to the step, in the order they * should be passed From e3b8c48a992ae52bfcf1b244cc7f40eedd34ad21 Mon Sep 17 00:00:00 2001 From: David Goss Date: Thu, 24 Sep 2026 17:24:44 +0100 Subject: [PATCH 3/3] Update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6172491..630040d 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 +- Support both datatable and docstring ([#136](https://github.com/cucumber/javascript-core/pull/136)) ## [0.9.0] - 2026-06-11 ### Changed