Upgrade async GPU analysis and workbench UX

This commit is contained in:
Jens
2026-08-23 21:50:11 +02:00
parent 4040cbca7b
commit b996986d20
59 changed files with 3999 additions and 274 deletions
+141 -5
View File
@@ -63,9 +63,34 @@ async function auditInteractiveNames(page, label) {
return unnamed.length
}
async function prepareAuditSession(page, baseUrl) {
const sessionResponse = await page.request.get(`${baseUrl}/api/v1/auth/session`)
assert(sessionResponse.ok(), `Session preflight failed with HTTP ${sessionResponse.status()}`)
const sessionEnvelope = await sessionResponse.json()
const session = sessionEnvelope?.data
if (!session?.authentication_required || session.authenticated) return session
assert.equal(
session.guest_access_enabled,
true,
'UX audit needs an authenticated session or enabled guest access',
)
const guestResponse = await page.request.post(`${baseUrl}/api/v1/auth/guest`)
assert(guestResponse.ok(), `Guest audit session failed with HTTP ${guestResponse.status()}`)
const guestEnvelope = await guestResponse.json()
return guestEnvelope?.data
}
async function layoutEvidence(page) {
return page.evaluate(() => {
const root = document.documentElement
const rect = (selector) => {
const bounds = document.querySelector(selector)?.getBoundingClientRect()
return bounds
? { top: bounds.top, bottom: bounds.bottom, left: bounds.left, right: bounds.right, width: bounds.width, height: bounds.height }
: null
}
const main = document.querySelector('.workbench-main')?.getBoundingClientRect()
const map = document.querySelector('.geo-map-stage')?.getBoundingClientRect()
const theme = document.querySelector('.geo-theme-panel')?.getBoundingClientRect()
@@ -75,6 +100,11 @@ async function layoutEvidence(page) {
document_width: root.scrollWidth,
body_width: document.body.scrollWidth,
horizontal_overflow_px: Math.max(0, root.scrollWidth - root.clientWidth),
shell_navigation: rect('.workbench-sidebar'),
topbar: rect('.workbench-topbar'),
guest_banner: rect('.guest-mode-banner'),
explorer_header: rect('.geo-explorer-header'),
live_analysis_journey: rect('.live-analysis-journey'),
main: main ? { left: main.left, right: main.right, width: main.width } : null,
map: map ? { left: map.left, right: map.right, width: map.width, height: map.height } : null,
theme: theme ? { left: theme.left, right: theme.right, width: theme.width } : null,
@@ -82,6 +112,57 @@ async function layoutEvidence(page) {
})
}
async function runLandingViewport(browser, baseUrl, outputDir, viewport) {
const page = await browser.newPage({ viewport })
const consoleErrors = []
const failedRequests = []
page.on('console', (message) => {
if (message.type() === 'error') consoleErrors.push(message.text())
})
page.on('pageerror', (error) => consoleErrors.push(error.message))
page.on('requestfailed', (request) => {
if (request.url().startsWith(baseUrl)) {
failedRequests.push(`${request.method()} ${request.url()}: ${request.failure()?.errorText}`)
}
})
try {
await page.goto(baseUrl, { waitUntil: 'networkidle', timeout: 60_000 })
await page.locator('.landing-page').waitFor({ state: 'visible', timeout: 15_000 })
await auditInteractiveNames(page, `${viewport.width}px landing`)
const horizontalOverflow = await page.evaluate(() => (
Math.max(0, document.documentElement.scrollWidth - document.documentElement.clientWidth)
))
assert.equal(horizontalOverflow, 0, `${viewport.width}px landing overflows horizontally`)
assert.equal(
await page.getByRole('heading', { level: 1 }).count(),
1,
`${viewport.width}px landing needs one clear primary heading`,
)
if (viewport.width <= 760) {
const menu = page.locator('.landing-menu-toggle')
assert.equal(await menu.getAttribute('aria-label'), 'Navigatie openen')
await menu.click()
assert.equal(await menu.getAttribute('aria-expanded'), 'true')
await page.getByRole('navigation', { name: 'Landingspagina' }).waitFor({ state: 'visible' })
await page.getByRole('button', { name: 'Navigatie sluiten' }).click()
}
await page.screenshot({
path: path.join(outputDir, `landing-${viewport.width}x${viewport.height}.png`),
fullPage: true,
})
return {
viewport,
horizontal_overflow_px: horizontalOverflow,
console_errors: consoleErrors,
failed_requests: failedRequests,
}
} finally {
await page.close()
}
}
async function runViewport(browser, baseUrl, outputDir, viewport) {
const page = await browser.newPage({ viewport })
const consoleErrors = []
@@ -96,14 +177,41 @@ async function runViewport(browser, baseUrl, outputDir, viewport) {
}
})
try {
await prepareAuditSession(page, baseUrl)
const startedAt = Date.now()
await page.goto(baseUrl, { waitUntil: 'networkidle', timeout: 60_000 })
await page.getByTestId('map-workspace').waitFor({ state: 'visible', timeout: 30_000 })
const readyMs = Date.now() - startedAt
await auditInteractiveNames(page, `${viewport.width}px map explorer`)
const layout = await layoutEvidence(page)
const clippedNavigationLabels = await page.locator('.nav-item span').evaluateAll((labels) => labels
.filter((label) => label.getClientRects().length > 0 && label.scrollWidth > label.clientWidth + 1)
.map((label) => label.textContent?.trim() || ''))
assert.equal(layout.horizontal_overflow_px, 0, `${viewport.width}px layout overflows horizontally`)
assert.deepEqual(clippedNavigationLabels, [], `${viewport.width}px navigation clips visible labels`)
assert(layout.map && layout.map.width >= Math.min(320, viewport.width - 32), `${viewport.width}px map is too narrow`)
if (layout.topbar && layout.guest_banner) {
assert(
layout.topbar.bottom <= layout.guest_banner.top + 1,
`${viewport.width}px topbar overlaps the guest access banner`,
)
}
if (layout.guest_banner && layout.explorer_header) {
assert(
layout.guest_banner.bottom <= layout.explorer_header.top + 1,
`${viewport.width}px guest access banner overlaps the explorer heading`,
)
}
if (layout.shell_navigation && layout.live_analysis_journey) {
const verticalOverlap = Math.min(layout.shell_navigation.bottom, layout.live_analysis_journey.bottom)
- Math.max(layout.shell_navigation.top, layout.live_analysis_journey.top)
const horizontalOverlap = Math.min(layout.shell_navigation.right, layout.live_analysis_journey.right)
- Math.max(layout.shell_navigation.left, layout.live_analysis_journey.left)
assert(
verticalOverlap <= 1 || horizontalOverlap <= 1,
`${viewport.width}px navigation overlaps the live analysis journey`,
)
}
const currentTab = page.getByRole('tab', { name: 'Laatste toestand' })
const evolutionTab = page.getByRole('tab', { name: 'Evolutie' })
@@ -127,6 +235,7 @@ async function runViewport(browser, baseUrl, outputDir, viewport) {
viewport,
ready_ms: readyMs,
layout,
clipped_navigation_labels: clippedNavigationLabels,
console_errors: consoleErrors,
failed_requests: failedRequests,
}
@@ -144,6 +253,7 @@ async function runLoadingAndAdvancedAudit(browser, baseUrl, outputDir) {
await route.continue()
})
try {
const auditSession = await prepareAuditSession(page, baseUrl)
await page.goto(baseUrl, { waitUntil: 'domcontentloaded', timeout: 60_000 })
const loadingStatus = page.getByRole('status', { name: '' }).filter({
hasText: 'Databronnen worden gecontroleerd',
@@ -175,10 +285,30 @@ async function runLoadingAndAdvancedAudit(browser, baseUrl, outputDir) {
await page.screenshot({ path: path.join(outputDir, 'advanced-coverage-budget.png') })
const auditedWorkspaces = []
for (const workspace of ['data', 'assistant', 'analysis', 'ai', 'exports', 'overview', 'system']) {
const workspaceKeys = ['data', 'assistant', 'analysis', 'ai', 'exports', 'overview']
if (auditSession?.role === 'guest') {
assert.equal(
await page.getByTestId('workspace-nav-system').count(),
0,
'Guest session exposes operator-only system settings',
)
} else {
workspaceKeys.push('system')
}
for (const workspace of workspaceKeys) {
await page.getByTestId(`workspace-nav-${workspace}`).click()
await page.waitForTimeout(100)
await auditInteractiveNames(page, `${workspace} workspace`)
if (workspace === 'ai') {
await page.screenshot({ path: path.join(outputDir, 'ai-workspace.png'), fullPage: true })
const segmentationDisclosure = page.locator('.segmentation-disclosure')
await segmentationDisclosure.scrollIntoViewIfNeeded()
await segmentationDisclosure.locator('summary').first().click()
await page.waitForTimeout(150)
await auditInteractiveNames(page, 'open segmentation lab')
await segmentationDisclosure.locator('.ai-lab-run-surface').scrollIntoViewIfNeeded()
await page.screenshot({ path: path.join(outputDir, 'ai-segmentation.png') })
}
auditedWorkspaces.push(workspace)
}
@@ -203,21 +333,27 @@ async function main() {
schema_version: 1,
base_url: args.baseUrl,
started_at: new Date().toISOString(),
landing_viewports: [],
viewports: [],
bootstrap: null,
status: 'running',
}
try {
for (const viewport of [
const viewports = [
{ width: 390, height: 844 },
{ width: 1366, height: 768 },
{ width: 2560, height: 1080 },
]) {
]
for (const viewport of viewports) {
evidence.landing_viewports.push(await runLandingViewport(browser, args.baseUrl, outputDir, viewport))
}
for (const viewport of viewports) {
evidence.viewports.push(await runViewport(browser, args.baseUrl, outputDir, viewport))
}
evidence.bootstrap = await runLoadingAndAdvancedAudit(browser, args.baseUrl, outputDir)
const unexpectedConsoleErrors = evidence.viewports.flatMap((item) => item.console_errors)
const unexpectedFailedRequests = evidence.viewports.flatMap((item) => item.failed_requests)
const auditedPages = [...evidence.landing_viewports, ...evidence.viewports]
const unexpectedConsoleErrors = auditedPages.flatMap((item) => item.console_errors)
const unexpectedFailedRequests = auditedPages.flatMap((item) => item.failed_requests)
assert.deepEqual(unexpectedConsoleErrors, [], 'UX audit captured console errors')
assert.deepEqual(unexpectedFailedRequests, [], 'UX audit captured failed API requests')
evidence.status = 'passed'