diff --git a/apps/web/src/components/dashboard/widget-catalog-modal.test.tsx b/apps/web/src/components/dashboard/widget-catalog-modal.test.tsx new file mode 100644 index 0000000..d3d33ad --- /dev/null +++ b/apps/web/src/components/dashboard/widget-catalog-modal.test.tsx @@ -0,0 +1,128 @@ +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', + }, + common: { + close: 'Schließen', + }, + }; + return translations[namespace]?.[key] ?? key; + }, +})); + +import { WidgetCatalogModal } from './widget-catalog-modal'; + +const baseProps = { + isOpen: true, + onClose: vi.fn(), + onAddWidget: vi.fn(), +}; + +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(); + }); +}); diff --git a/apps/web/src/components/dashboard/widget-catalog-modal.tsx b/apps/web/src/components/dashboard/widget-catalog-modal.tsx index 118a144..5060c02 100644 --- a/apps/web/src/components/dashboard/widget-catalog-modal.tsx +++ b/apps/web/src/components/dashboard/widget-catalog-modal.tsx @@ -31,6 +31,7 @@ export function WidgetCatalogModal({ onAddWidget, }: WidgetCatalogModalProps) { const t = useTranslations('widgets'); + const tCommon = useTranslations('common'); const dialogRef = useRef(null); // Focus trap + Escape handler @@ -53,12 +54,16 @@ export function WidgetCatalogModal({ if (!isOpen) return null; return ( -
- {/* Backdrop — visually only, hidden from assistive tech */} -