fix(custom-modules): null beim Aendern ablehnen, Ladefehler statt 404, Fehlertexte, Seitenleiste eingeklappt
- PATCH mit null fuer name/url/category ergibt 400 statt 500 - Modulansicht unterscheidet Ladefehler von "nicht gefunden" - Formular/Loeschdialog nennen 403 und 400 eigens - eingeklappte Seitenleiste folgt der Gruppenreihenfolge der ausgeklappten - neue Eintraege sind mit "Eigene Module" vorbelegt - Verwaltung zeigt bei Ladefehler nicht zusaetzlich "keine Eintraege" Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,8 @@ const messages: Record<string, Record<string, unknown>> = {
|
||||
urlCredentials: 'Die Adresse darf keinen Benutzernamen und kein Kennwort enthalten.',
|
||||
saveError: 'Das Modul konnte nicht gespeichert werden.',
|
||||
deleteError: 'Das Modul konnte nicht gelöscht werden.',
|
||||
forbidden: 'Sie haben keine Berechtigung für diese Aktion.',
|
||||
invalidInput: 'Die Angaben wurden nicht angenommen. Bitte prüfen Sie Name, Adresse und Kategorie.',
|
||||
deleteConfirm: {
|
||||
title: 'Eigenes Modul löschen',
|
||||
bodyShared: 'Möchten Sie „{name}“ wirklich löschen? Für alle Benutzer.',
|
||||
@@ -58,8 +60,18 @@ vi.mock('next-intl', () => ({
|
||||
}));
|
||||
|
||||
vi.mock('next/link', () => ({
|
||||
default: ({ children, href, className }: { children: React.ReactNode; href: string; className?: string }) => (
|
||||
<a href={href} className={className}>{children}</a>
|
||||
default: ({
|
||||
children,
|
||||
href,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
href: string;
|
||||
className?: string;
|
||||
}) => (
|
||||
<a href={href} className={className}>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
}));
|
||||
|
||||
@@ -68,13 +80,7 @@ vi.mock('@/lib/use-category-label', () => ({
|
||||
({ infrastructure: 'Infrastruktur', 'security-tools': 'Sicherheit' })[slug] ?? slug,
|
||||
}));
|
||||
|
||||
const {
|
||||
mockList,
|
||||
mockCreate,
|
||||
mockUpdate,
|
||||
mockDelete,
|
||||
mockBump,
|
||||
} = vi.hoisted(() => ({
|
||||
const { mockList, mockCreate, mockUpdate, mockDelete, mockBump } = vi.hoisted(() => ({
|
||||
mockList: vi.fn(),
|
||||
mockCreate: vi.fn(),
|
||||
mockUpdate: vi.fn(),
|
||||
@@ -106,6 +112,7 @@ vi.mock('@/lib/stores/auth-store', () => ({
|
||||
selector({ user: { role: mockRole } }),
|
||||
}));
|
||||
|
||||
import { CustomModuleRequestError } from '@/lib/custom-modules-api';
|
||||
import AdminCustomModulesPage from './page';
|
||||
|
||||
const wiki = {
|
||||
@@ -136,7 +143,9 @@ describe('AdminCustomModulesPage', () => {
|
||||
render(<AdminCustomModulesPage />);
|
||||
expect(await screen.findByText('Noch keine gemeinsamen Einträge')).toBeInTheDocument();
|
||||
expect(screen.getByText('Legen Sie Ihren ersten gemeinsamen Eintrag an.')).toBeInTheDocument();
|
||||
expect(screen.getAllByRole('button', { name: 'Gemeinsamen Eintrag anlegen' }).length).toBeGreaterThan(0);
|
||||
expect(
|
||||
screen.getAllByRole('button', { name: 'Gemeinsamen Eintrag anlegen' }).length,
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('zeigt mit Einträgen die Tabelle mit Link, Adresse und Kategorie-Anzeigename', async () => {
|
||||
@@ -154,13 +163,17 @@ describe('AdminCustomModulesPage', () => {
|
||||
it('Anlegen: http-Adresse zeigt die Meldung und ruft createCustomModule nicht auf', async () => {
|
||||
render(<AdminCustomModulesPage />);
|
||||
await screen.findByText('Noch keine gemeinsamen Einträge');
|
||||
await userEvent.click(screen.getAllByRole('button', { name: 'Gemeinsamen Eintrag anlegen' })[0]);
|
||||
await userEvent.click(
|
||||
screen.getAllByRole('button', { name: 'Gemeinsamen Eintrag anlegen' })[0],
|
||||
);
|
||||
|
||||
await userEvent.type(screen.getByLabelText('Name'), 'Beispielseite');
|
||||
await userEvent.type(screen.getByLabelText('Adresse'), 'http://example.com');
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Speichern' }));
|
||||
|
||||
expect(await screen.findByText('Bitte geben Sie eine Adresse ein, die mit https:// beginnt.')).toBeInTheDocument();
|
||||
expect(
|
||||
await screen.findByText('Bitte geben Sie eine Adresse ein, die mit https:// beginnt.'),
|
||||
).toBeInTheDocument();
|
||||
expect(mockCreate).not.toHaveBeenCalled();
|
||||
expect(mockBump).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -168,7 +181,9 @@ describe('AdminCustomModulesPage', () => {
|
||||
it('Anlegen: Adresse mit Zugangsdaten zeigt die passende Meldung', async () => {
|
||||
render(<AdminCustomModulesPage />);
|
||||
await screen.findByText('Noch keine gemeinsamen Einträge');
|
||||
await userEvent.click(screen.getAllByRole('button', { name: 'Gemeinsamen Eintrag anlegen' })[0]);
|
||||
await userEvent.click(
|
||||
screen.getAllByRole('button', { name: 'Gemeinsamen Eintrag anlegen' })[0],
|
||||
);
|
||||
|
||||
await userEvent.type(screen.getByLabelText('Name'), 'Beispielseite');
|
||||
await userEvent.type(screen.getByLabelText('Adresse'), 'https://user:pw@example.com');
|
||||
@@ -183,7 +198,9 @@ describe('AdminCustomModulesPage', () => {
|
||||
it('Anlegen: leerer Name zeigt die Meldung', async () => {
|
||||
render(<AdminCustomModulesPage />);
|
||||
await screen.findByText('Noch keine gemeinsamen Einträge');
|
||||
await userEvent.click(screen.getAllByRole('button', { name: 'Gemeinsamen Eintrag anlegen' })[0]);
|
||||
await userEvent.click(
|
||||
screen.getAllByRole('button', { name: 'Gemeinsamen Eintrag anlegen' })[0],
|
||||
);
|
||||
|
||||
await userEvent.type(screen.getByLabelText('Name'), ' ');
|
||||
await userEvent.type(screen.getByLabelText('Adresse'), 'https://example.com');
|
||||
@@ -196,7 +213,9 @@ describe('AdminCustomModulesPage', () => {
|
||||
it('Anlegen: gültige Eingabe ruft create mit getrimmtem Namen, lädt neu und aktualisiert die Seitenleiste genau einmal', async () => {
|
||||
render(<AdminCustomModulesPage />);
|
||||
await screen.findByText('Noch keine gemeinsamen Einträge');
|
||||
await userEvent.click(screen.getAllByRole('button', { name: 'Gemeinsamen Eintrag anlegen' })[0]);
|
||||
await userEvent.click(
|
||||
screen.getAllByRole('button', { name: 'Gemeinsamen Eintrag anlegen' })[0],
|
||||
);
|
||||
|
||||
await userEvent.type(screen.getByLabelText('Name'), ' Beispielseite ');
|
||||
await userEvent.type(screen.getByLabelText('Adresse'), 'https://example.com');
|
||||
@@ -217,11 +236,62 @@ describe('AdminCustomModulesPage', () => {
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Anlegen: die Kategorie ist vorbelegt mit Infrastruktur', async () => {
|
||||
it('Anlegen: die Kategorie ist vorbelegt mit „Eigene Module“', async () => {
|
||||
render(<AdminCustomModulesPage />);
|
||||
await screen.findByText('Noch keine gemeinsamen Einträge');
|
||||
await userEvent.click(screen.getAllByRole('button', { name: 'Gemeinsamen Eintrag anlegen' })[0]);
|
||||
expect(screen.getByLabelText('Kategorie')).toHaveValue('infrastructure');
|
||||
await userEvent.click(
|
||||
screen.getAllByRole('button', { name: 'Gemeinsamen Eintrag anlegen' })[0],
|
||||
);
|
||||
expect(screen.getByLabelText('Kategorie')).toHaveValue('custom-modules');
|
||||
});
|
||||
|
||||
it('Anlegen ohne Kategoriewahl sendet die Kategorie „Eigene Module“', async () => {
|
||||
render(<AdminCustomModulesPage />);
|
||||
await screen.findByText('Noch keine gemeinsamen Einträge');
|
||||
await userEvent.click(
|
||||
screen.getAllByRole('button', { name: 'Gemeinsamen Eintrag anlegen' })[0],
|
||||
);
|
||||
await userEvent.type(screen.getByLabelText('Name'), 'Beispielseite');
|
||||
await userEvent.type(screen.getByLabelText('Adresse'), 'https://example.com');
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Speichern' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockCreate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ category: 'custom-modules' }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('Speichern mit 403 zeigt „keine Berechtigung“, nicht den allgemeinen Fehler', async () => {
|
||||
mockCreate.mockRejectedValue(new CustomModuleRequestError(403, 'Forbidden'));
|
||||
render(<AdminCustomModulesPage />);
|
||||
await screen.findByText('Noch keine gemeinsamen Einträge');
|
||||
await userEvent.click(
|
||||
screen.getAllByRole('button', { name: 'Gemeinsamen Eintrag anlegen' })[0],
|
||||
);
|
||||
await userEvent.type(screen.getByLabelText('Name'), 'Beispielseite');
|
||||
await userEvent.type(screen.getByLabelText('Adresse'), 'https://example.com');
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Speichern' }));
|
||||
|
||||
expect(await screen.findByRole('alert')).toHaveTextContent(
|
||||
'Sie haben keine Berechtigung für diese Aktion.',
|
||||
);
|
||||
expect(screen.queryByText('Das Modul konnte nicht gespeichert werden.')).toBeNull();
|
||||
});
|
||||
|
||||
it('Speichern mit 400 zeigt die Meldung des Servers', async () => {
|
||||
mockList.mockResolvedValue([wiki]);
|
||||
mockUpdate.mockRejectedValue(
|
||||
new CustomModuleRequestError(400, 'Nur https-Adressen ohne Zugangsdaten sind erlaubt.'),
|
||||
);
|
||||
render(<AdminCustomModulesPage />);
|
||||
await userEvent.click(await screen.findByRole('button', { name: 'Bearbeiten' }));
|
||||
const dialog = screen.getByRole('dialog');
|
||||
await userEvent.click(within(dialog).getByRole('button', { name: 'Speichern' }));
|
||||
|
||||
expect(await within(dialog).findByRole('alert')).toHaveTextContent(
|
||||
'Die Angaben wurden nicht angenommen. Bitte prüfen Sie Name, Adresse und Kategorie.',
|
||||
);
|
||||
});
|
||||
|
||||
it('Bearbeiten: Formular ist vorbelegt, Speichern ruft update und aktualisiert die Seitenleiste', async () => {
|
||||
@@ -254,13 +324,17 @@ describe('AdminCustomModulesPage', () => {
|
||||
mockCreate.mockRejectedValue(new Error('500'));
|
||||
render(<AdminCustomModulesPage />);
|
||||
await screen.findByText('Noch keine gemeinsamen Einträge');
|
||||
await userEvent.click(screen.getAllByRole('button', { name: 'Gemeinsamen Eintrag anlegen' })[0]);
|
||||
await userEvent.click(
|
||||
screen.getAllByRole('button', { name: 'Gemeinsamen Eintrag anlegen' })[0],
|
||||
);
|
||||
|
||||
await userEvent.type(screen.getByLabelText('Name'), 'Beispielseite');
|
||||
await userEvent.type(screen.getByLabelText('Adresse'), 'https://example.com');
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Speichern' }));
|
||||
|
||||
expect(await screen.findByText('Das Modul konnte nicht gespeichert werden.')).toBeInTheDocument();
|
||||
expect(
|
||||
await screen.findByText('Das Modul konnte nicht gespeichert werden.'),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
||||
expect(mockBump).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -271,7 +345,9 @@ describe('AdminCustomModulesPage', () => {
|
||||
await userEvent.click(await screen.findByRole('button', { name: 'Löschen' }));
|
||||
|
||||
const dialog = screen.getByRole('dialog');
|
||||
expect(within(dialog).getByText('Möchten Sie „Wiki“ wirklich löschen? Für alle Benutzer.')).toBeInTheDocument();
|
||||
expect(
|
||||
within(dialog).getByText('Möchten Sie „Wiki“ wirklich löschen? Für alle Benutzer.'),
|
||||
).toBeInTheDocument();
|
||||
await userEvent.click(within(dialog).getByRole('button', { name: 'Löschen' }));
|
||||
|
||||
await waitFor(() => expect(mockDelete).toHaveBeenCalledWith('cm-1'));
|
||||
@@ -283,7 +359,9 @@ describe('AdminCustomModulesPage', () => {
|
||||
mockList.mockResolvedValue([wiki]);
|
||||
render(<AdminCustomModulesPage />);
|
||||
await userEvent.click(await screen.findByRole('button', { name: 'Löschen' }));
|
||||
await userEvent.click(within(screen.getByRole('dialog')).getByRole('button', { name: 'Abbrechen' }));
|
||||
await userEvent.click(
|
||||
within(screen.getByRole('dialog')).getByRole('button', { name: 'Abbrechen' }),
|
||||
);
|
||||
|
||||
expect(mockDelete).not.toHaveBeenCalled();
|
||||
expect(mockBump).not.toHaveBeenCalled();
|
||||
@@ -295,13 +373,38 @@ describe('AdminCustomModulesPage', () => {
|
||||
mockDelete.mockRejectedValue(new Error('500'));
|
||||
render(<AdminCustomModulesPage />);
|
||||
await userEvent.click(await screen.findByRole('button', { name: 'Löschen' }));
|
||||
await userEvent.click(within(screen.getByRole('dialog')).getByRole('button', { name: 'Löschen' }));
|
||||
await userEvent.click(
|
||||
within(screen.getByRole('dialog')).getByRole('button', { name: 'Löschen' }),
|
||||
);
|
||||
|
||||
expect(await screen.findByText('Das Modul konnte nicht gelöscht werden.')).toBeInTheDocument();
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
||||
expect(mockBump).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Löschen mit 403 zeigt „keine Berechtigung“', async () => {
|
||||
mockList.mockResolvedValue([wiki]);
|
||||
mockDelete.mockRejectedValue(new CustomModuleRequestError(403, 'Forbidden'));
|
||||
render(<AdminCustomModulesPage />);
|
||||
await userEvent.click(await screen.findByRole('button', { name: 'Löschen' }));
|
||||
await userEvent.click(
|
||||
within(screen.getByRole('dialog')).getByRole('button', { name: 'Löschen' }),
|
||||
);
|
||||
|
||||
expect(await screen.findByRole('alert')).toHaveTextContent(
|
||||
'Sie haben keine Berechtigung für diese Aktion.',
|
||||
);
|
||||
expect(screen.queryByText('Das Modul konnte nicht gelöscht werden.')).toBeNull();
|
||||
});
|
||||
|
||||
it('Ladefehler zeigt nur die Fehlermeldung, nicht zusätzlich den Leer-Zustand', async () => {
|
||||
mockList.mockRejectedValue(new CustomModuleRequestError(500, 'Internal server error'));
|
||||
render(<AdminCustomModulesPage />);
|
||||
|
||||
expect(await screen.findByText('Laden fehlgeschlagen.')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Noch keine gemeinsamen Einträge')).toBeNull();
|
||||
});
|
||||
|
||||
it('zeigt nur gemeinsame Einträge, nie persönliche (Filter über personal)', async () => {
|
||||
mockList.mockResolvedValue([
|
||||
wiki,
|
||||
|
||||
Reference in New Issue
Block a user