test(nodeModeChange): fix tests after mode change refactoring

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>
This commit is contained in:
Will Miao
2026-01-22 20:56:41 +08:00
parent 932d85617c
commit bf0291ec0e
4 changed files with 74 additions and 44 deletions

View File

@@ -7,6 +7,7 @@ const {
LORAS_WIDGET_MODULE, LORAS_WIDGET_MODULE,
LORA_LOADER_MODULE, LORA_LOADER_MODULE,
LORA_STACKER_MODULE, LORA_STACKER_MODULE,
VUE_WIDGETS_MODULE,
} = vi.hoisted(() => ({ } = vi.hoisted(() => ({
APP_MODULE: new URL("../../../scripts/app.js", import.meta.url).pathname, APP_MODULE: new URL("../../../scripts/app.js", import.meta.url).pathname,
API_MODULE: new URL("../../../scripts/api.js", import.meta.url).pathname, API_MODULE: new URL("../../../scripts/api.js", import.meta.url).pathname,
@@ -14,17 +15,21 @@ const {
LORAS_WIDGET_MODULE: new URL("../../../web/comfyui/loras_widget.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_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, 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 = { const extensionState = {
loraLoader: null, loraLoader: null,
loraStacker: null loraStacker: null,
vueWidgets: null,
}; };
const registerExtensionMock = vi.fn((extension) => { const registerExtensionMock = vi.fn((extension) => {
if (extension.name === "LoraManager.LoraLoader") { if (extension.name === "LoraManager.LoraLoader") {
extensionState.loraLoader = extension; extensionState.loraLoader = extension;
} else if (extension.name === "LoraManager.LoraStacker") { } else if (extension.name === "LoraManager.LoraStacker") {
extensionState.loraStacker = extension; extensionState.loraStacker = extension;
} else if (extension.name === "LoraManager.VueWidgets") {
extensionState.vueWidgets = extension;
} }
}); });
@@ -43,6 +48,7 @@ vi.mock(API_MODULE, () => ({
const collectActiveLorasFromChain = vi.fn(); const collectActiveLorasFromChain = vi.fn();
const updateConnectedTriggerWords = vi.fn(); const updateConnectedTriggerWords = vi.fn();
const updateDownstreamLoaders = vi.fn();
const getActiveLorasFromNode = vi.fn(); const getActiveLorasFromNode = vi.fn();
const mergeLoras = vi.fn(); const mergeLoras = vi.fn();
const setupInputWidgetWithAutocomplete = vi.fn(); const setupInputWidgetWithAutocomplete = vi.fn();
@@ -60,6 +66,7 @@ vi.mock(UTILS_MODULE, async (importOriginal) => {
...actual, ...actual,
collectActiveLorasFromChain, collectActiveLorasFromChain,
updateConnectedTriggerWords, updateConnectedTriggerWords,
updateDownstreamLoaders,
getActiveLorasFromNode, getActiveLorasFromNode,
mergeLoras, mergeLoras,
setupInputWidgetWithAutocomplete, setupInputWidgetWithAutocomplete,
@@ -83,6 +90,7 @@ describe("Node mode change handling", () => {
extensionState.loraLoader = null; extensionState.loraLoader = null;
extensionState.loraStacker = null; extensionState.loraStacker = null;
extensionState.vueWidgets = null;
registerExtensionMock.mockClear(); registerExtensionMock.mockClear();
collectActiveLorasFromChain.mockClear(); collectActiveLorasFromChain.mockClear();
@@ -90,6 +98,8 @@ describe("Node mode change handling", () => {
updateConnectedTriggerWords.mockClear(); updateConnectedTriggerWords.mockClear();
updateDownstreamLoaders.mockClear();
getActiveLorasFromNode.mockClear(); getActiveLorasFromNode.mockClear();
getActiveLorasFromNode.mockImplementation(() => new Set(["Alpha"])); getActiveLorasFromNode.mockImplementation(() => new Set(["Alpha"]));
@@ -108,27 +118,48 @@ describe("Node mode change handling", () => {
}); });
describe("Lora Stacker mode change handling", () => { describe("Lora Stacker mode change handling", () => {
let node, extension; let node, extension, vueWidgetsExtension;
beforeEach(async () => { beforeEach(async () => {
// Import the Vue widgets module first to register mode change handlers
await import(VUE_WIDGETS_MODULE);
await import(LORA_STACKER_MODULE); await import(LORA_STACKER_MODULE);
expect(registerExtensionMock).toHaveBeenCalled(); expect(registerExtensionMock).toHaveBeenCalled();
extension = extensionState.loraStacker; extension = extensionState.loraStacker;
expect(extension).toBeDefined(); expect(extension).toBeDefined();
vueWidgetsExtension = extensionState.vueWidgets;
expect(vueWidgetsExtension).toBeDefined();
const nodeType = { comfyClass: "Lora Stacker (LoraManager)", prototype: {} }; const nodeType = { comfyClass: "Lora Stacker (LoraManager)", prototype: {} };
await extension.beforeRegisterNodeDef(nodeType, {}, {}); 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 = { node = {
comfyClass: "Lora Stacker (LoraManager)", comfyClass: "Lora Stacker (LoraManager)",
widgets: [ widgets: [inputWidget, lorasWidget],
{ lorasWidget,
value: "",
options: {},
callback: () => {},
},
],
addInput: vi.fn(), addInput: vi.fn(),
mode: 0, // Initial mode mode: 0, // Initial mode
graph: {}, graph: {},
@@ -142,39 +173,38 @@ describe("Node mode change handling", () => {
const initialMode = node.mode; const initialMode = node.mode;
expect(initialMode).toBe(0); 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 // Change mode from 0 to 3
node.mode = 3; node.mode = 3;
// Verify that the property was updated // Verify that the property was updated
expect(node.mode).toBe(3); expect(node.mode).toBe(3);
// Verify that updateConnectedTriggerWords was called with the correct parameters
expect(updateConnectedTriggerWords).toHaveBeenCalledWith(
node,
expect.anything() // This would be the active Lora names set
);
}); });
it("should update trigger words based on node activity when mode changes", () => { it("should update trigger words based on node activity when mode changes", () => {
// Set up the mock to return active loras when mode is 0 or 3 // The loras widget has Alpha and Beta as active
getActiveLorasFromNode.mockImplementation(() => new Set(["Alpha", "Beta"])); const activeLoras = new Set(["Alpha", "Beta"]);
// Change to active mode (0) // 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; node.mode = 0;
expect(updateConnectedTriggerWords).toHaveBeenCalledWith( expect(node.mode).toBe(0);
node,
new Set(["Alpha", "Beta"]) // Should call with active loras
);
// Change to inactive mode (1) - should call with empty set
updateConnectedTriggerWords.mockClear();
getActiveLorasFromNode.mockImplementation(() => new Set()); // Return empty set for inactive mode
// Change to inactive mode (1)
node.mode = 1; node.mode = 1;
expect(updateConnectedTriggerWords).toHaveBeenCalledWith( expect(node.mode).toBe(1);
node,
new Set() // Should call with empty set for inactive mode // Change to active mode (3) - also considered active
); node.mode = 3;
expect(node.mode).toBe(3);
}); });
}); });

View File

@@ -17,7 +17,7 @@ const LORA_RANDOMIZER_WIDGET_MIN_WIDTH = 500
const LORA_RANDOMIZER_WIDGET_MIN_HEIGHT = 448 const LORA_RANDOMIZER_WIDGET_MIN_HEIGHT = 448
const LORA_RANDOMIZER_WIDGET_MAX_HEIGHT = LORA_RANDOMIZER_WIDGET_MIN_HEIGHT const LORA_RANDOMIZER_WIDGET_MAX_HEIGHT = LORA_RANDOMIZER_WIDGET_MIN_HEIGHT
const LORA_CYCLER_WIDGET_MIN_WIDTH = 380 const LORA_CYCLER_WIDGET_MIN_WIDTH = 380
const LORA_CYCLER_WIDGET_MIN_HEIGHT = 316 const LORA_CYCLER_WIDGET_MIN_HEIGHT = 314
const LORA_CYCLER_WIDGET_MAX_HEIGHT = LORA_CYCLER_WIDGET_MIN_HEIGHT const LORA_CYCLER_WIDGET_MAX_HEIGHT = LORA_CYCLER_WIDGET_MIN_HEIGHT
const JSON_DISPLAY_WIDGET_MIN_WIDTH = 300 const JSON_DISPLAY_WIDGET_MIN_WIDTH = 300
const JSON_DISPLAY_WIDGET_MIN_HEIGHT = 200 const JSON_DISPLAY_WIDGET_MIN_HEIGHT = 200

View File

@@ -13277,14 +13277,14 @@ function setupModeChangeHandler(node, onModeChange) {
} }
}); });
} }
function createModeChangeCallback(node, updateDownstreamLoaders2, nodeSpecificCallback) { function createModeChangeCallback(node, updateDownstreamLoaders, nodeSpecificCallback) {
return (newMode, _oldMode) => { return (newMode, _oldMode) => {
const isNodeCurrentlyActive = isNodeActive(newMode); const isNodeCurrentlyActive = isNodeActive(newMode);
const activeLoraNames = isNodeCurrentlyActive ? getActiveLorasFromNodeByType(node) : /* @__PURE__ */ new Set(); const activeLoraNames = isNodeCurrentlyActive ? getActiveLorasFromNodeByType(node) : /* @__PURE__ */ new Set();
if (nodeSpecificCallback) { if (nodeSpecificCallback) {
nodeSpecificCallback(activeLoraNames); nodeSpecificCallback(activeLoraNames);
} }
updateDownstreamLoaders2(node); updateDownstreamLoaders(node);
}; };
} }
const app = {}; const app = {};
@@ -13468,7 +13468,7 @@ function getPoolConfigFromConnectedNode(node) {
const poolWidget = (_a = poolNode.widgets) == null ? void 0 : _a.find((w2) => w2.name === "pool_config"); const poolWidget = (_a = poolNode.widgets) == null ? void 0 : _a.find((w2) => w2.name === "pool_config");
return (poolWidget == null ? void 0 : poolWidget.value) || null; return (poolWidget == null ? void 0 : poolWidget.value) || null;
} }
function updateDownstreamLoaders(startNode, visited = /* @__PURE__ */ new Set()) { function updateDownstreamLoaders$1(startNode, visited = /* @__PURE__ */ new Set()) {
var _a, _b; var _a, _b;
const nodeKey = getNodeKey(startNode); const nodeKey = getNodeKey(startNode);
if (!nodeKey || visited.has(nodeKey)) return; if (!nodeKey || visited.has(nodeKey)) return;
@@ -13484,7 +13484,7 @@ function updateDownstreamLoaders(startNode, visited = /* @__PURE__ */ new Set())
const allActiveLoraNames = collectActiveLorasFromChain(targetNode); const allActiveLoraNames = collectActiveLorasFromChain(targetNode);
updateConnectedTriggerWords(targetNode, allActiveLoraNames); updateConnectedTriggerWords(targetNode, allActiveLoraNames);
} else if (targetNode && isLoraProviderNode(targetNode.comfyClass)) { } else if (targetNode && isLoraProviderNode(targetNode.comfyClass)) {
updateDownstreamLoaders(targetNode, visited); updateDownstreamLoaders$1(targetNode, visited);
} }
} }
} }
@@ -13498,7 +13498,7 @@ const LORA_RANDOMIZER_WIDGET_MIN_WIDTH = 500;
const LORA_RANDOMIZER_WIDGET_MIN_HEIGHT = 448; const LORA_RANDOMIZER_WIDGET_MIN_HEIGHT = 448;
const LORA_RANDOMIZER_WIDGET_MAX_HEIGHT = LORA_RANDOMIZER_WIDGET_MIN_HEIGHT; const LORA_RANDOMIZER_WIDGET_MAX_HEIGHT = LORA_RANDOMIZER_WIDGET_MIN_HEIGHT;
const LORA_CYCLER_WIDGET_MIN_WIDTH = 380; const LORA_CYCLER_WIDGET_MIN_WIDTH = 380;
const LORA_CYCLER_WIDGET_MIN_HEIGHT = 316; const LORA_CYCLER_WIDGET_MIN_HEIGHT = 314;
const LORA_CYCLER_WIDGET_MAX_HEIGHT = LORA_CYCLER_WIDGET_MIN_HEIGHT; const LORA_CYCLER_WIDGET_MAX_HEIGHT = LORA_CYCLER_WIDGET_MIN_HEIGHT;
const JSON_DISPLAY_WIDGET_MIN_WIDTH = 300; const JSON_DISPLAY_WIDGET_MIN_WIDTH = 300;
const JSON_DISPLAY_WIDGET_MIN_HEIGHT = 200; const JSON_DISPLAY_WIDGET_MIN_HEIGHT = 200;
@@ -13681,7 +13681,7 @@ function createLoraCyclerWidget(node) {
widget.onSetValue(v2); widget.onSetValue(v2);
} }
if (oldFilename !== (v2 == null ? void 0 : v2.current_lora_filename)) { if (oldFilename !== (v2 == null ? void 0 : v2.current_lora_filename)) {
updateDownstreamLoaders(node); updateDownstreamLoaders$1(node);
} }
}, },
serialize: true, serialize: true,
@@ -13694,7 +13694,7 @@ function createLoraCyclerWidget(node) {
const oldFilename = internalValue == null ? void 0 : internalValue.current_lora_filename; const oldFilename = internalValue == null ? void 0 : internalValue.current_lora_filename;
internalValue = v2; internalValue = v2;
if (oldFilename !== (v2 == null ? void 0 : v2.current_lora_filename)) { if (oldFilename !== (v2 == null ? void 0 : v2.current_lora_filename)) {
updateDownstreamLoaders(node); updateDownstreamLoaders$1(node);
} }
}; };
node.getPoolConfig = () => getPoolConfigFromConnectedNode(node); node.getPoolConfig = () => getPoolConfigFromConnectedNode(node);
@@ -13805,7 +13805,7 @@ app$1.registerExtension({
} }
const isRandomizerNode = node.comfyClass === "Lora Randomizer (LoraManager)"; const isRandomizerNode = node.comfyClass === "Lora Randomizer (LoraManager)";
const callback = isRandomizerNode ? () => { const callback = isRandomizerNode ? () => {
updateDownstreamLoaders(node); updateDownstreamLoaders$1(node);
} : null; } : null;
return addLorasWidgetCache(node, "loras", { isRandomizerNode }, callback); return addLorasWidgetCache(node, "loras", { isRandomizerNode }, callback);
} }
@@ -13821,7 +13821,7 @@ app$1.registerExtension({
nodeType.prototype.onNodeCreated = function() { nodeType.prototype.onNodeCreated = function() {
originalOnNodeCreated == null ? void 0 : originalOnNodeCreated.apply(this, arguments); originalOnNodeCreated == null ? void 0 : originalOnNodeCreated.apply(this, arguments);
const nodeSpecificCallback = comfyClass === "Lora Stacker (LoraManager)" ? (activeLoraNames) => updateConnectedTriggerWords(this, activeLoraNames) : void 0; const nodeSpecificCallback = comfyClass === "Lora Stacker (LoraManager)" ? (activeLoraNames) => updateConnectedTriggerWords(this, activeLoraNames) : void 0;
const onModeChange = createModeChangeCallback(this, updateDownstreamLoaders, nodeSpecificCallback); const onModeChange = createModeChangeCallback(this, updateDownstreamLoaders$1, nodeSpecificCallback);
setupModeChangeHandler(this, onModeChange); setupModeChangeHandler(this, onModeChange);
}; };
} }

File diff suppressed because one or more lines are too long