From 123474389769478a25475ef6bb6058bd5ccf8d74 Mon Sep 17 00:00:00 2001 From: David Goss Date: Tue, 26 Aug 2025 15:58:29 +0100 Subject: [PATCH 1/3] capture support code --- src/SupportCodeBuilderImpl.ts | 78 +++++++++++++++++++++++++++++---- src/SupportCodeLibraryImpl.ts | 21 +++++++-- src/buildSupportCode.spec.ts | 81 +++++++++++++++++++++++++++++++++- src/types.ts | 82 ++++++++++++++++++++++++++++++----- 4 files changed, 239 insertions(+), 23 deletions(-) diff --git a/src/SupportCodeBuilderImpl.ts b/src/SupportCodeBuilderImpl.ts index 86f8162..348a2ef 100644 --- a/src/SupportCodeBuilderImpl.ts +++ b/src/SupportCodeBuilderImpl.ts @@ -9,12 +9,14 @@ import parse from '@cucumber/tag-expressions' import { SupportCodeLibraryImpl } from './SupportCodeLibraryImpl' import { - DefinedHook, DefinedParameterType, DefinedStep, - NewHook, + DefinedTestCaseHook, + DefinedTestRunHook, NewParameterType, NewStep, + NewTestCaseHook, + NewTestRunHook, SupportCodeBuilder, UndefinedParameterType, } from './types' @@ -29,8 +31,10 @@ export class SupportCodeBuilderImpl implements SupportCodeBuilder { private readonly undefinedParameterTypes: Map> = new Map() private readonly parameterTypes: Array> = [] private readonly steps: Array> = [] - private readonly beforeHooks: Array> = [] - private readonly afterHooks: Array> = [] + private readonly beforeHooks: Array> = [] + private readonly afterHooks: Array> = [] + private readonly beforeAllHooks: Array> = [] + private readonly afterAllHooks: Array> = [] constructor(private readonly newId: () => string) {} @@ -42,7 +46,7 @@ export class SupportCodeBuilderImpl implements SupportCodeBuilder { return this } - beforeHook(options: NewHook) { + beforeHook(options: NewTestCaseHook) { this.beforeHooks.push({ id: this.newId(), ...options, @@ -50,7 +54,7 @@ export class SupportCodeBuilderImpl implements SupportCodeBuilder { return this } - afterHook(options: NewHook) { + afterHook(options: NewTestCaseHook) { this.afterHooks.push({ id: this.newId(), ...options, @@ -66,6 +70,22 @@ export class SupportCodeBuilderImpl implements SupportCodeBuilder { return this } + beforeAllHook(options: NewTestRunHook) { + this.beforeAllHooks.push({ + id: this.newId(), + ...options, + }) + return this + } + + afterAllHook(options: NewTestRunHook) { + this.afterAllHooks.push({ + id: this.newId(), + ...options, + }) + return this + } + private buildParameterTypes(): ReadonlyArray { return this.parameterTypes.map((registered) => { const parameterType = new ParameterType( @@ -155,7 +175,7 @@ export class SupportCodeBuilderImpl implements SupportCodeBuilder { .flat() } - private buildBeforeHooks(): ReadonlyArray { + private buildBeforeHooks(): ReadonlyArray { return this.beforeHooks.map(({ id, name, tags, fn, sourceReference }) => { return { id, @@ -181,7 +201,7 @@ export class SupportCodeBuilderImpl implements SupportCodeBuilder { }) } - private buildAfterHooks(): ReadonlyArray { + private buildAfterHooks(): ReadonlyArray { return this.afterHooks.map(({ id, name, tags, fn, sourceReference }) => { return { id, @@ -207,13 +227,53 @@ export class SupportCodeBuilderImpl implements SupportCodeBuilder { }) } + private buildBeforeAllHooks(): ReadonlyArray { + return this.beforeAllHooks.map(({ id, name, fn, sourceReference }) => { + return { + id, + name, + fn, + sourceReference, + toMessage() { + return { + id, + type: HookType.BEFORE_TEST_RUN, + name, + sourceReference, + } + }, + } + }) + } + + private buildAfterAllHooks(): ReadonlyArray { + return this.afterAllHooks.map(({ id, name, fn, sourceReference }) => { + return { + id, + name, + fn, + sourceReference, + toMessage() { + return { + id, + type: HookType.AFTER_TEST_RUN, + name, + sourceReference, + } + }, + } + }) + } + build() { return new SupportCodeLibraryImpl( this.buildParameterTypes(), this.buildSteps(), this.buildUndefinedParameterTypes(), this.buildBeforeHooks(), - this.buildAfterHooks() + this.buildAfterHooks(), + this.buildBeforeAllHooks(), + this.buildAfterAllHooks() ) } } diff --git a/src/SupportCodeLibraryImpl.ts b/src/SupportCodeLibraryImpl.ts index 0cf5592..82a1464 100644 --- a/src/SupportCodeLibraryImpl.ts +++ b/src/SupportCodeLibraryImpl.ts @@ -1,7 +1,8 @@ import { - DefinedHook, DefinedParameterType, DefinedStep, + DefinedTestCaseHook, + DefinedTestRunHook, MatchedStep, SupportCodeLibrary, UndefinedParameterType, @@ -15,8 +16,10 @@ export class SupportCodeLibraryImpl implements SupportCodeLibrary { private readonly parameterTypes: ReadonlyArray = [], private readonly steps: ReadonlyArray = [], private readonly undefinedParameterTypes: ReadonlyArray = [], - private readonly beforeHooks: ReadonlyArray = [], - private readonly afterHooks: ReadonlyArray = [] + private readonly beforeHooks: ReadonlyArray = [], + private readonly afterHooks: ReadonlyArray = [], + private readonly beforeAllHooks: ReadonlyArray = [], + private readonly afterAllHooks: ReadonlyArray = [] ) {} findAllStepsBy(text: string) { @@ -51,6 +54,14 @@ export class SupportCodeLibraryImpl implements SupportCodeLibrary { }) } + getAllBeforeAllHooks(): ReadonlyArray { + return [...this.beforeAllHooks] + } + + getAllAfterAllHooks(): ReadonlyArray { + return [...this.afterAllHooks] + } + toEnvelopes() { return [ ...this.parameterTypes.map((parameterType) => ({ parameterType })), @@ -60,6 +71,10 @@ export class SupportCodeLibraryImpl implements SupportCodeLibrary { ...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 })), ] } } diff --git a/src/buildSupportCode.spec.ts b/src/buildSupportCode.spec.ts index fd8c55d..be03ab7 100644 --- a/src/buildSupportCode.spec.ts +++ b/src/buildSupportCode.spec.ts @@ -13,7 +13,7 @@ describe('buildSupportCode', () => { newId = IdGenerator.incrementing() }) - describe('hooks', () => { + describe('test case hooks', () => { let library: SupportCodeLibrary beforeEach(() => { library = buildSupportCode({ newId: IdGenerator.incrementing() }) @@ -301,4 +301,83 @@ describe('buildSupportCode', () => { ]) }) }) + + describe('test run hooks', () => { + let library: SupportCodeLibrary + beforeEach(() => { + library = buildSupportCode({ newId: IdGenerator.incrementing() }) + .beforeAllHook({ + name: 'setup 1', + fn: sinon.stub(), + sourceReference: { uri: 'hooks.js', location: { line: 1, column: 1 } }, + }) + .beforeAllHook({ + name: 'setup 2', + fn: sinon.stub(), + sourceReference: { uri: 'hooks.js', location: { line: 2, column: 1 } }, + }) + .afterAllHook({ + name: 'teardown 1', + fn: sinon.stub(), + sourceReference: { uri: 'hooks.js', location: { line: 3, column: 1 } }, + }) + .afterAllHook({ + name: 'teardown 2', + fn: sinon.stub(), + sourceReference: { uri: 'hooks.js', location: { line: 4, column: 1 } }, + }) + .build() + }) + + it('gets just before all hooks', () => { + expect(library.getAllBeforeAllHooks().map((hook) => hook.name)).to.deep.eq([ + 'setup 1', + 'setup 2', + ]) + }) + + it('gets just after all hooks', () => { + expect(library.getAllAfterAllHooks().map((hook) => hook.name)).to.deep.eq([ + 'teardown 1', + 'teardown 2', + ]) + }) + + it('produces correct envelopes', () => { + expect(library.toEnvelopes()).to.deep.eq([ + { + hook: { + id: '0', + type: 'BEFORE_TEST_RUN', + name: 'setup 1', + sourceReference: { uri: 'hooks.js', location: { line: 1, column: 1 } }, + }, + }, + { + hook: { + id: '1', + type: 'BEFORE_TEST_RUN', + name: 'setup 2', + sourceReference: { uri: 'hooks.js', location: { line: 2, column: 1 } }, + }, + }, + { + hook: { + id: '2', + type: 'AFTER_TEST_RUN', + name: 'teardown 1', + sourceReference: { uri: 'hooks.js', location: { line: 3, column: 1 } }, + }, + }, + { + hook: { + id: '3', + type: 'AFTER_TEST_RUN', + name: 'teardown 2', + sourceReference: { uri: 'hooks.js', location: { line: 4, column: 1 } }, + }, + }, + ]) + }) + }) }) diff --git a/src/types.ts b/src/types.ts index e94a847..4850abb 100644 --- a/src/types.ts +++ b/src/types.ts @@ -71,10 +71,10 @@ export interface NewParameterType { } /** - * Attributes for creating a new hook + * Attributes for creating a new test case hook * @public */ -export interface NewHook { +export interface NewTestCaseHook { /** * Optional name for the hook */ @@ -113,6 +113,25 @@ export interface NewStep { sourceReference: SourceReference } +/** + * Attributes for creating a new test run hook + * @public + */ +export interface NewTestRunHook { + /** + * Optional name for the hook + */ + name?: string + /** + * The user-defined function for the hook + */ + fn: SupportCodeFunction + /** + * A reference to the source code of the user-defined function + */ + sourceReference: SourceReference +} + /** * Represents a parameter type that was referenced but not defined * @public @@ -162,10 +181,10 @@ export type DefinedParameterType = { } /** - * A hook that has been defined and is available for execution + * A test case hook that has been defined and is available for execution * @public */ -export type DefinedHook = { +export type DefinedTestCaseHook = { /** * A unique identifier for the hook */ @@ -227,6 +246,33 @@ export type DefinedStep = { toMessage(): StepDefinition } +/** + * A test run hook that has been defined and is available for execution + * @public + */ +export type DefinedTestRunHook = { + /** + * A unique identifier for the hook + */ + id: string + /** + * The name of the hook, if defined + */ + name?: string + /** + * The user-defined function for the hook + */ + fn: SupportCodeFunction + /** + * A reference to the source code of the user-defined function + */ + sourceReference: SourceReference + /** + * Creates a Cucumber Message representing this hook + */ + toMessage(): Hook +} + /** * Builder for collecting user-defined support code * @public @@ -236,18 +282,26 @@ export interface SupportCodeBuilder { * Define a new parameter type */ parameterType(options: NewParameterType): SupportCodeBuilder + /** + * Define a new before hook + */ + beforeHook(options: NewTestCaseHook): SupportCodeBuilder + /** + * Define a new after hook + */ + afterHook(options: NewTestCaseHook): SupportCodeBuilder /** * Define a new step */ step(options: NewStep): SupportCodeBuilder /** - * Define a new before hook + * Define a new before all hook */ - beforeHook(options: NewHook): SupportCodeBuilder + beforeAllHook(options: NewTestRunHook): SupportCodeBuilder /** - * Define a new after hook + * Define a new after all hook */ - afterHook(options: NewHook): SupportCodeBuilder + afterAllHook(options: NewTestRunHook): SupportCodeBuilder /** * Build and seal the support code library for use in {@link makeTestPlan} * @remarks @@ -264,17 +318,25 @@ export interface SupportCodeLibrary { /** * Find all Before hooks that match the given tags */ - findAllBeforeHooksBy(tags: ReadonlyArray): ReadonlyArray + findAllBeforeHooksBy(tags: ReadonlyArray): ReadonlyArray /** * Find all After hooks that match the given tags * @remarks * Hooks are returned in definition order. For execution, the order should be reversed. */ - findAllAfterHooksBy(tags: ReadonlyArray): ReadonlyArray + findAllAfterHooksBy(tags: ReadonlyArray): ReadonlyArray /** * Find all step definitions whose expression is a match for the given text */ findAllStepsBy(text: string): ReadonlyArray + /** + * Get all BeforeAll hooks + */ + getAllBeforeAllHooks(): ReadonlyArray + /** + * Get all AfterAll hooks + */ + getAllAfterAllHooks(): ReadonlyArray /** * Produces a list of Cucumber Messages envelopes for the support code */ From 626c0be475f2e58898ea8f35afdb0cf52b8174d9 Mon Sep 17 00:00:00 2001 From: David Goss Date: Tue, 26 Aug 2025 16:05:08 +0100 Subject: [PATCH 2/3] update api docs --- cucumber-core.api.md | 70 ++++++++++++++++++++++++++++---------------- 1 file changed, 45 insertions(+), 25 deletions(-) diff --git a/cucumber-core.api.md b/cucumber-core.api.md index caedc90..8fdfe50 100644 --- a/cucumber-core.api.md +++ b/cucumber-core.api.md @@ -67,19 +67,6 @@ export class DataTable { transpose(): DataTable; } -// @public -export type DefinedHook = { - id: string; - name?: string; - tags?: { - raw: string; - compiled: ReturnType; - }; - fn: SupportCodeFunction; - sourceReference: SourceReference; - toMessage(): Hook; -}; - // @public export type DefinedParameterType = { id: string; @@ -102,6 +89,28 @@ export type DefinedStep = { toMessage(): StepDefinition; }; +// @public +export type DefinedTestCaseHook = { + id: string; + name?: string; + tags?: { + raw: string; + compiled: ReturnType; + }; + fn: SupportCodeFunction; + sourceReference: SourceReference; + toMessage(): Hook; +}; + +// @public +export type DefinedTestRunHook = { + id: string; + name?: string; + fn: SupportCodeFunction; + sourceReference: SourceReference; + toMessage(): Hook; +}; + // @public export function makeTestPlan(ingredients: TestPlanIngredients, options?: TestPlanOptions): AssembledTestPlan; @@ -111,14 +120,6 @@ export type MatchedStep = { args: ReadonlyArray; }; -// @public -export interface NewHook { - fn: SupportCodeFunction; - name?: string; - sourceReference: SourceReference; - tags?: string; -} - // @public export interface NewParameterType { name: string; @@ -136,6 +137,21 @@ export interface NewStep { sourceReference: SourceReference; } +// @public +export interface NewTestCaseHook { + fn: SupportCodeFunction; + name?: string; + sourceReference: SourceReference; + tags?: string; +} + +// @public +export interface NewTestRunHook { + fn: SupportCodeFunction; + name?: string; + sourceReference: SourceReference; +} + // @public export type PreparedFunction = { fn: SupportCodeFunction; @@ -144,8 +160,10 @@ export type PreparedFunction = { // @public export interface SupportCodeBuilder { - afterHook(options: NewHook): SupportCodeBuilder; - beforeHook(options: NewHook): SupportCodeBuilder; + afterAllHook(options: NewTestRunHook): SupportCodeBuilder; + afterHook(options: NewTestCaseHook): SupportCodeBuilder; + beforeAllHook(options: NewTestRunHook): SupportCodeBuilder; + beforeHook(options: NewTestCaseHook): SupportCodeBuilder; build(): SupportCodeLibrary; parameterType(options: NewParameterType): SupportCodeBuilder; step(options: NewStep): SupportCodeBuilder; @@ -156,9 +174,11 @@ export type SupportCodeFunction = (...args: any[]) => any | Promise; // @public export interface SupportCodeLibrary { - findAllAfterHooksBy(tags: ReadonlyArray): ReadonlyArray; - findAllBeforeHooksBy(tags: ReadonlyArray): ReadonlyArray; + findAllAfterHooksBy(tags: ReadonlyArray): ReadonlyArray; + findAllBeforeHooksBy(tags: ReadonlyArray): ReadonlyArray; findAllStepsBy(text: string): ReadonlyArray; + getAllAfterAllHooks(): ReadonlyArray; + getAllBeforeAllHooks(): ReadonlyArray; toEnvelopes(): ReadonlyArray; } From 09a35146cc3cc651e5204ab75a57e2b815f8a9af Mon Sep 17 00:00:00 2001 From: David Goss Date: Tue, 26 Aug 2025 17:43:59 +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 67f0ae7..59eebba 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 +- Add global hooks ([#10](https://github.com/cucumber/javascript-core/pull/10)) ## [0.3.0] - 2025-07-30 ### Added