diff --git a/tests/frontend/components/loraLoader.triggerWords.test.js b/tests/frontend/components/loraLoader.triggerWords.test.js index fc25a768..7fcc2476 100644 --- a/tests/frontend/components/loraLoader.triggerWords.test.js +++ b/tests/frontend/components/loraLoader.triggerWords.test.js @@ -37,6 +37,13 @@ const updateConnectedTriggerWords = vi.fn(); const mergeLoras = vi.fn(); const getAllGraphNodes = vi.fn(); const getNodeFromGraph = vi.fn(); +const getWidgetByName = vi.fn((node, name) => + node?.widgets?.find((widget) => widget?.name === name) ?? null +); +const getWidgetSerializedValue = vi.fn((node, name) => { + const index = node?.widgets?.findIndex((widget) => widget?.name === name) ?? -1; + return index >= 0 ? node.widgets_values?.[index] : undefined; +}); vi.mock(UTILS_MODULE, () => ({ collectActiveLorasFromChain, @@ -47,6 +54,8 @@ vi.mock(UTILS_MODULE, () => ({ }, getAllGraphNodes, getNodeFromGraph, + getWidgetByName, + getWidgetSerializedValue, LORA_PATTERN: //g, })); @@ -71,6 +80,9 @@ describe("Lora Loader trigger word updates", () => { mergeLoras.mockClear(); mergeLoras.mockImplementation(() => [{ name: "Alpha", active: true }]); + getWidgetByName.mockClear(); + getWidgetSerializedValue.mockClear(); + addLorasWidget.mockClear(); addLorasWidget.mockImplementation((_node, _name, _opts, callback) => ({ widget: { value: [], callback }, @@ -89,14 +101,21 @@ describe("Lora Loader trigger word updates", () => { // Create mock widget (AUTOCOMPLETE_TEXT_LORAS type created by Vue widgets) const inputWidget = { + name: "text", value: "", options: {}, callback: null, // Will be set by onNodeCreated }; + const metadataWidget = { + name: "__autocomplete_metadata_text", + value: { version: 1, textWidgetName: "text" }, + options: {}, + }; + const node = { comfyClass: "Lora Loader (LoraManager)", - widgets: [inputWidget], + widgets: [metadataWidget, inputWidget], addInput: vi.fn(), graph: {}, }; @@ -106,6 +125,7 @@ describe("Lora Loader trigger word updates", () => { // The widget is now the AUTOCOMPLETE_TEXT_LORAS type, created automatically by Vue widgets expect(node.inputWidget).toBe(inputWidget); expect(node.lorasWidget).toBeDefined(); + expect(getWidgetByName).toHaveBeenCalledWith(node, "text"); // The callback should have been set up by onNodeCreated const inputCallback = inputWidget.callback; diff --git a/tests/frontend/components/nodeModeChange.test.js b/tests/frontend/components/nodeModeChange.test.js index 3990c1c6..12e8e077 100644 --- a/tests/frontend/components/nodeModeChange.test.js +++ b/tests/frontend/components/nodeModeChange.test.js @@ -50,6 +50,13 @@ const getAllGraphNodes = vi.fn(); const getNodeFromGraph = vi.fn(); const getNodeKey = vi.fn(); const getLinkFromGraph = vi.fn(); +const getWidgetByName = vi.fn((node, name) => + node?.widgets?.find((widget) => widget?.name === name) ?? null +); +const getWidgetSerializedValue = vi.fn((node, name) => { + const index = node?.widgets?.findIndex((widget) => widget?.name === name) ?? -1; + return index >= 0 ? node.widgets_values?.[index] : undefined; +}); const chainCallback = vi.fn((proto, property, callback) => { proto[property] = callback; }); @@ -68,6 +75,8 @@ vi.mock(UTILS_MODULE, async (importOriginal) => { getNodeFromGraph, getNodeKey, getLinkFromGraph, + getWidgetByName, + getWidgetSerializedValue, }; }); @@ -98,6 +107,9 @@ describe("Node mode change handling", () => { mergeLoras.mockClear(); mergeLoras.mockImplementation(() => [{ name: "Alpha", active: true }]); + getWidgetByName.mockClear(); + getWidgetSerializedValue.mockClear(); + addLorasWidget.mockClear(); addLorasWidget.mockImplementation((_node, _name, _opts, callback) => ({ widget: { value: [], callback }, @@ -119,8 +131,13 @@ describe("Node mode change handling", () => { await extension.beforeRegisterNodeDef(nodeType, nodeData, {}); - // Create widgets with proper structure for lora_stacker.js - // Widget at index 0 is the AUTOCOMPLETE_TEXT_LORAS widget (created by Vue widgets) + // Include a hidden metadata widget ahead of the actual text widget to match runtime ordering. + const metadataWidget = { + name: "__autocomplete_metadata_text", + value: { version: 1, textWidgetName: "text" }, + options: {}, + }; + const inputWidget = { name: "text", value: "", @@ -139,7 +156,7 @@ describe("Node mode change handling", () => { node = { comfyClass: "Lora Stacker (LoraManager)", - widgets: [inputWidget, lorasWidget], + widgets: [metadataWidget, inputWidget, lorasWidget], lorasWidget, addInput: vi.fn(), mode: 0, // Initial mode @@ -189,11 +206,18 @@ describe("Node mode change handling", () => { const nodeType = { comfyClass: "Lora Loader (LoraManager)", prototype: {} }; await extension.beforeRegisterNodeDef(nodeType, {}, {}); - // Widget at index 0 is the AUTOCOMPLETE_TEXT_LORAS widget (created by Vue widgets) + const metadataWidget = { + name: "__autocomplete_metadata_text", + value: { version: 1, textWidgetName: "text" }, + options: {}, + }; + node = { comfyClass: "Lora Loader (LoraManager)", widgets: [ + metadataWidget, { + name: "text", value: "", options: {}, callback: null, // Will be set by onNodeCreated diff --git a/web/comfyui/lora_loader.js b/web/comfyui/lora_loader.js index 28a86a84..f507c30a 100644 --- a/web/comfyui/lora_loader.js +++ b/web/comfyui/lora_loader.js @@ -7,6 +7,8 @@ import { mergeLoras, getAllGraphNodes, getNodeFromGraph, + getWidgetByName, + getWidgetSerializedValue, } from "./utils.js"; import { addLorasWidget } from "./loras_widget.js"; import { applyLoraValuesToText, debounce } from "./lora_syntax_utils.js"; @@ -148,7 +150,11 @@ app.registerExtension({ }; // Get the text input widget (AUTOCOMPLETE_TEXT_LORAS type, created by Vue widgets) - const inputWidget = this.widgets[0]; + const inputWidget = getWidgetByName(this, "text"); + if (!inputWidget) { + console.warn("LoRA Manager: text widget not found for Lora Loader"); + return; + } this.inputWidget = inputWidget; const scheduleInputSync = debounce((lorasValue) => { @@ -227,12 +233,16 @@ app.registerExtension({ // Restore saved value if exists let existingLoras = []; if (node.widgets_values && node.widgets_values.length > 0) { - // 0 for input widget, 1 for loras widget - const savedValue = node.widgets_values[1]; + const savedValue = getWidgetSerializedValue(node, "loras"); existingLoras = savedValue || []; } // Merge the loras data - const mergedLoras = mergeLoras(node.widgets[0].value, existingLoras); + const inputWidget = node.inputWidget || getWidgetByName(node, "text"); + if (!inputWidget) { + console.warn("LoRA Manager: text widget not found while restoring Lora Loader"); + return; + } + const mergedLoras = mergeLoras(inputWidget.value, existingLoras); node.lorasWidget.value = mergedLoras; } }, diff --git a/web/comfyui/lora_stacker.js b/web/comfyui/lora_stacker.js index 65570eb4..5e5c1168 100644 --- a/web/comfyui/lora_stacker.js +++ b/web/comfyui/lora_stacker.js @@ -5,6 +5,8 @@ import { updateDownstreamLoaders, chainCallback, mergeLoras, + getWidgetByName, + getWidgetSerializedValue, } from "./utils.js"; import { addLorasWidget } from "./loras_widget.js"; import { applyLoraValuesToText, debounce } from "./lora_syntax_utils.js"; @@ -28,7 +30,11 @@ app.registerExtension({ let isSyncingInput = false; // Get the text input widget (AUTOCOMPLETE_TEXT_LORAS type, created by Vue widgets) - const inputWidget = this.widgets[0]; + const inputWidget = getWidgetByName(this, "text"); + if (!inputWidget) { + console.warn("LoRA Manager: text widget not found for Lora Stacker"); + return; + } this.inputWidget = inputWidget; const scheduleInputSync = debounce((lorasValue) => { @@ -122,12 +128,15 @@ app.registerExtension({ // Restore saved value if exists let existingLoras = []; if (node.widgets_values && node.widgets_values.length > 0) { - // 0 for input widget, 1 for loras widget - const savedValue = node.widgets_values[1]; + const savedValue = getWidgetSerializedValue(node, "loras"); existingLoras = savedValue || []; } // Merge the loras data - const inputWidget = node.inputWidget || node.widgets[0]; + const inputWidget = node.inputWidget || getWidgetByName(node, "text"); + if (!inputWidget) { + console.warn("LoRA Manager: text widget not found while restoring Lora Stacker"); + return; + } const mergedLoras = mergeLoras(inputWidget.value, existingLoras); node.lorasWidget.value = mergedLoras; } diff --git a/web/comfyui/utils.js b/web/comfyui/utils.js index 8f0593c9..bb0edbd9 100644 --- a/web/comfyui/utils.js +++ b/web/comfyui/utils.js @@ -114,6 +114,27 @@ export function getNodeKey(node) { return `${getNodeGraphId(node)}:${node.id}`; } +export function getWidgetByName(node, widgetName) { + if (!node || !Array.isArray(node.widgets)) { + return null; + } + + return node.widgets.find((widget) => widget?.name === widgetName) || null; +} + +export function getWidgetSerializedValue(node, widgetName) { + if (!node || !Array.isArray(node.widgets) || !Array.isArray(node.widgets_values)) { + return undefined; + } + + const widgetIndex = node.widgets.findIndex((widget) => widget?.name === widgetName); + if (widgetIndex === -1) { + return undefined; + } + + return node.widgets_values[widgetIndex]; +} + export function getLinkFromGraph(graph, linkId) { if (!graph || graph.links == null) { return null; diff --git a/web/comfyui/wanvideo_lora_select.js b/web/comfyui/wanvideo_lora_select.js index faba8e30..5422ef7c 100644 --- a/web/comfyui/wanvideo_lora_select.js +++ b/web/comfyui/wanvideo_lora_select.js @@ -4,6 +4,8 @@ import { updateConnectedTriggerWords, chainCallback, mergeLoras, + getWidgetByName, + getWidgetSerializedValue, } from "./utils.js"; import { addLorasWidget } from "./loras_widget.js"; import { applyLoraValuesToText, debounce } from "./lora_syntax_utils.js"; @@ -31,7 +33,11 @@ app.registerExtension({ let isSyncingInput = false; // Get the text input widget (AUTOCOMPLETE_TEXT_LORAS type, at index 2 after low_mem_load and merge_loras) - const inputWidget = this.widgets[2]; + const inputWidget = getWidgetByName(this, "text"); + if (!inputWidget) { + console.warn("LoRA Manager: text widget not found for WanVideo Lora Select"); + return; + } this.inputWidget = inputWidget; const scheduleInputSync = debounce((lorasValue) => { @@ -107,12 +113,15 @@ app.registerExtension({ // Restore saved value if exists let existingLoras = []; if (node.widgets_values && node.widgets_values.length > 0) { - // 0 for low_mem_load, 1 for merge_loras, 2 for text widget, 3 for loras widget - const savedValue = node.widgets_values[3]; + const savedValue = getWidgetSerializedValue(node, "loras"); existingLoras = savedValue || []; } // Merge the loras data - const inputWidget = node.inputWidget || node.widgets[2]; + const inputWidget = node.inputWidget || getWidgetByName(node, "text"); + if (!inputWidget) { + console.warn("LoRA Manager: text widget not found while restoring WanVideo Lora Select"); + return; + } const mergedLoras = mergeLoras(inputWidget.value, existingLoras); node.lorasWidget.value = mergedLoras; }