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(); 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 }); });