import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; /** * Unit tests for the role context (roleContext.svelte.ts). * * This module uses Svelte 5 $state runes for reactive state management. * We test it through its public exported API and mock the underlying * roles API module to isolate the context logic. */ // Mock the roles API module const mockGetMyRoles = vi.fn(); const mockSwitchRole = vi.fn(); vi.mock('$lib/features/roles/api/roles', () => ({ getMyRoles: (...args: unknown[]) => mockGetMyRoles(...args), switchRole: (...args: unknown[]) => mockSwitchRole(...args) })); describe('roleContext', () => { let roleContext: typeof import('$lib/features/roles/roleContext.svelte'); beforeEach(async () => { vi.clearAllMocks(); // Fresh import to reset $state between tests vi.resetModules(); roleContext = await import('$lib/features/roles/roleContext.svelte'); }); afterEach(() => { vi.restoreAllMocks(); }); // ========================================================================== // Initial state // ========================================================================== describe('initial state', () => { it('should have no roles initially', () => { expect(roleContext.getRoles()).toEqual([]); }); it('should have null activeRole initially', () => { expect(roleContext.getActiveRole()).toBeNull(); }); it('should have null activeRoleLabel initially', () => { expect(roleContext.getActiveRoleLabel()).toBeNull(); }); it('should not be loading initially', () => { expect(roleContext.getIsLoading()).toBe(false); }); it('should not be switching initially', () => { expect(roleContext.getIsSwitching()).toBe(false); }); it('should not have multiple roles initially', () => { expect(roleContext.hasMultipleRoles()).toBe(false); }); }); // ========================================================================== // fetchRoles // ========================================================================== describe('fetchRoles', () => { it('should load roles from API and set state', async () => { mockGetMyRoles.mockResolvedValueOnce({ roles: [ { value: 'ROLE_ADMIN', label: 'Administrateur' }, { value: 'ROLE_TEACHER', label: 'Enseignant' } ], activeRole: 'ROLE_ADMIN', activeRoleLabel: 'Administrateur' }); await roleContext.fetchRoles(); expect(mockGetMyRoles).toHaveBeenCalledOnce(); expect(roleContext.getRoles()).toHaveLength(2); expect(roleContext.getActiveRole()).toBe('ROLE_ADMIN'); expect(roleContext.getActiveRoleLabel()).toBe('Administrateur'); expect(roleContext.getIsLoading()).toBe(false); }); it('should guard against double loading (isFetched)', async () => { mockGetMyRoles.mockResolvedValueOnce({ roles: [{ value: 'ROLE_ADMIN', label: 'Admin' }], activeRole: 'ROLE_ADMIN', activeRoleLabel: 'Admin' }); // First call loads data await roleContext.fetchRoles(); expect(mockGetMyRoles).toHaveBeenCalledOnce(); // Second call should be a no-op due to isFetched guard await roleContext.fetchRoles(); expect(mockGetMyRoles).toHaveBeenCalledOnce(); }); it('should handle API errors gracefully without throwing', async () => { const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); mockGetMyRoles.mockRejectedValueOnce(new Error('Network error')); // Should not throw await roleContext.fetchRoles(); expect(consoleSpy).toHaveBeenCalledWith( '[roleContext] Failed to fetch roles:', expect.any(Error) ); // State should remain empty expect(roleContext.getRoles()).toEqual([]); expect(roleContext.getActiveRole()).toBeNull(); expect(roleContext.getIsLoading()).toBe(false); consoleSpy.mockRestore(); }); }); // ========================================================================== // switchTo // ========================================================================== describe('switchTo', () => { it('should return true immediately when switching to same role', async () => { // First, load roles so activeRole is set mockGetMyRoles.mockResolvedValueOnce({ roles: [{ value: 'ROLE_ADMIN', label: 'Admin' }], activeRole: 'ROLE_ADMIN', activeRoleLabel: 'Admin' }); await roleContext.fetchRoles(); // Switch to the same role const result = await roleContext.switchTo('ROLE_ADMIN'); expect(result).toBe(true); // API should NOT be called for same-role switch expect(mockSwitchRole).not.toHaveBeenCalled(); }); it('should call API and update state when switching to different role', async () => { // Load initial roles mockGetMyRoles.mockResolvedValueOnce({ roles: [ { value: 'ROLE_ADMIN', label: 'Admin' }, { value: 'ROLE_TEACHER', label: 'Enseignant' } ], activeRole: 'ROLE_ADMIN', activeRoleLabel: 'Admin' }); await roleContext.fetchRoles(); // Switch to a different role mockSwitchRole.mockResolvedValueOnce({ activeRole: 'ROLE_TEACHER', activeRoleLabel: 'Enseignant' }); const result = await roleContext.switchTo('ROLE_TEACHER'); expect(result).toBe(true); expect(mockSwitchRole).toHaveBeenCalledWith('ROLE_TEACHER'); expect(roleContext.getActiveRole()).toBe('ROLE_TEACHER'); expect(roleContext.getActiveRoleLabel()).toBe('Enseignant'); expect(roleContext.getIsSwitching()).toBe(false); }); it('should return false when the API call fails', async () => { const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); // Load initial roles mockGetMyRoles.mockResolvedValueOnce({ roles: [ { value: 'ROLE_ADMIN', label: 'Admin' }, { value: 'ROLE_TEACHER', label: 'Enseignant' } ], activeRole: 'ROLE_ADMIN', activeRoleLabel: 'Admin' }); await roleContext.fetchRoles(); // Switch fails mockSwitchRole.mockRejectedValueOnce(new Error('Server error')); const result = await roleContext.switchTo('ROLE_TEACHER'); expect(result).toBe(false); expect(roleContext.getActiveRole()).toBe('ROLE_ADMIN'); // unchanged expect(roleContext.getIsSwitching()).toBe(false); consoleSpy.mockRestore(); }); }); // ========================================================================== // hasMultipleRoles // ========================================================================== describe('hasMultipleRoles', () => { it('should return true when user has more than one role', async () => { mockGetMyRoles.mockResolvedValueOnce({ roles: [ { value: 'ROLE_ADMIN', label: 'Admin' }, { value: 'ROLE_TEACHER', label: 'Enseignant' } ], activeRole: 'ROLE_ADMIN', activeRoleLabel: 'Admin' }); await roleContext.fetchRoles(); expect(roleContext.hasMultipleRoles()).toBe(true); }); it('should return false when user has zero roles', () => { // No fetch, so roles is empty expect(roleContext.hasMultipleRoles()).toBe(false); }); it('should return false when user has exactly one role', async () => { mockGetMyRoles.mockResolvedValueOnce({ roles: [{ value: 'ROLE_ADMIN', label: 'Admin' }], activeRole: 'ROLE_ADMIN', activeRoleLabel: 'Admin' }); await roleContext.fetchRoles(); expect(roleContext.hasMultipleRoles()).toBe(false); }); }); // ========================================================================== // resetRoleContext // ========================================================================== describe('resetRoleContext', () => { it('should clear all state back to initial values', async () => { // Load some data first mockGetMyRoles.mockResolvedValueOnce({ roles: [ { value: 'ROLE_ADMIN', label: 'Admin' }, { value: 'ROLE_TEACHER', label: 'Enseignant' } ], activeRole: 'ROLE_ADMIN', activeRoleLabel: 'Admin' }); await roleContext.fetchRoles(); // Verify state is set expect(roleContext.getRoles()).toHaveLength(2); expect(roleContext.getActiveRole()).toBe('ROLE_ADMIN'); // Reset roleContext.resetRoleContext(); expect(roleContext.getRoles()).toEqual([]); expect(roleContext.getActiveRole()).toBeNull(); expect(roleContext.getActiveRoleLabel()).toBeNull(); expect(roleContext.hasMultipleRoles()).toBe(false); }); it('should allow fetchRoles to be called again after reset', async () => { // Load initial data mockGetMyRoles.mockResolvedValueOnce({ roles: [{ value: 'ROLE_ADMIN', label: 'Admin' }], activeRole: 'ROLE_ADMIN', activeRoleLabel: 'Admin' }); await roleContext.fetchRoles(); expect(mockGetMyRoles).toHaveBeenCalledOnce(); // Reset clears isFetched roleContext.resetRoleContext(); // Now fetchRoles should call the API again mockGetMyRoles.mockResolvedValueOnce({ roles: [{ value: 'ROLE_TEACHER', label: 'Enseignant' }], activeRole: 'ROLE_TEACHER', activeRoleLabel: 'Enseignant' }); await roleContext.fetchRoles(); expect(mockGetMyRoles).toHaveBeenCalledTimes(2); expect(roleContext.getActiveRole()).toBe('ROLE_TEACHER'); }); }); // ========================================================================== // getIsLoading / getIsSwitching // ========================================================================== describe('loading and switching state', () => { it('should set isLoading to true during fetchRoles and false after', async () => { // Use a deferred promise to control resolution timing let resolvePromise: (value: unknown) => void; const pendingPromise = new Promise((resolve) => { resolvePromise = resolve; }); mockGetMyRoles.mockReturnValueOnce(pendingPromise); const fetchPromise = roleContext.fetchRoles(); // While the API call is pending, isLoading should be true expect(roleContext.getIsLoading()).toBe(true); // Resolve the API call resolvePromise!({ roles: [{ value: 'ROLE_ADMIN', label: 'Admin' }], activeRole: 'ROLE_ADMIN', activeRoleLabel: 'Admin' }); await fetchPromise; expect(roleContext.getIsLoading()).toBe(false); }); it('should set isSwitching to true during switchTo and false after', async () => { // Load roles first so activeRole is set mockGetMyRoles.mockResolvedValueOnce({ roles: [ { value: 'ROLE_ADMIN', label: 'Admin' }, { value: 'ROLE_TEACHER', label: 'Enseignant' } ], activeRole: 'ROLE_ADMIN', activeRoleLabel: 'Admin' }); await roleContext.fetchRoles(); // Use a deferred promise for switch let resolvePromise: (value: unknown) => void; const pendingPromise = new Promise((resolve) => { resolvePromise = resolve; }); mockSwitchRole.mockReturnValueOnce(pendingPromise); const switchPromise = roleContext.switchTo('ROLE_TEACHER'); // While switching, isSwitching should be true expect(roleContext.getIsSwitching()).toBe(true); // Resolve resolvePromise!({ activeRole: 'ROLE_TEACHER', activeRoleLabel: 'Enseignant' }); await switchPromise; expect(roleContext.getIsSwitching()).toBe(false); }); }); });