feat(quick-260923-ad9): Reiterleiste in der Oberflaeche - Task 3
dashboard-api.ts: fuenf neue Abrufe fuer die Reiter-Wege, die vier bestehenden Aufrufe (Layout/Widgets lesen/schreiben) tragen jetzt die Reiter-Kennung (Abfrageparameter bzw. Rumpf). dashboard-store.ts: Zustand um dashboards/activeDashboardId/ isSwitchingDashboard erweitert. loadDashboard() holt zuerst die Reiter, macht den ersten aktiv, laedt erst danach dessen Inhalt; ein modul-globales Versprechen schuetzt gegen doppeltes Laden der Reiterliste bei doppeltem Einhaengen. selectDashboard() schreibt eine ungespeicherte Anordnung ZUERST fuer den alten Reiter (Kennung vor dem Wechsel gelesen) und ersetzt danach Kacheln/Anordnung vollstaendig. createDashboard/renameDashboard/ deleteDashboard pflegen Reiterliste und aktiven Reiter; Loeschen des aktiven Reiters macht den dann ersten Reiter aktiv. Die Marker-Umrechnung (quick-260916-bwo) laeuft unveraendert je Reiter mit, auch beim Wechsel. dashboard-tabs.tsx (neu): Reiterleiste, Klick wechselt immer; im Bearbeitungsmodus zusaetzlich Anlegen, Umbenennen (an Ort und Stelle, Enter/Escape) und Loeschen (mit Rueckfrage) - Loeschen-Knopf fehlt beim letzten verbleibenden Reiter (D-10). Fokus beim Umbenennen ueber einen Ref statt autoFocus (lint/a11y/noAutofocus). (portal)/page.tsx: Leiste ueber dem Raster, kurze Ladezeile waehrend eines Reiterwechsels statt des Rasters - die Leiste bleibt stehen. DashboardGrid selbst unveraendert (D-06). Uebersetzungen: neue Schluessel unter widgets.tabs.* in de.json/en.json, Dialog-Knoepfe nutzen die vorhandenen common.cancel/common.delete. Deviations (Rule 3 - blockierende Nachwirkung dieses Tasks, ausserhalb der files_modified-Liste, aber direkt durch die dashboardId-Pflicht verursacht): - settings/dashboard/page.tsx: fetchWidgets() verlangt jetzt eine Reiter-Kennung; die Seite ist nicht reiterbewusst (ausserhalb des Umfangs) und zeigt jetzt die Kacheln des ERSTEN Reiters - deckungsgleich mit dem bisherigen Verhalten fuer den haeufigen Fall genau eines Reiters. - (portal)/page.test.tsx: mockStore brauchte die neuen Reiter-Felder/ -Methoden, sonst waere DashboardTabs auf `dashboards.map` von undefined gescheitert. Tests: dashboard-store.test.ts 15 (6 alte angepasste Signaturen + 9 neue), dashboard-tabs.test.tsx 12 (neu). web gesamt 682 Tests in 82 Dateien, dashboard-grid.test.tsx unveraendert bei 12. type-check 4/4, lint 5/5 mit weiterhin genau 53 Warnungen in web. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,275 @@
|
||||
import { cleanup, fireEvent, render, screen, within } from '@testing-library/react';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
/**
|
||||
* Mock next-intl mit einfacher `{param}`-Ersetzung — anders als das
|
||||
* schlichtere Muster in `widget-catalog-modal.test.tsx` (das Parameter
|
||||
* ignoriert), weil Test 8 unten die eingesetzte Reiterbezeichnung im
|
||||
* Loeschdialog prueft.
|
||||
*/
|
||||
vi.mock('next-intl', () => ({
|
||||
useTranslations: (namespace: string) => (key: string, params?: Record<string, string>) => {
|
||||
const translations: Record<string, Record<string, string>> = {
|
||||
widgets: {
|
||||
'tabs.navLabel': 'Dashboard-Reiter',
|
||||
'tabs.add': 'Dashboard hinzufügen',
|
||||
'tabs.renameButtonLabel': 'Dashboard umbenennen',
|
||||
'tabs.renameInputLabel': 'Name des Dashboards',
|
||||
'tabs.deleteButtonLabel': 'Dashboard löschen',
|
||||
'tabs.deleteDialogTitle': 'Dashboard löschen',
|
||||
'tabs.deleteDialogBody': 'Möchten Sie das Dashboard „{name}“ wirklich löschen?',
|
||||
},
|
||||
common: {
|
||||
cancel: 'Abbrechen',
|
||||
delete: 'Löschen',
|
||||
},
|
||||
};
|
||||
let text = translations[namespace]?.[key] ?? key;
|
||||
if (params) {
|
||||
for (const [k, v] of Object.entries(params)) {
|
||||
text = text.replace(`{${k}}`, v);
|
||||
}
|
||||
}
|
||||
return text;
|
||||
},
|
||||
}));
|
||||
|
||||
import { DashboardTabs } from './dashboard-tabs';
|
||||
|
||||
const DASHBOARDS = [
|
||||
{ id: 'd1', name: 'Dashboard', position: 0 },
|
||||
{ id: 'd2', name: 'Dashboard 2', position: 1 },
|
||||
];
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
describe('DashboardTabs (quick-260923-ad9, Task 3)', () => {
|
||||
it('Test 1: rendert jeden Reiter mit seinem Namen', () => {
|
||||
render(
|
||||
<DashboardTabs
|
||||
dashboards={DASHBOARDS}
|
||||
activeDashboardId="d1"
|
||||
isEditMode={false}
|
||||
onSelect={vi.fn()}
|
||||
onCreate={vi.fn()}
|
||||
onRename={vi.fn()}
|
||||
onDelete={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Dashboard' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Dashboard 2' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Test 2: ein Klick auf einen Reiter ruft onSelect mit dessen Kennung auf', () => {
|
||||
const onSelect = vi.fn();
|
||||
render(
|
||||
<DashboardTabs
|
||||
dashboards={DASHBOARDS}
|
||||
activeDashboardId="d1"
|
||||
isEditMode={false}
|
||||
onSelect={onSelect}
|
||||
onCreate={vi.fn()}
|
||||
onRename={vi.fn()}
|
||||
onDelete={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Dashboard 2' }));
|
||||
|
||||
expect(onSelect).toHaveBeenCalledWith('d2');
|
||||
});
|
||||
|
||||
it('Test 3: der aktive Reiter traegt aria-current, der andere nicht', () => {
|
||||
render(
|
||||
<DashboardTabs
|
||||
dashboards={DASHBOARDS}
|
||||
activeDashboardId="d1"
|
||||
isEditMode={false}
|
||||
onSelect={vi.fn()}
|
||||
onCreate={vi.fn()}
|
||||
onRename={vi.fn()}
|
||||
onDelete={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Dashboard' })).toHaveAttribute('aria-current', 'true');
|
||||
expect(screen.getByRole('button', { name: 'Dashboard 2' })).not.toHaveAttribute('aria-current');
|
||||
});
|
||||
|
||||
it('Test 4: ausserhalb des Bearbeitungsmodus gibt es weder Umbenennen- noch Loeschen- noch Hinzufuegen-Knoepfe', () => {
|
||||
render(
|
||||
<DashboardTabs
|
||||
dashboards={DASHBOARDS}
|
||||
activeDashboardId="d1"
|
||||
isEditMode={false}
|
||||
onSelect={vi.fn()}
|
||||
onCreate={vi.fn()}
|
||||
onRename={vi.fn()}
|
||||
onDelete={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByLabelText('Dashboard umbenennen')).not.toBeInTheDocument();
|
||||
expect(screen.queryByLabelText('Dashboard löschen')).not.toBeInTheDocument();
|
||||
expect(screen.queryByLabelText('Dashboard hinzufügen')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Test 5: im Bearbeitungsmodus mit mehreren Reitern sind Loeschen-Knoepfe und ein Hinzufuegen-Knopf da', () => {
|
||||
render(
|
||||
<DashboardTabs
|
||||
dashboards={DASHBOARDS}
|
||||
activeDashboardId="d1"
|
||||
isEditMode={true}
|
||||
onSelect={vi.fn()}
|
||||
onCreate={vi.fn()}
|
||||
onRename={vi.fn()}
|
||||
onDelete={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getAllByLabelText('Dashboard löschen')).toHaveLength(2);
|
||||
expect(screen.getByLabelText('Dashboard hinzufügen')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Test 6: beim letzten verbleibenden Reiter wird Loeschen gar nicht erst angeboten', () => {
|
||||
render(
|
||||
<DashboardTabs
|
||||
dashboards={[DASHBOARDS[0]]}
|
||||
activeDashboardId="d1"
|
||||
isEditMode={true}
|
||||
onSelect={vi.fn()}
|
||||
onCreate={vi.fn()}
|
||||
onRename={vi.fn()}
|
||||
onDelete={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByLabelText('Dashboard löschen')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Test 7: der Hinzufuegen-Knopf ruft onCreate auf', () => {
|
||||
const onCreate = vi.fn();
|
||||
render(
|
||||
<DashboardTabs
|
||||
dashboards={DASHBOARDS}
|
||||
activeDashboardId="d1"
|
||||
isEditMode={true}
|
||||
onSelect={vi.fn()}
|
||||
onCreate={onCreate}
|
||||
onRename={vi.fn()}
|
||||
onDelete={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByLabelText('Dashboard hinzufügen'));
|
||||
|
||||
expect(onCreate).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('Test 8: Umbenennen-Knopf des AKTIVEN Reiters verwandelt den Namen in ein Eingabefeld; Enter uebernimmt beschnitten', () => {
|
||||
const onRename = vi.fn();
|
||||
render(
|
||||
<DashboardTabs
|
||||
dashboards={DASHBOARDS}
|
||||
activeDashboardId="d1"
|
||||
isEditMode={true}
|
||||
onSelect={vi.fn()}
|
||||
onCreate={vi.fn()}
|
||||
onRename={onRename}
|
||||
onDelete={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByLabelText('Dashboard umbenennen'));
|
||||
const input = screen.getByLabelText('Name des Dashboards');
|
||||
fireEvent.change(input, { target: { value: ' Finanzen ' } });
|
||||
fireEvent.keyDown(input, { key: 'Enter' });
|
||||
|
||||
expect(onRename).toHaveBeenCalledWith('d1', 'Finanzen');
|
||||
});
|
||||
|
||||
it('Test 9: Escape verwirft die Umbenennung, ohne onRename aufzurufen', () => {
|
||||
const onRename = vi.fn();
|
||||
render(
|
||||
<DashboardTabs
|
||||
dashboards={DASHBOARDS}
|
||||
activeDashboardId="d1"
|
||||
isEditMode={true}
|
||||
onSelect={vi.fn()}
|
||||
onCreate={vi.fn()}
|
||||
onRename={onRename}
|
||||
onDelete={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByLabelText('Dashboard umbenennen'));
|
||||
const input = screen.getByLabelText('Name des Dashboards');
|
||||
fireEvent.change(input, { target: { value: 'Verworfen' } });
|
||||
fireEvent.keyDown(input, { key: 'Escape' });
|
||||
|
||||
expect(onRename).not.toHaveBeenCalled();
|
||||
expect(screen.getByRole('button', { name: 'Dashboard' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Test 10: der Umbenennen-Knopf steht nur beim AKTIVEN Reiter, nicht bei den anderen', () => {
|
||||
render(
|
||||
<DashboardTabs
|
||||
dashboards={DASHBOARDS}
|
||||
activeDashboardId="d1"
|
||||
isEditMode={true}
|
||||
onSelect={vi.fn()}
|
||||
onCreate={vi.fn()}
|
||||
onRename={vi.fn()}
|
||||
onDelete={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getAllByLabelText('Dashboard umbenennen')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('Test 11: Loeschen fragt mit dem Namen des Reiters zurueck; Bestaetigen ruft onDelete auf', () => {
|
||||
const onDelete = vi.fn();
|
||||
render(
|
||||
<DashboardTabs
|
||||
dashboards={DASHBOARDS}
|
||||
activeDashboardId="d1"
|
||||
isEditMode={true}
|
||||
onSelect={vi.fn()}
|
||||
onCreate={vi.fn()}
|
||||
onRename={vi.fn()}
|
||||
onDelete={onDelete}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getAllByLabelText('Dashboard löschen')[1]);
|
||||
const dialog = screen.getByRole('alertdialog');
|
||||
expect(dialog).toHaveTextContent('Dashboard 2');
|
||||
|
||||
fireEvent.click(within(dialog).getByRole('button', { name: 'Löschen' }));
|
||||
|
||||
expect(onDelete).toHaveBeenCalledWith('d2');
|
||||
});
|
||||
|
||||
it('Test 12: Abbrechen im Loeschdialog ruft onDelete NICHT auf', () => {
|
||||
const onDelete = vi.fn();
|
||||
render(
|
||||
<DashboardTabs
|
||||
dashboards={DASHBOARDS}
|
||||
activeDashboardId="d1"
|
||||
isEditMode={true}
|
||||
onSelect={vi.fn()}
|
||||
onCreate={vi.fn()}
|
||||
onRename={vi.fn()}
|
||||
onDelete={onDelete}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getAllByLabelText('Dashboard löschen')[0]);
|
||||
const dialog = screen.getByRole('alertdialog');
|
||||
fireEvent.click(within(dialog).getByRole('button', { name: 'Abbrechen' }));
|
||||
|
||||
expect(onDelete).not.toHaveBeenCalled();
|
||||
expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user