import { Children, isValidElement } from 'react'; import { cleanup, render, screen } from '@testing-library/react'; import { afterEach, describe, expect, it, vi } from 'vitest'; // Mock CSS imports that vitest cannot resolve vi.mock('react-grid-layout/css/styles.css', () => ({})); vi.mock('react-resizable/css/styles.css', () => ({})); // 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. // quick-260916-bwo: der Mock faengt zusaetzlich die Props ein, damit die // Raster-Konstanten (cols, rowHeight, margin, breakpoints) pruefbar sind. const captured = vi.hoisted(() => ({ props: null as Record | null })); vi.mock('react-grid-layout', () => ({ Responsive: (props: Record & { children: React.ReactNode }) => { captured.props = props; return
{props.children}
; }, noCompactor: () => null, })); // 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(); }); it('quick-260916-bwo Test 4: Grid-Props — 24/20/12/8/2 Spalten, rowHeight 20, margin 8, Breakpoints unveraendert, containerPadding folgt dem margin', async () => { captured.props = null; const { DashboardGrid } = await import('./dashboard-grid'); render( , ); expect(captured.props).not.toBeNull(); expect(captured.props?.cols).toEqual({ lg: 24, md: 20, sm: 12, xs: 8, xxs: 2 }); expect(captured.props?.rowHeight).toBe(20); expect(captured.props?.margin).toEqual([8, 8]); expect(captured.props?.breakpoints).toEqual({ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }); expect(captured.props?.containerPadding).toBeUndefined(); }); it('quick-260916-bwo Test 5: Widget ohne gespeicherten Eintrag bekommt die Vorgaben und die inhaltsgetriebenen Minima als data-grid', async () => { captured.props = null; const { DashboardGrid } = await import('./dashboard-grid'); render( , ); const children = Children.toArray(captured.props?.children as React.ReactNode); expect(children).toHaveLength(1); const first = children[0]; expect(isValidElement(first)).toBe(true); const props = (first as React.ReactElement>).props; // quick-260916-dyv: Uhr-Minimum 2x2 (vorher verdoppelt 4x4), Vorgabe 4x4 unveraendert. expect(props['data-grid']).toEqual({ x: 0, y: 0, w: 4, h: 4, minW: 2, minH: 2 }); }); it('quick-260916-dyv Test 9: gespeicherte minW/minH werden in JEDEM Breakpoint aus WIDGET_CONSTRAINTS ueberschrieben, x/y/w/h bleiben, unbekannte Typen und Schluessel unveraendert', async () => { captured.props = null; const { DashboardGrid } = await import('./dashboard-grid'); const layouts = { lg: [ { i: 'inst-1', x: 2, y: 4, w: 6, h: 6, minW: 8, minH: 8 }, { i: 'inst-x', x: 10, y: 0, w: 5, h: 5, minW: 7, minH: 7 }, ], md: [{ i: 'inst-1', x: 0, y: 0, w: 4, h: 4, minW: 4, minH: 4 }], sm: [], xs: [], xxs: [], }; render( , ); const passed = captured.props?.layouts as Record>>; expect(Object.keys(passed)).toEqual(Object.keys(layouts)); expect(passed.lg[0]).toEqual({ i: 'inst-1', x: 2, y: 4, w: 6, h: 6, minW: 2, minH: 2 }); expect(passed.md[0]).toEqual({ i: 'inst-1', x: 0, y: 0, w: 4, h: 4, minW: 2, minH: 2 }); expect(passed.md[0].minW).toBe(2); // Unbekannter Typ: Eintrag bleibt woertlich erhalten (kein Absturz, T-DYV-01). expect(passed.lg[1]).toEqual({ i: 'inst-x', x: 10, y: 0, w: 5, h: 5, minW: 7, minH: 7 }); expect(passed.sm).toEqual([]); }); it('quick-260916-dyv Test 9b: gespeicherte Groesse unter dem neuen Minimum wird auf das Minimum angehoben (Rechner h 8 -> 9, w bleibt wenn >= minW)', async () => { captured.props = null; const { DashboardGrid } = await import('./dashboard-grid'); const layouts = { lg: [ // Rechner: gespeichert mit h 8 (bwo-Minimum), neues Minimum 3x9 -> h wird 9, w 6 bleibt. { i: 'calc-1', x: 0, y: 0, w: 6, h: 8, minW: 4, minH: 8 }, // Suche: gespeichert mit w 4 (unter minW 6) -> w wird 6, h 4 bleibt. { i: 'srch-1', x: 6, y: 0, w: 4, h: 4, minW: 6, minH: 4 }, ], md: [{ i: 'calc-1', x: 0, y: 0, w: 3, h: 7, minW: 4, minH: 8 }], sm: [], xs: [], xxs: [], }; render( , ); const passed = captured.props?.layouts as Record>>; expect(passed.lg[0]).toEqual({ i: 'calc-1', x: 0, y: 0, w: 6, h: 9, minW: 3, minH: 9 }); expect(passed.lg[1]).toEqual({ i: 'srch-1', x: 6, y: 0, w: 6, h: 4, minW: 6, minH: 2 }); // Jeder Breakpoint: md-Rechner 3x7 -> 3x9 (w schon = minW, h angehoben). expect(passed.md[0]).toEqual({ i: 'calc-1', x: 0, y: 0, w: 3, h: 9, minW: 3, minH: 9 }); // Eingabe unveraendert (keine Mutation des Store-Objekts). expect(layouts.lg[0].h).toBe(8); expect(layouts.lg[0].minH).toBe(8); }); });