Files
tessera-ctl/apps/web/src/app/(portal)/page.test.tsx
T
schalli 39ea1474a5 feat: Link-Widget restlos entfernt (Web, API, Migration, Handbuch, Changelog)
- Web: Union-Mitglied, Constraints, Icon, Registry-Eintrag, wire-Funktion,
  Katalog, Seiten-Verdrahtung, Tests und i18n (de/en) fuer den Typ link
  entfernt; link-widget.tsx/.test.tsx geloescht
- API: create-widget.dto.ts (@IsIn-Liste) und widget-module-map.ts
  (Kommentare) auf sieben Typen angepasst; schema.prisma unveraendert
- Neue Migration 20260916120000_remove_link_widget: eine idempotente
  DELETE-Anweisung auf WidgetInstance, FavoriteLink kaskadiert ueber den
  bestehenden FK; wird in diesem Auftrag NICHT ausgefuehrt
- Neuer widget-wrapper-Test belegt den grauen Fallback fuer unbekannte
  Widget-Typen (Bestandsverhalten, jetzt festgeschrieben)
- Handbuch (Widget-Tabelle, Einstellungs-Hinweise) und CHANGELOG
  (Geaendert/Entfernt/Behoben) aktualisiert
- Web: 52 Dateien / 344 Tests gruen, tsc Exit 0; API: tsc Exit 0,
  dashboard-Spec 31 Tests gruen

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 13:44:09 +02:00

118 lines
4.8 KiB
TypeScript

import { cleanup, fireEvent, render, screen } 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<string, Record<string, string>> = {
widgets: {
editMode: 'Dashboard bearbeiten',
saveChanges: 'Änderungen speichern',
addWidget: 'Widget hinzufügen',
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,
}));
const mockStore = {
layouts: { lg: [], md: [], sm: [], xs: [], xxs: [] },
widgets: [] as Array<{ id: string; widgetType: string; config: Record<string, unknown> }>,
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(),
};
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 }) => (
<div data-testid="dashboard-grid" data-edit={String(p.isEditMode)} />
),
}));
vi.mock('@/components/dashboard/widget-catalog-modal', () => ({
WidgetCatalogModal: () => 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 }));
beforeEach(() => {
mockStore.isEditMode = false;
mockStore.isLoading = false;
mockStore.error = null;
mockStore.setEditMode = vi.fn();
mockStore.loadDashboard = vi.fn();
});
afterEach(() => {
cleanup();
});
describe('DashboardPage (quick-260916-dyv)', () => {
it('Test 1: Ansichtsmodus — Stift in fester Leiste unten rechts, kein "Widget hinzufuegen", kein mt-8/top-2, Grid direkt im Container p-2', async () => {
const { default: DashboardPage } = await import('./page');
render(<DashboardPage />);
const toggle = screen.getByRole('button', { name: 'Dashboard bearbeiten' });
const bar = toggle.parentElement as HTMLElement;
expect(bar.classList.contains('fixed')).toBe(true);
expect(bar.classList.contains('bottom-6')).toBe(true);
expect(bar.classList.contains('right-6')).toBe(true);
expect(bar.classList.contains('z-20')).toBe(true);
expect(screen.queryByRole('button', { name: 'Widget hinzufügen' })).toBeNull();
expect(document.querySelector('.mt-8')).toBeNull();
expect(document.querySelector('.top-2')).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 dem Haekchen in derselben Leiste, Grid im Bearbeitungsmodus', async () => {
mockStore.isEditMode = true;
const { default: DashboardPage } = await import('./page');
render(<DashboardPage />);
const toggle = screen.getByRole('button', { name: 'Änderungen speichern' });
const add = screen.getByRole('button', { name: 'Widget hinzufügen' });
expect(add.parentElement).toBe(toggle.parentElement);
// "Widget hinzufuegen" steht im DOM VOR dem Umschalter (links daneben).
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 "Dashboard bearbeiten" ruft setEditMode(true) genau einmal', async () => {
const { default: DashboardPage } = await import('./page');
render(<DashboardPage />);
fireEvent.click(screen.getByRole('button', { name: 'Dashboard bearbeiten' }));
expect(mockStore.setEditMode).toHaveBeenCalledTimes(1);
expect(mockStore.setEditMode).toHaveBeenCalledWith(true);
});
});