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,280 @@
import type { TestRequirements } from '../Types';
// #region Mock AI Responses
export const simpleAssistantResponse = {
sessionId: '1',
messages: [
{
role: 'assistant',
type: 'message',
text: 'Hey, this is an assistant message',
},
],
};
export const codeDiffSuggestionResponse = {
sessionId: '1',
messages: [
{
role: 'assistant',
type: 'message',
text: 'Hi there! Here is my top solution to fix the error in your **Code** node 👇',
},
{
role: 'assistant',
type: 'code-diff',
description:
"Fix the syntax error by changing '1asd' to a valid value. In this case, it seems like '1' was intended.",
suggestionId: '1',
codeDiff:
'@@ -2,2 +2,2 @@\\n item.json.myNewField = 1asd;\\n+ item.json.myNewField = 1;\\n',
quickReplies: [
{
text: 'Give me another solution',
type: 'new-suggestion',
},
],
},
],
};
export const applyCodeDiffResponse = {
data: {
sessionId:
'f9130bd7-c078-4862-a38a-369b27b0ff20-e96eb9f7-d581-4684-b6a9-fd3dfe9fe1fb-emTezIGat7bQsDdtIlbti',
parameters: {
jsCode:
"// Loop over input items and add a new field called 'myNewField' to the JSON of each one\\nfor (export const item of $input.all()) {\\n item.json.myNewField = 1;\\n}\\n\\nreturn $input.all();",
},
},
};
export const nodeExecutionSucceededResponse = {
sessionId: '1',
messages: [
{
role: 'assistant',
type: 'message',
text: '**Code** node ran successfully, did my solution help resolve your issue?',
quickReplies: [
{
text: 'Yes, thanks',
type: 'all-good',
isFeedback: true,
},
{
text: 'No, I am still stuck',
type: 'still-stuck',
isFeedback: true,
},
],
},
],
};
export const codeSnippetAssistantResponse = {
sessionId:
'f1d19ed5-0d55-4bad-b49a-f0c56bd6f76f-705b5dbf-12d4-4805-87a3-1e5b3c716d29-W1JgVNrpfitpSNF9rAjB4',
messages: [
{
role: 'assistant',
type: 'message',
text: 'To use expressions in n8n, follow these steps:\\n\\n1. Hover over the parameter where you want to use an expression.\\n2. Select **Expressions** in the **Fixed/Expression** toggle.\\n3. Write your expression in the parameter, or select **Open expression editor** to open the expressions editor. You can browse the available data in the **Variable selector**. All expressions have the format `{{ your expression here }}`.\\n\\n### Example: Get data from webhook body\\n\\nIf your webhook data looks like this:\\n\\n```json\\n[\\n {\\n \\"headers\\": {\\n \\"host\\": \\"n8n.instance.address\\",\\n ...\\n },\\n \\"params\\": {},\\n \\"query\\": {},\\n \\"body\\": {\\n \\"name\\": \\"Jim\\",\\n \\"age\\": 30,\\n \\"city\\": \\"New York\\"\\n }\\n }\\n]\\n```\\n\\nYou can use the following expression to get the value of `city`:\\n\\n```js\\n{{$json.body.city}}\\n```\\n\\nThis expression accesses the incoming JSON-formatted data using n8n\'s custom `$json` variable and finds the value of `city` (in this example, \\"New York\\").',
codeSnippet: '{{$json.body.city}}',
},
{
role: 'assistant',
type: 'message',
text: 'Did this answer solve your question?',
quickReplies: [
{
text: 'Yes, thanks',
type: 'all-good',
isFeedback: true,
},
{
text: 'No, I am still stuck',
type: 'still-stuck',
isFeedback: true,
},
],
},
],
};
// #endregion
// #region Test Requirements for different scenarios
export const aiDisabledRequirements: TestRequirements = {
config: {
settings: {
aiAssistant: { enabled: false, setup: false },
},
features: { aiAssistant: false },
},
};
export const aiEnabledRequirements: TestRequirements = {
config: {
settings: {
aiAssistant: { enabled: true, setup: true },
},
features: { aiAssistant: true, setup: true },
},
};
export const aiEnabledWithWorkflowRequirements: TestRequirements = {
config: {
settings: {
aiAssistant: { enabled: true, setup: true },
},
features: { aiAssistant: true, setup: true },
},
workflow: {
'ai_assistant_test_workflow.json': 'AI_Assistant_Test_Workflow',
},
intercepts: {
aiChat: {
url: '**/rest/ai/chat',
response: simpleAssistantResponse,
},
},
};
export const aiEnabledWithQuickRepliesRequirements: TestRequirements = {
config: {
settings: {
aiAssistant: { enabled: true, setup: true },
},
features: { aiAssistant: true },
},
workflow: {
'ai_assistant_test_workflow.json': 'AI_Assistant_Test_Workflow',
},
intercepts: {
aiChat: {
url: '**/rest/ai/chat',
response: {
sessionId: '1',
messages: [
{
role: 'assistant',
type: 'message',
text: 'Hey, this is an assistant message',
quickReplies: [
{
text: "Sure, let's do it",
type: 'yes',
},
{
text: "Nah, doesn't sound good",
type: 'no',
},
],
},
],
},
},
},
};
export const aiEnabledWithEndSessionRequirements: TestRequirements = {
config: {
settings: {
aiAssistant: { enabled: true, setup: true },
},
features: { aiAssistant: true },
},
workflow: {
'ai_assistant_test_workflow.json': 'AI_Assistant_Test_Workflow',
},
intercepts: {
aiChat: {
url: '**/rest/ai/chat',
response: {
sessionId: '1',
messages: [
{
role: 'assistant',
type: 'message',
title: 'Glad to Help',
text: "I'm glad I could help. If you have any more questions or need further assistance with your n8n workflows, feel free to ask!",
},
{
role: 'assistant',
type: 'event',
eventName: 'end-session',
},
],
},
},
},
};
export const aiEnabledWorkflowBaseRequirements: TestRequirements = {
config: {
settings: {
aiAssistant: { enabled: true, setup: true },
},
features: { aiAssistant: true, setup: true },
},
workflow: {
'ai_assistant_test_workflow.json': 'AI_Assistant_Test_Workflow',
},
};
export const aiEnabledWithCodeDiffRequirements: TestRequirements = {
...aiEnabledWorkflowBaseRequirements,
intercepts: {
aiChat: {
url: '**/rest/ai/chat',
response: codeDiffSuggestionResponse,
},
},
};
export const aiEnabledWithSimpleChatRequirements: TestRequirements = {
config: {
settings: {
aiAssistant: { enabled: true, setup: true },
},
features: { aiAssistant: true, setup: true },
},
intercepts: {
aiChat: {
url: '**/rest/ai/chat',
response: simpleAssistantResponse,
},
},
};
export const aiEnabledWithCodeSnippetRequirements: TestRequirements = {
config: {
settings: {
aiAssistant: { enabled: true, setup: true },
},
features: { aiAssistant: true, setup: true },
},
intercepts: {
aiChat: {
url: '**/rest/ai/chat',
response: codeSnippetAssistantResponse,
},
},
};
export const aiEnabledWithHttpWorkflowRequirements: TestRequirements = {
config: {
settings: {
aiAssistant: { enabled: true, setup: true },
},
features: { aiAssistant: true, setup: true },
},
workflow: {
'Simple_workflow_with_http_node.json': 'Simple HTTP Workflow',
},
};
// #endregion
@@ -0,0 +1,19 @@
import type { TestRequirements } from '../Types';
/**
* Requirements for enabling the AI workflow builder feature.
* These tests use the real Anthropic API for workflow generation,
* requiring N8N_AI_ANTHROPIC_KEY to be set in the environment.
*/
export const workflowBuilderEnabledRequirements: TestRequirements = {
config: {
settings: {
aiAssistant: { enabled: true, setup: true },
aiBuilder: { enabled: true, setup: true },
},
features: {
aiAssistant: true,
aiBuilder: true,
},
},
};
@@ -0,0 +1,51 @@
export const BACKEND_BASE_URL = 'http://localhost:5678';
export const N8N_AUTH_COOKIE = 'n8n-auth';
export const DEFAULT_USER_PASSWORD = 'PlaywrightTest123';
export const MANUAL_TRIGGER_NODE_NAME = 'Manual Trigger';
export const MANUAL_TRIGGER_NODE_DISPLAY_NAME = 'When clicking Execute workflow';
export const MANUAL_CHAT_TRIGGER_NODE_NAME = 'Chat';
export const CHAT_TRIGGER_NODE_DISPLAY_NAME = 'When chat message received';
export const SCHEDULE_TRIGGER_NODE_NAME = 'Schedule Trigger';
export const CODE_NODE_NAME = 'Code';
export const CODE_NODE_DISPLAY_NAME = 'Code in JavaScript';
export const SET_NODE_NAME = 'Set';
export const EDIT_FIELDS_SET_NODE_NAME = 'Edit Fields (Set)';
export const LOOP_OVER_ITEMS_NODE_NAME = 'Loop Over Items';
export const IF_NODE_NAME = 'If';
export const MERGE_NODE_NAME = 'Merge';
export const SWITCH_NODE_NAME = 'Switch';
export const GMAIL_NODE_NAME = 'Gmail';
export const TRELLO_NODE_NAME = 'Trello';
export const NOTION_NODE_NAME = 'Notion';
export const PIPEDRIVE_NODE_NAME = 'Pipedrive';
export const HTTP_REQUEST_NODE_NAME = 'HTTP Request';
export const AGENT_NODE_NAME = 'AI Agent';
export const BASIC_LLM_CHAIN_NODE_NAME = 'Basic LLM Chain';
export const AI_MEMORY_WINDOW_BUFFER_MEMORY_NODE_NAME = 'Simple Memory';
export const AI_TOOL_CALCULATOR_NODE_NAME = 'Calculator';
export const AI_TOOL_CODE_NODE_NAME = 'Code Tool';
export const AI_TOOL_WIKIPEDIA_NODE_NAME = 'Wikipedia';
export const AI_TOOL_HTTP_NODE_NAME = 'HTTP Request Tool';
export const AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME = 'OpenAI Chat Model';
export const AI_MEMORY_POSTGRES_NODE_NAME = 'Postgres Chat Memory';
export const AI_MEMORY_REDIS_CHAT_NODE_NAME = 'Redis Chat Memory';
export const AI_OUTPUT_PARSER_AUTO_FIXING_NODE_NAME = 'Auto-fixing Output Parser';
export const WEBHOOK_NODE_NAME = 'Webhook';
export const EXECUTE_WORKFLOW_NODE_NAME = 'Execute Workflow';
export const NO_OPERATION_NODE_NAME = 'No Operation, do nothing';
export const HACKER_NEWS_NODE_NAME = 'Hacker News';
export const NEW_GOOGLE_ACCOUNT_NAME = 'Gmail account';
export const NEW_TRELLO_ACCOUNT_NAME = 'Trello account';
export const NEW_NOTION_ACCOUNT_NAME = 'Notion account';
export const NEW_QUERY_AUTH_ACCOUNT_NAME = 'Query Auth account';
export const E2E_TEST_NODE_NAME = 'E2E Test';
export const TOOL_SUBCATEGORY = 'Action in an app';
export const HITL_TOOL_SUBCATEGORY = 'Human review';
export const ROUTES = {
NEW_WORKFLOW_PAGE: '/workflow/new',
};
@@ -0,0 +1,123 @@
import type { BrowserContext, Route } from '@playwright/test';
import cloneDeep from 'lodash/cloneDeep';
import merge from 'lodash/merge';
const contextSettings = new Map<BrowserContext, Partial<Record<string, unknown>>>();
export function setContextSettings(
context: BrowserContext,
settings: Partial<Record<string, unknown>>,
) {
contextSettings.set(context, settings);
}
export function getContextSettings(context: BrowserContext) {
return contextSettings.get(context);
}
export async function setupDefaultInterceptors(target: BrowserContext) {
// Global /rest/settings intercept - always active like Cypress
// TODO: Remove this as a global and move it per test
await target.route('**/rest/settings', async (route: Route) => {
try {
const originalResponse = await route.fetch();
const originalJson = await originalResponse.json();
// Get settings stored for this specific context
const testSettings = getContextSettings(target);
// Deep merge test settings with backend settings (like Cypress)
const modifiedData = {
data:
testSettings && Object.keys(testSettings).length > 0
? merge(cloneDeep(originalJson.data), testSettings)
: originalJson.data,
};
await route.fulfill({
status: originalResponse.status(),
headers: originalResponse.headers(),
contentType: 'application/json',
body: JSON.stringify(modifiedData),
});
} catch (error) {
console.error('Error in /rest/settings intercept:', error);
await route.continue();
}
});
// POST /rest/credentials/test
await target.route('**/rest/credentials/test', async (route: Route) => {
if (route.request().method() === 'POST') {
await route.fulfill({
contentType: 'application/json',
body: JSON.stringify({ data: { status: 'success', message: 'Tested successfully' } }),
});
} else {
await route.continue();
}
});
// POST /rest/license/renew
await target.route('**/rest/license/renew', async (route: Route) => {
if (route.request().method() === 'POST') {
await route.fulfill({
contentType: 'application/json',
body: JSON.stringify({
data: {
usage: { activeWorkflowTriggers: { limit: -1, value: 0, warningThreshold: 0.8 } },
license: { planId: '', planName: 'Community' },
},
}),
});
} else {
await route.continue();
}
});
// Pathname /api/health
await target.route(
(url) => url.pathname.endsWith('/api/health'),
async (route: Route) => {
await route.fulfill({
contentType: 'application/json',
body: JSON.stringify({ status: 'OK' }),
});
},
);
// Pathname /api/versions/*
await target.route(
(url) => url.pathname.startsWith('/api/versions/'),
async (route: Route) => {
await route.fulfill({
contentType: 'application/json',
body: JSON.stringify([
{
name: '1.45.1',
createdAt: '2023-08-18T11:53:12.857Z',
hasSecurityIssue: null,
hasSecurityFix: null,
securityIssueFixVersion: null,
hasBreakingChange: null,
documentationUrl: 'https://docs.n8n.io/release-notes/#n8n131',
nodes: [],
description: 'Includes <strong>bug fixes</strong>',
},
{
name: '1.0.5',
createdAt: '2023-07-24T10:54:56.097Z',
hasSecurityIssue: false,
hasSecurityFix: null,
securityIssueFixVersion: null,
hasBreakingChange: true,
documentationUrl: 'https://docs.n8n.io/release-notes/#n8n104',
nodes: [],
description:
'Includes <strong>core functionality</strong> and <strong>bug fixes</strong>',
},
]),
});
},
);
}
@@ -0,0 +1,96 @@
import { DEFAULT_USER_PASSWORD } from './constants';
export interface UserCredentials {
email: string;
password: string;
firstName: string;
lastName: string;
mfaEnabled?: boolean;
mfaSecret?: string;
mfaRecoveryCodes?: string[];
}
// Simple name generators
const FIRST_NAMES = [
'Alex',
'Jordan',
'Taylor',
'Morgan',
'Casey',
'Riley',
'Avery',
'Quinn',
'Sam',
'Drew',
'Blake',
'Sage',
'River',
'Rowan',
'Skylar',
'Emery',
];
const LAST_NAMES = [
'Smith',
'Johnson',
'Williams',
'Brown',
'Jones',
'Garcia',
'Miller',
'Davis',
'Rodriguez',
'Martinez',
'Hernandez',
'Lopez',
'Gonzalez',
'Wilson',
'Anderson',
'Thomas',
];
const getRandomName = (names: string[]): string => {
return names[Math.floor(Math.random() * names.length)];
};
const randFirstName = (): string => getRandomName(FIRST_NAMES);
const randLastName = (): string => getRandomName(LAST_NAMES);
export const INSTANCE_OWNER_CREDENTIALS: UserCredentials = {
email: 'nathan@n8n.io',
password: DEFAULT_USER_PASSWORD,
firstName: randFirstName(),
lastName: randLastName(),
mfaEnabled: false,
mfaSecret: 'KVKFKRCPNZQUYMLXOVYDSQKJKZDTSRLD',
mfaRecoveryCodes: ['d04ea17f-e8b2-4afa-a9aa-57a2c735b30e'],
};
export const INSTANCE_ADMIN_CREDENTIALS: UserCredentials = {
email: 'admin@n8n.io',
password: DEFAULT_USER_PASSWORD,
firstName: randFirstName(),
lastName: randLastName(),
};
export const INSTANCE_MEMBER_CREDENTIALS: UserCredentials[] = [
{
email: 'member@n8n.io',
password: DEFAULT_USER_PASSWORD,
firstName: randFirstName(),
lastName: randLastName(),
},
{
email: 'member2@n8n.io',
password: DEFAULT_USER_PASSWORD,
firstName: randFirstName(),
lastName: randLastName(),
},
];
export const INSTANCE_CHAT_CREDENTIALS: UserCredentials = {
email: 'chat@n8n.io',
password: DEFAULT_USER_PASSWORD,
firstName: randFirstName(),
lastName: randLastName(),
};