test(e2e): fix supersync test flakiness from parallel worker overload

Two sources of flakiness when running the full supersync suite:

1. Health check stampede: with many workers starting simultaneously,
   all workers called isServerHealthy() concurrently (up to 24 HTTP
   requests at once). This overloaded the server, causing false
   negatives — workers got serverHealthyCache=false and skipped all
   their tests (up to 192/198 tests). Fix: run the health check once
   in globalSetup before workers are forked and store the result in
   process.env.SUPERSYNC_SERVER_HEALTHY; workers read the env var
   instead of making HTTP requests.

2. ERR_CONNECTION_REFUSED in createSimulatedClient: each supersync
   test creates 2-3 browser contexts. Under parallel load the Angular
   dev server temporarily refuses connections. Fix: retry page.goto('/')
   up to 3 times with 1-2s backoff on ERR_CONNECTION_REFUSED.
This commit is contained in:
Johannes Millan 2026-04-15 19:23:06 +02:00
parent af7c7687e2
commit 8865dc0a50
3 changed files with 60 additions and 6 deletions

View file

@ -47,9 +47,18 @@ export const test = base.extend<SuperSyncFixtures>({
* 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<SuperSyncFixtures>({
* 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',

View file

@ -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<void> => {
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;

View file

@ -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}`);