diff --git a/src/Web/StellaOps.Web/tests/e2e/integrations/activity-timeline.e2e.spec.ts b/src/Web/StellaOps.Web/tests/e2e/integrations/activity-timeline.e2e.spec.ts index 2ff795294..e0d63ef7b 100644 --- a/src/Web/StellaOps.Web/tests/e2e/integrations/activity-timeline.e2e.spec.ts +++ b/src/Web/StellaOps.Web/tests/e2e/integrations/activity-timeline.e2e.spec.ts @@ -13,7 +13,7 @@ */ import { test, expect } from './live-auth.fixture'; -import { snap } from './helpers'; +import { snap, waitForAngular } from './helpers'; const BASE = process.env['PLAYWRIGHT_BASE_URL'] || 'https://stella-ops.local'; @@ -27,7 +27,7 @@ test.describe('Activity Timeline — Page Load', () => { waitUntil: 'domcontentloaded', timeout: 45_000, }); - await page.waitForTimeout(2_000); + await waitForAngular(page); // Should show activity timeline or related content const pageContent = await page.textContent('body'); @@ -46,7 +46,7 @@ test.describe('Activity Timeline — Page Load', () => { waitUntil: 'domcontentloaded', timeout: 45_000, }); - await page.waitForTimeout(2_000); + await waitForAngular(page); // Look for the timeline container or activity items const timeline = page.locator('.activity-timeline, .activity-list, [class*="activity"]').first(); @@ -70,7 +70,7 @@ test.describe('Activity Timeline — Stats', () => { waitUntil: 'domcontentloaded', timeout: 45_000, }); - await page.waitForTimeout(2_000); + await waitForAngular(page); const content = await page.textContent('body'); @@ -97,7 +97,7 @@ test.describe('Activity Timeline — Items', () => { waitUntil: 'domcontentloaded', timeout: 45_000, }); - await page.waitForTimeout(2_000); + await waitForAngular(page); // Look for individual activity/event items const items = page.locator('.activity-item, .event-item, [class*="activity-item"]'); @@ -125,7 +125,7 @@ test.describe('Activity Timeline — Filters', () => { waitUntil: 'domcontentloaded', timeout: 45_000, }); - await page.waitForTimeout(2_000); + await waitForAngular(page); // Look for filter dropdowns const filterSelect = page.locator('.filter-select, select, [class*="filter"]').first(); @@ -145,7 +145,7 @@ test.describe('Activity Timeline — Filters', () => { waitUntil: 'domcontentloaded', timeout: 45_000, }); - await page.waitForTimeout(2_000); + await waitForAngular(page); // Apply a filter first const filterSelect = page.locator('.filter-select, select').first(); @@ -175,12 +175,12 @@ test.describe('Activity Timeline — Navigation', () => { waitUntil: 'domcontentloaded', timeout: 45_000, }); - await page.waitForTimeout(2_000); + await waitForAngular(page); const backLink = page.locator('.back-link, a:has-text("Back"), a:has-text("Integrations")').first(); if (await backLink.isVisible({ timeout: 3_000 }).catch(() => false)) { await backLink.click(); - await page.waitForTimeout(2_000); + await waitForAngular(page); // Should navigate back to integrations const url = page.url(); diff --git a/src/Web/StellaOps.Web/tests/e2e/integrations/helpers.ts b/src/Web/StellaOps.Web/tests/e2e/integrations/helpers.ts index 9416aa9f8..e81a6e650 100644 --- a/src/Web/StellaOps.Web/tests/e2e/integrations/helpers.ts +++ b/src/Web/StellaOps.Web/tests/e2e/integrations/helpers.ts @@ -6,18 +6,34 @@ import type { APIRequestContext, Page } from '@playwright/test'; const SCREENSHOT_DIR = 'tests/e2e/screenshots/integrations'; /** - * Wait for Angular to bootstrap and render the app shell. + * Wait for Angular to bootstrap and render route content. * Call after page.goto() instead of a fixed waitForTimeout. - * Waits for the sidebar navigation or any heading to appear. + * + * Waits for route-level content (tables, tab bars, forms, headings inside main) + * not just the shell sidebar. Falls back to 8s delay if no element matches. */ -export async function waitForAngular(page: Page, timeoutMs = 15_000): Promise { +export async function waitForAngular(page: Page, timeoutMs = 20_000): Promise { try { - await page.waitForSelector('nav[aria-label], h1, h2, .stella-page, [class*="dashboard"]', { - timeout: timeoutMs, - }); + // Wait for content that only appears AFTER the route component renders. + // The shell sidebar (nav) loads first, but we need the route content. + await page.waitForSelector( + [ + 'stella-page-tabs', // Integration hub tabs + '.integration-list', // Integration list table + '.source-catalog', // Advisory source catalog + '.activity-timeline', // Activity timeline + 'table tbody tr', // Any data table with rows + '.detail-grid', // Detail page grid + '.wizard-step', // Onboarding wizard + 'form', // Any form + 'h1', // Page heading (rendered by route component) + '[role="tablist"]', // Tab bar (integration shell) + ].join(', '), + { timeout: timeoutMs }, + ); } catch { - // If no known element appears, fall back to a delay - await page.waitForTimeout(5_000); + // Last resort: wait 8s for slow pages + await page.waitForTimeout(8_000); } } 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 1071732ce..222925919 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 @@ -635,7 +635,7 @@ test.describe('Integration Services — UI Verification', () => { test('Registries tab lists registry integrations', async ({ liveAuthPage: page }) => { await page.goto(`${BASE}/setup/integrations/registries`, { waitUntil: 'domcontentloaded', timeout: 45_000 }); - await page.waitForTimeout(2_000); + await waitForAngular(page); const heading = page.getByRole('heading', { name: /registry/i }); await expect(heading).toBeVisible({ timeout: 5_000 }); @@ -650,7 +650,7 @@ test.describe('Integration Services — UI Verification', () => { test('SCM tab lists SCM integrations', async ({ liveAuthPage: page }) => { await page.goto(`${BASE}/setup/integrations/scm`, { waitUntil: 'domcontentloaded', timeout: 45_000 }); - await page.waitForTimeout(2_000); + await waitForAngular(page); const heading = page.getByRole('heading', { name: /scm/i }); await expect(heading).toBeVisible({ timeout: 5_000 }); @@ -664,7 +664,7 @@ test.describe('Integration Services — UI Verification', () => { test('CI/CD tab lists CI/CD integrations', async ({ liveAuthPage: page }) => { await page.goto(`${BASE}/setup/integrations/ci`, { waitUntil: 'domcontentloaded', timeout: 45_000 }); - await page.waitForTimeout(2_000); + await waitForAngular(page); const heading = page.getByRole('heading', { name: /ci\/cd/i }); await expect(heading).toBeVisible({ timeout: 5_000 }); @@ -678,7 +678,7 @@ test.describe('Integration Services — UI Verification', () => { test('tab switching navigates between all tabs', async ({ liveAuthPage: page }) => { await page.goto(`${BASE}/setup/integrations`, { waitUntil: 'domcontentloaded', timeout: 45_000 }); - await page.waitForTimeout(2_000); + await waitForAngular(page); const tabs = ['Registries', 'SCM', 'CI/CD', 'Runtimes / Hosts', 'Advisory & VEX', 'Secrets']; diff --git a/src/Web/StellaOps.Web/tests/e2e/integrations/pagination.e2e.spec.ts b/src/Web/StellaOps.Web/tests/e2e/integrations/pagination.e2e.spec.ts index 57317da61..238fec6db 100644 --- a/src/Web/StellaOps.Web/tests/e2e/integrations/pagination.e2e.spec.ts +++ b/src/Web/StellaOps.Web/tests/e2e/integrations/pagination.e2e.spec.ts @@ -16,6 +16,7 @@ import { test, expect } from './live-auth.fixture'; import { INTEGRATION_CONFIGS, createIntegrationViaApi, + waitForAngular, cleanupIntegrations, snap, } from './helpers'; @@ -131,10 +132,10 @@ test.describe('Pagination — API', () => { test.describe('Pagination — UI Pager', () => { test('pager info renders on registries tab', async ({ liveAuthPage: page }) => { await page.goto(`${BASE}/setup/integrations/registries`, { - waitUntil: 'networkidle', - timeout: 30_000, + waitUntil: 'domcontentloaded', + timeout: 45_000, }); - await page.waitForTimeout(2_000); + await waitForAngular(page); // The pager should show "X total · page Y of Z" const pagerInfo = page.locator('.pager__info'); @@ -151,10 +152,10 @@ test.describe('Pagination — UI Pager', () => { test('pager controls are present', async ({ liveAuthPage: page }) => { await page.goto(`${BASE}/setup/integrations/registries`, { - waitUntil: 'networkidle', - timeout: 30_000, + waitUntil: 'domcontentloaded', + timeout: 45_000, }); - await page.waitForTimeout(2_000); + await waitForAngular(page); // Check for pagination navigation const pager = page.locator('.pager');