diff options
-rw-r--r-- | packages/web-util/src/tests/hook.ts | 124 |
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++; |