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); });