UNPKG

@data-client/test

Version:
368 lines (346 loc) 12 kB
'use strict'; 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;