You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
snabbdom/test/unit/core.ts

1548 lines
59 KiB
TypeScript

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import { assert } from "@esm-bundle/chai";
import {
init,
classModule,
propsModule,
styleModule,
eventListenersModule,
h,
toVNode,
vnode,
VNode,
htmlDomApi,
CreateHook,
InsertHook,
PrePatchHook,
RemoveHook,
InitHook,
DestroyHook,
UpdateHook,
Key,
fragment
} from "../../src/index";
const hasSvgClassList = "classList" in SVGElement.prototype;
const patch = init(
[classModule, propsModule, eventListenersModule],
undefined,
{ experimental: { fragments: true } }
);
/** Shuffle an array using Durstenfeld's version of the FisherYates shuffle. */
function shuffle(arr) {
arr = arr.slice();
for (let i = arr.length; i--; ) {
const j = Math.floor(Math.random() * (i + 1));
const temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
return arr;
}
function prop<T>(name: string) {
return function (obj: { [index: string]: T }) {
return obj[name];
};
}
function map(fn: any, list: any[]) {
const ret = [];
for (let i = 0; i < list.length; ++i) {
ret[i] = fn(list[i]);
}
return ret;
}
const inner = prop("innerHTML");
describe("snabbdom", function () {
let elm: any, vnode0: any;
beforeEach(function () {
elm = document.createElement("div");
vnode0 = elm;
});
describe("hyperscript", function () {
it("can create vnode with proper tag", function () {
assert.strictEqual(h("div").sel, "div");
assert.strictEqual(h("a").sel, "a");
});
it("can create vnode with children", function () {
const vnode = h("div", [h("span#hello"), h("b.world")]);
assert.strictEqual(vnode.sel, "div");
const children = vnode.children as [VNode, VNode];
assert.strictEqual(children[0].sel, "span#hello");
assert.strictEqual(children[1].sel, "b.world");
});
it("can create vnode with one child vnode", function () {
const vnode = h("div", h("span#hello"));
assert.strictEqual(vnode.sel, "div");
const children = vnode.children as [VNode];
assert.strictEqual(children[0].sel, "span#hello");
});
it("can create vnode with props and one child vnode", function () {
const vnode = h("div", {}, h("span#hello"));
assert.strictEqual(vnode.sel, "div");
const children = vnode.children as [VNode];
assert.strictEqual(children[0].sel, "span#hello");
});
it("can create vnode with text content", function () {
const vnode = h("a", ["I am a string"]);
const children = vnode.children as [VNode];
assert.strictEqual(children[0].text, "I am a string");
});
it("can create vnode with text content in string", function () {
const vnode = h("a", "I am a string");
assert.strictEqual(vnode.text, "I am a string");
});
it("can create vnode with props and text content in string", function () {
const vnode = h("a", {}, "I am a string");
assert.strictEqual(vnode.text, "I am a string");
});
it("can create vnode with String obj content", function () {
const vnode = h("a", new String("b"));
assert.equal(vnode.text, "b");
});
it("can create vnode with props and String obj content", function () {
const vnode = h("a", {}, new String("b"));
assert.equal(vnode.text, "b");
});
it("can create vnode with Number obj content", function () {
const vnode = h("a", new Number(1));
assert.equal(vnode.text, "1");
});
it("can create vnode with null props", function () {
let vnode = h("a", null);
assert.deepEqual(vnode.data, {});
vnode = h("a", null, ["I am a string"]);
const children = vnode.children as [VNode];
assert.strictEqual(children[0].text, "I am a string");
});
it("can create vnode for comment", function () {
const vnode = h("!", "test");
assert.strictEqual(vnode.sel, "!");
assert.strictEqual(vnode.text, "test");
});
});
describe("created element", function () {
it("has tag", function () {
elm = patch(vnode0, h("div")).elm;
assert.strictEqual(elm.tagName, "DIV");
});
it("has different tag and id", function () {
const elm = document.createElement("div");
vnode0.appendChild(elm);
const vnode1 = h("span#id");
const patched = patch(elm, vnode1).elm as HTMLSpanElement;
assert.strictEqual(patched.tagName, "SPAN");
assert.strictEqual(patched.id, "id");
});
it("has id", function () {
elm = patch(vnode0, h("div", [h("div#unique")])).elm;
assert.strictEqual(elm.firstChild.id, "unique");
});
it("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;
assert.strictEqual(elm.firstChild.namespaceURI, 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;
assert.strictEqual(elm.namespaceURI, SVGNamespace);
assert.strictEqual(elm.firstChild.namespaceURI, SVGNamespace);
assert.strictEqual(
elm.firstChild.firstChild.namespaceURI,
XHTMLNamespace
);
// verify that svg tag with extra selectors gets svg namespace
elm = patch(vnode0, h("svg#some-id")).elm;
assert.strictEqual(elm.namespaceURI, SVGNamespace);
// verify that non-svg tag beginning with 'svg' does NOT get namespace
elm = patch(vnode0, h("svg-custom-el")).elm;
assert.notStrictEqual(elm.namespaceURI, SVGNamespace);
});
it("receives classes in selector", function () {
elm = patch(vnode0, h("div", [h("i.am.a.class")])).elm;
assert(elm.firstChild.classList.contains("am"));
assert(elm.firstChild.classList.contains("a"));
assert(elm.firstChild.classList.contains("class"));
});
it("receives classes in class property", function () {
elm = patch(
vnode0,
h("i", { class: { am: true, a: true, class: true, not: false } })
).elm;
assert(elm.classList.contains("am"));
assert(elm.classList.contains("a"));
assert(elm.classList.contains("class"));
assert(!elm.classList.contains("not"));
});
it("receives classes in selector when namespaced", function () {
if (!hasSvgClassList) {
this.skip();
} else {
elm = patch(vnode0, h("svg", [h("g.am.a.class.too")])).elm;
assert(elm.firstChild.classList.contains("am"));
assert(elm.firstChild.classList.contains("a"));
assert(elm.firstChild.classList.contains("class"));
}
});
it("receives classes in class property when namespaced", function () {
if (!hasSvgClassList) {
this.skip();
} else {
elm = patch(
vnode0,
h("svg", [
h("g", {
class: { am: true, a: true, class: true, not: false, too: true }
})
])
).elm;
assert(elm.firstChild.classList.contains("am"));
assert(elm.firstChild.classList.contains("a"));
assert(elm.firstChild.classList.contains("class"));
assert(!elm.firstChild.classList.contains("not"));
}
});
it("handles classes from both selector and property", function () {
elm = patch(
vnode0,
h("div", [h("i.has", { class: { classes: true } })])
).elm;
assert(elm.firstChild.classList.contains("has"), "has `has` class");
assert(
elm.firstChild.classList.contains("classes"),
"has `classes` class"
);
});
it("can create elements with text content", function () {
elm = patch(vnode0, h("div", ["I am a string"])).elm;
assert.strictEqual(elm.innerHTML, "I am a string");
});
it("can create elements with span and text content", function () {
elm = patch(vnode0, h("a", [h("span"), "I am a string"])).elm;
assert.strictEqual(elm.childNodes[0].tagName, "SPAN");
assert.strictEqual(elm.childNodes[1].textContent, "I am a string");
});
it("can create vnode with array String obj content", function () {
elm = patch(vnode0, h("a", ["b", new String("c")])).elm;
assert.strictEqual(elm.innerHTML, "bc");
});
it("can create elements with props", function () {
elm = patch(vnode0, h("a", { props: { src: "http://localhost/" } })).elm;
assert.strictEqual(elm.src, "http://localhost/");
});
it("can create an element created inside an iframe", function (done) {
// Only run if srcdoc is supported.
const frame = document.createElement("iframe");
if (typeof frame.srcdoc !== "undefined") {
frame.srcdoc = "<div>Thing 1</div>";
frame.onload = function () {
const div0 = frame.contentDocument!.body.querySelector(
"div"
) as HTMLDivElement;
patch(div0, h("div", "Thing 2"));
const div1 = frame.contentDocument!.body.querySelector(
"div"
) as HTMLDivElement;
assert.strictEqual(div1.textContent, "Thing 2");
frame.remove();
done();
};
document.body.appendChild(frame);
} else {
done();
}
});
it("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;
assert.strictEqual(elm, elmWithIdAndClass);
assert.strictEqual(elm.tagName, "DIV");
assert.strictEqual(elm.id, "id");
assert.strictEqual(elm.className, "class");
});
it("can create comments", function () {
elm = patch(vnode0, h("!", "test")).elm;
assert.strictEqual(elm.nodeType, document.COMMENT_NODE);
assert.strictEqual(elm.textContent, "test");
});
});
describe("created document fragment", function () {
it("is an instance of DocumentFragment", function () {
const vnode1 = fragment(["I am", h("span", [" a", " fragment"])]);
elm = patch(vnode0, vnode1).elm;
assert.strictEqual(elm.nodeType, document.DOCUMENT_FRAGMENT_NODE);
assert.strictEqual(elm.textContent, "I am a fragment");
});
});
describe("patching an element", function () {
it("changes the elements classes", function () {
const vnode1 = h("i", { class: { i: true, am: true, horse: true } });
const vnode2 = h("i", { class: { i: true, am: true, horse: false } });
patch(vnode0, vnode1);
elm = patch(vnode1, vnode2).elm;
assert(elm.classList.contains("i"));
assert(elm.classList.contains("am"));
assert(!elm.classList.contains("horse"));
});
it("changes classes in selector", function () {
const vnode1 = h("i", { class: { i: true, am: true, horse: true } });
const vnode2 = h("i", { class: { i: true, am: true, horse: false } });
patch(vnode0, vnode1);
elm = patch(vnode1, vnode2).elm;
assert(elm.classList.contains("i"));
assert(elm.classList.contains("am"));
assert(!elm.classList.contains("horse"));
});
it("preserves memoized classes", function () {
const cachedClass = { i: true, am: true, horse: false };
const vnode1 = h("i", { class: cachedClass });
const vnode2 = h("i", { class: cachedClass });
elm = patch(vnode0, vnode1).elm;
assert(elm.classList.contains("i"));
assert(elm.classList.contains("am"));
assert(!elm.classList.contains("horse"));
elm = patch(vnode1, vnode2).elm;
assert(elm.classList.contains("i"));
assert(elm.classList.contains("am"));
assert(!elm.classList.contains("horse"));
});
it("removes missing classes", function () {
const vnode1 = h("i", { class: { i: true, am: true, horse: true } });
const vnode2 = h("i", { class: { i: true, am: true } });
patch(vnode0, vnode1);
elm = patch(vnode1, vnode2).elm;
assert(elm.classList.contains("i"));
assert(elm.classList.contains("am"));
assert(!elm.classList.contains("horse"));
});
it("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;
assert.strictEqual(elm.src, "http://localhost/");
});
it("can set prop value to `0`", function () {
const patch = init([propsModule, styleModule]);
const view = (scrollTop: number) =>
h(
"div",
{
style: { height: "100px", overflowY: "scroll" },
props: { scrollTop }
},
[h("div", { style: { height: "200px" } })]
);
const vnode1 = view(0);
const mountPoint = document.body.appendChild(
document.createElement("div")
);
const { elm } = patch(mountPoint, vnode1);
if (!(elm instanceof HTMLDivElement)) throw new Error();
assert.strictEqual(elm.scrollTop, 0);
const vnode2 = view(20);
patch(vnode1, vnode2);
assert.isAtLeast(elm.scrollTop, 18);
assert.isAtMost(elm.scrollTop, 20);
const vnode3 = view(0);
patch(vnode2, vnode3);
assert.strictEqual(elm.scrollTop, 0);
document.body.removeChild(mountPoint);
});
it("can set prop value to empty string", function () {
const vnode1 = h("p", { props: { textContent: "foo" } });
const { elm } = patch(vnode0, vnode1);
if (!(elm instanceof HTMLParagraphElement)) throw new Error();
assert.strictEqual(elm.textContent, "foo");
const vnode2 = h("p", { props: { textContent: "" } });
patch(vnode1, vnode2);
assert.strictEqual(elm.textContent, "");
});
it("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;
assert.strictEqual(elm.src, "http://other/");
elm = patch(vnode1, vnode2).elm;
assert.strictEqual(elm.src, "http://other/");
});
it("removes custom props", function () {
const vnode1 = h("a", { props: { src: "http://other/" } });
const vnode2 = h("a");
patch(vnode0, vnode1);
patch(vnode1, vnode2);
assert.strictEqual(elm.src, undefined);
});
it("cannot remove native props", function () {
const vnode1 = h("a", { props: { href: "http://example.com/" } });
const vnode2 = h("a");
const { elm: elm1 } = patch(vnode0, vnode1);
if (!(elm1 instanceof HTMLAnchorElement)) throw new Error();
assert.strictEqual(elm1.href, "http://example.com/");
const { elm: elm2 } = patch(vnode1, vnode2);
if (!(elm2 instanceof HTMLAnchorElement)) throw new Error();
assert.strictEqual(elm2.href, "http://example.com/");
});
it("does not delete custom props", function () {
const vnode1 = h("p", { props: { a: "foo" } });
const vnode2 = h("p");
const { elm } = patch(vnode0, vnode1);
if (!(elm instanceof HTMLParagraphElement)) throw new Error();
assert.strictEqual((elm as any).a, "foo");
patch(vnode1, vnode2);
assert.strictEqual((elm as any).a, "foo");
});
describe("custom elements", function () {
if ("customElements" in window) {
describe("customized built-in element", function () {
const isSafari = /^((?!chrome|android).)*safari/i.test(
navigator.userAgent
);
if (!isSafari) {
class A extends HTMLParagraphElement {}
class B extends HTMLParagraphElement {}
before(function () {
if ("customElements" in window) {
customElements.define("p-a", A, { extends: "p" });
customElements.define("p-b", B, { extends: "p" });
}
});
it("can create custom elements", function () {
if ("customElements" in window) {
const vnode1 = h("p", { is: "p-a" });
elm = patch(vnode0, vnode1).elm;
assert(elm instanceof A);
} else {
this.skip();
}
});
it("handles changing is attribute", function () {
const vnode1 = h("p", { is: "p-a" });
const vnode2 = h("p", { is: "p-b" });
elm = patch(vnode0, vnode1).elm;
assert(elm instanceof A);
elm = patch(vnode1, vnode2).elm;
assert(elm instanceof B);
});
} else {
it.skip("safari does not support customized built-in elements", () => {
assert(false);
});
}
});
} else {
it.skip("browser does not support custom elements", () => {
assert(false);
});
}
});
describe("using toVNode()", function () {
it("can remove previous children of the root element", function () {
const h2 = document.createElement("h2");
h2.textContent = "Hello";
const prevElm = document.createElement("div");
prevElm.id = "id";
prevElm.className = "class";
prevElm.appendChild(h2);
const nextVNode = h("div#id.class", [h("span", "Hi")]);
elm = patch(toVNode(prevElm), nextVNode).elm;
assert.strictEqual(elm, prevElm);
assert.strictEqual(elm.tagName, "DIV");
assert.strictEqual(elm.id, "id");
assert.strictEqual(elm.className, "class");
assert.strictEqual(elm.childNodes.length, 1);
assert.strictEqual(elm.childNodes[0].tagName, "SPAN");
assert.strictEqual(elm.childNodes[0].textContent, "Hi");
});
it("can support patching in a DocumentFragment", function () {
const prevElm = document.createDocumentFragment();
const nextVNode = vnode(
"",
{},
[h("div#id.class", [h("span", "Hi")])],
undefined,
prevElm as any
);
elm = patch(toVNode(prevElm), nextVNode).elm;
assert.strictEqual(elm, prevElm);
assert.strictEqual(elm.nodeType, 11);
assert.strictEqual(elm.childNodes.length, 1);
assert.strictEqual(elm.childNodes[0].tagName, "DIV");
assert.strictEqual(elm.childNodes[0].id, "id");
assert.strictEqual(elm.childNodes[0].className, "class");
assert.strictEqual(elm.childNodes[0].childNodes.length, 1);
assert.strictEqual(elm.childNodes[0].childNodes[0].tagName, "SPAN");
assert.strictEqual(elm.childNodes[0].childNodes[0].textContent, "Hi");
});
it("can remove some children of the root element", function () {
const h2 = document.createElement("h2");
h2.textContent = "Hello";
const prevElm = document.createElement("div");
prevElm.id = "id";
prevElm.className = "class";
const text = document.createTextNode("Foobar");
const reference = {};
(text as any).testProperty = reference; // ensures we dont recreate the Text Node
prevElm.appendChild(text);
prevElm.appendChild(h2);
const nextVNode = h("div#id.class", ["Foobar"]);
elm = patch(toVNode(prevElm), nextVNode).elm;
assert.strictEqual(elm, prevElm);
assert.strictEqual(elm.tagName, "DIV");
assert.strictEqual(elm.id, "id");
assert.strictEqual(elm.className, "class");
assert.strictEqual(elm.childNodes.length, 1);
assert.strictEqual(elm.childNodes[0].nodeType, 3);
assert.strictEqual(elm.childNodes[0].wholeText, "Foobar");
assert.strictEqual(elm.childNodes[0].testProperty, reference);
});
it("can remove text elements", function () {
const h2 = document.createElement("h2");
h2.textContent = "Hello";
const prevElm = document.createElement("div");
prevElm.id = "id";
prevElm.className = "class";
const text = document.createTextNode("Foobar");
prevElm.appendChild(text);
prevElm.appendChild(h2);
const nextVNode = h("div#id.class", [h("h2", "Hello")]);
elm = patch(toVNode(prevElm), nextVNode).elm;
assert.strictEqual(elm, prevElm);
assert.strictEqual(elm.tagName, "DIV");
assert.strictEqual(elm.id, "id");
assert.strictEqual(elm.className, "class");
assert.strictEqual(elm.childNodes.length, 1);
assert.strictEqual(elm.childNodes[0].nodeType, 1);
assert.strictEqual(elm.childNodes[0].textContent, "Hello");
});
it("can work with domApi", function () {
const domApi = {
...htmlDomApi,
tagName: function (elm: Element) {
return "x-" + elm.tagName.toUpperCase();
}
};
const h2 = document.createElement("h2");
h2.id = "hx";
h2.setAttribute("data-env", "xyz");
const text = document.createTextNode("Foobar");
const elm = document.createElement("div");
elm.id = "id";
elm.className = "class other";
elm.setAttribute("data", "value");
elm.appendChild(h2);
elm.appendChild(text);
const vnode = toVNode(elm, domApi);
assert.strictEqual(vnode.sel, "x-div#id.class.other");
assert.deepEqual(vnode.data, { attrs: { data: "value" } });
const children = vnode.children as [VNode, VNode];
assert.strictEqual(children[0].sel, "x-h2#hx");
assert.deepEqual(children[0].data, { dataset: { env: "xyz" } });
assert.strictEqual(children[1].text, "Foobar");
});
it("can parsing dataset and attrs", function () {
const onlyAttrs = document.createElement("div");
onlyAttrs.setAttribute("foo", "bar");
assert.deepEqual(toVNode(onlyAttrs).data, { attrs: { foo: "bar" } });
const onlyDataset = document.createElement("div");
onlyDataset.setAttribute("data-foo", "bar");
assert.deepEqual(toVNode(onlyDataset).data, {
dataset: { foo: "bar" }
});
const onlyDatasets2 = document.createElement("div");
onlyDatasets2.dataset.foo = "bar";
assert.deepEqual(toVNode(onlyDatasets2).data, {
dataset: { foo: "bar" }
});
const bothAttrsAndDatasets = document.createElement("div");
bothAttrsAndDatasets.setAttribute("foo", "bar");
bothAttrsAndDatasets.setAttribute("data-foo", "bar");
bothAttrsAndDatasets.dataset.again = "again";
assert.deepEqual(toVNode(bothAttrsAndDatasets).data, {
attrs: { foo: "bar" },
dataset: { foo: "bar", again: "again" }
});
});
});
describe("updating children with keys", function () {
function spanNum(n?: null | Key) {
if (n == null) {
return n;
} else if (typeof n === "string") {
return h("span", {}, n);
} else if (typeof n === "number") {
return h("span", { key: n }, n.toString());
} else {
return h("span", { key: n }, "symbol");
}
}
describe("addition of elements", function () {
it("appends elements", function () {
const vnode1 = h("span", [1].map(spanNum));
const vnode2 = h("span", [1, 2, 3].map(spanNum));
elm = patch(vnode0, vnode1).elm;
assert.strictEqual(elm.children.length, 1);
elm = patch(vnode1, vnode2).elm;
assert.strictEqual(elm.children.length, 3);
assert.strictEqual(elm.children[1].innerHTML, "2");
assert.strictEqual(elm.children[2].innerHTML, "3");
});
it("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;
assert.strictEqual(elm.children.length, 2);
elm = patch(vnode1, vnode2).elm;
assert.deepEqual(map(inner, elm.children), ["1", "2", "3", "4", "5"]);
});
it("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;
assert.strictEqual(elm.children.length, 4);
assert.strictEqual(elm.children.length, 4);
elm = patch(vnode1, vnode2).elm;
assert.deepEqual(map(inner, elm.children), ["1", "2", "3", "4", "5"]);
});
it("add elements at begin 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;
assert.strictEqual(elm.children.length, 3);
elm = patch(vnode1, vnode2).elm;
assert.deepEqual(map(inner, elm.children), ["1", "2", "3", "4", "5"]);
});
it("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;
assert.strictEqual(elm.children.length, 0);
elm = patch(vnode1, vnode2).elm;
assert.deepEqual(map(inner, elm.children), ["1", "2", "3"]);
});
it("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;
assert.deepEqual(map(inner, elm.children), ["1", "2", "3"]);
elm = patch(vnode1, vnode2).elm;
assert.strictEqual(elm.children.length, 0);
});
it("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;
assert.deepEqual(map(inner, elm.children), ["1", "2", "3"]);
elm = patch(vnode1, vnode2).elm;
assert.deepEqual(map(inner, elm.children), ["1", "2", "3"]);
assert.strictEqual(elm.children.length, 3);
assert.strictEqual(elm.children[1].tagName, "I");
});
});
describe("removal of elements", function () {
it("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;
assert.strictEqual(elm.children.length, 5);
elm = patch(vnode1, vnode2).elm;
assert.deepEqual(map(inner, elm.children), ["3", "4", "5"]);
});
it("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;
assert.strictEqual(elm.children.length, 5);
elm = patch(vnode1, vnode2).elm;
assert.strictEqual(elm.children.length, 3);
assert.strictEqual(elm.children[0].innerHTML, "1");
assert.strictEqual(elm.children[1].innerHTML, "2");
assert.strictEqual(elm.children[2].innerHTML, "3");
});
it("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;
assert.strictEqual(elm.children.length, 5);
elm = patch(vnode1, vnode2).elm;
assert.strictEqual(elm.children.length, 4);
assert.deepEqual(elm.children[0].innerHTML, "1");
assert.strictEqual(elm.children[0].innerHTML, "1");
assert.strictEqual(elm.children[1].innerHTML, "2");
assert.strictEqual(elm.children[2].innerHTML, "4");
assert.strictEqual(elm.children[3].innerHTML, "5");
});
});
describe("element reordering", function () {
it("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;
assert.strictEqual(elm.children.length, 4);
elm = patch(vnode1, vnode2).elm;
assert.strictEqual(elm.children.length, 4);
assert.strictEqual(elm.children[0].innerHTML, "2");
assert.strictEqual(elm.children[1].innerHTML, "3");
assert.strictEqual(elm.children[2].innerHTML, "1");
assert.strictEqual(elm.children[3].innerHTML, "4");
});
it("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;
assert.strictEqual(elm.children.length, 3);
elm = patch(vnode1, vnode2).elm;
assert.strictEqual(elm.children.length, 3);
assert.strictEqual(elm.children[0].innerHTML, "2");
assert.strictEqual(elm.children[1].innerHTML, "3");
assert.strictEqual(elm.children[2].innerHTML, "1");
});
it("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;
assert.strictEqual(elm.children.length, 4);
elm = patch(vnode1, vnode2).elm;
assert.strictEqual(elm.children.length, 4);
assert.strictEqual(elm.children[0].innerHTML, "1");
assert.strictEqual(elm.children[1].innerHTML, "4");
assert.strictEqual(elm.children[2].innerHTML, "2");
assert.strictEqual(elm.children[3].innerHTML, "3");
});
it("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;
assert.strictEqual(elm.children.length, 4);
elm = patch(vnode1, vnode2).elm;
assert.strictEqual(elm.children.length, 4);
assert.strictEqual(elm.children[0].innerHTML, "4");
assert.strictEqual(elm.children[1].innerHTML, "2");
assert.strictEqual(elm.children[2].innerHTML, "3");
assert.strictEqual(elm.children[3].innerHTML, "1");
});
});
describe("combinations of additions, removals and reorderings", function () {
it("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;
assert.strictEqual(elm.children.length, 5);
elm = patch(vnode1, vnode2).elm;
assert.strictEqual(elm.children.length, 5);
assert.strictEqual(elm.children[0].innerHTML, "4");
assert.strictEqual(elm.children[1].innerHTML, "1");
assert.strictEqual(elm.children[2].innerHTML, "2");
assert.strictEqual(elm.children[3].innerHTML, "3");
assert.strictEqual(elm.children[4].innerHTML, "6");
});
it("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;
assert.strictEqual(elm.children.length, 3);
elm = patch(vnode1, vnode2).elm;
assert.deepEqual(map(inner, elm.children), ["4", "6"]);
});
it("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;
assert.strictEqual(elm.children.length, 3);
elm = patch(vnode1, vnode2).elm;
assert.strictEqual(elm.children.length, 3);
assert.strictEqual(elm.children[0].innerHTML, "4");
assert.strictEqual(elm.children[1].innerHTML, "5");
assert.strictEqual(elm.children[2].innerHTML, "3");
});
it("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;
assert.strictEqual(elm.childNodes.length, 4);
assert.strictEqual(elm.textContent, "1abc");
elm = patch(vnode1, vnode2).elm;
assert.strictEqual(elm.childNodes.length, 6);
assert.strictEqual(elm.textContent, "dabc1e");
});
it("accepts symbol as key", function () {
const vnode1 = h("span", [Symbol()].map(spanNum));
const vnode2 = h(
"span",
[Symbol("1"), Symbol("2"), Symbol("3")].map(spanNum)
);
elm = patch(vnode0, vnode1).elm;
assert.equal(elm.children.length, 1);
elm = patch(vnode1, vnode2).elm;
assert.equal(elm.children.length, 3);
assert.equal(elm.children[1].innerHTML, "symbol");
assert.equal(elm.children[2].innerHTML, "symbol");
});
});
it("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;
assert.strictEqual(elm.children.length, 8);
elm = patch(vnode1, vnode2).elm;
assert.deepEqual(map(inner, elm.children), [
"8",
"7",
"6",
"5",
"4",
"3",
"2",
"1"
]);
});
it("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;
assert.strictEqual(elm.children.length, 6);
elm = patch(vnode1, vnode2).elm;
assert.deepEqual(map(inner, elm.children), [
"4",
"3",
"2",
"1",
"5",
"0"
]);
});
it("handles random shuffles", function () {
let n;
let i;
const arr = [];
const opacities: string[] = [];
const elms = 14;
const samples = 5;
function spanNumWithOpacity(n: number, o: string) {
return h("span", { key: n, style: { opacity: o } }, n.toString());
}
for (n = 0; n < elms; ++n) {
arr[n] = n;
}
for (n = 0; n < samples; ++n) {
const vnode1 = h(
"span",
arr.map(function (n) {
return spanNumWithOpacity(n, "1");
})
);
const shufArr = shuffle(arr);
let elm: HTMLDivElement | HTMLSpanElement =
document.createElement("div");
elm = patch(elm, vnode1).elm as HTMLSpanElement;
for (i = 0; i < elms; ++i) {
assert.strictEqual(elm.children[i].innerHTML, i.toString());
opacities[i] = Math.random().toFixed(5).toString();
}
const vnode2 = h(
"span",
arr.map(function (n) {
return spanNumWithOpacity(shufArr[n], opacities[n]);
})
);
elm = patch(vnode1, vnode2).elm as HTMLSpanElement;
for (i = 0; i < elms; ++i) {
assert.strictEqual(
elm.children[i].innerHTML,
shufArr[i].toString()
);
const opacity = (elm.children[i] as HTMLSpanElement).style.opacity;
assert.strictEqual(opacities[i].indexOf(opacity), 0);
}
}
});
it("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;
assert.strictEqual(elm.children.length, 6);
elm = patch(vnode1, vnode2).elm;
assert.deepEqual(map(inner, elm.children), [
"2",
"1",
"0",
"5",
"4",
"3"
]);
});
it("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;
assert.strictEqual(elm.children.length, 0);
elm = patch(vnode2, vnode3).elm;
assert.deepEqual(map(inner, elm.children), [
"5",
"4",
"3",
"2",
"1",
"0"
]);
});
it("handles random shuffles with null/undefined children", function () {
let i;
let j;
let r;
let len;
let arr;
const maxArrLen = 15;
const samples = 5;
let vnode1 = vnode0;
let vnode2;
for (i = 0; i < samples; ++i, vnode1 = vnode2) {
len = Math.floor(Math.random() * maxArrLen);
arr = [];
for (j = 0; j < len; ++j) {
if ((r = Math.random()) < 0.5) arr[j] = String(j);
else if (r < 0.75) arr[j] = null;
else arr[j] = undefined;
}
shuffle(arr);
vnode2 = h("div", arr.map(spanNum));
elm = patch(vnode1, vnode2).elm;
assert.deepEqual(
map(inner, elm.children),
arr.filter(function (x) {
return x != null;
})
);
}
});
});
describe("updating children without keys", function () {
it("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;
assert.deepEqual(map(inner, elm.children), ["Hello"]);
elm = patch(vnode1, vnode2).elm;
assert.deepEqual(map(inner, elm.children), ["Hello", "World"]);
});
it("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;
assert.strictEqual(elm.childNodes[0].textContent, "Text");
elm = patch(vnode1, vnode2).elm;
assert.strictEqual(elm.childNodes[0].textContent, "Text");
});
it("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;
assert.strictEqual(elm.childNodes[0].textContent, "Text");
elm = patch(vnode1, vnode2).elm;
assert.strictEqual(elm.childNodes[0].textContent, "Text2");
});
it("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;
assert.strictEqual(elm.childNodes[0].textContent, "Text");
elm = patch(vnode1, vnode2).elm;
assert.strictEqual(elm.childNodes[0].textContent, "Text");
});
it("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;
assert.strictEqual(elm.childNodes[0].textContent, "Text");
elm = patch(vnode1, vnode2).elm;
assert.strictEqual(elm.childNodes[0].textContent, "Text2");
});
it("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;
assert.strictEqual(elm.childNodes[0].textContent, "");
elm = patch(vnode1, vnode2).elm;
assert.strictEqual(elm.childNodes[0].textContent, "Test");
});
it("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;
assert.deepEqual(map(inner, elm.children), ["World"]);
elm = patch(vnode1, vnode2).elm;
assert.deepEqual(map(inner, elm.children), ["Hello", "World"]);
});
it("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;
assert.deepEqual(map(inner, elm.children), ["World"]);
elm = patch(vnode1, vnode2).elm;
assert.deepEqual(map(prop("tagName"), elm.children), ["DIV", "SPAN"]);
assert.deepEqual(map(inner, elm.children), ["Hello", "World"]);
});
it("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;
assert.deepEqual(map(inner, elm.children), ["One", "Two", "Three"]);
elm = patch(vnode1, vnode2).elm;
assert.deepEqual(map(inner, elm.children), ["One", "Three"]);
});
it("removes a single text node", function () {
const vnode1 = h("div", "One");
const vnode2 = h("div");
patch(vnode0, vnode1);
assert.strictEqual(elm.textContent, "One");
patch(vnode1, vnode2);
assert.strictEqual(elm.textContent, "");
});
it("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);
assert.strictEqual(elm.textContent, "One");
patch(vnode1, vnode2);
assert.deepEqual(map(prop("textContent"), elm.childNodes), [
"Two",
"Three"
]);
});
it("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);
assert.deepEqual(map(prop("textContent"), elm.childNodes), [
"One",
"Two"
]);
patch(vnode1, vnode2);
assert.strictEqual(elm.childNodes.length, 1);
assert.strictEqual(elm.childNodes[0].tagName, "DIV");
assert.strictEqual(elm.childNodes[0].textContent, "Three");
});
it("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;
assert.deepEqual(map(inner, elm.children), ["One", "Two", "Three"]);
elm = patch(vnode1, vnode2).elm;
assert.deepEqual(map(prop("tagName"), elm.children), [
"B",
"SPAN",
"DIV"
]);
assert.deepEqual(map(inner, elm.children), ["Three", "One", "Two"]);
});
it("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;
assert.deepEqual(map(inner, elm.children), ["1", "2"]);
elm = patch(vnode1, vnode2).elm;
assert.deepEqual(map(inner, elm.children), ["2", "1"]);
elm = patch(vnode2, vnode3).elm;
assert.deepEqual(map(inner, elm.children), ["1", "2"]);
});
it("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;
assert.strictEqual(elm.children.length, 0);
elm = patch(vnode2, vnode3).elm;
assert.deepEqual(map(inner, elm.children), ["2", "1"]);
});
});
});
describe("patching a fragment", function () {
it("can patch on document fragments", function () {
let firstChild: HTMLElement;
const root = document.createElement("div");
const vnode1 = fragment(["I am", h("span", [" a", " fragment"])]);
const vnode2 = h("div", ["I am an element"]);
const vnode3 = fragment(["fragment ", "again"]);
root.appendChild(vnode0);
firstChild = root.firstChild as HTMLElement;
assert.strictEqual(firstChild, vnode0);
elm = patch(vnode0, vnode1).elm;
firstChild = root.firstChild as HTMLElement;
assert.strictEqual(firstChild.textContent, "I am");
assert.strictEqual(elm.nodeType, document.DOCUMENT_FRAGMENT_NODE);
assert.strictEqual(elm.parent, root);
elm = patch(vnode1, vnode2).elm;
firstChild = root.firstChild as HTMLElement;
assert.strictEqual(firstChild.tagName, "DIV");
assert.strictEqual(firstChild.textContent, "I am an element");
assert.strictEqual(elm.tagName, "DIV");
assert.strictEqual(elm.textContent, "I am an element");
assert.strictEqual(elm.parentNode, root);
elm = patch(vnode2, vnode3).elm;
firstChild = root.firstChild as HTMLElement;
assert.strictEqual(elm.nodeType, document.DOCUMENT_FRAGMENT_NODE);
assert.strictEqual(firstChild.textContent, "fragment ");
assert.strictEqual(elm.parent, root);
});
it("allows a document fragment as a container", function () {
const vnode0 = document.createDocumentFragment();
const vnode1 = fragment(["I", "am", "a", h("span", ["fragment"])]);
const vnode2 = h("div", "I am an element");
elm = patch(vnode0, vnode1).elm;
assert.strictEqual(elm.nodeType, document.DOCUMENT_FRAGMENT_NODE);
elm = patch(vnode1, vnode2).elm;
assert.strictEqual(elm.tagName, "DIV");
});
});
describe("hooks", function () {
describe("element hooks", function () {
it("calls `create` listener before inserted into parent but after children", function () {
const result = [];
const cb: CreateHook = (empty, vnode) => {
assert(vnode.elm instanceof Element);
assert.strictEqual(vnode.elm.children.length, 2);
assert.strictEqual(vnode.elm.parentNode, null);
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);
assert.strictEqual(1, result.length);
});
it("calls `insert` listener after both parents, siblings and children have been inserted", function () {
const result = [];
const cb: InsertHook = (vnode) => {
assert(vnode.elm instanceof Element);
assert.strictEqual(vnode.elm.children.length, 2);
assert.strictEqual(vnode.elm.parentNode!.children.length, 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);
assert.strictEqual(1, result.length);
});
it("calls `prepatch` listener", function () {
const result = [];
const cb: PrePatchHook = (oldVnode, vnode) => {
assert.strictEqual(oldVnode, vnode1.children![1]);
assert.strictEqual(vnode, 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);
assert.strictEqual(result.length, 1);
});
it("calls `postpatch` after `prepatch` listener", function () {
let pre = 0;
let post = 0;
function preCb() {
pre++;
}
function postCb() {
assert.strictEqual(pre, post + 1);
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);
assert.strictEqual(pre, 1);
assert.strictEqual(post, 1);
});
it("calls `update` listener", function () {
const result1: VNode[] = [];
const result2: VNode[] = [];
function cb(result: VNode[], oldVnode: VNode, vnode: VNode) {
if (result.length > 0) {
console.log(result[result.length - 1]);
console.log(oldVnode);
assert.strictEqual(result[result.length - 1], 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);
assert.strictEqual(result1.length, 1);
assert.strictEqual(result2.length, 1);
});
it("calls `remove` listener", function () {
const result = [];
const cb: RemoveHook = (vnode, rm) => {
const parent = vnode.elm!.parentNode as HTMLDivElement;
assert(vnode.elm instanceof Element);
assert.strictEqual((vnode.elm as HTMLDivElement).children.length, 2);
assert.strictEqual(parent.children.length, 2);
result.push(vnode);
rm();
assert.strictEqual(parent.children.length, 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);
assert.strictEqual(1, result.length);
});
it("calls `destroy` listener when patching text node over node with children", function () {
let calls = 0;
function cb() {
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);
assert.strictEqual(calls, 1);
});
it("calls `init` and `prepatch` listeners on root", function () {
let count = 0;
const init: InitHook = (vnode) => {
assert.strictEqual(vnode, vnode2);
count += 1;
};
const prepatch: PrePatchHook = (oldVnode, vnode) => {
assert.strictEqual(vnode, vnode1);
count += 1;
};
const vnode1 = h("div", { hook: { init: init, prepatch: prepatch } });
patch(vnode0, vnode1);
assert.strictEqual(1, count);
const vnode2 = h("span", { hook: { init: init, prepatch: prepatch } });
patch(vnode1, vnode2);
assert.strictEqual(2, count);
});
it("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;
assert.strictEqual(elm.children.length, 1);
elm = patch(vnode1, vnode2).elm;
assert.strictEqual(elm.children.length, 1);
(rm1 as any)();
assert.strictEqual(elm.children.length, 1);
(rm3 as any)();
assert.strictEqual(elm.children.length, 1);
(rm2 as any)();
assert.strictEqual(elm.children.length, 0);
});
it("invokes remove hook on replaced root", function () {
const result = [];
const parent = document.createElement("div");
const vnode0 = document.createElement("div");
parent.appendChild(vnode0);
const cb: RemoveHook = (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);
assert.strictEqual(1, result.length);
});
});
describe("module hooks", function () {
it("invokes `pre` and `post` hook", function () {
const result: string[] = [];
const patch = init([
{
pre: function () {
result.push("pre");
}
},
{
post: function () {
result.push("post");
}
}
]);
const vnode1 = h("div");
patch(vnode0, vnode1);
assert.deepEqual(result, ["pre", "post"]);
});
it("invokes global `destroy` hook for all removed children", function () {
const result = [];
const cb: DestroyHook = (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);
assert.strictEqual(result.length, 1);
});
it("handles text vnodes with `undefined` `data` property", function () {
const vnode1 = h("div", [" "]);
const vnode2 = h("div", []);
patch(vnode0, vnode1);
patch(vnode1, vnode2);
});
it("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);
assert.strictEqual(created, 4);
assert.strictEqual(destroyed, 4);
});
it("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);
assert.strictEqual(created, 2);
assert.strictEqual(removed, 2);
});
it("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);
assert.strictEqual(created, 4);
assert.strictEqual(destroyed, 4);
});
});
});
describe("short circuiting", function () {
it("does not update strictly equal vnodes", function () {
const result = [];
const cb: UpdateHook = (vnode) => {
result.push(vnode);
};
const vnode1 = h("div", [
h("span", { hook: { update: cb } }, "Hello"),
h("span", "there")
]);
patch(vnode0, vnode1);
patch(vnode1, vnode1);
assert.strictEqual(result.length, 0);
});
it("does not update strictly equal children", function () {
const result = [];
function cb(vnode: VNode) {
result.push(vnode);
}
const vnode1 = h("div", [
h("span", { hook: { patch: cb } as any }, "Hello"),
h("span", "there")
]);
const vnode2 = h("div");
vnode2.children = vnode1.children;
patch(vnode0, vnode1);
patch(vnode1, vnode2);
assert.strictEqual(result.length, 0);
});
});
});