first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled

This commit is contained in:
2026-03-17 16:22:57 +03:30
commit 3d5eaf9445
15349 changed files with 2847338 additions and 0 deletions
@@ -0,0 +1,98 @@
import {
MANUAL_TRIGGER_NODE_NAME,
MANUAL_TRIGGER_NODE_DISPLAY_NAME,
} from '../../../config/constants';
import { test, expect } from '../../../fixtures/base';
test.describe('Canvas Node Actions', {
annotation: [
{ type: 'owner', description: 'Catalysts' },
],
}, () => {
test.beforeEach(async ({ n8n }) => {
await n8n.start.fromBlankCanvas();
});
test.describe('Node Search and Add', () => {
test('should search and add a basic node', async ({ n8n }) => {
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(1);
await expect(n8n.canvas.nodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME)).toBeVisible();
});
test('should search and add Linear node with action', async ({ n8n }) => {
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
await n8n.canvas.addNode('Linear', { action: 'Create an issue' });
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(2);
await expect(n8n.canvas.nodeConnections()).toHaveCount(1);
await expect(n8n.canvas.nodeByName('Create an issue')).toBeVisible();
});
test('should search and add Webhook node (no actions)', async ({ n8n }) => {
await n8n.canvas.addNode('Webhook');
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(1);
await expect(n8n.canvas.nodeByName('Webhook')).toBeVisible();
});
test('should search and add Jira node with trigger', async ({ n8n }) => {
await n8n.canvas.addNode('Jira Software', { trigger: 'On issue created' });
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(1);
await expect(n8n.canvas.nodeByName('Jira Trigger')).toBeVisible();
});
test('should clear search and show all nodes', async ({ n8n }) => {
await n8n.canvas.clickCanvasPlusButton();
await n8n.canvas.fillNodeCreatorSearchBar('Linear');
const searchCount = await n8n.canvas.nodeCreatorNodeItems().count();
await expect(n8n.canvas.nodeCreatorNodeItems()).toHaveCount(1);
await n8n.canvas.nodeCreatorSearchBar().clear();
const nodeCount = await n8n.canvas.nodeCreatorNodeItems().count();
expect(nodeCount).toBeGreaterThan(searchCount);
});
test('should add connected node via plus endpoint', async ({ n8n }) => {
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
await n8n.canvas.clickNodePlusEndpoint(MANUAL_TRIGGER_NODE_DISPLAY_NAME);
await n8n.canvas.fillNodeCreatorSearchBar('Code');
await n8n.page.keyboard.press('Enter');
await n8n.canvas.clickNodeCreatorItemName('Code in JavaScript');
await n8n.page.keyboard.press('Enter');
await n8n.page.keyboard.press('Escape');
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(2);
await expect(n8n.canvas.nodeConnections()).toHaveCount(1);
});
test('should add disconnected node when nothing selected', async ({ n8n }) => {
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
await n8n.canvas.deselectAll();
await n8n.canvas.addNode('Code', { action: 'Code in JavaScript', closeNDV: true });
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(2);
await expect(n8n.canvas.nodeConnections()).toHaveCount(0);
});
});
test.describe('Node Creator Interactions', () => {
test('should close node creator with escape key', async ({ n8n }) => {
await n8n.canvas.clickCanvasPlusButton();
await expect(n8n.canvas.nodeCreatorSearchBar()).toBeVisible();
await n8n.page.keyboard.press('Escape');
await expect(n8n.canvas.nodeCreatorSearchBar()).toBeHidden();
});
test('should filter nodes by search term', async ({ n8n }) => {
await n8n.canvas.clickCanvasPlusButton();
await n8n.canvas.fillNodeCreatorSearchBar('HTTP');
const filteredItems = n8n.canvas.nodeCreatorNodeItems();
await expect(filteredItems.first()).toContainText('HTTP');
});
});
});
@@ -0,0 +1,97 @@
import { nanoid } from 'nanoid';
import { test, expect } from '../../../fixtures/base';
test.describe('Credentials', {
annotation: [
{ type: 'owner', description: 'Catalysts' },
],
}, () => {
test('composer: createFromList creates credential', async ({ n8n }) => {
const projectId = await n8n.start.fromNewProject();
const credentialName = `credential-${nanoid()}`;
await n8n.navigate.toCredentials(projectId);
await n8n.credentialsComposer.createFromList(
'Notion API',
{ apiKey: '1234567890' },
{
name: credentialName,
closeDialog: false,
},
);
await expect(n8n.credentials.cards.getCredential(credentialName)).toBeVisible();
});
test('composer: createFromNdv creates credential for node', async ({ n8n }) => {
const name = `credential-${nanoid()}`;
await n8n.start.fromNewProjectBlankCanvas();
await n8n.canvas.addNode('Manual Trigger');
await n8n.canvas.addNode('Notion', { action: 'Append a block' });
await n8n.credentialsComposer.createFromNdv({ apiKey: '1234567890' }, { name });
await expect(n8n.ndv.getCredentialSelect()).toHaveValue(name);
});
test('composer: createFromApi creates credential (then NDV picks it up)', async ({ n8n }) => {
const name = `credential-${nanoid()}`;
const projectId = await n8n.start.fromNewProjectBlankCanvas();
await n8n.credentialsComposer.createFromApi({
name,
type: 'notionApi',
data: { apiKey: '1234567890' },
projectId,
});
await n8n.canvas.addNode('Manual Trigger');
await n8n.canvas.addNode('Notion', { action: 'Append a block' });
await expect(n8n.ndv.getCredentialSelect()).toHaveValue(name);
});
test('create a new credential from empty state using the credential chooser list', async ({
n8n,
}) => {
const projectId = await n8n.start.fromNewProject();
await n8n.navigate.toCredentials(projectId);
await n8n.credentials.emptyListCreateCredentialButton.click();
await n8n.credentials.createCredentialFromCredentialPicker('Notion API', {
apiKey: '1234567890',
});
await expect(n8n.credentials.cards.getCredentials()).toHaveCount(1);
});
test('create a new credential from the NDV', async ({ n8n }) => {
const uniqueCredentialName = `credential-${nanoid()}`;
await n8n.start.fromNewProjectBlankCanvas();
await n8n.canvas.addNode('Manual Trigger');
await n8n.canvas.addNode('Notion', { action: 'Append a block' });
await n8n.ndv.getNodeCredentialsSelect().click();
await n8n.ndv.credentialDropdownCreateNewCredential().click();
await n8n.canvas.credentialModal.addCredential(
{
apiKey: '1234567890',
},
{ name: uniqueCredentialName },
);
await expect(n8n.ndv.getCredentialSelect()).toHaveValue(uniqueCredentialName);
});
test('add an existing credential from the NDV', async ({ n8n }) => {
const uniqueCredentialName = `credential-${nanoid()}`;
const projectId = await n8n.start.fromNewProjectBlankCanvas();
await n8n.api.credentials.createCredential({
name: uniqueCredentialName,
type: 'notionApi',
data: {
apiKey: '1234567890',
},
projectId,
});
await n8n.canvas.addNode('Manual Trigger');
await n8n.canvas.addNode('Notion', { action: 'Append a block' });
await expect(n8n.ndv.getCredentialSelect()).toHaveValue(uniqueCredentialName);
});
});
@@ -0,0 +1,92 @@
import { test, expect } from '../../../fixtures/base';
test.describe('Node Details Configuration', {
annotation: [
{ type: 'owner', description: 'Catalysts' },
],
}, () => {
test.beforeEach(async ({ n8n }) => {
await n8n.start.fromBlankCanvas();
});
test('should configure webhook node', async ({ n8n }) => {
await n8n.canvas.addNode('Webhook');
await n8n.ndv.setupHelper.webhook({
httpMethod: 'POST',
path: 'test-webhook',
authentication: 'Basic Auth',
});
await expect(n8n.ndv.getParameterInputField('path')).toHaveValue('test-webhook');
});
test('should configure HTTP Request node', async ({ n8n }) => {
await n8n.canvas.addNode('HTTP Request');
await n8n.ndv.setupHelper.httpRequest({
method: 'POST',
url: 'https://api.example.com/test',
sendQuery: true,
sendHeaders: false,
});
await expect(n8n.ndv.getParameterInputField('url')).toHaveValue('https://api.example.com/test');
});
test('should auto-detect parameter types', async ({ n8n }) => {
await n8n.canvas.addNode('Webhook');
await n8n.ndv.setupHelper.setParameter('httpMethod', 'PUT');
await n8n.ndv.setupHelper.setParameter('path', 'auto-detect-test');
await expect(n8n.ndv.getParameterInputField('path')).toHaveValue('auto-detect-test');
});
test('should use explicit types for better performance', async ({ n8n }) => {
await n8n.canvas.addNode('Webhook');
await n8n.ndv.setupHelper.setParameter('httpMethod', 'PATCH', 'dropdown');
await n8n.ndv.setupHelper.setParameter('path', 'explicit-types', 'text');
await expect(n8n.ndv.getParameterInputField('path')).toHaveValue('explicit-types');
});
test('should configure Edit Fields node with single field', async ({ n8n }) => {
await n8n.canvas.addNode('Edit Fields (Set)');
await n8n.ndv.editFields.setSingleFieldValue('testField', 'string', 'Hello World');
const nameInput = n8n.ndv.getAssignmentName('assignments', 0).getByRole('textbox');
await expect(nameInput).toHaveValue('testField');
});
test('should configure Edit Fields node with multiple fields', async ({ n8n }) => {
await n8n.canvas.addNode('Edit Fields (Set)');
await n8n.ndv.editFields.setFieldsValues([
{ name: 'stringField', type: 'string', value: 'Test String' },
{ name: 'numberField', type: 'number', value: 123 },
{ name: 'booleanField', type: 'boolean', value: true },
]);
await expect(
n8n.ndv.getAssignmentCollectionContainer('assignments').getByTestId('assignment'),
).toHaveCount(3);
});
test('should configure Edit Fields node with all field types', async ({ n8n }) => {
await n8n.canvas.addNode('Edit Fields (Set)');
await n8n.ndv.editFields.setFieldsValues([
{ name: 'myString', type: 'string', value: 'Hello' },
{ name: 'myNumber', type: 'number', value: 42 },
{ name: 'myBoolean', type: 'boolean', value: false },
{ name: 'myArray', type: 'array', value: '["item1", "item2"]' },
]);
await expect(
n8n.ndv.getAssignmentCollectionContainer('assignments').getByTestId('assignment'),
).toHaveCount(4);
});
});
@@ -0,0 +1,139 @@
import { nanoid } from 'nanoid';
import { expect, test } from '../../../fixtures/base';
test.describe('User API Service', {
annotation: [
{ type: 'owner', description: 'Catalysts' },
],
}, () => {
test.describe('Internal API (Cookie Auth)', () => {
test('should create a user with default values', async ({ api }) => {
const user = await api.users.create();
expect(user.email).toContain('testuser');
expect(user.email).toContain('@test.com');
expect(user.firstName).toBe('Test');
expect(user.lastName).toContain('User');
expect(user.role).toContain('member');
});
test('should create a user with custom values', async ({ api }) => {
const customEmail = `custom-${nanoid()}@test.com`;
const customPassword = 'CustomPass123!';
const user = await api.users.create({
email: customEmail,
password: customPassword,
firstName: 'John',
lastName: 'Doe',
role: 'global:member',
});
expect(user.email.toLowerCase()).toBe(customEmail.toLowerCase());
expect(user.firstName).toBe('John');
expect(user.lastName).toBe('Doe');
expect(user.role).toContain('member');
});
test('should create a member user by default', async ({ api }) => {
const user = await api.users.create();
expect(user.role).toContain('member');
expect(user.role).toBe('global:member');
});
test('should maintain separate sessions for multiple users', async ({ n8n, api }) => {
await n8n.navigate.toPersonalSettings();
const user = await api.users.create();
await n8n.page.reload();
await expect(n8n.settingsPersonal.getUserRole()).toHaveText('Owner');
// New user page should have test name
const memberN8n = await n8n.start.withUser(user);
await memberN8n.navigate.toPersonalSettings();
await expect(memberN8n.settingsPersonal.getUserRole()).toHaveText('Member');
// n8n main should still have owner context
await n8n.page.reload();
await expect(n8n.settingsPersonal.getUserRole()).toHaveText('Owner');
// user page should still have member role
await memberN8n.page.reload();
await expect(memberN8n.settingsPersonal.getUserRole()).toHaveText('Member');
});
});
test.describe('Public API (API Key Auth)', () => {
test('should create an API key', async ({ api }) => {
const label = `Test Key ${nanoid()}`;
const apiKey = await api.publicApi.createApiKey(label);
expect(apiKey.label).toBe(label);
expect(apiKey.rawApiKey).toBeDefined();
expect(apiKey.rawApiKey.length).toBeGreaterThan(0);
});
test('should create a user via public API', async ({ api }) => {
const user = await api.publicApi.createUser({
email: `public-api-user-${nanoid()}@test.com`,
firstName: 'Public',
lastName: 'ApiUser',
});
expect(user.email).toContain('public-api-user');
expect(user.firstName).toBe('Public');
expect(user.lastName).toBe('ApiUser');
expect(user.role).toBe('global:member');
});
test('should list users via public API', async ({ api }) => {
// Create a user first
await api.publicApi.createUser({
email: `list-test-user-${nanoid()}@test.com`,
});
const users = await api.publicApi.getUsers({ includeRole: true });
expect(users.length).toBeGreaterThan(0);
// Should have at least the owner
const owner = users.find((u) => u.role === 'global:owner');
expect(owner).toBeDefined();
});
test('should create multiple users and maintain separate sessions', async ({ n8n, api }) => {
// Create users via public API
const user1 = await api.publicApi.createUser({
email: `multi-user-1-${nanoid()}@test.com`,
firstName: 'User',
lastName: 'One',
});
const user2 = await api.publicApi.createUser({
email: `multi-user-2-${nanoid()}@test.com`,
firstName: 'User',
lastName: 'Two',
});
// Owner should see their settings
await n8n.navigate.toPersonalSettings();
await expect(n8n.settingsPersonal.getUserRole()).toHaveText('Owner');
// Create isolated browser contexts for each user
const user1N8n = await n8n.start.withUser(user1);
const user2N8n = await n8n.start.withUser(user2);
// Verify each user sees their own context
await user1N8n.navigate.toPersonalSettings();
await expect(user1N8n.settingsPersonal.getUserRole()).toHaveText('Member');
await user2N8n.navigate.toPersonalSettings();
await expect(user2N8n.settingsPersonal.getUserRole()).toHaveText('Member');
// Owner should still be owner after all this
await n8n.page.reload();
await expect(n8n.settingsPersonal.getUserRole()).toHaveText('Owner');
});
});
});
@@ -0,0 +1,52 @@
import { test, expect } from '../../../fixtures/base';
test.describe('UI Test Entry Points', {
annotation: [
{ type: 'owner', description: 'Catalysts' },
],
}, () => {
test.describe('Entry Point: Home Page', () => {
test('should navigate from home', async ({ n8n }) => {
await n8n.start.fromHome();
expect(n8n.page.url()).toContain('/home/workflows');
});
});
test.describe('Entry Point: Blank Canvas', () => {
test('should navigate from blank canvas', async ({ n8n }) => {
await n8n.start.fromBlankCanvas();
await expect(n8n.canvas.canvasPane()).toBeVisible();
});
});
test.describe('Entry Point: Basic Workflow Creation', () => {
test('should create a new project and workflow', async ({ n8n }) => {
await n8n.start.fromNewProjectBlankCanvas();
await expect(n8n.canvas.canvasPane()).toBeVisible();
});
});
test.describe('Entry Point: Imported Workflow', () => {
test('should import a webhook workflow', async ({ n8n }) => {
const workflowImportResult = await n8n.start.fromImportedWorkflow('simple-webhook-test.json');
const { webhookPath } = workflowImportResult;
const testPayload = { message: 'Hello from Playwright test' };
await n8n.canvas.clickExecuteWorkflowButton();
await expect(n8n.canvas.getExecuteWorkflowButton()).toHaveText('Waiting for trigger event');
const webhookResponse = await n8n.page.request.post(`/webhook-test/${webhookPath}`, {
data: testPayload,
});
expect(webhookResponse.ok()).toBe(true);
});
test('should import a workflow', async ({ n8n }) => {
await n8n.start.fromImportedWorkflow('manual.json');
await n8n.workflowComposer.executeWorkflowAndWaitForNotification('Success');
await expect(n8n.canvas.canvasPane()).toBeVisible();
});
});
});