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 { 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); });