Skip to content

Instantly share code, notes, and snippets.

@antischematic
Created April 24, 2023 06:23
Show Gist options
  • Save antischematic/5eee4b7c7872937a3346e8e8bb5481c1 to your computer and use it in GitHub Desktop.
Save antischematic/5eee4b7c7872937a3346e8e8bb5481c1 to your computer and use it in GitHub Desktop.
Typed BDD
class User {
constructor(public name: string) {}
toString() {
return this.name
}
}
///
const globalSteps = () => []
globalSteps['step 1'] = () => {}
///
const commonSteps = () => [globalSteps]
commonSteps['I log in as <user>'] = (user: User) => {
console.log(user)
}
///
export const steps = () => [commonSteps]
steps['My name is <actor> and <foo>'] = (actor: string, foo: boolean, bar = 0) => {
console.log(actor, foo, bar)
}
steps[`My name is <string>`] = (actor: string) => {
console.log(actor)
}
///
const { given, when, examples } = createTestSuite(steps)
skip.feature('Test feature', () => {
background(() => {
given('I log in as <user>', new User('Bob'))
})
only.scenario('Can do thing', () => {
given('My name is <actor> and <foo>')
// @ts-expect-error
when('writing a new step')
examples([
{ actor: 'Bob', foo: true }
])
})
})
type ExtractParams<T> = T extends `${string}<${infer TName}>${infer TRemainder}`
? [TName, ...ExtractParams<TRemainder>]
: []
type ExtractData<T extends keyof U, U> = {
[key in ExtractParams<T>[number]]?: Args<U[T]>[GetIndex<ExtractParams<T>, key>]
}
type GetIndex<T extends string[], R extends string> = {
[key in keyof T]: T[key] extends R ? key : never
}[number]
type ExtractAllData<T> = {
[key in keyof T]: key extends `${string}<${string}>` ? ExtractData<key, T> : never
}[keyof T]
type Args<T> = T extends (...args: infer R) => void ? R : []
declare global {
function feature(tags: string, name: string, fn: () => void)
function feature(name: string, fn: () => void)
function background(fn: () => void)
function scenario(tags: string, name: string, fn: () => void)
function scenario(name: string, fn: () => void)
function beforeScenario(fn: () => void)
function beforeScenario(tags: string, fn: () => void)
function afterScenario(fn: () => void)
function afterScenario(tags: string, fn: () => void)
const skip: {
feature(tags: string, name: string, fn: () => void)
feature(name: string, fn: () => void)
scenario(tags: string, name: string, fn: () => void)
scenario(name: string, fn: () => void)
}
const only: {
feature(tags: string, name: string, fn: () => void)
feature(name: string, fn: () => void)
scenario(tags: string, name: string, fn: () => void)
scenario(name: string, fn: () => void)
}
}
interface TestSuite<T> {
given<U extends keyof T>(step: U): void
given<U extends keyof T>(step: U, ...params: Args<T[U]>): void
when<U extends keyof T>(step: U): void
when<U extends keyof T>(step: U, ...params: Args<T[U]>): void
then<U extends keyof T>(step: U): void
then<U extends keyof T>(step: U, ...params: Args<T[U]>): void
and<U extends keyof T>(step: U): void
and<U extends keyof T>(step: U, ...params: Args<T[U]>): void
but<U extends keyof T>(step: U): void
but<U extends keyof T>(step: U, ...params: Args<T[U]>): void
examples(tags: string, data: unknown extends T ? any[] : ExtractAllData<T>[]): void
examples(data: unknown extends T ? any[] : ExtractAllData<T>[]): void
}
type UnionToIntersection<U> =
(U extends any ? (k: U)=>void : never) extends ((k: infer I)=>void) ? I : never
type ExtractReturn<T> = T extends () => infer R ? R extends [...infer S] ? S[number] | ExtractReturn<S[number]> : {} : {}
function createTestSuite(): TestSuite<unknown>
function createTestSuite<T extends () => any>(steps: T): TestSuite<T & UnionToIntersection<ExtractReturn<T>>>
function createTestSuite<T extends () => any>(steps?: T): TestSuite<any> {
return {} as any
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment