From 338ee1f75000056a459f6468fc4d0a22e9eb8b7a Mon Sep 17 00:00:00 2001 From: FragginWagon Date: Wed, 28 Jan 2026 22:54:23 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9C=85=20Add=20unit=20tests=20for=20feature?= =?UTF-8?q?=20flag=20composable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../unit/composables/useFeatureFlags.test.js | 280 ++++++++++++++++++ 1 file changed, 280 insertions(+) create mode 100644 code/websites/pokedex.online/tests/unit/composables/useFeatureFlags.test.js diff --git a/code/websites/pokedex.online/tests/unit/composables/useFeatureFlags.test.js b/code/websites/pokedex.online/tests/unit/composables/useFeatureFlags.test.js new file mode 100644 index 0000000..e696df7 --- /dev/null +++ b/code/websites/pokedex.online/tests/unit/composables/useFeatureFlags.test.js @@ -0,0 +1,280 @@ +/** + * useFeatureFlags Composable Tests + * Verifies feature flag state management, permissions, and overrides + */ + +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { useFeatureFlags } from '../../src/composables/useFeatureFlags.js'; +import { useAuth } from '../../src/composables/useAuth.js'; + +vi.mock('../../src/composables/useAuth.js'); + +describe('useFeatureFlags', () => { + beforeEach(() => { + // Clear localStorage before each test + localStorage.clear(); + + // Mock useAuth + useAuth.mockReturnValue({ + user: { isAdmin: false, permissions: [] }, + token: null, + hasPermission: vi.fn((perm) => false) + }); + }); + + 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); + }); + }); +});