import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; // quick-260916-dyv: Dashboard-Seite — feste Aktionsleiste unten rechts // (Stift/Haekchen + "Widget hinzufuegen"), Grid direkt im Container p-2, // kein Abstands-Wrapper (mt-8) und kein Block oben rechts (top-2) mehr. // Muster: admin/groups/groups-page.test.tsx (next-intl-Mock mit Namensraum). const messages: Record> = { widgets: { editMode: 'Dashboard bearbeiten', saveChanges: 'Änderungen speichern', addWidget: 'Widget hinzufügen', editShort: 'Bearbeiten', done: 'Fertig', toolbarLabel: 'Dashboard-Aktionen', layoutLoadError: 'Dashboard konnte nicht geladen werden. Bitte laden Sie die Seite neu.', }, }; vi.mock('next-intl', () => ({ useTranslations: (ns: string) => (key: string) => messages[ns]?.[key] ?? key, useLocale: () => 'de', })); const mockStore = { // quick-260923-ad9: Reiter — leere Liste rendert die Reiterleiste ohne // Reiter-Knoepfe, damit diese Datei bei den bestehenden Zusammensetzungs- // Pruefungen bleibt, statt den Reiter-Speicher selbst nachzubauen. dashboards: [] as Array<{ id: string; name: string; position: number }>, activeDashboardId: null as string | null, isSwitchingDashboard: false, layouts: { lg: [], md: [], sm: [], xs: [], xxs: [] }, widgets: [] as Array<{ id: string; widgetType: string; config: Record }>, isEditMode: false, isDirty: false, isLoading: false, error: null as string | null, setEditMode: vi.fn(), updateLayouts: vi.fn(), addWidget: vi.fn(), removeWidget: vi.fn(), loadDashboard: vi.fn(), saveLayout: vi.fn(), selectDashboard: vi.fn(), createDashboard: vi.fn(), renameDashboard: vi.fn(), deleteDashboard: vi.fn(), reorderDashboards: vi.fn(), }; vi.mock('@/lib/stores/dashboard-store', () => ({ useDashboardStore: (selector?: (state: typeof mockStore) => unknown) => { if (selector) return selector(mockStore); return mockStore; }, })); vi.mock('@/components/dashboard/dashboard-grid', () => ({ DashboardGrid: (p: { isEditMode: boolean }) => (
), })); // quick-260922-m1h: Der Katalog bekommt die zugaenglichen Modul-Slugs als // Prop von dieser Seite — die Attrappe merkt sie sich, damit der Test sie // pruefen kann, ohne den echten Dialog zu rendern. const catalogProps: { accessibleModuleSlugs?: readonly string[] | null } = {}; vi.mock('@/components/dashboard/widget-catalog-modal', () => ({ WidgetCatalogModal: (p: { accessibleModuleSlugs: readonly string[] | null }) => { catalogProps.accessibleModuleSlugs = p.accessibleModuleSlugs; return null; }, })); vi.mock('@/components/dashboard/widgets/clock-widget', () => ({ ClockWidget: () => null })); vi.mock('@/components/dashboard/widgets/search-widget', () => ({ SearchWidget: () => null })); vi.mock('@/components/dashboard/widgets/calendar-widget', () => ({ CalendarWidget: () => null })); vi.mock('@/components/dashboard/widgets/note-widget', () => ({ NoteWidget: () => null })); vi.mock('@/components/dashboard/widgets/calculator-widget', () => ({ CalculatorWidget: () => null })); vi.mock('@/components/dashboard/widgets/stopwatch-widget', () => ({ StopwatchWidget: () => null })); vi.mock('@/components/dashboard/widgets/favorites-widget', () => ({ FavoritesWidget: () => null })); vi.mock('@/components/dashboard/widgets/picture-frame-widget', () => ({ PictureFrameWidget: () => null })); vi.mock('@/components/dashboard/widgets/xframe-widget', () => ({ XframeWidget: () => null })); vi.mock('@/components/dashboard/widgets/proxmox-widget', () => ({ ProxmoxWidget: () => null })); beforeEach(() => { catalogProps.accessibleModuleSlugs = undefined; vi.stubGlobal( 'fetch', vi.fn(async () => ({ ok: true, json: async () => [ { id: 'm1', slug: 'domaincheck', name: 'Domaincheck', category: 'tools' }, { id: 'm2', slug: 'tender-radar', name: 'Tender', category: 'tools' }, ], })), ); mockStore.isEditMode = false; mockStore.isLoading = false; mockStore.error = null; mockStore.setEditMode = vi.fn(); mockStore.loadDashboard = vi.fn(); }); afterEach(() => { cleanup(); vi.unstubAllGlobals(); }); describe('DashboardPage (quick-260916-dyv)', () => { it('Test 1: Ansichtsmodus — Befehlsleiste rechts oben mit „Bearbeiten“, kein "Widget hinzufuegen", Begruessung, Grid direkt im Container p-2 (Design „Mosaik“)', async () => { const { default: DashboardPage } = await import('./page'); render(); const toolbar = screen.getByRole('toolbar', { name: 'Dashboard-Aktionen' }); const toggle = screen.getByRole('button', { name: 'Bearbeiten' }); expect(toggle.parentElement).toBe(toolbar); expect(toggle).toHaveAttribute('aria-pressed', 'false'); // kein schwebender Knopf mehr unten rechts expect(toolbar.classList.contains('fixed')).toBe(false); expect(document.querySelector('.bottom-6')).toBeNull(); expect(screen.queryByRole('button', { name: 'Widget hinzufügen' })).toBeNull(); expect(document.querySelector('.mt-8')).toBeNull(); expect(document.querySelector('.top-2')).toBeNull(); // Runde 3: die Begruessung steht in der Seitenleiste, nicht mehr hier. expect(screen.queryByTestId('dashboard-greeting')).toBeNull(); const grid = screen.getByTestId('dashboard-grid'); expect((grid.parentElement as HTMLElement).classList.contains('p-2')).toBe(true); expect(grid.getAttribute('data-edit')).toBe('false'); }); it('Test 2: Bearbeitungsmodus — "Widget hinzufuegen" links neben „Fertig“ in derselben Leiste, Grid im Bearbeitungsmodus', async () => { mockStore.isEditMode = true; const { default: DashboardPage } = await import('./page'); render(); const toggle = screen.getByRole('button', { name: 'Fertig' }); const add = screen.getByRole('button', { name: 'Widget hinzufügen' }); expect(add.parentElement).toBe(toggle.parentElement); expect(add.compareDocumentPosition(toggle) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); expect(screen.getByTestId('dashboard-grid').getAttribute('data-edit')).toBe('true'); }); it('Test 3: Umschalten — Klick auf "Bearbeiten" ruft setEditMode(true) genau einmal', async () => { const { default: DashboardPage } = await import('./page'); render(); fireEvent.click(screen.getByRole('button', { name: 'Bearbeiten' })); expect(mockStore.setEditMode).toHaveBeenCalledTimes(1); expect(mockStore.setEditMode).toHaveBeenCalledWith(true); }); }); describe('DashboardPage: zugaengliche Module fuer den Katalog (quick-260922-m1h)', () => { it('Test 4: holt GET /modules/active mit Sitzungs-Keks und reicht die Slugs an den Katalog durch', async () => { const { default: DashboardPage } = await import('./page'); render(); await waitFor(() => { expect(catalogProps.accessibleModuleSlugs).toEqual(['domaincheck', 'tender-radar']); }); const call = vi.mocked(fetch).mock.calls[0]; expect(String(call[0])).toContain('/modules/active'); expect(call[1]).toMatchObject({ credentials: 'include' }); }); it('Test 5: fail-closed — schlaegt der Abruf fehl, bleibt die Liste unbekannt (null)', async () => { vi.stubGlobal( 'fetch', vi.fn(async () => { throw new Error('Netzwerk weg'); }), ); const { default: DashboardPage } = await import('./page'); render(); await waitFor(() => { expect(vi.mocked(fetch)).toHaveBeenCalled(); }); expect(catalogProps.accessibleModuleSlugs).toBeNull(); }); });