Files
fast-next-template/frontend/tests/lib/stores/eventStore.test.ts
Felipe Cardoso 5c35702caf test(frontend): comprehensive test coverage improvements and bug fixes
- Raise coverage thresholds to 90% statements/lines/functions, 85% branches
- Add comprehensive tests for ProjectDashboard, ProjectWizard, and all wizard steps
- Add tests for issue management: IssueDetailPanel, BulkActions, IssueFilters
- Expand IssueTable tests with keyboard navigation, dropdown menu, edge cases
- Add useIssues hook tests covering all mutations and optimistic updates
- Expand eventStore tests with selector hooks and additional scenarios
- Expand useProjectEvents tests with error recovery, ping events, edge cases
- Add PriorityBadge, StatusBadge, SyncStatusIndicator fallback branch tests
- Add constants.test.ts for comprehensive constant validation

Bug fixes:
- Fix false positive rollback test to properly verify onMutate context setup
- Replace deprecated substr() with substring() in mock helpers
- Fix type errors: ProjectComplexity, ClientMode enum values
- Fix unused imports and variables across test files
- Fix @ts-expect-error directives and method override signatures

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 19:53:41 +01:00

371 lines
12 KiB
TypeScript

/**
* Tests for Event Store
*/
import { useEventStore } from '@/lib/stores/eventStore';
import { EventType, type ProjectEvent } from '@/lib/types/events';
/**
* Helper to create mock event
*/
function createMockEvent(overrides: Partial<ProjectEvent> = {}): ProjectEvent {
return {
id: `event-${Math.random().toString(36).substring(2, 11)}`,
type: EventType.AGENT_MESSAGE,
timestamp: new Date().toISOString(),
project_id: 'project-123',
actor_id: 'agent-456',
actor_type: 'agent',
payload: { message: 'Test message' },
...overrides,
};
}
describe('Event Store', () => {
beforeEach(() => {
// Reset store state
useEventStore.setState({
eventsByProject: {},
maxEvents: 100,
});
});
describe('addEvent', () => {
it('should add an event to the store', () => {
const event = createMockEvent();
useEventStore.getState().addEvent(event);
const events = useEventStore.getState().getProjectEvents('project-123');
expect(events).toHaveLength(1);
expect(events[0]).toEqual(event);
});
it('should add events to correct project', () => {
const event1 = createMockEvent({ project_id: 'project-1' });
const event2 = createMockEvent({ project_id: 'project-2' });
useEventStore.getState().addEvent(event1);
useEventStore.getState().addEvent(event2);
expect(useEventStore.getState().getProjectEvents('project-1')).toHaveLength(1);
expect(useEventStore.getState().getProjectEvents('project-2')).toHaveLength(1);
});
it('should skip duplicate event IDs', () => {
const event = createMockEvent({ id: 'unique-id' });
useEventStore.getState().addEvent(event);
useEventStore.getState().addEvent(event);
const events = useEventStore.getState().getProjectEvents('project-123');
expect(events).toHaveLength(1);
});
it('should trim events to maxEvents limit', () => {
useEventStore.getState().setMaxEvents(5);
// Add 10 events
for (let i = 0; i < 10; i++) {
useEventStore.getState().addEvent(createMockEvent({ id: `event-${i}` }));
}
const events = useEventStore.getState().getProjectEvents('project-123');
expect(events).toHaveLength(5);
// Should keep the last 5 events
expect(events[0].id).toBe('event-5');
expect(events[4].id).toBe('event-9');
});
});
describe('addEvents', () => {
it('should add multiple events at once', () => {
const events = [
createMockEvent({ id: 'event-1' }),
createMockEvent({ id: 'event-2' }),
createMockEvent({ id: 'event-3' }),
];
useEventStore.getState().addEvents(events);
const storedEvents = useEventStore.getState().getProjectEvents('project-123');
expect(storedEvents).toHaveLength(3);
});
it('should handle empty array', () => {
useEventStore.getState().addEvents([]);
const events = useEventStore.getState().getProjectEvents('project-123');
expect(events).toHaveLength(0);
});
it('should filter out duplicate events', () => {
const existingEvent = createMockEvent({ id: 'existing-event' });
useEventStore.getState().addEvent(existingEvent);
const newEvents = [
createMockEvent({ id: 'existing-event' }), // Duplicate
createMockEvent({ id: 'new-event-1' }),
createMockEvent({ id: 'new-event-2' }),
];
useEventStore.getState().addEvents(newEvents);
const storedEvents = useEventStore.getState().getProjectEvents('project-123');
expect(storedEvents).toHaveLength(3);
});
it('should add events to multiple projects', () => {
const events = [
createMockEvent({ id: 'event-1', project_id: 'project-1' }),
createMockEvent({ id: 'event-2', project_id: 'project-2' }),
createMockEvent({ id: 'event-3', project_id: 'project-1' }),
];
useEventStore.getState().addEvents(events);
expect(useEventStore.getState().getProjectEvents('project-1')).toHaveLength(2);
expect(useEventStore.getState().getProjectEvents('project-2')).toHaveLength(1);
});
});
describe('clearProjectEvents', () => {
it('should clear events for a specific project', () => {
const event1 = createMockEvent({ project_id: 'project-1' });
const event2 = createMockEvent({ project_id: 'project-2' });
useEventStore.getState().addEvent(event1);
useEventStore.getState().addEvent(event2);
useEventStore.getState().clearProjectEvents('project-1');
expect(useEventStore.getState().getProjectEvents('project-1')).toHaveLength(0);
expect(useEventStore.getState().getProjectEvents('project-2')).toHaveLength(1);
});
it('should handle clearing non-existent project', () => {
expect(() => {
useEventStore.getState().clearProjectEvents('non-existent');
}).not.toThrow();
});
});
describe('clearAllEvents', () => {
it('should clear all events from all projects', () => {
const event1 = createMockEvent({ project_id: 'project-1' });
const event2 = createMockEvent({ project_id: 'project-2' });
useEventStore.getState().addEvent(event1);
useEventStore.getState().addEvent(event2);
useEventStore.getState().clearAllEvents();
expect(useEventStore.getState().getProjectEvents('project-1')).toHaveLength(0);
expect(useEventStore.getState().getProjectEvents('project-2')).toHaveLength(0);
});
});
describe('getProjectEvents', () => {
it('should return empty array for non-existent project', () => {
const events = useEventStore.getState().getProjectEvents('non-existent');
expect(events).toEqual([]);
});
it('should return events for existing project', () => {
const event = createMockEvent();
useEventStore.getState().addEvent(event);
const events = useEventStore.getState().getProjectEvents('project-123');
expect(events).toHaveLength(1);
expect(events[0]).toEqual(event);
});
});
describe('getFilteredEvents', () => {
it('should filter events by type', () => {
const agentEvent = createMockEvent({ type: EventType.AGENT_MESSAGE });
const issueEvent = createMockEvent({ type: EventType.ISSUE_CREATED });
const sprintEvent = createMockEvent({ type: EventType.SPRINT_STARTED });
useEventStore.getState().addEvents([agentEvent, issueEvent, sprintEvent]);
const filtered = useEventStore.getState().getFilteredEvents('project-123', [
EventType.AGENT_MESSAGE,
EventType.ISSUE_CREATED,
]);
expect(filtered).toHaveLength(2);
expect(filtered.map((e) => e.type)).toContain(EventType.AGENT_MESSAGE);
expect(filtered.map((e) => e.type)).toContain(EventType.ISSUE_CREATED);
});
it('should return all events when types array is empty', () => {
const event1 = createMockEvent({ type: EventType.AGENT_MESSAGE });
const event2 = createMockEvent({ type: EventType.ISSUE_CREATED });
useEventStore.getState().addEvents([event1, event2]);
const filtered = useEventStore.getState().getFilteredEvents('project-123', []);
expect(filtered).toHaveLength(2);
});
it('should return empty array for non-existent project', () => {
const filtered = useEventStore.getState().getFilteredEvents('non-existent', [
EventType.AGENT_MESSAGE,
]);
expect(filtered).toEqual([]);
});
});
describe('setMaxEvents', () => {
it('should update maxEvents setting', () => {
useEventStore.getState().setMaxEvents(50);
expect(useEventStore.getState().maxEvents).toBe(50);
});
it('should use default for invalid values', () => {
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
useEventStore.getState().setMaxEvents(0);
expect(useEventStore.getState().maxEvents).toBe(100); // Default
useEventStore.getState().setMaxEvents(-5);
expect(useEventStore.getState().maxEvents).toBe(100); // Default
expect(consoleWarnSpy).toHaveBeenCalled();
consoleWarnSpy.mockRestore();
});
it('should trim existing events when reducing maxEvents', () => {
// Add 10 events
for (let i = 0; i < 10; i++) {
useEventStore.getState().addEvent(createMockEvent({ id: `event-${i}` }));
}
expect(useEventStore.getState().getProjectEvents('project-123')).toHaveLength(10);
useEventStore.getState().setMaxEvents(5);
const events = useEventStore.getState().getProjectEvents('project-123');
expect(events).toHaveLength(5);
// Should keep the last 5 events
expect(events[0].id).toBe('event-5');
});
});
});
/**
* Tests for Selector Hooks
*/
import { renderHook } from '@testing-library/react';
import { useProjectEventsFromStore, useLatestEvent, useEventCount } from '@/lib/stores/eventStore';
describe('Event Store Selector Hooks', () => {
beforeEach(() => {
// Reset store state
useEventStore.setState({
eventsByProject: {},
maxEvents: 100,
});
});
describe('useProjectEventsFromStore', () => {
it('should return empty array for non-existent project', () => {
const { result } = renderHook(() => useProjectEventsFromStore('non-existent'));
expect(result.current).toEqual([]);
});
it('should return events for existing project', () => {
const event = createMockEvent();
useEventStore.getState().addEvent(event);
const { result } = renderHook(() => useProjectEventsFromStore('project-123'));
expect(result.current).toHaveLength(1);
expect(result.current[0]).toEqual(event);
});
it('should update when events are added', () => {
const { result, rerender } = renderHook(() => useProjectEventsFromStore('project-123'));
expect(result.current).toHaveLength(0);
useEventStore.getState().addEvent(createMockEvent());
rerender();
expect(result.current).toHaveLength(1);
});
});
describe('useLatestEvent', () => {
it('should return undefined for non-existent project', () => {
const { result } = renderHook(() => useLatestEvent('non-existent'));
expect(result.current).toBeUndefined();
});
it('should return undefined for empty project', () => {
const { result } = renderHook(() => useLatestEvent('project-123'));
expect(result.current).toBeUndefined();
});
it('should return the last event added', () => {
const event1 = createMockEvent({ id: 'event-1' });
const event2 = createMockEvent({ id: 'event-2' });
const event3 = createMockEvent({ id: 'event-3' });
useEventStore.getState().addEvents([event1, event2, event3]);
const { result } = renderHook(() => useLatestEvent('project-123'));
expect(result.current?.id).toBe('event-3');
});
it('should update when a new event is added', () => {
const event1 = createMockEvent({ id: 'event-1' });
useEventStore.getState().addEvent(event1);
const { result, rerender } = renderHook(() => useLatestEvent('project-123'));
expect(result.current?.id).toBe('event-1');
const event2 = createMockEvent({ id: 'event-2' });
useEventStore.getState().addEvent(event2);
rerender();
expect(result.current?.id).toBe('event-2');
});
});
describe('useEventCount', () => {
it('should return 0 for non-existent project', () => {
const { result } = renderHook(() => useEventCount('non-existent'));
expect(result.current).toBe(0);
});
it('should return correct count for existing project', () => {
const events = [
createMockEvent({ id: 'event-1' }),
createMockEvent({ id: 'event-2' }),
createMockEvent({ id: 'event-3' }),
];
useEventStore.getState().addEvents(events);
const { result } = renderHook(() => useEventCount('project-123'));
expect(result.current).toBe(3);
});
it('should update when events are added or removed', () => {
const { result, rerender } = renderHook(() => useEventCount('project-123'));
expect(result.current).toBe(0);
useEventStore.getState().addEvent(createMockEvent({ id: 'event-1' }));
rerender();
expect(result.current).toBe(1);
useEventStore.getState().addEvent(createMockEvent({ id: 'event-2' }));
rerender();
expect(result.current).toBe(2);
useEventStore.getState().clearProjectEvents('project-123');
rerender();
expect(result.current).toBe(0);
});
});
});