import AxeBuilder from '@axe-core/playwright'; import { expect, test } from '@playwright/test'; const now = new Date().toISOString(); test('mobile incident command mode is prioritized, bounded and accessible', async ({ page }, testInfo) => { test.skip(testInfo.project.name !== 'mobile-chromium', 'Mobile incident mode uses the supported 390x844 viewport.'); const mutations: Array<{ path: string; method: string; ifMatch: string | null; idempotencyKey: string | null; body: unknown }> = []; let canOperate = true; await page.route('**/api/v1/**', async (route) => { const path = new URL(route.request().url()).pathname; if (route.request().method() !== 'GET') mutations.push({ path, method: route.request().method(), ifMatch: route.request().headers()['if-match'] ?? null, idempotencyKey: route.request().headers()['idempotency-key'] ?? null, body: route.request().postDataJSON() }); const body = path === '/api/v1/session' ? { subject: canOperate ? 'operator-1' : 'viewer-1', role: canOperate ? 'operator' : 'viewer', environment: 'test', permissions: { view: true, operate: canOperate, edit: false, admin: false }, } : path === '/api/v1/system/status' ? { version: '1', generatedAt: now, overallState: 'degraded', components: [], backup: { state: 'disabled', reason: 'not_configured' }, sourceLag: [], } : path === '/api/v1/incidents/incident-1' ? { incident: { id: 'incident-1', correlationKey: 'cachepool', title: 'Cachepool bijna vol', summary: 'Cachepool is 92% gebruikt en groeit sneller dan verwacht.', severity: 'critical', status: 'open', startedAt: now, correlationMethod: 'temporal-window', confidence: .84, revision: 2, updatedAt: now, ownerUserId: '', alerts: [ { alertId: 'alert-1', rationale: 'Capaciteitsdrempel van 90% overschreden', confidence: .84, correlationMethod: 'temporal-window', manual: false, createdAt: now }, ], notes: [{ id: 'note-1', incidentId: 'incident-1', author: 'Pulse', body: 'Mover is niet actief.', createdAt: now }], }, } : path === '/api/v1/alerts/alert-1' ? { alert: { id: 'alert-1', state: 'firing', retainedState: 'firing', ruleName: 'Cachepoolcapaciteit', severity: 'critical', reason: 'Capaciteitsdrempel overschreden', revision: 7, updatedAt: now }, } : path === '/api/v1/alerts/alert-1/acknowledge' ? { alert: { id: 'alert-1', state: 'acknowledged', revision: 8 }, } : {}; await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(body) }); }); await page.goto('/incidents/incident-1'); await expect(page.getByRole('heading', { level: 1, name: 'Cachepool bijna vol' })).toBeVisible(); await expect(page.locator('.incident-command-strip')).toContainText('Kritiek'); await expect(page.locator('.incident-command-strip')).toContainText('84%'); await expect(page.locator('.incident-timeline')).toContainText('Capaciteitsdrempel van 90% overschreden'); await page.getByRole('button', { name: 'Melding erkennen' }).click(); await expect(page.getByRole('status')).toContainText('lost de infrastructuur niet automatisch op'); expect(mutations).toHaveLength(1); expect(mutations[0]).toMatchObject({ path: '/api/v1/alerts/alert-1/acknowledge', method: 'POST', ifMatch: '7', body: { evaluationKey: 'incident-incident-1-alert-alert-1-ack-7' } }); expect(mutations[0].idempotencyKey).toBe('incident-incident-1-alert-alert-1-ack-7'); expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBe(390); const undersizedTargets = await page.locator('button, .mobile-navigation a, .mobile-more summary').evaluateAll((items) => items.filter((item) => { const style = getComputedStyle(item); return style.display !== 'none' && style.visibility !== 'hidden' && item.getClientRects().length > 0; }).map((item) => ({ label: item.textContent?.trim(), height: item.getBoundingClientRect().height })).filter((item) => item.height < 44)); expect(undersizedTargets).toEqual([]); const axe = await new AxeBuilder({ page }).analyze(); expect(axe.violations.filter((item) => item.impact === 'critical' || item.impact === 'serious')).toEqual([]); canOperate = false; await page.reload(); await expect(page.getByRole('button', { name: 'Melding erkennen' })).toHaveCount(0); await expect(page.getByRole('button', { name: 'Eigenaar opslaan' })).toBeDisabled(); await expect(page.getByText('Je rol kan de opvolgcontext alleen bekijken.')).toBeVisible(); expect(mutations).toHaveLength(1); if (process.env.PULSE_CAPTURE_VISUALS) await page.screenshot({ path: '../../artifacts/evidence/M14-04/mobile-incident-command.png', fullPage: true }); });