mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
7786077921
This commit makes all errors thrown in owl use a custom error class. The main point of this is to always wrap user-code errors that happen during the owl lifecycle so that they can be treated uniformly in onError by checking the cause property, and also allows user code to differenciate owl errors from non-owl errors reliably at runtime.
271 lines
7.0 KiB
TypeScript
271 lines
7.0 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/runtime/template_helpers";
|
|
import { TemplateSet, globalTemplates } from "../src/runtime/template_set";
|
|
import { BDom } from "../src/runtime/blockdom";
|
|
import { compile } from "../src/compiler";
|
|
import { OwlError } from "../src/runtime/error_handling";
|
|
|
|
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();
|
|
}
|
|
const app = {
|
|
createComponent() {},
|
|
createDynamicComponent() {},
|
|
};
|
|
return fn(app 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 OwlError("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;
|
|
}
|
|
}
|
|
}
|