Files
olivv-cs-ComfyUI-FunPack/app/ui/features/generate/hub.test.js
T

212 lines
12 KiB
JavaScript

import test from "node:test";
import assert from "node:assert/strict";
import { setupDom, teardownDom } from "../../composer/tests/_dom.js";
let hub;
test.before(async () => { setupDom(); hub = (await import("./index.js")).default; });
test.after(() => teardownDom());
const rig = (phase, extra = {}) => {
const handlers = {};
const doc = { id: "P", num_frames_per_scene: 97, frame_rate: 25, scenes: [{ id: "a", text: "x" }, { id: "b", gen_unit_id: "a", cut_offset_frames: 49 }] };
const g = { DONE: "done", FAILED: "failed", CANCELLED: "cancelled", run: { state: { phase, images: [{ filename: "a.mp4" }] } },
state() { return this.run.state; }, on: (k, fn) => { handlers[k] = fn; }, subscribe: () => () => {}, cancel() {},
waitForTerminal: () => Object.assign(Promise.resolve("done"), { cancel() {} }), ...extra };
const project = { project: doc, get scenes() { return doc.scenes; }, selected: null,
edit: (fn) => fn(doc), editFor: async (_id, fn) => fn(doc), pref: (k, d) => ((doc.editor_settings || {})[k] ?? d) };
const app = { project, selection: { ids: [] }, lastRun: { n: 0 }, runner: {}, pipeline: { slots: () => [{ id: "s", roles: [{ at: "generation.seed", input: "seed" }], inputs: {} }] }, generate: g, api: { newTasteGeneration: () => Promise.resolve({}) }, on: () => () => {} };
const host = document.createElement("div");
hub.setup({ host, app });
return { handlers, doc, host, app };
};
test("a run that finished before the page reloaded lands on every clip of its unit", async () => {
const { handlers, doc } = rig("done");
handlers.adopt({ sceneId: "a", projectId: "P" });
await new Promise((r) => setTimeout(r, 0));
assert.equal(doc.scene_renders.a.media.filename, "a.mp4");
assert.ok(doc.scene_renders.b.inSec > 0, "the cut half plays from its cut");
});
test("a queue ComfyUI refuses is said in the refusal's own words, not thrown", async () => {
const { host } = rig("idle", { generate: async () => false, run: { state: { phase: "idle", images: [], error: { message: "node 7 refused the graph" } } } });
host.querySelector("button").click(); // ▶ Generate
await new Promise((r) => setTimeout(r, 20));
assert.match(document.body.textContent, /node 7 refused the graph/);
});
test("a batch of takes runs the unit N times, keeps every render as a take, and starts one taste generation for all of them", async () => {
let queued = 0, fresh = 0, n = 0;
const { app, doc } = rig("idle", { generate: async () => { queued += 1; return true; }, waitForTerminal: () => Object.assign(Promise.resolve("done"), { cancel() {} }) });
app.api.newTasteGeneration = () => { fresh += 1; return Promise.resolve({}); };
const g = app.generate;
Object.defineProperty(g.run.state, "images", { get: () => [{ filename: `t${++n}.mp4` }] });
g.run.state.promptId = "p";
await app.runner.units(["a"], 3);
assert.equal(queued, 3);
assert.equal(fresh, 1, "all takes stay pending for a rating");
assert.deepEqual(doc.scene_variants.a.map((t) => t.media.filename), ["t1.mp4", "t2.mp4", "t3.mp4"]);
assert.equal(doc.scene_renders.a.media.filename, "t3.mp4");
});
test("the take that was on the clip keeps its rating when a newer one replaces it; only the last twenty-four are kept", async () => {
const { app, doc } = rig("idle", { generate: async () => true });
doc.scenes[0].rating = "10";
doc.scene_renders = { a: { media: { filename: "old.mp4" }, promptId: "p0" } };
doc.scene_variants = { a: [{ media: { filename: "old.mp4" }, promptId: "p0", rating: "" }] };
app.generate.run.state.promptId = "p1";
await app.runner.units(["a"], 1);
assert.equal(doc.scene_variants.a[0].rating, "10");
doc.scene_variants.a = Array.from({ length: 30 }, (_, i) => ({ media: { filename: `${i}.mp4` }, promptId: `q${i}`, rating: "" }));
await app.runner.units(["a"], 1);
assert.equal(doc.scene_variants.a.length, 24);
});
test("a render from before takes becomes a take; the oldest unrated take goes first when the cap is hit, never a rated one", async () => {
const { app, doc } = rig("idle", { generate: async () => true });
doc.scenes[0].rating = "10";
doc.scene_renders = { a: { media: { filename: "legacy.mp4" } } };
app.generate.run.state.promptId = "p1";
await app.runner.units(["a"], 1);
assert.deepEqual(doc.scene_variants.a.map((t) => [t.media.filename, t.rating]), [["legacy.mp4", "10"], ["a.mp4", ""]]);
doc.scene_variants.a = Array.from({ length: 24 }, (_, i) => ({ media: { filename: `${i}.mp4` }, promptId: `q${i}`, rating: i === 0 ? "10" : "" }));
await app.runner.units(["a"], 1);
assert.equal(doc.scene_variants.a[0].media.filename, "0.mp4", "the rated one stays");
assert.ok(!doc.scene_variants.a.some((t) => t.media.filename === "1.mp4"), "the oldest unrated went");
});
test("a batch with no seed input says so and makes nothing", async () => {
let queued = 0;
const { app } = rig("idle", { generate: async () => { queued += 1; return true; } });
app.pipeline.slots = () => [];
await app.runner.units(["a"], 3);
assert.equal(queued, 0);
assert.match(document.body.textContent, /no seed input/);
});
test("Generate refuses, saying why, while the open project's pipeline is not the live one", async () => {
let queued = 0;
const { host, app } = rig("idle", { generate: async () => { queued += 1; return true; } });
app.pipelineOwned = () => false;
host.querySelector("button").click();
await new Promise((r) => setTimeout(r, 20));
assert.equal(queued, 0);
assert.match(document.body.textContent, /Not started: this project.s pipeline is not loaded yet/);
});
test("a run uses the pipeline as it was at the click, whatever is edited while its shots are made", async () => {
const sent = [];
const { app, doc } = rig("idle", { generate: async (a) => { sent.push(a.slots); app.pipeline.slots = () => [{ id: "s", roles: [], inputs: { steps: 99 } }]; return true; } });
doc.scenes.push({ id: "c", text: "y" });
app.pipeline.slots = () => [{ id: "s", roles: [{ at: "generation.seed", input: "seed" }], inputs: { steps: 20 } }];
await app.runner.units(["a", "c"]);
assert.equal(sent.length, 2);
assert.ok(sent.every((s) => s[0].inputs.steps === 20));
});
test("a scene edited while an earlier shot of the run is making goes as it read at the click; the run's own render still reaches the next shot", async () => {
const seen = [];
let calls = 0;
const { app, doc } = rig("idle", { generate: async () => { calls += 1; if (calls === 1) doc.scenes.find((s) => s.id === "c").text = "edited"; return true; } });
doc.scenes.push({ id: "c", text: "at click" });
app.inputHooks = [async ({ project, scene }) => { seen.push([scene.text, Boolean((project.scene_renders || {}).a)]); return {}; }];
await app.runner.units(["a", "c"]);
assert.deepEqual(seen, [["x", false], ["at click", true]]);
});
test("Generate this scene clicked while a run is going says so, and queues nothing more", async () => {
let queued = 0, finish;
const { app, doc, host } = rig("idle", { generate: async () => { queued += 1; return true; }, waitForTerminal: () => Object.assign(new Promise((r) => { finish = r; }), { cancel() {} }) });
let heard;
app.on = (fn) => { heard = fn; return () => {}; };
host.remove();
const h2 = document.createElement("div");
hub.setup({ host: h2, app });
app.project.selected = doc.scenes[0];
const before = document.body.textContent;
app.runner.units(["a"]);
await new Promise((r) => setTimeout(r, 0));
heard("generate.scene");
await new Promise((r) => setTimeout(r, 0));
assert.equal(queued, 1);
assert.match(document.body.textContent.replace(before, ""), /already going/);
finish("cancelled");
});
test("an edit still on its way when Generate is clicked (the Story box splitting) lands before the run reads the project", async () => {
const seen = [];
const { app, doc } = rig("idle", { generate: async () => true });
app.beforeRun = [async () => { await new Promise((r) => setTimeout(r, 10)); doc.scenes[0].text = "typed last"; }];
app.inputHooks = [({ scene }) => { seen.push(scene.text); return {}; }];
await app.runner.units(["a"]);
assert.deepEqual(seen, ["typed last"]);
});
test("a run that fails while running says which node and ComfyUI's own reason", async () => {
const { app } = rig("idle", { generate: async () => true, waitForTerminal: () => Object.assign(Promise.resolve("failed"), { cancel() {} }) });
app.generate.run.state.error = { node: "SaveVideo", message: "Saving image outside the output folder is not allowed." };
const before = document.body.textContent;
await app.runner.units(["a"]);
assert.match(document.body.textContent.replace(before, ""), /failed in SaveVideo: Saving image outside the output folder/);
});
test("a project that already has a run in ComfyUI's queue (another tab) is not queued again, and the click says why", async () => {
let queued = 0;
const { app } = rig("idle", { generate: async () => { queued += 1; return true; } });
app.api.projectQueued = async (id) => id === "P";
const before = document.body.textContent;
await app.runner.units(["a"]);
assert.equal(queued, 0);
assert.match(document.body.textContent.replace(before, ""), /already has a run in ComfyUI's queue/);
});
test("another tab's run that starts between this run's shots stops this run before its next shot", async () => {
let queued = 0, asks = 0;
const { app, doc } = rig("idle", { generate: async () => { queued += 1; return true; } });
doc.scenes.push({ id: "c", text: "y" });
app.api.projectQueued = async () => ++asks > 1;
await app.runner.units(["a", "c"]);
assert.equal(queued, 1);
});
test("a refusal said while a shot is being queued names the scene", async () => {
const handlers = {};
const { app, doc } = rig("idle", { on: (k, fn) => { handlers[k] = fn; }, generate: async () => { handlers.say("Length is 5000, above the largest 4096 it takes"); return false; } });
doc.scenes.push({ id: "c", text: "y" });
const before = document.body.textContent;
await app.runner.units(["c"]);
assert.match(document.body.textContent.replace(before, ""), /Scene 3: Length is 5000/);
});
test("a scene removed while it was being made: the result is not dropped in silence, it says where the file is", async () => {
const { app, doc } = rig("idle", { generate: async () => { doc.scenes.splice(0, 2); return true; } });
app.project.editFor = async (_id, fn) => fn(doc);
const before = document.body.textContent;
await app.runner.units(["a"]);
assert.match(document.body.textContent.replace(before, ""), /was removed while it was being made.*a\.mp4/);
});
test("a picture no longer in the media bin is dropped from the scene before the run, and said, instead of failing in ComfyUI", async () => {
const { app, doc } = rig("idle", { generate: async () => true });
doc.scenes[0].references = ["deadbeef0000", "aaaaaaaaaaaa"];
doc.scenes[0].source_image = "deadbeef0001";
app.api.media = async () => ({ media: [{ id: "aaaaaaaaaaaa" }] });
await app.runner.units(["a"], 1);
assert.deepEqual(doc.scenes[0].references, ["aaaaaaaaaaaa"]);
assert.equal(doc.scenes[0].source_image, "");
assert.match(document.body.textContent, /no longer in the media bin/);
});
test("with New seed every Generate off, the seed node's own number runs, and the seed used is kept for the Sampler window", async () => {
const sent = [];
const { app, doc } = rig("idle", { generate: async ({ inputs }) => { sent.push(inputs); return true; } });
app.pipeline.slots = () => [{ id: "s", roles: [{ at: "generation.seed", input: "seed" }], inputs: { seed: 42 } }];
doc.editor_settings = { random_seed: false };
await app.runner.units(["a"], 1);
assert.equal((sent[0].s || {}).seed, undefined, "nothing overrides the node's 42");
assert.equal(app.lastRun.seed, 42);
doc.editor_settings = {};
await app.runner.units(["a"], 1);
assert.notEqual(sent[1].s.seed, undefined, "the default draws a fresh one");
});