feat: Add testcontainers and Playwright (no-changelog) (#16662)

Co-authored-by: Tomi Turtiainen <10324676+tomi@users.noreply.github.com>
This commit is contained in:
shortstacked
2025-07-01 14:15:31 +01:00
committed by GitHub
parent 422aa82524
commit 852657c17e
52 changed files with 5686 additions and 1111 deletions

View File

@@ -0,0 +1,11 @@
import { test, expect } from '../fixtures/base';
// Example of importing a workflow from a file
test.describe('Workflows', () => {
test('should create a new workflow using empty state card @db:reset', async ({ n8n }) => {
await n8n.goHome();
await n8n.workflows.clickNewWorkflowCard();
await n8n.workflows.importWorkflow('Test_workflow_1.json', 'Empty State Card Workflow');
await expect(n8n.workflows.workflowTags()).toHaveText(['some-tag-1', 'some-tag-2']);
});
});

View File

@@ -0,0 +1,113 @@
import { test, expect } from '../fixtures/base';
// Example of using helper functions inside a test
test.describe('Debug mode', () => {
// Constants to avoid magic strings
const URLS = {
FAILING: 'https://foo.bar',
SUCCESS: 'https://postman-echo.com/get?foo1=bar1&foo2=bar2',
};
const NOTIFICATIONS = {
WORKFLOW_CREATED: 'Workflow successfully created',
EXECUTION_IMPORTED: 'Execution data imported',
PROBLEM_IN_NODE: 'Problem in node',
SUCCESSFUL: 'Successful',
DATA_NOT_IMPORTED: "Some execution data wasn't imported",
};
test.beforeEach(async ({ api, n8n }) => {
await api.enableFeature('debugInEditor');
await n8n.goHome();
});
// Helper function to create basic workflow
async function createBasicWorkflow(n8n, url = URLS.FAILING) {
await n8n.workflows.clickAddWorklowButton();
await n8n.canvas.addNode('Manual Trigger');
await n8n.canvas.addNode('HTTP Request');
await n8n.ndv.fillParameterInput('URL', url);
await n8n.ndv.close();
await n8n.canvas.clickSaveWorkflowButton();
await n8n.notifications.waitForNotificationAndClose(NOTIFICATIONS.WORKFLOW_CREATED);
}
// Helper function to import execution for debugging
async function importExecutionForDebugging(n8n) {
await n8n.canvas.clickExecutionsTab();
await n8n.executions.clickDebugInEditorButton();
await n8n.notifications.waitForNotificationAndClose(NOTIFICATIONS.EXECUTION_IMPORTED);
}
test('should enter debug mode for failed executions', async ({ n8n }) => {
await createBasicWorkflow(n8n, URLS.FAILING);
await n8n.workflowComposer.executeWorkflowAndWaitForNotification(NOTIFICATIONS.PROBLEM_IN_NODE);
await importExecutionForDebugging(n8n);
expect(n8n.page.url()).toContain('/debug');
});
test('should exit debug mode after successful execution', async ({ n8n }) => {
await createBasicWorkflow(n8n, URLS.FAILING);
await n8n.workflowComposer.executeWorkflowAndWaitForNotification(NOTIFICATIONS.PROBLEM_IN_NODE);
await importExecutionForDebugging(n8n);
await n8n.canvas.openNode('HTTP Request');
await n8n.ndv.fillParameterInput('URL', URLS.SUCCESS);
await n8n.ndv.close();
await n8n.canvas.clickSaveWorkflowButton();
await n8n.workflowComposer.executeWorkflowAndWaitForNotification(NOTIFICATIONS.SUCCESSFUL);
expect(n8n.page.url()).not.toContain('/debug');
});
test('should handle pinned data conflicts during execution import', async ({ n8n }) => {
await createBasicWorkflow(n8n, URLS.SUCCESS);
await n8n.workflowComposer.executeWorkflowAndWaitForNotification(NOTIFICATIONS.SUCCESSFUL);
await n8n.canvasComposer.pinNodeData('HTTP Request');
await n8n.workflowComposer.executeWorkflowAndWaitForNotification('Successful');
// Go to executions and try to copy execution to editor
await n8n.canvas.clickExecutionsTab();
await n8n.executions.clickLastExecutionItem();
await n8n.executions.clickCopyToEditorButton();
// Test CANCEL dialog
await n8n.executions.handlePinnedNodesConfirmation('Cancel');
// Try again and CONFIRM
await n8n.executions.clickLastExecutionItem();
await n8n.executions.clickCopyToEditorButton();
await n8n.executions.handlePinnedNodesConfirmation('Unpin');
expect(n8n.page.url()).toContain('/debug');
// Verify pinned status
const pinnedNodeNames = await n8n.canvas.getPinnedNodeNames();
expect(pinnedNodeNames).not.toContain('HTTP Request');
expect(pinnedNodeNames).toContain('When clicking Execute workflow');
});
test('should show error for pinned data mismatch', async ({ n8n }) => {
// Create workflow, execute, and pin data
await createBasicWorkflow(n8n, URLS.SUCCESS);
await n8n.workflowComposer.executeWorkflowAndWaitForNotification(NOTIFICATIONS.SUCCESSFUL);
await n8n.canvasComposer.pinNodeData('HTTP Request');
await n8n.workflowComposer.executeWorkflowAndWaitForNotification(NOTIFICATIONS.SUCCESSFUL);
// Delete node to create mismatch
await n8n.canvas.deleteNodeByName('HTTP Request');
// Try to copy execution and verify error
await attemptCopyToEditor(n8n);
await n8n.notifications.waitForNotificationAndClose(NOTIFICATIONS.DATA_NOT_IMPORTED);
expect(n8n.page.url()).toContain('/debug');
});
async function attemptCopyToEditor(n8n) {
await n8n.canvas.clickExecutionsTab();
await n8n.executions.clickLastExecutionItem();
await n8n.executions.clickCopyToEditorButton();
}
});

View File

@@ -0,0 +1,107 @@
import { test, expect } from '../fixtures/base';
import { n8nPage } from '../pages/n8nPage';
import type { ApiHelpers } from '../services/api-helper';
const MANUAL_TRIGGER_NODE_NAME = 'Manual Trigger';
const EXECUTE_WORKFLOW_NODE_NAME = 'Execute Sub-workflow';
const NOTION_NODE_NAME = 'Notion';
const NOTION_API_KEY = 'abc123Playwright';
// Example of using API calls in a test
async function getCredentialsForProject(api: ApiHelpers, projectId?: string) {
const params = new URLSearchParams({
includeScopes: 'true',
includeData: 'true',
...(projectId && { filter: JSON.stringify({ projectId }) }),
});
return await api.get('/rest/credentials', params);
}
test.describe('Projects @db:reset', () => {
test.beforeEach(async ({ api, n8n }) => {
await api.enableFeature('sharing');
await api.enableFeature('folders');
await api.enableFeature('advancedPermissions');
await api.enableFeature('projectRole:admin');
await api.enableFeature('projectRole:editor');
await api.setMaxTeamProjectsQuota(-1);
await n8n.goHome();
});
test('should not show project add button and projects to a member if not invited to any project @auth:member', async ({
n8n,
}) => {
await expect(n8n.sideBar.getAddFirstProjectButton()).toBeDisabled();
await expect(n8n.sideBar.getProjectMenuItems()).toHaveCount(0);
});
test('should filter credentials by project ID', async ({ n8n, api }) => {
const { projectName, projectId } = await n8n.projectComposer.createProject();
await n8n.projectComposer.addCredentialToProject(
projectName,
'Notion API',
'apiKey',
NOTION_API_KEY,
);
const credentials = await getCredentialsForProject(api, projectId);
expect(credentials).toHaveLength(1);
const { projectId: project2Id } = await n8n.projectComposer.createProject();
const credentials2 = await getCredentialsForProject(api, project2Id);
expect(credentials2).toHaveLength(0);
});
test('should create sub-workflow and credential in the sub-workflow in the same project @auth:owner', async ({
n8n,
}) => {
const { projectName } = await n8n.projectComposer.createProject();
await n8n.sideBar.addWorkflowFromUniversalAdd(projectName);
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
await n8n.canvas.saveWorkflow();
await expect(
n8n.page.getByText('Workflow successfully created', { exact: false }),
).toBeVisible();
await n8n.canvas.addNodeToCanvasWithSubItem(
EXECUTE_WORKFLOW_NODE_NAME,
'Execute A Sub Workflow',
);
const subWorkflowPagePromise = n8n.page.waitForEvent('popup');
await n8n.ndv.selectWorkflowResource(`Create a Sub-Workflow in '${projectName}'`);
const subn8n = new n8nPage(await subWorkflowPagePromise);
await subn8n.ndv.clickBackToCanvasButton();
await subn8n.canvas.deleteNodeByName('Replace me with your logic');
await subn8n.canvas.addNodeToCanvasWithSubItem(NOTION_NODE_NAME, 'Append a block');
await subn8n.credentials.createAndSaveNewCredential('apiKey', NOTION_API_KEY);
await subn8n.ndv.clickBackToCanvasButton();
await subn8n.canvas.saveWorkflow();
await subn8n.page.goto('/home/workflows');
await subn8n.projectWorkflows.clickProjectMenuItem(projectName);
await subn8n.page.getByRole('link', { name: 'Workflows' }).click();
// Get Workflow Count
await expect(subn8n.page.locator('[data-test-id="resources-list-item-workflow"]')).toHaveCount(
2,
);
// Assert that the sub-workflow is in the list
await expect(subn8n.page.getByRole('heading', { name: 'My Sub-Workflow' })).toBeVisible();
// Navigate to Credentials
await subn8n.page.getByRole('link', { name: 'Credentials' }).click();
// Assert that the credential is in the list
await expect(subn8n.page.locator('[data-test-id="resources-list-item"]')).toHaveCount(1);
await expect(subn8n.page.getByRole('heading', { name: 'Notion account' })).toBeVisible();
});
});

View File

@@ -0,0 +1,26 @@
import { test, expect } from '../fixtures/base';
test('default signin is as owner', async ({ n8n }) => {
await n8n.goHome();
await expect(n8n.page).toHaveURL(/\/workflow/);
});
test('owner can access dashboard @auth:owner', async ({ n8n }) => {
await n8n.goHome();
await expect(n8n.page).toHaveURL(/\/workflow/);
});
test('admin can access dashboard @auth:admin', async ({ n8n }) => {
await n8n.goHome();
await expect(n8n.page).toHaveURL(/\/workflow/);
});
test('member can access dashboard @auth:member', async ({ n8n }) => {
await n8n.goHome();
await expect(n8n.page).toHaveURL(/\/workflow/);
});
test('no auth can not access dashboard @auth:none', async ({ n8n }) => {
await n8n.goHome();
await expect(n8n.page).toHaveURL(/\/signin/);
});

View File

@@ -0,0 +1,22 @@
import { test, expect } from '../fixtures/base';
test('Leader election @mode:multi-main @chaostest', async ({ chaos }) => {
// First get the container (try main 1 first)
const namePattern = 'n8n-main-*';
const findContainerByLog = await chaos.waitForLog('Leader is now this', {
namePattern,
});
expect(findContainerByLog).toBeDefined();
const currentLeader = findContainerByLog.containerName;
// Stop leader
await chaos.stopContainer(currentLeader);
// Find new leader
const newLeader = await chaos.waitForLog('Leader is now this', {
namePattern,
});
expect(newLeader).toBeDefined();
});

View File

@@ -0,0 +1,15 @@
import { expect, test } from '../fixtures/base';
// Example of importing a workflow from a file
test.describe('PDF Test', () => {
// eslint-disable-next-line playwright/no-skipped-test
test.skip('Can read and write PDF files and extract text', async ({ n8n }) => {
await n8n.goHome();
await n8n.workflows.clickAddWorklowButton();
await n8n.workflows.importWorkflow('test_pdf_workflow.json', 'PDF Workflow');
await n8n.canvas.clickExecuteWorkflowButton();
await expect(
n8n.notifications.notificationContainerByText('Workflow executed successfully'),
).toBeVisible();
});
});