244 lines
7.2 KiB
TypeScript
244 lines
7.2 KiB
TypeScript
|
|
/**
|
||
|
|
* 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<string>;
|
||
|
|
}
|
||
|
|
|
||
|
|
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<TestState>()(
|
||
|
|
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<TestState>()(
|
||
|
|
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<TestState>()(
|
||
|
|
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<TestState>()(
|
||
|
|
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<string, unknown> };
|
||
|
|
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);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|