/** * Reopening the last folder on launch. * * Three cases matter: the folder is remembered when you open one, it comes * back by itself next launch, and a folder that has since moved or been * ejected leaves the launch screen up rather than an error — with the stale * path dropped so it can't fail twice. * * Run: npm i jsdom && node build/test-restore.js */ const fs = require("fs"); const os = require("os"); const path = require("path"); const assert = require("assert"); const { JSDOM, VirtualConsole } = require("jsdom"); const { build } = require("./make-sample.js"); const INDEX = path.join(__dirname, "..", "mac-app", "dist", "index.html"); const STORAGE_KEY = "bwfa_last_folder"; const root = fs.mkdtempSync(path.join(os.tmpdir(), "bwf-restore-")); const card = path.join(root, "Day 14 PR-2"); fs.mkdirSync(card, { recursive: true }); ["A001.wav", "A002.wav"].forEach((name, i) => fs.writeFileSync(path.join(card, name), build({ scene: "1", take: i + 1 }))); const results = []; function check(name, fn) { try { fn(); results.push(["PASS", name]); } catch (e) { results.push(["FAIL", name + " — " + e.message]); } } function waitFor(fn, label, timeout = 10000) { return new Promise((resolve, reject) => { const started = Date.now(); (function poll() { let value; try { value = fn(); } catch (e) { return reject(e); } if (value) return resolve(value); if (Date.now() - started > timeout) return reject(new Error("timed out waiting for " + label)); setTimeout(poll, 30); })(); }); } /** A window, with the Rust commands mirrored and a note of what was asked. */ function launch({ remembered, dialogAnswers = [] } = {}) { const asked = { dialog: 0 }; const dialogQueue = dialogAnswers.slice(); const dom = new JSDOM(fs.readFileSync(INDEX, "utf8"), { runScripts: "dangerously", url: "http://tauri.localhost/", pretendToBeVisual: true, virtualConsole: new VirtualConsole(), beforeParse(win) { win.HTMLCanvasElement.prototype.getContext = () => new Proxy({}, { get: (target, prop) => (prop === "canvas" ? null : () => {}), set: () => true, }); win.URL.createObjectURL = () => "blob:stub"; win.URL.revokeObjectURL = () => {}; if (remembered !== undefined) { win.localStorage.setItem(STORAGE_KEY, remembered); } const toRealm = (buffer) => { const view = new win.Uint8Array(buffer.length); view.set(buffer); return view.buffer; }; win.__TAURI__ = { event: { listen: () => Promise.resolve(() => {}) }, core: { invoke(command, payload) { if (command === "plugin:dialog|open") { asked.dialog++; const next = dialogQueue.shift(); return Promise.resolve(next === undefined ? null : next); } if (command === "bwf_list_dir") { // Rejects for a folder that isn't there, exactly as the // Rust command does. if (!fs.existsSync(payload.path)) { return Promise.reject(new Error(payload.path + ": No such file or directory")); } return Promise.resolve(fs.readdirSync(payload.path).sort().map((name) => ({ name, path: path.join(payload.path, name), kind: fs.statSync(path.join(payload.path, name)).isDirectory() ? "directory" : "file", }))); } if (command === "bwf_stat") { const stat = fs.statSync(payload.path); return Promise.resolve({ path: payload.path, relativePath: path.basename(payload.path), name: path.basename(payload.path), size: stat.size, lastModified: Math.floor(stat.mtimeMs), }); } if (command === "bwf_read_range") { const size = fs.statSync(payload.path).size; if (payload.offset >= size || !payload.length) return Promise.resolve(toRealm(Buffer.alloc(0))); const take = Math.min(payload.length, size - payload.offset); const fd = fs.openSync(payload.path, "r"); const buffer = Buffer.alloc(take); fs.readSync(fd, buffer, 0, take, payload.offset); fs.closeSync(fd); return Promise.resolve(toRealm(buffer)); } if (command === "bwf_read_all") { return Promise.resolve(toRealm(fs.readFileSync(payload.path))); } return Promise.reject(new Error("unexpected command " + command)); }, }, }; }, }); return new Promise((resolve) => { dom.window.addEventListener("load", () => resolve({ dom, window: dom.window, asked })); }); } const rowsIn = (doc) => Array.from(doc.querySelectorAll("[data-bwfa-table-body] tr")); (async () => { /* --- opening a folder is remembered --- */ const first = await launch({ dialogAnswers: [card] }); first.window.document.querySelector("[data-bwfa-edit-folder]") .dispatchEvent(new first.window.MouseEvent("click", { bubbles: true })); await waitFor(() => rowsIn(first.window.document).length === 2, "first open"); check("opening a folder remembers it", () => assert.strictEqual(first.window.localStorage.getItem(STORAGE_KEY), card, "stored: " + first.window.localStorage.getItem(STORAGE_KEY))); first.window.close(); /* --- next launch reopens it, with no dialog --- */ const again = await launch({ remembered: card }); await waitFor(() => rowsIn(again.window.document).length === 2, "reopen on launch"); const doc = again.window.document; check("the folder comes back on the next launch", () => { assert.strictEqual(rowsIn(doc).length, 2, "got " + rowsIn(doc).length + " rows"); assert.strictEqual(again.asked.dialog, 0, "it opened a file dialog instead of restoring"); assert.strictEqual(doc.querySelector("[data-bwfa-current-folder]").textContent, "Day 14 PR-2"); assert(doc.querySelector("[data-bwfa-app]").classList.contains("has-folder"), "still showing the launch screen"); }); check("it comes back read-write, not read-only", () => assert.strictEqual(doc.querySelector("[data-bwfa-editing-note]").hidden, false, "restored without edit mode, so saving would be impossible")); again.window.close(); /* --- a folder that has gone leaves the launch screen up --- */ const missing = path.join(root, "Ejected Card"); const gone = await launch({ remembered: missing }); // Nothing to wait for, so give the check a beat to have happened. await new Promise((r) => setTimeout(r, 300)); const goneDoc = gone.window.document; check("a folder that isn't there leaves the launch screen alone", () => { assert.strictEqual(rowsIn(goneDoc).length, 0, "it loaded something"); assert.strictEqual(goneDoc.querySelector("[data-bwfa-app]").classList.contains("has-folder"), false, "the app left its launch state for a folder that doesn't exist"); assert.strictEqual(gone.asked.dialog, 0, "it prompted on launch, which is not the ask"); assert.strictEqual(goneDoc.querySelector("[data-bwfa-status]").textContent.trim(), "", "an error was left on screen: " + goneDoc.querySelector("[data-bwfa-status]").textContent); }); check("the stale path is dropped so it can't fail twice", () => assert.strictEqual(gone.window.localStorage.getItem(STORAGE_KEY), null, "still remembered: " + gone.window.localStorage.getItem(STORAGE_KEY))); gone.window.close(); /* --- eject puts the app back to a first run --- */ const open = await launch({ dialogAnswers: [card] }); const openDoc = open.window.document; openDoc.querySelector("[data-bwfa-edit-folder]") .dispatchEvent(new open.window.MouseEvent("click", { bubbles: true })); await waitFor(() => rowsIn(openDoc).length === 2, "open before reset"); check("eject only offers itself once a folder is open", () => { const reset = openDoc.querySelector("[data-bwfa-reset]"); assert(reset, "no reset control in the markup"); assert.notStrictEqual(open.window.getComputedStyle(reset).display, "none", "reset is hidden while a folder is open"); }); openDoc.querySelector("[data-bwfa-reset]") .dispatchEvent(new open.window.MouseEvent("click", { bubbles: true })); await waitFor(() => rowsIn(openDoc).length === 0, "reset to clear the table", 5000); check("eject returns the app to its empty state", () => { const app = openDoc.querySelector("[data-bwfa-app]"); assert.strictEqual(app.classList.contains("has-folder"), false, "still in folder mode"); assert.strictEqual(openDoc.querySelector("[data-bwfa-results]").hidden, true, "the results panel survived"); assert.strictEqual(openDoc.querySelector("[data-bwfa-player]").hidden, true, "the player survived"); assert.strictEqual(openDoc.querySelector("[data-bwfa-current-folder]").hidden, true, "the folder name is still shown"); assert(/open folder/i.test(openDoc.querySelector("[data-bwfa-edit-folder]").textContent), "the button still says Change Folder: " + openDoc.querySelector("[data-bwfa-edit-folder]").textContent); assert.strictEqual(openDoc.querySelector("[data-bwfa-status]").textContent.trim(), "", "a status message was left behind"); assert.strictEqual(open.window.getComputedStyle( openDoc.querySelector("[data-bwfa-reset]")).display, "none", "reset is still offered with nothing open"); }); check("eject also forgets what to reopen", () => assert.strictEqual(open.window.localStorage.getItem(STORAGE_KEY), null, "next launch would reopen " + open.window.localStorage.getItem(STORAGE_KEY))); open.window.close(); console.log(""); results.forEach(([s, n]) => console.log((s === "PASS" ? " ok " : " FAIL") + " " + n)); const failed = results.filter(([s]) => s === "FAIL").length; console.log("\n" + (results.length - failed) + "/" + results.length + " checks passed"); fs.rmSync(root, { recursive: true, force: true }); process.exit(failed ? 1 : 0); })().catch((e) => { console.error("harness error:", e); process.exit(1); });