[IMP] *: use a custom error class for all errors thrown by owl

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.
This commit is contained in:
Samuel Degueldre
2022-07-18 10:41:50 +02:00
committed by Géry Debongnie
parent 30bc605c84
commit 7786077921
24 changed files with 261 additions and 100 deletions
+5 -3
View File
@@ -30,6 +30,7 @@ import {
Attrs,
EventHandlers,
} from "./parser";
import { OwlError } from "../runtime/error_handling";
type BlockType = "block" | "text" | "multi" | "list" | "html" | "comment";
@@ -535,7 +536,7 @@ export class CodeGenerator {
.slice(1)
.map((m) => {
if (!MODS.has(m)) {
throw new Error(`Unknown event modifier: '${m}'`);
throw new OwlError(`Unknown event modifier: '${m}'`);
}
return `"${m}"`;
});
@@ -871,8 +872,9 @@ export class CodeGenerator {
this.define(`key${this.target.loopLevel}`, ast.key ? compileExpr(ast.key) : loopVar);
if (this.dev) {
// Throw error on duplicate keys in dev mode
this.helpers.add("OwlError");
this.addLine(
`if (keys${block.id}.has(key${this.target.loopLevel})) { throw new Error(\`Got duplicate key in t-foreach: \${key${this.target.loopLevel}}\`)}`
`if (keys${block.id}.has(key${this.target.loopLevel})) { throw new OwlError(\`Got duplicate key in t-foreach: \${key${this.target.loopLevel}}\`)}`
);
this.addLine(`keys${block.id}.add(key${this.target.loopLevel});`);
}
@@ -1094,7 +1096,7 @@ export class CodeGenerator {
name = _name;
value = `bind(ctx, ${value || undefined})`;
} else {
throw new Error("Invalid prop suffix");
throw new OwlError("Invalid prop suffix");
}
}
name = /^[a-z_]+$/i.test(name) ? name : `'${name}'`;
+5 -3
View File
@@ -1,3 +1,5 @@
import { OwlError } from "../runtime/error_handling";
/**
* Owl QWeb Expression Parser
*
@@ -106,14 +108,14 @@ let tokenizeString: Tokenizer = function (expr) {
i++;
cur = expr[i];
if (!cur) {
throw new Error("Invalid expression");
throw new OwlError("Invalid expression");
}
s += cur;
}
i++;
}
if (expr[i] !== start) {
throw new Error("Invalid expression");
throw new OwlError("Invalid expression");
}
s += start;
if (start === "`") {
@@ -223,7 +225,7 @@ export function tokenize(expr: string): Token[] {
error = e; // Silence all errors and throw a generic error below
}
if (current.length || error) {
throw new Error(`Tokenizer error: could not tokenize \`${expr}\``);
throw new OwlError(`Tokenizer error: could not tokenize \`${expr}\``);
}
return result;
}
+23 -17
View File
@@ -1,3 +1,5 @@
import { OwlError } from "../runtime/error_handling";
// -----------------------------------------------------------------------------
// AST Type definition
// -----------------------------------------------------------------------------
@@ -319,7 +321,7 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
return null;
}
if (tagName.startsWith("block-")) {
throw new Error(`Invalid tag name: '${tagName}'`);
throw new OwlError(`Invalid tag name: '${tagName}'`);
}
ctx = Object.assign({}, ctx);
if (tagName === "pre") {
@@ -340,13 +342,15 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
const value = node.getAttribute(attr)!;
if (attr.startsWith("t-on")) {
if (attr === "t-on") {
throw new Error("Missing event name with t-on directive");
throw new OwlError("Missing event name with t-on directive");
}
on = on || {};
on[attr.slice(5)] = value;
} else if (attr.startsWith("t-model")) {
if (!["input", "select", "textarea"].includes(tagName)) {
throw new Error("The t-model directive only works with <input>, <textarea> and <select>");
throw new OwlError(
"The t-model directive only works with <input>, <textarea> and <select>"
);
}
let baseExpr, expr;
@@ -359,7 +363,7 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
baseExpr = value.slice(0, index);
expr = value.slice(index + 1, -1);
} else {
throw new Error(`Invalid t-model expression: "${value}" (it should be assignable)`);
throw new OwlError(`Invalid t-model expression: "${value}" (it should be assignable)`);
}
const typeAttr = node.getAttribute("type");
@@ -390,10 +394,10 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
ctx.tModelInfo = model;
}
} else if (attr.startsWith("block-")) {
throw new Error(`Invalid attribute: '${attr}'`);
throw new OwlError(`Invalid attribute: '${attr}'`);
} else if (attr !== "t-name") {
if (attr.startsWith("t-") && !attr.startsWith("t-att")) {
throw new Error(`Unknown QWeb directive: '${attr}'`);
throw new OwlError(`Unknown QWeb directive: '${attr}'`);
}
const tModel = ctx.tModelInfo;
if (tModel && ["t-att-value", "t-attf-value"].includes(attr)) {
@@ -447,7 +451,7 @@ function parseTEscNode(node: Element, ctx: ParsingContext): AST | null {
};
}
if (ast.type === ASTType.TComponent) {
throw new Error("t-esc is not supported on Component nodes");
throw new OwlError("t-esc is not supported on Component nodes");
}
return tesc;
}
@@ -503,7 +507,7 @@ function parseTForEach(node: Element, ctx: ParsingContext): AST | null {
node.removeAttribute("t-as");
const key = node.getAttribute("t-key");
if (!key) {
throw new Error(
throw new OwlError(
`"Directive t-foreach should always be used with a t-key!" (expression: t-foreach="${collection}" t-as="${elem}")`
);
}
@@ -686,7 +690,9 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
let isDynamic = node.hasAttribute("t-component");
if (isDynamic && name !== "t") {
throw new Error(`Directive 't-component' can only be used on <t> nodes (used on a <${name}>)`);
throw new OwlError(
`Directive 't-component' can only be used on <t> nodes (used on a <${name}>)`
);
}
if (!(firstLetter === firstLetter.toUpperCase() || isDynamic)) {
@@ -713,7 +719,7 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
on[name.slice(5)] = value;
} else {
const message = directiveErrorMap.get(name.split("-").slice(0, 2).join("-"));
throw new Error(message || `unsupported directive on Component: ${name}`);
throw new OwlError(message || `unsupported directive on Component: ${name}`);
}
} else {
props = props || {};
@@ -729,7 +735,7 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
const slotNodes = Array.from(clone.querySelectorAll("[t-set-slot]"));
for (let slotNode of slotNodes) {
if (slotNode.tagName !== "t") {
throw new Error(
throw new OwlError(
`Directive 't-set-slot' can only be used on <t> nodes (used on a <${slotNode.tagName}>)`
);
}
@@ -904,7 +910,7 @@ function normalizeTIf(el: Element) {
let nattr = (name: string) => +!!node.getAttribute(name);
if (prevElem && (pattr("t-if") || pattr("t-elif"))) {
if (pattr("t-foreach")) {
throw new Error(
throw new OwlError(
"t-if cannot stay at the same level as t-foreach when using t-elif or t-else"
);
}
@@ -913,19 +919,19 @@ function normalizeTIf(el: Element) {
return a + b;
}) > 1
) {
throw new Error("Only one conditional branching directive is allowed per node");
throw new OwlError("Only one conditional branching directive is allowed per node");
}
// All text (with only spaces) and comment nodes (nodeType 8) between
// branch nodes are removed
let textNode;
while ((textNode = node.previousSibling) !== prevElem) {
if (textNode!.nodeValue!.trim().length && textNode!.nodeType !== 8) {
throw new Error("text is not allowed between branching directives");
throw new OwlError("text is not allowed between branching directives");
}
textNode!.remove();
}
} else {
throw new Error(
throw new OwlError(
"t-elif and t-else directives must be preceded by a t-if or t-elif directive"
);
}
@@ -946,7 +952,7 @@ function normalizeTEsc(el: Element) {
);
for (const el of elements) {
if (el.childNodes.length) {
throw new Error("Cannot have t-esc on a component that already has content");
throw new OwlError("Cannot have t-esc on a component that already has content");
}
const value = el.getAttribute("t-esc");
el.removeAttribute("t-esc");
@@ -1000,7 +1006,7 @@ function parseXML(xml: string): XMLDocument {
}
}
}
throw new Error(msg);
throw new OwlError(msg);
}
return doc;
+3 -3
View File
@@ -1,6 +1,6 @@
import { Component, ComponentConstructor, Props } from "./component";
import { ComponentNode } from "./component_node";
import { nodeErrorHandlers } from "./error_handling";
import { nodeErrorHandlers, OwlError } from "./error_handling";
import { Fiber, MountOptions } from "./fibers";
import { Scheduler } from "./scheduler";
import { validateProps } from "./template_helpers";
@@ -154,9 +154,9 @@ export class App<
if (isStatic) {
C = parent.constructor.components[name as any];
if (!C) {
throw new Error(`Cannot find the definition of component "${name}"`);
throw new OwlError(`Cannot find the definition of component "${name}"`);
} else if (!(C.prototype instanceof Component)) {
throw new Error(
throw new OwlError(
`"${name}" is not a Component. It must inherit from the Component class`
);
}
+2 -1
View File
@@ -1,3 +1,4 @@
import { OwlError } from "../error_handling";
import {
attrsSetter,
attrsUpdater,
@@ -245,7 +246,7 @@ function buildTree(
};
}
}
throw new Error("boom");
throw new OwlError("boom");
}
function addRef(tree: IntermediateTree) {
+2 -2
View File
@@ -1,7 +1,7 @@
import type { App, Env } from "./app";
import { BDom, VNode } from "./blockdom";
import { Component, ComponentConstructor, Props } from "./component";
import { fibersInError, handleError } from "./error_handling";
import { fibersInError, handleError, OwlError } from "./error_handling";
import { Fiber, makeChildFiber, makeRootFiber, MountFiber, MountOptions } from "./fibers";
import {
clearReactivesForCallback,
@@ -18,7 +18,7 @@ let currentNode: ComponentNode | null = null;
export function getCurrent(): ComponentNode {
if (!currentNode) {
throw new Error("No active component (a hook function should only be called in 'setup')");
throw new OwlError("No active component (a hook function should only be called in 'setup')");
}
return currentNode;
}
+10 -1
View File
@@ -1,6 +1,11 @@
import type { ComponentNode } from "./component_node";
import type { Fiber } from "./fibers";
// Custom error class that wraps error that happen in the owl lifecycle
export class OwlError extends Error {
cause?: any;
}
// Maps fibers to thrown errors
export const fibersInError: WeakMap<Fiber, any> = new WeakMap();
export const nodeErrorHandlers: WeakMap<ComponentNode, ((error: any) => void)[]> = new WeakMap();
@@ -37,7 +42,11 @@ function _handleError(node: ComponentNode | null, error: any): boolean {
type ErrorParams = { error: any } & ({ node: ComponentNode } | { fiber: Fiber });
export function handleError(params: ErrorParams) {
const error = params.error;
let { error } = params;
// Wrap error if it wasn't wrapped by wrapError (ie when not in dev mode)
if (!(error instanceof OwlError)) {
error = Object.assign(new OwlError("An error occured in the owl lifecycle"), { cause: error });
}
const node = "node" in params ? params.node : params.fiber.node;
const fiber = "fiber" in params ? params.fiber : node.fiber!;
+2 -1
View File
@@ -1,5 +1,6 @@
import { filterOutModifiersFromData } from "./blockdom/config";
import { STATUS } from "./status";
import { OwlError } from "./error_handling";
export const mainEventHandler = (data: any, ev: Event, currentTarget?: EventTarget | null) => {
const { data: _data, modifiers } = filterOutModifiersFromData(data);
@@ -33,7 +34,7 @@ export const mainEventHandler = (data: any, ev: Event, currentTarget?: EventTarg
if (Object.hasOwnProperty.call(data, 0)) {
const handler = data[0];
if (typeof handler !== "function") {
throw new Error(`Invalid handler (expected a function, received: '${handler}')`);
throw new OwlError(`Invalid handler (expected a function, received: '${handler}')`);
}
let node = data[1] ? data[1].__owl__ : null;
if (node ? node.status === STATUS.MOUNTED : true) {
+2 -2
View File
@@ -1,6 +1,6 @@
import { BDom, mount } from "./blockdom";
import type { ComponentNode } from "./component_node";
import { fibersInError, handleError } from "./error_handling";
import { fibersInError, handleError, OwlError } from "./error_handling";
import { STATUS } from "./status";
export function makeChildFiber(node: ComponentNode, parent: Fiber): Fiber {
@@ -43,7 +43,7 @@ export function makeRootFiber(node: ComponentNode): Fiber {
}
function throwOnRender() {
throw new Error("Attempted to render cancelled fiber");
throw new OwlError("Attempted to render cancelled fiber");
}
/**
+1
View File
@@ -54,5 +54,6 @@ export {
onError,
} from "./lifecycle_hooks";
export { validate } from "./validation";
export { OwlError } from "./error_handling";
export const __info__ = {};
+12 -14
View File
@@ -1,14 +1,21 @@
import { getCurrent } from "./component_node";
import { nodeErrorHandlers } from "./error_handling";
import { nodeErrorHandlers, OwlError } from "./error_handling";
const TIMEOUT = Symbol("timeout");
function wrapError(fn: (...args: any[]) => any, hookName: string) {
const error = new Error(`The following error occurred in ${hookName}: `) as Error & {
const error = new OwlError(`The following error occurred in ${hookName}: `) as Error & {
cause: any;
};
const timeoutError = new Error(`${hookName}'s promise hasn't resolved after 3 seconds`);
const timeoutError = new OwlError(`${hookName}'s promise hasn't resolved after 3 seconds`);
const node = getCurrent();
return (...args: any[]) => {
const onError = (cause: any) => {
if (cause instanceof Error) {
error.cause = cause;
error.message += `"${cause.message}"`;
}
throw error;
};
try {
const result = fn(...args);
if (result instanceof Promise) {
@@ -23,20 +30,11 @@ function wrapError(fn: (...args: any[]) => any, hookName: string) {
}
});
}
return result.catch((cause) => {
error.cause = cause;
if (cause instanceof Error) {
error.message += `"${cause.message}"`;
}
throw error;
});
return result.catch(onError);
}
return result;
} catch (cause) {
if (cause instanceof Error) {
error.message += `"${cause.message}"`;
}
throw error;
onError(cause);
}
};
}
+2 -1
View File
@@ -1,6 +1,7 @@
import { onWillUnmount } from "./lifecycle_hooks";
import { BDom, text, VNode } from "./blockdom";
import { Component } from "./component";
import { OwlError } from "./error_handling";
const VText: any = text("").constructor;
@@ -24,7 +25,7 @@ class VPortal extends VText implements Partial<VNode<VPortal>> {
}
this.target = el && el.querySelector(this.selector);
if (!this.target) {
throw new Error("invalid portal target");
throw new OwlError("invalid portal target");
}
}
this.realBDom!.mount(this.target!, null);
+2 -1
View File
@@ -1,4 +1,5 @@
import { Callback } from "./utils";
import { OwlError } from "./error_handling";
// Allows to get the target of a Reactive (used for making a new Reactive from the underlying object)
export const TARGET = Symbol("Target");
@@ -197,7 +198,7 @@ export function reactive<T extends Target>(
callback: Callback = () => {}
): Reactive<T> | NonReactive<T> {
if (!canBeMadeReactive(target)) {
throw new Error(`Cannot make the given value reactive`);
throw new OwlError(`Cannot make the given value reactive`);
}
if (SKIP in target) {
return target as NonReactive<T>;
+8 -4
View File
@@ -4,6 +4,7 @@ import { html } from "./blockdom/index";
import { isOptional, validateSchema } from "./validation";
import type { ComponentConstructor } from "./component";
import { markRaw } from "./reactivity";
import { OwlError } from "./error_handling";
const ObjectCreate = Object.create;
/**
@@ -70,7 +71,7 @@ function prepareList(collection: any): [any[], any[], number, any[]] {
values = Object.keys(collection);
keys = Object.values(collection);
} else {
throw new Error("Invalid loop expression");
throw new OwlError("Invalid loop expression");
}
const n = values.length;
return [keys, values, n, new Array(n)];
@@ -191,7 +192,7 @@ function multiRefSetter(refs: RefMap, name: string): RefSetter {
if (el) {
count++;
if (count > 1) {
throw new Error("Cannot have 2 elements with same ref name at the same time");
throw new OwlError("Cannot have 2 elements with same ref name at the same time");
}
}
if (count === 0 || el) {
@@ -233,7 +234,7 @@ export function validateProps<P>(name: string | ComponentConstructor<P>, props:
: name in schema && !("*" in schema) && !isOptional(schema[name]);
for (let p in defaultProps) {
if (isMandatory(p)) {
throw new Error(
throw new OwlError(
`A default value cannot be defined for a mandatory prop (name: '${p}', component: ${ComponentClass.name})`
);
}
@@ -242,7 +243,9 @@ export function validateProps<P>(name: string | ComponentConstructor<P>, props:
const errors = validateSchema(props, schema);
if (errors.length) {
throw new Error(`Invalid props for component '${ComponentClass.name}': ` + errors.join(", "));
throw new OwlError(
`Invalid props for component '${ComponentClass.name}': ` + errors.join(", ")
);
}
}
@@ -264,4 +267,5 @@ export const helpers = {
bind,
createCatcher,
markRaw,
OwlError,
};
+5 -4
View File
@@ -3,6 +3,7 @@ import { comment, createBlock, html, list, multi, text, toggler } from "./blockd
import { getCurrent } from "./component_node";
import { Portal, portalTemplate } from "./portal";
import { helpers } from "./template_helpers";
import { OwlError } from "./error_handling";
const bdom = { text, createBlock, list, multi, html, toggler, comment };
@@ -31,7 +32,7 @@ function parseXML(xml: string): Document {
}
}
}
throw new Error(msg);
throw new OwlError(msg);
}
return doc;
}
@@ -76,7 +77,7 @@ export class TemplateSet {
if (currentAsString === newAsString) {
return;
}
throw new Error(`Template ${name} already defined with different content`);
throw new OwlError(`Template ${name} already defined with different content`);
}
this.rawTemplates[name] = template;
}
@@ -102,7 +103,7 @@ export class TemplateSet {
const componentName = getCurrent().component.constructor.name;
extraInfo = ` (for component "${componentName}")`;
} catch {}
throw new Error(`Missing template: "${name}"${extraInfo}`);
throw new OwlError(`Missing template: "${name}"${extraInfo}`);
}
const isFn = typeof rawTemplate === "function" && !(rawTemplate instanceof Element);
const templateFn = isFn ? rawTemplate : this._compileTemplate(name, rawTemplate);
@@ -119,7 +120,7 @@ export class TemplateSet {
}
_compileTemplate(name: string, template: string | Element): ReturnType<typeof compile> {
throw new Error(`Unable to compile a template. Please use owl full build instead`);
throw new OwlError(`Unable to compile a template. Please use owl full build instead`);
}
callTemplate(owner: any, subTemplate: string, ctx: any, parent: any, key: any): any {
+4 -3
View File
@@ -1,3 +1,4 @@
import { OwlError } from "./error_handling";
export type Callback = () => void;
/**
@@ -28,10 +29,10 @@ export function batched(callback: Callback): Callback {
export function validateTarget(target: HTMLElement) {
if (!(target instanceof HTMLElement)) {
throw new Error("Cannot mount component: the target is not a valid DOM element");
throw new OwlError("Cannot mount component: the target is not a valid DOM element");
}
if (!document.body.contains(target)) {
throw new Error("Cannot mount a component on a detached dom node");
throw new OwlError("Cannot mount a component on a detached dom node");
}
}
@@ -54,7 +55,7 @@ export function whenReady(fn?: any): Promise<void> {
export async function loadFile(url: string): Promise<string> {
const result = await fetch(url);
if (!result.ok) {
throw new Error("Error while fetching xml templates");
throw new OwlError("Error while fetching xml templates");
}
return await result.text();
}
+3 -1
View File
@@ -1,3 +1,5 @@
import { OwlError } from "./error_handling";
type BaseType =
| typeof String
| typeof Boolean
@@ -70,7 +72,7 @@ function toSchema(spec: SimplifiedSchema): NormalizedSchema {
export function validate(obj: { [key: string]: any }, spec: Schema) {
let errors = validateSchema(obj, spec);
if (errors.length) {
throw new Error("Invalid object: " + errors.join(", "));
throw new OwlError("Invalid object: " + errors.join(", "));
}
}
@@ -101,6 +101,50 @@ exports[`basics simple catchError 2`] = `
}"
`;
exports[`can catch errors Errors in owl lifecycle are wrapped in dev mode: async hook 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(ctx['state'].value);
}
}"
`;
exports[`can catch errors Errors in owl lifecycle are wrapped in dev mode: sync hook 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(ctx['state'].value);
}
}"
`;
exports[`can catch errors Errors in owl lifecycle are wrapped out of dev mode: async hook 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(ctx['state'].value);
}
}"
`;
exports[`can catch errors Errors in owl lifecycle are wrapped outside dev mode: sync hook 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(ctx['state'].value);
}
}"
`;
exports[`can catch errors an error in onWillDestroy 1`] = `
"function anonymous(app, bdom, helpers
) {
@@ -1257,19 +1301,6 @@ exports[`errors and promises an error in willPatch call will reject the render p
}"
`;
exports[`errors and promises error type is kept when it is wrapped 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div>abc</div>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`errors and promises errors in mounted and in willUnmount 1`] = `
"function anonymous(app, bdom, helpers
) {
@@ -43,7 +43,7 @@ exports[`list of components crash on duplicate key in dev mode 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { prepareList, withKey } = helpers;
let { prepareList, OwlError, withKey } = helpers;
const comp1 = app.createComponent(\`Child\`, true, false, false, true);
return function template(ctx, node, key = \\"\\") {
@@ -53,7 +53,7 @@ exports[`list of components crash on duplicate key in dev mode 1`] = `
for (let i1 = 0; i1 < l_block1; i1++) {
ctx[\`item\`] = v_block1[i1];
const key1 = 'child';
if (keys1.has(key1)) { throw new Error(\`Got duplicate key in t-foreach: \${key1}\`)}
if (keys1.has(key1)) { throw new OwlError(\`Got duplicate key in t-foreach: \${key1}\`)}
keys1.add(key1);
const props1 = {};
helpers.validateProps(\`Child\`, props1, ctx);
+110 -15
View File
@@ -19,6 +19,7 @@ import {
snapshotEverything,
useLogLifecycle,
} from "../helpers";
import { OwlError } from "../../src/runtime/error_handling";
let fixture: HTMLElement;
@@ -159,16 +160,17 @@ describe("errors and promises", () => {
static template = xml`<div><t t-esc="this.will.crash"/></div>`;
}
let error: Error;
let error: OwlError;
try {
await mount(App, fixture);
} catch (e) {
error = e as Error;
error = e as OwlError;
}
expect(error!).toBeDefined();
expect(error!.cause).toBeDefined();
const regexp =
/Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
expect(error!.message).toMatch(regexp);
expect(error!.cause.message).toMatch(regexp);
expect(mockConsoleError).toBeCalledTimes(0);
expect(mockConsoleError).toBeCalledTimes(0);
});
@@ -183,14 +185,15 @@ describe("errors and promises", () => {
}
}
let error: Error;
let error: OwlError;
try {
await mount(App, fixture);
} catch (e) {
error = e as Error;
error = e as OwlError;
}
expect(error!).toBeDefined();
expect(error!.message).toBe("boom");
expect(error!.cause).toBeDefined();
expect(error!.cause.message).toBe("boom");
expect(fixture.innerHTML).toBe("");
expect(mockConsoleError).toBeCalledTimes(0);
expect(mockConsoleWarn).toBeCalledTimes(1);
@@ -344,16 +347,17 @@ describe("errors and promises", () => {
static components = { Child };
}
let error: Error;
let error: OwlError;
try {
await mount(App, fixture);
} catch (e) {
error = e as Error;
error = e as OwlError;
}
expect(error!).toBeDefined();
expect(error!.cause).toBeDefined();
const regexp =
/Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
expect(error!.message).toMatch(regexp);
expect(error!.cause.message).toMatch(regexp);
expect(mockConsoleError).toBeCalledTimes(0);
expect(mockConsoleWarn).toBeCalledTimes(1);
});
@@ -363,7 +367,7 @@ describe("errors and promises", () => {
static template = xml`<div><t t-if="flag" t-esc="this.will.crash"/></div>`;
flag = false;
setup() {
onError((e) => (error = e));
onError(({ cause }) => (error = cause));
}
}
@@ -390,16 +394,17 @@ describe("errors and promises", () => {
static components = { Child };
}
let error: Error;
let error: OwlError;
try {
await mount(Parent, fixture);
} catch (e) {
error = e as Error;
error = e as OwlError;
}
expect(error!).toBeDefined();
expect(error!.cause).toBeDefined();
const regexp =
/Cannot read properties of undefined \(reading 'y'\)|Cannot read property 'y' of undefined/g;
expect(error!.message).toMatch(regexp);
expect(error!.cause.message).toMatch(regexp);
expect(mockConsoleError).toBeCalledTimes(0);
expect(mockConsoleWarn).toBeCalledTimes(1);
});
@@ -506,6 +511,96 @@ describe("can catch errors", () => {
);
});
test("Errors in owl lifecycle are wrapped in dev mode: sync hook", async () => {
const err = new Error("test error");
class Root extends Component {
static template = xml`<t t-esc="state.value"/>`;
state = useState({ value: 1 });
setup() {
onMounted(() => {
throw err;
});
}
}
let e: OwlError;
try {
await mount(Root, fixture, { test: true });
} catch (error) {
e = error as OwlError;
}
expect(e!.message).toBe(`The following error occurred in onMounted: "test error"`);
expect(e!.cause).toBe(err);
});
test("Errors in owl lifecycle are wrapped in dev mode: async hook", async () => {
const err = new Error("test error");
class Root extends Component {
static template = xml`<t t-esc="state.value"/>`;
state = useState({ value: 1 });
setup() {
onWillStart(async () => {
await nextMicroTick();
throw err;
});
}
}
let e: OwlError;
try {
await mount(Root, fixture, { test: true });
} catch (error) {
e = error as OwlError;
}
expect(e!.message).toBe(`The following error occurred in onWillStart: "test error"`);
expect(e!.cause).toBe(err);
});
test("Errors in owl lifecycle are wrapped outside dev mode: sync hook", async () => {
const err = new Error("test error");
class Root extends Component {
static template = xml`<t t-esc="state.value"/>`;
state = useState({ value: 1 });
setup() {
onMounted(() => {
throw err;
});
}
}
let e: OwlError;
try {
await mount(Root, fixture);
} catch (error) {
e = error as OwlError;
}
expect(e!.message).toBe("An error occured in the owl lifecycle");
expect(e!.cause).toBe(err);
});
test("Errors in owl lifecycle are wrapped out of dev mode: async hook", async () => {
const err = new Error("test error");
class Root extends Component {
static template = xml`<t t-esc="state.value"/>`;
state = useState({ value: 1 });
setup() {
onWillStart(async () => {
await nextMicroTick();
throw err;
});
}
}
let e: OwlError;
try {
await mount(Root, fixture);
} catch (error) {
e = error as OwlError;
}
expect(e!.message).toBe("An error occured in the owl lifecycle");
expect(e!.cause).toBe(err);
});
test("can catch an error in the initial call of a component render function (parent mounted)", async () => {
class ErrorComponent extends Component {
static template = xml`<div>hey<t t-esc="state.this.will.crash"/></div>`;
@@ -1204,8 +1299,8 @@ describe("can catch errors", () => {
class Catch extends Component {
static template = xml`<t t-slot="default" />`;
setup() {
onError((error) => {
this.props.onError(error);
onError(({ cause }) => {
this.props.onError(cause);
});
}
}
+1 -1
View File
@@ -653,7 +653,7 @@ describe("hooks", () => {
try {
await mount(MyComponent, fixture);
} catch (e: any) {
expect(e.message).toBe("Intentional error");
expect(e.cause.message).toBe("Intentional error");
}
// no console.error because the error has been caught in this test
expect(console.error).toHaveBeenCalledTimes(0);
+5 -3
View File
@@ -1,3 +1,4 @@
import { OwlError } from "../../src/runtime/error_handling";
import { Component, mount, onMounted, useState, xml } from "../../src";
import { makeTestFixture, nextTick, snapshotEverything } from "../helpers";
@@ -346,16 +347,17 @@ describe("style and class handling", () => {
static template = xml`<Child class="'a'"/>`;
static components = { Child };
}
let error: Error;
let error: OwlError;
try {
await mount(ParentWidget, fixture);
} catch (e) {
error = e as Error;
error = e as OwlError;
}
expect(error!).toBeDefined();
expect(error!.cause).toBeDefined();
const regexp =
/Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
expect(error!.message).toMatch(regexp);
expect(error!.cause.message).toMatch(regexp);
expect(fixture.innerHTML).toBe("");
expect(mockConsoleWarn).toBeCalledTimes(1);
});
+2 -1
View File
@@ -19,6 +19,7 @@ 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;
@@ -221,7 +222,7 @@ export async function editInput(input: HTMLInputElement | HTMLTextAreaElement, v
afterEach(() => {
if (steps.length) {
steps.splice(0);
throw new Error("Remaining steps! Should be checked by a .toBeLogged() assertion!");
throw new OwlError("Remaining steps! Should be checked by a .toBeLogged() assertion!");
}
});
+6 -4
View File
@@ -1,3 +1,4 @@
import { OwlError } from "../../src/runtime/error_handling";
import {
App,
Component,
@@ -499,7 +500,7 @@ describe("Portal", () => {
</div>`;
state = { error: false };
setup() {
onError((e) => (error = e));
onError(({ cause }) => (error = cause));
}
}
addOutsideDiv(fixture);
@@ -958,14 +959,15 @@ describe("Portal: Props validation", () => {
</t>
</div>`;
}
let error: Error;
let error: OwlError;
try {
await mount(Parent, fixture, { dev: true });
} catch (e) {
error = e as Error;
error = e as OwlError;
}
expect(error!).toBeDefined();
expect(error!.message).toBe(`' ' is not a valid selector`);
expect(error!.cause).toBeDefined();
expect(error!.cause.message).toBe(`' ' is not a valid selector`);
});
test("target must be a valid selector 2", async () => {