From 0e5ef789b7eeda8f3bf4cf131edc3a04aa5be2a8 Mon Sep 17 00:00:00 2001 From: Agent Zero Date: Fri, 24 Jul 2026 01:29:49 +0200 Subject: [PATCH] test(7.7): add tests for authStore, uiStore, commStore, pluginToolbarStore, calendarStore --- .../src/store/__tests__/authStore.test.ts | 155 +++++++++++ .../src/store/__tests__/calendarStore.test.ts | 256 ++++++++++++++++++ .../src/store/__tests__/commStore.test.ts | 197 ++++++++++++++ .../__tests__/pluginToolbarStore.test.ts | 134 +++++++++ frontend/src/store/__tests__/uiStore.test.ts | 247 +++++++++++++++++ 5 files changed, 989 insertions(+) create mode 100644 frontend/src/store/__tests__/authStore.test.ts create mode 100644 frontend/src/store/__tests__/calendarStore.test.ts create mode 100644 frontend/src/store/__tests__/commStore.test.ts create mode 100644 frontend/src/store/__tests__/pluginToolbarStore.test.ts create mode 100644 frontend/src/store/__tests__/uiStore.test.ts diff --git a/frontend/src/store/__tests__/authStore.test.ts b/frontend/src/store/__tests__/authStore.test.ts new file mode 100644 index 0000000..184afc4 --- /dev/null +++ b/frontend/src/store/__tests__/authStore.test.ts @@ -0,0 +1,155 @@ +/** + * Tests for authStore — Phase 7 Task 7.7 + * + * Tests: Initial state, setUser, setTenant, setAuthenticated, setLoading, + * setError, setPermissions, logout + */ +import { describe, it, expect, beforeEach } from 'vitest'; +import { useAuthStore, type User, type Tenant } from '../authStore'; + +const mockTenant: Tenant = { id: 't1', name: 'Tenant A', slug: 'tenant-a' }; +const mockTenant2: Tenant = { id: 't2', name: 'Tenant B', slug: 'tenant-b' }; + +const mockUser: User = { + id: 'u1', + email: 'test@test.com', + first_name: 'Test', + last_name: 'User', + role: 'admin', + avatar_url: null, + tenants: [mockTenant, mockTenant2], +}; + +describe('authStore', () => { + beforeEach(() => { + useAuthStore.getState().logout(); + }); + + describe('initial state', () => { + it('starts with null user', () => { + expect(useAuthStore.getState().user).toBeNull(); + }); + + it('starts with null currentTenant', () => { + expect(useAuthStore.getState().currentTenant).toBeNull(); + }); + + it('starts unauthenticated', () => { + expect(useAuthStore.getState().isAuthenticated).toBe(false); + }); + + it('starts not loading', () => { + expect(useAuthStore.getState().isLoading).toBe(false); + }); + + it('starts with null error', () => { + expect(useAuthStore.getState().error).toBeNull(); + }); + }); + + describe('setUser', () => { + it('sets user and marks as authenticated', () => { + useAuthStore.getState().setUser(mockUser); + expect(useAuthStore.getState().user).toEqual(mockUser); + expect(useAuthStore.getState().isAuthenticated).toBe(true); + }); + + it('sets currentTenant to first tenant from user', () => { + useAuthStore.getState().setUser(mockUser); + expect(useAuthStore.getState().currentTenant).toEqual(mockTenant); + }); + + it('sets currentTenant to null when user has no tenants', () => { + useAuthStore.getState().setUser({ ...mockUser, tenants: [] }); + expect(useAuthStore.getState().currentTenant).toBeNull(); + }); + + it('sets user to null and unauthenticates', () => { + useAuthStore.getState().setUser(mockUser); + useAuthStore.getState().setUser(null); + expect(useAuthStore.getState().user).toBeNull(); + expect(useAuthStore.getState().isAuthenticated).toBe(false); + expect(useAuthStore.getState().currentTenant).toBeNull(); + }); + }); + + describe('setTenant', () => { + it('sets current tenant', () => { + useAuthStore.getState().setTenant(mockTenant2); + expect(useAuthStore.getState().currentTenant).toEqual(mockTenant2); + }); + + it('sets tenant to null', () => { + useAuthStore.getState().setTenant(mockTenant); + useAuthStore.getState().setTenant(null); + expect(useAuthStore.getState().currentTenant).toBeNull(); + }); + }); + + describe('setAuthenticated', () => { + it('sets authenticated to true', () => { + useAuthStore.getState().setAuthenticated(true); + expect(useAuthStore.getState().isAuthenticated).toBe(true); + }); + + it('sets authenticated to false', () => { + useAuthStore.getState().setAuthenticated(true); + useAuthStore.getState().setAuthenticated(false); + expect(useAuthStore.getState().isAuthenticated).toBe(false); + }); + }); + + describe('setLoading', () => { + it('sets loading to true', () => { + useAuthStore.getState().setLoading(true); + expect(useAuthStore.getState().isLoading).toBe(true); + }); + + it('sets loading to false', () => { + useAuthStore.getState().setLoading(true); + useAuthStore.getState().setLoading(false); + expect(useAuthStore.getState().isLoading).toBe(false); + }); + }); + + describe('setError', () => { + it('sets error message', () => { + useAuthStore.getState().setError('Login failed'); + expect(useAuthStore.getState().error).toBe('Login failed'); + }); + + it('clears error with null', () => { + useAuthStore.getState().setError('Error'); + useAuthStore.getState().setError(null); + expect(useAuthStore.getState().error).toBeNull(); + }); + }); + + describe('setPermissions', () => { + it('sets permissions on existing user', () => { + useAuthStore.getState().setUser(mockUser); + useAuthStore.getState().setPermissions(['read', 'write'], true, { annual_revenue: 'hidden' }); + const user = useAuthStore.getState().user!; + expect(user.permissions).toEqual(['read', 'write']); + expect(user.is_system_admin).toBe(true); + expect(user.field_permissions).toEqual({ annual_revenue: 'hidden' }); + }); + + it('does nothing when user is null', () => { + useAuthStore.getState().setPermissions(['read'], false, {}); + expect(useAuthStore.getState().user).toBeNull(); + }); + }); + + describe('logout', () => { + it('clears all auth state', () => { + useAuthStore.getState().setUser(mockUser); + useAuthStore.getState().setError('Some error'); + useAuthStore.getState().logout(); + expect(useAuthStore.getState().user).toBeNull(); + expect(useAuthStore.getState().currentTenant).toBeNull(); + expect(useAuthStore.getState().isAuthenticated).toBe(false); + expect(useAuthStore.getState().error).toBeNull(); + }); + }); +}); diff --git a/frontend/src/store/__tests__/calendarStore.test.ts b/frontend/src/store/__tests__/calendarStore.test.ts new file mode 100644 index 0000000..f525c17 --- /dev/null +++ b/frontend/src/store/__tests__/calendarStore.test.ts @@ -0,0 +1,256 @@ +/** + * Tests for calendarStore — Phase 7 Task 7.7 + * + * Tests: Initial state, view mode, navigation (month/week/day), + * activeCalendarId, calendars, selectedEntry, toggleCalendarVisibility + */ +import { describe, it, expect, beforeEach } from 'vitest'; +import { useCalendarStore } from '../calendarStore'; + +describe('calendarStore', () => { + beforeEach(() => { + // Reset to known state + const today = new Date(); + useCalendarStore.setState({ + visibleMonth: new Date(today.getFullYear(), today.getMonth(), 1), + visibleWeek: new Date(today), + visibleDay: new Date(today), + rangeStart: new Date(today), + rangeEnd: new Date(today.getFullYear(), today.getMonth(), today.getDate() + 30), + viewMode: 'month', + activeCalendarId: null, + calendars: [], + selectedEntry: null, + visibleCalendarIds: new Set(), + }); + }); + + describe('initial state', () => { + it('starts with month view mode', () => { + expect(useCalendarStore.getState().viewMode).toBe('month'); + }); + + it('starts with null activeCalendarId', () => { + expect(useCalendarStore.getState().activeCalendarId).toBeNull(); + }); + + it('starts with empty calendars', () => { + expect(useCalendarStore.getState().calendars).toEqual([]); + }); + + it('starts with null selectedEntry', () => { + expect(useCalendarStore.getState().selectedEntry).toBeNull(); + }); + + it('starts with empty visibleCalendarIds', () => { + expect(useCalendarStore.getState().visibleCalendarIds.size).toBe(0); + }); + }); + + describe('setViewMode', () => { + it('sets view mode to day', () => { + useCalendarStore.getState().setViewMode('day'); + expect(useCalendarStore.getState().viewMode).toBe('day'); + }); + + it('sets view mode to week', () => { + useCalendarStore.getState().setViewMode('week'); + expect(useCalendarStore.getState().viewMode).toBe('week'); + }); + + it('sets view mode to range', () => { + useCalendarStore.getState().setViewMode('range'); + expect(useCalendarStore.getState().viewMode).toBe('range'); + }); + }); + + describe('goToNextMonth', () => { + it('advances visible month by 1', () => { + const initial = new Date(2024, 5, 1); // June 2024 + useCalendarStore.getState().setVisibleMonth(initial); + useCalendarStore.getState().goToNextMonth(); + expect(useCalendarStore.getState().visibleMonth.getMonth()).toBe(6); // July + expect(useCalendarStore.getState().visibleMonth.getFullYear()).toBe(2024); + }); + + it('wraps to next year when in December', () => { + const initial = new Date(2024, 11, 1); // December 2024 + useCalendarStore.getState().setVisibleMonth(initial); + useCalendarStore.getState().goToNextMonth(); + expect(useCalendarStore.getState().visibleMonth.getMonth()).toBe(0); // January + expect(useCalendarStore.getState().visibleMonth.getFullYear()).toBe(2025); + }); + }); + + describe('goToPrevMonth', () => { + it('decreases visible month by 1', () => { + const initial = new Date(2024, 5, 1); // June 2024 + useCalendarStore.getState().setVisibleMonth(initial); + useCalendarStore.getState().goToPrevMonth(); + expect(useCalendarStore.getState().visibleMonth.getMonth()).toBe(4); // May + }); + + it('wraps to previous year when in January', () => { + const initial = new Date(2024, 0, 1); // January 2024 + useCalendarStore.getState().setVisibleMonth(initial); + useCalendarStore.getState().goToPrevMonth(); + expect(useCalendarStore.getState().visibleMonth.getMonth()).toBe(11); // December + expect(useCalendarStore.getState().visibleMonth.getFullYear()).toBe(2023); + }); + }); + + describe('goToNextWeek', () => { + it('advances visible week by 7 days', () => { + const initial = new Date(2024, 5, 10); // June 10, 2024 + useCalendarStore.getState().setVisibleWeek(initial); + useCalendarStore.getState().goToNextWeek(); + expect(useCalendarStore.getState().visibleWeek.getDate()).toBe(17); + }); + }); + + describe('goToPrevWeek', () => { + it('decreases visible week by 7 days', () => { + const initial = new Date(2024, 5, 17); // June 17, 2024 + useCalendarStore.getState().setVisibleWeek(initial); + useCalendarStore.getState().goToPrevWeek(); + expect(useCalendarStore.getState().visibleWeek.getDate()).toBe(10); + }); + }); + + describe('goToNextDay', () => { + it('advances visible day by 1', () => { + const initial = new Date(2024, 5, 15); + useCalendarStore.getState().setVisibleDay(initial); + useCalendarStore.getState().goToNextDay(); + expect(useCalendarStore.getState().visibleDay.getDate()).toBe(16); + }); + }); + + describe('goToPrevDay', () => { + it('decreases visible day by 1', () => { + const initial = new Date(2024, 5, 15); + useCalendarStore.getState().setVisibleDay(initial); + useCalendarStore.getState().goToPrevDay(); + expect(useCalendarStore.getState().visibleDay.getDate()).toBe(14); + }); + }); + + describe('goToToday', () => { + it('sets visibleMonth to current month start', () => { + useCalendarStore.getState().setVisibleMonth(new Date(2020, 0, 1)); + useCalendarStore.getState().goToToday(); + const today = new Date(); + expect(useCalendarStore.getState().visibleMonth.getMonth()).toBe(today.getMonth()); + expect(useCalendarStore.getState().visibleMonth.getFullYear()).toBe(today.getFullYear()); + }); + + it('sets visibleDay to today', () => { + useCalendarStore.getState().setVisibleDay(new Date(2020, 0, 1)); + useCalendarStore.getState().goToToday(); + const today = new Date(); + expect(useCalendarStore.getState().visibleDay.getDate()).toBe(today.getDate()); + }); + }); + + describe('setActiveCalendarId', () => { + it('sets active calendar id', () => { + useCalendarStore.getState().setActiveCalendarId('cal-1'); + expect(useCalendarStore.getState().activeCalendarId).toBe('cal-1'); + }); + + it('sets active calendar id to null', () => { + useCalendarStore.getState().setActiveCalendarId('cal-1'); + useCalendarStore.getState().setActiveCalendarId(null); + expect(useCalendarStore.getState().activeCalendarId).toBeNull(); + }); + }); + + describe('setCalendars', () => { + it('sets calendars list', () => { + const mockCalendars = [ + { id: 'cal-1', name: 'Calendar 1', color: '#ff0000', tenant_id: 't1', owner_id: 'u1', is_active: true, created_at: '', updated_at: '' }, + ] as any; + useCalendarStore.getState().setCalendars(mockCalendars); + expect(useCalendarStore.getState().calendars).toHaveLength(1); + }); + }); + + describe('setSelectedEntry', () => { + it('sets selected entry', () => { + const mockEntry = { id: 'entry-1', title: 'Meeting' } as any; + useCalendarStore.getState().setSelectedEntry(mockEntry); + expect(useCalendarStore.getState().selectedEntry).toEqual(mockEntry); + }); + + it('clears selected entry with null', () => { + const mockEntry = { id: 'entry-1', title: 'Meeting' } as any; + useCalendarStore.getState().setSelectedEntry(mockEntry); + useCalendarStore.getState().setSelectedEntry(null); + expect(useCalendarStore.getState().selectedEntry).toBeNull(); + }); + }); + + describe('toggleCalendarVisibility', () => { + it('adds calendar id to visible set', () => { + useCalendarStore.getState().toggleCalendarVisibility('cal-1'); + expect(useCalendarStore.getState().visibleCalendarIds.has('cal-1')).toBe(true); + }); + + it('removes calendar id from visible set when already present', () => { + useCalendarStore.getState().toggleCalendarVisibility('cal-1'); + useCalendarStore.getState().toggleCalendarVisibility('cal-1'); + expect(useCalendarStore.getState().visibleCalendarIds.has('cal-1')).toBe(false); + }); + + it('does not affect other calendar ids', () => { + useCalendarStore.getState().toggleCalendarVisibility('cal-1'); + useCalendarStore.getState().toggleCalendarVisibility('cal-2'); + useCalendarStore.getState().toggleCalendarVisibility('cal-1'); + expect(useCalendarStore.getState().visibleCalendarIds.has('cal-2')).toBe(true); + expect(useCalendarStore.getState().visibleCalendarIds.has('cal-1')).toBe(false); + }); + }); + + describe('setVisibleMonth', () => { + it('normalizes date to start of month', () => { + useCalendarStore.getState().setVisibleMonth(new Date(2024, 5, 15)); + expect(useCalendarStore.getState().visibleMonth.getDate()).toBe(1); + expect(useCalendarStore.getState().visibleMonth.getMonth()).toBe(5); + }); + }); + + describe('setVisibleDay', () => { + it('normalizes date to start of day (midnight)', () => { + useCalendarStore.getState().setVisibleDay(new Date(2024, 5, 15, 14, 30, 45)); + expect(useCalendarStore.getState().visibleDay.getHours()).toBe(0); + expect(useCalendarStore.getState().visibleDay.getMinutes()).toBe(0); + expect(useCalendarStore.getState().visibleDay.getSeconds()).toBe(0); + }); + }); + + describe('setRangeStart', () => { + it('sets range start to start of day', () => { + useCalendarStore.getState().setRangeStart(new Date(2024, 5, 15, 14, 30)); + expect(useCalendarStore.getState().rangeStart?.getHours()).toBe(0); + expect(useCalendarStore.getState().rangeStart?.getDate()).toBe(15); + }); + + it('sets range start to null', () => { + useCalendarStore.getState().setRangeStart(null); + expect(useCalendarStore.getState().rangeStart).toBeNull(); + }); + }); + + describe('setRangeEnd', () => { + it('sets range end to start of day', () => { + useCalendarStore.getState().setRangeEnd(new Date(2024, 5, 20, 16, 45)); + expect(useCalendarStore.getState().rangeEnd?.getHours()).toBe(0); + expect(useCalendarStore.getState().rangeEnd?.getDate()).toBe(20); + }); + + it('sets range end to null', () => { + useCalendarStore.getState().setRangeEnd(null); + expect(useCalendarStore.getState().rangeEnd).toBeNull(); + }); + }); +}); diff --git a/frontend/src/store/__tests__/commStore.test.ts b/frontend/src/store/__tests__/commStore.test.ts new file mode 100644 index 0000000..401bed6 --- /dev/null +++ b/frontend/src/store/__tests__/commStore.test.ts @@ -0,0 +1,197 @@ +/** + * Tests for commStore — Phase 7 Task 7.7 + * + * Tests: Initial state, setConversations, setActiveConversation, + * addMessage, setMessages, updateConversation, setTyping, setUnread, setLoading + */ +import { describe, it, expect, beforeEach } from 'vitest'; +import { useCommStore, type Conversation, type Message } from '../commStore'; + +const mockConversation: Conversation = { + id: 'conv-1', + title: 'Test Conversation', + is_locked: false, + locked_by: null, + is_direct: false, + is_archived: false, + is_pinned: false, + created_by: 'user-1', + created_by_type: 'user', + last_msg_at: null, + last_msg_preview: null, + last_msg_sender_type: null, + participants: [], + unread_count: 0, + metadata: {}, +}; + +const mockMessage: Message = { + id: 'msg-1', + conversation_id: 'conv-1', + sender_id: 'user-1', + sender_type: 'user', + content: 'Hello World', + content_format: 'text', + metadata: {}, + reply_to_id: null, + is_pinned: false, + created_at: '2024-01-01T00:00:00Z', + edited_at: null, + blocks: [], + attachments: [], + reactions: [], +}; + +describe('commStore', () => { + beforeEach(() => { + useCommStore.setState({ + conversations: [], + activeConversationId: null, + messages: {}, + typingUsers: {}, + unreadCounts: {}, + loading: false, + }); + }); + + describe('initial state', () => { + it('starts with empty conversations', () => { + expect(useCommStore.getState().conversations).toEqual([]); + }); + + it('starts with null activeConversationId', () => { + expect(useCommStore.getState().activeConversationId).toBeNull(); + }); + + it('starts with empty messages map', () => { + expect(useCommStore.getState().messages).toEqual({}); + }); + + it('starts with empty typingUsers map', () => { + expect(useCommStore.getState().typingUsers).toEqual({}); + }); + + it('starts with empty unreadCounts map', () => { + expect(useCommStore.getState().unreadCounts).toEqual({}); + }); + + it('starts not loading', () => { + expect(useCommStore.getState().loading).toBe(false); + }); + }); + + describe('setConversations', () => { + it('sets conversations list', () => { + useCommStore.getState().setConversations([mockConversation]); + expect(useCommStore.getState().conversations).toHaveLength(1); + expect(useCommStore.getState().conversations[0].id).toBe('conv-1'); + }); + + it('replaces existing conversations', () => { + useCommStore.getState().setConversations([mockConversation]); + useCommStore.getState().setConversations([]); + expect(useCommStore.getState().conversations).toEqual([]); + }); + }); + + describe('setActiveConversation', () => { + it('sets active conversation id', () => { + useCommStore.getState().setActiveConversation('conv-1'); + expect(useCommStore.getState().activeConversationId).toBe('conv-1'); + }); + + it('sets active conversation to null', () => { + useCommStore.getState().setActiveConversation('conv-1'); + useCommStore.getState().setActiveConversation(null); + expect(useCommStore.getState().activeConversationId).toBeNull(); + }); + }); + + describe('addMessage', () => { + it('adds message to empty conversation', () => { + useCommStore.getState().addMessage('conv-1', mockMessage); + expect(useCommStore.getState().messages['conv-1']).toHaveLength(1); + expect(useCommStore.getState().messages['conv-1'][0].id).toBe('msg-1'); + }); + + it('appends message to existing conversation messages', () => { + useCommStore.getState().addMessage('conv-1', mockMessage); + useCommStore.getState().addMessage('conv-1', { ...mockMessage, id: 'msg-2' }); + expect(useCommStore.getState().messages['conv-1']).toHaveLength(2); + }); + + it('does not affect other conversations', () => { + useCommStore.getState().addMessage('conv-1', mockMessage); + useCommStore.getState().addMessage('conv-2', { ...mockMessage, id: 'msg-3', conversation_id: 'conv-2' }); + expect(useCommStore.getState().messages['conv-1']).toHaveLength(1); + expect(useCommStore.getState().messages['conv-2']).toHaveLength(1); + }); + }); + + describe('setMessages', () => { + it('sets messages for a conversation', () => { + const msgs = [mockMessage, { ...mockMessage, id: 'msg-2' }]; + useCommStore.getState().setMessages('conv-1', msgs); + expect(useCommStore.getState().messages['conv-1']).toHaveLength(2); + }); + + it('replaces existing messages for a conversation', () => { + useCommStore.getState().addMessage('conv-1', mockMessage); + useCommStore.getState().setMessages('conv-1', []); + expect(useCommStore.getState().messages['conv-1']).toEqual([]); + }); + }); + + describe('updateConversation', () => { + it('updates an existing conversation by id', () => { + useCommStore.getState().setConversations([mockConversation]); + useCommStore.getState().updateConversation({ ...mockConversation, title: 'Updated Title' }); + expect(useCommStore.getState().conversations[0].title).toBe('Updated Title'); + }); + + it('does not add new conversation if id does not match', () => { + useCommStore.getState().setConversations([mockConversation]); + useCommStore.getState().updateConversation({ ...mockConversation, id: 'nonexistent' }); + expect(useCommStore.getState().conversations).toHaveLength(1); + }); + }); + + describe('setTyping', () => { + it('sets typing users for a conversation', () => { + useCommStore.getState().setTyping('conv-1', ['user-1', 'user-2']); + expect(useCommStore.getState().typingUsers['conv-1']).toEqual(['user-1', 'user-2']); + }); + + it('overwrites existing typing users', () => { + useCommStore.getState().setTyping('conv-1', ['user-1']); + useCommStore.getState().setTyping('conv-1', []); + expect(useCommStore.getState().typingUsers['conv-1']).toEqual([]); + }); + }); + + describe('setUnread', () => { + it('sets unread count for a conversation', () => { + useCommStore.getState().setUnread('conv-1', 5); + expect(useCommStore.getState().unreadCounts['conv-1']).toBe(5); + }); + + it('sets unread count to 0', () => { + useCommStore.getState().setUnread('conv-1', 5); + useCommStore.getState().setUnread('conv-1', 0); + expect(useCommStore.getState().unreadCounts['conv-1']).toBe(0); + }); + }); + + describe('setLoading', () => { + it('sets loading to true', () => { + useCommStore.getState().setLoading(true); + expect(useCommStore.getState().loading).toBe(true); + }); + + it('sets loading to false', () => { + useCommStore.getState().setLoading(true); + useCommStore.getState().setLoading(false); + expect(useCommStore.getState().loading).toBe(false); + }); + }); +}); diff --git a/frontend/src/store/__tests__/pluginToolbarStore.test.ts b/frontend/src/store/__tests__/pluginToolbarStore.test.ts new file mode 100644 index 0000000..d4f553a --- /dev/null +++ b/frontend/src/store/__tests__/pluginToolbarStore.test.ts @@ -0,0 +1,134 @@ +/** + * Tests for pluginToolbarStore — Phase 7 Task 7.7 + * + * Tests: Initial state, registerItems, unregisterPlugin, setActivePlugin, updateItem + */ +import { describe, it, expect, beforeEach } from 'vitest'; +import { usePluginToolbarStore, type ToolbarItem } from '../pluginToolbarStore'; + +const mockItems: ToolbarItem[] = [ + { + id: 'btn-1', + plugin: 'plugin-a', + label: 'Button A', + onClick: () => {}, + group: 'group-1', + type: 'button', + }, + { + id: 'btn-2', + plugin: 'plugin-a', + label: 'Button B', + onClick: () => {}, + group: 'group-1', + type: 'button', + }, +]; + +const mockItemsB: ToolbarItem[] = [ + { + id: 'btn-3', + plugin: 'plugin-b', + label: 'Button B1', + onClick: () => {}, + type: 'button', + }, +]; + +describe('pluginToolbarStore', () => { + beforeEach(() => { + usePluginToolbarStore.setState({ items: [], activePlugin: null }); + }); + + describe('initial state', () => { + it('starts with empty items', () => { + expect(usePluginToolbarStore.getState().items).toEqual([]); + }); + + it('starts with null activePlugin', () => { + expect(usePluginToolbarStore.getState().activePlugin).toBeNull(); + }); + }); + + describe('registerItems', () => { + it('registers items for a plugin', () => { + usePluginToolbarStore.getState().registerItems('plugin-a', mockItems); + expect(usePluginToolbarStore.getState().items).toHaveLength(2); + }); + + it('sets activePlugin when registering', () => { + usePluginToolbarStore.getState().registerItems('plugin-a', mockItems); + expect(usePluginToolbarStore.getState().activePlugin).toBe('plugin-a'); + }); + + it('replaces existing items for same plugin', () => { + usePluginToolbarStore.getState().registerItems('plugin-a', mockItems); + usePluginToolbarStore.getState().registerItems('plugin-a', [mockItems[0]]); + expect(usePluginToolbarStore.getState().items).toHaveLength(1); + }); + + it('adds items for different plugin without removing others', () => { + usePluginToolbarStore.getState().registerItems('plugin-a', mockItems); + usePluginToolbarStore.getState().registerItems('plugin-b', mockItemsB); + expect(usePluginToolbarStore.getState().items).toHaveLength(3); + }); + }); + + describe('unregisterPlugin', () => { + it('removes all items for a plugin', () => { + usePluginToolbarStore.getState().registerItems('plugin-a', mockItems); + usePluginToolbarStore.getState().registerItems('plugin-b', mockItemsB); + usePluginToolbarStore.getState().unregisterPlugin('plugin-a'); + expect(usePluginToolbarStore.getState().items).toHaveLength(1); + expect(usePluginToolbarStore.getState().items[0].plugin).toBe('plugin-b'); + }); + + it('clears activePlugin when unregistering active plugin', () => { + usePluginToolbarStore.getState().registerItems('plugin-a', mockItems); + usePluginToolbarStore.getState().unregisterPlugin('plugin-a'); + expect(usePluginToolbarStore.getState().activePlugin).toBeNull(); + }); + + it('does not clear activePlugin when unregistering different plugin', () => { + usePluginToolbarStore.getState().registerItems('plugin-a', mockItems); + usePluginToolbarStore.getState().registerItems('plugin-b', mockItemsB); + usePluginToolbarStore.getState().unregisterPlugin('plugin-a'); + expect(usePluginToolbarStore.getState().activePlugin).toBe('plugin-b'); + }); + }); + + describe('setActivePlugin', () => { + it('sets active plugin', () => { + usePluginToolbarStore.getState().setActivePlugin('plugin-a'); + expect(usePluginToolbarStore.getState().activePlugin).toBe('plugin-a'); + }); + + it('sets active plugin to null', () => { + usePluginToolbarStore.getState().setActivePlugin('plugin-a'); + usePluginToolbarStore.getState().setActivePlugin(null); + expect(usePluginToolbarStore.getState().activePlugin).toBeNull(); + }); + }); + + describe('updateItem', () => { + it('updates a specific item by plugin and id', () => { + usePluginToolbarStore.getState().registerItems('plugin-a', mockItems); + usePluginToolbarStore.getState().updateItem('plugin-a', 'btn-1', { disabled: true }); + expect(usePluginToolbarStore.getState().items[0].disabled).toBe(true); + }); + + it('does not affect items from other plugins', () => { + usePluginToolbarStore.getState().registerItems('plugin-a', mockItems); + usePluginToolbarStore.getState().registerItems('plugin-b', mockItemsB); + usePluginToolbarStore.getState().updateItem('plugin-a', 'btn-1', { disabled: true }); + const pluginBItem = usePluginToolbarStore.getState().items.find((i) => i.plugin === 'plugin-b'); + expect(pluginBItem?.disabled).toBeUndefined(); + }); + + it('does nothing when item id does not match', () => { + usePluginToolbarStore.getState().registerItems('plugin-a', mockItems); + usePluginToolbarStore.getState().updateItem('plugin-a', 'nonexistent', { disabled: true }); + expect(usePluginToolbarStore.getState().items[0].disabled).toBeUndefined(); + }); + }); +}); diff --git a/frontend/src/store/__tests__/uiStore.test.ts b/frontend/src/store/__tests__/uiStore.test.ts new file mode 100644 index 0000000..c89ba57 --- /dev/null +++ b/frontend/src/store/__tests__/uiStore.test.ts @@ -0,0 +1,247 @@ +/** + * Tests for uiStore — Phase 7 Task 7.7 + * + * Tests: Initial state, theme, locale, sidebar toggles, AI sidebar, + * message sidebar, toasts, notifications + */ +import { describe, it, expect, beforeEach } from 'vitest'; +import { useUIStore, type Toast } from '../uiStore'; + +describe('uiStore', () => { + beforeEach(() => { + // Reset to default state + useUIStore.setState({ + theme: 'light', + locale: 'de', + sidebarOpen: true, + suggestionSidebarOpen: false, + aiSidebarCollapsed: true, + aiSidebarTab: 'chat', + messageSidebarCollapsed: true, + toasts: [], + notifications: [], + }); + localStorage.clear(); + }); + + describe('initial state', () => { + it('starts with light theme (when no localStorage)', () => { + expect(useUIStore.getState().theme).toBe('light'); + }); + + it('starts with de locale (when no localStorage)', () => { + expect(useUIStore.getState().locale).toBe('de'); + }); + + it('starts with sidebar open', () => { + expect(useUIStore.getState().sidebarOpen).toBe(true); + }); + + it('starts with suggestion sidebar closed', () => { + expect(useUIStore.getState().suggestionSidebarOpen).toBe(false); + }); + + it('starts with AI sidebar collapsed', () => { + expect(useUIStore.getState().aiSidebarCollapsed).toBe(true); + }); + + it('starts with chat tab for AI sidebar', () => { + expect(useUIStore.getState().aiSidebarTab).toBe('chat'); + }); + + it('starts with message sidebar collapsed', () => { + expect(useUIStore.getState().messageSidebarCollapsed).toBe(true); + }); + + it('starts with empty toasts', () => { + expect(useUIStore.getState().toasts).toEqual([]); + }); + + it('starts with empty notifications', () => { + expect(useUIStore.getState().notifications).toEqual([]); + }); + }); + + describe('setTheme', () => { + it('sets theme to dark', () => { + useUIStore.getState().setTheme('dark'); + expect(useUIStore.getState().theme).toBe('dark'); + }); + + it('persists theme to localStorage', () => { + useUIStore.getState().setTheme('dark'); + expect(localStorage.getItem('leocrm_theme')).toBe('dark'); + }); + + it('sets theme to system', () => { + useUIStore.getState().setTheme('system'); + expect(useUIStore.getState().theme).toBe('system'); + }); + }); + + describe('setLocale', () => { + it('sets locale to en', () => { + useUIStore.getState().setLocale('en'); + expect(useUIStore.getState().locale).toBe('en'); + }); + + it('persists locale to localStorage', () => { + useUIStore.getState().setLocale('en'); + expect(localStorage.getItem('leocrm_lang')).toBe('en'); + }); + }); + + describe('toggleSidebar', () => { + it('toggles sidebar from open to closed', () => { + useUIStore.getState().toggleSidebar(); + expect(useUIStore.getState().sidebarOpen).toBe(false); + }); + + it('toggles sidebar from closed to open', () => { + useUIStore.getState().setSidebarOpen(false); + useUIStore.getState().toggleSidebar(); + expect(useUIStore.getState().sidebarOpen).toBe(true); + }); + }); + + describe('setSidebarOpen', () => { + it('sets sidebar open explicitly', () => { + useUIStore.getState().setSidebarOpen(false); + useUIStore.getState().setSidebarOpen(true); + expect(useUIStore.getState().sidebarOpen).toBe(true); + }); + }); + + describe('toggleSuggestionSidebar', () => { + it('toggles suggestion sidebar', () => { + expect(useUIStore.getState().suggestionSidebarOpen).toBe(false); + useUIStore.getState().toggleSuggestionSidebar(); + expect(useUIStore.getState().suggestionSidebarOpen).toBe(true); + useUIStore.getState().toggleSuggestionSidebar(); + expect(useUIStore.getState().suggestionSidebarOpen).toBe(false); + }); + }); + + describe('toggleAISidebar', () => { + it('toggles AI sidebar collapsed state', () => { + expect(useUIStore.getState().aiSidebarCollapsed).toBe(true); + useUIStore.getState().toggleAISidebar(); + expect(useUIStore.getState().aiSidebarCollapsed).toBe(false); + useUIStore.getState().toggleAISidebar(); + expect(useUIStore.getState().aiSidebarCollapsed).toBe(true); + }); + }); + + describe('setAISidebarCollapsed', () => { + it('sets AI sidebar collapsed explicitly', () => { + useUIStore.getState().setAISidebarCollapsed(false); + expect(useUIStore.getState().aiSidebarCollapsed).toBe(false); + }); + }); + + describe('setAISidebarTab', () => { + it('sets AI sidebar tab to proactive', () => { + useUIStore.getState().setAISidebarTab('proactive'); + expect(useUIStore.getState().aiSidebarTab).toBe('proactive'); + }); + + it('sets AI sidebar tab to notifications', () => { + useUIStore.getState().setAISidebarTab('notifications'); + expect(useUIStore.getState().aiSidebarTab).toBe('notifications'); + }); + }); + + describe('openAISidebarProactive', () => { + it('opens AI sidebar and sets tab to proactive', () => { + useUIStore.getState().setAISidebarCollapsed(true); + useUIStore.getState().setAISidebarTab('chat'); + useUIStore.getState().openAISidebarProactive(); + expect(useUIStore.getState().aiSidebarCollapsed).toBe(false); + expect(useUIStore.getState().aiSidebarTab).toBe('proactive'); + }); + }); + + describe('toggleMessageSidebar', () => { + it('toggles message sidebar collapsed state', () => { + expect(useUIStore.getState().messageSidebarCollapsed).toBe(true); + useUIStore.getState().toggleMessageSidebar(); + expect(useUIStore.getState().messageSidebarCollapsed).toBe(false); + }); + }); + + describe('setMessageSidebarCollapsed', () => { + it('sets message sidebar collapsed explicitly', () => { + useUIStore.getState().setMessageSidebarCollapsed(false); + expect(useUIStore.getState().messageSidebarCollapsed).toBe(false); + }); + }); + + describe('addToast', () => { + it('adds a toast to the list', () => { + useUIStore.getState().addToast({ type: 'success', message: 'Test toast' }); + expect(useUIStore.getState().toasts).toHaveLength(1); + expect(useUIStore.getState().toasts[0].message).toBe('Test toast'); + expect(useUIStore.getState().toasts[0].type).toBe('success'); + }); + + it('generates unique id for each toast', () => { + useUIStore.getState().addToast({ type: 'info', message: 'Toast 1' }); + useUIStore.getState().addToast({ type: 'info', message: 'Toast 2' }); + expect(useUIStore.getState().toasts).toHaveLength(2); + expect(useUIStore.getState().toasts[0].id).not.toBe(useUIStore.getState().toasts[1].id); + }); + + it('preserves toast duration when provided', () => { + useUIStore.getState().addToast({ type: 'warning', message: 'Timed toast', duration: 5000 }); + expect(useUIStore.getState().toasts[0].duration).toBe(5000); + }); + }); + + describe('removeToast', () => { + it('removes toast by id', () => { + useUIStore.getState().addToast({ type: 'success', message: 'Toast 1' }); + useUIStore.getState().addToast({ type: 'error', message: 'Toast 2' }); + const idToRemove = useUIStore.getState().toasts[0].id; + useUIStore.getState().removeToast(idToRemove); + expect(useUIStore.getState().toasts).toHaveLength(1); + expect(useUIStore.getState().toasts[0].message).toBe('Toast 2'); + }); + + it('does nothing when id does not match', () => { + useUIStore.getState().addToast({ type: 'success', message: 'Toast 1' }); + useUIStore.getState().removeToast('nonexistent-id'); + expect(useUIStore.getState().toasts).toHaveLength(1); + }); + }); + + describe('clearToasts', () => { + it('removes all toasts', () => { + useUIStore.getState().addToast({ type: 'success', message: 'Toast 1' }); + useUIStore.getState().addToast({ type: 'error', message: 'Toast 2' }); + useUIStore.getState().clearToasts(); + expect(useUIStore.getState().toasts).toEqual([]); + }); + }); + + describe('removeNotification', () => { + it('removes notification by index', () => { + useUIStore.setState({ notifications: ['notif-1', 'notif-2', 'notif-3'] }); + useUIStore.getState().removeNotification(1); + expect(useUIStore.getState().notifications).toEqual(['notif-1', 'notif-3']); + }); + + it('removes first notification when index is 0', () => { + useUIStore.setState({ notifications: ['notif-1', 'notif-2'] }); + useUIStore.getState().removeNotification(0); + expect(useUIStore.getState().notifications).toEqual(['notif-2']); + }); + }); + + describe('clearNotifications', () => { + it('removes all notifications', () => { + useUIStore.setState({ notifications: ['notif-1', 'notif-2'] }); + useUIStore.getState().clearNotifications(); + expect(useUIStore.getState().notifications).toEqual([]); + }); + }); +});