Files
ITWorx-Pulse-Public/apps/web/tests/unit/MetricWidgets.test.tsx
T
2026-09-30 01:14:48 +00:00

98 lines
4.7 KiB
TypeScript

import { render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, expect, it, vi } from 'vitest';
import { MetricWidget, RankedListWidget, StatusGridWidget, metricStatus, type MetricWidgetProps } from '../../src/MetricWidgets';
const now = Date.parse('2026-08-10T04:00:00Z');
const freshPoint = { timestamp: now, value: 24, freshness: 'fresh' as const, labels: { host: 'tower' } };
function props(overrides: Partial<MetricWidgetProps> = {}): MetricWidgetProps {
return {
kind: 'stat',
series: [{ key: 'tower', points: [freshPoint] }],
freshness: 'fresh',
expectedStepSeconds: 15,
availability: 'success',
metricName: 'host.cpu.utilization',
visualization: { unit: 'percent', decimals: 0 },
...overrides,
};
}
describe('metricStatus ADR-0008 invariant', () => {
it('marks only fresh, available data as ready', () => {
expect(metricStatus(props())).toMatchObject({ tone: 'ready' });
});
it.each([
props({ availability: 'idle' }),
props({ availability: 'loading' }),
props({ availability: 'connecting' }),
props({ availability: 'error' }),
props({ series: [] }),
props({ freshness: 'stale' }),
props({ freshness: 'unavailable' }),
props({ series: [{ key: 'tower', points: [{ ...freshPoint, freshness: 'delayed' }] }] }),
])('never presents missing, stale or unavailable telemetry as ready', (value) => {
expect(metricStatus(value).tone).toBe('unknown');
});
it('renders an explicit unavailable notice without a numeric value', () => {
render(<MetricWidget {...props({ freshness: 'unavailable', series: [], availability: 'error', error: 'Bron tijdelijk onbereikbaar.' })} />);
expect(screen.getByText('Bron tijdelijk onbereikbaar.')).toBeVisible();
expect(screen.getByText(/De waarde wordt niet als gezond geïnterpreteerd/)).toBeVisible();
expect(screen.queryByText('24 %')).not.toBeInTheDocument();
});
});
describe('interactive widgets', () => {
it('presents metric series labels as human-readable values without raw JSON', () => {
const rawKey = '{"interface":"eth0","host":"tower"}';
render(<MetricWidget {...props({
kind: 'timeseries',
series: [{ key: rawKey, points: [
{ ...freshPoint, timestamp: now - 15_000, labels: { __name__: 'host.network.receive', host: 'tower', interface: 'eth0' } },
{ ...freshPoint, labels: { __name__: 'host.network.receive', host: 'tower', interface: 'eth0' } },
] }],
metricName: 'host.network.receive',
})} />);
expect(screen.getByRole('list', { name: 'Legenda' })).toHaveTextContent('tower · eth0');
expect(screen.getByRole('img', { name: /Tijdreeks voor Goedgekeurde meting/ })).toBeVisible();
expect(document.querySelector('.metric-chart-line')).toHaveAttribute('d', 'M 28.000 192.000 L 628.000 192.000');
expect(document.body).not.toHaveTextContent(rawKey);
});
it('supports series toggles, bounded zoom, a pointer crosshair and annotations', async () => {
const user = userEvent.setup();
const view = render(<MetricWidget {...props({ kind: 'timeseries', series: [
{ key: 'tower', points: [{ ...freshPoint, timestamp: now - 30_000 }, freshPoint] },
{ key: 'backup', points: [{ ...freshPoint, timestamp: now - 30_000, labels: { host: 'backup' } }, { ...freshPoint, labels: { host: 'backup' } }] },
], annotations: [{ id: 'incident-1', label: 'Opslagincident', timestamp: now, tone: 'critical' }] })} />);
const widget = within(view.container);
const backup = widget.getByRole('button', { name: 'backup' });
expect(backup).toHaveAttribute('aria-pressed', 'true');
await user.click(backup);
expect(backup).toHaveAttribute('aria-pressed', 'false');
await user.click(widget.getByRole('button', { name: 'Zoom op recente helft' }));
expect(widget.getByRole('button', { name: 'Volledige periode' })).toHaveAttribute('aria-pressed', 'true');
expect(widget.getByRole('list', { name: 'Incident- en meldingsannotaties' })).toHaveTextContent('Opslagincident');
});
it('supports keyboard activation of ranked rows', async () => {
const onSelect = vi.fn();
render(<RankedListWidget items={[{ id: 'disk-1', label: 'Disk 1', value: '78%' }]} onSelect={onSelect} />);
const row = screen.getByRole('button', { name: /Disk 1/ });
row.focus();
await userEvent.keyboard('{Enter}');
expect(onSelect).toHaveBeenCalledWith('disk-1');
});
it('localizes status codes in status-grid fallbacks', () => {
render(<StatusGridWidget items={[{ id: 'pool-1', label: 'Cachepool', status: 'degraded' }]} onSelect={vi.fn()} />);
expect(screen.getByText('Verstoord')).toBeVisible();
expect(screen.queryByText('degraded')).not.toBeInTheDocument();
});
});