import { h, patch } from "../src/vdom"; import { init, addNS } from "../src/vdom/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 const vnode = h("svg", [h("foreignObject", [h("div", ["I am HTML embedded in SVG"])])]); // need to add namespace manually. it is usually done by the template // compiler addNS(vnode.data, (vnode as any).children, vnode.sel); elm = patch(vnode0, vnode).elm; expect(elm.namespaceURI).toBe(SVGNamespace); expect(elm.firstChild.namespaceURI).toBe(SVGNamespace); expect(elm.firstChild.firstChild.namespaceURI).toBe(XHTMLNamespace); }); 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); }); }); });