aboutsummaryrefslogtreecommitdiff
path: root/packages/anastasis-webui/src
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2022-06-08 15:18:41 -0300
committerSebastian <sebasjm@gmail.com>2022-06-08 15:19:26 -0300
commitb419db505b8cd5e7aa92043696f42a0d710d9226 (patch)
treec18eb877999de68c5be6821710a1c0ba7ace4a1b /packages/anastasis-webui/src
parentb00635c1404ed3cc6ed36940bd54ff70cb837f0f (diff)
downloadwallet-core-b419db505b8cd5e7aa92043696f42a0d710d9226.tar.xz
ui testing
Diffstat (limited to 'packages/anastasis-webui/src')
-rw-r--r--packages/anastasis-webui/src/main.test.ts48
-rw-r--r--packages/anastasis-webui/src/pages/home/AddingProviderScreen.tsx2
-rw-r--r--packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.tsx16
-rw-r--r--packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSetup.tsx1
-rw-r--r--packages/anastasis-webui/src/stories.tsx21
-rw-r--r--packages/anastasis-webui/src/test-utils.ts201
-rw-r--r--packages/anastasis-webui/src/utils/index.tsx14
7 files changed, 278 insertions, 25 deletions
diff --git a/packages/anastasis-webui/src/main.test.ts b/packages/anastasis-webui/src/main.test.ts
new file mode 100644
index 000000000..e65cca454
--- /dev/null
+++ b/packages/anastasis-webui/src/main.test.ts
@@ -0,0 +1,48 @@
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
+
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ *
+ * @author Sebastian Javier Marchano (sebasjm)
+ */
+import { setupI18n } from "@gnu-taler/taler-util";
+import { renderNodeOrBrowser } from "./test-utils.js";
+import * as pages from "./pages/home/index.storiesNo.js";
+
+setupI18n("en", { en: {} });
+
+function testThisStory(st: any): any {
+ describe(`render examples for ${(st as any).default.title}`, () => {
+ Object.keys(st).forEach((k) => {
+ const Component = (st as any)[k];
+ if (k === "default" || !Component) return;
+
+ it(`example: ${k}`, () => {
+ renderNodeOrBrowser(Component, Component.args);
+ });
+ });
+ });
+}
+
+describe("render every storybook example", () => {
+ [pages].forEach(function testAll(st: any) {
+ if (Array.isArray(st.default)) {
+ st.default.forEach(testAll);
+ } else {
+ testThisStory(st);
+ }
+ });
+});
diff --git a/packages/anastasis-webui/src/pages/home/AddingProviderScreen.tsx b/packages/anastasis-webui/src/pages/home/AddingProviderScreen.tsx
index feeac274d..d4675f9da 100644
--- a/packages/anastasis-webui/src/pages/home/AddingProviderScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/AddingProviderScreen.tsx
@@ -223,7 +223,7 @@ function TableRow({
onDelete: (s: string) => void;
url: string;
info: AuthenticationProviderStatusOk;
-}) {
+}): VNode {
const [status, setStatus] = useState("checking");
useEffect(function () {
testProvider(url.endsWith("/") ? url.substring(0, url.length - 1) : url)
diff --git a/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.tsx b/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.tsx
index 43d865b48..534f9136d 100644
--- a/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.tsx
+++ b/packages/anastasis-webui/src/pages/home/ContinentSelectionScreen.tsx
@@ -125,13 +125,6 @@ export function ContinentSelectionScreen(): VNode {
</div>
</div>
</div>
-
- {/* {theCountry && <div class="field">
- <label class="label">Available currencies:</label>
- <div class="control">
- <input class="input is-small" type="text" readonly value={theCountry.currency} />
- </div>
- </div>} */}
</div>
<div class="column is-two-third">
<p>
@@ -151,18 +144,11 @@ export function ContinentSelectionScreen(): VNode {
}}
>
<p>
- If you just want to try out Anastasis, we recomment that you
+ If you just want to try out Anastasis, we recommend that you
choose <b>Testcontinent</b> with <b>Demoland</b>. For this special
country, you will be asked for a simple number and not real,
personal identifiable information.
</p>
- {/*
- <p>
- Because of the diversity of personally identifying information in
- different countries and cultures, we do not support all countries
- yet. If you want to improve the supported countries,{" "}
- <a href="mailto:contact@anastasis.lu">contact us</a>.
- </p> */}
</div>
</div>
</div>
diff --git a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSetup.tsx b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSetup.tsx
index f9b292d94..0285c87e1 100644
--- a/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSetup.tsx
+++ b/packages/anastasis-webui/src/pages/home/authMethod/AuthMethodTotpSetup.tsx
@@ -36,6 +36,7 @@ export function AuthMethodTotpSetup({
const [test, setTest] = useState("");
const secretKey = useMemo(() => {
const array = new Uint8Array(32);
+ if (typeof window === "undefined") return array;
return window.crypto.getRandomValues(array);
}, []);
diff --git a/packages/anastasis-webui/src/stories.tsx b/packages/anastasis-webui/src/stories.tsx
index a51dfb20f..351d6f37b 100644
--- a/packages/anastasis-webui/src/stories.tsx
+++ b/packages/anastasis-webui/src/stories.tsx
@@ -143,21 +143,24 @@ function ExampleList({
{k.examples.map((r) => {
const e = encodeURIComponent;
const eId = `${e(r.group)}-${e(r.component)}-${e(r.name)}`;
+ function doSelection(e: any): void {
+ e.preventDefault();
+ location.hash = `#${eId}`;
+ onSelectStory(r, eId);
+ }
const isSelected =
selected &&
selected.component === r.component &&
selected.group === r.group &&
selected.name === r.name;
return (
- <dd id={eId} key={r.name} data-selected={isSelected}>
- <a
- href={`#${eId}`}
- onClick={(e) => {
- e.preventDefault();
- location.hash = `#${eId}`;
- onSelectStory(r, eId);
- }}
- >
+ <dd
+ id={eId}
+ key={r.name}
+ data-selected={isSelected}
+ onClick={doSelection}
+ >
+ <a href={`#${eId}`} onClick={doSelection}>
{r.name}
</a>
</dd>
diff --git a/packages/anastasis-webui/src/test-utils.ts b/packages/anastasis-webui/src/test-utils.ts
new file mode 100644
index 000000000..1fcc753ee
--- /dev/null
+++ b/packages/anastasis-webui/src/test-utils.ts
@@ -0,0 +1,201 @@
+/*
+ This file is part of GNU Anastasis
+ (C) 2021-2022 Anastasis SARL
+
+ GNU Anastasis is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Anastasis 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ GNU Anastasis; 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";
+import { render as renderToString } from "preact-render-to-string";
+
+// 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) => {
+ // console.log("RAF called")
+ return fn();
+};
+
+export function createExample<Props>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props> | (() => Partial<Props>),
+): ComponentChildren {
+ //FIXME: props are evaluated on build time
+ // in some cases we want to evaluated the props on render time so we can get some relative timestamp
+ // check how we can build evaluatedProps in render time
+ const evaluatedProps = typeof props === "function" ? props() : props;
+ const Render = (args: any): VNode => create(Component, args);
+ Render.args = evaluatedProps;
+ return Render;
+}
+
+export function createExampleWithCustomContext<Props, ContextProps>(
+ Component: FunctionalComponent<Props>,
+ props: Partial<Props> | (() => Partial<Props>),
+ ContextProvider: FunctionalComponent<ContextProps>,
+ contextProps: Partial<ContextProps>,
+): ComponentChildren {
+ 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);
+ WithContext.args = evaluatedProps;
+ return WithContext;
+}
+
+export function NullLink({
+ children,
+}: {
+ children?: ComponentChildren;
+}): VNode {
+ return create("a", { children, href: "javascript:void(0);" });
+}
+
+export function renderNodeOrBrowser(Component: any, args: any): void {
+ const vdom = create(Component, args);
+ if (typeof window === "undefined") {
+ renderToString(vdom);
+ } else {
+ const div = document.createElement("div");
+ document.body.appendChild(div);
+ renderIntoDom(vdom, div);
+ renderIntoDom(null, div);
+ document.body.removeChild(div);
+ }
+}
+
+interface Mounted<T> {
+ unmount: () => void;
+ getLastResultOrThrow: () => T;
+ assertNoPendingUpdate: () => void;
+ waitNextUpdate: (s?: string) => Promise<void>;
+}
+
+const isNode = typeof window === "undefined";
+
+export function mountHook<T>(
+ callback: () => T,
+ Context?: ({ children }: { children: any }) => VNode,
+): Mounted<T> {
+ // const result: { current: T | null } = {
+ // current: null
+ // }
+ let lastResult: T | Error | null = null;
+
+ const listener: Array<() => void> = [];
+
+ // component that's going to hold the hook
+ function Component(): VNode {
+ try {
+ lastResult = callback();
+ } 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, {})] });
+
+ // waiter callback
+ async function waitNextUpdate(_label = ""): Promise<void> {
+ if (_label) _label = `. label: "${_label}"`;
+ await new Promise((res, rej) => {
+ const tid = setTimeout(() => {
+ rej(
+ Error(`waiting for an update but the hook didn't make one${_label}`),
+ );
+ }, 100);
+
+ listener.push(() => {
+ clearTimeout(tid);
+ res(undefined);
+ });
+ });
+ }
+
+ 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 getLastResult(): T | Error | null {
+ const copy = lastResult;
+ lastResult = null;
+ return copy;
+ }
+
+ function getLastResultOrThrow(): T {
+ const r = getLastResult();
+ 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 = getLastResult();
+ 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 getLastResult`);
+ }
+ return {
+ unmount,
+ getLastResultOrThrow,
+ waitNextUpdate,
+ assertNoPendingUpdate,
+ };
+}
diff --git a/packages/anastasis-webui/src/utils/index.tsx b/packages/anastasis-webui/src/utils/index.tsx
index ce21071f7..204c48d18 100644
--- a/packages/anastasis-webui/src/utils/index.tsx
+++ b/packages/anastasis-webui/src/utils/index.tsx
@@ -209,10 +209,12 @@ export const reducerStatesExample = {
initial: undefined,
recoverySelectCountry: {
...base,
+ reducer_type: "recovery",
recovery_state: RecoveryStates.CountrySelecting,
} as ReducerState,
recoverySelectContinent: {
...base,
+ reducer_type: "recovery",
recovery_state: RecoveryStates.ContinentSelecting,
} as ReducerState,
secretSelection: {
@@ -222,10 +224,12 @@ export const reducerStatesExample = {
} as ReducerState,
recoveryFinished: {
...base,
+ reducer_type: "recovery",
recovery_state: RecoveryStates.RecoveryFinished,
} as ReducerState,
challengeSelecting: {
...base,
+ reducer_type: "recovery",
recovery_state: RecoveryStates.ChallengeSelecting,
} as ReducerState,
challengeSolving: {
@@ -235,34 +239,42 @@ export const reducerStatesExample = {
} as ReducerStateRecovery,
challengePaying: {
...base,
+ reducer_type: "recovery",
recovery_state: RecoveryStates.ChallengePaying,
} as ReducerState,
recoveryAttributeEditing: {
...base,
+ reducer_type: "recovery",
recovery_state: RecoveryStates.UserAttributesCollecting,
} as ReducerState,
backupSelectCountry: {
...base,
+ reducer_type: "backup",
backup_state: BackupStates.CountrySelecting,
} as ReducerState,
backupSelectContinent: {
...base,
+ reducer_type: "backup",
backup_state: BackupStates.ContinentSelecting,
} as ReducerState,
secretEdition: {
...base,
+ reducer_type: "backup",
backup_state: BackupStates.SecretEditing,
} as ReducerState,
policyReview: {
...base,
+ reducer_type: "backup",
backup_state: BackupStates.PoliciesReviewing,
} as ReducerState,
policyPay: {
...base,
+ reducer_type: "backup",
backup_state: BackupStates.PoliciesPaying,
} as ReducerState,
backupFinished: {
...base,
+ reducer_type: "backup",
backup_state: BackupStates.BackupFinished,
} as ReducerState,
authEditing: {
@@ -272,10 +284,12 @@ export const reducerStatesExample = {
} as ReducerState,
backupAttributeEditing: {
...base,
+ reducer_type: "backup",
backup_state: BackupStates.UserAttributesCollecting,
} as ReducerState,
truthsPaying: {
...base,
+ reducer_type: "backup",
backup_state: BackupStates.TruthsPaying,
} as ReducerState,
};