mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
test(frontend): add storage and core initialization specs
This commit is contained in:
277
static/js/core.test.js
Normal file
277
static/js/core.test.js
Normal file
@@ -0,0 +1,277 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
|
||||
const migrateStorageItemsMock = vi.fn();
|
||||
const initializeInfiniteScrollMock = vi.fn();
|
||||
const initThemeMock = vi.fn();
|
||||
const initBackToTopMock = vi.fn();
|
||||
const initializeEventManagementMock = vi.fn();
|
||||
const createPageContextMenuMock = vi.fn().mockReturnValue('page-menu');
|
||||
const createGlobalContextMenuMock = vi.fn().mockReturnValue('global-menu');
|
||||
const bulkManagerInitializeMock = vi.fn();
|
||||
const setBulkContextMenuMock = vi.fn();
|
||||
const helpManagerInitializeMock = vi.fn();
|
||||
const updateServiceInitializeMock = vi.fn();
|
||||
const bannerServiceInitializeMock = vi.fn();
|
||||
const isBannerVisibleMock = vi.fn().mockReturnValue(false);
|
||||
const onboardingStartMock = vi.fn();
|
||||
const settingsWaitMock = vi.fn().mockResolvedValue();
|
||||
const i18nWaitMock = vi.fn().mockResolvedValue();
|
||||
const i18nLocaleMock = vi.fn().mockReturnValue('en');
|
||||
|
||||
const mockState = {
|
||||
currentPageType: 'loras',
|
||||
global: {
|
||||
settings: {
|
||||
card_info_display: 'hover'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const mockModalManager = { initialize: vi.fn() };
|
||||
const mockBulkManager = {
|
||||
initialize: bulkManagerInitializeMock,
|
||||
setBulkContextMenu: setBulkContextMenuMock
|
||||
};
|
||||
const mockHelpManager = { initialize: helpManagerInitializeMock };
|
||||
const mockUpdateService = { initialize: updateServiceInitializeMock };
|
||||
const mockBannerService = {
|
||||
initialize: bannerServiceInitializeMock,
|
||||
isBannerVisible: isBannerVisibleMock
|
||||
};
|
||||
const mockOnboardingManager = { start: onboardingStartMock };
|
||||
const mockSettingsManager = { waitForInitialization: settingsWaitMock };
|
||||
const mockMoveManager = {};
|
||||
const mockI18n = {
|
||||
waitForReady: i18nWaitMock,
|
||||
getCurrentLocale: i18nLocaleMock
|
||||
};
|
||||
|
||||
const loadingManagerInstances = [];
|
||||
const HeaderManagerInstances = [];
|
||||
const bulkContextMenuInstances = [];
|
||||
const exampleImagesManagerInitializeMock = vi.fn();
|
||||
|
||||
const LoadingManagerMock = vi.fn(() => {
|
||||
const instance = { id: Symbol('LoadingManager') };
|
||||
loadingManagerInstances.push(instance);
|
||||
return instance;
|
||||
});
|
||||
|
||||
const HeaderManagerMock = vi.fn(() => {
|
||||
const instance = { id: Symbol('HeaderManager') };
|
||||
HeaderManagerInstances.push(instance);
|
||||
return instance;
|
||||
});
|
||||
|
||||
const BulkContextMenuMock = vi.fn(() => {
|
||||
const instance = { id: Symbol('BulkContextMenu') };
|
||||
bulkContextMenuInstances.push(instance);
|
||||
return instance;
|
||||
});
|
||||
|
||||
const ExampleImagesManagerMock = vi.fn(() => {
|
||||
const instance = { initialize: exampleImagesManagerInitializeMock };
|
||||
globalThis.exampleImagesManager = instance;
|
||||
return instance;
|
||||
});
|
||||
|
||||
vi.stubGlobal('exampleImagesManager', null);
|
||||
|
||||
vi.mock('./utils/storageHelpers.js', () => ({
|
||||
migrateStorageItems: migrateStorageItemsMock
|
||||
}));
|
||||
|
||||
vi.mock('./state/index.js', () => ({
|
||||
state: mockState
|
||||
}));
|
||||
|
||||
vi.mock('./managers/LoadingManager.js', () => ({
|
||||
LoadingManager: LoadingManagerMock
|
||||
}));
|
||||
|
||||
vi.mock('./managers/ModalManager.js', () => ({
|
||||
ModalManager: vi.fn(),
|
||||
modalManager: mockModalManager
|
||||
}));
|
||||
|
||||
vi.mock('./managers/UpdateService.js', () => ({
|
||||
updateService: mockUpdateService
|
||||
}));
|
||||
|
||||
vi.mock('./components/Header.js', () => ({
|
||||
HeaderManager: HeaderManagerMock
|
||||
}));
|
||||
|
||||
vi.mock('./managers/SettingsManager.js', () => ({
|
||||
settingsManager: mockSettingsManager
|
||||
}));
|
||||
|
||||
vi.mock('./managers/MoveManager.js', () => ({
|
||||
moveManager: mockMoveManager
|
||||
}));
|
||||
|
||||
vi.mock('./managers/BulkManager.js', () => ({
|
||||
bulkManager: mockBulkManager
|
||||
}));
|
||||
|
||||
vi.mock('./managers/ExampleImagesManager.js', () => ({
|
||||
ExampleImagesManager: ExampleImagesManagerMock
|
||||
}));
|
||||
|
||||
vi.mock('./managers/HelpManager.js', () => ({
|
||||
helpManager: mockHelpManager
|
||||
}));
|
||||
|
||||
vi.mock('./managers/BannerService.js', () => ({
|
||||
bannerService: mockBannerService
|
||||
}));
|
||||
|
||||
vi.mock('./utils/uiHelpers.js', () => ({
|
||||
initTheme: initThemeMock,
|
||||
initBackToTop: initBackToTopMock
|
||||
}));
|
||||
|
||||
vi.mock('./utils/infiniteScroll.js', () => ({
|
||||
initializeInfiniteScroll: initializeInfiniteScrollMock
|
||||
}));
|
||||
|
||||
vi.mock('./i18n/index.js', () => ({
|
||||
i18n: mockI18n
|
||||
}));
|
||||
|
||||
vi.mock('./managers/OnboardingManager.js', () => ({
|
||||
onboardingManager: mockOnboardingManager
|
||||
}));
|
||||
|
||||
vi.mock('./components/ContextMenu/BulkContextMenu.js', () => ({
|
||||
BulkContextMenu: BulkContextMenuMock
|
||||
}));
|
||||
|
||||
vi.mock('./components/ContextMenu/index.js', () => ({
|
||||
createPageContextMenu: createPageContextMenuMock,
|
||||
createGlobalContextMenu: createGlobalContextMenuMock
|
||||
}));
|
||||
|
||||
vi.mock('./utils/eventManagementInit.js', () => ({
|
||||
initializeEventManagement: initializeEventManagementMock
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
document.body.innerHTML = '';
|
||||
document.body.removeAttribute('data-page');
|
||||
mockState.currentPageType = 'loras';
|
||||
mockState.global.settings.card_info_display = 'hover';
|
||||
isBannerVisibleMock.mockReturnValue(false);
|
||||
loadingManagerInstances.length = 0;
|
||||
HeaderManagerInstances.length = 0;
|
||||
bulkContextMenuInstances.length = 0;
|
||||
delete window.pageContextMenu;
|
||||
delete window.globalContextMenuInstance;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
const loadCoreModule = async () => {
|
||||
vi.resetModules();
|
||||
return import('./core.js');
|
||||
};
|
||||
|
||||
describe('AppCore module bootstrapping', () => {
|
||||
it('registers storage migration on DOMContentLoaded', async () => {
|
||||
const addEventListenerSpy = vi.spyOn(document, 'addEventListener');
|
||||
await loadCoreModule();
|
||||
|
||||
expect(addEventListenerSpy).toHaveBeenCalledWith('DOMContentLoaded', expect.any(Function));
|
||||
const listener = addEventListenerSpy.mock.calls.find(([event]) => event === 'DOMContentLoaded')[1];
|
||||
listener();
|
||||
expect(migrateStorageItemsMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('initializes managers, UI helpers, and onboarding sequence', async () => {
|
||||
vi.useFakeTimers();
|
||||
const { AppCore } = await loadCoreModule();
|
||||
const appCore = new AppCore();
|
||||
|
||||
document.body.dataset.page = 'loras';
|
||||
|
||||
const result = await appCore.initialize();
|
||||
|
||||
expect(result).toBe(appCore);
|
||||
expect(i18nWaitMock).toHaveBeenCalled();
|
||||
expect(i18nLocaleMock).toHaveBeenCalled();
|
||||
expect(settingsWaitMock).toHaveBeenCalled();
|
||||
|
||||
expect(LoadingManagerMock).toHaveBeenCalledTimes(1);
|
||||
const loadingInstance = LoadingManagerMock.mock.results[0].value;
|
||||
expect(mockState.loadingManager).toBe(loadingInstance);
|
||||
|
||||
expect(mockModalManager.initialize).toHaveBeenCalled();
|
||||
expect(updateServiceInitializeMock).toHaveBeenCalled();
|
||||
expect(bannerServiceInitializeMock).toHaveBeenCalled();
|
||||
expect(window.modalManager).toBe(mockModalManager);
|
||||
expect(window.settingsManager).toBe(mockSettingsManager);
|
||||
expect(window.bulkManager).toBe(mockBulkManager);
|
||||
expect(window.helpManager).toBe(mockHelpManager);
|
||||
|
||||
expect(HeaderManagerMock).toHaveBeenCalledTimes(1);
|
||||
expect(initThemeMock).toHaveBeenCalled();
|
||||
expect(initBackToTopMock).toHaveBeenCalled();
|
||||
|
||||
expect(bulkManagerInitializeMock).toHaveBeenCalled();
|
||||
expect(BulkContextMenuMock).toHaveBeenCalledTimes(1);
|
||||
expect(setBulkContextMenuMock).toHaveBeenCalledWith(bulkContextMenuInstances[0]);
|
||||
|
||||
expect(ExampleImagesManagerMock).toHaveBeenCalledTimes(1);
|
||||
expect(exampleImagesManagerInitializeMock).toHaveBeenCalled();
|
||||
expect(helpManagerInitializeMock).toHaveBeenCalled();
|
||||
|
||||
expect(document.body.classList.contains('hover-reveal')).toBe(true);
|
||||
expect(initializeEventManagementMock).toHaveBeenCalled();
|
||||
|
||||
vi.runAllTimers();
|
||||
expect(onboardingStartMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('skips bulk manager when recipes page is active', async () => {
|
||||
const { AppCore } = await loadCoreModule();
|
||||
const appCore = new AppCore();
|
||||
mockState.currentPageType = 'recipes';
|
||||
|
||||
await appCore.initialize();
|
||||
|
||||
expect(bulkManagerInitializeMock).not.toHaveBeenCalled();
|
||||
expect(BulkContextMenuMock).not.toHaveBeenCalled();
|
||||
expect(setBulkContextMenuMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('initializes page features with context menus and infinite scroll', async () => {
|
||||
const { AppCore } = await loadCoreModule();
|
||||
const appCore = new AppCore();
|
||||
document.body.dataset.page = 'loras';
|
||||
|
||||
appCore.initializeContextMenus = vi.fn(appCore.initializeContextMenus.bind(appCore));
|
||||
|
||||
appCore.initializePageFeatures();
|
||||
|
||||
expect(appCore.initializeContextMenus).toHaveBeenCalledWith('loras');
|
||||
expect(initializeInfiniteScrollMock).toHaveBeenCalledWith('loras');
|
||||
expect(createPageContextMenuMock).toHaveBeenCalledWith('loras');
|
||||
expect(window.pageContextMenu).toBe('page-menu');
|
||||
expect(createGlobalContextMenuMock).toHaveBeenCalled();
|
||||
expect(window.globalContextMenuInstance).toBe('global-menu');
|
||||
});
|
||||
|
||||
it('does not reinitialize once initialized', async () => {
|
||||
const { AppCore } = await loadCoreModule();
|
||||
const appCore = new AppCore();
|
||||
|
||||
await appCore.initialize();
|
||||
await appCore.initialize();
|
||||
|
||||
expect(i18nWaitMock).toHaveBeenCalledTimes(1);
|
||||
expect(settingsWaitMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
142
static/js/utils/storageHelpers.test.js
Normal file
142
static/js/utils/storageHelpers.test.js
Normal file
@@ -0,0 +1,142 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import * as storageHelpers from './storageHelpers.js';
|
||||
|
||||
const {
|
||||
getStorageItem,
|
||||
setStorageItem,
|
||||
removeStorageItem,
|
||||
getSessionItem,
|
||||
setSessionItem,
|
||||
removeSessionItem,
|
||||
migrateStorageItems
|
||||
} = storageHelpers;
|
||||
|
||||
const createFakeStorage = () => {
|
||||
const store = new Map();
|
||||
return {
|
||||
getItem: vi.fn((key) => (store.has(key) ? store.get(key) : null)),
|
||||
setItem: vi.fn((key, value) => {
|
||||
store.set(key, value);
|
||||
}),
|
||||
removeItem: vi.fn((key) => {
|
||||
store.delete(key);
|
||||
}),
|
||||
clear: vi.fn(() => {
|
||||
store.clear();
|
||||
}),
|
||||
key: vi.fn((index) => Array.from(store.keys())[index] ?? null),
|
||||
get length() {
|
||||
return store.size;
|
||||
},
|
||||
_store: store
|
||||
};
|
||||
};
|
||||
|
||||
let localStorageMock;
|
||||
let sessionStorageMock;
|
||||
let consoleLogMock;
|
||||
|
||||
beforeEach(() => {
|
||||
localStorageMock = createFakeStorage();
|
||||
sessionStorageMock = createFakeStorage();
|
||||
vi.stubGlobal('localStorage', localStorageMock);
|
||||
vi.stubGlobal('sessionStorage', sessionStorageMock);
|
||||
consoleLogMock = vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('storageHelpers namespace utilities', () => {
|
||||
it('returns parsed JSON for prefixed localStorage items', () => {
|
||||
localStorage.setItem('lora_manager_preferences', JSON.stringify({ theme: 'dark' }));
|
||||
|
||||
const result = getStorageItem('preferences');
|
||||
|
||||
expect(result).toEqual({ theme: 'dark' });
|
||||
expect(localStorage.getItem).toHaveBeenCalledWith('lora_manager_preferences');
|
||||
});
|
||||
|
||||
it('falls back to legacy keys and migrates them to the namespace', () => {
|
||||
localStorage.setItem('legacy_key', 'value');
|
||||
|
||||
const value = getStorageItem('legacy_key');
|
||||
|
||||
expect(value).toBe('value');
|
||||
expect(localStorage.getItem('lora_manager_legacy_key')).toBe('value');
|
||||
});
|
||||
|
||||
it('serializes objects when setting prefixed localStorage values', () => {
|
||||
const data = { ids: [1, 2, 3] };
|
||||
|
||||
setStorageItem('data', data);
|
||||
|
||||
expect(localStorage.setItem).toHaveBeenCalledWith('lora_manager_data', JSON.stringify(data));
|
||||
expect(localStorage.getItem('lora_manager_data')).toEqual(JSON.stringify(data));
|
||||
});
|
||||
|
||||
it('removes both prefixed and legacy localStorage entries', () => {
|
||||
localStorage.setItem('lora_manager_temp', '123');
|
||||
localStorage.setItem('temp', '456');
|
||||
|
||||
removeStorageItem('temp');
|
||||
|
||||
expect(localStorage.getItem('lora_manager_temp')).toBeNull();
|
||||
expect(localStorage.getItem('temp')).toBeNull();
|
||||
});
|
||||
|
||||
it('returns parsed JSON for session storage items', () => {
|
||||
sessionStorage.setItem('lora_manager_session', JSON.stringify({ page: 'loras' }));
|
||||
|
||||
const session = getSessionItem('session');
|
||||
|
||||
expect(session).toEqual({ page: 'loras' });
|
||||
});
|
||||
|
||||
it('stores primitives in session storage directly', () => {
|
||||
setSessionItem('token', 'abc123');
|
||||
|
||||
expect(sessionStorage.setItem).toHaveBeenCalledWith('lora_manager_token', 'abc123');
|
||||
expect(sessionStorage.getItem('lora_manager_token')).toBe('abc123');
|
||||
});
|
||||
|
||||
it('removes session storage entries by namespace', () => {
|
||||
sessionStorage.setItem('lora_manager_flag', '1');
|
||||
|
||||
removeSessionItem('flag');
|
||||
|
||||
expect(sessionStorage.getItem('lora_manager_flag')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('migrateStorageItems', () => {
|
||||
it('migrates known keys and logs completion', () => {
|
||||
const setStorageSpy = vi.spyOn(storageHelpers, 'setStorageItem');
|
||||
localStorage.setItem('theme', '"light"');
|
||||
localStorage.setItem('loras_filters', JSON.stringify({ sort: 'asc' }));
|
||||
localStorage.setItem('nsfwBlurLevel', '3');
|
||||
|
||||
migrateStorageItems();
|
||||
|
||||
expect(setStorageSpy).toHaveBeenCalledTimes(3);
|
||||
expect(localStorage.getItem('lora_manager_theme')).toBe('light');
|
||||
expect(localStorage.getItem('lora_manager_loras_filters')).toBe(JSON.stringify({ sort: 'asc' }));
|
||||
expect(localStorage.getItem('loras_filters')).toBeNull();
|
||||
expect(localStorage.getItem('lora_manager_nsfwBlurLevel')).toBe('3');
|
||||
expect(localStorage.getItem('nsfwBlurLevel')).toBeNull();
|
||||
expect(localStorage.getItem('lora_manager_migration_completed')).toBe('true');
|
||||
expect(consoleLogMock).toHaveBeenCalledWith('Lora Manager: Storage migration completed');
|
||||
});
|
||||
|
||||
it('skips migration when already completed and logs notice', () => {
|
||||
const setStorageSpy = vi.spyOn(storageHelpers, 'setStorageItem');
|
||||
localStorage.setItem('lora_manager_migration_completed', 'true');
|
||||
|
||||
migrateStorageItems();
|
||||
|
||||
expect(setStorageSpy).not.toHaveBeenCalled();
|
||||
expect(consoleLogMock).toHaveBeenCalledWith('Lora Manager: Storage migration already completed');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user