aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2023-04-21 10:43:59 -0300
committerSebastian <sebasjm@gmail.com>2023-04-21 10:43:59 -0300
commit3772ff85db61fd2260659e370e882d08d698f981 (patch)
tree417b7dc8019c47f5936da2a4f099c73248bfc9a4
parent9fe1c4b5ec26b6420a30d328fa058f2eb0bb118a (diff)
render hook and render ui are not the same function (node and browser)
-rw-r--r--packages/web-util/src/tests/hook.ts124
1 files changed, 80 insertions, 44 deletions
diff --git a/packages/web-util/src/tests/hook.ts b/packages/web-util/src/tests/hook.ts
index fb9f979e5..58ac7d8c0 100644
--- a/packages/web-util/src/tests/hook.ts
+++ b/packages/web-util/src/tests/hook.ts
@@ -15,14 +15,16 @@
*/
import {
- ComponentChildren,
Fragment,
+ FunctionComponent,
FunctionalComponent,
+ VNode,
h as create,
options,
render as renderIntoDom,
- VNode
} from "preact";
+import { render as renderToString } from "preact-render-to-string";
+import { ExampleItem, ExampleItemSetup } from "../stories.js";
// This library is expected to be included in testing environment only
// When doing tests we want the requestAnimationFrame to be as fast as possible.
@@ -31,51 +33,83 @@ options.requestAnimationFrame = (fn: () => void) => {
return fn();
};
-export function createExample<Props>(
+/**
+ *
+ * @param Component component to be tested
+ * @param props allow partial props for easier example setup
+ * @param contextProps if the context requires params for this example
+ * @returns
+ */
+export function createExample<T extends object, Props extends object>(
Component: FunctionalComponent<Props>,
props: Partial<Props> | (() => Partial<Props>),
-): ComponentChildren {
+ contextProps?: T | (() => T),
+): ExampleItemSetup<Props> {
const evaluatedProps = typeof props === "function" ? props() : props;
const Render = (args: any): VNode => create(Component, args);
-
+ const evaluatedContextProps =
+ typeof contextProps === "function" ? contextProps() : contextProps;
return {
component: Render,
- props: evaluatedProps,
+ props: evaluatedProps as Props,
+ contextProps: !evaluatedContextProps ? {} : evaluatedContextProps,
};
}
-const isNode = typeof window === "undefined";
-
/**
- * To be used on automated unit test.
- * So test will run under node or browser
+ * Should render HTML on node and browser
+ * Browser: mount update and unmount
+ * Node: render to string
+ *
* @param Component
* @param args
*/
-export function renderNodeOrBrowser(
- Component: any,
- args: any,
- Context?: any,
-): void {
+export function renderUI(example: ExampleItemSetup<any>, Context?: any): void {
const vdom = !Context
- ? create(Component, args)
- : create(Context, { children: [create(Component, args)] });
+ ? create(example.component, example.props)
+ : create(Context, {
+ ...example.contextProps,
+ children: [create(example.component, example.props)],
+ });
- 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);
+ 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);
}
}
+
+/**
+ * No need to render.
+ * Should mount, update and run effects.
+ *
+ * Browser: mount update and unmount
+ * Node: mount on a mock virtual dom
+ *
+ * Mounting hook doesn't use DOM api so is
+ * safe to use normal mounting api in node
+ *
+ * @param Component
+ * @param props
+ * @param Context
+ */
+function renderHook(
+ Component: FunctionComponent,
+ Context?: ({ children }: { children: any }) => VNode | null,
+): void {
+ const vdom = !Context
+ ? create(Component, {})
+ : create(Context, { children: [create(Component, {})] });
+
+ //use normal mounting API since we expect
+ //useEffect to be called ( and similar APIs )
+ renderIntoDom(vdom, {} as Element);
+}
+
type RecursiveState<S> = S | (() => RecursiveState<S>);
interface Mounted<T> {
@@ -89,14 +123,13 @@ interface Mounted<T> {
/**
* Manual API mount the hook and return testing API
* Consider using hookBehaveLikeThis() function
- *
+ *
* @param hookToBeTested
* @param Context
- *
+ *
* @returns testing API
*/
-// eslint-disable-next-line @typescript-eslint/ban-types
-export function mountHook<T extends object>(
+function mountHook<T extends object>(
hookToBeTested: () => RecursiveState<T>,
Context?: ({ children }: { children: any }) => VNode | null,
): Mounted<T> {
@@ -108,6 +141,11 @@ export function mountHook<T extends object>(
function Component(): VNode {
try {
let componentOrResult = hookToBeTested();
+
+ // special loop
+ // since Taler use a special type of hook that can return
+ // a function and it will be treated as a composed component
+ // then tests should be aware of it and reproduce the same behavior
while (typeof componentOrResult === "function") {
componentOrResult = componentOrResult();
}
@@ -127,7 +165,7 @@ export function mountHook<T extends object>(
return create(Fragment, {});
}
- renderNodeOrBrowser(Component, {}, Context);
+ renderHook(Component, Context);
function pullLastResult(): Exclude<T | Error | null, VoidFunction> {
const copy: Exclude<T | Error | null, VoidFunction> = lastResult;
@@ -165,7 +203,6 @@ export function mountHook<T extends object>(
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> {
@@ -182,7 +219,6 @@ export function mountHook<T extends object>(
}
return {
- // unmount,
pullLastResultOrThrow,
waitForStateUpdate,
assertNoPendingUpdate,
@@ -209,13 +245,13 @@ interface HookTestResultError {
/**
* Main testing driver.
- * It will assert that there are no more and no less hook updates than expected.
- *
+ * 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"
*/
// eslint-disable-next-line @typescript-eslint/ban-types
@@ -228,7 +264,7 @@ export async function hookBehaveLikeThis<T extends object, PropsType>(
const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
mountHook<T>(() => hookFunction(props), Context);
- const [firstCheck, ...resultOfTheChecks] = checks;
+ const [firstCheck, ...restOfTheChecks] = checks;
{
const state = pullLastResultOrThrow();
const checkError = firstCheck(state);
@@ -236,13 +272,13 @@ export async function hookBehaveLikeThis<T extends object, PropsType>(
return {
result: "fail",
index: 0,
- error: `Check return not undefined error: ${checkError}`,
+ error: `First check returned with error: ${checkError}`,
};
}
}
let index = 1;
- for (const check of resultOfTheChecks) {
+ for (const check of restOfTheChecks) {
const hasNext = await waitForStateUpdate();
if (!hasNext) {
return {
@@ -257,7 +293,7 @@ export async function hookBehaveLikeThis<T extends object, PropsType>(
return {
result: "fail",
index,
- error: `Check return not undefined error: ${checkError}`,
+ error: `Check returned with error: ${checkError}`,
};
}
index++;