import { cleanup, fireEvent, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { afterEach, describe, expect, it, vi } from 'vitest'; // Mock next-intl useTranslations, namespaced like the real catalog/common usage. vi.mock('next-intl', () => ({ useTranslations: (namespace: string) => (key: string) => { const translations: Record> = { widgets: { catalogTitle: 'Widget hinzufügen', catalogClose: 'Dialog schließen', 'clock.name': 'Uhr', 'clock.description': 'Zeigt die aktuelle Zeit', 'search.name': 'Suche', 'search.description': 'Schnellsuche', 'calendar.name': 'Kalender', 'calendar.description': 'Kommende Termine', 'note.name': 'Notiz', 'note.description': 'Schnelle Notizen', 'calculator.name': 'Taschenrechner', 'calculator.description': 'Einfache Berechnungen', 'favorites.name': 'Favoriten', 'favorites.description': 'Schnellzugriff auf Favoriten', 'stopwatch.name': 'Stoppuhr', 'stopwatch.description': 'Zeit messen', 'pictureFrame.name': 'Bilderrahmen', 'pictureFrame.description': 'Bilder als Diashow', 'xframe.name': 'XFrame', 'xframe.description': 'Webseite einbetten', 'proxmox.name': 'Proxmox', 'proxmox.description': 'Zustand Ihrer Proxmox-Server auf einen Blick', }, common: { close: 'Schließen', }, }; return translations[namespace]?.[key] ?? key; }, })); import { WIDGET_TYPES } from '@tessera/shared'; import { WIDGET_REGISTRY } from './widget-registry'; import { WidgetCatalogModal } from './widget-catalog-modal'; // Neun Kacheln sind Plattform-Kacheln ohne moduleSlug und erscheinen auch bei // leerer Modulliste; die Proxmox-Kachel (quick-260924-i8v) nur mit Zugriff // auf das Modul 'proxmox'. const baseProps = { isOpen: true, onClose: vi.fn(), onAddWidget: vi.fn(), accessibleModuleSlugs: [] as string[], }; afterEach(() => { cleanup(); vi.clearAllMocks(); }); describe('WidgetCatalogModal', () => { it('renders nothing when isOpen is false', () => { render(); expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); }); it('renders the dialog with title and all widget type options when open', () => { render(); expect(screen.getByRole('dialog', { name: 'Widget hinzufügen' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: /Uhr/ })).toBeInTheDocument(); }); it('clicking the backdrop button closes the dialog (D-01)', async () => { const onClose = vi.fn(); const user = userEvent.setup(); render(); await user.click(screen.getByRole('button', { name: 'Dialog schließen' })); expect(onClose).toHaveBeenCalledOnce(); }); it('activating the backdrop button via a real keyboard Enter closes the dialog (D-01)', async () => { const onClose = vi.fn(); const user = userEvent.setup(); render(); const backdrop = screen.getByRole('button', { name: 'Dialog schließen' }); backdrop.focus(); await user.keyboard('{Enter}'); expect(onClose).toHaveBeenCalledOnce(); }); it('clicking inside the dialog surface does not close the dialog', async () => { const onClose = vi.fn(); const user = userEvent.setup(); render(); await user.click(screen.getByRole('dialog', { name: 'Widget hinzufügen' })); expect(onClose).not.toHaveBeenCalled(); }); it('clicking a widget type option adds the widget and closes the dialog', async () => { const onAddWidget = vi.fn(); const onClose = vi.fn(); const user = userEvent.setup(); render(); await user.click(screen.getByRole('button', { name: /Uhr/ })); expect(onAddWidget).toHaveBeenCalledWith('clock'); expect(onClose).toHaveBeenCalledOnce(); }); it('pressing Escape closes the dialog', () => { const onClose = vi.fn(); render(); fireEvent.keyDown(document, { key: 'Escape' }); expect(onClose).toHaveBeenCalledOnce(); }); it('the close button in the dialog header carries the shared common.close label', () => { render(); // Two buttons named "Schließen": the header close icon button. // Query specifically among the dialog's own buttons, excluding the backdrop // (backdrop is named "Dialog schließen", the header button "Schließen"). expect(screen.getByRole('button', { name: 'Schließen' })).toBeInTheDocument(); }); }); /** * quick-260922-m1h: Der Katalog fuehrt keine zweite Typliste mehr — er leitet * sie aus der Registry ab und filtert nach Modulzugriff. */ describe('WidgetCatalogModal: Liste kommt aus der Registry (quick-260922-m1h)', () => { function catalogTypes(): (string | null)[] { const dialog = screen.getByRole('dialog', { name: 'Widget hinzufügen' }); return Array.from( dialog.querySelectorAll('button[data-widget-type]'), ).map((c) => c.getAttribute('data-widget-type')); } it("zeigt mit Zugriff auf 'proxmox' alle elf Kacheln in der Reihenfolge der Registry, Erinnerungen zuletzt", () => { render(); expect(catalogTypes()).toEqual([...WIDGET_TYPES]); expect(catalogTypes().at(-1)).toBe('reminder'); expect(Object.keys(WIDGET_REGISTRY)).toEqual([...WIDGET_TYPES]); }); it('zeigt ohne Modulzugriff die neun Plattform-Kacheln ohne Proxmox', () => { render(); expect(catalogTypes()).toEqual(WIDGET_TYPES.filter((type) => type !== 'proxmox')); }); it('die Kachel „Proxmox“ fehlt, wenn das Modul nicht zugaenglich ist, und erscheint, wenn doch', () => { const { rerender } = render(); expect(screen.queryByRole('button', { name: /Proxmox/ })).toBeNull(); rerender(); expect(screen.getByRole('button', { name: /Proxmox/ })).toBeInTheDocument(); }); it('fail-closed: schlaegt der Modulabruf fehl (null), fehlt „Proxmox“, Plattform-Kacheln bleiben', () => { render(); expect(screen.queryByRole('button', { name: /Proxmox/ })).toBeNull(); expect(screen.getByRole('button', { name: /Notiz/ })).toBeInTheDocument(); }); });