@data-client/test
Version:
Testing utilities for Data Client
368 lines (346 loc) • 12 kB
JavaScript
;
var react = require('@data-client/react');
var React = require('react');
var jsxRuntime = require('react/jsx-runtime');
async function collapseFixture(fixture, args, interceptorData) {
let error = 'error' in fixture ? fixture.error : false;
let response = fixture.response;
if (typeof fixture.response === 'function') {
try {
response = await fixture.response.apply(interceptorData, args);
// dispatch goes through user-code that can sometimes fail.
// let's ensure we always handle errors
} catch (e) {
response = e;
error = true;
}
}
return {
response,
error
};
}
function createFixtureMap(fixtures = []) {
const map = new Map();
const computed = [];
for (const fixture of fixtures) {
if ('args' in fixture) {
if (typeof fixture.response !== 'function') {
const key = fixture.endpoint.key(...fixture.args);
map.set(key, fixture);
} else {
// this has to be a typo. probably needs to remove args
console.warn(`Fixture found with function response, and explicit args. Interceptors should not specify args.
${fixture.endpoint.name}: ${JSON.stringify(fixture.args)}
Treating as Interceptor`);
computed.push(fixture);
}
} else {
computed.push(fixture);
}
}
return [map, computed];
}
function MockController(Base, {
fixtures = [],
getInitialInterceptorData = () => ({})
}) {
const [fixtureMap, interceptors] = createFixtureMap(fixtures);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return class MockedController extends Base {
// legacy compatibility (re-declaration)
// TODO: drop when drop support for destructuring (0.14 and below)
fixtureMap = fixtureMap;
interceptors = interceptors;
interceptorData = getInitialInterceptorData();
constructor(...args) {
super(...args);
// legacy compatibility
// TODO: drop when drop support for destructuring (0.14 and below)
if (!this._dispatch) {
this._dispatch = args[0].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) {
this._dispatch = dispatch;
}
get dispatch() {
return action => {
var _actionTypes$FETCH;
// support legacy that has _TYPE suffix
if (action.type === ((_actionTypes$FETCH = react.actionTypes.FETCH) != null ? _actionTypes$FETCH : react.actionTypes.FETCH_TYPE)) {
// eslint-disable-next-line prefer-const
let {
key,
args
} = action;
let fixture;
if (this.fixtureMap.has(key)) {
fixture = this.fixtureMap.get(key);
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) {
var _fixture$delay;
const replacedAction = {
...action
};
const delayMs = typeof fixture.delay === 'function' ? fixture.delay(...args) : (_fixture$delay = fixture.delay) != null ? _fixture$delay : 0;
if ('fetchResponse' in fixture) {
const {
fetchResponse
} = fixture;
fixture = {
endpoint: fixture.endpoint,
response(...args) {
const endpoint = action.endpoint.extend({
fetchResponse: (input, init) => {
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(...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, args, 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.extend === 'function') {
replacedAction.endpoint = replacedAction.endpoint.extend({
fetch
});
} else {
// TODO: full testing of this
replacedAction.endpoint = fetch;
replacedAction.endpoint.__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);
};
}
};
}
/** Can be used to mock responses based on fixtures provided.
*
* <MockResolver fixtures={postFixtures[state]}><MyComponent /></MockResolver>
*
* Place below <DataProvider /> and above any components you want to mock.
*/
function MockResolver({
children,
fixtures,
getInitialInterceptorData = () => ({})
}) {
const controller = react.useController();
const controllerInterceptor = React.useMemo(() => {
var _controller$_dispatch;
const MockedController = MockController(controller.constructor, fixtures ? {
fixtures,
getInitialInterceptorData
} : {});
const controllerInterceptor = new MockedController({
...controller,
dispatch: (_controller$_dispatch = controller['_dispatch']) != null ? _controller$_dispatch : controller.dispatch
});
return controllerInterceptor;
}, [controller, fixtures, getInitialInterceptorData]);
return /*#__PURE__*/jsxRuntime.jsx(react.ControllerContext.Provider, {
value: controllerInterceptor,
children: children
});
}
// @testing-library/react-hooks does not support node/SSR, so we must fallback to previous testing
const USE18 = Number(React.version.substring(0, 3)) >= 18 && typeof document !== 'undefined';
/**
* Provides an abstraction over react 17 and 18 compatible libraries
*/
const renderHook = USE18 ? require('./render18HookWrapped.js').render18Wrapper : require('@testing-library/react-hooks').renderHook;
// we declare our own type here because the one is marked as deprecated in the react library
// this is for react native + react web compatibility, not actually 18 compatibility
const act = USE18 ? require('./render18HookWrapped.js').act : require('@testing-library/react-hooks').act;
const {
initialState,
createReducer
} = react.__INTERNAL__;
function mockInitialState(fixtures = []) {
const actions = [];
const dispatch = action => {
actions.push(action);
return Promise.resolve();
};
const controller = new react.Controller({
dispatch
});
const reducer = createReducer(controller);
fixtures.forEach(fixture => {
dispatchFixture(fixture, fixture.args, controller);
});
return actions.reduce(reducer, initialState);
}
function dispatchFixture(fixture, args, controller, fetchedAt) {
// eslint-disable-next-line prefer-const
let {
endpoint
} = fixture;
const {
response,
error
} = fixture;
if (controller.resolve) {
controller.resolve(endpoint, {
args,
response,
error,
fetchedAt: Date.now()
});
} else {
if (error === true) {
controller.setError(endpoint, ...args, response);
} else {
controller.setResponse(endpoint, ...args, response);
}
}
}
/** @see https://dataclient.io/docs/api/makeRenderDataHook */
function makeRenderDataHook(Provider) {
const renderDataClient = (callback, {
initialFixtures,
resolverFixtures,
getInitialInterceptorData = () => ({}),
...options
} = {}) => {
/** Wraps dispatches that are typically called declaratively in act() */
class ActController extends MockController(react.Controller, resolverFixtures ? {
fixtures: resolverFixtures,
getInitialInterceptorData
} : {}) {
constructor(options) {
super(options);
const {
setResponse,
resolve
} = this;
this.setResponse = (...args) => {
let promise;
act(() => {
promise = setResponse.call(this, ...args);
});
return promise;
};
this.resolve = (...args) => {
let promise;
act(() => {
promise = resolve.call(this, ...args);
});
return promise;
};
}
}
// we want fresh manager state in each instance
const nm = new react.NetworkManager();
const sm = new react.SubscriptionManager(react.PollingSubscription);
const managers = [nm, sm];
// this pattern is dangerous if renderDataClient is shared between tests
// TODO: move to return value
renderDataClient.cleanup = () => {
nm.cleanupDate = Infinity;
Object.values(nm['rejectors']).forEach(rej => {
rej();
});
nm['clearAll']();
managers.forEach(manager => manager.cleanup());
};
renderDataClient.allSettled = () => {
return nm.allSettled();
};
const initialState = mockInitialState(initialFixtures);
const ProviderWithResolver = /*#__PURE__*/React.memo(function ProviderWithResolver({
children
}) {
return /*#__PURE__*/jsxRuntime.jsx(Provider, {
initialState: initialState,
Controller: ActController,
managers: managers,
devButton: null,
children: children
});
});
const Wrapper = options == null ? void 0 : options.wrapper;
const ProviderWithWrapper = Wrapper ? function ProviderWrapped(props) {
return /*#__PURE__*/jsxRuntime.jsx(ProviderWithResolver, {
children: /*#__PURE__*/jsxRuntime.jsx(Wrapper, {
...props
})
});
} : ProviderWithResolver;
const wrapper = ({
children,
...props
}) => /*#__PURE__*/jsxRuntime.jsx(ProviderWithWrapper, {
...props,
children: /*#__PURE__*/jsxRuntime.jsx(React.Suspense, {
fallback: null,
children: children
})
});
const ret = renderHook(callback, {
...options,
wrapper
});
ret.controller = nm['controller'];
return ret;
};
renderDataClient.cleanup = () => {};
renderDataClient.allSettled = () => Promise.allSettled([]);
return renderDataClient;
}
/** Unit test hooks that rely on DataProvider
*
* @see https://dataclient.io/docs/api/renderDataHook
*/
const renderDataHook = makeRenderDataHook(react.DataProvider);
exports.MockResolver = MockResolver;
exports.act = act;
exports.makeRenderDataClient = makeRenderDataHook;
exports.makeRenderDataHook = makeRenderDataHook;
exports.mockInitialState = mockInitialState;
exports.renderDataHook = renderDataHook;
exports.renderHook = renderHook;