This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
import { cleanup, render, screen, waitFor, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import App from '../../src/App';
|
||||
import { resetSystemStatusForTests } from '../../src/systemStatus';
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
resetSystemStatusForTests();
|
||||
vi.unstubAllGlobals();
|
||||
window.history.replaceState({}, '', '/');
|
||||
});
|
||||
|
||||
function json(value: unknown, status = 200): Response {
|
||||
return new Response(JSON.stringify(value), { status, headers: { 'Content-Type': 'application/json' } });
|
||||
}
|
||||
|
||||
const healthySource = { state: 'healthy', freshness: 'fresh' };
|
||||
|
||||
describe('Stitch command overview', () => {
|
||||
it('shows real source values while failed sources remain explicitly unavailable', async () => {
|
||||
window.history.replaceState({}, '', '/');
|
||||
const now = new Date().toISOString();
|
||||
vi.stubGlobal('fetch', vi.fn(async (input: RequestInfo | URL) => {
|
||||
const url = String(input);
|
||||
if (url === '/api/v1/system/status') return json({
|
||||
version: '1', generatedAt: now, overallState: 'healthy', components: [
|
||||
{ id: 'database', state: 'healthy', reason: 'ok' },
|
||||
{ id: 'prometheus', state: 'healthy', reason: 'ok' },
|
||||
], backup: { state: 'disabled', reason: 'not_configured' }, sourceLag: [{ sourceId: 'prometheus', state: 'degraded', reason: 'source_stale', ageSeconds: 420 }],
|
||||
});
|
||||
if (url === '/api/v1/host') return json({
|
||||
identity: { name: 'tower-lab' },
|
||||
cpu: { totalPercent: 37.5, perCore: [25, 50] },
|
||||
memory: { utilizationPercent: 62.25 },
|
||||
source: healthySource,
|
||||
});
|
||||
if (url.startsWith('/api/v1/containers')) return json({ source: healthySource, containers: [{ id: 'running', state: 'RUNNING', health: 'healthy' }, { id: 'stopped', state: 'stopped', health: 'unknown' }], total: 2 });
|
||||
if (url.startsWith('/api/v1/services')) return json({ capabilityState: 'available', configurationState: 'configured', services: [{ id: 'svc-1', name: 'API', state: 'up' }], total: 1 });
|
||||
if (url.startsWith('/api/v1/incidents')) return json({ items: [] });
|
||||
if (url.startsWith('/api/v1/pools')) return json({ error: 'source unavailable' }, 503);
|
||||
return json({ error: 'unexpected request' }, 404);
|
||||
}));
|
||||
|
||||
render(<App />);
|
||||
|
||||
expect((await screen.findAllByText('37,5%'))[0]).toBeVisible();
|
||||
expect(document.querySelector('.instrument-band')).toBeInTheDocument();
|
||||
expect(document.querySelector('.data-plane')).toBeInTheDocument();
|
||||
expect(document.querySelector('.focus-panel')).toBeInTheDocument();
|
||||
expect(document.querySelector('.context-inspector')).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: 'Signaalpad' })).toBeVisible();
|
||||
const unavailableStorage = screen.getByRole('button', { name: /Opslag: Niet beschikbaar/ });
|
||||
expect(unavailableStorage.closest('.signal-path-stage')).toHaveAttribute('data-tone', 'unknown');
|
||||
const staleSource = screen.getByRole('button', { name: /Bronnen: Aandacht.*0\/1/ });
|
||||
expect(staleSource.closest('.signal-path-stage')).toHaveAttribute('data-tone', 'attention');
|
||||
expect(screen.getAllByText('62,3%')[0]).toBeVisible();
|
||||
expect(screen.getByText('tower-lab')).toBeVisible();
|
||||
expect(screen.getByRole('button', { name: /Workloads: Kritiek.*1\/2/ })).toBeVisible();
|
||||
expect(screen.queryByText('0 pools')).not.toBeInTheDocument();
|
||||
const storageCard = screen.getByRole('heading', { name: 'Capaciteit en toestand' }).closest('article');
|
||||
expect(storageCard).not.toBeNull();
|
||||
expect(within(storageCard!).getByText(/Deze overzichtsbron kon niet worden geladen/)).toBeVisible();
|
||||
expect(screen.getByText('Opslag: Niet beschikbaar')).toBeVisible();
|
||||
expect(screen.getByRole('heading', { name: 'Aandacht vereist' })).toBeVisible();
|
||||
});
|
||||
|
||||
it('suppresses current workload claims when retained container data is stale', async () => {
|
||||
const now = new Date().toISOString();
|
||||
vi.stubGlobal('fetch', vi.fn(async (input: RequestInfo | URL) => {
|
||||
const url = String(input);
|
||||
if (url === '/api/v1/system/status') return json({ version: '1', generatedAt: now, overallState: 'healthy', components: [], backup: { state: 'disabled', reason: 'not_configured' }, sourceLag: [{ sourceId: 'unraid', state: 'healthy', reason: 'ok' }] });
|
||||
if (url === '/api/v1/host') return json({ source: healthySource, cpu: { totalPercent: 30 }, memory: { utilizationPercent: 40 } });
|
||||
if (url.startsWith('/api/v1/containers')) return json({ source: { state: 'unknown', freshness: 'stale' }, containers: [{ id: 'retained', state: 'running', health: 'healthy' }], total: 1 });
|
||||
if (url.startsWith('/api/v1/pools')) return json({ source: healthySource, pools: [], total: 0 });
|
||||
if (url.startsWith('/api/v1/services')) return json({ capabilityState: 'available', configurationState: 'not_configured', services: [], total: 0 });
|
||||
if (url.startsWith('/api/v1/incidents')) return json({ items: [] });
|
||||
return json({}, 404);
|
||||
}));
|
||||
|
||||
render(<App />);
|
||||
|
||||
expect(await screen.findByRole('button', { name: /Workloads: Verouderd.*actieve containers: —/ })).toBeVisible();
|
||||
const workloadCard = screen.getByRole('heading', { name: 'Containers' }).closest('article');
|
||||
expect(workloadCard).not.toBeNull();
|
||||
expect(within(workloadCard!).getByText(/laatst bekende waarden worden niet als actueel getoond/i)).toBeVisible();
|
||||
expect(within(workloadCard!).queryByText(/1 van 1 containers zijn actief/)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('settles independent resources while a single endpoint is still pending', async () => {
|
||||
const now = new Date().toISOString();
|
||||
vi.stubGlobal('fetch', vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url = String(input);
|
||||
if (url === '/api/v1/system/status') return json({ version: '1', generatedAt: now, overallState: 'healthy', components: [], backup: { state: 'disabled', reason: 'not_configured' }, sourceLag: [{ sourceId: 'unraid', state: 'healthy', reason: 'ok' }] });
|
||||
if (url === '/api/v1/host') return json({ source: healthySource, cpu: { totalPercent: 30 }, memory: { utilizationPercent: 40 } });
|
||||
if (url.startsWith('/api/v1/containers')) return json({ source: healthySource, containers: [], total: 0 });
|
||||
if (url.startsWith('/api/v1/pools')) return json({ source: healthySource, pools: [{ id: 'cache', name: 'Cache', state: 'healthy', capacitySeverity: 'normal', utilizationPercent: 50 }], total: 1 });
|
||||
if (url.startsWith('/api/v1/services')) return json({ capabilityState: 'available', configurationState: 'configured', services: [{ id: 'api', name: 'API', state: 'up' }], total: 1 });
|
||||
if (url.startsWith('/api/v1/incidents')) return new Promise<Response>((_resolve, reject) => {
|
||||
init?.signal?.addEventListener('abort', () => reject(new DOMException('aborted', 'AbortError')), { once: true });
|
||||
});
|
||||
return json({}, 404);
|
||||
}));
|
||||
|
||||
render(<App />);
|
||||
|
||||
expect(await screen.findByRole('button', { name: /Host: Gezond.*30%.*40%/ })).toBeVisible();
|
||||
expect(screen.getByRole('button', { name: /Incidenten: Wordt geladen/ })).toBeVisible();
|
||||
expect(screen.getByRole('heading', { level: 1, name: 'Status nog niet bevestigd' })).toBeVisible();
|
||||
const incidentCard = screen.getByRole('heading', { name: 'Incidenten' }).closest('article');
|
||||
expect(incidentCard).not.toBeNull();
|
||||
expect(within(incidentCard!).getByText(/wordt geladen; er wordt nog geen toestand verondersteld/)).toBeVisible();
|
||||
});
|
||||
|
||||
it('does not present an unavailable incident feed as an empty healthy feed', async () => {
|
||||
const now = new Date().toISOString();
|
||||
vi.stubGlobal('fetch', vi.fn(async (input: RequestInfo | URL) => {
|
||||
const url = String(input);
|
||||
if (url === '/api/v1/system/status') return json({ version: '1', generatedAt: now, overallState: 'healthy', components: [], backup: { state: 'disabled', reason: 'not_configured' }, sourceLag: [{ sourceId: 'unraid', state: 'healthy', reason: 'ok' }] });
|
||||
if (url === '/api/v1/host') return json({ source: healthySource, cpu: { totalPercent: 30 }, memory: { utilizationPercent: 40 } });
|
||||
if (url.startsWith('/api/v1/containers')) return json({ source: healthySource, containers: [], total: 0 });
|
||||
if (url.startsWith('/api/v1/pools')) return json({ source: healthySource, pools: [], total: 0 });
|
||||
if (url.startsWith('/api/v1/services')) return json({ capabilityState: 'available', configurationState: 'not_configured', services: [], total: 0 });
|
||||
if (url.startsWith('/api/v1/incidents')) return json({ code: 'UNAVAILABLE' }, 503);
|
||||
return json({}, 404);
|
||||
}));
|
||||
|
||||
render(<App />);
|
||||
|
||||
expect(await screen.findByRole('button', { name: /Incidenten: Niet beschikbaar/ })).toBeVisible();
|
||||
const incidentCard = screen.getByRole('heading', { name: 'Incidenten' }).closest('article');
|
||||
expect(incidentCard).not.toBeNull();
|
||||
expect(within(incidentCard!).getByText(/Deze overzichtsbron kon niet worden geladen/)).toBeVisible();
|
||||
expect(within(incidentCard!).queryByText('Er zijn geen open incidenten geregistreerd.')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('distinguishes forbidden resources from an expired session', async () => {
|
||||
const now = new Date().toISOString();
|
||||
vi.stubGlobal('fetch', vi.fn(async (input: RequestInfo | URL) => {
|
||||
const url = String(input);
|
||||
if (url === '/api/v1/system/status') return json({ code: 'FORBIDDEN' }, 403);
|
||||
if (url === '/api/v1/host') return json({ source: healthySource, cpu: { totalPercent: 30 }, memory: { utilizationPercent: 40 } });
|
||||
if (url.startsWith('/api/v1/containers')) return json({ source: healthySource, containers: [], total: 0 });
|
||||
if (url.startsWith('/api/v1/pools')) return json({ code: 'FORBIDDEN' }, 403);
|
||||
if (url.startsWith('/api/v1/services')) return json({ capabilityState: 'available', configurationState: 'not_configured', services: [], total: 0 });
|
||||
if (url.startsWith('/api/v1/incidents')) return json({ items: [] });
|
||||
return json({}, 404);
|
||||
}));
|
||||
|
||||
render(<App />);
|
||||
|
||||
expect(await screen.findByRole('button', { name: /Bronnen: Geen toegang/ })).toBeVisible();
|
||||
expect(await screen.findByRole('button', { name: /Opslag: Geen toegang/ })).toBeVisible();
|
||||
expect(screen.getByText('Opslag: Geen toegang')).toBeVisible();
|
||||
expect(screen.getAllByText(/account heeft geen toegang tot deze overzichtsbron/).length).toBeGreaterThan(0);
|
||||
expect(screen.queryByRole('link', { name: /Aanmelden/ })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('prioriteert kritieke poolcapaciteit boven gezonde device-health', async () => {
|
||||
const now = new Date().toISOString();
|
||||
vi.stubGlobal('fetch', vi.fn(async (input: RequestInfo | URL) => {
|
||||
const url = String(input);
|
||||
if (url === '/api/v1/system/status') return json({ version: '1', generatedAt: now, overallState: 'healthy', components: [], backup: { state: 'disabled', reason: 'not_configured' }, sourceLag: [] });
|
||||
if (url.startsWith('/api/v1/pools')) return json({ source: healthySource, pools: [{ id: 'ssd', name: 'ssd', state: 'healthy', capacitySeverity: 'critical', utilizationPercent: 98.8 }], total: 1 });
|
||||
if (url.startsWith('/api/v1/containers')) return json({ source: healthySource, containers: [], total: 0 });
|
||||
if (url.startsWith('/api/v1/services')) return json({ capabilityState: 'available', configurationState: 'not_configured', services: [], total: 0 });
|
||||
if (url.startsWith('/api/v1/incidents')) return json({ items: [] });
|
||||
if (url === '/api/v1/host') return json({ source: healthySource });
|
||||
return json({}, 404);
|
||||
}));
|
||||
|
||||
render(<App />);
|
||||
|
||||
expect(await screen.findByText('ssd: Kritiek')).toBeVisible();
|
||||
expect(screen.getByText(/kritieke capaciteitsgrens is overschreden/)).toBeVisible();
|
||||
expect(screen.getByText('Kritiek · device-health gezond')).toBeVisible();
|
||||
expect(await screen.findByRole('heading', { name: 'Aandacht vereist' })).toBeVisible();
|
||||
await waitFor(() => expect(screen.getByRole('button', { name: /Opslag: Kritiek/ })).toHaveAttribute('aria-pressed', 'true'));
|
||||
});
|
||||
|
||||
it('retries every overview resource from the shared retry action', async () => {
|
||||
const user = userEvent.setup();
|
||||
const now = new Date().toISOString();
|
||||
let poolCalls = 0;
|
||||
vi.stubGlobal('fetch', vi.fn(async (input: RequestInfo | URL) => {
|
||||
const url = String(input);
|
||||
if (url === '/api/v1/system/status') return json({ version: '1', generatedAt: now, overallState: 'healthy', components: [{ id: 'database', state: 'healthy', reason: 'ok' }], backup: { state: 'disabled', reason: 'not_configured' }, sourceLag: [] });
|
||||
if (url === '/api/v1/host') return json({ source: healthySource, cpu: { totalPercent: 30 }, memory: { utilizationPercent: 40 } });
|
||||
if (url.startsWith('/api/v1/containers')) return json({ source: healthySource, containers: [], total: 0 });
|
||||
if (url.startsWith('/api/v1/services')) return json({ capabilityState: 'available', configurationState: 'not_configured', services: [], total: 0 });
|
||||
if (url.startsWith('/api/v1/incidents')) return json({ items: [] });
|
||||
if (url.startsWith('/api/v1/pools')) {
|
||||
poolCalls += 1;
|
||||
if (poolCalls === 1) return json({ code: 'POOLS_UNAVAILABLE' }, 503);
|
||||
return json({ source: healthySource, pools: [{ id: 'cache', name: 'Cache', state: 'healthy', capacitySeverity: 'normal', utilizationPercent: 50 }], total: 1 });
|
||||
}
|
||||
return json({}, 404);
|
||||
}));
|
||||
|
||||
render(<App />);
|
||||
expect(await screen.findByText('Opslag: Niet beschikbaar')).toBeVisible();
|
||||
await user.click(screen.getByRole('button', { name: 'Opnieuw laden' }));
|
||||
|
||||
expect(await screen.findByRole('button', { name: /Opslag: Gezond/ })).toBeVisible();
|
||||
expect(poolCalls).toBe(2);
|
||||
expect(screen.queryByText('Opslag: Niet beschikbaar')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('reads the bounded container target scale and marks truncated services partial', async () => {
|
||||
const now = new Date().toISOString();
|
||||
const containers = Array.from({ length: 150 }, (_, index) => ({ id: `container-${String(index).padStart(3, '0')}`, state: 'running', health: 'healthy' }));
|
||||
const services = Array.from({ length: 100 }, (_, index) => ({ id: `service-${String(index).padStart(3, '0')}`, name: `Service ${index}`, state: 'up' }));
|
||||
let containerCalls = 0;
|
||||
vi.stubGlobal('fetch', vi.fn(async (input: RequestInfo | URL) => {
|
||||
const raw = String(input);
|
||||
const url = new URL(raw, 'http://pulse.test');
|
||||
if (raw === '/api/v1/system/status') return json({ version: '1', generatedAt: now, overallState: 'healthy', components: [{ id: 'database', state: 'healthy', reason: 'ok' }], backup: { state: 'disabled', reason: 'not_configured' }, sourceLag: [] });
|
||||
if (url.pathname === '/api/v1/host') return json({ source: healthySource, cpu: { totalPercent: 30 }, memory: { utilizationPercent: 40 } });
|
||||
if (url.pathname === '/api/v1/containers') {
|
||||
containerCalls += 1;
|
||||
const offset = Number(url.searchParams.get('after') ?? 0);
|
||||
const page = containers.slice(offset, offset + 100);
|
||||
return json({ source: healthySource, containers: page, total: containers.length, nextCursor: offset + page.length < containers.length ? String(offset + page.length) : undefined });
|
||||
}
|
||||
if (url.pathname === '/api/v1/pools') return json({ source: healthySource, pools: [{ id: 'cache', name: 'Cache', state: 'healthy', capacitySeverity: 'normal', utilizationPercent: 50 }], total: 1 });
|
||||
if (url.pathname === '/api/v1/services') return json({ capabilityState: 'available', configurationState: 'configured', services, total: 150 });
|
||||
if (url.pathname === '/api/v1/incidents') return json({ items: [] });
|
||||
return json({}, 404);
|
||||
}));
|
||||
|
||||
render(<App />);
|
||||
|
||||
expect(await screen.findByRole('button', { name: /Workloads: Gezond.*150\/150/ })).toBeVisible();
|
||||
const serviceStage = screen.getByRole('button', { name: /Services: Gedeeltelijke data.*≥100\/150/ });
|
||||
expect(serviceStage.closest('.signal-path-stage')).toHaveAttribute('data-tone', 'unknown');
|
||||
expect(screen.getByText('Services: Gedeeltelijke data')).toBeVisible();
|
||||
expect(containerCalls).toBe(2);
|
||||
});
|
||||
|
||||
it('bounds container paging and does not inflate totals with overlapping rows', async () => {
|
||||
const now = new Date().toISOString();
|
||||
let containerCalls = 0;
|
||||
vi.stubGlobal('fetch', vi.fn(async (input: RequestInfo | URL) => {
|
||||
const raw = String(input);
|
||||
const url = new URL(raw, 'http://pulse.test');
|
||||
if (raw === '/api/v1/system/status') return json({ version: '1', generatedAt: now, overallState: 'healthy', components: [], backup: { state: 'disabled', reason: 'not_configured' }, sourceLag: [{ sourceId: 'unraid', state: 'healthy', reason: 'ok' }] });
|
||||
if (url.pathname === '/api/v1/host') return json({ source: healthySource, cpu: { totalPercent: 30 }, memory: { utilizationPercent: 40 } });
|
||||
if (url.pathname === '/api/v1/containers') {
|
||||
containerCalls += 1;
|
||||
const start = url.searchParams.get('after') === '100' ? 99 : url.searchParams.get('after') === '200' ? 199 : 0;
|
||||
const items = Array.from({ length: 100 }, (_, index) => ({ id: `container-${start + index}`, state: 'running', health: 'healthy' }));
|
||||
const nextCursor = containerCalls === 1 ? '100' : containerCalls === 2 ? '200' : '300';
|
||||
return json({ source: healthySource, containers: items, total: 400, nextCursor });
|
||||
}
|
||||
if (url.pathname === '/api/v1/pools') return json({ source: healthySource, pools: [], total: 0 });
|
||||
if (url.pathname === '/api/v1/services') return json({ capabilityState: 'available', configurationState: 'not_configured', services: [], total: 0 });
|
||||
if (url.pathname === '/api/v1/incidents') return json({ items: [] });
|
||||
return json({}, 404);
|
||||
}));
|
||||
|
||||
render(<App />);
|
||||
|
||||
const workloadStage = await screen.findByRole('button', { name: /Workloads: Gedeeltelijke data.*≥299\/400/ });
|
||||
expect(workloadStage.closest('.signal-path-stage')).toHaveAttribute('data-tone', 'unknown');
|
||||
expect(screen.getByText('Workloads: Gedeeltelijke data')).toBeVisible();
|
||||
expect(containerCalls).toBe(3);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user