search and ai stabilization work, localization stablized.

This commit is contained in:
master
2026-02-24 23:29:36 +02:00
parent 4f947a8b61
commit b07d27772e
766 changed files with 55299 additions and 3221 deletions

View File

@@ -0,0 +1,297 @@
/**
* Click Navigation Tests
* Verifies actual sidebar click navigation between sections.
* Tests that clicking menu items transitions pages correctly.
*/
import { test, expect } from './fixtures/auth.fixture';
const SCREENSHOT_DIR = 'e2e/screenshots';
async function snap(page: import('@playwright/test').Page, label: string) {
await page.screenshot({ path: `${SCREENSHOT_DIR}/${label}.png`, fullPage: true });
}
function collectErrors(page: import('@playwright/test').Page) {
const errors: string[] = [];
page.on('console', (msg) => {
if (msg.type() === 'error') errors.push(msg.text());
});
page.on('pageerror', (err) => errors.push(err.message));
return errors;
}
async function go(page: import('@playwright/test').Page, path: string) {
await page.goto(path, { waitUntil: 'networkidle', timeout: 30_000 });
await page.waitForLoadState('domcontentloaded');
await page.waitForTimeout(1500);
}
test.describe('Sidebar Click Navigation', () => {
test('click through main sidebar sections from dashboard', async ({ authenticatedPage: page }) => {
const errors = collectErrors(page);
await go(page, '/');
// Click Releases in sidebar
const releasesLink = page.locator('text=Releases').first();
if (await releasesLink.isVisible({ timeout: 3000 }).catch(() => false)) {
await releasesLink.click();
await page.waitForTimeout(2000);
await snap(page, 'click-01-releases');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
}
// Click Release Versions
const versionsLink = page.locator('text=Release Versions').first();
if (await versionsLink.isVisible({ timeout: 3000 }).catch(() => false)) {
await versionsLink.click();
await page.waitForTimeout(2000);
await snap(page, 'click-02-release-versions');
}
// Click Approvals Queue
const approvalsLink = page.locator('text=Approvals Queue').first();
if (await approvalsLink.isVisible({ timeout: 3000 }).catch(() => false)) {
await approvalsLink.click();
await page.waitForTimeout(2000);
await snap(page, 'click-03-approvals-queue');
}
const criticalErrors = errors.filter(e =>
e.includes('NG0') || e.includes('TypeError') || e.includes('ReferenceError')
);
expect(criticalErrors, 'Critical errors during release nav: ' + criticalErrors.join('\n')).toHaveLength(0);
});
test('click through security section', async ({ authenticatedPage: page }) => {
const errors = collectErrors(page);
await go(page, '/');
// Click Posture in sidebar
const postureLink = page.locator('text=Posture').first();
if (await postureLink.isVisible({ timeout: 3000 }).catch(() => false)) {
await postureLink.click();
await page.waitForTimeout(2000);
await snap(page, 'click-04-posture');
}
// Click Triage
const triageLink = page.locator('text=Triage').first();
if (await triageLink.isVisible({ timeout: 3000 }).catch(() => false)) {
await triageLink.click();
await page.waitForTimeout(2000);
await snap(page, 'click-05-triage');
}
// Click Supply-Chain Data
const supplyLink = page.locator('text=Supply-Chain Data').first();
if (await supplyLink.isVisible({ timeout: 3000 }).catch(() => false)) {
await supplyLink.click();
await page.waitForTimeout(2000);
await snap(page, 'click-06-supply-chain');
}
// Click Reachability
const reachLink = page.locator('text=Reachability').first();
if (await reachLink.isVisible({ timeout: 3000 }).catch(() => false)) {
await reachLink.click();
await page.waitForTimeout(2000);
await snap(page, 'click-07-reachability');
}
// Click Reports
const reportsLink = page.locator('text=Reports').first();
if (await reportsLink.isVisible({ timeout: 3000 }).catch(() => false)) {
await reportsLink.click();
await page.waitForTimeout(2000);
await snap(page, 'click-08-reports');
}
const criticalErrors = errors.filter(e =>
e.includes('NG0') || e.includes('TypeError') || e.includes('ReferenceError')
);
expect(criticalErrors, 'Critical errors during security nav: ' + criticalErrors.join('\n')).toHaveLength(0);
});
test('click through evidence section', async ({ authenticatedPage: page }) => {
const errors = collectErrors(page);
await go(page, '/');
// Click Evidence > Overview
const overviewLink = page.locator('text=Overview').first();
if (await overviewLink.isVisible({ timeout: 3000 }).catch(() => false)) {
await overviewLink.click();
await page.waitForTimeout(2000);
await snap(page, 'click-09-evidence-overview');
}
// Click Decision Capsules
const capsulesLink = page.locator('text=Decision Capsules').first();
if (await capsulesLink.isVisible({ timeout: 3000 }).catch(() => false)) {
await capsulesLink.click();
await page.waitForTimeout(2000);
await snap(page, 'click-10-decision-capsules');
}
// Click Replay & Verify
const replayLink = page.locator('text=Replay').first();
if (await replayLink.isVisible({ timeout: 3000 }).catch(() => false)) {
await replayLink.click();
await page.waitForTimeout(2000);
await snap(page, 'click-11-replay');
}
const criticalErrors = errors.filter(e =>
e.includes('NG0') || e.includes('TypeError') || e.includes('ReferenceError')
);
expect(criticalErrors, 'Critical errors during evidence nav: ' + criticalErrors.join('\n')).toHaveLength(0);
});
});
test.describe('Header Toolbar Interactions', () => {
test('region dropdown opens', async ({ authenticatedPage: page }) => {
await go(page, '/');
// Try to click the Region dropdown
const regionBtn = page.locator('text=All regions').first();
if (await regionBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
await regionBtn.click();
await page.waitForTimeout(1000);
await snap(page, 'click-12-region-dropdown');
// Close by clicking elsewhere
await page.locator('body').click({ position: { x: 600, y: 400 } });
}
});
test('environment dropdown opens', async ({ authenticatedPage: page }) => {
await go(page, '/');
const envBtn = page.locator('text=All environments').first();
if (await envBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
await envBtn.click();
await page.waitForTimeout(1000);
await snap(page, 'click-13-env-dropdown');
await page.locator('body').click({ position: { x: 600, y: 400 } });
}
});
test('window dropdown opens', async ({ authenticatedPage: page }) => {
await go(page, '/');
const windowBtn = page.locator('text=24h').first();
if (await windowBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
await windowBtn.click();
await page.waitForTimeout(1000);
await snap(page, 'click-14-window-dropdown');
await page.locator('body').click({ position: { x: 600, y: 400 } });
}
});
test('dashboard button returns home', async ({ authenticatedPage: page }) => {
await go(page, '/security/posture');
const dashBtn = page.locator('text=Dashboard').first();
if (await dashBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
await dashBtn.click();
await page.waitForTimeout(2000);
// Should be back at dashboard
const url = page.url();
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(20);
await snap(page, 'click-15-back-to-dashboard');
}
});
});
test.describe('Tab Navigation Within Pages', () => {
test('topology page tab switching', async ({ authenticatedPage: page }) => {
await go(page, '/setup/topology/overview');
// Click Map tab
const mapTab = page.locator('text=Map').first();
if (await mapTab.isVisible({ timeout: 3000 }).catch(() => false)) {
await mapTab.click();
await page.waitForTimeout(2000);
await snap(page, 'click-16-topology-map-tab');
}
// Click Targets tab
const targetsTab = page.locator('text=Targets').first();
if (await targetsTab.isVisible({ timeout: 3000 }).catch(() => false)) {
await targetsTab.click();
await page.waitForTimeout(2000);
await snap(page, 'click-17-topology-targets-tab');
}
// Click Hosts tab
const hostsTab = page.locator('text=Hosts').first();
if (await hostsTab.isVisible({ timeout: 3000 }).catch(() => false)) {
await hostsTab.click();
await page.waitForTimeout(2000);
await snap(page, 'click-18-topology-hosts-tab');
}
// Click Agents tab
const agentsTab = page.locator('text=Agents').first();
if (await agentsTab.isVisible({ timeout: 3000 }).catch(() => false)) {
await agentsTab.click();
await page.waitForTimeout(2000);
await snap(page, 'click-19-topology-agents-tab');
}
});
test('policy governance tab switching', async ({ authenticatedPage: page }) => {
await go(page, '/ops/policy/overview');
// Click Risk Budget tab
const riskTab = page.locator('text=Risk Budget').first();
if (await riskTab.isVisible({ timeout: 3000 }).catch(() => false)) {
await riskTab.click();
await page.waitForTimeout(2000);
await snap(page, 'click-20-policy-risk-budget-tab');
}
// Click Sealed Mode tab
const sealedTab = page.locator('text=Sealed Mode').first();
if (await sealedTab.isVisible({ timeout: 3000 }).catch(() => false)) {
await sealedTab.click();
await page.waitForTimeout(2000);
await snap(page, 'click-21-policy-sealed-mode-tab');
}
// Click Profiles tab
const profilesTab = page.locator('text=Profiles').first();
if (await profilesTab.isVisible({ timeout: 3000 }).catch(() => false)) {
await profilesTab.click();
await page.waitForTimeout(2000);
await snap(page, 'click-22-policy-profiles-tab');
}
});
test('notifications page tab switching', async ({ authenticatedPage: page }) => {
await go(page, '/setup/notifications/rules');
// Click Channels tab
const channelsTab = page.locator('text=Channels').first();
if (await channelsTab.isVisible({ timeout: 3000 }).catch(() => false)) {
await channelsTab.click();
await page.waitForTimeout(2000);
await snap(page, 'click-23-notifications-channels-tab');
}
// Click Templates tab
const templatesTab = page.locator('text=Templates').first();
if (await templatesTab.isVisible({ timeout: 3000 }).catch(() => false)) {
await templatesTab.click();
await page.waitForTimeout(2000);
await snap(page, 'click-24-notifications-templates-tab');
}
// Click Delivery tab
const deliveryTab = page.locator('text=Delivery').first();
if (await deliveryTab.isVisible({ timeout: 3000 }).catch(() => false)) {
await deliveryTab.click();
await page.waitForTimeout(2000);
await snap(page, 'click-25-notifications-delivery-tab');
}
});
});

View File

@@ -55,8 +55,75 @@ const adminTestSession: StubAuthSession = {
],
};
/** Minimal runtime config for deterministic SPA bootstrap in E2E. */
const e2eRuntimeConfig = {
setup: 'complete',
authority: {
issuer: 'https://127.0.0.1',
clientId: 'stellaops-web-e2e',
authorizeEndpoint: 'https://127.0.0.1/connect/authorize',
tokenEndpoint: 'https://127.0.0.1/connect/token',
logoutEndpoint: 'https://127.0.0.1/connect/logout',
redirectUri: 'https://127.0.0.1/auth/callback',
postLogoutRedirectUri: 'https://127.0.0.1/',
scope: 'openid profile ui.read',
audience: 'stellaops',
dpopAlgorithms: ['ES256'],
refreshLeewaySeconds: 60,
},
apiBaseUrls: {
authority: '',
gateway: '',
policy: '',
scanner: '',
concelier: '',
attestor: '',
},
telemetry: {
sampleRate: 0,
},
};
export const test = base.extend<{ authenticatedPage: Page }>({
authenticatedPage: async ({ page }, use) => {
// Ensure APP_INITIALIZER config resolution does not hang on missing backend proxy targets.
await page.route('**/platform/envsettings.json', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(e2eRuntimeConfig),
});
});
await page.route('**/config.json', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(e2eRuntimeConfig),
});
});
// Keep backend probe guard reachable in isolated E2E runs.
await page.route('https://127.0.0.1/.well-known/openid-configuration', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
issuer: 'https://127.0.0.1',
authorization_endpoint: 'https://127.0.0.1/connect/authorize',
}),
});
});
// Prevent background health polling from failing the shell bootstrap path.
await page.route('**/health', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ status: 'ok' }),
});
});
// Intercept branding endpoint that can return 500 in dev/Docker
await page.route('**/console/branding**', (route) => {
route.fulfill({

View File

@@ -0,0 +1,504 @@
/**
* i18n Translation System — E2E Tests
*
* Verifies that:
* 1. Platform translation API (/platform/i18n/{locale}.json) is requested on load
* 2. Translated strings render correctly in the UI (no raw keys visible)
* 3. Offline fallback works when Platform API is unavailable
* 4. Locale switching re-fetches and updates displayed text
* 5. Multiple routes render translated content without raw keys
*/
import { test, expect } from './fixtures/auth.fixture';
import { navigateAndWait } from './helpers/nav.helper';
/** Regex to match the Platform i18n API URL pattern */
const I18N_API_PATTERN = /\/platform\/i18n\/.*\.json/;
/** Subset of en-US translations for verification */
const EN_US_BUNDLE: Record<string, string> = {
'ui.loading.skeleton': 'Loading...',
'ui.error.generic': 'Something went wrong.',
'ui.error.network': 'Network error. Check your connection.',
'ui.actions.save': 'Save',
'ui.actions.cancel': 'Cancel',
'ui.actions.delete': 'Delete',
'ui.actions.confirm': 'Confirm',
'ui.actions.close': 'Close',
'ui.actions.retry': 'Retry',
'ui.actions.search': 'Search',
'ui.actions.export': 'Export',
'ui.actions.refresh': 'Refresh',
'ui.actions.sign_in': 'Sign in',
'ui.labels.status': 'Status',
'ui.labels.severity': 'Severity',
'ui.labels.details': 'Details',
'ui.labels.filters': 'Filters',
'ui.severity.critical': 'Critical',
'ui.severity.high': 'High',
'ui.severity.medium': 'Medium',
'ui.severity.low': 'Low',
'ui.severity.info': 'Info',
'ui.severity.none': 'None',
'ui.release_orchestrator.title': 'Release Orchestrator',
'ui.release_orchestrator.subtitle': 'Pipeline overview and release management',
'ui.release_orchestrator.pipeline_runs': 'Pipeline Runs',
'ui.risk_dashboard.title': 'Risk Profiles',
'ui.risk_dashboard.subtitle': 'Tenant-scoped risk posture with deterministic ordering.',
'ui.risk_dashboard.search_placeholder': 'Title contains',
'ui.findings.title': 'Findings',
'ui.findings.search_placeholder': 'Search findings...',
'ui.findings.no_findings': 'No findings to display.',
'ui.sources_dashboard.title': 'Sources Dashboard',
'ui.timeline.title': 'Timeline',
'ui.timeline.empty_state': 'Enter a correlation ID to view the event timeline',
'ui.exception_center.title': 'Exception Center',
'ui.evidence_thread.title_default': 'Evidence Thread',
'ui.first_signal.label': 'First signal',
'ui.first_signal.waiting': 'Waiting for first signal\u2026',
'ui.first_signal.kind.queued': 'Queued',
'ui.first_signal.kind.started': 'Started',
'ui.first_signal.kind.succeeded': 'Succeeded',
'ui.first_signal.kind.failed': 'Failed',
'ui.locale.en_us': 'English (US)',
'ui.locale.de_de': 'German (DE)',
'common.error.generic': 'Something went wrong.',
'common.error.not_found': 'The requested resource was not found.',
'common.actions.save': 'Save',
'common.actions.cancel': 'Cancel',
'common.status.healthy': 'Healthy',
'common.status.active': 'Active',
'common.status.pending': 'Pending',
'common.status.failed': 'Failed',
'common.severity.critical': 'Critical',
'common.severity.high': 'High',
'common.severity.medium': 'Medium',
'common.severity.low': 'Low',
};
/** de-DE translation bundle for locale switch test */
const DE_DE_BUNDLE: Record<string, string> = {
'ui.actions.save': 'Speichern',
'ui.actions.cancel': 'Abbrechen',
'ui.actions.delete': 'L\u00f6schen',
'ui.actions.search': 'Suche',
'ui.release_orchestrator.title': 'Release-Orchestrator',
'ui.risk_dashboard.title': 'Risikoprofile',
'ui.findings.title': 'Ergebnisse',
'ui.timeline.title': 'Zeitleiste',
'ui.exception_center.title': 'Ausnahmezentrum',
'ui.locale.en_us': 'Englisch (US)',
'ui.locale.de_de': 'Deutsch (DE)',
};
/**
* Collect any console warnings about missing translation keys.
*/
function setupTranslationWarningCollector(page: import('@playwright/test').Page) {
const warnings: string[] = [];
page.on('console', (msg) => {
const text = msg.text();
if (msg.type() === 'warning' && text.includes('Translation key not found')) {
warnings.push(text);
}
});
return warnings;
}
/**
* Intercept translation API requests using a regex pattern for reliability.
* Returns a tracker object with captured request data.
*/
async function mockTranslationApi(
page: import('@playwright/test').Page,
bundle: Record<string, string> = EN_US_BUNDLE,
options?: { bundleByLocale?: Record<string, Record<string, string>> }
) {
const tracker = {
requested: false,
locales: [] as string[],
urls: [] as string[],
headers: {} as Record<string, string>,
};
await page.route(I18N_API_PATTERN, async (route) => {
const url = route.request().url();
const locale = url.match(/\/platform\/i18n\/(.+?)\.json/)?.[1] ?? '';
tracker.requested = true;
tracker.locales.push(locale);
tracker.urls.push(url);
tracker.headers = route.request().headers();
const responseBundle = options?.bundleByLocale?.[locale] ?? bundle;
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(responseBundle),
});
});
return tracker;
}
test.describe('i18n Translation Loading', () => {
test('translations are loaded and page renders without raw keys', async ({ authenticatedPage: page }) => {
const tracker = await mockTranslationApi(page);
// Passive listener to capture ALL request URLs
const allRequestUrls: string[] = [];
page.on('request', (req) => allRequestUrls.push(req.url()));
await navigateAndWait(page, '/', { timeout: 30_000 });
await page.waitForTimeout(2000);
// Verify translations are active — page renders meaningful content, not raw keys
const bodyText = await page.locator('body').innerText();
expect(bodyText.trim().length, 'Page should render content').toBeGreaterThan(50);
// No raw translation keys should be visible in the page
const rawKeyLines = bodyText.split('\n').filter((line) => {
const trimmed = line.trim();
return /^(ui|common)\.\w+\.\w+/.test(trimmed) && !trimmed.includes('http');
});
expect(rawKeyLines, `Raw keys found: ${rawKeyLines.join(', ')}`).toHaveLength(0);
// If the new Platform i18n API is active, verify it was called correctly
const i18nRequests = allRequestUrls.filter((url) => url.includes('/platform/i18n/'));
if (tracker.requested) {
expect(tracker.locales[0], 'Default locale should be en-US').toBe('en-US');
} else if (i18nRequests.length > 0) {
expect(i18nRequests[0]).toContain('en-US');
}
// If neither detected, translations are loaded via embedded/inline bundle (pre-build)
});
test('loads translations and renders them (no raw keys visible)', async ({ authenticatedPage: page }) => {
const translationWarnings = setupTranslationWarningCollector(page);
await mockTranslationApi(page);
await navigateAndWait(page, '/', { timeout: 30_000 });
await page.waitForTimeout(2000);
// Page should render without excessive missing-key warnings
expect(
translationWarnings.length,
`Unexpected missing translations: ${translationWarnings.join(', ')}`
).toBeLessThan(5);
});
test('falls back to embedded offline bundle when Platform API fails', async ({ authenticatedPage: page }) => {
// Make the Platform API return 500
await page.route(I18N_API_PATTERN, async (route) => {
await route.fulfill({
status: 500,
contentType: 'text/plain',
body: 'Internal Server Error',
});
});
await navigateAndWait(page, '/', { timeout: 30_000 });
await page.waitForTimeout(2000);
// Page should still render (fallback bundle loaded)
const bodyText = await page.locator('body').innerText();
expect(
bodyText.trim().length,
'Page should render content even when API fails'
).toBeGreaterThan(10);
});
test('falls back to embedded offline bundle when Platform API times out', async ({ authenticatedPage: page }) => {
// Simulate network timeout by aborting
await page.route(I18N_API_PATTERN, async (route) => {
await route.abort('timedout');
});
await navigateAndWait(page, '/', { timeout: 30_000 });
await page.waitForTimeout(2000);
const bodyText = await page.locator('body').innerText();
expect(
bodyText.trim().length,
'Page should render content even with network timeout'
).toBeGreaterThan(10);
});
});
test.describe('i18n Translated Content on Routes', () => {
test.beforeEach(async ({ authenticatedPage: page }) => {
await mockTranslationApi(page);
});
/**
* Routes to verify. Each route should render some translated content.
* Some routes may only show minimal content (e.g., "Skip to main content")
* if their data APIs are not mocked, so we verify no raw keys are shown.
*/
const ROUTES_WITH_TITLES: {
path: string;
name: string;
expectedText?: string;
}[] = [
{ path: '/findings', name: 'Findings', expectedText: 'Findings' },
{ path: '/', name: 'Control Plane' },
{ path: '/operations/orchestrator', name: 'Release Orchestrator' },
{ path: '/security', name: 'Risk Dashboard' },
{ path: '/timeline', name: 'Timeline' },
{ path: '/policy/exceptions', name: 'Exception Center' },
];
for (const route of ROUTES_WITH_TITLES) {
test(`renders ${route.name} (${route.path}) without raw translation keys`, async ({
authenticatedPage: page,
}) => {
const translationWarnings = setupTranslationWarningCollector(page);
await navigateAndWait(page, route.path, { timeout: 30_000 });
await page.waitForTimeout(2000);
const bodyText = await page.locator('body').innerText();
// Page should have some content
expect(bodyText.trim().length, `${route.name} should render content`).toBeGreaterThan(5);
// No raw translation keys should be visible
const lines = bodyText.split('\n');
const rawKeyLines = lines.filter((line) => {
const trimmed = line.trim();
return /^(ui|common)\.\w+\.\w+/.test(trimmed) && !trimmed.includes('http');
});
expect(
rawKeyLines,
`Raw translation keys on ${route.path}: ${rawKeyLines.join(', ')}`
).toHaveLength(0);
// If we expect specific text AND it's available, verify
if (route.expectedText && bodyText.length > 50) {
expect(bodyText).toContain(route.expectedText);
}
});
}
});
test.describe('i18n No Raw Keys on Navigation', () => {
test.beforeEach(async ({ authenticatedPage: page }) => {
await mockTranslationApi(page);
});
test('no raw i18n keys across multi-route navigation', async ({ authenticatedPage: page }) => {
const translationWarnings = setupTranslationWarningCollector(page);
const routesToVisit = ['/', '/security', '/findings', '/policy/exceptions'];
for (const route of routesToVisit) {
await navigateAndWait(page, route, { timeout: 30_000 });
await page.waitForTimeout(1000);
const bodyText = await page.locator('body').innerText();
const lines = bodyText.split('\n');
const rawKeyLines = lines.filter((line) => {
const trimmed = line.trim();
return /^(ui|common)\.\w+\.\w+/.test(trimmed) && !trimmed.includes('http');
});
expect(
rawKeyLines,
`Raw translation keys on ${route}: ${rawKeyLines.join(', ')}`
).toHaveLength(0);
}
});
});
test.describe('i18n Locale Switching', () => {
test('switching locale from selector fetches de-DE bundle and renders German text', async ({
authenticatedPage: page,
}) => {
const tracker = await mockTranslationApi(page, EN_US_BUNDLE, {
bundleByLocale: {
'en-US': EN_US_BUNDLE,
'de-DE': DE_DE_BUNDLE,
},
});
// This route maintains background activity; avoid networkidle waits for this case.
await page.goto('/operations/orchestrator', {
waitUntil: 'domcontentloaded',
timeout: 30_000,
});
await expect(page.locator('#topbar-locale-select')).toBeVisible({ timeout: 30_000 });
await page.waitForTimeout(1000);
await page.selectOption('#topbar-locale-select', 'de-DE');
await page.waitForFunction(
() => localStorage.getItem('stellaops_locale') === 'de-DE',
{ timeout: 10_000 }
);
await page.waitForTimeout(1000);
expect(
tracker.locales.includes('de-DE'),
`Expected de-DE translation request, got locales: ${tracker.locales.join(', ')}`
).toBeTruthy();
await expect(page.locator('#topbar-locale-select option[value="de-DE"]')).toHaveText(
'Deutsch (DE)',
{ timeout: 10_000 }
);
await expect(page.locator('body')).toContainText('Deutsch (DE)');
});
test('locale preference can be saved and persists in localStorage', async ({
authenticatedPage: page,
}) => {
await mockTranslationApi(page, EN_US_BUNDLE, {
bundleByLocale: {
'en-US': EN_US_BUNDLE,
'de-DE': DE_DE_BUNDLE,
},
});
await navigateAndWait(page, '/', { timeout: 30_000 });
await page.waitForTimeout(1000);
// Set locale preference to de-DE (simulates what I18nService.setLocale does)
await page.evaluate(() => {
localStorage.setItem('stellaops_locale', 'de-DE');
});
// Verify the preference was persisted
const savedLocale = await page.evaluate(() => localStorage.getItem('stellaops_locale'));
expect(savedLocale).toBe('de-DE');
// Reload the page
await page.reload({ waitUntil: 'networkidle' });
await page.waitForTimeout(2000);
// Verify locale preference survived the reload
const persistedLocale = await page.evaluate(() => localStorage.getItem('stellaops_locale'));
expect(persistedLocale).toBe('de-DE');
// Page should still render without raw keys after locale switch
const bodyText = await page.locator('body').innerText();
expect(bodyText.trim().length, 'Page should render after locale switch').toBeGreaterThan(50);
const rawKeyLines = bodyText.split('\n').filter((line) => {
const trimmed = line.trim();
return /^(ui|common)\.\w+\.\w+/.test(trimmed) && !trimmed.includes('http');
});
expect(rawKeyLines, `Raw keys after locale switch: ${rawKeyLines.join(', ')}`).toHaveLength(0);
});
test('saved locale persists in localStorage', async ({ authenticatedPage: page }) => {
await mockTranslationApi(page);
await navigateAndWait(page, '/', { timeout: 30_000 });
await page.waitForTimeout(1000);
// Set locale via localStorage (as setLocale would)
await page.evaluate(() => {
localStorage.setItem('stellaops_locale', 'fr-FR');
});
// Verify the preference was persisted
const savedLocale = await page.evaluate(() => {
return localStorage.getItem('stellaops_locale');
});
expect(savedLocale).toBe('fr-FR');
// After reload, the app should read from localStorage
const allRequestUrls: string[] = [];
page.on('request', (req) => allRequestUrls.push(req.url()));
await page.reload({ waitUntil: 'networkidle' });
await page.waitForTimeout(2000);
// Verify either: the fr-FR locale was requested, or localStorage still has fr-FR
const frFrRequested = allRequestUrls.some(
(url) => url.includes('/platform/i18n/') && url.includes('fr-FR')
);
const stillPersisted = await page.evaluate(() => {
return localStorage.getItem('stellaops_locale');
});
// At minimum, the locale preference should persist in localStorage
expect(stillPersisted).toBe('fr-FR');
});
});
test.describe('i18n Translation Pipe in Templates', () => {
test.beforeEach(async ({ authenticatedPage: page }) => {
await mockTranslationApi(page);
});
test('severity labels render as translated text, not raw keys', async ({
authenticatedPage: page,
}) => {
await navigateAndWait(page, '/security', { timeout: 30_000 });
await page.waitForTimeout(2000);
const bodyText = await page.locator('body').innerText();
// Should NOT show raw keys
expect(bodyText).not.toContain('ui.severity.critical');
expect(bodyText).not.toContain('ui.severity.high');
expect(bodyText).not.toContain('ui.severity.medium');
});
test('action buttons render translated labels, not raw keys', async ({
authenticatedPage: page,
}) => {
await navigateAndWait(page, '/', { timeout: 30_000 });
await page.waitForTimeout(2000);
const bodyText = await page.locator('body').innerText();
// Should NOT show raw keys as visible text
expect(bodyText).not.toContain('ui.actions.save');
expect(bodyText).not.toContain('ui.actions.cancel');
expect(bodyText).not.toContain('ui.actions.delete');
expect(bodyText).not.toContain('ui.actions.sign_in');
});
});
test.describe('i18n API Contract', () => {
test('translation bundle keys follow flat dot-path format', async ({
authenticatedPage: page,
}) => {
// Verify our expected bundle format is valid
for (const [key, value] of Object.entries(EN_US_BUNDLE)) {
expect(typeof key).toBe('string');
expect(typeof value).toBe('string');
// Keys follow the dot-path pattern: namespace.feature.field[.subfield...]
expect(key).toMatch(/^[\w]+\.[\w]+\.[\w.]+$/);
}
});
test('Platform API is requested with correct URL structure', async ({
authenticatedPage: page,
}) => {
const tracker = await mockTranslationApi(page);
const requestPromise = page
.waitForRequest((req) => I18N_API_PATTERN.test(req.url()), { timeout: 15_000 })
.catch(() => null);
await navigateAndWait(page, '/', { timeout: 30_000 });
const req = await requestPromise;
// Verify the request URL structure (either from mock or actual)
if (req) {
const url = new URL(req.url());
expect(url.pathname).toMatch(/^\/platform\/i18n\/[\w-]+\.json$/);
} else if (tracker.urls.length > 0) {
expect(tracker.urls[0]).toMatch(/\/platform\/i18n\/[\w-]+\.json/);
}
// At minimum, translations loaded (either from mock or real server)
const bodyText = await page.locator('body').innerText();
expect(bodyText.trim().length).toBeGreaterThan(5);
});
});

View File

@@ -0,0 +1,329 @@
import { test, expect } from './fixtures/auth.fixture';
import { navigateAndWait, assertPageHasContent } from './helpers/nav.helper';
/**
* E2E tests for the Identity Providers settings page.
*
* These tests use the auth fixture which mocks the backend API.
* The MockIdentityProviderClient in app.config.ts serves mock data,
* so these tests verify UI rendering and interaction without a live backend.
*/
const sampleLdapProvider = {
id: 'e2e-ldap-id',
name: 'E2E LDAP',
type: 'ldap',
enabled: true,
configuration: {
host: 'ldap.e2e.test',
port: '389',
bindDn: 'cn=admin,dc=e2e,dc=test',
bindPassword: 'secret',
searchBase: 'dc=e2e,dc=test',
},
description: 'E2E LDAP test provider',
healthStatus: 'healthy',
createdAt: '2026-02-24T00:00:00Z',
updatedAt: '2026-02-24T00:00:00Z',
createdBy: 'e2e-admin',
updatedBy: 'e2e-admin',
};
const sampleSamlProvider = {
id: 'e2e-saml-id',
name: 'E2E SAML',
type: 'saml',
enabled: true,
configuration: {
spEntityId: 'stellaops-e2e-sp',
idpEntityId: 'https://idp.e2e.test',
idpSsoUrl: 'https://idp.e2e.test/sso',
},
description: 'E2E SAML test provider',
healthStatus: 'healthy',
createdAt: '2026-02-24T00:00:00Z',
updatedAt: '2026-02-24T00:00:00Z',
createdBy: 'e2e-admin',
updatedBy: 'e2e-admin',
};
const sampleOidcProvider = {
id: 'e2e-oidc-id',
name: 'E2E OIDC',
type: 'oidc',
enabled: false,
configuration: {
authority: 'https://oidc.e2e.test',
clientId: 'stellaops-e2e',
clientSecret: 'e2e-secret',
},
description: 'E2E OIDC test provider',
healthStatus: 'disabled',
createdAt: '2026-02-24T00:00:00Z',
updatedAt: '2026-02-24T00:00:00Z',
createdBy: 'e2e-admin',
updatedBy: 'e2e-admin',
};
test.describe('Identity Providers Settings Page', () => {
test('should load page and display content', async ({ authenticatedPage: page }) => {
const errors: string[] = [];
page.on('console', (msg) => {
if (msg.type() === 'error' && /NG0\d{3,4}/.test(msg.text())) {
errors.push(msg.text());
}
});
// Mock the identity providers API
await page.route('**/api/v1/platform/identity-providers', (route) => {
if (route.request().method() === 'GET') {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([sampleLdapProvider, sampleSamlProvider, sampleOidcProvider]),
});
} else {
route.continue();
}
});
await page.route('**/api/v1/platform/identity-providers/types', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([
{ type: 'standard', displayName: 'Standard', requiredFields: [], optionalFields: [] },
{
type: 'ldap',
displayName: 'LDAP / Active Directory',
requiredFields: [
{ name: 'host', displayName: 'Host', fieldType: 'text', defaultValue: null, description: null },
{ name: 'port', displayName: 'Port', fieldType: 'number', defaultValue: '389', description: null },
{ name: 'bindDn', displayName: 'Bind DN', fieldType: 'text', defaultValue: null, description: null },
{ name: 'bindPassword', displayName: 'Bind Password', fieldType: 'secret', defaultValue: null, description: null },
{ name: 'searchBase', displayName: 'Search Base', fieldType: 'text', defaultValue: null, description: null },
],
optionalFields: [],
},
{
type: 'saml',
displayName: 'SAML 2.0',
requiredFields: [
{ name: 'spEntityId', displayName: 'SP Entity ID', fieldType: 'text', defaultValue: null, description: null },
{ name: 'idpEntityId', displayName: 'IdP Entity ID', fieldType: 'text', defaultValue: null, description: null },
],
optionalFields: [],
},
{
type: 'oidc',
displayName: 'OpenID Connect',
requiredFields: [
{ name: 'authority', displayName: 'Authority', fieldType: 'url', defaultValue: null, description: null },
{ name: 'clientId', displayName: 'Client ID', fieldType: 'text', defaultValue: null, description: null },
],
optionalFields: [],
},
]),
});
});
await navigateAndWait(page, '/settings/identity-providers', { timeout: 30_000 });
await page.waitForTimeout(2000);
await assertPageHasContent(page);
expect(errors).toHaveLength(0);
});
test('should show empty state with no providers', async ({ authenticatedPage: page }) => {
await page.route('**/api/v1/platform/identity-providers', (route) => {
if (route.request().method() === 'GET') {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([]),
});
} else {
route.continue();
}
});
await page.route('**/api/v1/platform/identity-providers/types', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([]),
});
});
await navigateAndWait(page, '/settings/identity-providers', { timeout: 30_000 });
await page.waitForTimeout(2000);
const emptyState = page.locator('.idp-empty-state');
if (await emptyState.isVisible({ timeout: 5000 }).catch(() => false)) {
await expect(emptyState).toContainText('No identity providers');
}
});
test('should display provider cards with correct type badges', async ({ authenticatedPage: page }) => {
await page.route('**/api/v1/platform/identity-providers', (route) => {
if (route.request().method() === 'GET') {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([sampleLdapProvider, sampleSamlProvider, sampleOidcProvider]),
});
} else {
route.continue();
}
});
await page.route('**/api/v1/platform/identity-providers/types', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([]),
});
});
await navigateAndWait(page, '/settings/identity-providers', { timeout: 30_000 });
await page.waitForTimeout(2000);
// Verify provider names are visible
const ldapName = page.locator('text=E2E LDAP').first();
const samlName = page.locator('text=E2E SAML').first();
const oidcName = page.locator('text=E2E OIDC').first();
if (await ldapName.isVisible({ timeout: 5000 }).catch(() => false)) {
await expect(ldapName).toBeVisible();
}
if (await samlName.isVisible({ timeout: 5000 }).catch(() => false)) {
await expect(samlName).toBeVisible();
}
if (await oidcName.isVisible({ timeout: 5000 }).catch(() => false)) {
await expect(oidcName).toBeVisible();
}
});
test('should open add provider wizard on button click', async ({ authenticatedPage: page }) => {
await page.route('**/api/v1/platform/identity-providers', (route) => {
if (route.request().method() === 'GET') {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([]),
});
} else {
route.continue();
}
});
await page.route('**/api/v1/platform/identity-providers/types', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([
{ type: 'standard', displayName: 'Standard', requiredFields: [], optionalFields: [] },
{ type: 'ldap', displayName: 'LDAP', requiredFields: [], optionalFields: [] },
{ type: 'saml', displayName: 'SAML', requiredFields: [], optionalFields: [] },
{ type: 'oidc', displayName: 'OIDC', requiredFields: [], optionalFields: [] },
]),
});
});
await navigateAndWait(page, '/settings/identity-providers', { timeout: 30_000 });
await page.waitForTimeout(2000);
const addButton = page.locator('button:has-text("Add Provider")').first();
if (await addButton.isVisible({ timeout: 5000 }).catch(() => false)) {
await addButton.click();
await page.waitForTimeout(1000);
// Wizard should be visible
const wizard = page.locator('app-add-provider-wizard, .wizard-overlay').first();
if (await wizard.isVisible({ timeout: 5000 }).catch(() => false)) {
await expect(wizard).toBeVisible();
}
}
});
test('should handle enable/disable toggle', async ({ authenticatedPage: page }) => {
await page.route('**/api/v1/platform/identity-providers', (route) => {
if (route.request().method() === 'GET') {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([sampleLdapProvider]),
});
} else {
route.continue();
}
});
await page.route('**/api/v1/platform/identity-providers/types', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([]),
});
});
// Mock disable endpoint
await page.route('**/api/v1/platform/identity-providers/*/disable', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ ...sampleLdapProvider, enabled: false, healthStatus: 'disabled' }),
});
});
await navigateAndWait(page, '/settings/identity-providers', { timeout: 30_000 });
await page.waitForTimeout(2000);
// Find disable/enable toggle button
const toggleBtn = page.locator('button:has-text("Disable")').first();
if (await toggleBtn.isVisible({ timeout: 5000 }).catch(() => false)) {
await toggleBtn.click();
await page.waitForTimeout(1000);
}
});
test('should handle delete provider', async ({ authenticatedPage: page }) => {
await page.route('**/api/v1/platform/identity-providers', (route) => {
if (route.request().method() === 'GET') {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([sampleLdapProvider]),
});
} else {
route.continue();
}
});
await page.route('**/api/v1/platform/identity-providers/types', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([]),
});
});
// Mock delete endpoint
await page.route('**/api/v1/platform/identity-providers/*', (route) => {
if (route.request().method() === 'DELETE') {
route.fulfill({ status: 204 });
} else {
route.continue();
}
});
await navigateAndWait(page, '/settings/identity-providers', { timeout: 30_000 });
await page.waitForTimeout(2000);
const deleteBtn = page.locator('button:has-text("Delete")').first();
if (await deleteBtn.isVisible({ timeout: 5000 }).catch(() => false)) {
await deleteBtn.click();
await page.waitForTimeout(1000);
}
});
});

View File

@@ -0,0 +1,568 @@
/**
* Interactive Smoke Tests - Section by Section
* Tests actual UI interactions, clicks, navigation elements on every screen.
* Takes screenshots for visual verification.
*/
import { test, expect } from './fixtures/auth.fixture';
const SCREENSHOT_DIR = 'e2e/screenshots';
async function snap(page: import('@playwright/test').Page, label: string) {
await page.screenshot({ path: `${SCREENSHOT_DIR}/${label}.png`, fullPage: true });
}
function collectErrors(page: import('@playwright/test').Page) {
const errors: string[] = [];
page.on('console', (msg) => {
if (msg.type() === 'error') errors.push(msg.text());
});
page.on('pageerror', (err) => errors.push(err.message));
return errors;
}
async function go(page: import('@playwright/test').Page, path: string) {
await page.goto(path, { waitUntil: 'networkidle', timeout: 30_000 });
await page.waitForLoadState('domcontentloaded');
await page.waitForTimeout(1500);
}
// SECTION 1: Mission Control
test.describe('Section 1: Mission Control', () => {
test('dashboard loads with widgets', async ({ authenticatedPage: page }) => {
const errors = collectErrors(page);
await go(page, '/');
await snap(page, '01-mission-control-dashboard');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(20);
const nav = page.locator('nav, [role="navigation"], .sidebar, .sidenav, mat-sidenav');
const navCount = await nav.count();
expect(navCount, 'Should have navigation element').toBeGreaterThanOrEqual(1);
const criticalErrors = errors.filter(e =>
e.includes('NG0') || e.includes('TypeError') || e.includes('ReferenceError')
);
expect(criticalErrors, 'Critical errors: ' + criticalErrors.join('\n')).toHaveLength(0);
});
test('sidebar navigation has main sections', async ({ authenticatedPage: page }) => {
await go(page, '/');
const links = page.locator('a[href], [routerlink], mat-list-item, .nav-item, .menu-item');
const count = await links.count();
expect(count, 'Should have navigation links').toBeGreaterThan(3);
await snap(page, '01-mission-control-nav');
});
test('mission control alerts page', async ({ authenticatedPage: page }) => {
const errors = collectErrors(page);
await go(page, '/mission-control/alerts');
await snap(page, '01-mission-control-alerts');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
const criticalErrors = errors.filter(e => e.includes('NG0'));
expect(criticalErrors).toHaveLength(0);
});
test('mission control activity page', async ({ authenticatedPage: page }) => {
const errors = collectErrors(page);
await go(page, '/mission-control/activity');
await snap(page, '01-mission-control-activity');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
const criticalErrors = errors.filter(e => e.includes('NG0'));
expect(criticalErrors).toHaveLength(0);
});
});
// SECTION 2: Releases
test.describe('Section 2: Releases', () => {
test('releases overview loads', async ({ authenticatedPage: page }) => {
const errors = collectErrors(page);
await go(page, '/releases/overview');
await snap(page, '02-releases-overview');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
const criticalErrors = errors.filter(e => e.includes('NG0'));
expect(criticalErrors).toHaveLength(0);
});
test('release versions page', async ({ authenticatedPage: page }) => {
await go(page, '/releases/versions');
await snap(page, '02-releases-versions');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
test('releases runs page', async ({ authenticatedPage: page }) => {
await go(page, '/releases/runs');
await snap(page, '02-releases-runs');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
test('approvals queue page', async ({ authenticatedPage: page }) => {
await go(page, '/releases/approvals');
await snap(page, '02-releases-approvals');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
test('promotion queue page', async ({ authenticatedPage: page }) => {
await go(page, '/releases/promotion-queue');
await snap(page, '02-releases-promotion-queue');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
test('environments page', async ({ authenticatedPage: page }) => {
await go(page, '/releases/environments');
await snap(page, '02-releases-environments');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
test('deployments page', async ({ authenticatedPage: page }) => {
await go(page, '/releases/deployments');
await snap(page, '02-releases-deployments');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
});
// SECTION 3: Security
test.describe('Section 3: Security', () => {
test('security posture page', async ({ authenticatedPage: page }) => {
const errors = collectErrors(page);
await go(page, '/security/posture');
await snap(page, '03-security-posture');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
const criticalErrors = errors.filter(e => e.includes('NG0'));
expect(criticalErrors).toHaveLength(0);
});
test('security triage page', async ({ authenticatedPage: page }) => {
await go(page, '/security/triage');
await snap(page, '03-security-triage');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
test('advisories and VEX page', async ({ authenticatedPage: page }) => {
await go(page, '/security/advisories-vex');
await snap(page, '03-security-advisories-vex');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
test('disposition center page', async ({ authenticatedPage: page }) => {
await go(page, '/security/disposition');
await snap(page, '03-security-disposition');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
test('supply chain data page', async ({ authenticatedPage: page }) => {
await go(page, '/security/supply-chain-data');
await snap(page, '03-security-supply-chain');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
test('reachability center page', async ({ authenticatedPage: page }) => {
await go(page, '/security/reachability');
await snap(page, '03-security-reachability');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
test('security reports page', async ({ authenticatedPage: page }) => {
await go(page, '/security/reports');
await snap(page, '03-security-reports');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
});
// SECTION 4: Evidence
test.describe('Section 4: Evidence', () => {
test('evidence overview', async ({ authenticatedPage: page }) => {
await go(page, '/evidence/overview');
await snap(page, '04-evidence-overview');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
test('decision capsules page', async ({ authenticatedPage: page }) => {
await go(page, '/evidence/capsules');
await snap(page, '04-evidence-capsules');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
test('verify and replay page', async ({ authenticatedPage: page }) => {
await go(page, '/evidence/verify-replay');
await snap(page, '04-evidence-verify-replay');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
test('evidence exports page', async ({ authenticatedPage: page }) => {
await go(page, '/evidence/exports');
await snap(page, '04-evidence-exports');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
test('audit log dashboard', async ({ authenticatedPage: page }) => {
await go(page, '/evidence/audit-log');
await snap(page, '04-evidence-audit-log');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
test('audit log events', async ({ authenticatedPage: page }) => {
await go(page, '/evidence/audit-log/events');
await snap(page, '04-evidence-audit-events');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
test('audit timeline search', async ({ authenticatedPage: page }) => {
await go(page, '/evidence/audit-log/timeline');
await snap(page, '04-evidence-audit-timeline');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
});
// SECTION 5: Ops - Operations
test.describe('Section 5: Ops - Operations', () => {
test('ops overview', async ({ authenticatedPage: page }) => {
await go(page, '/ops/operations');
await snap(page, '05-ops-overview');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
test('jobs and queues', async ({ authenticatedPage: page }) => {
await go(page, '/ops/operations/jobs-queues');
await snap(page, '05-ops-jobs-queues');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
test('system health', async ({ authenticatedPage: page }) => {
await go(page, '/ops/operations/system-health');
await snap(page, '05-ops-system-health');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
test('orchestrator dashboard', async ({ authenticatedPage: page }) => {
await go(page, '/ops/operations/orchestrator');
await snap(page, '05-ops-orchestrator');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
test('doctor diagnostics', async ({ authenticatedPage: page }) => {
await go(page, '/ops/operations/doctor');
await snap(page, '05-ops-doctor');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
test('notifications', async ({ authenticatedPage: page }) => {
await go(page, '/ops/operations/notifications');
await snap(page, '05-ops-notifications');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
test('AI runs list', async ({ authenticatedPage: page }) => {
await go(page, '/ops/operations/ai-runs');
await snap(page, '05-ops-ai-runs');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
});
// SECTION 6: Ops - Integrations
test.describe('Section 6: Ops - Integrations', () => {
test('integration hub', async ({ authenticatedPage: page }) => {
await go(page, '/ops/integrations');
await snap(page, '06-integrations-hub');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
test('registries page', async ({ authenticatedPage: page }) => {
await go(page, '/ops/integrations/registries');
await snap(page, '06-integrations-registries');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
test('source control page', async ({ authenticatedPage: page }) => {
await go(page, '/ops/integrations/scm');
await snap(page, '06-integrations-scm');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
test('CI/CD page', async ({ authenticatedPage: page }) => {
await go(page, '/ops/integrations/ci');
await snap(page, '06-integrations-ci');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
test('runtime hosts page', async ({ authenticatedPage: page }) => {
await go(page, '/ops/integrations/runtime-hosts');
await snap(page, '06-integrations-runtime');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
});
// SECTION 7: Ops - Policy
test.describe('Section 7: Ops - Policy', () => {
test('policy overview', async ({ authenticatedPage: page }) => {
await go(page, '/ops/policy/overview');
await snap(page, '07-policy-overview');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
test('policy baselines', async ({ authenticatedPage: page }) => {
await go(page, '/ops/policy/baselines');
await snap(page, '07-policy-baselines');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
test('gate catalog', async ({ authenticatedPage: page }) => {
await go(page, '/ops/policy/gates');
await snap(page, '07-policy-gates');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
test('shadow mode / simulation', async ({ authenticatedPage: page }) => {
await go(page, '/ops/policy/simulation');
await snap(page, '07-policy-simulation');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
test('policy lint', async ({ authenticatedPage: page }) => {
await go(page, '/ops/policy/simulation/lint');
await snap(page, '07-policy-lint');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
test('risk budget', async ({ authenticatedPage: page }) => {
await go(page, '/ops/policy/risk-budget');
await snap(page, '07-policy-risk-budget');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
test('sealed mode', async ({ authenticatedPage: page }) => {
await go(page, '/ops/policy/sealed-mode');
await snap(page, '07-policy-sealed-mode');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
test('policy profiles', async ({ authenticatedPage: page }) => {
await go(page, '/ops/policy/profiles');
await snap(page, '07-policy-profiles');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
});
// SECTION 8: Setup
test.describe('Section 8: Setup and Configuration', () => {
test('identity and access', async ({ authenticatedPage: page }) => {
await go(page, '/setup/identity-access');
await snap(page, '08-setup-identity');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
test('tenant and branding', async ({ authenticatedPage: page }) => {
await go(page, '/setup/tenant-branding');
await snap(page, '08-setup-tenant');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
test('notifications rules', async ({ authenticatedPage: page }) => {
await go(page, '/setup/notifications/rules');
await snap(page, '08-setup-notifications-rules');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
test('notifications channels', async ({ authenticatedPage: page }) => {
await go(page, '/setup/notifications/channels');
await snap(page, '08-setup-notifications-channels');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
test('notifications templates', async ({ authenticatedPage: page }) => {
await go(page, '/setup/notifications/templates');
await snap(page, '08-setup-notifications-templates');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
test('usage and limits', async ({ authenticatedPage: page }) => {
await go(page, '/setup/usage');
await snap(page, '08-setup-usage');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
test('system settings', async ({ authenticatedPage: page }) => {
await go(page, '/setup/system');
await snap(page, '08-setup-system');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
});
// SECTION 9: Topology
test.describe('Section 9: Topology', () => {
test('topology overview', async ({ authenticatedPage: page }) => {
await go(page, '/setup/topology/overview');
await snap(page, '09-topology-overview');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
test('topology map', async ({ authenticatedPage: page }) => {
await go(page, '/setup/topology/map');
await snap(page, '09-topology-map');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
test('regions and environments', async ({ authenticatedPage: page }) => {
await go(page, '/setup/topology/regions');
await snap(page, '09-topology-regions');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
test('targets', async ({ authenticatedPage: page }) => {
await go(page, '/setup/topology/targets');
await snap(page, '09-topology-targets');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
test('hosts', async ({ authenticatedPage: page }) => {
await go(page, '/setup/topology/hosts');
await snap(page, '09-topology-hosts');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
test('agent fleet', async ({ authenticatedPage: page }) => {
await go(page, '/setup/topology/agents');
await snap(page, '09-topology-agents');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
test('promotion graph', async ({ authenticatedPage: page }) => {
await go(page, '/setup/topology/promotion-graph');
await snap(page, '09-topology-promotion-graph');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
});
// SECTION 10: Platform Setup
test.describe('Section 10: Platform Setup', () => {
test('platform setup home', async ({ authenticatedPage: page }) => {
await go(page, '/ops/platform-setup');
await snap(page, '10-platform-setup-home');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
test('promotion paths', async ({ authenticatedPage: page }) => {
await go(page, '/ops/platform-setup/promotion-paths');
await snap(page, '10-platform-promotion-paths');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
test('workflows and gates', async ({ authenticatedPage: page }) => {
await go(page, '/ops/platform-setup/workflows-gates');
await snap(page, '10-platform-workflows-gates');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
test('trust and signing', async ({ authenticatedPage: page }) => {
await go(page, '/ops/platform-setup/trust-signing');
await snap(page, '10-platform-trust-signing');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
});
// SECTION 11: AI and Analysis
test.describe('Section 11: AI and Analysis', () => {
test('AI chat', async ({ authenticatedPage: page }) => {
await go(page, '/ai/chat');
await snap(page, '11-ai-chat');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
test('AI autofix', async ({ authenticatedPage: page }) => {
await go(page, '/ai/autofix');
await snap(page, '11-ai-autofix');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
test('graph explorer', async ({ authenticatedPage: page }) => {
await go(page, '/graph');
await snap(page, '11-graph-explorer');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
test('timeline', async ({ authenticatedPage: page }) => {
await go(page, '/timeline');
await snap(page, '11-timeline');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
test('change trace', async ({ authenticatedPage: page }) => {
await go(page, '/change-trace');
await snap(page, '11-change-trace');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
});
// SECTION 12: Welcome
test.describe('Section 12: Welcome and Setup Wizard', () => {
test('welcome page (no auth)', async ({ page }) => {
await page.goto('/welcome', { waitUntil: 'networkidle', timeout: 30_000 });
await page.waitForTimeout(1500);
await snap(page, '12-welcome');
const body = await page.locator('body').innerText();
expect(body.length).toBeGreaterThan(10);
});
});