diff --git a/src/Web/StellaOps.Web/tests/e2e/integrations/aaa-advisory-sync.e2e.spec.ts b/src/Web/StellaOps.Web/tests/e2e/integrations/aaa-advisory-sync.e2e.spec.ts index c26deb4d0..8b75cb740 100644 --- a/src/Web/StellaOps.Web/tests/e2e/integrations/aaa-advisory-sync.e2e.spec.ts +++ b/src/Web/StellaOps.Web/tests/e2e/integrations/aaa-advisory-sync.e2e.spec.ts @@ -9,7 +9,7 @@ */ import { test, expect } from './live-auth.fixture'; -import { snap } from './helpers'; +import { snap, waitForAngular } from './helpers'; test.setTimeout(180_000); @@ -179,7 +179,7 @@ test.describe('Advisory Sync — UI Verification', () => { waitUntil: 'domcontentloaded', timeout: 45_000, }); - await page.waitForTimeout(3_000); + await waitForAngular(page); const content = await page.textContent('body'); expect(content?.length).toBeGreaterThan(100); @@ -193,7 +193,7 @@ test.describe('Advisory Sync — UI Verification', () => { waitUntil: 'domcontentloaded', timeout: 45_000, }); - await page.waitForTimeout(3_000); + await waitForAngular(page); const tab = page.getByRole('tab', { name: /advisory/i }); await expect(tab).toBeVisible({ timeout: 10_000 }); diff --git a/src/Web/StellaOps.Web/tests/e2e/integrations/error-resilience.e2e.spec.ts b/src/Web/StellaOps.Web/tests/e2e/integrations/error-resilience.e2e.spec.ts index 935eacb04..701910760 100644 --- a/src/Web/StellaOps.Web/tests/e2e/integrations/error-resilience.e2e.spec.ts +++ b/src/Web/StellaOps.Web/tests/e2e/integrations/error-resilience.e2e.spec.ts @@ -18,6 +18,7 @@ import { createIntegrationViaApi, cleanupIntegrations, snap, + waitForAngular, } from './helpers'; const BASE = process.env['PLAYWRIGHT_BASE_URL'] || 'https://stella-ops.local'; @@ -150,7 +151,7 @@ test.describe('Error Resilience — UI Empty States', () => { waitUntil: 'domcontentloaded', timeout: 45_000, }); - await page.waitForTimeout(3_000); + await waitForAngular(page); const content = await page.textContent('body'); expect(content!.length).toBeGreaterThan(50); @@ -172,7 +173,7 @@ test.describe('Error Resilience — UI Empty States', () => { waitUntil: 'domcontentloaded', timeout: 45_000, }); - await page.waitForTimeout(3_000); + await waitForAngular(page); const content = await page.textContent('body'); expect(content!.length).toBeGreaterThan(20); diff --git a/src/Web/StellaOps.Web/tests/e2e/integrations/gitlab-integration.e2e.spec.ts b/src/Web/StellaOps.Web/tests/e2e/integrations/gitlab-integration.e2e.spec.ts index 0448509b9..7714c5c74 100644 --- a/src/Web/StellaOps.Web/tests/e2e/integrations/gitlab-integration.e2e.spec.ts +++ b/src/Web/StellaOps.Web/tests/e2e/integrations/gitlab-integration.e2e.spec.ts @@ -18,6 +18,7 @@ import { createIntegrationViaApi, cleanupIntegrations, snap, + waitForAngular, } from './helpers'; const BASE = process.env['PLAYWRIGHT_BASE_URL'] || 'https://stella-ops.local'; @@ -119,7 +120,7 @@ test.describe('GitLab Integration — UI Verification', () => { waitUntil: 'domcontentloaded', timeout: 45_000, }); - await page.waitForTimeout(3_000); + await waitForAngular(page); const pageContent = await page.textContent('body'); expect(pageContent).toContain('GitLab'); diff --git a/src/Web/StellaOps.Web/tests/e2e/integrations/helpers.ts b/src/Web/StellaOps.Web/tests/e2e/integrations/helpers.ts index 68eb961f5..9416aa9f8 100644 --- a/src/Web/StellaOps.Web/tests/e2e/integrations/helpers.ts +++ b/src/Web/StellaOps.Web/tests/e2e/integrations/helpers.ts @@ -5,6 +5,22 @@ import type { APIRequestContext, Page } from '@playwright/test'; const SCREENSHOT_DIR = 'tests/e2e/screenshots/integrations'; +/** + * Wait for Angular to bootstrap and render the app shell. + * Call after page.goto() instead of a fixed waitForTimeout. + * Waits for the sidebar navigation or any heading to appear. + */ +export async function waitForAngular(page: Page, timeoutMs = 15_000): Promise { + try { + await page.waitForSelector('nav[aria-label], h1, h2, .stella-page, [class*="dashboard"]', { + timeout: timeoutMs, + }); + } catch { + // If no known element appears, fall back to a delay + await page.waitForTimeout(5_000); + } +} + // --------------------------------------------------------------------------- // Integration configs for each provider type // --------------------------------------------------------------------------- diff --git a/src/Web/StellaOps.Web/tests/e2e/integrations/integrations.e2e.spec.ts b/src/Web/StellaOps.Web/tests/e2e/integrations/integrations.e2e.spec.ts index 558eea195..1071732ce 100644 --- a/src/Web/StellaOps.Web/tests/e2e/integrations/integrations.e2e.spec.ts +++ b/src/Web/StellaOps.Web/tests/e2e/integrations/integrations.e2e.spec.ts @@ -19,6 +19,7 @@ import { execSync } from 'child_process'; import { test, expect } from './live-auth.fixture'; +import { waitForAngular } from './helpers'; const SCREENSHOT_DIR = 'e2e/screenshots/integrations'; const BASE = process.env['PLAYWRIGHT_BASE_URL'] || 'https://stella-ops.local'; @@ -620,7 +621,7 @@ test.describe('Integration Services — UI Verification', () => { test('landing page redirects to first populated tab or shows onboarding', async ({ liveAuthPage: page }) => { await page.goto(`${BASE}/setup/integrations`, { waitUntil: 'domcontentloaded', timeout: 45_000 }); await page.waitForLoadState('domcontentloaded'); - await page.waitForTimeout(3_000); + await waitForAngular(page); const url = page.url(); // Should either redirect to a tab (registries/scm/ci) or show onboarding diff --git a/src/Web/StellaOps.Web/tests/e2e/integrations/runtime-hosts.e2e.spec.ts b/src/Web/StellaOps.Web/tests/e2e/integrations/runtime-hosts.e2e.spec.ts index 9e734afa4..762318760 100644 --- a/src/Web/StellaOps.Web/tests/e2e/integrations/runtime-hosts.e2e.spec.ts +++ b/src/Web/StellaOps.Web/tests/e2e/integrations/runtime-hosts.e2e.spec.ts @@ -19,6 +19,7 @@ import { createIntegrationViaApi, cleanupIntegrations, snap, + waitForAngular, } from './helpers'; const BASE = process.env['PLAYWRIGHT_BASE_URL'] || 'https://stella-ops.local'; @@ -147,7 +148,7 @@ test.describe('Runtime Host — UI Verification', () => { waitUntil: 'domcontentloaded', timeout: 45_000, }); - await page.waitForTimeout(3_000); + await waitForAngular(page); const heading = page.getByRole('heading', { name: /runtime host/i }); await expect(heading).toBeVisible({ timeout: 5_000 }); diff --git a/src/Web/StellaOps.Web/tests/e2e/integrations/ui-crud-operations.e2e.spec.ts b/src/Web/StellaOps.Web/tests/e2e/integrations/ui-crud-operations.e2e.spec.ts index 669a0079b..50d4a8cf6 100644 --- a/src/Web/StellaOps.Web/tests/e2e/integrations/ui-crud-operations.e2e.spec.ts +++ b/src/Web/StellaOps.Web/tests/e2e/integrations/ui-crud-operations.e2e.spec.ts @@ -18,6 +18,7 @@ import { createIntegrationViaApi, cleanupIntegrations, snap, + waitForAngular, } from './helpers'; const BASE = process.env['PLAYWRIGHT_BASE_URL'] || 'https://stella-ops.local'; @@ -48,7 +49,7 @@ test.describe('UI CRUD — Search and Filter', () => { waitUntil: 'domcontentloaded', timeout: 45_000, }); - await page.waitForTimeout(3_000); + await waitForAngular(page); // Find the search input const searchInput = page.locator('input[aria-label*="Search"], input[placeholder*="Search"]').first(); @@ -80,7 +81,7 @@ test.describe('UI CRUD — Search and Filter', () => { waitUntil: 'domcontentloaded', timeout: 45_000, }); - await page.waitForTimeout(3_000); + await waitForAngular(page); const searchInput = page.locator('input[aria-label*="Search"], input[placeholder*="Search"]').first(); await expect(searchInput).toBeVisible({ timeout: 5_000 }); @@ -130,7 +131,7 @@ test.describe('UI CRUD — Sorting', () => { waitUntil: 'domcontentloaded', timeout: 45_000, }); - await page.waitForTimeout(3_000); + await waitForAngular(page); // Find a sortable column header (Name is typically first) const nameHeader = page.locator('th:has-text("Name"), th:has-text("name")').first(); @@ -171,7 +172,7 @@ test.describe('UI CRUD — Delete', () => { waitUntil: 'domcontentloaded', timeout: 45_000, }); - await page.waitForTimeout(3_000); + await waitForAngular(page); const deleteBtn = page.locator('button:has-text("Delete"), button[aria-label*="delete" i]').first(); if (await deleteBtn.isVisible({ timeout: 3_000 }).catch(() => false)) { diff --git a/src/Web/StellaOps.Web/tests/e2e/integrations/ui-integration-detail.e2e.spec.ts b/src/Web/StellaOps.Web/tests/e2e/integrations/ui-integration-detail.e2e.spec.ts index f96b1c6c2..f18a1d9b6 100644 --- a/src/Web/StellaOps.Web/tests/e2e/integrations/ui-integration-detail.e2e.spec.ts +++ b/src/Web/StellaOps.Web/tests/e2e/integrations/ui-integration-detail.e2e.spec.ts @@ -18,6 +18,7 @@ import { createIntegrationViaApi, cleanupIntegrations, snap, + waitForAngular, } from './helpers'; const BASE = process.env['PLAYWRIGHT_BASE_URL'] || 'https://stella-ops.local'; @@ -43,7 +44,7 @@ test.describe('UI Integration Detail — Harbor', () => { waitUntil: 'domcontentloaded', timeout: 45_000, }); - await page.waitForTimeout(3_000); + await waitForAngular(page); const pageContent = await page.textContent('body'); expect(pageContent).toContain('Harbor'); @@ -57,7 +58,7 @@ test.describe('UI Integration Detail — Harbor', () => { waitUntil: 'domcontentloaded', timeout: 45_000, }); - await page.waitForTimeout(3_000); + await waitForAngular(page); // Should display provider, type, endpoint info const pageContent = await page.textContent('body'); @@ -74,7 +75,7 @@ test.describe('UI Integration Detail — Harbor', () => { waitUntil: 'domcontentloaded', timeout: 45_000, }); - await page.waitForTimeout(3_000); + await waitForAngular(page); // StellaPageTabsComponent renders buttons with role="tab" and aria-selected // Tab labels from HUB_DETAIL_TABS: Overview, Credentials, Scopes & Rules, Events, Health, Config Audit @@ -99,7 +100,7 @@ test.describe('UI Integration Detail — Harbor', () => { waitUntil: 'domcontentloaded', timeout: 45_000, }); - await page.waitForTimeout(3_000); + await waitForAngular(page); // Click Health tab const healthTab = page.getByRole('tab', { name: /health/i }); diff --git a/src/Web/StellaOps.Web/tests/e2e/integrations/ui-onboarding-wizard.e2e.spec.ts b/src/Web/StellaOps.Web/tests/e2e/integrations/ui-onboarding-wizard.e2e.spec.ts index a0b2b946b..9adede3a3 100644 --- a/src/Web/StellaOps.Web/tests/e2e/integrations/ui-onboarding-wizard.e2e.spec.ts +++ b/src/Web/StellaOps.Web/tests/e2e/integrations/ui-onboarding-wizard.e2e.spec.ts @@ -15,7 +15,7 @@ */ import { test, expect } from './live-auth.fixture'; -import { cleanupIntegrations, snap } from './helpers'; +import { cleanupIntegrations, snap, waitForAngular } from './helpers'; const BASE = process.env['PLAYWRIGHT_BASE_URL'] || 'https://stella-ops.local'; const runId = process.env['E2E_RUN_ID'] || 'run1'; @@ -32,7 +32,7 @@ test.describe('UI Onboarding Wizard — Registry', () => { waitUntil: 'domcontentloaded', timeout: 45_000, }); - await page.waitForTimeout(3_000); + await waitForAngular(page); // Should show the provider catalog or wizard const pageContent = await page.textContent('body'); @@ -51,7 +51,7 @@ test.describe('UI Onboarding Wizard — Registry', () => { waitUntil: 'domcontentloaded', timeout: 45_000, }); - await page.waitForTimeout(3_000); + await waitForAngular(page); // Look for Harbor option (could be button, pill, or card) const harborOption = page.locator('text=Harbor').first(); @@ -68,7 +68,7 @@ test.describe('UI Onboarding Wizard — Registry', () => { waitUntil: 'domcontentloaded', timeout: 45_000, }); - await page.waitForTimeout(3_000); + await waitForAngular(page); // Select Harbor first const harborOption = page.locator('text=Harbor').first(); @@ -117,7 +117,7 @@ test.describe('UI Onboarding Wizard — SCM', () => { waitUntil: 'domcontentloaded', timeout: 45_000, }); - await page.waitForTimeout(3_000); + await waitForAngular(page); const pageContent = await page.textContent('body'); const hasScmContent = @@ -142,7 +142,7 @@ test.describe('UI Onboarding Wizard — CI/CD', () => { waitUntil: 'domcontentloaded', timeout: 45_000, }); - await page.waitForTimeout(3_000); + await waitForAngular(page); const pageContent = await page.textContent('body'); const hasCiContent = diff --git a/src/Web/StellaOps.Web/tests/e2e/integrations/vault-consul-secrets.e2e.spec.ts b/src/Web/StellaOps.Web/tests/e2e/integrations/vault-consul-secrets.e2e.spec.ts index 7a9736bf1..189534d18 100644 --- a/src/Web/StellaOps.Web/tests/e2e/integrations/vault-consul-secrets.e2e.spec.ts +++ b/src/Web/StellaOps.Web/tests/e2e/integrations/vault-consul-secrets.e2e.spec.ts @@ -20,6 +20,7 @@ import { createIntegrationViaApi, cleanupIntegrations, snap, + waitForAngular, } from './helpers'; const BASE = process.env['PLAYWRIGHT_BASE_URL'] || 'https://stella-ops.local'; @@ -172,7 +173,7 @@ test.describe('Secrets Integration — UI Verification', () => { createdIds.push(vaultId, consulId); await page.goto(`${BASE}/setup/integrations/secrets`, { waitUntil: 'domcontentloaded', timeout: 45_000 }); - await page.waitForTimeout(3_000); + await waitForAngular(page); // Verify the page loaded with the correct heading const heading = page.getByRole('heading', { name: /secrets/i }); @@ -193,7 +194,7 @@ test.describe('Secrets Integration — UI Verification', () => { waitUntil: 'domcontentloaded', timeout: 45_000, }); - await page.waitForTimeout(3_000); + await waitForAngular(page); // Verify detail page loaded — should show integration name const pageContent = await page.textContent('body');