diff --git a/CHANGELOG.md b/CHANGELOG.md index 822f7f5..ed13332 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] +### Changed +- Return messages in definition order ([#36](https://github.com/cucumber/javascript-core/pull/36)) ## [0.8.1] - 2025-11-28 ### Added diff --git a/cucumber-core.api.md b/cucumber-core.api.md index b66b0bf..c71688b 100644 --- a/cucumber-core.api.md +++ b/cucumber-core.api.md @@ -12,6 +12,7 @@ import { GherkinDocument } from '@cucumber/messages'; import { Hook } from '@cucumber/messages'; import { IdGenerator } from '@cucumber/messages'; import { NamingStrategy } from '@cucumber/query'; +import { ParameterType } from '@cucumber/messages'; import parse from '@cucumber/tag-expressions'; import { Pickle } from '@cucumber/messages'; import { PickleDocString } from '@cucumber/messages'; @@ -84,16 +85,19 @@ export class DataTable { // @public export type DefinedParameterType = { id: string; + order: number; name: string; regularExpressions: ReadonlyArray; preferForRegularExpressionMatch: boolean; useForSnippets: boolean; sourceReference: SourceReference; + toMessage(): ParameterType; }; // @public export type DefinedStep = { id: string; + order: number; expression: { raw: string | RegExp; compiled: CucumberExpression | RegularExpression; @@ -106,6 +110,7 @@ export type DefinedStep = { // @public export type DefinedTestCaseHook = { id: string; + order: number; name?: string; tags?: { raw: string; @@ -119,6 +124,7 @@ export type DefinedTestCaseHook = { // @public export type DefinedTestRunHook = { id: string; + order: number; name?: string; fn: SupportCodeFunction; sourceReference: SourceReference; diff --git a/src/AmbiguousError.spec.ts b/src/AmbiguousError.spec.ts index 8b89a3e..2542d38 100644 --- a/src/AmbiguousError.spec.ts +++ b/src/AmbiguousError.spec.ts @@ -21,6 +21,7 @@ describe('AmbiguousError', () => { const matches: ReadonlyArray = references.map((ref) => ({ id: 'def-id', + order: 0, expression: { raw: 'text', compiled: {} as any, diff --git a/src/SupportCodeBuilderImpl.ts b/src/SupportCodeBuilderImpl.ts index e1e1c6e..5a39435 100644 --- a/src/SupportCodeBuilderImpl.ts +++ b/src/SupportCodeBuilderImpl.ts @@ -21,7 +21,7 @@ import { UndefinedParameterType, } from './types' -type WithId = { id: string } & T +type Registered = { id: string; order: number } & T /** * @internal @@ -29,18 +29,20 @@ type WithId = { id: string } & T export class SupportCodeBuilderImpl implements SupportCodeBuilder { private readonly parameterTypeRegistry = new ParameterTypeRegistry() private readonly undefinedParameterTypes: Map> = new Map() - private readonly parameterTypes: Array> = [] - private readonly steps: Array> = [] - private readonly beforeHooks: Array> = [] - private readonly afterHooks: Array> = [] - private readonly beforeAllHooks: Array> = [] - private readonly afterAllHooks: Array> = [] + private readonly parameterTypes: Array> = [] + private readonly steps: Array> = [] + private readonly beforeHooks: Array> = [] + private readonly afterHooks: Array> = [] + private readonly beforeAllHooks: Array> = [] + private readonly afterAllHooks: Array> = [] + private sequence = 0 constructor(private readonly newId: () => string) {} parameterType(options: NewParameterType) { this.parameterTypes.push({ id: this.newId(), + order: this.sequence++, ...options, }) return this @@ -49,6 +51,7 @@ export class SupportCodeBuilderImpl implements SupportCodeBuilder { beforeHook(options: NewTestCaseHook) { this.beforeHooks.push({ id: this.newId(), + order: this.sequence++, ...options, }) return this @@ -57,6 +60,7 @@ export class SupportCodeBuilderImpl implements SupportCodeBuilder { afterHook(options: NewTestCaseHook) { this.afterHooks.push({ id: this.newId(), + order: this.sequence++, ...options, }) return this @@ -65,6 +69,7 @@ export class SupportCodeBuilderImpl implements SupportCodeBuilder { step(options: NewStep) { this.steps.push({ id: this.newId(), + order: this.sequence++, ...options, }) return this @@ -73,6 +78,7 @@ export class SupportCodeBuilderImpl implements SupportCodeBuilder { beforeAllHook(options: NewTestRunHook) { this.beforeAllHooks.push({ id: this.newId(), + order: this.sequence++, ...options, }) return this @@ -81,6 +87,7 @@ export class SupportCodeBuilderImpl implements SupportCodeBuilder { afterAllHook(options: NewTestRunHook) { this.afterAllHooks.push({ id: this.newId(), + order: this.sequence++, ...options, }) return this @@ -99,18 +106,29 @@ export class SupportCodeBuilderImpl implements SupportCodeBuilder { this.parameterTypeRegistry.defineParameterType(parameterType) return { id: registered.id, + order: registered.order, name: registered.name, regularExpressions: [...parameterType.regexpStrings], preferForRegularExpressionMatch: parameterType.preferForRegexpMatch as boolean, useForSnippets: parameterType.useForSnippets as boolean, sourceReference: registered.sourceReference, + toMessage() { + return { + id: this.id, + name: this.name, + regularExpressions: this.regularExpressions, + preferForRegularExpressionMatch: this.preferForRegularExpressionMatch, + useForSnippets: this.useForSnippets, + sourceReference: this.sourceReference, + } + }, } }) } private buildSteps(): ReadonlyArray { return this.steps - .map(({ id, pattern, fn, sourceReference }) => { + .map(({ id, order, pattern, fn, sourceReference }) => { const compiled = this.compileExpression(pattern) if (!compiled) { return undefined @@ -118,6 +136,7 @@ export class SupportCodeBuilderImpl implements SupportCodeBuilder { const source = this.extractPatternSource(pattern) return { id, + order, expression: { raw: pattern, compiled, @@ -184,9 +203,10 @@ export class SupportCodeBuilderImpl implements SupportCodeBuilder { } private buildBeforeHooks(): ReadonlyArray { - return this.beforeHooks.map(({ id, name, tags, fn, sourceReference }) => { + return this.beforeHooks.map(({ id, order, name, tags, fn, sourceReference }) => { return { id, + order, name, tags: tags ? { @@ -210,9 +230,10 @@ export class SupportCodeBuilderImpl implements SupportCodeBuilder { } private buildAfterHooks(): ReadonlyArray { - return this.afterHooks.map(({ id, name, tags, fn, sourceReference }) => { + return this.afterHooks.map(({ id, order, name, tags, fn, sourceReference }) => { return { id, + order, name, tags: tags ? { @@ -236,9 +257,10 @@ export class SupportCodeBuilderImpl implements SupportCodeBuilder { } private buildBeforeAllHooks(): ReadonlyArray { - return this.beforeAllHooks.map(({ id, name, fn, sourceReference }) => { + return this.beforeAllHooks.map(({ id, order, name, fn, sourceReference }) => { return { id, + order, name, fn, sourceReference, @@ -255,9 +277,10 @@ export class SupportCodeBuilderImpl implements SupportCodeBuilder { } private buildAfterAllHooks(): ReadonlyArray { - return this.afterAllHooks.map(({ id, name, fn, sourceReference }) => { + return this.afterAllHooks.map(({ id, order, name, fn, sourceReference }) => { return { id, + order, name, fn, sourceReference, diff --git a/src/SupportCodeLibraryImpl.ts b/src/SupportCodeLibraryImpl.ts index 88e92ef..f7b26a4 100644 --- a/src/SupportCodeLibraryImpl.ts +++ b/src/SupportCodeLibraryImpl.ts @@ -1,5 +1,5 @@ import { CucumberExpressionGenerator, ParameterTypeRegistry } from '@cucumber/cucumber-expressions' -import { SourceReference } from '@cucumber/messages' +import { Envelope, SourceReference } from '@cucumber/messages' import { DefinedParameterType, @@ -11,6 +11,11 @@ import { UndefinedParameterType, } from './types' +type OrderedEnvelope = { + order: number + envelope: Envelope +} + /** * @internal */ @@ -82,18 +87,50 @@ export class SupportCodeLibraryImpl implements SupportCodeLibrary { } toEnvelopes() { + const definedThings: ReadonlyArray = [ + ...this.parameterTypes.map((definedParameterType) => ({ + order: definedParameterType.order, + envelope: { + parameterType: definedParameterType.toMessage(), + }, + })), + ...this.steps.map((definedStep) => ({ + order: definedStep.order, + envelope: { + stepDefinition: definedStep.toMessage(), + }, + })), + ...this.beforeHooks.map((definedHook) => ({ + order: definedHook.order, + envelope: { + hook: definedHook.toMessage(), + }, + })), + ...this.afterHooks.map((definedHook) => ({ + order: definedHook.order, + envelope: { + hook: definedHook.toMessage(), + }, + })), + ...this.beforeAllHooks.map((definedHook) => ({ + order: definedHook.order, + envelope: { + hook: definedHook.toMessage(), + }, + })), + ...this.afterAllHooks.map((definedHook) => ({ + order: definedHook.order, + envelope: { + hook: definedHook.toMessage(), + }, + })), + ] + return [ - ...this.parameterTypes.map((parameterType) => ({ parameterType })), - ...this.steps - .map((definedStep) => definedStep.toMessage()) - .map((stepDefinition) => ({ stepDefinition })), - ...this.undefinedParameterTypes.map((undefinedParameterType) => ({ undefinedParameterType })), - ...this.beforeHooks.map((definedHook) => definedHook.toMessage()).map((hook) => ({ hook })), - ...this.afterHooks.map((definedHook) => definedHook.toMessage()).map((hook) => ({ hook })), - ...this.beforeAllHooks - .map((definedHook) => definedHook.toMessage()) - .map((hook) => ({ hook })), - ...this.afterAllHooks.map((definedHook) => definedHook.toMessage()).map((hook) => ({ hook })), + ...definedThings.toSorted((a, b) => a.order - b.order).map(({ envelope }) => envelope), + ...this.undefinedParameterTypes.map((undefinedParameterType) => ({ + undefinedParameterType, + })), ] } } diff --git a/src/buildSupportCode.spec.ts b/src/buildSupportCode.spec.ts index 2a316db..e65be13 100644 --- a/src/buildSupportCode.spec.ts +++ b/src/buildSupportCode.spec.ts @@ -1,5 +1,12 @@ import { CucumberExpression, RegularExpression } from '@cucumber/cucumber-expressions' -import { IdGenerator, StepDefinitionPatternType } from '@cucumber/messages' +import { + Envelope, + Hook, + IdGenerator, + ParameterType, + StepDefinition, + StepDefinitionPatternType, +} from '@cucumber/messages' import { expect } from 'chai' import sinon from 'sinon' @@ -424,6 +431,58 @@ describe('buildSupportCode', () => { }) }) + describe('envelopes', () => { + it('should return envelopes in the same order the support code was registered', () => { + const library = buildSupportCode({ newId }) + .afterHook({ + name: 'teardown 2', + fn: sinon.stub(), + sourceReference: { uri: 'hooks.js', location: { line: 4, column: 1 } }, + }) + .afterAllHook({ + name: 'big teardown', + fn: sinon.stub(), + sourceReference: { uri: 'hooks.js', location: { line: 2, column: 1 } }, + }) + .beforeAllHook({ + name: 'big setup', + fn: sinon.stub(), + sourceReference: { uri: 'hooks.js', location: { line: 1, column: 1 } }, + }) + .beforeHook({ + name: 'teardown 1', + fn: sinon.stub(), + sourceReference: { uri: 'hooks.js', location: { line: 3, column: 1 } }, + }) + .parameterType({ + name: 'flight', + regexp: /([A-Z]{3})-([A-Z]{3})/, + /* c8 ignore next 3 */ + transformer(from: string, to: string) { + return [from, to] + }, + sourceReference: { uri: 'support.js', location: { line: 1, column: 1 } }, + }) + .step({ + pattern: '{flight} has been delayed', + fn: sinon.stub(), + sourceReference: { uri: 'steps.js', location: { line: 1, column: 1 } }, + }) + .build() + + const envelopes = library.toEnvelopes() + + expect( + envelopes.flatMap((envelope) => + Object.keys(envelope).map( + (key) => + `${key}:${(envelope[key as keyof Envelope] as Hook | ParameterType | StepDefinition).id}` + ) + ) + ).to.deep.eq(['hook:0', 'hook:1', 'hook:2', 'hook:3', 'parameterType:4', 'stepDefinition:5']) + }) + }) + describe('sources', () => { it('should return all source references from parameter types, steps, and hooks', () => { const library = buildSupportCode({ newId }) diff --git a/src/types.ts b/src/types.ts index b21d5cb..6ef066a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -9,6 +9,7 @@ import { GherkinDocument, Hook, IdGenerator, + ParameterType, Pickle, PickleDocString, PickleTable, @@ -163,6 +164,10 @@ export type DefinedParameterType = { * A unique identifier for the parameter type */ id: string + /** + * The number of this parameter type in the definition-ordered sequence of support code + */ + order: number /** * The name of the parameter type */ @@ -185,6 +190,10 @@ export type DefinedParameterType = { * A reference to the source code of the user-defined parameter type */ sourceReference: SourceReference + /** + * Creates a Cucumber Message representing this parameter type + */ + toMessage(): ParameterType } /** @@ -196,6 +205,10 @@ export type DefinedTestCaseHook = { * A unique identifier for the hook */ id: string + /** + * The number of this hook in the definition-ordered sequence of support code + */ + order: number /** * The name of the hook, if defined */ @@ -230,6 +243,10 @@ export type DefinedStep = { * A unique identifier for the step */ id: string + /** + * The number of this step definition in the definition-ordered sequence of support code + */ + order: number /** * The text expression for the step, including both raw and compiled forms * @remarks @@ -262,6 +279,10 @@ export type DefinedTestRunHook = { * A unique identifier for the hook */ id: string + /** + * The number of this hook in the definition-ordered sequence of support code + */ + order: number /** * The name of the hook, if defined */ @@ -353,7 +374,8 @@ export interface SupportCodeLibrary { */ getAllAfterAllHooks(): ReadonlyArray /** - * Produces a list of Cucumber Messages envelopes for the support code + * Produces a list of Cucumber Messages envelopes for the support code, + * with the original definition order retained */ toEnvelopes(): ReadonlyArray }