moving
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled

This commit is contained in:
Oleg Maslov
2026-09-02 10:10:29 +02:00
commit 0c3e2ead3b
3841 changed files with 970576 additions and 0 deletions

View File

@@ -0,0 +1,876 @@
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<void>((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<void>((resolve) => {
resolveOverrideCreate = resolve;
});
const requestDecisionStarted = new Promise<void>((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<void>((resolve) => {
resolveInvite = resolve;
});
const settingsStarted = new Promise<void>((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<void>((resolve) => {
resolveRoleChange = resolve;
});
const memberRemovalStarted = new Promise<void>((resolve) => {
resolveMemberRemoval = resolve;
});
const overrideRemovalStarted = new Promise<void>((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([]);
});
});