Upgrade waitForAngular to wait for route content, fix remaining UI tests

The generic waitForAngular matched the sidebar nav immediately but
route content (tables, tabs, forms) hadn't rendered yet.

Updated waitForAngular selector to wait for route-level elements:
stella-page-tabs, .integration-list, .source-catalog, table tbody tr,
h1, [role=tablist], .detail-grid, .wizard-step, form.

Also fixed activity-timeline and pagination tests (still had
waitForTimeout(2_000) instead of waitForAngular).

Increased fallback timeout from 5s to 8s for slow-loading pages.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
master
2026-04-02 21:45:40 +03:00
parent 744637c7c6
commit ae64042759
4 changed files with 44 additions and 27 deletions

View File

@@ -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();

View File

@@ -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<void> {
export async function waitForAngular(page: Page, timeoutMs = 20_000): Promise<void> {
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);
}
}

View File

@@ -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'];

View File

@@ -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');