mirror of
https://github.com/zoldar/jenot.git
synced 2026-01-03 14:32:54 +00:00
Create a minimal test framework and comment code somewhat
This commit is contained in:
parent
0c196ec2b1
commit
9119a66bea
5 changed files with 197 additions and 20 deletions
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
123
priv/static/js/jenot-tests.js
Normal file
123
priv/static/js/jenot-tests.js
Normal 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)
|
||||
})
|
||||
|
|
@ -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
38
priv/static/test.html
Normal 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>
|
||||
Loading…
Add table
Reference in a new issue