UNPKG

@data-client/test

Version:

Testing utilities for Data Client

338 lines (316 loc) 11.1 kB
'use strict'; var react = require('@data-client/react'); var React = require('react'); var jsxRuntime = require('react/jsx-runtime'); var mock = require('@data-client/react/mock'); 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 => { // support legacy that has _TYPE suffix if (action.type === (react.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) { const replacedAction = { ...action }; const delayMs = typeof fixture.delay === 'function' ? fixture.delay(...args) : 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(() => { const MockedController = MockController(controller.constructor, fixtures ? { fixtures, getInitialInterceptorData } : {}); const controllerInterceptor = new MockedController({ ...controller, dispatch: controller['_dispatch'] ?? controller.dispatch }); return controllerInterceptor; }, [controller, fixtures, getInitialInterceptorData]); return /*#__PURE__*/jsxRuntime.jsx(react.ControllerContext.Provider, { value: controllerInterceptor, children: children }); } const USE18 = Number(React.version.substring(0, 3)) >= 18; /** * 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 activeCleanups = new Set(); if (typeof afterEach === 'function') { afterEach(() => { for (const fn of activeCleanups) fn(); activeCleanups.clear(); }); } /** @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]; const cleanup = () => { activeCleanups.delete(cleanup); nm.cleanupDate = Infinity; if (nm['rejectors']) Object.values(nm['rejectors']).forEach(rej => { rej(); });else if (nm['fetching']) nm['fetching'].forEach(({ reject }) => reject()); nm['clearAll'](); managers.forEach(manager => manager.cleanup()); }; const allSettled = () => { return nm.allSettled(); }; activeCleanups.add(cleanup); renderDataClient.cleanup = cleanup; renderDataClient.allSettled = allSettled; const initialState = mock.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?.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']; ret.cleanup = cleanup; ret.allSettled = allSettled; 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); Object.defineProperty(exports, "mockInitialState", { enumerable: true, get: function () { return mock.mockInitialState; } }); exports.MockResolver = MockResolver; exports.act = act; exports.makeRenderDataClient = makeRenderDataHook; exports.makeRenderDataHook = makeRenderDataHook; exports.renderDataHook = renderDataHook; exports.renderHook = renderHook;