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 { import {
ComponentChildren,
Fragment, Fragment,
FunctionComponent,
FunctionalComponent, FunctionalComponent,
VNode,
h as create, h as create,
options, options,
render as renderIntoDom, render as renderIntoDom,
VNode
} from "preact"; } 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 // This library is expected to be included in testing environment only
// When doing tests we want the requestAnimationFrame to be as fast as possible. // When doing tests we want the requestAnimationFrame to be as fast as possible.
@ -31,51 +33,83 @@ options.requestAnimationFrame = (fn: () => void) => {
return fn(); 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>, Component: FunctionalComponent<Props>,
props: Partial<Props> | (() => Partial<Props>), props: Partial<Props> | (() => Partial<Props>),
): ComponentChildren { contextProps?: T | (() => T),
): ExampleItemSetup<Props> {
const evaluatedProps = typeof props === "function" ? props() : props; const evaluatedProps = typeof props === "function" ? props() : props;
const Render = (args: any): VNode => create(Component, args); const Render = (args: any): VNode => create(Component, args);
const evaluatedContextProps =
typeof contextProps === "function" ? contextProps() : contextProps;
return { return {
component: Render, component: Render,
props: evaluatedProps, props: evaluatedProps as Props,
contextProps: !evaluatedContextProps ? {} : evaluatedContextProps,
}; };
} }
const isNode = typeof window === "undefined";
/** /**
* To be used on automated unit test. * Should render HTML on node and browser
* So test will run under node or browser * Browser: mount update and unmount
* Node: render to string
*
* @param Component * @param Component
* @param args * @param args
*/ */
export function renderNodeOrBrowser( export function renderUI(example: ExampleItemSetup<any>, Context?: any): void {
Component: any,
args: any,
Context?: any,
): void {
const vdom = !Context const vdom = !Context
? create(Component, args) ? create(example.component, example.props)
: create(Context, { children: [create(Component, args)] }); : create(Context, {
...example.contextProps,
children: [create(example.component, example.props)],
});
const customElement = {} as Element; if (typeof window === "undefined") {
const parentElement = isNode ? customElement : document.createElement("div"); renderToString(vdom);
if (!isNode) { } else {
document.body.appendChild(parentElement); const div = document.createElement("div");
} document.body.appendChild(div);
// renderIntoDom works also in nodejs renderIntoDom(vdom, div);
// if the VirtualDOM is composed only by functional components renderIntoDom(null, div);
// then no called is going to be made to the DOM api. document.body.removeChild(div);
// vdom should not have any 'div' or other html component
renderIntoDom(vdom, parentElement);
if (!isNode) {
document.body.removeChild(parentElement);
} }
} }
/**
* 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>); type RecursiveState<S> = S | (() => RecursiveState<S>);
interface Mounted<T> { interface Mounted<T> {
@ -95,8 +129,7 @@ interface Mounted<T> {
* *
* @returns testing API * @returns testing API
*/ */
// eslint-disable-next-line @typescript-eslint/ban-types function mountHook<T extends object>(
export function mountHook<T extends object>(
hookToBeTested: () => RecursiveState<T>, hookToBeTested: () => RecursiveState<T>,
Context?: ({ children }: { children: any }) => VNode | null, Context?: ({ children }: { children: any }) => VNode | null,
): Mounted<T> { ): Mounted<T> {
@ -108,6 +141,11 @@ export function mountHook<T extends object>(
function Component(): VNode { function Component(): VNode {
try { try {
let componentOrResult = hookToBeTested(); 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") { while (typeof componentOrResult === "function") {
componentOrResult = componentOrResult(); componentOrResult = componentOrResult();
} }
@ -127,7 +165,7 @@ export function mountHook<T extends object>(
return create(Fragment, {}); return create(Fragment, {});
} }
renderNodeOrBrowser(Component, {}, Context); renderHook(Component, Context);
function pullLastResult(): Exclude<T | Error | null, VoidFunction> { function pullLastResult(): Exclude<T | Error | null, VoidFunction> {
const copy: Exclude<T | Error | null, VoidFunction> = lastResult; 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(false);
} }
return Promise.resolve(true); 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`); // 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> { async function waitForStateUpdate(): Promise<boolean> {
@ -182,7 +219,6 @@ export function mountHook<T extends object>(
} }
return { return {
// unmount,
pullLastResultOrThrow, pullLastResultOrThrow,
waitForStateUpdate, waitForStateUpdate,
assertNoPendingUpdate, assertNoPendingUpdate,
@ -228,7 +264,7 @@ export async function hookBehaveLikeThis<T extends object, PropsType>(
const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } = const { pullLastResultOrThrow, waitForStateUpdate, assertNoPendingUpdate } =
mountHook<T>(() => hookFunction(props), Context); mountHook<T>(() => hookFunction(props), Context);
const [firstCheck, ...resultOfTheChecks] = checks; const [firstCheck, ...restOfTheChecks] = checks;
{ {
const state = pullLastResultOrThrow(); const state = pullLastResultOrThrow();
const checkError = firstCheck(state); const checkError = firstCheck(state);
@ -236,13 +272,13 @@ export async function hookBehaveLikeThis<T extends object, PropsType>(
return { return {
result: "fail", result: "fail",
index: 0, index: 0,
error: `Check return not undefined error: ${checkError}`, error: `First check returned with error: ${checkError}`,
}; };
} }
} }
let index = 1; let index = 1;
for (const check of resultOfTheChecks) { for (const check of restOfTheChecks) {
const hasNext = await waitForStateUpdate(); const hasNext = await waitForStateUpdate();
if (!hasNext) { if (!hasNext) {
return { return {
@ -257,7 +293,7 @@ export async function hookBehaveLikeThis<T extends object, PropsType>(
return { return {
result: "fail", result: "fail",
index, index,
error: `Check return not undefined error: ${checkError}`, error: `Check returned with error: ${checkError}`,
}; };
} }
index++; index++;