The note said "installed" for a model kept in a subfolder while the
workflow's loader still asked for the bare file name, which ComfyUI could
not find and marked red. Such a row now says where the file is ("in
LTXV 2.3/") and has a Use it button that points those loaders at the found
copy. The check stays on the ComfyUI server's own model lists.
165 lines
7.9 KiB
JavaScript
165 lines
7.9 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import {
|
|
baseName,
|
|
loadersNeeding,
|
|
subfolderOf,
|
|
emptyNote,
|
|
folderPath,
|
|
groupModels,
|
|
hostLabel,
|
|
matchInstalled,
|
|
noteIsEmpty,
|
|
normalizeNote,
|
|
packsFromGraph,
|
|
parseInline,
|
|
parseMarkdown,
|
|
safeUrl,
|
|
serializeNote,
|
|
} from "../js/shared/workflow_note.mjs";
|
|
|
|
test("normalizeNote accepts the widget string, an object, or garbage", () => {
|
|
assert.deepEqual(normalizeNote(""), emptyNote());
|
|
assert.deepEqual(normalizeNote("not json"), emptyNote());
|
|
assert.deepEqual(normalizeNote(null), emptyNote());
|
|
assert.deepEqual(normalizeNote([1, 2]), emptyNote());
|
|
const note = normalizeNote({ title: " Krea 2 Outpaint ", layout: "banner", accent: "#00B4AA" });
|
|
assert.equal(note.title, "Krea 2 Outpaint");
|
|
assert.equal(note.layout, "banner");
|
|
assert.equal(note.accent, "#00b4aa");
|
|
});
|
|
|
|
test("rows drop empties, coerce types, and keep only http(s) urls", () => {
|
|
const note = normalizeNote({
|
|
models: [
|
|
{ name: "a.safetensors", dir: "/loras/", url: "javascript:alert(1)", size: 12 },
|
|
{ name: "", url: "https://x" },
|
|
"nonsense",
|
|
{ name: "b.safetensors", dir: "vae", url: "https://huggingface.co/x/b.safetensors" },
|
|
],
|
|
packs: [{ name: "ComfyUI-AusBoss", url: "http://github.com/a", node: "AUSBOSS_NODES_Seed" }, { url: "https://x" }],
|
|
links: [{ label: "", url: "https://civitai.com/user/AusBoss" }, { label: "", url: "ftp://nope" }],
|
|
layout: "weird",
|
|
accent: "red",
|
|
});
|
|
assert.deepEqual(note.models, [
|
|
{ name: "a.safetensors", dir: "loras", url: "", size: "12", note: "" },
|
|
{ name: "b.safetensors", dir: "vae", url: "https://huggingface.co/x/b.safetensors", size: "", note: "" },
|
|
]);
|
|
assert.deepEqual(note.packs, [{ name: "ComfyUI-AusBoss", url: "http://github.com/a", node: "AUSBOSS_NODES_Seed" }]);
|
|
assert.deepEqual(note.links, [{ label: "civitai.com", url: "https://civitai.com/user/AusBoss" }]);
|
|
assert.equal(note.layout, "card");
|
|
assert.equal(note.accent, "");
|
|
});
|
|
|
|
test("serializeNote round-trips through normalizeNote", () => {
|
|
const note = normalizeNote({ title: "T", body: "b", models: [{ name: "m", dir: "vae" }] });
|
|
assert.deepEqual(normalizeNote(serializeNote(note)), note);
|
|
});
|
|
|
|
test("noteIsEmpty is true only for a card with nothing on it", () => {
|
|
assert.equal(noteIsEmpty(emptyNote()), true);
|
|
assert.equal(noteIsEmpty(normalizeNote({ body: " \n" })), true);
|
|
assert.equal(noteIsEmpty(normalizeNote({ links: [{ url: "https://a.b" }] })), false);
|
|
});
|
|
|
|
test("safeUrl, hostLabel and baseName", () => {
|
|
assert.equal(safeUrl(" https://huggingface.co/a/b "), "https://huggingface.co/a/b");
|
|
assert.equal(safeUrl("data:text/html,hi"), "");
|
|
assert.equal(safeUrl("https://a b"), "");
|
|
assert.equal(hostLabel("https://www.youtube.com/@ausboss"), "youtube.com");
|
|
assert.equal(hostLabel("nope"), "");
|
|
assert.equal(baseName("Krea 2\\krea2_turbo_fp8.safetensors"), "krea2_turbo_fp8.safetensors");
|
|
assert.equal(baseName("plain.png"), "plain.png");
|
|
});
|
|
|
|
test("folderPath and groupModels read like the install tree", () => {
|
|
assert.equal(folderPath("loras"), "ComfyUI/models/loras");
|
|
assert.equal(folderPath("input"), "ComfyUI/input");
|
|
assert.equal(folderPath(""), "ComfyUI/models");
|
|
const groups = groupModels([
|
|
{ name: "a", dir: "vae" }, { name: "b", dir: "loras" }, { name: "c", dir: "vae" },
|
|
]);
|
|
assert.deepEqual(groups.map((g) => [g.dir, g.rows.map((r) => r.name)]), [["vae", ["a", "c"]], ["loras", ["b"]]]);
|
|
});
|
|
|
|
test("matchInstalled finds exact paths first, then basenames in any subfolder", () => {
|
|
const files = ["Krea 2/krea2_turbo_fp8.safetensors", "other/thing.safetensors", "top.safetensors"];
|
|
assert.deepEqual(matchInstalled("krea2_turbo_fp8.safetensors", files), { found: true, path: "Krea 2/krea2_turbo_fp8.safetensors" });
|
|
assert.deepEqual(matchInstalled("TOP.safetensors", files), { found: true, path: "top.safetensors" });
|
|
assert.deepEqual(matchInstalled("other\\thing.safetensors", files), { found: true, path: "other/thing.safetensors" });
|
|
assert.deepEqual(matchInstalled("missing.safetensors", files), { found: false, path: null });
|
|
assert.deepEqual(matchInstalled("x", null), { found: false, path: null });
|
|
});
|
|
|
|
test("packsFromGraph lists each custom pack once and skips core modules", () => {
|
|
const moduleOf = (type) => ({
|
|
KSampler: "nodes",
|
|
ImageBlend: "comfy_extras.nodes_post_processing",
|
|
AUSBOSS_NODES_Seed: "custom_nodes.ComfyUI-AusBoss",
|
|
AUSBOSS_NODES_LoraLoader: "custom_nodes.ComfyUI-AusBoss",
|
|
"Power Lora Loader (rgthree)": "custom_nodes.rgthree-comfy",
|
|
Unknown: undefined,
|
|
})[type];
|
|
assert.deepEqual(
|
|
packsFromGraph(["KSampler", "AUSBOSS_NODES_Seed", "ImageBlend", "AUSBOSS_NODES_LoraLoader", "Power Lora Loader (rgthree)", "Unknown"], moduleOf),
|
|
[
|
|
{ name: "ComfyUI-AusBoss", url: "", node: "AUSBOSS_NODES_Seed" },
|
|
{ name: "rgthree-comfy", url: "", node: "Power Lora Loader (rgthree)" },
|
|
],
|
|
);
|
|
});
|
|
|
|
test("parseInline handles bold, italic, code and safe links", () => {
|
|
assert.deepEqual(parseInline("a **b** *c* `d` [e](https://f.g) [h](javascript:x) i"), [
|
|
{ type: "text", text: "a " },
|
|
{ type: "strong", text: "b" },
|
|
{ type: "text", text: " " },
|
|
{ type: "em", text: "c" },
|
|
{ type: "text", text: " " },
|
|
{ type: "code", text: "d" },
|
|
{ type: "text", text: " " },
|
|
{ type: "link", text: "e", url: "https://f.g" },
|
|
{ type: "text", text: " " },
|
|
{ type: "text", text: "h" },
|
|
{ type: "text", text: " i" },
|
|
]);
|
|
// A lone asterisk or one inside a word is just text.
|
|
assert.deepEqual(parseInline("2 * 3 and snake_case*x"), [{ type: "text", text: "2 * 3 and snake_case*x" }]);
|
|
});
|
|
|
|
test("parseMarkdown yields headings, paragraphs with soft breaks, lists, rules and code", () => {
|
|
const blocks = parseMarkdown("# Title\n\nline one\nline two\n\n- a\n- **b**\n more\n1. one\n2. two\n\n---\n```\nx = 1\n```\n");
|
|
assert.deepEqual(blocks.map((b) => b.type), ["heading", "paragraph", "list", "list", "hr", "code"]);
|
|
assert.equal(blocks[0].level, 1);
|
|
assert.deepEqual(blocks[1].spans, [
|
|
{ type: "text", text: "line one" }, { type: "br" }, { type: "text", text: "line two" },
|
|
]);
|
|
assert.equal(blocks[2].ordered, false);
|
|
assert.deepEqual(blocks[2].items[1], [{ type: "strong", text: "b" }, { type: "br" }, { type: "text", text: "more" }]);
|
|
assert.equal(blocks[3].ordered, true);
|
|
assert.equal(blocks[5].text, "x = 1");
|
|
});
|
|
|
|
test("parseMarkdown on empty or plain text is a single paragraph or nothing", () => {
|
|
assert.deepEqual(parseMarkdown(""), []);
|
|
assert.deepEqual(parseMarkdown("just words"), [{ type: "paragraph", spans: [{ type: "text", text: "just words" }] }]);
|
|
});
|
|
|
|
test("a loader asking for a bare name is pointed at the copy in a subfolder", () => {
|
|
assert.equal(subfolderOf("LTXV 2.3/ltx-2.3.safetensors"), "LTXV 2.3");
|
|
assert.equal(subfolderOf("a\\b\\c.safetensors"), "a/b");
|
|
assert.equal(subfolderOf("top.safetensors"), "");
|
|
const unet = { name: "unet_name", value: "ltx-2.3.safetensors", options: ["LTXV 2.3/ltx-2.3.safetensors", "other.safetensors"] };
|
|
const fine = { name: "vae_name", value: "LTX/vae.safetensors", options: ["LTX/vae.safetensors"] };
|
|
const bothCopies = { name: "clip", value: "gemma.safetensors", options: ["gemma.safetensors", "sub/gemma.safetensors"] };
|
|
const unrelated = { name: "sampler", value: "euler", options: ["euler", "dpmpp"] };
|
|
assert.deepEqual(loadersNeeding("ltx-2.3.safetensors", "LTXV 2.3/ltx-2.3.safetensors", [unet, fine, bothCopies, unrelated]), [unet]);
|
|
// A loader that already holds a copy it can load is left alone.
|
|
assert.deepEqual(loadersNeeding("vae.safetensors", "LTX/vae.safetensors", [fine]), []);
|
|
assert.deepEqual(loadersNeeding("gemma.safetensors", "sub/gemma.safetensors", [bothCopies]), []);
|
|
// Case and separators do not matter for the name.
|
|
const windows = { value: "LTX-2.3.SAFETENSORS", options: ["LTXV 2.3/ltx-2.3.safetensors"] };
|
|
assert.deepEqual(loadersNeeding("ltx-2.3.safetensors", "LTXV 2.3\\ltx-2.3.safetensors", [windows]), [windows]);
|
|
});
|