UNPKG

hydro-js

Version:

A lightweight reactive library

1,415 lines (1,229 loc) 81.8 kB
<!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);