Compare commits

...

2 Commits

Author SHA1 Message Date
Géry Debongnie ba5365e9d9 [IMP] app: add a setting to share compiled templates between apps 2022-07-22 09:32:26 +02:00
Samuel Degueldre 163366997c [FIX] components: fix cause left unset when thrown object is not Error
Previously, when wrapping errors in wrapError, if the error was not an
actual error object, we wouldn't set the cause property on the wrapping
error correctly. The "instanceof Error" check is simply there so that we
can know whether we can add the original errors message to the wrapping
error, but the line that sets the error's cause was mistakenly moved
into that condition.

This commit also fixes the wrapping error's message in the case of
non-Error objects, to avoid having "the following error occurred in
hookname:" with nothing after the colon which is confusing/misleading.
2022-07-22 09:21:19 +02:00
6 changed files with 122 additions and 17 deletions
+3
View File
@@ -55,6 +55,9 @@ The `config` object is an object with some of the following keys:
[`dev` mode](#dev-mode);
- **`test (boolean, default=false)`**: `test` mode is the same as `dev` mode, except
that Owl will not log a message to warn that Owl is in `dev` mode.
- **`shareTemplates (boolean, default=false)`**: if `true`, each compiled template
will be shared between instances of `App`. Useful for speeding test suites, because
it prevent recompiling the same templates again and again.
- **`translatableAttributes (string[])`**: a list of additional attributes that should
be translated (see [translations](translations.md))
- **`translateFn (function)`**: a function that will be called by owl to translate
+4 -1
View File
@@ -45,7 +45,10 @@ export function handleError(params: ErrorParams) {
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 });
error = Object.assign(
new OwlError(`An error occured in the owl lifecycle (see this Error's "cause" property)`),
{ cause: error }
);
}
const node = "node" in params ? params.node : params.fiber.node;
const fiber = "fiber" in params ? params.fiber : node.fiber!;
+3 -1
View File
@@ -10,9 +10,11 @@ function wrapError(fn: (...args: any[]) => any, hookName: string) {
const node = getCurrent();
return (...args: any[]) => {
const onError = (cause: any) => {
error.cause = cause;
if (cause instanceof Error) {
error.cause = cause;
error.message += `"${cause.message}"`;
} else {
error.message = `Something that is not an Error was thrown in ${hookName} (see this Error's "cause" property)`;
}
throw error;
};
+35 -10
View File
@@ -37,11 +37,17 @@ function parseXML(xml: string): Document {
return doc;
}
const sharedTemplates: Map<
Function | undefined,
{ [key: string]: { [name: string]: TemplateFunction } }
> = new Map();
export interface TemplateSetConfig {
dev?: boolean;
translatableAttributes?: string[];
translateFn?: (s: string) => string;
templates?: string | Document;
shareTemplates?: boolean;
}
export class TemplateSet {
@@ -51,12 +57,27 @@ export class TemplateSet {
dev: boolean;
rawTemplates: typeof globalTemplates = Object.create(globalTemplates);
templates: { [name: string]: Template } = {};
templateFunctions: { [name: string]: TemplateFunction } = {};
translateFn?: (s: string) => string;
translatableAttributes?: string[];
Portal = Portal;
constructor(config: TemplateSetConfig = {}) {
this.dev = config.dev || false;
if (config.shareTemplates) {
let cache = sharedTemplates.get(this.translateFn);
if (!cache) {
cache = {};
sharedTemplates.set(this.translateFn, cache);
}
let key = `${this.dev ? "d" : "p"}${(this.translatableAttributes || []).toString()}`;
let templates = cache[key];
if (!templates) {
cache[key] = {};
templates = cache[key];
}
this.templateFunctions = templates;
}
this.translateFn = config.translateFn;
this.translatableAttributes = config.translatableAttributes;
if (config.templates) {
@@ -96,17 +117,21 @@ export class TemplateSet {
getTemplate(name: string): Template {
if (!(name in this.templates)) {
const rawTemplate = this.rawTemplates[name];
if (rawTemplate === undefined) {
let extraInfo = "";
try {
const componentName = getCurrent().component.constructor.name;
extraInfo = ` (for component "${componentName}")`;
} catch {}
throw new OwlError(`Missing template: "${name}"${extraInfo}`);
let templateFn = this.templateFunctions[name];
if (!templateFn) {
const rawTemplate = this.rawTemplates[name];
if (rawTemplate === undefined) {
let extraInfo = "";
try {
const componentName = getCurrent().component.constructor.name;
extraInfo = ` (for component "${componentName}")`;
} catch {}
throw new OwlError(`Missing template: "${name}"${extraInfo}`);
}
const isFn = typeof rawTemplate === "function" && !(rawTemplate instanceof Element);
templateFn = isFn ? rawTemplate : this._compileTemplate(name, rawTemplate);
this.templateFunctions[name] = templateFn;
}
const isFn = typeof rawTemplate === "function" && !(rawTemplate instanceof Element);
const templateFn = isFn ? rawTemplate : this._compileTemplate(name, rawTemplate);
// first add a function to lazily get the template, in case there is a
// recursive call to the template name
const templates = this.templates;
@@ -101,7 +101,7 @@ exports[`basics simple catchError 2`] = `
}"
`;
exports[`can catch errors Errors in owl lifecycle are wrapped in dev mode: async hook 1`] = `
exports[`can catch errors Errors have the right cause 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
@@ -112,7 +112,7 @@ exports[`can catch errors Errors in owl lifecycle are wrapped in dev mode: async
}"
`;
exports[`can catch errors Errors in owl lifecycle are wrapped in dev mode: sync hook 1`] = `
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;
@@ -145,6 +145,28 @@ exports[`can catch errors Errors in owl lifecycle are wrapped outside dev mode:
}"
`;
exports[`can catch errors Thrown values that are not errors are wrapped in dev mode 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 Thrown values that are not errors are wrapped outside dev mode 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
) {
+53 -3
View File
@@ -511,7 +511,7 @@ describe("can catch errors", () => {
);
});
test("Errors in owl lifecycle are wrapped in dev mode: sync hook", async () => {
test("Errors have the right cause", async () => {
const err = new Error("test error");
class Root extends Component {
static template = xml`<t t-esc="state.value"/>`;
@@ -574,7 +574,9 @@ describe("can catch errors", () => {
} catch (error) {
e = error as OwlError;
}
expect(e!.message).toBe("An error occured in the owl lifecycle");
expect(e!.message).toBe(
`An error occured in the owl lifecycle (see this Error's "cause" property)`
);
expect(e!.cause).toBe(err);
});
@@ -597,10 +599,58 @@ describe("can catch errors", () => {
} catch (error) {
e = error as OwlError;
}
expect(e!.message).toBe("An error occured in the owl lifecycle");
expect(e!.message).toBe(
`An error occured in the owl lifecycle (see this Error's "cause" property)`
);
expect(e!.cause).toBe(err);
});
test("Thrown values that are not errors are wrapped in dev mode", async () => {
class Root extends Component {
static template = xml`<t t-esc="state.value"/>`;
state = useState({ value: 1 });
setup() {
onMounted(() => {
throw "This is not an error";
});
}
}
let e: OwlError;
try {
await mount(Root, fixture, { test: true });
} catch (error) {
e = error as OwlError;
}
expect(e!.message).toBe(
`Something that is not an Error was thrown in onMounted (see this Error's "cause" property)`
);
expect(e!.cause).toBe("This is not an error");
});
test("Thrown values that are not errors are wrapped outside dev mode", async () => {
class Root extends Component {
static template = xml`<t t-esc="state.value"/>`;
state = useState({ value: 1 });
setup() {
onMounted(() => {
throw "This is not an error";
});
}
}
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 (see this Error's "cause" property)`
);
expect(e!.cause).toBe("This is not an error");
});
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>`;