Files
ComfyUI-Lora-Manager/tests/frontend/components/lorasWidgetEvents.saveRecipeMenu.test.js
T
Will Miao faeb66a23d feat(recipes): opt-in workflow embedding for widget recipe saves
Add a "Save Recipe with Workflow" action next to "Save Recipe" in the LoRA
widget context menu. It posts the current UI-format graph alongside the save
request so the stored preview embeds it and the recipe can send the graph back
to ComfyUI. Embedding stays opt-in rather than folded into "Save Recipe": the
workflow is by far the largest metadata field and its widget values may carry
sensitive data.

- web/comfyui: new menu entry; saveRecipeDirectly({ embedWorkflow }) posts the
  UI graph and reports the outcome (embedded / skipped) via toasts.
- save_recipe_from_widget handler: reads an optional JSON workflow field so the
  long-standing body-less POST keeps working, including from cached clients.
- RecipePersistenceService.save_recipe_from_widget: embeds the graph through
  the existing optimize_image workflow path, derives has_workflow by detection,
  and skips graphs above MAX_WORKFLOW_EMBED_BYTES with workflow_skipped.
2026-09-29 09:03:26 +08:00

80 lines
2.5 KiB
JavaScript

import { describe, it, beforeEach, afterEach, expect, vi } from 'vitest';
const { EVENTS_MODULE, API_MODULE, APP_MODULE, COMPONENTS_MODULE, UTILS_MODULE } =
vi.hoisted(() => ({
EVENTS_MODULE: new URL('../../../web/comfyui/loras_widget_events.js', import.meta.url)
.pathname,
API_MODULE: new URL('../../../scripts/api.js', import.meta.url).pathname,
APP_MODULE: new URL('../../../scripts/app.js', import.meta.url).pathname,
COMPONENTS_MODULE: new URL('../../../web/comfyui/loras_widget_components.js', import.meta.url)
.pathname,
UTILS_MODULE: new URL('../../../web/comfyui/loras_widget_utils.js', import.meta.url)
.pathname,
}));
const saveRecipeDirectly = vi.fn();
vi.mock(API_MODULE, () => ({ api: {} }));
vi.mock(APP_MODULE, () => ({ app: {} }));
vi.mock(COMPONENTS_MODULE, () => ({
createMenuItem: (text, icon, onClick) => {
const el = document.createElement('div');
el.className = 'lm-lora-menu-item';
el.textContent = text;
if (onClick) el.addEventListener('click', onClick);
return el;
},
createDropIndicator: vi.fn(),
}));
vi.mock(UTILS_MODULE, () => ({
parseLoraValue: vi.fn(() => []),
formatLoraValue: vi.fn((value) => value),
syncClipStrengthIfCollapsed: vi.fn(),
saveRecipeDirectly,
copyToClipboard: vi.fn(),
showToast: vi.fn(),
moveLoraByDirection: vi.fn(),
getDropTargetIndex: vi.fn(),
getLoraStrengthRange: vi.fn(),
applyStrengthRangeCue: vi.fn(),
}));
function findMenuItem(label) {
return Array.from(document.querySelectorAll('.lm-lora-menu-item')).find(
(item) => item.textContent === label
);
}
describe('LoRA widget context menu save options', () => {
beforeEach(() => {
document.body.innerHTML = '';
});
afterEach(() => {
document.body.innerHTML = '';
vi.clearAllMocks();
});
it('offers workflow embedding as a separate, opt-in action', async () => {
const { createContextMenu } = await import(EVENTS_MODULE);
const widget = { value: [], callback: vi.fn() };
createContextMenu(10, 10, 'lora-a', widget, null, vi.fn());
const plain = findMenuItem('Save Recipe');
const withWorkflow = findMenuItem('Save Recipe with Workflow');
expect(plain).toBeTruthy();
expect(withWorkflow).toBeTruthy();
plain.click();
expect(saveRecipeDirectly).toHaveBeenLastCalledWith();
// Re-open: the first click removed the menu.
createContextMenu(10, 10, 'lora-a', widget, null, vi.fn());
findMenuItem('Save Recipe with Workflow').click();
expect(saveRecipeDirectly).toHaveBeenLastCalledWith({ embedWorkflow: true });
});
});