test: Wait for backend readiness before the Playwright database reset (#38366)

Co-authored-by: n8n-cat-bot[bot] <n8n-cat-bot[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Declan Carroll <declan@n8n.io>
This commit is contained in:
n8n-cat-bot[bot] 2026-09-10 16:48:50 +00:00 committed by GitHub
parent 1abdc9222b
commit ec267acdbc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 74 additions and 18 deletions

View File

@ -73,6 +73,7 @@ describe('useWorkflowPublicationStatusSync', () => {
settingsStore = mockedStore(useSettingsStore);
workflowDocumentStore = useWorkflowDocumentStore(TEST_DOCUMENT_ID);
workflowsStore.isNewWorkflow = false;
settingsStore.isWorkflowPublicationServiceEnabled = true;
vi.spyOn(workflowsStore, 'fetchPublicationStatus').mockResolvedValue(makeStatus('published'));

View File

@ -41,7 +41,12 @@ export function useWorkflowPublicationStatusSync(documentId: MaybeRefOrGetter<Wo
}
async function refetch() {
if (disposed || !settingsStore.isWorkflowPublicationServiceEnabled) return;
if (
disposed ||
!settingsStore.isWorkflowPublicationServiceEnabled ||
workflowsStore.isNewWorkflow
)
return;
// Resolve the store from the current documentId on every call so a
// workflow switch is immediately reflected without remounting.

View File

@ -137,9 +137,9 @@ situations where `test:local`'s defaults aren't enough:
`@db:reset` tests are picked up by the local `e2e` project. Their fixtures
are responsible for detecting the missing container and skipping or falling
back.
- **Self-managed n8n.** Boots n8n with a real readiness check against
`/rest/e2e/reset` (Playwright's default `webServer` favicon check is racy
with slower module startups) and skips Playwright's own webServer.
- **Self-managed n8n.** Boots n8n with a readiness check against
`/rest/e2e/reset`, so the run waits for the E2E controller itself, and skips
Playwright's own webServer.
Pass extra n8n env via `N8N_TEST_ENV` (the same convention `test:local` uses):

View File

@ -8,7 +8,7 @@ import path from 'path';
import currentsConfig from './currents.config';
import { getProjects } from './playwright-projects';
import { getBackendUrl, getFrontendUrl, getPortFromUrl } from './utils/url-helper';
import { getBackendUrl, getFrontendUrl, getPortFromUrl, getReadinessUrl } from './utils/url-helper';
const IS_CI = !!process.env.CI;
const IS_DEV = !!process.env.N8N_EDITOR_URL;
@ -68,10 +68,19 @@ if (BACKEND_URL && !SKIP_WEB_SERVER) {
process.env.N8N_TEST_USER_FOLDER =
typeof envUserFolder === 'string' ? envUserFolder : USER_FOLDER;
}
// Probe readiness, not a static asset: the startup middleware answers assets
// with 200 while the migrations still run, so global setup could reset the
// database before n8n registered the E2E controller.
const envHealthEndpoint: unknown =
getTestEnv().N8N_ENDPOINT_HEALTH ?? process.env.N8N_ENDPOINT_HEALTH;
webServer.push({
command: 'pnpm --dir ../../.. start',
url: `${BACKEND_URL}/favicon.ico`,
timeout: 30000,
url: getReadinessUrl(
BACKEND_URL,
typeof envHealthEndpoint === 'string' ? envHealthEndpoint : undefined,
),
// Readiness lands later than a bound port: allow for the migrations too.
timeout: 120000,
reuseExistingServer: IS_DEV ? false : true,
env: {
DB_SQLITE_POOL_SIZE: '40',

View File

@ -11,10 +11,10 @@
* - **Throwaway `N8N_USER_FOLDER`** under the OS temp dir (cleaned up on
* exit). n8n creates `.n8n/` (sqlite DB, encryption key) inside it, fully
* isolated from your local `~/.n8n` install.
* - **Self-managed n8n process** with a real readiness check against
* `/rest/e2e/reset` (Playwright's default `webServer` favicon check is
* racy with slower module startups). Sets `PLAYWRIGHT_SKIP_WEBSERVER=true`
* so Playwright doesn't race to spawn its own n8n.
* - **Self-managed n8n process** with a readiness check against
* `/rest/e2e/reset`, so the run waits for the E2E controller itself. Sets
* `PLAYWRIGHT_SKIP_WEBSERVER=true` so Playwright doesn't race to spawn its
* own n8n.
* - **Container-only tests included.** Sets `PLAYWRIGHT_ALLOW_CONTAINER_ONLY=true`
* so `@capability:*`, `@licensed`, and `@db:reset` tests are picked up by
* the local `e2e` project. Their fixtures are responsible for detecting
@ -157,8 +157,8 @@ n8n.on('exit', (code, signal) => {
}
});
// Poll an actual REST route, not just /favicon.ico, so we know controllers are
// registered. We POST `/rest/e2e/reset` with no body — a registered route
// Poll the actual REST route, not just a health endpoint, so we know controllers
// are registered. We POST `/rest/e2e/reset` with no body — a registered route
// returns 4xx/5xx, an unregistered one returns 404 with an HTML body.
async function waitForN8n(timeoutMs = 120_000) {
const deadline = Date.now() + timeoutMs;

View File

@ -9,7 +9,7 @@ import { expect, test } from '../../fixtures/base';
* USER_FOLDER in playwright.config.ts), not in the default ~/.n8n. Guards the
* smoke suite against silently polluting the developer's own install.
*
* Playwright polls the backend's favicon and global-setup resets its database
* Playwright waits for backend readiness and global-setup resets its database
* before any test runs, so the file exists by now without visiting the app.
*/
test.describe(

View File

@ -100,10 +100,10 @@ From the isolated runner you get:
`~/.n8n/database.sqlite` is never touched.
- **`PLAYWRIGHT_ALLOW_CONTAINER_ONLY=true`** so the `@capability:proxy` tag
is honoured locally.
- **Self-managed n8n** with a real `/rest/e2e/reset` readiness check (not the
racy default favicon poll), `PLAYWRIGHT_SKIP_WEBSERVER=true` to stop
Playwright from spawning a duplicate, and process-group cleanup so
`node ./n8n` doesn't get orphaned.
- **Self-managed n8n** with a `/rest/e2e/reset` readiness check that waits for
the E2E controller, `PLAYWRIGHT_SKIP_WEBSERVER=true` to stop Playwright from
spawning a duplicate, and process-group cleanup so `node ./n8n` doesn't get
orphaned.
The `instanceAiProxySetup` fixture (`fixtures.ts`) detects the missing
`n8nContainer` and short-circuits all proxy + tool-trace setup, so every LLM

View File

@ -0,0 +1,29 @@
import { describe, expect, test } from 'vitest';
import { getReadinessUrl } from './url-helper';
describe('getReadinessUrl', () => {
test('uses the default health endpoint', () => {
expect(getReadinessUrl('http://localhost:5678')).toBe(
'http://localhost:5678/healthz/readiness',
);
});
test('uses a custom health endpoint', () => {
expect(getReadinessUrl('http://localhost:5678', '/health')).toBe(
'http://localhost:5678/health/readiness',
);
});
test('adds the leading slash a custom health endpoint may omit', () => {
expect(getReadinessUrl('http://localhost:5678', 'health')).toBe(
'http://localhost:5678/health/readiness',
);
});
test('drops a trailing slash of the base URL', () => {
expect(getReadinessUrl('http://localhost:5678/')).toBe(
'http://localhost:5678/healthz/readiness',
);
});
});

View File

@ -22,3 +22,14 @@ export function getBackendUrl(): string | undefined {
export function getFrontendUrl(): string | undefined {
return process.env.N8N_EDITOR_URL ?? process.env.N8N_BASE_URL;
}
/**
* Build the readiness URL of a backend.
* The backend serves readiness at `<N8N_ENDPOINT_HEALTH>/readiness` and adds a
* missing leading slash to that variable, so mirror both rules here.
*/
export function getReadinessUrl(baseUrl: string, healthEndpoint?: string): string {
const health = healthEndpoint ?? '/healthz';
const healthPath = health.startsWith('/') ? health : `/${health}`;
return `${baseUrl.replace(/\/+$/, '')}${healthPath}/readiness`;
}

View File

@ -8,6 +8,7 @@ export default defineConfig({
'scripts/**/*.test.ts',
'fixtures/**/*.test.ts',
'reporters/**/*.test.ts',
'utils/**/*.test.ts',
'tests/framework/telemetry.test.ts',
'tests/framework/telemetry-integration.test.ts',
'*.test.ts',