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,661 @@
// @vitest-environment jsdom
/**
* Admin web — comprehensive page component and API module tests.
*
* Tests module exports, API client methods, URL construction,
* and rendering of each page component in various states.
*/
import { existsSync, readFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import React from 'react';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { App } from '../src/App.js';
import { Dashboard } from '../src/pages/Dashboard.js';
import { Members } from '../src/pages/Members.js';
import { Capabilities } from '../src/pages/Capabilities.js';
import { Jobs } from '../src/pages/Jobs.js';
import { Audit } from '../src/pages/Audit.js';
import { TeamSettings } from '../src/pages/TeamSettings.js';
import { Analytics } from '../src/pages/Analytics.js';
import { api } from '../src/api.js';
// ─── API Client Tests ──────────────────────────────────────────────────
describe('API client', () => {
it('exports team management methods', () => {
expect(typeof api.listTeams).toBe('function');
expect(typeof api.getTeam).toBe('function');
expect(typeof api.updateTeam).toBe('function');
expect(typeof api.inviteMember).toBe('function');
expect(typeof api.removeMember).toBe('function');
expect(typeof api.updateMemberRole).toBe('function');
});
it('exports job and audit methods', () => {
expect(typeof api.listJobs).toBe('function');
expect(typeof api.listCron).toBe('function');
expect(typeof api.listAudit).toBe('function');
expect(typeof api.getStats).toBe('function');
});
it('exports analytics method', () => {
expect(typeof api.getAnalytics).toBe('function');
});
it('exports task management method', () => {
expect(typeof api.listTasks).toBe('function');
});
it('exports capability governance methods', () => {
expect(typeof api.listCapabilityPolicies).toBe('function');
expect(typeof api.updateCapabilityPolicy).toBe('function');
expect(typeof api.listCapabilityOverrides).toBe('function');
expect(typeof api.createCapabilityOverride).toBe('function');
expect(typeof api.deleteCapabilityOverride).toBe('function');
expect(typeof api.listCapabilityRequests).toBe('function');
expect(typeof api.decideCapabilityRequest).toBe('function');
});
it('exports scout and suggestions methods', () => {
expect(typeof api.listScoutFindings).toBe('function');
expect(typeof api.listSuggestions).toBe('function');
});
});
// ─── Page Module Export Tests ──────────────────────────────────────────
describe('Admin page module exports', () => {
it('Dashboard is a function component', () => {
expect(typeof Dashboard).toBe('function');
});
it('Members is a function component', () => {
expect(typeof Members).toBe('function');
});
it('Capabilities is a function component', () => {
expect(typeof Capabilities).toBe('function');
});
it('Jobs is a function component', () => {
expect(typeof Jobs).toBe('function');
});
it('Audit is a function component', () => {
expect(typeof Audit).toBe('function');
});
it('TeamSettings is a function component', () => {
expect(typeof TeamSettings).toBe('function');
});
it('Analytics is a function component', () => {
expect(typeof Analytics).toBe('function');
});
it('App root is a function component', () => {
expect(typeof App).toBe('function');
});
});
// ─── Rendering Tests ───────────────────────────────────────────────────
// Mock fetch globally for all rendering tests
function mockFetch(responses: Record<string, unknown>) {
// Sort patterns longest-first so more specific URLs match before shorter ones
const sortedPatterns = Object.entries(responses).sort(
([a], [b]) => b.length - a.length,
);
const mock = vi.fn(async (url: string, _opts?: RequestInit) => {
// Match URL patterns (longest match first)
for (const [pattern, data] of sortedPatterns) {
if (url.includes(pattern)) {
return {
ok: true,
status: 200,
json: async () => data,
text: async () => JSON.stringify(data),
};
}
}
// Default: 404
return {
ok: false,
status: 404,
statusText: 'Not Found',
json: async () => ({}),
text: async () => 'Not Found',
};
});
vi.stubGlobal('fetch', mock);
return mock;
}
function mockPendingFetch() {
const mock = vi.fn(() => new Promise(() => {}));
vi.stubGlobal('fetch', mock);
return mock;
}
describe('App root component', () => {
beforeEach(() => {
window.location.hash = '';
});
afterEach(() => {
window.location.hash = '';
vi.restoreAllMocks();
});
it('renders the Waggle Admin title', () => {
const { container } = render(React.createElement(App));
expect(container.textContent).toContain('Waggle Admin');
});
it('shows connect prompt when no token/slug entered', () => {
render(React.createElement(App));
expect(screen.getByText('Connect to a Team')).toBeDefined();
});
it('renders all navigation items', () => {
render(React.createElement(App));
expect(screen.getByText('Dashboard')).toBeDefined();
expect(screen.getByText('Analytics')).toBeDefined();
expect(screen.getByText('Members')).toBeDefined();
expect(screen.getByText('Capabilities')).toBeDefined();
expect(screen.getByText('Jobs')).toBeDefined();
expect(screen.getByText('Audit Log')).toBeDefined();
expect(screen.getByText('Team Settings')).toBeDefined();
});
it('exposes connection inputs with accessible labels and metadata', () => {
render(React.createElement(App));
const teamSlug = screen.getByLabelText(/team slug/i) as HTMLInputElement;
const authToken = screen.getByLabelText(/auth token/i) as HTMLInputElement;
expect(teamSlug.name).toBe('teamSlug');
expect(teamSlug.autocomplete).toBe('organization');
expect(authToken.name).toBe('authToken');
expect(authToken.autocomplete).toBe('off');
});
it('uses hash deep links and aria-current for admin navigation', () => {
window.location.hash = '#members';
render(React.createElement(App));
const members = screen.getByRole('button', { name: 'Members' });
const capabilities = screen.getByRole('button', { name: 'Capabilities' });
expect(members.getAttribute('aria-current')).toBe('page');
fireEvent.click(capabilities);
expect(window.location.hash).toBe('#capabilities');
expect(capabilities.getAttribute('aria-current')).toBe('page');
});
it('ships a responsive admin shell stylesheet for narrow viewports', () => {
const cssPath = resolve(dirname(fileURLToPath(import.meta.url)), '../src/admin.css');
const css = existsSync(cssPath) ? readFileSync(cssPath, 'utf8') : '';
expect(css).toContain('@media (max-width: 720px)');
expect(css).toContain('.admin-shell');
expect(css).toContain('.admin-sidebar');
expect(css).toContain('.admin-main');
});
});
describe('Dashboard page', () => {
afterEach(() => {
vi.restoreAllMocks();
});
it('shows loading state initially', () => {
mockPendingFetch();
render(React.createElement(Dashboard, { token: 'tok', teamSlug: 'test' }));
expect(screen.getByText('Loading dashboard...')).toBeDefined();
});
it('renders team name and stat cards after data loads', async () => {
mockFetch({
'/api/teams/test': {
id: 't1', name: 'Test Team', slug: 'test', ownerId: 'u1', createdAt: '2025-01-01',
members: [
{ userId: 'u1', displayName: 'Alice', role: 'owner' },
{ userId: 'u2', displayName: 'Bob', role: 'member' },
],
},
'/api/teams/test/tasks': [
{ id: 'task1', teamId: 't1', title: 'Fix bug', status: 'open', priority: 'high', createdBy: 'u1', createdAt: '2025-01-10', updatedAt: '2025-01-10' },
{ id: 'task2', teamId: 't1', title: 'Write docs', status: 'completed', priority: 'medium', createdBy: 'u2', createdAt: '2025-01-11', updatedAt: '2025-01-11' },
],
});
render(React.createElement(Dashboard, { token: 'tok', teamSlug: 'test' }));
await waitFor(() => {
expect(screen.getByText('Test Team Dashboard')).toBeDefined();
});
// Stat cards exist with correct labels
expect(screen.getByText('Members')).toBeDefined();
expect(screen.getByText('Active Tasks')).toBeDefined();
expect(screen.getByText('Total Tasks')).toBeDefined();
expect(screen.getByText('Team Slug')).toBeDefined();
// Member count '2' appears in stat card (may also appear in task count area)
expect(screen.getAllByText('2').length).toBeGreaterThanOrEqual(1);
// Team members section
expect(screen.getByText('Alice')).toBeDefined();
expect(screen.getByText('Bob')).toBeDefined();
// Recent tasks
expect(screen.getByText('Fix bug')).toBeDefined();
expect(screen.getByText('Write docs')).toBeDefined();
});
it('shows error message when both API calls fail', async () => {
vi.stubGlobal('fetch', vi.fn(async () => ({
ok: false,
status: 500,
statusText: 'Internal Server Error',
json: async () => ({}),
text: async () => 'Internal Server Error',
})));
render(React.createElement(Dashboard, { token: 'tok', teamSlug: 'test' }));
await waitFor(() => {
expect(screen.getByText(/Could not connect to team server/)).toBeDefined();
});
});
it('shows auth guidance when both API calls reject the token', async () => {
vi.stubGlobal('fetch', vi.fn(async () => ({
ok: false,
status: 401,
statusText: 'Unauthorized',
json: async () => ({}),
text: async () => 'Unauthorized',
})));
render(React.createElement(Dashboard, { token: 'bad-token', teamSlug: 'test' }));
await waitFor(() => {
expect(screen.getByText(/Authentication failed. Check the admin auth token./)).toBeDefined();
});
});
});
describe('Members page', () => {
afterEach(() => {
vi.restoreAllMocks();
});
it('renders heading and invite form', async () => {
mockFetch({
'/api/teams/test': {
id: 't1', name: 'Test Team', slug: 'test', ownerId: 'u1', createdAt: '2025-01-01',
members: [
{ userId: 'u1', displayName: 'Alice', role: 'owner', email: 'alice@test.com' },
],
},
});
render(React.createElement(Members, { token: 'tok', teamSlug: 'test' }));
expect(screen.getByText('Team Members')).toBeDefined();
await waitFor(() => {
expect(screen.getByText('Alice')).toBeDefined();
});
// Owner should have a badge, not a select
expect(screen.getByText('Owner')).toBeDefined();
// Invite form elements
expect(screen.getByPlaceholderText('user@example.com')).toBeDefined();
expect(screen.getByText('Invite')).toBeDefined();
});
it('labels invite fields and member role controls', async () => {
mockFetch({
'/api/teams/test': {
id: 't1', name: 'Test Team', slug: 'test', ownerId: 'u1', createdAt: '2025-01-01',
members: [
{ userId: 'u1', displayName: 'Alice', role: 'owner', email: 'alice@test.com' },
{ userId: 'u2', displayName: 'Bob', role: 'member', email: 'bob@test.com' },
],
},
});
render(React.createElement(Members, { token: 'tok', teamSlug: 'test' }));
await screen.findByText('Bob');
expect(screen.getByLabelText(/invite email/i)).toBeDefined();
expect(screen.getByLabelText(/invite role/i)).toBeDefined();
expect(screen.getByLabelText(/role for bob/i)).toBeDefined();
});
it('shows empty state when no members', async () => {
mockFetch({
'/api/teams/test': {
id: 't1', name: 'Test Team', slug: 'test', ownerId: 'u1', createdAt: '2025-01-01',
members: [],
},
});
render(React.createElement(Members, { token: 'tok', teamSlug: 'test' }));
await waitFor(() => {
expect(screen.getByText(/No members found/)).toBeDefined();
});
});
it('asks in-app before removing a member', async () => {
const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(false);
const fetchMock = mockFetch({
'/api/teams/test/members/u2': {},
'/api/teams/test': {
id: 't1', name: 'Test Team', slug: 'test', ownerId: 'u1', createdAt: '2025-01-01',
members: [
{ userId: 'u1', displayName: 'Alice', role: 'owner', email: 'alice@test.com' },
{ userId: 'u2', displayName: 'Bob', role: 'member', email: 'bob@test.com' },
],
},
});
render(React.createElement(Members, { token: 'tok', teamSlug: 'test' }));
await screen.findByText('Bob');
fireEvent.click(screen.getByRole('button', { name: /remove bob/i }));
expect(confirmSpy).not.toHaveBeenCalled();
expect(fetchMock.mock.calls.some(([url, opts]) => String(url).includes('/api/teams/test/members/u2') && opts?.method === 'DELETE')).toBe(false);
expect(screen.getByRole('dialog').textContent).toMatch(/remove bob from the team/i);
fireEvent.click(screen.getByRole('button', { name: /remove member/i }));
await waitFor(() => {
expect(fetchMock.mock.calls.some(([url, opts]) => String(url).includes('/api/teams/test/members/u2') && opts?.method === 'DELETE')).toBe(true);
});
});
});
describe('Jobs page', () => {
afterEach(() => {
vi.restoreAllMocks();
});
it('shows loading state', () => {
mockPendingFetch();
render(React.createElement(Jobs, { token: 'tok', teamSlug: 'test' }));
expect(screen.getByText('Loading jobs...')).toBeDefined();
});
it('renders job list after loading', async () => {
mockFetch({
'/api/jobs': [
{
id: 'job-abc12345-long-id',
teamId: 't1', userId: 'u1', jobType: 'agent_task',
status: 'completed', input: {}, output: {},
createdAt: '2025-01-10T12:00:00Z', completedAt: '2025-01-10T12:05:00Z',
},
{
id: 'job-def67890-long-id',
teamId: 't1', userId: 'u1', jobType: 'research',
status: 'running', input: {},
createdAt: '2025-01-11T10:00:00Z',
},
],
});
render(React.createElement(Jobs, { token: 'tok', teamSlug: 'test' }));
await waitFor(() => {
expect(screen.getByText('agent_task')).toBeDefined();
});
expect(screen.getByText('completed')).toBeDefined();
expect(screen.getByText('running')).toBeDefined();
expect(screen.getByText('research')).toBeDefined();
});
it('shows empty state when no jobs', async () => {
mockFetch({ '/api/jobs': [] });
render(React.createElement(Jobs, { token: 'tok', teamSlug: 'test' }));
await waitFor(() => {
expect(screen.getByText('No jobs found')).toBeDefined();
});
});
});
describe('Audit page', () => {
afterEach(() => {
vi.restoreAllMocks();
});
it('shows loading state', () => {
mockPendingFetch();
render(React.createElement(Audit, { token: 'tok', teamSlug: 'test' }));
expect(screen.getByText('Loading audit log...')).toBeDefined();
});
it('renders audit entries after loading', async () => {
mockFetch({
'/api/admin/teams/test/audit': [
{
id: 'a1', userId: 'u1', agentName: 'waggle-1', actionType: 'tool_use',
description: 'Executed shell command', requiresApproval: true, approved: true,
createdAt: '2025-01-10T10:00:00Z',
},
{
id: 'a2', userId: 'u1', agentName: 'waggle-1', actionType: 'memory_write',
description: 'Stored project context', requiresApproval: false,
createdAt: '2025-01-10T11:00:00Z',
},
],
});
render(React.createElement(Audit, { token: 'tok', teamSlug: 'test' }));
await waitFor(() => {
expect(screen.getByText('tool_use')).toBeDefined();
});
expect(screen.getByText('memory_write')).toBeDefined();
expect(screen.getByText('Approved')).toBeDefined();
});
it('shows empty state when no audit entries', async () => {
mockFetch({ '/api/admin/teams/test/audit': [] });
render(React.createElement(Audit, { token: 'tok', teamSlug: 'test' }));
await waitFor(() => {
expect(screen.getByText('No audit entries')).toBeDefined();
});
});
});
describe('TeamSettings page', () => {
afterEach(() => {
vi.restoreAllMocks();
});
it('shows loading state', () => {
mockPendingFetch();
render(React.createElement(TeamSettings, { token: 'tok', teamSlug: 'test' }));
expect(screen.getByText('Loading team settings...')).toBeDefined();
});
it('renders team info after loading', async () => {
mockFetch({
'/api/teams/test': {
id: 'team-uuid-123', name: 'Test Team', slug: 'test', ownerId: 'u1', createdAt: '2025-01-01T00:00:00Z',
},
});
render(React.createElement(TeamSettings, { token: 'tok', teamSlug: 'test' }));
await waitFor(() => {
expect(screen.getByDisplayValue('Test Team')).toBeDefined();
});
// Slug display
expect(screen.getByText('test')).toBeDefined();
// Team ID display
expect(screen.getByText('team-uuid-123')).toBeDefined();
// Labels
expect(screen.getByText('Team Name')).toBeDefined();
expect(screen.getByText('Slug')).toBeDefined();
expect(screen.getByText('Team ID')).toBeDefined();
expect(screen.getByText('Created')).toBeDefined();
});
it('labels the editable team name field', async () => {
mockFetch({
'/api/teams/test': {
id: 'team-uuid-123', name: 'Test Team', slug: 'test', ownerId: 'u1', createdAt: '2025-01-01T00:00:00Z',
},
});
render(React.createElement(TeamSettings, { token: 'tok', teamSlug: 'test' }));
const teamName = await screen.findByLabelText(/team name/i);
expect((teamName as HTMLInputElement).name).toBe('teamName');
});
it('save button is disabled when name unchanged', async () => {
mockFetch({
'/api/teams/test': {
id: 't1', name: 'Test Team', slug: 'test', ownerId: 'u1', createdAt: '2025-01-01',
},
});
render(React.createElement(TeamSettings, { token: 'tok', teamSlug: 'test' }));
await waitFor(() => {
expect(screen.getByDisplayValue('Test Team')).toBeDefined();
});
const saveButton = screen.getByText('Save') as HTMLButtonElement;
expect(saveButton.disabled).toBe(true);
});
});
describe('Capabilities page', () => {
afterEach(() => {
vi.restoreAllMocks();
});
it('renders heading and tab bar', () => {
mockPendingFetch();
render(React.createElement(Capabilities, { token: 'tok', teamSlug: 'test' }));
expect(screen.getByText('Capabilities')).toBeDefined();
expect(screen.getByText('Role Policies')).toBeDefined();
expect(screen.getByText('Overrides')).toBeDefined();
expect(screen.getByText('Requests')).toBeDefined();
});
});
describe('Analytics page', () => {
afterEach(() => {
vi.restoreAllMocks();
});
it('shows loading state', () => {
mockPendingFetch();
render(React.createElement(Analytics, { token: 'tok', teamSlug: 'test' }));
expect(screen.getByText('Loading analytics...')).toBeDefined();
});
it('renders analytics cards after data loads', async () => {
mockFetch({
'/api/admin/teams/test/analytics': {
activeUsers: { daily: 7, weekly: 19, monthly: 42 },
tokenUsage: {
total: 1500000,
byUser: [
{ userId: 'u1', name: 'Alice', tokens: 800000, cost: 4.50 },
{ userId: 'u2', name: 'Bob', tokens: 700000, cost: 3.20 },
],
},
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,
},
},
});
render(React.createElement(Analytics, { token: 'tok', teamSlug: 'test' }));
await waitFor(() => {
expect(screen.getByText('Usage Analytics')).toBeDefined();
});
// Active users section
expect(screen.getByText('Active Users')).toBeDefined();
expect(screen.getByText('7')).toBeDefined(); // daily
expect(screen.getByText('19')).toBeDefined(); // weekly
expect(screen.getByText('42')).toBeDefined(); // monthly
// Token usage
expect(screen.getByText('Token Usage')).toBeDefined();
expect(screen.getByText('1.5M tokens')).toBeDefined();
expect(screen.getByText('Alice')).toBeDefined();
expect(screen.getByText('Bob')).toBeDefined();
// Top tools
expect(screen.getByText('Top Tools')).toBeDefined();
expect(screen.getByText('memory_search')).toBeDefined();
expect(screen.getByText('shell_exec')).toBeDefined();
// Top commands
expect(screen.getByText('Top Commands')).toBeDefined();
expect(screen.getByText('/research')).toBeDefined();
expect(screen.getByText('/draft')).toBeDefined();
// Capability gaps
expect(screen.getByText('Capability Gaps')).toBeDefined();
expect(screen.getByText('browser_navigate')).toBeDefined();
// Performance trends
expect(screen.getByText('Performance Trends')).toBeDefined();
expect(screen.getByText('15.0%')).toBeDefined(); // correction rate
expect(screen.getByText('8.5s')).toBeDefined(); // avg response time
});
it('shows error when analytics API fails', async () => {
vi.stubGlobal('fetch', vi.fn(async () => ({
ok: false,
status: 500,
statusText: 'Server Error',
json: async () => ({}),
text: async () => 'Server Error',
})));
render(React.createElement(Analytics, { token: 'tok', teamSlug: 'test' }));
await waitFor(() => {
expect(screen.getByText(/API error: 500/)).toBeDefined();
});
});
});

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB