362 lines
10 KiB
JavaScript
362 lines
10 KiB
JavaScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import {
|
|
buildExternalLinks,
|
|
extractLoraNames,
|
|
getSelectedGraphNodes,
|
|
isVideoMedia,
|
|
matchModelItems,
|
|
mergeCivitaiMetadata,
|
|
normalizeLoraIdentifier,
|
|
normalizeMediaSettings,
|
|
normalizeSharedMedia,
|
|
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("detects direct and encoded video preview URLs", () => {
|
|
assert.equal(
|
|
isVideoMedia(
|
|
"/api/lm/previews?path=%2Fmodels%2Floras%2Fexample.MP4"
|
|
),
|
|
true
|
|
);
|
|
assert.equal(isVideoMedia("https://example.com/demo.WEBM?download=1"), true);
|
|
assert.equal(isVideoMedia("https://example.com/media/42", "video"), true);
|
|
assert.equal(isVideoMedia("https://example.com/still.png"), false);
|
|
assert.doesNotThrow(() => isVideoMedia("/preview?path=%E0%A4%A"));
|
|
});
|
|
|
|
test("merges full Civitai metadata without losing resolve fields", () => {
|
|
assert.deepEqual(
|
|
mergeCivitaiMetadata(
|
|
{ id: 7, modelId: 9, trainedWords: ["portrait"] },
|
|
{ description: "Full details", images: [{ id: 1 }] }
|
|
),
|
|
{
|
|
id: 7,
|
|
modelId: 9,
|
|
trainedWords: ["portrait"],
|
|
description: "Full details",
|
|
images: [{ id: 1 }],
|
|
}
|
|
);
|
|
});
|
|
|
|
test("normalizes community and Civitai media with associated prompts", () => {
|
|
const media = normalizeSharedMedia(
|
|
[
|
|
{
|
|
civitai_image_id: 10,
|
|
preview_url:
|
|
"/api/lm/previews?path=%2Fexamples%2Fcommunity-video.mp4",
|
|
media_type: "video",
|
|
prompt: "community prompt",
|
|
negative_prompt: "community negative",
|
|
username: "artist",
|
|
steps: 20,
|
|
cfg_scale: 4.5,
|
|
width: 1280,
|
|
height: 720,
|
|
},
|
|
],
|
|
{
|
|
images: [
|
|
{
|
|
id: 10,
|
|
url: "https://example.com/duplicate.mp4",
|
|
type: "video",
|
|
nsfwLevel: 16,
|
|
meta: { prompt: "duplicate prompt" },
|
|
},
|
|
{
|
|
id: 11,
|
|
url: "https://example.com/still.webp",
|
|
meta: {
|
|
meta: {
|
|
prompt: "example prompt",
|
|
negativePrompt: "example negative",
|
|
sampler: "Euler",
|
|
seed: 12,
|
|
Size: "640x480",
|
|
Model: "Example checkpoint",
|
|
clipSkip: 2,
|
|
},
|
|
},
|
|
},
|
|
],
|
|
customImages: [
|
|
{ id: 12, url: "javascript:alert(1)", meta: { prompt: "unsafe" } },
|
|
{
|
|
id: 14,
|
|
url: "https://user:password@example.com/private.webp",
|
|
meta: { prompt: "credential leak" },
|
|
},
|
|
{
|
|
id: 13,
|
|
url: "https://example.com/failed.webp",
|
|
downloadFailed: true,
|
|
},
|
|
{
|
|
id: "local-one",
|
|
url: "",
|
|
meta: { prompt: "local custom prompt" },
|
|
},
|
|
],
|
|
},
|
|
[
|
|
{
|
|
name: "custom_local-one.webm",
|
|
path: "/example_images_static/abc/custom_local-one.webm",
|
|
is_video: true,
|
|
},
|
|
]
|
|
);
|
|
|
|
assert.equal(media.length, 3);
|
|
assert.deepEqual(
|
|
{
|
|
source: media[0].source,
|
|
mediaType: media[0].mediaType,
|
|
prompt: media[0].prompt,
|
|
negativePrompt: media[0].negativePrompt,
|
|
username: media[0].username,
|
|
steps: media[0].steps,
|
|
cfgScale: media[0].cfgScale,
|
|
},
|
|
{
|
|
source: "Community creation",
|
|
mediaType: "video",
|
|
prompt: "community prompt",
|
|
negativePrompt: "community negative",
|
|
username: "artist",
|
|
steps: 20,
|
|
cfgScale: 4.5,
|
|
}
|
|
);
|
|
assert.equal(media[0].url.includes("/api/lm/previews"), true);
|
|
assert.equal(media[0].nsfwLevel, 16);
|
|
assert.equal(media[1].prompt, "example prompt");
|
|
assert.equal(media[1].negativePrompt, "example negative");
|
|
assert.equal(media[1].sampler, "Euler");
|
|
assert.equal(media[1].seed, 12);
|
|
assert.equal(media[1].width, 640);
|
|
assert.equal(media[1].height, 480);
|
|
assert.equal(media[1].modelName, "Example checkpoint");
|
|
assert.equal(media[1].clipSkip, 2);
|
|
assert.equal(media[2].url, "/example_images_static/abc/custom_local-one.webm");
|
|
assert.equal(media[2].mediaType, "video");
|
|
assert.equal(media[2].prompt, "local custom prompt");
|
|
});
|
|
|
|
test("optimizes Civitai media and normalizes mature-content settings", () => {
|
|
const media = normalizeSharedMedia([], {
|
|
images: [
|
|
{
|
|
id: 1,
|
|
url: "https://image.civitai.com/example/original=true/video.mp4",
|
|
type: "video",
|
|
},
|
|
],
|
|
});
|
|
assert.equal(media.length, 1);
|
|
assert.match(media[0].url, /transcode=true,width=450,optimized=true/);
|
|
|
|
assert.deepEqual(
|
|
normalizeMediaSettings({
|
|
blur_mature_content: false,
|
|
mature_blur_level: "XXX",
|
|
show_only_sfw: true,
|
|
}),
|
|
{
|
|
blurMatureContent: false,
|
|
matureBlurLevel: 16,
|
|
showOnlySfw: true,
|
|
}
|
|
);
|
|
});
|
|
|
|
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"
|
|
);
|
|
});
|