import { describe, it, beforeEach, expect, vi } from 'vitest'; const { SIDEBAR_MANAGER_MODULE, STORAGE_HELPERS_MODULE, MODEL_API_FACTORY_MODULE, I18N_MODULE, BULK_MANAGER_MODULE, UI_HELPERS_MODULE, UPDATE_CHECK_MODULE, STATE_MODULE, } = vi.hoisted(() => ({ SIDEBAR_MANAGER_MODULE: new URL('../../../static/js/components/SidebarManager.js', import.meta.url).pathname, STORAGE_HELPERS_MODULE: new URL('../../../static/js/utils/storageHelpers.js', import.meta.url).pathname, MODEL_API_FACTORY_MODULE: new URL('../../../static/js/api/modelApiFactory.js', import.meta.url).pathname, I18N_MODULE: new URL('../../../static/js/utils/i18nHelpers.js', import.meta.url).pathname, BULK_MANAGER_MODULE: new URL('../../../static/js/managers/BulkManager.js', import.meta.url).pathname, UI_HELPERS_MODULE: new URL('../../../static/js/utils/uiHelpers.js', import.meta.url).pathname, UPDATE_CHECK_MODULE: new URL('../../../static/js/utils/updateCheckHelpers.js', import.meta.url).pathname, STATE_MODULE: new URL('../../../static/js/state/index.js', import.meta.url).pathname, })); vi.mock(MODEL_API_FACTORY_MODULE, () => ({ getModelApiClient: vi.fn() })); vi.mock(I18N_MODULE, () => ({ translate: (key, _args, fallback) => fallback || key })); vi.mock(BULK_MANAGER_MODULE, () => ({ bulkManager: {} })); vi.mock(UI_HELPERS_MODULE, () => ({ showToast: vi.fn() })); vi.mock(UPDATE_CHECK_MODULE, () => ({ performFolderUpdateCheck: vi.fn() })); const { SidebarManager } = await import(SIDEBAR_MANAGER_MODULE); const { state } = await import(STATE_MODULE); const { setStorageItem, getStorageItem } = await import(STORAGE_HELPERS_MODULE); function createApiClient(overrides = {}) { return { apiConfig: { config: { displayName: 'LoRA', singularName: 'lora', supportsMove: true, supportsFolderManagement: true, }, }, fetchUnifiedFolderTree: vi.fn().mockResolvedValue({ tree: { full: {}, empty: {} } }), fetchModelFolders: vi.fn().mockResolvedValue({ folders: ['', 'full'] }), fetchModelRoots: vi.fn().mockResolvedValue({ roots: ['/models/loras'] }), createFolder: vi.fn().mockResolvedValue({ success: true, folder: 'new-folder', created: true }), ...overrides, }; } function createManager(apiClient, { displayMode = 'tree' } = {}) { const manager = new SidebarManager(); manager.pageType = 'loras'; manager.displayMode = displayMode; manager.apiClient = apiClient; manager.pageControls = { pageState: { searchOptions: {} } }; manager.renderFolderDisplay = vi.fn(); manager.renderEmptyState = vi.fn(); return manager; } describe('SidebarManager empty folders toggle', () => { beforeEach(() => { localStorage.clear(); document.body.innerHTML = ''; }); it('requests the models-only tree by default', async () => { const apiClient = createApiClient(); const manager = createManager(apiClient); await manager.loadFolderTree(); expect(apiClient.fetchUnifiedFolderTree).toHaveBeenCalledWith(); expect(apiClient.fetchModelFolders).not.toHaveBeenCalled(); expect(manager.nonEmptyFolders).toBeNull(); expect(manager.treeData).toEqual({ full: {}, empty: {} }); }); it('includes empty folders and tracks the models-only set when enabled', async () => { const apiClient = createApiClient(); const manager = createManager(apiClient); manager.showEmptyFolders = true; await manager.loadFolderTree(); expect(apiClient.fetchUnifiedFolderTree).toHaveBeenCalledWith({ includeEmpty: true }); expect(apiClient.fetchModelFolders).toHaveBeenCalledWith(); expect(manager.nonEmptyFolders).toEqual(new Set(['', 'full'])); }); it('passes includeEmpty to the folder list in list display mode', async () => { const apiClient = createApiClient(); apiClient.fetchModelFolders .mockResolvedValueOnce({ folders: ['', 'full', 'empty'] }) .mockResolvedValueOnce({ folders: ['', 'full'] }); const manager = createManager(apiClient, { displayMode: 'list' }); manager.showEmptyFolders = true; await manager.loadFolderTree(); expect(apiClient.fetchModelFolders).toHaveBeenNthCalledWith(1, { includeEmpty: true }); expect(manager.foldersList).toEqual(['', 'full', 'empty']); expect(manager.nonEmptyFolders).toEqual(new Set(['', 'full'])); }); it('ignores the preference when the page does not support folder management', async () => { const apiClient = createApiClient(); apiClient.apiConfig.config.supportsFolderManagement = false; const manager = createManager(apiClient); manager.showEmptyFolders = true; await manager.loadFolderTree(); expect(apiClient.fetchUnifiedFolderTree).toHaveBeenCalledWith(); expect(manager.nonEmptyFolders).toBeNull(); }); it('persists the toggle and reloads the tree', async () => { const apiClient = createApiClient(); const manager = createManager(apiClient); manager.loadFolderTree = vi.fn(); manager.handleEmptyFoldersToggle({ stopPropagation: vi.fn() }); expect(manager.showEmptyFolders).toBe(true); expect(getStorageItem('loras_showEmptyFolders')).toBe(true); expect(manager.loadFolderTree).toHaveBeenCalledTimes(1); }); it('dims folders that contain no models', () => { const manager = createManager(createApiClient()); manager.treeData = { full: {}, empty: {} }; manager.nonEmptyFolders = new Set(['', 'full']); const html = manager.renderTreeNode(manager.treeData, ''); expect(html).toContain('sidebar-tree-node-content empty'); expect(html.match(/empty/g).length).toBeGreaterThan(0); expect(html).not.toContain('sidebar-tree-node-content empty" data-path="full"'); }); it('does not dim folders whose subtree contains models', () => { const manager = createManager(createApiClient()); // Models live in "characters/anime" only; "characters" itself holds no // direct models but must not be dimmed. manager.nonEmptyFolders = manager._buildNonEmptyFolderSet(['characters/anime']); manager.treeData = { characters: { anime: {} }, empty: {} }; const html = manager.renderTreeNode(manager.treeData, ''); const charactersNode = html.match(/