Publish curated ITWorx-Pulse source from 0e0633c8ac91cb71312a26837f791035a78d81b8
Public source validation / validate (push) Failing after 2m10s
Public source validation / validate (push) Failing after 2m10s
This commit is contained in:
1 parent
a4956c2af9
commit
db8093b608
88 files changed
+3685
-469
No files matched your search
@@ -19,6 +19,14 @@ async function mockAPI(page: Page): Promise<void> {
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (path === '/api/v1/session') {
|
||||
await route.fulfill({ contentType: 'application/json', body: JSON.stringify({ subject: 'operator@example.test', role: 'operator', environment: 'test', permissions: { view: true, operate: true, edit: false, admin: false } }) });
|
||||
return;
|
||||
}
|
||||
if (path === '/api/v1/alerts') {
|
||||
await route.fulfill({ contentType: 'application/json', body: JSON.stringify({ items: [] }) });
|
||||
return;
|
||||
}
|
||||
if (path === '/api/v1/dashboards') {
|
||||
await route.fulfill({ contentType: 'application/json', body: JSON.stringify({ items: [] }) });
|
||||
return;
|
||||
@@ -70,7 +78,8 @@ test('overview exposes degraded/unknown state and passes axe', async ({ page },
|
||||
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);
|
||||
await expect(page.locator('.mobile-primary-list .nav-link')).toHaveCount(5);
|
||||
await expect(page.locator('.mobile-primary-list .nav-link span:last-child')).toHaveText(['Overzicht', 'Incidenten', 'Containers', 'Opslag', 'Services']);
|
||||
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);
|
||||
@@ -82,9 +91,9 @@ test('overview exposes degraded/unknown state and passes axe', async ({ page },
|
||||
} else {
|
||||
await expect(page.locator('.desktop-navigation')).toBeVisible();
|
||||
const rail = await page.locator('.sidebar').boundingBox();
|
||||
const commandHeader = await page.locator('.context-bar').boundingBox();
|
||||
const commandHeader = await page.locator('.operator-command-bar').boundingBox();
|
||||
expect(rail?.width).toBeLessThanOrEqual(72);
|
||||
expect(commandHeader?.height).toBe(56);
|
||||
expect(commandHeader?.height).toBeGreaterThanOrEqual(48);
|
||||
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);
|
||||
@@ -101,18 +110,22 @@ test('overview exposes degraded/unknown state and passes axe', async ({ page },
|
||||
}
|
||||
});
|
||||
|
||||
test('grouped navigation reaches infrastructure in two actions', async ({ page }, testInfo) => {
|
||||
test('navigation reaches the storage domain in at most 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();
|
||||
await page.getByRole('link', { name: 'Opslag' }).click();
|
||||
} else {
|
||||
await page.locator('.nav-group').filter({ hasText: 'Infrastructuur' }).locator('summary').click();
|
||||
await page.getByRole('link', { name: /Disks/ }).click();
|
||||
await page.getByRole('link', { name: /Opslagkaart/ }).click();
|
||||
}
|
||||
await expect(page).toHaveURL(/\/disks$/);
|
||||
await expect(page).toHaveURL(/\/storage$/);
|
||||
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
|
||||
if (testInfo.project.name === 'mobile-chromium') {
|
||||
await page.getByText('Meer', { exact: true }).click();
|
||||
await page.locator('.mobile-more').getByRole('link', { name: /Disks/ }).click();
|
||||
await expect(page).toHaveURL(/\/disks$/);
|
||||
}
|
||||
expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth + 1)).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
@@ -57,6 +57,12 @@ test('alertwerkruimte prioriteert operatie en opent configuratie doelgericht', a
|
||||
await expect(rows.first()).toContainText('Kritiek');
|
||||
await expect(page.getByRole('heading', { name: 'Geregistreerde regels' })).toHaveCount(0);
|
||||
|
||||
if (testInfo.project.name === 'mobile-chromium') {
|
||||
const detailBox = await rows.first().locator('.alert-operation-button').boundingBox();
|
||||
const actionBox = await rows.first().locator('.alert-operation-action').boundingBox();
|
||||
expect((detailBox?.y ?? 0) + (detailBox?.height ?? 0)).toBeLessThanOrEqual(actionBox?.y ?? 0);
|
||||
}
|
||||
|
||||
const acknowledge = page.getByRole('button', { name: 'Erkennen' }).first();
|
||||
page.once('dialog', (dialog) => dialog.dismiss());
|
||||
await acknowledge.click();
|
||||
|
||||
@@ -21,7 +21,9 @@ const widgets = [
|
||||
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'
|
||||
const body = requestPath === '/api/v1/session'
|
||||
? { subject: 'editor-1', role: 'editor', environment: 'test', permissions: { view: true, operate: true, edit: true, admin: false } }
|
||||
: 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 }
|
||||
@@ -75,7 +77,8 @@ test('dashboard and editor use human labels, safe modes and accessible keyboard
|
||||
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']);
|
||||
await expect(editorWidgets.locator('h3')).toHaveText(['Netwerkbelasting', 'Recente wijzigingen']);
|
||||
await expect(editorWidgets.first()).toHaveCSS('grid-row-start', '2');
|
||||
|
||||
const resize = page.getByRole('slider', { name: 'Breedte aanpassen: Netwerkbelasting' });
|
||||
await expect(resize).toHaveAttribute('aria-valuenow', '9');
|
||||
@@ -89,3 +92,21 @@ test('dashboard and editor use human labels, safe modes and accessible keyboard
|
||||
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 });
|
||||
});
|
||||
|
||||
test('mobile composer uses reorder and per-viewport visibility without horizontal overflow', async ({ page }, testInfo) => {
|
||||
test.skip(testInfo.project.name !== 'mobile-chromium', 'De mobiele composer gebruikt de ondersteunde 390px-viewport.');
|
||||
await mockDashboard(page);
|
||||
await page.goto('/dashboards/polish-dashboard');
|
||||
await page.getByRole('button', { name: 'Bewerken' }).click();
|
||||
await expect(page.getByRole('heading', { level: 1, name: 'Dashboard aanpassen' })).toBeVisible();
|
||||
await expect(page.getByLabel('Layout')).toHaveValue('mobile');
|
||||
await expect(page.getByLabel('Breedte in kolommen')).toHaveValue('1');
|
||||
const widgets = page.locator('.editor-widget');
|
||||
await widgets.nth(1).getByRole('button', { name: 'Widget omhoog: Recente wijzigingen' }).click();
|
||||
await expect(widgets.locator('h3')).toHaveText(['Recente wijzigingen', 'Netwerkbelasting']);
|
||||
await widgets.first().getByRole('button', { name: 'Verberg in Mobiel' }).click();
|
||||
await expect(widgets.first()).toHaveClass(/editor-widget--hidden/);
|
||||
await expect(page.getByRole('button', { name: /Widget naar links/ })).toHaveCount(0);
|
||||
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1)).toBe(true);
|
||||
await expectNoSeriousAxeViolations(page);
|
||||
});
|
||||
@@ -0,0 +1,94 @@
|
||||
import AxeBuilder from '@axe-core/playwright';
|
||||
import { expect, test, type Page, type Route } from '@playwright/test';
|
||||
|
||||
type Dashboard = { id: string; slug: string; name: string; description: string; ownerId: string; scope: string; revision: number; currentVersion: number; archivedAt?: string };
|
||||
|
||||
const observedAt = new Date().toISOString();
|
||||
const initial: Dashboard = { id: 'dashboard-1', slug: 'operations', name: 'Operations', description: 'Dagelijkse operatie', ownerId: 'editor-1', scope: 'shared', revision: 1, currentVersion: 2 };
|
||||
|
||||
async function fulfill(route: Route, body: unknown, status = 200): Promise<void> {
|
||||
await route.fulfill({ status, contentType: status >= 400 ? 'application/problem+json' : 'application/json', body: JSON.stringify(body) });
|
||||
}
|
||||
|
||||
async function mockShell(page: Page, session: unknown): Promise<void> {
|
||||
await page.route('**/api/v1/system/status', (route) => fulfill(route, { version: '1.5.0', generatedAt: observedAt, overallState: 'healthy', components: [], backup: { state: 'disabled', reason: 'not_configured' }, sourceLag: [{ sourceId: 'test', state: 'healthy', reason: 'ok' }] }));
|
||||
await page.route('**/api/v1/session', (route) => fulfill(route, session));
|
||||
await page.route('**/api/v1/host', (route) => fulfill(route, { identity: { name: 'lifecycle-fixture' } }));
|
||||
await page.route('**/api/v1/alerts**', (route) => fulfill(route, { items: [] }));
|
||||
await page.route('**/api/v1/incidents**', (route) => fulfill(route, { items: [] }));
|
||||
}
|
||||
|
||||
test('editor completes the dashboard lifecycle with revision locks', async ({ page }, testInfo) => {
|
||||
test.skip(testInfo.project.name === 'wallboard-chromium', 'Dashboardbeheer is geen wallboardtaak.');
|
||||
const dashboards: Dashboard[] = [{ ...initial }];
|
||||
const revisionHeaders: string[] = [];
|
||||
await mockShell(page, { subject: 'editor-1', role: 'editor', environment: 'test', permissions: { view: true, operate: true, edit: true, admin: false } });
|
||||
await page.route('**/api/v1/dashboards**', async (route) => {
|
||||
const request = route.request();
|
||||
const url = new URL(request.url());
|
||||
const path = url.pathname;
|
||||
const method = request.method();
|
||||
if (path === '/api/v1/dashboards' && method === 'GET') return fulfill(route, { items: dashboards });
|
||||
if (path === '/api/v1/dashboards' && method === 'POST') {
|
||||
const document = request.postDataJSON() as { slug: string; name: string; description: string; scope: string };
|
||||
const created = { id: 'dashboard-created', slug: document.slug, name: document.name, description: document.description, ownerId: 'editor-1', scope: document.scope, revision: 1, currentVersion: 1 };
|
||||
dashboards.push(created); return fulfill(route, { dashboard: created }, 201);
|
||||
}
|
||||
const item = dashboards.find((candidate) => path.includes(candidate.id));
|
||||
if (!item) return fulfill(route, { code: 'NOT_FOUND' }, 404);
|
||||
if (path.endsWith('/versions') && method === 'GET') return fulfill(route, { items: [
|
||||
{ id: 'v2', dashboardId: item.id, number: 2, schemaVersion: 2, document: {}, changeSummary: 'metadata', createdBy: 'editor-1', createdAt: observedAt },
|
||||
{ id: 'v1', dashboardId: item.id, number: 1, schemaVersion: 2, document: {}, changeSummary: 'initial', createdBy: 'editor-1', createdAt: observedAt },
|
||||
] });
|
||||
if (path.endsWith('/restore/1') && method === 'POST') { revisionHeaders.push(request.headers()['if-match'] ?? ''); item.revision += 1; item.currentVersion += 1; return fulfill(route, { dashboard: item }); }
|
||||
if (path.endsWith('/clone') && method === 'POST') {
|
||||
const body = request.postDataJSON() as { slug: string; name: string };
|
||||
const clone = { ...item, id: 'dashboard-clone', slug: body.slug, name: body.name, revision: 1, currentVersion: 1 };
|
||||
dashboards.push(clone); return fulfill(route, { dashboard: clone }, 201);
|
||||
}
|
||||
if (method === 'PATCH') { revisionHeaders.push(request.headers()['if-match'] ?? ''); const body = request.postDataJSON() as { name: string; description: string }; Object.assign(item, body, { revision: item.revision + 1 }); return fulfill(route, { dashboard: item }); }
|
||||
if (method === 'DELETE') { revisionHeaders.push(request.headers()['if-match'] ?? ''); item.revision += 1; item.archivedAt = observedAt; return fulfill(route, { dashboard: item }); }
|
||||
return fulfill(route, { code: 'NOT_FOUND' }, 404);
|
||||
});
|
||||
|
||||
await page.goto('/dashboards');
|
||||
await expect(page.getByRole('heading', { level: 1, name: 'Dashboardbeheer' })).toBeVisible();
|
||||
await page.getByRole('button', { name: 'Nieuw dashboard' }).click();
|
||||
await page.getByLabel('Naam').fill('Nachtzicht');
|
||||
await page.getByLabel('Template').selectOption('blank');
|
||||
await page.getByRole('button', { name: 'Dashboard maken' }).click();
|
||||
await expect(page.locator('.dashboard-manager-row').filter({ has: page.getByText('Nachtzicht', { exact: true }) })).toBeVisible();
|
||||
|
||||
let row = page.locator('.dashboard-manager-row').filter({ hasText: 'Operations' });
|
||||
await row.getByRole('button', { name: 'Beheren' }).click();
|
||||
await row.getByLabel('Naam').fill('Operations EU');
|
||||
await row.getByRole('button', { name: 'Naam en beschrijving opslaan' }).click();
|
||||
row = page.locator('.dashboard-manager-row').filter({ has: page.getByText('Operations EU', { exact: true }) });
|
||||
await row.getByRole('button', { name: 'Beheren' }).click();
|
||||
await row.getByRole('button', { name: 'Dupliceren' }).click();
|
||||
await expect(page.getByText('Operations EU (kopie)', { exact: true })).toBeVisible();
|
||||
await row.getByRole('button', { name: 'Versies bekijken' }).click();
|
||||
await row.getByRole('button', { name: 'Herstellen als huidige versie' }).click();
|
||||
await row.getByRole('button', { name: 'Archiveren' }).click();
|
||||
await row.getByRole('button', { name: 'Ja, archiveren' }).click();
|
||||
await expect(page.getByText('Dashboard gearchiveerd.')).toBeVisible();
|
||||
expect(revisionHeaders).toEqual(['1', '2', '3']);
|
||||
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([]);
|
||||
});
|
||||
|
||||
test('viewer gets a read-only dashboard manager', async ({ page }, testInfo) => {
|
||||
test.skip(testInfo.project.name === 'wallboard-chromium', 'Dashboardbeheer is geen wallboardtaak.');
|
||||
await mockShell(page, { subject: 'viewer-1', role: 'viewer', environment: 'test', permissions: { view: true, operate: false, edit: false, admin: false } });
|
||||
await page.route('**/api/v1/dashboards**', (route) => new URL(route.request().url()).pathname === '/api/v1/dashboards/dashboard-1'
|
||||
? fulfill(route, { dashboard: initial, version: { document: { schemaVersion: 2, id: '00000000-0000-4000-8000-000000000001', slug: 'operations', name: 'Operations', scope: 'shared', variables: [], widgets: [], settings: { defaultTimeRange: '1h' } } } })
|
||||
: fulfill(route, { items: [initial] }));
|
||||
await page.goto('/dashboards');
|
||||
await expect(page.getByText('Alleen bekijken')).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Nieuw dashboard' })).toHaveCount(0);
|
||||
await expect(page.getByRole('button', { name: 'Beheren' })).toHaveCount(0);
|
||||
await page.locator('.dashboard-manager-open').click();
|
||||
await expect(page.getByRole('heading', { level: 1, name: 'Operations' })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Bewerken' })).toHaveCount(0);
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import AxeBuilder from '@axe-core/playwright';
|
||||
import { expect, test, type Page, type Route } from '@playwright/test';
|
||||
|
||||
const observedAt = '2026-09-01T10:00:00Z';
|
||||
const layout = { desktop: { x: 0, y: 0, w: 6, h: 4, visible: true }, tablet: { x: 0, y: 0, w: 6, h: 4, visible: true }, mobile: { x: 0, y: 0, w: 1, h: 4, visible: true }, wallboard: { x: 0, y: 0, w: 6, h: 4, visible: true } };
|
||||
|
||||
async function fulfill(route: Route, body: unknown): Promise<void> { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(body) }); }
|
||||
async function shell(page: Page): Promise<void> {
|
||||
await page.route('**/api/v1/system/status', (route) => fulfill(route, { version: '1.5.0', generatedAt: observedAt, overallState: 'healthy', components: [], backup: { state: 'disabled', reason: 'not_configured' }, sourceLag: [] }));
|
||||
await page.route('**/api/v1/session', (route) => fulfill(route, { subject: 'viewer-1', role: 'viewer', environment: 'test', permissions: { view: true, operate: false, edit: false, admin: false } }));
|
||||
await page.route('**/api/v1/host', (route) => fulfill(route, { identity: { name: 'runtime-fixture' } }));
|
||||
}
|
||||
|
||||
test('dashboard renders bounded alerts, incidents, text and contains an unknown type', async ({ page }, testInfo) => {
|
||||
test.skip(testInfo.project.name === 'wallboard-chromium', 'De runtimeflow wordt apart als wallboard gevalideerd.');
|
||||
await shell(page);
|
||||
await page.route('**/api/v1/alerts**', (route) => fulfill(route, { items: [{ id: 'alert-1', ruleName: 'Disk warm', state: 'firing', severity: 'critical', entityName: 'disk1', reason: 'Temperatuur hoog', updatedAt: observedAt }] }));
|
||||
await page.route('**/api/v1/incidents**', (route) => fulfill(route, { items: [{ id: 'incident-1', title: 'Opslagincident', status: 'open', severity: 'warning', summary: 'Controle nodig', updatedAt: observedAt }] }));
|
||||
await page.route('**/api/v1/dashboards/runtime', (route) => fulfill(route, {
|
||||
dashboard: { id: 'runtime', slug: 'runtime', name: 'Runtimebewijs', description: 'Widgetadapters', scope: 'shared', revision: 1, currentVersion: 1 },
|
||||
version: { document: { variables: [], widgets: [
|
||||
{ id: 'alert-widget', type: 'alert-summary', title: 'Actieve meldingen', data: { sourceType: 'alerts', limit: 1000 }, behavior: { liveIntervalSeconds: 1 }, layouts: layout },
|
||||
{ id: 'incident-widget', type: 'table', title: 'Open incidenten', data: { sourceType: 'incidents', limit: 1000 }, behavior: { liveIntervalSeconds: 1 }, layouts: { ...layout, desktop: { ...layout.desktop, x: 6 } } },
|
||||
{ id: 'text-widget', type: 'text', title: 'Runbook', data: { sourceType: 'text', text: '# Start\n[Open incidenten](/incidents)\n<script>alert(1)</script>' }, behavior: {}, layouts: { ...layout, desktop: { ...layout.desktop, y: 4 } } },
|
||||
{ id: 'unknown-widget', type: 'made-up', title: 'Onbekend', data: { sourceType: 'alerts', limit: 5 }, behavior: {}, layouts: { ...layout, desktop: { ...layout.desktop, x: 6, y: 4 } } },
|
||||
] } },
|
||||
}));
|
||||
|
||||
await page.goto('/dashboards/runtime');
|
||||
await expect(page.getByRole('heading', { level: 1, name: 'Runtimebewijs' })).toBeVisible();
|
||||
await expect(page.getByText('Disk warm')).toBeVisible();
|
||||
await expect(page.getByText('Opslagincident')).toBeVisible();
|
||||
await expect(page.getByRole('link', { name: 'Open incidenten' })).toHaveAttribute('href', '/incidents');
|
||||
await expect(page.getByText('<script>alert(1)</script>')).toBeVisible();
|
||||
await expect(page.getByText('Onbekend widgettype')).toBeVisible();
|
||||
expect(await page.evaluate(() => document.querySelectorAll('script').length)).toBeLessThan(10);
|
||||
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([]);
|
||||
});
|
||||
@@ -47,12 +47,14 @@ test('100 events blijven compact, filterbaar en toetsenbordnavigeerbaar', async
|
||||
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 page.getByRole('searchbox', { name: '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' });
|
||||
// Keep keyboard focus reliable when animation frames are throttled.
|
||||
await page.evaluate(() => { window.requestAnimationFrame = () => 0; });
|
||||
await next.focus();
|
||||
await page.keyboard.press('Enter');
|
||||
await expect(page.locator('.list-pager [role="status"]')).toBeFocused();
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
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<void> { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(body) }); }
|
||||
async function shell(page: Page): Promise<void> {
|
||||
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<Record<string, unknown>> = [];
|
||||
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<string, unknown>; 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([]);
|
||||
});
|
||||
@@ -46,7 +46,7 @@ test('process and inventory filters survive navigation with mobile-first cards',
|
||||
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.getByRole('searchbox', { name: '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);
|
||||
@@ -55,7 +55,7 @@ test('process and inventory filters survive navigation with mobile-first cards',
|
||||
|
||||
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.getByRole('searchbox', { name: '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);
|
||||
@@ -72,7 +72,8 @@ test('all 150 containers remain reachable with shareable filters and bounded mob
|
||||
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 firstPageBudgetMs = process.env.CI ? 8_000 : 3_000;
|
||||
expect(Date.now() - started).toBeLessThan(firstPageBudgetMs);
|
||||
const seen = new Set<string>();
|
||||
for (let pageNumber = 1; pageNumber <= 6; pageNumber += 1) {
|
||||
await expect(visibleRows).toHaveCount(25);
|
||||
@@ -91,7 +92,7 @@ test('all 150 containers remain reachable with shareable filters and bounded mob
|
||||
expect(controls.every((height) => height >= 44)).toBe(true);
|
||||
}
|
||||
|
||||
await page.getByLabel('Zoeken').fill('container-150');
|
||||
await page.getByRole('searchbox', { name: 'Zoeken' }).fill('container-150');
|
||||
await expect(visibleRows).toHaveCount(1);
|
||||
await expect(page).toHaveURL(/q=container-150/);
|
||||
await page.reload();
|
||||
|
||||
@@ -39,19 +39,28 @@ test('beheerhub, afgeronde onboarding en backupouderdom blijven taakgericht en w
|
||||
await route.fulfill({ contentType: 'application/json', body: JSON.stringify(onboarding) });
|
||||
return;
|
||||
}
|
||||
if (pathname === '/api/v1/host') {
|
||||
await route.fulfill({ contentType: 'application/json', body: JSON.stringify({ hardware: { status: { state: 'healthy' }, capabilities: [], temperatures: [], fans: [], gpus: [] } }) });
|
||||
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')).toHaveCount(8);
|
||||
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: beheerder')).toBeVisible();
|
||||
await expect(hub.getByText('Wijzigen: operator')).toBeVisible();
|
||||
const capabilities = page.getByRole('region', { name: 'Capabilitystatus' });
|
||||
await expect(capabilities).toContainText('Unraid');
|
||||
await expect(capabilities).toContainText('HardwaretelemetrieBeschikbaar');
|
||||
await expect(capabilities).toContainText('UPSNiet geconfigureerd');
|
||||
await expect(capabilities.getByRole('link', { name: 'Open systeemstatus en back-up' })).toHaveAttribute('href', '/status');
|
||||
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 });
|
||||
}
|
||||
@@ -70,7 +79,19 @@ test('beheerhub, afgeronde onboarding en backupouderdom blijven taakgericht en w
|
||||
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();
|
||||
const reconfigure = page.getByRole('button', { name: 'Herconfiguratie openen' });
|
||||
if (testInfo.project.name === 'mobile-chromium') {
|
||||
await reconfigure.scrollIntoViewIfNeeded();
|
||||
expect(await reconfigure.evaluate((button) => {
|
||||
const box = button.getBoundingClientRect();
|
||||
const target = document.elementFromPoint(box.left + box.width / 2, box.top + box.height / 2);
|
||||
return target === button || button.contains(target);
|
||||
})).toBe(true);
|
||||
await reconfigure.focus();
|
||||
await page.keyboard.press('Enter');
|
||||
} else {
|
||||
await reconfigure.click();
|
||||
}
|
||||
await expect(page.getByRole('radio')).toHaveCount(4);
|
||||
await page.getByRole('button', { name: 'Annuleren' }).click();
|
||||
await expect(page.getByRole('radio')).toHaveCount(0);
|
||||
|
||||
@@ -5,9 +5,14 @@ const now = new Date().toISOString();
|
||||
|
||||
test('mobile incident command mode is prioritized, bounded and accessible', async ({ page }, testInfo) => {
|
||||
test.skip(testInfo.project.name !== 'mobile-chromium', 'Mobile incident mode uses the supported 390x844 viewport.');
|
||||
const mutations: Array<{ path: string; method: string; ifMatch: string | null; idempotencyKey: string | null; body: unknown }> = [];
|
||||
let canOperate = true;
|
||||
await page.route('**/api/v1/**', async (route) => {
|
||||
const path = new URL(route.request().url()).pathname;
|
||||
const body = path === '/api/v1/system/status' ? {
|
||||
if (route.request().method() !== 'GET') mutations.push({ path, method: route.request().method(), ifMatch: route.request().headers()['if-match'] ?? null, idempotencyKey: route.request().headers()['idempotency-key'] ?? null, body: route.request().postDataJSON() });
|
||||
const body = path === '/api/v1/session' ? {
|
||||
subject: canOperate ? 'operator-1' : 'viewer-1', role: canOperate ? 'operator' : 'viewer', environment: 'test', permissions: { view: true, operate: canOperate, edit: false, admin: false },
|
||||
} : path === '/api/v1/system/status' ? {
|
||||
version: '1', generatedAt: now, overallState: 'degraded', components: [],
|
||||
backup: { state: 'disabled', reason: 'not_configured' }, sourceLag: [],
|
||||
} : path === '/api/v1/incidents/incident-1' ? {
|
||||
@@ -19,6 +24,10 @@ test('mobile incident command mode is prioritized, bounded and accessible', asyn
|
||||
{ alertId: 'alert-1', rationale: 'Capaciteitsdrempel van 90% overschreden', confidence: .84, correlationMethod: 'temporal-window', manual: false, createdAt: now },
|
||||
], notes: [{ id: 'note-1', incidentId: 'incident-1', author: 'Pulse', body: 'Mover is niet actief.', createdAt: now }],
|
||||
},
|
||||
} : path === '/api/v1/alerts/alert-1' ? {
|
||||
alert: { id: 'alert-1', state: 'firing', retainedState: 'firing', ruleName: 'Cachepoolcapaciteit', severity: 'critical', reason: 'Capaciteitsdrempel overschreden', revision: 7, updatedAt: now },
|
||||
} : path === '/api/v1/alerts/alert-1/acknowledge' ? {
|
||||
alert: { id: 'alert-1', state: 'acknowledged', revision: 8 },
|
||||
} : {};
|
||||
await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(body) });
|
||||
});
|
||||
@@ -28,12 +37,23 @@ test('mobile incident command mode is prioritized, bounded and accessible', asyn
|
||||
await expect(page.locator('.incident-command-strip')).toContainText('Kritiek');
|
||||
await expect(page.locator('.incident-command-strip')).toContainText('84%');
|
||||
await expect(page.locator('.incident-timeline')).toContainText('Capaciteitsdrempel van 90% overschreden');
|
||||
await page.getByRole('button', { name: 'Melding erkennen' }).click();
|
||||
await expect(page.getByRole('status')).toContainText('lost de infrastructuur niet automatisch op');
|
||||
expect(mutations).toHaveLength(1);
|
||||
expect(mutations[0]).toMatchObject({ path: '/api/v1/alerts/alert-1/acknowledge', method: 'POST', ifMatch: '7', body: { evaluationKey: 'incident-incident-1-alert-alert-1-ack-7' } });
|
||||
expect(mutations[0].idempotencyKey).toBe('incident-incident-1-alert-alert-1-ack-7');
|
||||
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBe(390);
|
||||
const 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 undersizedTargets = await page.locator('button, .mobile-navigation a, .mobile-more summary').evaluateAll((items) => items.filter((item) => {
|
||||
const style = getComputedStyle(item); return style.display !== 'none' && style.visibility !== 'hidden' && item.getClientRects().length > 0;
|
||||
}).map((item) => ({ label: item.textContent?.trim(), height: item.getBoundingClientRect().height })).filter((item) => item.height < 44));
|
||||
expect(undersizedTargets).toEqual([]);
|
||||
const axe = await new AxeBuilder({ page }).analyze();
|
||||
expect(axe.violations.filter((item) => item.impact === 'critical' || item.impact === 'serious')).toEqual([]);
|
||||
canOperate = false;
|
||||
await page.reload();
|
||||
await expect(page.getByRole('button', { name: 'Melding erkennen' })).toHaveCount(0);
|
||||
await expect(page.getByRole('button', { name: 'Eigenaar opslaan' })).toBeDisabled();
|
||||
await expect(page.getByText('Je rol kan de opvolgcontext alleen bekijken.')).toBeVisible();
|
||||
expect(mutations).toHaveLength(1);
|
||||
if (process.env.PULSE_CAPTURE_VISUALS) await page.screenshot({ path: '../../artifacts/evidence/M14-04/mobile-incident-command.png', fullPage: true });
|
||||
});
|
||||
@@ -33,7 +33,9 @@ test('mobile primary navigation and operational reasons remain readable at 390px
|
||||
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);
|
||||
await expect(labels).toHaveCount(5);
|
||||
const navigation = await page.locator('.mobile-navigation').boundingBox();
|
||||
expect(navigation?.height).toBeLessThanOrEqual(65);
|
||||
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) };
|
||||
@@ -45,7 +47,7 @@ test('mobile primary navigation and operational reasons remain readable at 390px
|
||||
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)
|
||||
.filter((item) => item.clientWidth > 1 && (item.right > innerWidth + 1 || item.scrollWidth > item.clientWidth + 1))
|
||||
.slice(0, 12));
|
||||
expect(overflow).toEqual([]);
|
||||
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBe(390);
|
||||
|
||||
Reference in new issue
Block a user