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( {ui} , ); } // Mock @/lib/proxmox-api — die Seite ruft `listServers` beim Laden auf, // "Jetzt aktualisieren" ruft `pollServer` je Server. const mockListServers = vi.fn(); const mockPollServer = vi.fn(); vi.mock('@/lib/proxmox-api', () => ({ listServers: (...args: unknown[]) => mockListServers(...args), pollServer: (...args: unknown[]) => mockPollServer(...args), })); // Mock the auth store — mirrors settings-roles.test.tsx (selector-passthrough). const mockAuthStore = vi.fn(); vi.mock('@/lib/stores/auth-store', () => ({ useAuthStore: (selector: (state: unknown) => unknown) => mockAuthStore(selector), })); vi.mock('next/link', () => ({ default: ({ href, children, ...rest }: { href: string; children: React.ReactNode }) => ( {children} ), })); function mockUser(user: { role: 'SUPER_ADMIN' | 'ADMIN' | 'USER' } | null) { mockAuthStore.mockImplementation((selector: (state: { user: typeof user }) => unknown) => selector({ user }), ); } // Ein nie abgefragter Server (wie im Befund-2-Test von ServerCard.test.tsx): // leere Zwischenlagerzeile direkt nach `createServer`. function makeUnpolledServer(overrides: Partial = {}): ProxmoxServer { return { id: 'srv-1', tenantId: 't1', name: 'pve-1', productType: 'pve', baseUrl: 'https://pve.intern', authMethod: 'token', tokenId: 'root@pam!x', username: null, tlsRejectUnauthorized: true, isActive: true, pollIntervalMin: 5, position: 0, createdAt: '2026-01-01T00:00:00.000Z', updatedAt: '2026-01-01T00:00:00.000Z', status: { id: 's1', serverId: 'srv-1', lastPolledAt: null, lastOkAt: null, reachable: false, errorKind: null, errorDetail: null, rawSample: null, updatedAt: '2026-01-01T00:00:00.000Z', metrics: null, }, ...overrides, } as ProxmoxServer; } afterEach(() => { cleanup(); mockListServers.mockReset(); mockPollServer.mockReset(); mockAuthStore.mockReset(); }); describe('ProxmoxPage role gating (260923-le6)', () => { it('Rolle USER: kein Knopf "Jetzt aktualisieren", Karten-Hinweis ist der automatische Text', async () => { mockUser({ role: 'USER' }); mockListServers.mockResolvedValue([makeUnpolledServer()]); const { default: ProxmoxPage } = await import('./page'); render(); await screen.findByText('pve-1'); expect(screen.queryByRole('button', { name: 'Jetzt aktualisieren' })).not.toBeInTheDocument(); expect( screen.getByText( 'Noch keine Abfrage gelaufen. Die Werte erscheinen nach der nächsten automatischen Abfrage.', ), ).toBeInTheDocument(); }); it('kein Benutzer geladen (user: null): kein Knopf', async () => { mockUser(null); mockListServers.mockResolvedValue([makeUnpolledServer()]); const { default: ProxmoxPage } = await import('./page'); render(); await screen.findByText('pve-1'); expect(screen.queryByRole('button', { name: 'Jetzt aktualisieren' })).not.toBeInTheDocument(); }); it('Rolle ADMIN: Knopf sichtbar, Karten-Hinweis ist der Admin-Text mit Knopfverweis', async () => { mockUser({ role: 'ADMIN' }); mockListServers.mockResolvedValue([makeUnpolledServer()]); const { default: ProxmoxPage } = await import('./page'); render(); await screen.findByText('pve-1'); expect(screen.getByRole('button', { name: 'Jetzt aktualisieren' })).toBeInTheDocument(); expect( screen.getByText('Noch keine Abfrage gelaufen. Klicken Sie oben auf „Jetzt aktualisieren“.'), ).toBeInTheDocument(); }); it('Rolle SUPER_ADMIN: Knopf sichtbar', async () => { mockUser({ role: 'SUPER_ADMIN' }); mockListServers.mockResolvedValue([makeUnpolledServer()]); const { default: ProxmoxPage } = await import('./page'); render(); await screen.findByText('pve-1'); expect(screen.getByRole('button', { name: 'Jetzt aktualisieren' })).toBeInTheDocument(); }); it('leere Serverliste bei ADMIN: weiterhin kein Knopf', async () => { mockUser({ role: 'ADMIN' }); mockListServers.mockResolvedValue([]); const { default: ProxmoxPage } = await import('./page'); render(); await waitFor(() => { expect( screen.getByText( 'Noch kein Server eingetragen. Legen Sie in den Moduleinstellungen einen Server an.', ), ).toBeInTheDocument(); }); expect(screen.queryByRole('button', { name: 'Jetzt aktualisieren' })).not.toBeInTheDocument(); }); }); // Ein erreichbarer, abgefragter Server; Zustand ueber die Ueberschreibungen. function makePolledServer( overrides: Partial = {}, statusOverrides: Record = {}, ) { 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(); 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(); 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(); 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(); 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(); await screen.findByText('pve-1'); expect(screen.queryByRole('link', { name: 'Einstellungen' })).not.toBeInTheDocument(); cleanup(); mockUser({ role: 'ADMIN' }); render(); await screen.findByText('pve-1'); expect(screen.getByRole('link', { name: 'Einstellungen' })).toHaveAttribute( 'href', '/modules/proxmox/settings', ); }); });