hydro-js
Version:
A lightweight reactive library
1,771 lines (1,535 loc) • 68.3 kB
text/typescript
import { Window } from "happy-dom";
const window = new Window({ url: "https://localhost:8080" });
window.document.write(`
<!doctype html>
<html lang="en">
<head>
<title>Hello SSR</title>
</head>
<body>
</body>
</html>
`);
// @ts-expect-error
globalThis.window = window;
// @ts-expect-error
globalThis.document = window.document;
await window.happyDOM.waitUntilComplete();
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: number) =>
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: Array<{ name: string; success: boolean }> = [];
// --------- 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]) as Element).localName === "div";
});
it("handles documentFragment", () => {
return (
(h(h, null, h("p", null, "hi"), h("p", null, "ho")) as Element)
.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} />` as Element;
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>` as Element;
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>` as DocumentFragment;
return (
node.nodeName !== "svg" &&
"getElementById" in node &&
node.childElementCount === 2
);
});
it("returns element", () => {
const elem = html`<p>hello</p>` as Element;
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: any) => (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: any) =>
(e.currentTarget.textContent =
Number(e.currentTarget.textContent) + 1);
const elem = html`<div onclick=${onClick}>0</div>` as Element;
//@ts-ignore
elem.click();
return elem.textContent!.includes("1");
});
it("variable input (eventListener as object)", () => {
const onClick = {
event: (e: any) =>
(e.currentTarget.textContent =
Number(e.currentTarget.textContent) + 1),
options: {
once: true,
},
};
const elem = html`<div onclick=${onClick}>0</div>` as Element;
//@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: any) =>
(e.currentTarget.textContent =
Number(e.currentTarget.textContent) + 1),
target: "_blank",
};
const elem = html`<a ${props}>0</a>` as HTMLAnchorElement;
//@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: any) =>
(e.currentTarget.textContent =
Number(e.currentTarget.textContent) + 1),
options: {
once: true,
},
},
target: "_blank",
};
const elem = html`<a ${props}>0</a>` as HTMLAnchorElement;
//@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: typeof person) => {
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: typeof list) => {
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 as HTMLDivElement).innerHTML.trim()
);
});
it("removes {{..}}) from html attribute", () => {
const attr = reactive({ id: "test" });
const elem = html`<p ${attr}></p>` as Element;
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
// @ts-expect-error
$("#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
>` as HTMLAnchorElement;
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` as Element;
const elem2 = html`a` as Element;
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` as Element;
const elem2 = html`a` as Element;
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>` as Element;
const subelem2 = html`hello`;
onRender(() => 3, subelem2);
const elem2 = html`<p>${subelem2}</p>` as Element;
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>` as Element;
const subelem2 = html`hello`;
onRender(() => 3, subelem2);
const elem2 = html`<p>${subelem2}</p>` as Element;
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` as Element;
const elem2 = html`a` as Element;
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>` as Element;
const elem2 = html`<p onclick=${fn2}></p>` as Element;
return internals.compare(elem1, elem2) === true;
});
it("same lifecycle hooks return true", () => {
const fn1 = () => 2;
const fn2 = () => 2;
const elem1 = html`<p>1</p>` as Element;
onRender(fn1, elem1);
onCleanup(fn2, elem1);
const elem2 = html`<p>1</p>` as Element;
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>` as Element;
const elem2 = html`<p onclick=${fn2}></p>` as Element;
return internals.compare(elem1, elem2) === false;
});
it("different lifecycle hooks return false", () => {
const fn1 = () => 2;
const fn2 = () => 3;
const elem1 = html`<p>1</p>` as Element;
onRender(fn1, elem1);
onCleanup(fn2, elem1);
const elem2 = html`<p>1</p>` as Element;
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 as Element);
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: any) => (e.currentTarget.textContent = 1);
const click2 = (e: any) => (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: string | null;
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: string | null;
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: string | null;
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: string | null;
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: string | null;
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: string | null;
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: number) => 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: typeof obj1) => {
current.a.b = 777;
return current;
})}
>
${obj1.a.b}
</div>
<div
id="reactiveObj2"
onclick=${() =>
obj2((current: typeof obj2) => {
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: any) => {
current[0] += 1;
return current;
})}
>
${arr1[0]}
</div>
<div
id="reactiveArr2"
onclick=${() =>
arr1((current: any) => {
current[1][0] += 1;
return current;
})}
>
${arr1[1][0]}
</div>
<div
id="reactiveArr3"
onclick=${() =>
arr2((current: any) => {
current[0] += 1;
})}
>
${arr2[0]}
</div>
<div
id="reactiveArr4"
onclick=${() =>
arr2((current: any) => {
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: any) => 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;
hydro.count2 = 1;
setTimeout(() => {
hydro.count1 = null;
hydro.count2 = null;
}, 200);
await sleep(300);
return watchCounter === 5;
});
it("tracks and dependencies and re-runs the function (getter)", () => {
let watchCounter = 0;
const count3 = reactive(0);
const count4 = reactive(0);
watchEffect(() => {
getValue(count3);
getValue(count4);
watchCounter++;
});
count3(1);
count4(1);
setTimeout(() => {
unset(count3);
unset(count4);
});
return watchCounter === 3;
});
it("tracks and dependencies and re-runs the function (stop)", () => {
let watchCounter = 0;
const count5 = reactive(0);
const stop = watchEffect(() => {
getValue(count5);
watchCounter++;
});
stop();
count5(1);
setTimeout(() => {
unset(count5);
});
return watchCounter === 1;
});
});
describe("observe", () => {
it("observe hydro", () => {
let test = 0;
hydro.test = 0;
hydro.observe("test", () => {
test++;
});
hydro.test++;
setTimeout(() => {
hydro.test = null;
});
return test === 1;
});
it("observe reactive", () => {
let result = 0;
const test = reactive(0);
observe(test, () => {
result++;
});
test(1);
setTimeout(() => {
unobserve(test);
unset(test);
});
return result === 1;
});
it("observe primitive - function", () => {
let result = 0;
const test = reactive(0);
observe(test, () => {
result++;
});
test((prev: typeof test) => ++prev);
setTimeout(() => {
unset(test);
});
return result === 1;
});
it("observe not working for primitive with function and no return", () => {
let result = 0;
const test = reactive(0);
observe(test, () => {
result++;
});
test((prev: number) => {
prev = 1;
});
setTimeout(() => {
unset(test);
});
return result === 0;
});
it("observe object", () => {
let result = 0;
const test = reactive({ value: 0 });
observe(test, () => {
result++;
});
test({ value: 1 });
setTimeout(() => {
unset(test);
});
return result === 1;
});
it("observe object (return another) function", () => {
let result = 0;
const test = reactive({ value: 0 });
observe(test, () => {
result++;
});
test(() => {
return { value: 1 };
});
setTimeout(() => {
unset(test);
});
return result === 1;
});
it("observe object (modified arg)", () => {
let result = 0;
const test = reactive({ value: 0 });
observe(test.value, () => {
result++;
});
test((prev: typeof test) => {
prev.value++;
return prev;
});
setTimeout(() => {
unset(test);
});
return result === 1;
});
it("observe object and modify arg plus no return", () => {
let result = 0;
const test = reactive({ value: 0 });
observe(test.value, () => {
result++;
});
test((prev: typeof test) => {
prev.value++;
});
setTimeout(() => {
unset(test);
});
return result === 1;
});
it("observe object and modify arg plus no return - new syntax", () => {
let result = 0;
const test = reactive({ value: 0 });
observe(test.value, () => {
result++;
});
test.value.setter(5);
setTimeout(() => {
unset(test);
});
return result === 1;
});
});
describe("getValue", () => {
it("primitive value", () => {
const x = reactive(4);
setTimeout(unset, 0, x);
return getValue(x) === 4;
});
it("object", () => {
const obj = { test: 4 };
const x = reactive(obj);
setTimeout(unset, 0, x);
return getValue(x).test === obj.test;
});
it("array", () => {