Created
December 6, 2022 22:24
-
-
Save GrantJamesPowell/8ad1e9371645064b92c0aaa908e55350 to your computer and use it in GitHub Desktop.
spec.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Spread } from 'type-fest'; | |
type AfterCallback = () => void | Promise<void>; | |
type BeforeCallback = (input: any) => any; | |
type DoneCallback = (value?: Error | void) => void; | |
type TestCode<T> = ( | |
args: T & { done: (value?: Error | void) => void } | |
) => Promise<void> | void; | |
type TestCase<T> = (name: string, testCode: TestCode<T>) => void; | |
// Jest, I love you to death, but I hate you for making me to write this type | |
export type Spec<T> = TestCase<T> & { | |
skip: TestCase<T>; | |
only: TestCase<T>; | |
_jestTest: jest.It; | |
_befores: BeforeCallback[]; | |
}; | |
const makeCallback = | |
<T>(it: jest.It, befores: BeforeCallback[]) => | |
(name: string, testCode: TestCode<T>) => { | |
it(name, (done: DoneCallback) => { | |
let doneCalled = false; | |
const wrappedDone: DoneCallback = (value) => { | |
done(value); | |
doneCalled = true; | |
}; | |
const run = async () => { | |
const afters: AfterCallback[] = []; | |
let params = {}; | |
try { | |
for (const before of befores) { | |
const result = await before(params); | |
if (result !== undefined) { | |
const { _after, ...rest } = result; | |
if (_after !== undefined) { | |
afters.push(_after); | |
} | |
params = { ...params, ...rest }; | |
} | |
} | |
await testCode({ | |
...params, | |
done: wrappedDone, | |
} as any); | |
} finally { | |
for (const callback of afters) { | |
await callback(); | |
} | |
} | |
}; | |
run() | |
.then((_) => { | |
if (!doneCalled) { | |
wrappedDone(); | |
} | |
}) | |
.catch((e) => { | |
if (!doneCalled) { | |
wrappedDone(e); | |
} | |
}); | |
}); | |
}; | |
const specFromIt = (it: jest.It): Spec<{}> => specFromItAndbefore(it, []); | |
const specFromItAndbefore = <T extends object>( | |
it: jest.It, | |
befores: BeforeCallback[] | |
): Spec<T> => { | |
const main = makeCallback(it, befores); | |
(main as any)['_jestTest'] = it; | |
(main as any)['_befores'] = befores; | |
(main as any)['skip'] = makeCallback(test.skip, befores); | |
(main as any)['only'] = makeCallback(test.only, befores); | |
return main as Spec<T>; | |
}; | |
const wrapSpec = < | |
T extends object, | |
U extends { _after?: AfterCallback } | void | |
>( | |
spec: Spec<T>, | |
before: (prev: T) => Promise<U> | U | |
): Spec<Spread<T, U extends void ? {} : Omit<U, '_after'>>> => { | |
return specFromItAndbefore(spec['_jestTest'], [ | |
...spec['_befores'], | |
before, | |
]) as any; | |
}; | |
type SpecOf<T> = T extends Spec<infer T> | |
? Spec<T> | |
: T extends jest.It | |
? Spec<{}> | |
: never; | |
type Before<T> = T extends Spec<infer T> ? (T extends object ? T : never) : {}; | |
export function buildSpec<Test>(test: Test): SpecOf<Test>; | |
export function buildSpec<Test, U extends object | void>( | |
test: Test, | |
before?: (input: Before<Test>) => U | Promise<U> | |
): Test extends Spec<any> | jest.It | |
? Spec<Spread<Before<SpecOf<Test>>, U extends void ? {} : Omit<U, '_after'>>> | |
: never; | |
export function buildSpec<Test, U extends object | void>( | |
test: Test, | |
before: (input: Before<Test>) => U | Promise<U> = () => ({} as U) | |
): Test extends Spec<any> | jest.It | |
? Spec<Spread<Before<SpecOf<Test>>, U extends void ? {} : Omit<U, '_after'>>> | |
: never { | |
const spec = | |
(test as any)['_jestTest'] !== undefined ? test : specFromIt(test as any); | |
const wrapped = wrapSpec(spec as any, before); | |
return wrapped as any; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment