ui fixes
This commit is contained in:
@@ -9,6 +9,7 @@ const shellSession = {
|
||||
...policyAuthorSession.scopes,
|
||||
'ui.read',
|
||||
'admin',
|
||||
'ui.admin',
|
||||
'orch:read',
|
||||
'orch:operate',
|
||||
'orch:quota',
|
||||
@@ -23,6 +24,19 @@ const shellSession = {
|
||||
'exceptions:read',
|
||||
'exceptions:approve',
|
||||
'aoc:verify',
|
||||
'policy:read',
|
||||
'policy:author',
|
||||
'policy:review',
|
||||
'policy:approve',
|
||||
'policy:simulate',
|
||||
'policy:audit',
|
||||
'health:read',
|
||||
'notify:viewer',
|
||||
'release:read',
|
||||
'release:write',
|
||||
'release:publish',
|
||||
'sbom:read',
|
||||
'signer:read',
|
||||
]),
|
||||
],
|
||||
};
|
||||
@@ -79,42 +93,42 @@ async function setupShell(page: Page): Promise<void> {
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(mockConfig),
|
||||
})
|
||||
}),
|
||||
);
|
||||
await page.route('**/config.json', (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(mockConfig),
|
||||
})
|
||||
}),
|
||||
);
|
||||
await page.route('**/authority/.well-known/openid-configuration', (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(oidcConfig),
|
||||
})
|
||||
}),
|
||||
);
|
||||
await page.route('**/.well-known/openid-configuration', (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(oidcConfig),
|
||||
})
|
||||
}),
|
||||
);
|
||||
await page.route('**/authority/.well-known/jwks.json', (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ keys: [] }),
|
||||
})
|
||||
}),
|
||||
);
|
||||
await page.route('**/authority/connect/**', (route) =>
|
||||
route.fulfill({
|
||||
status: 400,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ error: 'not-used-in-shell-e2e' }),
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -136,23 +150,6 @@ async function assertMainHasContent(page: Page): Promise<void> {
|
||||
expect(text.length > 12 || childNodes > 4).toBe(true);
|
||||
}
|
||||
|
||||
async function openSidebarGroupRoute(
|
||||
page: Page,
|
||||
groupLabel: string,
|
||||
targetHref: string
|
||||
): Promise<void> {
|
||||
const sidebar = page.locator('aside.sidebar');
|
||||
const targetLink = sidebar.locator(`a[href="${targetHref}"]`).first();
|
||||
const isVisible = await targetLink.isVisible().catch(() => false);
|
||||
|
||||
if (!isVisible) {
|
||||
await sidebar.getByRole('button', { name: groupLabel, exact: true }).click();
|
||||
}
|
||||
|
||||
await expect(targetLink).toBeVisible();
|
||||
await targetLink.click();
|
||||
}
|
||||
|
||||
function collectConsoleErrors(page: Page): string[] {
|
||||
const errors: string[] = [];
|
||||
page.on('console', (msg) => {
|
||||
@@ -170,18 +167,19 @@ test.beforeEach(async ({ page }) => {
|
||||
|
||||
test.describe('Nav shell canonical domains', () => {
|
||||
test('sidebar renders all canonical root labels', async ({ page }) => {
|
||||
await go(page, '/dashboard');
|
||||
await go(page, '/mission-control/board');
|
||||
await ensureShell(page);
|
||||
const navText = (await page.locator('aside.sidebar').textContent()) ?? '';
|
||||
|
||||
const labels = [
|
||||
'Dashboard',
|
||||
'Release Control',
|
||||
'Security & Risk',
|
||||
'Evidence & Audit',
|
||||
'Integrations',
|
||||
'Platform Ops',
|
||||
'Administration',
|
||||
'Mission Board',
|
||||
'Mission Alerts',
|
||||
'Mission Activity',
|
||||
'Releases',
|
||||
'Security',
|
||||
'Evidence',
|
||||
'Ops',
|
||||
'Setup',
|
||||
];
|
||||
|
||||
for (const label of labels) {
|
||||
@@ -189,78 +187,50 @@ test.describe('Nav shell canonical domains', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('sidebar excludes deprecated v1 labels', async ({ page }) => {
|
||||
await go(page, '/dashboard');
|
||||
test('sidebar excludes deprecated root labels', async ({ page }) => {
|
||||
await go(page, '/mission-control/board');
|
||||
await ensureShell(page);
|
||||
const navText = (await page.locator('aside.sidebar').textContent()) ?? '';
|
||||
|
||||
expect(navText).not.toContain('Operations');
|
||||
expect(navText).not.toContain('Security & Risk');
|
||||
expect(navText).not.toContain('Evidence & Audit');
|
||||
expect(navText).not.toContain('Platform Ops');
|
||||
expect(navText).not.toContain('Administration');
|
||||
expect(navText).not.toContain('Policy Studio');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Nav shell critical legacy redirects', () => {
|
||||
const redirects: Array<{ from: string; expectedPrefix: string }> = [
|
||||
{ from: '/findings', expectedPrefix: '/security-risk/findings' },
|
||||
{ from: '/vulnerabilities', expectedPrefix: '/security-risk/vulnerabilities' },
|
||||
{ from: '/evidence-packs', expectedPrefix: '/evidence-audit/packs' },
|
||||
{ from: '/admin/audit', expectedPrefix: '/evidence-audit/audit' },
|
||||
{ from: '/ops/health', expectedPrefix: '/platform-ops/health' },
|
||||
{ from: '/admin/notifications', expectedPrefix: '/administration/notifications' },
|
||||
{ from: '/release-orchestrator/releases', expectedPrefix: '/release-control/releases' },
|
||||
{ from: '/release-orchestrator/approvals', expectedPrefix: '/release-control/approvals' },
|
||||
{ from: '/release-orchestrator/environments', expectedPrefix: '/release-control/regions' },
|
||||
{ from: '/settings/release-control', expectedPrefix: '/release-control/setup' },
|
||||
test.describe('No redirect contracts', () => {
|
||||
const legacyPaths = [
|
||||
'/release-control/releases',
|
||||
'/security-risk/findings',
|
||||
'/evidence-audit/packs',
|
||||
'/administration',
|
||||
];
|
||||
|
||||
for (const redirect of redirects) {
|
||||
test(`${redirect.from} redirects correctly`, async ({ page }) => {
|
||||
await go(page, redirect.from);
|
||||
for (const path of legacyPaths) {
|
||||
test(`${path} does not rewrite URL`, async ({ page }) => {
|
||||
await go(page, path);
|
||||
const finalUrl = new URL(page.url());
|
||||
expect(finalUrl.pathname.startsWith(redirect.expectedPrefix)).toBe(true);
|
||||
expect(finalUrl.pathname).toBe(path);
|
||||
await expect(page.getByRole('heading', { level: 1, name: /dashboard/i })).toBeVisible();
|
||||
});
|
||||
}
|
||||
|
||||
test('redirect preserves query parameters', async ({ page }) => {
|
||||
await go(page, '/findings?filter=critical&sort=severity');
|
||||
const finalUrl = page.url();
|
||||
expect(finalUrl).toContain('/security-risk/findings');
|
||||
expect(finalUrl).toContain('filter=critical');
|
||||
expect(finalUrl).toContain('sort=severity');
|
||||
});
|
||||
|
||||
test('redirect preserves fragments', async ({ page }) => {
|
||||
await go(page, '/admin/audit#recent');
|
||||
const finalUrl = page.url();
|
||||
expect(finalUrl).toContain('/evidence-audit/audit');
|
||||
expect(finalUrl).toContain('#recent');
|
||||
});
|
||||
|
||||
test('release-orchestrator root redirect does not loop', async ({ page }) => {
|
||||
await go(page, '/release-orchestrator');
|
||||
const finalUrl = new URL(page.url());
|
||||
expect(finalUrl.pathname).not.toBe('/release-orchestrator');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Nav shell breadcrumbs and stability', () => {
|
||||
const breadcrumbRoutes: Array<{ path: string; expected: string }> = [
|
||||
{ path: '/release-control/releases', expected: 'Release Control' },
|
||||
{ path: '/release-control/setup', expected: 'Setup' },
|
||||
{ path: '/security-risk/advisory-sources', expected: 'Advisory Sources' },
|
||||
{ path: '/evidence-audit/replay', expected: 'Replay & Verify' },
|
||||
{ path: '/platform-ops/data-integrity', expected: 'Data Integrity' },
|
||||
{ path: '/administration/trust-signing', expected: 'Trust & Signing' },
|
||||
{ path: '/mission-control/board', expected: 'Mission Board' },
|
||||
{ path: '/releases/versions', expected: 'Release Versions' },
|
||||
{ path: '/security/triage', expected: 'Triage' },
|
||||
{ path: '/evidence/verify-replay', expected: 'Verify & Replay' },
|
||||
{ path: '/ops/operations/data-integrity', expected: 'Data Integrity' },
|
||||
{ path: '/setup/topology/agents', expected: 'Agent Fleet' },
|
||||
];
|
||||
|
||||
for (const route of breadcrumbRoutes) {
|
||||
test(`breadcrumb renders on ${route.path}`, async ({ page }) => {
|
||||
if (route.path === '/platform-ops/data-integrity') {
|
||||
await go(page, '/dashboard');
|
||||
await openSidebarGroupRoute(page, 'Platform Ops', '/platform-ops/data-integrity');
|
||||
} else {
|
||||
await go(page, route.path);
|
||||
}
|
||||
await go(page, route.path);
|
||||
await ensureShell(page);
|
||||
const breadcrumb = page.locator('app-breadcrumb nav.breadcrumb');
|
||||
await expect(breadcrumb).toHaveCount(1);
|
||||
@@ -270,129 +240,116 @@ test.describe('Nav shell breadcrumbs and stability', () => {
|
||||
|
||||
test('canonical roots produce no app runtime errors', async ({ page }) => {
|
||||
const errors = collectConsoleErrors(page);
|
||||
const routes = [
|
||||
'/dashboard',
|
||||
'/release-control',
|
||||
'/security-risk',
|
||||
'/evidence-audit',
|
||||
'/administration',
|
||||
];
|
||||
const routes = ['/mission-control/board', '/releases', '/security', '/evidence', '/ops', '/setup'];
|
||||
|
||||
for (const route of routes) {
|
||||
await go(page, route);
|
||||
await ensureShell(page);
|
||||
}
|
||||
|
||||
// /platform-ops and /integrations are proxy-captured in dev mode.
|
||||
// Validate them through client-side navigation instead of direct reload.
|
||||
await go(page, '/dashboard');
|
||||
await openSidebarGroupRoute(page, 'Platform Ops', '/platform-ops/data-integrity');
|
||||
await expect(page).toHaveURL(/\/platform-ops\/data-integrity$/);
|
||||
|
||||
await go(page, '/dashboard');
|
||||
await openSidebarGroupRoute(page, 'Integrations', '/integrations');
|
||||
await expect(page).toHaveURL(/\/integrations$/);
|
||||
|
||||
const appErrors = errors.filter(
|
||||
(error) =>
|
||||
!error.includes('ERR_FAILED') &&
|
||||
!error.includes('ERR_BLOCKED') &&
|
||||
!error.includes('ERR_CONNECTION_REFUSED') &&
|
||||
!error.includes('404') &&
|
||||
error.length > 0
|
||||
error.length > 0,
|
||||
);
|
||||
expect(appErrors).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Pack route render checks', () => {
|
||||
test('release-control pack routes render non-blank content', async ({ page }) => {
|
||||
test('release routes render non-blank content', async ({ page }) => {
|
||||
test.setTimeout(60_000);
|
||||
|
||||
const routes = [
|
||||
'/release-control/control-plane',
|
||||
'/release-control/bundles',
|
||||
'/release-control/regions',
|
||||
'/release-control/governance',
|
||||
'/release-control/hotfixes',
|
||||
'/releases/overview',
|
||||
'/releases/versions',
|
||||
'/releases/runs',
|
||||
'/releases/approvals',
|
||||
'/releases/hotfixes',
|
||||
'/releases/promotion-queue',
|
||||
'/releases/environments',
|
||||
'/releases/deployments',
|
||||
'/releases/versions/new',
|
||||
];
|
||||
|
||||
for (const route of routes) {
|
||||
await go(page, route);
|
||||
await ensureShell(page);
|
||||
const finalUrl = new URL(page.url());
|
||||
expect(finalUrl.pathname.startsWith(route)).toBe(true);
|
||||
expect(new URL(page.url()).pathname).toBe(route);
|
||||
await assertMainHasContent(page);
|
||||
}
|
||||
});
|
||||
|
||||
test('security and evidence pack routes render non-blank content', async ({ page }) => {
|
||||
test('security and evidence routes render non-blank content', async ({ page }) => {
|
||||
test.setTimeout(60_000);
|
||||
|
||||
const routes = [
|
||||
'/security-risk/findings',
|
||||
'/security-risk/vulnerabilities',
|
||||
'/security-risk/vex',
|
||||
'/security-risk/sbom-lake',
|
||||
'/security-risk/exceptions',
|
||||
'/evidence-audit/packs',
|
||||
'/evidence-audit/bundles',
|
||||
'/evidence-audit/evidence/export',
|
||||
'/evidence-audit/audit-log',
|
||||
'/evidence-audit/trust-signing',
|
||||
'/security/posture',
|
||||
'/security/triage',
|
||||
'/security/advisories-vex',
|
||||
'/security/supply-chain-data',
|
||||
'/security/reachability',
|
||||
'/security/reports',
|
||||
'/evidence/overview',
|
||||
'/evidence/capsules',
|
||||
'/evidence/verify-replay',
|
||||
'/evidence/exports',
|
||||
'/evidence/audit-log',
|
||||
];
|
||||
|
||||
for (const route of routes) {
|
||||
await go(page, route);
|
||||
await ensureShell(page);
|
||||
const finalUrl = new URL(page.url());
|
||||
expect(finalUrl.pathname.startsWith(route)).toBe(true);
|
||||
expect(new URL(page.url()).pathname).toBe(route);
|
||||
await assertMainHasContent(page);
|
||||
}
|
||||
});
|
||||
|
||||
test('platform-ops and integrations routes render via sidebar navigation', async ({ page }) => {
|
||||
test('ops and setup routes render non-blank content', async ({ page }) => {
|
||||
test.setTimeout(60_000);
|
||||
|
||||
const platformOpsRoutes = [
|
||||
'/platform-ops/data-integrity',
|
||||
'/platform-ops/orchestrator',
|
||||
'/platform-ops/health',
|
||||
'/platform-ops/quotas',
|
||||
'/platform-ops/feeds',
|
||||
const routes = [
|
||||
'/ops',
|
||||
'/ops/operations',
|
||||
'/ops/operations/data-integrity',
|
||||
'/ops/operations/orchestrator',
|
||||
'/ops/integrations',
|
||||
'/ops/integrations/advisory-vex-sources',
|
||||
'/ops/policy',
|
||||
'/ops/platform-setup',
|
||||
'/setup',
|
||||
'/setup/topology/overview',
|
||||
'/setup/topology/targets',
|
||||
'/setup/topology/hosts',
|
||||
'/setup/topology/agents',
|
||||
];
|
||||
|
||||
for (const route of platformOpsRoutes) {
|
||||
await go(page, '/dashboard');
|
||||
await openSidebarGroupRoute(page, 'Platform Ops', route);
|
||||
await expect(page).toHaveURL(new RegExp(`${route.replace(/\//g, '\\/')}$`));
|
||||
const currentPath = new URL(page.url()).pathname;
|
||||
expect(currentPath).toBe(route);
|
||||
for (const route of routes) {
|
||||
await go(page, route);
|
||||
await ensureShell(page);
|
||||
expect(new URL(page.url()).pathname).toBe(route);
|
||||
await assertMainHasContent(page);
|
||||
}
|
||||
|
||||
await go(page, '/dashboard');
|
||||
await openSidebarGroupRoute(page, 'Integrations', '/integrations');
|
||||
await expect(page).toHaveURL(/\/integrations$/);
|
||||
expect(new URL(page.url()).pathname).toBe('/integrations');
|
||||
await assertMainHasContent(page);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Nav shell responsive layout', () => {
|
||||
test('desktop viewport shows sidebar', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 1440, height: 900 });
|
||||
await go(page, '/dashboard');
|
||||
await go(page, '/mission-control/board');
|
||||
await expect(page.locator('aside.sidebar')).toBeVisible();
|
||||
});
|
||||
|
||||
test('mobile viewport remains usable without horizontal overflow', async ({ page }) => {
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await go(page, '/dashboard');
|
||||
await go(page, '/mission-control/board');
|
||||
await expect(page.locator('.topbar__menu-toggle')).toBeVisible();
|
||||
|
||||
const hasHorizontalScroll = await page.evaluate(
|
||||
() => document.documentElement.scrollWidth > document.documentElement.clientWidth
|
||||
() => document.documentElement.scrollWidth > document.documentElement.clientWidth,
|
||||
);
|
||||
expect(hasHorizontalScroll).toBe(false);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user