/** * UI Store Tests * * Tests for toast notifications, modal stack management, and UI state. */ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest' import { setActivePinia, createPinia } from 'pinia' import { useUiStore } from '~/store/ui' describe('useUiStore', () => { beforeEach(() => { // Ensure process.env exists before creating Pinia if (!process.env) { (process as any).env = {} } process.env.NODE_ENV = 'test' // Create fresh Pinia instance for each test setActivePinia(createPinia()) vi.useFakeTimers() }) afterEach(() => { vi.restoreAllMocks() vi.useRealTimers() }) describe('initialization', () => { it('initializes with empty state', () => { const store = useUiStore() expect(store.toasts).toEqual([]) expect(store.modals).toEqual([]) expect(store.isSidebarOpen).toBe(false) expect(store.isFullscreen).toBe(false) expect(store.globalLoading).toBe(false) expect(store.globalLoadingMessage).toBeNull() }) it('has correct computed properties on init', () => { const store = useUiStore() expect(store.hasToasts).toBe(false) expect(store.hasModals).toBe(false) expect(store.currentModal).toBeNull() }) }) describe('toast management', () => { it('adds toast with showToast', () => { const store = useUiStore() const id = store.showToast('Test message', 'info', 5000) expect(store.toasts.length).toBe(1) expect(store.toasts[0]).toMatchObject({ id, type: 'info', message: 'Test message', duration: 5000, }) expect(store.hasToasts).toBe(true) }) it('adds success toast with showSuccess', () => { const store = useUiStore() const id = store.showSuccess('Success!') expect(store.toasts.length).toBe(1) expect(store.toasts[0].type).toBe('success') expect(store.toasts[0].message).toBe('Success!') expect(store.toasts[0].duration).toBe(5000) // default }) it('adds error toast with showError', () => { const store = useUiStore() const id = store.showError('Error occurred') expect(store.toasts.length).toBe(1) expect(store.toasts[0].type).toBe('error') expect(store.toasts[0].message).toBe('Error occurred') expect(store.toasts[0].duration).toBe(7000) // default for errors }) it('adds warning toast with showWarning', () => { const store = useUiStore() const id = store.showWarning('Warning!') expect(store.toasts.length).toBe(1) expect(store.toasts[0].type).toBe('warning') expect(store.toasts[0].duration).toBe(6000) // default for warnings }) it('adds info toast with showInfo', () => { const store = useUiStore() const id = store.showInfo('Info message') expect(store.toasts.length).toBe(1) expect(store.toasts[0].type).toBe('info') expect(store.toasts[0].duration).toBe(5000) }) it('auto-removes toast after duration', () => { const store = useUiStore() store.showToast('Auto-remove', 'info', 1000) expect(store.toasts.length).toBe(1) // Fast-forward time vi.advanceTimersByTime(1000) expect(store.toasts.length).toBe(0) }) it('does not auto-remove toast with duration 0', () => { const store = useUiStore() store.showToast('Persistent', 'info', 0) expect(store.toasts.length).toBe(1) // Fast-forward time vi.advanceTimersByTime(10000) // Should still be there expect(store.toasts.length).toBe(1) }) it('removes specific toast by ID', () => { const store = useUiStore() const id1 = store.showToast('Toast 1', 'info', 0) const id2 = store.showToast('Toast 2', 'info', 0) const id3 = store.showToast('Toast 3', 'info', 0) expect(store.toasts.length).toBe(3) store.removeToast(id2) expect(store.toasts.length).toBe(2) expect(store.toasts.find(t => t.id === id2)).toBeUndefined() expect(store.toasts.find(t => t.id === id1)).toBeDefined() expect(store.toasts.find(t => t.id === id3)).toBeDefined() }) it('clears all toasts', () => { const store = useUiStore() store.showToast('Toast 1', 'info', 0) store.showToast('Toast 2', 'info', 0) store.showToast('Toast 3', 'info', 0) expect(store.toasts.length).toBe(3) store.clearToasts() expect(store.toasts.length).toBe(0) expect(store.hasToasts).toBe(false) }) it('supports toast with action callback', () => { const store = useUiStore() const actionCallback = vi.fn() const id = store.showToast('Toast with action', 'info', 0, { label: 'Undo', callback: actionCallback, }) expect(store.toasts[0].action).toBeDefined() expect(store.toasts[0].action?.label).toBe('Undo') // Simulate action click store.toasts[0].action?.callback() expect(actionCallback).toHaveBeenCalledOnce() }) }) describe('modal management', () => { it('opens modal', () => { const store = useUiStore() const id = store.openModal('SubstitutionModal', { teamId: 1 }) expect(store.modals.length).toBe(1) expect(store.modals[0]).toMatchObject({ id, component: 'SubstitutionModal', props: { teamId: 1 }, }) expect(store.hasModals).toBe(true) expect(store.currentModal).toEqual(store.modals[0]) }) it('supports modal stack (LIFO)', () => { const store = useUiStore() const id1 = store.openModal('Modal1') const id2 = store.openModal('Modal2') const id3 = store.openModal('Modal3') expect(store.modals.length).toBe(3) expect(store.currentModal?.component).toBe('Modal3') // Last opened is current }) it('closes current modal (top of stack)', () => { const store = useUiStore() store.openModal('Modal1') store.openModal('Modal2') store.openModal('Modal3') expect(store.modals.length).toBe(3) store.closeModal() expect(store.modals.length).toBe(2) expect(store.currentModal?.component).toBe('Modal2') }) it('calls onClose callback when closing modal', () => { const store = useUiStore() const onCloseSpy = vi.fn() store.openModal('TestModal', {}, onCloseSpy) expect(store.modals.length).toBe(1) store.closeModal() expect(onCloseSpy).toHaveBeenCalledOnce() expect(store.modals.length).toBe(0) }) it('closes specific modal by ID', () => { const store = useUiStore() const id1 = store.openModal('Modal1') const id2 = store.openModal('Modal2') const id3 = store.openModal('Modal3') expect(store.modals.length).toBe(3) store.closeModalById(id2) expect(store.modals.length).toBe(2) expect(store.modals.find(m => m.id === id2)).toBeUndefined() expect(store.modals.find(m => m.id === id1)).toBeDefined() expect(store.modals.find(m => m.id === id3)).toBeDefined() }) it('calls onClose when closing modal by ID', () => { const store = useUiStore() const onCloseSpy = vi.fn() const id = store.openModal('TestModal', {}, onCloseSpy) store.closeModalById(id) expect(onCloseSpy).toHaveBeenCalledOnce() }) it('closes all modals', () => { const store = useUiStore() const onClose1 = vi.fn() const onClose2 = vi.fn() const onClose3 = vi.fn() store.openModal('Modal1', {}, onClose1) store.openModal('Modal2', {}, onClose2) store.openModal('Modal3', {}, onClose3) expect(store.modals.length).toBe(3) store.closeAllModals() expect(store.modals.length).toBe(0) expect(store.hasModals).toBe(false) expect(store.currentModal).toBeNull() // All onClose callbacks should be called expect(onClose1).toHaveBeenCalledOnce() expect(onClose2).toHaveBeenCalledOnce() expect(onClose3).toHaveBeenCalledOnce() }) }) describe('UI state management', () => { it('toggles sidebar', () => { const store = useUiStore() expect(store.isSidebarOpen).toBe(false) store.toggleSidebar() expect(store.isSidebarOpen).toBe(true) store.toggleSidebar() expect(store.isSidebarOpen).toBe(false) }) it('sets sidebar state directly', () => { const store = useUiStore() store.setSidebarOpen(true) expect(store.isSidebarOpen).toBe(true) store.setSidebarOpen(false) expect(store.isSidebarOpen).toBe(false) }) it('sets fullscreen state', () => { const store = useUiStore() store.setFullscreen(true) expect(store.isFullscreen).toBe(true) store.setFullscreen(false) expect(store.isFullscreen).toBe(false) }) it('shows loading overlay with message', () => { const store = useUiStore() store.showLoading('Processing...') expect(store.globalLoading).toBe(true) expect(store.globalLoadingMessage).toBe('Processing...') }) it('shows loading overlay without message', () => { const store = useUiStore() store.showLoading() expect(store.globalLoading).toBe(true) expect(store.globalLoadingMessage).toBeNull() }) it('hides loading overlay', () => { const store = useUiStore() store.showLoading('Loading...') expect(store.globalLoading).toBe(true) store.hideLoading() expect(store.globalLoading).toBe(false) expect(store.globalLoadingMessage).toBeNull() }) }) describe('edge cases', () => { it('handles removing non-existent toast gracefully', () => { const store = useUiStore() store.showToast('Toast', 'info', 0) expect(store.toasts.length).toBe(1) // Try to remove non-existent toast store.removeToast('non-existent-id') // Should not crash, toast count unchanged expect(store.toasts.length).toBe(1) }) it('handles closing non-existent modal gracefully', () => { const store = useUiStore() store.openModal('Modal1') expect(store.modals.length).toBe(1) // Try to close non-existent modal store.closeModalById('non-existent-id') // Should not crash, modal count unchanged expect(store.modals.length).toBe(1) }) it('handles closing modal when stack is empty', () => { const store = useUiStore() expect(store.modals.length).toBe(0) // Should not crash expect(() => store.closeModal()).not.toThrow() expect(store.modals.length).toBe(0) }) it('generates unique IDs for toasts', () => { const store = useUiStore() const id1 = store.showToast('Toast 1', 'info', 0) const id2 = store.showToast('Toast 2', 'info', 0) const id3 = store.showToast('Toast 3', 'info', 0) // All IDs should be unique expect(new Set([id1, id2, id3]).size).toBe(3) }) it('generates unique IDs for modals', () => { const store = useUiStore() const id1 = store.openModal('Modal1') const id2 = store.openModal('Modal2') const id3 = store.openModal('Modal3') // All IDs should be unique expect(new Set([id1, id2, id3]).size).toBe(3) }) }) })