Files
ausboss-ComfyUI-AusBoss/tests/scrub_input.test.mjs
ausboss d759ca1cea Add tests for shared frontend helpers; fix link restore slot check
Restoring links after a failed change checked the source node but not
its output slot, so a link from another output of the same node could
be kept. 29 new tests cover scrub math, graph links, panel sizing,
Civitai parsing, brush math and widget card values.
2026-11-04 00:20:56 -06:00

285 lines
12 KiB
JavaScript

// The shared scrub control's pure gesture math: dead zone, step travel,
// fine steps, snapped steps, clamping, and precision.
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 {
SCRUB_DEAD_ZONE,
SCRUB_PIXELS_PER_STEP,
formatScrubValue,
isScrubGesture,
quantizeScrubValue,
scrubbedValue,
stepDecimals,
steppedValue,
sumValue,
SCRUB_SLOW_FACTOR,
} from "../js/shared/scrub_input.mjs";
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
const MP = { min: 0.01, max: 16, step: 0.05, fineStep: 0.01, decimals: 2 };
test("inside the dead zone nothing changes, so a click stays a click", () => {
assert.equal(scrubbedValue(1, SCRUB_DEAD_ZONE, false, MP), 1);
assert.equal(scrubbedValue(1, -SCRUB_DEAD_ZONE, false, MP), 1);
});
test("travel past the dead zone steps by the coarse step", () => {
// 43px right: (43-3)/4 = 10 steps of 0.05.
assert.equal(scrubbedValue(1, 43, false, MP), 1.5);
assert.equal(scrubbedValue(1, -43, false, MP), 0.5);
});
test("shift scrubs by the fine step", () => {
assert.equal(scrubbedValue(1, 43, true, MP), 1.1);
});
test("scrubs clamp at the range edges", () => {
assert.equal(scrubbedValue(15.9, 400, false, MP), 16);
assert.equal(scrubbedValue(0.1, -400, false, MP), 0.01);
});
test("values come back rounded to the control's precision", () => {
assert.equal(quantizeScrubValue(0.1 + 0.2, MP), 0.3);
assert.equal(quantizeScrubValue(7.777, { decimals: 0, min: 1, max: 256 }), 8);
});
test("junk quantizes to the range floor, not NaN", () => {
assert.equal(quantizeScrubValue("wide", MP), 0.01);
});
test("a mostly-vertical drag is not a scrub", () => {
assert.equal(isScrubGesture(10, 4), true);
assert.equal(isScrubGesture(6, 30), false);
assert.equal(isScrubGesture(2, 0), false);
});
test("integer controls step whole numbers", () => {
const STEPS = { min: 1, max: 256, step: 1, decimals: 0 };
assert.equal(scrubbedValue(64, 3 + SCRUB_PIXELS_PER_STEP * 4, false, STEPS), 68);
assert.equal(scrubbedValue(1, -400, false, STEPS), 1);
});
test("Shift slows a whole-number box that has no finer step", () => {
// Feather: 1 px steps, no fine step. Shift used to change nothing.
const FEATHER = { step: 1, decimals: 0, min: 0, max: 4096 };
assert.equal(scrubbedValue(24, 40, false, FEATHER), 33);
const slow = scrubbedValue(24, 40, true, FEATHER);
assert.ok(slow > 24 && slow < 33, String(slow));
assert.equal(scrubbedValue(24, 3 + SCRUB_PIXELS_PER_STEP * SCRUB_SLOW_FACTOR * 2, true, FEATHER), 26);
// A box with a finer step still uses it at the normal speed.
assert.equal(scrubbedValue(1, 43, true, { step: 0.05, fineStep: 0.01, decimals: 2 }), 1.1);
});
// Divisible by (canvas_multiple): 1 is off, and the sizes people want are
// multiples of 8. Stepping used to add 8 to 1 and land on 9, 17, 25.
const DIVISIBLE = { min: 1, max: 4096, step: 8, fineStep: 1, decimals: 0, snap: true };
// What one chevron click or arrow key commits (the DOM factory's path).
const click = (value, direction, fine = false) => {
const size = fine ? DIVISIBLE.fineStep : DIVISIBLE.step;
return quantizeScrubValue(steppedValue(value, direction, size, !fine), DIVISIBLE);
};
test("a snap box steps from 1 to 8, 16, 24 and back down to 1", () => {
const up = [];
for (let value = 1; up.length < 4;) up.push(value = click(value, 1));
assert.deepEqual(up, [8, 16, 24, 32]);
const down = [];
for (let value = 32; down.length < 5;) down.push(value = click(value, -1));
assert.deepEqual(down, [24, 16, 8, 1, 1]);
});
test("a snap box off the grid steps to the nearest multiple that way", () => {
assert.equal(click(9, 1), 16);
assert.equal(click(9, -1), 8);
assert.equal(click(12, 1), 16);
assert.equal(click(12, -1), 8);
assert.equal(click(4095, 1), 4096);
});
test("Shift steps a snap box by 1, off the grid", () => {
assert.equal(click(8, 1, true), 9);
assert.equal(click(8, -1, true), 7);
// ... and the next plain step goes back onto it.
assert.equal(click(click(8, 1, true), 1), 16);
});
test("dragging a snap box passes only multiples of the step", () => {
const passed = new Set();
for (let dx = 0; dx <= 64; dx++) passed.add(scrubbedValue(1, dx, false, DIVISIBLE));
assert.deepEqual([...passed], [1, 8, 16, 24, 32, 40, 48, 56, 64, 72, 80, 88, 96, 104, 112, 120]);
assert.equal(scrubbedValue(24, -43, false, DIVISIBLE), 1);
// A Shift drag moves by 1 from wherever it is.
assert.equal(scrubbedValue(8, 3 + SCRUB_PIXELS_PER_STEP * 3, true, DIVISIBLE), 11);
});
test("without snap a step still adds the step to the value", () => {
assert.equal(steppedValue(1, 1, 8), 9);
assert.equal(scrubbedValue(1, 3 + SCRUB_PIXELS_PER_STEP, false, { ...DIVISIBLE, snap: false }), 9);
});
test("a value on a fractional grid counts as on it", () => {
// 0.3 / 0.1 is 2.9999999999999996 in floating point.
assert.equal(quantizeScrubValue(steppedValue(0.3, 1, 0.1, true), { decimals: 2 }), 0.4);
assert.equal(quantizeScrubValue(steppedValue(0.3, -1, 0.1, true), { decimals: 2 }), 0.2);
});
test("every divisible-by box on the transform nodes and Load Image + Pad snaps", () => {
// Divisible by and the resize Step, on the node face and in the editor.
const editor = readFileSync(join(ROOT, "js", "shared", "transform_editor.mjs"), "utf-8");
const boxes = editor.split("makeScrubInput(").slice(1)
.map((rest) => rest.slice(0, rest.indexOf("onChange")))
.filter((options) => /"(canvas_multiple|resolution_steps)"/.test(options));
assert.equal(boxes.length, 4);
for (const options of boxes) assert.match(options, /\bsnap: true\b/);
const cards = readFileSync(join(ROOT, "js", "widget_cards", "index.js"), "utf-8");
assert.match(cards, /\{ widget: "canvas_multiple", [^}]*\bsnap: true\b/);
});
test("a step reaches as many decimals as it is written with", () => {
assert.equal(stepDecimals(0.05), 2);
assert.equal(stepDecimals(0.01), 2);
assert.equal(stepDecimals(0.25), 2);
assert.equal(stepDecimals(0.5), 1);
assert.equal(stepDecimals(0.1), 1);
assert.equal(stepDecimals(0.001), 3);
assert.equal(stepDecimals(1), 0);
assert.equal(stepDecimals(8), 0);
});
test("a box shows every decimal it holds unless told how few are enough", () => {
assert.equal(formatScrubValue(1, { decimals: 3 }), "1.000");
assert.equal(formatScrubValue(0.5, { decimals: 2 }), "0.50");
assert.equal(formatScrubValue(512, { decimals: 0 }), "512");
assert.equal(formatScrubValue(null, { decimals: 3 }), "");
});
test("zeroes the steps never reach are dropped, a value that uses them is shown in full", () => {
// Float wired to denoise: three decimals held, steps of 0.05 and 0.01.
const denoise = { decimals: 3, minDecimals: 2 };
assert.equal(formatScrubValue(1, denoise), "1.00");
assert.equal(formatScrubValue(0.85, denoise), "0.85");
assert.equal(formatScrubValue(0.855, denoise), "0.855");
assert.equal(formatScrubValue(0, denoise), "0.00");
// A step of 1 (CFG) still reads as a number with decimals.
assert.equal(formatScrubValue(8, { decimals: 3, minDecimals: 1 }), "8.0");
assert.equal(formatScrubValue(7.25, { decimals: 3, minDecimals: 1 }), "7.25");
// Nothing to keep: the dot goes with the last zero.
assert.equal(formatScrubValue(8, { decimals: 3, minDecimals: 0 }), "8");
assert.equal(formatScrubValue(100, { decimals: 2, minDecimals: 0 }), "100");
assert.equal(formatScrubValue(-0.5, { decimals: 3, minDecimals: 2 }), "-0.50");
// Asking for more than the box holds changes nothing.
assert.equal(formatScrubValue(1, { decimals: 2, minDecimals: 4 }), "1.00");
});
test("Float 🆎 drops the zeroes its steps never reach", () => {
const cards = readFileSync(join(ROOT, "js", "widget_cards", "index.js"), "utf-8");
assert.match(cards, /AUSBOSS_NODES_Float: \{[\s\S]{0,200}?\btrimZeros: true\b/);
});
test("a sum typed into a number box comes to its answer", () => {
assert.equal(sumValue("1024*1.5"), 1536);
assert.equal(sumValue("512+64"), 576);
assert.equal(sumValue("(1920 - 64) / 2"), 928);
assert.equal(sumValue("10/4"), 2.5);
assert.equal(sumValue("7 % 4"), 3);
assert.equal(sumValue("2+3*4"), 14);
assert.equal(sumValue("(2+3)*4"), 20);
assert.equal(sumValue("-(2+3)"), -5);
assert.equal(sumValue("2*-3"), -6);
assert.equal(sumValue("--4"), 4);
assert.equal(sumValue(".5+.25"), 0.75);
assert.equal(sumValue("3."), 3);
assert.equal(sumValue(" 64 "), 64);
assert.equal(sumValue("((((1))))"), 1);
assert.equal(Object.is(sumValue("0*-1"), 0), true);
});
test("text that is not a sum is no answer", () => {
for (const text of ["", " ", "abc", "2+", "*2", "(2+3", "2+3)", "2 3", "1,5", "1e3*2", "2**3", "2^3", "1/0", "0/0", "5%0", "()", ".", "1..2"]) {
assert.equal(sumValue(text), null, JSON.stringify(text));
}
// A very long text is refused before it is read, so it cannot run deep.
assert.equal(sumValue("(".repeat(5000) + "1" + ")".repeat(5000)), null);
assert.equal(sumValue("-".repeat(199) + "1"), -1);
assert.equal(sumValue(null), null);
});
test("a number box reads a typed sum only where a plain number fails", () => {
const source = readFileSync(join(ROOT, "js", "shared", "scrub_input.mjs"), "utf-8");
assert.match(source, /Number\.isFinite\(number\) \? number : sumValue\(input\.value\)/);
});
test("formatting keeps significant digits while trimming only fractional zeroes", () => {
for (const [value, options, expected] of [
[undefined, {}, ""],
["12.340", { decimals: 3, minDecimals: 0 }, "12.34"],
[-120, { decimals: 3, minDecimals: 0 }, "-120"],
[-1.2346, { decimals: 3, minDecimals: 1 }, "-1.235"],
[0.0001, { decimals: 4, minDecimals: 0 }, "0.0001"],
[12, { decimals: -2 }, "12"],
[1, { decimals: 2, minDecimals: -1 }, "1"],
]) {
assert.equal(formatScrubValue(value, options), expected);
}
});
test("invalid scrub values use a finite floor or zero before rounding", () => {
for (const value of [undefined, NaN, Infinity, -Infinity, "not a number"]) {
assert.equal(quantizeScrubValue(value), 0);
assert.equal(quantizeScrubValue(value, { min: -2, max: 4 }), -2);
}
assert.equal(quantizeScrubValue("2.718", { decimals: 2 }), 2.72);
assert.equal(quantizeScrubValue(-10, { min: -2, max: 4 }), -2);
assert.equal(quantizeScrubValue(10, { min: -2, max: 4 }), 4);
});
test("snapped steps cross zero and take several steps from an off-grid value", () => {
for (const [start, count, expected] of [
[-9, 1, -8], [-9, -1, -16], [-9, 3, 8],
[9, -3, -8], [9, 3, 32], [0, -1, -8], [9, 0, 9],
]) {
assert.equal(steppedValue(start, count, 8, true), expected, `${start}, ${count}`);
}
assert.equal(steppedValue("9", -2, 8), -7);
});
test("gesture boundaries accept equal diagonal travel only past the dead zone", () => {
for (const direction of [-1, 1]) {
assert.equal(isScrubGesture(direction * SCRUB_DEAD_ZONE, 0), false);
assert.equal(isScrubGesture(direction * 4, -4), true);
assert.equal(isScrubGesture(direction * 4, 5), false);
}
});
test("Shift slows dragging when the supplied fine step is equal or larger", () => {
const delta = SCRUB_DEAD_ZONE + SCRUB_PIXELS_PER_STEP * SCRUB_SLOW_FACTOR * 2;
for (const fineStep of [null, 1, 2]) {
const options = { step: 1, fineStep, decimals: 0 };
assert.equal(scrubbedValue(10, delta, true, options), 12);
assert.equal(scrubbedValue(10, -delta, true, options), 8);
assert.equal(scrubbedValue(10, delta, false, options), 18);
}
});
test("step precision accepts negative and string increments and rounds at six decimals", () => {
for (const [step, expected] of [[-0.025, 3], ["0.000001", 6], [0.1234567, 6], [0, 0], [undefined, 0]]) {
assert.equal(stepDecimals(step), expected, String(step));
}
});
test("typed sums associate division and subtraction left to right", () => {
assert.equal(sumValue("24/4/2"), 3);
assert.equal(sumValue("10-3-2"), 5);
assert.equal(sumValue("20%6*2"), 4);
assert.equal(sumValue("\t-(2 + +3) * -2\t"), 10);
assert.equal(sumValue("1" + " ".repeat(199)), 1);
assert.equal(sumValue("1" + " ".repeat(200)), null);
assert.equal(sumValue("1/(2-2)"), null);
});