Files
ComfyUI-Lora-Manager/tests/frontend/utils/interceptModeChange.test.js
T
Will Miao 067e605e75 fix: keep bypass/mute state on frontend 1.53+ node shell state (#1123)
ComfyUI frontend 1.53 turned LGraphNode.mode into a prototype accessor
backed by node._state, and serialize() now reads that state directly.
Redefining mode on the instance shadowed the setter, so bypass/mute
never reached the serialized workflow and silently reverted to Always
on save/reload or workflow tab switch.

Add interceptModeChange() in web/comfyui/utils.js: it delegates to the
prototype accessor when one exists (observing changes only), and falls
back to the legacy closure accessor on older frontends. Use it in
lora_loader.js and in the Vue widgets' setupModeChangeHandler, which
covers the LoRA provider/aggregator nodes with the same latent bug.
2026-09-25 18:12:01 +08:00

100 lines
2.6 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);
});
});
});