import { expect, test, type Page, type Route } from '@playwright/test'; import Fastify, { type FastifyInstance } from 'fastify'; import { securityMiddleware } from '../../server/src/local/security-middleware.js'; const teamSlug = 'design-team'; const authToken = 'test-token'; const realAuthTeamSlug = 'admin-real-auth'; const realAuthToken = 'admin-real-token'; const pages = [ { key: 'dashboard', label: 'Dashboard', heading: /Test Team Dashboard|Dashboard/ }, { key: 'analytics', label: 'Analytics', heading: /Usage Analytics/ }, { key: 'members', label: 'Members', heading: /Team Members/ }, { key: 'capabilities', label: 'Capabilities', heading: /Capabilities/ }, { key: 'jobs', label: 'Jobs', heading: /Agent Jobs/ }, { key: 'audit', label: 'Audit Log', heading: /Audit Log/ }, { key: 'settings', label: 'Team Settings', heading: /Team Settings/ }, ] as const; const mockTeam = { id: 'team-1', name: 'Test Team', slug: teamSlug, ownerId: 'u1', createdAt: '2026-01-01T00:00:00Z', members: [ { userId: 'u1', displayName: 'Alice Admin', email: 'alice@example.com', role: 'owner', joinedAt: '2026-01-02T00:00:00Z' }, { userId: 'u2', displayName: 'Bob Builder', email: 'bob@example.com', role: 'admin', joinedAt: '2026-01-03T00:00:00Z' }, { userId: 'u3', displayName: 'Cara Coordinator', email: 'cara@example.com', role: 'member', joinedAt: '2026-01-04T00:00:00Z' }, ], }; const mockTasks = [ { id: 'task-1', teamId: 'team-1', title: 'Review enterprise rollout', status: 'open', priority: 'high', createdBy: 'u1', createdAt: '2026-01-05T10:00:00Z', updatedAt: '2026-01-05T10:00:00Z' }, { id: 'task-2', teamId: 'team-1', title: 'Prepare audit packet', status: 'in-progress', priority: 'medium', createdBy: 'u2', createdAt: '2026-01-06T10:00:00Z', updatedAt: '2026-01-06T10:00:00Z' }, ]; const mockAnalytics = { activeUsers: { daily: 7, weekly: 19, monthly: 42 }, tokenUsage: { total: 1500000, byUser: [ { userId: 'u1', name: 'Alice Admin', tokens: 800000, cost: 4.5 }, { userId: 'u2', name: 'Bob Builder', tokens: 700000, cost: 3.2 }, ], }, topTools: [ { name: 'memory_search', invocations: 150 }, { name: 'shell_exec', invocations: 80 }, ], topCommands: [ { name: '/research', count: 45 }, { name: '/draft', count: 30 }, ], capabilityGaps: [ { tool: 'browser_navigate', requestCount: 9, suggestion: 'Install browser skill' }, ], performanceTrends: { correctionRate: 0.15, correctionTrend: -0.03, avgResponseTime: 8.5 }, }; const mockPolicies = [ { id: 'p1', teamId: 'team-1', role: 'owner', allowedSources: ['native', 'skill', 'mcp'], blockedTools: [], approvalThreshold: 'none', createdAt: '2026-01-01T00:00:00Z', updatedAt: '2026-01-01T00:00:00Z' }, { id: 'p2', teamId: 'team-1', role: 'member', allowedSources: ['native', 'skill'], blockedTools: ['shell_exec'], approvalThreshold: 'medium', createdAt: '2026-01-01T00:00:00Z', updatedAt: '2026-01-01T00:00:00Z' }, ]; const mockOverrides = [ { id: 'o1', teamId: 'team-1', capabilityName: 'browser_navigate', capabilityType: 'mcp', decision: 'approved', reason: 'Needed for research', decidedBy: 'u1', createdAt: '2026-01-01T00:00:00Z', decidedAt: '2026-01-01T00:00:00Z' }, ]; const mockRequests = [ { id: 'r1', teamId: 'team-1', requestedBy: 'u2', capabilityName: 'send_email', capabilityType: 'plugin', justification: 'Send customer follow-up', status: 'pending', createdAt: '2026-01-01T00:00:00Z' }, ]; const mockJobs = [ { id: 'job-abc12345-long-id', teamId: 'team-1', userId: 'u1', jobType: 'agent_task', status: 'completed', input: {}, output: {}, createdAt: '2026-01-07T12:00:00Z', completedAt: '2026-01-07T12:05:00Z' }, { id: 'job-def67890-long-id', teamId: 'team-1', userId: 'u2', jobType: 'research', status: 'running', input: {}, createdAt: '2026-01-08T10:00:00Z' }, ]; const mockAudit = [ { id: 'a1', userId: 'u1', teamId: 'team-1', agentName: 'waggle-1', actionType: 'tool_use', description: 'Executed shell command for deployment diagnostics', requiresApproval: true, approved: true, approvedBy: 'u1', createdAt: '2026-01-08T10:00:00Z' }, { id: 'a2', userId: 'u2', teamId: 'team-1', agentName: 'waggle-2', actionType: 'memory_write', description: 'Stored project context', requiresApproval: false, createdAt: '2026-01-08T11:00:00Z' }, ]; async function fulfillMockAdminApi(route: Route) { const url = new URL(route.request().url()); const path = `${url.pathname}${url.search}`; let body: unknown; if (path === `/api/teams/${teamSlug}`) body = mockTeam; else if (path === `/api/teams/${teamSlug}/tasks`) body = mockTasks; else if (path === `/api/admin/teams/${teamSlug}/analytics`) body = mockAnalytics; else if (path === `/api/teams/${teamSlug}/capability-policies`) body = mockPolicies; else if (path === `/api/teams/${teamSlug}/capability-overrides`) body = mockOverrides; else if (path === `/api/teams/${teamSlug}/capability-requests`) body = mockRequests; else if (path === `/api/jobs?teamSlug=${teamSlug}`) body = mockJobs; else if (path === `/api/admin/teams/${teamSlug}/audit`) body = mockAudit; else body = {}; await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(body), }); } async function mockAdminApi(page: Page) { await page.route('http://localhost:3100/**', fulfillMockAdminApi); } type RealAuthServer = FastifyInstance; async function buildRealAuthServer() { const server = Fastify({ logger: false }); await server.register(securityMiddleware, { sessionToken: realAuthToken }); server.get(`/api/teams/${realAuthTeamSlug}`, async () => ({ id: 'real-auth-team-id', name: 'Real Auth Team', slug: realAuthTeamSlug, ownerId: 'real-auth-owner-id', createdAt: '2026-07-08T00:00:00Z', members: [ { userId: 'real-auth-owner-id', displayName: 'Real Auth Owner', email: 'admin-real-auth-owner@test.com', role: 'owner', joinedAt: '2026-07-08T00:00:00Z', }, ], })); server.get(`/api/teams/${realAuthTeamSlug}/tasks`, async () => ([ { id: 'real-auth-task-id', teamId: 'real-auth-team-id', title: 'Review real-auth launch gate', status: 'open', priority: 'high', createdBy: 'real-auth-owner-id', createdAt: '2026-07-08T00:00:00Z', updatedAt: '2026-07-08T00:00:00Z', }, ])); await server.ready(); return server; } async function routeAdminApiThroughServer(page: Page, server: RealAuthServer) { await page.unroute('http://localhost:3100/**'); await page.route('http://localhost:3100/**', async (route) => { const request = route.request(); const url = new URL(request.url()); const requestHeaders = request.headers(); const response = await server.inject({ method: request.method(), url: `${url.pathname}${url.search}`, headers: { ...(requestHeaders.authorization ? { authorization: requestHeaders.authorization } : {}), ...(requestHeaders['content-type'] ? { 'content-type': requestHeaders['content-type'] } : {}), }, payload: request.postData() ?? undefined, }); const contentTypeHeader = response.headers['content-type']; const contentType = Array.isArray(contentTypeHeader) ? contentTypeHeader[0] : contentTypeHeader; await route.fulfill({ status: response.statusCode, contentType: typeof contentType === 'string' ? contentType : 'application/json', body: response.body, }); }); } async function connect(page: Page, hash: string) { await page.goto(`/#${hash}`); await page.getByLabel('Team Slug').fill(teamSlug); await page.getByLabel('Auth Token').fill(authToken); } async function renderedMetrics(page: Page) { return page.evaluate(() => { const viewportWidth = window.innerWidth; const overflowers = Array.from(document.querySelectorAll('body *')) .map((el) => { const rect = el.getBoundingClientRect(); return { inScrollRegion: Boolean(el.closest('[data-admin-scroll-region="true"]')), tag: el.tagName.toLowerCase(), text: (el.textContent ?? '').replace(/\s+/g, ' ').trim().slice(0, 80), width: Math.round(rect.width), left: Math.round(rect.left), right: Math.round(rect.right), }; }) .filter((item) => !item.inScrollRegion && item.width > 0 && (item.left < -1 || item.right > viewportWidth + 1)) .slice(0, 10); const tableProblems = Array.from(document.querySelectorAll('table')) .map((table) => { const wrapper = table.closest('[data-admin-scroll-region="true"]'); return { hasWrapper: Boolean(wrapper), wrapperLabel: wrapper?.getAttribute('aria-label') ?? '', }; }) .filter((item) => !item.hasWrapper || !item.wrapperLabel); const unlabeledControls = Array.from(document.querySelectorAll('input, select, textarea')) .filter((control) => { const id = control.getAttribute('id'); const hasLabel = Boolean(id && document.querySelector(`label[for="${CSS.escape(id)}"]`)); const hasAria = Boolean(control.getAttribute('aria-label') || control.getAttribute('aria-labelledby')); const nestedLabel = Boolean(control.closest('label')); return !hasLabel && !hasAria && !nestedLabel; }) .map((control) => ({ tag: control.tagName.toLowerCase(), name: control.getAttribute('name') ?? '', placeholder: control.getAttribute('placeholder') ?? '', })); return { documentScrollWidth: document.documentElement.scrollWidth, viewportWidth, overflowers, tableProblems, unlabeledControls, activeText: document.querySelector('[aria-current="page"]')?.textContent?.trim() ?? null, activeCurrent: document.querySelector('[aria-current="page"]')?.getAttribute('aria-current') ?? null, hasFrameworkOverlay: Boolean(document.querySelector('[data-nextjs-dialog-overlay], vite-error-overlay, .vite-error-overlay')), }; }); } async function focusedControlName(page: Page) { return page.evaluate(() => { const active = document.activeElement; if (!(active instanceof HTMLElement)) return ''; const labelFor = active.id ? document.querySelector(`label[for="${CSS.escape(active.id)}"]`)?.textContent?.trim() : ''; const labelElement = active.closest('label'); const nestedLabel = labelElement ? Array.from(labelElement.childNodes) .filter((node) => node.nodeType === Node.TEXT_NODE) .map((node) => node.textContent?.trim() ?? '') .filter(Boolean) .join(' ') : ''; return active.getAttribute('aria-label') || active.getAttribute('aria-labelledby') || labelFor || nestedLabel || active.textContent?.replace(/\s+/g, ' ').trim() || active.getAttribute('placeholder') || active.getAttribute('name') || active.tagName.toLowerCase(); }); } async function tabFocusNames(page: Page, count: number) { const names: string[] = []; for (let i = 0; i < count; i += 1) { await page.keyboard.press('Tab'); names.push(await focusedControlName(page)); } return names; } async function blurActiveElement(page: Page) { await page.evaluate(() => { const active = document.activeElement; if (active instanceof HTMLElement) active.blur(); }); } test.describe('admin web rendered UX', () => { test.beforeEach(async ({ page }) => { await mockAdminApi(page); }); for (const viewport of [ { name: 'desktop', width: 1200, height: 800 }, { name: 'mobile', width: 390, height: 844 }, ]) { test(`renders all admin pages without overflow or unlabeled controls on ${viewport.name}`, async ({ page }) => { await page.setViewportSize({ width: viewport.width, height: viewport.height }); const consoleMessages: string[] = []; page.on('console', (message) => { if (['error', 'warning'].includes(message.type())) consoleMessages.push(message.text()); }); page.on('pageerror', (error) => consoleMessages.push(error.message)); for (const item of pages) { await connect(page, item.key); await expect(page.getByRole('heading', { name: item.heading })).toBeVisible(); await expect(page).toHaveURL(new RegExp(`#${item.key}$`)); await expect(page.getByRole('button', { name: item.label })).toHaveAttribute('aria-current', 'page'); await expect.poll(() => page.evaluate(() => window.scrollY), { message: `${item.key} scroll position` }).toBe(0); const metrics = await renderedMetrics(page); expect(metrics.hasFrameworkOverlay, item.key).toBe(false); expect(metrics.documentScrollWidth, item.key).toBeLessThanOrEqual(metrics.viewportWidth); expect(metrics.overflowers, item.key).toEqual([]); expect(metrics.tableProblems, item.key).toEqual([]); expect(metrics.unlabeledControls, item.key).toEqual([]); } expect(consoleMessages).toEqual([]); }); } test('keeps admin pages visually stable on desktop and mobile', async ({ page }) => { for (const viewport of [ { name: 'desktop', width: 1200, height: 800 }, { name: 'mobile', width: 390, height: 844 }, ] as const) { await page.setViewportSize({ width: viewport.width, height: viewport.height }); for (const item of pages) { await connect(page, item.key); await expect(page.getByRole('heading', { name: item.heading })).toBeVisible(); await expect.poll(() => page.evaluate(() => window.scrollY), { message: `${item.key} scroll position` }).toBe(0); await blurActiveElement(page); await expect(page).toHaveScreenshot(`admin-${item.key}-${viewport.name}.png`, { animations: 'disabled', fullPage: true, maxDiffPixelRatio: 0.01, }); } } }); test('surfaces real server auth failures and renders real server data after a valid token', async ({ page }) => { const server = await buildRealAuthServer(); try { await routeAdminApiThroughServer(page, server); await page.setViewportSize({ width: 390, height: 844 }); await page.goto('/#dashboard'); await page.getByLabel('Team Slug').fill(realAuthTeamSlug); await page.getByLabel('Auth Token').fill('wrong-token'); await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible(); await expect(page.getByRole('alert')).toContainText('Authentication failed'); await page.getByLabel('Auth Token').fill(realAuthToken); await expect(page.getByRole('heading', { name: 'Real Auth Team Dashboard' })).toBeVisible(); await expect(page.getByText('Real Auth Owner')).toBeVisible(); await expect(page.getByText('Review real-auth launch gate')).toBeVisible(); const metrics = await renderedMetrics(page); expect(metrics.hasFrameworkOverlay).toBe(false); expect(metrics.overflowers).toEqual([]); expect(metrics.unlabeledControls).toEqual([]); } finally { await server.close(); } }); test('moves focus through the admin shell with keyboard alone', async ({ page }) => { await page.setViewportSize({ width: 390, height: 844 }); await connect(page, 'dashboard'); await expect(page.getByRole('heading', { name: /Test Team Dashboard|Dashboard/ })).toBeVisible(); const focusNames: string[] = []; for (let i = 0; i < 8; i += 1) { await page.keyboard.press('Tab'); focusNames.push(await page.evaluate(() => { const active = document.activeElement; return active?.getAttribute('aria-label') || active?.textContent?.replace(/\s+/g, ' ').trim() || active?.getAttribute('placeholder') || active?.getAttribute('name') || active?.tagName.toLowerCase() || ''; })); } expect(focusNames).toContain('Dashboard'); expect(focusNames).toContain('Analytics'); expect(focusNames).toContain('Members'); expect(focusNames).toContain('Capabilities'); }); test('moves keyboard focus from connection fields into page-level admin controls', async ({ page }) => { await page.setViewportSize({ width: 390, height: 844 }); const keyboardTargets = [ { key: 'dashboard', expected: ['Recent tasks table'] }, { key: 'members', expected: ['Invite email', 'Invite role', 'Invite'] }, { key: 'capabilities', expected: ['Role Policies', 'Overrides', 'Requests', 'Capability role policies table'] }, { key: 'jobs', expected: ['Agent jobs table'] }, { key: 'audit', expected: ['Audit log table'] }, { key: 'settings', expected: ['Team Name'] }, ] as const; for (const item of keyboardTargets) { await connect(page, item.key); const pageInfo = pages.find((candidate) => candidate.key === item.key); if (!pageInfo) throw new Error(`Unknown admin page ${item.key}`); await expect(page.getByRole('heading', { name: pageInfo.heading })).toBeVisible(); if (item.key === 'members') { await page.getByLabel('Invite email').fill('keyboard@example.com'); } await page.getByLabel('Auth Token').focus(); const focusNames = await tabFocusNames(page, 8); for (const expected of item.expected) { expect(focusNames, `${item.key} keyboard focus`).toContain(expected); } } }); test('keeps hash navigation aligned with browser back and forward', async ({ page }) => { await page.setViewportSize({ width: 390, height: 844 }); await connect(page, 'dashboard'); await expect(page.getByRole('button', { name: 'Dashboard' })).toHaveAttribute('aria-current', 'page'); await page.evaluate(() => window.scrollTo(0, document.documentElement.scrollHeight)); expect(await page.evaluate(() => window.scrollY)).toBeGreaterThan(0); await page.getByRole('button', { name: 'Members' }).click(); await expect(page).toHaveURL(/#members$/); await expect(page.getByRole('heading', { name: /Team Members/ })).toBeVisible(); await expect(page.getByRole('button', { name: 'Members' })).toHaveAttribute('aria-current', 'page'); await expect.poll(() => page.evaluate(() => window.scrollY)).toBe(0); await page.getByRole('button', { name: 'Capabilities' }).click(); await expect(page).toHaveURL(/#capabilities$/); await expect(page.getByRole('heading', { name: /Capabilities/ })).toBeVisible(); await expect(page.getByRole('button', { name: 'Capabilities' })).toHaveAttribute('aria-current', 'page'); await page.goBack(); await expect(page).toHaveURL(/#members$/); await expect(page.getByRole('heading', { name: /Team Members/ })).toBeVisible(); await expect(page.getByRole('button', { name: 'Members' })).toHaveAttribute('aria-current', 'page'); await page.goBack(); await expect(page).toHaveURL(/#dashboard$/); await expect(page.getByRole('heading', { name: /Test Team Dashboard|Dashboard/ })).toBeVisible(); await expect(page.getByRole('button', { name: 'Dashboard' })).toHaveAttribute('aria-current', 'page'); await page.goForward(); await expect(page).toHaveURL(/#members$/); await expect(page.getByRole('heading', { name: /Team Members/ })).toBeVisible(); await expect(page.getByRole('button', { name: 'Members' })).toHaveAttribute('aria-current', 'page'); await page.goForward(); await expect(page).toHaveURL(/#capabilities$/); await expect(page.getByRole('heading', { name: /Capabilities/ })).toBeVisible(); await expect(page.getByRole('button', { name: 'Capabilities' })).toHaveAttribute('aria-current', 'page'); }); test('capability governance forms stay labelled and mobile-safe', async ({ page }) => { await page.setViewportSize({ width: 390, height: 844 }); await connect(page, 'capabilities'); await expect(page.getByRole('heading', { name: /Capabilities/ })).toBeVisible(); await page.getByRole('button', { name: 'Edit' }).first().click(); let metrics = await renderedMetrics(page); expect(metrics.overflowers, 'policy edit').toEqual([]); expect(metrics.tableProblems, 'policy edit').toEqual([]); expect(metrics.unlabeledControls, 'policy edit').toEqual([]); await page.getByRole('button', { name: 'Cancel' }).click(); await page.getByRole('button', { name: 'Overrides' }).click(); await expect(page.getByText('browser_navigate')).toBeVisible(); await page.getByRole('button', { name: '+ Add Override' }).click(); metrics = await renderedMetrics(page); expect(metrics.overflowers, 'override form').toEqual([]); expect(metrics.tableProblems, 'override form').toEqual([]); expect(metrics.unlabeledControls, 'override form').toEqual([]); await page.getByRole('button', { name: 'Requests' }).click(); await expect(page.getByText('send_email')).toBeVisible(); await page.getByRole('button', { name: 'Approve' }).click(); metrics = await renderedMetrics(page); expect(metrics.overflowers, 'request decision').toEqual([]); expect(metrics.unlabeledControls, 'request decision').toEqual([]); }); test('keeps capability policy save recoverable when the mutation fails', async ({ page }) => { await page.unroute('http://localhost:3100/**'); let policySaveRoute: Route | null = null; let resolvePolicySave: () => void = () => {}; const policySaveStarted = new Promise((resolve) => { resolvePolicySave = resolve; }); await page.route('http://localhost:3100/**', async (route) => { const request = route.request(); const url = new URL(request.url()); if (request.method() === 'PUT' && url.pathname.includes('/capability-policies/')) { policySaveRoute = route; resolvePolicySave(); return; } await fulfillMockAdminApi(route); }); await page.setViewportSize({ width: 390, height: 844 }); await connect(page, 'capabilities'); await expect(page.getByRole('heading', { name: /Capabilities/ })).toBeVisible(); await page.getByRole('button', { name: 'Edit' }).first().click(); await expect(page.getByRole('heading', { name: /Edit Policy:/ })).toBeVisible(); const saveButton = page.getByRole('button', { name: 'Save' }); await saveButton.click(); await policySaveStarted; const savingButton = page.getByRole('button', { name: 'Saving...' }); await expect(savingButton).toBeDisabled(); const pendingRoute = policySaveRoute; expect(pendingRoute, 'policy save request was sent').not.toBeNull(); await pendingRoute!.fulfill({ status: 500, contentType: 'text/plain', body: 'policy denied', }); await expect(page.getByRole('alert')).toContainText('policy denied'); await expect(page.getByRole('heading', { name: /Edit Policy:/ })).toBeVisible(); await expect(page.getByRole('button', { name: 'Save' })).toBeEnabled(); const metrics = await renderedMetrics(page); expect(metrics.hasFrameworkOverlay).toBe(false); expect(metrics.overflowers).toEqual([]); expect(metrics.unlabeledControls).toEqual([]); }); test('keeps capability override and request mutations recoverable when they fail', async ({ page }) => { await page.unroute('http://localhost:3100/**'); let overrideCreateRoute: Route | null = null; let requestDecisionRoute: Route | null = null; let resolveOverrideCreate: () => void = () => {}; let resolveRequestDecision: () => void = () => {}; const overrideCreateStarted = new Promise((resolve) => { resolveOverrideCreate = resolve; }); const requestDecisionStarted = new Promise((resolve) => { resolveRequestDecision = resolve; }); await page.route('http://localhost:3100/**', async (route) => { const request = route.request(); const url = new URL(request.url()); if (request.method() === 'POST' && url.pathname.endsWith('/capability-overrides')) { overrideCreateRoute = route; resolveOverrideCreate(); return; } if (request.method() === 'PATCH' && url.pathname.includes('/capability-requests/')) { requestDecisionRoute = route; resolveRequestDecision(); return; } await fulfillMockAdminApi(route); }); await page.setViewportSize({ width: 390, height: 844 }); await connect(page, 'capabilities'); await expect(page.getByRole('heading', { name: /Capabilities/ })).toBeVisible(); await page.getByRole('button', { name: 'Overrides' }).click(); await page.getByRole('button', { name: '+ Add Override' }).click(); await page.getByLabel('Capability Name').fill('shell_exec'); await page.getByLabel('Reason').fill('Production incident response'); await page.getByRole('button', { name: 'Submit' }).click(); await overrideCreateStarted; await expect(page.getByRole('button', { name: 'Submitting...' })).toBeDisabled(); const pendingOverrideRoute = overrideCreateRoute; expect(pendingOverrideRoute, 'override create request was sent').not.toBeNull(); await pendingOverrideRoute!.fulfill({ status: 500, contentType: 'text/plain', body: 'override denied', }); await expect(page.getByRole('alert')).toContainText('override denied'); await expect(page.getByLabel('Capability Name')).toHaveValue('shell_exec'); await expect(page.getByRole('button', { name: 'Submit' })).toBeEnabled(); await page.getByRole('button', { name: 'Requests' }).click(); await page.getByRole('button', { name: 'Approve' }).click(); await page.getByLabel('Decision reason').fill('Reviewed by admin'); await page.getByRole('button', { name: 'Approve' }).click(); await requestDecisionStarted; await expect(page.getByRole('button', { name: 'Saving...' })).toBeDisabled(); const pendingDecisionRoute = requestDecisionRoute; expect(pendingDecisionRoute, 'request decision request was sent').not.toBeNull(); await pendingDecisionRoute!.fulfill({ status: 500, contentType: 'text/plain', body: 'decision denied', }); await expect(page.getByRole('alert')).toContainText('decision denied'); await expect(page.getByLabel('Decision reason')).toHaveValue('Reviewed by admin'); await expect(page.getByRole('button', { name: 'Approve' })).toBeEnabled(); const metrics = await renderedMetrics(page); expect(metrics.hasFrameworkOverlay).toBe(false); expect(metrics.overflowers).toEqual([]); expect(metrics.unlabeledControls).toEqual([]); }); test('keeps member invite and team settings mutations recoverable when they fail', async ({ page }) => { await page.unroute('http://localhost:3100/**'); let inviteRoute: Route | null = null; let settingsRoute: Route | null = null; let resolveInvite: () => void = () => {}; let resolveSettings: () => void = () => {}; const inviteStarted = new Promise((resolve) => { resolveInvite = resolve; }); const settingsStarted = new Promise((resolve) => { resolveSettings = resolve; }); await page.route('http://localhost:3100/**', async (route) => { const request = route.request(); const url = new URL(request.url()); if (request.method() === 'POST' && url.pathname.endsWith('/members')) { inviteRoute = route; resolveInvite(); return; } if (request.method() === 'PATCH' && url.pathname === `/api/teams/${teamSlug}`) { settingsRoute = route; resolveSettings(); return; } await fulfillMockAdminApi(route); }); await page.setViewportSize({ width: 390, height: 844 }); await connect(page, 'members'); await expect(page.getByRole('heading', { name: /Team Members/ })).toBeVisible(); await page.getByLabel('Invite email').fill('new.admin@example.com'); await page.getByRole('button', { name: 'Invite' }).click(); await inviteStarted; await expect(page.getByRole('button', { name: 'Inviting...' })).toBeDisabled(); const pendingInviteRoute = inviteRoute; expect(pendingInviteRoute, 'member invite request was sent').not.toBeNull(); await pendingInviteRoute!.fulfill({ status: 500, contentType: 'text/plain', body: 'invite rejected', }); await expect(page.getByRole('alert')).toContainText('invite rejected'); await expect(page.getByLabel('Invite email')).toHaveValue('new.admin@example.com'); await expect(page.getByRole('button', { name: 'Invite' })).toBeEnabled(); await page.getByRole('button', { name: 'Team Settings' }).click(); await expect(page.getByRole('heading', { name: /Team Settings/ })).toBeVisible(); await page.getByLabel('Team Name').fill('Renamed Team'); await page.getByRole('button', { name: 'Save' }).click(); await settingsStarted; await expect(page.getByRole('button', { name: 'Saving...' })).toBeDisabled(); const pendingSettingsRoute = settingsRoute; expect(pendingSettingsRoute, 'team settings save request was sent').not.toBeNull(); await pendingSettingsRoute!.fulfill({ status: 500, contentType: 'text/plain', body: 'settings rejected', }); await expect(page.getByRole('alert')).toContainText('settings rejected'); await expect(page.getByLabel('Team Name')).toHaveValue('Renamed Team'); await expect(page.getByRole('button', { name: 'Save' })).toBeEnabled(); const metrics = await renderedMetrics(page); expect(metrics.hasFrameworkOverlay).toBe(false); expect(metrics.overflowers).toEqual([]); expect(metrics.unlabeledControls).toEqual([]); }); test('keeps member role, member removal, and override removal recoverable when they fail', async ({ page }) => { await page.unroute('http://localhost:3100/**'); let roleChangeRoute: Route | null = null; let memberRemovalRoute: Route | null = null; let overrideRemovalRoute: Route | null = null; let resolveRoleChange: () => void = () => {}; let resolveMemberRemoval: () => void = () => {}; let resolveOverrideRemoval: () => void = () => {}; const roleChangeStarted = new Promise((resolve) => { resolveRoleChange = resolve; }); const memberRemovalStarted = new Promise((resolve) => { resolveMemberRemoval = resolve; }); const overrideRemovalStarted = new Promise((resolve) => { resolveOverrideRemoval = resolve; }); await page.route('http://localhost:3100/**', async (route) => { const request = route.request(); const url = new URL(request.url()); if (request.method() === 'PATCH' && url.pathname.endsWith('/members/u2')) { roleChangeRoute = route; resolveRoleChange(); return; } if (request.method() === 'DELETE' && url.pathname.endsWith('/members/u2')) { memberRemovalRoute = route; resolveMemberRemoval(); return; } if (request.method() === 'DELETE' && url.pathname.endsWith('/capability-overrides/o1')) { overrideRemovalRoute = route; resolveOverrideRemoval(); return; } await fulfillMockAdminApi(route); }); await page.setViewportSize({ width: 390, height: 844 }); await connect(page, 'members'); await expect(page.getByRole('heading', { name: /Team Members/ })).toBeVisible(); const bobRole = page.getByLabel('Role for Bob Builder'); await bobRole.selectOption('viewer'); await roleChangeStarted; await expect(page.getByRole('status')).toContainText('Updating role'); await expect(bobRole).toBeDisabled(); const pendingRoleRoute = roleChangeRoute; expect(pendingRoleRoute, 'member role-change request was sent').not.toBeNull(); await pendingRoleRoute!.fulfill({ status: 500, contentType: 'text/plain', body: 'role rejected', }); await expect(page.getByRole('alert')).toContainText('role rejected'); await expect(page.getByLabel('Role for Bob Builder')).toBeEnabled(); await expect(page.getByLabel('Role for Bob Builder')).toHaveValue('admin'); await page.getByRole('button', { name: 'Remove Bob Builder' }).click(); await expect(page.getByRole('dialog')).toContainText('Remove Bob Builder from the team?'); await page.getByRole('button', { name: 'Remove member' }).click(); await memberRemovalStarted; await expect(page.getByRole('button', { name: 'Removing...' })).toBeDisabled(); const pendingMemberRemovalRoute = memberRemovalRoute; expect(pendingMemberRemovalRoute, 'member removal request was sent').not.toBeNull(); await pendingMemberRemovalRoute!.fulfill({ status: 500, contentType: 'text/plain', body: 'remove rejected', }); await expect(page.getByRole('alert')).toContainText('remove rejected'); await expect(page.getByRole('dialog')).toContainText('Remove Bob Builder from the team?'); await expect(page.getByRole('button', { name: 'Remove member' })).toBeEnabled(); await page.getByRole('button', { name: 'Capabilities' }).click(); await page.getByRole('button', { name: 'Overrides' }).click(); await expect(page.getByText('browser_navigate')).toBeVisible(); await page.getByRole('button', { name: 'Remove' }).click(); await overrideRemovalStarted; await expect(page.getByRole('button', { name: 'Removing...' })).toBeDisabled(); const pendingOverrideRemovalRoute = overrideRemovalRoute; expect(pendingOverrideRemovalRoute, 'override removal request was sent').not.toBeNull(); await pendingOverrideRemovalRoute!.fulfill({ status: 500, contentType: 'text/plain', body: 'override removal rejected', }); await expect(page.getByRole('alert')).toContainText('override removal rejected'); await expect(page.getByText('browser_navigate')).toBeVisible(); await expect(page.getByRole('button', { name: 'Remove' })).toBeEnabled(); const metrics = await renderedMetrics(page); expect(metrics.hasFrameworkOverlay).toBe(false); expect(metrics.overflowers).toEqual([]); expect(metrics.unlabeledControls).toEqual([]); }); test('keeps the shell usable when analytics returns malformed data', async ({ page }) => { await page.unroute('http://localhost:3100/**'); await page.route('http://localhost:3100/**', async (route) => { const url = new URL(route.request().url()); const path = `${url.pathname}${url.search}`; const body = path === `/api/admin/teams/${teamSlug}/analytics` ? { tokenUsage: { total: 5, byUser: [] } } : {}; await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(body), }); }); const pageErrors: string[] = []; page.on('pageerror', (error) => pageErrors.push(error.message)); await page.setViewportSize({ width: 390, height: 844 }); await connect(page, 'analytics'); await expect(page.getByRole('button', { name: 'Analytics' })).toHaveAttribute('aria-current', 'page'); await expect(page.getByRole('alert')).toContainText('Analytics data is incomplete'); await expect(page.getByRole('button', { name: 'Dashboard' })).toBeVisible(); expect(pageErrors).toEqual([]); }); test('shows accessible page errors without breaking the admin shell when APIs fail', async ({ page }) => { await page.unroute('http://localhost:3100/**'); await page.route('http://localhost:3100/**', async (route) => { await route.fulfill({ status: 500, contentType: 'text/plain', body: 'server unavailable', }); }); const pageErrors: string[] = []; page.on('pageerror', (error) => pageErrors.push(error.message)); await page.setViewportSize({ width: 390, height: 844 }); for (const item of pages) { await connect(page, item.key); await expect(page.getByRole('button', { name: item.label })).toHaveAttribute('aria-current', 'page'); await expect(page.getByRole('heading', { name: item.heading })).toBeVisible(); await expect(page.getByRole('alert')).toBeVisible(); const metrics = await renderedMetrics(page); expect(metrics.hasFrameworkOverlay, item.key).toBe(false); expect(metrics.documentScrollWidth, item.key).toBeLessThanOrEqual(metrics.viewportWidth); expect(metrics.overflowers, item.key).toEqual([]); expect(metrics.unlabeledControls, item.key).toEqual([]); await expect(page.getByRole('button', { name: 'Dashboard' })).toBeVisible(); } expect(pageErrors).toEqual([]); }); });