200 lines
6.1 KiB
TypeScript
200 lines
6.1 KiB
TypeScript
// SimpleSyrup - workflow-focused ComfyUI extensions for image generation
|
|
// Copyright (C) 2026 Artificial Sweetener and contributors
|
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
|
|
import { NativePreviewNavigator } from "../src/nativePreviewNavigator";
|
|
import type { ComfyApp } from "../src/types";
|
|
import { createFakeComfyApp } from "./testUtils";
|
|
|
|
describe("NativePreviewNavigator", () => {
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
document.body.replaceChildren();
|
|
});
|
|
|
|
it("clicks the matching Nodes 2.0 native gallery item", () => {
|
|
const app = previewApp();
|
|
const node = { id: 7, imageIndex: null as number | null };
|
|
const root = document.createElement("section");
|
|
root.dataset.nodeId = "7";
|
|
const first = nativeImageButton("first.png");
|
|
const second = nativeImageButton("second.png");
|
|
root.append(first.button, second.button);
|
|
document.body.append(root);
|
|
const clicked = vi.fn();
|
|
second.button.addEventListener("click", clicked);
|
|
|
|
const navigator = new NativePreviewNavigator(app, node);
|
|
navigator.inspect(1);
|
|
|
|
expect(clicked).toHaveBeenCalledOnce();
|
|
expect(node.imageIndex).toBeNull();
|
|
navigator.dispose();
|
|
});
|
|
|
|
it("waits for a restored Nodes 2.0 gallery before opening detail", async () => {
|
|
const app = previewApp();
|
|
const node = { id: 7, imageIndex: null as number | null };
|
|
const root = document.createElement("section");
|
|
root.dataset.nodeId = "7";
|
|
document.body.append(root);
|
|
const navigator = new NativePreviewNavigator(app, node);
|
|
const second = nativeImageButton("second.png");
|
|
const clicked = vi.fn();
|
|
second.button.addEventListener("click", clicked);
|
|
|
|
navigator.inspect(1);
|
|
root.append(nativeImageButton("first.png").button, second.button);
|
|
|
|
await vi.waitFor(() => {
|
|
expect(clicked).toHaveBeenCalledOnce();
|
|
});
|
|
navigator.dispose();
|
|
});
|
|
|
|
it("uses Comfy's native return-to-grid control in Nodes 2.0", () => {
|
|
const app = previewApp();
|
|
const node = { id: 7, imageIndex: null as number | null };
|
|
const root = document.createElement("section");
|
|
root.dataset.nodeId = "7";
|
|
const grid = document.createElement("button");
|
|
grid.setAttribute("aria-label", "Grid view");
|
|
const clicked = vi.fn();
|
|
grid.addEventListener("click", clicked);
|
|
root.append(grid);
|
|
document.body.append(root);
|
|
const navigator = new NativePreviewNavigator(app, node);
|
|
|
|
navigator.showGrid();
|
|
|
|
expect(clicked).toHaveBeenCalledOnce();
|
|
navigator.dispose();
|
|
});
|
|
|
|
it("uses the Nodes 1.0 image index when no DOM gallery is mounted", async () => {
|
|
const app = createFakeComfyApp();
|
|
const dirty = vi.fn();
|
|
const node = {
|
|
id: 7,
|
|
imageIndex: null as number | null,
|
|
imageRects: [[0, 0, 10, 10]],
|
|
imgs: [document.createElement("img"), document.createElement("img")],
|
|
graph: { setDirtyCanvas: dirty }
|
|
};
|
|
|
|
const navigator = new NativePreviewNavigator(app, node);
|
|
navigator.inspect(1);
|
|
|
|
await vi.waitFor(() => {
|
|
expect(node.imageIndex).toBe(1);
|
|
expect(node.imageRects).toBeUndefined();
|
|
expect(dirty).toHaveBeenCalledWith(true, true);
|
|
});
|
|
navigator.dispose();
|
|
});
|
|
|
|
it("uses Nodes 1.0 native pointer selection when the canvas is available", async () => {
|
|
const app = createFakeComfyApp();
|
|
app.canvas = { graph_mouse: [12, 34] } as unknown as NonNullable<
|
|
ComfyApp["canvas"]
|
|
>;
|
|
const dirty = vi.fn<(foreground: boolean, background: boolean) => void>();
|
|
const node: {
|
|
id: number;
|
|
imageIndex: number | null;
|
|
imgs: HTMLImageElement[];
|
|
pointerDown?: { index: number | null; pos: [number, number] };
|
|
graph: {
|
|
setDirtyCanvas: (foreground: boolean, background: boolean) => void;
|
|
};
|
|
} = {
|
|
id: 7,
|
|
imageIndex: null,
|
|
imgs: [document.createElement("img"), document.createElement("img")],
|
|
graph: { setDirtyCanvas: dirty }
|
|
};
|
|
|
|
const navigator = new NativePreviewNavigator(app, node);
|
|
navigator.inspect(1);
|
|
|
|
await vi.waitFor(() => {
|
|
expect(node.pointerDown).toEqual({ index: 1, pos: [12, 34] });
|
|
expect(node.imageIndex).toBe(1);
|
|
expect(dirty).toHaveBeenCalledWith(true, true);
|
|
});
|
|
navigator.dispose();
|
|
});
|
|
|
|
it("waits for Nodes 1.0 images before selecting native detail", async () => {
|
|
vi.useFakeTimers();
|
|
vi.stubGlobal(
|
|
"requestAnimationFrame",
|
|
(callback: FrameRequestCallback): number =>
|
|
window.setTimeout(() => {
|
|
callback(performance.now());
|
|
}, 16)
|
|
);
|
|
vi.stubGlobal("cancelAnimationFrame", (handle: number): void => {
|
|
window.clearTimeout(handle);
|
|
});
|
|
const app = createFakeComfyApp();
|
|
const node: {
|
|
id: number;
|
|
imageIndex: number | null;
|
|
imgs?: HTMLImageElement[];
|
|
graph: {
|
|
setDirtyCanvas: ReturnType<
|
|
typeof vi.fn<(foreground: boolean, background: boolean) => void>
|
|
>;
|
|
};
|
|
} = {
|
|
id: 7,
|
|
imageIndex: null as number | null,
|
|
graph: {
|
|
setDirtyCanvas:
|
|
vi.fn<(foreground: boolean, background: boolean) => void>()
|
|
}
|
|
};
|
|
const navigator = new NativePreviewNavigator(app, node);
|
|
|
|
navigator.inspect(1);
|
|
await vi.advanceTimersByTimeAsync(16);
|
|
|
|
expect(node.imageIndex).toBeNull();
|
|
|
|
node.imgs = [document.createElement("img"), document.createElement("img")];
|
|
await vi.advanceTimersByTimeAsync(16);
|
|
|
|
expect(node.imageIndex).toBe(1);
|
|
navigator.dispose();
|
|
});
|
|
|
|
});
|
|
|
|
function previewApp(): ComfyApp {
|
|
const app = createFakeComfyApp();
|
|
app.nodeOutputs = {
|
|
"7": {
|
|
images: [reference("first.png"), reference("second.png")]
|
|
}
|
|
};
|
|
return app;
|
|
}
|
|
|
|
function reference(filename: string) {
|
|
return { filename, subfolder: "", type: "temp" as const };
|
|
}
|
|
|
|
function nativeImageButton(filename: string): {
|
|
button: HTMLButtonElement;
|
|
image: HTMLImageElement;
|
|
} {
|
|
const button = document.createElement("button");
|
|
const image = document.createElement("img");
|
|
image.src = `/view?filename=${filename}&subfolder=&type=temp`;
|
|
button.append(image);
|
|
return { button, image };
|
|
}
|