mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
77ff5ee895
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.
263 lines
6.8 KiB
TypeScript
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;
|
|
}
|
|
}
|
|
}
|