diff --git a/apps/web/src/components/dashboard/dashboard-grid.test.tsx b/apps/web/src/components/dashboard/dashboard-grid.test.tsx new file mode 100644 index 0000000..70c4be7 --- /dev/null +++ b/apps/web/src/components/dashboard/dashboard-grid.test.tsx @@ -0,0 +1,151 @@ +import { cleanup, render, screen } from '@testing-library/react'; +import { afterEach, describe, expect, it, vi } from 'vitest'; + +// Mock next-intl +vi.mock('next-intl', () => ({ + useTranslations: (ns: string) => (key: string) => { + const map: Record> = { + widgets: { + emptyHeading: 'No active widgets', + emptyBody: 'Click edit to add widgets to your dashboard.', + addWidget: 'Add widget', + catalogTitle: 'Add widget', + deleteTooltip: 'Remove widget', + 'clock.name': 'Clock', + 'clock.description': 'Shows the current time', + 'search.name': 'Search', + 'search.description': 'Quick web search', + 'calendar.name': 'Calendar', + 'calendar.description': 'Upcoming events', + 'note.name': 'Notes', + 'note.description': 'Free-text notes with Markdown', + }, + dashboard: { + title: 'Dashboard', + editMode: 'Edit dashboard', + saveChanges: 'Save changes', + }, + }; + return map[ns]?.[key] ?? key; + }, +})); + +// Mock react-grid-layout Responsive to a simple passthrough for jsdom stability +vi.mock('react-grid-layout', () => ({ + Responsive: ({ + children, + }: { + children: React.ReactNode; + }) =>
{children}
, +})); + +// Mock dashboard-api +vi.mock('@/lib/dashboard-api', () => ({ + fetchLayout: vi.fn().mockResolvedValue({ lg: [], md: [], sm: [], xs: [], xxs: [] }), + saveLayout: vi.fn().mockResolvedValue({}), + fetchWidgets: vi.fn().mockResolvedValue([]), + addWidget: vi.fn(), + removeWidget: vi.fn(), + updateWidgetConfig: vi.fn(), +})); + +// Mock dashboard-store +const mockStore = { + layouts: { lg: [], md: [], sm: [], xs: [], xxs: [] }, + widgets: [] as Array<{ id: string; widgetType: string; config: Record }>, + isEditMode: false, + isDirty: false, + setEditMode: vi.fn(), + updateLayouts: vi.fn(), + addWidget: vi.fn(), + removeWidget: vi.fn(), + loadDashboard: vi.fn(), + saveLayout: vi.fn(), +}; + +vi.mock('@/lib/stores/dashboard-store', () => ({ + useDashboardStore: (selector?: (state: typeof mockStore) => unknown) => { + if (selector) return selector(mockStore); + return mockStore; + }, +})); + +afterEach(() => { + cleanup(); + vi.restoreAllMocks(); +}); + +describe('DashboardGrid', () => { + it('renders empty state heading when no widgets', async () => { + const { DashboardGrid } = await import('./dashboard-grid'); + render( + , + ); + + expect(screen.getByText('No active widgets')).toBeInTheDocument(); + }); + + it('renders widget instance when widgets are provided', async () => { + const widgets = [ + { id: 'inst-1', widgetType: 'clock', config: { timezone: 'Europe/Berlin' } }, + ]; + const layouts = { + lg: [{ i: 'inst-1', x: 0, y: 0, w: 2, h: 2 }], + md: [], + sm: [], + xs: [], + xxs: [], + }; + + const { DashboardGrid } = await import('./dashboard-grid'); + render( + , + ); + + // Widget is rendered inside grid, keyed by instance id + const grid = screen.getByTestId('responsive-grid'); + expect(grid).toBeInTheDocument(); + // The widget wrapper should be in the document + expect(grid.querySelector('[data-widget-id="inst-1"]')).toBeInTheDocument(); + }); + + it('shows edit affordances (delete button) when isEditMode is true', async () => { + const widgets = [ + { id: 'inst-2', widgetType: 'clock', config: {} }, + ]; + const layouts = { + lg: [{ i: 'inst-2', x: 0, y: 0, w: 2, h: 2 }], + md: [], + sm: [], + xs: [], + xxs: [], + }; + + const { DashboardGrid } = await import('./dashboard-grid'); + render( + , + ); + + // In edit mode, delete button and drag handle should exist + expect(screen.getByLabelText('Remove widget')).toBeInTheDocument(); + expect(document.querySelector('.widget-drag-handle')).toBeInTheDocument(); + }); +}); diff --git a/apps/web/src/components/dashboard/widgets/clock-widget.test.tsx b/apps/web/src/components/dashboard/widgets/clock-widget.test.tsx new file mode 100644 index 0000000..3b4f9ed --- /dev/null +++ b/apps/web/src/components/dashboard/widgets/clock-widget.test.tsx @@ -0,0 +1,68 @@ +import { cleanup, render, screen } from '@testing-library/react'; +import { afterEach, describe, expect, it, vi } from 'vitest'; + +// Mock next-intl +vi.mock('next-intl', () => ({ + useTranslations: (ns: string) => (key: string) => { + const map: Record> = { + widgets: { + 'clock.name': 'Clock', + 'clock.description': 'Shows the current time', + 'clock.dateHint': 'Show date', + }, + }; + return map[ns]?.[key] ?? key; + }, +})); + +afterEach(() => { + cleanup(); + vi.useRealTimers(); +}); + +describe('ClockWidget', () => { + it('renders a time string with timezone config', async () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date('2026-06-24T12:30:00Z')); + + const { ClockWidget } = await import('./clock-widget'); + render( + , + ); + + // Should render a time element + const timeEl = screen.getByRole('time'); + expect(timeEl).toBeInTheDocument(); + // Time text should be non-empty + expect(timeEl.textContent).toBeTruthy(); + + // Should render a date element when showDate is true + const dateEl = screen.getByTestId('clock-date'); + expect(dateEl).toBeInTheDocument(); + expect(dateEl.textContent).toBeTruthy(); + + vi.useRealTimers(); + }); + + it('does not render date element when showDate is false', async () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date('2026-06-24T12:30:00Z')); + + const { ClockWidget } = await import('./clock-widget'); + render( + , + ); + + expect(screen.queryByTestId('clock-date')).not.toBeInTheDocument(); + + vi.useRealTimers(); + }); +});