Create a minimal test framework and comment code somewhat

This commit is contained in:
Adrian Gruntkowski 2024-11-28 13:44:16 +01:00
parent 0c196ec2b1
commit 9119a66bea
5 changed files with 197 additions and 20 deletions

View file

@ -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")

View file

@ -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;

View file

@ -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 = `<editable-area>Test 123</editable-area>`;
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<br>");
});
test("editable-area updates on input", (container) => {
container.innerHTML = `<editable-area></editable-area>`;
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<br>with a newline<br>`);
assert(() => eventCalled === true);
});
test("editable-area respects readonly attribute", (container) => {
container.innerHTML = `<editable-area readonly>Some text</editable-area>`;
const textarea = container.querySelector("textarea");
assert(() => textarea.disabled)
})

View file

@ -9,6 +9,28 @@ import {
} from "./notifications.js";
import "./components.js";
const URL_PARAMS = new URLSearchParams(window.location.search);
// Notes storage configuration.
// Currently supports either simple, local storage based implementation
// and a more elaborate one, using a combination of IndexedDB + network sync.
const Notes = URL_PARAMS.has("localStorage")
? new LocalNoteStore("jenot-app")
: new SyncedNoteStore("jenot-app", "notes", "/");
// Very rudimentary periodic sync. It will be refactored into a more real-time
// solution using either websocket of long-polling, so that server can notify about
// new data to sync.
const sync = async () => {
await Notes.sync();
Notes.saveStorage();
};
sync();
setInterval(sync, 5000);
// Notifications API test - to be reused for push notifications later on
const notificationsButton = document.querySelector("#enable-notifications");
const notificationsTestButton = document.querySelector("#test-notifications");
@ -25,27 +47,20 @@ notificationsTestButton.addEventListener("click", () => {
}, 8000);
});
const urlParams = new URLSearchParams(window.location.search);
const Notes = urlParams.has("localStorage")
? new LocalNoteStore("jenot-app")
: new SyncedNoteStore("jenot-app", "notes", "/");
const sync = async () => {
await Notes.sync();
Notes.saveStorage();
};
sync();
setInterval(sync, 5000);
// There are two note-form component instances - one for
// composing new notes and another one for editing existing notes.
const newNote = document.querySelector("#new-note");
const editNote = document.querySelector("#edit-note");
// Each save event originating from storage triggers a re-render
// of notes list.
Notes.addEventListener("save", render.bind(this));
// Initial notes render.
render();
// note-form component specific event handlers
newNote.addEventListener("addNote", async (e) => {
await Notes.add(e.detail);
Notes.saveStorage();
@ -65,6 +80,11 @@ editNote.addEventListener("deleteNote", async (e) => {
Notes.saveStorage();
});
// All notes are currently re-rendered on each storage
// update. The routine will be optimized to replace only
// nodes that actually changed - most likely based on unique
// note IDs associated with block elements.
async function render() {
const notes = await Notes.all();
const notesContainer = document.querySelector("#notes");

38
priv/static/test.html Normal file
View file

@ -0,0 +1,38 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Jenot Tests</title>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/img/favicon-16x16.png"
/>
<link rel="stylesheet" href="/style.css" />
<script type="module" src="/js/jenot-tests.js" defer></script>
<style type="text/css">
#test-log li {
color: lightgrey;
}
#test-log li.success {
color: green;
}
#test-log li.failure {
color: red;
}
#test-log li pre {
font-family: "Courier New", Courier, monospace;
font-weight: bold;
}
</style>
</head>
<body>
<div id="content"></div>
<ol id="test-log"></ul>
</body>
</html>