import AxeBuilder from '@axe-core/playwright'; import { expect, test, type Page } from '@playwright/test'; import path from 'node:path'; const generatedAt = new Date().toISOString(); async function mockAPI(page: Page): Promise { await page.route('**/api/v1/**', async (route) => { const path = new URL(route.request().url()).pathname; if (path === '/api/v1/system/status') { await route.fulfill({ contentType: 'application/json', body: JSON.stringify({ version: '1', generatedAt, overallState: 'degraded', components: [{ id: 'database', state: 'healthy', reason: 'ok' }, { id: 'prometheus', state: 'unknown', reason: 'source_stale' }], backup: { state: 'disabled', reason: 'not_configured' }, sourceLag: [{ sourceId: 'reverse-proxy', state: 'unknown', reason: 'source_unavailable', ageSeconds: 180 }], }), }); return; } if (path === '/api/v1/dashboards') { await route.fulfill({ contentType: 'application/json', body: JSON.stringify({ items: [] }) }); return; } if (path === '/api/v1/host') { await route.fulfill({ contentType: 'application/json', body: JSON.stringify({ identity: { name: 'mobile-fixture' }, cpu: { totalPercent: 42, perCore: Array.from({ length: 16 }, (_, index) => index + 1) }, memory: { utilizationPercent: 61 }, source: { state: 'healthy', freshness: 'fresh' }, }) }); return; } if (path === '/api/v1/containers') { await route.fulfill({ contentType: 'application/json', body: JSON.stringify({ source: { state: 'healthy', freshness: 'fresh' }, containers: [{ id: 'proxy', state: 'running', health: 'healthy' }], total: 1 }) }); return; } if (path === '/api/v1/pools') { await route.fulfill({ contentType: 'application/json', body: JSON.stringify({ source: { state: 'healthy', freshness: 'fresh' }, pools: [{ id: 'cache', name: 'Cache', state: 'healthy', utilizationPercent: 63 }], total: 1 }) }); return; } if (path === '/api/v1/services') { await route.fulfill({ contentType: 'application/json', body: JSON.stringify({ capabilityState: 'available', configurationState: 'configured', services: [{ id: 'proxy', name: 'Proxy', state: 'up' }], total: 1 }) }); return; } if (path === '/api/v1/incidents') { await route.fulfill({ contentType: 'application/json', body: JSON.stringify({ items: [{ id: 'incident-1', title: 'Bronvertraging', severity: 'warning', startedAt: generatedAt }] }) }); return; } await route.fulfill({ status: 404, contentType: 'application/problem+json', body: JSON.stringify({ code: 'NOT_FOUND' }) }); }); } async function expectNoCriticalA11yViolations(page: Page): Promise { const results = await new AxeBuilder({ page }).analyze(); const severe = results.violations.filter((violation) => violation.impact === 'critical' || violation.impact === 'serious'); expect(severe, severe.map((violation) => `${violation.id}: ${violation.help}`).join('\n')).toEqual([]); } test.beforeEach(async ({ page }) => { await mockAPI(page); }); test('overview exposes degraded/unknown state and passes axe', async ({ page }, testInfo) => { test.skip(testInfo.project.name === 'wallboard-chromium', 'Wallboard has a dedicated route test.'); await page.goto('/'); await expect(page.getByRole('heading', { level: 1 })).toBeVisible(); await expect(page.getByText(/Onbekend|Verminderd/).first()).toBeVisible(); if (testInfo.project.name === 'mobile-chromium') { await expect(page.locator('.desktop-navigation')).toBeHidden(); await expect(page.locator('.mobile-navigation')).toBeVisible(); await expect(page.locator('.mobile-navigation')).toHaveCSS('position', 'fixed'); await expect(page.locator('.mobile-primary-list .nav-link')).toHaveCount(4); const mobileTargets = await page.locator('.mobile-primary-list .nav-link').evaluateAll((items) => items.map((item) => item.getBoundingClientRect().height)); expect(mobileTargets.every((height) => height >= 44)).toBe(true); await expect(page.locator('.signal-path-stage')).toHaveCount(6); await expect(page.locator('.signal-path-inspector')).toBeVisible(); const incidentBox = await page.locator('.incident-queue').boundingBox(); const signalBox = await page.locator('.signal-path-panel').boundingBox(); expect(incidentBox?.y).toBeLessThan(signalBox?.y ?? 0); expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true); } else { await expect(page.locator('.desktop-navigation')).toBeVisible(); const rail = await page.locator('.sidebar').boundingBox(); const commandHeader = await page.locator('.context-bar').boundingBox(); expect(rail?.width).toBeLessThanOrEqual(72); expect(commandHeader?.height).toBe(56); await expect(page.locator('.overview-kpi')).toHaveCount(4); await expect(page.locator('.source-health-strip')).toHaveAttribute('tabindex', '0'); await expect(page.locator('.nav-group')).toHaveCount(6); await expect(page.locator('.nav-group').first()).toHaveAttribute('open', ''); await expect(page.locator('.nav-group').filter({ hasText: 'Infrastructuur' })).not.toHaveAttribute('open', ''); } await page.keyboard.press('Tab'); await expect(page.locator(':focus')).toBeVisible(); await expectNoCriticalA11yViolations(page); if (process.env.PULSE_E2E_REAL_BASE_URL || process.env.PULSE_CAPTURE_VISUALS) { const evidenceDirectory = process.env.PULSE_CAPTURE_VISUALS ? 'M14-02' : 'M11-10'; await page.evaluate(() => (document.activeElement as HTMLElement | null)?.blur()); await page.screenshot({ path: path.resolve('../../artifacts/evidence', evidenceDirectory, `overview-${testInfo.project.name}.png`), fullPage: true }); } }); test('grouped navigation reaches infrastructure in two actions', async ({ page }, testInfo) => { test.skip(testInfo.project.name === 'wallboard-chromium', 'Wallboard heeft geen productnavigatie.'); await page.goto('/'); if (testInfo.project.name === 'mobile-chromium') { await page.getByText('Meer', { exact: true }).click(); await page.getByRole('link', { name: /Disks/ }).click(); } else { await page.locator('.nav-group').filter({ hasText: 'Infrastructuur' }).locator('summary').click(); await page.getByRole('link', { name: /Disks/ }).click(); } await expect(page).toHaveURL(/\/disks$/); await expect(page.getByRole('heading', { level: 1 })).toBeVisible(); expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth + 1)).toBe(true); }); test('wallboard remains read-only, bounded and accessible', async ({ page }, testInfo) => { test.skip(testInfo.project.name !== 'wallboard-chromium', 'Wallboard is verified at 1920x1080.'); await page.goto('/wallboard'); await expect(page.getByRole('heading', { level: 1 })).toBeVisible(); await expect(page.getByText(/Geen dashboards|Wallboard/).first()).toBeVisible(); await expect(page.locator('.sidebar')).toHaveCount(0); await expect(page.getByRole('button', { name: /Bewerken|Exporteren/ })).toHaveCount(0); expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth + 1)).toBe(true); expect(await page.evaluate(() => document.documentElement.scrollHeight <= window.innerHeight + 1)).toBe(true); await expectNoCriticalA11yViolations(page); });