import {
App,
Component,
mount,
useState,
xml,
onWillPatch,
onWillUnmount,
onPatched,
onWillUpdateProps,
onWillRender,
onWillDestroy,
onRendered,
onMounted,
onWillStart,
} from "../../src";
import { status } from "../../src/runtime/status";
import {
elem,
logStep,
makeDeferred,
makeTestFixture,
nextMicroTick,
nextTick,
snapshotEverything,
steps,
useLogLifecycle,
} from "../helpers";
let fixture: HTMLElement;
snapshotEverything();
beforeEach(() => {
fixture = makeTestFixture();
});
describe("lifecycle hooks", () => {
test("basic checks for a component", async () => {
expect.assertions(6); // 1 for snapshots
class Test extends Component {
static template = xml`test `;
setup() {
expect(status(this)).toBe("new");
}
}
const app = new App(Test);
const component = await app.mount(fixture);
expect(fixture.innerHTML).toBe("test ");
expect(status(component)).toBe("mounted");
app.destroy();
expect(fixture.innerHTML).toBe("");
expect(status(component)).toBe("destroyed");
});
test("willStart is called", async () => {
let willstart = false;
class Test extends Component {
static template = xml`simple vnode `;
setup() {
onWillStart(() => {
willstart = true;
});
}
}
await mount(Test, fixture);
expect(willstart).toBe(true);
});
test("willStart hook is called on sub component", async () => {
let ok = false;
class Child extends Component {
static template = xml`
`;
setup() {
onWillStart(() => {
ok = true;
});
}
}
class Parent extends Component {
static template = xml` `;
static components = { Child };
}
await mount(Parent, fixture);
expect(ok).toBe(true);
});
test("willStart is called with component as this", async () => {
expect.assertions(3);
let comp: any;
class Test extends Component {
static template = xml`simple vnode `;
setup() {
comp = this;
onWillStart(this.willStart);
}
willStart() {
expect(this).toBeInstanceOf(Test);
expect(this).toBe(comp);
}
}
await mount(Test, fixture);
});
test("timeout in onWillStart emits a console log", async () => {
const { log } = console;
let logArgs: any[];
console.log = jest.fn((...args) => (logArgs = args));
const { setTimeout } = window;
let timeoutCbs: any = {};
let timeoutId = 0;
window.setTimeout = ((cb: any) => {
timeoutCbs[++timeoutId] = cb;
return timeoutId;
}) as any;
try {
class Test extends Component {
static template = xml` `;
setup() {
onWillStart(() => new Promise(() => {}));
}
}
mount(Test, fixture, { test: true });
nextTick();
for (const id in timeoutCbs) {
timeoutCbs[id]();
delete timeoutCbs[id];
}
await nextMicroTick();
await nextMicroTick();
expect(console.log).toHaveBeenCalledTimes(1);
expect(logArgs![0]!.message).toBe("onWillStart's promise hasn't resolved after 3 seconds");
} finally {
console.log = log;
window.setTimeout = setTimeout;
}
});
test("timeout in onWillStart doesn't emit a console log if app is destroyed", async () => {
const { log } = console;
console.log = jest.fn();
const { setTimeout } = window;
let timeoutCbs: any = {};
let timeoutId = 0;
window.setTimeout = ((cb: any) => {
timeoutCbs[++timeoutId] = cb;
return timeoutId;
}) as any;
try {
class Test extends Component {
static template = xml` `;
setup() {
onWillStart(() => new Promise(() => {}));
}
}
const app = new App(Test, { test: true });
app.mount(fixture);
app.destroy();
for (const id in timeoutCbs) {
timeoutCbs[id]();
delete timeoutCbs[id];
}
await nextMicroTick();
await nextMicroTick();
expect(console.log).toHaveBeenCalledTimes(0);
} finally {
console.log = log;
window.setTimeout = setTimeout;
}
});
test("timeout in onWillUpdateProps emits a console log", async () => {
class Child extends Component {
static template = xml``;
setup() {
onWillUpdateProps(() => new Promise(() => {}));
}
}
class Parent extends Component {
static template = xml` `;
static components = { Child };
state = useState({ prop: 1 });
}
const parent = await mount(Parent, fixture, { test: true });
const { log } = console;
let logArgs: any[];
console.log = jest.fn((...args) => (logArgs = args));
const { setTimeout } = window;
let timeoutCbs: any = {};
let timeoutId = 0;
window.setTimeout = ((cb: any) => {
timeoutCbs[++timeoutId] = cb;
return timeoutId;
}) as any;
try {
parent.state.prop = 2;
let tick = nextTick();
for (const id in timeoutCbs) {
timeoutCbs[id]();
delete timeoutCbs[id];
}
await tick;
tick = nextTick();
for (const id in timeoutCbs) {
timeoutCbs[id]();
delete timeoutCbs[id];
}
await tick;
expect(console.log).toHaveBeenCalledTimes(1);
expect(logArgs![0]!.message).toBe(
"onWillUpdateProps's promise hasn't resolved after 3 seconds"
);
} finally {
console.log = log;
window.setTimeout = setTimeout;
}
});
test("mounted hook is called if mounted in DOM", async () => {
let mounted = false;
class Test extends Component {
static template = xml`
`;
setup() {
onMounted(() => {
mounted = true;
});
}
}
await mount(Test, fixture);
expect(mounted).toBe(true);
});
test("mounted hook is called on subcomponents, in proper order", async () => {
const steps: any[] = [];
class Child extends Component {
static template = xml`
`;
setup() {
onMounted(() => {
expect(document.body.contains(elem(this))).toBe(true);
steps.push("child:mounted");
});
}
}
class Parent extends Component {
static template = xml`
`;
static components = { Child };
setup() {
onMounted(() => {
steps.push("parent:mounted");
});
}
}
await mount(Parent, fixture);
expect(steps).toEqual(["child:mounted", "parent:mounted"]);
Object.freeze(steps);
});
test("mounted hook is called on subsubcomponents, in proper order", async () => {
const steps: any[] = [];
class ChildChild extends Component {
static template = xml`
`;
setup() {
onMounted(() => {
steps.push("childchild:mounted");
});
onWillUnmount(() => {
steps.push("childchild:willUnmount");
});
}
}
class Child extends Component {
static template = xml`
`;
static components = { ChildChild };
setup() {
onMounted(() => {
steps.push("child:mounted");
});
onWillUnmount(() => {
steps.push("child:willUnmount");
});
}
}
class Parent extends Component {
static template = xml`
`;
static components = { Child };
state = useState({ flag: false });
setup() {
onMounted(() => {
steps.push("parent:mounted");
});
onWillUnmount(() => {
steps.push("parent:willUnmount");
});
}
}
const app = new App(Parent);
const widget = await app.mount(fixture);
expect(steps).toEqual(["parent:mounted"]);
widget.state.flag = true;
await nextTick();
app.destroy();
expect(steps).toEqual([
"parent:mounted",
"childchild:mounted",
"child:mounted",
"parent:willUnmount",
"child:willUnmount",
"childchild:willUnmount",
]);
Object.freeze(steps);
});
test("willPatch, patched hook are called on subsubcomponents, in proper order", async () => {
const steps: any[] = [];
class ChildChild extends Component {
static template = xml`
`;
setup() {
onWillPatch(() => {
steps.push("childchild:willPatch");
});
onPatched(() => {
steps.push("childchild:patched");
});
}
}
class Child extends Component {
static template = xml`
`;
static components = { ChildChild };
setup() {
onWillPatch(() => {
steps.push("child:willPatch");
});
onPatched(() => {
steps.push("child:patched");
});
}
}
class Parent extends Component {
static template = xml`
`;
static components = { Child };
state = useState({ n: 1 });
setup() {
onWillPatch(() => {
steps.push("parent:willPatch");
});
onPatched(() => {
steps.push("parent:patched");
});
}
}
const app = new App(Parent);
const parent = await app.mount(fixture);
expect(steps).toEqual([]);
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);
});
test("willStart, mounted on subwidget rendered after main is mounted in some other position", async () => {
const steps: string[] = [];
class Child extends Component {
static template = xml`
`;
setup() {
onWillStart(() => {
steps.push("child:willStart");
});
onMounted(() => {
steps.push("child:mounted");
});
}
}
class Parent extends Component {
// the t-else part in the template is important. This is
// necessary to have a situation that could confuse the vdom
// patching algorithm
static template = xml`
`;
static components = { Child };
state = useState({ ok: false });
}
const parent = await mount(Parent, fixture);
expect(steps).toEqual([]);
parent.state.ok = true;
await nextTick();
expect(steps).toEqual(["child:willStart", "child:mounted"]);
Object.freeze(steps);
Object.freeze(steps);
});
test("components are unmounted destroyed if no longer in DOM", async () => {
let steps: string[] = [];
class Child extends Component {
static template = xml`
`;
setup() {
steps.push("setup");
onWillStart(() => {
steps.push("willstart");
});
onMounted(() => {
steps.push("mounted");
});
onWillUnmount(() => {
steps.push("willunmount");
});
}
}
class Parent extends Component {
static template = xml` `;
static components = { Child };
state = useState({ ok: true });
}
const parent = await mount(Parent, fixture);
expect(steps).toEqual(["setup", "willstart", "mounted"]);
parent.state.ok = false;
await nextTick();
expect(steps).toEqual(["setup", "willstart", "mounted", "willunmount"]);
Object.freeze(steps);
});
test("components are unmounted and destroyed if no longer in DOM, even after updateprops", async () => {
class Child extends Component {
static template = xml` `;
setup() {
useLogLifecycle();
}
}
class Parent extends Component {
static template = xml`
`;
static components = { Child };
setup() {
useLogLifecycle();
}
state = useState({ n: 0, flag: true });
increment() {
this.state.n += 1;
}
toggleSubWidget() {
this.state.flag = !this.state.flag;
}
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("0
");
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted",
"Parent:mounted",
]
`);
parent.increment();
await nextTick();
expect(fixture.innerHTML).toBe("1
");
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:willRender",
"Child:willUpdateProps",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Parent:willPatch",
"Child:willPatch",
"Child:patched",
"Parent:patched",
]
`);
parent.toggleSubWidget();
await nextTick();
expect(fixture.innerHTML).toBe("");
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 () => {
class Child extends Component {
static template = xml`
`;
setup() {
useLogLifecycle();
}
}
class Parent extends Component {
static template = xml`
`;
static components = { Child };
setup() {
useLogLifecycle();
}
}
const app = new App(Parent);
await app.mount(fixture);
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted",
"Parent:mounted",
]
`);
app.destroy();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:willUnmount",
"Child:willUnmount",
"Child:willDestroy",
"Parent:willDestroy",
]
`);
});
test("willUpdateProps hook is called", async () => {
let def = makeDeferred();
class Child extends Component {
static template = xml` `;
setup() {
onWillUpdateProps((nextProps) => {
expect(nextProps.n).toBe(2);
return def;
});
}
}
class Parent extends Component {
static template = xml` `;
static components = { Child };
state = useState({ n: 1 });
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("1 ");
parent.state.n = 2;
await nextTick();
expect(fixture.innerHTML).toBe("1 ");
def.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("2 ");
});
test("patched hook is called after updating State", async () => {
let n = 0;
class Test extends Component {
static template = xml`
`;
state = useState({ a: 1 });
setup() {
onPatched(() => n++);
}
}
const widget = await mount(Test, fixture);
expect(n).toBe(0);
widget.state.a = 1; // empty update, should do nothing
await nextTick();
expect(n).toBe(0);
widget.state.a = 3;
await nextTick();
expect(n).toBe(1);
});
test("patched hook is called after updateProps", async () => {
let n = 0;
class Child extends Component {
static template = xml`
`;
setup() {
onWillUpdateProps(() => {
n++;
});
}
}
class Parent extends Component {
static template = xml`
`;
state = useState({ a: 1 });
static components = { Child };
}
const widget = await mount(Parent, fixture);
expect(n).toBe(0);
widget.state.a = 2;
await nextTick();
expect(n).toBe(1);
});
test("lifecycle semantics", async () => {
class Child extends Component {
static template = xml`
`;
setup() {
useLogLifecycle();
}
}
class Parent extends Component {
static template = xml`
`;
static components = { Child };
state = useState({ a: 1 });
setup() {
useLogLifecycle();
}
}
const app = new App(Parent);
await app.mount(fixture);
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted",
"Parent:mounted",
]
`);
app.destroy();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:willUnmount",
"Child:willUnmount",
"Child:willDestroy",
"Parent:willDestroy",
]
`);
});
test("lifecycle semantics, part 2", async () => {
class GrandChild extends Component {
static template = xml`
`;
setup() {
useLogLifecycle();
}
}
class Child extends Component {
static template = xml` `;
static components = { GrandChild };
setup() {
useLogLifecycle();
}
}
class Parent extends Component {
static template = xml` `;
static components = { Child };
state = useState({ hasChild: false });
setup() {
useLogLifecycle();
}
}
const app = new App(Parent);
const parent = await app.mount(fixture);
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Parent:rendered",
"Parent:mounted",
]
`);
parent.state.hasChild = true;
await nextTick();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"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",
]
`);
app.destroy();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:willUnmount",
"Child:willUnmount",
"GrandChild:willUnmount",
"GrandChild:willDestroy",
"Child:willDestroy",
"Parent:willDestroy",
]
`);
});
test("lifecycle semantics, part 3", async () => {
class GrandChild extends Component {
static template = xml`
`;
setup() {
useLogLifecycle();
}
}
class Child extends Component {
static template = xml` `;
static components = { GrandChild };
setup() {
useLogLifecycle();
}
}
class Parent extends Component {
static template = xml` `;
static components = { Child };
state = useState({ hasChild: false });
setup() {
useLogLifecycle();
}
}
const app = new App(Parent);
const parent = await app.mount(fixture);
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(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:willUnmount",
"Parent:willDestroy",
]
`);
});
test("lifecycle semantics, part 4", async () => {
let def = makeDeferred();
class GrandChild extends Component {
static template = xml`
`;
setup() {
useLogLifecycle();
onWillStart(() => def);
}
}
class Child extends Component {
static template = xml` `;
static components = { GrandChild };
setup() {
useLogLifecycle();
}
}
class Parent extends Component {
static template = xml` `;
static components = { Child };
state = useState({ hasChild: false });
setup() {
useLogLifecycle();
}
}
const app = new App(Parent);
const parent = await app.mount(fixture);
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Parent:rendered",
"Parent:mounted",
]
`);
parent.state.hasChild = true;
await nextTick();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"GrandChild:setup",
"GrandChild:willStart",
"Child:rendered",
]
`);
app.destroy();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:willUnmount",
"GrandChild:willDestroy",
"Child:willDestroy",
"Parent:willDestroy",
]
`);
});
test("lifecycle semantics, part 5", async () => {
class Child extends Component {
static template = xml`
`;
setup() {
useLogLifecycle();
}
}
class Parent extends Component {
static template = xml` `;
static components = { Child };
state = useState({ hasChild: true });
setup() {
useLogLifecycle();
}
}
const parent = await mount(Parent, fixture);
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted",
"Parent:mounted",
]
`);
parent.state.hasChild = false;
await nextTick();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:willRender",
"Parent:rendered",
"Parent:willPatch",
"Child:willUnmount",
"Child:willDestroy",
"Parent:patched",
]
`);
});
test("lifecycle semantics, part 6", async () => {
class Child extends Component {
static template = xml`
`;
setup() {
useLogLifecycle();
}
}
class Parent extends Component {
static template = xml` `;
static components = { Child };
state = useState({ value: 1 });
setup() {
useLogLifecycle();
}
}
const parent = await mount(Parent, fixture);
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted",
"Parent:mounted",
]
`);
parent.state.value = 2;
await nextTick();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:willRender",
"Child:willUpdateProps",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Parent:willPatch",
"Child:willPatch",
"Child:patched",
"Parent:patched",
]
`);
});
test("onWillRender", async () => {
const def = makeDeferred();
class Child extends Component {
static template = xml` `;
state = useState({ value: 1 });
visibleState = this.state.value;
setup() {
useLogLifecycle();
onWillUpdateProps(() => def);
onWillRender(() => (this.visibleState = this.state.value));
}
increment() {
this.state.value++;
}
}
class Parent extends Component {
static template = xml`
`;
static components = { Child };
state = useState({ value: 1 });
setup() {
useLogLifecycle();
}
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("1 ");
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted",
"Parent:mounted",
]
`);
parent.state.value++; // to block child render
await nextTick();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:willRender",
"Child:willUpdateProps",
"Parent:rendered",
]
`);
fixture.querySelector("button")!.click();
await nextTick();
expect(steps.splice(0)).toMatchInlineSnapshot(`Array []`);
fixture.querySelector("button")!.click();
await nextTick();
expect(steps.splice(0)).toMatchInlineSnapshot(`Array []`);
expect(fixture.innerHTML).toBe("1 ");
def.resolve();
await nextTick();
expect(fixture.innerHTML).toBe("3 ");
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)
test("sub widget (inside sub node): hooks are correctly called", async () => {
let created = false;
let mounted = false;
class Child extends Component {
static template = xml`
`;
setup() {
created = true;
onMounted(() => {
mounted = true;
});
}
}
class Parent extends Component {
static template = xml` `;
static components = { Child };
state = useState({ flag: false });
}
const parent = await mount(Parent, fixture);
expect(created).toBe(false);
expect(mounted).toBe(false);
parent.state.flag = true;
await nextTick();
expect(mounted).toBe(true);
expect(created).toBe(true);
});
// TODO: rename (corresponds to https://github.com/odoo/owl/blob/master/doc/reference/concurrency_model.md#semantics)
test("component semantics", async () => {
class TestWidget extends Component {
name: string = "test";
setup() {
useLogLifecycle();
}
}
class B extends TestWidget {
static template = xml`B
`;
name = "B";
}
class D extends TestWidget {
static template = xml`D
`;
name = "D";
}
class E extends TestWidget {
static template = xml`E
`;
name = "E";
}
class F extends TestWidget {
static template = xml`F
`;
name = "F";
}
let c: C;
class C extends TestWidget {
static template = xml`
C
`;
static components = { D, E, F };
name = "C";
state = useState({ flag: true });
setup() {
c = this;
super.setup();
}
}
class A extends TestWidget {
static template = xml`A
`;
static components = { B, C };
name = "A";
}
await mount(A, fixture);
expect(fixture.innerHTML).toBe(``);
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"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",
]
`);
// update
c!.state.flag = false;
await nextTick();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"C:willRender",
"F:setup",
"F:willStart",
"C:rendered",
"F:willRender",
"F: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 () => {
class Child extends Component {
static template = xml`child
`;
setup() {
useLogLifecycle();
}
}
class Parent extends Component {
static template = xml` `;
static components = { Child };
state = useState({ hasChild: true });
setup() {
useLogLifecycle();
}
}
const parent = await mount(Parent, fixture);
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Child:mounted",
"Parent:mounted",
]
`);
parent.state.hasChild = false;
await nextTick();
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(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Child:willRender",
"Child:rendered",
"Parent:willPatch",
"Child:mounted",
"Parent:patched",
]
`);
});
test("render in mounted", async () => {
class Parent extends Component {
static template = xml` `;
patched: any;
setup() {
useLogLifecycle();
onMounted(() => {
this.patched = "Patched";
this.render();
});
}
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe(" ");
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("Patched ");
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:willPatch",
"Parent:patched",
]
`);
});
test("render in patched", async () => {
class Parent extends Component {
static template = xml` `;
patched: any;
setup() {
useLogLifecycle();
onPatched(() => {
if (this.patched === "Patched") {
return;
}
this.patched = "Patched";
this.render();
});
}
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe(" ");
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Parent:rendered",
"Parent:mounted",
]
`);
parent.render();
await nextTick();
expect(fixture.innerHTML).toBe(" ");
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:willRender",
"Parent:rendered",
"Parent:willPatch",
"Parent:patched",
"Parent:willRender",
"Parent:rendered",
]
`);
await nextTick();
expect(fixture.innerHTML).toBe("Patched ");
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:willPatch",
"Parent:patched",
]
`);
});
test("render in willPatch", async () => {
class Parent extends Component {
static template = xml` `;
patched: any;
setup() {
useLogLifecycle();
onWillPatch(() => {
if (this.patched === "Patched") {
return;
}
this.patched = "Patched";
this.render();
});
}
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe(" ");
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:setup",
"Parent:willStart",
"Parent:willRender",
"Parent:rendered",
"Parent:mounted",
]
`);
parent.render();
await nextTick();
expect(fixture.innerHTML).toBe(" ");
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:willRender",
"Parent:rendered",
"Parent:willPatch",
"Parent:patched",
"Parent:willRender",
"Parent:rendered",
]
`);
await nextTick();
expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:willPatch",
"Parent:patched",
]
`);
expect(fixture.innerHTML).toBe("Patched ");
});
test("lifecycle callbacks are bound to component", async () => {
expect.assertions(14);
let instance: any;
class Test extends Component {
static template = xml` `;
setup() {
instance = this;
onWillStart(this.logger("onWillStart"));
onMounted(this.logger("onMounted"));
onWillUpdateProps(this.logger("onWillUpdateProps"));
onWillPatch(this.logger("onWillPatch"));
onPatched(this.logger("onPatched"));
onWillUnmount(this.logger("onWillUnmount"));
onWillDestroy(this.logger("onWillDestroy"));
onWillRender(this.logger("onWillRender"));
onRendered(this.logger("onRendered"));
}
logger(hookName: string) {
return function (this: Test) {
logStep(hookName);
expect(this === instance).toBe(true);
};
}
}
class Parent extends Component {
static template = xml` `;
static components = { Test };
rev = 0;
}
const app = new App(Parent);
const comp = await app.mount(fixture);
comp.rev++;
comp.render();
await nextTick();
app.destroy();
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 () => {
class Child extends Component {
static template = xml`child`;
setup() {
useLogLifecycle();
}
}
class Parent extends Component {
static template = xml`before after`;
static components = { Child };
state = useState({ flag: false });
setup() {
useLogLifecycle();
onRendered(async () => {
// we destroy here the app after the new child component has been
// created, but before this rendering has been patched to the DOM
if (this.state.flag) {
await Promise.resolve();
app.destroy();
}
});
}
}
const app = new App(Parent);
const parent = await app.mount(fixture);
expect(fixture.innerHTML).toBe("beforeafter");
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(steps.splice(0)).toMatchInlineSnapshot(`
Array [
"Parent:willRender",
"Child:setup",
"Child:willStart",
"Parent:rendered",
"Parent:willUnmount",
"Child:willDestroy",
"Parent:willDestroy",
]
`);
});
});