325c5ddbf2
- Reminder-Tabelle mit Zeilenschutz (Mandant+Benutzer, Systemlesen fuer den E-Mail-Planer), API reminders (Liste, Anlegen) - Kachel "Erinnerungen", globaler Melder im Portalrahmen (Browser und Desktop, je Faelligkeit einmal) - Desktop: Laufzeit-Berechtigung fuer Benachrichtigungen nur fuer die gespeicherte Server-Adresse - Zugriffsklassifikation nachgemessen Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
183 lines
6.7 KiB
TypeScript
183 lines
6.7 KiB
TypeScript
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<string, Record<string, string>> = {
|
|
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(<WidgetCatalogModal {...baseProps} isOpen={false} />);
|
|
|
|
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('renders the dialog with title and all widget type options when open', () => {
|
|
render(<WidgetCatalogModal {...baseProps} />);
|
|
|
|
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(<WidgetCatalogModal {...baseProps} onClose={onClose} />);
|
|
|
|
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(<WidgetCatalogModal {...baseProps} onClose={onClose} />);
|
|
|
|
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(<WidgetCatalogModal {...baseProps} onClose={onClose} />);
|
|
|
|
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(<WidgetCatalogModal {...baseProps} onAddWidget={onAddWidget} onClose={onClose} />);
|
|
|
|
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(<WidgetCatalogModal {...baseProps} onClose={onClose} />);
|
|
|
|
fireEvent.keyDown(document, { key: 'Escape' });
|
|
|
|
expect(onClose).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it('the close button in the dialog header carries the shared common.close label', () => {
|
|
render(<WidgetCatalogModal {...baseProps} />);
|
|
|
|
// 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<HTMLButtonElement>('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(<WidgetCatalogModal {...baseProps} accessibleModuleSlugs={['proxmox']} />);
|
|
|
|
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(<WidgetCatalogModal {...baseProps} />);
|
|
|
|
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(<WidgetCatalogModal {...baseProps} />);
|
|
expect(screen.queryByRole('button', { name: /Proxmox/ })).toBeNull();
|
|
|
|
rerender(<WidgetCatalogModal {...baseProps} accessibleModuleSlugs={['proxmox']} />);
|
|
expect(screen.getByRole('button', { name: /Proxmox/ })).toBeInTheDocument();
|
|
});
|
|
|
|
it('fail-closed: schlaegt der Modulabruf fehl (null), fehlt „Proxmox“, Plattform-Kacheln bleiben', () => {
|
|
render(<WidgetCatalogModal {...baseProps} accessibleModuleSlugs={null} />);
|
|
|
|
expect(screen.queryByRole('button', { name: /Proxmox/ })).toBeNull();
|
|
expect(screen.getByRole('button', { name: /Notiz/ })).toBeInTheDocument();
|
|
});
|
|
});
|