Compare commits

..

1 Commits

Author SHA1 Message Date
Lucas Perais (lpe) 45fa6745dd [FIX] component: fiber, don't render a destroyed component 2022-04-04 16:01:32 +02:00
14 changed files with 49 additions and 749 deletions
+1 -2
View File
@@ -76,8 +76,7 @@ The `Component` class has a very small API.
By default, the render initiated by this method will stop at each child By default, the render initiated by this method will stop at each child
component if their props are (shallow) equal. To force a render to update component if their props are (shallow) equal. To force a render to update
all child components, one can use the optional `deep` argument. Note that the all child components, one can use the optional `deep` argument.
value of the `deep` argument needs to be a boolean, not a truthy value.
## Static Properties ## Static Properties
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@odoo/owl", "name": "@odoo/owl",
"version": "2.0.0-beta-6", "version": "2.0.0-beta-4",
"description": "Odoo Web Library (OWL)", "description": "Odoo Web Library (OWL)",
"main": "dist/owl.cjs.js", "main": "dist/owl.cjs.js",
"browser": "dist/owl.iife.js", "browser": "dist/owl.iife.js",
+1 -1
View File
@@ -67,7 +67,7 @@ export class App<
} }
makeNode(Component: ComponentConstructor, props: any): ComponentNode { makeNode(Component: ComponentConstructor, props: any): ComponentNode {
return new ComponentNode(Component, props, this, null, null); return new ComponentNode(Component, props, this);
} }
mountNode(node: ComponentNode, target: HTMLElement, options?: MountOptions) { mountNode(node: ComponentNode, target: HTMLElement, options?: MountOptions) {
+1 -1
View File
@@ -37,6 +37,6 @@ export class Component<Props = any, Env = any> {
setup() {} setup() {}
render(deep: boolean = false) { render(deep: boolean = false) {
this.__owl__.render(deep === true); this.__owl__.render(deep);
} }
} }
+17 -25
View File
@@ -47,7 +47,7 @@ export function useState<T extends object>(state: T): Reactive<T> | NonReactive<
const node = getCurrent(); const node = getCurrent();
let render = batchedRenderFunctions.get(node)!; let render = batchedRenderFunctions.get(node)!;
if (!render) { if (!render) {
render = batched(node.render.bind(node, false)); render = batched(node.render.bind(node));
batchedRenderFunctions.set(node, render); batchedRenderFunctions.set(node, render);
// manual implementation of onWillDestroy to break cyclic dependency // manual implementation of onWillDestroy to break cyclic dependency
node.willDestroy.push(clearReactivesForCallback.bind(null, render)); node.willDestroy.push(clearReactivesForCallback.bind(null, render));
@@ -79,8 +79,13 @@ export function component<P extends object>(
let node: any = ctx.children[key]; let node: any = ctx.children[key];
let isDynamic = typeof name !== "string"; let isDynamic = typeof name !== "string";
if (node && node.status === STATUS.DESTROYED) { if (node) {
node = undefined; if (node.status < STATUS.MOUNTED) {
node.destroy();
node = undefined;
} else if (node.status === STATUS.DESTROYED) {
node = undefined;
}
} }
if (isDynamic && node && node.component.constructor !== name) { if (isDynamic && node && node.component.constructor !== name) {
node = undefined; node = undefined;
@@ -109,11 +114,13 @@ export function component<P extends object>(
throw new Error(`Cannot find the definition of component "${name}"`); throw new Error(`Cannot find the definition of component "${name}"`);
} }
} }
node = new ComponentNode(C, props, ctx.app, ctx, key); node = new ComponentNode(C, props, ctx.app, ctx);
ctx.children[key] = node; ctx.children[key] = node;
node.initiateRender(new Fiber(node, parentFiber)); node.initiateRender(new Fiber(node, parentFiber));
} }
parentFiber.childrenMap[key] = node; parentFiber.childrenMap[key] = node;
parentFiber.root!.reachedChildren.add(node);
return node; return node;
} }
@@ -131,7 +138,6 @@ export class ComponentNode<P extends object = any, E = any> implements VNode<Com
bdom: BDom | null = null; bdom: BDom | null = null;
status: STATUS = STATUS.NEW; status: STATUS = STATUS.NEW;
forceNextRender: boolean = false; forceNextRender: boolean = false;
parentKey: string | null;
renderFn: Function; renderFn: Function;
parent: ComponentNode | null; parent: ComponentNode | null;
@@ -148,17 +154,10 @@ export class ComponentNode<P extends object = any, E = any> implements VNode<Com
patched: LifecycleHook[] = []; patched: LifecycleHook[] = [];
willDestroy: LifecycleHook[] = []; willDestroy: LifecycleHook[] = [];
constructor( constructor(C: ComponentConstructor<P, E>, props: P, app: App, parent?: ComponentNode) {
C: ComponentConstructor<P, E>,
props: P,
app: App,
parent: ComponentNode | null,
parentKey: string | null
) {
currentNode = this; currentNode = this;
this.app = app; this.app = app;
this.parent = parent; this.parent = parent || null;
this.parentKey = parentKey;
this.level = parent ? parent.level + 1 : 0; this.level = parent ? parent.level + 1 : 0;
applyDefaultProps(props, C); applyDefaultProps(props, C);
const env = (parent && parent.childEnv) || app.env; const env = (parent && parent.childEnv) || app.env;
@@ -193,9 +192,9 @@ export class ComponentNode<P extends object = any, E = any> implements VNode<Com
} }
} }
async render(deep: boolean) { async render(deep: boolean = false) {
let current = this.fiber; let current = this.fiber;
if (current && (current.root!.locked || (current as any).bdom === true)) { if (current && current.root!.locked) {
await Promise.resolve(); await Promise.resolve();
// situation may have changed after the microtask tick // situation may have changed after the microtask tick
current = this.fiber; current = this.fiber;
@@ -217,7 +216,6 @@ export class ComponentNode<P extends object = any, E = any> implements VNode<Com
const fiber = makeRootFiber(this); const fiber = makeRootFiber(this);
fiber.deep = deep; fiber.deep = deep;
this.fiber = fiber; this.fiber = fiber;
this.app.scheduler.addFiber(fiber); this.app.scheduler.addFiber(fiber);
await Promise.resolve(); await Promise.resolve();
if (this.status === STATUS.DESTROYED) { if (this.status === STATUS.DESTROYED) {
@@ -257,14 +255,8 @@ export class ComponentNode<P extends object = any, E = any> implements VNode<Com
for (let child of Object.values(this.children)) { for (let child of Object.values(this.children)) {
child._destroy(); child._destroy();
} }
if (this.willDestroy.length) { for (let cb of this.willDestroy) {
try { cb.call(component);
for (let cb of this.willDestroy) {
cb.call(component);
}
} catch (e) {
handleError({ error: e, node: this });
}
} }
this.status = STATUS.DESTROYED; this.status = STATUS.DESTROYED;
} }
+9 -5
View File
@@ -5,7 +5,7 @@ import type { Fiber } from "./fibers";
export const fibersInError: WeakMap<Fiber, any> = new WeakMap(); export const fibersInError: WeakMap<Fiber, any> = new WeakMap();
export const nodeErrorHandlers: WeakMap<ComponentNode, ((error: any) => void)[]> = new WeakMap(); export const nodeErrorHandlers: WeakMap<ComponentNode, ((error: any) => void)[]> = new WeakMap();
function _handleError(node: ComponentNode | null, error: any): boolean { function _handleError(node: ComponentNode | null, error: any, isFirstRound = false): boolean {
if (!node) { if (!node) {
return false; return false;
} }
@@ -16,19 +16,23 @@ function _handleError(node: ComponentNode | null, error: any): boolean {
const errorHandlers = nodeErrorHandlers.get(node); const errorHandlers = nodeErrorHandlers.get(node);
if (errorHandlers) { if (errorHandlers) {
let handled = false; let stopped = false;
// execute in the opposite order // execute in the opposite order
for (let i = errorHandlers.length - 1; i >= 0; i--) { for (let i = errorHandlers.length - 1; i >= 0; i--) {
try { try {
errorHandlers[i](error); errorHandlers[i](error);
handled = true; stopped = true;
break; break;
} catch (e) { } catch (e) {
error = e; error = e;
} }
} }
if (handled) { if (stopped) {
if (isFirstRound && fiber && fiber.node.fiber) {
const root = fiber.root!;
root.setCounter(root.counter - 1);
}
return true; return true;
} }
} }
@@ -51,7 +55,7 @@ export function handleError(params: ErrorParams) {
fibersInError.set(fiber.root!, error); fibersInError.set(fiber.root!, error);
const handled = _handleError(node, error); const handled = _handleError(node, error, true);
if (!handled) { if (!handled) {
console.warn(`[Owl] Unhandled error. Destroying the root component`); console.warn(`[Owl] Unhandled error. Destroying the root component`);
try { try {
+17 -10
View File
@@ -16,15 +16,13 @@ export function makeRootFiber(node: ComponentNode): Fiber {
let current = node.fiber; let current = node.fiber;
if (current) { if (current) {
let root = current.root!; let root = current.root!;
// lock root fiber because canceling children fibers may destroy components,
// which means any arbitrary code can be run in onWillDestroy, which may
// trigger new renderings
root.locked = true;
root.setCounter(root.counter + 1 - cancelFibers(current.children)); root.setCounter(root.counter + 1 - cancelFibers(current.children));
root.locked = false;
current.children = []; current.children = [];
current.childrenMap = {}; current.childrenMap = {};
current.bdom = null; current.bdom = null;
if (current === root) {
root.reachedChildren = new WeakSet();
}
if (fibersInError.has(current)) { if (fibersInError.has(current)) {
fibersInError.delete(current); fibersInError.delete(current);
fibersInError.delete(root); fibersInError.delete(root);
@@ -101,11 +99,16 @@ export class Fiber {
while (current) { while (current) {
if (current.fiber) { if (current.fiber) {
let root = current.fiber.root!; let root = current.fiber.root!;
if (root.counter === 0 && prev.parentKey! in current.fiber.childrenMap) { if (root.counter) {
current = root.node;
} else {
scheduler.delayedRenders.push(this); scheduler.delayedRenders.push(this);
return; return;
} else {
if (!root.reachedChildren.has(prev)) {
// is dead. but we keep the render around just in case
scheduler.delayedRenders.push(this);
return;
}
current = root.node;
} }
} }
prev = current; prev = current;
@@ -118,15 +121,17 @@ export class Fiber {
_render() { _render() {
const node = this.node; const node = this.node;
if (node.status > STATUS.MOUNTED) {
return;
}
const root = this.root; const root = this.root;
if (root) { if (root) {
try { try {
(this.bdom as any) = true;
this.bdom = node.renderFn(); this.bdom = node.renderFn();
root.setCounter(root.counter - 1);
} catch (e) { } catch (e) {
handleError({ node, error: e }); handleError({ node, error: e });
} }
root.setCounter(root.counter - 1);
} }
} }
} }
@@ -142,6 +147,8 @@ export class RootFiber extends Fiber {
// i.e.: render triggered in onWillUnmount or in willPatch will be delayed // i.e.: render triggered in onWillUnmount or in willPatch will be delayed
locked: boolean = false; locked: boolean = false;
reachedChildren: WeakSet<ComponentNode> = new WeakSet();
complete() { complete() {
const node = this.node; const node = this.node;
this.locked = true; this.locked = true;
+1 -1
View File
@@ -32,7 +32,7 @@ export class Scheduler {
let renders = this.delayedRenders; let renders = this.delayedRenders;
this.delayedRenders = []; this.delayedRenders = [];
for (let f of renders) { for (let f of renders) {
if (f.root && f.node.status !== STATUS.DESTROYED) { if (f.root) {
f.render(); f.render();
} }
} }
@@ -1164,69 +1164,6 @@ exports[`delayed rendering, but then initial rendering is cancelled by yet anoth
}" }"
`; `;
exports[`delayed rendering, destruction, stuff happens 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
const b2 = text(\`A\`);
const b3 = component(\`B\`, {value: ctx['state'].value}, key + \`__1\`, node, ctx);
return multi([b2, b3]);
}
}"
`;
exports[`delayed rendering, destruction, stuff happens 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
let b2,b3;
b2 = text(\`B\`);
if (ctx['state'].hasChild) {
b3 = component(\`C\`, {value: ctx['state'].someValue+ctx['props'].value}, key + \`__1\`, node, ctx);
}
return multi([b2, b3]);
}
}"
`;
exports[`delayed rendering, destruction, stuff happens 3`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block4 = createBlock(\`<p><block-text-0/></p>\`);
return function template(ctx, node, key = \\"\\") {
const b2 = text(\`C\`);
const b3 = component(\`D\`, {}, key + \`__1\`, node, ctx);
let txt1 = ctx['props'].value;
const b4 = block4([txt1]);
return multi([b2, b3, b4]);
}
}"
`;
exports[`delayed rendering, destruction, stuff happens 4`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block3 = createBlock(\`<button block-handler-0=\\"click\\"><block-text-1/></button>\`);
return function template(ctx, node, key = \\"\\") {
const b2 = text(\`D\`);
let hdlr1 = [ctx['increment'], ctx];
let txt1 = ctx['state'].val;
const b3 = block3([hdlr1, txt1]);
return multi([b2, b3]);
}
}"
`;
exports[`delayed rendering, reusing fiber and stuff 1`] = ` exports[`delayed rendering, reusing fiber and stuff 1`] = `
"function anonymous(bdom, helpers "function anonymous(bdom, helpers
) { ) {
@@ -1352,46 +1289,6 @@ exports[`delayed rendering, then component is destroyed and stuff 3`] = `
}" }"
`; `;
exports[`destroyed component causes other soon to be destroyed component to rerender, weird stuff happens 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
let b2,b3;
b2 = text(\` A \`);
if (ctx['state'].flag) {
const b4 = component(\`B\`, {value: ctx['state'].valueB}, key + \`__1\`, node, ctx);
const b5 = component(\`C\`, {value: ctx['state'].valueC}, key + \`__2\`, node, ctx);
b3 = multi([b4, b5]);
}
return multi([b2, b3]);
}
}"
`;
exports[`destroyed component causes other soon to be destroyed component to rerender, weird stuff happens 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(ctx['props'].value);
}
}"
`;
exports[`destroyed component causes other soon to be destroyed component to rerender, weird stuff happens 3`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(ctx['state'].val+ctx['props'].value);
}
}"
`;
exports[`destroying/recreating a subcomponent, other scenario 1`] = ` exports[`destroying/recreating a subcomponent, other scenario 1`] = `
"function anonymous(bdom, helpers "function anonymous(bdom, helpers
) { ) {
@@ -1569,70 +1466,6 @@ exports[`rendering parent twice, with different props on child and stuff 2`] = `
}" }"
`; `;
exports[`renderings, destruction, patch, stuff, ... yet another variation 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
const b2 = text(\`A\`);
const b3 = component(\`B\`, {value: ctx['state'].value}, key + \`__1\`, node, ctx);
const b4 = component(\`D\`, {}, key + \`__2\`, node, ctx);
return multi([b2, b3, b4]);
}
}"
`;
exports[`renderings, destruction, patch, stuff, ... yet another variation 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
let b2,b3;
b2 = text(\`B\`);
if (ctx['props'].value===33) {
b3 = component(\`C\`, {}, key + \`__1\`, node, ctx);
}
return multi([b2, b3]);
}
}"
`;
exports[`renderings, destruction, patch, stuff, ... yet another variation 3`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block3 = createBlock(\`<p block-handler-0=\\"click\\"><block-text-1/></p>\`);
return function template(ctx, node, key = \\"\\") {
const b2 = text(\`D\`);
let hdlr1 = [ctx['increment'], ctx];
let txt1 = ctx['state'].val;
const b3 = block3([hdlr1, txt1]);
return multi([b2, b3]);
}
}"
`;
exports[`renderings, destruction, patch, stuff, ... yet another variation 4`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block3 = createBlock(\`<span block-handler-0=\\"click\\"><block-text-1/></span>\`);
return function template(ctx, node, key = \\"\\") {
const b2 = text(\`C\`);
let hdlr1 = [ctx['increment'], ctx];
let txt1 = ctx['state'].val;
const b3 = block3([hdlr1, txt1]);
return multi([b2, b3]);
}
}"
`;
exports[`t-foreach with dynamic async component 1`] = ` exports[`t-foreach with dynamic async component 1`] = `
"function anonymous(bdom, helpers "function anonymous(bdom, helpers
) { ) {
@@ -85,64 +85,6 @@ exports[`basics simple catchError 2`] = `
}" }"
`; `;
exports[`can catch errors an error in onWillDestroy 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
let b2,b3;
b2 = text(ctx['state'].value);
if (ctx['state'].hasChild) {
b3 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
}
return multi([b2, b3]);
}
}"
`;
exports[`can catch errors an error in onWillDestroy 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div>abc</div>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`can catch errors an error in onWillDestroy, variation 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
let b2,b3;
b2 = text(ctx['state'].value);
if (ctx['state'].hasChild) {
b3 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
}
return multi([b2, b3]);
}
}"
`;
exports[`can catch errors an error in onWillDestroy, variation 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
let block1 = createBlock(\`<div>abc</div>\`);
return function template(ctx, node, key = \\"\\") {
return block1();
}
}"
`;
exports[`can catch errors calling a hook outside setup should crash 1`] = ` exports[`can catch errors calling a hook outside setup should crash 1`] = `
"function anonymous(bdom, helpers "function anonymous(bdom, helpers
) { ) {
@@ -127,30 +127,6 @@ exports[`rendering semantics props are reactive 2`] = `
}" }"
`; `;
exports[`rendering semantics render need a boolean = true to be 'deep' 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
const b2 = text(ctx['state'].value);
const b3 = component(\`Child\`, {}, key + \`__1\`, node, ctx);
return multi([b2, b3]);
}
}"
`;
exports[`rendering semantics render need a boolean = true to be 'deep' 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(\`child\`);
}
}"
`;
exports[`rendering semantics render with deep=true followed by render with deep=false work as expected 1`] = ` exports[`rendering semantics render with deep=true followed by render with deep=false work as expected 1`] = `
"function anonymous(bdom, helpers "function anonymous(bdom, helpers
) { ) {
-294
View File
@@ -4,7 +4,6 @@ import {
mount, mount,
onMounted, onMounted,
onRendered, onRendered,
onWillDestroy,
onWillStart, onWillStart,
onWillUnmount, onWillUnmount,
onWillUpdateProps, onWillUpdateProps,
@@ -173,11 +172,6 @@ test("destroying/recreating a subcomponent, other scenario", async () => {
await nextTick(); await nextTick();
expect([ expect([
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willDestroy",
"Parent:willRender", "Parent:willRender",
"Child:setup", "Child:setup",
"Child:willStart", "Child:willStart",
@@ -3696,294 +3690,6 @@ test("another scenario with delayed rendering", async () => {
]).toBeLogged(); ]).toBeLogged();
}); });
test("destroyed component causes other soon to be destroyed component to rerender, weird stuff happens", async () => {
let def = makeDeferred();
let c: any = null;
class B extends Component {
static template = xml`<t t-esc="props.value"/>`;
setup() {
useLogLifecycle();
onRendered(() => {
def.resolve();
});
onWillDestroy(() => {
c.state.val++;
c.render();
});
}
}
class C extends Component {
static template = xml`<t t-esc="state.val + props.value"/>`;
state = useState({ val: 0 });
setup() {
c = this;
useLogLifecycle();
}
}
class A extends Component {
static template = xml`
A
<t t-if="state.flag">
<B value="state.valueB"/>
<C value="state.valueC"/>
</t>`;
static components = { B, C };
state = useState({ flag: false, valueB: 1, valueC: 2 });
setup() {
useLogLifecycle();
}
}
const parent = await mount(A, fixture);
expect(fixture.innerHTML).toBe(" A ");
expect(["A:setup", "A:willStart", "A:willRender", "A:rendered", "A:mounted"]).toBeLogged();
// initiate a render in A, but is blocked in B
parent.state.flag = true;
await def;
await nextMicroTick();
expect([
"A:willRender",
"B:setup",
"B:willStart",
"C:setup",
"C:willStart",
"A:rendered",
"B:willRender",
"B:rendered",
"C:willRender",
"C:rendered",
]).toBeLogged();
// initiate render in A => will cancel renders in B/C and restarts
parent.state.valueB = 2;
await nextTick();
expect([
"B:willDestroy",
"C:willDestroy",
"A:willRender",
"B:setup",
"B:willStart",
"C:setup",
"C:willStart",
"A:rendered",
"B:willRender",
"B:rendered",
"C:willRender",
"C:rendered",
"A:willPatch",
"C:mounted",
"B:mounted",
"A:patched",
]).toBeLogged();
expect(fixture.innerHTML).toBe(" A 22");
});
test("delayed rendering, destruction, stuff happens", async () => {
const promC = makeDeferred();
let stateB: any = null;
class D extends Component {
static template = xml`D<button t-on-click="increment"><t t-esc="state.val"/></button>`;
state = useState({ val: 1 });
setup() {
useLogLifecycle();
}
increment() {
this.state.val++;
}
}
class C extends Component {
static template = xml`C<D/><p><t t-esc="props.value"/></p>`;
static components = { D };
setup() {
useLogLifecycle();
onWillUpdateProps(() => promC);
}
}
class B extends Component {
static template = xml`B<t t-if="state.hasChild"><C value="state.someValue + props.value"/></t>`;
static components = { C };
state = useState({ someValue: 3, hasChild: true });
setup() {
useLogLifecycle();
stateB = this.state;
}
}
class A extends Component {
static template = xml`A<B value="state.value"/>`;
static components = { B };
state = useState({ value: 33 });
setup() {
useLogLifecycle();
}
}
const parent = await mount(A, fixture);
expect(fixture.innerHTML).toBe("ABCD<button>1</button><p>36</p>");
expect([
"A:setup",
"A:willStart",
"A:willRender",
"B:setup",
"B:willStart",
"A:rendered",
"B:willRender",
"C:setup",
"C:willStart",
"B:rendered",
"C:willRender",
"D:setup",
"D:willStart",
"C:rendered",
"D:willRender",
"D:rendered",
"D:mounted",
"C:mounted",
"B:mounted",
"A:mounted",
]).toBeLogged();
// render in A, it updates B and C, but render is blocked in C
parent.state.value = 50;
await nextTick();
expect([
"A:willRender",
"B:willUpdateProps",
"A:rendered",
"B:willRender",
"C:willUpdateProps",
"B:rendered",
]).toBeLogged();
// update B => removes child C
stateB.hasChild = false;
// update D => render should be delayed, because AB is currently rendering
fixture.querySelector("button")!.click();
await nextTick();
expect([
"B:willRender",
"B:rendered",
"A:willPatch",
"B:willPatch",
"C:willUnmount",
"D:willUnmount",
"D:willDestroy",
"C:willDestroy",
"B:patched",
"A:patched",
]).toBeLogged();
expect(fixture.innerHTML).toBe("AB");
});
test("renderings, destruction, patch, stuff, ... yet another variation", async () => {
const promB = makeDeferred();
class D extends Component {
static template = xml`D<p t-on-click="increment"><t t-esc="state.val"/></p>`;
state = useState({ val: 1 });
setup() {
useLogLifecycle();
}
increment() {
this.state.val++;
}
}
// almost the same as D
class C extends Component {
static template = xml`C<span t-on-click="increment"><t t-esc="state.val"/></span>`;
state = useState({ val: 1 });
setup() {
useLogLifecycle();
}
increment() {
this.state.val++;
}
}
class B extends Component {
static template = xml`B<t t-if="props.value === 33"><C/></t>`;
static components = { C };
setup() {
useLogLifecycle();
onWillUpdateProps(() => promB);
}
}
class A extends Component {
static template = xml`A<B value="state.value"/><D/>`;
static components = { B, D };
state = useState({ value: 33 });
setup() {
useLogLifecycle();
}
}
const parent = await mount(A, fixture);
expect(fixture.innerHTML).toBe("ABC<span>1</span>D<p>1</p>");
expect([
"A:setup",
"A:willStart",
"A:willRender",
"B:setup",
"B:willStart",
"D:setup",
"D:willStart",
"A:rendered",
"B:willRender",
"C:setup",
"C:willStart",
"B:rendered",
"D:willRender",
"D:rendered",
"C:willRender",
"C:rendered",
"C:mounted",
"D:mounted",
"B:mounted",
"A:mounted",
]).toBeLogged();
// render in A, it updates B, will remove C, stopped in B
parent.state.value = 50;
await nextTick();
expect(["A:willRender", "B:willUpdateProps", "A:rendered"]).toBeLogged();
// update C => render should be delayed, because AB is currently rendering
fixture.querySelector("span")!.click();
await nextTick();
expect([]).toBeLogged();
// resolve prom B => render is done, component C is destroyed
promB.resolve();
await nextTick();
expect([
"B:willRender",
"B:rendered",
"A:willPatch",
"B:willPatch",
"C:willUnmount",
"C:willDestroy",
"B:patched",
"A:patched",
]).toBeLogged();
expect(fixture.innerHTML).toBe("ABD<p>1</p>");
// update D => should just render completely independently
fixture.querySelector("p")!.click();
await nextTick();
expect(["D:willRender", "D:rendered", "D:willPatch", "D:patched"]).toBeLogged();
expect(fixture.innerHTML).toBe("ABD<p>2</p>");
});
// test.skip("components with shouldUpdate=false", async () => { // test.skip("components with shouldUpdate=false", async () => {
// const state = { p: 1, cc: 10 }; // const state = { p: 1, cc: 10 };
+1 -123
View File
@@ -1,4 +1,4 @@
import { Component, mount, onWillDestroy } from "../../src"; import { Component, mount } from "../../src";
import { import {
onError, onError,
onMounted, onMounted,
@@ -1197,126 +1197,4 @@ describe("can catch errors", () => {
expect(fixture.innerHTML).toBe("<div>Child 2</div>"); expect(fixture.innerHTML).toBe("<div>Child 2</div>");
expect(steps).toEqual(["Error Component"]); expect(steps).toEqual(["Error Component"]);
}); });
test("an error in onWillDestroy", async () => {
class Child extends Component {
static template = xml`<div>abc</div>`;
setup() {
useLogLifecycle();
onWillDestroy(() => {
throw new Error("boom");
});
}
}
class Parent extends Component {
static template = xml`
<t t-esc="state.value"/>
<t t-if="state.hasChild"><Child/></t>`;
static components = { Child };
state = useState({ value: 1, hasChild: true });
setup() {
useLogLifecycle();
onError(() => {
this.state.value++;
});
}
}
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();
parent.state.hasChild = false;
await nextTick();
await nextTick();
await nextTick();
await nextTick();
expect([
"Parent:willRender",
"Parent:rendered",
"Parent:willPatch",
"Child:willUnmount",
"Child:willDestroy",
"Parent:willRender",
"Parent:rendered",
"Parent:willPatch",
"Parent:patched",
]).toBeLogged();
expect(fixture.innerHTML).toBe("2");
});
test("an error in onWillDestroy, variation", async () => {
class Child extends Component {
static template = xml`<div>abc</div>`;
setup() {
useLogLifecycle();
onWillDestroy(() => {
throw new Error("boom");
});
}
}
class Parent extends Component {
static template = xml`
<t t-esc="state.value"/>
<t t-if="state.hasChild"><Child/></t>`;
static components = { Child };
state = useState({ value: 1, hasChild: false });
setup() {
useLogLifecycle();
onError(() => {
this.state.value++;
});
}
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("1");
expect([
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Parent:rendered",
"Parent:mounted",
]).toBeLogged();
parent.state.hasChild = true;
await nextMicroTick();
await nextMicroTick();
await nextMicroTick();
await nextMicroTick();
await nextMicroTick();
expect([
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
]).toBeLogged();
parent.state.hasChild = false;
await nextTick();
expect([
"Child:willDestroy",
"Parent:willRender",
"Parent:rendered",
"Parent:willPatch",
"Parent:patched",
]).toBeLogged();
expect(fixture.innerHTML).toBe("2");
});
}); });
-37
View File
@@ -102,43 +102,6 @@ describe("rendering semantics", () => {
expect(childN).toBe(2); expect(childN).toBe(2);
}); });
test("render need a boolean = true to be 'deep'", async () => {
let childN = 0;
let parentN = 0;
class Child extends Component {
static template = xml`child`;
setup() {
onRendered(() => childN++);
}
}
class Parent extends Component {
static template = xml`
<t t-esc="state.value"/>
<Child/>
`;
static components = { Child };
state = { value: "A" };
setup() {
onRendered(() => parentN++);
}
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("Achild");
expect(parentN).toBe(1);
expect(childN).toBe(1);
parent.state.value = "B";
parent.render("true" as any as boolean);
await nextTick();
expect(fixture.innerHTML).toBe("Bchild");
expect(parentN).toBe(2);
expect(childN).toBe(1);
});
test("render with deep=true followed by render with deep=false work as expected", async () => { test("render with deep=true followed by render with deep=false work as expected", async () => {
class Child extends Component { class Child extends Component {
static template = xml`child<t t-esc="env.getValue()"/>`; static template = xml`child<t t-esc="env.getValue()"/>`;