feat(260924-h7x): Dashboard-Reiter als Umschalter in der Kopfzeile
- Kopfzeile bekommt Einhaengepunkt header-center-slot; auf / entfaellt der Text Startseite - DashboardTabs rendert per createPortal dorthin, eigene Zeile ueber dem Raster entfaellt - eingelassene Spur mit erhabenem aktivem Reiter, waagrecht scrollbar mit weicher Randausblendung - role=tablist/tab, Pfeiltasten/Pos1/Ende, sichtbarer Fokusring, aktiver Reiter wird ins Bild gescrollt - Plus-Knopf ausserhalb der Spur, Loeschdialog am Dokumentkoerper, Ziehhinweis als sr-only/Tooltip Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { cleanup, fireEvent, render, screen, within } from '@testing-library/react';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
/**
|
||||
* Mock next-intl mit einfacher `{param}`-Ersetzung — anders als das
|
||||
@@ -90,8 +90,17 @@ function defaultProps(overrides: Partial<Parameters<typeof DashboardTabs>[0]> =
|
||||
};
|
||||
}
|
||||
|
||||
// Seit quick-260924-h7x rendert die Leiste per Portal in den Einhaengepunkt
|
||||
// der Kopfzeile — der Test legt ihn so an, wie `header.tsx` ihn rendert.
|
||||
beforeEach(() => {
|
||||
const slot = document.createElement('div');
|
||||
slot.id = 'header-center-slot';
|
||||
document.body.appendChild(slot);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
document.getElementById('header-center-slot')?.remove();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
@@ -99,24 +108,27 @@ describe('DashboardTabs (quick-260923-ad9, Task 3)', () => {
|
||||
it('Test 1: rendert jeden Reiter mit seinem Namen', () => {
|
||||
render(<DashboardTabs {...defaultProps()} />);
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Dashboard' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Dashboard 2' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('tab', { name: 'Dashboard' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('tab', { name: 'Dashboard 2' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Test 2: ein Klick auf einen Reiter ruft onSelect mit dessen Kennung auf', () => {
|
||||
const onSelect = vi.fn();
|
||||
render(<DashboardTabs {...defaultProps({ onSelect })} />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Dashboard 2' }));
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'Dashboard 2' }));
|
||||
|
||||
expect(onSelect).toHaveBeenCalledWith('d2');
|
||||
});
|
||||
|
||||
it('Test 3: der aktive Reiter traegt aria-current, der andere nicht', () => {
|
||||
it('Test 3: der aktive Reiter traegt aria-selected=true, der andere false', () => {
|
||||
render(<DashboardTabs {...defaultProps()} />);
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Dashboard' })).toHaveAttribute('aria-current', 'true');
|
||||
expect(screen.getByRole('button', { name: 'Dashboard 2' })).not.toHaveAttribute('aria-current');
|
||||
expect(screen.getByRole('tab', { name: 'Dashboard' })).toHaveAttribute('aria-selected', 'true');
|
||||
expect(screen.getByRole('tab', { name: 'Dashboard 2' })).toHaveAttribute(
|
||||
'aria-selected',
|
||||
'false',
|
||||
);
|
||||
});
|
||||
|
||||
it('Test 4: ausserhalb des Bearbeitungsmodus gibt es weder Umbenennen- noch Loeschen- noch Hinzufuegen-Knoepfe', () => {
|
||||
@@ -171,7 +183,7 @@ describe('DashboardTabs (quick-260923-ad9, Task 3)', () => {
|
||||
fireEvent.keyDown(input, { key: 'Escape' });
|
||||
|
||||
expect(onRename).not.toHaveBeenCalled();
|
||||
expect(screen.getByRole('button', { name: 'Dashboard' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('tab', { name: 'Dashboard' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Test 10: der Umbenennen-Knopf steht nur beim AKTIVEN Reiter, nicht bei den anderen', () => {
|
||||
@@ -219,7 +231,7 @@ describe('DashboardTabs — Ziehen zum Umsortieren (quick-260923-ad9, Task 4)',
|
||||
|
||||
expect(onReorder).not.toHaveBeenCalled();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Dashboard 2' }));
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'Dashboard 2' }));
|
||||
expect(onSelect).toHaveBeenCalledWith('d2');
|
||||
});
|
||||
|
||||
@@ -261,14 +273,16 @@ describe('DashboardTabs — Ziehen zum Umsortieren (quick-260923-ad9, Task 4)',
|
||||
fireEvent.pointerMove(tab, { clientX: 180, pointerId: 1 });
|
||||
|
||||
// Vorschau: d2 steht jetzt vor d1 im DOM.
|
||||
const buttons = screen.getAllByRole('button').filter((b) => b.textContent === 'Dashboard' || b.textContent === 'Dashboard 2');
|
||||
const buttons = screen
|
||||
.getAllByRole('tab')
|
||||
.filter((b) => b.textContent === 'Dashboard' || b.textContent === 'Dashboard 2');
|
||||
expect(buttons.map((b) => b.textContent)).toEqual(['Dashboard 2', 'Dashboard']);
|
||||
|
||||
fireEvent.pointerCancel(tab, { pointerId: 1 });
|
||||
|
||||
expect(onReorder).not.toHaveBeenCalled();
|
||||
const buttonsAfterCancel = screen
|
||||
.getAllByRole('button')
|
||||
.getAllByRole('tab')
|
||||
.filter((b) => b.textContent === 'Dashboard' || b.textContent === 'Dashboard 2');
|
||||
expect(buttonsAfterCancel.map((b) => b.textContent)).toEqual(['Dashboard', 'Dashboard 2']);
|
||||
});
|
||||
@@ -289,13 +303,17 @@ describe('DashboardTabs — Ziehen zum Umsortieren (quick-260923-ad9, Task 4)',
|
||||
|
||||
it('Test 18: der aktive Reiter bleibt beim Ziehen aktiv, auch wenn er seine Position wechselt', () => {
|
||||
stubTabRects();
|
||||
render(<DashboardTabs {...defaultProps({ dashboards: DASHBOARDS, activeDashboardId: 'd1', onReorder: vi.fn() })} />);
|
||||
render(
|
||||
<DashboardTabs
|
||||
{...defaultProps({ dashboards: DASHBOARDS, activeDashboardId: 'd1', onReorder: vi.fn() })}
|
||||
/>,
|
||||
);
|
||||
const tab = screen.getByTestId('dashboard-tab-d1');
|
||||
|
||||
fireEvent.pointerDown(tab, { clientX: 10, pointerId: 1, button: 0 });
|
||||
fireEvent.pointerMove(tab, { clientX: 180, pointerId: 1 });
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Dashboard' })).toHaveAttribute('aria-current', 'true');
|
||||
expect(screen.getByRole('tab', { name: 'Dashboard' })).toHaveAttribute('aria-selected', 'true');
|
||||
});
|
||||
|
||||
it('Test 19: nach einem echten Ziehen unterdrueckt der naechste Klick die Auswahl EINMAL, danach funktioniert Klicken wieder normal', () => {
|
||||
@@ -311,16 +329,18 @@ describe('DashboardTabs — Ziehen zum Umsortieren (quick-260923-ad9, Task 4)',
|
||||
expect(onReorder).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Ein echter Browser wuerde nach dem Ziehen noch einen "click" nachreichen.
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Dashboard' }));
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'Dashboard' }));
|
||||
expect(onSelect).not.toHaveBeenCalled();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Dashboard' }));
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'Dashboard' }));
|
||||
expect(onSelect).toHaveBeenCalledWith('d1');
|
||||
});
|
||||
|
||||
it('Test 20: der Hinweistext zum Ziehen steht bei mehr als einem Reiter, aber nicht bei genau einem', () => {
|
||||
const { rerender } = render(<DashboardTabs {...defaultProps({ dashboards: DASHBOARDS })} />);
|
||||
expect(screen.getByText('Ziehen Sie einen Reiter, um ihn zum Standard zu machen.')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('Ziehen Sie einen Reiter, um ihn zum Standard zu machen.'),
|
||||
).toBeInTheDocument();
|
||||
|
||||
rerender(<DashboardTabs {...defaultProps({ dashboards: [DASHBOARDS[0]] })} />);
|
||||
expect(
|
||||
@@ -331,7 +351,9 @@ describe('DashboardTabs — Ziehen zum Umsortieren (quick-260923-ad9, Task 4)',
|
||||
it('Test 21: Ziehen ist auch AUSSERHALB des Bearbeitungsmodus moeglich (D-09)', () => {
|
||||
stubTabRects();
|
||||
const onReorder = vi.fn();
|
||||
render(<DashboardTabs {...defaultProps({ dashboards: DASHBOARDS, isEditMode: false, onReorder })} />);
|
||||
render(
|
||||
<DashboardTabs {...defaultProps({ dashboards: DASHBOARDS, isEditMode: false, onReorder })} />,
|
||||
);
|
||||
const tab = screen.getByTestId('dashboard-tab-d1');
|
||||
|
||||
fireEvent.pointerDown(tab, { clientX: 10, pointerId: 1, button: 0 });
|
||||
@@ -341,3 +363,72 @@ describe('DashboardTabs — Ziehen zum Umsortieren (quick-260923-ad9, Task 4)',
|
||||
expect(onReorder).toHaveBeenCalledWith(['d2', 'd1']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DashboardTabs — in der Kopfzeile (quick-260924-h7x)', () => {
|
||||
it('Test 22: rendert in den Einhaengepunkt der Kopfzeile, nicht an Ort und Stelle', () => {
|
||||
const { container } = render(<DashboardTabs {...defaultProps()} />);
|
||||
|
||||
const slot = document.getElementById('header-center-slot') as HTMLElement;
|
||||
expect(within(slot).getByRole('navigation', { name: 'Dashboard-Reiter' })).toBeInTheDocument();
|
||||
expect(within(slot).getByRole('tablist')).toBeInTheDocument();
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('Test 23: ohne Einhaengepunkt rendert die Leiste gar nichts', () => {
|
||||
document.getElementById('header-center-slot')?.remove();
|
||||
render(<DashboardTabs {...defaultProps()} />);
|
||||
|
||||
expect(screen.queryByRole('tablist')).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('tab')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Test 24: Pfeiltasten wandern zwischen den Reitern (mit Umlauf), ohne zu waehlen; nur der aktive ist per Tab erreichbar', () => {
|
||||
const onSelect = vi.fn();
|
||||
render(<DashboardTabs {...defaultProps({ dashboards: THREE_DASHBOARDS, onSelect })} />);
|
||||
const [t1, t2, t3] = screen.getAllByRole('tab');
|
||||
|
||||
expect(t1).toHaveAttribute('tabindex', '0');
|
||||
expect(t2).toHaveAttribute('tabindex', '-1');
|
||||
expect(t3).toHaveAttribute('tabindex', '-1');
|
||||
|
||||
t1.focus();
|
||||
fireEvent.keyDown(t1, { key: 'ArrowRight' });
|
||||
expect(t2).toHaveFocus();
|
||||
fireEvent.keyDown(t2, { key: 'ArrowRight' });
|
||||
expect(t3).toHaveFocus();
|
||||
fireEvent.keyDown(t3, { key: 'ArrowRight' });
|
||||
expect(t1).toHaveFocus();
|
||||
fireEvent.keyDown(t1, { key: 'ArrowLeft' });
|
||||
expect(t3).toHaveFocus();
|
||||
fireEvent.keyDown(t3, { key: 'Home' });
|
||||
expect(t1).toHaveFocus();
|
||||
fireEvent.keyDown(t1, { key: 'End' });
|
||||
expect(t3).toHaveFocus();
|
||||
|
||||
expect(onSelect).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Test 25: genau ein Reiter — der Umschalter steht trotzdem, im Bearbeitungsmodus nur mit „+“ daneben', () => {
|
||||
render(<DashboardTabs {...defaultProps({ dashboards: [DASHBOARDS[0]], isEditMode: true })} />);
|
||||
|
||||
expect(screen.getByRole('tab', { name: 'Dashboard' })).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Dashboard hinzufügen')).toBeInTheDocument();
|
||||
expect(screen.queryByLabelText('Dashboard löschen')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Test 26: „+“ sitzt ausserhalb der scrollenden Spur und bleibt so immer sichtbar', () => {
|
||||
render(<DashboardTabs {...defaultProps({ isEditMode: true })} />);
|
||||
|
||||
const tablist = screen.getByRole('tablist');
|
||||
expect(tablist).not.toContainElement(screen.getByLabelText('Dashboard hinzufügen'));
|
||||
});
|
||||
|
||||
it('Test 27: der Loeschdialog haengt am Dokumentkoerper, nicht in der Kopfzeile', () => {
|
||||
render(<DashboardTabs {...defaultProps({ isEditMode: true })} />);
|
||||
|
||||
fireEvent.click(screen.getAllByLabelText('Dashboard löschen')[0]);
|
||||
const dialog = screen.getByRole('alertdialog');
|
||||
const slot = document.getElementById('header-center-slot') as HTMLElement;
|
||||
expect(slot).not.toContainElement(dialog);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user