mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
0290f63ba3
The initial problem solved by this commit is that it was possible to get into a situation where a mounting/rendering was started, then the component was updated, but then another mounting operation begins, and it tries to reuse the previous rendering operation, which is no longer uptodate. The underlying issue is that Owl did not track properly the various internal state change of a component. These issue should be solved by the introduction of the status enum, which currently tracks 6 possible states: - CREATED - WILLSTARTED - RENDERED - MOUNTED - UNMOUNTED - DESTROYED This status number replaces the isMounted and isDestroyed boolean flags. It has the advantage of making sure that the component is in a consistent state (it is no longer possible to be destroyed and mounted, for example) Another advantage is that it gives us an easy way to track the fact that a component has been rendered, but is not in the DOM. This is a subtle situation where some various events can happen, and we need to be able to react to that case. Note that there is a change of behaviour: if a component is mounted in a specific target, then before the mounting is complete, the component is mounted in another target, we no longer reject the first mounting operation.
733 lines
22 KiB
TypeScript
733 lines
22 KiB
TypeScript
import { Component, Env, mount } from "../../src/component/component";
|
|
import { useState } from "../../src/hooks";
|
|
import { xml } from "../../src/tags";
|
|
import { makeDeferred, makeTestEnv, makeTestFixture, nextTick, nextMicroTick } from "../helpers";
|
|
import { scheduler } from "../../src/component/scheduler";
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Setup and helpers
|
|
//------------------------------------------------------------------------------
|
|
|
|
// We create before each test:
|
|
// - fixture: a div, appended to the DOM, intended to be the target of dom
|
|
// manipulations. Note that it is removed after each test.
|
|
// - env: a WEnv, necessary to create new components
|
|
|
|
let fixture: HTMLElement;
|
|
let env: Env;
|
|
|
|
beforeEach(() => {
|
|
fixture = makeTestFixture();
|
|
env = makeTestEnv();
|
|
Component.env = env;
|
|
});
|
|
|
|
afterEach(() => {
|
|
fixture.remove();
|
|
});
|
|
|
|
describe("mount targets", () => {
|
|
test("can attach a component to an existing node (if same tagname)", async () => {
|
|
class App extends Component {
|
|
static template = xml`<div t-att-class="state.customClass">app<p>another tag</p></div>`;
|
|
state = useState({ customClass: "custom" });
|
|
}
|
|
const div = document.createElement("div");
|
|
div.classList.add("arbitrary");
|
|
div.innerHTML = `<p>pre-existing</p>`;
|
|
fixture.appendChild(div);
|
|
|
|
const app = await mount(App, { target: div, position: "self" });
|
|
|
|
expect(fixture.innerHTML).toBe(
|
|
`<div class="arbitrary custom"><p>pre-existing</p>app<p>another tag</p></div>`
|
|
);
|
|
expect(div).toBe(app.el);
|
|
|
|
app.state.customClass = "custom2";
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe(
|
|
`<div class="arbitrary custom2"><p>pre-existing</p>app<p>another tag</p></div>`
|
|
);
|
|
expect(div).toBe(app.el);
|
|
|
|
app.unmount();
|
|
// This assert is a best guess
|
|
// The use case it covers was not really thought through
|
|
// and may change in the future
|
|
expect(fixture.innerHTML).toBe("");
|
|
});
|
|
|
|
test("cannot attach a component to an existing node (if not same tagname)", async () => {
|
|
class App extends Component {
|
|
static template = xml`<span>app</span>`;
|
|
}
|
|
const div = document.createElement("div");
|
|
fixture.appendChild(div);
|
|
|
|
let error;
|
|
try {
|
|
await mount(App, { target: div, position: "self" });
|
|
} catch (e) {
|
|
error = e;
|
|
}
|
|
expect(error).toBeDefined();
|
|
expect(error.message).toBe("Cannot attach 'App' to target node (not same tag name)");
|
|
});
|
|
|
|
test("can mount a component (with position='first-child')", async () => {
|
|
class App extends Component {
|
|
static template = xml`<div>app</div>`;
|
|
}
|
|
const span = document.createElement("span");
|
|
fixture.appendChild(span);
|
|
|
|
await mount(App, { target: fixture, position: "first-child" });
|
|
expect(fixture.innerHTML).toBe("<div>app</div><span></span>");
|
|
});
|
|
|
|
test("can mount a component (with position='last-child')", async () => {
|
|
class App extends Component {
|
|
static template = xml`<div>app</div>`;
|
|
}
|
|
const span = document.createElement("span");
|
|
fixture.appendChild(span);
|
|
|
|
await mount(App, { target: fixture, position: "last-child" });
|
|
expect(fixture.innerHTML).toBe("<span></span><div>app</div>");
|
|
});
|
|
|
|
test("default mount option is 'last-child'", async () => {
|
|
class App extends Component {
|
|
static template = xml`<div>app</div>`;
|
|
}
|
|
const span = document.createElement("span");
|
|
fixture.appendChild(span);
|
|
|
|
await mount(App, { target: fixture });
|
|
expect(fixture.innerHTML).toBe("<span></span><div>app</div>");
|
|
});
|
|
});
|
|
|
|
describe("unmounting and remounting", () => {
|
|
test("widget can be unmounted and remounted", async () => {
|
|
const steps: string[] = [];
|
|
class MyWidget extends Component {
|
|
static template = xml`<div>Hey</div>`;
|
|
async willStart() {
|
|
steps.push("willstart");
|
|
}
|
|
mounted() {
|
|
steps.push("mounted");
|
|
}
|
|
willUnmount() {
|
|
steps.push("willunmount");
|
|
}
|
|
patched() {
|
|
throw new Error("patched should not be called");
|
|
}
|
|
}
|
|
|
|
const w = await mount(MyWidget, { target: fixture });
|
|
expect(fixture.innerHTML).toBe("<div>Hey</div>");
|
|
expect(steps).toEqual(["willstart", "mounted"]);
|
|
|
|
w.unmount();
|
|
expect(fixture.innerHTML).toBe("");
|
|
expect(steps).toEqual(["willstart", "mounted", "willunmount"]);
|
|
|
|
await w.mount(fixture);
|
|
expect(fixture.innerHTML).toBe("<div>Hey</div>");
|
|
expect(steps).toEqual(["willstart", "mounted", "willunmount", "mounted"]);
|
|
});
|
|
|
|
test("widget can be mounted twice without ill effect", async () => {
|
|
const steps: string[] = [];
|
|
class MyWidget extends Component {
|
|
static template = xml`<div>Hey</div>`;
|
|
async willStart() {
|
|
steps.push("willstart");
|
|
}
|
|
mounted() {
|
|
steps.push("mounted");
|
|
}
|
|
willUnmount() {
|
|
steps.push("willunmount");
|
|
}
|
|
}
|
|
|
|
const w = await mount(MyWidget, { target: fixture });
|
|
await w.mount(fixture);
|
|
expect(fixture.innerHTML).toBe("<div>Hey</div>");
|
|
expect(steps).toEqual(["willstart", "mounted"]);
|
|
});
|
|
|
|
test("state changes in willUnmount do not trigger rerender", async () => {
|
|
const steps: string[] = [];
|
|
|
|
class Child extends Component {
|
|
static template = xml`
|
|
<span><t t-esc="props.val"/><t t-esc="state.n"/></span>
|
|
`;
|
|
state = useState({ n: 2 });
|
|
__render(f) {
|
|
steps.push("render");
|
|
return super.__render(f);
|
|
}
|
|
willPatch() {
|
|
steps.push("willPatch");
|
|
}
|
|
patched() {
|
|
steps.push("patched");
|
|
}
|
|
|
|
willUnmount() {
|
|
steps.push("willUnmount");
|
|
this.state.n = 3;
|
|
}
|
|
}
|
|
class Parent extends Component {
|
|
static template = xml`
|
|
<div>
|
|
<Child t-if="state.flag" val="state.val"/>
|
|
</div>
|
|
`;
|
|
static components = { Child };
|
|
state = useState({ val: 1, flag: true });
|
|
}
|
|
|
|
const widget = await mount(Parent, { target: fixture });
|
|
expect(steps).toEqual(["render"]);
|
|
expect(fixture.innerHTML).toBe("<div><span>12</span></div>");
|
|
widget.state.flag = false;
|
|
await nextTick();
|
|
// we make sure here that no call to __render is done
|
|
expect(steps).toEqual(["render", "willUnmount"]);
|
|
});
|
|
|
|
test("state changes in willUnmount will be applied on remount", async () => {
|
|
class TestWidget extends Component {
|
|
static template = xml`
|
|
<div><t t-esc="state.val"/></div>
|
|
`;
|
|
state = useState({ val: 1 });
|
|
willUnmount() {
|
|
this.state.val = 3;
|
|
}
|
|
}
|
|
|
|
const widget = new TestWidget();
|
|
await widget.mount(fixture);
|
|
expect(fixture.innerHTML).toBe("<div>1</div>");
|
|
widget.unmount();
|
|
expect(fixture.innerHTML).toBe("");
|
|
await nextTick(); // wait for changes to be detected before remounting
|
|
await widget.mount(fixture);
|
|
expect(fixture.innerHTML).toBe("<div>3</div>");
|
|
// we want to make sure that there are no remaining tasks left at this point.
|
|
expect(Component.scheduler.tasks.length).toBe(0);
|
|
});
|
|
|
|
test("sub component is still active after being unmounted and remounted", async () => {
|
|
class Child extends Component {
|
|
static template = xml`
|
|
<p t-on-click="state.value++">
|
|
<t t-esc="state.value"/>
|
|
</p>`;
|
|
|
|
state = useState({ value: 1 });
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static components = { Child };
|
|
static template = xml`<div><Child/></div>`;
|
|
}
|
|
|
|
const w = new Parent();
|
|
await w.mount(fixture);
|
|
expect(fixture.innerHTML).toBe("<div><p>1</p></div>");
|
|
|
|
fixture.querySelector("p")!.click();
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><p>2</p></div>");
|
|
w.unmount();
|
|
await nextTick();
|
|
await w.mount(fixture);
|
|
expect(fixture.innerHTML).toBe("<div><p>2</p></div>");
|
|
fixture.querySelector("p")!.click();
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><p>3</p></div>");
|
|
});
|
|
|
|
test("change state just before mounting component", async () => {
|
|
const steps: number[] = [];
|
|
class TestWidget extends Component {
|
|
static template = xml`
|
|
<div><t t-esc="state.val"/></div>
|
|
`;
|
|
state = useState({ val: 1 });
|
|
__render(f) {
|
|
steps.push(this.state.val);
|
|
return super.__render(f);
|
|
}
|
|
}
|
|
TestWidget.prototype.__render = jest.fn(TestWidget.prototype.__render);
|
|
|
|
const widget = new TestWidget();
|
|
widget.state.val = 2;
|
|
await widget.mount(fixture);
|
|
expect(fixture.innerHTML).toBe("<div>2</div>");
|
|
expect(TestWidget.prototype.__render).toHaveBeenCalledTimes(1);
|
|
|
|
// unmount and re-mount, as in this case, willStart won't be called, so it's
|
|
// slightly different
|
|
widget.unmount();
|
|
widget.state.val = 3;
|
|
await widget.mount(fixture);
|
|
expect(fixture.innerHTML).toBe("<div>3</div>");
|
|
expect(TestWidget.prototype.__render).toHaveBeenCalledTimes(2);
|
|
expect(steps).toEqual([2, 3]);
|
|
});
|
|
|
|
test("change state while mounting component", async () => {
|
|
const steps: number[] = [];
|
|
class TestWidget extends Component {
|
|
static template = xml`
|
|
<div><t t-esc="state.val"/></div>
|
|
`;
|
|
state = useState({ val: 1 });
|
|
__render(f) {
|
|
steps.push(this.state.val);
|
|
return super.__render(f);
|
|
}
|
|
}
|
|
TestWidget.prototype.__render = jest.fn(TestWidget.prototype.__render);
|
|
TestWidget.prototype.__patch = jest.fn(TestWidget.prototype.__patch);
|
|
|
|
const widget = new TestWidget();
|
|
let prom = widget.mount(fixture);
|
|
widget.state.val = 2;
|
|
await prom;
|
|
expect(fixture.innerHTML).toBe("<div>2</div>");
|
|
expect(TestWidget.prototype.__render).toHaveBeenCalledTimes(1);
|
|
|
|
// unmount and re-mount, as in this case, willStart won't be called, so it's
|
|
// slightly different
|
|
widget.unmount();
|
|
prom = widget.mount(fixture);
|
|
widget.state.val = 3;
|
|
await prom;
|
|
expect(fixture.innerHTML).toBe("<div>3</div>");
|
|
expect(TestWidget.prototype.__render).toHaveBeenCalledTimes(3);
|
|
expect(TestWidget.prototype.__patch).toHaveBeenCalledTimes(2);
|
|
expect(steps).toEqual([2, 2, 3]);
|
|
});
|
|
|
|
test("change state and render while mounted in detached dom", async () => {
|
|
class App extends Component {
|
|
static template = xml`<div><t t-esc="state.val"/></div>`;
|
|
state = useState({ val: 1 });
|
|
}
|
|
|
|
const detachedDiv = document.createElement("div");
|
|
const app = await mount(App, { target: detachedDiv });
|
|
|
|
expect(detachedDiv.innerHTML).toBe("<div>1</div>");
|
|
app.state.val = 2;
|
|
await nextTick();
|
|
expect(detachedDiv.innerHTML).toBe("<div>2</div>");
|
|
});
|
|
|
|
test("change state and render while not mounted ", async () => {
|
|
class App extends Component {
|
|
static template = xml`<div><t t-esc="state.val"/></div>`;
|
|
state = useState({ val: 1 });
|
|
}
|
|
|
|
const app = new App(null);
|
|
|
|
app.state.val = 2; // will call the render method (before being mounted)
|
|
await nextTick();
|
|
|
|
await app.mount(fixture);
|
|
|
|
expect(fixture.innerHTML).toBe("<div>2</div>");
|
|
});
|
|
|
|
test("destroy and change state after mounted in detached dom", async () => {
|
|
class App extends Component {
|
|
static template = xml`<div><t t-esc="state.val"/></div>`;
|
|
state = useState({ val: 1 });
|
|
}
|
|
|
|
const detachedDiv = document.createElement("div");
|
|
const app = await mount(App, { target: detachedDiv });
|
|
|
|
expect(detachedDiv.innerHTML).toBe("<div>1</div>");
|
|
|
|
app.destroy();
|
|
app.state.val = 2;
|
|
await nextTick();
|
|
expect(detachedDiv.innerHTML).toBe("");
|
|
});
|
|
|
|
test("change state while component is unmounted", async () => {
|
|
let child;
|
|
class Child extends Component {
|
|
static template = xml`<span t-esc="state.val"/>`;
|
|
state = useState({
|
|
val: "C1",
|
|
});
|
|
constructor(parent, props) {
|
|
super(parent, props);
|
|
child = this;
|
|
}
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static components = { Child };
|
|
static template = xml`<div><t t-esc="state.val"/><Child/></div>`;
|
|
state = useState({ val: "P1" });
|
|
}
|
|
|
|
const parent = new Parent();
|
|
await parent.mount(fixture);
|
|
expect(fixture.innerHTML).toBe("<div>P1<span>C1</span></div>");
|
|
|
|
parent.unmount();
|
|
expect(fixture.innerHTML).toBe("");
|
|
|
|
parent.state.val = "P2";
|
|
child.state.val = "C2";
|
|
|
|
await parent.mount(fixture);
|
|
expect(fixture.innerHTML).toBe("<div>P2<span>C2</span></div>");
|
|
});
|
|
|
|
test("change state while component is mounted in a fragment", async () => {
|
|
class Child1 extends Component {
|
|
static template = xml`<span>C1</span>`;
|
|
}
|
|
|
|
class Child2 extends Component {
|
|
static template = xml`<span>C2</span>`;
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static components = { Child1, Child2 };
|
|
static template = xml`
|
|
<div>
|
|
<Child1 t-if="child == 'c1'"/>
|
|
<Child2 t-if="child == 'c2'"/>
|
|
</div>`;
|
|
child: string | false = false;
|
|
}
|
|
|
|
const fragment = document.createDocumentFragment();
|
|
const parent = new Parent();
|
|
await parent.mount(fragment);
|
|
expect(parent.el.outerHTML).toBe("<div></div>");
|
|
|
|
parent.child = "c1";
|
|
parent.render();
|
|
await Promise.resolve();
|
|
|
|
parent.child = "c2";
|
|
await parent.mount(fixture);
|
|
|
|
expect(fixture.innerHTML).toBe("<div><span>C2</span></div>");
|
|
});
|
|
|
|
test("unmount component during a re-rendering", async () => {
|
|
const def = makeDeferred();
|
|
class Child extends Component {
|
|
static template = xml`<span><t t-esc="props.val"/></span>`;
|
|
willUpdateProps() {
|
|
return def;
|
|
}
|
|
}
|
|
Child.prototype.__render = jest.fn(Child.prototype.__render);
|
|
|
|
class Parent extends Component {
|
|
static template = xml`<div><Child val="state.val"/></div>`;
|
|
static components = { Child };
|
|
state = useState({ val: 1 });
|
|
}
|
|
|
|
const parent = new Parent();
|
|
await parent.mount(fixture);
|
|
expect(fixture.innerHTML).toBe("<div><span>1</span></div>");
|
|
expect(Child.prototype.__render).toBeCalledTimes(1);
|
|
|
|
parent.state.val = 2;
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><span>1</span></div>");
|
|
|
|
parent.unmount();
|
|
expect(fixture.innerHTML).toBe("");
|
|
|
|
def.resolve();
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("");
|
|
expect(Child.prototype.__render).toBeCalledTimes(1);
|
|
});
|
|
|
|
test("widget can be mounted on different target", async () => {
|
|
class MyWidget extends Component {
|
|
static template = xml`<div>Hey</div>`;
|
|
patched() {
|
|
throw new Error("patched should not be called");
|
|
}
|
|
}
|
|
const div = document.createElement("div");
|
|
const span = document.createElement("span");
|
|
fixture.appendChild(div);
|
|
fixture.appendChild(span);
|
|
const w = new MyWidget();
|
|
await w.mount(div);
|
|
|
|
expect(fixture.innerHTML).toBe("<div><div>Hey</div></div><span></span>");
|
|
|
|
await w.mount(span);
|
|
expect(fixture.innerHTML).toBe("<div></div><span><div>Hey</div></span>");
|
|
});
|
|
|
|
test("widget can be mounted on different target, another situation", async () => {
|
|
const def = makeDeferred();
|
|
const steps: string[] = [];
|
|
|
|
class MyWidget extends Component {
|
|
static template = xml`<div>Hey</div>`;
|
|
async willStart() {
|
|
return def;
|
|
}
|
|
patched() {
|
|
throw new Error("patched should not be called");
|
|
}
|
|
}
|
|
const div = document.createElement("div");
|
|
const span = document.createElement("span");
|
|
fixture.appendChild(div);
|
|
fixture.appendChild(span);
|
|
const w = new MyWidget();
|
|
|
|
w.mount(div).catch(() => steps.push("1 catch"));
|
|
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div></div><span></span>");
|
|
|
|
w.mount(span).then(() => steps.push("2 resolved"));
|
|
|
|
// we wait two microticks because this is the number of internal promises
|
|
// that need to be resolved/rejected, and because we want to prove here
|
|
// that the first mount operation is cancelled immediately, and not after
|
|
// one full tick.
|
|
await nextMicroTick();
|
|
await nextMicroTick();
|
|
expect(steps).toEqual([]);
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div></div><span></span>");
|
|
|
|
def.resolve();
|
|
await nextTick();
|
|
expect(steps).toEqual(["2 resolved"]);
|
|
expect(fixture.innerHTML).toBe("<div></div><span><div>Hey</div></span>");
|
|
});
|
|
|
|
test("component can be mounted on same target, another situation", async () => {
|
|
const def = makeDeferred();
|
|
const steps: string[] = [];
|
|
|
|
class MyWidget extends Component {
|
|
static template = xml`<div>Hey</div>`;
|
|
async willStart() {
|
|
return def;
|
|
}
|
|
patched() {
|
|
throw new Error("patched should not be called");
|
|
}
|
|
}
|
|
const w = new MyWidget();
|
|
|
|
w.mount(fixture).then(() => steps.push("1 resolved"));
|
|
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("");
|
|
|
|
w.mount(fixture).then(() => steps.push("2 resolved"));
|
|
|
|
await nextTick();
|
|
expect(steps).toEqual([]);
|
|
expect(fixture.innerHTML).toBe("");
|
|
|
|
def.resolve();
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div>Hey</div>");
|
|
expect(steps).toEqual(["1 resolved", "2 resolved"]);
|
|
});
|
|
|
|
test("mounting a destroyed widget", async () => {
|
|
class MyWidget extends Component {
|
|
static template = xml`<div>Hey</div>`;
|
|
}
|
|
const w = new MyWidget();
|
|
w.destroy(); // because, why not
|
|
|
|
let error;
|
|
try {
|
|
await w.mount(fixture);
|
|
} catch (e) {
|
|
error = e;
|
|
}
|
|
expect(scheduler.tasks.length).toBe(0);
|
|
expect(error).toBeDefined();
|
|
expect(error.message).toBe("Cannot mount a destroyed component");
|
|
});
|
|
|
|
test("destroying a sub-component cleans itself from parent's vnode", async () => {
|
|
class C1 extends Component {
|
|
static template = xml`<div><div><t t-esc="props.a"/></div></div>`;
|
|
}
|
|
class P extends Component {
|
|
static components = { C1 };
|
|
static template = xml`<div><div><C1 t-props="state" t-if="state.a"/></div></div>`;
|
|
state = {
|
|
a: "first",
|
|
};
|
|
}
|
|
const parent = new P();
|
|
await parent.mount(fixture);
|
|
expect(fixture.textContent).toBe("first");
|
|
parent.unmount();
|
|
parent.state.a = "";
|
|
parent.mount(fixture);
|
|
parent.state.a = "fixed";
|
|
await parent.render();
|
|
expect(fixture.textContent).toBe("fixed");
|
|
});
|
|
|
|
test("destroying a sub-component cleans itself from parent's vnode, part 2", async () => {
|
|
class C1 extends Component {
|
|
static template = xml`<div><div><t t-esc="props.a"/></div></div>`;
|
|
}
|
|
class P extends Component {
|
|
static components = { C1 };
|
|
static template = xml`<div><div><C1 t-props="state" t-if="state.a"/>some text</div></div>`;
|
|
state = {
|
|
a: "first",
|
|
};
|
|
}
|
|
const parent = new P();
|
|
await parent.mount(fixture);
|
|
expect(fixture.textContent).toBe("firstsome text");
|
|
parent.unmount();
|
|
parent.state.a = "";
|
|
parent.mount(fixture);
|
|
parent.state.a = "fixed";
|
|
await parent.render();
|
|
expect(fixture.textContent).toBe("fixedsome text");
|
|
});
|
|
|
|
test("destroying a sub-component cleans itself from parent's vnode, part 3", async () => {
|
|
class C1 extends Component {
|
|
static template = xml`<div><div><t t-esc="props.a"/></div></div>`;
|
|
}
|
|
|
|
class C2 extends Component {
|
|
static template = xml`<C1 a="props.a"/>`;
|
|
static components = { C1 };
|
|
}
|
|
|
|
class P extends Component {
|
|
static components = { C2 };
|
|
static template = xml`<div><div><C2 t-props="state" t-if="state.a"/></div></div>`;
|
|
state = {
|
|
a: "first",
|
|
};
|
|
}
|
|
const parent = new P();
|
|
await parent.mount(fixture);
|
|
expect(fixture.textContent).toBe("first");
|
|
parent.unmount();
|
|
parent.state.a = "";
|
|
parent.mount(fixture);
|
|
parent.state.a = "fixed";
|
|
await parent.render();
|
|
expect(fixture.textContent).toBe("fixed");
|
|
});
|
|
|
|
test("destroying a sub-component cleans itself from parent's vnode, part 4", async () => {
|
|
class C1 extends Component {
|
|
static template = xml`<div><div><t t-esc="props.a"/></div></div>`;
|
|
}
|
|
|
|
class C2 extends Component {
|
|
static template = xml`<C1 a="props.a"/>`;
|
|
static components = { C1 };
|
|
}
|
|
class P extends Component {
|
|
static components = { C2 };
|
|
static template = xml`<div><div><C2 t-props="state" t-if="state.a"/>some text</div></div>`;
|
|
state = {
|
|
a: "first",
|
|
};
|
|
}
|
|
const parent = new P();
|
|
await parent.mount(fixture);
|
|
expect(fixture.textContent).toBe("firstsome text");
|
|
parent.unmount();
|
|
parent.state.a = "";
|
|
parent.mount(fixture);
|
|
parent.state.a = "fixed";
|
|
await parent.render();
|
|
expect(fixture.textContent).toBe("fixedsome text");
|
|
});
|
|
|
|
test("remounting component tree where a component implement shouldupdate", async () => {
|
|
let state: any;
|
|
const steps = [];
|
|
class Child extends Component {
|
|
static template = xml`<div><t t-esc="state.word"/><t t-esc="props.name"/></div>`;
|
|
|
|
state = useState({ word: "hello" });
|
|
|
|
constructor(parent, props) {
|
|
super(parent, props);
|
|
state = this.state;
|
|
}
|
|
patched() {
|
|
steps.push("patched");
|
|
}
|
|
mounted() {
|
|
steps.push("mounted");
|
|
}
|
|
willUnmount() {
|
|
steps.push("willUnmount");
|
|
}
|
|
shouldUpdate() {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
class Parent extends Component {
|
|
static template = xml`<div><Child name="state.name"/></div>`;
|
|
static components = { Child };
|
|
state = useState({ name: "World" });
|
|
}
|
|
|
|
const parent = await mount(Parent, { target: fixture });
|
|
expect(fixture.innerHTML).toBe("<div><div>helloWorld</div></div>");
|
|
|
|
parent.unmount();
|
|
expect(fixture.innerHTML).toBe("");
|
|
await parent.mount(fixture);
|
|
expect(fixture.innerHTML).toBe("<div><div>helloWorld</div></div>");
|
|
|
|
state.word = "test";
|
|
|
|
await nextTick();
|
|
expect(fixture.innerHTML).toBe("<div><div>testWorld</div></div>");
|
|
expect(steps).toEqual(["mounted", "willUnmount", "mounted", "patched"]);
|
|
});
|
|
});
|