UNPKG

@testduet/given-when-then

Version:

Write behavior-driven development (BDD) tests using "given-when-then" pattern and execute them in any traditional "describe-before-it-after" test framework.

1 lines 15.1 kB
{"version":3,"sources":["../src/private/isPromiseLike.ts","../src/givenWhenThen.ts","../src/mergeInto.ts"],"sourcesContent":["export default function isPromiseLike(promise: unknown): promise is PromiseLike<unknown> {\n return !!promise && typeof promise === 'object' && 'then' in promise && typeof promise.then === 'function';\n}\n","import isPromiseLike from './private/isPromiseLike.ts';\n\ninterface BehaviorDrivenDevelopment {\n given: Given<void>;\n}\n\ntype GivenPermutation<TPrecondition, TNextPrecondition> =\n | readonly [\n string,\n (precondition: TPrecondition) => PromiseLike<TNextPrecondition> | TNextPrecondition,\n ((precondition: TNextPrecondition) => PromiseLike<void> | void) | undefined\n ]\n | readonly [string, (precondition: TPrecondition) => PromiseLike<TNextPrecondition> | TNextPrecondition];\n\ninterface Given<TPrecondition> {\n <TNextPrecondition>(\n message: string,\n setup: (precondition: TPrecondition) => PromiseLike<TNextPrecondition> | TNextPrecondition,\n teardown?: ((precondition: TNextPrecondition) => PromiseLike<void> | void) | undefined\n ): {\n and: Given<TNextPrecondition>;\n when: When<TNextPrecondition, void>;\n };\n\n oneOf<TNextPrecondition>(permutations: readonly GivenPermutation<TPrecondition, TNextPrecondition>[]): {\n and: Given<TNextPrecondition>;\n when: When<TNextPrecondition, void>;\n };\n}\n\ntype WhenPermutation<TPrecondition, TOutcome, TNextOutcome> =\n | readonly [\n string,\n (precondition: TPrecondition, outcome: TOutcome) => PromiseLike<TNextOutcome> | TNextOutcome,\n ((precondition: TPrecondition, outcome: TNextOutcome) => PromiseLike<void> | void) | undefined\n ]\n | readonly [string, (precondition: TPrecondition, outcome: TOutcome) => PromiseLike<TNextOutcome> | TNextOutcome];\n\ninterface When<TPrecondition, TOutcome> {\n <TNextOutcome>(\n message: string,\n setup: (precondition: TPrecondition, outcome: TOutcome) => PromiseLike<TNextOutcome> | TNextOutcome,\n teardown?: ((precondition: TPrecondition, outcome: TNextOutcome) => PromiseLike<void> | void) | undefined\n ): {\n then: Then<TPrecondition, TNextOutcome>;\n };\n\n oneOf<TNextOutcome>(permutations: readonly WhenPermutation<TPrecondition, TOutcome, TNextOutcome>[]): {\n then: Then<TPrecondition, TNextOutcome>;\n };\n}\n\ninterface Then<TPrecondition, TOutcome> {\n (\n message: string,\n fn: (precondition: TPrecondition, outcome: TOutcome) => void\n ): {\n and: Then<TPrecondition, TOutcome>;\n when: When<TPrecondition, TOutcome>;\n };\n}\n\ninterface TestFacility {\n afterEach: (fn: () => Promise<void> | void) => void;\n beforeEach: (fn: () => Promise<void> | void) => void;\n describe: (message: string, fn: () => void) => void;\n it: (message: string, fn: (() => Promise<void>) | (() => void)) => void;\n}\n\ntype Ref<T> = { value: T };\n\ntype GivenFrame<TPrecondition, TNextPrecondition> = {\n isConjunction: boolean;\n operation: 'given';\n permutations: readonly GivenPermutation<TPrecondition, TNextPrecondition>[];\n};\n\ntype WhenFrame<TPrecondition, TOutcome, TNextOutcome> = {\n permutations: readonly WhenPermutation<TPrecondition, TOutcome, TNextOutcome>[];\n operation: 'when';\n};\n\ntype ThenFrame<TPrecondition, TOutcome> = {\n isConjunction: boolean;\n message: string;\n operation: 'then';\n fn: (precondition: TPrecondition, outcome: TOutcome) => PromiseLike<void> | void;\n};\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype Frame = GivenFrame<unknown, any> | ThenFrame<unknown, unknown> | WhenFrame<unknown, unknown, any>;\n\nfunction createChain(mutableStacks: (readonly Frame[])[], stack: readonly Frame[]) {\n const given: (isConjunction: boolean) => Given<unknown> = (isConjunction: boolean) => {\n const given: Given<unknown> = <TNextPrecondition>(\n message: string,\n setup: (value: unknown) => PromiseLike<TNextPrecondition> | TNextPrecondition,\n teardown?: ((value: TNextPrecondition) => PromiseLike<void> | void) | undefined\n ) => {\n const nextChain = createChain(\n mutableStacks,\n Object.freeze([\n ...stack,\n {\n isConjunction,\n operation: 'given',\n permutations: [[message, setup, teardown]]\n } satisfies GivenFrame<unknown, TNextPrecondition>\n ])\n );\n\n return {\n and: nextChain.given(true) as Given<Awaited<ReturnType<typeof setup>>>,\n when: nextChain.when() as When<Awaited<ReturnType<typeof setup>>, void>\n } satisfies ReturnType<Given<Awaited<ReturnType<typeof setup>>>>;\n };\n\n given.oneOf = (<TNextPrecondition>(permutations: readonly GivenPermutation<unknown, TNextPrecondition>[]) => {\n const nextChain = createChain(\n mutableStacks,\n Object.freeze([\n ...stack,\n {\n isConjunction,\n operation: 'given',\n permutations\n } satisfies GivenFrame<unknown, TNextPrecondition>\n ])\n );\n\n return {\n and: nextChain.given(true) as Given<TNextPrecondition>,\n when: nextChain.when() as When<TNextPrecondition, void>\n } satisfies ReturnType<Given<TNextPrecondition>>;\n }) satisfies Given<unknown>['oneOf'];\n\n return given;\n };\n\n const when: () => When<unknown, unknown> = () => {\n const when: When<unknown, unknown> = (message, setup, teardown) => {\n const nextChain = createChain(\n mutableStacks,\n Object.freeze([...stack, { operation: 'when', permutations: [[message, setup, teardown]] }])\n );\n\n return {\n then: nextChain.then(false) as Then<unknown, Awaited<ReturnType<typeof setup>>>\n } satisfies ReturnType<When<unknown, Awaited<ReturnType<typeof setup>>>>;\n };\n\n when.oneOf = (<TNextOutcome>(permutations: readonly WhenPermutation<unknown, unknown, TNextOutcome>[]) => {\n const nextChain = createChain(\n mutableStacks,\n Object.freeze([\n ...stack,\n {\n operation: 'when',\n permutations\n } satisfies WhenFrame<unknown, unknown, TNextOutcome>\n ])\n );\n\n return {\n then: nextChain.then(false) as Then<unknown, TNextOutcome>\n } satisfies ReturnType<When<unknown, TNextOutcome>>;\n }) satisfies When<unknown, unknown>['oneOf'];\n\n return when;\n };\n\n const then: (isConjunction: boolean) => Then<unknown, unknown> = isConjunction => {\n return ((message, fn) => {\n mutableStacks.push(\n Object.freeze([\n ...stack,\n {\n isConjunction,\n fn,\n message,\n operation: 'then'\n } satisfies ThenFrame<unknown, unknown>\n ])\n );\n\n const currentChain = createChain(mutableStacks, stack);\n\n return {\n and: currentChain.then(true),\n when: currentChain.when()\n } satisfies ReturnType<Then<unknown, unknown>>;\n }) satisfies Then<unknown, unknown>;\n };\n\n return { given, then, when };\n}\n\nfunction scenario(\n message: string,\n fn: (bdd: BehaviorDrivenDevelopment) => void,\n options?: Partial<TestFacility>\n): void {\n const stacks: (readonly Frame[])[] = [];\n\n const fnResult = fn({\n given: createChain(stacks, Object.freeze([])).given(false) as Given<void>\n });\n\n if (isPromiseLike(fnResult)) {\n // This is a soft block.\n // While we can technically allow fn() to be asynchronous, we are blocking it\n // to prevent potentially bad code patterns\n throw new Error('The function passed to scenario() cannot asynchronous');\n } else if (!stacks.some(frames => frames.some(frame => frame.operation === 'then'))) {\n throw new Error('Scenario should contains at least one then clause');\n }\n\n const facility = {\n // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n // @ts-ignore\n afterEach: options?.afterEach || (globalThis.afterEach as TestFacility['afterEach']),\n // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n // @ts-ignore\n beforeEach: options?.beforeEach || (globalThis.beforeEach as TestFacility['beforeEach']),\n // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n // @ts-ignore\n describe: options?.describe || (globalThis.describe as TestFacility['describe']),\n // eslint-disable-next-line @typescript-eslint/ban-ts-comment\n // @ts-ignore\n it: options?.it || (globalThis.it as TestFacility['it'])\n };\n\n facility.describe(message, () => {\n for (const stack of stacks) {\n runStack(stack, { value: undefined }, { value: undefined }, facility);\n }\n });\n}\n\nfunction runStack(\n stack: readonly Frame[],\n preconditionRef: Ref<unknown>,\n outcomeRef: Ref<unknown>,\n facility: TestFacility\n): void {\n const frame = stack.at(0);\n\n if (!frame) {\n return;\n }\n\n if (frame.operation === 'given') {\n for (const [message, setup, teardown] of frame.permutations) {\n facility.describe(`${frame.isConjunction ? 'and' : 'given'} ${message}`, () => {\n let currentPreconditionRef: Ref<unknown>;\n\n facility.beforeEach(() => {\n const save = (value: unknown): void => {\n currentPreconditionRef = { value };\n preconditionRef.value = value;\n };\n\n const value = setup(preconditionRef.value);\n\n return isPromiseLike(value) ? Promise.resolve(value.then(save)) : save(value);\n });\n\n facility.afterEach(() => {\n const value = teardown?.(currentPreconditionRef.value);\n\n return isPromiseLike(value) ? Promise.resolve(value) : value;\n });\n\n return runStack(stack.slice(1), preconditionRef, outcomeRef, facility);\n });\n }\n } else if (frame.operation === 'when') {\n for (const [message, setup, teardown] of frame.permutations) {\n facility.describe(`when ${message}`, () => {\n let currentOutcomeRef: { value: unknown };\n\n facility.beforeEach(() => {\n const save = (value: unknown): void => {\n currentOutcomeRef = { value };\n outcomeRef.value = value;\n };\n\n const value = setup(preconditionRef.value, outcomeRef.value);\n\n return isPromiseLike(value) ? Promise.resolve(value.then(save)) : save(value);\n });\n\n facility.afterEach(() => {\n const value = teardown?.(preconditionRef.value, currentOutcomeRef.value);\n\n return isPromiseLike(value) ? Promise.resolve(value) : value;\n });\n\n return runStack(stack.slice(1), preconditionRef, outcomeRef, facility);\n });\n }\n } else if (frame.operation === 'then') {\n facility.it(`${frame.isConjunction ? 'and' : 'then'} ${frame.message}`, () =>\n frame.fn(preconditionRef.value, outcomeRef.value)\n );\n\n return runStack(stack.slice(1), preconditionRef, outcomeRef, facility);\n }\n}\n\nexport { scenario, type TestFacility };\n","export default function mergeInto<T, TMerge>(objectToMerge: TMerge): (scope: T) => T & TMerge {\n return scope => ({ ...scope, ...objectToMerge });\n}\n"],"mappings":"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","names":["given","when","value"]}