type ManualTests = { [key: string]: ManualTests | [string, string] | [string]; } ((data: Record) => { class Counter extends Text { #parent?: Counter; #count = 0; constructor(start: string, parent?: Counter) { super(start); this.#parent = parent; } add() { this.textContent = (++this.#count) + ""; this.#parent?.add(); } } let queue = Promise.resolve(); const icon = document.head.getElementsByTagName("link")[0], processTests = (breadcrumbs: string, t: ManualTests, totalCount: Counter, successCount: Counter, errorCount: Counter) => { const df = document.createDocumentFragment(), testList = document.createElement("ul"); for (const [name, test] of Object.entries(t)) { if (test instanceof Array) { const li = testList.appendChild(document.createElement("li")), button = li.appendChild(document.createElement("button")); li.append(name); totalCount.add(); button.innerText = "Run Test"; button.addEventListener("click", () => { queue = queue.finally(() => new Promise(s => { const w = window.open("/test", "", ""), script = document.createElement("script"); if (!w) { alert("Cannot create window"); return; } button.toggleAttribute("disabled", true); let resultFn: (pass: boolean) => void, errorFn: (error: any) => void; new Promise((sFn, eFn) => { resultFn = sFn; errorFn = eFn; }).catch(error => { console.log({"section": breadcrumbs.slice(1, -1).split("/"), name, error}); alert(`Error in section ${breadcrumbs}, test "${name}": check console for details`); }).then(pass => { li.setAttribute("class", pass ? "pass" : "fail"); if (pass) { successCount.add(); } else { for (let node = li.parentNode; node; node = node.parentNode) { if (node instanceof HTMLDetailsElement) { node.toggleAttribute("open", true); } } errorCount.add(); } w.close(); button.remove(); }); Object.assign(w, {"result": resultFn!}); w.addEventListener("beforeunload", () => { button.toggleAttribute("disabled", false) s(); }); w.addEventListener("error", (e: ErrorEvent) => { w.close(); errorFn(e.error); s(); }); script.setAttribute("type", "module"); script.innerText = test[0]; w.addEventListener("load", () => { w.document.title = `${breadcrumbs}: ${name}`; w.document.body.innerHTML = (test[1] ?? "") + `
`; w.document.head.append(script); w.document.head.append(icon.cloneNode()); }); })); }); } else { const details = df.appendChild(document.createElement("details")), summary = details.appendChild(document.createElement("summary")), total = new Counter("0", totalCount), successful = new Counter("0", successCount), errors = document.createElement("span"); summary.append(name, ": ", successful, "/", total, errors); details.append(processTests(breadcrumbs + name + "/", test, total, successful, errors.appendChild(new Counter("", errorCount)))); } } if (testList.childElementCount > 0) { df.append(testList); } return df; }, total = new Counter("0"), successful = new Counter("0"), errors = document.createElement("span"), tests = processTests("/", data, total, successful, errors.appendChild(new Counter(""))); let opened = false; window.addEventListener("load", () => document.body.append("Tests: ", successful, "/", total, errors, tests)); window.addEventListener("keypress", (e: KeyboardEvent) => { if (e.key === "o") { opened = !opened; Array.from(document.getElementsByTagName("details"), e => e.toggleAttribute("open", opened)); } else if (e.key === "R") { Array.from(document.getElementsByTagName("button")).forEach(e => e.click()); } else if (e.key === "r") { document.getElementsByTagName("button")[0]?.click(); } }); })({ "router.js": { "html": { "x-router": { "simple non-match": [`import './lib/router.js';`, ``], "simple match": [`import './lib/router.js';`, ``], "match after link": [`import './lib/router.js';`, `Click here to make button`], "match after button (goto)": [`import './lib/router.js';`, ``], "history check": [`import './lib/router.js';`, `
Use the back button.
`], "path param": [`import './lib/router.js';`, `Click Here`], "match query": [`import './lib/router.js';`, `Click here to make button`], "query param": [`import './lib/router.js';`, ``], "match hash": [`import './lib/router.js';`, `Click here to make button`], "prefix match": [`import './lib/router.js';`, `Click Here`], "suffix match": [`import './lib/router.js';`, `Click Here`], "no-suffix match": [`import './lib/router.js';`, `
Failed
Click Here
`], "goto params": [`import './lib/router.js';`, ``], "goto overwrite params": [`import './lib/router.js';`, ``] }, "x-route": { "title change": [`import './lib/router.js';`, `Click Here`], "id change": [`import './lib/router.js';`, `Click Here`], "class change": [`import './lib/router.js';`, `Click Here`], "all change": [`import './lib/router.js';`, `Click Here`] } }, "js": { "simple non-match": [`import {router} from './lib/router.js'; document.body.prepend(router().add("", () => { const button = document.createElement("button"); button.textContent = "Success"; button.addEventListener("click", () => result(true)); return button; }));`], "simple match": [`import {router} from './lib/router.js'; document.body.prepend(router().add("/test", () => { const button = document.createElement("button"); button.textContent = "Success"; button.addEventListener("click", () => result(true)); return button; }));`], "match after link": [`import {router} from './lib/router.js'; document.body.prepend(router().add("/other-page", () => { const button = document.createElement("button"); button.textContent = "Success"; button.addEventListener("click", () => result(true)); return button; }).add("", () => { const a = document.createElement("a"); a.textContent = "Click Here"; a.setAttribute("href", "/other-page"); return a; }));`], "match after button (goto)": [`import {goto, router} from './lib/router.js'; document.body.prepend(router().add("/other-page", () => { const button = document.createElement("button"); button.textContent = "Success"; button.addEventListener("click", () => result(true)); return button; }).add("", () => { const a = document.createElement("button"); a.textContent = "Click Here"; a.addEventListener("click", () => goto("/other-page")); return a; }));`], "history check": [`import {goto, router} from './lib/router.js'; document.body.prepend(router().add("/other-page", () => { const div = document.createElement("div"); div.textContent = "Use the back button"; return div; }).add("", () => { const button = document.createElement("button"); let clicked = false; button.textContent = "Click Here"; button.addEventListener("click", () => { if (clicked) { result(true); } else { clicked = true; button.textContent = "Success"; goto("/other-page"); } }); return button; }));`], "path param": [`import {goto, router} from './lib/router.js'; document.body.prepend(router().add("/page-:page", ({page}) => { result(page === "15"); return new Text(""); }).add("", () => { const a = document.createElement("a"); a.textContent = "Click Here"; a.setAttribute("href", "/page-15"); return a; }));`], "match query": [`import {router} from './lib/router.js'; document.body.prepend(router().add("?page=other", () => { result(true); return new Text("Success"); }).add("", () => { const a = document.createElement("a"); a.textContent = "Click Here"; a.setAttribute("href", "?page=other"); return a; }));`], "query param": [`import {goto, router} from './lib/router.js'; document.body.prepend(router().add("/?page=:page", ({page}) => { if (page === "15") { result(true); } const a = document.createElement("a"); a.textContent = "Click Here"; a.setAttribute("href", "?page=15"); return a; }));`], "match hash": [`import {router} from './lib/router.js'; document.body.prepend(router().add("#match", () => { result(true); return new Text("Success"); }).add("", () => { const a = document.createElement("a"); a.textContent = "Click Here"; a.setAttribute("href", "#match"); return a; }));`], "prefix match": [`import {router} from './lib/router.js'; document.body.prepend(router().add("/other-page/", () => { result(true); return new Text("Success"); }).add("", () => { const a = document.createElement("a"); a.textContent = "Click Here"; a.setAttribute("href", "/other-page/name"); return a; }));`], "suffix match": [`import {router} from './lib/router.js'; document.body.prepend(router().add("other-page/", () => { result(true); return new Text("Success"); }).add("", () => { const a = document.createElement("a"); a.textContent = "Click Here"; a.setAttribute("href", "/something/other-page/name"); return a; }));`], "no-suffix match": [`import {router} from './lib/router.js'; document.body.prepend(router().add("other-page/", () => { result(false); return new Text("Failed"); }).add("/other-page", () => { result(true); return new Text("Success"); }).add("", () => { const a = document.createElement("a"); a.textContent = "Click Here"; a.setAttribute("href", "/other-page"); return a; }));`], "goto params": [`import {goto, router} from './lib/router.js'; document.body.prepend(router().add("/other-page", data => { result(data.test === 1 && data.data === "abc"); return new Text("Success"); }).add("", () => { const button = document.createElement("button"); button.textContent = "Click Here"; button.addEventListener("click", () => goto("/other-page", {'test': 1, 'data': 'abc'})); return button; }));`], "goto overwrite params": [`import {goto, router} from './lib/router.js'; document.body.prepend(router().add("/other-page/:id/:data", data => { result(data.id === "123" && data.test === 1 && data.data === "abc"); return new Text("Success"); }).add("", () => { const button = document.createElement("button"); button.textContent = "Click Here"; button.addEventListener("click", () => goto("/other-page/123/def", {"test": 1, "data": "abc"})); return button; }));`], } }, "urlstate.js": { "StateBound": { "hasDefault": [`import URLState from './lib/urlstate.js'; import {button} from './lib/html.js'; const sb = URLState("some-name", "default"); document.body.prepend(button({"data-value": sb, "onclick": function() { result(this.dataset.value === "default"); }}, "Click Here")); `], "value updates with URL": [`import URLState, {goto} from './lib/urlstate.js'; import {button} from './lib/html.js'; const sb = URLState("some-name", "default"); document.body.prepend(button({"data-value": sb, "onclick": function() { if (this.dataset.value === "default") { goto('?some-name="123"'); this.innerText = "Click Here Again"; } else { result(window.location.search === "?some-name=%22123%22" && this.dataset.value === "123"); } }}, "Click Here")); `], "value updates with setter": [`import URLState, {goto} from './lib/urlstate.js'; import {button} from './lib/html.js'; const sb = URLState("some-name", "default"); document.body.prepend(button({"data-value": sb, "onclick": function() { if (this.dataset.value === "default") { sb.value = "123"; this.innerText = "Click Here Again"; } else { result(window.location.search === "?some-name=%22123%22" && this.dataset.value === "123"); } }}, "Click Here")); `], "url and value updates when link clicked": [`import URLState, {goto} from './lib/urlstate.js'; URLState("some-name", "default").onChange(v => result(v === "123")); `, `Click Here`] }, "type checker": { "correct value": [`import URLState, {goto, setParam} from './lib/urlstate.js'; import {button} from './lib/html.js'; const sb = URLState("some-name", "default", v => typeof v === "string"); sb.value = "other"; let wasOther = false; document.body.prepend(button({"onclick": function() { if (sb.value === "other") { wasOther = true; goto("?some-name=%22123%22"); } else if (sb.value === "123") { result(wasOther); } else { result(false); } }}, "Click Here")); `], "incorrect value": [`import URLState, {goto, setParam} from './lib/urlstate.js'; import {button} from './lib/html.js'; const sb = URLState("some-name", "default", v => typeof v === "string"); sb.value = "other"; let wasOther = false; document.body.prepend(button({"onclick": function() { if (sb.value === "other") { wasOther = true; goto("?some-name=123"); } else if (sb.value === "default") { result(wasOther); } else { result(false); } }}, "Click Here")); `] }, "setParam": { "correct value": [`import URLState, {goto, setParam} from './lib/urlstate.js'; import {button} from './lib/html.js'; const sb = URLState("some-name", "default", v => typeof v === "string"); sb.value = "other"; let wasOther = false; document.body.prepend(button({"onclick": function() { if (sb.value === "other") { wasOther = true; setParam("some-name", '"123"'); } else if (sb.value === "123") { result(wasOther); } else { result(false); } }}, "Click Here")); `], "incorrect value": [`import URLState, {goto, setParam} from './lib/urlstate.js'; import {button} from './lib/html.js'; const sb = URLState("some-name", "default", v => typeof v === "string"); sb.value = "other"; let wasOther = false; document.body.prepend(button({"onclick": function() { if (sb.value === "other") { wasOther = true; setParam("some-name", "123"); } else if (sb.value === "default") { result(wasOther); } else { result(false); } }}, "Click Here")); `] }, "toURL": [`import {goto, toURL} from './lib/urlstate.js'; const check = (a, b) => { if (a !== b) { result(false); } }; check(toURL(), "?"); check(toURL({"a": "1"}), "?a=1"); check(toURL({"a": "1", "b": 2}), "?a=1&b=2"); goto("?a=3"); check(toURL(), "?a=3"); check(toURL({"a": "4"}), "?a=4"); check(toURL({"b": "5"}), "?a=3&b=5"); check(toURL({"b": "5"}, ["a"]), "?b=5"); result(true); `], "urlChanged": [`import {goto, urlChanged} from './lib/urlstate.js'; urlChanged.when(() => result(true)); goto("?changed"); `] } });