import { describe, it, beforeEach, afterEach, expect, vi } from 'vitest'; const showToastMock = vi.fn(); const translateMock = vi.fn((key, params, fallback) => (typeof fallback === 'string' ? fallback : key)); const copyToClipboardMock = vi.fn(); const getNSFWLevelNameMock = vi.fn((level) => { if (level >= 16) return 'XXX'; if (level >= 8) return 'X'; if (level >= 4) return 'R'; if (level >= 2) return 'PG13'; if (level >= 1) return 'PG'; return 'Unknown'; }); const copyLoraSyntaxMock = vi.fn(); const sendLoraToWorkflowMock = vi.fn(); const buildLoraSyntaxMock = vi.fn((fileName) => `lora:${fileName}`); const openExampleImagesFolderMock = vi.fn(); const modalManagerMock = { showModal: vi.fn(), closeModal: vi.fn(), registerModal: vi.fn(), getModal: vi.fn(() => ({ element: { style: { display: 'none' } }, isOpen: false })), isAnyModalOpen: vi.fn(), }; const loadingManagerStub = { showSimpleLoading: vi.fn(), hide: vi.fn(), show: vi.fn(), restoreProgressBar: vi.fn(), }; const stateStub = { global: { settings: {}, loadingManager: loadingManagerStub }, loadingManager: loadingManagerStub, virtualScroller: { updateSingleItem: vi.fn() }, }; const saveModelMetadataMock = vi.fn(); const downloadExampleImagesApiMock = vi.fn(); const replaceModelPreviewMock = vi.fn(); const refreshSingleModelMetadataMock = vi.fn(); const resetAndReloadMock = vi.fn(); const getCompleteApiConfigMock = vi.fn(() => ({ config: { displayName: 'LoRA' }, endpoints: { refreshUpdates: '/api/lm/loras/updates/refresh', fetchMissingLicenses: '/api/lm/loras/updates/fetch-missing-license', }, })); const getCurrentModelTypeMock = vi.fn(() => 'loras'); const getModelApiClientMock = vi.fn(() => ({ saveModelMetadata: saveModelMetadataMock, downloadExampleImages: downloadExampleImagesApiMock, replaceModelPreview: replaceModelPreviewMock, refreshSingleModelMetadata: refreshSingleModelMetadataMock, })); const updateRecipeMetadataMock = vi.fn(() => Promise.resolve({ success: true })); vi.mock('../../../static/js/utils/uiHelpers.js', () => ({ showToast: showToastMock, copyToClipboard: copyToClipboardMock, getNSFWLevelName: getNSFWLevelNameMock, copyLoraSyntax: copyLoraSyntaxMock, sendLoraToWorkflow: sendLoraToWorkflowMock, buildLoraSyntax: buildLoraSyntaxMock, openExampleImagesFolder: openExampleImagesFolderMock, })); vi.mock('../../../static/js/managers/ModalManager.js', () => ({ modalManager: modalManagerMock, })); vi.mock('../../../static/js/utils/storageHelpers.js', () => ({ setSessionItem: vi.fn(), removeSessionItem: vi.fn(), getSessionItem: vi.fn(), getStorageItem: vi.fn((key, defaultValue = null) => { const value = localStorage.getItem(`lora_manager_${key}`); if (value === null) { return defaultValue; } try { return JSON.parse(value); } catch (error) { return value; } }), setStorageItem: vi.fn((key, value) => { const prefixedKey = `lora_manager_${key}`; if (typeof value === 'object' && value !== null) { localStorage.setItem(prefixedKey, JSON.stringify(value)); } else { localStorage.setItem(prefixedKey, value); } }), })); vi.mock('../../../static/js/api/modelApiFactory.js', () => ({ getModelApiClient: getModelApiClientMock, resetAndReload: resetAndReloadMock, })); vi.mock('../../../static/js/api/apiConfig.js', () => ({ getCompleteApiConfig: getCompleteApiConfigMock, getCurrentModelType: getCurrentModelTypeMock, MODEL_TYPES: { LORA: 'loras', CHECKPOINT: 'checkpoints', EMBEDDING: 'embeddings', }, })); vi.mock('../../../static/js/state/index.js', () => ({ state: stateStub, })); vi.mock('../../../static/js/utils/modalUtils.js', () => ({ showExcludeModal: vi.fn(), showDeleteModal: vi.fn(), })); vi.mock('../../../static/js/managers/MoveManager.js', () => ({ moveManager: { showMoveModal: vi.fn() }, })); vi.mock('../../../static/js/api/recipeApi.js', () => ({ updateRecipeMetadata: updateRecipeMetadataMock, })); vi.mock('../../../static/js/utils/i18nHelpers.js', () => ({ translate: translateMock, })); async function flushAsyncTasks() { await Promise.resolve(); await new Promise((resolve) => setTimeout(resolve, 0)); } describe('Interaction-level regression coverage', () => { beforeEach(() => { vi.clearAllMocks(); vi.useRealTimers(); document.body.innerHTML = ''; stateStub.global.settings = {}; saveModelMetadataMock.mockResolvedValue(undefined); downloadExampleImagesApiMock.mockResolvedValue(undefined); updateRecipeMetadataMock.mockResolvedValue({ success: true }); resetAndReloadMock.mockResolvedValue(undefined); getCompleteApiConfigMock.mockReturnValue({ config: { displayName: 'LoRA' }, endpoints: { refreshUpdates: '/api/lm/loras/updates/refresh', fetchMissingLicenses: '/api/lm/loras/updates/fetch-missing-license', }, }); getCurrentModelTypeMock.mockReturnValue('loras'); translateMock.mockImplementation((key, params, fallback) => (typeof fallback === 'string' ? fallback : key)); global.modalManager = modalManagerMock; }); afterEach(() => { vi.useRealTimers(); document.body.innerHTML = ''; delete window.exampleImagesManager; delete global.fetch; delete global.modalManager; }); it('opens the NSFW selector from the LoRA context menu and persists the new rating', async () => { document.body.innerHTML = `
`; const card = document.createElement('div'); card.className = 'model-card'; card.dataset.filepath = '/models/test.safetensors'; card.dataset.meta = JSON.stringify({ preview_nsfw_level: 1 }); document.body.appendChild(card); const { LoraContextMenu } = await import('../../../static/js/components/ContextMenu/LoraContextMenu.js'); const helpers = await import('../../../static/js/utils/uiHelpers.js'); expect(helpers.showToast).toBe(showToastMock); const contextMenu = new LoraContextMenu(); contextMenu.showMenu(120, 140, card); const nsfwMenuItem = document.querySelector('#loraContextMenu .context-menu-item[data-action="set-nsfw"]'); nsfwMenuItem.dispatchEvent(new Event('click', { bubbles: true })); const selector = document.getElementById('nsfwLevelSelector'); expect(selector.style.display).toBe('block'); expect(selector.dataset.cardPath).toBe('/models/test.safetensors'); expect(document.getElementById('currentNSFWLevel').textContent).toBe('PG'); const levelButton = selector.querySelector('.nsfw-level-btn[data-level="4"]'); levelButton.dispatchEvent(new Event('click', { bubbles: true })); expect(saveModelMetadataMock).toHaveBeenCalledWith('/models/test.safetensors', { preview_nsfw_level: 4 }); expect(saveModelMetadataMock).toHaveBeenCalledTimes(1); await saveModelMetadataMock.mock.results[0].value; await flushAsyncTasks(); expect(selector.style.display).toBe('none'); expect(document.getElementById('loraContextMenu').style.display).toBe('none'); }); it('wires recipe modal title editing to update metadata and UI state', async () => { document.body.innerHTML = `