- 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>
371 lines
12 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|