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
10 changed files with 45 additions and 683 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@odoo/owl",
"version": "2.0.0-beta-5",
"version": "2.0.0-beta-4",
"description": "Odoo Web Library (OWL)",
"main": "dist/owl.cjs.js",
"browser": "dist/owl.iife.js",
+1 -1
View File
@@ -67,7 +67,7 @@ export class App<
}
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) {
+15 -23
View File
@@ -79,8 +79,13 @@ export function component<P extends object>(
let node: any = ctx.children[key];
let isDynamic = typeof name !== "string";
if (node && node.status === STATUS.DESTROYED) {
node = undefined;
if (node) {
if (node.status < STATUS.MOUNTED) {
node.destroy();
node = undefined;
} else if (node.status === STATUS.DESTROYED) {
node = undefined;
}
}
if (isDynamic && node && node.component.constructor !== name) {
node = undefined;
@@ -109,11 +114,13 @@ export function component<P extends object>(
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;
node.initiateRender(new Fiber(node, parentFiber));
}
parentFiber.childrenMap[key] = node;
parentFiber.root!.reachedChildren.add(node);
return node;
}
@@ -131,7 +138,6 @@ export class ComponentNode<P extends object = any, E = any> implements VNode<Com
bdom: BDom | null = null;
status: STATUS = STATUS.NEW;
forceNextRender: boolean = false;
parentKey: string | null;
renderFn: Function;
parent: ComponentNode | null;
@@ -148,17 +154,10 @@ export class ComponentNode<P extends object = any, E = any> implements VNode<Com
patched: LifecycleHook[] = [];
willDestroy: LifecycleHook[] = [];
constructor(
C: ComponentConstructor<P, E>,
props: P,
app: App,
parent: ComponentNode | null,
parentKey: string | null
) {
constructor(C: ComponentConstructor<P, E>, props: P, app: App, parent?: ComponentNode) {
currentNode = this;
this.app = app;
this.parent = parent;
this.parentKey = parentKey;
this.parent = parent || null;
this.level = parent ? parent.level + 1 : 0;
applyDefaultProps(props, C);
const env = (parent && parent.childEnv) || app.env;
@@ -195,7 +194,7 @@ export class ComponentNode<P extends object = any, E = any> implements VNode<Com
async render(deep: boolean = false) {
let current = this.fiber;
if (current && (current.root!.locked || (current as any).bdom === true)) {
if (current && current.root!.locked) {
await Promise.resolve();
// situation may have changed after the microtask tick
current = this.fiber;
@@ -217,7 +216,6 @@ export class ComponentNode<P extends object = any, E = any> implements VNode<Com
const fiber = makeRootFiber(this);
fiber.deep = deep;
this.fiber = fiber;
this.app.scheduler.addFiber(fiber);
await Promise.resolve();
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)) {
child._destroy();
}
if (this.willDestroy.length) {
try {
for (let cb of this.willDestroy) {
cb.call(component);
}
} catch (e) {
handleError({ error: e, node: this });
}
for (let cb of this.willDestroy) {
cb.call(component);
}
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 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) {
return false;
}
@@ -16,19 +16,23 @@ function _handleError(node: ComponentNode | null, error: any): boolean {
const errorHandlers = nodeErrorHandlers.get(node);
if (errorHandlers) {
let handled = false;
let stopped = false;
// execute in the opposite order
for (let i = errorHandlers.length - 1; i >= 0; i--) {
try {
errorHandlers[i](error);
handled = true;
stopped = true;
break;
} catch (e) {
error = e;
}
}
if (handled) {
if (stopped) {
if (isFirstRound && fiber && fiber.node.fiber) {
const root = fiber.root!;
root.setCounter(root.counter - 1);
}
return true;
}
}
@@ -51,7 +55,7 @@ export function handleError(params: ErrorParams) {
fibersInError.set(fiber.root!, error);
const handled = _handleError(node, error);
const handled = _handleError(node, error, true);
if (!handled) {
console.warn(`[Owl] Unhandled error. Destroying the root component`);
try {
+17 -10
View File
@@ -16,15 +16,13 @@ export function makeRootFiber(node: ComponentNode): Fiber {
let current = node.fiber;
if (current) {
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.locked = false;
current.children = [];
current.childrenMap = {};
current.bdom = null;
if (current === root) {
root.reachedChildren = new WeakSet();
}
if (fibersInError.has(current)) {
fibersInError.delete(current);
fibersInError.delete(root);
@@ -101,11 +99,16 @@ export class Fiber {
while (current) {
if (current.fiber) {
let root = current.fiber.root!;
if (root.counter === 0 && prev.parentKey! in current.fiber.childrenMap) {
current = root.node;
} else {
if (root.counter) {
scheduler.delayedRenders.push(this);
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;
@@ -118,15 +121,17 @@ export class Fiber {
_render() {
const node = this.node;
if (node.status > STATUS.MOUNTED) {
return;
}
const root = this.root;
if (root) {
try {
(this.bdom as any) = true;
this.bdom = node.renderFn();
root.setCounter(root.counter - 1);
} catch (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
locked: boolean = false;
reachedChildren: WeakSet<ComponentNode> = new WeakSet();
complete() {
const node = this.node;
this.locked = true;
+1 -1
View File
@@ -32,7 +32,7 @@ export class Scheduler {
let renders = this.delayedRenders;
this.delayedRenders = [];
for (let f of renders) {
if (f.root && f.node.status !== STATUS.DESTROYED) {
if (f.root) {
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`] = `
"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`] = `
"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`] = `
"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`] = `
"function anonymous(bdom, helpers
) {
-294
View File
@@ -4,7 +4,6 @@ import {
mount,
onMounted,
onRendered,
onWillDestroy,
onWillStart,
onWillUnmount,
onWillUpdateProps,
@@ -173,11 +172,6 @@ test("destroying/recreating a subcomponent, other scenario", async () => {
await nextTick();
expect([
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willDestroy",
"Parent:willRender",
"Child:setup",
"Child:willStart",
@@ -3696,294 +3690,6 @@ test("another scenario with delayed rendering", async () => {
]).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 () => {
// 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 {
onError,
onMounted,
@@ -1197,126 +1197,4 @@ describe("can catch errors", () => {
expect(fixture.innerHTML).toBe("<div>Child 2</div>");
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");
});
});