Files
ITWorx-Pulse-Public/apps/web/tests/e2e/sol-ultra.spec.ts
T
ITWorx Pulse release export bd774932d5
Public source validation / validate (push) Failing after 3m8s
Publish ITWorx Pulse source
2026-09-03 02:09:19 +02:00

131 lines
7.3 KiB
TypeScript

import AxeBuilder from '@axe-core/playwright';
import { expect, test, type Page } from '@playwright/test';
import path from 'node:path';
const observedAt = new Date().toISOString();
async function mockSignalFlow(page: Page): Promise<void> {
await page.route('**/api/v1/**', async (route) => {
const pathname = new URL(route.request().url()).pathname;
const body = pathname === '/api/v1/system/status' ? {
version: '1', generatedAt: observedAt, overallState: 'degraded',
components: [
{ id: 'database', state: 'healthy', reason: 'ok' },
{ id: 'worker', state: 'healthy', reason: 'ok' },
{ id: 'prometheus', state: 'degraded', reason: 'source_stale' },
{ id: 'unraid', state: 'healthy', reason: 'ok' },
{ id: 'storage', state: 'healthy', reason: 'ok' },
],
backup: { state: 'healthy', reason: 'ok', ageSeconds: 3600 },
sourceLag: [{ sourceId: 'prometheus', state: 'degraded', reason: 'source_stale', ageSeconds: 420 }],
} : pathname === '/api/v1/host' ? {
identity: { name: 'tower' }, cpu: { totalPercent: 68.4, perCore: [72, 64, 66, 71] },
memory: { utilizationPercent: 71.2 }, source: { state: 'healthy', freshness: 'fresh' },
} : pathname === '/api/v1/containers' ? {
source: { state: 'healthy', freshness: 'fresh' }, total: 6,
containers: [
{ id: 'one', state: 'running', health: 'healthy' }, { id: 'two', state: 'running', health: 'healthy' },
{ id: 'three', state: 'running', health: 'healthy' }, { id: 'four', state: 'running', health: 'healthy' },
{ id: 'five', state: 'running', health: 'healthy' }, { id: 'six', state: 'restarting', health: 'unknown' },
],
} : pathname === '/api/v1/pools' ? {
source: { state: 'healthy', freshness: 'fresh' }, total: 2,
pools: [
{ id: 'cache', name: 'Cache', state: 'healthy', capacitySeverity: 'normal', utilizationPercent: 63.8 },
{ id: 'array', name: 'Array', state: 'healthy', capacitySeverity: 'attention', utilizationPercent: 86.1 },
],
} : pathname === '/api/v1/services' ? {
capabilityState: 'available', configurationState: 'configured', total: 3,
services: [
{ id: 'proxy', name: 'Reverse proxy', state: 'up' },
{ id: 'auth', name: 'Authentik', state: 'up' },
{ id: 'media', name: 'Media', state: 'degraded' },
],
} : pathname === '/api/v1/incidents' ? {
items: [{ id: 'incident-1', title: 'Prometheus-bron loopt achter', severity: 'warning', startedAt: observedAt }],
} : pathname === '/api/v1/dashboards' ? { items: [] } : {};
await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(body) });
});
}
test.beforeEach(async ({ page }) => mockSignalFlow(page));
test('operational signal path is diagnostic, keyboard reachable and responsive', async ({ page }, testInfo) => {
test.skip(testInfo.project.name === 'wallboard-chromium', 'The wallboard keeps its dedicated bounded composition.');
const runtimeErrors: string[] = [];
page.on('pageerror', (error) => runtimeErrors.push(error.message));
page.on('console', (message) => { if (message.type() === 'error') runtimeErrors.push(message.text()); });
await page.goto('/');
const panel = page.locator('.signal-path-panel');
await expect(panel).toBeVisible();
await expect(panel.getByRole('heading', { name: 'Signaalpad' })).toBeVisible();
const stages = panel.locator('.signal-path-stage button');
await expect(stages).toHaveCount(6);
await expect(stages.first()).toHaveAttribute('aria-pressed', 'true');
await stages.nth(1).focus();
await page.keyboard.press('Enter');
await expect(stages.nth(1)).toHaveAttribute('aria-pressed', 'true');
await expect(panel.locator('.signal-path-inspector')).toContainText('68,4%');
await expect(panel.locator('.signal-path-inspector')).toContainText('71,2%');
await expect(panel.getByRole('button', { name: 'Open host' })).toBeVisible();
const stageHeights = await stages.evaluateAll((items) => items.map((item) => item.getBoundingClientRect().height));
expect(stageHeights.every((height) => height >= 44)).toBe(true);
expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth + 1)).toBe(true);
if (testInfo.project.name === 'desktop-chromium') {
const severe = (await new AxeBuilder({ page }).analyze()).violations.filter((violation) => violation.impact === 'critical' || violation.impact === 'serious');
expect(severe, severe.map((violation) => `${violation.id}: ${violation.help}`).join('\n')).toEqual([]);
}
if (process.env.PULSE_SOL_ULTRA_CAPTURE) {
await page.evaluate(() => { window.scrollTo(0, 0); (document.activeElement as HTMLElement | null)?.blur(); });
const mobile = testInfo.project.name === 'mobile-chromium';
await page.screenshot({ path: path.resolve('../../artifacts/evidence/SOL-ULTRA/visual', `overview-${testInfo.project.name}.png`), fullPage: !mobile });
if (mobile) {
await page.addStyleTag({ content: '.context-bar, .mobile-navigation { display: none !important; }' });
await panel.screenshot({ path: path.resolve('../../artifacts/evidence/SOL-ULTRA/visual/overview-mobile-chromium-signal.png') });
}
}
expect(runtimeErrors).toEqual([]);
if (testInfo.project.name === 'desktop-chromium') {
await panel.getByRole('button', { name: 'Open host' }).click();
await expect(page).toHaveURL(/\/host$/);
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
}
});
test('failed overview resources remain unknown and recover through the shared retry', async ({ page }, testInfo) => {
test.skip(testInfo.project.name !== 'desktop-chromium', 'The data-state contract is viewport independent.');
let poolCalls = 0;
let allowPoolRecovery = false;
await page.route('**/api/v1/pools?**', async (route) => {
poolCalls += 1;
if (!allowPoolRecovery) {
await route.fulfill({ status: 503, contentType: 'application/problem+json', body: JSON.stringify({ code: 'SOURCE_UNAVAILABLE' }) });
return;
}
await route.fallback();
});
await page.goto('/');
const storage = page.locator('.signal-path-stage').filter({ hasText: 'Opslag' });
await expect(storage).toHaveAttribute('data-tone', 'unknown');
await expect(storage).toContainText('Niet beschikbaar');
await expect(page.locator('.overview-kpi').filter({ hasText: 'Hoogste poolgebruik' }).locator('strong')).toHaveText('—');
await expect(page.locator('.capacity-plane')).toContainText('Deze overzichtsbron kon niet worden geladen');
await expect(page.getByRole('heading', { level: 1, name: 'Aandacht vereist' })).toBeVisible();
allowPoolRecovery = true;
await page.getByRole('button', { name: 'Opnieuw laden' }).click();
await expect(storage).toContainText('Aandacht');
await expect(storage).not.toContainText('Niet beschikbaar');
expect(poolCalls).toBeGreaterThanOrEqual(2);
});
test('signal animation yields to reduced-motion preference', async ({ page }, testInfo) => {
test.skip(testInfo.project.name !== 'desktop-chromium', 'Reduced-motion CSS is viewport independent.');
await page.emulateMedia({ reducedMotion: 'reduce' });
await page.goto('/');
const animation = await page.locator('.signal-path-stage--healthy').first().evaluate((element) => getComputedStyle(element, '::before').animationDuration);
expect(Number.parseFloat(animation)).toBeLessThanOrEqual(0.001);
});