render hook and render ui are not the same function (node and browser)

This commit is contained in:
Sebastian 2023-04-21 10:43:59 -03:00
parent 9fe1c4b5ec
commit 3772ff85db
No known key found for this signature in database
GPG Key ID: 173909D1A5F66069

View File

@ -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++;