import { expect, test, type APIRequestContext, type TestInfo } from '@playwright/test'; import AxeBuilder from '@axe-core/playwright'; import { spawnSync } from 'node:child_process'; import { realpathSync } from 'node:fs'; import path from 'node:path'; import os from 'node:os'; import { translations } from '../src/i18n/translations'; const api = 'http://localhost:5302/api'; const password = 'E2ePassword123!'; async function login(client: APIRequestContext, email: string) { expect((await client.post(`${api}/auth/login`, { data: { email, password } })).status()).toBe(200); const me = await (await client.get(`${api}/auth/me`)).json(); return { id: me.id as string, email, headers: { 'X-CSRF-TOKEN': (await client.storageState()).cookies.find(cookie => cookie.name === 'XSRF-TOKEN')!.value, 'X-Session-Context': me.sessionContext as string } }; } function seed(info: TestInfo, owner: Awaited>) { const root = info.config.metadata.isolatedDataRoot; if (typeof root !== 'string' || process.env.PLAYWRIGHT_REUSE_SERVERS === 'true') throw new Error('Explicit isolated non-reused test data required.'); const dataRoot = realpathSync(root); expect(path.dirname(dataRoot)).toBe(realpathSync(os.tmpdir())); const result = spawnSync(process.env.PLAYWRIGHT_PYTHON_PATH || (process.platform === 'win32' ? 'python' : 'python3'), ['-X', 'utf8', path.resolve('e2e/helpers/seed-operation.py')], { input: JSON.stringify({ dataRoot, ownerId: owner.id, ownerEmail: owner.email }), encoding: 'utf8', timeout: 15000, maxBuffer: 1024 * 1024 }); expect(result.status, result.stderr).toBe(0); return (JSON.parse(result.stdout) as { operationId: string }).operationId; } test('[D] Real administrator/owner boundaries, actual cancellation history and responsive EN/NB themes', async ({ page, playwright }, info) => { test.setTimeout(180000); const admin = await playwright.request.newContext(); const ownerClient = await playwright.request.newContext(); const otherClient = await playwright.request.newContext(); const anonymous = await playwright.request.newContext(); try { const administrator = await login(admin, 'e2e@example.test'); const email = `processing-owner-${Date.now()}@example.test`; const otherEmail = `processing-other-${Date.now()}@example.test`; for (const userEmail of [email, otherEmail]) expect((await admin.post(`${api}/users`, { headers: administrator.headers, data: { email: userEmail, password, roles: [] } })).status()).toBe(200); const owner = await login(ownerClient, email); const other = await login(otherClient, otherEmail); const id = seed(info, owner); for (const endpoint of ['/admin/processing', `/admin/processing/${id}`, `/admin/processing/${id}/events`]) { expect((await anonymous.get(`${api}${endpoint}`)).status()).toBe(401); expect((await ownerClient.get(`${api}${endpoint}`)).status()).toBe(403); } expect((await otherClient.get(`${api}/operations/${id}`)).status()).toBe(404); expect((await otherClient.post(`${api}/operations/${id}/cancel`, { headers: other.headers })).status()).toBe(404); const pageAdmin = await login(page.request, 'e2e@example.test'); await page.context().route('**/*', route => ['localhost', '127.0.0.1'].includes(new URL(route.request().url()).hostname) ? route.continue() : route.abort()); await page.goto('/admin/processing'); await page.getByLabel('Owner ID', { exact: true }).fill(owner.id); await page.getByLabel('Search users, jobs or task ID', { exact: true }).fill(id); await page.getByLabel('Worker filter', { exact: true }).selectOption('untracked'); await page.getByLabel('Submitted from (UTC)', { exact: true }).fill('2020-01-01'); await page.getByLabel('Submitted before (UTC)', { exact: true }).fill('2100-01-01'); await page.getByLabel('Application ID', { exact: true }).fill('2147483647'); await page.getByRole('button', { name: 'Apply filters', exact: true }).click(); await expect(page.getByText('No processing jobs match these filters.', { exact: true })).toBeVisible(); await page.getByLabel('Application ID', { exact: true }).fill(''); await page.getByRole('button', { name: 'Apply filters', exact: true }).click(); await page.getByRole('button', { name: `View processing details ${id}`, exact: true }).click(); await expect(page.getByLabel('Correlation ID', { exact: true })).toHaveValue(id); await expect(page.getByLabel('Submitting user ID', { exact: true })).toHaveValue(owner.id); await expect(page.getByText('No linked job', { exact: true })).toBeVisible(); await expect(page.getByText('No retained events. Older jobs may predate event recording.', { exact: true })).toBeVisible(); const cancellation = await ownerClient.post(`${api}/operations/${id}/cancel`, { headers: owner.headers }); expect(cancellation.status()).toBe(200); expect((await cancellation.json()).status).toBe('cancelled'); await page.getByRole('button', { name: 'Refresh', exact: true }).click(); await expect(page.getByRole('list').filter({ has: page.getByText(/^Processing cancelled/) })).toBeVisible(); const history = await (await admin.get(`${api}/admin/processing/${id}/events`)).json(); expect(history.items.map((item: { kind: string }) => item.kind)).toEqual(['Cancelled']); expect(history.items[0].atUtc).toMatch(/Z$/); const detail = await (await admin.get(`${api}/admin/processing/${id}`)).json(); expect(detail.owner.id).toBe(owner.id); for (const field of ['leaseToken', 'resultReference', 'failureMessage', 'subjectId', 'idempotencyKey']) expect(detail).not.toHaveProperty(field); expect(detail.worker).toBe('untracked'); for (const language of ['en', 'nb'] as const) for (const mode of ['light', 'dark'] as const) { const t = translations[language]; expect((await page.request.put(`${api}/auth/preferences/language`, { headers: pageAdmin.headers, data: { language: language === 'nb' ? 'nb-NO' : 'en' } })).status()).toBe(204); await page.evaluate(({ language, mode }) => { localStorage.setItem('uiLanguage', language); localStorage.setItem('jobtracker.themeMode', mode); }, { language, mode }); await page.reload(); await page.getByLabel(t.processingOwnerFilter, { exact: true }).fill(owner.id); await page.getByRole('button', { name: t.processingApplyFilters, exact: true }).click(); await page.getByRole('button', { name: t.processingViewDetails.replace('{id}', id), exact: true }).click(); await expect(page.getByLabel(t.processingCorrelation, { exact: true })).toHaveValue(id); for (const width of [1440, 768, 390]) { await page.setViewportSize({ width, height: 900 }); await expect.poll(() => page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1)).toBe(true); await page.screenshot({ path: info.outputPath(`processing-${language}-${mode}-${width}.png`), fullPage: true }); } const axe = await new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']).analyze(); await info.attach(`processing-${language}-${mode}-axe`, { body: JSON.stringify({ violations: axe.violations, incomplete: axe.incomplete }), contentType: 'application/json' }); expect(axe.violations).toEqual([]); } await info.attach('fixture-evidence-limit', { body: 'Guarded insertion of one inert unregistered task; actual native cancel creates its only event. No historical Created event, model job, inference, human accessibility or physical-device acceptance is fabricated.', contentType: 'text/plain' }); } finally { await Promise.all([admin.dispose(), ownerClient.dispose(), otherClient.dispose(), anonymous.dispose()]); } }); test('[D] Controlled unavailable/forbidden reads recover without exposing raw failures or cached metadata', async ({ page }) => { const administrator = await login(page.request, 'e2e@example.test'); expect((await page.request.put(`${api}/auth/preferences/language`, { headers: administrator.headers, data: { language: 'en' } })).status()).toBe(204); await page.addInitScript(() => localStorage.setItem('uiLanguage', 'en')); await page.context().route('**/*', route => ['localhost', '127.0.0.1'].includes(new URL(route.request().url()).hostname) ? route.continue() : route.abort()); let failure: number | null = 503; await page.route('**/api/admin/processing?**', route => failure ? route.fulfill({ status: failure, json: { message: 'SECRET_EXCEPTION_DOCUMENT_CONTENT' } }) : route.continue()); await page.goto('/admin/processing'); await expect(page.getByRole('alert').filter({ hasText: 'Processing information could not be loaded.' })).toBeVisible(); await expect(page.getByText(/SECRET_EXCEPTION/)).toHaveCount(0); failure = null; await page.getByRole('button', { name: 'Retry', exact: true }).click(); await expect(page.getByRole('region', { name: 'Processing jobs', exact: true })).toBeVisible(); let calls = 0; page.on('request', request => { if (new URL(request.url()).pathname === '/api/admin/processing') calls++; }); await page.getByRole('button', { name: 'Pause updates', exact: true }).click(); const paused = calls; await page.waitForTimeout(5500); // Negative assertion across one real polling interval. expect(calls).toBe(paused); failure = 403; await page.getByRole('button', { name: 'Resume updates', exact: true }).click(); await expect(page.getByRole('alert').filter({ hasText: 'Administrator access is required.' })).toBeVisible(); await expect(page.getByRole('region', { name: 'Processing jobs', exact: true })).toHaveCount(0); await expect(page.getByText(/SECRET_EXCEPTION/)).toHaveCount(0); });