mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 02:21:13 +00:00
feat: Add testcontainers and Playwright (no-changelog) (#16662)
Co-authored-by: Tomi Turtiainen <10324676+tomi@users.noreply.github.com>
This commit is contained in:
11
packages/testing/playwright/tests/1-workflows.spec.ts
Normal file
11
packages/testing/playwright/tests/1-workflows.spec.ts
Normal 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']);
|
||||
});
|
||||
});
|
||||
113
packages/testing/playwright/tests/28-debug.spec.ts
Normal file
113
packages/testing/playwright/tests/28-debug.spec.ts
Normal 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();
|
||||
}
|
||||
});
|
||||
107
packages/testing/playwright/tests/39-projects.spec.ts
Normal file
107
packages/testing/playwright/tests/39-projects.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
26
packages/testing/playwright/tests/authenticated.spec.ts
Normal file
26
packages/testing/playwright/tests/authenticated.spec.ts
Normal 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/);
|
||||
});
|
||||
22
packages/testing/playwright/tests/multimain.spec.ts
Normal file
22
packages/testing/playwright/tests/multimain.spec.ts
Normal 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();
|
||||
});
|
||||
15
packages/testing/playwright/tests/pdf.spec.ts
Normal file
15
packages/testing/playwright/tests/pdf.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user