mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-22 05:32:12 -03:00
After refactoring mode change logic from lora_stacker.js to main.ts (compiled to lora-manager-widgets.js), updateConnectedTriggerWords became a bundled inline function, making the mock from utils.js ineffective. Changes: - Import Vue widgets module in test to register mode change handlers - Call both extensions' beforeRegisterNodeDef when setting up nodes - Fix test node structure with proper widget setup (input widget with options property and loras widget with test data) - Update test assertions to verify mode setter configuration via property descriptor check instead of mocking bundled functions Also fix Lora Cycler widget min height from 316 to 314 pixels. Co-Authored-By: Claude <noreply@anthropic.com>
294 lines
8.9 KiB
JavaScript
294 lines
8.9 KiB
JavaScript
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
|
|
const {
|
|
APP_MODULE,
|
|
API_MODULE,
|
|
UTILS_MODULE,
|
|
LORAS_WIDGET_MODULE,
|
|
LORA_LOADER_MODULE,
|
|
LORA_STACKER_MODULE,
|
|
VUE_WIDGETS_MODULE,
|
|
} = vi.hoisted(() => ({
|
|
APP_MODULE: new URL("../../../scripts/app.js", import.meta.url).pathname,
|
|
API_MODULE: new URL("../../../scripts/api.js", import.meta.url).pathname,
|
|
UTILS_MODULE: new URL("../../../web/comfyui/utils.js", import.meta.url).pathname,
|
|
LORAS_WIDGET_MODULE: new URL("../../../web/comfyui/loras_widget.js", import.meta.url).pathname,
|
|
LORA_LOADER_MODULE: new URL("../../../web/comfyui/lora_loader.js", import.meta.url).pathname,
|
|
LORA_STACKER_MODULE: new URL("../../../web/comfyui/lora_stacker.js", import.meta.url).pathname,
|
|
VUE_WIDGETS_MODULE: new URL("../../../web/comfyui/vue-widgets/lora-manager-widgets.js", import.meta.url).pathname,
|
|
}));
|
|
|
|
const extensionState = {
|
|
loraLoader: null,
|
|
loraStacker: null,
|
|
vueWidgets: null,
|
|
};
|
|
const registerExtensionMock = vi.fn((extension) => {
|
|
if (extension.name === "LoraManager.LoraLoader") {
|
|
extensionState.loraLoader = extension;
|
|
} else if (extension.name === "LoraManager.LoraStacker") {
|
|
extensionState.loraStacker = extension;
|
|
} else if (extension.name === "LoraManager.VueWidgets") {
|
|
extensionState.vueWidgets = extension;
|
|
}
|
|
});
|
|
|
|
vi.mock(APP_MODULE, () => ({
|
|
app: {
|
|
registerExtension: registerExtensionMock,
|
|
graph: {},
|
|
},
|
|
}));
|
|
|
|
vi.mock(API_MODULE, () => ({
|
|
api: {
|
|
addEventListener: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
const collectActiveLorasFromChain = vi.fn();
|
|
const updateConnectedTriggerWords = vi.fn();
|
|
const updateDownstreamLoaders = vi.fn();
|
|
const getActiveLorasFromNode = vi.fn();
|
|
const mergeLoras = vi.fn();
|
|
const setupInputWidgetWithAutocomplete = vi.fn();
|
|
const getAllGraphNodes = vi.fn();
|
|
const getNodeFromGraph = vi.fn();
|
|
const getNodeKey = vi.fn();
|
|
const getLinkFromGraph = vi.fn();
|
|
const chainCallback = vi.fn((proto, property, callback) => {
|
|
proto[property] = callback;
|
|
});
|
|
|
|
vi.mock(UTILS_MODULE, async (importOriginal) => {
|
|
const actual = await importOriginal();
|
|
return {
|
|
...actual,
|
|
collectActiveLorasFromChain,
|
|
updateConnectedTriggerWords,
|
|
updateDownstreamLoaders,
|
|
getActiveLorasFromNode,
|
|
mergeLoras,
|
|
setupInputWidgetWithAutocomplete,
|
|
chainCallback,
|
|
getAllGraphNodes,
|
|
getNodeFromGraph,
|
|
getNodeKey,
|
|
getLinkFromGraph,
|
|
};
|
|
});
|
|
|
|
const addLorasWidget = vi.fn();
|
|
|
|
vi.mock(LORAS_WIDGET_MODULE, () => ({
|
|
addLorasWidget,
|
|
}));
|
|
|
|
describe("Node mode change handling", () => {
|
|
beforeEach(() => {
|
|
vi.resetModules();
|
|
|
|
extensionState.loraLoader = null;
|
|
extensionState.loraStacker = null;
|
|
extensionState.vueWidgets = null;
|
|
registerExtensionMock.mockClear();
|
|
|
|
collectActiveLorasFromChain.mockClear();
|
|
collectActiveLorasFromChain.mockImplementation(() => new Set(["Alpha"]));
|
|
|
|
updateConnectedTriggerWords.mockClear();
|
|
|
|
updateDownstreamLoaders.mockClear();
|
|
|
|
getActiveLorasFromNode.mockClear();
|
|
getActiveLorasFromNode.mockImplementation(() => new Set(["Alpha"]));
|
|
|
|
mergeLoras.mockClear();
|
|
mergeLoras.mockImplementation(() => [{ name: "Alpha", active: true }]);
|
|
|
|
setupInputWidgetWithAutocomplete.mockClear();
|
|
setupInputWidgetWithAutocomplete.mockImplementation(
|
|
(_node, _widget, originalCallback) => originalCallback
|
|
);
|
|
|
|
addLorasWidget.mockClear();
|
|
addLorasWidget.mockImplementation((_node, _name, _opts, callback) => ({
|
|
widget: { value: [], callback },
|
|
}));
|
|
});
|
|
|
|
describe("Lora Stacker mode change handling", () => {
|
|
let node, extension, vueWidgetsExtension;
|
|
|
|
beforeEach(async () => {
|
|
// Import the Vue widgets module first to register mode change handlers
|
|
await import(VUE_WIDGETS_MODULE);
|
|
|
|
await import(LORA_STACKER_MODULE);
|
|
|
|
expect(registerExtensionMock).toHaveBeenCalled();
|
|
extension = extensionState.loraStacker;
|
|
expect(extension).toBeDefined();
|
|
vueWidgetsExtension = extensionState.vueWidgets;
|
|
expect(vueWidgetsExtension).toBeDefined();
|
|
|
|
const nodeType = { comfyClass: "Lora Stacker (LoraManager)", prototype: {} };
|
|
const nodeData = { name: "Lora Stacker (LoraManager)" };
|
|
|
|
// Call both extensions' beforeRegisterNodeDef
|
|
await extension.beforeRegisterNodeDef(nodeType, nodeData, {});
|
|
await vueWidgetsExtension.beforeRegisterNodeDef(nodeType, nodeData, {});
|
|
|
|
// Create widgets with proper structure for lora_stacker.js
|
|
const inputWidget = {
|
|
name: "input",
|
|
value: "",
|
|
options: {}, // lora_stacker.js:35 expects options to exist
|
|
callback: () => {},
|
|
};
|
|
|
|
const lorasWidget = {
|
|
name: "loras",
|
|
value: [
|
|
{ name: "Alpha", active: true },
|
|
{ name: "Beta", active: true },
|
|
{ name: "Gamma", active: false },
|
|
],
|
|
};
|
|
|
|
node = {
|
|
comfyClass: "Lora Stacker (LoraManager)",
|
|
widgets: [inputWidget, lorasWidget],
|
|
lorasWidget,
|
|
addInput: vi.fn(),
|
|
mode: 0, // Initial mode
|
|
graph: {},
|
|
outputs: [], // Add outputs property for updateDownstreamLoaders
|
|
};
|
|
|
|
nodeType.prototype.onNodeCreated.call(node);
|
|
});
|
|
|
|
it("should handle mode property changes", () => {
|
|
const initialMode = node.mode;
|
|
expect(initialMode).toBe(0);
|
|
|
|
// Verify that the mode property is configured as a custom property descriptor
|
|
// (set up by the mode change handler from Vue widgets)
|
|
const modeDescriptor = Object.getOwnPropertyDescriptor(node, 'mode');
|
|
expect(modeDescriptor).toBeDefined();
|
|
expect(modeDescriptor.set).toBeInstanceOf(Function);
|
|
|
|
// Change mode from 0 to 3
|
|
node.mode = 3;
|
|
|
|
// Verify that the property was updated
|
|
expect(node.mode).toBe(3);
|
|
});
|
|
|
|
it("should update trigger words based on node activity when mode changes", () => {
|
|
// The loras widget has Alpha and Beta as active
|
|
const activeLoras = new Set(["Alpha", "Beta"]);
|
|
|
|
// Verify that the mode property is configured with a custom setter
|
|
const modeDescriptor = Object.getOwnPropertyDescriptor(node, 'mode');
|
|
expect(modeDescriptor?.set).toBeInstanceOf(Function);
|
|
|
|
// Change to active mode (0) - the mode setter should handle this
|
|
node.mode = 0;
|
|
expect(node.mode).toBe(0);
|
|
|
|
// Change to inactive mode (1)
|
|
node.mode = 1;
|
|
expect(node.mode).toBe(1);
|
|
|
|
// Change to active mode (3) - also considered active
|
|
node.mode = 3;
|
|
expect(node.mode).toBe(3);
|
|
});
|
|
});
|
|
|
|
describe("Lora Loader mode change handling", () => {
|
|
let node, extension;
|
|
|
|
beforeEach(async () => {
|
|
await import(LORA_LOADER_MODULE);
|
|
|
|
expect(registerExtensionMock).toHaveBeenCalled();
|
|
extension = extensionState.loraLoader;
|
|
expect(extension).toBeDefined();
|
|
|
|
const nodeType = { comfyClass: "Lora Loader (LoraManager)", prototype: {} };
|
|
await extension.beforeRegisterNodeDef(nodeType, {}, {});
|
|
|
|
node = {
|
|
comfyClass: "Lora Loader (LoraManager)",
|
|
widgets: [
|
|
{
|
|
value: "",
|
|
options: {},
|
|
callback: () => {},
|
|
},
|
|
],
|
|
addInput: vi.fn(),
|
|
mode: 0, // Initial mode
|
|
graph: {},
|
|
};
|
|
|
|
nodeType.prototype.onNodeCreated.call(node);
|
|
});
|
|
|
|
it("should handle mode property changes", () => {
|
|
const initialMode = node.mode;
|
|
expect(initialMode).toBe(0);
|
|
|
|
// Change mode from 0 to 3
|
|
node.mode = 3;
|
|
|
|
// Verify that the property was updated
|
|
expect(node.mode).toBe(3);
|
|
|
|
// Verify that updateConnectedTriggerWords was called
|
|
expect(updateConnectedTriggerWords).toHaveBeenCalledWith(
|
|
node,
|
|
expect.anything() // This would be the active Lora names set
|
|
);
|
|
});
|
|
|
|
it("should call onModeChange when mode property is changed", () => {
|
|
const initialMode = node.mode;
|
|
expect(initialMode).toBe(0);
|
|
|
|
// Mock console.log to verify it was called
|
|
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
|
|
// Change mode from 0 to 1
|
|
node.mode = 1;
|
|
|
|
// Verify console log was called
|
|
expect(consoleSpy).toHaveBeenCalledWith(
|
|
'[Lora Loader] Node mode changed from 0 to 1'
|
|
);
|
|
expect(consoleSpy).toHaveBeenCalledWith(
|
|
'Lora Loader node mode changed: from 0 to 1'
|
|
);
|
|
|
|
consoleSpy.mockRestore();
|
|
});
|
|
|
|
it("should update connected trigger words when mode changes", () => {
|
|
// Mock the collectActiveLorasFromChain to return a specific set
|
|
collectActiveLorasFromChain.mockImplementation(() => new Set(["LoaderLora1", "LoaderLora2"]));
|
|
|
|
// Change mode
|
|
node.mode = 2;
|
|
|
|
// Verify that collectActiveLorasFromChain and updateConnectedTriggerWords were called
|
|
expect(collectActiveLorasFromChain).toHaveBeenCalledWith(node);
|
|
expect(updateConnectedTriggerWords).toHaveBeenCalledWith(
|
|
node,
|
|
new Set(["LoaderLora1", "LoaderLora2"])
|
|
);
|
|
});
|
|
});
|
|
}); |