TriggerWord Toggle serialized the transient `highlighted` UI state into widgets_values, but highlight is recomputed after load from the current selection/library, so an untouched workflow always compared dirty against the saved JSON and ComfyUI prompted to save on close. Strip `highlighted` from serialized tag values (backend never reads it). Also restore saved widget values by widget name instead of positional index (position drift corrupted unrelated widget slots on load), and preserve `locked` in mergeLoras so load-time merging no longer drops it. Workflows saved with `highlighted` keys will show as modified once after upgrading; saving them once writes the clean format. Fixes #893
198 lines
5.8 KiB
JavaScript
198 lines
5.8 KiB
JavaScript
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
|
|
const {
|
|
APP_MODULE,
|
|
API_MODULE,
|
|
UTILS_MODULE,
|
|
TAGS_WIDGET_MODULE,
|
|
SETTINGS_MODULE,
|
|
TRIGGER_WORD_TOGGLE_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,
|
|
TAGS_WIDGET_MODULE: new URL("../../../web/comfyui/tags_widget.js", import.meta.url).pathname,
|
|
SETTINGS_MODULE: new URL("../../../web/comfyui/settings.js", import.meta.url).pathname,
|
|
TRIGGER_WORD_TOGGLE_MODULE: new URL("../../../web/comfyui/trigger_word_toggle.js", import.meta.url).pathname,
|
|
}));
|
|
|
|
const extensionState = { current: null };
|
|
const registerExtensionMock = vi.fn((extension) => {
|
|
extensionState.current = extension;
|
|
});
|
|
|
|
vi.mock(APP_MODULE, () => ({
|
|
app: {
|
|
registerExtension: registerExtensionMock,
|
|
graph: {},
|
|
},
|
|
}));
|
|
|
|
vi.mock(API_MODULE, () => ({
|
|
api: {
|
|
addEventListener: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
vi.mock(UTILS_MODULE, () => ({
|
|
CONVERTED_TYPE: "converted-widget",
|
|
getNodeFromGraph: vi.fn(),
|
|
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);
|
|
return widgetIndex >= 0 ? node.widgets_values[widgetIndex] : undefined;
|
|
},
|
|
}));
|
|
|
|
vi.mock(TAGS_WIDGET_MODULE, () => ({
|
|
addTagsWidget: (node, name) => {
|
|
const widget = { name, value: [] };
|
|
node.widgets.push(widget);
|
|
return { widget };
|
|
},
|
|
}));
|
|
|
|
vi.mock(SETTINGS_MODULE, () => ({
|
|
getWheelSensitivity: () => 0.02,
|
|
}));
|
|
|
|
function createNode({ widgetsValues } = {}) {
|
|
const node = {
|
|
comfyClass: "TriggerWord Toggle (LoraManager)",
|
|
widgets: [
|
|
{ name: "group_mode", value: true },
|
|
{ name: "default_active", value: true },
|
|
{ name: "allow_strength_adjustment", value: false },
|
|
],
|
|
widgets_values: widgetsValues,
|
|
addInput: vi.fn(),
|
|
addWidget: vi.fn((type, name, value) => {
|
|
const widget = { type, name, value };
|
|
node.widgets.push(widget);
|
|
return widget;
|
|
}),
|
|
};
|
|
return node;
|
|
}
|
|
|
|
describe("TriggerWord Toggle serialization", () => {
|
|
const originalRequestAnimationFrame = globalThis.requestAnimationFrame;
|
|
|
|
beforeEach(async () => {
|
|
vi.resetModules();
|
|
|
|
extensionState.current = null;
|
|
registerExtensionMock.mockClear();
|
|
|
|
// Run nodeCreated's deferred setup synchronously.
|
|
globalThis.requestAnimationFrame = (callback) => {
|
|
callback();
|
|
return 0;
|
|
};
|
|
|
|
await import(TRIGGER_WORD_TOGGLE_MODULE);
|
|
});
|
|
|
|
afterEach(() => {
|
|
globalThis.requestAnimationFrame = originalRequestAnimationFrame;
|
|
});
|
|
|
|
it("restores saved widget values by widget name", async () => {
|
|
const savedTags = [
|
|
{ text: "foo", active: true, strength: null, highlighted: true },
|
|
];
|
|
const node = createNode({
|
|
widgetsValues: [true, true, false, savedTags, "foo, bar"],
|
|
});
|
|
|
|
await extensionState.current.nodeCreated(node);
|
|
|
|
expect(node.tagWidget).toBeDefined();
|
|
expect(node.tagWidget.value).toHaveLength(1);
|
|
expect(node.tagWidget.value[0]).toMatchObject({ text: "foo", active: true });
|
|
expect(node.originalMessageWidget.value).toBe("foo, bar");
|
|
});
|
|
|
|
it("strips transient highlighted state from serialized tag values", async () => {
|
|
const node = createNode({
|
|
widgetsValues: [true, true, false, [], ""],
|
|
});
|
|
|
|
await extensionState.current.nodeCreated(node);
|
|
|
|
node.tagWidget.value = [
|
|
{ text: "plain", active: true, highlighted: false, strength: null },
|
|
{
|
|
text: "group a, group b",
|
|
active: true,
|
|
highlighted: true,
|
|
strength: null,
|
|
items: [
|
|
{ text: "group a", active: true, highlighted: true, strength: null },
|
|
{ text: "group b", active: false, highlighted: false, strength: null },
|
|
],
|
|
},
|
|
];
|
|
|
|
const serialized = node.tagWidget.serializeValue();
|
|
|
|
expect(serialized).toEqual([
|
|
{ text: "plain", active: true, strength: null },
|
|
{
|
|
text: "group a, group b",
|
|
active: true,
|
|
strength: null,
|
|
items: [
|
|
{ text: "group a", active: true, strength: null },
|
|
{ text: "group b", active: false, strength: null },
|
|
],
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("keeps strength text formatting while stripping highlighted state", async () => {
|
|
const node = createNode({
|
|
widgetsValues: [true, true, false, [], ""],
|
|
});
|
|
|
|
await extensionState.current.nodeCreated(node);
|
|
|
|
node.tagWidget.value = [
|
|
{ text: "styled", active: true, highlighted: true, strength: 0.5 },
|
|
];
|
|
|
|
const serialized = node.tagWidget.serializeValue();
|
|
|
|
expect(serialized).toEqual([
|
|
{ text: "(styled:0.50)", active: true, strength: 0.5 },
|
|
]);
|
|
});
|
|
|
|
it("serializes restored values identically to the loaded save data", async () => {
|
|
// Simulates a workflow saved by the current version (no highlighted keys):
|
|
// load -> restore -> highlight recompute must not change serialized output.
|
|
const savedTags = [
|
|
{
|
|
text: "masterpiece, best quality",
|
|
active: true,
|
|
strength: null,
|
|
items: [
|
|
{ text: "masterpiece", active: true, strength: null },
|
|
{ text: "best quality", active: true, strength: null },
|
|
],
|
|
},
|
|
];
|
|
const node = createNode({
|
|
widgetsValues: [true, true, false, savedTags, "masterpiece, best quality"],
|
|
});
|
|
|
|
await extensionState.current.nodeCreated(node);
|
|
|
|
// Highlight recompute ran during nodeCreated (empty highlight set).
|
|
const serialized = node.tagWidget.serializeValue();
|
|
expect(serialized).toEqual(savedTags);
|
|
});
|
|
});
|