128 lines
5.3 KiB
JavaScript
128 lines
5.3 KiB
JavaScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import fs from "node:fs";
|
|
import vm from "node:vm";
|
|
|
|
const source = fs.readFileSync(new URL("../src/public_training.js", import.meta.url), "utf8");
|
|
|
|
function profileHarness() {
|
|
const ui = { editing: false, saving: false, requests: 0, renders: 0, html: "existing profile" };
|
|
const root = {
|
|
contains: () => ui.editing,
|
|
querySelector: () => ui.saving,
|
|
set innerHTML(value) { ui.html = value; },
|
|
};
|
|
const context = vm.createContext({
|
|
URL, URLSearchParams, AbortController, setTimeout, clearTimeout,
|
|
document: { getElementById: (id) => id === "publicTrainingRoot" ? root : null, activeElement: { matches: () => true } },
|
|
window: { location: { pathname: "/public-training/test", search: "?lang=sv", origin: "http://localhost" } },
|
|
fetch: async () => { ui.requests++; return { ok: true, json: async () => ({}) }; },
|
|
onRender: () => { ui.renders++; },
|
|
});
|
|
vm.runInContext(source.slice(0, source.lastIndexOf("\nloadProfile();")), context);
|
|
vm.runInContext('readTimerState = () => null; renderProfile = () => onRender(); profileData = {};', context);
|
|
return { ui, context, refresh: () => vm.runInContext("loadProfile(true)", context) };
|
|
}
|
|
|
|
test("mobile polling leaves an active field or pending save untouched", async () => {
|
|
const { ui, refresh } = profileHarness();
|
|
ui.editing = true;
|
|
await refresh();
|
|
ui.editing = false;
|
|
ui.saving = true;
|
|
await refresh();
|
|
assert.equal(ui.requests, 0);
|
|
assert.equal(ui.renders, 0);
|
|
ui.saving = false;
|
|
await refresh();
|
|
assert.equal(ui.renders, 1);
|
|
});
|
|
|
|
test("mobile polling rechecks editing after an in-flight response", async () => {
|
|
const { ui, context, refresh } = profileHarness();
|
|
context.fetch = async () => ({ ok: true, json: async () => { ui.editing = true; return {}; } });
|
|
await refresh();
|
|
assert.equal(ui.renders, 0);
|
|
});
|
|
|
|
test("mobile polling retains loaded statistics after a network failure", async () => {
|
|
const { ui, context, refresh } = profileHarness();
|
|
context.fetch = async () => { throw new Error("offline"); };
|
|
await refresh();
|
|
assert.equal(ui.html, "existing profile");
|
|
assert.equal(ui.renders, 0);
|
|
});
|
|
|
|
test("an initial mobile load failure still displays the error", async () => {
|
|
const { ui, context, refresh } = profileHarness();
|
|
vm.runInContext("profileData = null", context);
|
|
context.fetch = async () => { throw new Error("offline"); };
|
|
await refresh();
|
|
assert.match(ui.html, /public-training-error/);
|
|
});
|
|
|
|
test("mobile status detects stale data and recovery after failure", async () => {
|
|
const { context, refresh } = profileHarness();
|
|
await refresh();
|
|
assert.equal(vm.runInContext('getProfileConnectionState()', context), "current");
|
|
assert.equal(vm.runInContext('getProfileConnectionState(profileConnection.lastSuccessAt + 30000)', context), "stale");
|
|
context.fetch = async () => { throw new Error("offline"); };
|
|
await refresh();
|
|
assert.equal(vm.runInContext('getProfileConnectionState()', context), "failed");
|
|
context.fetch = async () => ({ ok: true, json: async () => ({}) });
|
|
await refresh();
|
|
assert.equal(vm.runInContext('getProfileConnectionState()', context), "current");
|
|
});
|
|
|
|
test("mobile requests cannot overlap and a skipped edit never refreshes the timestamp", async () => {
|
|
const { context, ui, refresh } = profileHarness();
|
|
let resolve;
|
|
context.fetch = () => { ui.requests++; return new Promise(done => { resolve = done; }); };
|
|
const pending = refresh();
|
|
await refresh();
|
|
assert.equal(ui.requests, 1);
|
|
ui.editing = true;
|
|
resolve({ ok: true, json: async () => ({}) });
|
|
await pending;
|
|
assert.equal(vm.runInContext('profileConnection.lastSuccessAt', context), null);
|
|
assert.equal(vm.runInContext('profileConnection.loading', context), false);
|
|
});
|
|
|
|
test("mobile status updates its own nodes without replacing loaded profile", async () => {
|
|
const { context, ui, refresh } = profileHarness();
|
|
const nodes = {
|
|
publicTrainingConnection: { dataset: {} },
|
|
publicTrainingConnectionText: { textContent: "" },
|
|
publicTrainingRetry: {},
|
|
};
|
|
context.document.getElementById = id => nodes[id] || null;
|
|
await refresh();
|
|
assert.equal(nodes.publicTrainingConnection.dataset.state, "current");
|
|
assert.equal(nodes.publicTrainingRetry.hidden, true);
|
|
context.fetch = async () => { throw new Error("offline"); };
|
|
await refresh();
|
|
assert.equal(nodes.publicTrainingConnection.dataset.state, "failed");
|
|
assert.equal(nodes.publicTrainingRetry.hidden, false);
|
|
assert.equal(nodes.publicTrainingRetry.disabled, false);
|
|
assert.equal(ui.html, "existing profile");
|
|
assert.equal(ui.renders, 1);
|
|
});
|
|
|
|
test("a timed-out mobile fetch releases its lock and allows retry", async () => {
|
|
const { context, refresh } = profileHarness();
|
|
let expire;
|
|
context.setTimeout = callback => { expire = callback; return 1; };
|
|
context.clearTimeout = () => {};
|
|
context.fetch = (_, { signal }) => new Promise((resolve, reject) => {
|
|
signal.addEventListener("abort", () => reject(new Error("aborted")));
|
|
});
|
|
const pending = refresh();
|
|
expire();
|
|
await pending;
|
|
assert.equal(vm.runInContext('profileConnection.loading', context), false);
|
|
assert.equal(vm.runInContext('getProfileConnectionState()', context), "failed");
|
|
context.fetch = async () => ({ ok: true, json: async () => ({}) });
|
|
await refresh();
|
|
assert.equal(vm.runInContext('getProfileConnectionState()', context), "current");
|
|
});
|