Files
olivv-cs-ComfyUI-FunPack/app/shell/tests/ui.test.js
T
digital-garbage 2e384c48b2 core, app: shortcuts, autocomplete, and prompt-craft expansion
A trigger->replacement library (core/shortcuts.py, a close port of v4's
apply_prompt_shortcuts/resolve_variables), a Settings section to manage it,
autocomplete on the real prompt textarea (composer.autocomplete.default, so
it outranks the Constructor modal it lives inside), and a stateless
/api/prompt/expand that combines anchor + shortcuts + $variables + postfix
at the moment a run is queued -- scene.text itself stays exactly what was
typed. Project gained anchor/postfix/variables editing UI on its own tab.

Along the way: a real layout bug in the Properties column (every composer
stack can flex-shrink below its own content, and once the Project tab grew
past a couple of rows the column started squeezing its own children's
content into each other and, at the outer level, into the sibling Generation
section below) -- fixed with flex-shrink:0 at each level that needs it,
caught by a Playwright regression test.
2026-09-08 11:49:49 +03:00

241 lines
9.4 KiB
JavaScript

// The parts added to make v5 an app rather than a generate button: the action
// registry and the wheel that shows it, the inspector, and the way in.
//
// One file, because each is small and they are all the same shape: something
// announces, something else draws whatever announced.
import test from "node:test";
import assert from "node:assert/strict";
import { setupDom, teardownDom, fire } from "../../composer/tests/_dom.js";
let actions, createWheel, createInspector, openWizard;
test.before(async () => {
setupDom();
await import("../../composer/composer.js");
actions = await import("../actions.js");
({ createWheel } = await import("../wheel.js"));
({ createInspector } = await import("../inspector.js"));
({ open: openWizard } = await import("../wizard.js"));
});
test.after(() => teardownDom());
// --- actions ----------------------------------------------------------------
test("an action is offered by whoever owns it, and taken away with it", () => {
actions._reset();
const ran = [];
const drop = actions.offerAction({ id: "go", label: "Go", run: () => ran.push("go") });
assert.deepEqual(actions.offered().map((a) => a.id), ["go"]);
assert.equal(actions.run("go"), true);
assert.deepEqual(ran, ["go"]);
drop();
assert.deepEqual(actions.offered(), []);
// Not an error: whatever offered it may simply be gone.
assert.equal(actions.run("go"), false);
});
test("one broken action does not take down the list that showed it", () => {
actions._reset();
actions.offerAction({ id: "bad", run: () => { throw new Error("nope"); } });
actions.offerAction({ id: "good", run: () => {} });
assert.equal(actions.run("bad"), false);
assert.equal(actions.run("good"), true, "a neighbour's failure stopped this one");
});
test("offering the same id twice replaces it", () => {
// Two entries that look identical and do different things is worse than one.
actions._reset();
const ran = [];
actions.offerAction({ id: "go", label: "First", run: () => ran.push(1) });
actions.offerAction({ id: "go", label: "Second", run: () => ran.push(2) });
assert.equal(actions.offered().length, 1);
assert.equal(actions.offered()[0].label, "Second");
actions.run("go");
assert.deepEqual(ran, [2]);
});
// --- the wheel --------------------------------------------------------------
const middle = (node, init = {}) => node.dispatchEvent(
new window.MouseEvent("mousedown", { button: 1, bubbles: true, cancelable: true, ...init }));
test("the middle button opens the wheel on whatever is offered", () => {
actions._reset();
actions.offerAction({ id: "generate", label: "Generate", run: () => {} });
actions.offerAction({ id: "cancel", label: "Cancel", run: () => {} });
const wheel = createWheel();
middle(document.body);
assert.equal(wheel.isOpen, true);
assert.match(document.body.textContent, /Generate/);
wheel.close();
assert.equal(wheel.isOpen, false);
wheel.destroy();
});
test("a wheel of one is not a wheel", () => {
// Every action comes from a part of the app that loaded, so "only one" is a
// real state -- and there is nothing to aim between. The kit refuses it; no
// wheel is a better answer than an error thrown under the pointer.
actions._reset();
const wheel = createWheel();
middle(document.body);
assert.equal(wheel.isOpen, false, "a wheel opened with nothing in it");
actions.offerAction({ id: "a", run: () => {} });
middle(document.body);
assert.equal(wheel.isOpen, false, "a wheel opened with one item in it");
wheel.destroy();
});
test("the middle button's own behaviour is prevented", () => {
// Otherwise Chrome starts autoscroll under the wheel.
actions._reset();
actions.offerAction({ id: "a", run: () => {} });
actions.offerAction({ id: "b", run: () => {} });
const wheel = createWheel();
const event = new window.MouseEvent("mousedown", { button: 1, bubbles: true, cancelable: true });
document.body.dispatchEvent(event);
assert.equal(event.defaultPrevented, true);
wheel.destroy();
});
test("a wheel that is already open is not opened again", () => {
actions._reset();
actions.offerAction({ id: "a", run: () => {} });
actions.offerAction({ id: "b", run: () => {} });
const wheel = createWheel();
middle(document.body);
const first = document.querySelectorAll(".cx-wheel").length;
middle(document.body);
assert.equal(document.querySelectorAll(".cx-wheel").length, first);
wheel.destroy();
});
// --- the inspector ----------------------------------------------------------
function fakeProject(over = {}) {
const scene = { id: "aaaaaaaaaaaa", text: "", result: null, length: null, rating: null };
const written = [];
return {
written,
project: { id: "p", name: "Untitled" },
scenes: [scene],
selectedId: scene.id,
selected: scene,
video: { length: 97 },
anchor: "", postfix: "", postfixEnabled: true, variables: [],
setScene: (id, key, value) => { written.push([id, key, value]); scene[key] = value; },
rename: (name) => written.push(["project", "name", name]),
setAnchor: (v) => written.push(["project", "anchor", v]),
setPostfix: (v) => written.push(["project", "postfix", v]),
setPostfixEnabled: (v) => written.push(["project", "postfix_enabled", v]),
setVariables: (v) => written.push(["project", "variables", v]),
...over,
};
}
test("the scene tab edits the scene, and says the crop is not what regenerates", () => {
const project = fakeProject();
const inspector = createInspector({ project });
document.body.replaceChildren(inspector.node);
assert.match(inspector.node.textContent, /Regenerating uses the project's length/);
const length = inspector.node.querySelector('input[type="number"]');
assert.equal(length.value, "97", "a scene with no crop of its own shows the project's length");
length.value = "48";
fire(length, "blur");
assert.deepEqual(project.written.at(-1), [project.selected.id, "length", 48]);
});
test("a rating is kept with the scene, and can be taken back", () => {
// Binary, because the sign is all anything downstream reads. And reversible:
// without a way back a mis-click is a permanent opinion, and "no opinion" is
// a real answer nothing should have to guess at.
const project = fakeProject();
const inspector = createInspector({ project });
document.body.replaceChildren(inspector.node);
const press = (label) => fire([...inspector.node.querySelectorAll("button")]
.find((b) => b.textContent === label), "click");
press("Liked");
assert.deepEqual(project.written.at(-1), [project.selected.id, "rating", "liked"]);
press("Disliked");
assert.deepEqual(project.written.at(-1), [project.selected.id, "rating", "disliked"]);
press("Disliked");
assert.deepEqual(project.written.at(-1), [project.selected.id, "rating", null]);
});
test("the project tab renames the project", () => {
const project = fakeProject();
const renamed = [];
const inspector = createInspector({ project, onRename: (n) => renamed.push(n) });
document.body.replaceChildren(inspector.node);
inspector.show("project");
const name = inspector.node.querySelector('input[type="text"]');
assert.equal(name.value, "Untitled");
name.value = "Rooftops";
fire(name, "blur");
assert.deepEqual(renamed, ["Rooftops"]);
});
test("whatever mounted at project.negative shows only on the Project tab", () => {
// The host has to survive a redraw (a scene selection, a rating) without
// losing whatever a module appended into it -- it lives OUTSIDE the part
// of the tree draw() tears down and rebuilds.
const inspector = createInspector({ project: fakeProject() });
document.body.replaceChildren(inspector.node);
const marker = document.createElement("div");
marker.textContent = "Negative prompt field";
inspector.negativeHost.node.appendChild(marker);
assert.equal(inspector.negativeHost.node.hidden, true, "showing on the Scene tab");
inspector.show("project");
assert.equal(inspector.negativeHost.node.hidden, false);
assert.ok(inspector.node.contains(marker), "the field did not survive the tab switch");
inspector.show("scene");
assert.equal(inspector.negativeHost.node.hidden, true);
assert.ok(inspector.node.contains(marker), "the field did not survive switching away and back");
});
test("with no scene selected the inspector says so instead of drawing nothing", () => {
const inspector = createInspector({ project: fakeProject({ selected: null, scenes: [] }) });
document.body.replaceChildren(inspector.node);
assert.match(inspector.node.textContent, /No scene/);
});
// --- the way in -------------------------------------------------------------
test("the wizard can be left, and leaving is an answer", () => {
// v4's could not be dismissed: a page refresh was the only way out, which
// makes it a trap rather than a wizard.
const picked = [];
const window_ = openWizard({ onPick: (id) => picked.push(id) });
assert.ok(document.querySelector(".cx-modal"));
window_.close("dismissed");
assert.deepEqual(picked, [null], "dismissing said nothing at all");
assert.equal(document.querySelector(".cx-modal"), null);
});
test("a choice is answered once, however it was closed", () => {
const picked = [];
const window_ = openWizard({ onPick: (id) => picked.push(id) });
const card = [...document.querySelectorAll(".cx-card")]
.find((c) => /A few scenes/.test(c.textContent));
fire(card, "click");
assert.deepEqual(picked, ["scenes"]);
window_.close("again");
assert.deepEqual(picked, ["scenes"], "closing after a choice answered a second time");
});