// Page-side helpers of the web app (js/host.js) under Node with a fake DOM, no browser or Wasm // build needed: // // node apps/effectcraft-web/tests/page.mjs // // - Add Files… / Upload Folder…: a picked file that can't be read is listed with its error // instead of failing the whole pick silently, and cancelling picks nothing (#226). // - A lost WebGPU device: the page says so over the canvas and offers Save Project and Reload; // the reload writes pending changes to browser storage first, and a failed write is reported // instead of reloading (#225). // Exits non-zero on failure. import assert from "node:assert/strict"; import { dirname, join } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; const here = dirname(fileURLToPath(import.meta.url)); const tick = () => new Promise((r) => setTimeout(r, 0)); globalThis.self = globalThis; class FakeElement { constructor(tag) { this.tag = tag; this.children = []; this.attributes = {}; this.textContent = ""; } setAttribute(k, v) { this.attributes[k] = v; } append(...c) { this.children.push(...c); } click() { this.onclick?.(); } *all() { yield this; for (const c of this.children) yield* c.all(); } } let picker; globalThis.document = { body: new FakeElement("body"), createElement(tag) { const el = new FakeElement(tag); if (tag === "input") { picker = el; el.click = () => {}; } return el; }, getElementById(id) { return [...this.body.all()].find((e) => e.id === id) ?? null; }, }; let reloads = 0; globalThis.location = { reload: () => reloads++ }; const host = await import(pathToFileURL(join(here, "../js/host.js"))); // #226: one readable and one unreadable file. const file = (name, read) => ({ name, webkitRelativePath: "", arrayBuffer: read }); const picked = host.browsePickFiles(false); picker.files = [ file("ok.png", async () => new Uint8Array([1, 2, 3]).buffer), file("red-corner.png", async () => { throw new DOMException("Generated unreadable selected file", "NotReadableError"); }), ]; await picker.onchange(); const out = await picked; assert.equal(out.length, 2); assert.deepEqual([out[0].path, [...out[0].bytes]], ["ok.png", [1, 2, 3]]); assert.deepEqual(out[1], { path: "red-corner.png", error: "Generated unreadable selected file" }); const cancelled = host.browsePickFiles(false); picker.oncancel(); assert.deepEqual(await cancelled, [], "cancelling picks nothing"); // #225: the device-lost notice. const calls = []; let flushFails = false; self.effectcraft = { flush: async () => { calls.push("flush"); if (flushFails) throw new Error("quota exceeded"); }, execute: async (command, params) => { calls.push([command, params]); if (command === "file.save") throw new Error("no path: use file.saveAs {path}"); }, }; const notice = () => document.getElementById("device-lost"); const buttons = () => [...notice().all()].filter((e) => e.tag === "button"); const text = () => [...notice().all()].find((e) => e.tag === "p").textContent; host.showDeviceLost("GPU device lost (Destroyed): destroyed", true); host.showDeviceLost("again", true); assert.equal(document.body.children.length, 1, "one notice"); assert.equal([...notice().all()].find((e) => e.tag === "small").textContent, "GPU device lost (Destroyed): destroyed"); assert.match(text(), /Reload to continue/); assert.deepEqual( buttons().map((b) => b.textContent), ["Save Project", "Reload"], ); // Save Project: Save, or Save As for an untitled project (a download). await buttons()[0].onclick(); assert.deepEqual(calls.splice(0), [ ["file.save", {}], ["file.saveAs", { path: "/Untitled Project.ecproj" }], ]); // Reload writes pending changes first. await buttons()[1].onclick(); assert.deepEqual(calls.splice(0), ["flush"]); assert.equal(reloads, 1); // A failed write is reported instead of reloading; Reload Anyway reloads. document.body.children = []; flushFails = true; host.showDeviceLost("GPU device lost (Unknown): gone", false); assert.match(text(), /isn't kept in browser storage/); await buttons()[1].onclick(); await tick(); assert.equal(reloads, 1, "no reload after a failed write"); assert.match(text(), /quota exceeded/); assert.equal(buttons()[1].textContent, "Reload Anyway"); buttons()[1].onclick(); assert.equal(reloads, 2); console.log("page: ok");