From 6ec72ff36fd2e2c87a67c1ff3704c0d8b56c3d31 Mon Sep 17 00:00:00 2001 From: FragginWagon Date: Wed, 28 Jan 2026 22:59:34 +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 | 216 ++++++++++++++++++ 1 file changed, 216 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..87f96a0 --- /dev/null +++ b/code/websites/pokedex.online/tests/unit/composables/useFeatureFlags.test.js @@ -0,0 +1,216 @@ +/** + * 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 with default no-permissions user +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('returns false for unknown flag name', () => { + const { isEnabled } = useFeatureFlags(); + expect(isEnabled.value('nonexistent-flag')).toBe(false); + }); + + it('respects local override when set in same instance', () => { + const { isEnabled, toggle } = useFeatureFlags(); + + expect(isEnabled.value('dark-mode')).toBe(false); + + toggle('dark-mode'); + // Should be toggled in same instance + expect(isEnabled.value('dark-mode')).toBe(true); + }); + + it('returns false for flag requiring permission without it', () => { + const { isEnabled } = useFeatureFlags(); + + // GAMEMASTER_DIFF_VIEWER requires 'gamemaster-advanced' permission + expect(isEnabled.value('gamemaster-diff-viewer')).toBe(false); + }); + }); + + describe('toggle', () => { + it('toggles flag override in development mode', () => { + const { toggle, isEnabled } = useFeatureFlags(); + + expect(isEnabled.value('dark-mode')).toBe(false); + + const result = toggle('dark-mode'); + expect(result).toBe(true); + expect(isEnabled.value('dark-mode')).toBe(true); + }); + + it('persists override to localStorage', () => { + const { toggle } = useFeatureFlags(); + + toggle('dark-mode'); + + const stored = localStorage.getItem('feature_flag_overrides'); + expect(stored).not.toBeNull(); + + const overrides = JSON.parse(stored); + expect(overrides['dark-mode']).toBe(true); + }); + + it('loads overrides from localStorage on init', () => { + // Set override in storage first + 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 + const beforeToggle = isEnabled.value('enable-caching'); + toggle('enable-caching'); + expect(isEnabled.value('enable-caching')).not.toBe(beforeToggle); + + reset('enable-caching'); + expect(isEnabled.value('enable-caching')).toBe(beforeToggle); + }); + }); + + describe('resetAll', () => { + it('clears all overrides', () => { + const { isEnabled, toggle, resetAll } = useFeatureFlags(); + + toggle('dark-mode'); + toggle('experimental-search'); + + resetAll(); + + // Should return to defaults + expect(isEnabled.value('dark-mode')).toBe(false); + expect(isEnabled.value('experimental-search')).toBe(false); + }); + + it('clears localStorage after reset', () => { + const { toggle, resetAll } = useFeatureFlags(); + + toggle('dark-mode'); + resetAll(); + + const stored = localStorage.getItem('feature_flag_overrides'); + expect(stored).toBe('{}'); + }); + }); + + describe('getFlags', () => { + it('returns array of all flags with status', () => { + 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'); + }); + + 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 requirements for flags', () => { + const { getFlags } = useFeatureFlags(); + + const flags = getFlags(); + + // Should have flags with and without permissions + const withPerm = flags.find(f => f.requiresPermission); + const withoutPerm = flags.find(f => !f.requiresPermission); + + expect(withPerm).toBeDefined(); + expect(withoutPerm).toBeDefined(); + }); + }); + + 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'); // Local override to true + + setBackendFlags({ + 'dark-mode': false // Backend says false + }); + + // Local override should win + expect(isEnabled.value('dark-mode')).toBe(true); + }); + }); +});