mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-12 08:00:27 +02:00
test: Add Playwright smoke spec for Vite dev-server boot (#29539)
This commit is contained in:
parent
701f9a4627
commit
d4e9705749
25
.github/workflows/ci-pull-requests.yml
vendored
25
.github/workflows/ci-pull-requests.yml
vendored
|
|
@ -22,6 +22,7 @@ jobs:
|
|||
ci: ${{ fromJSON(steps.ci-filter.outputs.results).ci == true }}
|
||||
unit: ${{ fromJSON(steps.ci-filter.outputs.results).unit == true }}
|
||||
e2e: ${{ fromJSON(steps.ci-filter.outputs.results).e2e == true }}
|
||||
dev_server_smoke: ${{ fromJSON(steps.ci-filter.outputs.results)['dev-server-smoke'] == true }}
|
||||
workflows: ${{ fromJSON(steps.ci-filter.outputs.results).workflows == true }}
|
||||
workflow_scripts: ${{ fromJSON(steps.ci-filter.outputs.results)['workflow-scripts'] == true }}
|
||||
db: ${{ fromJSON(steps.ci-filter.outputs.results).db == true }}
|
||||
|
|
@ -63,6 +64,16 @@ jobs:
|
|||
.github/actions/load-n8n-docker/**
|
||||
packages/testing/playwright/**
|
||||
packages/testing/containers/**
|
||||
dev-server-smoke:
|
||||
packages/frontend/editor-ui/vite.config.mts
|
||||
pnpm-workspace.yaml
|
||||
packages/@n8n/*/package.json
|
||||
packages/testing/playwright/tests/dev-server-smoke/**
|
||||
packages/testing/playwright/playwright.config.ts
|
||||
packages/testing/playwright/playwright-projects.ts
|
||||
packages/testing/playwright/package.json
|
||||
.github/workflows/test-dev-server-smoke-reusable.yml
|
||||
.github/workflows/ci-pull-requests.yml
|
||||
workflows: .github/**
|
||||
workflow-scripts: .github/scripts/**
|
||||
performance:
|
||||
|
|
@ -221,6 +232,19 @@ jobs:
|
|||
upload-failure-artifacts: ${{ github.event.pull_request.head.repo.fork == true }}
|
||||
secrets: inherit
|
||||
|
||||
# Boots the editor-ui against the Vite dev server and fails on any console
|
||||
# or page error during load. Catches regressions in dev-mode module
|
||||
# resolution (missing Vite alias, broken workspace package interop) that
|
||||
# the production-bundle e2e job bundles around.
|
||||
dev-server-smoke:
|
||||
name: Dev-server boot smoke
|
||||
needs: install-and-build
|
||||
if: needs.install-and-build.outputs.dev_server_smoke == 'true' && github.event_name != 'merge_group'
|
||||
uses: ./.github/workflows/test-dev-server-smoke-reusable.yml
|
||||
with:
|
||||
ref: ${{ needs.install-and-build.outputs.commit_sha }}
|
||||
secrets: inherit
|
||||
|
||||
db-tests:
|
||||
name: DB Tests
|
||||
needs: install-and-build
|
||||
|
|
@ -296,6 +320,7 @@ jobs:
|
|||
check-packaging,
|
||||
sqlite-sanity,
|
||||
e2e,
|
||||
dev-server-smoke,
|
||||
db-tests,
|
||||
performance,
|
||||
security-checks,
|
||||
|
|
|
|||
49
.github/workflows/test-dev-server-smoke-reusable.yml
vendored
Normal file
49
.github/workflows/test-dev-server-smoke-reusable.yml
vendored
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
name: 'Test: Dev-server boot smoke'
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
ref:
|
||||
description: 'Git ref to test'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: '--max-old-space-size=6144'
|
||||
PLAYWRIGHT_BROWSERS_PATH: packages/testing/playwright/.playwright-browsers
|
||||
|
||||
jobs:
|
||||
smoke:
|
||||
name: Dev-server smoke
|
||||
runs-on: ${{ vars.RUNNER_PROVIDER == 'github' && 'ubuntu-latest' || 'blacksmith-4vcpu-ubuntu-2204' }}
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 1
|
||||
ref: ${{ inputs.ref }}
|
||||
|
||||
- name: Setup and Build
|
||||
uses: ./.github/actions/setup-nodejs
|
||||
|
||||
- name: Install Browsers
|
||||
run: pnpm turbo run install-browsers --filter=n8n-playwright
|
||||
|
||||
- name: Run dev-server smoke spec
|
||||
# Run from repo root so PLAYWRIGHT_BROWSERS_PATH (relative) resolves
|
||||
# correctly. cd-ing into the playwright package double-nests it.
|
||||
run: pnpm --filter=n8n-playwright test:dev-server-smoke --reporter=list
|
||||
|
||||
- name: Upload Failure Artifacts
|
||||
if: ${{ failure() }}
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: dev-server-smoke-report
|
||||
path: |
|
||||
packages/testing/playwright/test-results/
|
||||
packages/testing/playwright/playwright-report/
|
||||
retention-days: 7
|
||||
|
|
@ -7,12 +7,18 @@ function globalTeardown() {
|
|||
|
||||
for (const port of ports) {
|
||||
try {
|
||||
// Find process ID using the port
|
||||
const pid = execSync(`lsof -ti :${port}`, { encoding: 'utf-8' }).trim();
|
||||
// `lsof -ti` returns one PID per line. Dev-mode n8n holds the port
|
||||
// from multiple PIDs (parent + worker), so split and space-join
|
||||
// before passing to `kill` — otherwise the second PID lands on its
|
||||
// own shell line and gets executed as a command.
|
||||
const pids = execSync(`lsof -ti :${port}`, { encoding: 'utf-8' })
|
||||
.trim()
|
||||
.split('\n')
|
||||
.filter(Boolean);
|
||||
|
||||
if (pid) {
|
||||
console.log(`- Killing process ${pid} on port ${port}`);
|
||||
execSync(`kill -9 ${pid}`);
|
||||
if (pids.length > 0) {
|
||||
console.log(`- Killing process(es) ${pids.join(', ')} on port ${port}`);
|
||||
execSync(`kill -9 ${pids.join(' ')}`);
|
||||
}
|
||||
} catch (error) {
|
||||
// lsof returns non-zero exit code if no process is found
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "N8N_BASE_URL=http://localhost:5678 N8N_EDITOR_URL=http://localhost:8080 RESET_E2E_DB=true playwright test --project=e2e",
|
||||
"test:dev-server-smoke": "N8N_BASE_URL=http://localhost:5678 N8N_EDITOR_URL=http://localhost:8080 RESET_E2E_DB=true playwright test --project=dev-server-smoke",
|
||||
"test:all": "playwright test",
|
||||
"test:local": "N8N_BASE_URL=http://localhost:5680 RESET_E2E_DB=true playwright test --project=e2e",
|
||||
"test:local:isolated": "node scripts/run-local-isolated.mjs",
|
||||
|
|
|
|||
|
|
@ -172,6 +172,15 @@ export function getProjects(): Project[] {
|
|||
fullyParallel: true,
|
||||
use: { baseURL: getFrontendUrl() },
|
||||
});
|
||||
projects.push({
|
||||
name: 'dev-server-smoke',
|
||||
testDir: './tests/dev-server-smoke',
|
||||
fullyParallel: false,
|
||||
// Vite dev cold-start can take 15-25s on the first navigation while it
|
||||
// pre-bundles deps. Subsequent navigations are sub-second.
|
||||
timeout: 90_000,
|
||||
use: { baseURL: getFrontendUrl(), navigationTimeout: 30_000 },
|
||||
});
|
||||
} else {
|
||||
for (const { name, config } of CONTAINER_CONFIGS) {
|
||||
projects.push(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,112 @@
|
|||
import type { ConsoleMessage, Page } from '@playwright/test';
|
||||
|
||||
import { test } from '../../fixtures/base';
|
||||
|
||||
/**
|
||||
* Smoke tests that catch app-wide regressions where the editor-ui fails to boot —
|
||||
* for example a workspace package whose import is silently broken in dev mode
|
||||
* (missing Vite alias, stale dist-only re-export, CJS interop failure, etc.).
|
||||
*
|
||||
* These tests visit a small set of representative routes and fail the suite if
|
||||
* any error-level console message or uncaught page error is observed during the
|
||||
* load. They are intentionally light — UI behaviour is covered elsewhere.
|
||||
*
|
||||
* Must run against the Vite dev server (`N8N_EDITOR_URL` set), which is what the
|
||||
* `test:dev-server-smoke` script wires up.
|
||||
*/
|
||||
|
||||
const BENIGN_PATTERNS: Array<{ messageRe: RegExp; reason: string }> = [
|
||||
{
|
||||
messageRe: /\[vite\] (server connection lost|connecting\.\.\.)/,
|
||||
reason: 'HMR transport noise',
|
||||
},
|
||||
];
|
||||
|
||||
const isBenign = (text: string) => BENIGN_PATTERNS.some((p) => p.messageRe.test(text));
|
||||
|
||||
const navigateAndAssertNoErrors = async (
|
||||
page: Page,
|
||||
label: string,
|
||||
navigate: () => Promise<void>,
|
||||
) => {
|
||||
const consoleErrors: string[] = [];
|
||||
const pageErrors: string[] = [];
|
||||
|
||||
const onConsole = (message: ConsoleMessage) => {
|
||||
if (message.type() !== 'error') return;
|
||||
if (isBenign(message.text())) return;
|
||||
consoleErrors.push(`${message.text()} (at ${message.location().url ?? '<unknown>'})`);
|
||||
};
|
||||
const onPageError = (error: Error) => {
|
||||
const firstFrame = error.stack?.split('\n')[1]?.trim() ?? '';
|
||||
pageErrors.push(`${error.name}: ${error.message}\n ${firstFrame}`);
|
||||
};
|
||||
|
||||
page.on('console', onConsole);
|
||||
page.on('pageerror', onPageError);
|
||||
|
||||
// When dev-mode module resolution is broken, the JS app never bootstraps —
|
||||
// so entry points like `fromHome()` time out waiting for the post-redirect
|
||||
// URL. That timeout would mask the real cause (the SyntaxError captured
|
||||
// below). Capture any navigation failure and surface page/console errors
|
||||
// as the primary diagnostic when both happened.
|
||||
let navigationError: Error | undefined;
|
||||
try {
|
||||
await navigate();
|
||||
// `load` (and not `networkidle`) is the project convention; entry points
|
||||
// already wait for landing-zone elements, so by the time `load` fires all
|
||||
// initial module evaluation has completed and any SyntaxError-on-import
|
||||
// has surfaced to either pageerror or console.error.
|
||||
await page.waitForLoadState('load', { timeout: 15_000 });
|
||||
} catch (error) {
|
||||
navigationError = error instanceof Error ? error : new Error(String(error));
|
||||
} finally {
|
||||
page.off('console', onConsole);
|
||||
page.off('pageerror', onPageError);
|
||||
}
|
||||
|
||||
if (pageErrors.length > 0 || consoleErrors.length > 0) {
|
||||
const sections = [
|
||||
pageErrors.length > 0 && `Uncaught page errors:\n ${pageErrors.join('\n ')}`,
|
||||
consoleErrors.length > 0 && `Error-level console messages:\n ${consoleErrors.join('\n ')}`,
|
||||
navigationError &&
|
||||
`Navigation also failed (likely a downstream effect): ${navigationError.message.split('\n')[0]}`,
|
||||
].filter(Boolean);
|
||||
throw new Error(`[${label}] dev-server boot failed.\n\n${sections.join('\n\n')}`);
|
||||
}
|
||||
|
||||
if (navigationError) throw navigationError;
|
||||
};
|
||||
|
||||
test.describe(
|
||||
'Dev-server boot smoke',
|
||||
{
|
||||
annotation: [
|
||||
{
|
||||
type: 'description',
|
||||
description:
|
||||
'Boots representative routes against the Vite dev server and fails on any error-level console message or uncaught page error during load.',
|
||||
},
|
||||
],
|
||||
},
|
||||
() => {
|
||||
test('home page boots cleanly', async ({ n8n }) => {
|
||||
await navigateAndAssertNoErrors(n8n.page, 'home', async () => {
|
||||
await n8n.start.fromHome();
|
||||
});
|
||||
});
|
||||
|
||||
test('blank canvas boots cleanly', async ({ n8n }) => {
|
||||
await navigateAndAssertNoErrors(n8n.page, 'blank-canvas', async () => {
|
||||
await n8n.start.fromBlankCanvas();
|
||||
});
|
||||
});
|
||||
|
||||
test('credentials page boots cleanly', async ({ n8n }) => {
|
||||
await navigateAndAssertNoErrors(n8n.page, 'credentials', async () => {
|
||||
await n8n.start.fromHome();
|
||||
await n8n.navigate.toCredentials();
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
|
@ -54,4 +54,15 @@ cd(dir);
|
|||
const cmd = normalizeCommand(run);
|
||||
|
||||
echo(chalk.cyan(`$ Running (dir: ${dir}) ${cmd} ${args.join(' ')}`));
|
||||
await $({ stdio: 'inherit' })`${cmd} ${args}`;
|
||||
try {
|
||||
await $({ stdio: 'inherit' })`${cmd} ${args}`;
|
||||
} catch (err) {
|
||||
// Forward signal-based exits silently (e.g. Playwright's globalTeardown
|
||||
// SIGKILLs the n8n process, or the user Ctrl-C's `pnpm start`). Without
|
||||
// this, zx logs an unhandled `_ProcessOutput` traceback as the parent
|
||||
// is already shutting us down — pure noise.
|
||||
if (err?.signal || err?.exitCode === 137 || err?.exitCode === 143) {
|
||||
process.exit(err.exitCode ?? 0);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user