Files
ausboss-ComfyUI-AusBoss/tests/appearance.test.mjs
T
ausboss f39908de05 Adopt the brand scheme pack-wide and add the title-bar help badge
The teal pair the video nodes hard-painted becomes the AusBoss scheme row
and the pack-wide default; Load/Save Video stop painting themselves so the
appearance setting and per-node menu govern every node the same way. Every
AusBoss node also gets a quiet ? badge whose card is built from the node's
own DESCRIPTION and tooltips.
2026-08-16 19:42:15 -05:00

226 lines
9.2 KiB
JavaScript

import test from "node:test";
import assert from "node:assert/strict";
import {
CUSTOM_SCHEME,
DEFAULT_SCHEME,
NODE_COLOR_SCHEMES,
SCHEME_NAMES,
collectGraphNodes,
customColors,
deriveBody,
normalizeHexColor,
schemeColors,
shouldRecolor,
titleInk,
} from "../js/shared/appearance.mjs";
test("scheme table is well formed", () => {
const names = NODE_COLOR_SCHEMES.map((scheme) => scheme.name);
assert.equal(new Set(names).size, names.length, "scheme names must be unique");
assert.equal(NODE_COLOR_SCHEMES[0].name, DEFAULT_SCHEME);
assert.equal(DEFAULT_SCHEME, "AusBoss", "the brand scheme is the pack-wide default");
const themeDefault = NODE_COLOR_SCHEMES.find((scheme) => scheme.name === "Theme default");
assert.equal(themeDefault?.colors, null, "Theme default leaves theme colors alone");
for (const scheme of NODE_COLOR_SCHEMES) {
if (!scheme.colors) continue;
assert.match(scheme.colors.title, /^#[0-9a-f]{6}$/);
assert.match(scheme.colors.body, /^#[0-9a-f]{6}$/);
assert.notEqual(scheme.colors.title, scheme.colors.body);
}
});
test("schemeColors resolves names and rejects unknowns", () => {
assert.equal(schemeColors("Theme default"), null);
assert.equal(schemeColors("Slate"), NODE_COLOR_SCHEMES.find((s) => s.name === "Slate").colors);
assert.equal(schemeColors("Not A Scheme"), null);
assert.equal(schemeColors(undefined), null);
});
test("SCHEME_NAMES is the table plus Custom, all unique", () => {
assert.deepEqual(SCHEME_NAMES, [...NODE_COLOR_SCHEMES.map((s) => s.name), CUSTOM_SCHEME]);
assert.equal(new Set(SCHEME_NAMES).size, SCHEME_NAMES.length);
});
test("normalizeHexColor accepts bare and prefixed hex, drops alpha", () => {
assert.equal(normalizeHexColor("#aabbcc"), "#aabbcc");
assert.equal(normalizeHexColor("aabbcc"), "#aabbcc");
assert.equal(normalizeHexColor(" AABBCC "), "#aabbcc");
assert.equal(normalizeHexColor("#AABBCCDD"), "#aabbcc");
assert.equal(normalizeHexColor("aabbccdd"), "#aabbcc");
});
test("normalizeHexColor rejects junk", () => {
assert.equal(normalizeHexColor(undefined), null);
assert.equal(normalizeHexColor(""), null);
assert.equal(normalizeHexColor("#abc"), null);
assert.equal(normalizeHexColor("zzzzzz"), null);
assert.equal(normalizeHexColor("#aabbccd"), null);
assert.equal(normalizeHexColor(0xaabbcc), null);
});
test("deriveBody mixes the title halfway toward the dark neutral", () => {
assert.equal(deriveBody("#000000"), "#181818");
assert.equal(deriveBody("#ffffff"), "#979797");
assert.equal(deriveBody("00b4aa"), "#18726d");
});
test("deriveBody never collapses the title and body into one flat color", () => {
// The neutral is the mix's fixed point, so it would otherwise return itself
// and draw a node with no visible title bar.
assert.equal(deriveBody("#2f2f2f"), "#393939");
assert.equal(deriveBody("#303030"), "#3a3a3a");
// Picks outside that band already separate on their own.
assert.equal(deriveBody("#f0f0f0"), "#909090");
for (let value = 0; value <= 255; value += 1) {
const title = `#${value.toString(16).padStart(2, "0").repeat(3)}`;
const body = deriveBody(title);
assert.notEqual(body, title, `${title} produced an identical body`);
}
});
test("deriveBody normalizes its input and rejects junk", () => {
assert.equal(deriveBody("#FFFFFF"), deriveBody("ffffff"));
assert.equal(deriveBody(undefined), null);
assert.equal(deriveBody("#abc"), null);
assert.equal(deriveBody("not hex"), null);
});
test("deriveBody always emits well-formed hex", () => {
for (const title of ["#010203", "#f00baa", "#deadbe", "#7f7f7f"]) {
assert.match(deriveBody(title), /^#[0-9a-f]{6}$/, title);
}
});
test("customColors pairs the normalized title with its derived body", () => {
assert.deepEqual(customColors("AABBCC"), { title: "#aabbcc", body: deriveBody("#aabbcc") });
assert.equal(customColors("junk"), null);
assert.equal(customColors(undefined), null);
});
test("schemeColors resolves Custom from the picked color only", () => {
assert.deepEqual(schemeColors(CUSTOM_SCHEME, "00b4aa"), customColors("00b4aa"));
assert.equal(schemeColors(CUSTOM_SCHEME), null);
assert.equal(schemeColors(CUSTOM_SCHEME, "junk"), null);
// Static names ignore the custom title entirely.
assert.equal(schemeColors("Slate", "00b4aa"), schemeColors("Slate"));
});
test("uncolored nodes follow the setting only from the theme default", () => {
const bare = {};
assert.equal(shouldRecolor(bare, null), true);
// Unset while a scheme was active means the user cleared it by hand.
assert.equal(shouldRecolor(bare, schemeColors("Teal")), false);
});
test("nodes wearing the previous scheme are repainted, manual colors win", () => {
const teal = schemeColors("Teal");
const wearing = { color: teal.title, bgcolor: teal.body };
assert.equal(shouldRecolor(wearing, teal), true);
assert.equal(shouldRecolor({ color: "#123456", bgcolor: "#654321" }, teal), false);
assert.equal(shouldRecolor({ color: "#123456", bgcolor: "#654321" }, null), false);
// Half-matching colors mean the user edited one channel — leave both alone.
assert.equal(shouldRecolor({ color: teal.title, bgcolor: "#654321" }, teal), false);
});
test("color comparison ignores case and whitespace", () => {
const plum = schemeColors("Plum");
const noisy = { color: ` ${plum.title.toUpperCase()} `, bgcolor: plum.body.toUpperCase() };
assert.equal(shouldRecolor(noisy, plum), true);
});
test("custom-tinted nodes sweep like any scheme pair", () => {
const custom = schemeColors(CUSTOM_SCHEME, "00b4aa");
const wearing = { color: custom.title, bgcolor: custom.body };
// Still following the setting: a sweep away from Custom repaints it.
assert.equal(shouldRecolor(wearing, custom), true);
// A CustomColor edit sweeps too: the old pair is the "previous" colors.
const edited = schemeColors(CUSTOM_SCHEME, "ff0000");
assert.notDeepEqual(edited, custom);
assert.equal(shouldRecolor(wearing, edited), false);
// Hand-edited bodies win over any custom sweep.
assert.equal(shouldRecolor({ color: custom.title, bgcolor: "#123456" }, custom), false);
});
test("a menu-picked scheme survives a settings sweep", () => {
// The user picked Plum from the node menu while the setting was Teal;
// sweeping the setting to anything else must leave the node alone.
const plum = schemeColors("Plum");
const picked = { color: plum.title, bgcolor: plum.body };
assert.equal(shouldRecolor(picked, schemeColors("Teal")), false);
assert.equal(shouldRecolor(picked, null), false);
});
test("titleInk keeps light ink on every shipped scheme", () => {
for (const scheme of NODE_COLOR_SCHEMES) {
if (!scheme.colors) continue;
assert.equal(titleInk(scheme.colors.title), "#ffffff", scheme.name);
}
});
test("titleInk flips to dark ink on light colors", () => {
assert.equal(titleInk("#e0e0e0"), "#1a1a1a");
assert.equal(titleInk(" #FFFFFF "), "#1a1a1a");
});
test("titleInk falls back to light ink on malformed input", () => {
assert.equal(titleInk(undefined), "#ffffff");
assert.equal(titleInk(""), "#ffffff");
assert.equal(titleInk("#e0e"), "#ffffff");
assert.equal(titleInk("e0e0e0"), "#ffffff");
assert.equal(titleInk("#zzzzzz"), "#ffffff");
});
test("collectGraphNodes flattens nested subgraphs", () => {
const inner = { _nodes: [{ id: 3 }] };
const graph = { _nodes: [{ id: 1 }, { id: 2, subgraph: inner }] };
assert.deepEqual(
collectGraphNodes(graph).map((node) => node.id),
[1, 2, 3],
);
});
test("collectGraphNodes accepts nodes arrays and plain arrays", () => {
const viaNodes = { nodes: [{ id: 1 }] };
assert.deepEqual(collectGraphNodes(viaNodes).map((node) => node.id), [1]);
const plain = [{ id: 1 }, { id: 2, subgraph: [{ id: 3 }] }];
assert.deepEqual(collectGraphNodes(plain).map((node) => node.id), [1, 2, 3]);
assert.deepEqual(collectGraphNodes(null), []);
assert.deepEqual(collectGraphNodes({}), []);
});
test("collectGraphNodes survives subgraph reference cycles", () => {
const graph = { _nodes: [{ id: 1 }] };
graph._nodes.push({ id: 2, subgraph: graph });
assert.deepEqual(
collectGraphNodes(graph).map((node) => node.id),
[1, 2],
);
});
// The node-color setting resolves a scheme to a colour pair before repainting.
// Custom with an unusable stored colour resolves to null, which downstream
// reads as "Theme default" and strips the colour off every AusBoss node - so
// both the setting and the per-node menu have to refuse it.
function repaintDecision(nextScheme, previousScheme, customColor) {
if (previousScheme === nextScheme) return "no change";
const colors = schemeColors(nextScheme, customColor);
if (nextScheme === "Custom" && !colors) return "refused";
return colors ? "repaint" : "theme default";
}
test("switching to Custom with an unusable colour repaints nothing", () => {
for (const bad of ["#abc", "", "red", "not a colour", null, undefined]) {
assert.equal(repaintDecision("Custom", "Teal", bad), "refused", String(bad));
}
});
test("switching to Custom with a usable colour repaints", () => {
assert.equal(repaintDecision("Custom", "Teal", "#ffee00"), "repaint");
assert.equal(repaintDecision("Custom", "Teal", "1f2c38"), "repaint");
});
test("Theme default still clears colours, which is its whole job", () => {
assert.equal(repaintDecision("Theme default", "Teal", "#ffee00"), "theme default");
});