hydro-js
Version:
A lightweight reactive library
1,415 lines (1,229 loc) • 81.8 kB
HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tests</title>
<script type="module">
import { runTests } from "@web/test-runner-mocha";
import { expect } from "@esm-bundle/chai";
import {
html,
h,
hydro,
render,
setGlobalSchedule,
setReuseElements,
reactive,
unset,
emit,
watchEffect,
observe,
getValue,
onRender,
onCleanup,
internals,
ternary,
setInsertDiffing,
$,
unobserve,
setAsyncUpdate,
setShouldSetReactivity,
view,
} from "../dist/library.js";
runTests(async () => {
describe("library", () => {
setGlobalSchedule(false); // Simplifies testing
const sleep = (time) =>
new Promise((resolve) => setTimeout(resolve, time));
describe("functions", () => {
describe("h", () => {
it("handles functions correctly", () => {
expect(
h(() => h("p", null, ["Hello World"]), null, [])
.textContent === "Hello World"
).to.be.true;
});
it("returns a valid element", () => {
const test = reactive("A");
setTimeout(() => {
unset(test);
});
expect(h("div", null, [test]).localName === "div").to.be.true;
});
it("returns a valid element when it has children", () => {
expect(
h("div", null, [h("p", null, ["test"])]).childNodes.length ===
1
).to.be.true;
});
it("handles documentFragment", () => {
expect(
h(h, null, h("p", null, "hi"), h("p", null, "ho"))
.nodeType === 11
).to.be.true;
});
});
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();
expect(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();
expect(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();
expect(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();
expect(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();
expect(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);
expect(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);
expect(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);
expect(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);
expect(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);
expect(condition && document.body.childElementCount === 0);
});
});
describe("setShouldSetReactivity", () => {
it("code coverage", () => {
setShouldSetReactivity(true);
});
});
describe("setReuseElements", () => {
it("code coverage", () => {
setReuseElements(true);
});
});
describe("setGlobalSchedule", () => {
it("sets asnycUpdate on hydro objects", () => {
hydro.schedule = {};
expect(hydro.schedule.asyncUpdate).to.be.false;
setGlobalSchedule(true);
expect(hydro.schedule.asyncUpdate).to.be.true;
setGlobalSchedule(false);
expect(hydro.schedule.asyncUpdate).to.be.false;
hydro.schedule = null;
});
});
describe("setAsyncUpdate", () => {
it("sets asnycUpdate on reactive object", () => {
const schedule = reactive({});
setAsyncUpdate(schedule, false);
setTimeout(unset, 0, schedule);
});
it("works chained", () => {
const abc = reactive({ a: { b: 4 } });
setAsyncUpdate(abc.a, false);
setTimeout(unset, 0, abc);
});
});
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} />`;
expect(elem.localName === tag).to.be.true;
});
});
it("returns empty text node", () => {
expect(
html`<input type="checkbox" required=${false} />`
.textContent === ""
).to.be.true;
});
it("handles a new html doc correctly", () => {
const elem = html`<html>
<head></head>
<body>
a
</body>
</html>`;
expect(
elem.localName === "html" &&
!!elem.querySelector("head") &&
!!elem.querySelector("body")
).to.be.true;
});
it("returns empty text node", () => {
expect(document.createTextNode("").isEqualNode(html``)).to.be
.true;
});
it("returns text node", () => {
expect(
document.createTextNode("hello").isEqualNode(html`hello`)
).to.be.true;
});
it("returns document fragment", () => {
const node = html`<div><p>hi</p></div>
<div><span>ho</span></div>`;
expect(
node.nodeName !== "svg" &&
"getElementById" in node &&
node.childElementCount === 2
).to.be.true;
});
it("returns element", () => {
const elem = html`<p>hello</p>`;
expect(
elem.localName === "p" && elem.textContent.includes("hello")
).to.be.true;
});
it("variable input (node)", () => {
const p = html`<p>hi</p>`;
const elem = html`<div>${p}</div>`;
expect(elem.contains(p) && elem.textContent.includes("hi")).to
.be.true;
});
it("variable input (primitive value)", () => {
const test = "test";
const elem = html`<div>${test}</div>`;
expect(elem.textContent.includes(test)).to.be.true;
});
it("variable input (hydro)", () => {
hydro.testValue = "test";
const elem = html`<div>{{ testValue }}</div>`;
setTimeout(() => {
hydro.testValue = null;
});
expect(elem.textContent.includes(hydro.testValue)).to.be.true;
});
it("variable input (reactive) - does not include undefined", () => {
const data = reactive({});
const elem = html`<div>${data.test}</div>`;
setTimeout(unset, 0, data);
expect(elem.textContent.includes(undefined)).to.be.false;
});
it("variable input (reactive)", () => {
const test = reactive("test");
const elem = html`<div>${test}</div>`;
setTimeout(unset, 0, test);
expect(elem.textContent.includes(getValue(test))).to.be.true;
});
it("variable input (eventListener)", () => {
const onClick = (e) => (e.currentTarget.textContent = 1);
const elem = html`<div onclick=${onClick}>0</div>`;
elem.click();
expect(elem.textContent.includes("1")).to.be.true;
});
it("variable input (array - normal)", () => {
const arr = [42, "test"];
const elem = html`<div>${arr}</div>`;
expect(
elem.textContent.includes("42") &&
elem.textContent.includes("test")
).to.be.true;
});
it("variable input (function)", () => {
const onClick = (e) =>
(e.currentTarget.textContent =
Number(e.currentTarget.textContent) + 1);
const elem = html`<div onclick=${onClick}>0</div>`;
elem.click();
expect(elem.textContent.includes("1")).to.be.true;
});
it("variable input (eventListener )", () => {
const onClick = {
event: (e) =>
(e.currentTarget.textContent =
Number(e.currentTarget.textContent) + 1),
options: {
once: true,
},
};
const elem = html`<div onclick=${onClick}>0</div>`;
elem.click();
elem.click();
expect(elem.textContent.includes("1")).to.be.true;
});
it("variable input (array - node)", () => {
const p = html`<p>test</p>`;
const arr = [42, p];
const elem = html`<div>${arr}</div>`;
expect(
elem.textContent.includes("42") &&
elem.textContent.includes("test") &&
elem.contains(p)
).to.be.true;
});
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>`;
elem.click();
expect(
elem.id === "test" &&
elem.target === "_blank" &&
elem.textContent === "1"
).to.be.true;
});
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>`;
elem.click();
elem.click();
expect(
elem.id === "test" &&
elem.target === "_blank" &&
elem.textContent === "1"
).to.be.true;
});
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);
});
expect(
document.body.textContent.includes("Fabian") &&
document.body.textContent.includes("123") &&
document.body.textContent.includes("0")
).to.be.true;
});
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();
});
expect(native.innerHTML.trim() === elem.innerHTML.trim()).to.be
.true;
});
it("removes {{..}}) from html attribute", () => {
const attr = reactive({ id: "test" });
const elem = html`<p ${attr}></p>`;
const unmount = render(elem);
setTimeout(() => {
unmount();
unset(attr);
});
expect(elem.id === "test" && !elem.hasAttribute("{{attr}}")).to
.be.true;
});
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 =
$("#text").value === "text" &&
$("textarea").value === "text" &&
$("#checkbox1").checked &&
$("#radio1").checked &&
!$("#radio2").checked &&
$("select").value === "cat" &&
$("#datetime").value === "2018-06-08T00:00";
// Code Coverage
$("#radio1").dispatchEvent(new window.Event("change"));
$("#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);
});
expect(
cond &&
$("#text").value === "haha" &&
$("textarea").value === "haha" &&
!$("#checkbox1").checked &&
!$("#radio1").checked &&
$("#radio2").checked &&
$("select").value === "dog" &&
$("#datetime").value === "2018-06-09T00:00"
).to.be.true;
});
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);
expect(a).to.be.true;
expect(b).to.be.true;
expect(c).to.be.true;
});
it("stringifies object", () => {
hydro.x = { a: 3 };
const elem = html`<p>{{x}}</p>`;
setTimeout(() => (hydro.x = null));
expect(elem.textContent).to.equal('{"a":3}');
});
it("removes bind element", () => {
hydro.y = { a: 3 };
const elem = html`<p bind="{{y}}">asd</p>`;
render(elem);
hydro.y = null;
expect(elem.isConnected).to.be.false;
});
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;
expect(elem.isConnected).to.be.false;
expect(elem2.isConnected).to.be.false;
});
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));
});
});
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);
expect(internals.compare(elem1, elem2) === false).to.be.true;
});
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);
expect(internals.compare(elem1, elem2) === false).to.be.true;
});
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>`;
expect(internals.compare(elem1, elem2) === false).to.be.true;
});
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>`;
expect(internals.compare(elem1, elem2, true) === false).to.be
.true;
});
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);
expect(internals.compare(elem1, elem2) === true).to.be.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>`;
expect(internals.compare(elem1, elem2) === true).to.be.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);
expect(internals.compare(elem1, elem2) === true).to.be.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>`;
expect(internals.compare(elem1, elem2) === false).to.be.true;
});
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);
expect(internals.compare(elem1, elem2) === false).to.be.true;
});
});
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);
expect(
!document.body.textContent.includes("hi") &&
document.body.textContent.includes("hello") &&
document.body.textContent.includes("world")
).to.be.true;
});
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);
expect(!elem1.isConnected && elem2.isConnected).to.be.true;
});
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);
expect(
cond &&
getValue(elem).textContent.includes(
String(getValue(number))
)
).to.be.true;
});
it("where does not exist - no render", () => {
const elemCount = document.body.querySelectorAll("*").length;
const unmount = render(html`<p>what</p>`, "#doesNotExist");
setTimeout(unmount);
expect(document.body.querySelectorAll("*").length === elemCount)
.to.be.true;
});
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);
expect(
$("#first").textContent.includes("1") &&
$("#second").textContent.includes("2")
).to.be.true;
});
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);
expect(
elem.isConnected && !!document.body.querySelector("circle")
).to.be.true;
});
it("elem is textNode, no where", () => {
const elem = html`what`;
const unmount = render(elem);
setTimeout(unmount);
expect(
elem.isConnected && document.body.textContent.includes("what")
).to.be.true;
});
it("elem is Element, no where", () => {
const elem = html`<p id="whatWhere">what</p>`;
const unmount = render(elem);
setTimeout(unmount);
expect(
elem.isConnected &&
$("#whatWhere").textContent.includes("what")
).to.be.true;
});
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);
expect(
$("#firstOne").textContent.includes("1") &&
$("#secondOne").textContent.includes("2") &&
!document.body.querySelector("#hello")
).to.be.true;
});
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);
expect(
elem.isConnected && !!document.body.querySelector("circle")
).to.be.true;
});
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);
expect(
elem.isConnected &&
document.body.textContent.includes("what") &&
!document.body.querySelector("#hello3")
).to.be.true;
});
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);
expect(
elem.isConnected &&
$("#testThisWhat").textContent.includes("what") &&
!document.body.querySelector("#hello4")
).to.be.true;
});
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);
elem.click();
let cond = elem.textContent.includes("1");
elem = html` <div id="event" onclick=${click2}>0</div> `;
const unmount = render(elem, "#event");
elem.click();
setTimeout(unmount);
expect(cond && elem.textContent.includes("2")).to.be.true;
});
it("replacing elements will not stop their state", async () => {
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();
});
expect(time <= $("video").currentTime).to.be.true;
});
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();
expect(
subOnRender && subOnCleanup && elemOnRender && elemOnCleanup
).to.be.true;
});
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);
expect(
subOnRender && subOnCleanup && elemOnRender && !elemOnCleanup
).to.be.true;
});
});
describe("reactive", () => {
it("primitive value", () => {
const counter = reactive(0);
const unmount = render(
html`
<div
id="reactClick"
onclick=${() => counter((prev) => prev + 1)}
>
${counter}
</div>
`
);
$("#reactClick").click();
setTimeout(() => {
unmount();
unset(counter);
});
expect($("#reactClick").textContent.includes("1")).to.be.true;
});
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>
`
);
$("#reactiveObj1").click();
$("#reactiveObj2").click();
setTimeout(() => {
unmount();
unset(obj1);
unset(obj2);
});
expect(
$("#reactiveObj1").textContent.includes("777") &&
$("#reactiveObj2").textContent.includes("777")
).to.be.true;
});
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>
`
);
$("#reactiveArr1").click();
$("#reactiveArr2").click();
$("#reactiveArr3").click();
$("#reactiveArr4").click();
setTimeout(() => {
unmount();
unset(arr1);
unset(arr2);
});
expect(
$("#reactiveArr1").textContent.includes("2") &&
$("#reactiveArr2").textContent.includes("3") &&
$("#reactiveArr3").textContent.includes("4") &&
$("#reactiveArr4").textContent.includes("5")
).to.be.true;
});
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);
});
expect(
getValue(a) === undefined &&
getValue(b) === undefined &&
hydro.c === undefined &&
hydro.d === undefined,
getValue(e) === 44
).to.be.true;
});
});
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++; // initial run + set + set (previous line)
});
hydro.count1 = 1;
hydro.count2 = 1;
setTimeout(() => {
hydro.count1 = null;
hydro.count2 = null;
}, 200);
await sleep(300);
expect(watchCounter === 5).to.be.true;
});
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);