hydro-js
Version:
A lightweight reactive library
1,246 lines (1,242 loc) • 75.6 kB
JavaScript
import { JSDOM } from "jsdom";
const { window } = new JSDOM(`<!doctype html>
<html lang="en">
<head>
</head>
<body>
</body>
</html>`);
// @ts-expect-error
globalThis.window = window;
globalThis.document = window.document;
const { html, h, hydro, render, setGlobalSchedule, setReuseElements, reactive, unset, emit, watchEffect, observe, getValue, onRender, onCleanup, internals, ternary, setInsertDiffing, $, unobserve, setAsyncUpdate, setReactivity, view, } = await import("./library.js");
// Local debugging
//@ts-ignore
window.html = html;
//@ts-ignore
window.render = render;
//@ts-ignore
window.hydro = hydro;
//@ts-ignore
window.setReactivity = setReactivity;
setGlobalSchedule(false); // Simplifies testing
const sleep = (time) => new Promise((resolve) => setTimeout(resolve, time));
document.head.insertAdjacentHTML("beforeend", `<style>
.badge {
display: inline-block;
padding: 0.25em 0.4em;
border-radius: 0.25rem;
color: #fff;
}
.success {
background-color: #28a745;
}
.error {
background-color: #dc3545;
}
</style>`);
const results = [];
// --------- TESTS START ------------
let condition = true;
describe("library", () => {
describe("functions", () => {
describe("h", () => {
it("handles functions correctly", () => {
return (h(() => h("p", null, ["Hello World"]), null, []).textContent ===
"Hello World");
});
it("returns a valid element", () => {
const test = reactive("A");
setTimeout(() => {
unset(test);
});
return h("div", null, [test]).localName === "div";
});
it("handles documentFragment", () => {
return (h(h, null, h("p", null, "hi"), h("p", null, "ho"))
.nodeType === 11);
});
it("returns a valid element when it has children", () => {
return h("div", null, [h("p", null, ["test"])]).childNodes.length === 1;
});
});
describe("documentFragment", () => {
it("render elem in fragment", () => {
const fragment = html `<div>here</div>
<div>and here</div>`;
render(fragment);
const unmount = render(html `<p>a</p>`, fragment);
let condition = document.body.childElementCount === 1;
unmount();
return condition && document.body.childElementCount === 0;
});
it("render fragment in fragment", () => {
const fragment = html `<div>here</div>
<div>and here</div>`;
render(fragment);
const unmount = render(html `<p>a</p><p>b</b>`, fragment);
let condition = document.body.childElementCount === 2;
unmount();
return condition && document.body.childElementCount === 0;
});
it("render fragment in elem", () => {
const elem = html `<div>here</div>`;
render(elem);
const unmount = render(html `<p>a</p><p>b</b>`, elem);
let condition = document.body.childElementCount === 2;
unmount();
return condition && document.body.childElementCount === 0;
});
it("render text in fragment", () => {
const fragment = html `<div>here</div>
<div>and here</div>`;
render(fragment);
const unmount = render(html `text`, fragment);
let condition = document.body.childElementCount === 0;
unmount();
return condition && document.body.childElementCount === 0;
});
it("render fragment in text", () => {
const text = html `text`;
render(text);
const unmount = render(html `<div>here</div>
<div>and here</div>`, text);
let condition = document.body.childElementCount === 2;
unmount();
return condition && document.body.childElementCount === 0;
});
it("render elem in fragment - setInsertDiffing", () => {
setInsertDiffing(true);
const fragment = html `<div>here</div>
<div>and here</div>`;
render(fragment);
const unmount = render(html `<p>a</p>`, fragment);
let condition = document.body.childElementCount === 1;
unmount();
setInsertDiffing(false);
return condition && document.body.childElementCount === 0;
});
it("render fragment in fragment - setInsertDiffing", () => {
setInsertDiffing(true);
const fragment = html `<div>here</div>
<div>and here</div>`;
render(fragment);
const unmount = render(html `<p>a</p><p>b</b>`, fragment);
let condition = document.body.childElementCount === 2;
unmount();
setInsertDiffing(false);
return condition && document.body.childElementCount === 0;
});
it("render fragment in elem - setInsertDiffing", () => {
setInsertDiffing(true);
const elem = html `<div>here</div>`;
render(elem);
const unmount = render(html `<p>a</p><p>b</b>`, elem);
let condition = document.body.childElementCount === 2;
unmount();
setInsertDiffing(false);
return condition && document.body.childElementCount === 0;
});
it("render text in fragment - setInsertDiffing", () => {
setInsertDiffing(true);
const fragment = html `<div>here</div>
<div>and here</div>`;
render(fragment);
const unmount = render(html `text`, fragment);
let condition = document.body.childElementCount === 0;
unmount();
setInsertDiffing(false);
return condition && document.body.childElementCount === 0;
});
it("render fragment in text - setInsertDiffing", () => {
setInsertDiffing(true);
const text = html `text`;
render(text);
const unmount = render(html `<div>here</div>
<div>and here</div>`, text);
let condition = document.body.childElementCount === 2;
unmount();
setInsertDiffing(false);
return condition && document.body.childElementCount === 0;
});
});
describe("setReuseElements", () => {
it("code coverage", () => {
setReuseElements(true);
return true;
});
});
describe("setGlobalSchedule", () => {
it("sets asnycUpdate on hydro objects", () => {
hydro.schedule = {};
let cond = hydro.schedule.asyncUpdate === false;
setGlobalSchedule(true);
cond = cond && hydro.schedule.asyncUpdate === true;
setGlobalSchedule(false);
setTimeout(() => {
hydro.schedule = null;
});
return cond && hydro.schedule.asyncUpdate === false;
});
});
describe("setAsyncUpdate", () => {
it("sets asnycUpdate on reactive object", () => {
const schedule = reactive({});
setAsyncUpdate(schedule, false);
setTimeout(unset, 0, schedule);
return true;
});
it("works chained", () => {
const abc = reactive({ a: { b: 4 } });
setAsyncUpdate(abc.a, false);
setTimeout(unset, 0, abc);
return true;
});
});
describe("html", () => {
// https://html.spec.whatwg.org/
[
"a",
"abbr",
"address",
"area",
"article",
"aside",
"audio",
"b",
"base",
"bdi",
"bdo",
"blockquote",
"body",
"br",
"button",
"canvas",
"caption",
"cite",
"code",
"col",
"colgroup",
"data",
"datalist",
"dd",
"del",
"details",
"dfn",
"dialog",
"div",
"dl",
"dt",
"em",
"embed",
"fencedframe",
"fieldset",
"figcaption",
"figure",
"footer",
"form",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"head",
"header",
"hgroup",
"hr",
"html",
"i",
"iframe",
"img",
"input",
"ins",
"kbd",
"label",
"legend",
"li",
"link",
"main",
"map",
"mark",
"menu",
"meta",
"meter",
"nav",
"noscript",
"object",
"ol",
"optgroup",
"option",
"output",
"p",
"picture",
"pre",
"progress",
"q",
"rp",
"rt",
"ruby",
"s",
"samp",
"script",
"search",
"section",
"select",
"selectedcontent",
"slot",
"small",
"source",
"span",
"strong",
"style",
"sub",
"summary",
"sup",
"svg",
"table",
"tbody",
"td",
"template",
"textarea",
"tfoot",
"th",
"thead",
"time",
"title",
"tr",
"track",
"u",
"ul",
"var",
"video",
"wbr",
"custom-wc",
].forEach((tag) => {
it(`is able to create element ${tag}`, () => {
const elem = html `<${tag} />`;
return elem.localName === tag;
});
});
it("handles false variables correctly", () => {
return (html `<input type="checkbox" required=${false} />`.textContent === "");
});
it("handles a new html doc correctly", () => {
const elem = html `<html>
<head></head>
<body>
a
</body>
</html>`;
return (elem.localName === "html" &&
!!elem.querySelector("head") &&
!!elem.querySelector("body"));
});
it("returns empty text node", () => {
return document.createTextNode("").isEqualNode(html ``);
});
it("returns text node", () => {
return document.createTextNode("hello").isEqualNode(html `hello`);
});
it("returns document fragment", () => {
const node = html `<div><p>hi</p></div>
<div><span>ho</span></div>`;
return (node.nodeName !== "svg" &&
"getElementById" in node &&
node.childElementCount === 2);
});
it("returns element", () => {
const elem = html `<p>hello</p>`;
return elem.localName === "p" && elem.textContent.includes("hello");
});
it("variable input (node)", () => {
const p = html `<p>hi</p>`;
const elem = html `<div>${p}</div>`;
return elem.contains(p) && elem.textContent.includes("hi");
});
it("variable input (primitive value)", () => {
const test = "test";
const elem = html `<div>${test}</div>`;
return elem.textContent.includes(test);
});
it("variable input (hydro)", () => {
hydro.testValue = "test";
const elem = html `<div>{{ testValue }}</div>`;
setTimeout(() => {
hydro.testValue = null;
});
return elem.textContent.includes(hydro.testValue);
});
it("variable input (reactive) - does not include undefined", () => {
const data = reactive({});
const elem = html `<div>${data.test}</div>`;
setTimeout(unset, 0, data);
return !elem.textContent.includes(String(undefined));
});
it("variable input (reactive)", () => {
const test = reactive("test");
const elem = html `<div>${test}</div>`;
setTimeout(unset, 0, test);
return elem.textContent.includes(getValue(test));
});
it("variable input (eventListener)", () => {
const onClick = (e) => (e.currentTarget.textContent = 1);
const elem = html `<div onclick=${onClick}>0</div>`;
//@ts-ignore
elem.click();
return elem.textContent.includes("1");
});
it("variable input (array - normal)", () => {
const arr = [42, "test"];
const elem = html `<div>${arr}</div>`;
return (elem.textContent.includes("42") && elem.textContent.includes("test"));
});
it("variable input (function)", () => {
const onClick = (e) => (e.currentTarget.textContent =
Number(e.currentTarget.textContent) + 1);
const elem = html `<div onclick=${onClick}>0</div>`;
//@ts-ignore
elem.click();
return elem.textContent.includes("1");
});
it("variable input (eventListener as object)", () => {
const onClick = {
event: (e) => (e.currentTarget.textContent =
Number(e.currentTarget.textContent) + 1),
options: {
once: true,
},
};
const elem = html `<div onclick=${onClick}>0</div>`;
//@ts-ignore
elem.click();
//@ts-ignore
elem.click();
return elem.textContent.includes("1");
});
it("variable input (array - node)", () => {
const p = html `<p>test</p>`;
const arr = [42, p];
const elem = html `<div>${arr}</div>`;
return (elem.textContent.includes("42") &&
elem.textContent.includes("test") &&
elem.contains(p));
});
it("variable input (object)", () => {
const props = {
id: "test",
onclick: (e) => (e.currentTarget.textContent =
Number(e.currentTarget.textContent) + 1),
target: "_blank",
};
const elem = html `<a ${props}>0</a>`;
//@ts-ignore
elem.click();
return (elem.id === "test" &&
elem.target === "_blank" &&
elem.textContent === "1");
});
it("variable input (object - with eventListenerObject)", () => {
const props = {
id: "test",
onclick: {
event: (e) => (e.currentTarget.textContent =
Number(e.currentTarget.textContent) + 1),
options: {
once: true,
},
},
target: "_blank",
};
const elem = html `<a ${props}>0</a>`;
//@ts-ignore
elem.click();
//@ts-ignore
elem.click();
return (elem.id === "test" &&
elem.target === "_blank" &&
elem.textContent === "1");
});
it("resolves deep reactive", () => {
const person = reactive({
firstname: "Fabian",
lastname: "Krutsch",
char: { int: 777 },
items: [1, 2, 3],
});
const elem = html `
<p>
<span>His firstname is: </span><span>${person.firstname}</span
><br />
<span>His int value is: </span><span>${person.char.int}</span><br />
<span>His first item is: </span><span>${person.items[0]}</span
><br />
</p>
`;
const unmount = render(elem);
person((curr) => {
curr.char.int = 123;
curr.items[0] = 0;
});
setTimeout(() => {
unmount();
unset(person);
});
return ((document.body.textContent?.includes("Fabian") &&
document.body.textContent?.includes("123") &&
document.body.textContent?.includes("0")) ??
false);
});
it("nested reactive", () => {
const list = reactive([
{ text: "Lorem", success: true },
{ text: "ipsum", success: true },
]);
const elem = html `
<div>
${getValue(list).map((_, index) => {
return html `<p>${list[index].text}</p>`;
})}
</div>
`;
const unmount = render(elem);
list((curr) => {
curr[0].text = "Changed";
});
const native = document.createElement("div");
native.insertAdjacentHTML("beforeend", `<p>Changed</p><p>ipsum</p>`);
setTimeout(() => {
unset(list);
unmount();
});
return (native.innerHTML.trim() === elem.innerHTML.trim());
});
it("removes {{..}}) from html attribute", () => {
const attr = reactive({ id: "test" });
const elem = html `<p ${attr}></p>`;
const unmount = render(elem);
setTimeout(() => {
unmount();
unset(attr);
});
return elem.id === "test" && !elem.hasAttribute("{{attr}}");
});
it("two-way attribute", () => {
const text = reactive("text");
const checked = reactive(true);
const checkedRadio = reactive("A");
const select = reactive("cat");
const datetime = reactive("2018-06-08T00:00");
const unmount = render(html `
<div>
<input id="text" type="text" two-way=${text} />
<textarea two-way=${text}></textarea>
<label>
<input id="checkbox1" type="checkbox" two-way=${checked} />
John
</label>
<label>
<input
id="datetime"
type="datetime-local"
two-way=${datetime}
min="2018-06-07T00:00"
max="2020-06-14T00:00"
/>
</label>
<label>
<input
id="radio1"
type="radio"
name="group"
value="A"
two-way=${checkedRadio}
/>
A
</label>
<label>
<input
id="radio2"
type="radio"
name="group"
value="B"
two-way=${checkedRadio}
/>
B
</label>
<label for="pet-select">Choose a pet:</label>
<select name="pets" id="pet-select" two-way=${select}>
<option value="">--Please choose an option--</option>
<option value="dog">Dog</option>
<option value="cat">Cat</option>
<option value="hamster">Hamster</option>
<option value="parrot">Parrot</option>
<option value="spider">Spider</option>
<option value="goldfish">Goldfish</option>
</select>
</div>
`);
let cond =
//@ts-ignore
$("#text").value === "text" &&
//@ts-ignore
$("textarea").value === "text" &&
//@ts-ignore
$("#checkbox1").checked &&
//@ts-ignore
$("#radio1").checked &&
//@ts-ignore
!$("#radio2").checked &&
//@ts-ignore
$("select").value === "cat" &&
//@ts-ignore
$("#datetime").value === "2018-06-08T00:00";
// Code Coverage
$("#radio1").dispatchEvent(new window.Event("change"));
//@ts-ignore
$("#checkbox1").click();
text("haha");
checked(false);
checkedRadio("B");
select("dog");
datetime("2018-06-09T00:00");
setTimeout(() => {
unmount();
unset(text);
unset(checked);
unset(checkedRadio);
unset(select);
unset(datetime);
});
return (cond &&
//@ts-ignore
$("#text").value === "haha" &&
//@ts-ignore
$("textarea").value === "haha" &&
//@ts-ignore
!$("#checkbox1").checked &&
//@ts-ignore
!$("#radio1").checked &&
//@ts-ignore
$("#radio2").checked &&
//@ts-ignore
$("select").value === "dog" &&
//@ts-ignore
$("#datetime").value === "2018-06-09T00:00");
});
it("works with different events on one element", () => {
let a, b, c;
const elem = html `<p
ona=${() => (a = true)}
onb=${{ event: () => (b = true), options: {} }}
onc=${() => (c = true)}
>
test
</p>`;
emit("a", {}, elem);
emit("b", {}, elem);
emit("c", {}, elem);
return !!a && !!b && !!c;
});
it("stringifies object", () => {
hydro.x = { a: 3 };
const elem = html `<p>{{x}}</p>`;
setTimeout(() => (hydro.x = null));
return elem.textContent === '{"a":3}';
});
it("removes bind element", () => {
hydro.y = { a: 3 };
const elem = html `<p bind="{{y}}">asd</p>`;
render(elem);
hydro.y = null;
return !elem.isConnected;
});
it("removes bind element with multiple elements", () => {
hydro.z = 4;
const elem = html `<p bind="{{z}}">asd</p>`;
const elem2 = html `<p bind="{{z}}">asd2</p>`;
render(elem);
render(elem2);
hydro.z = null;
return !elem.isConnected && !elem2.isConnected;
});
it("super rare manipulation of DOM Element", () => {
hydro.abc = { id: "jja", href: "cool" };
const elem = html `<a id="{{abc.id}}" href="{{abc.href}}"
>asdad</a
>`;
elem.id = "{{abc.id}}";
elem.href = "{{abc.href}}";
html `<p>${elem}</p>`;
setTimeout(() => (hydro.abc = null));
return true;
});
});
describe("compare", () => {
it("lifecycle hooks and text Nodes - false - length", () => {
const renderFn1 = () => 2;
const renderFn2 = () => 3;
const cleanFn1 = () => 3;
const elem1 = html `a`;
const elem2 = html `a`;
onRender(renderFn1, elem1);
onRender(renderFn2, elem2);
onCleanup(cleanFn1, elem1);
return internals.compare(elem1, elem2) === false;
});
it("lifecycle hooks and text Nodes - false - string", () => {
const renderFn1 = () => 2;
const renderFn2 = () => 3;
const cleanFn1 = () => 3;
const cleanFn2 = () => 3;
const elem1 = html `a`;
const elem2 = html `a`;
onRender(renderFn1, elem1);
onRender(renderFn2, elem2);
onCleanup(cleanFn1, elem1);
onCleanup(cleanFn2, elem2);
return internals.compare(elem1, elem2) === false;
});
it("returns false if child has different lifecycle hooks - onlyTextChildren", () => {
const subelem1 = html `hello`;
onRender(() => 2, subelem1);
const elem1 = html `<p>${subelem1}</p>`;
const subelem2 = html `hello`;
onRender(() => 3, subelem2);
const elem2 = html `<p>${subelem2}</p>`;
return internals.compare(elem1, elem2, true) === false;
});
it("returns false if child has different lifecycle hooks", () => {
const subelem1 = html `hello`;
onRender(() => 2, subelem1);
const elem1 = html `<p>${subelem1}</p>`;
const subelem2 = html `hello`;
onRender(() => 3, subelem2);
const elem2 = html `<p>${subelem2}</p>`;
return internals.compare(elem1, elem2) === false;
});
it("lifecycle hooks and text Nodes - true", () => {
const renderFn1 = () => 2;
const renderFn2 = () => 2;
const cleanFn1 = () => 3;
const cleanFn2 = () => 3;
const elem1 = html `a`;
const elem2 = html `a`;
onRender(renderFn1, elem1);
onRender(renderFn2, elem2);
onCleanup(cleanFn1, elem1);
onCleanup(cleanFn2, elem2);
return internals.compare(elem1, elem2) === true;
});
it("same functions return true", () => {
const fn1 = () => 2;
const fn2 = () => 2;
const elem1 = html `<p onclick=${fn1}></p>`;
const elem2 = html `<p onclick=${fn2}></p>`;
return internals.compare(elem1, elem2) === true;
});
it("same lifecycle hooks return true", () => {
const fn1 = () => 2;
const fn2 = () => 2;
const elem1 = html `<p>1</p>`;
onRender(fn1, elem1);
onCleanup(fn2, elem1);
const elem2 = html `<p>1</p>`;
onRender(fn1, elem2);
onCleanup(fn2, elem2);
return internals.compare(elem1, elem2) === true;
});
it("different function return false", () => {
const fn1 = () => 2;
const fn2 = () => 3;
const elem1 = html `<p onclick=${fn1}></p>`;
const elem2 = html `<p onclick=${fn2}></p>`;
return internals.compare(elem1, elem2) === false;
});
it("different lifecycle hooks return false", () => {
const fn1 = () => 2;
const fn2 = () => 3;
const elem1 = html `<p>1</p>`;
onRender(fn1, elem1);
onCleanup(fn2, elem1);
const elem2 = html `<p>1</p>`;
onRender(fn1, elem2);
onCleanup(fn1, elem2);
return internals.compare(elem1, elem2) === false;
});
});
describe("render", () => {
it("does diffing with documentFragment", () => {
setInsertDiffing(true);
const elem1 = html `it`;
const elem2 = html `<p>hello</p>
<p>world</p>`;
render(elem1);
const unmount = render(elem2, elem1);
setInsertDiffing(false);
setTimeout(unmount);
return (!document.body.textContent.includes("hi") &&
document.body.textContent.includes("hello") &&
document.body.textContent.includes("world"));
});
it("do not reuseElements", () => {
setReuseElements(false);
const elem1 = html `a`;
const elem2 = html `a`;
render(elem1);
const unmount = render(elem2, elem1);
setTimeout(unmount);
setReuseElements(true);
return !elem1.isConnected && elem2.isConnected;
});
it("can render elements wrapped in reactive", async () => {
const number = reactive(5);
const elem = reactive(html `<p>${number}</p>`);
const unmount = render(elem);
const cond = getValue(elem).textContent.includes(String(getValue(number)));
setTimeout(() => number(6), 50);
setTimeout(() => {
unset(number);
unset(elem);
unmount();
}, 150);
await sleep(100);
return (cond && getValue(elem).textContent.includes(String(getValue(number))));
});
it("where does not exist - no render", () => {
const elemCount = document.body.querySelectorAll("*").length;
const unmount = render(html `<p>what</p>`, "#doesNotExist");
setTimeout(unmount);
return document.body.querySelectorAll("*").length === elemCount;
});
it("elem is DocumentFragment, no where", () => {
const elem = html `<div id="first">1</div>
<div id="second">2</div>`;
const unmount = render(elem);
setTimeout(unmount);
return ($("#first").textContent.includes("1") &&
$("#second").textContent.includes("2"));
});
it("elem is svg, no where", () => {
const elem = html `<svg height="100" width="100">
<circle
cx="50"
cy="50"
r="40"
stroke="black"
stroke-width="3"
fill="red"
/>
</svg>`;
const unmount = render(elem);
setTimeout(unmount);
return elem.isConnected && !!document.body.querySelector("circle");
});
it("elem is textNode, no where", () => {
const elem = html `what`;
const unmount = render(elem);
setTimeout(unmount);
return elem.isConnected && document.body.textContent.includes("what");
});
it("elem is Element, no where", () => {
const elem = html `<p id="whatWhere">what</p>`;
const unmount = render(elem);
setTimeout(unmount);
return (elem.isConnected && $("#whatWhere").textContent.includes("what"));
});
it("elem is DocumentFragment, with where", () => {
document.body.insertAdjacentHTML("beforeend", '<p id="hello">here</p>');
const elem = html `<div id="firstOne">1</div>
<div id="secondOne">2</div>`;
const unmount = render(elem, "#hello");
setTimeout(unmount);
return ($("#firstOne").textContent.includes("1") &&
$("#secondOne").textContent.includes("2") &&
!document.body.querySelector("#hello"));
});
it("elem is svg, with where", () => {
document.body.insertAdjacentHTML("beforeend", '<p id="hello2">here</p>');
const elem = html `<svg height="100" width="100">
<circle
cx="50"
cy="50"
r="40"
stroke="black"
stroke-width="3"
fill="red"
/>
</svg>`;
const unmount = render(elem, "#hello2");
setTimeout(unmount);
return elem.isConnected && !!document.body.querySelector("circle");
});
it("elem is textNode, with where", () => {
document.body.insertAdjacentHTML("beforeend", '<p id="hello3">here</p>');
const elem = html `what`;
const unmount = render(elem, "#hello3");
setTimeout(unmount);
return (elem.isConnected &&
document.body.textContent.includes("what") &&
!document.body.querySelector("#hello3"));
});
it("elem is Element, with where", () => {
document.body.insertAdjacentHTML("beforeend", '<p id="hello4">here</p>');
const elem = html `<p id="testThisWhat">what</p>`;
const unmount = render(elem, "#hello4");
setTimeout(unmount);
return (elem.isConnected &&
$("#testThisWhat").textContent.includes("what") &&
!document.body.querySelector("#hello4"));
});
it("replace an element will replace the event", () => {
const click1 = (e) => (e.currentTarget.textContent = 1);
const click2 = (e) => (e.currentTarget.textContent = 2);
let elem = html ` <div id="event" onclick=${click1}>0</div> `;
render(elem);
//@ts-ignore
elem.click();
let cond = elem.textContent.includes("1");
elem = html ` <div id="event" onclick=${click2}>0</div> `;
const unmount = render(elem, "#event");
//@ts-ignore
elem.click();
setTimeout(unmount);
return cond && elem.textContent.includes("2");
});
it("replacing elements will not stop their state", async () => {
await sleep(300);
setInsertDiffing(true);
const video1 = html `
<div id="video">
<p>Value: 0</p>
<video width="400" controls autoplay loop muted>
<source
src="https://www.w3schools.com/html/mov_bbb.mp4"
type="video/mp4"
/>
<p>code coverage</p>
</video>
</div>
`;
const video2 = html `
<div id="video">
<p>Value: 1</p>
<video width="400" controls autoplay loop muted>
<source
src="https://www.w3schools.com/html/mov_bbb.mp4"
type="video/mp4"
/>
<p>code coverage</p>
</video>
</div>
`;
// Video Test
render(video1);
await sleep(300);
const time = $("video").currentTime;
const unmount = render(video2, "#video");
setInsertDiffing(false);
await sleep(150);
setTimeout(() => {
unmount();
});
return time <= $("video").currentTime;
});
it("calls lifecyle hooks on deep elements", () => {
let subOnRender = false;
let subOnCleanup = false;
let elemOnRender = false;
let elemOnCleanup = false;
function SubElem() {
const subElem = html `<p></p>`;
onRender(() => (subOnRender = true), subElem);
onCleanup(() => (subOnCleanup = true), subElem);
return subElem;
}
function Elem() {
const elem = html `<p>${SubElem()}</p>`;
onRender(() => (elemOnRender = true), elem);
onCleanup(() => (elemOnCleanup = true), elem);
return elem;
}
const unmount = render(Elem());
unmount();
return subOnRender && subOnCleanup && elemOnRender && elemOnCleanup;
});
it("calls the correct lifecyle hooks when replacing elements", () => {
let subOnRender = false;
let subOnCleanup = false;
let elemOnRender = false;
let elemOnCleanup = false;
const subElem = html `<p id="replace"></p>`;
onRender(() => (subOnRender = true), subElem);
onCleanup(() => (subOnCleanup = true), subElem);
render(subElem);
const elem = html `<p id="replace"></p>`;
onRender(() => (elemOnRender = true), elem);
onCleanup(() => (elemOnCleanup = true), elem);
const unmount = render(elem, "#replace");
setTimeout(unmount);
return subOnRender && subOnCleanup && elemOnRender && !elemOnCleanup;
});
it("diffs head against head", () => {
let oldValue;
let oldChildCount = 0;
setTimeout(() => {
oldValue = document.head.outerHTML;
oldChildCount = document.head.childElementCount;
setInsertDiffing(false);
render(document.createElement("head"), document.head, false);
condition = condition && 0 === document.head.childElementCount;
}, 750);
setTimeout(() => {
setInsertDiffing(false);
render(html `${oldValue}`, document.head, false);
condition =
condition && oldChildCount === document.head.childElementCount;
}, 800);
return true;
});
it("diffs body against body", () => {
let oldValue;
let oldChildCount = 0;
setTimeout(() => {
oldValue = document.body.outerHTML;
oldChildCount = document.body.childElementCount;
setInsertDiffing(false);
render(document.createElement("body"), document.body, false);
condition = condition && 0 === document.body.childElementCount;
}, 850);
setTimeout(() => {
setInsertDiffing(false);
render(html `${oldValue}`, document.body, false);
condition =
condition && oldChildCount === document.body.childElementCount;
}, 900);
return true;
});
it("diffs html against html", () => {
let oldValue;
let oldChildCount = 0;
setTimeout(() => {
oldValue = document.documentElement.outerHTML;
oldChildCount = document.documentElement.childElementCount;
setInsertDiffing(false);
render(document.createElement("html"), document.documentElement, false);
condition =
condition && 0 === document.documentElement.childElementCount;
}, 950);
setTimeout(() => {
setInsertDiffing(false);
render(html `${oldValue}`, document.documentElement, false);
condition =
condition &&
oldChildCount === document.documentElement.childElementCount;
}, 1000);
return true;
});
it("diffs head against head - setInsertDiffing", () => {
let oldValue;
let oldChildCount = 0;
setTimeout(() => {
oldValue = document.head.outerHTML;
oldChildCount = document.head.childElementCount;
setInsertDiffing(true);
render(document.createElement("head"), document.head, false);
condition = condition && 0 === document.head.childElementCount;
}, 1050);
setTimeout(() => {
setInsertDiffing(true);
render(html `${oldValue}`, document.head, false);
condition =
condition && oldChildCount === document.head.childElementCount;
}, 1100);
return true;
});
it("diffs body against body - setInsertDiffing", () => {
let oldValue;
let oldChildCount = 0;
setTimeout(() => {
oldValue = document.body.outerHTML;
oldChildCount = document.body.childElementCount;
setInsertDiffing(true);
render(document.createElement("body"), document.body, false);
condition = condition && 0 === document.body.childElementCount;
}, 1150);
setTimeout(() => {
setInsertDiffing(true);
render(html `${oldValue}`, document.body, false);
condition =
condition && oldChildCount === document.body.childElementCount;
}, 1200);
return true;
});
it("diffs html against html - setInsertDiffing", () => {
let oldValue;
let oldChildCount = 0;
setTimeout(() => {
oldValue = document.documentElement.outerHTML;
oldChildCount = document.documentElement.childElementCount;
setInsertDiffing(true);
render(document.createElement("html"), document.documentElement, false);
condition =
condition && 0 === document.documentElement.childElementCount;
}, 1250);
setTimeout(() => {
setInsertDiffing(true);
render(html `${oldValue}`, document.documentElement, false);
condition =
condition &&
oldChildCount === document.documentElement.childElementCount;
done();
}, 1300);
return true;
});
});
describe("reactive", () => {
it("primitive value", () => {
const counter = reactive(0);
const unmount = render(html `
<div
id="reactClick"
onclick=${() => counter((prev) => prev + 1)}
>
${counter}
</div>
`);
//@ts-ignore
$("#reactClick").click();
setTimeout(() => {
unmount();
unset(counter);
});
return $("#reactClick").textContent.includes("1");
});
it("reactive (object)", () => {
let obj1 = reactive({ a: { b: 5 } });
let obj2 = reactive({ a: { b: 5 } });
const unmount = render(html `
<div>
<div
id="reactiveObj1"
onclick=${() => obj1((current) => {
current.a.b = 777;
return current;
})}
>
${obj1.a.b}
</div>
<div
id="reactiveObj2"
onclick=${() => obj2((current) => {
current.a.b = 777;
})}
>
${obj2.a.b}
</div>
</div>
`);
//@ts-ignore
$("#reactiveObj1").click();
//@ts-ignore
$("#reactiveObj2").click();
setTimeout(() => {
unmount();
unset(obj1);
unset(obj2);
});
return ($("#reactiveObj1").textContent.includes("777") &&
$("#reactiveObj2").textContent.includes("777"));
});
it("reactive (array)", () => {
const arr1 = reactive([1, [2]]);
const arr2 = reactive([3, [4]]);
const unmount = render(html `
<div
id="reactiveArr1"
onclick=${() => arr1((current) => {
current[0] += 1;
return current;
})}
>
${arr1[0]}
</div>
<div
id="reactiveArr2"
onclick=${() => arr1((current) => {
current[1][0] += 1;
return current;
})}
>
${arr1[1][0]}
</div>
<div
id="reactiveArr3"
onclick=${() => arr2((current) => {
current[0] += 1;
})}
>
${arr2[0]}
</div>
<div
id="reactiveArr4"
onclick=${() => arr2((current) => {
current[1][0] += 1;
})}
>
${arr2[1][0]}
</div>
`);
//@ts-ignore
$("#reactiveArr1").click();
//@ts-ignore
$("#reactiveArr2").click();
//@ts-ignore
$("#reactiveArr3").click();
//@ts-ignore
$("#reactiveArr4").click();
setTimeout(() => {
unmount();
unset(arr1);
unset(arr2);
});
return ($("#reactiveArr1").textContent.includes("2") &&
$("#reactiveArr2").textContent.includes("3") &&
$("#reactiveArr3").textContent.includes("4") &&
$("#reactiveArr4").textContent.includes("5"));
});
it("special logic for prev functions", () => {
const a = reactive(undefined);
const b = reactive(44);
b(undefined);
hydro.c = undefined;
hydro.d = 44;
hydro.d = undefined;
const e = reactive(44);
e((prev) => undefined);
setTimeout(() => {
unset(a);
unset(b);
hydro.c = null;
hydro.d = null;
unset(e);
});
return (getValue(a) === undefined &&
getValue(b) === undefined &&
hydro.c === undefined &&
hydro.d === undefined,
getValue(e) === 44);
});
});
describe("watchEffect", () => {
it("tracks and dependencies and re-runs the function (setter)", async () => {
let watchCounter = 0;
hydro.count1 = 0;
hydro.count2 = 0;
watchEffect(() => {
hydro.count1 = 2;
hydro.count2 = 2;
watchCounter++;
});
hydro.count1 = 1;
hy