mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
b25e988628
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
300 lines
10 KiB
TypeScript
300 lines
10 KiB
TypeScript
import { createBlock, mount, patch, remove, text } from "../../src/runtime/blockdom";
|
|
import { makeTestFixture } from "./helpers";
|
|
|
|
//------------------------------------------------------------------------------
|
|
// Setup and helpers
|
|
//------------------------------------------------------------------------------
|
|
|
|
let fixture: HTMLElement;
|
|
|
|
beforeEach(() => {
|
|
fixture = makeTestFixture();
|
|
});
|
|
|
|
afterEach(() => {
|
|
fixture.remove();
|
|
});
|
|
|
|
describe("adding/patching blocks", () => {
|
|
test("simple block", async () => {
|
|
const block = createBlock("<div>foo</div>");
|
|
const tree = block();
|
|
expect(tree.el).toBe(undefined);
|
|
|
|
mount(tree, fixture);
|
|
expect(tree.el).not.toBe(undefined);
|
|
expect(fixture.innerHTML).toBe("<div>foo</div>");
|
|
});
|
|
|
|
test("block with dynamic content", async () => {
|
|
const block = createBlock("<div><p><block-text-0/></p></div>");
|
|
const tree = block(["foo"]);
|
|
|
|
mount(tree, fixture);
|
|
expect(fixture.innerHTML).toBe("<div><p>foo</p></div>");
|
|
patch(tree, block(["bar"]));
|
|
expect(fixture.innerHTML).toBe("<div><p>bar</p></div>");
|
|
|
|
patch(tree, block(["foo"]));
|
|
expect(fixture.innerHTML).toBe("<div><p>foo</p></div>");
|
|
});
|
|
|
|
test("block with 2 dynamic text nodes", async () => {
|
|
const block = createBlock("<div><p><block-text-0/></p><span><block-text-1/></span></div>");
|
|
const tree = block(["foo", "bar"]);
|
|
|
|
mount(tree, fixture);
|
|
expect(fixture.innerHTML).toBe("<div><p>foo</p><span>bar</span></div>");
|
|
patch(tree, block(["appa", "yip yip"]));
|
|
expect(fixture.innerHTML).toBe("<div><p>appa</p><span>yip yip</span></div>");
|
|
});
|
|
|
|
test("block with multiple references", async () => {
|
|
const block1 = createBlock(
|
|
"<div><block-text-0/><p><block-text-1/><block-text-2/></p><block-text-3/></div>"
|
|
);
|
|
const tree = block1(["1", "2", "3", "4"]);
|
|
|
|
mount(tree, fixture);
|
|
expect(fixture.innerHTML).toBe("<div>1<p>23</p>4</div>");
|
|
});
|
|
|
|
test("falsy values in block nodes", () => {
|
|
const cases = [
|
|
[false, "false"],
|
|
[undefined, ""],
|
|
[null, ""],
|
|
[0, "0"],
|
|
["", ""],
|
|
];
|
|
const block = createBlock("<p><block-text-0/></p>");
|
|
|
|
for (let [value, result] of cases) {
|
|
const fixture = makeTestFixture();
|
|
mount(block([value as any]), fixture);
|
|
expect(fixture.innerHTML).toBe(`<p>${result}</p>`);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("sub blocks", () => {
|
|
test("block with subblock (only child)", async () => {
|
|
const block1 = createBlock("<div><block-child-0/></div>");
|
|
const block2 = createBlock("<p>yip yip</p>");
|
|
const tree = block1([], [block2()]);
|
|
|
|
mount(tree, fixture);
|
|
|
|
expect(fixture.innerHTML).toBe("<div><p>yip yip</p></div>");
|
|
});
|
|
|
|
test("block with subblock (first child with sibling)", async () => {
|
|
const block1 = createBlock("<div><block-child-0/><span>something</span></div>");
|
|
const block2 = createBlock("<p>yip yip</p>");
|
|
const tree = block1([], [block2()]);
|
|
|
|
mount(tree, fixture);
|
|
|
|
expect(fixture.innerHTML).toBe("<div><p>yip yip</p><span>something</span></div>");
|
|
});
|
|
|
|
test("block with subblock (last child with sibling)", async () => {
|
|
const block1 = createBlock("<div><span>something</span><block-child-0/></div>");
|
|
const block2 = createBlock("<p>yip yip</p>");
|
|
const tree = block1([], [block2()]);
|
|
|
|
mount(tree, fixture);
|
|
|
|
expect(fixture.innerHTML).toBe("<div><span>something</span><p>yip yip</p></div>");
|
|
});
|
|
|
|
test("block with 2 subblocks", async () => {
|
|
const block1 = createBlock("<div><block-child-0/><block-child-1/></div>");
|
|
const block2 = createBlock("<p>yip yip</p>");
|
|
const tree = block1([], [block2(), text("appa")]);
|
|
|
|
mount(tree, fixture);
|
|
|
|
expect(fixture.innerHTML).toBe("<div><p>yip yip</p>appa</div>");
|
|
});
|
|
|
|
test("block with subblock with siblings", async () => {
|
|
const block1 = createBlock("<div><p>1</p><block-child-0/><p>2</p></div>");
|
|
const block2 = createBlock("<p>yip yip</p>");
|
|
|
|
const tree = block1([], [block2()]);
|
|
|
|
mount(tree, fixture);
|
|
expect(fixture.innerHTML).toBe("<div><p>1</p><p>yip yip</p><p>2</p></div>");
|
|
});
|
|
|
|
test("block with text, subblock and siblings", async () => {
|
|
let block1 = createBlock(`<div><p>before<block-child-0/>after</p><block-child-1/></div>`);
|
|
|
|
let tree = block1([], [text("water"), text("fire")]);
|
|
|
|
mount(tree, fixture);
|
|
expect(fixture.innerHTML).toBe("<div><p>beforewaterafter</p>fire</div>");
|
|
});
|
|
|
|
test("block with conditional child", async () => {
|
|
const block1 = createBlock("<div><p><block-child-0/></p></div>");
|
|
const block2 = createBlock("<span>foo</span>");
|
|
|
|
const tree = block1();
|
|
mount(tree, fixture);
|
|
expect(fixture.innerHTML).toBe("<div><p></p></div>");
|
|
|
|
patch(tree, block1([], [block2()]));
|
|
expect(fixture.innerHTML).toBe("<div><p><span>foo</span></p></div>");
|
|
|
|
patch(tree, block1());
|
|
expect(fixture.innerHTML).toBe("<div><p></p></div>");
|
|
});
|
|
|
|
test("block with subblock with dynamic content", async () => {
|
|
const block1 = createBlock("<div><block-child-0/></div>");
|
|
const block2 = createBlock("<p><block-text-0/></p>");
|
|
const tree = block1([], [block2(["yip yip"])]);
|
|
|
|
mount(tree, fixture);
|
|
expect(fixture.innerHTML).toBe("<div><p>yip yip</p></div>");
|
|
|
|
patch(tree, block1([], [block2(["foo"])]));
|
|
expect(fixture.innerHTML).toBe("<div><p>foo</p></div>");
|
|
});
|
|
|
|
test("block with dynamic content and subblock", async () => {
|
|
const block1 = createBlock("<div><block-child-0/><p><block-text-0/></p></div>");
|
|
const block2 = createBlock("<p>sub block</p>");
|
|
const tree = block1(["yip yip"], [block2()]);
|
|
|
|
mount(tree, fixture);
|
|
expect(fixture.innerHTML).toBe("<div><p>sub block</p><p>yip yip</p></div>");
|
|
|
|
patch(tree, block1(["foo"], [block2()]));
|
|
expect(fixture.innerHTML).toBe("<div><p>sub block</p><p>foo</p></div>");
|
|
});
|
|
});
|
|
|
|
describe("remove elem blocks", () => {
|
|
test("elem block can be removed", async () => {
|
|
const block = createBlock("<div>foo</div>");
|
|
const tree = block();
|
|
mount(tree, fixture);
|
|
expect(fixture.innerHTML).toBe("<div>foo</div>");
|
|
remove(tree);
|
|
expect(fixture.innerHTML).toBe("");
|
|
expect(fixture.childNodes.length).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe("misc", () => {
|
|
test("constructed block as correct number of refs", () => {
|
|
const block = createBlock("<p><p><block-text-0/></p><block-text-1/></p>");
|
|
const tree = block(["a", "b"]);
|
|
mount(tree, fixture);
|
|
expect(fixture.innerHTML).toBe("<p><p>a</p>b</p>");
|
|
expect((tree as any).refs.length).toBe(2);
|
|
});
|
|
|
|
test("block vnode can be used as text", () => {
|
|
const block = createBlock("<p>a</p>");
|
|
mount(text(block() as any), fixture);
|
|
expect(fixture.textContent).toBe("<p>a</p>");
|
|
});
|
|
|
|
test("block vnode can be used to represent a <tr>", () => {
|
|
const block = createBlock("<tr><td>tomato</td></tr>");
|
|
const tree = block();
|
|
const fixture = document.createElement("table");
|
|
mount(tree, fixture);
|
|
expect(fixture.outerHTML).toBe("<table><tr><td>tomato</td></tr></table>");
|
|
});
|
|
|
|
test("block vnode with <tr> can be used as text ", () => {
|
|
const block = createBlock("<tr><td>tomato</td></tr>");
|
|
mount(text(block() as any), fixture);
|
|
expect(fixture.textContent).toBe("<tr><td>tomato</td></tr>");
|
|
});
|
|
|
|
test("call toString for function/objects if used as inline text in block", () => {
|
|
const block = createBlock("<p><block-text-0/><block-text-1/></p>");
|
|
const f = () => 3;
|
|
const g = () => 4;
|
|
g.toString = () => "tostring";
|
|
mount(block([f, g]), fixture);
|
|
expect(fixture.innerHTML).toBe("<p>() => 3tostring</p>");
|
|
});
|
|
|
|
test("block with 2 subblocks: variation", async () => {
|
|
const block = createBlock("<a><b><c><block-child-0/>2</c></b><block-child-1/></a>");
|
|
const tree = block([], [text("1"), text("3")]);
|
|
mount(tree, fixture);
|
|
expect(fixture.innerHTML).toBe("<a><b><c>12</c></b>3</a>");
|
|
});
|
|
|
|
test("block with 2 subblocks: another variation", async () => {
|
|
const block = createBlock(
|
|
`<a block-attribute-0="hello"><b><c><block-child-0/>2</c></b><block-child-1/></a>`
|
|
);
|
|
const tree = block(["world"], [text("1"), text("3")]);
|
|
mount(tree, fixture);
|
|
expect(fixture.innerHTML).toBe(`<a hello="world"><b><c>12</c></b>3</a>`);
|
|
});
|
|
|
|
test("namespace is not propagated to siblings", () => {
|
|
const block = createBlock(`<div><svg xmlns="someNameSpace"><g/></svg><div></div></div>`);
|
|
|
|
const fixture = makeTestFixture();
|
|
mount(block(), fixture);
|
|
|
|
expect(fixture.innerHTML).toBe(
|
|
'<div><svg xmlns="someNameSpace"><g></g></svg><div></div></div>'
|
|
);
|
|
expect(fixture.querySelector("svg")!.namespaceURI).toBe("someNameSpace");
|
|
expect(fixture.querySelector("g")!.namespaceURI).toBe("someNameSpace");
|
|
const allDivs = fixture.querySelectorAll("div");
|
|
expect(Array.from(allDivs).map((el) => el.namespaceURI)).toEqual([
|
|
"http://www.w3.org/1999/xhtml",
|
|
"http://www.w3.org/1999/xhtml",
|
|
]);
|
|
});
|
|
|
|
// test.skip("reusing a block skips patching process", async () => {
|
|
// const block = createBlock('<div><block-text-0/></div>');
|
|
// const foo = block(["foo"]);
|
|
// const bar = block(["bar"]);
|
|
// let fooCounter = 0;
|
|
// let barCounter = 0;
|
|
// let fooValue = "foo";
|
|
// let barValue = "bar";
|
|
// Object.defineProperty(foo.data, 0, {
|
|
// get() {
|
|
// fooCounter++;
|
|
// return fooValue;
|
|
// },
|
|
// });
|
|
// Object.defineProperty(bar.data, 0, {
|
|
// get() {
|
|
// barCounter++;
|
|
// return barValue;
|
|
// },
|
|
// set(val) {
|
|
// barValue = val;
|
|
// },
|
|
// });
|
|
|
|
// const bdom = multi([foo, bar]);
|
|
// mount(bdom, fixture);
|
|
// expect(fooCounter).toBe(1);
|
|
// expect(barCounter).toBe(1);
|
|
// expect(fixture.innerHTML).toBe("<div>foo</div><div>bar</div>");
|
|
|
|
// patch(bdom, multi([foo, block(["otherbar"])]));
|
|
// expect(fixture.innerHTML).toBe("foootherbar");
|
|
// expect(fooCounter).toBe(1);
|
|
// expect(barCounter).toBe(2);
|
|
// });
|
|
});
|