/** * Tests for State Middleware * FE-STATE-010: Test logging, analytics, and error handling */ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { create } from 'zustand'; import { stateMiddleware, setAnalyticsHandler, setErrorHandler, type AnalyticsEvent } from './stateMiddleware'; interface TestState { count: number; name: string; increment: () => void; setName: (name: string) => void; throwError: () => void; asyncAction: () => Promise; } describe('stateMiddleware', () => { let analyticsEvents: AnalyticsEvent[] = []; let errorEvents: Array<{ error: Error; context: unknown }> = []; beforeEach(() => { analyticsEvents = []; errorEvents = []; setAnalyticsHandler((event) => { analyticsEvents.push(event); }); setErrorHandler((error, context) => { errorEvents.push({ error, context }); }); }); it('should log state changes in development', () => { const consoleSpy = vi.spyOn(console, 'debug').mockImplementation(() => {}); const useTestStore = create()( stateMiddleware( (set) => ({ count: 0, name: '', increment: () => set((state) => ({ count: state.count + 1 })), setName: (name: string) => set({ name }), throwError: () => { throw new Error('Test error'); }, asyncAction: async () => { return 'success'; }, }), { storeName: 'TestStore', enableLogging: true }, ), ); const store = useTestStore.getState(); store.increment(); expect(consoleSpy).toHaveBeenCalled(); consoleSpy.mockRestore(); }); it('should track analytics events', () => { const useTestStore = create()( stateMiddleware( (set) => ({ count: 0, name: '', increment: () => set((state) => ({ count: state.count + 1 })), setName: (name: string) => set({ name }), throwError: () => { throw new Error('Test error'); }, asyncAction: async () => { return 'success'; }, }), { storeName: 'TestStore', enableAnalytics: true }, ), ); const store = useTestStore.getState(); store.increment(); expect(analyticsEvents.length).toBeGreaterThan(0); expect(analyticsEvents.some((e) => e.type === 'state_change')).toBe(true); expect(analyticsEvents.some((e) => e.type === 'action_called')).toBe(true); }); it('should handle errors', () => { const useTestStore = create()( stateMiddleware( (set) => ({ count: 0, name: '', increment: () => set((state) => ({ count: state.count + 1 })), setName: (name: string) => set({ name }), throwError: () => { throw new Error('Test error'); }, asyncAction: async () => { return 'success'; }, }), { storeName: 'TestStore', enableErrorHandling: true }, ), ); const store = useTestStore.getState(); expect(() => store.throwError()).toThrow('Test error'); expect(errorEvents.length).toBeGreaterThan(0); expect(errorEvents[0].error.message).toBe('Test error'); }); it('should track async action performance', async () => { // Clear previous events analyticsEvents = []; const useTestStore = create()( stateMiddleware( (set) => ({ count: 0, name: '', increment: () => set((state) => ({ count: state.count + 1 })), setName: (name: string) => set({ name }), throwError: () => { throw new Error('Test error'); }, asyncAction: async () => { await new Promise((resolve) => setTimeout(resolve, 50)); return 'success'; }, }), { storeName: 'TestStore', enableAnalytics: true }, ), ); const store = useTestStore.getState(); await store.asyncAction(); // Wait a bit for async analytics to be tracked await new Promise((resolve) => setTimeout(resolve, 10)); const performanceEvents = analyticsEvents.filter( (e) => e.type === 'performance_metric' && e.action === 'asyncAction', ); expect(performanceEvents.length).toBeGreaterThan(0); }); it('should sanitize sensitive data', () => { const consoleSpy = vi.spyOn(console, 'debug').mockImplementation(() => {}); const useTestStore = create<{ password: string; token: string; name: string; updateName: (name: string) => void }>()( stateMiddleware( (set) => ({ password: 'secret123', token: 'abc123', name: 'John', updateName: (name: string) => set({ name }), }), { storeName: 'TestStore', enableLogging: true, sanitizeState: (state) => { if (typeof state === 'object' && state !== null) { const sanitized = { ...state as Record }; sanitized.password = '[REDACTED]'; sanitized.token = '[REDACTED]'; return sanitized; } return state; }, }, ), ); const store = useTestStore.getState(); store.updateName('Jane'); // Trigger state change // Verify that logging was called (the middleware logs state changes) // The logger uses [DEBUG] prefix, so we check for that expect(consoleSpy).toHaveBeenCalled(); consoleSpy.mockRestore(); }); it('should respect shouldLog filter', () => { const consoleSpy = vi.spyOn(console, 'debug').mockImplementation(() => {}); const useTestStore = create<{ count: number; increment: () => void }>()( stateMiddleware( (set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), }), { storeName: 'TestStore', enableLogging: true, shouldLog: () => false, // Don't log state changes }, ), ); const store = useTestStore.getState(); store.increment(); // Action calls are still logged, but state changes are filtered // So we expect at least one call (action call), but state change logging is filtered const calls = consoleSpy.mock.calls; const stateChangeCalls = calls.filter((call) => call[0]?.toString().includes('State change') ); expect(stateChangeCalls.length).toBe(0); consoleSpy.mockRestore(); }); it('should respect shouldTrack filter', () => { const useTestStore = create<{ count: number; increment: () => void }>()( stateMiddleware( (set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), }), { storeName: 'TestStore', enableAnalytics: true, shouldTrack: (action) => action !== 'increment', // Don't track increment }, ), ); const store = useTestStore.getState(); store.increment(); // Should not track increment action const actionEvents = analyticsEvents.filter( (e) => e.type === 'action_called' && e.action === 'increment', ); expect(actionEvents.length).toBe(0); }); });