Files
owl/tests/helpers.ts
T
Samuel Degueldre cfdf7caa50 [IMP] app: rethrow errors that were not handled
This commit makes it so that when an error occurs in an owl app and none
of the registered error handlers are able to handle it, we rethrow the
error instead of just logging it to the console and swallowing it. This
allows users of owl to handle errors that happen in owl applications by
using event listeners for error and unhandledrejection events on the
window.
2022-09-09 09:23:32 +02:00

285 lines
7.4 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 };
},
});
export function nextAppError(app: any) {
const { handleError } = app;
return new Promise((resolve) => {
app.handleError = (...args: Parameters<typeof handleError>) => {
try {
handleError.call(app, ...args);
} catch (e: any) {
app.handleError = handleError;
resolve(e);
}
};
});
}
declare global {
namespace jest {
interface Matchers<R> {
toBeLogged(): R;
}
}
}