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"); });