Compare commits

...

13 Commits

Author SHA1 Message Date
Géry Debongnie b51756f356 [REL] v2.0.0-beta-20
# v2.0.0-beta-20

- app: properly rethrow unhandled errors
2022-09-09 09:26:12 +02:00
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
Florent Dardenne - dafl@odoo a5a6a592c1 [FIX] tutorial_todoapp: fix the final code mount issue
In app.js, `mount(Root, document.body, { dev: true, env });`  crash because `body` is not available yet.
Therefore, moving the script into the body fix the issue.
2022-09-08 13:30:38 +02:00
Géry Debongnie d0d7482b0f [REL] v2.0.0-beta-19
# v2.0.0-beta-19

- fix: events: correctly call handlers in iframes
2022-09-06 12:13:25 +02:00
Samuel Degueldre 8fe4c0c76e [FIX] events: correctly call handlers in iframes
Previously, event handlers would not work when an app was mounted in an
iframe, this is caused by a guard in the event handler that checks that
the target element is still in the document, but it doesn't check
against the correct document in the case of an iframe.

This commit changes the check to check against the target's
ownerDocument.
2022-09-06 12:06:59 +02:00
Géry Debongnie c1afaeb92a [REL] v2.0.0-beta-18
# v2.0.0-beta-18

- fix: allow multiple occurrences of same slot in different locations
2022-09-02 14:57:18 +02:00
Géry Debongnie 3883cec079 [FIX] slots: prevent crash when using same slot in different locations
Before this commit, a crash could occur when a component with no props
is defined in a slot, and that slot is conditionally displayed in
multiple locations.

The reason for that is that the key provided to the callSlot function
was identical, so from the perspective of the component function, it was
not possible to make the difference between a component located in
either places.  With this commit, we make sure that a unique key is used
when a slot is reused in a template (or if it is dynamic, because in
that case, we have no idea at compile time if it will be unique or not)

closes #1246
2022-09-02 12:12:00 +02:00
Géry Debongnie 9cb74d619b [REL] v2.0.0-beta-17
# v2.0.0-beta-17

- imp: types: expose ComponentConstructor for typing purpose
- fix: compiler: fix falsy values for properties not keeping input empty
- fix: app: allow mounting owl apps in iframe
2022-09-01 15:41:33 +02:00
Samuel Degueldre a93f015795 [FIX] app: allow mounting owl apps in iframe
Previously, attempting to mount an app in an iframe would crash, saying
that the target is not a valid DOM element, this is because instanceof
checks do not work cross-frame as global objects do not have the same
identity in frames as with the main window. This commit fixes that by
making sure the target is an instance of HTMLElement of the
corresponding window, and checks that the corresponding document body
contains it.
2022-08-17 10:05:46 +02:00
Samuel Degueldre 02a187d80b [FIX] compiler: fix falsy values for properties not keeping input empty
Recently, we made it so that when a component is rendered, it always
updates the property values for computed properties. This was done by
wrapping the value in a String or Boolean object. One issue with this is
that wrapping a falsy value in a String doesn't yield an empty string,
but a string containing the value as text (eg new String(undefined) ->
"undefined"), which causes the value to not remain empty as per the
spec. This commit fixes that by adding a fallback to the empty string
for falsy values before converting to a String object.

closes: #1236
2022-08-05 09:50:38 +02:00
Rémi Rahir d3b0d1971e [IMP] types: expose ComponentConstructor for typing purpose
We have been using this type in o-spreadsheet since https://github.com/odoo/o-spreadsheet/pull/1187
but the new typing file (https://github.com/odoo/owl/pull/1207) does not include it.
It would be useful to allow us to bump or version of owl witouht having to resort
to long aboslute paths (i.e. import from `@odoo/owl`and not
`@odoo/owl/dist/types/runtime/component@ everywhere).
2022-07-25 14:34:14 +02:00
Géry Debongnie b90aa0e23a [REL] v2.0.0-beta-16
# v2.0.0-beta-16

Notes

- fix: components: fix cause left unset when thrown object is not Error
2022-07-22 09:43:43 +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
30 changed files with 810 additions and 316 deletions
+3 -2
View File
@@ -770,10 +770,11 @@ For reference, here is the final code:
<meta charset="UTF-8" />
<title>OWL Todo App</title>
<link rel="stylesheet" href="app.css" />
</head>
<body>
<script src="owl.js"></script>
<script src="app.js"></script>
</head>
<body></body>
</body>
</html>
```
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@odoo/owl",
"version": "2.0.0-beta-15",
"version": "2.0.0-beta-20",
"description": "Odoo Web Library (OWL)",
"main": "dist/owl.cjs.js",
"module": "dist/owl.es.js",
+19 -5
View File
@@ -232,6 +232,7 @@ export class CodeGenerator {
translatableAttributes: string[] = TRANSLATABLE_ATTRS;
ast: AST;
staticDefs: { id: string; expr: string }[] = [];
slotNames: Set<String> = new Set();
helpers: Set<string> = new Set();
constructor(ast: AST, options: CodeGenOptions) {
@@ -584,8 +585,12 @@ export class CodeGenerator {
expr = compileExpr(ast.attrs[key]);
if (attrName && isProp(ast.tag, attrName)) {
// we force a new string or new boolean to bypass the equality check in blockdom when patching same value
const C = attrName === "value" ? "String" : "Boolean";
expr = `new ${C}(${expr})`;
if (attrName === "value") {
// When the expression is falsy, fall back to an empty string
expr = `new String((${expr}) || "")`;
} else {
expr = `new Boolean(${expr})`;
}
}
const idx = block!.insertData(expr, "attr");
if (key === "t-att") {
@@ -1241,28 +1246,37 @@ export class CodeGenerator {
let blockString: string;
let slotName;
let dynamic = false;
let isMultiple = false;
if (ast.name.match(INTERP_REGEXP)) {
dynamic = true;
isMultiple = true;
slotName = interpolate(ast.name);
} else {
slotName = "'" + ast.name + "'";
isMultiple = isMultiple || this.slotNames.has(ast.name);
this.slotNames.add(ast.name);
}
const dynProps = ast.attrs ? ast.attrs["t-props"] : null;
if (ast.attrs) {
delete ast.attrs["t-props"];
}
let key = this.target.loopLevel ? `key${this.target.loopLevel}` : "key";
if (isMultiple) {
key = `${key} + \`${this.generateComponentKey()}\``;
}
const props = ast.attrs ? this.formatPropObject(ast.attrs) : [];
const scope = this.getPropString(props, dynProps);
if (ast.defaultContent) {
const name = this.compileInNewTarget("defaultContent", ast.defaultContent, ctx);
blockString = `callSlot(ctx, node, key, ${slotName}, ${dynamic}, ${scope}, ${name})`;
blockString = `callSlot(ctx, node, ${key}, ${slotName}, ${dynamic}, ${scope}, ${name})`;
} else {
if (dynamic) {
let name = generateId("slot");
this.define(name, slotName);
blockString = `toggler(${name}, callSlot(ctx, node, key, ${name}, ${dynamic}, ${scope}))`;
blockString = `toggler(${name}, callSlot(ctx, node, ${key}, ${name}, ${dynamic}, ${scope}))`;
} else {
blockString = `callSlot(ctx, node, key, ${slotName}, ${dynamic}, ${scope})`;
blockString = `callSlot(ctx, node, ${key}, ${slotName}, ${dynamic}, ${scope})`;
}
}
// event handling
+6 -3
View File
@@ -6,6 +6,7 @@ import { Scheduler } from "./scheduler";
import { validateProps } from "./template_helpers";
import { TemplateSet, TemplateSetConfig } from "./template_set";
import { validateTarget } from "./utils";
import { handleError } from "./error_handling";
// reimplement dev mode stuff see last change in 0f7a8289a6fb8387c3c1af41c6664b2a8448758f
@@ -94,9 +95,7 @@ export class App<
nodeErrorHandlers.set(node, handlers);
}
handlers.unshift((e) => {
if (isResolved) {
console.error(e);
} else {
if (!isResolved) {
reject(e);
}
throw e;
@@ -169,6 +168,10 @@ export class App<
return node;
};
}
handleError(...args: Parameters<typeof handleError>) {
return handleError(...args);
}
}
export async function mount<
+2 -2
View File
@@ -27,8 +27,8 @@ function createElementHandler(evName: string, capture: boolean = false): EventHa
}
function listener(ev: Event) {
const currentTarget = ev.currentTarget;
if (!currentTarget || !document.contains(currentTarget as HTMLElement)) return;
const currentTarget = ev.currentTarget as HTMLElement;
if (!currentTarget || !currentTarget.ownerDocument.contains(currentTarget)) return;
const data = (currentTarget as any)[eventKey];
if (!data) return;
config.mainEventHandler(data, ev, currentTarget);
+3 -3
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, OwlError } from "./error_handling";
import { fibersInError, OwlError } from "./error_handling";
import { Fiber, makeChildFiber, makeRootFiber, MountFiber, MountOptions } from "./fibers";
import {
clearReactivesForCallback,
@@ -141,7 +141,7 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
try {
await Promise.all(this.willStart.map((f) => f.call(component)));
} catch (e) {
handleError({ node: this, error: e });
this.app.handleError({ node: this, error: e });
return;
}
if (this.status === STATUS.NEW && this.fiber === fiber) {
@@ -219,7 +219,7 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
cb.call(component);
}
} catch (e) {
handleError({ error: e, node: this });
this.app.handleError({ error: e, node: this });
}
}
this.status = STATUS.DESTROYED;
+5 -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!;
@@ -68,5 +71,6 @@ export function handleError(params: ErrorParams) {
} catch (e) {
console.error(e);
}
throw error;
}
}
+4 -4
View File
@@ -1,6 +1,6 @@
import { BDom, mount } from "./blockdom";
import type { ComponentNode } from "./component_node";
import { fibersInError, handleError, OwlError } from "./error_handling";
import { fibersInError, OwlError } from "./error_handling";
import { STATUS } from "./status";
export function makeChildFiber(node: ComponentNode, parent: Fiber): Fiber {
@@ -130,7 +130,7 @@ export class Fiber {
(this.bdom as any) = true;
this.bdom = node.renderFn();
} catch (e) {
handleError({ node, error: e });
node.app.handleError({ node, error: e });
}
root.setCounter(root.counter - 1);
}
@@ -195,7 +195,7 @@ export class RootFiber extends Fiber {
}
} catch (e) {
this.locked = false;
handleError({ fiber: current || this, error: e });
node.app.handleError({ fiber: current || this, error: e });
}
}
@@ -259,7 +259,7 @@ export class MountFiber extends RootFiber {
}
}
} catch (e) {
handleError({ fiber: current as Fiber, error: e });
this.node.app.handleError({ fiber: current as Fiber, error: e });
}
}
}
+1
View File
@@ -36,6 +36,7 @@ export const blockDom = {
export { App, mount } from "./app";
export { xml } from "./template_set";
export { Component } from "./component";
export type { ComponentConstructor } from "./component";
export { useComponent, useState } from "./component_node";
export { status } from "./status";
export { reactive, markRaw, toRaw } from "./reactivity";
+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;
};
+11 -5
View File
@@ -28,12 +28,18 @@ export function batched(callback: Callback): Callback {
}
export function validateTarget(target: HTMLElement) {
if (!(target instanceof HTMLElement)) {
throw new OwlError("Cannot mount component: the target is not a valid DOM element");
}
if (!document.body.contains(target)) {
throw new OwlError("Cannot mount a component on a detached dom node");
// Get the document and HTMLElement corresponding to the target to allow mounting in iframes
const document = target && target.ownerDocument;
if (document) {
const HTMLElement = document.defaultView!.HTMLElement;
if (target instanceof HTMLElement) {
if (!document.body.contains(target)) {
throw new OwlError("Cannot mount a component on a detached dom node");
}
return;
}
}
throw new OwlError("Cannot mount component: the target is not a valid DOM element");
}
export class EventBus extends EventTarget {
+13
View File
@@ -29,6 +29,19 @@ exports[`app can configure an app with props 1`] = `
}"
`;
exports[`app can mount app in an iframe 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div class=\\"my-div\\"/>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`app destroy remove the widget from the DOM 1`] = `
"function anonymous(app, bdom, helpers
) {
+18
View File
@@ -76,4 +76,22 @@ describe("app", () => {
"Component 'Root' does not have a static props description"
);
});
test("can mount app in an iframe", async () => {
class SomeComponent extends Component {
static template = xml`<div class="my-div"/>`;
}
const iframe = document.createElement("iframe");
fixture.appendChild(iframe);
const app = new App(SomeComponent);
const iframeDoc = iframe.contentDocument!;
const comp = await app.mount(iframeDoc.body);
const div = iframeDoc.querySelector(".my-div");
expect(div).not.toBe(null);
expect(iframeDoc.contains(div)).toBe(true);
app.destroy();
expect(iframeDoc.contains(div)).toBe(false);
expect(status(comp)).toBe("destroyed");
});
});
@@ -708,6 +708,20 @@ exports[`attributes updating classes (with obj notation) 1`] = `
}"
`;
exports[`attributes updating property with falsy value 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<input block-attribute-0=\\"value\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let attr1 = new String((ctx['v']) || \\"\\");
return block1([attr1]);
}
}"
`;
exports[`attributes various escapes 1`] = `
"function anonymous(app, bdom, helpers
) {
@@ -773,7 +787,7 @@ exports[`special cases for some specific html attributes/properties input with t
let block1 = createBlock(\`<input block-attribute-0=\\"value\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let attr1 = new String(ctx['v']);
let attr1 = new String((ctx['v']) || \\"\\");
return block1([attr1]);
}
}"
@@ -787,7 +801,7 @@ exports[`special cases for some specific html attributes/properties input with t
let block1 = createBlock(\`<input block-attribute-0=\\"value\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let attr1 = new String(ctx['v']);
let attr1 = new String((ctx['v']) || \\"\\");
return block1([attr1]);
}
}"
@@ -815,7 +829,7 @@ exports[`special cases for some specific html attributes/properties select with
let block1 = createBlock(\`<select block-attribute-0=\\"value\\"><option value=\\"potato\\">Potato</option><option value=\\"tomato\\">Tomato</option><option value=\\"onion\\">Onion</option></select>\`);
return function template(ctx, node, key = \\"\\") {
let attr1 = new String(ctx['value']);
let attr1 = new String((ctx['value']) || \\"\\");
return block1([attr1]);
}
}"
@@ -829,7 +843,7 @@ exports[`special cases for some specific html attributes/properties textarea wit
let block1 = createBlock(\`<textarea block-attribute-0=\\"value\\"/>\`);
return function template(ctx, node, key = \\"\\") {
let attr1 = new String(ctx['v']);
let attr1 = new String((ctx['v']) || \\"\\");
return block1([attr1]);
}
}"
@@ -277,7 +277,7 @@ exports[`misc other complex template 1`] = `
const b15 = list(c_block15);
b14 = block14([], [b15]);
}
let attr8 = new String(ctx['search'].value);
let attr8 = new String((ctx['search'].value) || \\"\\");
let hdlr4 = [ctx['updateFilter'], ctx];
let hdlr5 = [ctx['updateFilter'], ctx];
let hdlr6 = [ctx['clearSearch'], ctx];
+29
View File
@@ -329,6 +329,35 @@ describe("attributes", () => {
expect(fixture.innerHTML).toBe('<div value=""></div>');
});
test("updating property with falsy value", async () => {
// render input with initial value
const template = `<input t-att-value="v"></input>`;
const bnode1 = renderToBdom(template, { v: false });
const fixture = makeTestFixture();
mount(bnode1, fixture);
const input = fixture.querySelector("input")!;
expect(input.value).toBe("");
patch(bnode1, renderToBdom(template, { v: "owl" }));
expect(input.value).toBe("owl");
patch(bnode1, renderToBdom(template, { v: false }));
expect(input.value).toBe("");
patch(bnode1, renderToBdom(template, { v: "owl" }));
expect(input.value).toBe("owl");
patch(bnode1, renderToBdom(template, { v: undefined }));
expect(input.value).toBe("");
patch(bnode1, renderToBdom(template, { v: "owl" }));
expect(input.value).toBe("owl");
patch(bnode1, renderToBdom(template, { v: null }));
expect(input.value).toBe("");
});
test("changing a class with t-att-class", () => {
// render input with initial value
const template = `<div t-att-class="v"/>`;
@@ -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
) {
@@ -58,6 +58,20 @@ exports[`event handling handler receive the event as argument 2`] = `
}"
`;
exports[`event handling handler works when app is mounted in an iframe 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<span block-handler-0=\\"click\\">click me</span>\`);
return function template(ctx, node, key = \\"\\") {
let hdlr1 = [ctx['inc'], ctx];
return block1([hdlr1]);
}
}"
`;
exports[`event handling input blur event is not called if component is destroyed 1`] = `
"function anonymous(app, bdom, helpers
) {
@@ -604,6 +604,64 @@ exports[`slots default slot work with text nodes 2`] = `
}"
`;
exports[`slots dynamic slot in multiple locations 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { capture, markRaw } = helpers;
const comp1 = app.createComponent(\`Child\`, true, false, false, true);
const comp2 = app.createComponent(\`Slotter\`, true, true, false, false);
function slot1(ctx, node, key = \\"\\") {
const b2 = text(\`hello \`);
const b3 = comp1({}, key + \`__1\`, node, this, null);
return multi([b2, b3]);
}
return function template(ctx, node, key = \\"\\") {
const ctx1 = capture(ctx);
return comp2({location: ctx['state'].location,slots: markRaw({'coffee': {__render: slot1, __ctx: ctx1}})}, key + \`__2\`, node, this, null);
}
}"
`;
exports[`slots dynamic slot in multiple locations 2`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { callSlot } = helpers;
let block2 = createBlock(\`<p><block-child-0/></p>\`);
return function template(ctx, node, key = \\"\\") {
let b2,b4;
if (ctx['props'].location===1) {
const slot1 = ('coffee');
const b3 = toggler(slot1, callSlot(ctx, node, key + \`__1\`, slot1, true, {}));
b2 = block2([], [b3]);
}
if (ctx['props'].location===2) {
const slot2 = ('coffee');
b4 = toggler(slot2, callSlot(ctx, node, key + \`__2\`, slot2, true, {}));
}
return multi([b2, b4]);
}
}"
`;
exports[`slots dynamic slot in multiple locations 3`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div>child</div>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`slots dynamic t-slot call 1`] = `
"function anonymous(app, bdom, helpers
) {
@@ -645,7 +703,7 @@ exports[`slots dynamic t-slot call 2`] = `
return function template(ctx, node, key = \\"\\") {
let hdlr1 = [ctx['toggle'], ctx];
const slot1 = (ctx['current'].slot);
const b2 = toggler(slot1, callSlot(ctx, node, key, slot1, true, {}));
const b2 = toggler(slot1, callSlot(ctx, node, key + \`__1\`, slot1, true, {}));
return block1([hdlr1], [b2]);
}
}"
@@ -695,7 +753,7 @@ exports[`slots dynamic t-slot call with default 2`] = `
return function template(ctx, node, key = \\"\\") {
let hdlr1 = [ctx['toggle'], ctx];
const b3 = callSlot(ctx, node, key, (ctx['current'].slot), true, {}, defaultContent1);
const b3 = callSlot(ctx, node, key + \`__1\`, (ctx['current'].slot), true, {}, defaultContent1);
return block1([hdlr1], [b3]);
}
}"
@@ -726,7 +784,7 @@ exports[`slots fun: two calls to the same slot 2`] = `
return function template(ctx, node, key = \\"\\") {
const b2 = callSlot(ctx, node, key, 'default', false, {});
const b3 = callSlot(ctx, node, key, 'default', false, {});
const b3 = callSlot(ctx, node, key + \`__1\`, 'default', false, {});
return multi([b2, b3]);
}
}"
@@ -1527,7 +1585,7 @@ exports[`slots simple dynamic slot with slot scope 2`] = `
return function template(ctx, node, key = \\"\\") {
const slot1 = ('slotName');
const b2 = toggler(slot1, callSlot(ctx, node, key, slot1, true, {bool: ctx['state'].bool}));
const b2 = toggler(slot1, callSlot(ctx, node, key + \`__1\`, slot1, true, {bool: ctx['state'].bool}));
return block1([], [b2]);
}
}"
@@ -1854,7 +1912,7 @@ exports[`slots slot content has different key from other content -- dynamic slot
return function template(ctx, node, key = \\"\\") {
const b2 = comp1({parent: 'SlotDisplay'}, key + \`__1\`, node, this, null);
const slot1 = (ctx['slotName']);
const b3 = toggler(slot1, callSlot(ctx, node, key, slot1, true, {}));
const b3 = toggler(slot1, callSlot(ctx, node, key + \`__2\`, slot1, true, {}));
return multi([b2, b3]);
}
}"
@@ -1995,6 +2053,118 @@ exports[`slots slot content is bound to caller 2`] = `
}"
`;
exports[`slots slot in multiple locations 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { markRaw } = helpers;
const comp1 = app.createComponent(\`Child\`, true, false, false, true);
const comp2 = app.createComponent(\`Slotter\`, true, true, false, false);
function slot1(ctx, node, key = \\"\\") {
const b2 = text(\` hello \`);
const b3 = comp1({}, key + \`__1\`, node, this, null);
return multi([b2, b3]);
}
return function template(ctx, node, key = \\"\\") {
return comp2({location: ctx['state'].location,slots: markRaw({'default': {__render: slot1, __ctx: ctx}})}, key + \`__2\`, node, this, null);
}
}"
`;
exports[`slots slot in multiple locations 2`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { callSlot } = helpers;
let block2 = createBlock(\`<p><block-child-0/></p>\`);
return function template(ctx, node, key = \\"\\") {
let b2,b4;
if (ctx['props'].location===1) {
const b3 = callSlot(ctx, node, key, 'default', false, {});
b2 = block2([], [b3]);
}
if (ctx['props'].location===2) {
b4 = callSlot(ctx, node, key + \`__1\`, 'default', false, {});
}
return multi([b2, b4]);
}
}"
`;
exports[`slots slot in multiple locations 3`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div>child</div>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`slots slot in t-foreach locations 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { markRaw } = helpers;
const comp1 = app.createComponent(\`Child\`, true, false, false, true);
const comp2 = app.createComponent(\`Slotter\`, true, true, false, false);
function slot1(ctx, node, key = \\"\\") {
const b2 = text(\` hello \`);
const b3 = comp1({}, key + \`__1\`, node, this, null);
return multi([b2, b3]);
}
return function template(ctx, node, key = \\"\\") {
return comp2({list: ctx['state'].list,slots: markRaw({'default': {__render: slot1, __ctx: ctx}})}, key + \`__2\`, node, this, null);
}
}"
`;
exports[`slots slot in t-foreach locations 2`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { prepareList, callSlot, withKey } = helpers;
let block2 = createBlock(\`<p><block-text-0/><block-child-0/></p>\`);
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
const [k_block1, v_block1, l_block1, c_block1] = prepareList(ctx['props'].list);;
for (let i1 = 0; i1 < l_block1; i1++) {
ctx[\`elem\`] = v_block1[i1];
ctx[\`elem_index\`] = i1;
const key1 = ctx['elem_index'];
let txt1 = ctx['elem'];
const b3 = callSlot(ctx, node, key1, 'default', false, {});
c_block1[i1] = withKey(block2([txt1], [b3]), key1);
}
return list(c_block1);
}
}"
`;
exports[`slots slot in t-foreach locations 3`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div>child</div>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`slots slot preserves properly parented relationship 1`] = `
"function anonymous(app, bdom, helpers
) {
+14 -7
View File
@@ -1,5 +1,12 @@
import { App, Component, mount, status, toRaw, useState, xml } from "../../src";
import { elem, makeTestFixture, nextTick, snapshotEverything, useLogLifecycle } from "../helpers";
import {
elem,
makeTestFixture,
nextAppError,
nextTick,
snapshotEverything,
useLogLifecycle,
} from "../helpers";
import { markup } from "../../src/runtime/utils";
let fixture: HTMLElement;
@@ -208,14 +215,14 @@ describe("basics", () => {
static template = xml`<div/>`;
}
let error: Error;
const prom = mount(Test, fixture);
const app = new App(Test);
const prom = app.mount(fixture);
await Promise.resolve();
fixture.remove();
try {
await prom;
} catch (e) {
error = e as Error;
}
prom.catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow(
"Cannot mount a component on a detached dom node"
);
expect(error!).toBeDefined();
expect(error!.message).toBe("Cannot mount a component on a detached dom node");
expect(console.warn).toBeCalledTimes(1);
+151 -111
View File
@@ -1,4 +1,4 @@
import { Component, mount, onWillDestroy } from "../../src";
import { App, Component, mount, onWillDestroy } from "../../src";
import {
onError,
onMounted,
@@ -18,6 +18,7 @@ import {
nextMicroTick,
snapshotEverything,
useLogLifecycle,
nextAppError,
} from "../helpers";
import { OwlError } from "../../src/runtime/error_handling";
@@ -59,9 +60,10 @@ describe("basics", () => {
parent.state.flag = true;
parent.render();
await nextTick();
await expect(nextAppError(parent.__owl__.app)).resolves.toThrow(
"An error occured in the owl lifecycle"
);
expect(fixture.innerHTML).toBe("");
expect(mockConsoleError).toBeCalledTimes(1);
expect(mockConsoleWarn).toBeCalledTimes(1);
});
@@ -71,12 +73,13 @@ describe("basics", () => {
static template = xml`<SomeMispelledComponent />`;
static components = { SomeComponent };
}
const app = new App(Parent);
let error: Error;
try {
await mount(Parent, fixture);
} catch (e) {
error = e as Error;
}
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow(
'Cannot find the definition of component "SomeMispelledComponent"'
);
await mountProm;
expect(error!).toBeDefined();
expect(error!.message).toBe('Cannot find the definition of component "SomeMispelledComponent"');
expect(console.error).toBeCalledTimes(0);
@@ -90,12 +93,13 @@ describe("basics", () => {
static template = xml`<SomeMispelledComponent />`;
static components = { SomeComponent };
}
const app = new App(Parent, { test: true });
let error: Error;
try {
await mount(Parent, fixture, { test: true });
} catch (e) {
error = e as Error;
}
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow(
'Cannot find the definition of component "SomeMispelledComponent"'
);
await mountProm;
expect(error!).toBeDefined();
expect(error!.message).toBe('Cannot find the definition of component "SomeMispelledComponent"');
expect(console.error).toBeCalledTimes(0);
@@ -109,13 +113,13 @@ describe("basics", () => {
static template = xml`<SomeComponent />`;
static components = { SomeComponent: notAComponentConstructor };
}
const app = new App(Parent as typeof Component);
let error: Error;
try {
// @ts-expect-error
await mount(Parent, fixture);
} catch (e) {
error = e as Error;
}
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow(
'"SomeComponent" is not a Component. It must inherit from the Component class'
);
await mountProm;
expect(error!).toBeDefined();
expect(error!.message).toBe(
'"SomeComponent" is not a Component. It must inherit from the Component class'
@@ -156,16 +160,15 @@ describe("basics", () => {
describe("errors and promises", () => {
test("a rendering error will reject the mount promise", async () => {
// we do not catch error in willPatch anymore
class App extends Component {
class Root extends Component {
static template = xml`<div><t t-esc="this.will.crash"/></div>`;
}
const app = new App(Root);
let error: OwlError;
try {
await mount(App, fixture);
} catch (e) {
error = e as OwlError;
}
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("error occured in the owl lifecycle");
await mountProm;
expect(error!).toBeDefined();
expect(error!.cause).toBeDefined();
const regexp =
@@ -176,7 +179,7 @@ describe("errors and promises", () => {
});
test("an error in mounted call will reject the mount promise", async () => {
class App extends Component {
class Root extends Component {
static template = xml`<div>abc</div>`;
setup() {
onMounted(() => {
@@ -185,12 +188,11 @@ describe("errors and promises", () => {
}
}
const app = new App(Root);
let error: OwlError;
try {
await mount(App, fixture);
} catch (e) {
error = e as OwlError;
}
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("error occured in the owl lifecycle");
await mountProm;
expect(error!).toBeDefined();
expect(error!.cause).toBeDefined();
expect(error!.cause.message).toBe("boom");
@@ -200,7 +202,7 @@ describe("errors and promises", () => {
});
test("an error in onMounted callback will have the component's setup in its stack trace", async () => {
class App extends Component {
class Root extends Component {
static template = xml`<div>abc</div>`;
setup() {
onMounted(() => {
@@ -209,14 +211,13 @@ describe("errors and promises", () => {
}
}
let error: Error;
try {
await mount(App, fixture, { test: true });
} catch (e) {
error = e as Error;
}
const app = new App(Root, { test: true });
let error: OwlError;
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("error occurred in onMounted");
await mountProm;
expect(error!).toBeDefined();
expect(error!.stack).toContain("App.setup");
expect(error!.stack).toContain("Root.setup");
expect(error!.stack).toContain("error_handling.test.ts");
expect(fixture.innerHTML).toBe("");
expect(mockConsoleError).toBeCalledTimes(0);
@@ -224,7 +225,7 @@ describe("errors and promises", () => {
});
test("errors in onWillRender/onRender aren't wrapped more than once", async () => {
class App extends Component {
class Root extends Component {
static template = xml`<div>abc</div>`;
setup() {
onWillRender(() => {
@@ -236,12 +237,11 @@ describe("errors and promises", () => {
}
}
let error: Error;
try {
await mount(App, fixture, { test: true });
} catch (e) {
error = e as Error;
}
const app = new App(Root, { test: true });
let error: OwlError;
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("error occurred in onWillRender");
await mountProm;
expect(error!).toBeDefined();
expect(error!.message).toBe(
`The following error occurred in onWillRender: "boom in onWillRender"`
@@ -278,12 +278,11 @@ describe("errors and promises", () => {
}
}
let error: any;
try {
await mount(Root, fixture, { test: true });
} catch (e) {
error = e;
}
const app = new App(Root, { test: true });
let error: OwlError;
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("error occurred in onWillStart");
await mountProm;
expect(error!).toBeDefined();
expect(error!.message).toBe(
`The following error occurred in onWillStart: "boom in onWillStart"`
@@ -342,17 +341,16 @@ describe("errors and promises", () => {
class Child extends Component {
static template = xml`<div><t t-esc="this.will.crash"/></div>`;
}
class App extends Component {
class Parent extends Component {
static template = xml`<div><Child/></div>`;
static components = { Child };
}
const app = new App(Parent);
let error: OwlError;
try {
await mount(App, fixture);
} catch (e) {
error = e as OwlError;
}
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("error occured in the owl lifecycle");
await mountProm;
expect(error!).toBeDefined();
expect(error!.cause).toBeDefined();
const regexp =
@@ -394,12 +392,11 @@ describe("errors and promises", () => {
static components = { Child };
}
const app = new App(Parent);
let error: OwlError;
try {
await mount(Parent, fixture);
} catch (e) {
error = e as OwlError;
}
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("error occured in the owl lifecycle");
await mountProm;
expect(error!).toBeDefined();
expect(error!.cause).toBeDefined();
const regexp =
@@ -425,13 +422,12 @@ describe("errors and promises", () => {
}
}
try {
await mount(Example, fixture, { test: true });
} catch (e) {
expect((e as Error).message).toBe(
`The following error occurred in onMounted: "Error in mounted"`
);
}
const app = new App(Example, { test: true });
let error: OwlError;
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("error occurred in onMounted");
await mountProm;
expect(error!.message).toBe(`The following error occurred in onMounted: "Error in mounted"`);
// 1 additional error is logged because the destruction of the app causes
// the onWillUnmount hook to be called and to fail
expect(mockConsoleError).toBeCalledTimes(1);
@@ -448,9 +444,10 @@ describe("errors and promises", () => {
root.state = "boom";
root.render();
await nextTick();
await expect(nextAppError(root.__owl__.app)).resolves.toThrow(
"error occured in the owl lifecycle"
);
expect(fixture.innerHTML).toBe("");
expect(mockConsoleError).toBeCalledTimes(1);
expect(mockConsoleWarn).toBeCalledTimes(1);
});
});
@@ -500,18 +497,17 @@ describe("can catch errors", () => {
});
}
}
let e: Error;
try {
await mount(Root, fixture, { test: true });
} catch (error) {
e = error as Error;
}
expect(e!.message).toBe(
const app = new App(Root, { test: true });
let error: OwlError;
const crashProm = expect(nextAppError(app)).resolves.toThrow("error occurred in onWillStart");
await app.mount(fixture).catch((e: Error) => (error = e));
await crashProm;
expect(error!.message).toBe(
`The following error occurred in onWillStart: "No active component (a hook function should only be called in 'setup')"`
);
});
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"/>`;
@@ -523,14 +519,13 @@ describe("can catch errors", () => {
});
}
}
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);
const app = new App(Root, { test: true });
let error: OwlError;
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("error occurred in onMounted");
await mountProm;
expect(error!.message).toBe(`The following error occurred in onMounted: "test error"`);
expect(error!.cause).toBe(err);
});
test("Errors in owl lifecycle are wrapped in dev mode: async hook", async () => {
@@ -546,14 +541,13 @@ describe("can catch errors", () => {
});
}
}
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);
const app = new App(Root, { test: true });
let error: OwlError;
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("error occurred in onWillStart");
await mountProm;
expect(error!.message).toBe(`The following error occurred in onWillStart: "test error"`);
expect(error!.cause).toBe(err);
});
test("Errors in owl lifecycle are wrapped outside dev mode: sync hook", async () => {
@@ -568,14 +562,15 @@ describe("can catch errors", () => {
});
}
}
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);
const app = new App(Root);
let error: OwlError;
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("error occured in the owl lifecycle");
await mountProm;
expect(error!.message).toBe(
`An error occured in the owl lifecycle (see this Error's "cause" property)`
);
expect(error!.cause).toBe(err);
});
test("Errors in owl lifecycle are wrapped out of dev mode: async hook", async () => {
@@ -591,14 +586,59 @@ describe("can catch errors", () => {
});
}
}
let e: OwlError;
try {
await mount(Root, fixture);
} catch (error) {
e = error as OwlError;
const app = new App(Root);
let error: OwlError;
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("error occured in the owl lifecycle");
await mountProm;
expect(error!.message).toBe(
`An error occured in the owl lifecycle (see this Error's "cause" property)`
);
expect(error!.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";
});
}
}
expect(e!.message).toBe("An error occured in the owl lifecycle");
expect(e!.cause).toBe(err);
const app = new App(Root, { test: true });
let error: OwlError;
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("not an Error was thrown in onMounted");
await mountProm;
expect(error!.message).toBe(
`Something that is not an Error was thrown in onMounted (see this Error's "cause" property)`
);
expect(error!.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";
});
}
}
const app = new App(Root);
let error: OwlError;
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("error occured in the owl lifecycle");
await mountProm;
expect(error!.message).toBe(
`An error occured in the owl lifecycle (see this Error's "cause" property)`
);
expect(error!.cause).toBe("This is not an error");
});
test("can catch an error in the initial call of a component render function (parent mounted)", async () => {
+18
View File
@@ -171,4 +171,22 @@ describe("event handling", () => {
// input is removed when component is destroyed => nothing should happen
expect([]).toBeLogged();
});
test("handler works when app is mounted in an iframe", async () => {
let clickCount = 0;
class Parent extends Component {
static template = xml`<span t-on-click="inc">click me</span>`;
inc() {
clickCount++;
}
}
const iframe = document.createElement("iframe");
fixture.appendChild(iframe);
const iframeDoc = iframe.contentDocument!;
await mount(Parent, iframeDoc.body);
expect(clickCount).toBe(0);
iframeDoc.querySelector("span")!.click();
expect(clickCount).toBe(1);
});
});
+15 -6
View File
@@ -17,8 +17,16 @@ import {
useChildSubEnv,
useSubEnv,
xml,
OwlError,
} from "../../src/index";
import { elem, logStep, makeTestFixture, nextTick, snapshotEverything } from "../helpers";
import {
elem,
logStep,
makeTestFixture,
nextAppError,
nextTick,
snapshotEverything,
} from "../helpers";
let fixture: HTMLElement;
@@ -650,11 +658,12 @@ describe("hooks", () => {
}
}
try {
await mount(MyComponent, fixture);
} catch (e: any) {
expect(e.cause.message).toBe("Intentional error");
}
let error: OwlError;
const app = new App(MyComponent);
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("error occured in the owl lifecycle");
await mountProm;
expect(error!.cause.message).toBe("Intentional error");
// no console.error because the error has been caught in this test
expect(console.error).toHaveBeenCalledTimes(0);
console.error = originalconsoleError;
+98 -111
View File
@@ -1,6 +1,6 @@
import { makeTestFixture, nextTick, snapshotEverything } from "../helpers";
import { Component, onError, xml, mount } from "../../src";
import { DEV_MSG } from "../../src/runtime/app";
import { makeTestFixture, nextAppError, nextTick, snapshotEverything } from "../helpers";
import { Component, onError, xml, mount, OwlError } from "../../src";
import { App, DEV_MSG } from "../../src/runtime/app";
import { validateProps } from "../../src/runtime/template_helpers";
import { Schema } from "../../src/runtime/validation";
@@ -48,13 +48,14 @@ describe("props validation", () => {
static components = { SubComp };
static template = xml`<div><SubComp /></div>`;
}
let error: Error | undefined;
try {
await mount(Parent, fixture, { dev: true });
} catch (e) {
error = e as Error;
}
const app = new App(Parent, { test: true });
let error: OwlError | undefined;
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow(
"Invalid props for component 'SubComp': 'message' is missing"
);
await mountProm;
expect(error!).toBeDefined();
expect(error!.message).toBe("Invalid props for component 'SubComp': 'message' is missing");
error = undefined;
@@ -77,12 +78,13 @@ describe("props validation", () => {
static template = xml`<div><SubComp /></div>`;
}
let error: Error;
try {
await mount(Parent, fixture, { dev: true });
} catch (e) {
error = e as Error;
}
const app = new App(Parent, { test: true });
let error: OwlError | undefined;
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow(
"Invalid props for component 'SubComp': 'message' is missing"
);
await mountProm;
expect(error!).toBeDefined();
expect(error!.message).toBe("Invalid props for component 'SubComp': 'message' is missing");
});
@@ -126,14 +128,12 @@ describe("props validation", () => {
};
(Parent as any).components = { SubComp };
let error: Error | undefined;
props = {};
try {
await mount(Parent, fixture, { dev: true });
} catch (e) {
error = e as Error;
}
let app = new App(Parent, { test: true });
let error: OwlError | undefined;
let mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("Invalid props for component '_a'");
await mountProm;
expect(error!).toBeDefined();
expect(error!.message).toBe(
`Invalid props for component '_a': 'p' is undefined (should be a ${test.type.name.toLowerCase()})`
@@ -147,11 +147,10 @@ describe("props validation", () => {
}
expect(error!).toBeUndefined();
props = { p: test.ko };
try {
await mount(Parent, fixture, { dev: true });
} catch (e) {
error = e as Error;
}
app = new App(Parent, { test: true });
mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("Invalid props for component '_a'");
await mountProm;
expect(error!).toBeDefined();
expect(error!.message).toBe(
`Invalid props for component '_a': 'p' is not a ${test.type.name.toLowerCase()}`
@@ -181,13 +180,12 @@ describe("props validation", () => {
static template = xml`<div>hey</div>`;
};
(Parent as any).components = { SubComp };
let error: Error | undefined;
props = {};
try {
await mount(Parent, fixture, { dev: true });
} catch (e) {
error = e as Error;
}
let app = new App(Parent, { test: true });
let error: OwlError | undefined;
let mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("Invalid props for component '_a'");
await mountProm;
expect(error!).toBeDefined();
expect(error!.message).toBe(
`Invalid props for component '_a': 'p' is undefined (should be a ${test.type.name.toLowerCase()})`
@@ -201,11 +199,10 @@ describe("props validation", () => {
}
expect(error!).toBeUndefined();
props = { p: test.ko };
try {
await mount(Parent, fixture, { dev: true });
} catch (e) {
error = e as Error;
}
app = new App(Parent, { test: true });
mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("Invalid props for component '_a'");
await mountProm;
expect(error!).toBeDefined();
expect(error!.message).toBe(
`Invalid props for component '_a': 'p' is not a ${test.type.name.toLowerCase()}`
@@ -227,26 +224,25 @@ describe("props validation", () => {
}
let error: Error;
let props: { p?: any };
props = { p: "string" };
try {
props = { p: "string" };
await mount(Parent, fixture, { dev: true });
} catch (e) {
error = e as Error;
}
expect(error!).toBeUndefined();
props = { p: true };
try {
props = { p: true };
await mount(Parent, fixture, { dev: true });
} catch (e) {
error = e as Error;
}
expect(error!).toBeUndefined();
try {
props = { p: 1 };
await mount(Parent, fixture, { dev: true });
} catch (e) {
error = e as Error;
}
props = { p: 1 };
const app = new App(Parent, { test: true });
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("Invalid props for component 'SubComp'");
await mountProm;
expect(error!).toBeDefined();
expect(error!.message).toBe(
"Invalid props for component 'SubComp': 'p' is not a string or boolean"
@@ -267,26 +263,25 @@ describe("props validation", () => {
}
let error: Error;
let props: { p?: any };
props = { p: "key" };
try {
props = { p: "key" };
await mount(Parent, fixture, { dev: true });
} catch (e) {
error = e as Error;
}
expect(error!).toBeUndefined();
props = {};
try {
props = {};
await mount(Parent, fixture, { dev: true });
} catch (e) {
error = e as Error;
}
expect(error!).toBeUndefined();
try {
props = { p: 1 };
await mount(Parent, fixture, { dev: true });
} catch (e) {
error = e as Error;
}
props = { p: 1 };
const app = new App(Parent, { test: true });
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("Invalid props for component 'SubComp'");
await mountProm;
expect(error!).toBeDefined();
expect(error!.message).toBe("Invalid props for component 'SubComp': 'p' is not a string");
});
@@ -319,20 +314,18 @@ describe("props validation", () => {
error = e as Error;
}
expect(error!).toBeUndefined();
try {
props = { p: [1] };
await mount(Parent, fixture, { dev: true });
} catch (e) {
error = e as Error;
}
props = { p: [1] };
let app = new App(Parent, { test: true });
let mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("Invalid props for component 'SubComp'");
await mountProm;
expect(error!).toBeDefined();
error = undefined;
try {
props = { p: ["string", 1] };
await mount(Parent, fixture, { dev: true });
} catch (e) {
error = e as Error;
}
app = new App(Parent, { test: true });
mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("Invalid props for component 'SubComp'");
await mountProm;
expect(error!).toBeDefined();
});
test("can validate an array with multiple sub element types", async () => {
@@ -370,12 +363,11 @@ describe("props validation", () => {
error = e as Error;
}
expect(error!).toBeUndefined();
try {
props = { p: [true, 1] };
await mount(Parent, fixture, { dev: true });
} catch (e) {
error = e as Error;
}
props = { p: [true, 1] };
const app = new App(Parent, { test: true });
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("Invalid props for component 'SubComp'");
await mountProm;
expect(error!).toBeDefined();
expect(error!.message).toBe(
"Invalid props for component 'SubComp': 'p[1]' is not a string or boolean"
@@ -405,33 +397,30 @@ describe("props validation", () => {
error = e as Error;
}
expect(error!).toBeUndefined();
try {
props = { p: { id: 1, url: "url", extra: true } };
await mount(Parent, fixture, { dev: true });
} catch (e) {
error = e as Error;
}
props = { p: { id: 1, url: "url", extra: true } };
let app = new App(Parent, { test: true });
let mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("Invalid props for component 'SubComp'");
await mountProm;
expect(error!).toBeDefined();
expect(error!.message).toBe(
"Invalid props for component 'SubComp': 'p' has not the correct shape (unknown key 'extra')"
);
try {
props = { p: { id: "1", url: "url" } };
await mount(Parent, fixture, { dev: true });
} catch (e) {
error = e as Error;
}
props = { p: { id: "1", url: "url" } };
app = new App(Parent, { test: true });
mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("Invalid props for component 'SubComp'");
await mountProm;
expect(error!).toBeDefined();
expect(error!.message).toBe(
"Invalid props for component 'SubComp': 'p' has not the correct shape ('id' is not a number)"
);
error = undefined;
try {
props = { p: { id: 1 } };
await mount(Parent, fixture, { dev: true });
} catch (e) {
error = e as Error;
}
props = { p: { id: 1 } };
app = new App(Parent, { test: true });
mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("Invalid props for component 'SubComp'");
await mountProm;
expect(error!).toBeDefined();
expect(error!.message).toBe(
"Invalid props for component 'SubComp': 'p' has not the correct shape ('url' is missing (should be a string))"
@@ -474,12 +463,11 @@ describe("props validation", () => {
error = e as Error;
}
expect(error!).toBeUndefined();
try {
props = { p: { id: 1, url: [12, true] } };
await mount(Parent, fixture, { dev: true });
} catch (e) {
error = e as Error;
}
props = { p: { id: 1, url: [12, true] } };
const app = new App(Parent, { test: true });
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("Invalid props for component 'SubComp'");
await mountProm;
expect(error!).toBeDefined();
expect(error!.message).toBe(
"Invalid props for component 'SubComp': 'p' has not the correct shape ('url' is not a boolean or list of numbers)"
@@ -686,11 +674,10 @@ describe("props validation", () => {
static components = { SubComp };
}
let error: Error;
try {
await mount(Parent, fixture, { dev: true });
} catch (e) {
error = e as Error;
}
const app = new App(Parent, { test: true });
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("Invalid props for component 'SubComp'");
await mountProm;
expect(error!).toBeDefined();
expect(error!.message).toBe("Invalid props for component 'SubComp': 'p' is missing");
});
@@ -754,11 +741,10 @@ describe("props validation", () => {
static template = xml`<div><Child/></div>`;
}
let error: Error;
try {
await mount(Parent, fixture, { dev: true });
} catch (e) {
error = e as Error;
}
const app = new App(Parent, { test: true });
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("Invalid props for component 'Child'");
await mountProm;
expect(error!).toBeDefined();
expect(error!.message).toBe(
"Invalid props for component 'Child': 'mandatory' is missing (should be a number)"
@@ -859,11 +845,12 @@ describe("default props", () => {
static template = xml`<Child/>`;
}
let error: Error;
try {
await mount(Parent, fixture, { dev: true });
} catch (e) {
error = e as Error;
}
const app = new App(Parent, { test: true });
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow(
"default value cannot be defined for a mandatory prop"
);
await mountProm;
expect(error!).toBeDefined();
expect(error!.message).toBe(
"A default value cannot be defined for a mandatory prop (name: 'mandatory', component: Child)"
+10 -5
View File
@@ -1,5 +1,5 @@
import { Component, mount, onMounted, useRef, useState } from "../../src/index";
import { logStep, makeTestFixture, nextTick, snapshotEverything } from "../helpers";
import { App, Component, mount, onMounted, useRef, useState } from "../../src/index";
import { logStep, makeTestFixture, nextAppError, nextTick, snapshotEverything } from "../helpers";
import { xml } from "../../src/index";
snapshotEverything();
@@ -94,9 +94,14 @@ describe("refs", () => {
ref = useRef("coucou");
}
await expect(async () => {
await mount(Test, fixture);
}).rejects.toThrowError("Cannot have 2 elements with same ref name at the same time");
const app = new App(Test, { test: true });
const mountProm = expect(app.mount(fixture)).rejects.toThrowError(
"Cannot have 2 elements with same ref name at the same time"
);
await expect(nextAppError(app)).resolves.toThrow(
"Cannot have 2 elements with same ref name at the same time"
);
await mountProm;
expect(console.warn).toBeCalledTimes(1);
console.warn = consoleWarn;
});
+105 -8
View File
@@ -1,5 +1,5 @@
import { App, Component, mount, onMounted, useState, xml } from "../../src/index";
import { children, makeTestFixture, nextTick, snapshotEverything } from "../helpers";
import { children, makeTestFixture, nextAppError, nextTick, snapshotEverything } from "../helpers";
snapshotEverything();
let originalconsoleWarn = console.warn;
@@ -204,13 +204,12 @@ describe("slots", () => {
static components = { Child };
}
let error = null;
try {
await mount(Parent, fixture);
} catch (e) {
error = e;
}
expect(error).not.toBeNull();
let error: Error;
const app = new App(Parent);
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("error occured in the owl lifecycle");
await mountProm;
expect(error!).not.toBeNull();
expect(mockConsoleWarn).toBeCalledTimes(1);
});
@@ -1819,4 +1818,102 @@ describe("slots", () => {
await nextTick();
expect(fixture.innerHTML).toBe("<button>inc</button>[B][C][A][sub1] [sub2334]");
});
test("slot in multiple locations", async () => {
class Child extends Component {
static template = xml`<div>child</div>`;
}
class Slotter extends Component {
static components = { Child };
static template = xml`
<t t-if="props.location === 1">
<p><t t-slot="default"/></p>
</t>
<t t-if="props.location === 2">
<t t-slot="default"/>
</t>
`;
}
class Parent extends Component {
static components = { Child, Slotter };
static template = xml`
<Slotter location="state.location">
hello <Child/>
</Slotter>`;
state = useState({ location: 1 });
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("<p> hello <div>child</div></p>");
parent.state.location = 2;
await nextTick();
expect(fixture.innerHTML).toBe(" hello <div>child</div>");
});
test("dynamic slot in multiple locations", async () => {
class Child extends Component {
static template = xml`<div>child</div>`;
}
class Slotter extends Component {
static components = { Child };
static template = xml`
<t t-if="props.location === 1">
<p><t t-slot="{{'coffee'}}"/></p>
</t>
<t t-if="props.location === 2">
<t t-slot="{{'coffee'}}"/>
</t>
`;
}
class Parent extends Component {
static components = { Child, Slotter };
static template = xml`
<Slotter location="state.location">
<t t-set-slot="coffee">hello <Child/></t>
</Slotter>`;
state = useState({ location: 1 });
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("<p>hello <div>child</div></p>");
parent.state.location = 2;
await nextTick();
expect(fixture.innerHTML).toBe("hello <div>child</div>");
});
test("slot in t-foreach locations", async () => {
class Child extends Component {
static template = xml`<div>child</div>`;
}
class Slotter extends Component {
static components = { Child };
static template = xml`
<t t-foreach="props.list" t-as="elem" t-key="elem_index">
<p><t t-esc="elem"/><t t-slot="default"/></p>
</t>
`;
}
class Parent extends Component {
static components = { Child, Slotter };
static template = xml`
<Slotter list="state.list">
hello <Child/>
</Slotter>`;
state = useState({ list: [1] });
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("<p>1 hello <div>child</div></p>");
parent.state.list.push(2);
await nextTick();
expect(fixture.innerHTML).toBe(
"<p>1 hello <div>child</div></p><p>2 hello <div>child</div></p>"
);
});
});
+7 -8
View File
@@ -1,6 +1,6 @@
import { OwlError } from "../../src/runtime/error_handling";
import { Component, mount, onMounted, useState, xml } from "../../src";
import { makeTestFixture, nextTick, snapshotEverything } from "../helpers";
import { App, Component, mount, onMounted, useState, xml } from "../../src";
import { makeTestFixture, nextAppError, nextTick, snapshotEverything } from "../helpers";
snapshotEverything();
let fixture: HTMLElement;
@@ -343,16 +343,15 @@ describe("style and class handling", () => {
class Child extends Component {
static template = xml`<div t-att-class="props.class" t-esc="this.will.crash"/>`;
}
class ParentWidget extends Component {
class Parent extends Component {
static template = xml`<Child class="'a'"/>`;
static components = { Child };
}
let error: OwlError;
try {
await mount(ParentWidget, fixture);
} catch (e) {
error = e as OwlError;
}
const app = new App(Parent);
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("error occured in the owl lifecycle");
await mountProm;
expect(error!).toBeDefined();
expect(error!.cause).toBeDefined();
const regexp =
+15 -5
View File
@@ -1,5 +1,11 @@
import { Component, mount, onMounted, useState, xml } from "../../src/index";
import { makeTestFixture, nextTick, snapshotEverything, useLogLifecycle } from "../helpers";
import { App, Component, mount, onMounted, useState, xml } from "../../src/index";
import {
makeTestFixture,
nextAppError,
nextTick,
snapshotEverything,
useLogLifecycle,
} from "../helpers";
snapshotEverything();
@@ -315,9 +321,13 @@ describe("list of components", () => {
`;
static components = { Child };
}
await expect(async () => {
await mount(Parent, fixture, { dev: true });
}).rejects.toThrowError("Got duplicate key in t-foreach: child");
const app = new App(Parent, { test: true });
const mountProm = expect(app.mount(fixture)).rejects.toThrow(
"Got duplicate key in t-foreach: child"
);
await expect(nextAppError(app)).resolves.toThrow("Got duplicate key in t-foreach: child");
await mountProm;
console.info = consoleInfo;
expect(mockConsoleWarn).toBeCalledTimes(1);
});
+14
View File
@@ -261,6 +261,20 @@ expect.extend({
},
});
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> {
+13 -16
View File
@@ -12,7 +12,7 @@ import {
} from "../../src";
import { xml } from "../../src/";
import { DEV_MSG } from "../../src/runtime/app";
import { elem, makeTestFixture, nextTick, snapshotEverything } from "../helpers";
import { elem, makeTestFixture, nextAppError, nextTick, snapshotEverything } from "../helpers";
let fixture: HTMLElement;
let originalconsoleWarn = console.warn;
@@ -269,11 +269,10 @@ describe("Portal", () => {
}
let error: Error;
try {
await mount(Parent, fixture);
} catch (e) {
error = e as Error;
}
const app = new App(Parent);
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("invalid portal target");
await mountProm;
expect(error!).toBeDefined();
expect(error!.message).toBe("invalid portal target");
@@ -960,11 +959,10 @@ describe("Portal: Props validation", () => {
</div>`;
}
let error: OwlError;
try {
await mount(Parent, fixture, { dev: true });
} catch (e) {
error = e as OwlError;
}
const app = new App(Parent);
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("error occured in the owl lifecycle");
await mountProm;
expect(error!).toBeDefined();
expect(error!.cause).toBeDefined();
expect(error!.cause.message).toBe(`' ' is not a valid selector`);
@@ -980,11 +978,10 @@ describe("Portal: Props validation", () => {
</div>`;
}
let error: Error;
try {
await mount(Parent, fixture, { dev: true });
} catch (e) {
error = e as Error;
}
const app = new App(Parent);
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("invalid portal target");
await mountProm;
expect(error!).toBeDefined();
expect(error!.message).toBe(`invalid portal target`);
});