@data-client/test
Version:
Testing utilities for Data Client
148 lines (137 loc) • 5.36 kB
text/typescript
import {
actionTypes,
Controller,
DataClientDispatch,
GenericDispatch,
} from '@data-client/react';
import { collapseFixture } from './collapseFixture.js';
import { createFixtureMap } from './createFixtureMap.js';
import type { Fixture, Interceptor } from './fixtureTypes.js';
import { MockProps } from './mockTypes.js';
export function MockController<TBase extends typeof Controller, T>(
Base: TBase,
{
fixtures = [],
getInitialInterceptorData = () => ({}) as any,
}: MockProps<T>,
): TBase {
const [fixtureMap, interceptors] = createFixtureMap(fixtures);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return class MockedController<
D extends GenericDispatch = DataClientDispatch,
> extends Base<D> {
// legacy compatibility (re-declaration)
// TODO: drop when drop support for destructuring (0.14 and below)
declare protected _dispatch: D;
fixtureMap: Map<string, Fixture> = fixtureMap;
interceptors: Interceptor<any>[] = interceptors;
interceptorData: T = getInitialInterceptorData();
constructor(...args: any[]) {
super(...args);
// legacy compatibility
// TODO: drop when drop support for destructuring (0.14 and below)
if (!this._dispatch) {
this._dispatch = (args[0] as any).dispatch;
}
}
// legacy compatibility - we need this to work with 0.14 and below as they do not have this setter
// TODO: drop when drop support for destructuring (0.14 and below)
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
set dispatch(dispatch: D) {
this._dispatch = dispatch;
}
get dispatch(): D {
return ((action: Parameters<D>[0]): Promise<void> => {
// support legacy that has _TYPE suffix
if (action.type === (actionTypes.FETCH ?? actionTypes.FETCH_TYPE)) {
// eslint-disable-next-line prefer-const
let { key, args } = action;
let fixture: Fixture | Interceptor | undefined;
if (this.fixtureMap.has(key)) {
fixture = this.fixtureMap.get(key) as Fixture;
if (!args) args = fixture.args;
// exact matches take priority; now test ComputedFixture
} else {
for (const cfix of this.interceptors) {
if (cfix.endpoint.testKey(key)) {
fixture = cfix;
break;
}
}
}
// we have a match
if (fixture !== undefined) {
const replacedAction: typeof action = {
...action,
};
const delayMs =
typeof fixture.delay === 'function' ?
fixture.delay(...(args as any))
: (fixture.delay ?? 0);
if ('fetchResponse' in fixture) {
const { fetchResponse } = fixture;
fixture = {
endpoint: fixture.endpoint,
response(...args) {
const endpoint = (action.endpoint as any).extend({
fetchResponse: (input: RequestInfo, init: RequestInit) => {
const ret = fetchResponse.call(this, input, init);
return Promise.resolve(
new Response(JSON.stringify(ret), {
status: 200,
headers: new Headers({
'Content-Type': 'application/json',
}),
}),
);
},
});
return (endpoint as any)(...args);
},
};
}
const fetch = async () => {
if (!fixture) {
throw new Error('No fixture found');
}
// delayCollapse determines when the fixture function is 'collapsed' (aka 'run')
// collapsed: https://en.wikipedia.org/wiki/Copenhagen_interpretation
if (fixture.delayCollapse) {
await new Promise(resolve => setTimeout(resolve, delayMs));
}
const result = await collapseFixture(
fixture as any,
args as any,
this.interceptorData,
);
if (!fixture.delayCollapse && delayMs) {
await new Promise(resolve => setTimeout(resolve, delayMs));
}
if (result.error) {
throw result.response;
}
return result.response;
};
if (typeof (replacedAction.endpoint as any).extend === 'function') {
replacedAction.endpoint = (replacedAction.endpoint as any).extend(
{
fetch,
},
);
} else {
// TODO: full testing of this
replacedAction.endpoint = fetch as any;
(replacedAction.endpoint as any).__proto__ = action.endpoint;
}
// TODO: make super.dispatch (once we drop support for destructuring)
return this._dispatch(replacedAction);
}
}
// TODO: make super.dispatch (once we drop support for destructuring)
return this._dispatch(action);
}) as any;
}
};
}