import { beforeEach, describe, expect, it, vi } from 'vitest'; import { moveManager } from '../../../static/js/managers/MoveManager.js'; import { state } from '../../../static/js/state/index.js'; import { modalManager } from '../../../static/js/managers/ModalManager.js'; import { getModelApiClient } from '../../../static/js/api/modelApiFactory.js'; import * as storageHelpers from '../../../static/js/utils/storageHelpers.js'; // Mock dependencies vi.mock('../../../static/js/state/index.js', () => ({ state: { currentPageType: 'loras', selectedModels: new Set(), global: { settings: { download_path_templates: { lora: '{base_model}/unstaged' } } } }, getCurrentPageState: vi.fn(() => ({ activeFolder: null, searchOptions: {} })) })); vi.mock('../../../static/js/managers/ModalManager.js', () => ({ modalManager: { showModal: vi.fn(), closeModal: vi.fn() } })); vi.mock('../../../static/js/api/modelApiFactory.js', () => ({ getModelApiClient: vi.fn() })); vi.mock('../../../static/js/utils/storageHelpers.js', () => ({ getStorageItem: vi.fn(), setStorageItem: vi.fn() })); vi.mock('../../../static/js/utils/uiHelpers.js', () => ({ showToast: vi.fn() })); vi.mock('../../../static/js/utils/i18nHelpers.js', () => ({ translate: vi.fn(key => key) })); describe('MoveManager', () => { let mockApiClient; beforeEach(() => { vi.clearAllMocks(); // Setup DOM document.body.innerHTML = `

`; mockApiClient = { apiConfig: { config: { displayName: 'LoRA', supportsMove: true }, endpoints: { moveModel: '/api/move' } }, modelType: 'loras', fetchModelRoots: vi.fn().mockResolvedValue({ roots: ['/models/loras'] }), fetchUnifiedFolderTree: vi.fn().mockResolvedValue({ success: true, tree: {} }), moveSingleModel: vi.fn().mockResolvedValue({ success: true }) }; getModelApiClient.mockReturnValue(mockApiClient); }); it('should reset folder selection when showing move modal', async () => { // Manually set a selected path in folderTreeManager moveManager.folderTreeManager.selectedPath = 'previous/path'; await moveManager.showMoveModal('some/file.safetensors'); expect(moveManager.folderTreeManager.getSelectedPath()).toBe(''); }); it('should fetch the folder tree including empty directories', async () => { await moveManager.initializeFolderTree(); expect(mockApiClient.fetchUnifiedFolderTree).toHaveBeenCalledWith({ includeEmpty: true }); }); it('should ignore manual folder selection when useDefaultPath is true', async () => { // Setup state moveManager.useDefaultPath = true; moveManager.currentFilePath = '/models/loras/flux/my-lora.safetensors'; document.getElementById('moveModelRoot').innerHTML = ''; document.getElementById('moveModelRoot').value = '/models/loras'; // Manually set a selected path despite useDefaultPath being true moveManager.folderTreeManager.selectedPath = 'wrong/folder'; await moveManager.moveModel(); // Should call moveSingleModel with the root path, NOT including the 'wrong/folder' expect(mockApiClient.moveSingleModel).toHaveBeenCalledWith( '/models/loras/flux/my-lora.safetensors', '/models/loras', true ); }); it('should include manual folder selection when useDefaultPath is false', async () => { // Setup state moveManager.useDefaultPath = false; moveManager.currentFilePath = '/models/loras/flux/my-lora.safetensors'; document.getElementById('moveModelRoot').innerHTML = ''; document.getElementById('moveModelRoot').value = '/models/loras'; // Set a selected path moveManager.folderTreeManager.selectedPath = 'my/organized/folder'; await moveManager.moveModel(); // Should call moveSingleModel with root + selected folder expect(mockApiClient.moveSingleModel).toHaveBeenCalledWith( '/models/loras/flux/my-lora.safetensors', '/models/loras/my/organized/folder', false ); }); it('should handle bulk move and ignore manual folder selection when useDefaultPath is true', async () => { // Setup state moveManager.useDefaultPath = true; moveManager.bulkFilePaths = [ '/models/loras/flux/lora1.safetensors', '/models/loras/flux/lora2.safetensors' ]; document.getElementById('moveModelRoot').innerHTML = ''; document.getElementById('moveModelRoot').value = '/models/loras'; // Manually set a selected path moveManager.folderTreeManager.selectedPath = 'wrong/folder'; mockApiClient.moveBulkModels = vi.fn().mockResolvedValue({ success: true }); await moveManager.moveModel(); // Should call moveBulkModels with the root path, NOT including the 'wrong/folder' expect(mockApiClient.moveBulkModels).toHaveBeenCalledWith( moveManager.bulkFilePaths, '/models/loras', true ); }); it('should propagate the recalculated sub_type from the move response to the card', async () => { // Setup state: moving a checkpoint into the unet root moveManager.useDefaultPath = false; moveManager.bulkFilePaths = null; moveManager.currentFilePath = '/models/checkpoints/model.safetensors'; moveManager.modelRoots = ['/models/checkpoints', '/models/unet']; document.getElementById('moveModelRoot').innerHTML = ''; document.getElementById('moveModelRoot').value = '/models/unet'; moveManager.folderTreeManager.selectedPath = ''; const updateSingleItem = vi.fn(); state.virtualScroller = { updateSingleItem, removeMultipleItemsByFilePath: vi.fn() }; mockApiClient.moveSingleModel = vi.fn().mockResolvedValue({ success: true, original_file_path: '/models/checkpoints/model.safetensors', new_file_path: '/models/unet/model.safetensors', cache_entry: { sub_type: 'diffusion_model' } }); try { await moveManager.moveModel(); expect(updateSingleItem).toHaveBeenCalledWith( '/models/checkpoints/model.safetensors', expect.objectContaining({ file_path: '/models/unet/model.safetensors', sub_type: 'diffusion_model' }) ); } finally { delete state.virtualScroller; } }); it('should omit sub_type from the card update when the response has no cache entry', async () => { moveManager.useDefaultPath = false; moveManager.bulkFilePaths = null; moveManager.currentFilePath = '/models/loras/a.safetensors'; moveManager.modelRoots = ['/models/loras']; document.getElementById('moveModelRoot').innerHTML = ''; document.getElementById('moveModelRoot').value = '/models/loras'; moveManager.folderTreeManager.selectedPath = ''; const updateSingleItem = vi.fn(); state.virtualScroller = { updateSingleItem, removeMultipleItemsByFilePath: vi.fn() }; mockApiClient.moveSingleModel = vi.fn().mockResolvedValue({ success: true, original_file_path: '/models/loras/a.safetensors', new_file_path: '/models/loras/b/a.safetensors' }); try { await moveManager.moveModel(); expect(updateSingleItem).toHaveBeenCalledWith( '/models/loras/a.safetensors', expect.not.objectContaining({ sub_type: expect.anything() }) ); } finally { delete state.virtualScroller; } }); });