import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { createFeatureFlagClient } from './client.js'; describe('createFeatureFlagClient', () => { const baseConfig = { baseUrl: 'http://localhost:4003/api', productId: 'testapp', platform: 'web', }; beforeEach(() => { vi.useFakeTimers(); }); afterEach(() => { vi.useRealTimers(); vi.restoreAllMocks(); }); it('should create a client with isEnabled returning false before init', () => { const client = createFeatureFlagClient(baseConfig); expect(client.isEnabled('any_flag')).toBe(false); }); it('should fetch flags on init', async () => { const mockFlags = { premium: true, beta_feature: false }; vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve({ flags: mockFlags }), }) ); const client = createFeatureFlagClient(baseConfig); await client.init(); expect(client.isEnabled('premium')).toBe(true); expect(client.isEnabled('beta_feature')).toBe(false); expect(client.isEnabled('nonexistent')).toBe(false); client.stop(); }); it('should return all flags via getAllFlags', async () => { const mockFlags = { a: true, b: false }; vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve({ flags: mockFlags }), }) ); const client = createFeatureFlagClient(baseConfig); await client.init(); expect(client.getAllFlags()).toEqual({ a: true, b: false }); client.stop(); }); it('should send correct headers', async () => { const fetchMock = vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve({ flags: {} }), }); vi.stubGlobal('fetch', fetchMock); const client = createFeatureFlagClient(baseConfig); await client.init({ userId: 'user-123' }); expect(fetchMock).toHaveBeenCalledWith( expect.stringContaining('/flags/poll?'), expect.objectContaining({ headers: expect.objectContaining({ 'x-product-id': 'testapp' }), }) ); const url = fetchMock.mock.calls[0][0] as string; expect(url).toContain('platform=web'); expect(url).toContain('userId=user-123'); client.stop(); }); it('should keep existing flags on network error', async () => { let callCount = 0; vi.stubGlobal( 'fetch', vi.fn().mockImplementation(() => { callCount++; if (callCount === 1) { return Promise.resolve({ ok: true, json: () => Promise.resolve({ flags: { initial: true } }), }); } return Promise.reject(new Error('network')); }) ); const client = createFeatureFlagClient(baseConfig); await client.init(); expect(client.isEnabled('initial')).toBe(true); await client.refresh(); expect(client.isEnabled('initial')).toBe(true); client.stop(); }); it('should persist to storage when provided', async () => { const store: Record = {}; const storage = { getItem: (k: string) => store[k] ?? null, setItem: (k: string, v: string) => { store[k] = v; }, }; vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve({ flags: { cached: true } }), }) ); const client = createFeatureFlagClient({ ...baseConfig, storage }); await client.init(); expect(store['testapp-feature-flags']).toBeDefined(); expect(JSON.parse(store['testapp-feature-flags'])).toEqual({ cached: true }); client.stop(); }); it('should restore flags from storage on creation', () => { const store: Record = { 'testapp-feature-flags': JSON.stringify({ restored: true }), }; const storage = { getItem: (k: string) => store[k] ?? null, setItem: (k: string, v: string) => { store[k] = v; }, }; const client = createFeatureFlagClient({ ...baseConfig, storage }); expect(client.isEnabled('restored')).toBe(true); }); it('should stop polling on stop()', async () => { vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue({ ok: true, json: () => Promise.resolve({ flags: {} }), }) ); const client = createFeatureFlagClient({ ...baseConfig, pollIntervalMs: 1000 }); await client.init(); client.stop(); expect(client.isEnabled('anything')).toBe(false); expect(client.getAllFlags()).toEqual({}); }); });