From 744637c7c693e23e4d48c440cddbe073fba47c48 Mon Sep 17 00:00:00 2001 From: master <> Date: Thu, 2 Apr 2026 20:31:34 +0300 Subject: [PATCH] Replace fixed waits with waitForAngular in UI tests The 3s waitForTimeout after page.goto wasn't enough for Angular to bootstrap and render content. Replace with waitForAngular() helper that waits for actual DOM elements (nav, headings) up to 15s, with 5s fallback. 32 calls updated across 10 test files. Also adds waitForAngular to helpers.ts export. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../integrations/aaa-advisory-sync.e2e.spec.ts | 6 +++--- .../integrations/error-resilience.e2e.spec.ts | 5 +++-- .../integrations/gitlab-integration.e2e.spec.ts | 3 ++- .../tests/e2e/integrations/helpers.ts | 16 ++++++++++++++++ .../e2e/integrations/integrations.e2e.spec.ts | 3 ++- .../e2e/integrations/runtime-hosts.e2e.spec.ts | 3 ++- .../integrations/ui-crud-operations.e2e.spec.ts | 9 +++++---- .../ui-integration-detail.e2e.spec.ts | 9 +++++---- .../ui-onboarding-wizard.e2e.spec.ts | 12 ++++++------ .../vault-consul-secrets.e2e.spec.ts | 5 +++-- 10 files changed, 47 insertions(+), 24 deletions(-) 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');