With format set to gif or webp the file was saved, but the node's stage stayed empty and said "Execution finished without video metadata". The node has shown a gif or webp in an <img> since the two formats were added, because a <video> cannot play either. The run's result was still searched with a list of video file endings only, so for these two nothing was found and the <img> was never filled. The list of picture endings now sits beside that lookup in js/shared/video_preview.mjs, and both the lookup and the choice of tag read it. A png is still not taken. In the classic renderer a saved webp was also drawn a second time under the stage. ComfyUI shows an animated webp in a widget of its own, neither its video preview nor its picture preview, and only those two were stood down. suppressCoreVideoPreview hides that widget as well. The node's smallest height with a saved webp goes from 469 px back to 415 px. Tests: the lookup finds a saved gif and webp and sends them to the <img>; every row of the backend's VIDEO_FORMATS is read from its source and has to be found and sent to the right tag; the animated widget is hidden on arrival and when it is already on the node.
180 lines
8.0 KiB
JavaScript
180 lines
8.0 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import {
|
|
CORE_ANIMATED_PREVIEW_WIDGET,
|
|
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 suppressor also stands down Nodes 2.0's own copy", () => {
|
|
// Nodes 2.0 draws a node's output images under its widgets unless the node
|
|
// sets hideOutputImages (classic never reads it). Left on, Save Image showed
|
|
// the result twice there and grew past the node under it.
|
|
const node = fakeNode();
|
|
assert.equal(node.hideOutputImages, undefined);
|
|
suppressCoreImagePreview(node);
|
|
assert.equal(node.hideOutputImages, true);
|
|
});
|
|
|
|
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");
|
|
});
|
|
|
|
// The Crop + Rotate + Pad nodes send no picture of their own, so a core
|
|
// preview on one can only be another node's result: in practice a run from
|
|
// another workflow tab, filed under the same node id. All three kinds stand
|
|
// both core previews down, not just the image node.
|
|
test("every transform node stands down both core previews", async () => {
|
|
const { readFileSync } = await import("node:fs");
|
|
const source = readFileSync(new URL("../js/shared/transform_editor.mjs", import.meta.url), "utf-8");
|
|
const install = source.slice(source.indexOf("export function installTransformNode"), source.indexOf("function installVideoDrop"));
|
|
assert.match(install, /\n\s*suppressCoreImagePreview\(node\);/);
|
|
assert.match(install, /\n\s*suppressCoreVideoPreview\(node\);/);
|
|
assert.doesNotMatch(install, /if \(kind === "image"\) suppressCoreImagePreview/);
|
|
});
|
|
|
|
test("the video suppression also stands down Nodes 2.0's own output copy", () => {
|
|
const node = fakeNode();
|
|
suppressCoreVideoPreview(node);
|
|
assert.equal(node.hideOutputImages, true);
|
|
});
|
|
|
|
test("the video suppression also hides the animated picture preview", () => {
|
|
// A clip saved as an animated webp is a picture to the frontend. It comes
|
|
// in a third widget, neither the video one nor the canvas picture, so
|
|
// Save Video drew it twice in the classic renderer: in its stage and again
|
|
// underneath, where it also made the node's smallest height 54 px taller.
|
|
const node = fakeNode();
|
|
const early = node.addDOMWidget(CORE_ANIMATED_PREVIEW_WIDGET, "img", { style: {} });
|
|
suppressCoreVideoPreview(node);
|
|
assert.ok(isHidden(early), "one already on the node is swept up");
|
|
const element = { style: {} };
|
|
const late = node.addDOMWidget(CORE_ANIMATED_PREVIEW_WIDGET, "img", element);
|
|
assert.ok(isHidden(late), "one the frontend adds after a run is hidden on arrival");
|
|
assert.equal(element.style.display, "none");
|
|
// The video widget is still caught, and the node's own panel is left alone.
|
|
assert.ok(isHidden(node.addDOMWidget(CORE_VIDEO_PREVIEW_WIDGET, "video", { style: {} })));
|
|
assert.equal(node.addDOMWidget("ausboss_save_video_viewer", "ausboss_video", { style: {} }).hidden, undefined);
|
|
});
|
|
|
|
// Either way of standing Nodes 2.0's copy down: setting the flag, or calling
|
|
// one of the shared suppressors above, which set it.
|
|
function standsVueCopyDown(source) {
|
|
if (/\bhideOutputImages\s*=\s*true\b/.test(source)) return true;
|
|
const shared = source.match(/import\s*\{([^}]*)\}\s*from\s*"[^"]*core_preview\.mjs"/)?.[1] ?? "";
|
|
return ["suppressCoreImagePreview", "suppressCoreVideoPreview"]
|
|
.some((name) => new RegExp(`\\b${name}\\b`).test(shared) && source.includes(`${name}(`));
|
|
}
|
|
|
|
// A file that stands the classic preview down by hand names its widget.
|
|
// Nodes 2.0 has no such widget to remove, only the flag. Load Image + Pad
|
|
// removed the widget and never set the flag, so in Nodes 2.0 its picture
|
|
// showed twice and the second copy took the pad stage's height.
|
|
test("every file that stands the classic image preview down also stands Nodes 2.0's copy down", async () => {
|
|
const { readdirSync, readFileSync } = await import("node:fs");
|
|
const { fileURLToPath } = await import("node:url");
|
|
const { join } = await import("node:path");
|
|
const js = fileURLToPath(new URL("../js", import.meta.url));
|
|
const files = readdirSync(js, { recursive: true }).map(String).filter((file) => /\.m?js$/.test(file)).map((file) => file.replaceAll("\\", "/"));
|
|
const byHand = files.filter((file) => readFileSync(join(js, file), "utf-8").includes(`"${CORE_IMAGE_PREVIEW_WIDGET}"`));
|
|
assert.ok(byHand.includes("shared/core_preview.mjs"), "the scan found the pack's scripts");
|
|
const twice = byHand.filter((file) => !standsVueCopyDown(readFileSync(join(js, file), "utf-8")));
|
|
assert.deepEqual(twice, [], "these show the node's picture twice in Nodes 2.0");
|
|
// Load Image + Pad by name, however it comes to do it.
|
|
assert.ok(standsVueCopyDown(readFileSync(join(js, "load_image_pad/index.js"), "utf-8")));
|
|
});
|
|
|
|
test("the scan tells a file that sets the flag from one that only removes the widget", () => {
|
|
assert.equal(standsVueCopyDown("node.hideOutputImages = true;"), true);
|
|
assert.equal(standsVueCopyDown('import { suppressCoreVideoPreview } from "./core_preview.mjs";\nsuppressCoreVideoPreview(node);'), true);
|
|
// Imported but never called, and a local function of the same name.
|
|
assert.equal(standsVueCopyDown('import { suppressCoreVideoPreview } from "./core_preview.mjs";'), false);
|
|
assert.equal(standsVueCopyDown("function suppressCoreImagePreview(node) { node.widgets.splice(0, 1); }\nsuppressCoreImagePreview(node);"), false);
|
|
});
|