Files
owl/tests/helpers.ts
T
Géry Debongnie b10a700381 [REF] add static App.registerTemplate and update Portal to use it
The goal is to get closer to the possibility of using Owl without the
compiler. This commit removes Portal dependency on the compiler, and
opens the way to register pre-compiled template functions.
2022-05-31 14:00:01 +02:00

266 lines
6.9 KiB
TypeScript

import { diff } from "jest-diff";
import {
blockDom,
Component,
onMounted,
onPatched,
onRendered,
onWillDestroy,
onWillPatch,
onWillRender,
onWillStart,
onWillUnmount,
onWillUpdateProps,
status,
useComponent,
xml,
} from "../src";
import { helpers } from "../src/app/template_helpers";
import { TemplateSet, globalTemplates } from "../src/app/template_set";
import { BDom } from "../src/blockdom";
import { compile } from "../src/compiler";
const mount = blockDom.mount;
export function nextMicroTick(): Promise<void> {
return Promise.resolve();
}
let lastFixture: any = null;
export function makeTestFixture() {
let fixture = document.createElement("div");
document.body.appendChild(fixture);
if (lastFixture) {
lastFixture.remove();
}
lastFixture = fixture;
return fixture;
}
beforeEach(() => {
xml.nextId = 999;
});
export async function nextTick(): Promise<void> {
await new Promise((resolve) => setTimeout(resolve));
await new Promise((resolve) => requestAnimationFrame(resolve));
}
interface Deferred extends Promise<any> {
resolve(val?: any): void;
reject(val?: any): void;
}
export function makeDeferred(): Deferred {
let resolve, reject;
let def = new Promise((_resolve, _reject) => {
resolve = _resolve;
reject = _reject;
});
(def as any).resolve = resolve;
(def as any).reject = reject;
return <Deferred>def;
}
export function trim(str: string): string {
return str.replace(/\s/g, "");
}
// -----------------------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------------------
let shouldSnapshot = false;
let snapshottedTemplates: Set<string> = new Set();
export function snapshotTemplate(template: string) {
const fn = compile(template);
expect(fn.toString()).toMatchSnapshot();
}
export function renderToBdom(template: string, context: any = {}, node?: any): BDom {
if (!node) {
if (!context.__owl__) {
context.__owl__ = { component: context };
} else {
context.__owl__.component = context;
}
}
const fn = compile(template);
if (shouldSnapshot && !snapshottedTemplates.has(template)) {
snapshottedTemplates.add(template);
expect(fn.toString()).toMatchSnapshot();
}
return fn(null as any, blockDom, helpers)(context, node);
}
export function renderToString(template: string, context: any = {}, node?: any): string {
const fixture = makeTestFixture();
const bdom = renderToBdom(template, context, node);
mount(bdom, fixture);
return fixture.innerHTML;
}
export class TestContext extends TemplateSet {
renderToString(name: string, context: any = {}): string {
const renderFn = this.getTemplate(name);
const bdom = renderFn(context, {});
const fixture = makeTestFixture();
mount(bdom, fixture);
return fixture.innerHTML;
}
}
export function snapshotEverything() {
if (shouldSnapshot) {
// this function has already been called
return;
}
const globalTemplateNames = new Set(Object.keys(globalTemplates));
shouldSnapshot = true;
beforeEach(() => {
snapshottedTemplates.clear();
});
const originalCompileTemplate = TemplateSet.prototype._compileTemplate;
TemplateSet.prototype._compileTemplate = function (name: string, template: string | Element) {
const fn = originalCompileTemplate.call(this, "", template);
if (!globalTemplateNames.has(name)) {
expect(fn.toString()).toMatchSnapshot();
}
return fn;
};
}
const steps: string[] = [];
export function logStep(step: string) {
steps.push(step);
}
export function useLogLifecycle(key?: string, skipAsyncHooks: boolean = false) {
const component = useComponent();
let name = component.constructor.name;
if (key) {
name = `${name} (${key})`;
}
logStep(`${name}:setup`);
expect(name + ": " + status(component)).toBe(name + ": " + "new");
if (!skipAsyncHooks) {
onWillStart(() => {
expect(name + ": " + status(component)).toBe(name + ": " + "new");
logStep(`${name}:willStart`);
});
}
onMounted(() => {
expect(name + ": " + status(component)).toBe(name + ": " + "mounted");
logStep(`${name}:mounted`);
});
if (!skipAsyncHooks) {
onWillUpdateProps(() => {
expect(name + ": " + status(component)).toBe(name + ": " + "mounted");
logStep(`${name}:willUpdateProps`);
});
}
onWillRender(() => {
logStep(`${name}:willRender`);
});
onRendered(() => {
logStep(`${name}:rendered`);
});
onWillPatch(() => {
expect(name + ": " + status(component)).toBe(name + ": " + "mounted");
logStep(`${name}:willPatch`);
});
onPatched(() => {
expect(name + ": " + status(component)).toBe(name + ": " + "mounted");
logStep(`${name}:patched`);
});
onWillUnmount(() => {
expect(name + ": " + status(component)).toBe(name + ": " + "mounted");
logStep(`${name}:willUnmount`);
});
onWillDestroy(() => {
expect(status(component)).not.toBe("destroyed");
logStep(`${name}:willDestroy`);
});
}
export function children(w: Component): Component[] {
const childrenMap = w.__owl__.children;
return Object.keys(childrenMap).map((id) => childrenMap[id].component);
}
export function isDirectChildOf(child: Component, parent: Component): boolean {
return children(parent).includes(child);
}
export function elem(component: Component): any {
return component.__owl__.firstNode();
}
export async function editInput(input: HTMLInputElement | HTMLTextAreaElement, value: string) {
input.value = value;
input.dispatchEvent(new Event("input"));
input.dispatchEvent(new Event("change"));
return nextTick();
}
afterEach(() => {
if (steps.length) {
steps.splice(0);
throw new Error("Remaining steps! Should be checked by a .toBeLogged() assertion!");
}
});
expect.extend({
toBeLogged(expected) {
const options = {
comment: "steps equality",
isNot: this.isNot,
promise: this.promise,
};
const currentSteps = steps.splice(0);
const pass = this.equals(currentSteps, expected);
const message = pass
? () =>
this.utils.matcherHint("toEqual", undefined, undefined, options) +
"\n\n" +
`Expected: not ${this.utils.printExpected(expected)}\n` +
`Received: ${this.utils.printReceived(currentSteps)}`
: () => {
const diffString = diff(expected, currentSteps, {
expand: this.expand,
});
return (
this.utils.matcherHint("toBe", undefined, undefined, options) +
"\n\n" +
(diffString && diffString.includes("- Expect")
? `Difference:\n\n${diffString}`
: `Expected: ${this.utils.printExpected(expected)}\n` +
`Received: ${this.utils.printReceived(currentSteps)}`)
);
};
return { actual: currentSteps, message, pass };
},
});
declare global {
namespace jest {
interface Matchers<R> {
toBeLogged(): R;
}
}
}