Files
Will Miao f8ecc00f6b feat(workflow): user-controlled send-to-workflow target filtering
- Global exclusion lists (node classes + title patterns) in settings,
  applied at the shared filterRegistryNodes choke point; settings UI
  section is hidden in standalone mode
- Per-node "Exclude from Send Targets" context-menu toggle persisted
  in the workflow JSON (lm_exclude_send)
- Quick-exclude button on node selector items writes the comfy class
  to the exclusion list
- Precedence: per-node exclusion > explicit marker > global lists
- interceptModeChange now supports multiple listeners; registry hooks
  mode changes so mute/bypass refreshes candidates immediately
2026-10-18 09:42:44 +08:00

135 lines
3.7 KiB
JavaScript

import { beforeEach, describe, expect, it, vi } from "vitest";
const { APP_MODULE, UTILS_MODULE } = vi.hoisted(() => ({
APP_MODULE: new URL("../../../scripts/app.js", import.meta.url).pathname,
UTILS_MODULE: new URL("../../../web/comfyui/utils.js", import.meta.url).pathname,
}));
vi.mock(APP_MODULE, () => ({
app: {
graph: null,
registerExtension: vi.fn(),
ui: {
settings: {
getSettingValue: vi.fn(),
},
},
},
}));
describe("interceptModeChange", () => {
let interceptModeChange;
beforeEach(async () => {
vi.resetModules();
({ interceptModeChange } = await import(UTILS_MODULE));
});
describe("legacy frontend (mode as plain data property)", () => {
it("reads and writes the mode through the installed accessor", () => {
const node = { mode: 0 };
interceptModeChange(node, vi.fn());
node.mode = 4;
expect(node.mode).toBe(4);
});
it("invokes the callback only when the mode actually changes", () => {
const node = { mode: 0 };
const onModeChange = vi.fn();
interceptModeChange(node, onModeChange);
node.mode = 0;
expect(onModeChange).not.toHaveBeenCalled();
node.mode = 4;
expect(onModeChange).toHaveBeenCalledWith(4, 0);
});
});
describe("ECS frontend (mode as prototype accessor backed by shell state)", () => {
function createEcsNode() {
class LGraphNode {
constructor() {
this._state = { mode: 0 };
}
get mode() {
return this._state.mode;
}
set mode(value) {
this._state.mode = value;
}
}
return new LGraphNode();
}
it("keeps writes flowing into the shell state so serialization stays correct", () => {
const node = createEcsNode();
interceptModeChange(node, vi.fn());
node.mode = 4;
expect(node._state.mode).toBe(4);
expect(node.mode).toBe(4);
});
it("invokes the callback with new and old mode on change", () => {
const node = createEcsNode();
const onModeChange = vi.fn();
interceptModeChange(node, onModeChange);
node.mode = 4;
expect(onModeChange).toHaveBeenCalledWith(4, 0);
node.mode = 4;
expect(onModeChange).toHaveBeenCalledTimes(1);
node.mode = 0;
expect(onModeChange).toHaveBeenCalledWith(0, 4);
});
it("keeps the installed accessor configurable so it can be redefined", () => {
const node = createEcsNode();
interceptModeChange(node, vi.fn());
const descriptor = Object.getOwnPropertyDescriptor(node, "mode");
expect(descriptor.configurable).toBe(true);
});
});
describe("multiple listeners", () => {
it("notifies every registered listener while intercepting only once", () => {
const node = { mode: 0 };
const first = vi.fn();
const second = vi.fn();
interceptModeChange(node, first);
interceptModeChange(node, second);
node.mode = 2;
expect(first).toHaveBeenCalledWith(2, 0);
expect(second).toHaveBeenCalledWith(2, 0);
// The second registration must not replace the accessor: the value is
// still tracked across registrations.
node.mode = 4;
expect(first).toHaveBeenCalledWith(4, 2);
expect(second).toHaveBeenCalledWith(4, 2);
});
it("keeps notifying remaining listeners when one of them throws", () => {
const node = { mode: 0 };
const failing = vi.fn(() => {
throw new Error("boom");
});
const healthy = vi.fn();
interceptModeChange(node, failing);
interceptModeChange(node, healthy);
node.mode = 2;
expect(failing).toHaveBeenCalledWith(2, 0);
expect(healthy).toHaveBeenCalledWith(2, 0);
expect(node.mode).toBe(2);
});
});
});