diff options
Diffstat (limited to 'packages/web-util/src/tests')
-rw-r--r-- | packages/web-util/src/tests/axios.ts | 136 | ||||
-rw-r--r-- | packages/web-util/src/tests/hook.ts | 310 | ||||
-rw-r--r-- | packages/web-util/src/tests/index.ts | 2 | ||||
-rw-r--r-- | packages/web-util/src/tests/mock.ts | 458 | ||||
-rw-r--r-- | packages/web-util/src/tests/swr.ts | 82 |
5 files changed, 988 insertions, 0 deletions
diff --git a/packages/web-util/src/tests/axios.ts b/packages/web-util/src/tests/axios.ts new file mode 100644 index 000000000..38f8a9899 --- /dev/null +++ b/packages/web-util/src/tests/axios.ts @@ -0,0 +1,136 @@ +/* + This file is part of GNU Taler + (C) 2022 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +// import axios, { AxiosPromise, AxiosRequestConfig } from "axios"; +import * as axios from "axios"; +import { + setAxiosRequestAsTestingEnvironment, + mockAxiosOnce, +} from "../utils/axios.js"; + +const TESTING_DEBUG_LOG = process.env["TESTING_DEBUG_LOG"] !== undefined; + +const defaultCallback = ( + actualQuery?: axios.AxiosRequestConfig, +): axios.AxiosPromise<any> => { + if (TESTING_DEBUG_LOG) { + console.log("UNEXPECTED QUERY", actualQuery); + } + throw Error( + "Default Axios mock callback is called, this mean that the test did a tried to use axios but there was no expectation in place, try using JEST_DEBUG_LOG env", + ); +}; + +setAxiosRequestAsTestingEnvironment(defaultCallback); + +export type Query<Req, Res> = { + method: axios.Method; + url: string; + code?: number; +}; + +type ExpectationValues = { + query: Query<any, any>; + params?: { + auth?: string; + request?: object; + qparam?: Record<string, string>; + response?: object; + }; +}; + +type TestValues = [ + axios.AxiosRequestConfig | undefined, + ExpectationValues | undefined, +]; + +export class AxiosMockEnvironment { + expectations: Array< + | { + query: Query<any, any>; + auth?: string; + params?: { + request?: object; + qparam?: Record<string, string>; + response?: object; + }; + result: { args: axios.AxiosRequestConfig | undefined }; + } + | undefined + > = []; + // axiosMock: jest.MockedFunction<axios.AxiosStatic> + + addRequestExpectation< + RequestType extends object, + ResponseType extends object, + >( + expectedQuery: Query<RequestType, ResponseType>, + params: { + auth?: string; + request?: RequestType; + qparam?: any; + response?: ResponseType; + }, + ): void { + const result = mockAxiosOnce(function ( + actualQuery?: axios.AxiosRequestConfig, + ): axios.AxiosPromise { + if (TESTING_DEBUG_LOG) { + console.log("query to the backend is made", actualQuery); + } + if (!expectedQuery) { + return Promise.reject("a query was made but it was not expected"); + } + if (TESTING_DEBUG_LOG) { + console.log("expected query:", params?.request); + console.log("expected qparams:", params?.qparam); + console.log("sending response:", params?.response); + } + + const responseCode = expectedQuery.code || 200; + + //This response is what buildRequestOk is expecting in file hook/backend.ts + if (responseCode >= 200 && responseCode < 300) { + return Promise.resolve({ + data: params?.response, + config: { + data: params?.response, + params: actualQuery?.params || {}, + }, + request: { params: actualQuery?.params || {} }, + } as any); + } + //This response is what buildRequestFailed is expecting in file hook/backend.ts + return Promise.reject({ + response: { + status: responseCode, + }, + request: { + data: params?.response, + params: actualQuery?.params || {}, + }, + }); + } as any); + + this.expectations.push({ query: expectedQuery, params, result }); + } + + getLastTestValues(): TestValues { + const expectedQuery = this.expectations.shift(); + + return [expectedQuery?.result.args, expectedQuery]; + } +} diff --git a/packages/web-util/src/tests/hook.ts b/packages/web-util/src/tests/hook.ts new file mode 100644 index 000000000..f5bebbd6d --- /dev/null +++ b/packages/web-util/src/tests/hook.ts @@ -0,0 +1,310 @@ +/* + This file is part of GNU Taler + (C) 2022 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import { + ComponentChildren, + Fragment, + FunctionalComponent, + h as create, + options, + render as renderIntoDom, + VNode +} from "preact"; + +// This library is expected to be included in testing environment only +// When doing tests we want the requestAnimationFrame to be as fast as possible. +// without this option the RAF will timeout after 100ms making the tests slower +options.requestAnimationFrame = (fn: () => void) => { + return fn(); +}; + +export function createExample<Props>( + Component: FunctionalComponent<Props>, + props: Partial<Props> | (() => Partial<Props>), +): ComponentChildren { + const evaluatedProps = typeof props === "function" ? props() : props; + const Render = (args: any): VNode => create(Component, args); + + return { + component: Render, + props: evaluatedProps, + }; +} + +// export function createExampleWithCustomContext<Props, ContextProps>( +// Component: FunctionalComponent<Props>, +// props: Partial<Props> | (() => Partial<Props>), +// ContextProvider: FunctionalComponent<ContextProps>, +// contextProps: Partial<ContextProps>, +// ): ComponentChildren { +// /** +// * FIXME: +// * This may not be useful since the example can be created with context +// * already +// */ +// const evaluatedProps = typeof props === "function" ? props() : props; +// const Render = (args: any): VNode => create(Component, args); +// const WithContext = (args: any): VNode => +// create(ContextProvider, { +// ...contextProps, +// children: [Render(args)], +// } as any); + +// return { +// component: WithContext, +// props: evaluatedProps, +// }; +// } + +const isNode = typeof window === "undefined"; + +/** + * To be used on automated unit test. + * So test will run under node or browser + * @param Component + * @param args + */ +export function renderNodeOrBrowser( + Component: any, + args: any, + Context: any, +): void { + const vdom = !Context + ? create(Component, args) + : create(Context, { children: [create(Component, args)] }); + + const customElement = {} as Element; + const parentElement = isNode ? customElement : document.createElement("div"); + if (!isNode) { + document.body.appendChild(parentElement); + } + + // renderIntoDom works also in nodejs + // if the VirtualDOM is composed only by functional components + // then no called is going to be made to the DOM api. + // vdom should not have any 'div' or other html component + renderIntoDom(vdom, parentElement); + + if (!isNode) { + document.body.removeChild(parentElement); + } +} +type RecursiveState<S> = S | (() => RecursiveState<S>); + +interface Mounted<T> { + // unmount: () => void; + pullLastResultOrThrow: () => Exclude<T, VoidFunction>; + assertNoPendingUpdate: () => Promise<boolean>; + // waitNextUpdate: (s?: string) => Promise<void>; + waitForStateUpdate: () => Promise<boolean>; +} + +/** + * Manual API mount the hook and return testing API + * Consider using hookBehaveLikeThis() function + * + * @param hookToBeTested + * @param Context + * + * @returns testing API + */ +export function mountHook<T extends object>( + hookToBeTested: () => RecursiveState<T>, + Context?: ({ children }: { children: any }) => VNode | null, +): Mounted<T> { + let lastResult: Exclude<T, VoidFunction> | Error | null = null; + + const listener: Array<() => void> = []; + + // component that's going to hold the hook + function Component(): VNode { + try { + let componentOrResult = hookToBeTested(); + while (typeof componentOrResult === "function") { + componentOrResult = componentOrResult(); + } + //typecheck fails here + const l: Exclude<T, () => void> = componentOrResult as any; + lastResult = l; + } catch (e) { + if (e instanceof Error) { + lastResult = e; + } else { + lastResult = new Error(`mounting the hook throw an exception: ${e}`); + } + } + + // notify to everyone waiting for an update and clean the queue + listener.splice(0, listener.length).forEach((cb) => cb()); + return create(Fragment, {}); + } + + renderNodeOrBrowser(Component, {}, Context); + + function pullLastResult(): Exclude<T | Error | null, VoidFunction> { + const copy: Exclude<T | Error | null, VoidFunction> = lastResult; + lastResult = null; + return copy; + } + + function pullLastResultOrThrow(): Exclude<T, VoidFunction> { + const r = pullLastResult(); + if (r instanceof Error) throw r; + //sanity check + if (!r) throw Error("there was no last result"); + return r; + } + + async function assertNoPendingUpdate(): Promise<boolean> { + await new Promise((res, rej) => { + const tid = setTimeout(() => { + res(true); + }, 10); + + listener.push(() => { + clearTimeout(tid); + res(false); + // Error(`Expecting no pending result but the hook got updated. + // If the update was not intended you need to check the hook dependencies + // (or dependencies of the internal state) but otherwise make + // sure to consume the result before ending the test.`), + // ); + }); + }); + + const r = pullLastResult(); + if (r) { + return Promise.resolve(false); + } + return Promise.resolve(true); + // throw Error(`There are still pending results. + // This may happen because the hook did a new update but the test didn't consume the result using pullLastResult`); + } + async function waitForStateUpdate(): Promise<boolean> { + return await new Promise((res, rej) => { + const tid = setTimeout(() => { + res(false); + }, 10); + + listener.push(() => { + clearTimeout(tid); + res(true); + }); + }); + } + + return { + // unmount, + pullLastResultOrThrow, + waitForStateUpdate, + assertNoPendingUpdate, + }; +} + +export const nullFunction = (): void => { + null; +}; +export const nullAsyncFunction = (): Promise<void> => { + return Promise.resolve(); +}; + +type HookTestResult = HookTestResultOk | HookTestResultError; + +interface HookTestResultOk { + result: "ok"; +} +interface HookTestResultError { + result: "fail"; + error: string; + index: number; +} + +/** + * Main testing driver. + * It will assert that there are no more and no less hook updates than expected. + * + * @param hookFunction hook function to be tested + * @param props initial props for the hook + * @param checks step by step state validation + * @param Context additional testing context for overrides + * + * @returns testing result, should also be checked to be "ok" + */ +export async function hookBehaveLikeThis<T extends object, PropsType>( + hookFunction: (p: PropsType) => RecursiveState<T>, + props: PropsType, + checks: Array<(state: T) => void>, + Context?: ({ children }: { children: any }) => VNode | null, +): Promise<HookTestResult> { + const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } = + mountHook<T>(() => hookFunction(props), Context); + + const [firstCheck, ...resultOfTheChecks] = checks; + { + const state = pullLastResultOrThrow(); + const checkError = firstCheck(state); + if (checkError !== undefined) { + return { + result: "fail", + index: 0, + error: `Check return not undefined error: ${checkError}`, + }; + } + } + + let index = 1; + for (const check of resultOfTheChecks) { + const hasNext = await waitForStateUpdate(); + if (!hasNext) { + return { + result: "fail", + error: "Component didn't update and the test expected one more state", + index, + }; + } + const state = pullLastResultOrThrow(); + const checkError = check(state); + if (checkError !== undefined) { + return { + result: "fail", + index, + error: `Check return not undefined error: ${checkError}`, + }; + } + index++; + } + + const hasNext = await waitForStateUpdate(); + if (hasNext) { + return { + result: "fail", + index, + error: "Component updated and test didn't expect more states", + }; + } + const noMoreUpdates = await assertNoPendingUpdate(); + if (noMoreUpdates === false) { + return { + result: "fail", + index, + error: "Component was updated but the test does not cover the update", + }; + } + + return { + result: "ok", + }; +} diff --git a/packages/web-util/src/tests/index.ts b/packages/web-util/src/tests/index.ts new file mode 100644 index 000000000..2c0d929f8 --- /dev/null +++ b/packages/web-util/src/tests/index.ts @@ -0,0 +1,2 @@ +export * from "./hook.js"; +// export * from "./axios.js" diff --git a/packages/web-util/src/tests/mock.ts b/packages/web-util/src/tests/mock.ts new file mode 100644 index 000000000..563e437e5 --- /dev/null +++ b/packages/web-util/src/tests/mock.ts @@ -0,0 +1,458 @@ +/* + This file is part of GNU Taler + (C) 2022 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import { Logger } from "@gnu-taler/taler-util"; + +type HttpMethod = + | "get" + | "GET" + | "delete" + | "DELETE" + | "head" + | "HEAD" + | "options" + | "OPTIONS" + | "post" + | "POST" + | "put" + | "PUT" + | "patch" + | "PATCH" + | "purge" + | "PURGE" + | "link" + | "LINK" + | "unlink" + | "UNLINK"; + +export type Query<Req, Res> = { + method: HttpMethod; + url: string; + code?: number; +}; + +type ExpectationValues = { + query: Query<any, any>; + auth?: string; + params?: { + request?: object; + qparam?: Record<string, string>; + response?: object; + }; +}; + +type TestValues = { + currentExpectedQuery: ExpectationValues | undefined; + lastQuery: ExpectationValues | undefined; +}; + +const logger = new Logger("testing/swr.ts"); + +export abstract class MockEnvironment { + expectations: Array<ExpectationValues> = []; + queriesMade: Array<ExpectationValues> = []; + index = 0; + + debug: boolean; + constructor(debug: boolean) { + this.debug = debug; + this.registerRequest.bind(this); + } + + public addRequestExpectation< + RequestType extends object, + ResponseType extends object, + >( + query: Query<RequestType, ResponseType>, + params: { + auth?: string; + request?: RequestType; + qparam?: any; + response?: ResponseType; + }, + ): void { + const expected = { query, params, auth: params.auth }; + this.expectations.push(expected); + if (this.debug) { + logger.info("saving query as expected", expected); + } + this.mockApiIfNeeded(); + } + + abstract mockApiIfNeeded(): void; + + public registerRequest< + RequestType extends object, + ResponseType extends object, + >( + query: Query<RequestType, ResponseType>, + params: { + auth?: string; + request?: RequestType; + qparam?: any; + response?: ResponseType; + }, + ): { status: number; payload: ResponseType } | undefined { + const queryMade = { query, params, auth: params.auth }; + this.queriesMade.push(queryMade); + const expectedQuery = this.expectations[this.index]; + if (!expectedQuery) { + if (this.debug) { + logger.info("unexpected query made", queryMade); + } + return undefined; + } + const responseCode = this.expectations[this.index].query.code ?? 200; + const mockedResponse = this.expectations[this.index].params + ?.response as ResponseType; + if (this.debug) { + logger.info("tracking query made", { + queryMade, + expectedQuery, + }); + } + this.index++; + return { status: responseCode, payload: mockedResponse }; + } + + public assertJustExpectedRequestWereMade(): AssertStatus { + let queryNumber = 0; + + while (queryNumber < this.expectations.length) { + const r = this.assertNextRequest(queryNumber); + if (r.result !== "ok") return r; + queryNumber++; + } + return this.assertNoMoreRequestWereMade(queryNumber); + } + + private getLastTestValues(idx: number): TestValues { + const currentExpectedQuery = this.expectations[idx]; + const lastQuery = this.queriesMade[idx]; + + return { currentExpectedQuery, lastQuery }; + } + + private assertNoMoreRequestWereMade(idx: number): AssertStatus { + const { currentExpectedQuery, lastQuery } = this.getLastTestValues(idx); + + if (lastQuery !== undefined) { + return { + result: "error-did-one-more", + made: lastQuery, + }; + } + if (currentExpectedQuery !== undefined) { + return { + result: "error-did-one-less", + expected: currentExpectedQuery, + }; + } + + return { + result: "ok", + }; + } + + private assertNextRequest(idx: number): AssertStatus { + const { currentExpectedQuery, lastQuery } = this.getLastTestValues(idx); + + if (!currentExpectedQuery) { + return { + result: "error-query-missing", + }; + } + + if (!lastQuery) { + return { + result: "error-did-one-less", + expected: currentExpectedQuery, + }; + } + + if (lastQuery.query.method) { + if (currentExpectedQuery.query.method !== lastQuery.query.method) { + return { + result: "error-difference", + diff: "method", + }; + } + if (currentExpectedQuery.query.url !== lastQuery.query.url) { + return { + result: "error-difference", + diff: "url", + }; + } + } + if ( + !deepEquals( + currentExpectedQuery.params?.request, + lastQuery.params?.request, + ) + ) { + return { + result: "error-difference", + diff: "query-body", + }; + } + if ( + !deepEquals(currentExpectedQuery.params?.qparam, lastQuery.params?.qparam) + ) { + return { + result: "error-difference", + diff: "query-params", + }; + } + if (!deepEquals(currentExpectedQuery.auth, lastQuery.auth)) { + return { + result: "error-difference", + diff: "query-auth", + }; + } + + return { + result: "ok", + }; + } +} + +type AssertStatus = + | AssertOk + | AssertQueryNotMadeButExpected + | AssertQueryMadeButNotExpected + | AssertQueryMissing + | AssertExpectedQueryMethodMismatch + | AssertExpectedQueryUrlMismatch + | AssertExpectedQueryAuthMismatch + | AssertExpectedQueryBodyMismatch + | AssertExpectedQueryParamsMismatch; + +interface AssertOk { + result: "ok"; +} + +//trying to assert for a expected query but there is +//no expected query in the queue +interface AssertQueryMissing { + result: "error-query-missing"; +} + +//tested component did one more query that expected +interface AssertQueryNotMadeButExpected { + result: "error-did-one-more"; + made: ExpectationValues; +} + +//tested component didn't make an expected query +interface AssertQueryMadeButNotExpected { + result: "error-did-one-less"; + expected: ExpectationValues; +} + +interface AssertExpectedQueryMethodMismatch { + result: "error-difference"; + diff: "method"; +} +interface AssertExpectedQueryUrlMismatch { + result: "error-difference"; + diff: "url"; +} +interface AssertExpectedQueryAuthMismatch { + result: "error-difference"; + diff: "query-auth"; +} +interface AssertExpectedQueryBodyMismatch { + result: "error-difference"; + diff: "query-body"; +} +interface AssertExpectedQueryParamsMismatch { + result: "error-difference"; + diff: "query-params"; +} + +/** + * helpers + * + */ +export type Tester = (a: any, b: any) => boolean | undefined; + +function deepEquals( + a: unknown, + b: unknown, + aStack: Array<unknown> = [], + bStack: Array<unknown> = [], +): boolean { + //one if the element is null or undefined + if (a === null || b === null || b === undefined || a === undefined) { + return a === b; + } + //both are errors + if (a instanceof Error && b instanceof Error) { + return a.message == b.message; + } + //is the same object + if (Object.is(a, b)) { + return true; + } + //both the same class + const name = Object.prototype.toString.call(a); + if (name != Object.prototype.toString.call(b)) { + return false; + } + // + switch (name) { + case "[object Boolean]": + case "[object String]": + case "[object Number]": + if (typeof a !== typeof b) { + // One is a primitive, one a `new Primitive()` + return false; + } else if (typeof a !== "object" && typeof b !== "object") { + // both are proper primitives + return Object.is(a, b); + } else { + // both are `new Primitive()`s + return Object.is(a.valueOf(), b.valueOf()); + } + case "[object Date]": { + const _a = a as Date; + const _b = b as Date; + return _a == _b; + } + case "[object RegExp]": { + const _a = a as RegExp; + const _b = b as RegExp; + return _a.source === _b.source && _a.flags === _b.flags; + } + case "[object Array]": { + const _a = a as Array<any>; + const _b = b as Array<any>; + if (_a.length !== _b.length) { + return false; + } + } + } + if (typeof a !== "object" || typeof b !== "object") { + return false; + } + + if ( + typeof a === "object" && + typeof b === "object" && + !Array.isArray(a) && + !Array.isArray(b) && + hasIterator(a) && + hasIterator(b) + ) { + return iterable(a, b); + } + + // Used to detect circular references. + let length = aStack.length; + while (length--) { + if (aStack[length] === a) { + return bStack[length] === b; + } else if (bStack[length] === b) { + return false; + } + } + aStack.push(a); + bStack.push(b); + + const aKeys = allKeysFromObject(a); + const bKeys = allKeysFromObject(b); + let keySize = aKeys.length; + + //same number of keys + if (bKeys.length !== keySize) { + return false; + } + + let keyIterator: string; + while (keySize--) { + const _a = a as Record<string, object>; + const _b = b as Record<string, object>; + + keyIterator = aKeys[keySize]; + + const de = deepEquals(_a[keyIterator], _b[keyIterator], aStack, bStack); + if (!de) { + return false; + } + } + + aStack.pop(); + bStack.pop(); + + return true; +} + +function allKeysFromObject(obj: object): Array<string> { + const keys = []; + for (const key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + keys.push(key); + } + } + return keys; +} + +const IteratorSymbol = Symbol.iterator; + +function hasIterator(object: any): boolean { + return !!(object != null && object[IteratorSymbol]); +} + +function iterable( + a: unknown, + b: unknown, + aStack: Array<unknown> = [], + bStack: Array<unknown> = [], +): boolean { + if (a === null || b === null || b === undefined || a === undefined) { + return a === b; + } + if (a.constructor !== b.constructor) { + return false; + } + let length = aStack.length; + while (length--) { + if (aStack[length] === a) { + return bStack[length] === b; + } + } + aStack.push(a); + bStack.push(b); + + const aIterator = (a as any)[IteratorSymbol](); + const bIterator = (b as any)[IteratorSymbol](); + + const nextA = aIterator.next(); + while (nextA.done) { + const nextB = bIterator.next(); + if (nextB.done || !deepEquals(nextA.value, nextB.value)) { + return false; + } + } + if (!bIterator.next().done) { + return false; + } + + // Remove the first value from the stack of traversed values. + aStack.pop(); + bStack.pop(); + return true; +} diff --git a/packages/web-util/src/tests/swr.ts b/packages/web-util/src/tests/swr.ts new file mode 100644 index 000000000..95c62ebea --- /dev/null +++ b/packages/web-util/src/tests/swr.ts @@ -0,0 +1,82 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import { ComponentChildren, FunctionalComponent, h, VNode } from "preact"; +import { MockEnvironment, Query } from "./mock.js"; +import { SWRConfig } from "swr"; + +export { Query }; +/** + * Helper for hook that use SWR inside. + * + * buildTestingContext() will return a testing context + * + */ +export class SwrMockEnvironment extends MockEnvironment { + constructor(debug = false) { + super(debug); + } + + mockApiIfNeeded(): void { + null; // do nothing + } + + public buildTestingContext(): FunctionalComponent<{ + children: ComponentChildren; + }> { + const __REGISTER_REQUEST = this.registerRequest.bind(this); + return function TestingContext({ + children, + }: { + children: ComponentChildren; + }): VNode { + return h( + SWRConfig, + { + value: { + fetcher: (url: string, options: object) => { + const mocked = __REGISTER_REQUEST( + { + method: "get", + url, + }, + {}, + ); + if (!mocked) return undefined; + if (mocked.status > 400) { + const e: any = Error("simulated error for testing"); + //example error handling from https://swr.vercel.app/docs/error-handling + e.status = mocked.status; + throw e; + } + return mocked.payload; + }, + //These options are set for ending the test faster + //otherwise SWR will create timeouts that will live after the test finished + loadingTimeout: 0, + dedupingInterval: 0, + shouldRetryOnError: false, + errorRetryInterval: 0, + errorRetryCount: 0, + //clean cache for every test + provider: () => new Map(), + }, + }, + children, + ); + }; + } +} |