Image Crop + Rotate + Pad's stitcher now remembers the picture and the crop's position. With Paste onto on original, Stitch Inpaint lays the result on the whole picture, straight, resized or turned. The default stays crop, so saved workflows run as before.
421 lines
24 KiB
JavaScript
421 lines
24 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import { readFileSync } from "node:fs";
|
|
import { dirname, join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
import {
|
|
CARD_PADDING, GROUP_HEIGHT, ROW_GAP, ROW_HEIGHT, SECTION_HEIGHT, SLOT_OFFSET, SOCKETLESS_CHOICE, SOCKETLESS_TEXT,
|
|
cardHeight, choiceLabel, commitWidgetValue, holdsUnknownValue, markSocketless, resetUnknownValues, rowHeight, rowInLook, rowKind, rowMuted, rowTops,
|
|
scrubSteps, segmentFits, socketWidgetY, visibleRows, widgetTooltip,
|
|
} from "../js/shared/widget_card_math.mjs";
|
|
|
|
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
|
|
test("visibleRows honors when() and closed groups but keeps the header", () => {
|
|
const rows = [
|
|
{ widget: "mode" },
|
|
{ widget: "width", when: (v) => v.mode === "size" },
|
|
{ group: "advanced", label: "More" },
|
|
{ widget: "extra", inGroup: "advanced" },
|
|
];
|
|
assert.deepEqual(visibleRows(rows, { mode: "size" }, { advanced: false }).map((r) => r.widget ?? r.group), ["mode", "width", "advanced"]);
|
|
assert.deepEqual(visibleRows(rows, { mode: "other" }, { advanced: true }).map((r) => r.widget ?? r.group), ["mode", "advanced", "extra"]);
|
|
});
|
|
|
|
test("cardHeight sums rows, captions and headers with one gap between each", () => {
|
|
assert.equal(cardHeight([]), CARD_PADDING * 2);
|
|
assert.equal(cardHeight([{ widget: "a" }]), CARD_PADDING * 2 + ROW_HEIGHT);
|
|
assert.equal(
|
|
cardHeight([{ section: "Crop" }, { widget: "a" }, { group: "x" }]),
|
|
CARD_PADDING * 2 + SECTION_HEIGHT + ROW_HEIGHT + GROUP_HEIGHT + ROW_GAP * 2,
|
|
);
|
|
});
|
|
|
|
test("segmentFits takes a few short labels and rejects long lists", () => {
|
|
assert.equal(segmentFits(["lab", "rgb", "mkl", "histogram"]), true);
|
|
assert.equal(segmentFits(["overwrite", "skip", "error"]), true);
|
|
assert.equal(segmentFits(["lanczos", "bicubic", "bilinear", "nearest", "area"]), false);
|
|
assert.equal(segmentFits(["width+height", "longest_edge", "shortest_edge"]), false);
|
|
assert.equal(segmentFits(["only"]), false);
|
|
});
|
|
|
|
test("scrubSteps: 0..1 floats move by 0.05, wide floats by ten increments, wide pixel sizes by 8", () => {
|
|
assert.deepEqual(scrubSteps({ min: 0, max: 1, step: 0.1, step2: 0.01, precision: 2 }, true), { step: 0.05, fineStep: 0.01, decimals: 2 });
|
|
assert.deepEqual(scrubSteps({ min: 0, max: 64, step: 0.1, step2: 0.01, precision: 2 }, true), { step: 0.1, fineStep: 0.01, decimals: 2 });
|
|
assert.deepEqual(scrubSteps({ min: 0, max: 16384, step: 10, step2: 1, precision: 0 }, false, true), { step: 8, fineStep: 1, decimals: 0 });
|
|
assert.deepEqual(scrubSteps({ min: 1, max: 128, step: 10, step2: 1, precision: 0 }, false), { step: 1, fineStep: 1, decimals: 0 });
|
|
assert.deepEqual(scrubSteps({ min: 0, max: 256, step: 10, step2: 1, precision: 0 }, false, true), { step: 1, fineStep: 1, decimals: 0 });
|
|
});
|
|
|
|
// The Integer node takes any whole number, so its range is as wide as a
|
|
// pixel size's. Steps, counts and frame numbers still move by 1: a Steps box
|
|
// at 25 has to reach 8.
|
|
test("scrubSteps: a whole number that is not a pixel size moves by 1, however wide its range", () => {
|
|
const integerNode = { min: -2147483648, max: 2147483647, step: 10, step2: 1, precision: 0 };
|
|
assert.deepEqual(scrubSteps(integerNode, false), { step: 1, fineStep: 1, decimals: 0 });
|
|
assert.deepEqual(scrubSteps({ min: 0, max: 100000, step: 10, step2: 1, precision: 0 }, false), { step: 1, fineStep: 1, decimals: 0 });
|
|
});
|
|
|
|
// Which rows are pixel sizes is said in js/widget_cards/index.js: a px unit,
|
|
// or `pixels` where the row shows no unit. A row can also name its own step.
|
|
test("the cards: Image Resize's size and Load Image + Pad's Multiple keep their step of 8, Integer has none", () => {
|
|
const source = readFileSync(join(ROOT, "js", "widget_cards", "index.js"), "utf-8");
|
|
const line = (text) => source.split("\n").find((candidate) => candidate.includes(text)) ?? "";
|
|
assert.match(line('pair: ["width", "height"]'), /pixels: true/);
|
|
assert.match(line('widget: "canvas_multiple"'), /step: 8/);
|
|
assert.ok(line("AUSBOSS_NODES_Integer:"), "js/widget_cards/index.js has no card for the Integer node");
|
|
assert.doesNotMatch(line("AUSBOSS_NODES_Integer:"), /suffix: "px"|pixels: true|step:/);
|
|
});
|
|
|
|
test("rowKind follows the widget type unless the row names a kind", () => {
|
|
assert.equal(rowKind({ widget: "strength" }, { type: "number" }), "scrub");
|
|
assert.equal(rowKind({ widget: "method" }, { type: "combo", options: { values: ["lab", "rgb"] } }), "segment");
|
|
assert.equal(rowKind({ widget: "interpolation" }, { type: "combo", options: { values: ["lanczos", "bicubic", "bilinear", "nearest", "area"] } }), "select");
|
|
assert.equal(rowKind({ widget: "flag" }, { type: "toggle" }), "switch");
|
|
assert.equal(rowKind({ widget: "fill_color" }, { type: "text", value: "#000000" }), "color");
|
|
assert.equal(rowKind({ widget: "prefix" }, { type: "text", value: "AusBoss" }), "text");
|
|
assert.equal(rowKind({ widget: "caption" }, { type: "customtext" }), "skip");
|
|
assert.equal(rowKind({ widget: "method", kind: "select" }, { type: "combo", options: { values: ["a", "b"] } }), "select");
|
|
});
|
|
|
|
test("rowHeight and cardHeight honor a textarea row's own height", () => {
|
|
assert.equal(rowHeight({ widget: "prompt", kind: "textarea", height: 96 }), 96);
|
|
assert.equal(rowHeight({ widget: "a" }), ROW_HEIGHT);
|
|
assert.equal(
|
|
cardHeight([{ widget: "prompt", kind: "textarea", height: 96 }, { widget: "flag" }]),
|
|
CARD_PADDING * 2 + 96 + ROW_HEIGHT + ROW_GAP,
|
|
);
|
|
});
|
|
|
|
test("a row takes its height and placeholder in the card's look, and is itself in any other", () => {
|
|
const row = { widget: "text", kind: "textarea", placeholder: "Prompt", height: 100, looks: { big: { height: 62, placeholder: "type here" } } };
|
|
assert.equal(rowInLook(row, "big").placeholder, "type here");
|
|
assert.equal(rowInLook(row, "big").widget, "text");
|
|
assert.equal(rowInLook(row, "normal"), row);
|
|
assert.equal(rowInLook(row, null), row);
|
|
assert.equal(rowInLook({ widget: "a" }, "big").widget, "a");
|
|
assert.equal(rowHeight(row), 100);
|
|
assert.equal(rowHeight(row, "big"), 62);
|
|
const rows = [row, { widget: "flag" }];
|
|
assert.equal(cardHeight(rows, "big"), CARD_PADDING * 2 + 62 + ROW_HEIGHT + ROW_GAP);
|
|
assert.deepEqual(rowTops(rows, "big"), [CARD_PADDING, CARD_PADDING + 62 + ROW_GAP]);
|
|
assert.deepEqual(rowTops(rows, "other"), rowTops(rows));
|
|
});
|
|
|
|
test("rowTops walks the visible rows from the card's padding", () => {
|
|
const rows = [{ widget: "a" }, { section: "More" }, { widget: "b", kind: "textarea", height: 60 }, { group: "x" }];
|
|
assert.deepEqual(rowTops(rows), [
|
|
CARD_PADDING,
|
|
CARD_PADDING + ROW_HEIGHT + ROW_GAP,
|
|
CARD_PADDING + ROW_HEIGHT + ROW_GAP + SECTION_HEIGHT + ROW_GAP,
|
|
CARD_PADDING + ROW_HEIGHT + ROW_GAP + SECTION_HEIGHT + ROW_GAP + 60 + ROW_GAP,
|
|
]);
|
|
});
|
|
|
|
test("socketWidgetY puts a row's socket on its centre line and a tall row's near its top", () => {
|
|
// The frontend draws the socket SLOT_OFFSET below the widget's y.
|
|
const cardY = 120;
|
|
const row = socketWidgetY(cardY, 10, CARD_PADDING, ROW_HEIGHT);
|
|
assert.equal(row + SLOT_OFFSET, cardY + 10 + CARD_PADDING + ROW_HEIGHT / 2);
|
|
const tall = socketWidgetY(cardY, 10, CARD_PADDING, 96);
|
|
assert.equal(tall + SLOT_OFFSET, cardY + 10 + CARD_PADDING + 13);
|
|
});
|
|
|
|
test("rowKind: an explicit textarea kind wins over the customtext skip", () => {
|
|
assert.equal(rowKind({ widget: "prompt", kind: "textarea" }, { type: "customtext" }), "textarea");
|
|
assert.equal(rowKind({ widget: "caption" }, { type: "customtext" }), "skip");
|
|
});
|
|
|
|
test("a muted row keeps its place and the card its height", () => {
|
|
const rows = [
|
|
{ widget: "tone", mute: (v) => v.seam === "blend in" },
|
|
{ widget: "halo", mute: (v) => v.seam === "blend in" },
|
|
];
|
|
assert.equal(rowMuted(rows[0], { seam: "blend in" }), true);
|
|
assert.equal(rowMuted(rows[0], { seam: "classic" }), false);
|
|
// No predicate, or one that says anything but true, never mutes.
|
|
assert.equal(rowMuted({ widget: "plain" }, { seam: "blend in" }), false);
|
|
assert.equal(rowMuted({ widget: "odd", mute: () => "yes" }, {}), false);
|
|
// Muting is not hiding: the same rows show and the card is as tall.
|
|
for (const seam of ["classic", "blend in"]) {
|
|
const visible = visibleRows(rows, { seam });
|
|
assert.deepEqual(visible.map((row) => row.widget), ["tone", "halo"]);
|
|
assert.equal(cardHeight(visible), CARD_PADDING * 2 + ROW_HEIGHT * 2 + ROW_GAP);
|
|
assert.deepEqual(rowTops(visible), [CARD_PADDING, CARD_PADDING + ROW_HEIGHT + ROW_GAP]);
|
|
}
|
|
});
|
|
|
|
test("commitWidgetValue sets the value, runs the callback, then tells the node", () => {
|
|
const calls = [];
|
|
const widget = { name: "image", value: "old.png", callback: (value, canvas, node) => calls.push(["callback", value, canvas, node.id]) };
|
|
const node = { id: 5, onWidgetChanged: (name, value, previous, changed) => calls.push(["changed", name, value, previous, changed === widget]) };
|
|
commitWidgetValue(node, widget, "new.png", "canvas");
|
|
assert.equal(widget.value, "new.png");
|
|
assert.deepEqual(calls, [
|
|
["callback", "new.png", "canvas", 5],
|
|
["changed", "image", "new.png", "old.png", true],
|
|
]);
|
|
});
|
|
|
|
test("commitWidgetValue copes with a widget that has no callback and a node that has no hook", () => {
|
|
const widget = { name: "seed", value: 1 };
|
|
commitWidgetValue({}, widget, 2, null);
|
|
assert.equal(widget.value, 2);
|
|
});
|
|
|
|
test("a switch holds only on or off, a choice only one of its options, a number only a number", () => {
|
|
for (const value of [true, false]) assert.equal(holdsUnknownValue({ type: "toggle", value }), false);
|
|
for (const value of ["", null, undefined, 0, "true"]) assert.equal(holdsUnknownValue({ type: "toggle", value }), true);
|
|
const seam = (value) => ({ type: "combo", value, options: { values: ["classic", "blend in"] } });
|
|
assert.equal(holdsUnknownValue(seam("blend in")), false);
|
|
for (const value of ["", null, "blend"]) assert.equal(holdsUnknownValue(seam(value)), true);
|
|
// A number holds only a finite number: an old save's "" is not one.
|
|
for (const value of [0, 2.5, -3]) assert.equal(holdsUnknownValue({ type: "number", value }), false);
|
|
for (const value of ["", "image", null, undefined, Number.NaN, "2"]) assert.equal(holdsUnknownValue({ type: "number", value }), true);
|
|
// Other widgets are never judged: an empty text field is a real value.
|
|
assert.equal(holdsUnknownValue({ type: "text", value: "" }), false);
|
|
});
|
|
|
|
// ---------- saves from before an input was appended ----------
|
|
// The frontend hands saved values out by position, and the card is a widget
|
|
// that saves an empty value of its own, so an input appended since a save
|
|
// receives the card's "" when that save loads.
|
|
|
|
function restoreByPosition(names, saved, types = {}) {
|
|
return names.map((name, index) => ({ name, type: types[name] ?? "number", value: saved[index] }));
|
|
}
|
|
|
|
// Today's widgets, in order: every input but the sockets, then the card.
|
|
function widgetNames(nodeId, sockets) {
|
|
const api = JSON.parse(readFileSync(join(ROOT, "tests", "fixtures", "node_api.json"), "utf-8"))[nodeId];
|
|
return [...api.required, ...api.optional].filter((name) => !sockets.includes(name)).concat("ausboss_widget_card");
|
|
}
|
|
|
|
// A node's entry in js/widget_cards/index.js, as text.
|
|
function cardEntry(nodeId) {
|
|
const source = readFileSync(join(ROOT, "js", "widget_cards", "index.js"), "utf-8");
|
|
const start = source.indexOf(`\n ${nodeId}: {`);
|
|
assert.ok(start >= 0, `js/widget_cards/index.js has no card for ${nodeId}`);
|
|
const next = source.slice(start + 1).search(/\n {2}AUSBOSS_NODES_\w+: \{/);
|
|
return next < 0 ? source.slice(start) : source.slice(start, start + 1 + next);
|
|
}
|
|
|
|
// The card's resetUnknown fallbacks, read from js/widget_cards/index.js.
|
|
function cardFallbacks(nodeId) {
|
|
const literal = cardEntry(nodeId).match(/resetUnknown: (\{[^}]*\})/)?.[1];
|
|
return literal ? JSON.parse(literal.replace(/(\w+):/g, '"$1":')) : {};
|
|
}
|
|
|
|
// The widgets the card lists as socketless.
|
|
function cardSocketless(nodeId) {
|
|
const literal = cardEntry(nodeId).match(/socketless: (\[[^\]]*\])/)?.[1];
|
|
return literal ? JSON.parse(literal) : [];
|
|
}
|
|
|
|
// Crop For Inpaint's keep_inside default, read from the Python source: the
|
|
// widget default a new node gets, and the argument default an API prompt
|
|
// without keep_inside runs with.
|
|
function keepInsideDefaults() {
|
|
const source = readFileSync(join(ROOT, "nodes", "node_inpaint_crop_stitch.py"), "utf-8");
|
|
const asBool = (text) => (text === "True" ? true : text === "False" ? false : undefined);
|
|
const widget = source.slice(source.indexOf('"keep_inside": (')).match(/"default": (True|False)/)?.[1];
|
|
const argument = source.match(/^\s+keep_inside=(True|False),$/m)?.[1];
|
|
return { widget: asBool(widget), argument: asBool(argument) };
|
|
}
|
|
|
|
// Crop For Inpaint as the LaMa Object Removal example saved it before 2.4.0:
|
|
// its fifteen settings, then the card's own empty value.
|
|
const CROP_BEFORE_KEEP_INSIDE = [2, 16, 8, 0, 0, 0, 0, false, 0, 0, "bilinear", 0, 0, 0, 0, ""];
|
|
const CROP_TYPES = { invert_mask: "toggle", keep_inside: "toggle", rescale_algorithm: "combo" };
|
|
|
|
test("Crop For Inpaint saved before Stay in picture opens with it on, as an API prompt without it runs", () => {
|
|
const names = widgetNames("AUSBOSS_NODES_CropForInpaint", ["image", "mask"]);
|
|
const widgets = restoreByPosition(names, CROP_BEFORE_KEEP_INSIDE, CROP_TYPES);
|
|
const keepInside = widgets.find((widget) => widget.name === "keep_inside");
|
|
assert.equal(keepInside.value, "", "the old save hands keep_inside the card's empty value");
|
|
|
|
assert.deepEqual(resetUnknownValues(widgets, cardFallbacks("AUSBOSS_NODES_CropForInpaint")), ["keep_inside"]);
|
|
const defaults = keepInsideDefaults();
|
|
assert.equal(keepInside.value, true);
|
|
assert.equal(keepInside.value, defaults.widget, "a new node's default");
|
|
assert.equal(keepInside.value, defaults.argument, "what an API prompt without keep_inside runs with");
|
|
// Every other setting keeps what was saved.
|
|
const others = widgets.filter((widget) => widget.name !== "keep_inside" && widget.name !== "ausboss_widget_card");
|
|
assert.deepEqual(others.map((widget) => widget.value), CROP_BEFORE_KEEP_INSIDE.slice(0, 15));
|
|
});
|
|
|
|
test("Crop For Inpaint saved since keeps its Stay in picture", () => {
|
|
const names = widgetNames("AUSBOSS_NODES_CropForInpaint", ["image", "mask"]);
|
|
const fallbacks = cardFallbacks("AUSBOSS_NODES_CropForInpaint");
|
|
for (const choice of [true, false]) {
|
|
const widgets = restoreByPosition(names, [...CROP_BEFORE_KEEP_INSIDE.slice(0, 15), choice, ""], CROP_TYPES);
|
|
assert.deepEqual(resetUnknownValues(widgets, fallbacks), []);
|
|
assert.equal(widgets.find((widget) => widget.name === "keep_inside").value, choice);
|
|
}
|
|
// Saved again on 2.4.0 while it still showed the stale value: on as well.
|
|
const resaved = restoreByPosition(names, [...CROP_BEFORE_KEEP_INSIDE.slice(0, 15), "", ""], CROP_TYPES);
|
|
resetUnknownValues(resaved, fallbacks);
|
|
assert.equal(resaved.find((widget) => widget.name === "keep_inside").value, true);
|
|
});
|
|
|
|
// Mask Refine's max_hole_size default, read from the Python source: the
|
|
// widget default a new node gets, and the argument default an API prompt
|
|
// without max_hole_size runs with.
|
|
function maxHoleSizeDefaults() {
|
|
const source = readFileSync(join(ROOT, "nodes", "node_refine_mask.py"), "utf-8");
|
|
const widget = source.slice(source.indexOf('"max_hole_size": (')).match(/"default": ([\d.]+)/)?.[1];
|
|
const argument = source.match(/^\s+max_hole_size=([\d.]+),$/m)?.[1];
|
|
return { widget: Number(widget), argument: Number(argument) };
|
|
}
|
|
|
|
// Mask Refine as the Krea 2 Inpaint Masked example saved it on 2.4.0: its
|
|
// eight settings, then the card's and the preview panel's empty values.
|
|
const REFINE_BEFORE_MAX_HOLE = [12, 4, true, 0, 0, 1, "off", true, "", ""];
|
|
const REFINE_TYPES = { fill_holes: "toggle", edge_refine: "combo", preview: "toggle" };
|
|
|
|
test("Mask Refine saved before Max hole size opens with no limit, as an API prompt without it runs", () => {
|
|
const names = widgetNames("AUSBOSS_NODES_RefineMask", ["mask", "guide_image"]);
|
|
const widgets = restoreByPosition(names, REFINE_BEFORE_MAX_HOLE, REFINE_TYPES);
|
|
widgets.find((widget) => widget.name === "edge_refine").options = { values: ["off", "guided filter", "matting"] };
|
|
const maxHole = widgets.find((widget) => widget.name === "max_hole_size");
|
|
assert.equal(maxHole.value, "", "the old save hands max_hole_size the card's empty value");
|
|
|
|
assert.deepEqual(resetUnknownValues(widgets, cardFallbacks("AUSBOSS_NODES_RefineMask")), ["max_hole_size"]);
|
|
const defaults = maxHoleSizeDefaults();
|
|
assert.equal(maxHole.value, 0);
|
|
assert.equal(maxHole.value, defaults.widget, "a new node's default");
|
|
assert.equal(maxHole.value, defaults.argument, "what an API prompt without max_hole_size runs with");
|
|
const others = widgets.filter((widget) => widget.name !== "max_hole_size" && widget.name !== "ausboss_widget_card");
|
|
assert.deepEqual(others.map((widget) => widget.value), REFINE_BEFORE_MAX_HOLE.slice(0, 8));
|
|
});
|
|
|
|
test("Mask Refine saved since keeps its Max hole size", () => {
|
|
const names = widgetNames("AUSBOSS_NODES_RefineMask", ["mask", "guide_image"]);
|
|
const fallbacks = cardFallbacks("AUSBOSS_NODES_RefineMask");
|
|
for (const size of [0, 2, 0.5]) {
|
|
const widgets = restoreByPosition(names, [...REFINE_BEFORE_MAX_HOLE.slice(0, 8), size], REFINE_TYPES);
|
|
widgets.find((widget) => widget.name === "edge_refine").options = { values: ["off", "guided filter", "matting"] };
|
|
assert.deepEqual(resetUnknownValues(widgets, fallbacks), []);
|
|
assert.equal(widgets.find((widget) => widget.name === "max_hole_size").value, size);
|
|
}
|
|
});
|
|
|
|
test("Stitch Inpaint saved before Seam still opens classic", () => {
|
|
const names = widgetNames("AUSBOSS_NODES_StitchInpaint", ["stitcher", "inpainted"]);
|
|
const types = { fix_edge_halo: "toggle", seam: "combo", paste_onto: "combo" };
|
|
const choices = (list) => {
|
|
list.find((widget) => widget.name === "seam").options = { values: ["classic", "blend in"] };
|
|
list.find((widget) => widget.name === "paste_onto").options = { values: ["crop", "original"] };
|
|
};
|
|
const widgets = restoreByPosition(names, [false, 0.5, ""], types);
|
|
choices(widgets);
|
|
assert.deepEqual(resetUnknownValues(widgets, cardFallbacks("AUSBOSS_NODES_StitchInpaint")), ["seam", "paste_onto"]);
|
|
assert.deepEqual(widgets.map((widget) => widget.value), [false, 0.5, "classic", "crop", undefined]);
|
|
// Saved with Seam: its choice stays, and the card's empty value is crop.
|
|
const seamed = restoreByPosition(names, [false, 0.5, "blend in", ""], types);
|
|
choices(seamed);
|
|
assert.deepEqual(resetUnknownValues(seamed, cardFallbacks("AUSBOSS_NODES_StitchInpaint")), ["paste_onto"]);
|
|
assert.equal(seamed.find((widget) => widget.name === "seam").value, "blend in");
|
|
assert.equal(seamed.find((widget) => widget.name === "paste_onto").value, "crop");
|
|
// A real choice is left alone.
|
|
const chosen = restoreByPosition(names, [false, 0.5, "blend in", "original", ""], types);
|
|
choices(chosen);
|
|
assert.deepEqual(resetUnknownValues(chosen, cardFallbacks("AUSBOSS_NODES_StitchInpaint")), []);
|
|
});
|
|
|
|
// ---------- widgets that take no link ----------
|
|
// The frontend gives every widget an input socket unless the widget is
|
|
// marked socketless. A link dragged over a node lands on its first free
|
|
// input of the link's type, hidden widgets included: a text link dragged
|
|
// over Mask by Touch lit a drop target at its card's top edge, for the
|
|
// `touches` its picture writes.
|
|
|
|
test("Mask by Touch's touches and ask take no link; its settings still do", () => {
|
|
// The definition as the frontend hands it to the pack, cut to a few inputs.
|
|
const touch = { input: {
|
|
required: {
|
|
image: ["IMAGE", { tooltip: "The picture to pick from." }],
|
|
touches: ["STRING", { default: "", multiline: false }],
|
|
grow: ["INT", { default: 0, min: -128, max: 256 }],
|
|
},
|
|
optional: {
|
|
ask: ["STRING", { default: "", multiline: false }],
|
|
fine_edges: [["off", "birefnet.safetensors"], { default: "off" }],
|
|
},
|
|
} };
|
|
const names = cardSocketless("AUSBOSS_NODES_MaskByTouch");
|
|
assert.deepEqual([...names].sort(), ["ask", "touches"]);
|
|
markSocketless(touch, names);
|
|
// A one-line text box with no socket, its own default kept.
|
|
assert.deepEqual(touch.input.required.touches[1], { default: "", multiline: false, widgetType: SOCKETLESS_TEXT, socketless: true });
|
|
assert.deepEqual(touch.input.optional.ask[1], { default: "", multiline: false, widgetType: SOCKETLESS_TEXT, socketless: true });
|
|
// What the picture does not write keeps its socket.
|
|
for (const [name, spec] of [["image", touch.input.required.image], ["grow", touch.input.required.grow], ["fine_edges", touch.input.optional.fine_edges]]) {
|
|
assert.equal(spec[1].socketless, undefined, `${name} lost its socket`);
|
|
assert.equal(spec[1].widgetType, undefined, `${name} lost its own widget`);
|
|
}
|
|
});
|
|
|
|
test("Image Folder's ticked pictures take no link; its Folder row still does", () => {
|
|
const folder = { input: { required: {
|
|
folder: ["STRING", { default: "", multiline: false }],
|
|
pictures: ["STRING", { default: "", multiline: false }],
|
|
} } };
|
|
const names = cardSocketless("AUSBOSS_NODES_ImageFolder");
|
|
assert.deepEqual(names, ["pictures"]);
|
|
markSocketless(folder, names);
|
|
assert.deepEqual(folder.input.required.pictures[1], { default: "", multiline: false, widgetType: SOCKETLESS_TEXT, socketless: true });
|
|
assert.deepEqual(folder.input.required.folder[1], { default: "", multiline: false });
|
|
});
|
|
|
|
test("a socketless choice stays the combo, and the newer form of a definition is marked too", () => {
|
|
const stitch = { input: { optional: { seam: [["classic", "blend in"], { default: "classic" }] } } };
|
|
markSocketless(stitch, cardSocketless("AUSBOSS_NODES_StitchInpaint"));
|
|
assert.deepEqual(stitch.input.optional.seam[1], { default: "classic", widgetType: SOCKETLESS_CHOICE, socketless: true });
|
|
const both = {
|
|
input: { required: { touches: ["STRING", { default: "" }] } },
|
|
inputs: { touches: { name: "touches", type: "STRING", default: "" } },
|
|
};
|
|
markSocketless(both, ["touches"]);
|
|
assert.deepEqual(both.inputs.touches, { name: "touches", type: "STRING", default: "", widgetType: SOCKETLESS_TEXT, socketless: true });
|
|
const newer = { inputs: { seam: { name: "seam", type: "COMBO", options: ["classic", "blend in"] } } };
|
|
markSocketless(newer, ["seam"]);
|
|
assert.equal(newer.inputs.seam.widgetType, SOCKETLESS_CHOICE);
|
|
// A name the node does not have changes nothing.
|
|
markSocketless(stitch, ["missing"]);
|
|
assert.deepEqual(Object.keys(stitch.input.optional), ["seam"]);
|
|
});
|
|
|
|
test("a card row's hover text comes from the node's definition", () => {
|
|
// The frontend keeps tooltips in the definition, not on the widget.
|
|
class Node {}
|
|
Node.nodeData = { input: {
|
|
required: { name: ["STRING", { tooltip: "What to find." }], image: ["IMAGE"] },
|
|
optional: { free_vram: ["BOOLEAN", { default: false, tooltip: "Takes SAM 3 out of memory." }] },
|
|
} };
|
|
const node = new Node();
|
|
assert.equal(widgetTooltip(node, "name", { options: {} }), "What to find.");
|
|
assert.equal(widgetTooltip(node, "free_vram"), "Takes SAM 3 out of memory.");
|
|
// A tooltip the widget carries itself wins.
|
|
assert.equal(widgetTooltip(node, "name", { options: { tooltip: "Its own." } }), "Its own.");
|
|
assert.equal(widgetTooltip(node, "name", { tooltip: "On the widget." }), "On the widget.");
|
|
// Nothing to show is an empty string, never undefined in a title.
|
|
assert.equal(widgetTooltip(node, "image"), "");
|
|
assert.equal(widgetTooltip(node, "missing"), "");
|
|
assert.equal(widgetTooltip({}, "name"), "");
|
|
assert.equal(widgetTooltip(null, "name"), "");
|
|
});
|
|
|
|
test("a choice's words come from the row: a map, or a function for lists only the user's machine knows", () => {
|
|
assert.equal(choiceLabel({}, "lanczos"), "lanczos");
|
|
assert.equal(choiceLabel({ labels: { rgb: "RGB" } }, "rgb"), "RGB");
|
|
assert.equal(choiceLabel({ labels: { rgb: "RGB" } }, "lab"), "lab");
|
|
const row = { labels: (value, values) => (value === "off" ? "off" : values.length > 2 ? value.replace(/\.safetensors$/, "") : "on") };
|
|
assert.equal(choiceLabel(row, "off", ["off", "model.safetensors"]), "off");
|
|
assert.equal(choiceLabel(row, "model.safetensors", ["off", "model.safetensors"]), "on");
|
|
assert.equal(choiceLabel(row, "b.safetensors", ["off", "a.safetensors", "b.safetensors"]), "b");
|
|
// The pill or the list is chosen from the words a person reads, so two long file names still make a short pill.
|
|
assert.equal(rowKind({ widget: "fine_edges", labels: row.labels }, { type: "combo", options: { values: ["off", "vitmatte-small-composition-1k/model.safetensors"] } }), "segment");
|
|
});
|