diff --git a/apps/web/src/components/dashboard/widgets/calendar-widget.test.tsx b/apps/web/src/components/dashboard/widgets/calendar-widget.test.tsx new file mode 100644 index 0000000..7004dda --- /dev/null +++ b/apps/web/src/components/dashboard/widgets/calendar-widget.test.tsx @@ -0,0 +1,120 @@ +import { cleanup, render, screen, waitFor } from '@testing-library/react'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +// Mock next-intl +vi.mock('next-intl', () => ({ + useTranslations: (ns: string) => (key: string) => { + const map: Record> = { + widgets: { + 'calendar.name': 'Calendar', + 'calendar.description': 'Upcoming events', + 'calendar.emptyNoSources': 'No calendar sources configured', + 'calendar.emptyNoEvents': 'No upcoming events', + 'calendar.loading': 'Loading...', + }, + }; + return map[ns]?.[key] ?? key; + }, +})); + +// Mock calendar-api +const mockFetchEvents = vi.fn(); +const mockFetchSources = vi.fn(); +vi.mock('@/lib/calendar-api', () => ({ + fetchEvents: (...args: unknown[]) => mockFetchEvents(...args), + fetchSources: (...args: unknown[]) => mockFetchSources(...args), +})); + +afterEach(() => { + cleanup(); + vi.restoreAllMocks(); +}); + +describe('CalendarWidget', () => { + beforeEach(() => { + // Default: sources exist so we get past the source check + mockFetchSources.mockResolvedValue([ + { id: 's1', name: 'Work', type: 'caldav', isVisible: true, color: '#ff0000' }, + ]); + }); + + it('renders event titles, times, and source color dots for two events', async () => { + mockFetchEvents.mockResolvedValue([ + { + id: 'e1', + sourceId: 's1', + title: 'Team Meeting', + start: '2026-06-24T10:00:00Z', + end: '2026-06-24T11:00:00Z', + allDay: false, + color: '#ff0000', + }, + { + id: 'e2', + sourceId: 's1', + title: 'Lunch Break', + start: '2026-06-24T12:00:00Z', + end: '2026-06-24T13:00:00Z', + allDay: false, + color: '#00ff00', + }, + ]); + + const { CalendarWidget } = await import('./calendar-widget'); + render( + , + ); + + // Wait for events to load + await waitFor(() => { + expect(screen.getByText('Team Meeting')).toBeInTheDocument(); + }); + + expect(screen.getByText('Lunch Break')).toBeInTheDocument(); + + // Source color dots should be rendered (8px circles) + const colorDots = screen.getAllByTestId('event-color-dot'); + expect(colorDots).toHaveLength(2); + }); + + it('renders no-events message when sources exist but no events returned', async () => { + mockFetchEvents.mockResolvedValue([]); + + const { CalendarWidget } = await import('./calendar-widget'); + render( + , + ); + + await waitFor(() => { + expect(screen.getByText('No upcoming events')).toBeInTheDocument(); + }); + }); + + it('renders no-sources message when no sources configured', async () => { + mockFetchSources.mockResolvedValue([]); + mockFetchEvents.mockResolvedValue([]); + + const { CalendarWidget } = await import('./calendar-widget'); + render( + , + ); + + await waitFor(() => { + expect( + screen.getByText('No calendar sources configured'), + ).toBeInTheDocument(); + }); + }); +});