import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; const { DOWNLOAD_MANAGER_MODULE, MODAL_MANAGER_MODULE, UI_HELPERS_MODULE, STATE_MODULE, LOADING_MANAGER_MODULE, API_FACTORY_MODULE, STORAGE_HELPERS_MODULE, FOLDER_TREE_MANAGER_MODULE, I18N_HELPERS_MODULE, SUMMARY_MODULE, mockApiClient, mockLoadingManager, showToastMock, showDownloadBatchSummaryMock, resetAndReloadMock, } = vi.hoisted(() => { // Shared API client returned by the mocked getModelApiClient factory. const mockApiClient = { apiConfig: { config: { displayName: 'LoRA', singularName: 'lora', }, }, fetchCivitaiVersions: vi.fn(), downloadModel: vi.fn(), downloadHfModel: vi.fn(), cancelDownload: vi.fn(), getPageState: vi.fn(() => ({})), }; // Shared loading manager served both via state.loadingManager and the // LoadingManager constructor mock. const mockLoadingManager = { showSimpleLoading: vi.fn(), setStatus: vi.fn(), hide: vi.fn(), restoreProgressBar: vi.fn(), showDownloadProgress: vi.fn(() => vi.fn()), showCancelButton: vi.fn(), }; return { DOWNLOAD_MANAGER_MODULE: new URL('../../../static/js/managers/DownloadManager.js', import.meta.url).pathname, MODAL_MANAGER_MODULE: new URL('../../../static/js/managers/ModalManager.js', import.meta.url).pathname, UI_HELPERS_MODULE: new URL('../../../static/js/utils/uiHelpers.js', import.meta.url).pathname, STATE_MODULE: new URL('../../../static/js/state/index.js', import.meta.url).pathname, LOADING_MANAGER_MODULE: new URL('../../../static/js/managers/LoadingManager.js', import.meta.url).pathname, API_FACTORY_MODULE: new URL('../../../static/js/api/modelApiFactory.js', import.meta.url).pathname, STORAGE_HELPERS_MODULE: new URL('../../../static/js/utils/storageHelpers.js', import.meta.url).pathname, FOLDER_TREE_MANAGER_MODULE: new URL('../../../static/js/components/FolderTreeManager.js', import.meta.url).pathname, I18N_HELPERS_MODULE: new URL('../../../static/js/utils/i18nHelpers.js', import.meta.url).pathname, SUMMARY_MODULE: new URL('../../../static/js/components/DownloadBatchSummaryModal.js', import.meta.url).pathname, mockApiClient, mockLoadingManager, showToastMock: vi.fn(), showDownloadBatchSummaryMock: vi.fn(), resetAndReloadMock: vi.fn(), }; }); vi.mock(MODAL_MANAGER_MODULE, () => ({ modalManager: { showModal: vi.fn(), closeModal: vi.fn(), }, })); vi.mock(UI_HELPERS_MODULE, () => ({ showToast: showToastMock, })); vi.mock(STATE_MODULE, () => ({ state: { global: { settings: {}, }, loadingManager: mockLoadingManager, }, })); vi.mock(LOADING_MANAGER_MODULE, () => ({ LoadingManager: vi.fn(() => mockLoadingManager), })); vi.mock(API_FACTORY_MODULE, () => ({ getModelApiClient: vi.fn(() => mockApiClient), resetAndReload: resetAndReloadMock, })); vi.mock(STORAGE_HELPERS_MODULE, () => ({ getStorageItem: vi.fn((_key, defaultValue) => defaultValue), setStorageItem: vi.fn(), })); vi.mock(FOLDER_TREE_MANAGER_MODULE, () => ({ FolderTreeManager: vi.fn(() => ({ clearSelection: vi.fn(), init: vi.fn(), })), })); vi.mock(I18N_HELPERS_MODULE, () => ({ translate: vi.fn((_, __, fallback) => fallback ?? ''), })); vi.mock(SUMMARY_MODULE, () => ({ showDownloadBatchSummary: showDownloadBatchSummaryMock, })); /** Minimal DOM used by validateAndFetchVersions / showVersionStep / batch preview. */ function setupDownloadDom() { document.body.innerHTML = `
`; } function makeVersion(id, name) { return { id, name, baseModel: 'SDXL', createdAt: '2025-01-01T00:00:00Z', availability: 'Public', images: [{ url: 'https://image.civitai.com/preview.jpg' }], files: [{ id: 1, type: 'Model', name: 'model.safetensors', sizeKB: 1000 }], modelSizeKB: 1000, existsLocally: false, hasBeenDownloaded: false, }; } // Newest-first, matching the Civitai API response order. The default selection // takes the first version, so the newest (largest id) must come first. const versions = [makeVersion(250, 'v3'), makeVersion(100, 'v1'), makeVersion(30, 'v0')]; describe('DownloadManager latest-version default', () => { let DownloadManager; let manager; beforeEach(async () => { document.body.innerHTML = ''; setupDownloadDom(); mockApiClient.fetchCivitaiVersions.mockReset(); mockLoadingManager.showSimpleLoading.mockClear(); mockLoadingManager.hide.mockClear(); vi.resetModules(); ({ DownloadManager } = await import(DOWNLOAD_MANAGER_MODULE)); manager = new DownloadManager(); manager.apiClient = mockApiClient; }); afterEach(() => { document.body.innerHTML = ''; }); describe('single URL without modelVersionId', () => { it('auto-selects the latest version so no manual version click is needed', async () => { document.getElementById('modelUrl').value = 'https://civitai.red/models/837884/midjourney-artful-nsfw'; mockApiClient.fetchCivitaiVersions.mockResolvedValue(versions); await manager.validateAndFetchVersions(); expect(manager.modelId).toBe('837884'); expect(manager.currentVersion.id).toBe(250); // The version step is shown with the latest version pre-selected. expect(document.getElementById('versionStep').style.display).toBe('block'); const selected = document.querySelector('.version-item.selected'); expect(selected.dataset.versionId).toBe('250'); expect(document.getElementById('nextFromVersion').disabled).toBe(false); }); it('still honours an explicit modelVersionId from the URL', async () => { document.getElementById('modelUrl').value = 'https://civitai.red/models/837884/midjourney-artful-nsfw?modelVersionId=30'; mockApiClient.fetchCivitaiVersions.mockResolvedValue(versions); await manager.validateAndFetchVersions(); expect(manager.currentVersion.id).toBe(30); }); }); describe('fetchVersionsForCurrentModel without modelVersionId', () => { it('auto-selects the latest version', async () => { manager.modelId = '837884'; manager.modelVersionId = null; mockApiClient.fetchCivitaiVersions.mockResolvedValue(versions); await manager.fetchVersionsForCurrentModel(); expect(manager.currentVersion.id).toBe(250); }); it('still honours an explicit modelVersionId', async () => { manager.modelId = '837884'; manager.modelVersionId = '100'; mockApiClient.fetchCivitaiVersions.mockResolvedValue(versions); await manager.fetchVersionsForCurrentModel(); expect(manager.currentVersion.id).toBe(100); }); }); describe('multi-URL batch without modelVersionId', () => { it('defaults each item to its latest version (first in API response)', async () => { document.getElementById('modelUrl').value = 'https://civitai.red/models/111/foo\nhttps://civitai.red/models/222/bar'; const versionsA = [makeVersion(40, 'a2'), makeVersion(10, 'a1')]; const versionsB = [makeVersion(500, 'b1'), makeVersion(7, 'b2')]; mockApiClient.fetchCivitaiVersions.mockImplementation(async modelId => modelId === '111' ? versionsA : versionsB ); await manager.validateAndFetchVersions(); expect(manager.isBatchMode).toBe(true); expect(manager.batchModels).toHaveLength(2); expect(manager.batchModels[0].selectedVersion.id).toBe(40); expect(manager.batchModels[1].selectedVersion.id).toBe(500); }); it('still honours an explicit modelVersionId per URL', async () => { document.getElementById('modelUrl').value = 'https://civitai.red/models/111/foo?modelVersionId=10\nhttps://civitai.red/models/222/bar'; const versionsA = [makeVersion(40, 'a2'), makeVersion(10, 'a1')]; const versionsB = [makeVersion(500, 'b1'), makeVersion(7, 'b2')]; mockApiClient.fetchCivitaiVersions.mockImplementation(async modelId => modelId === '111' ? versionsA : versionsB ); await manager.validateAndFetchVersions(); expect(manager.batchModels[0].selectedVersion.id).toBe(10); expect(manager.batchModels[1].selectedVersion.id).toBe(500); }); }); });