import AxeBuilder from '@axe-core/playwright'; import { expect, test, type Page, type Route } from '@playwright/test'; const nowSeconds = Math.floor(Date.parse('2026-09-01T10:00:00Z') / 1000); const metrics = [ { semanticName: 'host.cpu.utilization', description: 'CPU-gebruik van de host', unit: 'percent', valueKind: 'gauge', defaultAggregation: 'avg', allowedVisualizations: ['timeseries'], limits: { maxRangeSeconds: 2_592_000, maxSeries: 20, maxPoints: 4000, timeoutSeconds: 5 } }, { semanticName: 'host.memory.utilization', description: 'Geheugengebruik van de host', unit: 'percent', valueKind: 'gauge', defaultAggregation: 'avg', allowedVisualizations: ['timeseries'], limits: { maxRangeSeconds: 2_592_000, maxSeries: 20, maxPoints: 4000, timeoutSeconds: 5 } }, ]; async function fulfill(route: Route, body: unknown): Promise { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(body) }); } async function shell(page: Page): Promise { await page.route('**/api/v1/system/status', (route) => fulfill(route, { version: '1.5.0', generatedAt: '2026-09-01T10:00:00Z', overallState: 'healthy', components: [], backup: { state: 'disabled', reason: 'not_configured' }, sourceLag: [] })); await page.route('**/api/v1/session', (route) => fulfill(route, { subject: 'viewer', role: 'viewer', environment: 'test', permissions: { view: true, operate: false, edit: false, admin: false } })); await page.route('**/api/v1/host', (route) => fulfill(route, { identity: { name: 'explore-fixture' } })); await page.route('**/api/v1/incidents**', (route) => fulfill(route, { items: [{ id: 'incident-1', title: 'Opslagincident', severity: 'warning', updatedAt: '2026-09-01T09:55:00Z' }] })); await page.route('**/api/v1/alerts**', (route) => fulfill(route, { items: [] })); } test('operator finds, compares and queries a local custom range as UTC', async ({ page }, testInfo) => { test.skip(testInfo.project.name === 'wallboard-chromium', 'Explore is een interactieve operatorroute.'); const requests: Array> = []; await shell(page); await page.route('**/api/v1/metrics/catalog', (route) => fulfill(route, { version: 'v1', metrics, bindings: metrics.map((metric) => ({ semanticName: metric.semanticName, state: 'supported' })) })); await page.route('**/api/v1/metrics/query-range', async (route) => { const body = route.request().postDataJSON() as Record; requests.push(body); await fulfill(route, { status: 'success', data: { result: [{ metric: { host: 'tower' }, values: [[nowSeconds - 60, '41'], [nowSeconds, '42']] }] }, provenance: { source: 'fixture', metric: body.metric, catalogVersion: 'v1', cacheKey: 'bounded' }, sourceObservedAt: '2026-09-01T10:00:00Z', receivedAt: '2026-09-01T10:00:01Z', freshness: 'fresh', cacheHit: false }); }); await page.goto('/explore'); await expect(page.getByRole('heading', { level: 1, name: 'Metrics verkennen' })).toBeVisible(); await page.getByPlaceholder('CPU, opslag, responstijd…').fill('cpu'); await page.getByRole('button', { name: /host.cpu.utilization/ }).click(); await expect(page.getByRole('img', { name: /Tijdreeks/ })).toBeVisible(); await expect(page.getByRole('list', { name: 'Incident- en meldingsannotaties' })).toContainText('Opslagincident'); await page.getByRole('tab', { name: 'Vergelijken' }).click(); await page.getByPlaceholder('CPU, opslag, responstijd…').fill('memory'); await page.getByRole('button', { name: /host.memory.utilization/ }).click(); await expect(page.getByRole('heading', { name: 'host.memory.utilization' })).toBeVisible(); await page.getByLabel('Periode').last().selectOption('custom'); await page.getByLabel('Vanaf').last().fill('2026-09-01T08:00'); await page.getByLabel('Tot').last().fill('2026-09-01T10:00'); await expect.poll(() => requests.filter((request) => (request.range as { from?: string })?.from === new Date('2026-09-01T08:00').toISOString()).length).toBeGreaterThan(0); const latest = requests.at(-1)!; expect(latest).toMatchObject({ maxSeries: 20, maxPoints: 4000, range: { from: new Date('2026-09-01T08:00').toISOString(), to: new Date('2026-09-01T10:00').toISOString() } }); expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1)).toBe(true); const serious = (await new AxeBuilder({ page }).analyze()).violations.filter((violation) => violation.impact === 'critical' || violation.impact === 'serious'); expect(serious).toEqual([]); });