Files
olivv-cs-ComfyUI-FunPack/app/shell/tests/shortcuts.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

113 lines
4.5 KiB
JavaScript

// The shortcut library UI, over a mocked /funpack/api/shortcuts.
import test from "node:test";
import assert from "node:assert/strict";
import { setupDom, teardownDom } from "../../composer/tests/_dom.js";
let mount;
test.before(async () => {
setupDom();
await import("../../composer/composer.js");
({ mount } = await import("../shortcuts.js"));
});
test.after(() => teardownDom());
const sc = (name, over = {}) => ({ name, triggers: [name.toLowerCase()],
replacements: [`replacement for ${name}`], enabled: true, category: "", sub_category: "", ...over });
function mockFetch(handlers) {
globalThis.fetch = async (url, opts) => {
const method = (opts && opts.method) || "GET";
const key = `${method} ${new URL(url, "http://localhost/").pathname}`;
const h = handlers[key] || handlers[method + " *"];
if (!h) throw new Error(`unhandled: ${key}`);
return h(opts);
};
}
/** setFooter's actions are handed to whoever hosts this (the modal chrome
* in production) rather than living inside body.node -- a footer that
* did nothing with them would make "+ Add shortcut" and "Close" untestable
* and unclickable, so this stands in for that host: a footer strip actually
* appended beside the body, exactly like packs.js's own open() does. */
function open(opts = {}) {
let closed = false;
const footer = document.createElement("div");
const body = mount({
...opts,
setFooter: (f) => { footer.replaceChildren(...(f.actions || []).map((a) => a.node)); },
close: () => { closed = true; if (opts.close) opts.close(); },
});
const root = document.createElement("div");
root.append(body.node, footer);
document.body.replaceChildren(root);
return { root, get closed() { return closed; } };
}
const find = (root, text) => [...root.querySelectorAll("button")].find((b) => b.textContent.trim() === text);
test("the list loads on mount", async () => {
mockFetch({ "GET /funpack/api/shortcuts": async () => ({ ok: true, json: async () => ({ shortcuts: [sc("Fox")] }) }) });
const { root } = open();
await new Promise((r) => setTimeout(r, 0));
assert.match(root.textContent, /Fox/);
});
test("adding a shortcut with no trigger is refused before the request is sent", async () => {
let posted = false;
mockFetch({
"GET /funpack/api/shortcuts": async () => ({ ok: true, json: async () => ({ shortcuts: [] }) }),
"POST /funpack/api/shortcuts": async () => { posted = true; return { ok: true, json: async () => ({ shortcuts: [] }) }; },
});
const { root } = open();
await new Promise((r) => setTimeout(r, 0));
find(root, "+ Add shortcut").click();
find(root, "Save").click();
assert.equal(posted, false, "an empty trigger list reached the server");
assert.match(root.textContent, /at least one trigger/i);
});
test("delete calls the route for that name and redraws from the response", async () => {
const calls = [];
mockFetch({
"GET /funpack/api/shortcuts": async () => ({ ok: true, json: async () => ({ shortcuts: [sc("Fox"), sc("Owl")] }) }),
"DELETE /funpack/api/shortcuts/Fox": async () => { calls.push("Fox"); return { ok: true, json: async () => ({ shortcuts: [sc("Owl")] }) }; },
});
const { root } = open();
await new Promise((r) => setTimeout(r, 0));
find(root, "Delete").click();
await new Promise((r) => setTimeout(r, 0));
assert.deepEqual(calls, ["Fox"]);
assert.doesNotMatch(root.textContent, /Fox/);
assert.match(root.textContent, /Owl/);
});
test("a server refusal on save is shown rather than silently dropped", async () => {
mockFetch({
"GET /funpack/api/shortcuts": async () => ({ ok: true, json: async () => ({ shortcuts: [] }) }),
"POST /funpack/api/shortcuts": async () => ({ ok: false, status: 400, json: async () => ({ problems: ["a shortcut needs at least one trigger"] }) }),
});
const { root } = open();
await new Promise((r) => setTimeout(r, 0));
find(root, "+ Add shortcut").click();
const triggers = [...root.querySelectorAll("textarea")][0];
triggers.value = "fox";
triggers.dispatchEvent(new window.Event("change", { bubbles: true }));
find(root, "Save").click();
await new Promise((r) => setTimeout(r, 0));
assert.match(root.textContent, /a shortcut needs at least one trigger/);
});
test("the footer's Close action calls the close handler it was given", async () => {
mockFetch({ "GET /funpack/api/shortcuts": async () => ({ ok: true, json: async () => ({ shortcuts: [] }) }) });
const session = open();
await new Promise((r) => setTimeout(r, 0));
find(session.root, "Close").click();
assert.equal(session.closed, true);
});