From ae36cca2efe85a722a84219f3108dd3ddbb4b594 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Mon, 22 Apr 2019 21:37:10 +0200 Subject: [PATCH] [ADD] vdom: readd original snabbdom tests --- tests/vdom.test.ts | 1224 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1224 insertions(+) create mode 100644 tests/vdom.test.ts diff --git a/tests/vdom.test.ts b/tests/vdom.test.ts new file mode 100644 index 00000000..3516ba5b --- /dev/null +++ b/tests/vdom.test.ts @@ -0,0 +1,1224 @@ +import { h, patch, init } from "../src/vdom"; + +function map(list, fn) { + var ret: any[] = []; + for (var i = 0; i < list.length; ++i) { + ret[i] = fn(list[i]); + } + return ret; +} + +//------------------------------------------------------------------------------ +// Attributes +//------------------------------------------------------------------------------ +describe("attributes", function() { + let elm, vnode0; + beforeEach(function() { + elm = document.createElement("div"); + vnode0 = elm; + }); + + test("have their provided values", function() { + const vnode1 = h("div", { + attrs: { href: "/foo", minlength: 1, selected: true, disabled: false } + }); + elm = patch(vnode0, vnode1).elm; + + expect(elm.getAttribute("href")).toBe("/foo"); + expect(elm.getAttribute("minlength")).toBe("1"); + expect(elm.hasAttribute("selected")).toBe(true); + expect(elm.getAttribute("selected")).toBe(""); + expect(elm.hasAttribute("disabled")).toBe(false); + }); + + test("can be memoized", function() { + const cachedAttrs = { href: "/foo", minlength: 1, selected: true }; + const vnode1 = h("div", { attrs: cachedAttrs }); + const vnode2 = h("div", { attrs: cachedAttrs }); + elm = patch(vnode0, vnode1).elm; + expect(elm.getAttribute("href")).toBe("/foo"); + expect(elm.getAttribute("minlength")).toBe("1"); + expect(elm.getAttribute("selected")).toBe(""); + elm = patch(vnode1, vnode2).elm; + expect(elm.getAttribute("href")).toBe("/foo"); + expect(elm.getAttribute("minlength")).toBe("1"); + expect(elm.getAttribute("selected")).toBe(""); + }); + + test("are not omitted when falsy values are provided", function() { + const vnode1 = h("div", { + attrs: { href: null, minlength: 0, value: "", title: "undefined" } + }); + elm = patch(vnode0, vnode1).elm; + expect(elm.getAttribute("href")).toBe("null"); + expect(elm.getAttribute("minlength")).toBe("0"); + expect(elm.getAttribute("value")).toBe(""); + expect(elm.getAttribute("title")).toBe("undefined"); + }); + + test("are set correctly when namespaced", function() { + const vnode1 = h("div", { attrs: { "xlink:href": "#foo" } }); + elm = patch(vnode0, vnode1).elm; + expect(elm.getAttributeNS("http://www.w3.org/1999/xlink", "href")).toBe( + "#foo" + ); + }); + + test("should not touch class nor id fields", function() { + elm = document.createElement("div"); + elm.id = "myId"; + elm.className = "myClass"; + vnode0 = elm; + const vnode1 = h("div#myId.myClass", { attrs: {} }, ["Hello"]); + elm = patch(vnode0, vnode1).elm; + expect(elm.tagName).toBe("DIV"); + expect(elm.id).toBe("myId"); + expect(elm.className).toBe("myClass"); + expect(elm.textContent).toBe("Hello"); + }); + + describe("boolean attribute", function() { + test("is present and empty string if the value is truthy", function() { + const vnode1 = h("div", { + attrs: { required: true, readonly: 1, noresize: "truthy" } + }); + elm = patch(vnode0, vnode1).elm; + expect(elm.hasAttribute("required")).toBe(true); + expect(elm.getAttribute("required")).toBe(""); + expect(elm.hasAttribute("readonly")).toBe(true); + expect(elm.getAttribute("readonly")).toBe("1"); + expect(elm.hasAttribute("noresize")).toBe(true); + expect(elm.getAttribute("noresize")).toBe("truthy"); + }); + + test("is omitted if the value is false", function() { + const vnode1 = h("div", { attrs: { required: false } }); + elm = patch(vnode0, vnode1).elm; + expect(elm.hasAttribute("required")).toBe(false); + expect(elm.getAttribute("required")).toBe(null); + }); + + test("is not omitted if the value is falsy but casted to string", function() { + const vnode1 = h("div", { attrs: { readonly: 0, noresize: null } }); + elm = patch(vnode0, vnode1).elm; + expect(elm.getAttribute("readonly")).toBe("0"); + expect(elm.getAttribute("noresize")).toBe("null"); + }); + }); + + describe("Object.prototype property", function() { + test("is not considered as a boolean attribute and shouldn't be omitted", function() { + const vnode1 = h("div", { attrs: { constructor: true } }); + elm = patch(vnode0, vnode1).elm; + expect(elm.hasAttribute("constructor")).toBe(true); + expect(elm.getAttribute("constructor")).toBe(""); + const vnode2 = h("div", { attrs: { constructor: false } }); + elm = patch(vnode0, vnode2).elm; + expect(elm.hasAttribute("constructor")).toBe(false); + }); + }); +}); + +//------------------------------------------------------------------------------ +// Hyperscript +//------------------------------------------------------------------------------ +describe("hyperscript", function() { + test("can create vnode with proper tag", function() { + expect(h("div").sel).toBe("div"); + expect(h("a").sel).toBe("a"); + }); + + test("can create vnode with children", function() { + const vnode = h("div", [h("span#hello"), h("b.world")]); + expect(vnode.sel).toBe("div"); + expect((vnode).children[0].sel).toBe("span#hello"); + expect((vnode).children[1].sel).toBe("b.world"); + }); + + test("can create vnode with one child vnode", function() { + const vnode = h("div", h("span#hello")); + expect(vnode.sel).toBe("div"); + expect((vnode).children[0].sel).toBe("span#hello"); + }); + + test("can create vnode with props and one child vnode", function() { + const vnode = h("div", {}, h("span#hello")); + expect(vnode.sel).toBe("div"); + expect((vnode).children[0].sel).toBe("span#hello"); + }); + + test("can create vnode with text content", function() { + const vnode = h("a", ["I am a string"]); + expect((vnode).children[0].text).toBe("I am a string"); + }); + + test("can create vnode with text content in string", function() { + const vnode = h("a", "I am a string"); + expect(vnode.text).toBe("I am a string"); + }); + + test("can create vnode with props and text content in string", function() { + const vnode = h("a", {}, "I am a string"); + expect(vnode.text).toBe("I am a string"); + }); + + test("can create vnode for comment", function() { + const vnode = h("!", "test"); + expect(vnode.sel).toBe("!"); + expect(vnode.text).toBe("test"); + }); +}); + +//------------------------------------------------------------------------------ +// VDOM +//------------------------------------------------------------------------------ +describe("snabbdom", function() { + let elm: any, vnode0; + beforeEach(function() { + elm = document.createElement("div"); + vnode0 = elm; + }); + + describe("created element", function() { + test("has tag", function() { + elm = patch(vnode0, h("div")).elm; + expect(elm.tagName).toBe("DIV"); + }); + + test("has different tag and id", function() { + let elm: any = document.createElement("div"); + vnode0.appendChild(elm); + const vnode1 = h("span#id"); + elm = patch(elm, vnode1).elm; + expect(elm.tagName).toBe("SPAN"); + expect(elm.id).toBe("id"); + }); + + test("has id", function() { + elm = patch(vnode0, h("div", [h("div#unique")])).elm; + expect(elm.firstChild.id).toBe("unique"); + }); + + test("has correct namespace", function() { + const SVGNamespace = "http://www.w3.org/2000/svg"; + const XHTMLNamespace = "http://www.w3.org/1999/xhtml"; + + elm = patch(vnode0, h("div", [h("div", { ns: SVGNamespace })])).elm; + expect(elm.firstChild.namespaceURI).toBe(SVGNamespace); + + // verify that svg tag automatically gets svg namespace + elm = patch( + vnode0, + h("svg", [ + h("foreignObject", [h("div", ["I am HTML embedded in SVG"])]) + ]) + ).elm; + expect(elm.namespaceURI).toBe(SVGNamespace); + expect(elm.firstChild.namespaceURI).toBe(SVGNamespace); + expect(elm.firstChild.firstChild.namespaceURI).toBe(XHTMLNamespace); + + // verify that svg tag with extra selectors gets svg namespace + elm = patch(vnode0, h("svg#some-id")).elm; + expect(elm.namespaceURI).toBe(SVGNamespace); + + // verify that non-svg tag beginning with 'svg' does NOT get namespace + elm = patch(vnode0, h("svg-custom-el")).elm; + expect(elm.namespaceURI).not.toBe(SVGNamespace); + }); + + test("receives classes in selector", function() { + elm = patch(vnode0, h("div", [h("i.am.a.class")])).elm; + expect(elm.firstChild.classList.contains("am")).toBeTruthy(); + expect(elm.firstChild.classList.contains("a")).toBeTruthy(); + expect(elm.firstChild.classList.contains("class")).toBeTruthy(); + }); + + test("receives classes in selector when namespaced", function() { + elm = patch(vnode0, h("svg", [h("g.am.a.class.too")])).elm; + expect(elm.firstChild.classList.contains("am")).toBeTruthy(); + expect(elm.firstChild.classList.contains("a")).toBeTruthy(); + expect(elm.firstChild.classList.contains("class")).toBeTruthy(); + }); + + test("can create elements with text content", function() { + elm = patch(vnode0, h("div", ["I am a string"])).elm; + expect(elm.innerHTML).toBe("I am a string"); + }); + + test("can create elements with span and text content", function() { + elm = patch(vnode0, h("a", [h("span"), "I am a string"])).elm; + expect(elm.childNodes[0].tagName).toBe("SPAN"); + expect(elm.childNodes[1].textContent).toBe("I am a string"); + }); + + test("can create elements with props", function() { + elm = patch(vnode0, h("a", { props: { src: "http://localhost/" } })).elm; + expect(elm.src).toBe("http://localhost/"); + }); + + test("is a patch of the root element", function() { + const elmWithIdAndClass = document.createElement("div"); + elmWithIdAndClass.id = "id"; + elmWithIdAndClass.className = "class"; + const vnode1 = h("div#id.class", [h("span", "Hi")]); + elm = patch(elmWithIdAndClass, vnode1).elm; + expect(elm).toBe(elmWithIdAndClass); + expect(elm.tagName).toBe("DIV"); + expect(elm.id).toBe("id"); + expect(elm.className).toBe("class"); + }); + + test("can create comments", function() { + elm = patch(vnode0, h("!", "test")).elm; + expect(elm.nodeType).toBe(document.COMMENT_NODE); + expect(elm.textContent).toBe("test"); + }); + }); + + describe("patching an element", function() { + test("changes the elements classes", function() { + const vnode1 = h("i.i.am.horse"); + const vnode2 = h("i.i.am"); + patch(vnode0, vnode1); + elm = patch(vnode1, vnode2).elm; + expect(elm.classList.contains("i")).toBeTruthy(); + expect(elm.classList.contains("am")).toBeTruthy(); + expect(!elm.classList.contains("horse")).toBeTruthy(); + }); + + test("removes missing classes", function() { + const vnode1 = h("i.i.am.horse"); + const vnode2 = h("i.i.am"); + patch(vnode0, vnode1); + elm = patch(vnode1, vnode2).elm; + expect(elm.classList.contains("i")).toBeTruthy(); + expect(elm.classList.contains("am")).toBeTruthy(); + expect(!elm.classList.contains("horse")).toBeTruthy(); + }); + + test("changes an elements props", function() { + const vnode1 = h("a", { props: { src: "http://other/" } }); + const vnode2 = h("a", { props: { src: "http://localhost/" } }); + patch(vnode0, vnode1); + elm = patch(vnode1, vnode2).elm; + expect(elm.src).toBe("http://localhost/"); + }); + + test("preserves memoized props", function() { + const cachedProps = { src: "http://other/" }; + const vnode1 = h("a", { props: cachedProps }); + const vnode2 = h("a", { props: cachedProps }); + elm = patch(vnode0, vnode1).elm; + expect(elm.src).toBe("http://other/"); + elm = patch(vnode1, vnode2).elm; + expect(elm.src).toBe("http://other/"); + }); + + test("removes an elements props", function() { + const vnode1 = h("a", { props: { src: "http://other/" } }); + const vnode2 = h("a"); + patch(vnode0, vnode1); + patch(vnode1, vnode2); + expect(elm.src).toBeUndefined(); + }); + }); + + describe("updating children with keys", function() { + function spanNum(n) { + if (n == null) { + return n; + } else if (typeof n === "string") { + return h("span", {}, n); + } else { + return h("span", { key: n }, n.toString()); + } + } + + describe("addition of elements", function() { + test("appends elements", function() { + const vnode1 = h("span", [1].map(spanNum)); + const vnode2 = h("span", [1, 2, 3].map(spanNum)); + elm = patch(vnode0, vnode1).elm; + expect(elm.children.length).toBe(1); + elm = patch(vnode1, vnode2).elm; + expect(elm.children.length).toBe(3); + expect(elm.children[1].innerHTML).toBe("2"); + expect(elm.children[2].innerHTML).toBe("3"); + }); + + test("prepends elements", function() { + const vnode1 = h("span", [4, 5].map(spanNum)); + const vnode2 = h("span", [1, 2, 3, 4, 5].map(spanNum)); + elm = patch(vnode0, vnode1).elm; + expect(elm.children.length).toBe(2); + elm = patch(vnode1, vnode2).elm; + expect(map(elm.children, c => c.innerHTML)).toEqual([ + "1", + "2", + "3", + "4", + "5" + ]); + }); + + test("add elements in the middle", function() { + const vnode1 = h("span", [1, 2, 4, 5].map(spanNum)); + const vnode2 = h("span", [1, 2, 3, 4, 5].map(spanNum)); + elm = patch(vnode0, vnode1).elm; + expect(elm.children.length).toBe(4); + expect(elm.children.length).toBe(4); + elm = patch(vnode1, vnode2).elm; + expect(map(elm.children, c => c.innerHTML)).toEqual([ + "1", + "2", + "3", + "4", + "5" + ]); + }); + + test("add elements at beginning and end", function() { + const vnode1 = h("span", [2, 3, 4].map(spanNum)); + const vnode2 = h("span", [1, 2, 3, 4, 5].map(spanNum)); + elm = patch(vnode0, vnode1).elm; + expect(elm.children.length).toBe(3); + elm = patch(vnode1, vnode2).elm; + expect(map(elm.children, c => c.innerHTML)).toEqual([ + "1", + "2", + "3", + "4", + "5" + ]); + }); + + test("adds children to parent with no children", function() { + const vnode1 = h("span", { key: "span" }); + const vnode2 = h("span", { key: "span" }, [1, 2, 3].map(spanNum)); + elm = patch(vnode0, vnode1).elm; + expect(elm.children.length).toBe(0); + elm = patch(vnode1, vnode2).elm; + expect(map(elm.children, c => c.innerHTML)).toEqual(["1", "2", "3"]); + }); + + test("removes all children from parent", function() { + const vnode1 = h("span", { key: "span" }, [1, 2, 3].map(spanNum)); + const vnode2 = h("span", { key: "span" }); + elm = patch(vnode0, vnode1).elm; + expect(map(elm.children, c => c.innerHTML)).toEqual(["1", "2", "3"]); + elm = patch(vnode1, vnode2).elm; + expect(elm.children.length).toBe(0); + }); + + test("update one child with same key but different sel", function() { + const vnode1 = h("span", { key: "span" }, [1, 2, 3].map(spanNum)); + const vnode2 = h("span", { key: "span" }, [ + spanNum(1), + h("i", { key: 2 }, "2"), + spanNum(3) + ]); + elm = patch(vnode0, vnode1).elm; + expect(map(elm.children, c => c.innerHTML)).toEqual(["1", "2", "3"]); + elm = patch(vnode1, vnode2).elm; + expect(map(elm.children, c => c.innerHTML)).toEqual(["1", "2", "3"]); + expect(elm.children.length).toBe(3); + expect(elm.children[1].tagName).toBe("I"); + }); + }); + + describe("removal of elements", function() { + test("removes elements from the beginning", function() { + const vnode1 = h("span", [1, 2, 3, 4, 5].map(spanNum)); + const vnode2 = h("span", [3, 4, 5].map(spanNum)); + elm = patch(vnode0, vnode1).elm; + expect(elm.children.length).toBe(5); + elm = patch(vnode1, vnode2).elm; + expect(map(elm.children, c => c.innerHTML)).toEqual(["3", "4", "5"]); + }); + + test("removes elements from the end", function() { + const vnode1 = h("span", [1, 2, 3, 4, 5].map(spanNum)); + const vnode2 = h("span", [1, 2, 3].map(spanNum)); + elm = patch(vnode0, vnode1).elm; + expect(elm.children.length).toBe(5); + elm = patch(vnode1, vnode2).elm; + expect(elm.children.length).toBe(3); + expect(elm.children[0].innerHTML).toBe("1"); + expect(elm.children[1].innerHTML).toBe("2"); + expect(elm.children[2].innerHTML).toBe("3"); + }); + + test("removes elements from the middle", function() { + const vnode1 = h("span", [1, 2, 3, 4, 5].map(spanNum)); + const vnode2 = h("span", [1, 2, 4, 5].map(spanNum)); + elm = patch(vnode0, vnode1).elm; + expect(elm.children.length).toBe(5); + elm = patch(vnode1, vnode2).elm; + expect(elm.children.length).toBe(4); + expect(elm.children[0].innerHTML).toBe("1"); + expect(elm.children[1].innerHTML).toBe("2"); + expect(elm.children[2].innerHTML).toBe("4"); + expect(elm.children[3].innerHTML).toBe("5"); + }); + }); + + describe("element reordering", function() { + test("moves element forward", function() { + const vnode1 = h("span", [1, 2, 3, 4].map(spanNum)); + const vnode2 = h("span", [2, 3, 1, 4].map(spanNum)); + elm = patch(vnode0, vnode1).elm; + expect(elm.children.length).toBe(4); + elm = patch(vnode1, vnode2).elm; + expect(elm.children.length).toBe(4); + expect(elm.children[0].innerHTML).toBe("2"); + expect(elm.children[1].innerHTML).toBe("3"); + expect(elm.children[2].innerHTML).toBe("1"); + expect(elm.children[3].innerHTML).toBe("4"); + }); + + test("moves element to end", function() { + const vnode1 = h("span", [1, 2, 3].map(spanNum)); + const vnode2 = h("span", [2, 3, 1].map(spanNum)); + elm = patch(vnode0, vnode1).elm; + expect(elm.children.length).toBe(3); + elm = patch(vnode1, vnode2).elm; + expect(elm.children.length).toBe(3); + expect(elm.children[0].innerHTML).toBe("2"); + expect(elm.children[1].innerHTML).toBe("3"); + expect(elm.children[2].innerHTML).toBe("1"); + }); + + test("moves element backwards", function() { + const vnode1 = h("span", [1, 2, 3, 4].map(spanNum)); + const vnode2 = h("span", [1, 4, 2, 3].map(spanNum)); + elm = patch(vnode0, vnode1).elm; + expect(elm.children.length).toBe(4); + elm = patch(vnode1, vnode2).elm; + expect(elm.children.length).toBe(4); + expect(elm.children[0].innerHTML).toBe("1"); + expect(elm.children[1].innerHTML).toBe("4"); + expect(elm.children[2].innerHTML).toBe("2"); + expect(elm.children[3].innerHTML).toBe("3"); + }); + + test("swaps first and last", function() { + const vnode1 = h("span", [1, 2, 3, 4].map(spanNum)); + const vnode2 = h("span", [4, 2, 3, 1].map(spanNum)); + elm = patch(vnode0, vnode1).elm; + expect(elm.children.length).toBe(4); + elm = patch(vnode1, vnode2).elm; + expect(elm.children.length).toBe(4); + expect(elm.children[0].innerHTML).toBe("4"); + expect(elm.children[1].innerHTML).toBe("2"); + expect(elm.children[2].innerHTML).toBe("3"); + expect(elm.children[3].innerHTML).toBe("1"); + }); + }); + + describe("combinations of additions, removals and reorderings", function() { + test("move to left and replace", function() { + const vnode1 = h("span", [1, 2, 3, 4, 5].map(spanNum)); + const vnode2 = h("span", [4, 1, 2, 3, 6].map(spanNum)); + elm = patch(vnode0, vnode1).elm; + expect(elm.children.length).toBe(5); + elm = patch(vnode1, vnode2).elm; + expect(elm.children.length).toBe(5); + expect(elm.children[0].innerHTML).toBe("4"); + expect(elm.children[1].innerHTML).toBe("1"); + expect(elm.children[2].innerHTML).toBe("2"); + expect(elm.children[3].innerHTML).toBe("3"); + expect(elm.children[4].innerHTML).toBe("6"); + }); + + test("moves to left and leaves hole", function() { + const vnode1 = h("span", [1, 4, 5].map(spanNum)); + const vnode2 = h("span", [4, 6].map(spanNum)); + elm = patch(vnode0, vnode1).elm; + expect(elm.children.length).toBe(3); + elm = patch(vnode1, vnode2).elm; + expect(map(elm.children, c => c.innerHTML)).toEqual(["4", "6"]); + }); + + test("handles moved and set to undefined element ending at the end", function() { + const vnode1 = h("span", [2, 4, 5].map(spanNum)); + const vnode2 = h("span", [4, 5, 3].map(spanNum)); + elm = patch(vnode0, vnode1).elm; + expect(elm.children.length).toBe(3); + elm = patch(vnode1, vnode2).elm; + expect(elm.children.length).toBe(3); + expect(elm.children[0].innerHTML).toBe("4"); + expect(elm.children[1].innerHTML).toBe("5"); + expect(elm.children[2].innerHTML).toBe("3"); + }); + + test("moves a key in non-keyed nodes with a size up", function() { + const vnode1 = h("span", [1, "a", "b", "c"].map(spanNum)); + const vnode2 = h("span", ["d", "a", "b", "c", 1, "e"].map(spanNum)); + elm = patch(vnode0, vnode1).elm; + expect(elm.children.length).toBe(4); + expect(elm.textContent).toBe("1abc"); + elm = patch(vnode1, vnode2).elm; + expect(elm.childNodes.length).toBe(6); + expect(elm.textContent).toBe("dabc1e"); + }); + }); + + describe("misc", function() { + test("reverses elements", function() { + const vnode1 = h("span", [1, 2, 3, 4, 5, 6, 7, 8].map(spanNum)); + const vnode2 = h("span", [8, 7, 6, 5, 4, 3, 2, 1].map(spanNum)); + elm = patch(vnode0, vnode1).elm; + expect(elm.children.length).toBe(8); + elm = patch(vnode1, vnode2).elm; + expect(map(elm.children, c => c.innerHTML)).toEqual([ + "8", + "7", + "6", + "5", + "4", + "3", + "2", + "1" + ]); + }); + + test("something", function() { + const vnode1 = h("span", [0, 1, 2, 3, 4, 5].map(spanNum)); + const vnode2 = h("span", [4, 3, 2, 1, 5, 0].map(spanNum)); + elm = patch(vnode0, vnode1).elm; + expect(elm.children.length).toBe(6); + elm = patch(vnode1, vnode2).elm; + expect(map(elm.children, c => c.innerHTML)).toEqual([ + "4", + "3", + "2", + "1", + "5", + "0" + ]); + }); + + test("supports null/undefined children", function() { + const vnode1 = h("i", [0, 1, 2, 3, 4, 5].map(spanNum)); + const vnode2 = h( + "i", + [null, 2, undefined, null, 1, 0, null, 5, 4, null, 3, undefined].map( + spanNum + ) + ); + elm = patch(vnode0, vnode1).elm; + expect(elm.children.length).toBe(6); + elm = patch(vnode1, vnode2).elm; + expect(map(elm.children, c => c.innerHTML)).toEqual([ + "2", + "1", + "0", + "5", + "4", + "3" + ]); + }); + + test("supports all null/undefined children", function() { + const vnode1 = h("i", [0, 1, 2, 3, 4, 5].map(spanNum)); + const vnode2 = h("i", [null, null, undefined, null, null, undefined]); + const vnode3 = h("i", [5, 4, 3, 2, 1, 0].map(spanNum)); + patch(vnode0, vnode1); + elm = patch(vnode1, vnode2).elm; + expect(elm.children.length).toBe(0); + elm = patch(vnode2, vnode3).elm; + expect(map(elm.children, c => c.innerHTML)).toEqual([ + "5", + "4", + "3", + "2", + "1", + "0" + ]); + }); + }); + }); + + describe("updating children without keys", function() { + test("appends elements", function() { + const vnode1 = h("div", [h("span", "Hello")]); + const vnode2 = h("div", [h("span", "Hello"), h("span", "World")]); + elm = patch(vnode0, vnode1).elm; + expect(map(elm.children, c => c.innerHTML)).toEqual(["Hello"]); + elm = patch(vnode1, vnode2).elm; + expect(map(elm.children, c => c.innerHTML)).toEqual(["Hello", "World"]); + }); + + test("handles unmoved text nodes", function() { + const vnode1 = h("div", ["Text", h("span", "Span")]); + const vnode2 = h("div", ["Text", h("span", "Span")]); + elm = patch(vnode0, vnode1).elm; + expect(elm.childNodes[0].textContent).toBe("Text"); + elm = patch(vnode1, vnode2).elm; + expect(elm.childNodes[0].textContent).toBe("Text"); + }); + + test("handles changing text children", function() { + const vnode1 = h("div", ["Text", h("span", "Span")]); + const vnode2 = h("div", ["Text2", h("span", "Span")]); + elm = patch(vnode0, vnode1).elm; + expect(elm.childNodes[0].textContent).toBe("Text"); + elm = patch(vnode1, vnode2).elm; + expect(elm.childNodes[0].textContent).toBe("Text2"); + }); + + test("handles unmoved comment nodes", function() { + const vnode1 = h("div", [h("!", "Text"), h("span", "Span")]); + const vnode2 = h("div", [h("!", "Text"), h("span", "Span")]); + elm = patch(vnode0, vnode1).elm; + expect(elm.childNodes[0].textContent).toBe("Text"); + elm = patch(vnode1, vnode2).elm; + expect(elm.childNodes[0].textContent).toBe("Text"); + }); + + test("handles changing comment text", function() { + const vnode1 = h("div", [h("!", "Text"), h("span", "Span")]); + const vnode2 = h("div", [h("!", "Text2"), h("span", "Span")]); + elm = patch(vnode0, vnode1).elm; + expect(elm.childNodes[0].textContent).toBe("Text"); + elm = patch(vnode1, vnode2).elm; + expect(elm.childNodes[0].textContent).toBe("Text2"); + }); + + test("handles changing empty comment", function() { + const vnode1 = h("div", [h("!"), h("span", "Span")]); + const vnode2 = h("div", [h("!", "Test"), h("span", "Span")]); + elm = patch(vnode0, vnode1).elm; + expect(elm.childNodes[0].textContent).toBe(""); + elm = patch(vnode1, vnode2).elm; + expect(elm.childNodes[0].textContent).toBe("Test"); + }); + + test("prepends element", function() { + const vnode1 = h("div", [h("span", "World")]); + const vnode2 = h("div", [h("span", "Hello"), h("span", "World")]); + elm = patch(vnode0, vnode1).elm; + expect(map(elm.children, c => c.innerHTML)).toEqual(["World"]); + elm = patch(vnode1, vnode2).elm; + expect(map(elm.children, c => c.innerHTML)).toEqual(["Hello", "World"]); + }); + + test("prepends element of different tag type", function() { + const vnode1 = h("div", [h("span", "World")]); + const vnode2 = h("div", [h("div", "Hello"), h("span", "World")]); + elm = patch(vnode0, vnode1).elm; + expect(map(elm.children, c => c.innerHTML)).toEqual(["World"]); + elm = patch(vnode1, vnode2).elm; + expect(map(elm.children, c => c.tagName)).toEqual(["DIV", "SPAN"]); + expect(map(elm.children, c => c.innerHTML)).toEqual(["Hello", "World"]); + }); + + test("removes elements", function() { + const vnode1 = h("div", [ + h("span", "One"), + h("span", "Two"), + h("span", "Three") + ]); + const vnode2 = h("div", [h("span", "One"), h("span", "Three")]); + elm = patch(vnode0, vnode1).elm; + expect(map(elm.children, c => c.innerHTML)).toEqual([ + "One", + "Two", + "Three" + ]); + elm = patch(vnode1, vnode2).elm; + expect(map(elm.children, c => c.innerHTML)).toEqual(["One", "Three"]); + }); + + test("removes a single text node", function() { + const vnode1 = h("div", "One"); + const vnode2 = h("div"); + patch(vnode0, vnode1); + expect(elm.textContent).toBe("One"); + patch(vnode1, vnode2); + expect(elm.textContent).toBe(""); + }); + + test("removes a single text node when children are updated", function() { + const vnode1 = h("div", "One"); + const vnode2 = h("div", [h("div", "Two"), h("span", "Three")]); + patch(vnode0, vnode1); + expect(elm.textContent).toBe("One"); + patch(vnode1, vnode2); + expect(map(elm.children, c => c.textContent)).toEqual(["Two", "Three"]); + }); + + test("removes a text node among other elements", function() { + const vnode1 = h("div", ["One", h("span", "Two")]); + const vnode2 = h("div", [h("div", "Three")]); + patch(vnode0, vnode1); + expect(map(elm.childNodes, c => c.textContent)).toEqual(["One", "Two"]); + patch(vnode1, vnode2); + expect(elm.childNodes.length).toBe(1); + expect(elm.childNodes[0].tagName).toBe("DIV"); + expect(elm.childNodes[0].textContent).toBe("Three"); + }); + + test("reorders elements", function() { + const vnode1 = h("div", [ + h("span", "One"), + h("div", "Two"), + h("b", "Three") + ]); + const vnode2 = h("div", [ + h("b", "Three"), + h("span", "One"), + h("div", "Two") + ]); + elm = patch(vnode0, vnode1).elm; + expect(map(elm.children, c => c.innerHTML)).toEqual([ + "One", + "Two", + "Three" + ]); + elm = patch(vnode1, vnode2).elm; + expect(map(elm.children, c => c.tagName)).toEqual(["B", "SPAN", "DIV"]); + expect(map(elm.children, c => c.innerHTML)).toEqual([ + "Three", + "One", + "Two" + ]); + }); + + test("supports null/undefined children", function() { + const vnode1 = h("i", [null, h("i", "1"), h("i", "2"), null]); + const vnode2 = h("i", [ + h("i", "2"), + undefined, + undefined, + h("i", "1"), + undefined + ]); + const vnode3 = h("i", [ + null, + h("i", "1"), + undefined, + null, + h("i", "2"), + undefined, + null + ]); + elm = patch(vnode0, vnode1).elm; + expect(map(elm.children, c => c.innerHTML)).toEqual(["1", "2"]); + elm = patch(vnode1, vnode2).elm; + expect(map(elm.children, c => c.innerHTML)).toEqual(["2", "1"]); + elm = patch(vnode2, vnode3).elm; + expect(map(elm.children, c => c.innerHTML)).toEqual(["1", "2"]); + }); + + test("supports all null/undefined children", function() { + const vnode1 = h("i", [h("i", "1"), h("i", "2")]); + const vnode2 = h("i", [null, undefined]); + const vnode3 = h("i", [h("i", "2"), h("i", "1")]); + patch(vnode0, vnode1); + elm = patch(vnode1, vnode2).elm; + expect(elm.children.length).toBe(0); + elm = patch(vnode2, vnode3).elm; + expect(map(elm.children, c => c.innerHTML)).toEqual(["2", "1"]); + }); + }); + + describe("hooks", function() { + describe("element hooks", function() { + test("calls `create` listener before inserted into parent but after children", function() { + const result: any[] = []; + function cb(empty, vnode) { + expect(vnode.elm).toBeInstanceOf(Element); + expect(vnode.elm.children).toHaveLength(2); + expect(vnode.elm.parentNode).toBeNull(); + result.push(vnode); + } + const vnode1 = h("div", [ + h("span", "First sibling"), + h("div", { hook: { create: cb } }, [ + h("span", "Child 1"), + h("span", "Child 2") + ]), + h("span", "Can't touch me") + ]); + patch(vnode0, vnode1); + expect(result).toHaveLength(1); + }); + + test("calls `insert` listener after both parents, siblings and children have been inserted", function() { + const result: any[] = []; + function cb(vnode) { + expect(vnode.elm).toBeInstanceOf(Element); + expect(vnode.elm.children).toHaveLength(2); + expect(vnode.elm.parentNode.children).toHaveLength(3); + result.push(vnode); + } + const vnode1 = h("div", [ + h("span", "First sibling"), + h("div", { hook: { insert: cb } }, [ + h("span", "Child 1"), + h("span", "Child 2") + ]), + h("span", "Can touch me") + ]); + patch(vnode0, vnode1); + expect(result).toHaveLength(1); + }); + + test("calls `prepatch` listener", function() { + const result: any[] = []; + function cb(oldVnode, vnode) { + expect(oldVnode).toBe(vnode1.children![1]); + expect(vnode).toBe(vnode2.children![1]); + result.push(vnode); + } + const vnode1 = h("div", [ + h("span", "First sibling"), + h("div", { hook: { prepatch: cb } }, [ + h("span", "Child 1"), + h("span", "Child 2") + ]) + ]); + const vnode2 = h("div", [ + h("span", "First sibling"), + h("div", { hook: { prepatch: cb } }, [ + h("span", "Child 1"), + h("span", "Child 2") + ]) + ]); + patch(vnode0, vnode1); + patch(vnode1, vnode2); + expect(result).toHaveLength(1); + }); + + test("calls `postpatch` after `prepatch` listener", function() { + const pre: any[] = [], + post: any[] = []; + function preCb(oldVnode, vnode) { + pre.push(pre); + } + function postCb(oldVnode, vnode) { + expect(pre.length).toBe(post.length + 1); + post.push(post); + } + const vnode1 = h("div", [ + h("span", "First sibling"), + h("div", { hook: { prepatch: preCb, postpatch: postCb } }, [ + h("span", "Child 1"), + h("span", "Child 2") + ]) + ]); + const vnode2 = h("div", [ + h("span", "First sibling"), + h("div", { hook: { prepatch: preCb, postpatch: postCb } }, [ + h("span", "Child 1"), + h("span", "Child 2") + ]) + ]); + patch(vnode0, vnode1); + patch(vnode1, vnode2); + expect(pre).toHaveLength(1); + expect(post).toHaveLength(1); + }); + + test("calls `update` listener", function() { + const result1: any[] = []; + const result2: any[] = []; + function cb(result, oldVnode, vnode) { + if (result.length > 0) { + console.log(result[result.length - 1]); + console.log(oldVnode); + expect(result[result.length - 1]).toBe(oldVnode); + } + result.push(vnode); + } + const vnode1 = h("div", [ + h("span", "First sibling"), + h("div", { hook: { update: cb.bind(null, result1) } }, [ + h("span", "Child 1"), + h("span", { hook: { update: cb.bind(null, result2) } }, "Child 2") + ]) + ]); + const vnode2 = h("div", [ + h("span", "First sibling"), + h("div", { hook: { update: cb.bind(null, result1) } }, [ + h("span", "Child 1"), + h("span", { hook: { update: cb.bind(null, result2) } }, "Child 2") + ]) + ]); + patch(vnode0, vnode1); + patch(vnode1, vnode2); + expect(result1).toHaveLength(1); + expect(result2).toHaveLength(1); + }); + + test("calls `remove` listener", function() { + const result: any[] = []; + function cb(vnode, rm) { + const parent = vnode.elm.parentNode; + expect(vnode.elm).toBeInstanceOf(Element); + expect(vnode.elm.children).toHaveLength(2); + expect(parent.children).toHaveLength(2); + result.push(vnode); + rm(); + expect(parent.children).toHaveLength(1); + } + const vnode1 = h("div", [ + h("span", "First sibling"), + h("div", { hook: { remove: cb } }, [ + h("span", "Child 1"), + h("span", "Child 2") + ]) + ]); + const vnode2 = h("div", [h("span", "First sibling")]); + patch(vnode0, vnode1); + patch(vnode1, vnode2); + expect(result).toHaveLength(1); + }); + + test("calls `destroy` listener when patching text node over node with children", function() { + let calls = 0; + function cb(vnode) { + calls++; + } + const vnode1 = h("div", [ + h("div", { hook: { destroy: cb } }, [h("span", "Child 1")]) + ]); + const vnode2 = h("div", "Text node"); + patch(vnode0, vnode1); + patch(vnode1, vnode2); + expect(calls).toBe(1); + }); + + test("calls `init` and `prepatch` listeners on root", function() { + let count = 0; + function init(vnode) { + expect(vnode).toBe(vnode2); + count += 1; + } + function prepatch(oldVnode, vnode) { + expect(vnode).toBe(vnode1); + count += 1; + } + const vnode1 = h("div", { hook: { init: init, prepatch: prepatch } }); + patch(vnode0, vnode1); + expect(count).toBe(1); + const vnode2 = h("span", { hook: { init: init, prepatch: prepatch } }); + patch(vnode1, vnode2); + expect(count).toBe(2); + }); + + test("removes element when all remove listeners are done", function() { + let rm1, rm2, rm3; + const patch = init([ + { + remove: function(_, rm) { + rm1 = rm; + } + }, + { + remove: function(_, rm) { + rm2 = rm; + } + } + ]); + const vnode1 = h("div", [ + h("a", { + hook: { + remove: function(_, rm) { + rm3 = rm; + } + } + }) + ]); + const vnode2 = h("div", []); + elm = patch(vnode0, vnode1).elm; + expect(elm.children).toHaveLength(1); + elm = patch(vnode1, vnode2).elm; + expect(elm.children).toHaveLength(1); + rm1(); + expect(elm.children).toHaveLength(1); + rm3(); + expect(elm.children).toHaveLength(1); + rm2(); + expect(elm.children).toHaveLength(0); + }); + + test("invokes remove hook on replaced root", function() { + const result: any[] = []; + const parent = document.createElement("div"); + const vnode0 = document.createElement("div"); + parent.appendChild(vnode0); + function cb(vnode, rm) { + result.push(vnode); + rm(); + } + const vnode1 = h("div", { hook: { remove: cb } }, [ + h("b", "Child 1"), + h("i", "Child 2") + ]); + const vnode2 = h("span", [h("b", "Child 1"), h("i", "Child 2")]); + patch(vnode0, vnode1); + patch(vnode1, vnode2); + expect(result).toHaveLength(1); + }); + }); + + describe("module hooks", function() { + test("invokes `pre` and `post` hook", function() { + const result: any[] = []; + const patch = init([ + { + pre: function() { + result.push("pre"); + } + }, + { + post: function() { + result.push("post"); + } + } + ]); + const vnode1 = h("div"); + patch(vnode0, vnode1); + expect(result).toEqual(["pre", "post"]); + }); + + test("invokes global `destroy` hook for all removed children", function() { + const result: any[] = []; + function cb(vnode) { + result.push(vnode); + } + const vnode1 = h("div", [ + h("span", "First sibling"), + h("div", [ + h("span", { hook: { destroy: cb } }, "Child 1"), + h("span", "Child 2") + ]) + ]); + const vnode2 = h("div"); + patch(vnode0, vnode1); + patch(vnode1, vnode2); + expect(result).toHaveLength(1); + }); + + test("handles text vnodes with `undefined` `data` property", function() { + const vnode1 = h("div", [" "]); + const vnode2 = h("div", []); + patch(vnode0, vnode1); + patch(vnode1, vnode2); + }); + + test("invokes `destroy` module hook for all removed children", function() { + let created = 0; + let destroyed = 0; + const patch = init([ + { + create: function() { + created++; + } + }, + { + destroy: function() { + destroyed++; + } + } + ]); + const vnode1 = h("div", [ + h("span", "First sibling"), + h("div", [h("span", "Child 1"), h("span", "Child 2")]) + ]); + const vnode2 = h("div"); + patch(vnode0, vnode1); + patch(vnode1, vnode2); + expect(created).toBe(4); + expect(destroyed).toBe(4); + }); + + test("does not invoke `create` and `remove` module hook for text nodes", function() { + let created = 0; + let removed = 0; + const patch = init([ + { + create: function() { + created++; + } + }, + { + remove: function() { + removed++; + } + } + ]); + const vnode1 = h("div", [ + h("span", "First child"), + "", + h("span", "Third child") + ]); + const vnode2 = h("div"); + patch(vnode0, vnode1); + patch(vnode1, vnode2); + expect(created).toBe(2); + expect(removed).toBe(2); + }); + + test("does not invoke `destroy` module hook for text nodes", function() { + let created = 0; + let destroyed = 0; + const patch = init([ + { + create: function() { + created++; + } + }, + { + destroy: function() { + destroyed++; + } + } + ]); + const vnode1 = h("div", [ + h("span", "First sibling"), + h("div", [h("span", "Child 1"), h("span", ["Text 1", "Text 2"])]) + ]); + const vnode2 = h("div"); + patch(vnode0, vnode1); + patch(vnode1, vnode2); + expect(created).toBe(4); + expect(destroyed).toBe(4); + }); + }); + }); + + describe("short circuiting", function() { + test("does not update strictly equal vnodes", function() { + const result: any[] = []; + function cb(vnode) { + result.push(vnode); + } + const vnode1 = h("div", [ + h("span", { hook: { update: cb } }, "Hello"), + h("span", "there") + ]); + patch(vnode0, vnode1); + patch(vnode1, vnode1); + expect(result).toHaveLength(0); + }); + + test("does not update strictly equal children", function() { + const result: any[] = []; + function cb(vnode) { + result.push(vnode); + } + const vnode1 = h("div", [ + h("span", { hook: { patch: cb } }, "Hello"), + h("span", "there") + ]); + const vnode2 = h("div"); + vnode2.children = vnode1.children; + patch(vnode0, vnode1); + patch(vnode1, vnode2); + expect(result).toHaveLength(0); + }); + }); +});