diff --git a/priv/static/js/components-test.js b/priv/static/js/components-test.js
new file mode 100644
index 0000000..5438c14
--- /dev/null
+++ b/priv/static/js/components-test.js
@@ -0,0 +1,41 @@
+import { test, assert } from "./test-utils.js";
+import "./components.js";
+
+test("editable-area renders", async (container) => {
+ container.innerHTML = `Test 123`;
+
+ const textarea = container.querySelector("textarea");
+ const display = container.querySelector("p");
+
+ await assert(() => textarea.value === "Test 123");
+ await assert(() => display.classList.contains("display"));
+ await assert(() => display.innerHTML === "Test 123
");
+});
+
+test("editable-area updates on input", async (container) => {
+ container.innerHTML = ``;
+
+ const textarea = container.querySelector("textarea");
+ const display = container.querySelector("p");
+
+ let eventCalled = false;
+ container.addEventListener("contentChange", () => {
+ eventCalled = true;
+ });
+
+ textarea.value = "Some new content\nwith a newline";
+ textarea.dispatchEvent(
+ new Event("input", { bubbles: true, cancelable: true }),
+ );
+
+ await assert(() => display.innerHTML === `Some new content
with a newline
`);
+ await assert(() => eventCalled === true);
+});
+
+test("editable-area respects readonly attribute", async (container) => {
+ container.innerHTML = `Some text`;
+
+ const textarea = container.querySelector("textarea");
+
+ await assert(() => textarea.disabled);
+});
diff --git a/priv/static/js/jenot-tests.js b/priv/static/js/jenot-tests.js
index cdc6cb3..95bdf65 100644
--- a/priv/static/js/jenot-tests.js
+++ b/priv/static/js/jenot-tests.js
@@ -1,123 +1,11 @@
-import "./components.js";
+import { runTests } from "./test-utils.js";
+
+// tests to run
+import "./components-test.js";
const URL_PARAMS = new URLSearchParams(window.location.search);
-
const CONCRETE_TEST = URL_PARAMS.get("t");
-let counter = 1;
-
-const body = document.querySelector("body");
-const log = document.querySelector("#test-log");
-
-class AssertError extends Error {
- constructor(message, options) {
- super(message, options);
- }
-}
-
-const setup = (idx) => {
- const container = document.createElement("div", { id: `test-${idx}` });
- body.appendChild(container);
- return container;
-};
-
-async function assert(assertion, timeout) {
- timeout = timeout || 100;
- const interval = Math.max(timeout / 10, 10);
- const start = performance.now();
- let now = performance.now();
-
- while (true) {
- let result;
- let error;
-
- try {
- result = assertion();
- } catch (error) {
- result = false;
- error = error;
- }
-
- if (result === true) break;
-
- if (now - start >= timeout) {
- const assertionStr = assertion.toString();
- const opts = error ? { cause: error } : {};
- throw new AssertError(`Assertion failed: ${assertionStr}`, opts);
- }
-
- await new Promise((r) => setTimeout(r, interval));
- now = performance.now();
- }
-}
-
-function test(label, testFun) {
- if (CONCRETE_TEST && CONCRETE_TEST != counter) {
- counter++;
- return;
- }
- const container = setup(counter);
- const logRow = document.createElement("li");
- logRow.textContent = `[RUNNING] ${label}`;
- log.appendChild(logRow);
-
- try {
- testFun(container);
- const message = `[OK] ${label}`;
- console.info(message);
- logRow.textContent = message;
- logRow.classList.add("success");
- } catch (error) {
- const message = `[ERROR] ${label}`;
- console.error(message, error);
- logRow.textContent = message;
- logRow.classList.add("failure");
- const errorOutput = document.createElement("pre");
- errorOutput.textContent = `${error.name}: ${error.message}`;
- if (error.stack) {
- errorOutput.textContent += `\n${error.stack}`;
- }
- logRow.appendChild(errorOutput);
- }
- container.remove();
- counter++;
-}
-
-test("editable-area renders", (container) => {
- container.innerHTML = `Test 123`;
-
- const textarea = container.querySelector("textarea");
- const display = container.querySelector("p");
-
- assert(() => textarea.value === "Test 123");
- assert(() => display.classList.contains("display"));
- assert(() => display.innerHTML === "Test 123
");
+runTests({
+ only: CONCRETE_TEST ? [parseInt(CONCRETE_TEST)] : null,
});
-
-test("editable-area updates on input", (container) => {
- container.innerHTML = ``;
-
- const textarea = container.querySelector("textarea");
- const display = container.querySelector("p");
-
- let eventCalled = false;
- container.addEventListener("contentChange", () => {
- eventCalled = true;
- });
-
- textarea.value = "Some new content\nwith a newline";
- textarea.dispatchEvent(
- new Event("input", { bubbles: true, cancelable: true }),
- );
-
- assert(() => display.innerHTML === `Some new content
with a newline
`);
- assert(() => eventCalled === true);
-});
-
-test("editable-area respects readonly attribute", (container) => {
- container.innerHTML = `Some text`;
-
- const textarea = container.querySelector("textarea");
-
- assert(() => textarea.disabled)
-})
diff --git a/priv/static/js/test-utils.js b/priv/static/js/test-utils.js
new file mode 100644
index 0000000..9faec0f
--- /dev/null
+++ b/priv/static/js/test-utils.js
@@ -0,0 +1,101 @@
+let counter = 1;
+let tests = [];
+
+const body = document.querySelector("body");
+const log = document.querySelector("#test-log");
+
+class AssertError extends Error {
+ constructor(message, options) {
+ super(message, options);
+ }
+}
+
+const setup = (idx) => {
+ const container = document.createElement("div", { id: `test-${idx}` });
+ body.appendChild(container);
+ return container;
+};
+
+export async function assert(assertion, timeout) {
+ timeout = timeout || 100;
+ const interval = Math.max(timeout / 10, 10);
+ const start = performance.now();
+ let now = performance.now();
+
+ while (true) {
+ let result;
+ let error;
+
+ try {
+ result = assertion();
+ } catch (error) {
+ result = false;
+ error = error;
+ }
+
+ if (result === true) break;
+
+ if (now - start >= timeout) {
+ const assertionStr = assertion.toString();
+ const opts = error ? { cause: error } : {};
+ throw new AssertError(`Assertion failed: ${assertionStr}`, opts);
+ }
+
+ await new Promise((r) => setTimeout(r, interval));
+ now = performance.now();
+ }
+}
+
+export function runTests(globalOptions) {
+ tests.forEach(async ({ label, testFun, idx, options }) => {
+ const logRow = document.createElement("li");
+
+ if (globalOptions.only && globalOptions.only.indexOf(idx) < 0) {
+ return;
+ }
+
+ if (options.skip) {
+ const message = `[SKIPPED] ${label}`;
+ logRow.textContent = message;
+ log.appendChild(logRow);
+ console.info(message);
+ }
+
+ const container = setup(idx);
+ logRow.textContent = `[RUNNING] ${label}`;
+ log.appendChild(logRow);
+
+ try {
+ await testFun(container);
+ const message = `[OK] ${label}`;
+ console.info(message);
+ logRow.textContent = message;
+ logRow.classList.add("success");
+ } catch (error) {
+ const message = `[ERROR] ${label}`;
+ console.error(message, error);
+ logRow.textContent = message;
+ logRow.classList.add("failure");
+ const errorOutput = document.createElement("pre");
+ errorOutput.textContent = `${error.name}: ${error.message}`;
+ if (error.stack) {
+ errorOutput.textContent += `\n${error.stack}`;
+ }
+ logRow.appendChild(errorOutput);
+ }
+ container.remove();
+ });
+}
+
+export function test(label, testFun, options) {
+ options = options || {};
+
+ tests.push({
+ label: label,
+ testFun: testFun,
+ idx: counter,
+ options: options,
+ });
+
+ counter++;
+}