test(7.7): add tests for authStore, uiStore, commStore, pluginToolbarStore, calendarStore
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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<string>(),
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user