Add comprehensive e2e tests for IA restructure and UI fixes

New test suite: navigation-restructure.e2e.spec.ts (26 tests, 7 groups)

Group 1 - Sidebar structure (7 tests):
- Dashboard ungrouped and first
- Release Control has Deployments, Releases, Environments
- Policy group has 5 items (no Release Gates)
- Security group has no Reports
- Operations has no Policy or Trust Analytics
- Audit & Evidence has Trust (not Trust Audit)

Group 2 - Releases unified page (5 tests):
- Title is "Release Control"
- Has Releases + Versions tabs
- Default tab is Releases
- Versions tab changes button to New Version
- /releases/versions redirects to ?tab=versions

Group 3 - Deployments page (7 tests):
- Has Pipeline + Approvals tabs (no Table/Correlations)
- No redundant context chips
- Approvals tab has gate summary cards + Gate Type column + search
- No Angular errors

Group 4 - Route redirects (2 tests):
- /security/reports → /security
- /triage/audit-bundles → /evidence/bundles

Group 5 - Page content consistency (6 tests):
- Dashboard, Vulnerabilities, Scheduled Jobs, Notifications, Audit Log headings correct
- No NG02100 errors on Audit Log

Group 6 - Diagnostics (1 test):
- NOT RUN labels (not NOT RAN)

Group 7 - Breadcrumbs (2 tests):
- Operations breadcrumb not duplicated
- Says "Operations" not "Ops"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
master
2026-03-28 10:59:40 +02:00
parent 0210c66ef6
commit 6c07b0b374

View File

@@ -0,0 +1,291 @@
/**
* E2E Test Suite — Navigation IA Restructure
*
* Covers sidebar structure, tab behavior, redirects, approve/reject actions,
* and cross-page navigation after the IA restructure.
*
* Runs against the live Docker stack at https://stella-ops.local.
*/
import { test, expect } from '../fixtures/auth.fixture';
import { navigateAndWait, assertPageHasContent } from '../helpers/nav.helper';
import type { Page } from '@playwright/test';
async function go(page: Page, path: string) {
await navigateAndWait(page, path, { timeout: 30_000 });
}
function setupErrorCollector(page: Page): string[] {
const errors: string[] = [];
page.on('console', (msg) => {
if (msg.type() === 'error' && /NG0\d{3,4}/.test(msg.text())) {
errors.push(msg.text());
}
});
return errors;
}
// ===========================================================================
// GROUP 1 — Sidebar Navigation Structure
// ===========================================================================
test.describe('Sidebar: 7-group structure', () => {
test('Dashboard link is ungrouped and first', async ({ authenticatedPage: page }) => {
await go(page, '/');
const dashboard = page.locator('nav').getByRole('link', { name: 'Dashboard' });
await expect(dashboard).toBeVisible({ timeout: 10_000 });
});
test('Release Control group has Deployments, Releases, Environments', async ({ authenticatedPage: page }) => {
await go(page, '/');
const group = page.getByRole('group', { name: 'Release Control' });
await expect(group).toBeVisible({ timeout: 10_000 });
await expect(group.getByRole('link', { name: 'Deployments' })).toBeVisible();
await expect(group.getByRole('link', { name: 'Releases' })).toBeVisible();
await expect(group.getByRole('link', { name: 'Environments' })).toBeVisible();
});
test('Policy group has Packs, Governance, Simulation, VEX, Audit', async ({ authenticatedPage: page }) => {
await go(page, '/');
const group = page.getByRole('group', { name: 'Policy' });
await expect(group).toBeVisible({ timeout: 10_000 });
await expect(group.getByRole('link', { name: 'Packs' })).toBeVisible();
await expect(group.getByRole('link', { name: 'Governance' })).toBeVisible();
await expect(group.getByRole('link', { name: 'Simulation' })).toBeVisible();
await expect(group.getByRole('link', { name: 'VEX & Exceptions' })).toBeVisible();
await expect(group.getByRole('link', { name: 'Policy Audit' })).toBeVisible();
});
test('Policy group does NOT have Release Gates', async ({ authenticatedPage: page }) => {
await go(page, '/');
const group = page.getByRole('group', { name: 'Policy' });
await expect(group).toBeVisible({ timeout: 10_000 });
await expect(group.getByRole('link', { name: 'Release Gates' })).not.toBeVisible();
});
test('Security group does NOT have Reports', async ({ authenticatedPage: page }) => {
await go(page, '/');
const group = page.getByRole('group', { name: 'Security' });
await expect(group).toBeVisible({ timeout: 10_000 });
await expect(group.getByRole('link', { name: 'Reports' })).not.toBeVisible();
});
test('Operations group has 8 items (no Trust Analytics, no Policy)', async ({ authenticatedPage: page }) => {
await go(page, '/');
const group = page.getByRole('group', { name: 'Operations' });
await expect(group).toBeVisible({ timeout: 10_000 });
await expect(group.getByRole('link', { name: 'Operations Hub' })).toBeVisible();
await expect(group.getByRole('link', { name: 'Scheduled Jobs' })).toBeVisible();
await expect(group.getByRole('link', { name: 'Diagnostics' })).toBeVisible();
await expect(group.getByRole('link', { name: 'Feeds & Airgap' })).toBeVisible();
// Should NOT have Policy (moved to own group) or Trust Analytics (merged)
await expect(group.getByRole('link', { name: 'Policy' })).not.toBeVisible();
await expect(group.getByRole('link', { name: 'Trust Analytics' })).not.toBeVisible();
});
test('Audit & Evidence has Trust (not Trust Audit)', async ({ authenticatedPage: page }) => {
await go(page, '/');
const group = page.getByRole('group', { name: 'Audit & Evidence' });
await expect(group).toBeVisible({ timeout: 10_000 });
await expect(group.getByRole('link', { name: 'Trust' })).toBeVisible();
await expect(group.getByRole('link', { name: 'Trust Audit' })).not.toBeVisible();
});
});
// ===========================================================================
// GROUP 2 — Releases Page (unified tabs)
// ===========================================================================
test.describe('Releases Page: unified Releases + Versions tabs', () => {
test('Page title is Release Control', async ({ authenticatedPage: page }) => {
const ngErrors = setupErrorCollector(page);
await go(page, '/releases');
const heading = page.locator('h1').first();
await expect(heading).toBeVisible({ timeout: 10_000 });
await expect(heading).toContainText('Release Control');
expect(ngErrors).toHaveLength(0);
});
test('Has Releases and Versions tabs', async ({ authenticatedPage: page }) => {
await go(page, '/releases');
await expect(page.getByRole('tab', { name: 'Releases' })).toBeVisible({ timeout: 10_000 });
await expect(page.getByRole('tab', { name: 'Versions' })).toBeVisible();
});
test('Default tab is Releases', async ({ authenticatedPage: page }) => {
await go(page, '/releases');
const releasesTab = page.getByRole('tab', { name: 'Releases' });
await expect(releasesTab).toBeVisible({ timeout: 10_000 });
await expect(releasesTab).toHaveAttribute('aria-selected', 'true');
});
test('Switching to Versions tab changes button to New Version', async ({ authenticatedPage: page }) => {
await go(page, '/releases');
await page.getByRole('tab', { name: 'Versions' }).click();
await page.waitForTimeout(500);
// Page action should change
const newVersionLink = page.getByRole('link', { name: /New Version/i });
await expect(newVersionLink).toBeVisible({ timeout: 5_000 });
});
test('/releases/versions redirects to /releases?tab=versions', async ({ authenticatedPage: page }) => {
await go(page, '/releases/versions');
await page.waitForTimeout(1000);
expect(page.url()).toContain('tab=versions');
});
});
// ===========================================================================
// GROUP 3 — Deployments Page (Pipeline + Approvals)
// ===========================================================================
test.describe('Deployments Page: Pipeline + Approvals', () => {
test('Has Pipeline and Approvals tabs', async ({ authenticatedPage: page }) => {
await go(page, '/releases/deployments');
await expect(page.getByRole('tab', { name: 'Pipeline' })).toBeVisible({ timeout: 10_000 });
await expect(page.getByRole('tab', { name: 'Approvals' })).toBeVisible();
});
test('Does NOT have Table or Correlations tabs', async ({ authenticatedPage: page }) => {
await go(page, '/releases/deployments');
await page.waitForTimeout(1000);
await expect(page.getByRole('tab', { name: 'Table' })).not.toBeVisible();
await expect(page.getByRole('tab', { name: 'Correlations' })).not.toBeVisible();
});
test('Does NOT have redundant context chips', async ({ authenticatedPage: page }) => {
await go(page, '/releases/deployments');
await page.waitForTimeout(1000);
// The old .context div with region/env/time chips should be gone
const contextDiv = page.locator('.context');
await expect(contextDiv).not.toBeVisible();
});
test('Approvals tab has gate summary cards', async ({ authenticatedPage: page }) => {
await go(page, '/releases/deployments?view=approvals');
await page.waitForTimeout(1000);
const summary = page.locator('.gate-summary');
await expect(summary).toBeVisible({ timeout: 10_000 });
});
test('Approvals tab has Gate Type column', async ({ authenticatedPage: page }) => {
await go(page, '/releases/deployments?view=approvals');
await page.waitForTimeout(1000);
await expect(page.locator('th', { hasText: 'Gate Type' })).toBeVisible({ timeout: 10_000 });
});
test('Approvals tab has search input', async ({ authenticatedPage: page }) => {
await go(page, '/releases/deployments?view=approvals');
await page.waitForTimeout(1000);
const search = page.locator('input[placeholder*="Search approvals"]');
await expect(search).toBeVisible({ timeout: 10_000 });
});
test('No Angular errors on Deployments page', async ({ authenticatedPage: page }) => {
const ngErrors = setupErrorCollector(page);
await go(page, '/releases/deployments');
await page.waitForTimeout(2000);
expect(ngErrors).toHaveLength(0);
});
});
// ===========================================================================
// GROUP 4 — Route Redirects
// ===========================================================================
test.describe('Route redirects preserve bookmarks', () => {
test('/security/reports redirects to /security', async ({ authenticatedPage: page }) => {
await go(page, '/security/reports');
await page.waitForTimeout(1000);
expect(page.url()).toContain('/security');
expect(page.url()).not.toContain('/reports');
});
test('/triage/audit-bundles redirects to /evidence/bundles', async ({ authenticatedPage: page }) => {
await go(page, '/triage/audit-bundles');
await page.waitForTimeout(1000);
expect(page.url()).toContain('/evidence/bundles');
});
});
// ===========================================================================
// GROUP 5 — Page Content (no duplicate headings, no eyebrows)
// ===========================================================================
test.describe('Page content: no duplicate headings or eyebrows', () => {
test('Dashboard heading matches breadcrumb', async ({ authenticatedPage: page }) => {
await go(page, '/');
const heading = page.locator('h1').first();
await expect(heading).toBeVisible({ timeout: 10_000 });
await expect(heading).toContainText('Dashboard');
});
test('Vulnerabilities heading (not Artifact workspace)', async ({ authenticatedPage: page }) => {
await go(page, '/triage/artifacts');
const heading = page.locator('h1').first();
await expect(heading).toBeVisible({ timeout: 10_000 });
await expect(heading).toContainText('Vulnerabilities');
});
test('Scheduled Jobs heading (not JobEngine)', async ({ authenticatedPage: page }) => {
await go(page, '/ops/operations/jobengine');
const heading = page.locator('h1').first();
await expect(heading).toBeVisible({ timeout: 10_000 });
await expect(heading).toContainText('Scheduled Jobs');
});
test('Notifications heading (not Notification Operations)', async ({ authenticatedPage: page }) => {
await go(page, '/ops/operations/notifications');
const heading = page.locator('h1').first();
await expect(heading).toBeVisible({ timeout: 10_000 });
await expect(heading).toContainText('Notifications');
await expect(heading).not.toContainText('Notification Operations');
});
test('Audit Log heading (not Unified Audit Log)', async ({ authenticatedPage: page }) => {
await go(page, '/evidence/audit-log');
const heading = page.locator('h1').first();
await expect(heading).toBeVisible({ timeout: 10_000 });
await expect(heading).toContainText('Audit Log');
await expect(heading).not.toContainText('Unified');
});
test('No Angular NG02100 errors on Audit Log', async ({ authenticatedPage: page }) => {
const ngErrors = setupErrorCollector(page);
await go(page, '/evidence/audit-log');
await page.waitForTimeout(2000);
expect(ngErrors).toHaveLength(0);
});
});
// ===========================================================================
// GROUP 6 — Doctor/Diagnostics
// ===========================================================================
test.describe('Diagnostics page', () => {
test('Check labels say NOT RUN (not NOT RAN)', async ({ authenticatedPage: page }) => {
await go(page, '/ops/operations/doctor');
await page.waitForTimeout(2000);
const notRunLabels = page.locator('text=NOT RUN');
const notRanLabels = page.locator('text=NOT RAN');
expect(await notRanLabels.count()).toBe(0);
expect(await notRunLabels.count()).toBeGreaterThan(0);
});
});
// ===========================================================================
// GROUP 7 — Breadcrumb consistency
// ===========================================================================
test.describe('Breadcrumb consistency', () => {
test('Operations breadcrumb not duplicated', async ({ authenticatedPage: page }) => {
await go(page, '/ops/operations/signals');
const breadcrumbs = page.locator('nav[aria-label="Breadcrumb"] li');
await expect(breadcrumbs).toHaveCount(2, { timeout: 10_000 }); // "Operations" > "Signals"
const texts = await breadcrumbs.allTextContents();
// Should NOT have "Operations" twice
const opsCount = texts.filter(t => t.trim() === 'Operations').length;
expect(opsCount).toBeLessThanOrEqual(1);
});
test('Ops breadcrumb says Operations (not Ops)', async ({ authenticatedPage: page }) => {
await go(page, '/ops/operations');
const breadcrumb = page.locator('nav[aria-label="Breadcrumb"]');
await expect(breadcrumb).toBeVisible({ timeout: 10_000 });
await expect(breadcrumb).toContainText('Operations');
await expect(breadcrumb).not.toContainText(/\bOps\b/);
});
});