search and ai stabilization work, localization stablized.
This commit is contained in:
297
src/Web/StellaOps.Web/e2e/click-navigation.e2e.spec.ts
Normal file
297
src/Web/StellaOps.Web/e2e/click-navigation.e2e.spec.ts
Normal 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');
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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({
|
||||
|
||||
504
src/Web/StellaOps.Web/e2e/i18n-translations.e2e.spec.ts
Normal file
504
src/Web/StellaOps.Web/e2e/i18n-translations.e2e.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
329
src/Web/StellaOps.Web/e2e/identity-providers.e2e.spec.ts
Normal file
329
src/Web/StellaOps.Web/e2e/identity-providers.e2e.spec.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
568
src/Web/StellaOps.Web/e2e/interactive-smoke.e2e.spec.ts
Normal file
568
src/Web/StellaOps.Web/e2e/interactive-smoke.e2e.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user