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:
@@ -1,17 +1,31 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
/**
|
||||
* dashboard-store.test — NEU (quick-260916-bwo, feineres Dashboard-Raster).
|
||||
* dashboard-store.test — quick-260916-bwo (Marker-Umrechnung) + erweitert um
|
||||
* quick-260923-ad9 (Task 3, Dashboard-Reiter).
|
||||
*
|
||||
* Sechs Tests fuer die Anbindung der einmaligen Umrechnung im Store:
|
||||
* `loadDashboard` rechnet alte Anordnungen um und speichert SOFORT mit
|
||||
* Marker; `saveLayout` traegt den Marker bei JEDEM Speichern (T-BWO-02 —
|
||||
* fehlt er, wuerde das naechste Laden erneut verdoppeln); der Zustand
|
||||
* selbst bleibt markerfrei (der Store iteriert mit Object.keys ueber die
|
||||
* Breakpoints); `addWidget` legt neue Eintraege in den verdoppelten
|
||||
* Vorgabegroessen an. Der Store hatte bisher keine Testdatei.
|
||||
* Sechs Tests (1-6) pinnen weiterhin die Anbindung der einmaligen
|
||||
* Umrechnung im Store — `loadDashboard`/`selectDashboard` rechnen alte
|
||||
* Anordnungen um und speichern SOFORT mit Marker; `saveLayout` traegt den
|
||||
* Marker bei JEDEM Speichern (T-BWO-02); der Zustand selbst bleibt
|
||||
* markerfrei; `addWidget` legt neue Eintraege in den verdoppelten
|
||||
* Vorgabegroessen an. Signaturen sind an die Reiter-Kennung angepasst
|
||||
* (`fetchLayout(dashboardId)`, `saveLayout(dashboardId, layouts)`,
|
||||
* `addWidget(dashboardId, widgetType)`), das gepruefte Verhalten ist
|
||||
* dasselbe.
|
||||
*
|
||||
* Ab Test 7: Reiter-Verwaltung — Laden holt zuerst die Reiterliste, macht
|
||||
* den ersten aktiv und laedt erst danach dessen Inhalt; ein Reiterwechsel
|
||||
* schreibt eine ungespeicherte Anordnung zuerst fuer den ALTEN Reiter;
|
||||
* Anlegen/Umbenennen/Loeschen pflegen die Reiterliste und den aktiven
|
||||
* Reiter.
|
||||
*/
|
||||
vi.mock('@/lib/dashboard-api', () => ({
|
||||
fetchDashboards: vi.fn(),
|
||||
createDashboardTab: vi.fn(),
|
||||
renameDashboardTab: vi.fn(),
|
||||
deleteDashboardTab: vi.fn(),
|
||||
reorderDashboardTabs: vi.fn(),
|
||||
fetchLayout: vi.fn(),
|
||||
fetchWidgets: vi.fn(),
|
||||
saveLayout: vi.fn(),
|
||||
@@ -25,9 +39,14 @@ import * as api from '@/lib/dashboard-api';
|
||||
const { useDashboardStore } = await import('./dashboard-store');
|
||||
|
||||
const EMPTY = { lg: [], md: [], sm: [], xs: [], xxs: [] };
|
||||
const DASH_1 = { id: 'dash-1', name: 'Dashboard', position: 0 };
|
||||
const DASH_2 = { id: 'dash-2', name: 'Dashboard 2', position: 1 };
|
||||
|
||||
beforeEach(() => {
|
||||
useDashboardStore.setState({
|
||||
dashboards: [],
|
||||
activeDashboardId: null,
|
||||
isSwitchingDashboard: false,
|
||||
layouts: { lg: [], md: [], sm: [], xs: [], xxs: [] },
|
||||
widgets: [],
|
||||
isEditMode: false,
|
||||
@@ -36,7 +55,9 @@ beforeEach(() => {
|
||||
error: null,
|
||||
});
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(api.fetchDashboards).mockResolvedValue([DASH_1]);
|
||||
vi.mocked(api.fetchWidgets).mockResolvedValue([]);
|
||||
vi.mocked(api.fetchLayout).mockResolvedValue({ ...EMPTY });
|
||||
vi.mocked(api.saveLayout).mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
@@ -45,7 +66,7 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe('dashboard-store — einmalige Umrechnung mit Marker (quick-260916-bwo)', () => {
|
||||
it('Test 1: alte Anordnung wird beim Laden umgerechnet und SOFORT mit Marker gespeichert', async () => {
|
||||
it('Test 1: alte Anordnung wird beim Laden umgerechnet und SOFORT fuer den ersten Reiter mit Marker gespeichert', async () => {
|
||||
vi.mocked(api.fetchLayout).mockResolvedValue({
|
||||
lg: [{ i: 'a', x: 1, y: 1, w: 2, h: 2 }], md: [], sm: [], xs: [], xxs: [],
|
||||
});
|
||||
@@ -57,6 +78,7 @@ describe('dashboard-store — einmalige Umrechnung mit Marker (quick-260916-bwo)
|
||||
expect(Object.keys(state.layouts)).not.toContain('__gridVersion');
|
||||
expect(api.saveLayout).toHaveBeenCalledTimes(1);
|
||||
expect(api.saveLayout).toHaveBeenCalledWith(
|
||||
'dash-1',
|
||||
expect.objectContaining({ __gridVersion: 2, lg: [{ i: 'a', x: 2, y: 2, w: 4, h: 4 }] }),
|
||||
);
|
||||
expect(state.isDirty).toBe(false);
|
||||
@@ -86,7 +108,10 @@ describe('dashboard-store — einmalige Umrechnung mit Marker (quick-260916-bwo)
|
||||
expect(useDashboardStore.getState().layouts).toEqual(EMPTY);
|
||||
});
|
||||
|
||||
it('Test 4: jedes Speichern traegt den Marker (T-BWO-02), der Zustand bleibt markerfrei', async () => {
|
||||
it('Test 4: jedes Speichern traegt den Marker (T-BWO-02) unter der aktiven Reiter-Kennung, der Zustand bleibt markerfrei', async () => {
|
||||
await useDashboardStore.getState().loadDashboard();
|
||||
vi.mocked(api.saveLayout).mockClear();
|
||||
|
||||
const layouts = { lg: [{ i: 'a', x: 2, y: 2, w: 4, h: 4 }], md: [], sm: [], xs: [], xxs: [] };
|
||||
useDashboardStore.getState().updateLayouts(layouts);
|
||||
expect(useDashboardStore.getState().isDirty).toBe(true);
|
||||
@@ -94,7 +119,10 @@ describe('dashboard-store — einmalige Umrechnung mit Marker (quick-260916-bwo)
|
||||
await useDashboardStore.getState().saveLayout();
|
||||
|
||||
expect(api.saveLayout).toHaveBeenCalledTimes(1);
|
||||
expect(api.saveLayout).toHaveBeenCalledWith(expect.objectContaining({ __gridVersion: 2, ...layouts }));
|
||||
expect(api.saveLayout).toHaveBeenCalledWith(
|
||||
'dash-1',
|
||||
expect.objectContaining({ __gridVersion: 2, ...layouts }),
|
||||
);
|
||||
expect(useDashboardStore.getState().isDirty).toBe(false);
|
||||
expect(Object.keys(useDashboardStore.getState().layouts)).not.toContain('__gridVersion');
|
||||
});
|
||||
@@ -115,13 +143,148 @@ describe('dashboard-store — einmalige Umrechnung mit Marker (quick-260916-bwo)
|
||||
expect(errorSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('Test 6: neues Widget wird in den neuen (verdoppelten) Vorgabegroessen angelegt', async () => {
|
||||
it('Test 6: neues Widget wird in den neuen (verdoppelten) Vorgabegroessen fuer den aktiven Reiter angelegt', async () => {
|
||||
await useDashboardStore.getState().loadDashboard();
|
||||
vi.mocked(api.addWidget).mockResolvedValue({ id: 'n1', widgetType: 'clock', config: {} });
|
||||
|
||||
await useDashboardStore.getState().addWidget('clock');
|
||||
|
||||
const state = useDashboardStore.getState();
|
||||
expect(api.addWidget).toHaveBeenCalledWith('dash-1', 'clock');
|
||||
expect(state.layouts.lg).toContainEqual({ i: 'n1', x: 0, y: 0, w: 4, h: 4 });
|
||||
expect(state.isDirty).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('dashboard-store — Reiter laden (quick-260923-ad9, Task 3)', () => {
|
||||
it('Test 7: holt zuerst die Reiterliste, macht den ERSTEN aktiv und laedt erst danach dessen Kacheln und Anordnung', async () => {
|
||||
vi.mocked(api.fetchDashboards).mockResolvedValue([DASH_1, DASH_2]);
|
||||
const callOrder: string[] = [];
|
||||
vi.mocked(api.fetchDashboards).mockImplementation(async () => {
|
||||
callOrder.push('tabs');
|
||||
return [DASH_1, DASH_2];
|
||||
});
|
||||
vi.mocked(api.fetchLayout).mockImplementation(async () => {
|
||||
callOrder.push('layout');
|
||||
return { ...EMPTY };
|
||||
});
|
||||
vi.mocked(api.fetchWidgets).mockImplementation(async () => {
|
||||
callOrder.push('widgets');
|
||||
return [];
|
||||
});
|
||||
|
||||
await useDashboardStore.getState().loadDashboard();
|
||||
|
||||
expect(callOrder[0]).toBe('tabs');
|
||||
expect(callOrder.slice(1).sort()).toEqual(['layout', 'widgets']);
|
||||
expect(api.fetchLayout).toHaveBeenCalledWith('dash-1');
|
||||
expect(api.fetchWidgets).toHaveBeenCalledWith('dash-1');
|
||||
expect(useDashboardStore.getState().activeDashboardId).toBe('dash-1');
|
||||
expect(useDashboardStore.getState().dashboards).toEqual([DASH_1, DASH_2]);
|
||||
});
|
||||
|
||||
it('Test 8: zweimaliges Aufrufen von loadDashboard hintereinander loest nur EINEN Abruf der Reiterliste aus', async () => {
|
||||
const first = useDashboardStore.getState().loadDashboard();
|
||||
const second = useDashboardStore.getState().loadDashboard();
|
||||
|
||||
await Promise.all([first, second]);
|
||||
|
||||
expect(api.fetchDashboards).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('dashboard-store — Reiterwechsel (quick-260923-ad9, Task 3)', () => {
|
||||
it('Test 9: ein Wechsel mit ungespeicherter Anordnung schreibt die Anordnung ZUERST fuer den ALTEN Reiter', async () => {
|
||||
vi.mocked(api.fetchDashboards).mockResolvedValue([DASH_1, DASH_2]);
|
||||
await useDashboardStore.getState().loadDashboard();
|
||||
useDashboardStore.getState().updateLayouts({ lg: [{ i: 'x', x: 0, y: 0, w: 1, h: 1 }], md: [], sm: [], xs: [], xxs: [] });
|
||||
vi.mocked(api.saveLayout).mockClear();
|
||||
vi.mocked(api.fetchLayout).mockResolvedValue({ ...EMPTY });
|
||||
vi.mocked(api.fetchWidgets).mockResolvedValue([]);
|
||||
|
||||
await useDashboardStore.getState().selectDashboard('dash-2');
|
||||
|
||||
expect(api.saveLayout).toHaveBeenCalledWith('dash-1', expect.anything());
|
||||
expect(useDashboardStore.getState().activeDashboardId).toBe('dash-2');
|
||||
});
|
||||
|
||||
it('Test 10: nach dem Wechsel stehen im Zustand AUSSCHLIESSLICH Kacheln und Anordnung des neuen Reiters', async () => {
|
||||
vi.mocked(api.fetchDashboards).mockResolvedValue([DASH_1, DASH_2]);
|
||||
await useDashboardStore.getState().loadDashboard();
|
||||
|
||||
vi.mocked(api.fetchLayout).mockResolvedValue({
|
||||
lg: [{ i: 'only-on-dash-2', x: 0, y: 0, w: 2, h: 2 }], md: [], sm: [], xs: [], xxs: [], __gridVersion: 2,
|
||||
});
|
||||
vi.mocked(api.fetchWidgets).mockResolvedValue([{ id: 'w-on-dash-2', widgetType: 'clock', config: {} }]);
|
||||
|
||||
await useDashboardStore.getState().selectDashboard('dash-2');
|
||||
|
||||
const state = useDashboardStore.getState();
|
||||
expect(state.widgets).toEqual([{ id: 'w-on-dash-2', widgetType: 'clock', config: {} }]);
|
||||
expect(state.layouts.lg).toEqual([{ i: 'only-on-dash-2', x: 0, y: 0, w: 2, h: 2 }]);
|
||||
});
|
||||
|
||||
it('Test 11: ein Wechsel auf den bereits aktiven Reiter tut nichts', async () => {
|
||||
await useDashboardStore.getState().loadDashboard();
|
||||
vi.mocked(api.fetchLayout).mockClear();
|
||||
vi.mocked(api.fetchWidgets).mockClear();
|
||||
|
||||
await useDashboardStore.getState().selectDashboard('dash-1');
|
||||
|
||||
expect(api.fetchLayout).not.toHaveBeenCalled();
|
||||
expect(api.fetchWidgets).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('dashboard-store — Reiter anlegen/umbenennen/loeschen (quick-260923-ad9, Task 3)', () => {
|
||||
it('Test 12: createDashboard haengt den neuen Reiter hinten an, macht ihn aktiv, die Kachelflaeche ist leer', async () => {
|
||||
await useDashboardStore.getState().loadDashboard();
|
||||
vi.mocked(api.createDashboardTab).mockResolvedValue(DASH_2);
|
||||
vi.mocked(api.fetchLayout).mockResolvedValue({ ...EMPTY });
|
||||
vi.mocked(api.fetchWidgets).mockResolvedValue([]);
|
||||
|
||||
await useDashboardStore.getState().createDashboard();
|
||||
|
||||
const state = useDashboardStore.getState();
|
||||
expect(state.dashboards.map((d) => d.id)).toEqual(['dash-1', 'dash-2']);
|
||||
expect(state.activeDashboardId).toBe('dash-2');
|
||||
expect(state.widgets).toEqual([]);
|
||||
});
|
||||
|
||||
it('Test 13: renameDashboard aktualisiert den Namen in der Reiterliste', async () => {
|
||||
await useDashboardStore.getState().loadDashboard();
|
||||
vi.mocked(api.renameDashboardTab).mockResolvedValue({ id: 'dash-1', name: 'Finanzen', position: 0 });
|
||||
|
||||
await useDashboardStore.getState().renameDashboard('dash-1', 'Finanzen');
|
||||
|
||||
expect(useDashboardStore.getState().dashboards[0].name).toBe('Finanzen');
|
||||
});
|
||||
|
||||
it('Test 14: deleteDashboard des AKTIVEN Reiters macht den dann ersten verbleibenden Reiter aktiv und laedt dessen Inhalt', async () => {
|
||||
vi.mocked(api.fetchDashboards).mockResolvedValue([DASH_1, DASH_2]);
|
||||
await useDashboardStore.getState().loadDashboard();
|
||||
vi.mocked(api.deleteDashboardTab).mockResolvedValue(undefined);
|
||||
vi.mocked(api.fetchLayout).mockResolvedValue({ ...EMPTY });
|
||||
vi.mocked(api.fetchWidgets).mockResolvedValue([{ id: 'w-on-dash-2', widgetType: 'clock', config: {} }]);
|
||||
|
||||
await useDashboardStore.getState().deleteDashboard('dash-1');
|
||||
|
||||
const state = useDashboardStore.getState();
|
||||
expect(state.dashboards.map((d) => d.id)).toEqual(['dash-2']);
|
||||
expect(state.activeDashboardId).toBe('dash-2');
|
||||
expect(state.widgets).toEqual([{ id: 'w-on-dash-2', widgetType: 'clock', config: {} }]);
|
||||
});
|
||||
|
||||
it('Test 15: deleteDashboard eines INAKTIVEN Reiters aendert den aktiven Reiter nicht', async () => {
|
||||
vi.mocked(api.fetchDashboards).mockResolvedValue([DASH_1, DASH_2]);
|
||||
await useDashboardStore.getState().loadDashboard();
|
||||
vi.mocked(api.deleteDashboardTab).mockResolvedValue(undefined);
|
||||
vi.mocked(api.fetchLayout).mockClear();
|
||||
|
||||
await useDashboardStore.getState().deleteDashboard('dash-2');
|
||||
|
||||
expect(useDashboardStore.getState().activeDashboardId).toBe('dash-1');
|
||||
expect(useDashboardStore.getState().dashboards.map((d) => d.id)).toEqual(['dash-1']);
|
||||
expect(api.fetchLayout).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user