diff --git a/e2e/fixtures/supersync.fixture.ts b/e2e/fixtures/supersync.fixture.ts index ad7013f1bc..1c91d4dbff 100644 --- a/e2e/fixtures/supersync.fixture.ts +++ b/e2e/fixtures/supersync.fixture.ts @@ -47,9 +47,18 @@ export const test = base.extend({ * Also checks server health and skips test if server unavailable. */ testRunId: async ({}, use, testInfo) => { - // Check server health once per worker + // Check server health once per worker. + // Prefer the result pre-computed in globalSetup (stored as an env var before workers + // were forked) to avoid a stampede of concurrent HTTP requests from all workers + // simultaneously — which can overload the server and produce false negatives. if (serverHealthyCache === null) { - serverHealthyCache = await isServerHealthy(); + if (process.env.SUPERSYNC_SERVER_HEALTHY !== undefined) { + serverHealthyCache = process.env.SUPERSYNC_SERVER_HEALTHY === 'true'; + } else { + // Fallback for when tests are run directly without going through globalSetup + // (e.g., `npx playwright test --config ... e2e/tests/sync/foo.spec.ts`) + serverHealthyCache = await isServerHealthy(); + } if (!serverHealthyCache) { console.warn( 'SuperSync server not healthy at http://localhost:1901 - skipping tests', @@ -69,9 +78,13 @@ export const test = base.extend({ * Tests are automatically skipped if the server is not healthy. */ serverHealthy: async ({}, use, testInfo) => { - // Only check once per worker + // Only check once per worker — use globalSetup env var if available (see testRunId above) if (serverHealthyCache === null) { - serverHealthyCache = await isServerHealthy(); + if (process.env.SUPERSYNC_SERVER_HEALTHY !== undefined) { + serverHealthyCache = process.env.SUPERSYNC_SERVER_HEALTHY === 'true'; + } else { + serverHealthyCache = await isServerHealthy(); + } if (!serverHealthyCache) { console.warn( 'SuperSync server not healthy at http://localhost:1901 - skipping tests', diff --git a/e2e/global-setup.ts b/e2e/global-setup.ts index 9217ded795..b390d4cb6e 100644 --- a/e2e/global-setup.ts +++ b/e2e/global-setup.ts @@ -2,6 +2,7 @@ import { FullConfig } from '@playwright/test'; import { execSync } from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; +import { isServerHealthy } from './utils/supersync-helpers'; /** * Warm up the dev server by fetching the app once before any tests start. @@ -69,6 +70,20 @@ const globalSetup = async (config: FullConfig): Promise => { config.projects[0]?.use?.baseURL || 'http://localhost:4242'; await warmUpDevServer(baseURL); + + // Check SuperSync server health ONCE here, before workers start. + // Without this, each worker checks independently on startup — with many workers + // running simultaneously, the concurrent health-check requests can overload the + // supersync server and cause false negatives, making workers skip all their tests. + // By storing the result in an env var set before workers are forked, every worker + // reads the cached result instantly instead of making HTTP requests. + const healthy = await isServerHealthy().catch(() => false); + process.env.SUPERSYNC_SERVER_HEALTHY = healthy ? 'true' : 'false'; + if (healthy) { + console.log('SuperSync server healthy — supersync tests will run'); + } else { + console.log('SuperSync server not available — supersync tests will be skipped'); + } }; export default globalSetup; diff --git a/e2e/utils/supersync-helpers.ts b/e2e/utils/supersync-helpers.ts index 5d7ffd404f..af68ed6797 100644 --- a/e2e/utils/supersync-helpers.ts +++ b/e2e/utils/supersync-helpers.ts @@ -239,8 +239,34 @@ export const createSimulatedClient = async ( } }); - // Navigate to app and wait for ready - await page.goto('/'); + // Navigate to app with retry for transient ERR_CONNECTION_REFUSED. + // Under parallel load (many workers × 2-3 browser contexts each), the Angular + // dev server can temporarily refuse connections. Retrying recovers from this + // without failing the test outright. + let lastGotoError: Error | null = null; + for (let attempt = 0; attempt < 3; attempt++) { + try { + await page.goto('/'); + lastGotoError = null; + break; + } catch (e) { + lastGotoError = e as Error; + const isConnectionRefused = lastGotoError.message.includes( + 'ERR_CONNECTION_REFUSED', + ); + if (attempt < 2 && isConnectionRefused) { + const delay = 1000 * (attempt + 1); + console.log( + `[Client ${clientName}] page.goto('/') failed (attempt ${attempt + 1}/3): ERR_CONNECTION_REFUSED — retrying in ${delay}ms`, + ); + await new Promise((r) => setTimeout(r, delay)); + } else { + break; // Non-connection error or last attempt — let it throw below + } + } + } + if (lastGotoError) throw lastGotoError; + await waitForAppReady(page); const workView = new WorkViewPage(page, `${clientName}-${testPrefix}`);