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:
@@ -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/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user