Compare commits

...

9 Commits

Author SHA1 Message Date
Samuel Degueldre 94e978815f sync rendering 2023-09-29 14:07:44 +02:00
Samuel Degueldre 9c40ee67e1 lifecycle-snapshots 2023-09-29 13:33:56 +02:00
Géry Debongnie b1690f19cc [IMP] tools: add extra information in release script 2023-09-25 13:58:01 +02:00
Géry Debongnie 5dcee2564c [REL] v2.2.6
# v2.2.6

 - [IMP] devtools: add svg elements detection
 - [FIX] reactivity: do not notify for NOOPs on collections
 - [IMP] app: export apps set as static property
 - [IMP] runtime: do not check template equality outside dev mode
 - [FIX] runtime: properly support t-foreach on strings
2023-09-25 13:48:13 +02:00
Julien Carion (juca) 752160fd85 [IMP] devtools: add svg elements detection
This commit extends the detection of component related dom elements to
svg elements in the page so that they can be searched and highlighted
correctly with the devtools.
2023-09-25 11:43:39 +02:00
Samuel Degueldre 3937966b74 [FIX] reactivity: do not notify for NOOPs on collections
Previously, if a reactive was observing the presence of an item in a
set that was originally not present, it would get notified when that
item was "deleted" from the set (even though there was nothing to delete
and the set did not change). The same applied to the key already being
present and then being "added". The same thing occured with Map, both
for presence but also for values (setting a key to the value it was
already set to would notify).

This commit fixes that.
2023-09-22 14:36:35 +02:00
Géry Debongnie e7ebb92104 [IMP] app: export apps set as static property
Before this commit, Owl would export the list of apps in a global object
`__OWL_DEVTOOLS__`.  However, it is sometimes useful to be able to
access that set, even outside of the devtools (for example, to register
templates in all active apps).

closes #1515
2023-09-04 15:01:48 +02:00
Samuel Degueldre c78e070636 [IMP] runtime: do not check template equality outside dev mode
When defining a template with a name that the template set already
contains, we currently always check whether the template is the same and
throw an error when it's not. This is potentially expensive as it can
involve serializing a pretty large XML document. This check is only
supposed to help during development so this commit disables this check
outside dev mode.
2023-08-25 09:24:18 +02:00
Samuel Degueldre 610ed02373 [FIX] runtime: properly support t-foreach on strings
Previously, support for iterables was added to t-foreach. The idea was
that anything that you can spread or on which you can use for..of would
be supported. Due to an implementation mistakes, strings, which are
iterable were not supported because we checked that the typeof the
iterable was 'object'.

To fix this, we coerce the iterable to an object and check whether that
coerced value has a Symbol.iterator property, which is what happens
behind the scenes when using for..of or spreading a primitive.

Closes: odoo/owl#1503
2023-08-25 09:24:04 +02:00
38 changed files with 3616 additions and 2650 deletions
+19 -21
View File
@@ -2177,7 +2177,7 @@ function delegateAndNotify(setterName, getterName, target) {
if (hadKey !== hasKey) {
notifyReactives(target, KEYCHANGES);
}
if (originalValue !== value) {
if (originalValue !== target[getterName](key)) {
notifyReactives(target, key);
}
return ret;
@@ -2998,15 +2998,13 @@ function prepareList(collection) {
keys = [...collection.keys()];
values = [...collection.values()];
}
else if (Symbol.iterator in Object(collection)) {
keys = [...collection];
values = keys;
}
else if (collection && typeof collection === "object") {
if (Symbol.iterator in collection) {
keys = [...collection];
values = keys;
}
else {
values = Object.values(collection);
keys = Object.keys(collection);
}
values = Object.values(collection);
keys = Object.keys(collection);
}
else {
throw new OwlError(`Invalid loop expression: "${collection}" is not iterable`);
@@ -3207,6 +3205,10 @@ class TemplateSet {
}
addTemplate(name, template) {
if (name in this.rawTemplates) {
// this check can be expensive, just silently ignore double definitions outside dev mode
if (!this.dev) {
return;
}
const rawTemplate = this.rawTemplates[name];
const currentAsString = typeof rawTemplate === "string"
? rawTemplate
@@ -5553,7 +5555,7 @@ function compile(template, options = {}) {
}
// do not modify manually. This file is generated by the release script.
const version = "2.2.5";
const version = "2.2.6";
// -----------------------------------------------------------------------------
// Scheduler
@@ -5642,13 +5644,8 @@ const DEV_MSG = () => {
This is not suitable for production use.
See https://github.com/odoo/owl/blob/${hash}/doc/reference/app.md#configuration for more information.`;
};
window.__OWL_DEVTOOLS__ || (window.__OWL_DEVTOOLS__ = {
apps: new Set(),
Fiber: Fiber,
RootFiber: RootFiber,
toRaw: toRaw,
reactive: reactive,
});
const apps = new Set();
window.__OWL_DEVTOOLS__ || (window.__OWL_DEVTOOLS__ = { apps, Fiber, RootFiber, toRaw, reactive });
class App extends TemplateSet {
constructor(Root, config = {}) {
super(config);
@@ -5656,7 +5653,7 @@ class App extends TemplateSet {
this.root = null;
this.name = config.name || "";
this.Root = Root;
window.__OWL_DEVTOOLS__.apps.add(this);
apps.add(this);
if (config.test) {
this.dev = true;
}
@@ -5713,7 +5710,7 @@ class App extends TemplateSet {
this.root.destroy();
this.scheduler.processTasks();
}
window.__OWL_DEVTOOLS__.apps.delete(this);
apps.delete(this);
}
createComponent(name, isStatic, hasSlotsProp, hasDynamicPropList, propList) {
const isDynamic = !isStatic;
@@ -5788,6 +5785,7 @@ class App extends TemplateSet {
}
}
App.validateTarget = validateTarget;
App.apps = apps;
App.version = version;
async function mount(C, target, config = {}) {
return new App(C, config).mount(target, config);
@@ -5986,6 +5984,6 @@ TemplateSet.prototype._compileTemplate = function _compileTemplate(name, templat
export { App, Component, EventBus, OwlError, __info__, blockDom, loadFile, markRaw, markup, mount, onError, onMounted, onPatched, onRendered, onWillDestroy, onWillPatch, onWillRender, onWillStart, onWillUnmount, onWillUpdateProps, reactive, status, toRaw, useChildSubEnv, useComponent, useEffect, useEnv, useExternalListener, useRef, useState, useSubEnv, validate, validateType, whenReady, xml };
__info__.date = '2023-08-07T10:26:30.557Z';
__info__.hash = 'b25e988';
__info__.date = '2023-09-25T11:48:01.531Z';
__info__.hash = '752160f';
__info__.url = 'https://github.com/odoo/owl';
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@odoo/owl",
"version": "2.2.5",
"version": "2.2.6",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@odoo/owl",
"version": "2.2.5",
"version": "2.2.6",
"description": "Odoo Web Library (OWL)",
"main": "dist/owl.cjs.js",
"module": "dist/owl.es.js",
+6 -9
View File
@@ -35,6 +35,8 @@ This is not suitable for production use.
See https://github.com/odoo/owl/blob/${hash}/doc/reference/app.md#configuration for more information.`;
};
const apps = new Set<App>();
declare global {
interface Window {
__OWL_DEVTOOLS__: {
@@ -47,13 +49,7 @@ declare global {
}
}
window.__OWL_DEVTOOLS__ ||= {
apps: new Set<App>(),
Fiber: Fiber,
RootFiber: RootFiber,
toRaw: toRaw,
reactive: reactive,
};
window.__OWL_DEVTOOLS__ ||= { apps, Fiber, RootFiber, toRaw, reactive };
export class App<
T extends abstract new (...args: any) => any = any,
@@ -61,6 +57,7 @@ export class App<
E = any
> extends TemplateSet {
static validateTarget = validateTarget;
static apps = apps;
static version = version;
name: string;
@@ -75,7 +72,7 @@ export class App<
super(config);
this.name = config.name || "";
this.Root = Root;
window.__OWL_DEVTOOLS__.apps.add(this);
apps.add(this);
if (config.test) {
this.dev = true;
}
@@ -140,7 +137,7 @@ export class App<
this.root.destroy();
this.scheduler.processTasks();
}
window.__OWL_DEVTOOLS__.apps.delete(this);
apps.delete(this);
}
createComponent<P extends Props>(
+43 -26
View File
@@ -6,7 +6,7 @@ import { OwlError } from "../common/owl_error";
import { Fiber, makeChildFiber, makeRootFiber, MountFiber, MountOptions } from "./fibers";
import { clearReactivesForCallback, getSubscriptions, reactive, targets } from "./reactivity";
import { STATUS } from "./status";
import { batched, Callback } from "./utils";
import { batched, Callback, possiblySync } from "./utils";
let currentNode: ComponentNode | null = null;
@@ -128,21 +128,29 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
this.initiateRender(fiber);
}
async initiateRender(fiber: Fiber | MountFiber) {
initiateRender(fiber: Fiber | MountFiber) {
this.fiber = fiber;
if (this.mounted.length) {
fiber.root!.mounted.push(fiber);
}
const component = this.component;
try {
await Promise.all(this.willStart.map((f) => f.call(component)));
} catch (e) {
this.app.handleError({ node: this, error: e });
return;
}
if (this.status === STATUS.NEW && this.fiber === fiber) {
fiber.render();
}
return possiblySync(
() => {
const willStartResults = this.willStart.map((f) => f.call(component));
if (willStartResults.some((r) => typeof r?.then === "function")) {
return Promise.all(willStartResults);
}
return;
},
() => {
if (this.status === STATUS.NEW && this.fiber === fiber) {
fiber.render();
}
},
(e: any) => {
this.app.handleError({ node: this, error: e });
}
);
}
async render(deep: boolean) {
@@ -238,7 +246,7 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
this.status = STATUS.DESTROYED;
}
async updateAndRender(props: P, parentFiber: Fiber) {
updateAndRender(props: P, parentFiber: Fiber) {
this.nextProps = props;
props = Object.assign({}, props);
// update
@@ -258,20 +266,29 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
}
}
currentNode = null;
const prom = Promise.all(this.willUpdateProps.map((f) => f.call(component, props)));
await prom;
if (fiber !== this.fiber) {
return;
}
component.props = props;
fiber.render();
const parentRoot = parentFiber.root!;
if (this.willPatch.length) {
parentRoot.willPatch.push(fiber);
}
if (this.patched.length) {
parentRoot.patched.push(fiber);
}
return possiblySync(
() => {
const willUpdateProps = this.willUpdateProps.map((f) => f.call(component, props));
if (willUpdateProps.some((p) => typeof p?.then === "function")) {
return Promise.all(willUpdateProps);
}
return;
},
() => {
if (fiber !== this.fiber) {
return;
}
component.props = props;
fiber.render();
const parentRoot = parentFiber.root!;
if (this.willPatch.length) {
parentRoot.willPatch.push(fiber);
}
if (this.patched.length) {
parentRoot.patched.push(fiber);
}
}
);
}
/**
+1 -1
View File
@@ -368,7 +368,7 @@ function delegateAndNotify(
if (hadKey !== hasKey) {
notifyReactives(target, KEYCHANGES);
}
if (originalValue !== value) {
if (originalValue !== target[getterName](key)) {
notifyReactives(target, key);
}
return ret;
+5 -7
View File
@@ -70,14 +70,12 @@ function prepareList(collection: unknown): [unknown[], unknown[], number, undefi
} else if (collection instanceof Map) {
keys = [...collection.keys()];
values = [...collection.values()];
} else if (Symbol.iterator in Object(collection)) {
keys = [...(<Iterable<unknown>>collection)];
values = keys;
} else if (collection && typeof collection === "object") {
if (Symbol.iterator in collection) {
keys = [...(<Iterable<unknown>>collection)];
values = keys;
} else {
values = Object.values(collection);
keys = Object.keys(collection);
}
values = Object.values(collection);
keys = Object.keys(collection);
} else {
throw new OwlError(`Invalid loop expression: "${collection}" is not iterable`);
}
+4
View File
@@ -66,6 +66,10 @@ export class TemplateSet {
addTemplate(name: string, template: string | Element) {
if (name in this.rawTemplates) {
// this check can be expensive, just silently ignore double definitions outside dev mode
if (!this.dev) {
return;
}
const rawTemplate = this.rawTemplates[name];
const currentAsString =
typeof rawTemplate === "string"
+21
View File
@@ -88,3 +88,24 @@ export class Markup extends String {}
export function markup(value: any) {
return new Markup(value);
}
export function possiblySync(computation: Function, onSuccess: Function, onError?: Function) {
try {
let result;
if (onError) {
try {
result = computation();
} catch (e) {
return onError(e);
}
} else {
result = computation();
}
if (typeof result?.then === "function") {
return result.then(onSuccess, onError);
}
return onSuccess(result);
} catch (e) {
return Promise.reject(e);
}
}
+1 -1
View File
@@ -1,2 +1,2 @@
// do not modify manually. This file is generated by the release script.
export const version = "2.2.5";
export const version = "2.2.6";
+1 -1
View File
@@ -32,7 +32,7 @@ exports[`app app: clear scheduler tasks and destroy cancelled nodes immediately
}"
`;
exports[`app app: clear scheduler tasks and destroy cancelled nodes immediately on destroy 2`] = `
exports[`app app: clear scheduler tasks and destroy cancelled nodes immediately on destroy 3`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
+34 -9
View File
@@ -8,6 +8,7 @@ import {
useLogLifecycle,
makeDeferred,
nextMicroTick,
steps,
} from "../helpers";
let fixture: HTMLElement;
@@ -123,23 +124,47 @@ describe("app", () => {
const app = new App(A);
const comp = await app.mount(fixture);
expect(["A:setup", "A:willStart", "A:willRender", "A:rendered", "A:mounted"]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"A:setup",
"A:willStart",
"A:willRender",
"A:rendered",
"A:mounted",
]
`);
comp.state.value = true;
await nextTick();
expect(["A:willRender", "B:setup", "B:willStart", "A:rendered"]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"A:willRender",
"B:setup",
"B:willStart",
"A:rendered",
]
`);
// rerender to force the instantiation of a new B component (and cancelling the first)
comp.render();
await nextMicroTick();
expect(["A:willRender", "B:setup", "B:willStart", "A:rendered"]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"A:willRender",
"B:setup",
"B:willStart",
"A:rendered",
]
`);
app.destroy();
expect([
"A:willUnmount",
"B:willDestroy",
"A:willDestroy",
"B:willDestroy", // make sure the 2 B instances have been destroyed synchronously
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"A:willUnmount",
"B:willDestroy",
"A:willDestroy",
"B:willDestroy",
]
`);
});
});
@@ -256,6 +256,34 @@ exports[`t-foreach iterate, position 1`] = `
}"
`;
exports[`t-foreach iterate, string param 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let { prepareList, withKey } = helpers;
return function template(ctx, node, key = \\"\\") {
ctx = Object.create(ctx);
const [k_block1, v_block1, l_block1, c_block1] = prepareList('abc');;
for (let i1 = 0; i1 < l_block1; i1++) {
ctx[\`item\`] = k_block1[i1];
ctx[\`item_index\`] = i1;
ctx[\`item_value\`] = v_block1[i1];
const key1 = ctx['item_index'];
const b3 = text(\` [\`);
const b4 = text(ctx['item_index']);
const b5 = text(\`: \`);
const b6 = text(ctx['item']);
const b7 = text(\` \`);
const b8 = text(ctx['item_value']);
const b9 = text(\`] \`);
c_block1[i1] = withKey(multi([b3, b4, b5, b6, b7, b8, b9]), key1);
}
return list(c_block1);
}
}"
`;
exports[`t-foreach simple iteration (in a node) 1`] = `
"function anonymous(app, bdom, helpers
) {
+9
View File
@@ -131,6 +131,15 @@ describe("t-foreach", () => {
expect(renderToString(template, context)).toBe(expected);
});
test("iterate, string param", () => {
const template = `
<t t-foreach="'abc'" t-as="item" t-key="item_index">
[<t t-esc="item_index"/>: <t t-esc="item"/> <t t-esc="item_value"/>]
</t>`;
const expected = ` [0: a a] [1: b b] [2: c c] `;
expect(renderToString(template)).toBe(expected);
});
test("iterate, iterable param", () => {
const template = `
<t t-foreach="map.values()" t-as="item" t-key="item_index">
+9 -2
View File
@@ -11,8 +11,8 @@ describe("basic validation", () => {
expect(() => context.getTemplate("invalidname")).toThrow("Missing template");
});
test("cannot add a different template with the same name", () => {
const context = new TemplateSet();
test("cannot add a different template with the same name in dev mode", () => {
const context = new TemplateSet({ dev: true });
context.addTemplate("test", `<t/>`);
// Same template with the same name is fine
expect(() => context.addTemplate("test", "<t/>")).not.toThrow();
@@ -20,6 +20,13 @@ describe("basic validation", () => {
expect(() => context.addTemplate("test", "<div/>")).toThrow("already defined");
});
test("adding different template with same name outside dev mode silently ignores it", () => {
const context = new TemplateSet({ dev: false });
context.addTemplate("test", `<t/>`);
expect(() => context.addTemplate("test", "<div/>")).not.toThrow();
expect(context.rawTemplates.test).toBe("<t/>");
});
test("invalid xml", () => {
const template = "<div>";
expect(() => snapshotTemplate(template)).toThrow("Invalid XML in template");
@@ -184,7 +184,7 @@ exports[`changing state before first render does not trigger a render (with pare
}"
`;
exports[`changing state before first render does not trigger a render (with parent) 2`] = `
exports[`changing state before first render does not trigger a render (with parent) 3`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
@@ -254,7 +254,7 @@ exports[`components are not destroyed between animation frame 1`] = `
}"
`;
exports[`components are not destroyed between animation frame 2`] = `
exports[`components are not destroyed between animation frame 3`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
@@ -268,7 +268,7 @@ exports[`components are not destroyed between animation frame 2`] = `
}"
`;
exports[`components are not destroyed between animation frame 3`] = `
exports[`components are not destroyed between animation frame 5`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
@@ -748,7 +748,7 @@ exports[`concurrent renderings scenario 10 2`] = `
}"
`;
exports[`concurrent renderings scenario 10 3`] = `
exports[`concurrent renderings scenario 10 4`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
@@ -993,7 +993,7 @@ exports[`concurrent renderings scenario 16 3`] = `
}"
`;
exports[`concurrent renderings scenario 16 4`] = `
exports[`concurrent renderings scenario 16 6`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
@@ -1024,7 +1024,7 @@ exports[`creating two async components, scenario 1 1`] = `
}"
`;
exports[`creating two async components, scenario 1 2`] = `
exports[`creating two async components, scenario 1 3`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
@@ -1038,7 +1038,7 @@ exports[`creating two async components, scenario 1 2`] = `
}"
`;
exports[`creating two async components, scenario 1 3`] = `
exports[`creating two async components, scenario 1 5`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
@@ -1085,7 +1085,7 @@ exports[`creating two async components, scenario 2 2`] = `
}"
`;
exports[`creating two async components, scenario 2 3`] = `
exports[`creating two async components, scenario 2 5`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
@@ -1133,7 +1133,7 @@ exports[`creating two async components, scenario 3 (patching in the same frame)
}"
`;
exports[`creating two async components, scenario 3 (patching in the same frame) 3`] = `
exports[`creating two async components, scenario 3 (patching in the same frame) 5`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
@@ -1308,7 +1308,7 @@ exports[`delayed render does not go through when t-component value changed 2`] =
}"
`;
exports[`delayed render does not go through when t-component value changed 3`] = `
exports[`delayed render does not go through when t-component value changed 4`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
@@ -1617,7 +1617,7 @@ exports[`destroyed component causes other soon to be destroyed component to rere
}"
`;
exports[`destroyed component causes other soon to be destroyed component to rerender, weird stuff happens 2`] = `
exports[`destroyed component causes other soon to be destroyed component to rerender, weird stuff happens 3`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
@@ -1628,7 +1628,7 @@ exports[`destroyed component causes other soon to be destroyed component to rere
}"
`;
exports[`destroyed component causes other soon to be destroyed component to rerender, weird stuff happens 3`] = `
exports[`destroyed component causes other soon to be destroyed component to rerender, weird stuff happens 4`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
@@ -1656,7 +1656,7 @@ exports[`destroying/recreating a subcomponent, other scenario 1`] = `
}"
`;
exports[`destroying/recreating a subcomponent, other scenario 2`] = `
exports[`destroying/recreating a subcomponent, other scenario 3`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
@@ -1685,7 +1685,7 @@ exports[`destroying/recreating a subwidget with different props (if start is not
}"
`;
exports[`destroying/recreating a subwidget with different props (if start is not over) 2`] = `
exports[`destroying/recreating a subwidget with different props (if start is not over) 3`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
@@ -1787,7 +1787,7 @@ exports[`rendering component again in next microtick 1`] = `
}"
`;
exports[`rendering component again in next microtick 2`] = `
exports[`rendering component again in next microtick 3`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
@@ -1861,10 +1861,10 @@ exports[`renderings, destruction, patch, stuff, ... yet another variation 3`] =
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block3 = createBlock(\`<p block-handler-0=\\"click\\"><block-text-1/></p>\`);
let block3 = createBlock(\`<span block-handler-0=\\"click\\"><block-text-1/></span>\`);
return function template(ctx, node, key = \\"\\") {
const b2 = text(\`D\`);
const b2 = text(\`C\`);
let hdlr1 = [ctx['increment'], ctx];
let txt1 = ctx['state'].val;
const b3 = block3([hdlr1, txt1]);
@@ -1878,10 +1878,10 @@ exports[`renderings, destruction, patch, stuff, ... yet another variation 4`] =
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block3 = createBlock(\`<span block-handler-0=\\"click\\"><block-text-1/></span>\`);
let block3 = createBlock(\`<p block-handler-0=\\"click\\"><block-text-1/></p>\`);
return function template(ctx, node, key = \\"\\") {
const b2 = text(\`C\`);
const b2 = text(\`D\`);
let hdlr1 = [ctx['increment'], ctx];
let txt1 = ctx['state'].val;
const b3 = block3([hdlr1, txt1]);
@@ -241,7 +241,7 @@ exports[`can catch errors an error in onWillDestroy, variation 1`] = `
}"
`;
exports[`can catch errors an error in onWillDestroy, variation 2`] = `
exports[`can catch errors an error in onWillDestroy, variation 3`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
@@ -92,7 +92,7 @@ exports[`lifecycle hooks component semantics 5`] = `
}"
`;
exports[`lifecycle hooks component semantics 6`] = `
exports[`lifecycle hooks component semantics 7`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
@@ -185,7 +185,7 @@ exports[`lifecycle hooks destroy new children before being mountged 1`] = `
}"
`;
exports[`lifecycle hooks destroy new children before being mountged 2`] = `
exports[`lifecycle hooks destroy new children before being mountged 3`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
@@ -291,7 +291,7 @@ exports[`lifecycle hooks lifecycle semantics, part 2 1`] = `
}"
`;
exports[`lifecycle hooks lifecycle semantics, part 2 2`] = `
exports[`lifecycle hooks lifecycle semantics, part 2 3`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
@@ -303,7 +303,7 @@ exports[`lifecycle hooks lifecycle semantics, part 2 2`] = `
}"
`;
exports[`lifecycle hooks lifecycle semantics, part 2 3`] = `
exports[`lifecycle hooks lifecycle semantics, part 2 4`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
@@ -348,7 +348,7 @@ exports[`lifecycle hooks lifecycle semantics, part 4 1`] = `
}"
`;
exports[`lifecycle hooks lifecycle semantics, part 4 2`] = `
exports[`lifecycle hooks lifecycle semantics, part 4 3`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
@@ -360,7 +360,7 @@ exports[`lifecycle hooks lifecycle semantics, part 4 2`] = `
}"
`;
exports[`lifecycle hooks lifecycle semantics, part 4 3`] = `
exports[`lifecycle hooks lifecycle semantics, part 4 4`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
@@ -180,7 +180,7 @@ exports[`t-component switching dynamic component 2`] = `
}"
`;
exports[`t-component switching dynamic component 3`] = `
exports[`t-component switching dynamic component 4`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
+51 -32
View File
@@ -5,6 +5,7 @@ import {
nextAppError,
nextTick,
snapshotEverything,
steps,
useLogLifecycle,
} from "../helpers";
import { markup } from "../../src/runtime/utils";
@@ -868,19 +869,26 @@ describe("basics", () => {
const parent = await mount(Parent, fixture);
expect(Object.keys(parent.__owl__.children).length).toStrictEqual(1);
expect([
"Child:setup",
"Child:willStart",
"Child:willRender",
"Child:rendered",
"Child:mounted",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Child:setup",
"Child:willStart",
"Child:willRender",
"Child:rendered",
"Child:mounted",
]
`);
parent.ifVar = false;
parent.render();
await nextTick();
expect(Object.keys(parent.__owl__.children).length).toStrictEqual(0);
expect(["Child:willUnmount", "Child:willDestroy"]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Child:willUnmount",
"Child:willDestroy",
]
`);
});
test("component children doesn't leak (t-key case)", async () => {
@@ -899,27 +907,31 @@ describe("basics", () => {
const parent = await mount(Parent, fixture);
expect(Object.keys(parent.__owl__.children).length).toStrictEqual(1);
expect([
"Child:setup",
"Child:willStart",
"Child:willRender",
"Child:rendered",
"Child:mounted",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Child:setup",
"Child:willStart",
"Child:willRender",
"Child:rendered",
"Child:mounted",
]
`);
parent.keyVar = 2;
parent.render();
await nextTick();
expect(Object.keys(parent.__owl__.children).length).toStrictEqual(1);
expect([
"Child:setup",
"Child:willStart",
"Child:willRender",
"Child:rendered",
"Child:willUnmount",
"Child:willDestroy",
"Child:mounted",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Child:setup",
"Child:willStart",
"Child:willRender",
"Child:rendered",
"Child:willUnmount",
"Child:willDestroy",
"Child:mounted",
]
`);
});
test("GrandChild display is controlled by its GrandParent", async () => {
@@ -943,20 +955,27 @@ describe("basics", () => {
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("<div></div>");
expect([
"GrandChild:setup",
"GrandChild:willStart",
"GrandChild:willRender",
"GrandChild:rendered",
"GrandChild:mounted",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"GrandChild:setup",
"GrandChild:willStart",
"GrandChild:willRender",
"GrandChild:rendered",
"GrandChild:mounted",
]
`);
parent.displayGrandChild = false;
parent.render();
await nextTick();
expect(fixture.innerHTML).toBe("");
expect(["GrandChild:willUnmount", "GrandChild:willDestroy"]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"GrandChild:willUnmount",
"GrandChild:willDestroy",
]
`);
});
});
File diff suppressed because it is too large Load Diff
+196 -153
View File
@@ -19,6 +19,7 @@ import {
snapshotEverything,
useLogLifecycle,
nextAppError,
steps,
} from "../helpers";
import { OwlError } from "../../src/common/owl_error";
@@ -75,8 +76,9 @@ describe("basics", () => {
}
const app = new App(Parent);
let error: Error;
const appError = nextAppError(app);
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow(
await expect(appError).resolves.toThrow(
'Cannot find the definition of component "SomeMispelledComponent"'
);
await mountProm;
@@ -95,8 +97,9 @@ describe("basics", () => {
}
const app = new App(Parent, { test: true });
let error: Error;
const appError = nextAppError(app);
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow(
await expect(appError).resolves.toThrow(
'Cannot find the definition of component "SomeMispelledComponent"'
);
await mountProm;
@@ -115,8 +118,9 @@ describe("basics", () => {
}
const app = new App(Parent as typeof Component);
let error: Error;
const appError = nextAppError(app);
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow(
await expect(appError).resolves.toThrow(
'"SomeComponent" is not a Component. It must inherit from the Component class'
);
await mountProm;
@@ -132,8 +136,9 @@ describe("basics", () => {
}
const app = new App(Parent as typeof Component);
let error: Error;
const appError = nextAppError(app);
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow(
await expect(appError).resolves.toThrow(
'Cannot find the definition of component "MissingChild", missing static components key in parent'
);
await mountProm;
@@ -196,8 +201,9 @@ function(app, bdom, helpers) {
}`;
const app = new App(Parent as typeof Component);
let error: Error;
const appError = nextAppError(app);
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow(expectedErrorMessage);
await expect(appError).resolves.toThrow(expectedErrorMessage);
await mountProm;
expect(error!).toBeDefined();
expect(error!.message).toBe(expectedErrorMessage);
@@ -243,8 +249,9 @@ describe("errors and promises", () => {
const app = new App(Root);
let error: OwlError;
const appError = nextAppError(app);
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("error occured in the owl lifecycle");
await expect(appError).resolves.toThrow("error occured in the owl lifecycle");
await mountProm;
expect(error!).toBeDefined();
expect(error!.cause).toBeDefined();
@@ -267,8 +274,9 @@ describe("errors and promises", () => {
const app = new App(Root);
let error: OwlError;
const appError = nextAppError(app);
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("error occured in the owl lifecycle");
await expect(appError).resolves.toThrow("error occured in the owl lifecycle");
await mountProm;
expect(error!).toBeDefined();
expect(error!.cause).toBeDefined();
@@ -290,8 +298,9 @@ describe("errors and promises", () => {
const app = new App(Root, { test: true });
let error: OwlError;
const appError = nextAppError(app);
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("error occurred in onMounted");
await expect(appError).resolves.toThrow("error occurred in onMounted");
await mountProm;
expect(error!).toBeDefined();
expect(error!.stack).toContain("Root.setup");
@@ -316,8 +325,9 @@ describe("errors and promises", () => {
const app = new App(Root, { test: true });
let error: OwlError;
const appError = nextAppError(app);
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("error occurred in onWillRender");
await expect(appError).resolves.toThrow("error occurred in onWillRender");
await mountProm;
expect(error!).toBeDefined();
expect(error!.message).toBe(
@@ -357,8 +367,9 @@ describe("errors and promises", () => {
const app = new App(Root, { test: true });
let error: OwlError;
const appError = nextAppError(app);
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("error occurred in onWillStart");
await expect(appError).resolves.toThrow("error occurred in onWillStart");
await mountProm;
expect(error!).toBeDefined();
expect(error!.message).toBe(
@@ -425,8 +436,9 @@ describe("errors and promises", () => {
const app = new App(Parent);
let error: OwlError;
const appError = nextAppError(app);
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("error occured in the owl lifecycle");
await expect(appError).resolves.toThrow("error occured in the owl lifecycle");
await mountProm;
expect(error!).toBeDefined();
expect(error!.cause).toBeDefined();
@@ -471,8 +483,9 @@ describe("errors and promises", () => {
const app = new App(Parent);
let error: OwlError;
const appError = nextAppError(app);
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("error occured in the owl lifecycle");
await expect(appError).resolves.toThrow("error occured in the owl lifecycle");
await mountProm;
expect(error!).toBeDefined();
expect(error!.cause).toBeDefined();
@@ -501,8 +514,9 @@ describe("errors and promises", () => {
const app = new App(Example, { test: true });
let error: OwlError;
const appError = nextAppError(app);
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("error occurred in onMounted");
await expect(appError).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
@@ -576,9 +590,9 @@ describe("can catch errors", () => {
}
const app = new App(Root, { test: true });
let error: OwlError;
const crashProm = expect(nextAppError(app)).resolves.toThrow("error occurred in onWillStart");
const appError = nextAppError(app);
await app.mount(fixture).catch((e: Error) => (error = e));
await crashProm;
await expect(appError).resolves.toThrow("error occurred in onWillStart");
expect(error!.message).toBe(
`The following error occurred in onWillStart: "No active component (a hook function should only be called in 'setup')"`
);
@@ -598,8 +612,9 @@ describe("can catch errors", () => {
}
const app = new App(Root, { test: true });
let error: OwlError;
const appError = nextAppError(app);
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("error occurred in onMounted");
await expect(appError).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);
@@ -620,8 +635,9 @@ describe("can catch errors", () => {
}
const app = new App(Root, { test: true });
let error: OwlError;
const appError = nextAppError(app);
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("error occurred in onWillStart");
await expect(appError).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);
@@ -641,8 +657,9 @@ describe("can catch errors", () => {
}
const app = new App(Root);
let error: OwlError;
const appError = nextAppError(app);
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("error occured in the owl lifecycle");
await expect(appError).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)`
@@ -665,8 +682,9 @@ describe("can catch errors", () => {
}
const app = new App(Root);
let error: OwlError;
const appError = nextAppError(app);
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("error occured in the owl lifecycle");
await expect(appError).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)`
@@ -687,8 +705,9 @@ describe("can catch errors", () => {
}
const app = new App(Root, { test: true });
let error: OwlError;
const appError = nextAppError(app);
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("not an Error was thrown in onMounted");
await expect(appError).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)`
@@ -709,8 +728,9 @@ describe("can catch errors", () => {
}
const app = new App(Root);
let error: OwlError;
const appError = nextAppError(app);
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("error occured in the owl lifecycle");
await expect(appError).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)`
@@ -948,26 +968,28 @@ describe("can catch errors", () => {
}
await mount(Root, fixture);
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
expect([
"Root:setup",
"Root:willStart",
"Root:willRender",
"ErrorBoundary:setup",
"ErrorBoundary:willStart",
"Root:rendered",
"ErrorBoundary:willRender",
"ErrorComponent:setup",
"ErrorComponent:willStart",
"ErrorBoundary:rendered",
"ErrorComponent:willRender",
"ErrorComponent:rendered",
"ErrorComponent:mounted",
"boom",
"ErrorBoundary:willRender",
"ErrorBoundary:rendered",
"ErrorBoundary:mounted",
"Root:mounted",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Root:setup",
"Root:willStart",
"Root:willRender",
"ErrorBoundary:setup",
"ErrorBoundary:willStart",
"ErrorBoundary:willRender",
"ErrorComponent:setup",
"ErrorComponent:willStart",
"ErrorComponent:willRender",
"ErrorComponent:rendered",
"ErrorBoundary:rendered",
"Root:rendered",
"ErrorComponent:mounted",
"boom",
"ErrorBoundary:willRender",
"ErrorBoundary:rendered",
"ErrorBoundary:mounted",
"Root:mounted",
]
`);
expect(mockConsoleError).toBeCalledTimes(0);
expect(mockConsoleWarn).toBeCalledTimes(0);
});
@@ -998,21 +1020,23 @@ describe("can catch errors", () => {
}
await mount(Root, fixture);
expect(fixture.innerHTML).toBe("<div>Error handled</div>");
expect([
"Root:setup",
"Root:willStart",
"Root:willRender",
"ErrorComponent:setup",
"ErrorComponent:willStart",
"Root:rendered",
"ErrorComponent:willRender",
"ErrorComponent:rendered",
"ErrorComponent:mounted",
"boom",
"Root:willRender",
"Root:rendered",
"Root:mounted",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Root:setup",
"Root:willStart",
"Root:willRender",
"ErrorComponent:setup",
"ErrorComponent:willStart",
"ErrorComponent:willRender",
"ErrorComponent:rendered",
"Root:rendered",
"ErrorComponent:mounted",
"boom",
"Root:willRender",
"Root:rendered",
"Root:mounted",
]
`);
expect(mockConsoleError).toBeCalledTimes(0);
expect(mockConsoleWarn).toBeCalledTimes(0);
});
@@ -1059,31 +1083,33 @@ describe("can catch errors", () => {
}
await mount(A, fixture);
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
expect([
"A:setup",
"A:willStart",
"A:willRender",
"B:setup",
"B:willStart",
"A:rendered",
"B:willRender",
"C:setup",
"C:willStart",
"B:rendered",
"C:willRender",
"Boom:setup",
"Boom:willStart",
"C:rendered",
"Boom:willRender",
"Boom:rendered",
"Boom:mounted",
"boom",
"C:willRender",
"C:rendered",
"C:mounted",
"B:mounted",
"A:mounted",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"A:setup",
"A:willStart",
"A:willRender",
"B:setup",
"B:willStart",
"B:willRender",
"C:setup",
"C:willStart",
"C:willRender",
"Boom:setup",
"Boom:willStart",
"Boom:willRender",
"Boom:rendered",
"C:rendered",
"B:rendered",
"A:rendered",
"Boom:mounted",
"boom",
"C:willRender",
"C:rendered",
"C:mounted",
"B:mounted",
"A:mounted",
]
`);
expect(mockConsoleError).toBeCalledTimes(0);
expect(mockConsoleWarn).toBeCalledTimes(0);
});
@@ -1130,31 +1156,33 @@ describe("can catch errors", () => {
}
await mount(Root, fixture);
expect(fixture.innerHTML).toBe("<div>OK<div>Error handled</div></div>");
expect([
"Root:setup",
"Root:willStart",
"Root:willRender",
"OK:setup",
"OK:willStart",
"ErrorBoundary:setup",
"ErrorBoundary:willStart",
"Root:rendered",
"OK:willRender",
"OK:rendered",
"ErrorBoundary:willRender",
"ErrorComponent:setup",
"ErrorComponent:willStart",
"ErrorBoundary:rendered",
"ErrorComponent:willRender",
"ErrorComponent:rendered",
"ErrorComponent:mounted",
"boom",
"ErrorBoundary:willRender",
"ErrorBoundary:rendered",
"ErrorBoundary:mounted",
"OK:mounted",
"Root:mounted",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Root:setup",
"Root:willStart",
"Root:willRender",
"OK:setup",
"OK:willStart",
"OK:willRender",
"OK:rendered",
"ErrorBoundary:setup",
"ErrorBoundary:willStart",
"ErrorBoundary:willRender",
"ErrorComponent:setup",
"ErrorComponent:willStart",
"ErrorComponent:willRender",
"ErrorComponent:rendered",
"ErrorBoundary:rendered",
"Root:rendered",
"ErrorComponent:mounted",
"boom",
"ErrorBoundary:willRender",
"ErrorBoundary:rendered",
"ErrorBoundary:mounted",
"OK:mounted",
"Root:mounted",
]
`);
expect(mockConsoleError).toBeCalledTimes(0);
expect(mockConsoleWarn).toBeCalledTimes(0);
});
@@ -1481,35 +1509,39 @@ describe("can catch errors", () => {
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("1<div>abc</div>");
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted",
"Parent:mounted",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Child:willRender",
"Child:rendered",
"Parent:rendered",
"Child:mounted",
"Parent:mounted",
]
`);
parent.state.hasChild = false;
await nextTick();
await nextTick();
await nextTick();
await nextTick();
expect([
"Parent:willRender",
"Parent:rendered",
"Parent:willPatch",
"Child:willUnmount",
"Child:willDestroy",
"Parent:patched",
"Parent:willRender",
"Parent:rendered",
"Parent:willPatch",
"Parent:patched",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:willRender",
"Parent:rendered",
"Parent:willPatch",
"Child:willUnmount",
"Child:willDestroy",
"Parent:patched",
"Parent:willRender",
"Parent:rendered",
"Parent:willPatch",
"Parent:patched",
]
`);
expect(fixture.innerHTML).toBe("2");
});
@@ -1542,13 +1574,15 @@ describe("can catch errors", () => {
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("1");
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Parent:rendered",
"Parent:mounted",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Parent:rendered",
"Parent:mounted",
]
`);
parent.state.hasChild = true;
await nextMicroTick();
@@ -1556,26 +1590,35 @@ describe("can catch errors", () => {
await nextMicroTick();
await nextMicroTick();
await nextMicroTick();
expect([
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Child:willRender",
"Child:rendered",
"Parent:rendered",
]
`);
parent.state.hasChild = false;
await nextTick();
expect([
"Parent:willRender",
"Parent:rendered",
"Child:willDestroy",
"Parent:willRender",
"Parent:rendered",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:willRender",
"Parent:rendered",
"Child:willDestroy",
"Parent:willRender",
"Parent:rendered",
]
`);
expect(fixture.innerHTML).toBe("1");
await nextTick();
expect(["Parent:willPatch", "Parent:patched"]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:willPatch",
"Parent:patched",
]
`);
expect(fixture.innerHTML).toBe("2");
});
});
+2 -1
View File
@@ -660,8 +660,9 @@ describe("hooks", () => {
let error: OwlError;
const app = new App(MyComponent);
const appError = nextAppError(app);
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("error occured in the owl lifecycle");
await expect(appError).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
+442 -346
View File
@@ -17,6 +17,7 @@ import {
nextMicroTick,
nextTick,
snapshotEverything,
steps,
useLogLifecycle,
} from "../helpers";
@@ -341,15 +342,16 @@ describe("lifecycle hooks", () => {
parent.state.n = 2;
await nextTick();
app.destroy();
expect(steps).toEqual([
"parent:willPatch",
"child:willPatch",
"childchild:willPatch",
"childchild:patched",
"child:patched",
"parent:patched",
]);
Object.freeze(steps);
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"parent:willPatch",
"childchild:willPatch",
"child:willPatch",
"child:patched",
"childchild:patched",
"parent:patched",
]
`);
});
test("willStart, mounted on subwidget rendered after main is mounted in some other position", async () => {
@@ -455,45 +457,51 @@ describe("lifecycle hooks", () => {
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("<div><span>0</span></div>");
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted",
"Parent:mounted",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Child:willRender",
"Child:rendered",
"Parent:rendered",
"Child:mounted",
"Parent:mounted",
]
`);
parent.increment();
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>1</span></div>");
expect([
"Parent:willRender",
"Child:willUpdateProps",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Parent:willPatch",
"Child:willPatch",
"Child:patched",
"Parent:patched",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:willRender",
"Child:willUpdateProps",
"Child:willRender",
"Child:rendered",
"Parent:rendered",
"Parent:willPatch",
"Child:willPatch",
"Child:patched",
"Parent:patched",
]
`);
parent.toggleSubWidget();
await nextTick();
expect(fixture.innerHTML).toBe("");
expect([
"Parent:willRender",
"Parent:rendered",
"Parent:willPatch",
"Child:willUnmount",
"Child:willDestroy",
"Parent:patched",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:willRender",
"Parent:rendered",
"Parent:willPatch",
"Child:willUnmount",
"Child:willDestroy",
"Parent:patched",
]
`);
});
test("hooks are called in proper order in widget creation/destruction", async () => {
@@ -514,26 +522,30 @@ describe("lifecycle hooks", () => {
const app = new App(Parent);
await app.mount(fixture);
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted",
"Parent:mounted",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Child:willRender",
"Child:rendered",
"Parent:rendered",
"Child:mounted",
"Parent:mounted",
]
`);
app.destroy();
expect([
"Parent:willUnmount",
"Child:willUnmount",
"Child:willDestroy",
"Parent:willDestroy",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:willUnmount",
"Child:willUnmount",
"Child:willDestroy",
"Parent:willDestroy",
]
`);
});
test("willUpdateProps hook is called", async () => {
@@ -632,26 +644,30 @@ describe("lifecycle hooks", () => {
const app = new App(Parent);
await app.mount(fixture);
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted",
"Parent:mounted",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Child:willRender",
"Child:rendered",
"Parent:rendered",
"Child:mounted",
"Parent:mounted",
]
`);
app.destroy();
expect([
"Parent:willUnmount",
"Child:willUnmount",
"Child:willDestroy",
"Parent:willDestroy",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:willUnmount",
"Child:willUnmount",
"Child:willDestroy",
"Parent:willDestroy",
]
`);
});
test("lifecycle semantics, part 2", async () => {
@@ -680,42 +696,48 @@ describe("lifecycle hooks", () => {
const app = new App(Parent);
const parent = await app.mount(fixture);
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Parent:rendered",
"Parent:mounted",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Parent:rendered",
"Parent:mounted",
]
`);
parent.state.hasChild = true;
await nextTick();
expect([
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"GrandChild:setup",
"GrandChild:willStart",
"Child:rendered",
"GrandChild:willRender",
"GrandChild:rendered",
"Parent:willPatch",
"GrandChild:mounted",
"Child:mounted",
"Parent:patched",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Child:willRender",
"GrandChild:setup",
"GrandChild:willStart",
"GrandChild:willRender",
"GrandChild:rendered",
"Child:rendered",
"Parent:rendered",
"Parent:willPatch",
"GrandChild:mounted",
"Child:mounted",
"Parent:patched",
]
`);
app.destroy();
expect([
"Parent:willUnmount",
"Child:willUnmount",
"GrandChild:willUnmount",
"GrandChild:willDestroy",
"Child:willDestroy",
"Parent:willDestroy",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:willUnmount",
"Child:willUnmount",
"GrandChild:willUnmount",
"GrandChild:willDestroy",
"Child:willDestroy",
"Parent:willDestroy",
]
`);
});
test("lifecycle semantics, part 3", async () => {
@@ -744,19 +766,26 @@ describe("lifecycle hooks", () => {
const app = new App(Parent);
const parent = await app.mount(fixture);
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Parent:rendered",
"Parent:mounted",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Parent:rendered",
"Parent:mounted",
]
`);
parent.state.hasChild = true;
// immediately destroy everything
app.destroy();
await nextTick();
expect(["Parent:willUnmount", "Parent:willDestroy"]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:willUnmount",
"Parent:willDestroy",
]
`);
});
test("lifecycle semantics, part 4", async () => {
@@ -789,34 +818,40 @@ describe("lifecycle hooks", () => {
const app = new App(Parent);
const parent = await app.mount(fixture);
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Parent:rendered",
"Parent:mounted",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Parent:rendered",
"Parent:mounted",
]
`);
parent.state.hasChild = true;
await nextTick();
expect([
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"GrandChild:setup",
"GrandChild:willStart",
"Child:rendered",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Child:willRender",
"GrandChild:setup",
"GrandChild:willStart",
"Child:rendered",
"Parent:rendered",
]
`);
app.destroy();
expect([
"Parent:willUnmount",
"GrandChild:willDestroy",
"Child:willDestroy",
"Parent:willDestroy",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:willUnmount",
"GrandChild:willDestroy",
"Child:willDestroy",
"Parent:willDestroy",
]
`);
});
test("lifecycle semantics, part 5", async () => {
@@ -837,29 +872,33 @@ describe("lifecycle hooks", () => {
}
const parent = await mount(Parent, fixture);
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted",
"Parent:mounted",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Child:willRender",
"Child:rendered",
"Parent:rendered",
"Child:mounted",
"Parent:mounted",
]
`);
parent.state.hasChild = false;
await nextTick();
expect([
"Parent:willRender",
"Parent:rendered",
"Parent:willPatch",
"Child:willUnmount",
"Child:willDestroy",
"Parent:patched",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:willRender",
"Parent:rendered",
"Parent:willPatch",
"Child:willUnmount",
"Child:willDestroy",
"Parent:patched",
]
`);
});
test("lifecycle semantics, part 6", async () => {
@@ -880,32 +919,36 @@ describe("lifecycle hooks", () => {
}
const parent = await mount(Parent, fixture);
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted",
"Parent:mounted",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Child:willRender",
"Child:rendered",
"Parent:rendered",
"Child:mounted",
"Parent:mounted",
]
`);
parent.state.value = 2;
await nextTick();
expect([
"Parent:willRender",
"Child:willUpdateProps",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Parent:willPatch",
"Child:willPatch",
"Child:patched",
"Parent:patched",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:willRender",
"Child:willUpdateProps",
"Child:willRender",
"Child:rendered",
"Parent:rendered",
"Parent:willPatch",
"Child:willPatch",
"Child:patched",
"Parent:patched",
]
`);
});
test("onWillRender", async () => {
@@ -937,43 +980,53 @@ describe("lifecycle hooks", () => {
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("<button>1</button>");
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted",
"Parent:mounted",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Child:willRender",
"Child:rendered",
"Parent:rendered",
"Child:mounted",
"Parent:mounted",
]
`);
parent.state.value++; // to block child render
await nextTick();
expect(["Parent:willRender", "Child:willUpdateProps", "Parent:rendered"]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:willRender",
"Child:willUpdateProps",
"Parent:rendered",
]
`);
fixture.querySelector("button")!.click();
await nextTick();
expect([]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`Array []`);
fixture.querySelector("button")!.click();
await nextTick();
expect([]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`Array []`);
expect(fixture.innerHTML).toBe("<button>1</button>");
def.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("<button>3</button>");
expect([
"Child:willRender",
"Child:rendered",
"Parent:willPatch",
"Child:willPatch",
"Child:patched",
"Parent:patched",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Child:willRender",
"Child:rendered",
"Parent:willPatch",
"Child:willPatch",
"Child:patched",
"Parent:patched",
]
`);
});
// TODO: rename (remove? seems covered by lifecycle semantics)
@@ -1054,50 +1107,54 @@ describe("lifecycle hooks", () => {
await mount(A, fixture);
expect(fixture.innerHTML).toBe(`<div>A<div>B</div><div>C<div>D</div><div>E</div></div></div>`);
expect([
"A:setup",
"A:willStart",
"A:willRender",
"B:setup",
"B:willStart",
"C:setup",
"C:willStart",
"A:rendered",
"B:willRender",
"B:rendered",
"C:willRender",
"D:setup",
"D:willStart",
"E:setup",
"E:willStart",
"C:rendered",
"D:willRender",
"D:rendered",
"E:willRender",
"E:rendered",
"E:mounted",
"D:mounted",
"C:mounted",
"B:mounted",
"A:mounted",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"A:setup",
"A:willStart",
"A:willRender",
"B:setup",
"B:willStart",
"B:willRender",
"B:rendered",
"C:setup",
"C:willStart",
"C:willRender",
"D:setup",
"D:willStart",
"D:willRender",
"D:rendered",
"E:setup",
"E:willStart",
"E:willRender",
"E:rendered",
"C:rendered",
"A:rendered",
"E:mounted",
"D:mounted",
"C:mounted",
"B:mounted",
"A:mounted",
]
`);
// update
c!.state.flag = false;
await nextTick();
expect([
"C:willRender",
"F:setup",
"F:willStart",
"C:rendered",
"F:willRender",
"F:rendered",
"C:willPatch",
"E:willUnmount",
"E:willDestroy",
"F:mounted",
"C:patched",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"C:willRender",
"F:setup",
"F:willStart",
"F:willRender",
"F:rendered",
"C:rendered",
"C:willPatch",
"E:willUnmount",
"E:willDestroy",
"F:mounted",
"C:patched",
]
`);
});
test("mounted hook is called on every mount, not just the first one", async () => {
@@ -1118,43 +1175,49 @@ describe("lifecycle hooks", () => {
}
const parent = await mount(Parent, fixture);
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted",
"Parent:mounted",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Child:willRender",
"Child:rendered",
"Parent:rendered",
"Child:mounted",
"Parent:mounted",
]
`);
parent.state.hasChild = false;
await nextTick();
expect([
"Parent:willRender",
"Parent:rendered",
"Parent:willPatch",
"Child:willUnmount",
"Child:willDestroy",
"Parent:patched",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:willRender",
"Parent:rendered",
"Parent:willPatch",
"Child:willUnmount",
"Child:willDestroy",
"Parent:patched",
]
`);
parent.state.hasChild = true;
await nextTick();
expect([
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Parent:willPatch",
"Child:mounted",
"Parent:patched",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Child:willRender",
"Child:rendered",
"Parent:rendered",
"Parent:willPatch",
"Child:mounted",
"Parent:patched",
]
`);
});
test("render in mounted", async () => {
@@ -1172,19 +1235,26 @@ describe("lifecycle hooks", () => {
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("<span></span>");
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Parent:rendered",
"Parent:mounted",
"Parent:willRender",
"Parent:rendered",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Parent:rendered",
"Parent:mounted",
"Parent:willRender",
"Parent:rendered",
]
`);
await nextTick();
expect(fixture.innerHTML).toBe("<span>Patched</span>");
expect(["Parent:willPatch", "Parent:patched"]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:willPatch",
"Parent:patched",
]
`);
});
test("render in patched", async () => {
@@ -1205,29 +1275,38 @@ describe("lifecycle hooks", () => {
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("<span></span>");
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Parent:rendered",
"Parent:mounted",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Parent:rendered",
"Parent:mounted",
]
`);
parent.render();
await nextTick();
expect(fixture.innerHTML).toBe("<span></span>");
expect([
"Parent:willRender",
"Parent:rendered",
"Parent:willPatch",
"Parent:patched",
"Parent:willRender",
"Parent:rendered",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:willRender",
"Parent:rendered",
"Parent:willPatch",
"Parent:patched",
"Parent:willRender",
"Parent:rendered",
]
`);
await nextTick();
expect(fixture.innerHTML).toBe("<span>Patched</span>");
expect(["Parent:willPatch", "Parent:patched"]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:willPatch",
"Parent:patched",
]
`);
});
test("render in willPatch", async () => {
@@ -1248,29 +1327,38 @@ describe("lifecycle hooks", () => {
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("<span></span>");
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Parent:rendered",
"Parent:mounted",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Parent:rendered",
"Parent:mounted",
]
`);
parent.render();
await nextTick();
expect(fixture.innerHTML).toBe("<span></span>");
expect([
"Parent:willRender",
"Parent:rendered",
"Parent:willPatch",
"Parent:patched",
"Parent:willRender",
"Parent:rendered",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:willRender",
"Parent:rendered",
"Parent:willPatch",
"Parent:patched",
"Parent:willRender",
"Parent:rendered",
]
`);
await nextTick();
expect(["Parent:willPatch", "Parent:patched"]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:willPatch",
"Parent:patched",
]
`);
expect(fixture.innerHTML).toBe("<span>Patched</span>");
});
@@ -1313,19 +1401,21 @@ describe("lifecycle hooks", () => {
await nextTick();
app.destroy();
expect([
"onWillStart",
"onWillRender",
"onRendered",
"onMounted",
"onWillUpdateProps",
"onWillRender",
"onRendered",
"onWillPatch",
"onPatched",
"onWillUnmount",
"onWillDestroy",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"onWillStart",
"onWillRender",
"onRendered",
"onMounted",
"onWillUpdateProps",
"onWillRender",
"onRendered",
"onWillPatch",
"onPatched",
"onWillUnmount",
"onWillDestroy",
]
`);
});
test("destroy new children before being mountged", async () => {
@@ -1357,26 +1447,32 @@ describe("lifecycle hooks", () => {
const app = new App(Parent);
const parent = await app.mount(fixture);
expect(fixture.innerHTML).toBe("beforeafter");
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Parent:rendered",
"Parent:mounted",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Parent:rendered",
"Parent:mounted",
]
`);
parent.state.flag = true;
await nextTick();
expect(fixture.innerHTML).toBe("");
expect([
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Parent:willUnmount",
"Child:willDestroy",
"Parent:willDestroy",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Child:willRender",
"Child:rendered",
"Parent:rendered",
"Parent:willUnmount",
"Child:willDestroy",
"Parent:willDestroy",
]
`);
});
});
+76 -64
View File
@@ -1,5 +1,5 @@
import { Component, mount, onWillUpdateProps, useState, xml } from "../../src";
import { makeTestFixture, nextTick, snapshotEverything, useLogLifecycle } from "../helpers";
import { makeTestFixture, nextTick, snapshotEverything, steps, useLogLifecycle } from "../helpers";
let fixture: HTMLElement;
@@ -272,26 +272,30 @@ test("bound functions are considered 'alike'", async () => {
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("1child");
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted",
"Parent:mounted",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Child:willRender",
"Child:rendered",
"Parent:rendered",
"Child:mounted",
"Parent:mounted",
]
`);
parent.state.val = 3;
await nextTick();
expect([
"Parent:willRender",
"Parent:rendered",
"Parent:willPatch",
"Parent:patched",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:willRender",
"Parent:rendered",
"Parent:willPatch",
"Parent:patched",
]
`);
expect(fixture.innerHTML).toBe("3child");
});
@@ -330,29 +334,33 @@ test(".alike suffix in a simple case", async () => {
}
const parent = await mount(Parent, fixture);
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted",
"Parent:mounted",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Child:willRender",
"Child:rendered",
"Parent:rendered",
"Child:mounted",
"Parent:mounted",
]
`);
expect(fixture.innerHTML).toBe("01");
parent.state.counter++;
await nextTick();
expect(fixture.innerHTML).toBe("11");
expect([
"Parent:willRender",
"Parent:rendered",
"Parent:willPatch",
"Parent:patched",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:willRender",
"Parent:rendered",
"Parent:willPatch",
"Parent:patched",
]
`);
});
test(".alike suffix in a list", async () => {
@@ -388,36 +396,40 @@ test(".alike suffix in a list", async () => {
}
await mount(Parent, fixture);
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Todo:setup",
"Todo:willStart",
"Todo:setup",
"Todo:willStart",
"Parent:rendered",
"Todo:willRender",
"Todo:rendered",
"Todo:willRender",
"Todo:rendered",
"Todo:mounted",
"Todo:mounted",
"Parent:mounted",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Todo:setup",
"Todo:willStart",
"Todo:willRender",
"Todo:rendered",
"Todo:setup",
"Todo:willStart",
"Todo:willRender",
"Todo:rendered",
"Parent:rendered",
"Todo:mounted",
"Todo:mounted",
"Parent:mounted",
]
`);
expect(fixture.innerHTML).toBe("<button>1</button><button>2V</button>");
fixture.querySelector("button")?.click();
await nextTick();
expect(fixture.innerHTML).toBe("<button>1V</button><button>2V</button>");
expect([
"Parent:willRender",
"Parent:rendered",
"Todo:willRender",
"Todo:rendered",
"Todo:willPatch",
"Todo:patched",
"Parent:willPatch",
"Parent:patched",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:willRender",
"Parent:rendered",
"Todo:willRender",
"Todo:rendered",
"Todo:willPatch",
"Todo:patched",
"Parent:willPatch",
"Parent:patched",
]
`);
});
+38 -21
View File
@@ -51,8 +51,9 @@ describe("props validation", () => {
const app = new App(Parent, { test: true });
let error: OwlError | undefined;
const appError = nextAppError(app);
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow(
await expect(appError).resolves.toThrow(
"Invalid props for component 'SubComp': 'message' is missing"
);
await mountProm;
@@ -80,8 +81,9 @@ describe("props validation", () => {
const app = new App(Parent, { test: true });
let error: OwlError | undefined;
const appError = nextAppError(app);
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow(
await expect(appError).resolves.toThrow(
"Invalid props for component 'SubComp': 'message' is missing"
);
await mountProm;
@@ -131,8 +133,9 @@ describe("props validation", () => {
props = {};
let app = new App(Parent, { test: true });
let error: OwlError | undefined;
let appError = nextAppError(app);
let mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("Invalid props for component '_a'");
await expect(appError).resolves.toThrow("Invalid props for component '_a'");
await mountProm;
expect(error!).toBeDefined();
expect(error!.message).toBe(
@@ -148,8 +151,9 @@ describe("props validation", () => {
expect(error!).toBeUndefined();
props = { p: test.ko };
app = new App(Parent, { test: true });
appError = nextAppError(app);
mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("Invalid props for component '_a'");
await expect(appError).resolves.toThrow("Invalid props for component '_a'");
await mountProm;
expect(error!).toBeDefined();
expect(error!.message).toBe(
@@ -183,8 +187,9 @@ describe("props validation", () => {
props = {};
let app = new App(Parent, { test: true });
let error: OwlError | undefined;
let appError = nextAppError(app);
let mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("Invalid props for component '_a'");
await expect(appError).resolves.toThrow("Invalid props for component '_a'");
await mountProm;
expect(error!).toBeDefined();
expect(error!.message).toBe(
@@ -200,8 +205,9 @@ describe("props validation", () => {
expect(error!).toBeUndefined();
props = { p: test.ko };
app = new App(Parent, { test: true });
appError = nextAppError(app);
mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("Invalid props for component '_a'");
await expect(appError).resolves.toThrow("Invalid props for component '_a'");
await mountProm;
expect(error!).toBeDefined();
expect(error!.message).toBe(
@@ -240,8 +246,9 @@ describe("props validation", () => {
expect(error!).toBeUndefined();
props = { p: 1 };
const app = new App(Parent, { test: true });
const appError = nextAppError(app);
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("Invalid props for component 'SubComp'");
await expect(appError).resolves.toThrow("Invalid props for component 'SubComp'");
await mountProm;
expect(error!).toBeDefined();
expect(error!.message).toBe(
@@ -279,8 +286,9 @@ describe("props validation", () => {
expect(error!).toBeUndefined();
props = { p: 1 };
const app = new App(Parent, { test: true });
const appError = nextAppError(app);
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("Invalid props for component 'SubComp'");
await expect(appError).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");
@@ -316,14 +324,16 @@ describe("props validation", () => {
expect(error!).toBeUndefined();
props = { p: [1] };
let app = new App(Parent, { test: true });
let appError = nextAppError(app);
let mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("Invalid props for component 'SubComp'");
await expect(appError).resolves.toThrow("Invalid props for component 'SubComp'");
await mountProm;
expect(error!).toBeDefined();
error = undefined;
app = new App(Parent, { test: true });
appError = nextAppError(app);
mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("Invalid props for component 'SubComp'");
await expect(appError).resolves.toThrow("Invalid props for component 'SubComp'");
await mountProm;
expect(error!).toBeDefined();
});
@@ -365,8 +375,9 @@ describe("props validation", () => {
expect(error!).toBeUndefined();
props = { p: [true, 1] };
const app = new App(Parent, { test: true });
const appError = nextAppError(app);
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("Invalid props for component 'SubComp'");
await expect(appError).resolves.toThrow("Invalid props for component 'SubComp'");
await mountProm;
expect(error!).toBeDefined();
expect(error!.message).toBe(
@@ -399,8 +410,9 @@ describe("props validation", () => {
expect(error!).toBeUndefined();
props = { p: { id: 1, url: "url", extra: true } };
let app = new App(Parent, { test: true });
let appError = nextAppError(app);
let mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("Invalid props for component 'SubComp'");
await expect(appError).resolves.toThrow("Invalid props for component 'SubComp'");
await mountProm;
expect(error!).toBeDefined();
expect(error!.message).toBe(
@@ -408,8 +420,9 @@ describe("props validation", () => {
);
props = { p: { id: "1", url: "url" } };
app = new App(Parent, { test: true });
appError = nextAppError(app);
mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("Invalid props for component 'SubComp'");
await expect(appError).resolves.toThrow("Invalid props for component 'SubComp'");
await mountProm;
expect(error!).toBeDefined();
expect(error!.message).toBe(
@@ -418,8 +431,9 @@ describe("props validation", () => {
error = undefined;
props = { p: { id: 1 } };
app = new App(Parent, { test: true });
appError = nextAppError(app);
mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("Invalid props for component 'SubComp'");
await expect(appError).resolves.toThrow("Invalid props for component 'SubComp'");
await mountProm;
expect(error!).toBeDefined();
expect(error!.message).toBe(
@@ -465,8 +479,9 @@ describe("props validation", () => {
expect(error!).toBeUndefined();
props = { p: { id: 1, url: [12, true] } };
const app = new App(Parent, { test: true });
const appError = nextAppError(app);
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("Invalid props for component 'SubComp'");
await expect(appError).resolves.toThrow("Invalid props for component 'SubComp'");
await mountProm;
expect(error!).toBeDefined();
expect(error!.message).toBe(
@@ -675,8 +690,9 @@ describe("props validation", () => {
}
let error: Error;
const app = new App(Parent, { test: true });
const appError = nextAppError(app);
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("Invalid props for component 'SubComp'");
await expect(appError).resolves.toThrow("Invalid props for component 'SubComp'");
await mountProm;
expect(error!).toBeDefined();
expect(error!.message).toBe("Invalid props for component 'SubComp': 'p' is missing");
@@ -765,8 +781,9 @@ describe("props validation", () => {
}
let error: Error;
const app = new App(Parent, { test: true });
const appError = nextAppError(app);
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("Invalid props for component 'Child'");
await expect(appError).resolves.toThrow("Invalid props for component 'Child'");
await mountProm;
expect(error!).toBeDefined();
expect(error!.message).toBe(
@@ -821,8 +838,9 @@ describe("props validation", () => {
const app = new App(Parent, { test: true });
let error: OwlError | undefined;
const appError = nextAppError(app);
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow(
await expect(appError).resolves.toThrow(
"Invalid props for component 'Child': 'message' is missing"
);
await mountProm;
@@ -895,10 +913,9 @@ describe("default props", () => {
}
let error: Error;
const app = new App(Parent, { test: true });
const appError = nextAppError(app);
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 expect(appError).resolves.toThrow("default value cannot be defined for a mandatory prop");
await mountProm;
expect(error!).toBeDefined();
expect(error!.message).toBe(
+27 -22
View File
@@ -9,7 +9,7 @@ import {
xml,
toRaw,
} from "../../src";
import { makeTestFixture, nextTick, snapshotEverything, useLogLifecycle } from "../helpers";
import { makeTestFixture, nextTick, snapshotEverything, steps, useLogLifecycle } from "../helpers";
let fixture: HTMLElement;
@@ -151,9 +151,10 @@ describe("reactivity in lifecycle", () => {
}
}
const prom = mount(Comp, fixture);
expect(steps).toEqual([1]);
(STATE as any).val = 2;
await prom;
expect(steps).toEqual([2]);
expect(steps).toEqual([1, 2]);
expect(fixture.innerHTML).toBe("<div>2</div>");
});
@@ -175,30 +176,34 @@ describe("reactivity in lifecycle", () => {
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("2");
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted",
"Parent:mounted",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Child:willRender",
"Child:rendered",
"Parent:rendered",
"Child:mounted",
"Parent:mounted",
]
`);
parent.state.content = null;
parent.state.renderChild = false;
await nextTick();
expect([
"Parent:willRender",
"Parent:rendered",
"Parent:willPatch",
"Child:willUnmount",
"Child:willDestroy",
"Parent:patched",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:willRender",
"Parent:rendered",
"Parent:willPatch",
"Child:willUnmount",
"Child:willDestroy",
"Parent:patched",
]
`);
});
test("Component is automatically subscribed to reactive object received as prop", async () => {
+2 -1
View File
@@ -125,10 +125,11 @@ describe("refs", () => {
}
const app = new App(Test, { test: true });
const appError = nextAppError(app);
const mountProm = expect(app.mount(fixture)).rejects.toThrowError(
'Cannot set the same ref more than once in the same component, ref "coucou" was set multiple times in Test'
);
await expect(nextAppError(app)).resolves.toThrow(
await expect(appError).resolves.toThrow(
'Cannot set the same ref more than once in the same component, ref "coucou" was set multiple times in Test'
);
await mountProm;
+217 -162
View File
@@ -6,6 +6,7 @@ import {
useLogLifecycle,
makeDeferred,
nextMicroTick,
steps,
} from "../helpers";
let fixture: HTMLElement;
@@ -41,28 +42,32 @@ describe("rendering semantics", () => {
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("Achild");
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted",
"Parent:mounted",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Child:willRender",
"Child:rendered",
"Parent:rendered",
"Child:mounted",
"Parent:mounted",
]
`);
parent.state.value = "B";
await nextTick();
expect(fixture.innerHTML).toBe("Bchild");
expect([
"Parent:willRender",
"Parent:rendered",
"Parent:willPatch",
"Parent:patched",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:willRender",
"Parent:rendered",
"Parent:willPatch",
"Parent:patched",
]
`);
});
test("can force a render to update sub tree", async () => {
@@ -167,18 +172,20 @@ describe("rendering semantics", () => {
const parent = await mount(Parent, fixture, { env });
expect(fixture.innerHTML).toBe("parentAchild3");
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted",
"Parent:mounted",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Child:willRender",
"Child:rendered",
"Parent:rendered",
"Child:mounted",
"Parent:mounted",
]
`);
value = 4;
parent.render(true);
@@ -187,30 +194,34 @@ describe("rendering semantics", () => {
await nextMicroTick();
await nextMicroTick();
await nextMicroTick();
expect([
"Parent:willRender",
"Child:willUpdateProps",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:willRender",
"Child:willUpdateProps",
"Child:willRender",
"Child:rendered",
"Parent:rendered",
]
`);
parent.state.value = "B";
await nextTick();
expect(fixture.innerHTML).toBe("parentBchild4");
expect([
"Parent:willRender",
"Child:willUpdateProps",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Parent:willPatch",
"Child:willPatch",
"Child:patched",
"Parent:patched",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:willRender",
"Child:willUpdateProps",
"Child:willRender",
"Child:rendered",
"Parent:rendered",
"Parent:willPatch",
"Child:willPatch",
"Child:patched",
"Parent:patched",
]
`);
});
test("props are reactive", async () => {
@@ -235,23 +246,32 @@ describe("rendering semantics", () => {
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("1");
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted",
"Parent:mounted",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Child:willRender",
"Child:rendered",
"Parent:rendered",
"Child:mounted",
"Parent:mounted",
]
`);
parent.state.b = 3;
await nextTick();
expect(fixture.innerHTML).toBe("3");
expect(["Child:willRender", "Child:rendered", "Child:willPatch", "Child:patched"]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Child:willRender",
"Child:rendered",
"Child:willPatch",
"Child:patched",
]
`);
});
test("props are reactive (nested prop)", async () => {
@@ -278,37 +298,48 @@ describe("rendering semantics", () => {
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("1");
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted",
"Parent:mounted",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Child:willRender",
"Child:rendered",
"Parent:rendered",
"Child:mounted",
"Parent:mounted",
]
`);
parent.state.b.c = 3; // parent is now subscribed to 'b' key
await nextTick();
expect(fixture.innerHTML).toBe("3");
expect(["Child:willRender", "Child:rendered", "Child:willPatch", "Child:patched"]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Child:willRender",
"Child:rendered",
"Child:willPatch",
"Child:patched",
]
`);
parent.state.b = { c: 444 }; // triggers a parent and a child render
await nextTick();
expect(fixture.innerHTML).toBe("444");
expect([
"Parent:willRender",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Parent:willPatch",
"Parent:patched",
"Child:willPatch",
"Child:patched",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:willRender",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Parent:willPatch",
"Parent:patched",
"Child:willPatch",
"Child:patched",
]
`);
});
test("works as expected for dynamic number of props", async () => {
@@ -366,41 +397,45 @@ describe("rendering semantics", () => {
const parent = await mount(A, fixture);
expect(fixture.innerHTML).toBe("11");
expect([
"A:setup",
"A:willStart",
"A:willRender",
"B:setup",
"B:willStart",
"A:rendered",
"B:willRender",
"C:setup",
"C:willStart",
"B:rendered",
"C:willRender",
"C:rendered",
"C:mounted",
"B:mounted",
"A:mounted",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"A:setup",
"A:willStart",
"A:willRender",
"B:setup",
"B:willStart",
"B:willRender",
"C:setup",
"C:willStart",
"C:willRender",
"C:rendered",
"B:rendered",
"A:rendered",
"C:mounted",
"B:mounted",
"A:mounted",
]
`);
parent.state.obj.val = 3;
await nextTick();
expect(fixture.innerHTML).toBe("33");
expect([
"A:willRender",
"A:rendered",
"C:willRender",
"C:rendered",
"A:willPatch",
"A:patched",
"C:willPatch",
"C:patched",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"A:willRender",
"A:rendered",
"C:willRender",
"C:rendered",
"A:willPatch",
"A:patched",
"C:willPatch",
"C:patched",
]
`);
def.resolve();
await nextTick();
expect([]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`Array []`);
});
});
@@ -431,51 +466,67 @@ test("force render in case of existing render", async () => {
}
const parent = await mount(A, fixture);
expect(fixture.innerHTML).toBe("C1");
expect([
"A:setup",
"A:willStart",
"A:willRender",
"B:setup",
"B:willStart",
"A:rendered",
"B:willRender",
"C:setup",
"C:willStart",
"B:rendered",
"C:willRender",
"C:rendered",
"C:mounted",
"B:mounted",
"A:mounted",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"A:setup",
"A:willStart",
"A:willRender",
"B:setup",
"B:willStart",
"B:willRender",
"C:setup",
"C:willStart",
"C:willRender",
"C:rendered",
"B:rendered",
"A:rendered",
"C:mounted",
"B:mounted",
"A:mounted",
]
`);
// trigger a new rendering, blocked in B
parent.state.val = 2;
await nextTick();
expect(["A:willRender", "B:willUpdateProps", "A:rendered"]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"A:willRender",
"B:willUpdateProps",
"A:rendered",
]
`);
// initiate a new render with deep=true. it should cancel the current render
// and also be blocked in B
parent.render(true);
await nextTick();
expect(["A:willRender", "B:willUpdateProps", "A:rendered"]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"A:willRender",
"B:willUpdateProps",
"A:rendered",
]
`);
def.resolve();
await nextTick();
// we check here that the render reaches C (so, that it was properly forced)
expect([
"B:willRender",
"C:willUpdateProps",
"B:rendered",
"C:willRender",
"C:rendered",
"A:willPatch",
"B:willPatch",
"C:willPatch",
"C:patched",
"B:patched",
"A:patched",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"B:willRender",
"C:willUpdateProps",
"C:willRender",
"C:rendered",
"B:rendered",
"A:willPatch",
"C:willPatch",
"B:willPatch",
"B:patched",
"C:patched",
"A:patched",
]
`);
});
test("children, default props and renderings", async () => {
@@ -503,26 +554,30 @@ test("children, default props and renderings", async () => {
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("Achild");
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted",
"Parent:mounted",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Child:willRender",
"Child:rendered",
"Parent:rendered",
"Child:mounted",
"Parent:mounted",
]
`);
parent.state.value = "B";
await nextTick();
expect(fixture.innerHTML).toBe("Bchild");
expect([
"Parent:willRender",
"Parent:rendered",
"Parent:willPatch",
"Parent:patched",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:willRender",
"Parent:rendered",
"Parent:willPatch",
"Parent:patched",
]
`);
});
+2 -1
View File
@@ -223,8 +223,9 @@ describe("slots", () => {
let error: Error;
const app = new App(Parent);
const appError = nextAppError(app);
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("error occured in the owl lifecycle");
await expect(appError).resolves.toThrow("error occured in the owl lifecycle");
await mountProm;
expect(error!).not.toBeNull();
expect(mockConsoleWarn).toBeCalledTimes(1);
+2 -1
View File
@@ -349,8 +349,9 @@ describe("style and class handling", () => {
}
let error: OwlError;
const app = new App(Parent);
const appError = nextAppError(app);
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("error occured in the owl lifecycle");
await expect(appError).resolves.toThrow("error occured in the owl lifecycle");
await mountProm;
expect(error!).toBeDefined();
expect(error!.cause).toBeDefined();
+44 -38
View File
@@ -1,5 +1,5 @@
import { Component, mount, useState, xml } from "../../src";
import { makeTestFixture, nextTick, snapshotEverything, useLogLifecycle } from "../helpers";
import { makeTestFixture, nextTick, snapshotEverything, steps, useLogLifecycle } from "../helpers";
let fixture: HTMLElement;
@@ -29,18 +29,20 @@ describe("t-component", () => {
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("<div>child</div>");
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted",
"Parent:mounted",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Child:willRender",
"Child:rendered",
"Parent:rendered",
"Child:mounted",
"Parent:mounted",
]
`);
});
test("switching dynamic component", async () => {
@@ -68,36 +70,40 @@ describe("t-component", () => {
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("<div>child a</div>");
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"ChildA:setup",
"ChildA:willStart",
"Parent:rendered",
"ChildA:willRender",
"ChildA:rendered",
"ChildA:mounted",
"Parent:mounted",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"ChildA:setup",
"ChildA:willStart",
"ChildA:willRender",
"ChildA:rendered",
"Parent:rendered",
"ChildA:mounted",
"Parent:mounted",
]
`);
parent.Child = ChildB;
parent.render();
await nextTick();
expect(fixture.innerHTML).toBe("child b");
expect([
"Parent:willRender",
"ChildB:setup",
"ChildB:willStart",
"Parent:rendered",
"ChildB:willRender",
"ChildB:rendered",
"Parent:willPatch",
"ChildA:willUnmount",
"ChildA:willDestroy",
"ChildB:mounted",
"Parent:patched",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:willRender",
"ChildB:setup",
"ChildB:willStart",
"ChildB:willRender",
"ChildB:rendered",
"Parent:rendered",
"Parent:willPatch",
"ChildA:willUnmount",
"ChildA:willDestroy",
"ChildB:mounted",
"Parent:patched",
]
`);
});
test("can switch between dynamic components without the need for a t-key", async () => {
+24 -21
View File
@@ -4,6 +4,7 @@ import {
nextAppError,
nextTick,
snapshotEverything,
steps,
useLogLifecycle,
} from "../helpers";
@@ -91,23 +92,25 @@ describe("list of components", () => {
expect(fixture.innerHTML).toBe(
"<div><ul><li><div>1</div></li><li><div>2</div></li></ul></div>"
);
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted",
"Child:mounted",
"Parent:mounted",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Child:willRender",
"Child:rendered",
"Child:setup",
"Child:willStart",
"Child:willRender",
"Child:rendered",
"Parent:rendered",
"Child:mounted",
"Child:mounted",
"Parent:mounted",
]
`);
});
test("reconciliation alg works for t-foreach in t-foreach", async () => {
@@ -323,10 +326,11 @@ describe("list of components", () => {
}
const app = new App(Parent, { test: true });
const appError = nextAppError(app);
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 expect(appError).resolves.toThrow("Got duplicate key in t-foreach: child");
await mountProm;
console.info = consoleInfo;
expect(mockConsoleWarn).toBeCalledTimes(1);
@@ -349,12 +353,11 @@ describe("list of components", () => {
}
const app = new App(Parent, { test: true });
const appError = nextAppError(app);
const mountProm = expect(app.mount(fixture)).rejects.toThrow(
"Got duplicate key in t-foreach: [object Object]"
);
await expect(nextAppError(app)).resolves.toThrow(
"Got duplicate key in t-foreach: [object Object]"
);
await expect(appError).resolves.toThrow("Got duplicate key in t-foreach: [object Object]");
await mountProm;
console.info = consoleInfo;
expect(mockConsoleWarn).toBeCalledTimes(1);
+2 -2
View File
@@ -137,7 +137,7 @@ export function snapshotEverything() {
};
}
const steps: string[] = [];
export const steps: string[] = [];
export function logStep(step: string) {
steps.push(step);
@@ -235,7 +235,7 @@ expect.extend({
};
const currentSteps = steps.splice(0);
const pass = this.equals(currentSteps, expected);
let pass = this.equals(currentSteps, expected);
const message = pass
? () =>
+6 -3
View File
@@ -270,8 +270,9 @@ describe("Portal", () => {
let error: Error;
const app = new App(Parent);
const appError = nextAppError(app);
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("invalid portal target");
await expect(appError).resolves.toThrow("invalid portal target");
await mountProm;
expect(error!).toBeDefined();
@@ -1002,8 +1003,9 @@ describe("Portal: Props validation", () => {
}
let error: OwlError;
const app = new App(Parent);
const appError = nextAppError(app);
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("error occured in the owl lifecycle");
await expect(appError).resolves.toThrow("error occured in the owl lifecycle");
await mountProm;
expect(error!).toBeDefined();
expect(error!.cause).toBeDefined();
@@ -1021,8 +1023,9 @@ describe("Portal: Props validation", () => {
}
let error: Error;
const app = new App(Parent);
const appError = nextAppError(app);
const mountProm = app.mount(fixture).catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow("invalid portal target");
await expect(appError).resolves.toThrow("invalid portal target");
await mountProm;
expect(error!).toBeDefined();
expect(error!.message).toBe(`invalid portal target`);
+158 -102
View File
@@ -17,6 +17,7 @@ import {
nextMicroTick,
nextTick,
snapshotEverything,
steps,
useLogLifecycle,
} from "./helpers";
@@ -1292,6 +1293,12 @@ describe("Collections", () => {
state.add(3); // setting unobserved key doesn't notify
expect(observer).toHaveBeenCalledTimes(3);
expect(state.has(3)).toBe(true); // subscribe to 3
state.add(3); // adding observed key doesn't notify if key was already present
expect(observer).toHaveBeenCalledTimes(3);
expect(state.has(4)).toBe(false); // subscribe to 4
state.delete(4); // deleting observed key doesn't notify if key was already not present
expect(observer).toHaveBeenCalledTimes(3);
});
test("iterating on keys returns reactives", async () => {
@@ -1485,6 +1492,12 @@ describe("Collections", () => {
state.set(3, 4); // setting unobserved key doesn't notify
expect(observer).toHaveBeenCalledTimes(3);
expect(state.has(3)).toBe(true); // subscribe to 3
state.set(3, 4); // setting the same value doesn't notify
expect(observer).toHaveBeenCalledTimes(3);
expect(state.has(4)).toBe(false); // subscribe to 4
state.delete(4); // deleting observed key doesn't notify if key was already not present
expect(observer).toHaveBeenCalledTimes(3);
});
test("checking for a key with 'get' subscribes the callback to changes to that key", () => {
@@ -1510,6 +1523,12 @@ describe("Collections", () => {
state.set(3, 4); // setting unobserved key doesn't notify
expect(observer).toHaveBeenCalledTimes(3);
expect(state.get(3)).toBe(4); // subscribe to 3
state.set(3, 4); // setting the same value doesn't notify
expect(observer).toHaveBeenCalledTimes(3);
expect(state.get(4)).toBe(undefined); // subscribe to 4
state.delete(4); // deleting observed key doesn't notify if key was already not present
expect(observer).toHaveBeenCalledTimes(3);
});
test("getting values returns a reactive", async () => {
@@ -1832,37 +1851,41 @@ describe("Reactivity: useState", () => {
}
}
await mount(Parent, fixture);
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted",
"Child:mounted",
"Parent:mounted",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Child:willRender",
"Child:rendered",
"Child:setup",
"Child:willStart",
"Child:willRender",
"Child:rendered",
"Parent:rendered",
"Child:mounted",
"Child:mounted",
"Parent:mounted",
]
`);
expect(fixture.innerHTML).toBe("<div><span>123</span><span>123</span></div>");
testContext.value = 321;
await nextTick();
expect([
"Child:willRender",
"Child:rendered",
"Child:willRender",
"Child:rendered",
"Child:willPatch",
"Child:patched",
"Child:willPatch",
"Child:patched",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Child:willRender",
"Child:rendered",
"Child:willRender",
"Child:rendered",
"Child:willPatch",
"Child:patched",
"Child:willPatch",
"Child:patched",
]
`);
expect(fixture.innerHTML).toBe("<div><span>321</span><span>321</span></div>");
});
@@ -1886,38 +1909,49 @@ describe("Reactivity: useState", () => {
}
await mount(Parent, fixture);
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted",
"Child:mounted",
"Parent:mounted",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Child:willRender",
"Child:rendered",
"Child:setup",
"Child:willStart",
"Child:willRender",
"Child:rendered",
"Parent:rendered",
"Child:mounted",
"Child:mounted",
"Parent:mounted",
]
`);
expect(fixture.innerHTML).toBe("<div><span>123</span><span>123</span></div>");
testContext.value = 321;
await nextMicroTick();
await nextMicroTick();
expect([
"Child:willRender",
"Child:rendered",
"Child:willRender",
"Child:rendered",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Child:willRender",
"Child:rendered",
"Child:willRender",
"Child:rendered",
]
`);
expect(fixture.innerHTML).toBe("<div><span>123</span><span>123</span></div>");
await nextTick();
expect(["Child:willPatch", "Child:patched", "Child:willPatch", "Child:patched"]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Child:willPatch",
"Child:patched",
"Child:willPatch",
"Child:patched",
]
`);
expect(fixture.innerHTML).toBe("<div><span>321</span><span>321</span></div>");
});
@@ -1951,43 +1985,54 @@ describe("Reactivity: useState", () => {
await mount(GrandFather, fixture);
expect(fixture.innerHTML).toBe("<div><span>123</span><div><span>123</span></div></div>");
expect([
"GrandFather:setup",
"GrandFather:willStart",
"GrandFather:willRender",
"Child:setup",
"Child:willStart",
"Parent:setup",
"Parent:willStart",
"GrandFather:rendered",
"Child:willRender",
"Child:rendered",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted",
"Parent:mounted",
"Child:mounted",
"GrandFather:mounted",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"GrandFather:setup",
"GrandFather:willStart",
"GrandFather:willRender",
"Child:setup",
"Child:willStart",
"Child:willRender",
"Child:rendered",
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Child:willRender",
"Child:rendered",
"Parent:rendered",
"GrandFather:rendered",
"Child:mounted",
"Parent:mounted",
"Child:mounted",
"GrandFather:mounted",
]
`);
testContext.value = 321;
await nextMicroTick();
await nextMicroTick();
expect(fixture.innerHTML).toBe("<div><span>123</span><div><span>123</span></div></div>");
expect([
"Child:willRender",
"Child:rendered",
"Child:willRender",
"Child:rendered",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Child:willRender",
"Child:rendered",
"Child:willRender",
"Child:rendered",
]
`);
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>321</span><div><span>321</span></div></div>");
expect(["Child:willPatch", "Child:patched", "Child:willPatch", "Child:patched"]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Child:willPatch",
"Child:patched",
"Child:willPatch",
"Child:patched",
]
`);
});
test("one components can subscribe twice to same context", async () => {
@@ -2163,38 +2208,49 @@ describe("Reactivity: useState", () => {
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("<div><span>123</span></div>");
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted",
"Parent:mounted",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Child:willRender",
"Child:rendered",
"Parent:rendered",
"Child:mounted",
"Parent:mounted",
]
`);
testContext.a = 321;
await nextTick();
expect(["Child:willRender", "Child:rendered", "Child:willPatch", "Child:patched"]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Child:willRender",
"Child:rendered",
"Child:willPatch",
"Child:patched",
]
`);
parent.state.flag = false;
await nextTick();
expect(fixture.innerHTML).toBe("<div></div>");
expect([
"Parent:willRender",
"Parent:rendered",
"Parent:willPatch",
"Child:willUnmount",
"Child:willDestroy",
"Parent:patched",
]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:willRender",
"Parent:rendered",
"Parent:willPatch",
"Child:willUnmount",
"Child:willDestroy",
"Parent:patched",
]
`);
testContext.a = 456;
await nextTick();
expect([]).toBeLogged();
expect(steps.splice(0)).toMatchInlineSnapshot(`Array []`);
});
test("destroyed component before being mounted is inactive", async () => {
@@ -1396,7 +1396,7 @@
return this.getDOMElementsRecursive(node.content);
}
if (node.hasOwnProperty("el")) {
if (node.el instanceof HTMLElement || node.el instanceof Text) {
if (node.el instanceof Element || node.el instanceof Text) {
return [node.el];
}
}
@@ -1415,7 +1415,7 @@
}
}
if (node.hasOwnProperty("parentEl")) {
if (node.parentEl instanceof HTMLElement) {
if (node.parentEl instanceof Element) {
return [node.parentEl];
}
}
+4
View File
@@ -31,6 +31,10 @@ async function startRelease() {
log(`*** Owl release script ***`);
log(`Current Version: ${package.version}`);
log(`Warning: this script will push to the master branch!`);
log(`Make sure that github is configured to allow it:`);
log(` settings => branches => edit master => uncheck Do not allow bypassing the above settings`);
log(` (and probably a good idea to readd the protection after)`)
const STEPS = 11;
let step = 1;