diff --git a/lib/jenot/web.ex b/lib/jenot/web.ex
index 34e9e32..d086aea 100644
--- a/lib/jenot/web.ex
+++ b/lib/jenot/web.ex
@@ -18,7 +18,7 @@ defmodule Jenot.Web do
plug Plug.Static,
at: "/",
- only: ~w(img js index.html site.webmanifest style.css),
+ only: ~w(img js index.html test.html site.webmanifest style.css),
from: {:jenot, "priv/static"}
plug Plug.Parsers,
@@ -30,11 +30,6 @@ defmodule Jenot.Web do
plug :dispatch
get "/" do
- priv_dir = :code.priv_dir(:jenot)
- index_path = Path.join([priv_dir, "static", "index.html"])
-
- send_file(conn, 200, index_path)
-
conn
|> resp(:found, "")
|> put_resp_header("location", "/index.html")
diff --git a/priv/static/js/components.js b/priv/static/js/components.js
index b2867a2..0fb92e5 100644
--- a/priv/static/js/components.js
+++ b/priv/static/js/components.js
@@ -11,7 +11,8 @@ class EditableArea extends HTMLElement {
connectedCallback() {
const text = this.textContent;
- this.displayElement = document.createElement("p", { class: "display" });
+ this.displayElement = document.createElement("p");
+ this.displayElement.classList.add("display");
this.inputElement = document.createElement("textarea");
this.inputElement.value = text;
diff --git a/priv/static/js/jenot-tests.js b/priv/static/js/jenot-tests.js
new file mode 100644
index 0000000..cdc6cb3
--- /dev/null
+++ b/priv/static/js/jenot-tests.js
@@ -0,0 +1,123 @@
+import "./components.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("editable-area updates on input", (container) => {
+ container.innerHTML = `
with a newline
`);
+ assert(() => eventCalled === true);
+});
+
+test("editable-area respects readonly attribute", (container) => {
+ container.innerHTML = `