b10734f382
- tenant-selector: Komponenten statisch statt im Test dynamisch importiert (Laden zaehlte in die 5-s-Frist des ersten Tests), SUPER_ADMIN/ADMIN in zwei it aufgetrennt - marketplace/marketplace-filters: gleiche Umstellung; userEvent an die falsche Uhr gekoppelt statt auf shouldAdvanceTime zu warten - proxmox-widget: Rendern wartet das erste Laden in act() ab (81 Warnungen weg) - Todo 2026-09-23 nach completed/ Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
658 lines
24 KiB
TypeScript
658 lines
24 KiB
TypeScript
import {
|
|
act,
|
|
cleanup,
|
|
fireEvent,
|
|
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';
|
|
import type { ProxmoxMetrics, ProxmoxServer } from '@/lib/proxmox-api';
|
|
import de from '@/messages/de.json';
|
|
// Statisch importiert (vi.mock wird darueber gehoben) — das Laden faellt in die
|
|
// Sammelphase, nicht in die Frist des ersten Tests (quick-260924-m4n).
|
|
import { ProxmoxWidget } from './proxmox-widget';
|
|
|
|
// Echter next-intl-Provider mit den deutschen Texten (Muster
|
|
// proxmox-page-roles.test.tsx) — die Kachel braucht ICU-Plural und useLocale.
|
|
function render(ui: ReactElement) {
|
|
return rtlRender(
|
|
<NextIntlClientProvider locale="de" messages={de} timeZone="Europe/Berlin">
|
|
{ui}
|
|
</NextIntlClientProvider>,
|
|
);
|
|
}
|
|
|
|
// T-I8V-02: `pollServer` ist nur als Spion da — die Kachel darf ihn nie rufen.
|
|
const mockListServers = vi.fn();
|
|
const mockPollServer = vi.fn();
|
|
|
|
vi.mock('@/lib/proxmox-api', () => ({
|
|
listServers: (...args: unknown[]) => mockListServers(...args),
|
|
pollServer: (...args: unknown[]) => mockPollServer(...args),
|
|
}));
|
|
|
|
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 }) => (
|
|
<a href={href} {...rest}>
|
|
{children}
|
|
</a>
|
|
),
|
|
}));
|
|
|
|
const mockUpdateWidgetConfig = vi.fn();
|
|
vi.mock('@/lib/dashboard-api', () => ({
|
|
updateWidgetConfig: (...args: unknown[]) => mockUpdateWidgetConfig(...args),
|
|
}));
|
|
|
|
function mockUser(user: { role: 'SUPER_ADMIN' | 'ADMIN' | 'USER' } | null) {
|
|
mockAuthStore.mockImplementation((selector: (state: { user: typeof user }) => unknown) =>
|
|
selector({ user }),
|
|
);
|
|
}
|
|
|
|
type Health = 'ok' | 'warn' | 'down' | 'idle' | 'orphan';
|
|
|
|
const POLLED_AT = new Date(Date.now() - 2 * 60 * 1000).toISOString();
|
|
|
|
function pveMetrics(overrides: Partial<Extract<ProxmoxMetrics, { productType: 'pve' }>> = {}) {
|
|
return {
|
|
productType: 'pve' as const,
|
|
nodeCount: 1,
|
|
guestsRunning: 3,
|
|
guestsStopped: 1,
|
|
nodes: [{ node: 'n1', cpu: 0.1, maxcpu: 8, mem: 1, maxmem: 10 }],
|
|
storages: [],
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Baukasten: ein Server mit Zwischenlager passend zum gewuenschten Zustand.
|
|
* `warn` = PVE-Knoten bei 87 % Last.
|
|
*/
|
|
function makeServer(
|
|
health: Health,
|
|
overrides: Partial<ProxmoxServer> & { metrics?: ProxmoxMetrics | null } = {},
|
|
): ProxmoxServer {
|
|
const { metrics: metricsOverride, ...rest } = overrides;
|
|
const id = rest.id ?? `srv-${health}`;
|
|
const metrics: ProxmoxMetrics | null =
|
|
metricsOverride !== undefined
|
|
? metricsOverride
|
|
: health === 'warn'
|
|
? pveMetrics({ nodes: [{ node: 'n1', cpu: 0.87, maxcpu: 8, mem: 1, maxmem: 10 }] })
|
|
: pveMetrics();
|
|
return {
|
|
id,
|
|
tenantId: 't1',
|
|
name: `server-${health}`,
|
|
productType: 'pve',
|
|
baseUrl: 'https://pve.intern',
|
|
authMethod: 'token',
|
|
tokenId: 'root@pam!x',
|
|
username: null,
|
|
tlsRejectUnauthorized: true,
|
|
isActive: health !== 'orphan',
|
|
pollIntervalMin: 5,
|
|
position: 0,
|
|
createdAt: '2026-01-01T00:00:00.000Z',
|
|
updatedAt: '2026-01-01T00:00:00.000Z',
|
|
status: {
|
|
id: `st-${id}`,
|
|
serverId: id,
|
|
lastPolledAt: health === 'idle' ? null : POLLED_AT,
|
|
lastOkAt: health === 'down' ? null : POLLED_AT,
|
|
reachable: health !== 'down' && health !== 'idle',
|
|
errorKind: health === 'down' ? 'netz' : null,
|
|
errorDetail: health === 'down' ? 'connect ECONNREFUSED 10.0.0.1:8006' : null,
|
|
rawSample: null,
|
|
updatedAt: POLLED_AT,
|
|
metrics: health === 'down' || health === 'idle' ? null : metrics,
|
|
},
|
|
...rest,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Rendert die Kachel und wartet INNERHALB von act() das Ende des ersten
|
|
* Ladens ab (aufgeloestes listServers-Versprechen -> setServers/setLoadFailed/
|
|
* setNow). Ohne das liefen diese Zustandsaenderungen nach dem Rendern ins Leere
|
|
* und React meldete je Test drei „not wrapped in act(...)“-Warnungen
|
|
* (quick-260924-m4n).
|
|
*/
|
|
async function renderWidget(
|
|
props: { config?: Record<string, unknown>; isEditMode?: boolean } = {},
|
|
) {
|
|
let result!: ReturnType<typeof render>;
|
|
await act(async () => {
|
|
result = render(
|
|
<ProxmoxWidget
|
|
instanceId="w-1"
|
|
config={props.config ?? {}}
|
|
isEditMode={props.isEditMode ?? false}
|
|
/>,
|
|
);
|
|
});
|
|
return result;
|
|
}
|
|
|
|
beforeEach(() => {
|
|
mockUser({ role: 'USER' });
|
|
});
|
|
|
|
afterEach(() => {
|
|
cleanup();
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe('ProxmoxWidget: Balken und Zusammenfassung (quick-260924-i8v)', () => {
|
|
it('alle Server in Ordnung: „Alles in Ordnung“ in der Ok-Farbe', async () => {
|
|
mockListServers.mockResolvedValue([
|
|
makeServer('ok', { id: 'a', name: 'pve-a' }),
|
|
makeServer('ok', { id: 'b', name: 'pve-b' }),
|
|
]);
|
|
await renderWidget();
|
|
|
|
const summary = await screen.findByTestId('proxmox-summary');
|
|
expect(summary).toHaveTextContent('Alles in Ordnung');
|
|
expect(summary).toHaveClass('text-status-ok-fg');
|
|
});
|
|
|
|
it('je ein down, warn, ok: „1 nicht erreichbar, 1 mit Warnung“ in der Farbe von down', async () => {
|
|
mockListServers.mockResolvedValue([makeServer('ok'), makeServer('warn'), makeServer('down')]);
|
|
await renderWidget();
|
|
|
|
const summary = await screen.findByTestId('proxmox-summary');
|
|
expect(summary).toHaveTextContent('1 nicht erreichbar, 1 mit Warnung');
|
|
expect(summary).toHaveClass('text-status-down-fg');
|
|
expect(summary).toHaveAttribute('title', '1 nicht erreichbar, 1 mit Warnung');
|
|
});
|
|
|
|
it('zeigt den kompakten Balken (6 px, aria-hidden) ohne Legende', async () => {
|
|
mockListServers.mockResolvedValue([makeServer('ok'), makeServer('down')]);
|
|
await renderWidget();
|
|
|
|
const bar = await screen.findByTestId('health-bar');
|
|
expect(bar).toHaveAttribute('data-variant', 'compact');
|
|
expect(bar).toHaveClass('h-1.5');
|
|
expect(bar).toHaveAttribute('aria-hidden', 'true');
|
|
expect(bar.querySelectorAll('[data-segment]')).toHaveLength(2);
|
|
// Keine Legende: kein Listeneintrag im Balken, kein role="img"
|
|
expect(bar.querySelector('li')).toBeNull();
|
|
expect(screen.queryByRole('img')).toBeNull();
|
|
});
|
|
|
|
it('zeigt die Fehlerdetails eines nicht erreichbaren Servers nicht (T-I8V-05)', async () => {
|
|
mockListServers.mockResolvedValue([makeServer('down')]);
|
|
await renderWidget();
|
|
|
|
await screen.findByTestId('proxmox-summary');
|
|
expect(screen.queryByText(/ECONNREFUSED/)).toBeNull();
|
|
expect(screen.queryByText(/pve\.intern/)).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('ProxmoxWidget: Lade-, Leer- und Fehlerzustand (quick-260924-i8v)', () => {
|
|
it('waehrend des Ladens: pulsierende Leiste und Vorlesetext', async () => {
|
|
mockListServers.mockReturnValue(new Promise(() => {}));
|
|
await renderWidget();
|
|
|
|
expect(screen.getByText('Lade Serverliste...')).toHaveClass('sr-only');
|
|
});
|
|
|
|
it('leere Liste: Satz; ein Admin sieht den Link zu den Einstellungen', async () => {
|
|
mockUser({ role: 'ADMIN' });
|
|
mockListServers.mockResolvedValue([]);
|
|
await renderWidget();
|
|
|
|
expect(await screen.findByText('Noch kein Proxmox-Server eingetragen.')).toBeInTheDocument();
|
|
expect(screen.getByRole('link', { name: 'Zu den Einstellungen' })).toHaveAttribute(
|
|
'href',
|
|
'/modules/proxmox/settings',
|
|
);
|
|
});
|
|
|
|
it('leere Liste: ein Benutzer mit Rolle USER sieht keinen Link', async () => {
|
|
mockListServers.mockResolvedValue([]);
|
|
await renderWidget();
|
|
|
|
expect(await screen.findByText('Noch kein Proxmox-Server eingetragen.')).toBeInTheDocument();
|
|
expect(screen.queryByRole('link')).toBeNull();
|
|
expect(screen.queryByText('Zu den Einstellungen')).toBeNull();
|
|
});
|
|
|
|
it('leere Liste im Bearbeitungsmodus: der Hinweis fuer Admins steht ohne Link da', async () => {
|
|
mockUser({ role: 'SUPER_ADMIN' });
|
|
mockListServers.mockResolvedValue([]);
|
|
await renderWidget({ isEditMode: true });
|
|
|
|
expect(await screen.findByText('Zu den Einstellungen')).toBeInTheDocument();
|
|
expect(screen.queryByRole('link')).toBeNull();
|
|
});
|
|
|
|
it('Laden schlaegt fehl: „Die Serverliste konnte nicht geladen werden.“', async () => {
|
|
mockListServers.mockRejectedValue(new Error('500'));
|
|
await renderWidget();
|
|
|
|
expect(
|
|
await screen.findByText('Die Serverliste konnte nicht geladen werden.'),
|
|
).toBeInTheDocument();
|
|
});
|
|
|
|
it('ruft nie die manuelle Abfrage auf (T-I8V-02)', async () => {
|
|
mockListServers.mockResolvedValue([makeServer('ok'), makeServer('down')]);
|
|
await renderWidget();
|
|
|
|
await screen.findByTestId('proxmox-summary');
|
|
await waitFor(() => expect(mockListServers).toHaveBeenCalledTimes(1));
|
|
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();
|
|
});
|
|
});
|
|
|
|
describe('ProxmoxWidget: Bearbeitungsmodus mit Titel und Serverauswahl (quick-260924-i8v, Aufgabe 3)', () => {
|
|
beforeEach(() => {
|
|
mockUpdateWidgetConfig.mockResolvedValue(undefined);
|
|
});
|
|
|
|
it('Titelfeld speichert entprellt nach 1500 ms per updateWidgetConfig({ title })', async () => {
|
|
mockListServers.mockResolvedValue([makeServer('ok')]);
|
|
await renderWidget({ isEditMode: true, config: { title: 'Alt' } });
|
|
await screen.findByTestId('proxmox-summary');
|
|
|
|
const input = screen.getByLabelText('Titel') as HTMLInputElement;
|
|
expect(input.value).toBe('Alt');
|
|
expect(input).toHaveAttribute('placeholder', 'Titel (optional)');
|
|
expect(input).toHaveClass('widgetNoDrag');
|
|
|
|
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] });
|
|
try {
|
|
fireEvent.change(input, { target: { value: 'N' } });
|
|
fireEvent.change(input, { target: { value: 'Neu' } });
|
|
vi.advanceTimersByTime(1499);
|
|
expect(mockUpdateWidgetConfig).not.toHaveBeenCalled();
|
|
vi.advanceTimersByTime(1);
|
|
expect(mockUpdateWidgetConfig).toHaveBeenCalledTimes(1);
|
|
expect(mockUpdateWidgetConfig).toHaveBeenCalledWith('w-1', { title: 'Neu' });
|
|
} finally {
|
|
vi.useRealTimers();
|
|
}
|
|
});
|
|
|
|
it('im Bearbeitungsmodus steht die Kopfzeile auch ohne Titel, mit Abstand zur Griffleiste', async () => {
|
|
mockListServers.mockResolvedValue([makeServer('ok')]);
|
|
const { container } = await renderWidget({ isEditMode: true });
|
|
await screen.findByTestId('proxmox-summary');
|
|
|
|
expect(screen.getByLabelText('Titel')).toBeInTheDocument();
|
|
expect(screen.queryByRole('heading')).toBeNull();
|
|
expect(container.querySelector('[data-testid="proxmox-widget"]')).toHaveClass('pt-5');
|
|
});
|
|
|
|
it('„Server auswählen“ oeffnet die Auswahl an Stelle der Liste; eine Aenderung speichert sofort und filtert', async () => {
|
|
mockListServers.mockResolvedValue([
|
|
makeServer('ok', { id: 'a', name: 'pve-a', position: 0 }),
|
|
makeServer('down', { id: 'b', name: 'pve-b', position: 1 }),
|
|
]);
|
|
await renderWidget({ isEditMode: true });
|
|
expect(await screen.findByTestId('proxmox-summary')).toHaveTextContent('1 nicht erreichbar');
|
|
|
|
const toggle = screen.getByRole('button', { name: 'Server auswählen' });
|
|
expect(toggle).toHaveAttribute('aria-expanded', 'false');
|
|
expect(toggle).toHaveAttribute('data-no-drag');
|
|
fireEvent.click(toggle);
|
|
expect(toggle).toHaveAttribute('aria-expanded', 'true');
|
|
|
|
expect(screen.queryByTestId('proxmox-list')).toBeNull();
|
|
const group = screen.getByRole('group', { name: 'Angezeigte Server' });
|
|
expect(group.closest('.widgetNoDrag')).not.toBeNull();
|
|
|
|
fireEvent.click(screen.getByRole('checkbox', { name: /pve-a/ }));
|
|
|
|
expect(mockUpdateWidgetConfig).toHaveBeenCalledWith('w-1', { serverIds: ['a'] });
|
|
expect(screen.getByTestId('proxmox-summary')).toHaveTextContent('Alles in Ordnung');
|
|
// Kein zusaetzlicher Abruf fuer die Auswahl
|
|
expect(mockListServers).toHaveBeenCalledTimes(1);
|
|
expect(mockPollServer).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('die Auswahl bekommt die ungefilterte Liste', async () => {
|
|
mockListServers.mockResolvedValue([
|
|
makeServer('ok', { id: 'a', name: 'pve-a' }),
|
|
makeServer('ok', { id: 'b', name: 'pve-b' }),
|
|
]);
|
|
await renderWidget({ isEditMode: true, config: { serverIds: ['a'] } });
|
|
await screen.findByTestId('proxmox-summary');
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'Server auswählen' }));
|
|
|
|
expect(screen.getByRole('checkbox', { name: /pve-a/ })).toBeChecked();
|
|
expect(screen.getByRole('checkbox', { name: /pve-b/ })).not.toBeChecked();
|
|
});
|
|
|
|
it('Verlassen des Bearbeitungsmodus schliesst die Auswahl', async () => {
|
|
mockListServers.mockResolvedValue([makeServer('ok')]);
|
|
const ui = (isEditMode: boolean) => (
|
|
<NextIntlClientProvider locale="de" messages={de} timeZone="Europe/Berlin">
|
|
<ProxmoxWidget instanceId="w-1" config={{}} isEditMode={isEditMode} />
|
|
</NextIntlClientProvider>
|
|
);
|
|
let rerender!: ReturnType<typeof rtlRender>['rerender'];
|
|
await act(async () => {
|
|
({ rerender } = rtlRender(ui(true)));
|
|
});
|
|
await screen.findByTestId('proxmox-summary');
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: 'Server auswählen' }));
|
|
expect(screen.getByRole('group', { name: 'Angezeigte Server' })).toBeInTheDocument();
|
|
|
|
rerender(ui(false));
|
|
expect(screen.queryByRole('group', { name: 'Angezeigte Server' })).toBeNull();
|
|
expect(screen.getByTestId('proxmox-list')).toBeInTheDocument();
|
|
|
|
rerender(ui(true));
|
|
expect(screen.getByRole('button', { name: 'Server auswählen' })).toHaveAttribute(
|
|
'aria-expanded',
|
|
'false',
|
|
);
|
|
});
|
|
});
|