Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d759ca1cea |
@@ -64,7 +64,7 @@ export function restoreOutputLinks(node, outputs, graph) {
|
||||
const targetNode = graph.getNodeById(target.nodeId);
|
||||
if (!targetNode) continue;
|
||||
const current = lookupLink(graph, targetNode.inputs?.[target.slot]?.link);
|
||||
if (current?.origin_id === node.id) continue; // never stolen
|
||||
if (current?.origin_id === node.id && current.origin_slot === output.slot) continue; // never stolen
|
||||
linkSlots(node, output.slot, targetNode, target.slot);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
BRUSH_SIZE,
|
||||
ADJUST_DEAD_ZONE,
|
||||
adjustAxis,
|
||||
adjustedBrush,
|
||||
brushAdjusts,
|
||||
brushFlips,
|
||||
@@ -15,6 +17,7 @@ import {
|
||||
moveAdjust,
|
||||
startAdjust,
|
||||
steppedBrushSize,
|
||||
steppedSoft,
|
||||
toolKey,
|
||||
} from "../js/shared/brush.mjs";
|
||||
|
||||
@@ -101,3 +104,53 @@ test("the brush under the pointer fades as its marks do", () => {
|
||||
assert.ok(brushProfile(brushReach(80, 1), 80, 1) < 0.01);
|
||||
for (let d = 0; d < 50; d += 5) assert.ok(brushProfile(d, 80, 0.6) >= brushProfile(d + 5, 80, 0.6), "never brighter further out");
|
||||
});
|
||||
|
||||
test("brush values round, clamp and fall back without producing NaN", () => {
|
||||
assert.equal(clampBrushSize("25.6"), 26);
|
||||
assert.equal(clampBrushSize(1), BRUSH_SIZE[0]);
|
||||
for (const value of [undefined, "large", NaN, Infinity, -Infinity]) {
|
||||
assert.equal(clampBrushSize(value, [4, 1000]), 4);
|
||||
assert.equal(clampSoft(value, 20), 20);
|
||||
}
|
||||
assert.equal(clampSoft("24.6"), 25);
|
||||
assert.equal(clampSoft(-5), 0);
|
||||
assert.equal(clampSoft(101), 100);
|
||||
assert.equal(clampSoft(undefined, 120), 100);
|
||||
});
|
||||
|
||||
test("brush keys change small sizes by at least two pixels and stop at the bounds", () => {
|
||||
assert.equal(steppedBrushSize(2, true), 4);
|
||||
assert.equal(steppedBrushSize(4, false), 2);
|
||||
assert.equal(steppedBrushSize(2, false), 2);
|
||||
assert.equal(steppedBrushSize(100, false), 84);
|
||||
assert.equal(steppedBrushSize(2000, true), 2000);
|
||||
assert.equal(steppedSoft(50, true), 75);
|
||||
assert.equal(steppedSoft(50, false), 25);
|
||||
assert.equal(steppedSoft(90, true), 100);
|
||||
assert.equal(steppedSoft(10, false), 0);
|
||||
});
|
||||
|
||||
test("brush adjustment starts at the dead-zone boundary and breaks diagonal ties by size", () => {
|
||||
const edge = ADJUST_DEAD_ZONE;
|
||||
assert.equal(adjustAxis(edge - 1, -(edge - 1)), null);
|
||||
for (const direction of [-1, 1]) {
|
||||
assert.equal(adjustAxis(direction * edge, 0), "size");
|
||||
assert.equal(adjustAxis(0, direction * edge), "soft");
|
||||
assert.equal(adjustAxis(direction * edge, -edge), "size");
|
||||
}
|
||||
});
|
||||
|
||||
test("a tiny blur keeps the hard-brush edge, while a soft brush scales with its size", () => {
|
||||
// Below a quarter pixel of blur the preview uses a solid disc.
|
||||
assert.equal(brushProfile(1, 2, 0.5), 1);
|
||||
assert.equal(brushProfile(1.01, 2, 0.5), 0);
|
||||
assert.equal(brushReach(2, 0.5), 1);
|
||||
for (const soft of [0.25, 0.5, 1]) {
|
||||
assert.equal(brushReach(160, soft), 2 * brushReach(80, soft));
|
||||
for (const distance of [0, 10, 20, 40, 80]) {
|
||||
const value = brushProfile(distance, 80, soft);
|
||||
assert.ok(value >= 0 && value <= 1);
|
||||
assert.equal(brushProfile(distance * 2, 160, soft), value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -89,3 +89,60 @@ test("a failed lookup says why in plain words", () => {
|
||||
assert.equal(civitaiFailureText(new Error("Civitai answered 500.")), "Civitai answered 500.");
|
||||
assert.equal(civitaiFailureText(null), "The Civitai lookup failed.");
|
||||
});
|
||||
|
||||
test("parsing accepts an object at the size limit and rejects oversized or non-text bodies", () => {
|
||||
const body = '{"name":"' + "x".repeat(MAX_CIVITAI_BYTES - 11) + '"}';
|
||||
assert.equal(body.length, MAX_CIVITAI_BYTES);
|
||||
assert.equal(parseCivitaiBody(body).name.length, MAX_CIVITAI_BYTES - 11);
|
||||
assert.throws(() => parseCivitaiBody(body + " "), /too large/);
|
||||
for (const value of [null, undefined, {}, 1]) {
|
||||
assert.throws(() => parseCivitaiBody(value), /too large/);
|
||||
}
|
||||
for (const text of ["true", "false", "0", '"model"']) {
|
||||
assert.throws(() => parseCivitaiBody(text), /not what we expected/);
|
||||
}
|
||||
assert.throws(() => parseCivitaiBody('{"id":'), SyntaxError);
|
||||
});
|
||||
|
||||
test("numeric picture ratings take priority over legacy words and booleans", () => {
|
||||
for (const level of [1, 2, "2"]) {
|
||||
assert.equal(isMildPicture({ nsfwLevel: level, nsfw: "Mature" }), true);
|
||||
}
|
||||
for (const level of [4, 8, 16, 3]) {
|
||||
assert.equal(isMildPicture({ nsfwLevel: level, nsfw: false }), false);
|
||||
}
|
||||
assert.equal(isMildPicture({ nsfw: false }), true);
|
||||
assert.equal(isMildPicture({ nsfw: true }), false);
|
||||
assert.equal(isMildPicture({ nsfwLevel: 0, nsfw: "SOFT" }), true);
|
||||
assert.equal(isMildPicture(null), false);
|
||||
});
|
||||
|
||||
test("picture selection skips malformed entries even when any rating is allowed", () => {
|
||||
const still = { url: picture("still") };
|
||||
const info = { images: [
|
||||
null, {}, { url: 42 },
|
||||
{ url: picture("clip"), type: "video", nsfwLevel: 1 },
|
||||
{ url: "https://example.org/picture.jpeg", nsfwLevel: 1 },
|
||||
still,
|
||||
{ url: picture("later"), nsfwLevel: 1 },
|
||||
] };
|
||||
assert.equal(pickCivitaiPicture(info, true), still, "the first usable still wins without copying it");
|
||||
assert.equal(pickCivitaiPicture(info), info.images[6], "an unrated still is skipped in mild mode");
|
||||
for (const images of [null, {}, "pictures", []]) {
|
||||
assert.equal(pickCivitaiPicture({ images }, true), null);
|
||||
}
|
||||
});
|
||||
|
||||
test("picture resizing drops query and fragment and refuses incomplete picture paths", () => {
|
||||
assert.equal(smallPictureUrl(picture("a") + "?width=999&token=example#preview"),
|
||||
"https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/0b1c2d3e/width=450/a.jpeg");
|
||||
assert.equal(smallPictureUrl("https://image.civitai.com/a.jpeg"), null);
|
||||
assert.equal(smallPictureUrl("https://image.civitai.com/"), null);
|
||||
for (const url of [
|
||||
"https://civitai.com.example.org/file.jpeg",
|
||||
"https://civitai.com@other.example/file.jpeg",
|
||||
"file:///tmp/file.jpeg", "not a url",
|
||||
]) {
|
||||
assert.equal(isCivitaiAddress(url), false, url);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -145,3 +145,82 @@ test("restoreOutputLinks hands back only the links a replacement took", () => {
|
||||
assert.equal(graph.links.get(saver.inputs[0].link).origin_id, 1);
|
||||
assert.equal(preview.inputs[0].link, untouched, "a link that was never taken stays as it is");
|
||||
});
|
||||
|
||||
test("lookupLink keeps link id zero and tolerates absent tables and stale ids", () => {
|
||||
const link = { id: 0 };
|
||||
for (const links of [new Map([[0, link]]), { 0: link }]) {
|
||||
assert.equal(lookupLink({ links }, 0), link);
|
||||
assert.equal(lookupLink({ links }, 99), null);
|
||||
assert.equal(lookupLink({ links }, undefined), null);
|
||||
}
|
||||
assert.equal(lookupLink({}, 0), null);
|
||||
});
|
||||
|
||||
test("snapshotLinks skips stale links in an older plain-object graph", () => {
|
||||
const graph = { links: {
|
||||
0: { origin_id: 1, origin_slot: 2, target_id: 5, target_slot: 0 },
|
||||
8: { origin_id: 5, origin_slot: 1, target_id: 9, target_slot: 3 },
|
||||
} };
|
||||
const node = {
|
||||
inputs: [{ name: "image", link: 0 }, { name: "empty", link: null }, { name: "stale", link: 99 }],
|
||||
outputs: [{ name: "unused" }, { name: "mask", links: [99, 8] }, { name: "stale", links: [77] }],
|
||||
};
|
||||
const snapshot = snapshotLinks(node, graph);
|
||||
assert.deepEqual(snapshot, {
|
||||
inputs: [{ name: "image", originId: 1, originSlot: 2 }],
|
||||
outputs: [{ name: "mask", slot: 1, targets: [{ nodeId: 9, slot: 3 }] }],
|
||||
});
|
||||
graph.links[0].origin_slot = 4;
|
||||
graph.links[8].target_slot = 0;
|
||||
node.inputs[0].name = "changed";
|
||||
assert.equal(snapshot.inputs[0].originSlot, 2, "the snapshot owns its slot values");
|
||||
assert.equal(snapshot.inputs[0].name, "image");
|
||||
assert.equal(snapshot.outputs[0].targets[0].slot, 3);
|
||||
assert.deepEqual(snapshotLinks({}, graph), { inputs: [], outputs: [] });
|
||||
});
|
||||
|
||||
test("linkSlots refuses redirects outside the target's inputs before calling connectSlots", () => {
|
||||
const { makeNode } = makeGraph();
|
||||
const source = makeNode(1, [], ["out"]);
|
||||
const sink = makeNode(2, ["in"], []);
|
||||
source.connectSlots = () => assert.fail("a refused redirect must not connect");
|
||||
for (const redirect of [null, false, "0", -1, 4, NaN]) {
|
||||
sink.onBeforeConnectInput = function (slot, requested) {
|
||||
assert.equal(this, sink);
|
||||
assert.equal(slot, 0);
|
||||
assert.equal(requested, 0);
|
||||
return redirect;
|
||||
};
|
||||
assert.equal(linkSlots(source, 0, sink, 0), null, String(redirect));
|
||||
}
|
||||
});
|
||||
|
||||
test("restoreOutputLinks skips missing targets and reconnects a disconnected input", () => {
|
||||
const { graph, makeNode } = makeGraph();
|
||||
const original = makeNode(1, [], ["image", "mask"]);
|
||||
const sink = makeNode(2, ["mask"], []);
|
||||
restoreOutputLinks(original, [{ slot: 1, targets: [
|
||||
{ nodeId: 99, slot: 0 }, { nodeId: 2, slot: 0 }, { nodeId: 2, slot: 99 },
|
||||
] }], graph);
|
||||
const link = lookupLink(graph, sink.inputs[0].link);
|
||||
assert.equal(link.origin_id, original.id);
|
||||
assert.equal(link.origin_slot, 1);
|
||||
assert.equal(graph.links.size, 1);
|
||||
});
|
||||
|
||||
test("restoreOutputLinks restores the output slot even when the source node is unchanged", () => {
|
||||
const { graph, makeNode } = makeGraph();
|
||||
const original = makeNode(1, [], ["first image", "second image"]);
|
||||
const sink = makeNode(2, ["image"], []);
|
||||
linkSlots(original, 1, sink, 0);
|
||||
const { outputs } = snapshotLinks(original, graph);
|
||||
linkSlots(original, 0, sink, 0);
|
||||
|
||||
restoreOutputLinks(original, outputs, graph);
|
||||
const restored = lookupLink(graph, sink.inputs[0].link);
|
||||
assert.equal(restored.origin_id, original.id);
|
||||
assert.equal(restored.origin_slot, 1);
|
||||
assert.deepEqual(original.outputs[0].links, []);
|
||||
assert.deepEqual(original.outputs[1].links, [restored.id]);
|
||||
assert.equal(graph.links.size, 1);
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import test from "node:test";
|
||||
import {
|
||||
ensureNodeMinHeight,
|
||||
fillNodeHeight,
|
||||
growToNodeFloor,
|
||||
holdNodeMinWidth,
|
||||
holdNodeMinHeight,
|
||||
holdVueNodeMinWidth,
|
||||
@@ -319,3 +320,90 @@ test("a new node hugs its floor and a loading workflow keeps its saved height",
|
||||
assert.equal(nodeHeightAfterCardChange({ floor: 469, current: 700, change: "x" }), 700);
|
||||
assert.equal(nodeHeightAfterCardChange(), 0);
|
||||
});
|
||||
|
||||
test("a failed width probe restores the widget and allows the next measurement", () => {
|
||||
const own = () => ({ minWidth: 320, minHeight: 80 });
|
||||
const widget = { computeLayoutSize: own };
|
||||
assert.equal(measureLayoutWidthPadding({ computeSize() { throw new Error("not laid out"); } }, widget), null);
|
||||
assert.equal(widget.computeLayoutSize, own);
|
||||
assert.equal(measureLayoutWidthPadding(frontendNode([widget]), widget), 104);
|
||||
assert.equal(widget.computeLayoutSize, own);
|
||||
});
|
||||
|
||||
test("a width probe cannot recursively start another probe", () => {
|
||||
const widget = { computeLayoutSize: () => ({ minWidth: 320, minHeight: 80 }) };
|
||||
const own = widget.computeLayoutSize;
|
||||
const node = {
|
||||
computeSize() {
|
||||
assert.equal(measureLayoutWidthPadding(this, widget), null);
|
||||
return [widget.computeLayoutSize().minWidth + 80, 200];
|
||||
},
|
||||
};
|
||||
assert.equal(measureLayoutWidthPadding(node, widget), 80);
|
||||
assert.equal(widget.computeLayoutSize, own);
|
||||
});
|
||||
|
||||
test("growing to a changing width floor preserves height and leaves wider nodes alone", () => {
|
||||
const sizes = [];
|
||||
const dirty = [];
|
||||
const host = { style: {} };
|
||||
const node = {
|
||||
size: [240, 480],
|
||||
setSize(size) { sizes.push(size); this.size = size; },
|
||||
setDirtyCanvas(...args) { dirty.push(args); },
|
||||
};
|
||||
const widget = { node, element: { closest: () => host } };
|
||||
let floor = 320;
|
||||
holdNodeMinWidth(widget, () => floor);
|
||||
assert.equal(growToNodeFloor(widget, 1), true);
|
||||
assert.deepEqual(node.size, [320, 480]);
|
||||
assert.equal(host.style.minWidth, "320px");
|
||||
assert.deepEqual(dirty, [[true, true]]);
|
||||
assert.equal(growToNodeFloor(widget, 1), false);
|
||||
floor = 280;
|
||||
assert.equal(growToNodeFloor(widget, 1), false);
|
||||
assert.equal(host.style.minWidth, "280px", "the CSS floor can shrink without shrinking the node");
|
||||
floor = 400;
|
||||
assert.equal(growToNodeFloor(widget, 1), true);
|
||||
assert.deepEqual(sizes, [[320, 480], [400, 480]]);
|
||||
node.size[0] = NaN;
|
||||
assert.equal(growToNodeFloor(widget, 1), false);
|
||||
assert.equal(growToNodeFloor({ node: { size: [100, 100] } }), false);
|
||||
assert.equal(growToNodeFloor(null), false);
|
||||
});
|
||||
|
||||
test("growing a node measures its floor again on the next frame with a bounded retry", () => {
|
||||
const saved = globalThis.requestAnimationFrame;
|
||||
const frames = [];
|
||||
globalThis.requestAnimationFrame = (callback) => { frames.push(callback); };
|
||||
try {
|
||||
let floor = 320;
|
||||
const node = { size: [240, 480], setSize(size) { this.size = size; } };
|
||||
const widget = { node };
|
||||
holdNodeMinWidth(widget, () => floor);
|
||||
assert.equal(growToNodeFloor(widget), true);
|
||||
assert.equal(frames.length, 1);
|
||||
floor = 360;
|
||||
frames.shift()();
|
||||
assert.deepEqual(node.size, [360, 480]);
|
||||
assert.equal(frames.length, 0, "the second pass must not schedule another frame");
|
||||
} finally {
|
||||
if (saved === undefined) delete globalThis.requestAnimationFrame;
|
||||
else globalThis.requestAnimationFrame = saved;
|
||||
}
|
||||
});
|
||||
|
||||
test("dynamic panel floors sanitize invalid sizes and retain unrelated options", () => {
|
||||
let width = "240";
|
||||
let height = "160";
|
||||
const widget = { options: { serialize: false } };
|
||||
fillNodeHeight(widget, { minWidth: () => width, minHeight: () => height });
|
||||
assert.deepEqual(widget.computeLayoutSize(), { minWidth: 240, minHeight: 160 });
|
||||
for (const invalid of [-4, NaN, Infinity, undefined, "wide"]) {
|
||||
width = invalid;
|
||||
height = invalid;
|
||||
assert.deepEqual(widget.computeLayoutSize(), { minWidth: 0, minHeight: 0 });
|
||||
}
|
||||
assert.equal(widget.options.serialize, false);
|
||||
assert.equal(fillNodeHeight(null), null);
|
||||
});
|
||||
|
||||
@@ -214,3 +214,71 @@ 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);
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ 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,
|
||||
cardHeight, choiceLabel, comboValues, commitWidgetValue, holdsUnknownValue, markSocketless, resetUnknownValues, rowHeight, rowInLook, rowKind, rowMuted, rowTops,
|
||||
scrubSteps, segmentFits, socketWidgetY, visibleRows, widgetTooltip,
|
||||
} from "../js/shared/widget_card_math.mjs";
|
||||
|
||||
@@ -418,3 +418,49 @@ test("a choice's words come from the row: a map, or a function for lists only th
|
||||
// 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");
|
||||
});
|
||||
|
||||
test("dynamic combo values receive the widget and preserve numeric and boolean choices", () => {
|
||||
const values = [0, false, "0"];
|
||||
const widget = { type: "combo", value: 0, options: { values: (own) => {
|
||||
assert.equal(own, widget);
|
||||
return values;
|
||||
} } };
|
||||
assert.equal(comboValues(widget), values);
|
||||
assert.equal(holdsUnknownValue(widget), false);
|
||||
widget.value = false;
|
||||
assert.equal(holdsUnknownValue(widget), false);
|
||||
widget.value = true;
|
||||
assert.equal(holdsUnknownValue(widget), true);
|
||||
for (const value of [undefined, null, "choices", {}]) {
|
||||
assert.deepEqual(comboValues({ options: { values: () => value } }), []);
|
||||
}
|
||||
assert.deepEqual(comboValues(null), []);
|
||||
});
|
||||
|
||||
test("choice labels keep explicit empty and numeric labels and use values only as a fallback", () => {
|
||||
const row = { labels: { off: "", zero: 0, missing: null } };
|
||||
assert.equal(choiceLabel(row, "off"), "");
|
||||
assert.equal(choiceLabel(row, "zero"), 0);
|
||||
assert.equal(choiceLabel(row, "missing"), "missing");
|
||||
assert.equal(choiceLabel({ labels: () => undefined }, 0), "0");
|
||||
assert.equal(choiceLabel(null, false), "false");
|
||||
});
|
||||
|
||||
test("segmented choices include the exact label-length limit and reject the next character", () => {
|
||||
assert.equal(segmentFits(["a".repeat(13), "b".repeat(13)]), true);
|
||||
assert.equal(segmentFits(["a".repeat(13), "b".repeat(14)]), false);
|
||||
assert.equal(segmentFits([0, false]), true);
|
||||
assert.equal(segmentFits([]), false);
|
||||
assert.equal(segmentFits(null), false);
|
||||
});
|
||||
|
||||
test("scrub steps honor legacy increments, explicit precision and the pixel-range boundary", () => {
|
||||
assert.deepEqual(scrubSteps(), { step: 0.1, fineStep: 0.01, decimals: 2 });
|
||||
assert.deepEqual(scrubSteps({}, false), { step: 1, fineStep: 1, decimals: 0 });
|
||||
assert.deepEqual(scrubSteps({ step: 0.5 }), { step: 0.5, fineStep: 0.05, decimals: 2 });
|
||||
assert.deepEqual(scrubSteps({ step: 5, step2: 0.25, precision: 3, min: 0, max: 2 }),
|
||||
{ step: 0.25, fineStep: 0.25, decimals: 3 });
|
||||
assert.equal(scrubSteps({ min: 0, max: 2048 }, false, true).step, 1);
|
||||
assert.equal(scrubSteps({ min: 0, max: 2049 }, false, true).step, 8);
|
||||
assert.equal(scrubSteps({}, false, true).step, 1, "an unknown range is not assumed to be a wide pixel size");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user