gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-wallet-core] 01/06: compose, testing and async into web-util


From: gnunet
Subject: [taler-wallet-core] 01/06: compose, testing and async into web-util
Date: Wed, 14 Dec 2022 19:35:46 +0100

This is an automated email from the git hooks/post-receive script.

sebasjm pushed a commit to branch master
in repository wallet-core.

commit 880961034c81e85e191c6c4b845d96506bbd4ea7
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Mon Dec 12 10:57:14 2022 -0300

    compose, testing and async into web-util
---
 packages/web-util/package.json                |   1 +
 packages/web-util/src/components/index.ts     |   2 +
 packages/web-util/src/components/utils.ts     |  36 +++++
 packages/web-util/src/hooks/index.ts          |   3 +-
 packages/web-util/src/hooks/useAsyncAsHook.ts |  91 +++++++++++
 packages/web-util/src/index.browser.ts        |   2 +
 packages/web-util/src/test/index.ts           | 224 ++++++++++++++++++++++++++
 pnpm-lock.yaml                                |   2 +
 8 files changed, 360 insertions(+), 1 deletion(-)

diff --git a/packages/web-util/package.json b/packages/web-util/package.json
index a4d1c116b..1add56d87 100644
--- a/packages/web-util/package.json
+++ b/packages/web-util/package.json
@@ -31,6 +31,7 @@
     "esbuild": "^0.14.21",
     "express": "^4.18.2",
     "preact": "10.11.3",
+    "preact-render-to-string": "^5.2.6",
     "prettier": "^2.5.1",
     "rimraf": "^3.0.2",
     "tslib": "^2.4.0",
diff --git a/packages/web-util/src/components/index.ts 
b/packages/web-util/src/components/index.ts
new file mode 100644
index 000000000..dc7c86d7d
--- /dev/null
+++ b/packages/web-util/src/components/index.ts
@@ -0,0 +1,2 @@
+
+export * as utils from "./utils.js";
diff --git a/packages/web-util/src/components/utils.ts 
b/packages/web-util/src/components/utils.ts
new file mode 100644
index 000000000..71824e14f
--- /dev/null
+++ b/packages/web-util/src/components/utils.ts
@@ -0,0 +1,36 @@
+import { createElement, VNode } from "preact";
+
+export type StateFunc<S> = (p: S) => VNode;
+
+export type StateViewMap<StateType extends { status: string }> = {
+  [S in StateType as S["status"]]: StateFunc<S>;
+};
+
+export type RecursiveState<S extends object> = S | (() => RecursiveState<S>);
+
+export function compose<SType extends { status: string }, PType>(
+  hook: (p: PType) => RecursiveState<SType>,
+  viewMap: StateViewMap<SType>,
+): (p: PType) => VNode {
+  function withHook(stateHook: () => RecursiveState<SType>): () => VNode {
+    function ComposedComponent(): VNode {
+      const state = stateHook();
+
+      if (typeof state === "function") {
+        const subComponent = withHook(state);
+        return createElement(subComponent, {});
+      }
+
+      const statusName = state.status as unknown as SType["status"];
+      const viewComponent = viewMap[statusName] as unknown as StateFunc<SType>;
+      return createElement(viewComponent, state);
+    }
+
+    return ComposedComponent;
+  }
+
+  return (p: PType) => {
+    const h = withHook(() => hook(p));
+    return h();
+  };
+}
diff --git a/packages/web-util/src/hooks/index.ts 
b/packages/web-util/src/hooks/index.ts
index f18d61b9c..9ac56c4ac 100644
--- a/packages/web-util/src/hooks/index.ts
+++ b/packages/web-util/src/hooks/index.ts
@@ -1,3 +1,4 @@
 
 export { useLang } from "./useLang.js";
-export { useLocalStorage, useNotNullLocalStorage } from "./useLocalStorage.js"
\ No newline at end of file
+export { useLocalStorage, useNotNullLocalStorage } from "./useLocalStorage.js"
+export { useAsyncAsHook, HookError, HookOk, HookResponse, 
HookResponseWithRetry, HookGenericError, HookOperationalError } from 
"./useAsyncAsHook.js"
\ No newline at end of file
diff --git a/packages/web-util/src/hooks/useAsyncAsHook.ts 
b/packages/web-util/src/hooks/useAsyncAsHook.ts
new file mode 100644
index 000000000..48d29aa45
--- /dev/null
+++ b/packages/web-util/src/hooks/useAsyncAsHook.ts
@@ -0,0 +1,91 @@
+/*
+ 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 { TalerErrorDetail } from "@gnu-taler/taler-util";
+// import { TalerError } from "@gnu-taler/taler-wallet-core";
+import { useEffect, useMemo, useState } from "preact/hooks";
+
+export interface HookOk<T> {
+  hasError: false;
+  response: T;
+}
+
+export type HookError = HookGenericError | HookOperationalError;
+
+export interface HookGenericError {
+  hasError: true;
+  operational: false;
+  message: string;
+}
+
+export interface HookOperationalError {
+  hasError: true;
+  operational: true;
+  details: TalerErrorDetail;
+}
+
+interface WithRetry {
+  retry: () => void;
+}
+
+export type HookResponse<T> = HookOk<T> | HookError | undefined;
+export type HookResponseWithRetry<T> =
+  | ((HookOk<T> | HookError) & WithRetry)
+  | undefined;
+
+export function useAsyncAsHook<T>(
+  fn: () => Promise<T | false>,
+  deps?: any[],
+): HookResponseWithRetry<T> {
+  const [result, setHookResponse] = useState<HookResponse<T>>(undefined);
+
+  const args = useMemo(
+    () => ({
+      fn,
+      // eslint-disable-next-line react-hooks/exhaustive-deps
+    }),
+    deps || [],
+  );
+
+  async function doAsync(): Promise<void> {
+    try {
+      const response = await args.fn();
+      if (response === false) return;
+      setHookResponse({ hasError: false, response });
+    } catch (e) {
+      // if (e instanceof TalerError) {
+      //   setHookResponse({
+      //     hasError: true,
+      //     operational: true,
+      //     details: e.errorDetail,
+      //   });
+      // } else
+      if (e instanceof Error) {
+        setHookResponse({
+          hasError: true,
+          operational: false,
+          message: e.message,
+        });
+      }
+    }
+  }
+
+  useEffect(() => {
+    doAsync();
+  }, [args]);
+
+  if (!result) return undefined;
+  return { ...result, retry: doAsync };
+}
diff --git a/packages/web-util/src/index.browser.ts 
b/packages/web-util/src/index.browser.ts
index 2197d1b24..734a2f426 100644
--- a/packages/web-util/src/index.browser.ts
+++ b/packages/web-util/src/index.browser.ts
@@ -1,3 +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 { renderStories, parseGroupImport } from "./stories.js";
diff --git a/packages/web-util/src/test/index.ts 
b/packages/web-util/src/test/index.ts
new file mode 100644
index 000000000..623115e79
--- /dev/null
+++ b/packages/web-util/src/test/index.ts
@@ -0,0 +1,224 @@
+/*
+ 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/pnpm-lock.yaml b/pnpm-lock.yaml
index daf339e8d..43c45691b 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -701,6 +701,7 @@ importers:
       esbuild: ^0.14.21
       express: ^4.18.2
       preact: 10.11.3
+      preact-render-to-string: ^5.2.6
       prettier: ^2.5.1
       rimraf: ^3.0.2
       tslib: ^2.4.0
@@ -716,6 +717,7 @@ importers:
       esbuild: 0.14.54
       express: 4.18.2
       preact: 10.11.3
+      preact-render-to-string: 5.2.6_preact@10.11.3
       prettier: 2.7.1
       rimraf: 3.0.2
       tslib: 2.4.1

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]