test(260924-i8v): RED fuer Serverliste, Kennzahlen, Auswahl und Minutentakt der Proxmox-Kachel
- Modell: resolveProxmoxWidgetConfig, selectServers, widgetKeyFigure - formatPercent/formatCount gemeinsam in proxmox-status - Kachel: Sortierung, Kennzahltexte, unbekannt statt 0, Filter, Links, Titel, Takt Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,290 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { ProxmoxMetrics, ProxmoxServer } from '@/lib/proxmox-api';
|
||||
import {
|
||||
healthSummary,
|
||||
resolveProxmoxWidgetConfig,
|
||||
selectServers,
|
||||
widgetKeyFigure,
|
||||
} from './proxmox-widget-model';
|
||||
|
||||
const NOW = Date.parse('2026-09-24T12:00:00.000Z');
|
||||
const NOW_S = NOW / 1000;
|
||||
const POLLED_AT = '2026-09-24T11:58:00.000Z';
|
||||
|
||||
function server(
|
||||
overrides: Partial<ProxmoxServer> & {
|
||||
metrics?: ProxmoxMetrics | null;
|
||||
reachable?: boolean;
|
||||
polled?: boolean;
|
||||
} = {},
|
||||
): ProxmoxServer {
|
||||
const { metrics = null, reachable = true, polled = true, ...rest } = overrides;
|
||||
const id = rest.id ?? 'srv';
|
||||
return {
|
||||
id,
|
||||
tenantId: 't1',
|
||||
name: id,
|
||||
productType: metrics?.productType ?? 'pve',
|
||||
baseUrl: 'https://x.intern',
|
||||
authMethod: 'token',
|
||||
tokenId: 'a@pam!b',
|
||||
username: null,
|
||||
tlsRejectUnauthorized: true,
|
||||
isActive: true,
|
||||
pollIntervalMin: 5,
|
||||
position: 0,
|
||||
createdAt: POLLED_AT,
|
||||
updatedAt: POLLED_AT,
|
||||
status: {
|
||||
id: `st-${id}`,
|
||||
serverId: id,
|
||||
lastPolledAt: polled ? POLLED_AT : null,
|
||||
lastOkAt: reachable ? POLLED_AT : null,
|
||||
reachable,
|
||||
errorKind: reachable ? null : 'netz',
|
||||
errorDetail: null,
|
||||
rawSample: null,
|
||||
updatedAt: POLLED_AT,
|
||||
metrics,
|
||||
},
|
||||
...rest,
|
||||
};
|
||||
}
|
||||
|
||||
function pve(
|
||||
overrides: Partial<Extract<ProxmoxMetrics, { productType: 'pve' }>> = {},
|
||||
): ProxmoxMetrics {
|
||||
return {
|
||||
productType: 'pve',
|
||||
nodeCount: 1,
|
||||
guestsRunning: 3,
|
||||
guestsStopped: 1,
|
||||
nodes: [{ node: 'n1', cpu: 0.1, maxcpu: 8, mem: 1, maxmem: 10 }],
|
||||
storages: [{ storage: 'local', node: 'n1', disk: 1, maxdisk: 10 }],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function pbsDatastore(lastBackupAt: number | null) {
|
||||
return {
|
||||
name: `ds-${lastBackupAt}`,
|
||||
total: 100,
|
||||
used: 10,
|
||||
free: 90,
|
||||
lastBackupAt,
|
||||
lastVerifyState: 'ok',
|
||||
};
|
||||
}
|
||||
|
||||
describe('healthSummary', () => {
|
||||
it('alle ok -> allOk, keine Eintraege, worst ok', () => {
|
||||
expect(healthSummary({ down: 0, warn: 0, ok: 3, idle: 0, orphan: 0 })).toEqual({
|
||||
allOk: true,
|
||||
entries: [],
|
||||
worst: 'ok',
|
||||
});
|
||||
});
|
||||
|
||||
it('Abweichungen in HEALTH_ORDER, ok wird nicht genannt, worst = schlimmster', () => {
|
||||
expect(healthSummary({ down: 1, warn: 2, ok: 5, idle: 0, orphan: 1 })).toEqual({
|
||||
allOk: false,
|
||||
entries: [
|
||||
{ health: 'down', count: 1 },
|
||||
{ health: 'warn', count: 2 },
|
||||
{ health: 'orphan', count: 1 },
|
||||
],
|
||||
worst: 'down',
|
||||
});
|
||||
});
|
||||
|
||||
it('keine Server -> nicht allOk, worst null', () => {
|
||||
expect(healthSummary({ down: 0, warn: 0, ok: 0, idle: 0, orphan: 0 })).toEqual({
|
||||
allOk: false,
|
||||
entries: [],
|
||||
worst: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveProxmoxWidgetConfig', () => {
|
||||
it('leere Konfiguration -> leerer Titel, keine Auswahl', () => {
|
||||
expect(resolveProxmoxWidgetConfig({})).toEqual({ title: '', serverIds: [] });
|
||||
});
|
||||
|
||||
it('Titel, der kein String ist, wird leer', () => {
|
||||
expect(resolveProxmoxWidgetConfig({ title: 42 }).title).toBe('');
|
||||
expect(resolveProxmoxWidgetConfig({ title: null }).title).toBe('');
|
||||
});
|
||||
|
||||
it('Titel als String bleibt erhalten', () => {
|
||||
expect(resolveProxmoxWidgetConfig({ title: 'Rechenzentrum' }).title).toBe('Rechenzentrum');
|
||||
});
|
||||
|
||||
it('serverIds, das kein Array ist, wird leer', () => {
|
||||
expect(resolveProxmoxWidgetConfig({ serverIds: 'a' }).serverIds).toEqual([]);
|
||||
expect(resolveProxmoxWidgetConfig({ serverIds: { a: 1 } }).serverIds).toEqual([]);
|
||||
});
|
||||
|
||||
it('Nicht-Strings und leere Strings fallen weg, doppelte Kennungen einmal', () => {
|
||||
expect(
|
||||
resolveProxmoxWidgetConfig({ serverIds: ['a', 1, '', null, 'b', 'a', { id: 'c' }] })
|
||||
.serverIds,
|
||||
).toEqual(['a', 'b']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectServers', () => {
|
||||
const list = [server({ id: 'a' }), server({ id: 'b' }), server({ id: 'c' })];
|
||||
|
||||
it('leere Auswahl liefert alle Server', () => {
|
||||
expect(selectServers(list, [])).toEqual({ servers: list, selectionGone: false });
|
||||
});
|
||||
|
||||
it('Auswahl filtert nach Kennung', () => {
|
||||
const result = selectServers(list, ['c', 'a']);
|
||||
expect(result.servers.map((s) => s.id)).toEqual(['a', 'c']);
|
||||
expect(result.selectionGone).toBe(false);
|
||||
});
|
||||
|
||||
it('nur noch geloeschte Kennungen ausgewaehlt -> leere Liste, selectionGone', () => {
|
||||
expect(selectServers(list, ['weg', 'auch-weg'])).toEqual({
|
||||
servers: [],
|
||||
selectionGone: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('eine geloeschte und eine vorhandene Kennung -> nur die vorhandene', () => {
|
||||
const result = selectServers(list, ['weg', 'b']);
|
||||
expect(result.servers.map((s) => s.id)).toEqual(['b']);
|
||||
expect(result.selectionGone).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('widgetKeyFigure', () => {
|
||||
it('down, idle, orphan liefern den Zustand, auch mit alten Messwerten', () => {
|
||||
expect(widgetKeyFigure(server({ reachable: false, metrics: pve() }), NOW)).toEqual({
|
||||
kind: 'status',
|
||||
health: 'down',
|
||||
});
|
||||
expect(widgetKeyFigure(server({ polled: false }), NOW)).toEqual({
|
||||
kind: 'status',
|
||||
health: 'idle',
|
||||
});
|
||||
expect(widgetKeyFigure(server({ isActive: false, metrics: pve() }), NOW)).toEqual({
|
||||
kind: 'status',
|
||||
health: 'orphan',
|
||||
});
|
||||
});
|
||||
|
||||
it('erreichbarer Server ohne Messwerte -> unknown', () => {
|
||||
expect(widgetKeyFigure(server({ metrics: null }), NOW)).toEqual({ kind: 'unknown' });
|
||||
});
|
||||
|
||||
it('PVE ok -> laufende Gaeste, total = laufend + gestoppt', () => {
|
||||
expect(widgetKeyFigure(server({ metrics: pve() }), NOW)).toEqual({
|
||||
kind: 'guests',
|
||||
running: 3,
|
||||
total: 4,
|
||||
});
|
||||
});
|
||||
|
||||
it('PVE ok ohne Gaeste -> noGuests', () => {
|
||||
expect(
|
||||
widgetKeyFigure(server({ metrics: pve({ guestsRunning: 0, guestsStopped: 0 }) }), NOW),
|
||||
).toEqual({ kind: 'noGuests' });
|
||||
});
|
||||
|
||||
it('PVE mit unbekannter Gaestezahl -> unknown, nie 0', () => {
|
||||
const metrics = pve({
|
||||
guestsRunning: null as unknown as number,
|
||||
guestsStopped: 1,
|
||||
});
|
||||
expect(widgetKeyFigure(server({ metrics }), NOW)).toEqual({ kind: 'unknown' });
|
||||
});
|
||||
|
||||
it('PVE warn -> hoechster bekannter Anteil aus CPU, RAM und Speicher', () => {
|
||||
const metrics = pve({
|
||||
nodes: [
|
||||
{ node: 'n1', cpu: 0.5, maxcpu: 8, mem: 87, maxmem: 100 },
|
||||
{ node: 'n2', cpu: null, maxcpu: 8, mem: null, maxmem: 100 },
|
||||
],
|
||||
storages: [{ storage: 's', node: 'n1', disk: 70, maxdisk: 100 }],
|
||||
});
|
||||
expect(widgetKeyFigure(server({ metrics }), NOW)).toEqual({
|
||||
kind: 'load',
|
||||
fraction: 0.87,
|
||||
level: 'warn',
|
||||
});
|
||||
});
|
||||
|
||||
it('PVE warn durch Speicher -> Anteil des Speichers mit Stufe crit', () => {
|
||||
const metrics = pve({
|
||||
nodes: [{ node: 'n1', cpu: 0.2, maxcpu: 8, mem: 1, maxmem: 10 }],
|
||||
storages: [{ storage: 's', node: 'n1', disk: 95, maxdisk: 100 }],
|
||||
});
|
||||
expect(widgetKeyFigure(server({ metrics }), NOW)).toEqual({
|
||||
kind: 'load',
|
||||
fraction: 0.95,
|
||||
level: 'crit',
|
||||
});
|
||||
});
|
||||
|
||||
it('PBS -> aelteste bekannte letzte Sicherung samt stale-Flag', () => {
|
||||
const old = NOW_S - 30 * 3600;
|
||||
const fresh = NOW_S - 2 * 3600;
|
||||
const metrics: ProxmoxMetrics = {
|
||||
productType: 'pbs',
|
||||
datastores: [pbsDatastore(fresh), pbsDatastore(null), pbsDatastore(old)],
|
||||
};
|
||||
expect(widgetKeyFigure(server({ metrics }), NOW)).toEqual({
|
||||
kind: 'backup',
|
||||
at: old,
|
||||
stale: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('PBS mit frischer Sicherung -> nicht stale', () => {
|
||||
const fresh = NOW_S - 5 * 3600;
|
||||
const metrics: ProxmoxMetrics = { productType: 'pbs', datastores: [pbsDatastore(fresh)] };
|
||||
expect(widgetKeyFigure(server({ metrics }), NOW)).toEqual({
|
||||
kind: 'backup',
|
||||
at: fresh,
|
||||
stale: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('PBS: Datenspeicher ohne jede Sicherung -> noBackup', () => {
|
||||
const metrics: ProxmoxMetrics = {
|
||||
productType: 'pbs',
|
||||
datastores: [pbsDatastore(null), pbsDatastore(null)],
|
||||
};
|
||||
expect(widgetKeyFigure(server({ metrics }), NOW)).toEqual({ kind: 'noBackup' });
|
||||
});
|
||||
|
||||
it('PBS ohne Datenspeicher -> unknown', () => {
|
||||
const metrics: ProxmoxMetrics = { productType: 'pbs', datastores: [] };
|
||||
expect(widgetKeyFigure(server({ metrics }), NOW)).toEqual({ kind: 'unknown' });
|
||||
});
|
||||
|
||||
it('PMG -> eingehende Mails aus countIn', () => {
|
||||
const metrics: ProxmoxMetrics = {
|
||||
productType: 'pmg',
|
||||
countIn: 12904,
|
||||
countOut: 5,
|
||||
spamCount: 1,
|
||||
virusCount: 0,
|
||||
};
|
||||
expect(widgetKeyFigure(server({ metrics }), NOW)).toEqual({ kind: 'mailIn', count: 12904 });
|
||||
});
|
||||
|
||||
it('PMG mit countIn null -> unknown, nie 0', () => {
|
||||
const metrics: ProxmoxMetrics = {
|
||||
productType: 'pmg',
|
||||
countIn: null,
|
||||
countOut: null,
|
||||
spamCount: null,
|
||||
virusCount: null,
|
||||
};
|
||||
expect(widgetKeyFigure(server({ metrics }), NOW)).toEqual({ kind: 'unknown' });
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { cleanup, render as rtlRender, screen, waitFor } from '@testing-library/react';
|
||||
import { act, cleanup, render as rtlRender, screen, waitFor, within } from '@testing-library/react';
|
||||
import { NextIntlClientProvider } from 'next-intl';
|
||||
import type { ReactElement } from 'react';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
@@ -231,3 +231,293 @@ describe('ProxmoxWidget: Lade-, Leer- und Fehlerzustand (quick-260924-i8v)', ()
|
||||
expect(mockPollServer).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
function rowNames(): string[] {
|
||||
return screen
|
||||
.getAllByTestId('proxmox-row')
|
||||
.map((row) => row.getAttribute('data-server-name') ?? '');
|
||||
}
|
||||
|
||||
describe('ProxmoxWidget: Serverliste mit Kennzahl (quick-260924-i8v, Aufgabe 2)', () => {
|
||||
it('Zeilen stehen in der Reihenfolge down, warn, ok, idle, orphan', async () => {
|
||||
mockListServers.mockResolvedValue([
|
||||
makeServer('orphan', { position: 0 }),
|
||||
makeServer('ok', { position: 1 }),
|
||||
makeServer('idle', { position: 2 }),
|
||||
makeServer('warn', { position: 3 }),
|
||||
makeServer('down', { position: 4 }),
|
||||
]);
|
||||
await renderWidget();
|
||||
|
||||
await screen.findByTestId('proxmox-summary');
|
||||
expect(rowNames()).toEqual([
|
||||
'server-down',
|
||||
'server-warn',
|
||||
'server-ok',
|
||||
'server-idle',
|
||||
'server-orphan',
|
||||
]);
|
||||
});
|
||||
|
||||
it('zeigt je Zeile genau eine Kennzahl in Worten', async () => {
|
||||
const backupAt = Math.floor(Date.now() / 1000) - 5 * 3600;
|
||||
mockListServers.mockResolvedValue([
|
||||
makeServer('ok', { id: 'pve', name: 'pve-ok' }),
|
||||
makeServer('warn', { id: 'pve-w', name: 'pve-warn' }),
|
||||
makeServer('ok', {
|
||||
id: 'pbs',
|
||||
name: 'pbs-ok',
|
||||
productType: 'pbs',
|
||||
metrics: {
|
||||
productType: 'pbs',
|
||||
datastores: [
|
||||
{
|
||||
name: 'ds',
|
||||
total: 100,
|
||||
used: 10,
|
||||
free: 90,
|
||||
lastBackupAt: backupAt,
|
||||
lastVerifyState: 'ok',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
makeServer('ok', {
|
||||
id: 'pmg',
|
||||
name: 'pmg-ok',
|
||||
productType: 'pmg',
|
||||
metrics: {
|
||||
productType: 'pmg',
|
||||
countIn: 12904,
|
||||
countOut: 10,
|
||||
spamCount: 2,
|
||||
virusCount: 0,
|
||||
},
|
||||
}),
|
||||
makeServer('down'),
|
||||
makeServer('orphan'),
|
||||
makeServer('idle'),
|
||||
]);
|
||||
await renderWidget();
|
||||
|
||||
await screen.findByTestId('proxmox-summary');
|
||||
const figure = (name: string) =>
|
||||
within(
|
||||
screen
|
||||
.getAllByTestId('proxmox-row')
|
||||
.find((row) => row.getAttribute('data-server-name') === name) as HTMLElement,
|
||||
).getByTestId('proxmox-key-figure');
|
||||
|
||||
expect(figure('pve-ok')).toHaveTextContent('3/4 Gäste laufen');
|
||||
expect(figure('pve-warn').textContent).toMatch(/^Auslastung 87\s%$/);
|
||||
expect(figure('pve-warn')).toHaveClass('text-status-warn-fg');
|
||||
expect(figure('pbs-ok')).toHaveTextContent('Sicherung vor 5 Std.');
|
||||
expect(figure('pmg-ok')).toHaveTextContent('12.904 eingehend');
|
||||
expect(figure('server-down')).toHaveTextContent('nicht erreichbar');
|
||||
expect(figure('server-down')).toHaveClass('text-status-down-fg');
|
||||
expect(figure('server-down')).toHaveAttribute('aria-hidden', 'true');
|
||||
expect(figure('server-orphan')).toHaveTextContent('offline & verwaist');
|
||||
expect(figure('server-idle')).toHaveTextContent('noch nicht abgefragt');
|
||||
});
|
||||
|
||||
it('unbekannte Werte heissen „unbekannt“ und nie 0', async () => {
|
||||
mockListServers.mockResolvedValue([
|
||||
makeServer('ok', { id: 'pve', name: 'pve-leer', metrics: null }),
|
||||
makeServer('ok', {
|
||||
id: 'pmg',
|
||||
name: 'pmg-leer',
|
||||
productType: 'pmg',
|
||||
metrics: {
|
||||
productType: 'pmg',
|
||||
countIn: null,
|
||||
countOut: null,
|
||||
spamCount: null,
|
||||
virusCount: null,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
await renderWidget();
|
||||
|
||||
await screen.findByTestId('proxmox-summary');
|
||||
const figures = screen.getAllByTestId('proxmox-key-figure');
|
||||
expect(figures.map((f) => f.textContent)).toEqual(['unbekannt', 'unbekannt']);
|
||||
expect(screen.queryByText(/0 eingehend/)).toBeNull();
|
||||
expect(screen.queryByText(/0\s%/)).toBeNull();
|
||||
});
|
||||
|
||||
it('jede Zeile nennt den Zustand auch fuer Vorleser (nicht nur Farbe)', async () => {
|
||||
mockListServers.mockResolvedValue([makeServer('warn')]);
|
||||
await renderWidget();
|
||||
|
||||
await screen.findByTestId('proxmox-summary');
|
||||
expect(screen.getByText('Warnung')).toHaveClass('sr-only');
|
||||
});
|
||||
|
||||
it('Kennzahl, Liste und Name tragen die Klassen der Groessenstufen', async () => {
|
||||
mockListServers.mockResolvedValue([makeServer('ok'), makeServer('orphan')]);
|
||||
await renderWidget();
|
||||
|
||||
await screen.findByTestId('proxmox-summary');
|
||||
const list = screen.getByTestId('proxmox-list');
|
||||
expect(list.tagName).toBe('UL');
|
||||
expect(list).toHaveClass('[@container(max-height:7.5rem)]:hidden');
|
||||
expect(list).toHaveClass('@max-[8rem]:hidden');
|
||||
for (const figure of screen.getAllByTestId('proxmox-key-figure')) {
|
||||
expect(figure).toHaveClass('@max-[15rem]:hidden');
|
||||
}
|
||||
// verwaist: nur der Name wird gedaempft
|
||||
expect(screen.getByText('server-orphan')).toHaveClass('opacity-70');
|
||||
expect(screen.getByText('server-ok')).not.toHaveClass('opacity-70');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ProxmoxWidget: Serverauswahl, Titel, Links (quick-260924-i8v, Aufgabe 2)', () => {
|
||||
it('config.serverIds beschraenkt Zeilen UND Balken/Zusammenfassung auf die Auswahl', async () => {
|
||||
mockListServers.mockResolvedValue([
|
||||
makeServer('ok', { id: 'a', name: 'pve-a' }),
|
||||
makeServer('down', { id: 'b', name: 'pve-b' }),
|
||||
]);
|
||||
await renderWidget({ config: { serverIds: ['a'] } });
|
||||
|
||||
const summary = await screen.findByTestId('proxmox-summary');
|
||||
expect(summary).toHaveTextContent('Alles in Ordnung');
|
||||
expect(rowNames()).toEqual(['pve-a']);
|
||||
expect(screen.getByTestId('health-bar').querySelectorAll('[data-segment]')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('nur geloeschte Kennungen ausgewaehlt: Satz statt Balken und Liste', async () => {
|
||||
mockListServers.mockResolvedValue([makeServer('ok', { id: 'a' })]);
|
||||
await renderWidget({ config: { serverIds: ['weg'] } });
|
||||
|
||||
expect(
|
||||
await screen.findByText(
|
||||
'Die ausgewählten Server gibt es nicht mehr. Wählen Sie im Bearbeitungsmodus andere aus.',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('health-bar')).toBeNull();
|
||||
expect(screen.queryByTestId('proxmox-list')).toBeNull();
|
||||
});
|
||||
|
||||
it('Ansichtsmodus: jede Zeile ist ein Link auf /modules/proxmox', async () => {
|
||||
mockListServers.mockResolvedValue([makeServer('ok'), makeServer('down')]);
|
||||
await renderWidget();
|
||||
|
||||
await screen.findByTestId('proxmox-summary');
|
||||
const links = screen.getAllByRole('link');
|
||||
expect(links).toHaveLength(2);
|
||||
for (const link of links) expect(link).toHaveAttribute('href', '/modules/proxmox');
|
||||
});
|
||||
|
||||
it('Bearbeitungsmodus: keine Links in der Kachel', async () => {
|
||||
mockListServers.mockResolvedValue([makeServer('ok'), makeServer('down')]);
|
||||
await renderWidget({ isEditMode: true });
|
||||
|
||||
await screen.findByTestId('proxmox-summary');
|
||||
expect(screen.getAllByTestId('proxmox-row')).toHaveLength(2);
|
||||
expect(screen.queryByRole('link')).toBeNull();
|
||||
});
|
||||
|
||||
it('Titel gesetzt: Ueberschrift h2', async () => {
|
||||
mockListServers.mockResolvedValue([makeServer('ok')]);
|
||||
await renderWidget({ config: { title: 'Rechenzentrum' } });
|
||||
|
||||
expect(await screen.findByRole('heading', { level: 2, name: 'Rechenzentrum' })).toBeVisible();
|
||||
});
|
||||
|
||||
it('Titel leer: keine Kopfzeile', async () => {
|
||||
mockListServers.mockResolvedValue([makeServer('ok')]);
|
||||
await renderWidget({ config: { title: ' ' } });
|
||||
|
||||
await screen.findByTestId('proxmox-summary');
|
||||
expect(screen.queryByRole('heading')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('ProxmoxWidget: Minutentakt (quick-260924-i8v, Aufgabe 2)', () => {
|
||||
let visibility: DocumentVisibilityState = 'visible';
|
||||
|
||||
beforeEach(() => {
|
||||
visibility = 'visible';
|
||||
Object.defineProperty(document, 'visibilityState', {
|
||||
configurable: true,
|
||||
get: () => visibility,
|
||||
});
|
||||
// Nur die Intervall-Zeitgeber faelschen: die Warte-Helfer der Testing
|
||||
// Library laufen weiter ueber echte Zeitgeber.
|
||||
vi.useFakeTimers({ toFake: ['setInterval', 'clearInterval'] });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
// Eigene Eigenschaft entfernen, damit wieder die von Document.prototype gilt.
|
||||
delete (document as unknown as Record<string, unknown>).visibilityState;
|
||||
});
|
||||
|
||||
async function flush() {
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
async function advance(ms: number) {
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(ms);
|
||||
});
|
||||
}
|
||||
|
||||
it('laedt nach 60 s erneut aus dem Zwischenlager', async () => {
|
||||
mockListServers.mockResolvedValue([makeServer('ok')]);
|
||||
await renderWidget();
|
||||
await flush();
|
||||
expect(mockListServers).toHaveBeenCalledTimes(1);
|
||||
|
||||
await advance(60_000);
|
||||
expect(mockListServers).toHaveBeenCalledTimes(2);
|
||||
expect(mockPollServer).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('pausiert bei verborgenem Tab und laedt sofort, sobald er wieder sichtbar ist', async () => {
|
||||
mockListServers.mockResolvedValue([makeServer('ok')]);
|
||||
await renderWidget();
|
||||
await flush();
|
||||
expect(mockListServers).toHaveBeenCalledTimes(1);
|
||||
|
||||
visibility = 'hidden';
|
||||
await advance(120_000);
|
||||
expect(mockListServers).toHaveBeenCalledTimes(1);
|
||||
|
||||
visibility = 'visible';
|
||||
await act(async () => {
|
||||
document.dispatchEvent(new Event('visibilitychange'));
|
||||
});
|
||||
expect(mockListServers).toHaveBeenCalledTimes(2);
|
||||
expect(mockPollServer).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('nach dem Aushaengen keine weiteren Abrufe', async () => {
|
||||
mockListServers.mockResolvedValue([makeServer('ok')]);
|
||||
const { unmount } = await renderWidget();
|
||||
await flush();
|
||||
unmount();
|
||||
|
||||
await advance(180_000);
|
||||
document.dispatchEvent(new Event('visibilitychange'));
|
||||
expect(mockListServers).toHaveBeenCalledTimes(1);
|
||||
expect(mockPollServer).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('scheitert ein spaeteres Nachladen, bleibt die letzte Liste stehen', async () => {
|
||||
mockListServers.mockResolvedValueOnce([makeServer('down')]);
|
||||
await renderWidget();
|
||||
await flush();
|
||||
expect(await screen.findByTestId('proxmox-summary')).toHaveTextContent('1 nicht erreichbar');
|
||||
|
||||
mockListServers.mockRejectedValueOnce(new Error('500'));
|
||||
await advance(60_000);
|
||||
await flush();
|
||||
|
||||
expect(mockListServers).toHaveBeenCalledTimes(2);
|
||||
expect(screen.getByTestId('proxmox-summary')).toHaveTextContent('1 nicht erreichbar');
|
||||
expect(screen.queryByText('Die Serverliste konnte nicht geladen werden.')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,6 +2,8 @@ import { describe, expect, it } from 'vitest';
|
||||
import type { ProxmoxMetrics, ProxmoxServer, ProxmoxServerStatus } from '@/lib/proxmox-api';
|
||||
import {
|
||||
formatAge,
|
||||
formatCount,
|
||||
formatPercent,
|
||||
HEALTH_ORDER,
|
||||
isBackupStale,
|
||||
meterLevel,
|
||||
@@ -307,3 +309,14 @@ describe('formatAge', () => {
|
||||
expect(formatAge('2026-09-24T11:57:00.000Z', NOW, 'en')).toBe('3 min. ago');
|
||||
});
|
||||
});
|
||||
|
||||
// quick-260924-i8v: Zahlformat EINMAL, gemeinsam fuer ServerCard und Kachel.
|
||||
describe('formatPercent / formatCount', () => {
|
||||
it('deutsches Prozentformat mit geschuetztem Leerzeichen', () => {
|
||||
expect(formatPercent(0.87, 'de')).toMatch(/^87\s%$/);
|
||||
});
|
||||
|
||||
it('deutsches Tausendertrennzeichen', () => {
|
||||
expect(formatCount(12904, 'de')).toBe('12.904');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user