Files
owl/tests/helpers.ts
T
Samuel Degueldre 77ff5ee895 [IMP] component: emit warning when async hooks take too long
This commit adds a warning when an async hook
(onWillUpdateProps/onWillStart) takes longer than 3 seconds, as these
hooks block the rendering and patching of the application, it is rarely
desirable and often a sign of a deadlock. This warning will contain the
stack trace of the call to the hook to help in debugging.
2022-03-14 09:51:47 +01:00

263 lines
6.8 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 } from "../src/app/template_set";
import { BDom } from "../src/blockdom";
import { compile } from "../src/compiler";
import { globalTemplates } from "../src/utils";
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(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) {
const component = useComponent();
let name = component.constructor.name;
if (key) {
name = `${name} (${key})`;
}
logStep(`${name}:setup`);
expect(name + ": " + status(component)).toBe(name + ": " + "new");
onWillStart(() => {
expect(name + ": " + status(component)).toBe(name + ": " + "new");
logStep(`${name}:willStart`);
});
onMounted(() => {
expect(name + ": " + status(component)).toBe(name + ": " + "mounted");
logStep(`${name}:mounted`);
});
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;
}
}
}