import { App, Component, mount, status, useState } from "../../src"; import { xml } from "../../src/tags"; import { makeTestFixture, nextTick, snapshotEverything } from "../helpers"; let fixture: HTMLElement; snapshotEverything(); beforeEach(() => { fixture = makeTestFixture(); }); describe("basics", () => { test("can mount a simple component", async () => { class Test extends Component { static template = xml`simple vnode`; } const component = await mount(Test, fixture); expect(fixture.innerHTML).toBe("simple vnode"); expect(component.el).toEqual(fixture.querySelector("span")); }); test("has no el after creation", async () => { let el: any = null; class Test extends Component { static template = xml`simple`; setup() { el = this.el; } } await mount(Test, fixture); expect(el).toBeUndefined(); }); test("cannot mount on a documentFragment", async () => { class SomeWidget extends Component { static template = xml`
content
`; } let error; try { await mount(SomeWidget, document.createDocumentFragment() as any); } catch (e) { error = e; } expect(error).toBeDefined(); expect(error.message).toBe("Cannot mount component: the target is not a valid DOM element"); }); test("can mount a simple component with props", async () => { class Test extends Component { static template = xml``; } const app = new App(Test, { value: 3 }); const component = await app.mount(fixture); expect(fixture.innerHTML).toBe("3"); expect(component.el).toEqual(fixture.querySelector("span")); }); test("can mount a component with just some text", async () => { class Test extends Component { static template = xml`just text`; } const component = await mount(Test, fixture); expect(fixture.innerHTML).toBe("just text"); expect(component.el).toBeInstanceOf(Text); }); test("can mount a component with no text", async () => { class Test extends Component { static template = xml``; } const component = await mount(Test, fixture); expect(fixture.innerHTML).toBe(""); expect(component.el).toBeInstanceOf(Text); }); test("can mount a simple component with multiple roots", async () => { class Test extends Component { static template = xml`
`; } const component = await mount(Test, fixture); expect(fixture.innerHTML).toBe("
"); expect((component.el as any).tagName).toBe("SPAN"); }); test("component with dynamic content can be updated", async () => { class Test extends Component { static template = xml``; value = 1; } const component = await mount(Test, fixture); expect(fixture.innerHTML).toBe("1"); component.value = 2; await component.render(); expect(fixture.innerHTML).toBe("2"); }); test("updating a component with t-foreach as root", async () => { class Test extends Component { static template = xml` `; items = ["one", "two", "three"]; } const component = await mount(Test, fixture); expect(fixture.innerHTML).toBe("onetwothree"); component.items = ["two", "three", "one"]; await component.render(); expect(fixture.innerHTML).toBe("twothreeone"); }); test("props is set on root component", async () => { expect.assertions(2); const p = {}; class Test extends Component { static template = xml`simple vnode`; setup() { expect(this.props).toBe(p); } } const app = new App(Test, p); await app.mount(fixture); }); test("some simple sanity checks (el/status)", async () => { expect.assertions(4); class Test extends Component { static template = xml`simple vnode`; setup() { expect(this.el).toBe(undefined); expect(status(this)).toBe("new"); } } const test = await mount(Test, fixture); expect(status(test)).toBe("mounted"); }); test("throws if mounting on target=null", async () => { class Test extends Component { static template = xml`simple vnode`; } let error; try { await mount(Test, null as any); } catch (e) { error = e; } expect(error).toBeDefined(); expect(error.message).toBe("Cannot mount component: the target is not a valid DOM element"); }); test("a component cannot be mounted in a detached node", async () => { class Test extends Component { static template = xml`
`; } let error; try { await mount(Test, document.createElement("div")); } catch (e) { error = e; } expect(error).toBeDefined(); expect(error.message).toBe("Cannot mount a component on a detached dom node"); }); test("crashes if it cannot find a template", async () => { class Test extends Component { static template = "wrongtemplate"; } let error; try { await mount(Test, fixture); } catch (e) { error = e; } expect(error).toBeDefined(); expect(error.message).toBe('Missing template: "wrongtemplate"'); }); test("class component with dynamic text", async () => { class Test extends Component { static template = xml`My value: `; value = 42; } await mount(Test, fixture); expect(fixture.innerHTML).toBe("My value: 42"); }); test("Multi root component", async () => { class Test extends Component { static template = xml`1text2`; value = 42; } await mount(Test, fixture); expect(fixture.innerHTML).toBe(`1text2`); }); test("a component inside a component", async () => { class Child extends Component { static template = xml`
simple vnode
`; } class Parent extends Component { static template = xml``; static components = { Child }; } await mount(Parent, fixture); expect(fixture.innerHTML).toBe("
simple vnode
"); }); test("a class component inside a class component, no external dom", async () => { class Child extends Component { static template = xml`
simple vnode
`; } class Parent extends Component { static template = xml``; static components = { Child }; } await mount(Parent, fixture); expect(fixture.innerHTML).toBe("
simple vnode
"); }); test("simple component with a dynamic text", async () => { class Test extends Component { static template = xml`
`; value = 3; } const test = await mount(Test, fixture); expect(fixture.innerHTML).toBe("
3
"); test.value = 5; await test.render(); expect(fixture.innerHTML).toBe("
5
"); }); test("simple component, useState", async () => { class Test extends Component { static template = xml`
`; state = useState({ value: 3 }); } const test = await mount(Test, fixture); expect(fixture.innerHTML).toBe("
3
"); test.state.value = 5; await nextTick(); expect(fixture.innerHTML).toBe("
5
"); }); test("two child components", async () => { class Child extends Component { static template = xml`
simple vnode
`; } class Parent extends Component { static template = xml``; static components = { Child }; } await mount(Parent, fixture); expect(fixture.innerHTML).toBe("
simple vnode
simple vnode
"); }); test("class parent, class child component with props", async () => { class Child extends Component { static template = xml`
`; } class Parent extends Component { static template = xml``; static components = { Child }; } await mount(Parent, fixture); expect(fixture.innerHTML).toBe("
42
"); }); test("parent, child and grandchild", async () => { class GrandChild extends Component { static template = xml`
hey
`; } class Child extends Component { static template = xml``; static components = { GrandChild }; } class Parent extends Component { static template = xml``; static components = { Child }; } await mount(Parent, fixture); expect(fixture.innerHTML).toBe("
hey
"); }); test("zero or one child components", async () => { class Child extends Component { static template = xml`
simple vnode
`; } class Parent extends Component { static template = xml``; static components = { Child }; state = useState({ hasChild: false }); } const parent = await mount(Parent, fixture); expect(fixture.innerHTML).toBe(""); parent.state.hasChild = true; await nextTick(); expect(fixture.innerHTML).toBe("
simple vnode
"); }); test("text after a conditional component", async () => { class Child extends Component { static template = xml`

simple vnode

`; } class Parent extends Component { static template = xml`
`; static components = { Child }; state = useState({ hasChild: false, text: "1" }); } const parent = await mount(Parent, fixture); expect(fixture.innerHTML).toBe("
1
"); parent.state.hasChild = true; await nextTick(); expect(fixture.innerHTML).toBe("

simple vnode

1
"); parent.state.hasChild = false; parent.state.text = "2"; await nextTick(); expect(fixture.innerHTML).toBe("
2
"); }); test("can be clicked on and updated", async () => { class Counter extends Component { static template = xml`
`; state = useState({ counter: 0, }); } const counter = await mount(Counter, fixture); expect(fixture.innerHTML).toBe("
0
"); const button = (counter.el).getElementsByTagName("button")[0]; button.click(); await nextTick(); expect(fixture.innerHTML).toBe("
1
"); }); // TODO: rename test("rerendering a widget with a sub widget", async () => { class Counter extends Component { static template = xml`
`; state = useState({ counter: 0, }); } class Parent extends Component { static template = xml``; static components = { Counter }; } const parent = await mount(Parent, fixture); const button = fixture.getElementsByTagName("button")[0]; button.click(); await nextTick(); expect(fixture.innerHTML).toBe("
1
"); await parent.render(); expect(fixture.innerHTML).toBe("
1
"); }); test("can handle empty props", async () => { class Child extends Component { static template = xml``; } class Parent extends Component { static template = xml`
`; static components = { Child }; } await mount(Parent, fixture); expect(fixture.innerHTML).toBe("
"); }); test("child can be updated", async () => { class Child extends Component { static template = xml``; } class Parent extends Component { static template = xml``; static components = { Child }; state = useState({ counter: 0, }); } const parent = await mount(Parent, fixture); expect(fixture.innerHTML).toBe("0"); parent.state.counter = 1; await nextTick(); expect(fixture.innerHTML).toBe("1"); }); test("higher order components parent and child", async () => { class ChildA extends Component { static template = xml`
a
`; } class ChildB extends Component { static template = xml`b`; } class Child extends Component { static template = xml``; static components = { ChildA, ChildB }; } class Parent extends Component { static template = xml``; static components = { Child }; state = useState({ child: "a" }); } const parent = await mount(Parent, fixture); expect(fixture.innerHTML).toBe(`
a
`); parent.state.child = "b"; await nextTick(); expect(fixture.innerHTML).toBe(`b`); }); test("three level of components with collapsing root nodes", async () => { class GrandChild extends Component { static template = xml`
2
`; } class Child extends Component { static components = { GrandChild }; static template = xml``; } class Parent extends Component { static components = { Child }; static template = xml``; } await mount(Parent, fixture); expect(fixture.innerHTML).toBe("
2
"); }); test("do not remove previously rendered dom if not necessary", async () => { class SomeComponent extends Component { static template = xml`
`; } const widget = await mount(SomeComponent, fixture); expect(fixture.innerHTML).toBe(`
`); widget.el!.appendChild(document.createElement("span")); expect(fixture.innerHTML).toBe(`
`); await widget.render(); expect(fixture.innerHTML).toBe(`
`); }); test("do not remove previously rendered dom if not necessary, variation", async () => { class SomeComponent extends Component { static template = xml`

h1

`; state = useState({ value: 1 }); } const comp = await mount(SomeComponent, fixture); expect(fixture.innerHTML).toBe(`

h1

1
`); (comp.el! as any).querySelector("h1")!.appendChild(document.createElement("p")); expect(fixture.innerHTML).toBe("

h1

1
"); comp.state.value++; await nextTick(); expect(fixture.innerHTML).toBe("

h1

2
"); }); test("reconciliation alg is not confused in some specific situation", async () => { // in this test, we set t-key to 4 because it was in conflict with the // template id corresponding to the first child. class Child extends Component { static template = xml`child`; } class Parent extends Component { static template = xml`
`; static components = { Child }; } await mount(Parent, fixture); expect(fixture.innerHTML).toBe("
childchild
"); }); test("same t-keys in two different places", async () => { class Child extends Component { static template = xml``; } class Parent extends Component { static template = xml`
`; static components = { Child }; } await mount(Parent, fixture); expect(fixture.innerHTML).toBe("
1
2
"); }); test("t-key on a component with t-if, and a sibling component", async () => { class Child extends Component { static template = xml`child`; } class Parent extends Component { static template = xml`
`; static components = { Child }; } await mount(Parent, fixture); expect(fixture.innerHTML).toBe("
child
"); }); test("widget after a t-foreach", async () => { class SomeComponent extends Component { static template = xml`
`; } class Test extends Component { static template = xml`
txt
`; static components = { SomeComponent }; } await mount(Test, fixture); expect(fixture.innerHTML).toBe("
txttxt
"); }); test("t-if works with t-component", async () => { class Child extends Component { static template = xml`hey`; } class Parent extends Component { static template = xml`
`; static components = { Child }; state = useState({ flag: true }); } const parent = await mount(Parent, fixture); expect(fixture.innerHTML).toBe("
hey
"); parent.state.flag = false; await nextTick(); expect(fixture.innerHTML).toBe("
"); parent.state.flag = true; await nextTick(); expect(fixture.innerHTML).toBe("
hey
"); }); test("t-else works with t-component", async () => { class Child extends Component { static template = xml`hey`; } class Parent extends Component { static template = xml`
somediv
`; static components = { Child }; state = useState({ flag: true }); } const parent = await mount(Parent, fixture); expect(fixture.innerHTML).toBe("
somediv
"); parent.state.flag = false; await nextTick(); expect(fixture.innerHTML).toBe("
hey
"); }); test("t-elif works with t-component", async () => { class Child extends Component { static template = xml`hey`; } class Parent extends Component { static template = xml`
somediv
`; static components = { Child }; state = useState({ flag: true }); } const parent = await mount(Parent, fixture); expect(fixture.innerHTML).toBe("
somediv
"); parent.state.flag = false; await nextTick(); expect(fixture.innerHTML).toBe("
hey
"); }); test("t-else with empty string works with t-component", async () => { class Child extends Component { static template = xml`hey`; } class Parent extends Component { static template = xml`
somediv
`; static components = { Child }; state = useState({ flag: true }); } const parent = await mount(Parent, fixture); expect(fixture.innerHTML).toBe("
somediv
"); parent.state.flag = false; await nextTick(); expect(fixture.innerHTML).toBe("
hey
"); }); // TODO: implement scope lookup for components test.skip("don't fallback to component's registry if widget defined in the instance's context", async () => { class ChildA extends Component { static template = xml`ChildA`; } class ChildB extends Component { static template = xml`ChildB`; } class Parent extends Component { static template = xml``; static components = { Child: ChildA }; Child = ChildB; } await mount(Parent, fixture); expect(fixture.innerHTML).toBe("ChildB"); }); test("sub components between t-ifs", async () => { // this confuses the patching algorithm... class Child extends Component { static template = xml`child`; } class Parent extends Component { static template = xml`

hey

noo

test
`; static components = { Child }; state = useState({ flag: false }); } const parent = await mount(Parent, fixture); const child = Object.values(parent.__owl__.children)[0].component; expect(fixture.innerHTML).toBe(`

noo

child
`); parent.state.flag = true; await nextTick(); expect(Object.values(parent.__owl__.children)[0].component).toBe(child); expect(status(child)).toBe("mounted"); expect(fixture.innerHTML).toBe( `

hey

childtest
` ); }); test("list of two sub components inside other nodes", async () => { class SubWidget extends Component { static template = xml`asdf`; } class Parent extends Component { static template = xml`
`; static components = { SubWidget }; state = useState({ blips: [{ a: "a", id: 1 }] }); } await mount(Parent, fixture); expect(fixture.innerHTML).toBe("
asdfasdf
"); }); }); describe.skip("mount targets", () => { test("can attach a component to an existing node (if same tagname)", async () => { // class App extends Component { // static template = xml`
app

another tag

`; // state = useState({ customClass: "custom" }); // } // const div = document.createElement("div"); // div.classList.add("arbitrary"); // div.innerHTML = `

pre-existing

`; // fixture.appendChild(div); // const app = await mount(App, { target: div, position: "self" }); // expect(fixture.innerHTML).toBe( // `

pre-existing

app

another tag

` // ); // expect(div).toBe(app.el); // app.state.customClass = "custom2"; // await nextTick(); // expect(fixture.innerHTML).toBe( // `

pre-existing

app

another tag

` // ); // 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`app`; // } // 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`
app
`; // } // const span = document.createElement("span"); // fixture.appendChild(span); // await mount(App, { target: fixture, position: "first-child" }); // expect(fixture.innerHTML).toBe("
app
"); }); test("can mount a component (with position='last-child')", async () => { // class App extends Component { // static template = xml`
app
`; // } // const span = document.createElement("span"); // fixture.appendChild(span); // await mount(App, { target: fixture, position: "last-child" }); // expect(fixture.innerHTML).toBe("
app
"); }); test("default mount option is 'last-child'", async () => { // class App extends Component { // static template = xml`
app
`; // } // const span = document.createElement("span"); // fixture.appendChild(span); // await mount(App, { target: fixture }); // expect(fixture.innerHTML).toBe("
app
"); }); }); describe.skip("mount special cases", () => { test("widget can be mounted on different target", async () => { // class MyWidget extends Component { // static template = xml`
Hey
`; // 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("
Hey
"); // await w.mount(span); // expect(fixture.innerHTML).toBe("
Hey
"); }); test("widget can be mounted on different target, another situation", async () => { // const def = makeDeferred(); // const steps: string[] = []; // class MyWidget extends Component { // static template = xml`
Hey
`; // 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("
"); // 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("
"); // def.resolve(); // await nextTick(); // expect(steps).toEqual(["2 resolved"]); // expect(fixture.innerHTML).toBe("
Hey
"); }); test("component can be mounted on same target, another situation", async () => { // const def = makeDeferred(); // const steps: string[] = []; // class MyWidget extends Component { // static template = xml`
Hey
`; // 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("
Hey
"); // expect(steps).toEqual(["1 resolved", "2 resolved"]); }); test("mounting a destroyed widget", async () => { // class MyWidget extends Component { // static template = xml`
Hey
`; // } // 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`
`; // } // class P extends Component { // static components = { C1 }; // static template = xml`
`; // 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`
`; // } // class P extends Component { // static components = { C1 }; // static template = xml`
some text
`; // 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`
`; // } // class C2 extends Component { // static template = xml``; // static components = { C1 }; // } // class P extends Component { // static components = { C2 }; // static template = xml`
`; // 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`
`; // } // class C2 extends Component { // static template = xml``; // static components = { C1 }; // } // class P extends Component { // static components = { C2 }; // static template = xml`
some text
`; // 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"); }); });