aboutsummaryrefslogtreecommitdiff
path: root/packages/web-util
diff options
context:
space:
mode:
Diffstat (limited to 'packages/web-util')
-rwxr-xr-xpackages/web-util/build.mjs4
-rw-r--r--packages/web-util/package.json6
-rw-r--r--packages/web-util/src/components/index.ts1
-rw-r--r--packages/web-util/src/context/index.ts7
-rw-r--r--packages/web-util/src/context/translation.ts6
-rw-r--r--packages/web-util/src/hooks/index.ts13
-rw-r--r--packages/web-util/src/hooks/useLocalStorage.ts36
-rw-r--r--packages/web-util/src/index.browser.ts2
-rw-r--r--packages/web-util/src/live-reload.ts24
-rw-r--r--packages/web-util/src/serve.ts25
-rw-r--r--packages/web-util/src/test/index.ts224
-rw-r--r--packages/web-util/src/tests/axios.ts136
-rw-r--r--packages/web-util/src/tests/hook.ts310
-rw-r--r--packages/web-util/src/tests/index.ts2
-rw-r--r--packages/web-util/src/tests/mock.ts458
-rw-r--r--packages/web-util/src/tests/swr.ts82
-rw-r--r--packages/web-util/src/utils/axios.ts79
-rw-r--r--packages/web-util/src/utils/index.ts1
18 files changed, 1139 insertions, 277 deletions
diff --git a/packages/web-util/build.mjs b/packages/web-util/build.mjs
index ba277b666..e7aede81c 100755
--- a/packages/web-util/build.mjs
+++ b/packages/web-util/build.mjs
@@ -78,13 +78,13 @@ const buildConfigNode = {
const buildConfigBrowser = {
...buildConfigBase,
- entryPoints: ["src/index.browser.ts", "src/live-reload.ts", 'src/stories.tsx'],
+ entryPoints: ["src/tests/axios.ts", "src/tests/swr.ts", "src/index.browser.ts", "src/live-reload.ts", 'src/stories.tsx'],
outExtension: {
'.js': '.mjs'
},
format: 'esm',
platform: 'browser',
- external: ["preact", "@gnu-taler/taler-util", "jed"],
+ external: ["preact", "@gnu-taler/taler-util", "jed","swr","axios"],
jsxFactory: 'h',
jsxFragment: 'Fragment',
};
diff --git a/packages/web-util/package.json b/packages/web-util/package.json
index 1add56d87..ad87304fe 100644
--- a/packages/web-util/package.json
+++ b/packages/web-util/package.json
@@ -12,6 +12,8 @@
"license": "AGPL-3.0-or-later",
"private": false,
"exports": {
+ "./lib/tests/swr": "./lib/tests/swr.mjs",
+ "./lib/tests/axios": "./lib/tests/axios.mjs",
"./lib/index.browser": "./lib/index.browser.mjs",
"./lib/index.node": "./lib/index.node.cjs"
},
@@ -27,6 +29,7 @@
"@types/node": "^18.11.9",
"@types/web": "^0.0.82",
"@types/ws": "^8.5.3",
+ "axios": "^1.2.1",
"chokidar": "^3.5.3",
"esbuild": "^0.14.21",
"express": "^4.18.2",
@@ -34,8 +37,9 @@
"preact-render-to-string": "^5.2.6",
"prettier": "^2.5.1",
"rimraf": "^3.0.2",
+ "swr": "1.3.0",
"tslib": "^2.4.0",
"typescript": "^4.8.4",
"ws": "7.4.5"
}
-}
+} \ No newline at end of file
diff --git a/packages/web-util/src/components/index.ts b/packages/web-util/src/components/index.ts
index dc7c86d7d..9441e971d 100644
--- a/packages/web-util/src/components/index.ts
+++ b/packages/web-util/src/components/index.ts
@@ -1,2 +1 @@
-
export * as utils from "./utils.js";
diff --git a/packages/web-util/src/context/index.ts b/packages/web-util/src/context/index.ts
index 0ac2c752a..4bc1b22f2 100644
--- a/packages/web-util/src/context/index.ts
+++ b/packages/web-util/src/context/index.ts
@@ -1,2 +1,5 @@
-
-export { InternationalizationAPI, TranslationProvider, useTranslationContext } from "./translation.js";
+export {
+ InternationalizationAPI,
+ TranslationProvider,
+ useTranslationContext,
+} from "./translation.js";
diff --git a/packages/web-util/src/context/translation.ts b/packages/web-util/src/context/translation.ts
index ce140ec42..3b79e31d3 100644
--- a/packages/web-util/src/context/translation.ts
+++ b/packages/web-util/src/context/translation.ts
@@ -19,7 +19,7 @@ import { ComponentChildren, createContext, h, VNode } from "preact";
import { useContext, useEffect } from "preact/hooks";
import { useLang } from "../hooks/index.js";
-export type InternationalizationAPI = typeof i18n
+export type InternationalizationAPI = typeof i18n;
interface Type {
lang: string;
@@ -54,7 +54,7 @@ interface Props {
initial?: string;
children: ComponentChildren;
forceLang?: string;
- source: Record<string, any>
+ source: Record<string, any>;
}
// Outmost UI wrapper.
@@ -62,7 +62,7 @@ export const TranslationProvider = ({
initial,
children,
forceLang,
- source
+ source,
}: Props): VNode => {
const [lang, changeLanguage, isSaved] = useLang(initial);
useEffect(() => {
diff --git a/packages/web-util/src/hooks/index.ts b/packages/web-util/src/hooks/index.ts
index 9ac56c4ac..393a6fcbb 100644
--- a/packages/web-util/src/hooks/index.ts
+++ b/packages/web-util/src/hooks/index.ts
@@ -1,4 +1,11 @@
-
export { useLang } from "./useLang.js";
-export { useLocalStorage, useNotNullLocalStorage } from "./useLocalStorage.js"
-export { useAsyncAsHook, HookError, HookOk, HookResponse, HookResponseWithRetry, HookGenericError, HookOperationalError } from "./useAsyncAsHook.js" \ No newline at end of file
+export { useLocalStorage, useNotNullLocalStorage } from "./useLocalStorage.js";
+export {
+ useAsyncAsHook,
+ HookError,
+ HookOk,
+ HookResponse,
+ HookResponseWithRetry,
+ HookGenericError,
+ HookOperationalError,
+} from "./useAsyncAsHook.js";
diff --git a/packages/web-util/src/hooks/useLocalStorage.ts b/packages/web-util/src/hooks/useLocalStorage.ts
index f518405b6..ab786db13 100644
--- a/packages/web-util/src/hooks/useLocalStorage.ts
+++ b/packages/web-util/src/hooks/useLocalStorage.ts
@@ -35,13 +35,13 @@ export function useLocalStorage(
useEffect(() => {
const listener = buildListenerForKey(key, (newValue) => {
- setStoredValue(newValue ?? initialValue)
- })
- window.addEventListener('storage', listener)
+ setStoredValue(newValue ?? initialValue);
+ });
+ window.addEventListener("storage", listener);
return () => {
- window.removeEventListener('storage', listener)
- }
- }, [])
+ window.removeEventListener("storage", listener);
+ };
+ }, []);
const setValue = (
value?: string | ((val?: string) => string | undefined),
@@ -62,11 +62,14 @@ export function useLocalStorage(
return [storedValue, setValue];
}
-function buildListenerForKey(key: string, onUpdate: (newValue: string | undefined) => void): () => void {
+function buildListenerForKey(
+ key: string,
+ onUpdate: (newValue: string | undefined) => void,
+): () => void {
return function listenKeyChange() {
- const value = window.localStorage.getItem(key)
- onUpdate(value ?? undefined)
- }
+ const value = window.localStorage.getItem(key);
+ onUpdate(value ?? undefined);
+ };
}
//TODO: merge with the above function
@@ -80,16 +83,15 @@ export function useNotNullLocalStorage(
: initialValue;
});
-
useEffect(() => {
const listener = buildListenerForKey(key, (newValue) => {
- setStoredValue(newValue ?? initialValue)
- })
- window.addEventListener('storage', listener)
+ setStoredValue(newValue ?? initialValue);
+ });
+ window.addEventListener("storage", listener);
return () => {
- window.removeEventListener('storage', listener)
- }
- })
+ window.removeEventListener("storage", listener);
+ };
+ });
const setValue = (value: string | ((val: string) => string)): void => {
const valueToStore = value instanceof Function ? value(storedValue) : value;
diff --git a/packages/web-util/src/index.browser.ts b/packages/web-util/src/index.browser.ts
index 734a2f426..d3aeae168 100644
--- a/packages/web-util/src/index.browser.ts
+++ b/packages/web-util/src/index.browser.ts
@@ -1,5 +1,5 @@
export * from "./hooks/index.js";
export * from "./context/index.js";
export * from "./components/index.js";
-export * as test from "./test/index.js";
+export * as tests from "./tests/index.js";
export { renderStories, parseGroupImport } from "./stories.js";
diff --git a/packages/web-util/src/live-reload.ts b/packages/web-util/src/live-reload.ts
index 901127f83..74d542956 100644
--- a/packages/web-util/src/live-reload.ts
+++ b/packages/web-util/src/live-reload.ts
@@ -15,24 +15,24 @@ function setupLiveReload(): void {
return;
}
if (event.type === "file-updated-failed") {
- const h1 = document.getElementById("overlay-text")
+ const h1 = document.getElementById("overlay-text");
if (h1) {
- h1.innerHTML = "compilation failed"
- h1.style.color = 'red'
- h1.style.margin = ''
+ h1.innerHTML = "compilation failed";
+ h1.style.color = "red";
+ h1.style.margin = "";
}
- const div = document.getElementById("overlay")
+ const div = document.getElementById("overlay");
if (div) {
- const content = JSON.stringify(event.data, undefined, 2)
+ const content = JSON.stringify(event.data, undefined, 2);
const pre = document.createElement("pre");
- pre.id = "error-text"
+ pre.id = "error-text";
pre.style.margin = "";
pre.textContent = content;
div.style.backgroundColor = "rgba(0,0,0,0.8)";
- div.style.flexDirection = 'column'
+ div.style.flexDirection = "column";
div.appendChild(pre);
}
- console.error(event.data.error)
+ console.error(event.data.error);
return;
}
if (event.type === "file-updated") {
@@ -56,17 +56,17 @@ setupLiveReload();
function showReloadOverlay(): void {
const d = document.createElement("div");
- d.id = "overlay"
+ d.id = "overlay";
d.style.position = "absolute";
d.style.width = "100%";
d.style.height = "100%";
d.style.color = "white";
d.style.backgroundColor = "rgba(0,0,0,0.5)";
d.style.display = "flex";
- d.style.zIndex = String(Number.MAX_SAFE_INTEGER)
+ d.style.zIndex = String(Number.MAX_SAFE_INTEGER);
d.style.justifyContent = "center";
const h = document.createElement("h1");
- h.id = "overlay-text"
+ h.id = "overlay-text";
h.style.margin = "auto";
h.innerHTML = "reloading...";
d.appendChild(h);
diff --git a/packages/web-util/src/serve.ts b/packages/web-util/src/serve.ts
index 3248bbeb8..f3a97e2e2 100644
--- a/packages/web-util/src/serve.ts
+++ b/packages/web-util/src/serve.ts
@@ -77,23 +77,26 @@ export async function serve(opts: {
if (opts.onUpdate) {
sendToAllClients({ type: "file-updated-start", data: { path } });
- opts.onUpdate().then((result) => {
- sendToAllClients({
- type: "file-updated-done",
- data: { path, result },
+ opts
+ .onUpdate()
+ .then((result) => {
+ sendToAllClients({
+ type: "file-updated-done",
+ data: { path, result },
+ });
+ })
+ .catch((error) => {
+ sendToAllClients({
+ type: "file-updated-failed",
+ data: { path, error },
+ });
});
- }).catch((error) => {
- sendToAllClients({
- type: "file-updated-failed",
- data: { path, error },
- });
- });
} else {
sendToAllClients({ type: "file-change", data: { path } });
}
});
- if (opts.onUpdate) opts.onUpdate()
+ if (opts.onUpdate) opts.onUpdate();
app.get(PATHS.EXAMPLE, function (req: any, res: any) {
res.set("Content-Type", "text/html");
diff --git a/packages/web-util/src/test/index.ts b/packages/web-util/src/test/index.ts
deleted file mode 100644
index 623115e79..000000000
--- a/packages/web-util/src/test/index.ts
+++ /dev/null
@@ -1,224 +0,0 @@
-/*
- 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 { NotificationType } from "@gnu-taler/taler-util";
-// import {
-// WalletCoreApiClient,
-// WalletCoreOpKeys,
-// WalletCoreRequestType,
-// WalletCoreResponseType,
-// } from "@gnu-taler/taler-wallet-core";
-import {
- ComponentChildren,
- Fragment,
- FunctionalComponent,
- h as create,
- options,
- render as renderIntoDom,
- VNode,
-} from "preact";
-import { render as renderToString } from "preact-render-to-string";
-// import { BackgroundApiClient, wxApi } from "./wxApi.js";
-
-// 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): void {
- const vdom = create(Component, args);
- if (isNode) {
- renderToString(vdom);
- } else {
- const div = document.createElement("div");
- document.body.appendChild(div);
- renderIntoDom(vdom, div);
- renderIntoDom(null, div);
- document.body.removeChild(div);
- }
-}
-type RecursiveState<S> = S | (() => RecursiveState<S>);
-
-interface Mounted<T> {
- unmount: () => void;
- pullLastResultOrThrow: () => Exclude<T, VoidFunction>;
- assertNoPendingUpdate: () => void;
- // waitNextUpdate: (s?: string) => Promise<void>;
- waitForStateUpdate: () => Promise<boolean>;
-}
-
-/**
- * Main test API, mount the hook and return testing API
- * @param callback
- * @param Context
- * @returns
- */
-export function mountHook<T extends object>(
- callback: () => RecursiveState<T>,
- Context?: ({ children }: { children: any }) => VNode,
-): 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 = callback();
- 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, {});
- }
-
- // create the vdom with context if required
- const vdom = !Context
- ? create(Component, {})
- : create(Context, { children: [create(Component, {})] });
-
- const customElement = {} as Element;
- const parentElement = isNode ? customElement : document.createElement("div");
- if (!isNode) {
- document.body.appendChild(parentElement);
- }
-
- renderIntoDom(vdom, parentElement);
-
- // clean up callback
- function unmount(): void {
- if (!isNode) {
- document.body.removeChild(parentElement);
- }
- }
-
- 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;
- if (!r) throw Error("there was no last result");
- return r;
- }
-
- async function assertNoPendingUpdate(): Promise<void> {
- await new Promise((res, rej) => {
- const tid = setTimeout(() => {
- res(undefined);
- }, 10);
-
- listener.push(() => {
- clearTimeout(tid);
- rej(
- 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)
- 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() } \ No newline at end of file
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,
+ );
+ };
+ }
+}
diff --git a/packages/web-util/src/utils/axios.ts b/packages/web-util/src/utils/axios.ts
new file mode 100644
index 000000000..c38314009
--- /dev/null
+++ b/packages/web-util/src/utils/axios.ts
@@ -0,0 +1,79 @@
+/*
+ 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 axios, { AxiosPromise, AxiosRequestConfig } from "axios";
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+
+//FIXME: remove this, since it is not used anymore
+/**
+ * @deprecated
+ */
+export let removeAxiosCancelToken = false;
+
+export let axiosHandler = function doAxiosRequest(
+ config: AxiosRequestConfig,
+): AxiosPromise<any> {
+ return axios(config);
+};
+
+const listOfHandlersToUseOnce = new Array<AxiosHandler>();
+
+/**
+ * Set this backend library to testing mode.
+ * Instead of calling the axios library the @handler will be called
+ *
+ * @param handler callback that will mock axios
+ */
+export function setAxiosRequestAsTestingEnvironment(
+ handler: AxiosHandler,
+): void {
+ removeAxiosCancelToken = true;
+ axiosHandler = function defaultTestingHandler(config) {
+ const currentHanlder = listOfHandlersToUseOnce.shift();
+ if (!currentHanlder) {
+ return handler(config);
+ }
+
+ return currentHanlder(config);
+ };
+}
+
+type AxiosHandler = (config: AxiosRequestConfig) => AxiosPromise<any>;
+type AxiosArguments = { args: AxiosRequestConfig | undefined };
+
+/**
+ * Replace Axios handler with a mock.
+ * Throw if is called more than once
+ *
+ * @param handler mock function
+ * @returns savedArgs
+ */
+export function mockAxiosOnce(handler: AxiosHandler): {
+ args: AxiosRequestConfig | undefined;
+} {
+ const savedArgs: AxiosArguments = { args: undefined };
+ listOfHandlersToUseOnce.push(
+ (config: AxiosRequestConfig): AxiosPromise<any> => {
+ savedArgs.args = config;
+ return handler(config);
+ },
+ );
+ return savedArgs;
+}
diff --git a/packages/web-util/src/utils/index.ts b/packages/web-util/src/utils/index.ts
new file mode 100644
index 000000000..6dfbd5f8d
--- /dev/null
+++ b/packages/web-util/src/utils/index.ts
@@ -0,0 +1 @@
+export * from "./axios.js";