Files
owl/tests/components/basics.test.ts
T
Samuel Degueldre b25e988628 [FIX] compiler: fix xmlns attribute not being set correctly in firefox
Firefox will not serialize the xmlns attributes of node inside
XMLDocuments, see https://bugzilla.mozilla.org/show_bug.cgi?id=175946
it will only output an xmlns attribute if the node being serialized has
a non-null namespaceURI.

When compiling templates, we currently use a special attribute,
"block-ns" to keep track of the namespace, but we do not make use of the
namespace to create the elements that will be serialized to the compiled
block string. This causes a difference in behaviour between Chrome and
Firefox: since we end up with an Element with two attributes: the
block-ns attribute and the xmlns attribute. Chrome will serialize both,
but Firefox will only serialize the block-ns because the Element does
not have a namespaceURI.

To fix this issue, we get rid of the block-ns magic attribute
completely, and use xmlns instead. We also use the namespace when
creating intermediate elements that will be used to create the
serialized block string: the namespace is only used to create the
elements but *not* set as an attribute, as this would cause chrome to
serialize it twice, causing a duplicate attribute error when parsing it
further down the line.

When creating the template element for the block at runtime, the xmlns
attribute is used both as the namespace with which to create the
element, and set as an attribute, this doesn't cause issues when
serializing later because the namespaceURI is never serialized as an
attribute when serializing an HTML document[1], so we avoid the
double-serialization in Chrome, and when doing HTML serialization,
Firefox will correctly serialize the attribute.

It's desirable that the xmlns is set as an attribute to allow users to
use owl to render SVG, and then use the HTML serialization of it as a
SVG even outside the context of an HTML document (eg to generate an SVG
file or an SVG data URL).

[1]: https://w3c.github.io/DOM-Parsing/#xml-serialization
2023-08-07 12:21:43 +02:00

1116 lines
33 KiB
TypeScript

import { App, Component, mount, status, toRaw, useState, xml } from "../../src";
import {
elem,
makeTestFixture,
nextAppError,
nextTick,
snapshotEverything,
useLogLifecycle,
} from "../helpers";
import { markup } from "../../src/runtime/utils";
let fixture: HTMLElement;
snapshotEverything();
beforeEach(() => {
fixture = makeTestFixture();
});
describe("basics", () => {
test("can mount a simple component", async () => {
class Test extends Component {
static template = xml`<span>simple vnode</span>`;
}
const component = await mount(Test, fixture);
expect(fixture.innerHTML).toBe("<span>simple vnode</span>");
expect(elem(component)).toEqual(fixture.querySelector("span"));
});
test("cannot mount on a documentFragment", async () => {
class SomeWidget extends Component {
static template = xml`<div>content</div>`;
}
let error: Error;
try {
await mount(SomeWidget, document.createDocumentFragment() as any);
} catch (e) {
error = e as Error;
}
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`<span><t t-esc="props.value"/></span>`;
}
const app = new App(Test, { props: { value: 3 } });
const component = await app.mount(fixture);
expect(fixture.innerHTML).toBe("<span>3</span>");
expect(elem(component)).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(elem(component)).toBeInstanceOf(Text);
});
test("can mount a component with no text", async () => {
class Test extends Component {
static template = xml`<t></t>`;
}
const component = await mount(Test, fixture);
expect(fixture.innerHTML).toBe("");
expect(elem(component)).toBeInstanceOf(Text);
});
test("can mount a simple component with multiple roots", async () => {
class Test extends Component {
static template = xml`<span></span><div></div>`;
}
const component = await mount(Test, fixture);
expect(fixture.innerHTML).toBe("<span></span><div></div>");
expect(elem(component).tagName).toBe("SPAN");
});
test("component with dynamic content can be updated", async () => {
class Test extends Component {
static template = xml`<span><t t-esc="value"/></span>`;
value = 1;
}
const component = await mount(Test, fixture);
expect(fixture.innerHTML).toBe("<span>1</span>");
component.value = 2;
component.render();
await nextTick();
expect(fixture.innerHTML).toBe("<span>2</span>");
});
test("updating a component with t-foreach as root", async () => {
class Test extends Component {
static template = xml`
<t t-foreach="items" t-as="item" t-key="item">
<t t-esc="item"/>
</t>`;
items = ["one", "two", "three"];
}
const component = await mount(Test, fixture);
expect(fixture.innerHTML).toBe("onetwothree");
component.items = ["two", "three", "one"];
component.render();
await nextTick();
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`<span>simple vnode</span>`;
setup() {
expect(toRaw(this.props)).not.toBe(p);
}
}
const app = new App(Test, { props: p });
await app.mount(fixture);
});
test("props value are own property of props object", async () => {
expect.assertions(2);
const p = { a: 1 };
class Test extends Component {
static template = xml`<span>simple vnode</span>`;
setup() {
expect(Object.prototype.hasOwnProperty.call(this.props, "a")).toBe(true);
}
}
const app = new App(Test, { props: p });
await app.mount(fixture);
});
test("props value are own property of props object, even with default values", async () => {
expect.assertions(3);
const p = { a: 1 };
class Test extends Component {
static template = xml`<span>simple vnode</span>`;
static defaultProps = { b: 1 };
setup() {
expect(Object.prototype.hasOwnProperty.call(this.props, "a")).toBe(true);
expect(Object.prototype.hasOwnProperty.call(this.props, "b")).toBe(true);
}
}
const app = new App(Test, { props: p });
await app.mount(fixture);
});
test("some simple sanity checks (el/status)", async () => {
expect.assertions(3);
class Test extends Component {
static template = xml`<span>simple vnode</span>`;
setup() {
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`<span>simple vnode</span>`;
}
let error: Error;
try {
await mount(Test, null as any);
} catch (e) {
error = e as Error;
}
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`<div/>`;
}
let error: Error;
try {
await mount(Test, document.createElement("div"));
} catch (e) {
error = e as Error;
}
expect(error!).toBeDefined();
expect(error!.message).toBe("Cannot mount a component on a detached dom node");
});
test("a component cannot be mounted in a detached node (even if node is detached later)", async () => {
const warn = console.warn;
console.warn = jest.fn();
class Test extends Component {
static template = xml`<div/>`;
}
let error: Error;
const app = new App(Test);
const prom = app.mount(fixture);
await Promise.resolve();
fixture.remove();
prom.catch((e: Error) => (error = e));
await expect(nextAppError(app)).resolves.toThrow(
"Cannot mount a component on a detached dom node"
);
expect(error!).toBeDefined();
expect(error!.message).toBe("Cannot mount a component on a detached dom node");
expect(console.warn).toBeCalledTimes(1);
console.warn = warn;
});
test("crashes if it cannot find a template", async () => {
class Test extends Component {
static template = "wrongtemplate";
}
let error: Error;
try {
await mount(Test, fixture);
} catch (e) {
error = e as Error;
}
expect(error!).toBeDefined();
expect(error!.message).toBe('Missing template: "wrongtemplate" (for component "Test")');
});
test("class component with dynamic text", async () => {
class Test extends Component {
static template = xml`<span>My value: <t t-esc="value"/></span>`;
value = 42;
}
await mount(Test, fixture);
expect(fixture.innerHTML).toBe("<span>My value: 42</span>");
});
test("Multi root component", async () => {
class Test extends Component {
static template = xml`<span>1</span>text<span>2</span>`;
value = 42;
}
await mount(Test, fixture);
expect(fixture.innerHTML).toBe(`<span>1</span>text<span>2</span>`);
});
test("a component inside a component", async () => {
class Child extends Component {
static template = xml`<div>simple vnode</div>`;
}
class Parent extends Component {
static template = xml`<span><Child/></span>`;
static components = { Child };
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("<span><div>simple vnode</div></span>");
});
test("a class component inside a class component, no external dom", async () => {
class Child extends Component {
static template = xml`<div>simple vnode</div>`;
}
class Parent extends Component {
static template = xml`<Child/>`;
static components = { Child };
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("<div>simple vnode</div>");
});
test("simple component with a dynamic text", async () => {
class Test extends Component {
static template = xml`<div><t t-esc="value" /></div>`;
value = 3;
}
const test = await mount(Test, fixture);
expect(fixture.innerHTML).toBe("<div>3</div>");
test.value = 5;
test.render();
await nextTick();
expect(fixture.innerHTML).toBe("<div>5</div>");
});
test("simple component, useState", async () => {
class Test extends Component {
static template = xml`<div><t t-esc="state.value" /></div>`;
state = useState({ value: 3 });
}
const test = await mount(Test, fixture);
expect(fixture.innerHTML).toBe("<div>3</div>");
test.state.value = 5;
await nextTick();
expect(fixture.innerHTML).toBe("<div>5</div>");
});
test("two child components", async () => {
class Child extends Component {
static template = xml`<div>simple vnode</div>`;
}
class Parent extends Component {
static template = xml`<Child/><Child/>`;
static components = { Child };
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("<div>simple vnode</div><div>simple vnode</div>");
});
test("class parent, class child component with props", async () => {
class Child extends Component {
static template = xml`<div><t t-esc="props.value" /></div>`;
}
class Parent extends Component {
static template = xml`<Child value="42" />`;
static components = { Child };
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("<div>42</div>");
});
test("parent, child and grandchild", async () => {
class GrandChild extends Component {
static template = xml`<div>hey</div>`;
}
class Child extends Component {
static template = xml`<GrandChild />`;
static components = { GrandChild };
}
class Parent extends Component {
static template = xml`<Child/>`;
static components = { Child };
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("<div>hey</div>");
});
test("zero or one child components", async () => {
class Child extends Component {
static template = xml`<div>simple vnode</div>`;
}
class Parent extends Component {
static template = xml`<Child t-if="state.hasChild"/>`;
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("<div>simple vnode</div>");
});
test("text after a conditional component", async () => {
class Child extends Component {
static template = xml`<p>simple vnode</p>`;
}
class Parent extends Component {
static template = xml`
<div>
<Child t-if="state.hasChild"/>
<span t-esc="state.text"/>
</div>`;
static components = { Child };
state = useState({ hasChild: false, text: "1" });
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("<div><span>1</span></div>");
parent.state.hasChild = true;
await nextTick();
expect(fixture.innerHTML).toBe("<div><p>simple vnode</p><span>1</span></div>");
parent.state.hasChild = false;
parent.state.text = "2";
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>2</span></div>");
});
test("can be clicked on and updated", async () => {
class Counter extends Component {
static template = xml`
<div><t t-esc="state.counter"/><button t-on-click="() => state.counter++">Inc</button></div>`;
state = useState({
counter: 0,
});
}
await mount(Counter, fixture);
expect(fixture.innerHTML).toBe("<div>0<button>Inc</button></div>");
const button = fixture.getElementsByTagName("button")[0];
button.click();
await nextTick();
expect(fixture.innerHTML).toBe("<div>1<button>Inc</button></div>");
});
// TODO: rename
test("rerendering a widget with a sub widget", async () => {
class Counter extends Component {
static template = xml`
<div><t t-esc="state.counter"/><button t-on-click="() => state.counter++">Inc</button></div>`;
state = useState({
counter: 0,
});
}
class Parent extends Component {
static template = xml`<Counter/>`;
static components = { Counter };
}
const parent = await mount(Parent, fixture);
const button = fixture.getElementsByTagName("button")[0];
button.click();
await nextTick();
expect(fixture.innerHTML).toBe("<div>1<button>Inc</button></div>");
await parent.render();
expect(fixture.innerHTML).toBe("<div>1<button>Inc</button></div>");
});
test("can handle empty props", async () => {
class Child extends Component {
static template = xml`<span><t t-esc="props.val"/></span>`;
}
class Parent extends Component {
static template = xml`<div><Child val=""/></div>`;
static components = { Child };
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("<div><span></span></div>");
});
test("child can be updated", async () => {
class Child extends Component {
static template = xml`<t t-esc="props.value"/>`;
}
class Parent extends Component {
static template = xml`<Child value="state.counter"/>`;
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`<div>a</div>`;
}
class ChildB extends Component {
static template = xml`<span>b</span>`;
}
class Child extends Component {
static template = xml`<ChildA t-if="props.child==='a'"/><ChildB t-else=""/>`;
static components = { ChildA, ChildB };
}
class Parent extends Component {
static template = xml`<Child child="state.child" />`;
static components = { Child };
state = useState({ child: "a" });
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe(`<div>a</div>`);
parent.state.child = "b";
await nextTick();
expect(fixture.innerHTML).toBe(`<span>b</span>`);
});
test("three level of components with collapsing root nodes", async () => {
class GrandChild extends Component {
static template = xml`<div>2</div>`;
}
class Child extends Component {
static components = { GrandChild };
static template = xml`<GrandChild/>`;
}
class Parent extends Component {
static components = { Child };
static template = xml`<Child></Child>`;
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("<div>2</div>");
});
test("do not remove previously rendered dom if not necessary", async () => {
class SomeComponent extends Component {
static template = xml`<div/>`;
}
const widget = await mount(SomeComponent, fixture);
expect(fixture.innerHTML).toBe(`<div></div>`);
fixture.querySelector("div")!.appendChild(document.createElement("span"));
expect(fixture.innerHTML).toBe(`<div><span></span></div>`);
await widget.render();
expect(fixture.innerHTML).toBe(`<div><span></span></div>`);
});
test("do not remove previously rendered dom if not necessary, variation", async () => {
class SomeComponent extends Component {
static template = xml`<div><h1>h1</h1><span><t t-esc="state.value"/></span></div>`;
state = useState({ value: 1 });
}
const comp = await mount(SomeComponent, fixture);
expect(fixture.innerHTML).toBe(`<div><h1>h1</h1><span>1</span></div>`);
fixture.querySelector("h1")!.appendChild(document.createElement("p"));
expect(fixture.innerHTML).toBe("<div><h1>h1<p></p></h1><span>1</span></div>");
comp.state.value++;
await nextTick();
expect(fixture.innerHTML).toBe("<div><h1>h1<p></p></h1><span>2</span></div>");
});
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`<span>child</span>`;
}
class Parent extends Component {
static template = xml`
<div>
<Child />
<Child t-key="4"/>
</div>
`;
static components = { Child };
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("<div><span>child</span><span>child</span></div>");
});
test("same t-keys in two different places", async () => {
class Child extends Component {
static template = xml`<span><t t-esc="props.blip"/></span>`;
}
class Parent extends Component {
static template = xml`
<div>
<div><Child t-key="1" blip="'1'"/></div>
<div><Child t-key="1" blip="'2'"/></div>
</div>`;
static components = { Child };
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("<div><div><span>1</span></div><div><span>2</span></div></div>");
});
test("t-key on a component with t-if, and a sibling component", async () => {
class Child extends Component {
static template = xml`<span>child</span>`;
}
class Parent extends Component {
static template = xml`
<div>
<Child t-if="false" t-key="'str'"/>
<Child/>
</div>`;
static components = { Child };
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("<div><span>child</span></div>");
});
test("widget after a t-foreach", async () => {
class SomeComponent extends Component {
static template = xml`<div/>`;
}
class Test extends Component {
static template = xml`<div><t t-foreach="Array(2)" t-as="elem" t-key="elem_index">txt</t><SomeComponent/></div>`;
static components = { SomeComponent };
}
await mount(Test, fixture);
expect(fixture.innerHTML).toBe("<div>txttxt<div></div></div>");
});
test("t-if works with t-component", async () => {
class Child extends Component {
static template = xml`<span>hey</span>`;
}
class Parent extends Component {
static template = xml`<div><Child t-if="state.flag"/></div>`;
static components = { Child };
state = useState({ flag: true });
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("<div><span>hey</span></div>");
parent.state.flag = false;
await nextTick();
expect(fixture.innerHTML).toBe("<div></div>");
parent.state.flag = true;
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>hey</span></div>");
});
test("t-else works with t-component", async () => {
class Child extends Component {
static template = xml`<span>hey</span>`;
}
class Parent extends Component {
static template = xml`
<div>
<div t-if="state.flag">somediv</div>
<Child t-else=""/>
</div>`;
static components = { Child };
state = useState({ flag: true });
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("<div><div>somediv</div></div>");
parent.state.flag = false;
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>hey</span></div>");
});
test("t-elif works with t-component", async () => {
class Child extends Component {
static template = xml`<span>hey</span>`;
}
class Parent extends Component {
static template = xml`
<div>
<div t-if="state.flag">somediv</div>
<Child t-elif="!state.flag" />
</div>`;
static components = { Child };
state = useState({ flag: true });
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("<div><div>somediv</div></div>");
parent.state.flag = false;
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>hey</span></div>");
});
test("t-else with empty string works with t-component", async () => {
class Child extends Component {
static template = xml`<span>hey</span>`;
}
class Parent extends Component {
static template = xml`
<div>
<div t-if="state.flag">somediv</div>
<Child t-else="" />
</div>`;
static components = { Child };
state = useState({ flag: true });
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("<div><div>somediv</div></div>");
parent.state.flag = false;
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>hey</span></div>");
});
test("sub components between t-ifs", async () => {
// this confuses the patching algorithm...
class Child extends Component {
static template = xml`<span>child</span>`;
}
class Parent extends Component {
static template = xml`
<div>
<h1 t-if="state.flag">hey</h1>
<h2 t-else="">noo</h2>
<span><Child/></span>
<t t-if="state.flag"><span>test</span></t>
</div>`;
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(`<div><h2>noo</h2><span><span>child</span></span></div>`);
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(
`<div><h1>hey</h1><span><span>child</span></span><span>test</span></div>`
);
});
test("list of two sub components inside other nodes", async () => {
class SubWidget extends Component {
static template = xml`<span>asdf</span>`;
}
class Parent extends Component {
static template = xml`
<div>
<div t-foreach="state.blips" t-as="blip" t-key="blip.id">
<SubWidget />
<SubWidget />
</div>
</div>`;
static components = { SubWidget };
state = useState({ blips: [{ a: "a", id: 1 }] });
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("<div><div><span>asdf</span><span>asdf</span></div></div>");
});
// TODO: rename
test("updating widget immediately", async () => {
// in this situation, we protect against a bug that occurred: because of the
// interplay between components and vnodes, a sub widget vnode was patched
// twice.
class Child extends Component {
static template = xml`<span>abc<t t-if="props.flag">def</t></span>`;
}
class Parent extends Component {
static template = xml`<Child flag="state.flag"/>`;
static components = { Child };
state = useState({ flag: false });
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("<span>abc</span>");
parent.state.flag = true;
await nextTick();
expect(fixture.innerHTML).toBe("<span>abcdef</span>");
});
test("can inject values in tagged templates", async () => {
const SUBTEMPLATE = xml`<span><t t-esc="state.n"/></span>`;
class Parent extends Component {
static template = xml`<t t-call="${SUBTEMPLATE}"/>`;
state = useState({ n: 42 });
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("<span>42</span>");
});
// Depends on t-props
test("update props of component without concrete own node", async () => {
class Custom extends Component {
static template = xml`
<div class="widget-subkey">
<t t-esc="props.key"/>__<t t-esc="props.subKey"/>
</div>`;
}
class Child extends Component {
static components = { Custom };
static template = xml`
<Custom
t-key="props.subKey"
key="props.key"
subKey="props.subKey"/>`;
}
class Parent extends Component {
static components = { Child };
static template = xml`
<div>
<Child t-key="childProps.key" t-props="childProps"/>
</div>`;
childProps = {
key: 1,
subKey: 1,
};
}
const parent = await mount(Parent, fixture);
expect(fixture.textContent!.trim()).toBe("1__1");
// First step: change the Custom's instance
Object.assign(parent.childProps, {
subKey: 2,
});
parent.render();
await nextTick();
expect(fixture.textContent!.trim()).toBe("1__2");
// Second step, change both Child's and Custom's instance
Object.assign(parent.childProps, {
key: 2,
subKey: 3,
});
parent.render();
await nextTick();
expect(fixture.textContent!.trim()).toBe("2__3");
});
test("component children doesn't leak (if case)", async () => {
class Child extends Component {
static template = xml`<div />`;
setup() {
useLogLifecycle();
}
}
class Parent extends Component {
static components = { Child };
static template = xml`<Child t-if="ifVar" />`;
ifVar = true;
}
const parent = await mount(Parent, fixture);
expect(Object.keys(parent.__owl__.children).length).toStrictEqual(1);
expect([
"Child:setup",
"Child:willStart",
"Child:willRender",
"Child:rendered",
"Child:mounted",
]).toBeLogged();
parent.ifVar = false;
parent.render();
await nextTick();
expect(Object.keys(parent.__owl__.children).length).toStrictEqual(0);
expect(["Child:willUnmount", "Child:willDestroy"]).toBeLogged();
});
test("component children doesn't leak (t-key case)", async () => {
// This test should encompass the t-foreach and t-call cases too (because they also use a flavor of some key)
class Child extends Component {
static template = xml`<div />`;
setup() {
useLogLifecycle();
}
}
class Parent extends Component {
static components = { Child };
static template = xml`<Child t-key="keyVar" />`;
keyVar = 1;
}
const parent = await mount(Parent, fixture);
expect(Object.keys(parent.__owl__.children).length).toStrictEqual(1);
expect([
"Child:setup",
"Child:willStart",
"Child:willRender",
"Child:rendered",
"Child:mounted",
]).toBeLogged();
parent.keyVar = 2;
parent.render();
await nextTick();
expect(Object.keys(parent.__owl__.children).length).toStrictEqual(1);
expect([
"Child:setup",
"Child:willStart",
"Child:willRender",
"Child:rendered",
"Child:willUnmount",
"Child:willDestroy",
"Child:mounted",
]).toBeLogged();
});
test("GrandChild display is controlled by its GrandParent", async () => {
class GrandChild extends Component {
static template = xml`<div />`;
setup() {
useLogLifecycle();
}
}
class Child extends Component {
static components = { GrandChild };
static template = xml`<GrandChild t-if="props.displayGrandChild" />`;
}
class Parent extends Component {
static template = xml`<t t-component="myComp" displayGrandChild="displayGrandChild"/>`;
myComp = Child;
displayGrandChild = true;
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("<div></div>");
expect([
"GrandChild:setup",
"GrandChild:willStart",
"GrandChild:willRender",
"GrandChild:rendered",
"GrandChild:mounted",
]).toBeLogged();
parent.displayGrandChild = false;
parent.render();
await nextTick();
expect(fixture.innerHTML).toBe("");
expect(["GrandChild:willUnmount", "GrandChild:willDestroy"]).toBeLogged();
});
});
describe("mount targets", () => {
test("can mount a component (with position='first-child')", async () => {
class Root extends Component {
static template = xml`<div>app</div>`;
}
const span = document.createElement("span");
fixture.appendChild(span);
const app = new App(Root);
await app.mount(fixture, { position: "first-child" });
expect(fixture.innerHTML).toBe("<div>app</div><span></span>");
});
test("can mount a component (with default position='last-child')", async () => {
class Root extends Component {
static template = xml`<div>app</div>`;
}
const span = document.createElement("span");
fixture.appendChild(span);
const app = new App(Root);
await app.mount(fixture, { position: "last-child" });
expect(fixture.innerHTML).toBe("<span></span><div>app</div>");
});
test("mount function: can mount a component (with default position='last-child')", async () => {
class Root extends Component {
static template = xml`<div>app</div>`;
}
const span = document.createElement("span");
fixture.appendChild(span);
await mount(Root, fixture, { position: "last-child" });
expect(fixture.innerHTML).toBe("<span></span><div>app</div>");
});
test("default mount option is 'last-child'", async () => {
class Root extends Component {
static template = xml`<div>app</div>`;
}
const span = document.createElement("span");
fixture.appendChild(span);
const app = new App(Root);
await app.mount(fixture);
expect(fixture.innerHTML).toBe("<span></span><div>app</div>");
});
});
describe("support svg components", () => {
test("add proper namespace to svg", async () => {
class GComp extends Component {
static template = xml`
<g>
<circle cx="50" cy="50" r="4" stroke="green" stroke-width="1" fill="yellow"/>
</g>`;
}
class Svg extends Component {
static template = xml`
<svg>
<GComp/>
</svg>`;
static components = { GComp };
}
await mount(Svg, fixture);
expect(fixture.innerHTML).toBe(
'<svg xmlns="http://www.w3.org/2000/svg"><g xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="50" r="4" stroke="green" stroke-width="1" fill="yellow"></circle></g></svg>'
);
});
});
describe("t-out in components", () => {
test("update properly on state changes", async () => {
class Test extends Component {
static template = xml`<div><t t-out="state.value"/></div>`;
state = useState({ value: markup("<b>content</b>") });
}
const component = await mount(Test, fixture);
markup("prout");
expect(fixture.innerHTML).toBe("<div><b>content</b></div>");
component.state.value = markup("<span>other content</span>");
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>other content</span></div>");
});
test("can render list of t-out ", async () => {
class Test extends Component {
static template = xml`
<div>
<t t-foreach="state.items" t-as="item" t-key="item">
<t t-esc="item"/>
<t t-out="item"/>
</t>
</div>`;
state = useState({
items: [markup("<b>one</b>"), markup("<b>two</b>"), markup("<b>tree</b>")],
});
}
await mount(Test, fixture);
expect(fixture.innerHTML).toBe(
"<div>&lt;b&gt;one&lt;/b&gt;<b>one</b>&lt;b&gt;two&lt;/b&gt;<b>two</b>&lt;b&gt;tree&lt;/b&gt;<b>tree</b></div>"
);
});
test("can switch the contents of two t-out repeatedly", async () => {
class Test extends Component {
static template = xml`
<t t-out="state.a"/>
<t t-out="state.b"/>
`;
state = useState({
a: markup("<div>1</div>"),
b: markup("<div>2</div>"),
});
reverse() {
const { state } = this;
[state.a, state.b] = [state.b, state.a];
}
}
const comp = await mount(Test, fixture);
expect(fixture.innerHTML).toBe("<div>1</div><div>2</div>");
comp.reverse();
await nextTick();
expect(fixture.innerHTML).toBe("<div>2</div><div>1</div>");
comp.reverse();
await nextTick();
expect(fixture.innerHTML).toBe("<div>1</div><div>2</div>");
});
test("t-out and updating falsy values, ", async () => {
class Test extends Component {
static template = xml`<t t-out="state.a"/>`;
state: any = useState({ a: 0 });
}
const comp = await mount(Test, fixture);
expect(fixture.innerHTML).toBe("0");
comp.state.a = undefined;
await nextTick();
expect(fixture.innerHTML).toBe("");
comp.state.a = "hello";
await nextTick();
expect(fixture.innerHTML).toBe("hello");
comp.state.a = false;
await nextTick();
expect(fixture.innerHTML).toBe("false");
});
});