feat: add selected-loader LoRA info sidebar
This commit is contained in:
@@ -0,0 +1,195 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
buildExternalLinks,
|
||||
extractLoraNames,
|
||||
getSelectedGraphNodes,
|
||||
matchModelItems,
|
||||
normalizeLoraIdentifier,
|
||||
normalizeUsageTips,
|
||||
} from "../../web/comfyui/lora_manager_sidebar_utils.js";
|
||||
|
||||
test("normalizes loader paths and weight extensions", () => {
|
||||
assert.equal(
|
||||
normalizeLoraIdentifier("Styles\\Portrait.safetensors"),
|
||||
"styles/portrait"
|
||||
);
|
||||
});
|
||||
|
||||
test("formats Manager usage presets and hides empty JSON", () => {
|
||||
assert.deepEqual(normalizeUsageTips("{}"), []);
|
||||
assert.deepEqual(
|
||||
normalizeUsageTips('{"strength_min":0.7,"clipStrength":1}'),
|
||||
[
|
||||
{ label: "Strength min", value: "0.7" },
|
||||
{ label: "Clip Strength", value: "1" },
|
||||
]
|
||||
);
|
||||
assert.deepEqual(normalizeUsageTips("Use at low strength"), [
|
||||
{ label: "Note", value: "Use at low strength" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("extracts stock and numbered LoRA loader widgets", () => {
|
||||
const node = {
|
||||
comfyClass: "Power Lora Loader",
|
||||
widgets: [
|
||||
{ name: "lora_name", value: "styles/portrait.safetensors" },
|
||||
{ name: "lora_01", value: "characters/alice.safetensors" },
|
||||
{ name: "strength_model", value: 0.8 },
|
||||
],
|
||||
};
|
||||
|
||||
assert.deepEqual(extractLoraNames(node), [
|
||||
"styles/portrait.safetensors",
|
||||
"characters/alice.safetensors",
|
||||
]);
|
||||
});
|
||||
|
||||
test("treats active Manager entries as authoritative over synchronized text", () => {
|
||||
const node = {
|
||||
comfyClass: "Lora Loader (Remote, LoraManager)",
|
||||
lorasWidget: {
|
||||
value: [
|
||||
{ name: "one", active: true },
|
||||
{ name: "two", active: false },
|
||||
],
|
||||
},
|
||||
widgets: [{ name: "text", value: "<lora:one:1> <lora:two:0.7>" }],
|
||||
};
|
||||
|
||||
assert.deepEqual(extractLoraNames(node), ["one"]);
|
||||
});
|
||||
|
||||
test("extracts LoRA syntax from text loaders without a Manager widget", () => {
|
||||
const node = {
|
||||
comfyClass: "LoRA Text Loader",
|
||||
widgets: [{ name: "text", value: "<lora:one:1> <lora:three:0.7>" }],
|
||||
};
|
||||
|
||||
assert.deepEqual(extractLoraNames(node), ["one", "three"]);
|
||||
});
|
||||
|
||||
test("extracts third-party generic selectors and keyed LoRA maps", () => {
|
||||
const node = {
|
||||
type: "ThirdPartyLoraLoader",
|
||||
widgets: [
|
||||
{ name: "model", value: "styles/four.safetensors" },
|
||||
{
|
||||
name: "loras",
|
||||
value: {
|
||||
"five.safetensors": 0.7,
|
||||
"disabled.safetensors": false,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
assert.deepEqual(extractLoraNames(node), [
|
||||
"styles/four.safetensors",
|
||||
"five.safetensors",
|
||||
]);
|
||||
});
|
||||
|
||||
test("supports dynamic stack widget names and their enable switches", () => {
|
||||
const node = {
|
||||
type: "LoRAStackDynamic",
|
||||
widgets: [
|
||||
{ name: "input_mode", value: "text" },
|
||||
{ name: "lora_count", value: 2 },
|
||||
{ name: "lora_name_1", value: "stale-one.safetensors" },
|
||||
{ name: "lora_name_text_1", value: "one.safetensors" },
|
||||
{ name: "enabled_1", value: true },
|
||||
{ name: "lora_name_2", value: "stale-two.safetensors" },
|
||||
{ name: "lora_name_text_2", value: "two.safetensors" },
|
||||
{ name: "enabled_2", value: false },
|
||||
{ name: "lora_name_text_3", value: "three.safetensors" },
|
||||
{ name: "enabled_3", value: true },
|
||||
],
|
||||
};
|
||||
|
||||
assert.deepEqual(extractLoraNames(node), ["one.safetensors"]);
|
||||
|
||||
node.widgets.find((widget) => widget.name === "input_mode").value = "dropdown";
|
||||
node.widgets.find((widget) => widget.name === "lora_count").value = 1;
|
||||
assert.deepEqual(extractLoraNames(node), ["stale-one.safetensors"]);
|
||||
});
|
||||
|
||||
test("reads current selectedItems with selected_nodes fallback", () => {
|
||||
const selected = { id: 4, type: "LoraLoader", widgets: [] };
|
||||
assert.deepEqual(
|
||||
getSelectedGraphNodes({ selectedItems: new Set([selected]) }),
|
||||
[selected]
|
||||
);
|
||||
assert.deepEqual(
|
||||
getSelectedGraphNodes({ selected_nodes: { 4: selected } }),
|
||||
[selected]
|
||||
);
|
||||
});
|
||||
|
||||
test("prefers exact relative paths and reports ambiguous basenames", () => {
|
||||
const items = [
|
||||
{
|
||||
file_name: "portrait.safetensors",
|
||||
model_name: "Portrait",
|
||||
folder: "styles",
|
||||
file_path: "/models/loras/styles/portrait.safetensors",
|
||||
},
|
||||
{
|
||||
file_name: "portrait.safetensors",
|
||||
model_name: "Portrait Alt",
|
||||
folder: "people",
|
||||
file_path: "/models/loras/people/portrait.safetensors",
|
||||
},
|
||||
];
|
||||
|
||||
const exact = matchModelItems("styles/portrait.safetensors", items);
|
||||
assert.equal(exact.found, true);
|
||||
assert.equal(exact.model.folder, "styles");
|
||||
|
||||
const ambiguous = matchModelItems("portrait.safetensors", items);
|
||||
assert.equal(ambiguous.ambiguous, true);
|
||||
assert.equal(ambiguous.candidates.length, 2);
|
||||
|
||||
const absolute = matchModelItems(
|
||||
"/models/loras/people/portrait.safetensors",
|
||||
items
|
||||
);
|
||||
assert.equal(absolute.found, true);
|
||||
assert.equal(absolute.model.folder, "people");
|
||||
});
|
||||
|
||||
test("builds exact Civitai mirrors and hash-based CivArchive search", () => {
|
||||
const links = buildExternalLinks("portrait", {
|
||||
model_name: "Portrait",
|
||||
sha256: "abc123",
|
||||
civitai: { modelId: 42, id: 84 },
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
links.civitai,
|
||||
"https://civitai.com/models/42?modelVersionId=84"
|
||||
);
|
||||
assert.equal(
|
||||
links.civitaiRed,
|
||||
"https://civitai.red/models/42?modelVersionId=84"
|
||||
);
|
||||
assert.equal(links.civArchive, "https://civarchive.com/search?q=abc123");
|
||||
});
|
||||
|
||||
test("builds encoded name searches when the Manager has no card", () => {
|
||||
const links = buildExternalLinks("Krea 2 portrait");
|
||||
assert.equal(
|
||||
links.civitai,
|
||||
"https://civitai.com/search/models?query=Krea%202%20portrait"
|
||||
);
|
||||
assert.equal(
|
||||
links.civitaiRed,
|
||||
"https://civitai.red/search/models?query=Krea%202%20portrait"
|
||||
);
|
||||
assert.equal(
|
||||
links.civArchive,
|
||||
"https://civarchive.com/search?q=Krea%202%20portrait"
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user