Publish ITWorx Pulse source
Public source validation / validate (push) Failing after 3m8s

This commit is contained in:
ITWorx Pulse release export
2026-09-03 02:09:19 +02:00
commit bd774932d5
614 changed files with 77116 additions and 0 deletions
+129
View File
@@ -0,0 +1,129 @@
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<void> {
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<void> {
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);
});
@@ -0,0 +1,94 @@
import AxeBuilder from '@axe-core/playwright';
import { expect, test } from '@playwright/test';
import path from 'node:path';
const alerts = Array.from({ length: 25 }, (_, index) => ({
id: `alert-${String(index + 1).padStart(2, '0')}`,
state: index === 1 ? 'acknowledged' : 'firing',
retainedState: 'firing',
ruleName: `Melding ${String(index + 1).padStart(2, '0')}`,
severity: index % 5 === 0 ? 'critical' : 'attention',
entityName: index % 2 === 0 ? 'Tower' : 'Database',
reason: 'threshold_exceeded',
revision: index + 1,
updatedAt: new Date(Date.UTC(2026, 7, 21, 12, 0, 0) - index * 60_000).toISOString(),
}));
test('alertwerkruimte prioriteert operatie en opent configuratie doelgericht', async ({ page }, testInfo) => {
test.skip(testInfo.project.name === 'wallboard-chromium', 'De alertwerkruimte gebruikt de desktop-, tablet- en mobiele shell.');
let operationHeaders: Record<string, string> | undefined;
await page.route('**/api/v1/**', async (route) => {
const request = route.request();
const pathname = new URL(request.url()).pathname;
if (pathname === '/api/v1/system/status') {
await route.fulfill({ contentType: 'application/json', body: JSON.stringify({ version: 'test', generatedAt: new Date().toISOString(), overallState: 'healthy', components: [], backup: { state: 'disabled', reason: 'not_configured' }, sourceLag: [] }) });
return;
}
if (pathname === '/api/v1/alert-rules') {
await route.fulfill({ contentType: 'application/json', body: JSON.stringify({ items: [] }) });
return;
}
if (pathname === '/api/v1/metrics/catalog') {
await route.fulfill({ contentType: 'application/json', body: JSON.stringify({ metrics: [{ semanticName: 'host.cpu.utilization', unit: 'percent', defaultAggregation: 'avg' }] }) });
return;
}
if (pathname === '/api/v1/alerts' && request.method() === 'GET') {
await route.fulfill({ contentType: 'application/json', body: JSON.stringify({ items: alerts }) });
return;
}
if (pathname.startsWith('/api/v1/alerts/') && request.method() === 'POST') {
operationHeaders = request.headers();
await route.fulfill({ contentType: 'application/json', body: '{}' });
return;
}
if (pathname === '/api/v1/alert-silences' || pathname === '/api/v1/maintenance-windows') {
await route.fulfill({ contentType: 'application/json', body: JSON.stringify({ items: [] }) });
return;
}
await route.fulfill({ status: 404, contentType: 'application/problem+json', body: '{}' });
});
await page.goto('/alerts');
await expect(page.getByRole('heading', { name: 'Meldingen en incidenten' })).toBeVisible();
await expect(page.getByRole('button', { name: /Actief 25/ })).toHaveAttribute('aria-pressed', 'true');
await expect(page.getByRole('button', { name: /Kritiek actief 5/ })).toBeVisible();
const rows = page.locator('.alert-operation-list-items > li');
await expect(rows).toHaveCount(20);
await expect(rows.first()).toContainText('Kritiek');
await expect(page.getByRole('heading', { name: 'Geregistreerde regels' })).toHaveCount(0);
const acknowledge = page.getByRole('button', { name: 'Erkennen' }).first();
page.once('dialog', (dialog) => dialog.dismiss());
await acknowledge.click();
expect(operationHeaders).toBeUndefined();
page.once('dialog', (dialog) => dialog.accept());
await acknowledge.click();
await expect.poll(() => operationHeaders?.['if-match']).toBe('1');
expect(operationHeaders?.['idempotency-key']).toBeTruthy();
await page.getByRole('button', { name: /Alertregels Detectie en drempels/ }).click();
await expect(page).toHaveURL(/section=rules/);
await expect(page.getByRole('heading', { name: 'Geregistreerde regels' })).toBeVisible();
await expect(page.getByRole('heading', { name: 'Actieve en recente meldingen' })).toHaveCount(0);
await page.getByRole('button', { name: /Stiltes en onderhoud Tijdelijke uitzonderingen/ }).click();
await expect(page).toHaveURL(/section=controls/);
await expect(page.getByRole('heading', { name: 'Tijdelijke onderdrukking en onderhoud' })).toBeVisible();
await expect(page.getByRole('heading', { name: 'Geregistreerde regels' })).toHaveCount(0);
await page.reload();
await expect(page.getByRole('heading', { name: 'Tijdelijke onderdrukking en onderhoud' })).toBeVisible();
await expect(page.getByRole('button', { name: /Stiltes en onderhoud Tijdelijke uitzonderingen/ })).toHaveAttribute('aria-current', 'page');
await page.getByRole('button', { name: /Actieve meldingen Prioriteiten en erkenning/ }).click();
await expect(page).not.toHaveURL(/section=/);
expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth + 1)).toBe(true);
expect(await page.evaluate(() => document.documentElement.scrollHeight / window.innerHeight)).toBeLessThan(10);
const axe = await new AxeBuilder({ page }).analyze();
expect(axe.violations.filter((item) => item.impact === 'serious' || item.impact === 'critical')).toEqual([]);
if (testInfo.project.name === 'mobile-chromium') {
const heights = await page.locator('.alert-section-nav button').evaluateAll((buttons) => buttons.map((button) => button.getBoundingClientRect().height));
expect(heights.every((height) => height >= 44)).toBe(true);
}
if (process.env.PULSE_CAPTURE_VISUALS && (testInfo.project.name === 'desktop-chromium' || testInfo.project.name === 'mobile-chromium')) {
await page.screenshot({ path: path.resolve('../../artifacts/evidence/M14-03', `alerts-${testInfo.project.name}.png`), fullPage: true });
}
});
+26
View File
@@ -0,0 +1,26 @@
import AxeBuilder from '@axe-core/playwright';
import { expect, test } from '@playwright/test';
const enabled = Boolean(process.env.PULSE_E2E_REAL_BASE_URL);
test('persisted capacity history produces one qualified explainable forecast', async ({ page }) => {
test.skip(!enabled, 'Requires an isolated real Pulse stack.');
expect((await page.request.get('/auth/test-login')).ok()).toBeTruthy();
const response = await page.request.get('/api/v1/forecasts');
expect(response.ok()).toBeTruthy();
const snapshot = await response.json() as { qualifiedCount: number; items: Array<{ entityId: string; dataPoints: number; confidence: string; projectedAt?: string }> };
expect(snapshot.qualifiedCount).toBe(1);
expect(snapshot.items[0]).toMatchObject({ dataPoints: 3, confidence: 'medium' });
expect(snapshot.items[0].projectedAt).toBeTruthy();
await page.goto('/capacity');
await expect(page.getByText(/1 gekwalificeerde prognoses/)).toBeVisible();
await expect(page.getByRole('heading', { name: 'Media forecast' })).toBeVisible();
await expect(page.getByText('Mediane dagelijkse groei', { exact: true })).toBeVisible();
await expect(page.getByText('Gemiddelde betrouwbaarheid')).toBeVisible();
await expect(page.getByText('42 dagen', { exact: false })).toBeVisible();
await expect(page.getByText('0 B / 0 B')).toHaveCount(0);
expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth + 1)).toBe(true);
const axe = await new AxeBuilder({ page }).analyze();
expect(axe.violations.filter((item) => item.impact === 'serious' || item.impact === 'critical')).toEqual([]);
});
@@ -0,0 +1,29 @@
import AxeBuilder from '@axe-core/playwright';
import { expect, test } from '@playwright/test';
import path from 'node:path';
const enabled = Boolean(process.env.PULSE_E2E_REAL_BASE_URL);
const coreRoutes = ['/', '/host', '/containers', '/storage', '/services', '/alerts', '/incidents', '/inventory'];
test('core routes remain accessible, bounded and visually stable', async ({ page }, testInfo) => {
test.skip(!enabled, 'Requires the isolated server-built Pulse stack.');
const errors: string[] = [];
page.on('console', (message) => { if (message.type() === 'error') errors.push(message.text()); });
page.on('pageerror', (error) => errors.push(error.message));
expect((await page.request.get('/auth/test-login')).ok()).toBeTruthy();
const routes = testInfo.project.name === 'wallboard-chromium' ? ['/wallboard'] : coreRoutes;
for (const route of routes) {
await page.goto(route);
await expect(page.getByRole('heading', { level: 1 }).first()).toBeVisible();
expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth + 1), `${route} has document overflow`).toBe(true);
if (route === '/wallboard') expect(await page.evaluate(() => document.documentElement.scrollHeight <= window.innerHeight + 1), `${route} has vertical overflow`).toBe(true);
const axe = await new AxeBuilder({ page }).analyze();
expect(axe.violations.filter((item) => item.impact === 'critical' || item.impact === 'serious'), `${route} axe findings`).toEqual([]);
if (process.env.PULSE_CAPTURE_VISUALS && (route === '/' || route === '/services' || route === '/wallboard')) {
const name = route === '/' ? 'overview' : route.slice(1);
await page.screenshot({ path: path.resolve('../../artifacts/evidence/M11-10', `${name}-${testInfo.project.name}.png`), fullPage: true });
}
}
expect(errors, errors.join('\n')).toEqual([]);
});
@@ -0,0 +1,91 @@
import AxeBuilder from '@axe-core/playwright';
import { expect, test, type Page } from '@playwright/test';
import path from 'node:path';
const dashboard = { id: 'polish-dashboard', slug: 'operations', name: 'Netwerkoperaties', description: 'Actuele netwerkbelasting en operationele wijzigingen.', scope: 'system', revision: 4, currentVersion: 7 };
const widgets = [
{
id: 'network', title: 'Netwerkbelasting', type: 'timeseries',
data: { sourceType: 'semantic-metric', metric: 'host.network.receive', aggregation: 'avg' },
visualization: { unit: 'bytesPerSecond', decimals: 0, legend: true },
behavior: { locked: false, hidden: false, liveIntervalSeconds: 30 },
layouts: { desktop: { x: 0, y: 0, w: 9, h: 5, visible: true } },
},
{
id: 'events', title: 'Recente wijzigingen', type: 'event-timeline',
data: { sourceType: 'events', limit: 12 }, behavior: { locked: false, hidden: false, liveIntervalSeconds: 30 },
layouts: { desktop: { x: 9, y: 0, w: 9, h: 5, visible: true } },
},
];
async function mockDashboard(page: Page): Promise<void> {
await page.route('**/api/v1/**', async (route) => {
const requestPath = new URL(route.request().url()).pathname;
const body = requestPath === '/api/v1/dashboards/polish-dashboard'
? { dashboard, version: { document: { schemaVersion: 2, widgets, variables: [], settings: { defaultTimeRange: '1h' } } } }
: requestPath === '/api/v1/metrics/query-range'
? { status: 'success', data: { result: [{ metric: { __name__: 'host_network_receive', host: 'tower', interface: 'eth0' }, values: [[1786420800, '1200'], [1786420815, '1500']] }] }, provenance: { source: 'prometheus', metric: 'host.network.receive', catalogVersion: '1', cacheKey: 'test' }, sourceObservedAt: '2026-08-11T04:01:00Z', receivedAt: '2026-08-11T04:01:01Z', freshness: 'fresh', cacheHit: false }
: requestPath === '/api/v1/events'
? { items: [{ id: 'event-1', type: 'container.restart', severity: 'warning', summary: 'container.restart', occurredAt: '2026-08-11T04:00:00Z' }] }
: requestPath === '/api/v1/system/status'
? { version: '1', generatedAt: '2026-08-11T04:01:00Z', overallState: 'healthy', components: [{ id: 'database', state: 'healthy', reason: 'database_ready' }], backup: { state: 'healthy', reason: 'backup_verified' }, sourceLag: [] }
: {};
await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(body) });
});
}
async function expectNoSeriousAxeViolations(page: Page): Promise<void> {
const results = await new AxeBuilder({ page }).analyze();
const violations = results.violations.filter((item) => item.impact === 'critical' || item.impact === 'serious');
expect(violations, violations.map((item) => `${item.id}: ${item.help}`).join('\n')).toEqual([]);
}
test('dashboard and editor use human labels, safe modes and accessible keyboard controls', async ({ page }, testInfo) => {
test.skip(testInfo.project.name !== 'desktop-chromium', 'The editor acceptance proof uses the desktop canvas.');
await mockDashboard(page);
await page.goto('/dashboards/polish-dashboard');
await expect(page.getByRole('heading', { level: 1, name: 'Netwerkoperaties' })).toBeVisible();
await expect(page.getByRole('heading', { level: 2, name: 'Dashboardwidgets' })).toBeAttached();
await expect(page.getByRole('heading', { level: 3, name: 'Netwerkbelasting' })).toBeVisible();
await expect(page.getByRole('list', { name: 'Legenda' })).toContainText('tower · eth0');
await expect(page.locator('.metric-chart-line')).toHaveAttribute('d', 'M 28.000 192.000 L 628.000 12.000');
await expect(page.getByText('Container herstart', { exact: false })).toBeVisible();
await expect(page.getByText('container.restart', { exact: true })).toHaveCount(0);
await expect(page.locator('body')).not.toContainText('{"__name__"');
await expectNoSeriousAxeViolations(page);
if (process.env.PULSE_CAPTURE_VISUALS) await page.screenshot({ path: path.resolve('../../artifacts/evidence/M12-07/dashboard-human-labels.png'), fullPage: true });
await page.getByRole('button', { name: 'Bewerken' }).click();
await expect(page.getByRole('heading', { level: 1, name: 'Dashboard aanpassen' })).toBeVisible();
await expect(page.getByRole('heading', { level: 2, name: 'Dashboardindeling' })).toBeAttached();
const advanced = page.getByText('Dashboardvariabelen, sjablonen en gegevensoverdracht', { exact: true });
await expect(advanced).toBeVisible();
await expect(page.getByRole('heading', { name: 'Import, export en templates' })).toBeHidden();
await expect(page.locator('.editor-widget-actions').first()).toContainText('Omhoog');
await expect(page.locator('.editor-widget-actions').first()).toContainText('Omlaag');
await expect(page.locator('.editor-widget-actions').first()).toContainText('Breedte');
await expect(page.getByText('Weergavemodus')).toHaveCount(0);
const editorWidgets = page.locator('.editor-widget');
await expect(editorWidgets.locator('h3')).toHaveText(['Netwerkbelasting', 'Recente wijzigingen']);
const firstWidget = await editorWidgets.first().boundingBox();
expect(firstWidget).not.toBeNull();
await page.mouse.move(firstWidget!.x + 30, firstWidget!.y + 30);
await page.mouse.down();
await page.mouse.move(firstWidget!.x + 30, firstWidget!.y + 75, { steps: 4 });
await page.mouse.up();
await expect(editorWidgets.locator('h3')).toHaveText(['Recente wijzigingen', 'Netwerkbelasting']);
const resize = page.getByRole('slider', { name: 'Breedte aanpassen: Netwerkbelasting' });
await expect(resize).toHaveAttribute('aria-valuenow', '9');
await resize.focus();
await page.keyboard.press('ArrowRight');
await expect(resize).toHaveAttribute('aria-valuenow', '10');
if (process.env.PULSE_CAPTURE_VISUALS) await page.screenshot({ path: path.resolve('../../artifacts/evidence/M12-07/editor-keyboard-and-disclosure.png'), fullPage: true });
await advanced.click();
await expect(page.getByRole('heading', { level: 2, name: 'Import, export en templates' })).toBeVisible();
await expectNoSeriousAxeViolations(page);
expect(await page.evaluate(() => document.documentElement.scrollWidth <= document.documentElement.clientWidth)).toBe(true);
if (process.env.PULSE_CAPTURE_VISUALS) await page.screenshot({ path: path.resolve('../../artifacts/evidence/M12-07/editor-advanced-open.png'), fullPage: true });
});
@@ -0,0 +1,40 @@
import AxeBuilder from '@axe-core/playwright';
import { expect, test } from '@playwright/test';
const enabled = Boolean(process.env.PULSE_E2E_REAL_BASE_URL);
test('default dashboard and wallboard expose usable real sources', async ({ page }) => {
test.skip(!enabled, 'Run against an isolated server smoke stack.');
test.setTimeout(90_000);
expect((await page.request.get('/auth/test-login')).ok()).toBeTruthy();
const failures: string[] = [];
page.on('pageerror', (error) => failures.push(error.message));
page.on('response', (response) => {
const path = new URL(response.url()).pathname;
if (path.startsWith('/api/') && response.status() >= 400) failures.push(`${response.status()} ${path}`);
});
await page.goto('/dashboards/11111111-1111-4111-8111-111111111111');
await expect(page.getByRole('heading', { level: 1, name: 'Overzicht' })).toBeVisible();
await expect(page.locator('.widget-card--runtime')).toHaveCount(5);
await expect(page.locator('.widget-placeholder')).toHaveCount(0);
await expect.poll(() => failures, { timeout: 5_000 }).toEqual([]);
await expect(page.locator('.metric-chart')).toBeVisible();
await expect(page.getByText('Disk 1').first()).toBeVisible();
await expect(page.getByText('pulse', { exact: true }).first()).toBeVisible();
await expect(page.getByText('Inventaris succesvol bijgewerkt')).toBeVisible();
await expect(page.locator('.widget-card--runtime[data-runtime-state="usable"]')).toHaveCount(5);
expect(await page.evaluate(() => document.documentElement.scrollWidth <= document.documentElement.clientWidth)).toBe(true);
expect((await new AxeBuilder({ page }).analyze()).violations.filter((item) => item.impact === 'critical' || item.impact === 'serious')).toEqual([]);
await page.goto('/wallboard?refresh=300&interval=300');
await expect(page.getByRole('heading', { name: 'Operationeel wallboard' })).toBeVisible();
await expect(page.locator('.wallboard-connection').filter({ hasText: 'Transport' })).toContainText('Verbonden');
await expect(page.locator('.wallboard-connection').filter({ hasText: 'Data' })).toContainText('Bruikbaar');
expect(await page.evaluate(() => document.documentElement.scrollHeight <= window.innerHeight + 1)).toBe(true);
await expect(page.locator('.metric-chart')).toBeVisible();
await page.setViewportSize({ width: 390, height: 844 });
expect(await page.evaluate(() => document.documentElement.scrollWidth <= document.documentElement.clientWidth)).toBe(true);
expect((await new AxeBuilder({ page }).analyze()).violations.filter((item) => item.impact === 'critical' || item.impact === 'serious')).toEqual([]);
expect(failures).toEqual([]);
});
+74
View File
@@ -0,0 +1,74 @@
import AxeBuilder from '@axe-core/playwright';
import { expect, test } from '@playwright/test';
import path from 'node:path';
const items = Array.from({ length: 100 }, (_, index) => ({
id: `event-${String(index + 1).padStart(3, '0')}`,
type: index % 2 === 0 ? 'service.down' : 'container.restart',
severity: index % 10 === 0 ? 'critical' : index % 3 === 0 ? 'warning' : 'info',
entityId: `entity-${String(index % 5).padStart(2, '0')}`,
sourceId: 'source-unraid',
occurredAt: new Date(Date.UTC(2026, 7, 21, 12, 0, 0) - index * 60_000).toISOString(),
receivedAt: new Date(Date.UTC(2026, 7, 21, 12, 0, 5) - index * 60_000).toISOString(),
summary: `Gebeurtenis ${String(index + 1).padStart(3, '0')}`,
}));
test('100 events blijven compact, filterbaar en toetsenbordnavigeerbaar', async ({ page }, testInfo) => {
test.skip(testInfo.project.name === 'wallboard-chromium', 'De eventwerkruimte gebruikt de desktop-, tablet- en mobiele shell.');
let eventRequests = 0;
await page.route('**/api/v1/**', async (route) => {
const pathname = new URL(route.request().url()).pathname;
if (pathname === '/api/v1/events') {
eventRequests += 1;
await route.fulfill({ contentType: 'application/json', body: JSON.stringify({ items }) });
return;
}
if (pathname === '/api/v1/system/status') {
await route.fulfill({ contentType: 'application/json', body: JSON.stringify({ version: '1', generatedAt: new Date().toISOString(), overallState: 'healthy', components: [], backup: { state: 'disabled', reason: 'not_configured' }, sourceLag: [] }) });
return;
}
await route.fulfill({ status: 404, contentType: 'application/problem+json', body: '{}' });
});
await page.goto('/events');
await expect(page.getByRole('heading', { name: 'Gebeurtenissen' })).toBeVisible();
const rows = page.locator('.event-list > li');
await expect(rows).toHaveCount(20);
const baselineRequests = eventRequests;
expect(baselineRequests).toBeLessThanOrEqual(2);
const criticalSummary = page.getByRole('button', { name: /Kritieke gebeurtenissen/i });
await expect(criticalSummary).toBeVisible();
await expect(criticalSummary).toContainText('10');
expect(await page.evaluate(() => document.documentElement.scrollHeight / window.innerHeight)).toBeLessThan(10);
await page.getByLabel('Ernst').selectOption('critical');
await expect(rows).toHaveCount(10);
await page.getByLabel('Soort').selectOption('service.down');
await expect(rows).toHaveCount(10);
await page.getByLabel('Onderdeel').selectOption('entity-00');
await expect(rows).toHaveCount(10);
await page.getByLabel('Zoeken').fill('Gebeurtenis 091');
await expect(rows).toHaveCount(1);
expect(eventRequests).toBe(baselineRequests);
await page.getByRole('button', { name: 'Filters wissen' }).click();
const next = page.getByRole('button', { name: 'Volgende pagina' });
await next.focus();
await page.keyboard.press('Enter');
await expect(page.locator('.list-pager [role="status"]')).toBeFocused();
await expect(page).toHaveURL(/page=2/);
await expect(rows).toHaveCount(20);
await expect(rows.first()).toContainText('event-021');
expect(eventRequests).toBe(baselineRequests);
expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth + 1)).toBe(true);
const axe = await new AxeBuilder({ page }).analyze();
expect(axe.violations.filter((item) => item.impact === 'critical' || item.impact === 'serious')).toEqual([]);
if (testInfo.project.name === 'mobile-chromium') {
const pagerHeights = await page.locator('.list-pager .button').evaluateAll((buttons) => buttons.map((button) => button.getBoundingClientRect().height));
expect(pagerHeights.every((height) => height >= 44)).toBe(true);
}
if (process.env.PULSE_CAPTURE_VISUALS && (testInfo.project.name === 'desktop-chromium' || testInfo.project.name === 'mobile-chromium')) {
await page.screenshot({ path: path.resolve('../../artifacts/evidence/M14-03', `events-${testInfo.project.name}.png`), fullPage: true });
}
});
+30
View File
@@ -0,0 +1,30 @@
import AxeBuilder from '@axe-core/playwright';
import { expect, test } from '@playwright/test';
import path from 'node:path';
const enabled = Boolean(process.env.PULSE_E2E_REAL_BASE_URL);
test('real inventory exposes effective overrides, provenance and relations', async ({ page }, testInfo) => {
test.skip(!enabled, 'Requires the isolated real PostgreSQL stack and inventory fixture.');
const login = await page.request.get('/auth/test-login');
expect(login.ok()).toBeTruthy();
await page.goto('/inventory');
await page.getByRole('searchbox', { name: 'Zoeken' }).fill('pulse-api');
const entity = page.getByRole('link', { name: /Pulse API · handmatig/ });
await expect(entity).toBeVisible();
await expect(entity).toContainText('1 bronnen · 2 feiten · 1 relaties · 2 correcties');
expect(await page.evaluate(() => document.documentElement.scrollWidth <= document.documentElement.clientWidth)).toBe(true);
await entity.click();
await expect(page.getByRole('heading', { level: 1, name: 'Pulse API · handmatig' })).toBeVisible();
await expect(page.getByText('itworx/pulse:pinned', { exact: true })).toBeVisible();
await expect(page.getByText('Handmatige correctie').first()).toBeVisible();
await expect(page.getByText('Verouderd', { exact: true })).toBeVisible();
await expect(page.getByRole('link', { name: /PostgreSQL.*depends_on.*bevestigd/ })).toBeVisible();
expect(await page.evaluate(() => document.documentElement.scrollWidth <= document.documentElement.clientWidth)).toBe(true);
const violations = await new AxeBuilder({ page }).analyze();
expect(violations.violations.filter((item) => ['serious', 'critical'].includes(item.impact ?? ''))).toEqual([]);
if (process.env.PULSE_CAPTURE_VISUALS) await page.screenshot({ path: path.resolve('../../artifacts/evidence/M11-05', `inventory-${testInfo.project.name}.png`), fullPage: true });
});
+104
View File
@@ -0,0 +1,104 @@
import AxeBuilder from '@axe-core/playwright';
import { expect, test } from '@playwright/test';
import path from 'node:path';
const containers = Array.from({ length: 150 }, (_, index) => ({
id: `container-${String(index + 1).padStart(3, '0')}`,
name: `container-${String(index + 1).padStart(3, '0')}`,
image: 'example/pulse:read-only', state: index % 17 === 0 ? 'exited' : 'running', health: index % 13 === 0 ? 'unhealthy' : 'healthy',
intentionalStop: false, metricsAvailable: true, lifecycleAvailable: true, uptimeSeconds: 3600, restartCount: 0, exitCode: 0,
cpuPercent: index / 10, memoryBytes: 1024 * (index + 1), memoryLimitBytes: 1024 * 1024,
networkRxBytes: 0, networkTxBytes: 0, blockReadBytes: 0, blockWriteBytes: 0,
}));
const processes = Array.from({ length: 60 }, (_, index) => ({ pid: index + 1, name: `worker-${String(index + 1).padStart(2, '0')}`, state: 'running', runtimeSeconds: 300, cpuPercent: index, memoryBytes: 2048 + index, containerName: index % 2 ? 'pulse' : 'database' }));
const entities = Array.from({ length: 60 }, (_, index) => ({ id: `entity-${index + 1}`, entityType: 'container', canonicalName: `container.${index + 1}`, displayName: `Entity ${String(index + 1).padStart(2, '0')}`, status: 'operational', factCount: 2, overrideCount: 0, relationCount: 1, sourceCount: 1, staleFactCount: 0 }));
test.beforeEach(async ({ page }) => {
await page.route('**/api/v1/containers?**', async (route) => {
const url = new URL(route.request().url());
const query = (url.searchParams.get('q') ?? '').toLowerCase();
const state = url.searchParams.get('state') ?? '';
const health = url.searchParams.get('health') ?? '';
const after = Number(url.searchParams.get('after') ?? '0');
const limit = Number(url.searchParams.get('limit') ?? '25');
const filtered = containers.filter((item) => (!query || item.name.includes(query)) && (!state || item.state === state) && (!health || item.health === health));
const items = filtered.slice(after, after + limit);
await route.fulfill({ contentType: 'application/json', body: JSON.stringify({ source: { id: 'target-scale', state: 'healthy' }, total: filtered.length, containers: items, nextCursor: after + limit < filtered.length ? String(after + limit) : '' }) });
});
await page.route('**/api/v1/processes?**', async (route) => {
const url = new URL(route.request().url());
const query = (url.searchParams.get('q') ?? '').toLowerCase();
const container = (url.searchParams.get('container') ?? '').toLowerCase();
const after = Number(url.searchParams.get('after') ?? '0');
const filtered = processes.filter((item) => (!query || item.name.includes(query)) && (!container || item.containerName.includes(container)));
await route.fulfill({ contentType: 'application/json', body: JSON.stringify({ source: { id: 'target-scale', state: 'healthy' }, total: filtered.length, processes: filtered.slice(after, after + 25), nextCursor: after + 25 < filtered.length ? String(after + 25) : '' }) });
});
await page.route('**/api/v1/entities?**', async (route) => {
const url = new URL(route.request().url());
const query = (url.searchParams.get('q') ?? '').toLowerCase();
const after = Number(url.searchParams.get('after') ?? '0');
const filtered = entities.filter((item) => !query || (item.displayName + item.canonicalName).toLowerCase().includes(query));
await route.fulfill({ contentType: 'application/json', body: JSON.stringify({ items: filtered.slice(after, after + 25), hasMore: after + 25 < filtered.length, nextCursor: after + 25 < filtered.length ? String(after + 25) : '' }) });
});
});
test('process and inventory filters survive navigation with mobile-first cards', async ({ page }, testInfo) => {
test.skip(testInfo.project.name === 'wallboard-chromium', 'Large lists target desktop and mobile routes.');
await page.goto('/processes?q=worker&container=pulse&sort=memory');
await expect(page.getByRole('heading', { name: 'Topprocessen' })).toBeVisible();
await expect(page.getByLabel('Zoeken')).toHaveValue('worker');
await expect(page.getByLabel('Container')).toHaveValue('pulse');
await expect(page).toHaveURL(/sort=memory/);
if (testInfo.project.name === 'mobile-chromium') await expect(page.locator('.mobile-data-list > li:visible')).toHaveCount(25);
else await expect(page.locator('.desktop-data-view tbody tr:visible')).toHaveCount(25);
expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth + 1)).toBe(true);
await page.goto('/inventory?q=Entity&type=container&status=operational&order=desc');
await expect(page.getByRole('heading', { name: 'Wat Pulse kan zien' })).toBeVisible();
await expect(page.getByLabel('Zoeken')).toHaveValue('Entity');
await expect(page.getByLabel('Type')).toHaveValue('container');
await expect(page.getByRole('textbox', { name: 'Status' })).toHaveValue('operational');
await expect(page.locator('.inventory-entity-list > li')).toHaveCount(25);
await page.getByRole('button', { name: 'Volgende pagina' }).click();
await expect(page.locator('.list-pager [role="status"]')).toBeFocused();
await expect(page).toHaveURL(/after=25/);
expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth + 1)).toBe(true);
});
test('all 150 containers remain reachable with shareable filters and bounded mobile cards', async ({ page }, testInfo) => {
test.skip(testInfo.project.name === 'wallboard-chromium', 'Large lists target desktop and mobile routes.');
const started = Date.now();
await page.goto('/containers');
await expect(page.getByRole('heading', { name: 'Containers' })).toBeVisible();
const visibleRows = testInfo.project.name === 'mobile-chromium' ? page.locator('.mobile-data-list > li:visible') : page.locator('.desktop-data-view tbody tr:visible');
await expect(visibleRows).toHaveCount(25);
expect(Date.now() - started).toBeLessThan(3000);
const seen = new Set<string>();
for (let pageNumber = 1; pageNumber <= 6; pageNumber += 1) {
await expect(visibleRows).toHaveCount(25);
for (const value of await visibleRows.locator('a').allTextContents()) seen.add(value.trim());
if (pageNumber < 6) {
const next = page.getByRole('button', { name: 'Volgende pagina' });
await next.click();
await expect(page.locator('.list-pager [role="status"]')).toBeFocused();
}
}
expect(seen.size).toBe(150);
expect(seen.has('container-150')).toBe(true);
expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth + 1)).toBe(true);
if (testInfo.project.name === 'mobile-chromium') {
const controls = await page.locator('.list-pager button').evaluateAll((buttons) => buttons.map((button) => button.getBoundingClientRect().height));
expect(controls.every((height) => height >= 44)).toBe(true);
}
await page.getByLabel('Zoeken').fill('container-150');
await expect(visibleRows).toHaveCount(1);
await expect(page).toHaveURL(/q=container-150/);
await page.reload();
await expect(visibleRows).toHaveCount(1);
await expect(visibleRows.getByText('container-150')).toBeVisible();
const axe = await new AxeBuilder({ page }).analyze();
expect(axe.violations.filter((item) => item.impact === 'serious' || item.impact === 'critical')).toEqual([]);
if (process.env.PULSE_CAPTURE_VISUALS) await page.screenshot({ path: path.resolve('../../artifacts/evidence/M11-08', `large-containers-${testInfo.project.name}.png`), fullPage: true });
});
@@ -0,0 +1,55 @@
import AxeBuilder from '@axe-core/playwright';
import { expect, test } from '@playwright/test';
import path from 'node:path';
const rule = {
id: '20000000-0000-4000-8000-000000000001', schemaVersion: 1, name: 'Hoge hostbelasting', enabled: true, severity: 'critical', scope: {},
condition: { inputType: 'metric', metric: 'host.cpu.utilization', operator: '>', threshold: 90, recoveryThreshold: 80, aggregation: 'avg', windowSeconds: 60 },
evaluationIntervalSeconds: 30, pendingSeconds: 60, resolveSeconds: 120, cooldownSeconds: 300,
unknownBehavior: 'retain-firing-as-unknown', groupBy: [], suppressWhen: ['host.unreachable'],
message: { titleKey: 'alerts.rule.title', bodyKey: 'alerts.rule.body' }, revision: 1, currentVersion: 1,
};
test.beforeEach(async ({ page }) => {
await page.route('**/api/v1/system/status', (route) => route.fulfill({ contentType: 'application/json', body: JSON.stringify({ version: 'test', generatedAt: new Date().toISOString(), overallState: 'healthy', components: [], backup: { state: 'disabled', reason: 'not_configured' }, sourceLag: [] }) }));
await page.route('**/api/v1/alert-rules?**', (route) => route.fulfill({ contentType: 'application/json', body: JSON.stringify({ items: [rule] }) }));
await page.route('**/api/v1/metrics/catalog', (route) => route.fulfill({ contentType: 'application/json', body: JSON.stringify({ metrics: [{ semanticName: 'host.cpu.utilization', unit: 'percent', defaultAggregation: 'avg' }] }) }));
await page.route('**/api/v1/alert-silences**', (route) => route.fulfill({ contentType: 'application/json', body: JSON.stringify({ items: [] }) }));
await page.route('**/api/v1/maintenance-windows**', (route) => route.fulfill({ contentType: 'application/json', body: JSON.stringify({ items: [] }) }));
await page.route('**/api/v1/alerts?**', (route) => route.fulfill({ contentType: 'application/json', body: JSON.stringify({ items: [] }) }));
});
test('alert editor uses Dutch guided choices and rejects an invalid draft', async ({ page }, testInfo) => {
test.skip(testInfo.project.name === 'wallboard-chromium', 'De alert-editor is geen wallboardroute.');
await page.goto('/alerts');
await expect(page.getByRole('heading', { name: 'Meldingen en incidenten' })).toBeVisible();
await page.getByRole('button', { name: /Alertregels Detectie en drempels/ }).click();
await expect(page.getByText('Kritiek · v1')).toBeVisible();
await expect(page.getByRole('combobox', { name: /Meting/ })).toHaveValue('host.cpu.utilization');
await expect(page.getByRole('option', { name: 'CPU-gebruik van de host (%)' })).toBeAttached();
await expect(page.getByText('host.cpu.utilization')).toHaveCount(0);
await expect(page.getByText('host.unreachable')).not.toBeVisible();
await page.getByRole('button', { name: 'Nieuwe regel' }).click();
const save = page.getByRole('button', { name: 'Regel opslaan' });
await expect(save).toBeDisabled();
await page.locator('#alert-rule-name').fill('CPU-waarschuwing');
await page.getByRole('combobox', { name: /Meting/ }).selectOption('host.cpu.utilization');
await expect(save).toBeEnabled();
await page.locator('#alert-rule-recovery-threshold').fill('90');
await expect(save).toBeDisabled();
await page.getByRole('combobox', { name: /Signaalbron/ }).selectOption('event');
await expect(page.getByRole('combobox', { name: /Meting/ })).toHaveCount(0);
await expect(page.locator('#alert-rule-threshold')).toHaveValue('3');
await expect(save).toBeEnabled();
await page.getByRole('button', { name: /Stiltes en onderhoud Tijdelijke uitzonderingen/ }).click();
await expect(page.locator('#silence-matcher')).toHaveValue('critical');
await expect(page.locator('#silence-matcher')).toContainText('Kritiek');
await expect(page.locator('#maintenance-selector')).toContainText('Host');
expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth + 1)).toBe(true);
const axe = await new AxeBuilder({ page }).analyze();
expect(axe.violations.filter((item) => item.impact === 'serious' || item.impact === 'critical')).toEqual([]);
if (process.env.PULSE_CAPTURE_VISUALS) await page.screenshot({ path: path.resolve('../../artifacts/evidence/M11-09', `alert-editor-${testInfo.project.name}.png`), fullPage: true });
});
@@ -0,0 +1,85 @@
import AxeBuilder from '@axe-core/playwright';
import { expect, test } from '@playwright/test';
import path from 'node:path';
const systemStatus = {
version: '1.3.0',
release: { version: '1.3.0', commit: 'abc1234', builtAt: '2026-08-21T12:00:00Z', migrationVersion: '0024' },
generatedAt: new Date().toISOString(),
overallState: 'healthy',
components: [],
backup: { state: 'healthy', reason: 'backup_verified', ageSeconds: 30 * 60 * 60, verifiedAt: '2026-08-20T06:00:00Z' },
sourceLag: [{ sourceId: 'unraid', state: 'healthy', reason: 'fresh', ageSeconds: 5 }],
};
const onboarding = {
state: { completed: true, step: 'completed', dashboardChoice: 'default', rulesChoice: 'default', dashboardId: 'overview', rulesReady: true },
capabilities: [
{ id: 'auth', state: 'ready', detail: 'Aanmelding geconfigureerd.' },
{ id: 'database', state: 'ready', detail: 'Database beschikbaar.' },
{ id: 'prometheus', state: 'ready', detail: 'Meetgegevens beschikbaar.' },
{ id: 'unraid', state: 'ready', detail: 'Unraid-bron beschikbaar.' },
],
resume: false,
};
test('beheerhub, afgeronde onboarding en backupouderdom blijven taakgericht en waarheidsgetrouw', async ({ page }, testInfo) => {
test.skip(testInfo.project.name === 'wallboard-chromium', 'Beheerflows gebruiken de desktop-, tablet- en mobiele shell.');
await page.route('**/api/v1/**', async (route) => {
const pathname = new URL(route.request().url()).pathname;
if (pathname === '/api/v1/system/status') {
if (route.request().method() === 'POST') {
await route.fulfill({ status: 403, contentType: 'application/problem+json', body: '{}' });
return;
}
await route.fulfill({ contentType: 'application/json', body: JSON.stringify({ ...systemStatus, generatedAt: new Date().toISOString() }) });
return;
}
if (pathname === '/api/v1/onboarding') {
await route.fulfill({ contentType: 'application/json', body: JSON.stringify(onboarding) });
return;
}
await route.fulfill({ status: 404, contentType: 'application/problem+json', body: '{}' });
});
await page.goto('/settings');
await expect(page.getByRole('heading', { name: 'Pulse configureren' })).toBeVisible();
const hub = page.getByRole('region', { name: 'Beheerfuncties' });
await expect(hub.getByRole('link')).toHaveCount(6);
await expect(hub.getByRole('link', { name: /Systeemstatus en backup/ })).toHaveAttribute('href', '/status');
await expect(hub.getByRole('link', { name: /Eerste configuratie/ })).toHaveAttribute('href', '/onboarding');
await expect(hub.getByRole('link', { name: /Alertregels/ })).toHaveAttribute('href', '/alerts?section=rules');
await expect(hub.getByRole('link', { name: /Stiltes en onderhoud/ })).toHaveAttribute('href', '/alerts?section=controls');
await expect(hub.getByText('Backupactie: beheerder')).toBeVisible();
await expect(hub.getByText('Wijzigen: operator')).toBeVisible();
if (process.env.PULSE_CAPTURE_VISUALS && (testInfo.project.name === 'desktop-chromium' || testInfo.project.name === 'mobile-chromium')) {
await page.screenshot({ path: path.resolve('../../artifacts/evidence/M14-03', `settings-${testInfo.project.name}.png`), fullPage: true });
}
await hub.getByRole('link', { name: /Systeemstatus en backup/ }).click();
const backup = page.getByRole('article').filter({ has: page.getByText('Backupstatus', { exact: true }) });
await expect(page.getByRole('heading', { name: 'Pulse-systeemstatus' })).toBeVisible();
await expect(backup.locator('.status-badge')).toContainText('Aandacht');
await expect(backup.getByText(/backup is verlopen/i)).toBeVisible();
await expect(backup.getByText(/1 dag geleden/)).toBeVisible();
await expect(backup.getByText(/ouder dan 24 uur/)).toBeVisible();
await backup.getByRole('button', { name: 'Maak geverifieerde backup' }).click();
await expect(backup.getByRole('alert')).toContainText('Alleen beheerders');
await page.goto('/settings');
await page.getByRole('region', { name: 'Beheerfuncties' }).locator('a[href="/onboarding"]').click();
await expect(page.getByRole('heading', { name: 'Pulse is geconfigureerd' })).toBeVisible();
await expect(page.getByRole('radio')).toHaveCount(0);
await page.getByRole('button', { name: 'Herconfiguratie openen' }).click();
await expect(page.getByRole('radio')).toHaveCount(4);
await page.getByRole('button', { name: 'Annuleren' }).click();
await expect(page.getByRole('radio')).toHaveCount(0);
expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth + 1)).toBe(true);
const axe = await new AxeBuilder({ page }).analyze();
expect(axe.violations.filter((item) => item.impact === 'serious' || item.impact === 'critical')).toEqual([]);
if (testInfo.project.name === 'mobile-chromium') {
const linkHeights = await page.locator('.settings-hub-card a').evaluateAll((links) => links.map((link) => link.getBoundingClientRect().height));
expect(linkHeights.every((height) => height >= 44)).toBe(true);
}
});
@@ -0,0 +1,39 @@
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.');
await page.route('**/api/v1/**', async (route) => {
const path = new URL(route.request().url()).pathname;
const body = 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 }],
},
} : {};
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');
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBe(390);
const targets = 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';
}).map((item) => item.getBoundingClientRect().height));
expect(targets.every((height) => height >= 44)).toBe(true);
const axe = await new AxeBuilder({ page }).analyze();
expect(axe.violations.filter((item) => item.impact === 'critical' || item.impact === 'serious')).toEqual([]);
if (process.env.PULSE_CAPTURE_VISUALS) await page.screenshot({ path: '../../artifacts/evidence/M14-04/mobile-incident-command.png', fullPage: true });
});
+198
View File
@@ -0,0 +1,198 @@
import AxeBuilder from '@axe-core/playwright';
import { expect, test } from '@playwright/test';
const enabled = Boolean(process.env.PULSE_E2E_REAL_BASE_URL);
test('real stack serves collector data through database, API and UI', async ({ page }) => {
test.skip(!enabled, 'Run through scripts/integration-smoke.ps1 with an isolated real stack.');
test.setTimeout(210_000);
const login = await page.request.get('/auth/test-login');
expect(login.ok()).toBeTruthy();
const issuedSession = (await page.context().cookies()).find((cookie) => cookie.name === 'pulse_session');
expect(issuedSession, 'mock login issues the same HttpOnly session contract as OIDC').toBeDefined();
expect(issuedSession?.httpOnly).toBe(true);
for (const endpoint of ['/healthz', '/readyz']) {
const response = await page.request.get(endpoint);
expect(response.status(), endpoint).toBe(200);
expect(response.headers()['content-type'], endpoint).toContain('text/plain');
expect((await response.text()).toLowerCase(), endpoint).not.toContain('<!doctype html>');
}
for (const endpoint of ['/metrics', '/debug/pprof/']) {
const response = await page.request.get(endpoint);
expect(response.status(), endpoint).toBe(404);
expect((await response.text()).toLowerCase(), endpoint).not.toContain('<!doctype html>');
}
const endpointChecks = [
'/api/v1/system/status', '/api/v1/host', '/api/v1/processes?limit=10',
'/api/v1/containers?limit=10', '/api/v1/array', '/api/v1/disks?limit=10',
'/api/v1/pools?limit=10', '/api/v1/shares?limit=10', '/api/v1/services?limit=10',
'/api/v1/network', '/api/v1/topology?limit=10', '/api/v1/applications?limit=10', '/api/v1/events?limit=10',
'/api/v1/entities?limit=10', '/api/v1/dashboards?limit=10', '/api/v1/alert-rules?limit=10',
'/api/v1/alerts?limit=10', '/api/v1/incidents?limit=10', '/api/v1/onboarding',
];
for (const endpoint of endpointChecks) {
const response = await page.request.get(endpoint);
expect(response.status(), endpoint).toBe(200);
}
const hostResponse = await page.request.get('/api/v1/host');
const host = await hostResponse.json() as Record<string, unknown>;
expect(JSON.stringify(host)).toContain('smoke-host');
expect(JSON.stringify(host)).toContain('fresh');
const containers = await (await page.request.get('/api/v1/containers?limit=10')).json() as { containers?: Array<{ name: string; state: string; health: string; metricsAvailable?: boolean; lifecycleAvailable?: boolean }> };
const smokeContainer = containers.containers?.find((item) => item.name === 'smoke-api');
expect(smokeContainer).toMatchObject({ state: 'running', health: 'healthy', metricsAvailable: false, lifecycleAvailable: false });
const applications = await (await page.request.get('/api/v1/applications?limit=10')).json() as { applications?: Array<{ name: string; status: string }> };
expect(applications.applications?.find((item) => item.name === 'smoke')).toMatchObject({ status: 'healthy' });
await page.goto('/containers');
await expect(page.getByText(/metingen niet beschikbaar/).first()).toBeVisible();
const systemStatus = await (await page.request.get('/api/v1/system/status')).json() as {
components?: Array<{ id: string; state: string; reason: string }>;
sourceLag?: Array<{ sourceId: string; state: string }>;
};
const unraidComponent = systemStatus.components?.find((item) => item.id === 'unraid');
expect(unraidComponent, 'unraid runtime component').toBeDefined();
expect(unraidComponent?.state, 'unraid is derived from every fresh bounded agent capability').toBe('healthy');
expect(unraidComponent?.reason, 'unraid no longer uses API-local configuration').toBe('source_sampled');
const storageComponent = systemStatus.components?.find((item) => item.id === 'storage');
expect(storageComponent, 'storage runtime component').toMatchObject({ state: 'healthy', reason: 'source_sampled' });
expect(systemStatus.sourceLag?.find((source) => source.sourceId === 'unraid')?.state).toBe('healthy');
const onboarding = await (await page.request.get('/api/v1/onboarding')).json() as { capabilities?: Array<{ id: string; state: string }> };
expect(onboarding.capabilities?.find((capability) => capability.id === 'unraid')?.state).toBe('ready');
const dashboardId = '61000000-0000-4000-8000-000000000001';
const dashboard = {
schemaVersion: 2, id: dashboardId, slug: 'real-stack-metric', name: 'Real-stack CPU', description: 'Metric planner browser gate.', scope: 'system',
variables: [{ name: 'server', type: 'server', label: 'Server', default: 'smoke-host' }],
widgets: [{
id: '62000000-0000-4000-8000-000000000001', type: 'timeseries', title: 'CPU live', description: 'Catalog-bounded metric.',
data: { sourceType: 'semantic-metric', metric: 'host.cpu.utilization', scope: { serverId: '$server' }, aggregation: 'avg', transformations: [] },
visualization: { unit: 'percent', decimals: 1, legend: true, showSparkline: false, min: 0, max: 100, thresholds: [] },
behavior: { locked: true, hidden: false, hideWhenEmpty: false, showOnlyOnProblem: false, liveIntervalSeconds: 2, independentTimeRange: null },
layouts: { desktop: { x: 0, y: 0, w: 18, h: 8, visible: true }, tablet: { x: 0, y: 0, w: 8, h: 8, visible: true }, mobile: { x: 0, y: 0, w: 1, h: 8, visible: true }, wallboard: { x: 0, y: 0, w: 24, h: 12, visible: true } },
}],
settings: { defaultTimeRange: 'live', live: true, refreshSeconds: 10, rotationSeconds: 30 },
};
const dashboardResponse = await page.request.post('/api/v1/dashboards', { data: dashboard });
expect([201, 409], await dashboardResponse.text()).toContain(dashboardResponse.status());
const peerDashboard = {
...dashboard,
id: '61000000-0000-4000-8000-000000000002',
slug: 'real-stack-memory',
name: 'Real-stack geheugen',
widgets: dashboard.widgets.map((widget) => ({
...widget,
id: '62000000-0000-4000-8000-000000000002',
title: 'Geheugen live',
data: { ...widget.data, metric: 'host.memory.utilization' },
})),
};
const peerDashboardResponse = await page.request.post('/api/v1/dashboards', { data: peerDashboard });
expect([201, 409], await peerDashboardResponse.text()).toContain(peerDashboardResponse.status());
await page.goto('/');
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
const websocketOpened = await page.evaluate(() => new Promise<boolean>((resolve) => {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const socket = new WebSocket(`${protocol}//${window.location.host}/api/v1/live`);
const timeout = window.setTimeout(() => { socket.close(); resolve(false); }, 5_000);
socket.addEventListener('open', () => { window.clearTimeout(timeout); socket.close(); resolve(true); }, { once: true });
socket.addEventListener('error', () => { window.clearTimeout(timeout); resolve(false); }, { once: true });
}));
expect(websocketOpened, 'same-origin WebSocket upgrade through nginx').toBe(true);
const runtimeFailures: string[] = [];
page.on('pageerror', (error) => runtimeFailures.push(error.message));
page.on('response', (response) => {
if (new URL(response.url()).pathname.startsWith('/api/') && response.status() >= 400) {
runtimeFailures.push(`${response.status()} ${response.url()}`);
}
});
const routes = ['/', '/host', '/pools', '/shares', '/storage', '/capacity', '/processes', '/containers', '/services', '/topology', '/network', '/applications', '/inventory', '/dashboards', '/alerts', '/incidents', '/settings', '/status', '/onboarding'];
for (const route of routes) {
await page.goto(route);
await expect(page.getByRole('heading', { level: 1 }).first(), route).toBeVisible();
await expect(page.locator('.state-page[role="alert"]'), route).toHaveCount(0);
}
expect(runtimeFailures).toEqual([]);
await page.addInitScript((id) => window.localStorage.setItem(`pulse.dashboard.view.${id}.range`, 'live'), dashboardId);
const metricResponse = page.waitForResponse((response) => new URL(response.url()).pathname === '/api/v1/metrics/query-range');
await page.goto('/dashboards/' + dashboardId);
await expect(page.getByRole('heading', { name: 'Real-stack CPU' })).toBeVisible();
expect((await metricResponse).status(), 'catalog-bounded query through deployed UI').toBe(200);
await expect(page.getByRole('heading', { name: 'CPU live' })).toBeVisible();
expect(runtimeFailures).toEqual([]);
// A rotating wallboard fetches the next dashboard document before its live
// subscription is ready. Exercise a response well beyond the 250 ms
// subscription-release grace and prove the bounded idle transport is reused
// across multiple rotations instead of closing and reopening.
const dashboardList = await (await page.request.get('/api/v1/dashboards?limit=100')).json() as { items?: Record<string, unknown>[] };
const wallboardDashboardIds = (dashboardList.items ?? []).map((item) => String(item.id ?? item.ID ?? '')).filter(Boolean);
expect(wallboardDashboardIds.length).toBeGreaterThanOrEqual(2);
await page.addInitScript(() => {
const NativeWebSocket = window.WebSocket;
const counters = { opened: 0, closed: 0 };
Object.defineProperty(window, '__pulseSocketLifecycle', { value: counters, configurable: true });
class TrackedWebSocket extends NativeWebSocket {
constructor(url: string | URL, protocols?: string | string[]) {
super(url, protocols);
counters.opened += 1;
this.addEventListener('close', () => { counters.closed += 1; }, { once: true });
}
}
window.WebSocket = TrackedWebSocket;
});
await page.addInitScript((ids) => {
ids.forEach((id) => window.localStorage.setItem(`pulse.dashboard.view.${id}.range`, 'live'));
}, wallboardDashboardIds);
let delayedDashboardLoads = 0;
await page.route('**/api/v1/dashboards/**', async (route) => {
delayedDashboardLoads += 1;
await new Promise((resolve) => setTimeout(resolve, 2_000));
try {
await route.continue();
} catch (error) {
// Going offline can settle an intentionally delayed request before the
// handler resumes. That is the failure mode under test, not a harness
// failure; every other routing error must still fail the flow.
if (!(error instanceof Error) || !error.message.includes('Route is already handled')) throw error;
}
});
await page.goto('/wallboard?interval=10&refresh=300');
await expect(page.getByRole('heading', { name: 'Operationeel wallboard' })).toBeVisible();
const visibleDashboard = page.locator('#dashboard-view-title');
await expect(visibleDashboard).toBeVisible();
await page.context().setOffline(true);
// Cross a rotation while both HTTP and WebSocket transport are unavailable.
// The wallboard must retain its last verified document instead of replacing
// operational context with a blank loading/error page.
await page.waitForTimeout(12_000);
await expect(visibleDashboard).toBeVisible();
await expect(visibleDashboard).not.toHaveText('');
await page.context().setOffline(false);
// The integration API has a 30-second idle session TTL. Staying on this page
// for 65 seconds after recovery crosses it more than twice. Dashboard refreshes must renew
// the HttpOnly cookie server-side without reopening the live transport.
await page.waitForTimeout(65_000);
expect(delayedDashboardLoads, 'initial document plus at least five rotations').toBeGreaterThanOrEqual(6);
const renewedSession = (await page.context().cookies()).find((cookie) => cookie.name === 'pulse_session');
expect(renewedSession, 'active wallboard retains its server session').toBeDefined();
expect(renewedSession?.value).toBe(issuedSession?.value);
expect(renewedSession?.expires ?? 0, 'idle deadline was renewed beyond its initial expiry').toBeGreaterThan(issuedSession?.expires ?? 0);
const statusAfterMultipleTTLs = await page.request.get('/api/v1/system/status');
expect(statusAfterMultipleTTLs.status(), 'authenticated API after multiple idle TTLs').toBe(200);
const socketLifecycle = await page.evaluate(() => (window as unknown as { __pulseSocketLifecycle: { opened: number; closed: number } }).__pulseSocketLifecycle);
expect(socketLifecycle).toEqual({ opened: 1, closed: 0 });
expect(runtimeFailures).toEqual([]);
await page.unroute('**/api/v1/dashboards/**');
await page.goto('/host');
await expect(page.getByText('smoke-host').first()).toBeVisible();
const axe = await new AxeBuilder({ page }).analyze();
expect(axe.violations.filter((violation) => violation.impact === 'critical' || violation.impact === 'serious')).toEqual([]);
});
@@ -0,0 +1,33 @@
import { expect, test } from '@playwright/test';
import path from 'node:path';
test('release metadata and verified backup are production truthful', async ({ page }, testInfo) => {
test.skip(!process.env.PULSE_E2E_REAL_BASE_URL, 'Requires the isolated server-built Pulse stack.');
expect((await page.request.get('/auth/test-login')).ok()).toBeTruthy();
const created = await page.request.post('/api/v1/system/backups');
expect(created.ok()).toBeTruthy();
const response = await page.request.get('/api/v1/system/status');
expect(response.ok()).toBeTruthy();
const status = await response.json() as {
version: string;
release: { version: string; commit: string; builtAt?: string; migrationVersion: string };
backup: { state: string; reason: string; verifiedAt?: string; ageSeconds?: number };
};
expect(status.version).not.toBe('development');
expect(status.release).toMatchObject({ version: 'm11.11-test', commit: 'm1111testcommit' });
expect(status.release.builtAt).toBe('2026-08-12T03:00:00Z');
expect(status.release.migrationVersion).toMatch(/^\d{4}_.+/);
expect(status.backup).toMatchObject({ state: 'healthy', reason: 'backup_verified' });
expect(status.backup.verifiedAt).toBeTruthy();
expect(status.backup.ageSeconds).toBeGreaterThanOrEqual(0);
await page.goto('/status');
await expect(page.getByText(/m11\.11-test/)).toBeVisible();
await expect(page.getByText(/De backup is geverifieerd/)).toBeVisible();
expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth + 1)).toBe(true);
if (process.env.PULSE_CAPTURE_VISUALS) await page.screenshot({
path: path.resolve('../../artifacts/evidence/M11-11', `release-backup-${testInfo.project.name}.png`),
fullPage: true,
});
});
@@ -0,0 +1,69 @@
import { expect, test, type Page } from '@playwright/test';
import path from 'node:path';
const observedAt = new Date().toISOString();
async function mockResponsiveData(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: 'prometheus', state: 'unknown', reason: 'source_stale' },
],
backup: { state: 'disabled', reason: 'not_configured' },
sourceLag: [{ sourceId: 'prometheus', state: 'unknown', reason: 'source_stale', ageSeconds: 900 }],
} : pathname === '/api/v1/host' ? {
identity: { name: 'responsive-fixture' }, cpu: { totalPercent: 42, perCore: [42, 38] },
memory: { utilizationPercent: 61 }, source: { state: 'healthy', freshness: 'fresh' },
} : pathname === '/api/v1/containers' ? { source: { state: 'healthy', freshness: 'fresh' }, containers: [], total: 0 }
: pathname === '/api/v1/pools' ? { source: { state: 'healthy', freshness: 'fresh' }, pools: [], total: 0 }
: pathname === '/api/v1/services' ? { capabilityState: 'available', configurationState: 'not_configured', services: [], total: 0 }
: pathname === '/api/v1/incidents' ? { items: [] }
: {};
await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(body) });
});
}
test.beforeEach(async ({ page }) => mockResponsiveData(page));
test('mobile primary navigation and operational reasons remain readable at 390px', async ({ page }, testInfo) => {
test.skip(testInfo.project.name !== 'mobile-chromium', 'Mobile evidence uses the supported 390px viewport.');
await page.goto('/');
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
const labels = page.locator('.mobile-primary-list .nav-link span:last-child');
await expect(labels).toHaveCount(4);
const metrics = await labels.evaluateAll((nodes) => nodes.map((node) => {
const box = node.getBoundingClientRect();
return { left: box.left, right: box.right, top: box.top, bottom: box.bottom, font: Number.parseFloat(getComputedStyle(node).fontSize) };
}));
for (let index = 1; index < metrics.length; index += 1) expect(metrics[index - 1].right).toBeLessThanOrEqual(metrics[index].left);
expect(metrics.every((metric) => metric.font >= 12 && metric.bottom - metric.top <= 16)).toBe(true);
const reason = page.locator('.action-queue small').first();
await expect(reason).toBeVisible();
await expect(reason).toHaveCSS('white-space', 'normal');
const overflow = await page.evaluate(() => [...document.querySelectorAll<HTMLElement>('body *')]
.map((element) => ({ tag: element.tagName, className: element.className, right: Math.round(element.getBoundingClientRect().right), scrollWidth: element.scrollWidth, clientWidth: element.clientWidth }))
.filter((item) => item.right > innerWidth + 1 || item.scrollWidth > item.clientWidth + 1)
.slice(0, 12));
expect(overflow).toEqual([]);
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBe(390);
if (process.env.PULSE_CAPTURE_VISUALS) await page.screenshot({ path: path.resolve('../../artifacts/evidence/M12-06/responsive-mobile-390.png'), fullPage: true });
});
test('tablet sidebar stays compact and content uses the remaining viewport', async ({ page }, testInfo) => {
test.skip(testInfo.project.name !== 'tablet-chromium', 'Tablet evidence uses the supported 1024px viewport.');
await page.goto('/');
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
const sidebar = await page.locator('.sidebar').boundingBox();
const workspace = await page.locator('.app-workspace').boundingBox();
const queue = await page.locator('.action-queue').boundingBox();
const dataPlane = await page.locator('.signal-path-panel').boundingBox();
expect(sidebar?.width).toBeLessThanOrEqual(208);
expect(workspace?.width).toBeGreaterThanOrEqual(816);
expect(queue?.width).toBeGreaterThan(300);
expect(dataPlane?.width).toBeGreaterThan(500);
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBe(1024);
if (process.env.PULSE_CAPTURE_VISUALS) await page.screenshot({ path: path.resolve('../../artifacts/evidence/M12-06/responsive-tablet-1024.png'), fullPage: true });
});
@@ -0,0 +1,30 @@
import AxeBuilder from '@axe-core/playwright';
import { expect, test } from '@playwright/test';
const enabled = Boolean(process.env.PULSE_E2E_REAL_BASE_URL);
test.beforeEach(async ({ page }) => {
test.skip(!enabled, 'Requires an isolated real Pulse stack.');
const login = await page.request.get('/auth/test-login');
expect(login.ok()).toBeTruthy();
});
test('real services and inventory topology retain explicit production states', async ({ page }) => {
await page.goto('/services');
await expect(page.getByRole('heading', { name: 'Bereikbaarheid en historie' })).toBeVisible();
await expect(page.getByText('Voor deze service is nog geen probe geconfigureerd.').first()).toBeVisible();
await page.goto('/topology');
await expect(page.getByRole('heading', { name: 'Relaties en services' })).toBeVisible();
await expect(page.getByRole('region', { name: 'Relaties', exact: true }).getByText('Ondersteunt')).toBeVisible();
await expect(page.getByText(/dependency-test-a-/).first()).toBeVisible();
await page.goto('/network');
const dns = page.getByRole('heading', { name: 'DNS' }).locator('../..');
await expect(dns.getByText('Niet geconfigureerd')).toBeVisible();
await expect(dns.getByText('Voor dit signaal is nog geen veilige probe geconfigureerd.')).toBeVisible();
expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth + 1)).toBe(true);
const accessibility = await new AxeBuilder({ page }).analyze();
expect(accessibility.violations.filter((violation) => ['serious', 'critical'].includes(violation.impact ?? ''))).toEqual([]);
});
@@ -0,0 +1,23 @@
import { expect, test } from '@playwright/test';
test('expired wallboard session is revoked once without periodic 401 churn', async ({ page }, testInfo) => {
test.skip(testInfo.project.name !== 'wallboard-chromium', 'The long-running wallboard owns this session boundary.');
let apiRequests = 0;
await page.route('**/api/v1/**', async (route) => {
apiRequests += 1;
await route.fulfill({ status: 401, contentType: 'application/problem+json', body: JSON.stringify({ code: 'UNAUTHENTICATED' }) });
});
await page.goto('/wallboard?interval=10&refresh=10');
await expect(page.getByRole('heading', { level: 1, name: 'Geen toegang' })).toBeVisible();
await expect(page.getByRole('link', { name: 'Aanmelden' })).toBeVisible();
await expect.poll(() => apiRequests).toBeGreaterThan(0);
const boundaryRequests = apiRequests;
// Development StrictMode mounts the four bootstrap readers twice. The
// production bundle issues one batch; neither mode may start a second one.
expect(boundaryRequests).toBeLessThanOrEqual(8);
await page.waitForTimeout(12_000);
expect(apiRequests).toBe(boundaryRequests);
await expect(page.getByRole('heading', { level: 1, name: 'Geen toegang' })).toBeVisible();
});
+130
View File
@@ -0,0 +1,130 @@
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);
});
@@ -0,0 +1,55 @@
import { expect, test, type Page } from '@playwright/test';
const zeroTimestamp = '0001-01-01T00:00:00Z';
async function mockSourceStatusData(page: Page): Promise<void> {
await page.route('**/api/v1/**', async (route) => {
const pathname = new URL(route.request().url()).pathname;
const source = { id: 'unraid', state: 'healthy', freshness: 'stale', observedAt: zeroTimestamp, reason: 'source_stale' };
const body = pathname === '/api/v1/host' ? {
source,
identity: { name: 'Tower', version: '7.2.0' },
uptimeSeconds: 3600,
cpu: { totalPercent: 12, perCore: [12], iowaitPercent: 0 },
load: { one: 0.1, five: 0.2, fifteen: 0.3 },
memory: { totalBytes: 1024, availableBytes: 512, usedBytes: 512, utilizationPercent: 50, swapTotalBytes: 0, swapUsedBytes: 0, swapUtilizationPercent: 0 },
filesystems: [], network: [], time: { synchronized: true, offsetSeconds: 0, state: 'healthy' },
status: { state: 'unknown', reasons: [{ code: 'filesystem_root_not_configured', message: 'technical' }] },
observedAt: zeroTimestamp, receivedAt: zeroTimestamp,
warnings: ['filesystem_root_not_configured'],
} : pathname === '/api/v1/array' ? {
source, state: 'unknown', parity: { present: false, state: 'unknown', errors: 0 }, members: [],
} : pathname === '/api/v1/disks' ? {
source: { ...source, id: 'unraid-disks', reason: 'filesystem_root_not_configured' }, total: 0, disks: [],
} : pathname === '/api/v1/pools' ? {
source: { ...source, id: 'unraid-pools' }, total: 0, pools: [],
} : pathname === '/api/v1/applications' ? {
source, total: 0, applications: [],
} : pathname === '/api/v1/system/status' ? {
version: '1', generatedAt: zeroTimestamp, overallState: 'unknown', components: [], backup: { state: 'disabled', reason: 'not_configured' }, sourceLag: [],
} : {};
await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(body) });
});
}
test.beforeEach(async ({ page }) => mockSourceStatusData(page));
test('source status is human-readable and technically progressive on core routes', async ({ page }) => {
for (const route of ['/host', '/storage', '/applications']) {
await page.goto(route);
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
await expect(page.getByText('Nooit ontvangen').first()).toBeVisible();
await expect(page.getByText(/laatste meting is verouderd/i).first()).toBeVisible();
const visibleText = await page.locator('body').innerText();
expect(visibleText).not.toContain('source_stale');
expect(visibleText).not.toContain('filesystem_root_not_configured');
expect(visibleText).not.toMatch(/1 jan(?:uari)? 1/i);
expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth + 1)).toBe(true);
}
await page.goto('/host');
const technical = page.locator('.source-status-technical').first();
await expect(technical).not.toHaveAttribute('open');
await technical.getByText('Technische broninformatie').click();
await expect(technical.getByText('source_stale')).toBeVisible();
});
@@ -0,0 +1,32 @@
import AxeBuilder from '@axe-core/playwright';
import { expect, test } from '@playwright/test';
const enabled = Boolean(process.env.PULSE_E2E_REAL_BASE_URL);
test('storage map keeps physical identity and signal severities truthful', async ({ page }) => {
test.skip(!enabled, 'Run against an isolated real stack with the M11-03 storage fixture.');
const login = await page.request.get('/auth/test-login');
expect(login.ok()).toBeTruthy();
const disks = await (await page.request.get('/api/v1/disks?limit=100')).json() as { disks: Array<{ id: string; state: string; capacitySeverity: string; thermalSeverity: string }> };
expect(disks.disks.find((disk) => disk.id === 'disk-10')).toMatchObject({ state: 'online', capacitySeverity: 'critical', thermalSeverity: 'normal' });
expect(disks.disks.find((disk) => disk.id === 'cache')).toMatchObject({ state: 'online', capacitySeverity: 'attention', thermalSeverity: 'critical' });
const pools = await (await page.request.get('/api/v1/pools?limit=100')).json() as { pools: Array<{ id: string; state: string; capacitySeverity: string }> };
expect(pools.pools.find((pool) => pool.id === 'cache')).toMatchObject({ state: 'healthy', capacitySeverity: 'attention' });
await page.goto('/storage');
await expect(page.getByRole('heading', { level: 1, name: 'Opslagoverzicht' })).toBeVisible();
const visualNodes = page.locator('.storage-map-node');
await expect(visualNodes).toHaveCount(3);
await expect(visualNodes.filter({ hasText: 'disk10' })).toHaveCount(1);
await expect(visualNodes.filter({ hasText: 'disk10' })).toContainText('capaciteit kritiek');
await expect(visualNodes.filter({ hasText: 'cache' })).toHaveCount(2);
await expect(page.locator('.storage-heatmap-cell')).toHaveCount(2);
await expect(page.locator('.storage-heatmap-cell--critical')).toHaveCount(1);
const overflow = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth);
expect(overflow).toBeLessThanOrEqual(1);
const accessibility = await new AxeBuilder({ page }).analyze();
expect(accessibility.violations.filter((violation) => ['serious', 'critical'].includes(violation.impact ?? ''))).toEqual([]);
});
@@ -0,0 +1,62 @@
import { expect, test } from '@playwright/test';
const dashboard = { id: 'wallboard-dashboard', slug: 'operations', name: 'Operaties', description: 'Kritieke infrastructuur en recente gebeurtenissen.', scope: 'system', revision: 1, currentVersion: 1 };
const widgets = [
['system', 'Serverstatus', 0, 0, 6, 4, 'inventory', { entityType: 'server' }],
['cpu', 'CPU en belasting', 6, 0, 10, 6, 'text', {}],
['storage', 'Array en pools', 0, 6, 12, 7, 'text', {}],
['apps', 'Applicaties', 12, 6, 12, 7, 'text', {}],
['events', 'Recente gebeurtenissen', 0, 13, 24, 6, 'events', {}],
].map(([id, title, x, y, w, h, sourceType, scope]) => ({
id, title, type: id === 'events' ? 'event-timeline' : 'stat',
data: { sourceType, scope, limit: 12 }, behavior: { liveIntervalSeconds: 30 },
layouts: { wallboard: { x, y, w, h, visible: true }, desktop: { x: 0, y: 0, w: 6, h: 4, visible: true } },
}));
test('wallboard rotates bounded 1080p slides with truthful transport and data status', async ({ page }, testInfo) => {
test.skip(testInfo.project.name !== 'wallboard-chromium', 'Requires the 1920x1080 wallboard viewport.');
let dashboardReads = 0;
await page.route('**/api/v1/**', async (route) => {
const path = new URL(route.request().url()).pathname;
if (path === '/api/v1/dashboards') {
dashboardReads += 1;
// React development mode performs an initial StrictMode re-read. Fail the
// first scheduled refresh, not either of the initial bootstrap reads.
if (dashboardReads === 3) {
await route.fulfill({ status: 503, contentType: 'application/json', body: JSON.stringify({ error: 'temporary_unavailable' }) });
return;
}
}
const body = path === '/api/v1/dashboards' ? { items: [dashboard] }
: path === '/api/v1/dashboards/wallboard-dashboard' ? { dashboard, version: { document: { widgets, variables: [] } } }
: path === '/api/v1/system/status' ? { version: '1', generatedAt: new Date().toISOString(), overallState: 'degraded', components: [{ id: 'storage', state: 'degraded', reason: 'capacity_critical' }], backup: { state: 'disabled', reason: 'not_configured' }, sourceLag: [{ sourceId: 'unraid', state: 'healthy', reason: 'fresh' }] }
: path === '/api/v1/services' ? { services: [{ id: 'up', state: 'up' }, { id: 'down', state: 'down' }] }
: path === '/api/v1/incidents' ? { items: [{ id: 'incident-1' }] }
: path === '/api/v1/events' ? { items: [{ id: 'event-1', type: 'service.down', severity: 'critical', summary: 'Service niet beschikbaar', occurredAt: new Date().toISOString() }] }
: {};
await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(body) });
});
const startedAt = Date.now();
await page.goto('/wallboard?interval=15&refresh=10');
await expect(page.getByRole('heading', { level: 1, name: 'Operationeel wallboard' })).toBeVisible();
expect(Date.now() - startedAt).toBeLessThan(3_000);
await expect(page.getByText('Slide 1 / 2')).toBeVisible();
await expect(page.locator('.wallboard-connection').filter({ hasText: 'Transport' })).toContainText('Verbonden');
await expect(page.locator('.wallboard-connection').filter({ hasText: 'Data' })).toContainText('Bruikbaar');
await expect(page.locator('.wallboard-priority')).toContainText('OpslagVerstoord');
await expect(page.locator('.wallboard-priority')).toContainText('Services1 problemen');
await expect(page.locator('.wallboard-priority')).toContainText('Incidenten1 open');
await expect(page.getByRole('button', { name: /Bewerken|Exporteren/ })).toHaveCount(0);
expect(await page.evaluate(() => ({ horizontal: document.documentElement.scrollWidth - innerWidth, vertical: document.documentElement.scrollHeight - innerHeight }))).toEqual({ horizontal: 0, vertical: 0 });
if (process.env.PULSE_CAPTURE_VISUALS) await page.screenshot({ path: `../../artifacts/evidence/M14-04/wallboard-slide-1-${testInfo.project.name}.png` });
await expect(page.getByText('Slide 2 / 2')).toBeVisible({ timeout: 18_000 });
await expect(page.getByRole('heading', { level: 3, name: 'Recente gebeurtenissen' })).toBeVisible();
await expect(page.locator('.wallboard-connection').filter({ hasText: 'Transport' })).toContainText('Bron niet beschikbaar', { timeout: 12_000 });
await expect(page.locator('.wallboard-connection').filter({ hasText: 'Data' })).toContainText('Bruikbaar');
await expect(page.locator('.wallboard-connection').filter({ hasText: 'Transport' })).toContainText('Verbonden', { timeout: 12_000 });
await expect(page.getByText('Slide 2 / 2')).toBeVisible();
expect(await page.evaluate(() => ({ horizontal: document.documentElement.scrollWidth - innerWidth, vertical: document.documentElement.scrollHeight - innerHeight }))).toEqual({ horizontal: 0, vertical: 0 });
if (process.env.PULSE_CAPTURE_VISUALS) await page.screenshot({ path: `../../artifacts/evidence/M14-04/wallboard-slide-2-${testInfo.project.name}.png` });
});