130 lines
4.3 KiB
JavaScript
130 lines
4.3 KiB
JavaScript
// 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");
|