/** * useFeatureFlags Composable Tests * Verifies feature flag state management, permissions, and overrides */ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import * as useAuthModule from '../../../src/composables/useAuth.js'; import { useFeatureFlags } from '../../../src/composables/useFeatureFlags.js'; // Mock useAuth module vi.spyOn(useAuthModule, 'useAuth').mockReturnValue({ user: { isAdmin: false, permissions: [] }, token: null, hasPermission: vi.fn(() => false) }); describe('useFeatureFlags', () => { beforeEach(() => { // Clear localStorage before each test localStorage.clear(); }); afterEach(() => { localStorage.clear(); }); describe('isEnabled', () => { it('returns flag default value when no override exists', () => { const { isEnabled } = useFeatureFlags(); // ENABLE_CACHING defaults to true expect(isEnabled.value('enable-caching')).toBe(true); // DARK_MODE defaults to false expect(isEnabled.value('dark-mode')).toBe(false); }); it('respects local override when set', () => { const { isEnabled, toggle } = useFeatureFlags(); expect(isEnabled.value('dark-mode')).toBe(false); toggle('dark-mode'); expect(isEnabled.value('dark-mode')).toBe(true); toggle('dark-mode'); expect(isEnabled.value('dark-mode')).toBe(false); }); it('returns false for flag requiring permission user does not have', () => { useAuth.mockReturnValue({ user: { isAdmin: false, permissions: [] }, token: null, hasPermission: vi.fn(() => false) }); const { isEnabled } = useFeatureFlags(); // GAMEMASTER_DIFF_VIEWER requires 'gamemaster-advanced' permission expect(isEnabled.value('gamemaster-diff-viewer')).toBe(false); }); it('returns true for flag when user has required permission', () => { useAuth.mockReturnValue({ user: { isAdmin: false, permissions: ['gamemaster-advanced'] }, token: null, hasPermission: vi.fn(perm => perm === 'gamemaster-advanced') }); const { isEnabled } = useFeatureFlags(); // With permission and enabled default, should be true expect(isEnabled.value('gamemaster-diff-viewer')).toBe(true); }); it('returns false for unknown flag name', () => { const { isEnabled } = useFeatureFlags(); expect(isEnabled.value('nonexistent-flag')).toBe(false); }); it('prioritizes local override over permission requirement', () => { useAuth.mockReturnValue({ user: { isAdmin: false, permissions: [] }, token: null, hasPermission: vi.fn(() => false) }); const { isEnabled, toggle } = useFeatureFlags(); // Initially disabled due to missing permission expect(isEnabled.value('gamemaster-diff-viewer')).toBe(false); // Local override enables it toggle('gamemaster-diff-viewer'); expect(isEnabled.value('gamemaster-diff-viewer')).toBe(true); }); }); describe('toggle', () => { it('toggles flag override in development mode', () => { const { isEnabled, toggle } = useFeatureFlags(); expect(isEnabled.value('dark-mode')).toBe(false); const result = toggle('dark-mode'); expect(result).toBe(true); expect(isEnabled.value('dark-mode')).toBe(true); toggle('dark-mode'); expect(isEnabled.value('dark-mode')).toBe(false); }); it('persists override to localStorage', () => { const { toggle } = useFeatureFlags(); toggle('dark-mode'); const stored = localStorage.getItem('feature_flag_overrides'); expect(stored).toBeDefined(); const overrides = JSON.parse(stored); expect(overrides['dark-mode']).toBe(true); }); it('loads overrides from localStorage on init', () => { // Set override in storage localStorage.setItem( 'feature_flag_overrides', JSON.stringify({ 'dark-mode': true, 'experimental-search': true }) ); const { isEnabled } = useFeatureFlags(); expect(isEnabled.value('dark-mode')).toBe(true); expect(isEnabled.value('experimental-search')).toBe(true); }); }); describe('reset', () => { it('removes override for specific flag', () => { const { isEnabled, toggle, reset } = useFeatureFlags(); toggle('dark-mode'); expect(isEnabled.value('dark-mode')).toBe(true); reset('dark-mode'); expect(isEnabled.value('dark-mode')).toBe(false); }); it('returns to default after reset', () => { const { isEnabled, toggle, reset } = useFeatureFlags(); // ENABLE_CACHING defaults to true toggle('enable-caching'); // Toggle to false expect(isEnabled.value('enable-caching')).toBe(false); reset('enable-caching'); // Reset to default expect(isEnabled.value('enable-caching')).toBe(true); }); }); describe('resetAll', () => { it('clears all overrides', () => { const { isEnabled, toggle, resetAll } = useFeatureFlags(); toggle('dark-mode'); toggle('experimental-search'); toggle('enable-caching'); expect(isEnabled.value('dark-mode')).toBe(true); expect(isEnabled.value('experimental-search')).toBe(true); expect(isEnabled.value('enable-caching')).toBe(false); resetAll(); expect(isEnabled.value('dark-mode')).toBe(false); expect(isEnabled.value('experimental-search')).toBe(false); expect(isEnabled.value('enable-caching')).toBe(true); // Back to default }); it('clears localStorage after reset', () => { const { toggle, resetAll } = useFeatureFlags(); toggle('dark-mode'); localStorage.getItem('feature_flag_overrides'); // Has value resetAll(); const stored = localStorage.getItem('feature_flag_overrides'); expect(stored).toBe('{}'); }); }); describe('getFlags', () => { it('returns array of all flags with status', () => { useAuth.mockReturnValue({ user: { isAdmin: true, permissions: ['admin'] }, token: null, hasPermission: vi.fn(() => true) }); const { getFlags } = useFeatureFlags(); const flags = getFlags(); expect(Array.isArray(flags)).toBe(true); expect(flags.length).toBeGreaterThan(0); const darkMode = flags.find(f => f.name === 'dark-mode'); expect(darkMode).toBeDefined(); expect(darkMode).toHaveProperty('isEnabled'); expect(darkMode).toHaveProperty('hasOverride'); expect(darkMode).toHaveProperty('requiresPermission'); expect(darkMode).toHaveProperty('hasPermission'); }); it('marks flags with overrides', () => { const { getFlags, toggle } = useFeatureFlags(); toggle('dark-mode'); const flags = getFlags(); const darkMode = flags.find(f => f.name === 'dark-mode'); expect(darkMode.hasOverride).toBe(true); expect(darkMode.override).toBe(true); }); it('indicates permission status for each flag', () => { useAuth.mockReturnValue({ user: { isAdmin: false, permissions: [] }, token: null, hasPermission: vi.fn(() => false) }); const { getFlags } = useFeatureFlags(); const flags = getFlags(); const permissionRequired = flags.find(f => f.requiresPermission); expect(permissionRequired).toBeDefined(); expect(permissionRequired.hasPermission).toBe(false); const noPermissionRequired = flags.find(f => !f.requiresPermission); expect(noPermissionRequired).toBeDefined(); expect(noPermissionRequired.hasPermission).toBe(true); }); }); describe('setBackendFlags', () => { it('sets flags from backend response', () => { const { isEnabled, setBackendFlags } = useFeatureFlags(); expect(isEnabled.value('dark-mode')).toBe(false); setBackendFlags({ 'dark-mode': true, 'experimental-search': true }); expect(isEnabled.value('dark-mode')).toBe(true); expect(isEnabled.value('experimental-search')).toBe(true); }); it('local overrides take precedence over backend flags', () => { const { isEnabled, toggle, setBackendFlags } = useFeatureFlags(); toggle('dark-mode'); // Override to true setBackendFlags({ 'dark-mode': false // Backend says false }); // Local override should win expect(isEnabled.value('dark-mode')).toBe(true); }); }); });