Skip to content

Instantly share code, notes, and snippets.

@GrantJamesPowell
Created December 6, 2022 22:24
Show Gist options
  • Save GrantJamesPowell/8ad1e9371645064b92c0aaa908e55350 to your computer and use it in GitHub Desktop.
Save GrantJamesPowell/8ad1e9371645064b92c0aaa908e55350 to your computer and use it in GitHub Desktop.
spec.ts
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