Skip to content
Merged
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"dev:be": "turbo run dev --parallel --env-mode=loose --filter=!@n8n/design-system --filter=!@n8n/chat --filter=!@n8n/task-runner --filter=!n8n-editor-ui",
"dev:ai": "turbo run dev --parallel --env-mode=loose --filter=@n8n/nodes-langchain --filter=n8n --filter=n8n-core",
"dev:fe": "run-p start \"dev:fe:editor --filter=@n8n/design-system\"",
"dev:fe:e2e": "run-p start dev:fe:editor",
"dev:fe:editor": "turbo run dev --parallel --env-mode=loose --filter=n8n-editor-ui",
"clean": "turbo run clean",
"reset": "node scripts/ensure-zx.mjs && zx scripts/reset.mjs",
Expand Down
55 changes: 55 additions & 0 deletions packages/testing/playwright/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,62 @@ pnpm test:local # Starts a local server and runs the UI tes
N8N_BASE_URL=localhost:5068 pnpm test:local # Runs the UI tests against the instance running
```

## Separate Backend and Frontend URLs

When developing with separate backend and frontend servers (e.g., backend on port 5680, frontend on port 8080), you can use the following environment variables:

```bash
# Development mode with separate backend and frontend
N8N_BASE_URL=http://localhost:5680 N8N_EDITOR_URL=http://localhost:8080 pnpm test:local

# Or use the convenience command
pnpm test:dev # Automatically sets N8N_EDITOR_URL=http://localhost:8080
```

### Environment Variables

- **`N8N_BASE_URL`**: Backend server URL (also used as frontend URL if `N8N_EDITOR_URL` is not set)
- **`N8N_EDITOR_URL`**: Frontend server URL (when set, overrides frontend URL while backend uses `N8N_BASE_URL`)

**How it works:**
- **Backend URL** (for API calls): Always uses `N8N_BASE_URL`
- **Frontend URL** (for browser navigation): Uses `N8N_EDITOR_URL` if set, otherwise falls back to `N8N_BASE_URL`

This allows you to:
- Test against a backend on port 5680 while the frontend dev server runs on port 8080
- Use different URLs for API calls vs browser navigation
- Maintain backward compatibility with single-URL setups

### Using Backend and Frontend URLs in Tests

Tests automatically receive the correct URLs through fixtures:

```typescript
import { test, expect } from '../fixtures/base';

test('should handle separate backend and frontend URLs', async ({
n8n, // Uses frontendUrl for browser navigation
api, // Uses backendUrl for API calls
baseURL, // Points to frontendUrl (for Playwright's page.goto)
backendUrl, // Direct access to backend URL
frontendUrl // Direct access to frontend URL
}) => {
// Browser navigation uses frontend URL automatically
await n8n.workflows.createNew();

// API calls use backend URL automatically
const workflows = await api.getWorkflows();
expect(workflows.length).toBeGreaterThan(0);

// You can also access the URLs directly if needed
console.log(`Frontend: ${frontendUrl}, Backend: ${backendUrl}`);
});
```

**Note:** When using containers (no environment URLs set), both `backendUrl` and `frontendUrl` point to the same container URL.

## Test Commands

```bash
# By Mode
pnpm test:container:standard # Sqlite
Expand Down
55 changes: 42 additions & 13 deletions packages/testing/playwright/fixtures/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { ApiHelpers } from '../services/api-helper';
import { ProxyServer } from '../services/proxy-server';
import { TestError, type TestRequirements } from '../Types';
import { setupTestRequirements } from '../utils/requirements';
import { getBackendUrl, getFrontendUrl } from '../utils/url-helper';

type TestFixtures = {
n8n: n8nPage;
Expand All @@ -22,6 +23,8 @@ type TestFixtures = {

type WorkerFixtures = {
n8nUrl: string;
backendUrl: string;
frontendUrl: string;
dbSetup: undefined;
chaos: ContainerTestHelpers;
n8nContainer: N8NStack;
Expand Down Expand Up @@ -90,10 +93,10 @@ export const test = base.extend<
{ scope: 'worker', box: true },
],

// Create a new n8n container if N8N_BASE_URL is not set, otherwise use the existing n8n instance
// Create a new n8n container if backend URL is not set, otherwise use the existing n8n instance
n8nContainer: [
async ({ containerConfig }, use) => {
const envBaseURL = process.env.N8N_BASE_URL;
const envBaseURL = getBackendUrl();

if (envBaseURL) {
await use(null as unknown as N8NStack);
Expand All @@ -120,12 +123,32 @@ export const test = base.extend<
{ scope: 'worker' },
],

// Backend URL - used for API calls
// When N8N_BACKEND_URL is set, use it; otherwise fall back to n8nUrl
backendUrl: [
async ({ n8nContainer }, use) => {
const envBackendURL = getBackendUrl() ?? n8nContainer?.baseUrl;
await use(envBackendURL);
},
{ scope: 'worker' },
],

// Frontend URL - used for browser navigation
// When N8N_EDITOR_URL is set (dev mode), use it; otherwise fall back to n8nUrl
frontendUrl: [
async ({ n8nContainer }, use) => {
const envFrontendURL = getFrontendUrl() ?? n8nContainer?.baseUrl;
await use(envFrontendURL);
},
{ scope: 'worker' },
],

// Reset the database for the new container
dbSetup: [
async ({ n8nUrl, n8nContainer }, use) => {
async ({ backendUrl, n8nContainer }, use) => {
if (n8nContainer) {
console.log('Resetting database for new container');
const apiContext = await request.newContext({ baseURL: n8nUrl });
const apiContext = await request.newContext({ baseURL: backendUrl });
const api = new ApiHelpers(apiContext);
await api.resetDatabase();
await apiContext.dispose();
Expand All @@ -138,9 +161,9 @@ export const test = base.extend<
// Create container test helpers for the n8n container.
chaos: [
async ({ n8nContainer }, use) => {
if (process.env.N8N_BASE_URL) {
if (getBackendUrl()) {
throw new TestError(
'Chaos testing is not supported when using N8N_BASE_URL environment variable. Remove N8N_BASE_URL to use containerized testing.',
'Chaos testing is not supported when using an external n8n instance. Remove backend URL environment variables to use containerized testing.',
);
}
const helpers = new ContainerTestHelpers(n8nContainer.containers);
Expand All @@ -149,24 +172,30 @@ export const test = base.extend<
{ scope: 'worker' },
],

baseURL: async ({ n8nUrl, dbSetup }, use) => {
baseURL: async ({ frontendUrl, dbSetup }, use) => {
void dbSetup; // Ensure dbSetup runs first
await use(n8nUrl);
await use(frontendUrl);
},

n8n: async ({ context }, use, testInfo) => {
n8n: async ({ context, backendUrl }, use, testInfo) => {
await setupDefaultInterceptors(context);
const page = await context.newPage();
const n8nInstance = new n8nPage(page);

// Create a separate API context with backend URL for API calls
const apiContext = await request.newContext({ baseURL: backendUrl });
const api = new ApiHelpers(apiContext);

const n8nInstance = new n8nPage(page, api);
await n8nInstance.api.setupFromTags(testInfo.tags);
// Enable project features for the tests, this is used in several tests, but is never disabled in tests, so we can have it on by default
await n8nInstance.start.withProjectFeatures();
await use(n8nInstance);
await apiContext.dispose();
},

// This is a completely isolated API context for tests that don't need the browser
api: async ({ baseURL }, use, testInfo) => {
const context = await request.newContext({ baseURL });
api: async ({ backendUrl }, use, testInfo) => {
const context = await request.newContext({ baseURL: backendUrl });
const api = new ApiHelpers(context);
await api.setupFromTags(testInfo.tags);
await use(api);
Expand All @@ -185,7 +214,7 @@ export const test = base.extend<
// n8nContainer is "null" if running tests in "local" mode
if (!n8nContainer) {
throw new TestError(
'Testing with Proxy server is not supported when using N8N_BASE_URL environment variable. Remove N8N_BASE_URL to use containerized testing.',
'Testing with Proxy server is not supported when using an external n8n instance. Remove backend URL environment variables to use containerized testing.',
);
}

Expand Down
5 changes: 3 additions & 2 deletions packages/testing/playwright/global-setup.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { request } from '@playwright/test';

import { ApiHelpers } from './services/api-helper';
import { getBackendUrl } from './utils/url-helper';

async function globalSetup() {
console.log('🚀 Starting global setup...');

// Check if N8N_BASE_URL is set
const n8nBaseUrl = process.env.N8N_BASE_URL;
// Check if backend URL is set (N8N_BACKEND_URL or N8N_BASE_URL)
const n8nBaseUrl = getBackendUrl();
if (!n8nBaseUrl) {
console.log('⚠️ N8N_BASE_URL environment variable is not set, skipping database reset');
return;
Expand Down
4 changes: 3 additions & 1 deletion packages/testing/playwright/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
"name": "n8n-playwright",
"private": true,
"scripts": {
"dev": "N8N_BASE_URL=http://localhost:5678 N8N_EDITOR_URL=http://localhost:8080 pnpm run test:local:base",
"test:all": "playwright test",
"test:local": "N8N_BASE_URL=http://localhost:5680 RESET_E2E_DB=true playwright test --project=ui --project=ui:isolated",
"test:local:base": "RESET_E2E_DB=true playwright test --project=ui --project=ui:isolated",
"test:local": "N8N_BASE_URL=http://localhost:5680 pnpm run test:local:base",
"test:ui": "playwright test --project=*ui*",
"test:performance": "playwright test --project=performance",
"test:chaos": "playwright test --project='*:chaos'",
Expand Down
4 changes: 2 additions & 2 deletions packages/testing/playwright/pages/n8nPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,9 @@ export class n8nPage {
readonly breadcrumbs: Breadcrumbs;
readonly clipboard: ClipboardHelper;

constructor(page: Page) {
constructor(page: Page, api?: ApiHelpers) {
this.page = page;
this.api = new ApiHelpers(page.context().request);
this.api = api ?? new ApiHelpers(page.context().request);

// Pages
this.aiAssistant = new AIAssistantPage(page);
Expand Down
8 changes: 5 additions & 3 deletions packages/testing/playwright/playwright-projects.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { Project } from '@playwright/test';
import type { N8NConfig } from 'n8n-containers/n8n-test-container-creation';

import { getBackendUrl, getFrontendUrl } from './utils/url-helper';

// Tags that require test containers environment
// These tests won't be run against local
const CONTAINER_ONLY_TAGS = [
Expand Down Expand Up @@ -31,7 +33,7 @@ const CONTAINER_CONFIGS: Array<{ name: string; config: N8NConfig }> = [
];

export function getProjects(): Project[] {
const isLocal = !!process.env.N8N_BASE_URL;
const isLocal = !!getBackendUrl();
const projects: Project[] = [];

if (isLocal) {
Expand All @@ -43,14 +45,14 @@ export function getProjects(): Project[] {
[CONTAINER_ONLY.source, SERIAL_EXECUTION.source, ISOLATED_ONLY.source].join('|'),
),
fullyParallel: true,
use: { baseURL: process.env.N8N_BASE_URL },
use: { baseURL: getFrontendUrl() },
},
{
name: 'ui:isolated',
testDir: './tests/ui',
grep: new RegExp([SERIAL_EXECUTION.source, ISOLATED_ONLY.source].join('|')),
workers: 1,
use: { baseURL: process.env.N8N_BASE_URL },
use: { baseURL: getFrontendUrl() },
},
);
} else {
Expand Down
18 changes: 12 additions & 6 deletions packages/testing/playwright/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ import path from 'path';

import currentsConfig from './currents.config';
import { getProjects } from './playwright-projects';
import { getPortFromUrl } from './utils/url-helper';
import { getBackendUrl, getFrontendUrl, getPortFromUrl } from './utils/url-helper';

const IS_CI = !!process.env.CI;
const IS_DEV = !!process.env.N8N_EDITOR_URL;

const MACBOOK_WINDOW_SIZE = { width: 1536, height: 960 };

Expand All @@ -36,6 +37,11 @@ const LOCAL_WORKERS = Math.min(6, Math.floor(CPU_COUNT / 2));
const CI_WORKERS = CPU_COUNT;
const WORKERS = IS_CI ? CI_WORKERS : LOCAL_WORKERS;

const BACKEND_URL = getBackendUrl();
const FRONTEND_URL = getFrontendUrl();
const START_COMMAND = IS_DEV ? 'pnpm dev:fe:e2e' : 'pnpm start';
const WEB_SERVER_URL = FRONTEND_URL ?? BACKEND_URL;

export default defineConfig<CurrentsFixtures, CurrentsWorkerFixtures>({
globalSetup: './global-setup.ts',
forbidOnly: IS_CI,
Expand All @@ -48,16 +54,16 @@ export default defineConfig<CurrentsFixtures, CurrentsWorkerFixtures>({
projects: getProjects(),

// We use this if an n8n url is passed in. If the server is already running, we reuse it.
webServer: process.env.N8N_BASE_URL
webServer: BACKEND_URL
? {
command: 'cd .. && pnpm start',
url: `${process.env.N8N_BASE_URL}/favicon.ico`,
timeout: 20000,
command: `cd .. && ${START_COMMAND}`,
url: `${WEB_SERVER_URL}/favicon.ico`,
timeout: 30000,
reuseExistingServer: true,
env: {
DB_SQLITE_POOL_SIZE: '40',
E2E_TESTS: 'true',
N8N_PORT: getPortFromUrl(process.env.N8N_BASE_URL),
N8N_PORT: getPortFromUrl(BACKEND_URL),
N8N_USER_FOLDER: USER_FOLDER,
N8N_LOG_LEVEL: 'debug',
N8N_METRICS: 'true',
Expand Down
17 changes: 17 additions & 0 deletions packages/testing/playwright/utils/url-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,20 @@ export function getPortFromUrl(url: string): string {
const parsedUrl = new URL(url);
return parsedUrl.port || (parsedUrl.protocol === 'https:' ? '443' : '80');
}

/**
* Get the backend URL from environment variables
* Returns N8N_BASE_URL
*/
export function getBackendUrl(): string | undefined {
return process.env.N8N_BASE_URL;
}

/**
* Get the frontend URL from environment variables
* When N8N_EDITOR_URL is set (dev mode), use it for the frontend
* Otherwise, use the same URL as the backend
*/
export function getFrontendUrl(): string | undefined {
return process.env.N8N_EDITOR_URL ?? process.env.N8N_BASE_URL;
}