feat(260924-h7x): Proxmox-Seite mit Status-Design und Tiefe
- Statusfarben als OKLCH-Tokens (Flaeche + kontrastgepruefte Textvariante) und Well-Token - Gesundheitsbalken mit Legende und vorgelesener Zusammenfassung - Karten nach Zustand sortiert, Statusleiste, getoenter Schatten, Statuspille - Messwerte in eingelassenen Feldern: PVE-Knoten mit Balken, PBS-Fuellstand/Sicherung/Pruefung, PMG-Zahlfelder - offline & verwaist: gestrichelt, gedaempft, keine alten Messwerte - Skelett-Karten beim Laden, Leerzustand als Well, relative Zeitangaben Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,19 @@
|
||||
import { cleanup, render, screen, waitFor } from '@testing-library/react';
|
||||
import { cleanup, render as rtlRender, screen, waitFor, within } from '@testing-library/react';
|
||||
import { NextIntlClientProvider } from 'next-intl';
|
||||
import type { ReactElement } from 'react';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { ProxmoxServer } from '@/lib/proxmox-api';
|
||||
import de from '@/messages/de.json';
|
||||
|
||||
// Echter next-intl-Provider mit den deutschen Texten (quick-260924-h7x) —
|
||||
// ServerCard braucht `useLocale` und ICU-Plural.
|
||||
function render(ui: ReactElement) {
|
||||
return rtlRender(
|
||||
<NextIntlClientProvider locale="de" messages={de} timeZone="Europe/Berlin">
|
||||
{ui}
|
||||
</NextIntlClientProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
// Mock @/lib/proxmox-api — die Seite ruft `listServers` beim Laden auf,
|
||||
// "Jetzt aktualisieren" ruft `pollServer` je Server.
|
||||
@@ -26,34 +39,6 @@ vi.mock('next/link', () => ({
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('next-intl', () => ({
|
||||
useTranslations: () => (key: string, params?: Record<string, string | number>) => {
|
||||
const translations: Record<string, string> = {
|
||||
title: 'Proxmox',
|
||||
description: 'Zustand Ihrer Proxmox-Server (PVE/PBS/PMG) auf einen Blick.',
|
||||
loading: 'Lade Serverliste...',
|
||||
loadError: 'Die Serverliste konnte nicht geladen werden.',
|
||||
emptyState:
|
||||
'Noch kein Server eingetragen. Legen Sie in den Moduleinstellungen einen Server an.',
|
||||
'card.refresh': 'Jetzt aktualisieren',
|
||||
'card.refreshing': 'Wird aktualisiert...',
|
||||
'card.settingsLink': 'Zu den Einstellungen',
|
||||
'card.unknownValue': 'unbekannt',
|
||||
'card.lastPolledLabel': 'Letzte Abfrage',
|
||||
'card.notPolledYet': 'Noch keine Abfrage gelaufen. Klicken Sie oben auf „{refreshLabel}“.',
|
||||
'card.notPolledYetAutomatic':
|
||||
'Noch keine Abfrage gelaufen. Die Werte erscheinen nach der nächsten automatischen Abfrage.',
|
||||
};
|
||||
let result = translations[key] ?? key;
|
||||
if (params) {
|
||||
for (const [k, v] of Object.entries(params)) {
|
||||
result = result.replace(`{${k}}`, String(v));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
}));
|
||||
|
||||
function mockUser(user: { role: 'SUPER_ADMIN' | 'ADMIN' | 'USER' } | null) {
|
||||
mockAuthStore.mockImplementation((selector: (state: { user: typeof user }) => unknown) =>
|
||||
selector({ user }),
|
||||
@@ -176,3 +161,119 @@ describe('ProxmoxPage role gating (260923-le6)', () => {
|
||||
expect(screen.queryByRole('button', { name: 'Jetzt aktualisieren' })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// Ein erreichbarer, abgefragter Server; Zustand ueber die Ueberschreibungen.
|
||||
function makePolledServer(
|
||||
overrides: Partial<ProxmoxServer> = {},
|
||||
statusOverrides: Record<string, unknown> = {},
|
||||
) {
|
||||
const base = makeUnpolledServer(overrides);
|
||||
return {
|
||||
...base,
|
||||
status: {
|
||||
...base.status,
|
||||
lastPolledAt: new Date(Date.now() - 3 * 60_000).toISOString(),
|
||||
lastOkAt: new Date(Date.now() - 3 * 60_000).toISOString(),
|
||||
reachable: true,
|
||||
...statusOverrides,
|
||||
},
|
||||
} as ProxmoxServer;
|
||||
}
|
||||
|
||||
describe('ProxmoxPage — Gesundheitsbalken und Sortierung (quick-260924-h7x)', () => {
|
||||
const servers = [
|
||||
makePolledServer({ id: 'a', name: 'ok-server', position: 0 }),
|
||||
makePolledServer(
|
||||
{ id: 'b', name: 'down-server', position: 1 },
|
||||
{ reachable: false, errorKind: 'netz' },
|
||||
),
|
||||
makePolledServer({ id: 'c', name: 'orphan-server', position: 2, isActive: false }),
|
||||
makePolledServer(
|
||||
{ id: 'd', name: 'warn-server', position: 3, productType: 'pmg' },
|
||||
{ metrics: { productType: 'pmg', countIn: 1, countOut: 1, spamCount: 0, virusCount: 2 } },
|
||||
),
|
||||
makeUnpolledServer({ id: 'e', name: 'idle-server', position: 4 }),
|
||||
];
|
||||
|
||||
it('Karten stehen in der Reihenfolge down, warn, ok, idle, orphan', async () => {
|
||||
mockUser({ role: 'USER' });
|
||||
mockListServers.mockResolvedValue(servers);
|
||||
|
||||
const { default: ProxmoxPage } = await import('./page');
|
||||
render(<ProxmoxPage />);
|
||||
await screen.findByText('ok-server');
|
||||
|
||||
const order = screen.getAllByTestId('server-card').map((c) => c.getAttribute('data-health'));
|
||||
expect(order).toEqual(['down', 'warn', 'ok', 'idle', 'orphan']);
|
||||
});
|
||||
|
||||
it('der Balken liest die Zusammenfassung vor und hat je belegtem Zustand ein Segment', async () => {
|
||||
mockUser({ role: 'USER' });
|
||||
mockListServers.mockResolvedValue(servers);
|
||||
|
||||
const { default: ProxmoxPage } = await import('./page');
|
||||
render(<ProxmoxPage />);
|
||||
await screen.findByText('ok-server');
|
||||
|
||||
const bar = screen.getByRole('img', { name: /Zustand der Server/ });
|
||||
expect(bar).toHaveAccessibleName(
|
||||
'Zustand der Server: 1 nicht erreichbar, 1 mit Warnung, 1 in Ordnung, 1 noch nicht abgefragt, 1 offline & verwaist',
|
||||
);
|
||||
expect(Array.from(bar.children).map((s) => s.getAttribute('data-segment'))).toEqual([
|
||||
'down',
|
||||
'warn',
|
||||
'ok',
|
||||
'idle',
|
||||
'orphan',
|
||||
]);
|
||||
});
|
||||
|
||||
it('leere Zustaende entfallen in Balken und Legende', async () => {
|
||||
mockUser({ role: 'USER' });
|
||||
mockListServers.mockResolvedValue([
|
||||
makePolledServer({ id: 'a', name: 'eins', position: 0 }),
|
||||
makePolledServer({ id: 'b', name: 'zwei', position: 1 }),
|
||||
]);
|
||||
|
||||
const { default: ProxmoxPage } = await import('./page');
|
||||
render(<ProxmoxPage />);
|
||||
await screen.findByText('eins');
|
||||
|
||||
const bar = screen.getByRole('img', { name: /Zustand der Server/ });
|
||||
expect(bar).toHaveAccessibleName('Zustand der Server: 2 in Ordnung');
|
||||
expect(bar.children).toHaveLength(1);
|
||||
const legend = within(screen.getByTestId('health-bar')).getByRole('list', { hidden: true });
|
||||
expect(legend.children).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('waehrend des Ladens erscheinen Skelett-Karten ohne Daueranimation bei reduzierter Bewegung', async () => {
|
||||
mockUser({ role: 'USER' });
|
||||
mockListServers.mockReturnValue(new Promise(() => undefined));
|
||||
|
||||
const { default: ProxmoxPage } = await import('./page');
|
||||
const { container } = render(<ProxmoxPage />);
|
||||
|
||||
const busy = container.querySelector('[aria-busy="true"]');
|
||||
expect(busy).not.toBeNull();
|
||||
expect(busy).toHaveClass('animate-pulse', 'motion-reduce:animate-none');
|
||||
expect(screen.getByText('Lade Serverliste...')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Einstellungen-Verweis nur fuer Admins', async () => {
|
||||
mockUser({ role: 'USER' });
|
||||
mockListServers.mockResolvedValue([makePolledServer()]);
|
||||
const { default: ProxmoxPage } = await import('./page');
|
||||
render(<ProxmoxPage />);
|
||||
await screen.findByText('pve-1');
|
||||
expect(screen.queryByRole('link', { name: 'Einstellungen' })).not.toBeInTheDocument();
|
||||
cleanup();
|
||||
|
||||
mockUser({ role: 'ADMIN' });
|
||||
render(<ProxmoxPage />);
|
||||
await screen.findByText('pve-1');
|
||||
expect(screen.getByRole('link', { name: 'Einstellungen' })).toHaveAttribute(
|
||||
'href',
|
||||
'/modules/proxmox/settings',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user