Files
Artificial-Sweetener-Simple…/web/tests/nativeNodePreview.test.ts
T

168 lines
4.7 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 { describe, expect, it, vi } from "vitest";
import { NativeNodePreview } from "../src/nativeNodePreview";
import type {
ComfyApi,
ComfyApp,
ComfyGraph,
ComfySetting,
ComfySettingDefinition,
SettingValue
} from "../src/types";
describe("NativeNodePreview", () => {
it("publishes execution-shaped output through Comfy's native event path", () => {
const app = { nodeOutputs: {} } as ComfyApp;
const events = new EventTarget();
const api = events as ComfyApi;
const listener = vi.fn();
events.addEventListener("executed", (event: Event) => {
listener((event as CustomEvent).detail);
});
const preview = new NativeNodePreview(app, api, { id: 42 });
const output = {
images: [{ filename: "one.png", subfolder: "", type: "input" as const }]
};
preview.publish(output);
expect(app.nodeOutputs?.["42"]).toBe(output);
expect(listener).toHaveBeenCalledWith({
node: "42",
display_node: "42",
output
});
});
it("publishes subgraph previews under the locator consumed by Nodes 2.0", () => {
const subgraph = { id: "cube-definition" };
const rootGraph = {
id: "root",
isRootGraph: true,
nodes: [{ id: "cube-instance", subgraph }]
};
const app = previewApp(rootGraph);
const events = new EventTarget();
const api = events as ComfyApi;
const listener = vi.fn();
events.addEventListener("executed", (event: Event) => {
listener((event as CustomEvent).detail);
});
const preview = new NativeNodePreview(app, api, {
id: "mask-loader",
graph: subgraph
});
const output = {
images: [{ filename: "mask.png", subfolder: "", type: "temp" as const }]
};
preview.publish(output);
expect(app.nodeOutputs?.["mask-loader"]).toBe(output);
expect(app.nodeOutputs?.["cube-definition:mask-loader"]).toBe(output);
expect(listener).toHaveBeenCalledWith({
node: "cube-instance:mask-loader",
display_node: "cube-instance:mask-loader",
output
});
});
it("publishes shared subgraph previews for every instance path", () => {
const subgraph = { id: "cube-definition", nodes: [] };
const nestedGraph = {
id: "nested-definition",
nodes: [{ id: "nested-cube", subgraph }]
};
const rootGraph = {
id: "root",
isRootGraph: true,
nodes: [
{ id: "cube-a", subgraph },
{ id: "cube-b", subgraph },
{ id: "container", subgraph: nestedGraph }
]
};
const app = previewApp(rootGraph);
const events = new EventTarget();
const api = events as ComfyApi;
const executionIds: string[] = [];
const listener = vi.fn((executionId: string) => {
executionIds.push(executionId);
});
events.addEventListener("executed", (event: Event) => {
listener(executedNodeId(event));
});
const preview = new NativeNodePreview(app, api, {
id: "mask-loader",
graph: subgraph
});
preview.clear();
expect(executionIds).toEqual([
"cube-a:mask-loader",
"cube-b:mask-loader",
"container:nested-cube:mask-loader"
]);
});
it("clears the same native output surface", () => {
const app = { nodeOutputs: {} } as ComfyApp;
const api = new EventTarget() as ComfyApi;
const preview = new NativeNodePreview(app, api, { id: 42 });
preview.clear();
expect(app.nodeOutputs?.["42"]).toEqual({ images: [], animated: [] });
});
it("notifies renderer adapters after native output changes", () => {
const app = { nodeOutputs: {} } as ComfyApp;
const api = new EventTarget() as ComfyApi;
const preview = new NativeNodePreview(app, api, { id: 42 });
const listener = vi.fn();
const unsubscribe = preview.subscribe(listener);
preview.clear();
unsubscribe();
preview.clear();
expect(listener).toHaveBeenCalledOnce();
});
});
function executedNodeId(event: Event): string {
const detail: unknown = (event as CustomEvent<unknown>).detail;
if (
typeof detail !== "object" ||
detail === null ||
!("node" in detail) ||
typeof detail.node !== "string"
) {
throw new TypeError("Executed event did not include a node identity.");
}
return detail.node;
}
function previewApp(rootGraph: ComfyGraph): ComfyApp {
return {
nodeOutputs: {},
rootGraph,
ui: {
settings: {
addSetting<TValue extends SettingValue>(
definition: ComfySettingDefinition<TValue>
): ComfySetting<TValue> {
return { value: definition.defaultValue };
}
}
},
registerExtension: () => undefined
};
}