render hook and render ui are not the same function (node and browser)
This commit is contained in:
parent
9fe1c4b5ec
commit
3772ff85db
@ -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,
|
const vdom = !Context
|
||||||
args: any,
|
? create(example.component, example.props)
|
||||||
Context?: any,
|
: create(Context, {
|
||||||
|
...example.contextProps,
|
||||||
|
children: [create(example.component, example.props)],
|
||||||
|
});
|
||||||
|
|
||||||
|
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 {
|
): void {
|
||||||
const vdom = !Context
|
const vdom = !Context
|
||||||
? create(Component, args)
|
? create(Component, {})
|
||||||
: create(Context, { children: [create(Component, args)] });
|
: create(Context, { children: [create(Component, {})] });
|
||||||
|
|
||||||
const customElement = {} as Element;
|
//use normal mounting API since we expect
|
||||||
const parentElement = isNode ? customElement : document.createElement("div");
|
//useEffect to be called ( and similar APIs )
|
||||||
if (!isNode) {
|
renderIntoDom(vdom, {} as Element);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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++;
|
||||||
|
Loading…
Reference in New Issue
Block a user