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 diff --git a/cucumber-core.api.md b/cucumber-core.api.md index 0b259a0..ec98578 100644 --- a/cucumber-core.api.md +++ b/cucumber-core.api.md @@ -177,10 +177,12 @@ export type PreparedStep = { type: 'prepared'; fn: SupportCodeFunction; 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..600ce3d 100644 --- a/src/makeTestPlan.spec.ts +++ b/src/makeTestPlan.spec.ts @@ -231,7 +231,7 @@ 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]) } }) @@ -259,7 +259,73 @@ 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]) + } + }) + + 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.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.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..5393563 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() { @@ -196,8 +200,7 @@ function fromPickleSteps( type: 'prepared', fn: def.fn, args, - dataTable: pickleStep.argument?.dataTable, - docString: pickleStep.argument?.docString, + stepArguments: orderStepArguments(pickleStep.argument), } } }, @@ -222,6 +225,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..f4f2c66 100644 --- a/src/types.ts +++ b/src/types.ts @@ -453,17 +453,26 @@ export type PreparedStep = { */ args: ReadonlyArray /** - * The data table to pass to the step, if there is one + * The data table and/or doc string to pass to the step, in the order they + * should be passed * @remarks - * Use {@link DataTable.from} to turn this into a user-friendly object + * 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. */ - dataTable?: PickleTable - /** - * The doc string to pass to the step, if there is one - */ - docString?: PickleDocString + 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 |