Something went wrong. Try again.
Web frontend and supporting services for lance.blue
Something went wrong. Try again.
3.7 kB · 91 lines
JavaScript
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192/** * Importing the same picture twice has to work. * * A file input fires `change` only when the picker hands back something * different from what it is already holding. Leave the last file in it and the * second pick of that file is not a change: no event, no import, and nothing on * screen to say why. The path is ordinary — import a picture, roll a new camo * over it, want the picture back — and it failed in the way that is hardest to * report, by doing nothing at all. * * Emptying the input is the whole fix, and it has to happen on every path * through the handler, the failure one included: someone who picks a file that * will not decode, repairs it and picks it again is in exactly the same trap. * * Structural, because there is nothing to catch at runtime. The handler is * correct, the browser is behaving as specified, and the bug is an event that * never arrives. * * Run with `npm test`. */import { test } from "node:test";import assert from "node:assert/strict";import { readFile } from "node:fs/promises";import { fileURLToPath } from "node:url";
const src = fileURLToPath(new URL("../src/", import.meta.url));const editor = await readFile(`${src}camo/editor.ts`, "utf8");
/** Every file input the app builds. There is one; the rule is for all of them. */function fileInputs(source) { return [ ...source.matchAll(/const (\w+) = el\("input", \{\s*\n\s*type: "file"/g), ].map((m) => m[1]);}
/** The body of `<name>.addEventListener("change", () => { ... })`. */function changeHandler(source, name) { const opener = `${name}.addEventListener("change", () => {`; const start = source.indexOf(opener); assert.notEqual(start, -1, `${name} has no change handler`); // The handler closes at the indentation it opened on, which is two spaces // here; every block inside it closes deeper. const end = source.indexOf("\n });", start); assert.notEqual(end, -1, `${name}'s change handler has no end`); return source.slice(start, end);}
test("the camo file input is emptied once its file is in hand", () => { const inputs = fileInputs(editor); assert.deepEqual( inputs, ["fileInput"], "the file input moved, was renamed, or gained a sibling this test does not know about", );
for (const name of inputs) { const handler = changeHandler(editor, name); assert.match( handler, new RegExp(`${name}\\.value = ""`), `${name} keeps its last file, so picking that same file again fires nothing`, ); }});
test("it is emptied in the handler itself, not in a callback", () => { const handler = changeHandler(editor, "fileInput");
// Nesting, read off the indentation prettier enforces. The handler's own // body sits at four spaces; anything inside img.onload or img.onerror sits // at six or deeper. Textual order cannot tell the two apart, because // img.onload is written above the img.src that runs it — a clear inside the // success callback still reads as "before the decode" and would leave the // failure path holding the file. assert.match( handler, /\n {4}fileInput\.value = "";/, 'fileInput.value = "" is nested inside a callback, so only some outcomes ' + "clear it — a file that failed to decode stays in the input and a " + "repaired copy of it will not reimport", );
// And the File has to be taken before the input is emptied, because emptying // it drops the FileList. const taken = handler.indexOf("fileInput.files?.[0]"); const cleared = handler.indexOf('fileInput.value = ""'); assert.ok( taken !== -1 && taken < cleared, "the file is read after the input is emptied, which reads nothing", );});