Five things surfaced by release testing. Node search: ComfyUI's search dialog cuts at 64 results and ranks by [match tier, global popularity, name length], where tier 1 needs the name to START with what was typed. Popularity is a fixed global table in which every node from a new pack scores zero, so tier is the only lever. As "Refine Mask" this node came 121st for "mask" and was never on screen. Renamed to Mask Refine, which is 15th, and kept "refine mask" as an alias. The mapping key is untouched, so saved workflows are unaffected. Aliases alone cannot fix this: the frontend indexes them but drops them from the tier scoring because they are a list rather than a string, so they decide whether a node is findable, not where it lands. Previews: Mask Refine, LaMa Inpaint and Select Frame showed whatever fed their input, which is nothing when a segmentation node is upstream - Mask Refine's panel was blank however the mask turned out. Each now returns its result as a preview and the panel shows that, falling back to the input before the first run. A node does not need to be an OUTPUT_NODE for this; returning a ui payload is enough. ComfyUI's own preview for these nodes is stood down so the picture appears once, in the panel, rather than also underneath the node. That also fixes LaMa finishing a single image with an empty panel: frame previews were only attached for batches over one frame, on the assumption that one image would preview through the output path, which a non-output node does not have. Mask Refine opens on expand and blur with the other five behind MORE. Hidden widgets keep their values and their saved positions. AUTO sets both from the mask's size, scaled off the 8/4 hand-tuned for the watermark workflow at 576px, because a feather is a fraction of the picture rather than a fixed pixel count. Save Video gains nvenc h264/h265, webm av1, ProRes mov, lossless ffv1 mkv, gif and webp, plus pingpong and save_metadata. Every format is encoded and read back in the tests rather than trusted from its table row. fps steps in whole frames but stays FLOAT: an INT would refuse Load Video's fps link and round 29.97 to 30, drifting picture off audio. mov files now carry the embedded workflow, which they silently did not. The reload button is gone - it only re-fetched what was already on screen. Not done: Select Frame's preview updates on a run, not as the number changes. Seeking to a frame needs the source fps, which the browser never learns, so live scrubbing needs a server route - the surface Frame Chooser was just demoted for.
97 lines
3.0 KiB
JavaScript
97 lines
3.0 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import {
|
|
CORE_IMAGE_PREVIEW_WIDGET,
|
|
CORE_VIDEO_PREVIEW_WIDGET,
|
|
hideCanvasWidget,
|
|
suppressCoreImagePreview,
|
|
suppressCoreVideoPreview,
|
|
} from "../js/shared/core_preview.mjs";
|
|
|
|
function fakeNode() {
|
|
const node = {
|
|
widgets: [],
|
|
addCustomWidget(widget) {
|
|
this.widgets.push(widget);
|
|
return widget;
|
|
},
|
|
addDOMWidget(name, type, element) {
|
|
const widget = { name, type, element };
|
|
this.widgets.push(widget);
|
|
return widget;
|
|
},
|
|
};
|
|
return node;
|
|
}
|
|
|
|
const isHidden = (widget) =>
|
|
widget.hidden === true && widget.computeSize()[1] === -4 &&
|
|
widget.computeLayoutSize().minHeight === 0;
|
|
|
|
test("a widget the frontend adds later is hidden on arrival", () => {
|
|
const node = fakeNode();
|
|
suppressCoreImagePreview(node);
|
|
const widget = node.addCustomWidget({ name: CORE_IMAGE_PREVIEW_WIDGET });
|
|
assert.ok(isHidden(widget));
|
|
});
|
|
|
|
test("a widget already on the node when we arrive is swept up", () => {
|
|
const node = fakeNode();
|
|
const widget = { name: CORE_IMAGE_PREVIEW_WIDGET };
|
|
node.widgets.push(widget);
|
|
suppressCoreImagePreview(node);
|
|
assert.ok(isHidden(widget));
|
|
});
|
|
|
|
test("only the core preview widget is touched", () => {
|
|
const node = fakeNode();
|
|
suppressCoreImagePreview(node);
|
|
const mine = node.addCustomWidget({ name: "ausboss_input_preview" });
|
|
assert.equal(mine.hidden, undefined);
|
|
assert.equal(typeof mine.computeSize, "undefined");
|
|
});
|
|
|
|
test("the image and video suppressors do not stand in for each other", () => {
|
|
// They wrap different methods and are flagged separately, so a node that
|
|
// draws both kinds can ask for both.
|
|
const node = fakeNode();
|
|
suppressCoreImagePreview(node);
|
|
const video = node.addDOMWidget(CORE_VIDEO_PREVIEW_WIDGET, "video", {
|
|
style: {},
|
|
});
|
|
assert.equal(video.hidden, undefined, "image suppression must not hide the video widget");
|
|
|
|
suppressCoreVideoPreview(node);
|
|
const second = node.addDOMWidget(CORE_VIDEO_PREVIEW_WIDGET, "video", { style: {} });
|
|
assert.ok(isHidden(second));
|
|
// ...and the one added before suppression is swept on the way in.
|
|
assert.ok(isHidden(video));
|
|
});
|
|
|
|
test("suppressing twice keeps one wrapper, not a growing chain", () => {
|
|
const node = fakeNode();
|
|
const original = node.addCustomWidget;
|
|
suppressCoreImagePreview(node);
|
|
const wrapped = node.addCustomWidget;
|
|
suppressCoreImagePreview(node);
|
|
assert.notEqual(wrapped, original);
|
|
assert.equal(node.addCustomWidget, wrapped);
|
|
});
|
|
|
|
test("hiding is idempotent and never re-stashes a live method", () => {
|
|
const widget = { name: CORE_IMAGE_PREVIEW_WIDGET };
|
|
hideCanvasWidget(widget);
|
|
const hiddenDraw = widget.draw;
|
|
hideCanvasWidget(widget);
|
|
assert.equal(widget.draw, hiddenDraw);
|
|
});
|
|
|
|
test("the DOM widget's element is hidden along with it", () => {
|
|
const node = fakeNode();
|
|
suppressCoreVideoPreview(node);
|
|
const element = { style: {} };
|
|
node.addDOMWidget(CORE_VIDEO_PREVIEW_WIDGET, "video", element);
|
|
assert.equal(element.style.display, "none");
|
|
});
|