Files
olivv-cs-ComfyUI-FunPack/app/shell/tests/layout.test.js
T
digital-garbageandClaude Sonnet 5 fd292259e7 core, app: media import -- a library separate from the run-output bin
Files the user brings IN (a reference image, an imported clip) now have a
real store: core/media.py, /api/media routes, and a "Media" tab beside the
bin's "Results" tab in the Assets zone. Upload, list, fetch, delete all
round-trip through the real server; nothing wires into a pipeline input yet,
since no model module or Studio conditioning takes a reference image today.

Two adversarial review rounds found and fixed three real issues along the
way: MAX_BYTES (512MB) was dead code because aiohttp's own client_max_size
capped every upload far lower first in both prod and dev, with a JSON-parse
error leaking to the user on rejection instead of a clean message; the
index.json read-modify-write had no guard against concurrent writers; and
the devserver's bare Application didn't match ComfyUI's own 100MB default,
which also broke an unrelated e2e assertion once the review polluted the
real, unisolated media/ directory testing the fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-07 16:58:18 +03:00

285 lines
12 KiB
JavaScript

// The regions, and the one wire between two of them.
//
// build() is mostly composition, which tests badly and breaks rarely. The
// exception is where two regions are joined: the bin decides what the viewer
// shows, and nothing else in either file says so. Tested here rather than in
// bin.js, because a bin with a fake viewer proves the fake was wired, not the
// app.
import test from "node:test";
import assert from "node:assert/strict";
import { setupDom, teardownDom, fire } from "../../composer/tests/_dom.js";
let build, resetMounts;
test.before(async () => {
setupDom();
await import("../../composer/composer.js");
({ build } = await import("../layout.js"));
({ _reset: resetMounts } = await import("../mounts.js"));
});
test.after(() => teardownDom());
const file = (filename) => ({ filename, subfolder: "", type: "output" });
function page() {
resetMounts();
const root = document.createElement("div");
document.body.replaceChildren(root);
return build(root);
}
test("a result taken into the bin is what the viewer shows", () => {
const p = page();
assert.match(p.viewer.node.textContent, /result of the last run/);
p.bin.absorb([file("a.png")]);
const img = p.viewer.node.querySelector("img");
assert.ok(img, "the viewer did not show the result");
assert.equal(img.getAttribute("src"), "/view?filename=a.png&subfolder=&type=output");
});
test("the Media tab is not fetched until it is opened", () => {
let asked = false;
globalThis.fetch = async () => { asked = true; return { ok: true, json: async () => ({ media: [] }) }; };
const p = page();
assert.equal(asked, false, "the media library fetched before its tab was opened");
assert.equal(p.assets.node.contains(p.bin.host.node), true, "Results is not the default tab");
assert.equal(p.assets.node.contains(p.mediaLibrary.host.node), false);
});
test("opening the Media tab shows the library and fetches it, once", async () => {
let calls = 0;
globalThis.fetch = async () => { calls += 1; return { ok: true, json: async () => ({ media: [] }) }; };
const p = page();
const [resultsBtn, mediaBtn] = p.assetsTabs.node.querySelectorAll(".cx-tab");
fire(mediaBtn, "click");
await new Promise((r) => setTimeout(r, 0));
assert.equal(p.assets.node.contains(p.mediaLibrary.host.node), true, "switching tabs did not swap the body");
assert.equal(p.assets.node.contains(p.bin.host.node), false);
assert.equal(calls, 1);
fire(resultsBtn, "click");
fire(mediaBtn, "click");
await new Promise((r) => setTimeout(r, 0));
assert.equal(calls, 1, "returning to Media re-fetched instead of keeping what it already had");
});
test("a video result reaches the viewer as a video", () => {
const p = page();
p.bin.absorb([file("clip.mp4")]);
assert.ok(p.viewer.node.querySelector("video"), "a video was handed to an <img>");
});
test("picking an older result in the bin changes what the viewer shows", () => {
const p = page();
p.bin.absorb([file("first.png")]);
p.bin.absorb([file("second.png")]);
assert.match(p.viewer.node.querySelector("img").getAttribute("src"), /second\.png/);
const rows = [...p.assets.node.querySelectorAll(".cx-cell, .cx-media-row")];
fire(rows[rows.length - 1], "click"); // the oldest is at the end
assert.match(p.viewer.node.querySelector("img").getAttribute("src"), /first\.png/);
});
test("what a run produced can be saved to the bin from wherever it is shown", () => {
// Opened from the temp browser, or anywhere else that hands the viewer a
// real file identity -- "keep this" should not mean a download and a
// re-upload by hand.
const p = page();
p.viewer.setSource("/view?filename=stray.png&subfolder=&type=temp", "image",
{ filename: "stray.png", subfolder: "", type: "temp" });
const saveToBin = p.preview.node.querySelector('button[aria-label="Save to bin"]');
assert.ok(saveToBin, "no Save to bin action in the Preview head");
saveToBin.click();
assert.equal(p.bin.items.length, 1);
assert.equal(p.bin.items[0].file.filename, "stray.png");
});
test("saving something already in the bin says so instead of duplicating it", () => {
const p = page();
p.bin.absorb([file("a.png")]); // also becomes what the viewer shows
const saveToBin = p.preview.node.querySelector('button[aria-label="Save to bin"]');
saveToBin.click();
assert.equal(p.bin.items.length, 1, "the same file was saved twice");
});
test("saving with nothing shown does not throw, and says why", () => {
const p = page();
const saveToBin = p.preview.node.querySelector('button[aria-label="Save to bin"]');
assert.doesNotThrow(() => saveToBin.click());
assert.equal(p.bin.items.length, 0);
});
test("saving a frame with no video playing says so rather than uploading nothing", async () => {
// jsdom has no real video decoder, so a genuine capture can only be proven
// in a real browser (see the Playwright spec) -- this is the guard that
// keeps a silent no-op from reaching the upload at all.
const p = page();
const saveFrame = p.preview.node.querySelector('button[aria-label="Save this frame"]');
assert.ok(saveFrame, "no Save frame action in the Preview head");
let asked = false;
globalThis.fetch = async () => { asked = true; return { ok: true, json: async () => ({}) }; };
saveFrame.click();
await new Promise((r) => setTimeout(r, 0)); // let the click's own async handler settle
assert.equal(asked, false, "uploaded with no frame to upload");
assert.equal(p.bin.items.length, 0);
});
test("a frame that dedupes to something already in the bin says so, not that it saved", async () => {
// saveToBin already checked bin.absorb()'s own dedup result; saveFrame did
// not, so a second capture that happened to upload to the same identity
// (two rapid clicks, a server that overwrites rather than renames on
// collision) claimed success while adding nothing.
const p = page();
p.viewer.captureFrame = async () => new Blob(["x"], { type: "image/png" });
globalThis.fetch = async () => ({
ok: true, json: async () => ({ name: "captured.png", subfolder: "", type: "input" }),
});
const saveFrame = p.preview.node.querySelector('button[aria-label="Save this frame"]');
saveFrame.click();
await new Promise((r) => setTimeout(r, 0));
assert.equal(p.bin.items.length, 1);
assert.match(p.preview.node.querySelector(".cx-panel-status").textContent, /Frame saved/);
saveFrame.click(); // resolves to the exact same identity again
await new Promise((r) => setTimeout(r, 0));
assert.equal(p.bin.items.length, 1, "the same capture was added twice");
assert.match(p.preview.node.querySelector(".cx-panel-status").textContent, /Already in the bin/);
});
test("a frame upload that resolves after the user moved on does not yank the viewer back", async () => {
// The upload is a real round trip -- by the time it lands the user may
// already be looking at something else. A save must add to the bin without
// moving what is on screen, whenever it finishes.
const p = page();
p.bin.absorb([file("first.png")]); // the viewer shows this
// jsdom cannot decode video; stand in for a real captured frame (the real
// capture itself is proven in the Playwright spec, against a real video).
p.viewer.captureFrame = async () => new Blob(["x"], { type: "image/png" });
let resolveUpload;
globalThis.fetch = () => new Promise((resolve) => { resolveUpload = resolve; });
p.preview.node.querySelector('button[aria-label="Save this frame"]').click();
// Let captureFrame()'s own await, then the call into fetch(), actually
// happen -- the upload is now in flight, waiting on resolveUpload.
await Promise.resolve(); await Promise.resolve();
assert.equal(typeof resolveUpload, "function", "the upload was never started");
// The user moves on before the upload resolves.
p.bin.absorb([file("second.png")]);
assert.match(p.viewer.node.querySelector("img").getAttribute("src"), /second\.png/);
resolveUpload({ ok: true, json: async () => ({ name: "captured.png", subfolder: "", type: "input" }) });
await Promise.resolve(); await Promise.resolve(); await Promise.resolve();
assert.equal(p.bin.items.length, 3, "the captured frame was never saved");
assert.match(p.viewer.node.querySelector("img").getAttribute("src"), /second\.png/,
"a save that finished late pulled the viewer back to what it saved");
});
test("a result that failed to load is not something Save to bin can still save", () => {
// The viewer's own error state says "nothing is showing here" -- `file`
// disagreeing with that let a dead reference be saved as if it were real.
const p = page();
p.bin.absorb([file("gone.png")]);
p.viewer.node.querySelector("img").dispatchEvent(new window.Event("error"));
assert.match(p.viewer.node.textContent, /could not be loaded/);
assert.equal(p.viewer.file, null, "the viewer still claims a file is showing after it failed to load");
const saveToBin = p.preview.node.querySelector('button[aria-label="Save to bin"]');
saveToBin.click();
assert.equal(p.bin.items.length, 1, "the failed file was saved again, over the guard");
});
test("a control sits in the head of the zone it acts on", () => {
// The arrangement that makes this an editor rather than a dashboard: the bin's
// view control is in the Assets head, and Generate is in the head of the zone
// the run fills -- not in one bar at the bottom acting on whatever is in front.
const p = page();
assert.ok(p.assets.node.contains(p.bin.control.node), "the view control left the Assets head");
assert.ok(p.timeline.node.contains(p.transport.generate.node), "Generate is not in the Timeline head");
assert.equal(p.assets.node.contains(p.transport.generate.node), false);
assert.equal(p.page.footer, null, "there is still a bar across the bottom");
});
test("what a run is doing is said in the same head that starts it", () => {
const p = page();
const status = p.timeline.node.querySelector(".cx-panel-status");
assert.ok(status, "the Timeline head says nothing about the run");
assert.ok(status.contains(p.transport.statusText.node));
assert.ok(status.contains(p.transport.progress.node));
});
test("a warning about the next run sits inside the zone that starts it", () => {
// Under the button it is about, not in the head beside it: it is a sentence
// rather than a chip, and a 34px band is not where a sentence goes.
const p = page();
assert.ok(p.timeline.body.contains(p.transport.warning.node));
});
test("the assets mount point still exists for a module", () => {
const p = page();
p.bin.absorb([file("a.png")]);
assert.ok(p.assets.body.contains(p.bin.host.node),
"the bin is not in the panel body a module also mounts into");
});
test("the prompt is written in a window, not in a zone", () => {
// A permanent third of the centre column for the thing that is written in
// bursts and read rarely, taken from the timeline, which is what the project
// IS. It opens, is written in, and closes.
const p = page();
assert.equal(p.constructor.isOpen, false, "the Constructor is open before anyone asked");
assert.equal(document.querySelector(".cx-modal"), null);
p.constructor.open();
assert.equal(p.constructor.isOpen, true);
const modal = document.querySelector(".cx-modal");
assert.ok(modal, "the Constructor did not open");
assert.ok(modal.contains(p.constructor.host.node), "the prompt is not in the window");
assert.match(modal.textContent, /Constructor/);
p.constructor.close();
assert.equal(document.querySelector(".cx-modal"), null);
});
test("what was typed survives the window being closed", () => {
// The host outlives every opening. A window that rebuilt its contents each
// time would throw away whatever was written and not saved -- which for a
// prompt is the whole of it.
const p = page();
const box = document.createElement("textarea");
p.constructor.host.node.appendChild(box);
box.value = "a cat on a rooftop";
p.constructor.open();
p.constructor.close();
p.constructor.open();
const found = document.querySelector(".cx-modal textarea");
assert.equal(found, box, "the window built itself a new prompt box");
assert.equal(found.value, "a cat on a rooftop");
p.constructor.close();
});
test("the timeline is where a run is started from", () => {
const p = page();
assert.match(p.timeline.node.textContent, /Nothing on the timeline/);
assert.ok(p.timeline.node.querySelector(".cx-panel-actions"));
assert.match(p.timeline.node.querySelector(".cx-panel-actions").textContent, /Constructor/);
});