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
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:
@@ -0,0 +1,148 @@
|
||||
import { test, expect } from '../../../fixtures/base';
|
||||
|
||||
test.describe('Folders - Advanced Operations', {
|
||||
annotation: [
|
||||
{ type: 'owner', description: 'Identity & Access' },
|
||||
],
|
||||
}, () => {
|
||||
test.describe('Duplicate workflows', () => {
|
||||
test('should duplicate workflow within root folder from personal projects', async ({ n8n }) => {
|
||||
const { id: projectId } = await n8n.api.projects.createProject();
|
||||
const { name: workflowName } = await n8n.api.workflows.createInProject(projectId);
|
||||
await n8n.navigate.toProject(projectId);
|
||||
const workflowCard = n8n.workflows.cards.getWorkflow(workflowName);
|
||||
await n8n.workflows.cards.openCardActions(workflowCard);
|
||||
await n8n.workflows.cards.getCardAction('duplicate').click();
|
||||
const duplicatePage = await n8n.start.fromNewPage(async () => {
|
||||
await n8n.modal.clickButton('Duplicate');
|
||||
});
|
||||
|
||||
const duplicatedName = `${workflowName} copy`;
|
||||
await duplicatePage.navigate.toProject(projectId);
|
||||
await expect(duplicatePage.workflows.cards.getWorkflow(duplicatedName)).toBeVisible();
|
||||
});
|
||||
|
||||
test('should duplicate workflow within a folder from personal projects', async ({ n8n }) => {
|
||||
const projectId = await n8n.start.fromNewProject();
|
||||
const folder = await n8n.api.projects.createFolder(projectId);
|
||||
const { name: workflowName } = await n8n.api.workflows.createInProject(projectId, {
|
||||
folder: folder.id,
|
||||
});
|
||||
await n8n.navigate.toFolder(folder.id, projectId);
|
||||
|
||||
const workflowCard = n8n.workflows.cards.getWorkflow(workflowName);
|
||||
await n8n.workflows.cards.openCardActions(workflowCard);
|
||||
await n8n.workflows.cards.getCardAction('duplicate').click();
|
||||
|
||||
const duplicatePage = await n8n.start.fromNewPage(async () => {
|
||||
await n8n.modal.clickButton('Duplicate');
|
||||
});
|
||||
|
||||
await duplicatePage.navigate.toFolder(folder.id);
|
||||
const duplicatedName = `${workflowName} copy`;
|
||||
await expect(duplicatePage.workflows.cards.getWorkflow(duplicatedName)).toBeVisible();
|
||||
});
|
||||
|
||||
test('should duplicate workflow within a folder from workflow page', async ({ n8n }) => {
|
||||
const { id: projectId } = await n8n.api.projects.createProject();
|
||||
const folder = await n8n.api.projects.createFolder(projectId);
|
||||
const { name: workflowName, id: workflowId } = await n8n.api.workflows.createInProject(
|
||||
projectId,
|
||||
{
|
||||
folder: folder.id,
|
||||
},
|
||||
);
|
||||
await n8n.navigate.toCanvas(workflowId);
|
||||
|
||||
await n8n.workflowSettingsModal.getWorkflowMenu().click();
|
||||
await n8n.workflowSettingsModal.getDuplicateMenuItem().click();
|
||||
const duplicatePage = await n8n.start.fromNewPage(async () => {
|
||||
await n8n.modal.clickButton('Duplicate');
|
||||
});
|
||||
|
||||
const duplicatedName = `${workflowName} copy`;
|
||||
await duplicatePage.navigate.toFolder(folder.id, projectId);
|
||||
await expect(duplicatePage.workflows.cards.getWorkflow(duplicatedName)).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Drag and drop', () => {
|
||||
test('should drag and drop folders into folders', async ({ n8n }) => {
|
||||
const { id: projectId } = await n8n.api.projects.createProject('Drag and Drop Test');
|
||||
await n8n.navigate.toProject(projectId);
|
||||
const targetFolder = await n8n.api.projects.createFolder(projectId, 'Drag me');
|
||||
const destinationFolder = await n8n.api.projects.createFolder(
|
||||
projectId,
|
||||
'Folder Destination',
|
||||
);
|
||||
|
||||
const sourceFolderCard = n8n.workflows.cards.getFolder(targetFolder.name);
|
||||
const destinationFolderCard = n8n.workflows.cards.getFolder(destinationFolder.name);
|
||||
|
||||
await n8n.interactions.precisionDragToTarget(sourceFolderCard, destinationFolderCard);
|
||||
|
||||
await expect(
|
||||
n8n.notifications.getNotificationByTitleOrContent(
|
||||
`${targetFolder.name} has been moved to ${destinationFolder.name}`,
|
||||
),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(n8n.workflows.cards.getFolders()).toHaveCount(1);
|
||||
|
||||
await n8n.workflows.cards.openFolder(destinationFolder.name);
|
||||
await expect(n8n.workflows.cards.getFolder(targetFolder.name)).toBeVisible();
|
||||
});
|
||||
|
||||
test('should drag and drop folders into project root breadcrumb', async ({ n8n }) => {
|
||||
const project = await n8n.api.projects.createProject('Drag to root test');
|
||||
await n8n.navigate.toProject(project.id);
|
||||
const parentFolder = await n8n.api.projects.createFolder(project.id, 'Parent Folder');
|
||||
const targetFolder = await n8n.api.projects.createFolder(
|
||||
project.id,
|
||||
'To Project root',
|
||||
parentFolder.id,
|
||||
);
|
||||
|
||||
await n8n.navigate.toFolder(parentFolder.id, project.id);
|
||||
|
||||
const sourceFolderCard = n8n.workflows.cards.getFolder(targetFolder.name);
|
||||
const projectBreadcrumb = n8n.breadcrumbs.getHomeProjectBreadcrumb();
|
||||
|
||||
await n8n.interactions.precisionDragToTarget(sourceFolderCard, projectBreadcrumb);
|
||||
|
||||
await expect(
|
||||
n8n.notifications.getNotificationByTitleOrContent(
|
||||
`${targetFolder.name} has been moved to ${project.name}`,
|
||||
),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(n8n.workflows.cards.getFolders()).toHaveCount(0);
|
||||
|
||||
await n8n.navigate.toProject(project.id);
|
||||
await expect(n8n.workflows.cards.getFolder(targetFolder.name)).toBeVisible();
|
||||
});
|
||||
|
||||
test('should drag and drop workflows into folders', async ({ n8n }) => {
|
||||
const { id: projectId } = await n8n.api.projects.createProject('Drag and Drop WF Test');
|
||||
const { name: workflowName } = await n8n.api.workflows.createInProject(projectId, {});
|
||||
const destinationFolder = await n8n.api.projects.createFolder(projectId);
|
||||
await n8n.navigate.toProject(projectId);
|
||||
|
||||
const sourceWorkflowCard = n8n.workflows.cards.getWorkflow(workflowName);
|
||||
const destinationFolderCard = n8n.workflows.cards.getFolder(destinationFolder.name);
|
||||
|
||||
await n8n.interactions.precisionDragToTarget(sourceWorkflowCard, destinationFolderCard);
|
||||
|
||||
await expect(
|
||||
n8n.notifications.getNotificationByTitleOrContent(
|
||||
`${workflowName} has been moved to ${destinationFolder.name}`,
|
||||
),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(n8n.workflows.cards.getWorkflows()).toHaveCount(0);
|
||||
|
||||
await n8n.workflows.cards.openFolder(destinationFolder.name);
|
||||
await expect(n8n.workflows.cards.getWorkflow(workflowName)).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,187 @@
|
||||
import { test, expect } from '../../../fixtures/base';
|
||||
|
||||
test.describe('Folders - Basic Operations', {
|
||||
annotation: [
|
||||
{ type: 'owner', description: 'Identity & Access' },
|
||||
],
|
||||
}, () => {
|
||||
const FOLDER_CREATED_NOTIFICATION = 'Folder created';
|
||||
test('should create folder from the workflows page using addResource dropdown', async ({
|
||||
n8n,
|
||||
}) => {
|
||||
await n8n.start.fromNewProject();
|
||||
const folderName = await n8n.workflows.addFolder();
|
||||
await expect(n8n.workflows.cards.getFolder(folderName)).toBeVisible();
|
||||
await expect(n8n.workflows.cards.getFolders()).toHaveCount(1);
|
||||
});
|
||||
|
||||
test('should create folder from inside a folder', async ({ n8n }) => {
|
||||
const projectId = await n8n.start.fromNewProject();
|
||||
const folder = await n8n.api.projects.createFolder(projectId);
|
||||
const folderName = folder.name;
|
||||
await n8n.workflows.cards.openFolder(folderName);
|
||||
const childFolderName = await n8n.workflows.addFolder();
|
||||
await expect(n8n.workflows.cards.getFolder(childFolderName)).toBeVisible();
|
||||
});
|
||||
|
||||
test('should create a folder from breadcrumbs', async ({ n8n }) => {
|
||||
const projectId = await n8n.start.fromNewProject();
|
||||
const folder = await n8n.api.projects.createFolder(projectId);
|
||||
const folderName = folder.name;
|
||||
await n8n.workflows.cards.openFolder(folderName);
|
||||
// This opens the folder actions menu
|
||||
await n8n.workflows.getFolderBreadcrumbsActions().click();
|
||||
|
||||
await n8n.workflows.getFolderBreadcrumbsAction('create').click();
|
||||
const childFolderName = 'My Child Folder';
|
||||
await n8n.workflows.fillFolderModal(childFolderName);
|
||||
|
||||
await expect(n8n.workflows.cards.getFolder(childFolderName)).toBeVisible();
|
||||
});
|
||||
|
||||
test('should create a folder from the list header button', async ({ n8n }) => {
|
||||
const projectId = await n8n.start.fromNewProject();
|
||||
await n8n.api.projects.createFolder(projectId);
|
||||
await n8n.workflows.addFolderButton().click();
|
||||
const childFolderName = 'My Child Folder';
|
||||
await n8n.workflows.fillFolderModal(childFolderName);
|
||||
|
||||
await expect(n8n.workflows.cards.getFolder(childFolderName)).toBeVisible();
|
||||
});
|
||||
|
||||
test('should create a folder from the card dropdown', async ({ n8n }) => {
|
||||
const projectId = await n8n.start.fromNewProject();
|
||||
const folder = await n8n.api.projects.createFolder(projectId);
|
||||
const folderName = folder.name;
|
||||
const folderCard = n8n.workflows.cards.getFolder(folderName);
|
||||
await n8n.workflows.cards.openCardActions(folderCard);
|
||||
await n8n.workflows.cards.getCardAction('create').click();
|
||||
const childFolderName = 'My Child Folder';
|
||||
await n8n.workflows.fillFolderModal(childFolderName);
|
||||
await expect(n8n.workflows.cards.getFolder(childFolderName)).toBeVisible();
|
||||
});
|
||||
|
||||
test('should navigate from nested folder back to project root via breadcrumbs', async ({
|
||||
n8n,
|
||||
}) => {
|
||||
const projectId = await n8n.start.fromNewProject();
|
||||
const parentFolder = await n8n.api.projects.createFolder(projectId);
|
||||
const childFolder = await n8n.api.projects.createFolder(
|
||||
projectId,
|
||||
'Child Folder',
|
||||
parentFolder.id,
|
||||
);
|
||||
const grandChildFolder = await n8n.api.projects.createFolder(
|
||||
projectId,
|
||||
'Grand Child Folder',
|
||||
childFolder.id,
|
||||
);
|
||||
|
||||
await n8n.navigate.toFolder(grandChildFolder.id, projectId);
|
||||
await expect(n8n.breadcrumbs.getCurrentBreadcrumb()).toContainText(grandChildFolder.name);
|
||||
|
||||
// Hidden breadcrumb should be visible because not all breadcrumbs can fit in the UI
|
||||
await n8n.breadcrumbs.getHiddenBreadcrumbs().click();
|
||||
await expect(n8n.breadcrumbs.getActionToggleDropdown(parentFolder.id)).toBeVisible();
|
||||
|
||||
await n8n.breadcrumbs.getBreadcrumb(childFolder.name).click();
|
||||
await expect(n8n.workflows.cards.getFolder(grandChildFolder.name)).toBeVisible();
|
||||
|
||||
await n8n.breadcrumbs.getBreadcrumb(parentFolder.name).click();
|
||||
await expect(n8n.workflows.cards.getFolder(childFolder.name)).toBeVisible();
|
||||
|
||||
await n8n.breadcrumbs.getHomeProjectBreadcrumb().click();
|
||||
await expect(n8n.workflows.cards.getFolder(parentFolder.name)).toBeVisible();
|
||||
});
|
||||
|
||||
test('should find nested folders through search from project root', async ({ n8n }) => {
|
||||
const projectId = await n8n.start.fromNewProject();
|
||||
const rootFolder = await n8n.api.projects.createFolder(projectId, 'Root Test Folder');
|
||||
const childFolder = await n8n.api.projects.createFolder(
|
||||
projectId,
|
||||
'Child Test Folder',
|
||||
rootFolder.id,
|
||||
);
|
||||
const grandChildFolder = await n8n.api.projects.createFolder(
|
||||
projectId,
|
||||
'Grand Child Test Folder',
|
||||
childFolder.id,
|
||||
);
|
||||
|
||||
// Start at project root
|
||||
await n8n.navigate.toProject(projectId);
|
||||
|
||||
// Search for "Grand Child" from root - should find the deeply nested folder
|
||||
await n8n.workflows.search('Grand Child');
|
||||
|
||||
// Verify the grandchild folder appears in search results
|
||||
await expect(n8n.workflows.cards.getFolder(grandChildFolder.name)).toBeVisible();
|
||||
|
||||
// Verify other folders are filtered out
|
||||
await expect(n8n.workflows.cards.getFolder(rootFolder.name)).toBeHidden();
|
||||
await expect(n8n.workflows.cards.getFolder(childFolder.name)).toBeHidden();
|
||||
|
||||
// Clear search and verify all folders are shown again
|
||||
await n8n.workflows.clearSearch();
|
||||
await expect(n8n.workflows.cards.getFolder(rootFolder.name)).toBeVisible();
|
||||
await expect(n8n.workflows.cards.getFolder(childFolder.name)).toBeHidden(); // Child is inside root
|
||||
await expect(n8n.workflows.cards.getFolder(grandChildFolder.name)).toBeHidden(); // Grandchild is inside child
|
||||
});
|
||||
|
||||
test('should create workflow in a folder', async ({ n8n }) => {
|
||||
const { id: projectId } = await n8n.api.projects.createProject();
|
||||
const folder = await n8n.api.projects.createFolder(projectId);
|
||||
await n8n.navigate.toFolder(folder.id, projectId);
|
||||
await n8n.workflows.addResource.workflow();
|
||||
// Change name to trigger save
|
||||
await n8n.canvas.setWorkflowName('Workflow in Folder');
|
||||
await n8n.page.keyboard.press('Enter');
|
||||
await n8n.canvas.waitForSaveWorkflowCompleted();
|
||||
await n8n.navigate.toFolder(folder.id, projectId);
|
||||
await expect(n8n.workflows.cards.getWorkflows()).toBeVisible();
|
||||
});
|
||||
|
||||
test('should not create folders with invalid names in the UI', async ({ n8n }) => {
|
||||
await n8n.start.fromNewProject();
|
||||
const invalidNames = ['folder[test]', 'folder/test'];
|
||||
const errorMessage = 'Folder name cannot contain the following characters';
|
||||
const emptyErrorMessage = 'Folder name cannot be empty';
|
||||
const tooLongErrorMessage = 'Folder name cannot be longer than 128 characters';
|
||||
const dotsErrorMessage = 'Folder name cannot contain only dots';
|
||||
await n8n.workflows.addResource.folder();
|
||||
|
||||
for (const invalidName of invalidNames) {
|
||||
await n8n.modal.fillInput(invalidName);
|
||||
await expect(n8n.modal.container.getByText(errorMessage, { exact: false })).toBeVisible();
|
||||
}
|
||||
|
||||
await n8n.modal.fillInput('');
|
||||
await expect(n8n.modal.container.getByText(emptyErrorMessage)).toBeVisible();
|
||||
|
||||
await n8n.modal.fillInput('a'.repeat(129));
|
||||
await expect(n8n.modal.container.getByText(tooLongErrorMessage)).toBeVisible();
|
||||
|
||||
await n8n.modal.fillInput('...');
|
||||
await expect(n8n.modal.container.getByText(dotsErrorMessage)).toBeVisible();
|
||||
});
|
||||
|
||||
test('should navigate to a folder using card actions', async ({ n8n }) => {
|
||||
const projectId = await n8n.start.fromNewProject();
|
||||
const folder = await n8n.api.projects.createFolder(projectId);
|
||||
const folderName = folder.name;
|
||||
const folderCard = n8n.workflows.cards.getFolder(folderName);
|
||||
await n8n.workflows.cards.openCardActions(folderCard);
|
||||
await n8n.workflows.cards.getCardAction('open').click();
|
||||
await expect(n8n.breadcrumbs.getCurrentBreadcrumb()).toContainText(folderName);
|
||||
});
|
||||
|
||||
test('should navigate to a folder using notification', async ({ n8n }) => {
|
||||
await n8n.start.fromNewProject();
|
||||
const folderName = await n8n.workflows.addFolder();
|
||||
await n8n.notifications
|
||||
.getNotificationByTitleOrContent(FOLDER_CREATED_NOTIFICATION)
|
||||
.getByText('Open folder')
|
||||
.click();
|
||||
await expect(n8n.breadcrumbs.getCurrentBreadcrumb()).toContainText(folderName);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,294 @@
|
||||
import { test, expect } from '../../../fixtures/base';
|
||||
|
||||
test.describe('Folders - Operations', {
|
||||
annotation: [
|
||||
{ type: 'owner', description: 'Identity & Access' },
|
||||
],
|
||||
}, () => {
|
||||
test.describe('Rename and delete folders', () => {
|
||||
test('should rename folder from breadcrumb dropdown', async ({ n8n }) => {
|
||||
await n8n.start.fromNewProject();
|
||||
const folderName = await n8n.workflows.addFolder();
|
||||
const folderCard = n8n.workflows.cards.getFolder(folderName);
|
||||
await n8n.workflows.cards.openCardActions(folderCard);
|
||||
await n8n.workflows.cards.getCardAction('open').click();
|
||||
await n8n.breadcrumbs.renameCurrentBreadcrumb('Renamed');
|
||||
await n8n.breadcrumbs.getHomeProjectBreadcrumb().click();
|
||||
await expect(n8n.workflows.cards.getFolder('Renamed')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should rename folder from card dropdown', async ({ n8n }) => {
|
||||
await n8n.start.fromNewProject();
|
||||
const folderName = await n8n.workflows.addFolder();
|
||||
const folderCard = n8n.workflows.cards.getFolder(folderName);
|
||||
await n8n.workflows.cards.openCardActions(folderCard);
|
||||
await n8n.workflows.cards.getCardAction('rename').click();
|
||||
await n8n.workflows.fillFolderModal('Renamed', 'Rename');
|
||||
await expect(n8n.workflows.cards.getFolder('Renamed')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should delete empty folder from card dropdown', async ({ n8n }) => {
|
||||
await n8n.start.fromNewProject();
|
||||
const folderName = await n8n.workflows.addFolder();
|
||||
await n8n.workflows.cards.deleteFolder(folderName);
|
||||
await expect(n8n.workflows.cards.getFolder(folderName)).toBeHidden();
|
||||
});
|
||||
|
||||
test('should delete empty folder from breadcrumb dropdown', async ({ n8n }) => {
|
||||
await n8n.start.fromNewProject();
|
||||
const folderName = await n8n.workflows.addFolder();
|
||||
await n8n.workflows.cards.openFolder(folderName);
|
||||
await n8n.breadcrumbs.getFolderBreadcrumbsActionToggle().click();
|
||||
await n8n.breadcrumbs.getActionToggleDropdown('delete').click();
|
||||
await expect(n8n.workflows.cards.getFolder(folderName)).toBeHidden();
|
||||
});
|
||||
|
||||
test('should warn before deleting non-empty folder from breadcrumb dropdown', async ({
|
||||
n8n,
|
||||
}) => {
|
||||
const { id: projectId } = await n8n.api.projects.createProject();
|
||||
const folder = await n8n.api.projects.createFolder(projectId);
|
||||
await n8n.api.workflows.createInProject(projectId, {
|
||||
folder: folder.id,
|
||||
});
|
||||
await n8n.navigate.toFolder(folder.id, projectId);
|
||||
await n8n.breadcrumbs.getFolderBreadcrumbsActionToggle().click();
|
||||
await n8n.breadcrumbs.getActionToggleDropdown('delete').click();
|
||||
await expect(n8n.workflows.deleteFolderModal()).toBeVisible();
|
||||
await expect(n8n.workflows.deleteModalConfirmButton()).toBeDisabled();
|
||||
});
|
||||
|
||||
test('should warn before deleting non-empty folder from card dropdown', async ({ n8n }) => {
|
||||
const { id: projectId } = await n8n.api.projects.createProject();
|
||||
const folder = await n8n.api.projects.createFolder(projectId);
|
||||
await n8n.api.workflows.createInProject(projectId, {
|
||||
folder: folder.id,
|
||||
});
|
||||
await n8n.navigate.toProject(projectId);
|
||||
const folderCard = n8n.workflows.cards.getFolder(folder.name);
|
||||
await n8n.workflows.cards.openCardActions(folderCard);
|
||||
await n8n.workflows.cards.getCardAction('delete').click();
|
||||
await expect(n8n.workflows.deleteFolderModal()).toBeVisible();
|
||||
await expect(n8n.workflows.deleteModalConfirmButton()).toBeDisabled();
|
||||
});
|
||||
|
||||
test('should transfer contents when deleting non-empty folder - from card dropdown', async ({
|
||||
n8n,
|
||||
}) => {
|
||||
const { id: projectId } = await n8n.api.projects.createProject();
|
||||
const folderToDelete = await n8n.api.projects.createFolder(projectId);
|
||||
await n8n.api.workflows.createInProject(projectId, {
|
||||
folder: folderToDelete.id,
|
||||
});
|
||||
const destinationFolder = await n8n.api.projects.createFolder(projectId);
|
||||
|
||||
await n8n.navigate.toProject(projectId);
|
||||
|
||||
const folderCard = n8n.workflows.cards.getFolder(folderToDelete.name);
|
||||
await n8n.workflows.cards.openCardActions(folderCard);
|
||||
await n8n.workflows.cards.getCardAction('delete').click();
|
||||
await n8n.workflows.deleteModalTransferRadioButton().click();
|
||||
await n8n.workflows.transferFolderDropdown().click();
|
||||
await n8n.workflows.transferFolderOption(destinationFolder.name).click();
|
||||
await n8n.workflows.deleteModalConfirmButton().click();
|
||||
|
||||
await expect(
|
||||
n8n.notifications.getNotificationByTitleOrContent('Folder deleted'),
|
||||
).toBeVisible();
|
||||
|
||||
await n8n.navigate.toFolder(destinationFolder.id, projectId);
|
||||
await expect(n8n.workflows.cards.getWorkflows()).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Move folders and workflows', () => {
|
||||
test('should move empty folder to another folder - from folder card action', async ({
|
||||
n8n,
|
||||
}) => {
|
||||
const { id: projectId } = await n8n.api.projects.createProject();
|
||||
const sourceFolder = await n8n.api.projects.createFolder(projectId);
|
||||
const destinationFolder = await n8n.api.projects.createFolder(projectId);
|
||||
|
||||
await n8n.navigate.toProject(projectId);
|
||||
|
||||
const sourceFolderCard = n8n.workflows.cards.getFolder(sourceFolder.name);
|
||||
await n8n.workflows.cards.openCardActions(sourceFolderCard);
|
||||
await n8n.workflows.cards.getCardAction('move').click();
|
||||
|
||||
await expect(n8n.workflows.moveFolderModal()).toBeVisible();
|
||||
await n8n.workflows.moveFolderDropdown().click();
|
||||
await n8n.workflows.moveFolderOption(destinationFolder.name).click();
|
||||
await n8n.workflows.moveFolderConfirmButton().click();
|
||||
|
||||
await expect(
|
||||
n8n.notifications.getNotificationByTitleOrContent('Successfully moved folder'),
|
||||
).toBeVisible();
|
||||
|
||||
await n8n.navigate.toFolder(destinationFolder.id, projectId);
|
||||
await expect(n8n.workflows.cards.getFolder(sourceFolder.name)).toBeVisible();
|
||||
});
|
||||
|
||||
test('should move folder with contents to another folder - from folder card action', async ({
|
||||
n8n,
|
||||
}) => {
|
||||
const { id: projectId } = await n8n.api.projects.createProject();
|
||||
const sourceFolder = await n8n.api.projects.createFolder(projectId);
|
||||
const destinationFolder = await n8n.api.projects.createFolder(projectId);
|
||||
|
||||
await n8n.api.workflows.createInProject(projectId, {
|
||||
folder: sourceFolder.id,
|
||||
});
|
||||
|
||||
await n8n.navigate.toProject(projectId);
|
||||
|
||||
const sourceFolderCard = n8n.workflows.cards.getFolder(sourceFolder.name);
|
||||
await n8n.workflows.cards.openCardActions(sourceFolderCard);
|
||||
await n8n.workflows.cards.getCardAction('move').click();
|
||||
|
||||
await expect(n8n.workflows.moveFolderModal()).toBeVisible();
|
||||
await n8n.workflows.moveFolderDropdown().click();
|
||||
await n8n.workflows.moveFolderOption(destinationFolder.name).click();
|
||||
await n8n.workflows.moveFolderConfirmButton().click();
|
||||
|
||||
await expect(
|
||||
n8n.notifications.getNotificationByTitleOrContent('Successfully moved folder'),
|
||||
).toBeVisible();
|
||||
|
||||
await n8n.navigate.toFolder(destinationFolder.id, projectId);
|
||||
await expect(n8n.workflows.cards.getFolder(sourceFolder.name)).toBeVisible();
|
||||
await n8n.workflows.cards.openFolder(sourceFolder.name);
|
||||
await expect(n8n.workflows.cards.getWorkflows()).toBeVisible();
|
||||
});
|
||||
|
||||
test('should move empty folder to another folder - from list breadcrumbs', async ({ n8n }) => {
|
||||
const { id: projectId } = await n8n.api.projects.createProject();
|
||||
const sourceFolder = await n8n.api.projects.createFolder(projectId);
|
||||
const destinationFolder = await n8n.api.projects.createFolder(projectId);
|
||||
|
||||
await n8n.navigate.toFolder(sourceFolder.id, projectId);
|
||||
await n8n.breadcrumbs.getFolderBreadcrumbsActionToggle().click();
|
||||
await n8n.breadcrumbs.getActionToggleDropdown('move').click();
|
||||
|
||||
await expect(n8n.workflows.moveFolderModal()).toBeVisible();
|
||||
await n8n.workflows.moveFolderDropdown().click();
|
||||
await n8n.workflows.moveFolderOption(destinationFolder.name).click();
|
||||
await n8n.workflows.moveFolderConfirmButton().click();
|
||||
|
||||
await n8n.navigate.toFolder(destinationFolder.id, projectId);
|
||||
await expect(n8n.workflows.cards.getFolder(sourceFolder.name)).toBeVisible();
|
||||
});
|
||||
|
||||
test('should move folder with contents to another folder - from list dropdown', async ({
|
||||
n8n,
|
||||
}) => {
|
||||
const { id: projectId } = await n8n.api.projects.createProject();
|
||||
const sourceFolder = await n8n.api.projects.createFolder(projectId);
|
||||
const destinationFolder = await n8n.api.projects.createFolder(projectId);
|
||||
|
||||
await n8n.api.workflows.createInProject(projectId, {
|
||||
folder: sourceFolder.id,
|
||||
});
|
||||
|
||||
await n8n.navigate.toFolder(sourceFolder.id, projectId);
|
||||
await n8n.breadcrumbs.getFolderBreadcrumbsActionToggle().click();
|
||||
await n8n.breadcrumbs.getActionToggleDropdown('move').click();
|
||||
|
||||
await expect(n8n.workflows.moveFolderModal()).toBeVisible();
|
||||
await n8n.workflows.moveFolderDropdown().click();
|
||||
await n8n.workflows.moveFolderOption(destinationFolder.name).click();
|
||||
await n8n.workflows.moveFolderConfirmButton().click();
|
||||
|
||||
await n8n.navigate.toFolder(destinationFolder.id, projectId);
|
||||
await expect(n8n.workflows.cards.getFolder(sourceFolder.name)).toBeVisible();
|
||||
await n8n.workflows.cards.openFolder(sourceFolder.name);
|
||||
await expect(n8n.workflows.cards.getWorkflows()).toBeVisible();
|
||||
});
|
||||
|
||||
test('should move folder to project root - from folder card action', async ({ n8n }) => {
|
||||
const project = await n8n.api.projects.createProject();
|
||||
const parentFolder = await n8n.api.projects.createFolder(project.id);
|
||||
const childFolderName = 'Child Folder';
|
||||
const childFolder = await n8n.api.projects.createFolder(
|
||||
project.id,
|
||||
childFolderName,
|
||||
parentFolder.id,
|
||||
);
|
||||
|
||||
await n8n.navigate.toFolder(parentFolder.id, project.id);
|
||||
|
||||
const childFolderCard = n8n.workflows.cards.getFolder(childFolder.name);
|
||||
await n8n.workflows.cards.openCardActions(childFolderCard);
|
||||
await n8n.workflows.cards.getCardAction('move').click();
|
||||
|
||||
await expect(n8n.workflows.moveFolderModal()).toBeVisible();
|
||||
await n8n.workflows.moveFolderDropdown().click();
|
||||
|
||||
const rootOption = 'No folder (project root)';
|
||||
await n8n.workflows.moveFolderOption(rootOption).click();
|
||||
await n8n.workflows.moveFolderConfirmButton().click();
|
||||
|
||||
await expect(
|
||||
n8n.notifications.getNotificationByTitleOrContent('Successfully moved folder'),
|
||||
).toBeVisible();
|
||||
|
||||
await n8n.navigate.toProject(project.id);
|
||||
await expect(n8n.workflows.cards.getFolder(childFolder.name)).toBeVisible();
|
||||
});
|
||||
|
||||
test('should move workflow from project root to folder', async ({ n8n }) => {
|
||||
const { id: projectId } = await n8n.api.projects.createProject();
|
||||
const destinationFolder = await n8n.api.projects.createFolder(projectId);
|
||||
|
||||
await n8n.api.workflows.createInProject(projectId);
|
||||
|
||||
await n8n.navigate.toProject(projectId);
|
||||
|
||||
const workflowCard = n8n.workflows.cards.getWorkflows().first();
|
||||
await n8n.workflows.cards.openCardActions(workflowCard);
|
||||
await n8n.workflows.cards.getCardAction('moveToFolder').click();
|
||||
|
||||
await expect(n8n.workflows.moveFolderModal()).toBeVisible();
|
||||
await n8n.workflows.moveFolderDropdown().click();
|
||||
await n8n.workflows.moveFolderOption(destinationFolder.name).click();
|
||||
await n8n.workflows.moveFolderConfirmButton().click();
|
||||
|
||||
await expect(
|
||||
n8n.notifications.getNotificationByTitleOrContent('Successfully moved workflow'),
|
||||
).toBeVisible();
|
||||
|
||||
await n8n.navigate.toFolder(destinationFolder.id, projectId);
|
||||
await expect(n8n.workflows.cards.getWorkflows()).toBeVisible();
|
||||
});
|
||||
|
||||
test('should move workflow to another folder', async ({ n8n }) => {
|
||||
const { id: projectId } = await n8n.api.projects.createProject();
|
||||
const sourceFolder = await n8n.api.projects.createFolder(projectId);
|
||||
const destinationFolder = await n8n.api.projects.createFolder(projectId);
|
||||
|
||||
const { name: workflowName } = await n8n.api.workflows.createInProject(projectId, {
|
||||
folder: sourceFolder.id,
|
||||
});
|
||||
|
||||
await n8n.navigate.toFolder(sourceFolder.id, projectId);
|
||||
|
||||
const workflowCard = n8n.workflows.cards.getWorkflow(workflowName);
|
||||
await n8n.workflows.cards.openCardActions(workflowCard);
|
||||
await n8n.workflows.cards.getCardAction('moveToFolder').click();
|
||||
|
||||
await expect(n8n.workflows.moveFolderModal()).toBeVisible();
|
||||
await n8n.workflows.moveFolderDropdown().click();
|
||||
await n8n.workflows.moveFolderOption(destinationFolder.name).click();
|
||||
await n8n.workflows.moveFolderConfirmButton().click();
|
||||
|
||||
await expect(
|
||||
n8n.notifications.getNotificationByTitleOrContent('Successfully moved workflow'),
|
||||
).toBeVisible();
|
||||
|
||||
await n8n.navigate.toFolder(destinationFolder.id, projectId);
|
||||
await expect(n8n.workflows.cards.getWorkflow(workflowName)).toBeVisible();
|
||||
|
||||
await n8n.navigate.toFolder(sourceFolder.id, projectId);
|
||||
await expect(n8n.workflows.cards.getWorkflow(workflowName)).toBeHidden();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,222 @@
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import { test, expect } from '../../../fixtures/base';
|
||||
|
||||
test.describe('Project Settings - Member Management', {
|
||||
annotation: [
|
||||
{ type: 'owner', description: 'Identity & Access' },
|
||||
],
|
||||
}, () => {
|
||||
test.beforeEach(async ({ n8n }) => {
|
||||
await n8n.goHome();
|
||||
});
|
||||
|
||||
test('should display project settings page with correct layout @auth:owner', async ({ n8n }) => {
|
||||
// Create a new project
|
||||
const projectName = `UI Test ${nanoid(8)}`;
|
||||
const { projectId } = await n8n.projectComposer.createProject(projectName);
|
||||
|
||||
// Navigate to project settings
|
||||
await n8n.navigate.toProjectSettings(projectId);
|
||||
await expect(n8n.projectSettings.getTitle()).toHaveText(projectName);
|
||||
|
||||
// Verify basic project settings form elements are visible (inner controls)
|
||||
await expect(n8n.projectSettings.getNameInput()).toBeVisible();
|
||||
await expect(n8n.projectSettings.getDescriptionTextarea()).toBeVisible();
|
||||
await n8n.projectSettings.expectMembersSelectIsVisible();
|
||||
|
||||
// Verify members table is visible when there are members
|
||||
await n8n.projectSettings.expectTableIsVisible();
|
||||
|
||||
// Initially should have only the owner (current user)
|
||||
await n8n.projectSettings.expectTableHasMemberCount(1);
|
||||
|
||||
// Verify save/cancel buttons are disabled initially (no changes)
|
||||
await expect(n8n.projectSettings.getSaveButton()).toBeDisabled();
|
||||
await expect(n8n.projectSettings.getCancelButton()).toBeDisabled();
|
||||
|
||||
// Delete button should always be visible
|
||||
await expect(n8n.projectSettings.getDeleteButton()).toBeVisible();
|
||||
});
|
||||
|
||||
test('should allow editing project name and description @auth:owner', async ({ n8n }) => {
|
||||
// Create a new project
|
||||
const projectName = `Edit Test ${nanoid(8)}`;
|
||||
const { projectId } = await n8n.projectComposer.createProject(projectName);
|
||||
|
||||
// Navigate to project settings
|
||||
await n8n.navigate.toProjectSettings(projectId);
|
||||
await expect(n8n.projectSettings.getTitle()).toHaveText(projectName);
|
||||
|
||||
// Update project name
|
||||
const newName = 'Updated Project Name';
|
||||
await n8n.projectSettings.fillProjectName(newName);
|
||||
|
||||
// Update project description
|
||||
const newDescription = 'This is an updated project description.';
|
||||
await n8n.projectSettings.fillProjectDescription(newDescription);
|
||||
|
||||
// Save changes
|
||||
await n8n.projectSettings.clickSaveButton();
|
||||
|
||||
// Wait for success notification
|
||||
await expect(
|
||||
n8n.page.getByText('Project Updated Project Name saved successfully', { exact: false }),
|
||||
).toBeVisible();
|
||||
|
||||
// Verify the form shows the updated values
|
||||
await n8n.projectSettings.expectProjectNameValue(newName);
|
||||
await n8n.projectSettings.expectProjectDescriptionValue(newDescription);
|
||||
});
|
||||
|
||||
test('should display members table with correct structure @auth:owner', async ({ n8n }) => {
|
||||
// Create a new project
|
||||
const projectName = `Table Structure ${nanoid(8)}`;
|
||||
const { projectId } = await n8n.projectComposer.createProject(projectName);
|
||||
|
||||
// Navigate to project settings
|
||||
await n8n.navigate.toProjectSettings(projectId);
|
||||
await expect(n8n.projectSettings.getTitle()).toHaveText(projectName);
|
||||
|
||||
const table = n8n.projectSettings.getMembersTable();
|
||||
|
||||
// Verify table headers are present
|
||||
await expect(table.getByText('User')).toBeVisible();
|
||||
await expect(table.getByText('Role')).toBeVisible();
|
||||
|
||||
// Verify the owner is displayed in the table
|
||||
const memberRows = table.locator('tbody tr');
|
||||
await expect(memberRows).toHaveCount(1);
|
||||
|
||||
// Verify owner cannot change their own role
|
||||
const ownerRow = memberRows.first();
|
||||
const roleDropdown = ownerRow.getByTestId('project-member-role-dropdown');
|
||||
await expect(roleDropdown).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('should display role dropdown for members but not for current user @auth:owner', async ({
|
||||
n8n,
|
||||
}) => {
|
||||
// Create a new project
|
||||
const projectName = `Role Dropdown ${nanoid(8)}`;
|
||||
const { projectId } = await n8n.projectComposer.createProject(projectName);
|
||||
|
||||
// Navigate to project settings
|
||||
await n8n.navigate.toProjectSettings(projectId);
|
||||
await expect(n8n.projectSettings.getTitle()).toHaveText(projectName);
|
||||
|
||||
// Current user (owner) should not have a role dropdown
|
||||
const currentUserRow = n8n.page.locator('tbody tr').first();
|
||||
await expect(currentUserRow.getByTestId('project-member-role-dropdown')).toHaveCount(0);
|
||||
|
||||
// The role should be displayed as static text for the current user
|
||||
await expect(currentUserRow.getByText('Admin')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should show project settings form validation @auth:owner', async ({ n8n }) => {
|
||||
// Create a new project
|
||||
const projectName = `Validation ${nanoid(8)}`;
|
||||
const { projectId } = await n8n.projectComposer.createProject(projectName);
|
||||
|
||||
// Navigate to project settings
|
||||
await n8n.navigate.toProjectSettings(projectId);
|
||||
await expect(n8n.projectSettings.getTitle()).toHaveText(projectName);
|
||||
|
||||
// Clear the project name (required field)
|
||||
await n8n.projectSettings.fillProjectName('');
|
||||
|
||||
// Save button should be disabled when required field is empty
|
||||
await expect(n8n.projectSettings.getSaveButton()).toBeDisabled();
|
||||
|
||||
// Fill in a valid name
|
||||
await n8n.projectSettings.fillProjectName('Valid Project Name');
|
||||
|
||||
// Save button should now be enabled
|
||||
await expect(n8n.projectSettings.getSaveButton()).toBeEnabled();
|
||||
});
|
||||
|
||||
test('should handle unsaved changes state @auth:owner', async ({ n8n }) => {
|
||||
// Create a new project
|
||||
const projectName = `Unsaved Changes ${nanoid(8)}`;
|
||||
const { projectId } = await n8n.projectComposer.createProject(projectName);
|
||||
|
||||
// Navigate to project settings
|
||||
await n8n.navigate.toProjectSettings(projectId);
|
||||
await expect(n8n.projectSettings.getTitle()).toHaveText(projectName);
|
||||
|
||||
// Initially, save and cancel buttons should be disabled (no changes)
|
||||
await expect(n8n.projectSettings.getSaveButton()).toBeDisabled();
|
||||
await expect(n8n.projectSettings.getCancelButton()).toBeDisabled();
|
||||
|
||||
// Make a change to the project name
|
||||
await n8n.projectSettings.fillProjectName('Modified Name');
|
||||
|
||||
// Save and cancel buttons should now be enabled
|
||||
await expect(n8n.projectSettings.getSaveButton()).toBeEnabled();
|
||||
await expect(n8n.projectSettings.getCancelButton()).toBeEnabled();
|
||||
|
||||
// Cancel changes
|
||||
await n8n.projectSettings.clickCancelButton();
|
||||
|
||||
// Buttons should be disabled again (no changes)
|
||||
await expect(n8n.projectSettings.getSaveButton()).toBeDisabled();
|
||||
await expect(n8n.projectSettings.getCancelButton()).toBeDisabled();
|
||||
});
|
||||
|
||||
test('should display delete project section with warning @auth:owner', async ({ n8n }) => {
|
||||
// Create a new project
|
||||
const projectName = `Delete Test ${nanoid(8)}`;
|
||||
const { projectId } = await n8n.projectComposer.createProject(projectName);
|
||||
|
||||
// Navigate to project settings
|
||||
await n8n.navigate.toProjectSettings(projectId);
|
||||
await expect(n8n.projectSettings.getTitle()).toHaveText(projectName);
|
||||
|
||||
// Scroll to bottom to see delete section
|
||||
await n8n.projectSettings.getDeleteButton().scrollIntoViewIfNeeded();
|
||||
|
||||
// Verify danger section is visible with warning
|
||||
// Copy was updated in UI to use sentence case and expanded description
|
||||
await expect(n8n.page.getByText('Danger zone')).toBeVisible();
|
||||
await expect(
|
||||
n8n.page.getByText(
|
||||
'When deleting a project, you can also choose to move all workflows and credentials to another project.',
|
||||
),
|
||||
).toBeVisible();
|
||||
await expect(n8n.projectSettings.getDeleteButton()).toBeVisible();
|
||||
});
|
||||
|
||||
test('should persist settings after page reload @auth:owner', async ({ n8n }) => {
|
||||
// Create a new project
|
||||
const projectName = `Persistence ${nanoid(8)}`;
|
||||
const { projectId } = await n8n.projectComposer.createProject(projectName);
|
||||
|
||||
// Navigate to project settings
|
||||
await n8n.navigate.toProjectSettings(projectId);
|
||||
await expect(n8n.projectSettings.getTitle()).toHaveText(projectName);
|
||||
|
||||
// Update project details
|
||||
const newProjectName = 'Persisted Project Name';
|
||||
const projectDescription = 'This description should persist after reload';
|
||||
|
||||
await n8n.projectSettings.fillProjectName(newProjectName);
|
||||
await n8n.projectSettings.fillProjectDescription(projectDescription);
|
||||
await n8n.projectSettings.clickSaveButton();
|
||||
|
||||
// Wait for save confirmation (partial match to include project name)
|
||||
await expect(
|
||||
n8n.page.getByText('Project Persisted Project Name saved successfully', { exact: false }),
|
||||
).toBeVisible();
|
||||
|
||||
// Reload the page
|
||||
await n8n.page.reload();
|
||||
await expect(n8n.projectSettings.getTitle()).toHaveText('Persisted Project Name');
|
||||
|
||||
// Verify data persisted
|
||||
await n8n.projectSettings.expectProjectNameValue(newProjectName);
|
||||
await n8n.projectSettings.expectProjectDescriptionValue(projectDescription);
|
||||
|
||||
// Verify table still shows the owner
|
||||
await n8n.projectSettings.expectTableHasMemberCount(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,151 @@
|
||||
import {
|
||||
INSTANCE_ADMIN_CREDENTIALS,
|
||||
INSTANCE_MEMBER_CREDENTIALS,
|
||||
INSTANCE_OWNER_CREDENTIALS,
|
||||
} from '../../../config/test-users';
|
||||
import { test, expect } from '../../../fixtures/base';
|
||||
|
||||
test.use({ capability: { env: { TEST_ISOLATION: 'projects-move-resources' } } });
|
||||
|
||||
test.describe('Projects - Moving Resources @db:reset', {
|
||||
annotation: [
|
||||
{ type: 'owner', description: 'Identity & Access' },
|
||||
],
|
||||
}, () => {
|
||||
test.describe.configure({ mode: 'serial' });
|
||||
|
||||
test.beforeEach(async ({ n8n }) => {
|
||||
await n8n.goHome();
|
||||
// Enable features required for project workflows and moving resources
|
||||
await n8n.api.enableFeature('sharing');
|
||||
await n8n.api.enableFeature('folders');
|
||||
await n8n.api.enableFeature('advancedPermissions');
|
||||
await n8n.api.enableFeature('projectRole:admin');
|
||||
await n8n.api.enableFeature('projectRole:editor');
|
||||
await n8n.api.setMaxTeamProjectsQuota(-1);
|
||||
|
||||
// Create workflow + credential in Home/Personal project
|
||||
await n8n.api.workflows.createWorkflow({
|
||||
name: 'Workflow in Home project',
|
||||
nodes: [],
|
||||
connections: {},
|
||||
active: false,
|
||||
});
|
||||
await n8n.api.credentials.createCredential({
|
||||
name: 'Credential in Home project',
|
||||
type: 'notionApi',
|
||||
data: { apiKey: '1234567890' },
|
||||
});
|
||||
|
||||
// Create Project 1 with resources
|
||||
const project1 = await n8n.api.projects.createProject('Project 1');
|
||||
await n8n.api.workflows.createInProject(project1.id, {
|
||||
name: 'Workflow in Project 1',
|
||||
});
|
||||
await n8n.api.credentials.createCredential({
|
||||
name: 'Credential in Project 1',
|
||||
type: 'notionApi',
|
||||
data: { apiKey: '1234567890' },
|
||||
projectId: project1.id,
|
||||
});
|
||||
|
||||
// Create Project 2 with resources
|
||||
const project2 = await n8n.api.projects.createProject('Project 2');
|
||||
await n8n.api.workflows.createInProject(project2.id, {
|
||||
name: 'Workflow in Project 2',
|
||||
});
|
||||
await n8n.api.credentials.createCredential({
|
||||
name: 'Credential in Project 2',
|
||||
type: 'notionApi',
|
||||
data: { apiKey: '1234567890' },
|
||||
projectId: project2.id,
|
||||
});
|
||||
|
||||
// Navigate to home to load sidebar with new projects
|
||||
await n8n.goHome();
|
||||
});
|
||||
|
||||
test('should move the workflow to expected projects @auth:owner', async ({ n8n }) => {
|
||||
// Move workflow from Personal to Project 2
|
||||
await n8n.sideBar.clickPersonalMenuItem();
|
||||
await expect(n8n.workflows.cards.getWorkflows()).toHaveCount(1);
|
||||
await n8n.workflowComposer.moveToProject('Workflow in Home project', 'Project 2');
|
||||
|
||||
// Verify Personal has 0 workflows
|
||||
await expect(n8n.workflows.cards.getWorkflows()).toHaveCount(0);
|
||||
|
||||
// Move workflow from Project 1 to Project 2
|
||||
await n8n.sideBar.clickProjectMenuItem('Project 1');
|
||||
await expect(n8n.workflows.cards.getWorkflows()).toHaveCount(1);
|
||||
await n8n.workflowComposer.moveToProject('Workflow in Project 1', 'Project 2');
|
||||
|
||||
// Move workflow from Project 2 to member user
|
||||
await n8n.sideBar.clickProjectMenuItem('Project 2');
|
||||
await expect(n8n.workflows.cards.getWorkflows()).toHaveCount(3);
|
||||
await n8n.workflowComposer.moveToProject(
|
||||
'Workflow in Home project',
|
||||
INSTANCE_MEMBER_CREDENTIALS[0].email,
|
||||
null,
|
||||
);
|
||||
|
||||
// Verify Project 2 has 2 workflows remaining
|
||||
await expect(n8n.workflows.cards.getWorkflows()).toHaveCount(2);
|
||||
});
|
||||
|
||||
test('should move the credential to expected projects @auth:owner', async ({ n8n }) => {
|
||||
// Move credential from Project 1 to Project 2
|
||||
await n8n.sideBar.clickProjectMenuItem('Project 1');
|
||||
await n8n.sideBar.clickCredentialsLink();
|
||||
await expect(n8n.credentials.cards.getCredentials()).toHaveCount(1);
|
||||
|
||||
const credentialCard1 = n8n.credentials.cards.getCredential('Credential in Project 1');
|
||||
await n8n.credentials.cards.openCardActions(credentialCard1);
|
||||
await n8n.credentials.cards.getCardAction('move').click();
|
||||
await expect(n8n.resourceMoveModal.getMoveCredentialButton()).toBeDisabled();
|
||||
|
||||
await n8n.resourceMoveModal.getProjectSelectCredential().locator('input').click();
|
||||
await expect(n8n.page.getByRole('option')).toHaveCount(6);
|
||||
await n8n.resourceMoveModal.selectProjectOption('Project 2');
|
||||
await n8n.resourceMoveModal.clickMoveCredentialButton();
|
||||
|
||||
await expect(n8n.credentials.cards.getCredentials()).toHaveCount(0);
|
||||
|
||||
// Move credential from Project 2 to admin user
|
||||
await n8n.sideBar.clickProjectMenuItem('Project 2');
|
||||
await n8n.sideBar.clickCredentialsLink();
|
||||
await expect(n8n.credentials.cards.getCredentials()).toHaveCount(2);
|
||||
|
||||
const credentialCard2 = n8n.credentials.cards.getCredential('Credential in Project 1');
|
||||
await n8n.credentials.cards.openCardActions(credentialCard2);
|
||||
await n8n.credentials.cards.getCardAction('move').click();
|
||||
await expect(n8n.resourceMoveModal.getMoveCredentialButton()).toBeDisabled();
|
||||
|
||||
await n8n.resourceMoveModal.getProjectSelectCredential().locator('input').click();
|
||||
await expect(n8n.page.getByRole('option')).toHaveCount(6);
|
||||
await n8n.resourceMoveModal.selectProjectOption(INSTANCE_ADMIN_CREDENTIALS.email);
|
||||
await n8n.resourceMoveModal.clickMoveCredentialButton();
|
||||
|
||||
await expect(n8n.credentials.cards.getCredentials()).toHaveCount(1);
|
||||
|
||||
// Move credential from admin user (Home) back to owner user
|
||||
await n8n.sideBar.clickHomeMenuItem();
|
||||
await n8n.navigate.toCredentials();
|
||||
await expect(n8n.credentials.cards.getCredentials()).toHaveCount(3);
|
||||
|
||||
const credentialCard3 = n8n.credentials.cards.getCredential('Credential in Project 1');
|
||||
await n8n.credentials.cards.openCardActions(credentialCard3);
|
||||
await n8n.credentials.cards.getCardAction('move').click();
|
||||
await expect(n8n.resourceMoveModal.getMoveCredentialButton()).toBeDisabled();
|
||||
|
||||
await n8n.resourceMoveModal.getProjectSelectCredential().locator('input').click();
|
||||
await expect(n8n.page.getByRole('option')).toHaveCount(6);
|
||||
await n8n.resourceMoveModal.selectProjectOption(INSTANCE_OWNER_CREDENTIALS.email);
|
||||
await n8n.resourceMoveModal.clickMoveCredentialButton();
|
||||
|
||||
// Verify final state: 3 credentials total, 2 with Personal badge
|
||||
await expect(n8n.credentials.cards.getCredentials()).toHaveCount(3);
|
||||
await expect(
|
||||
n8n.credentials.cards.getCredentials().filter({ hasText: 'Personal' }),
|
||||
).toHaveCount(2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,264 @@
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import { INSTANCE_MEMBER_CREDENTIALS } from '../../../config/test-users';
|
||||
import { test, expect } from '../../../fixtures/base';
|
||||
|
||||
test.use({ capability: { env: { TEST_ISOLATION: 'projects' } } });
|
||||
|
||||
const MANUAL_TRIGGER_NODE_NAME = 'Manual Trigger';
|
||||
const EXECUTE_WORKFLOW_NODE_NAME = 'Execute Sub-workflow';
|
||||
const NOTION_NODE_NAME = 'Notion';
|
||||
const EDIT_FIELDS_SET_NODE_NAME = 'Edit Fields (Set)';
|
||||
const NOTION_API_KEY = 'abc123Playwright';
|
||||
|
||||
test.describe('Projects @db:reset', {
|
||||
annotation: [
|
||||
{ type: 'owner', description: 'Identity & Access' },
|
||||
],
|
||||
}, () => {
|
||||
test.describe.configure({ mode: 'serial' });
|
||||
|
||||
test.beforeEach(async ({ n8n }) => {
|
||||
await n8n.goHome();
|
||||
// Enable features required for project workflows and moving resources
|
||||
await n8n.api.enableFeature('sharing');
|
||||
await n8n.api.enableFeature('folders');
|
||||
await n8n.api.enableFeature('advancedPermissions');
|
||||
await n8n.api.enableFeature('projectRole:admin');
|
||||
await n8n.api.enableFeature('projectRole:editor');
|
||||
await n8n.api.setMaxTeamProjectsQuota(-1);
|
||||
});
|
||||
|
||||
test.describe('when starting from scratch', () => {
|
||||
test('should not show project add button and projects to a member if not invited to any project @auth:member', async ({
|
||||
n8n,
|
||||
}) => {
|
||||
await n8n.sideBar.universalAdd();
|
||||
await expect(n8n.sideBar.getProjectButtonInUniversalAdd()).toContainClass('is-disabled');
|
||||
await expect(n8n.sideBar.getProjectMenuItems()).toHaveCount(0);
|
||||
});
|
||||
|
||||
// This test needs empty credentials list - must run before tests that create credentials
|
||||
test('should allow changing an inaccessible credential when the workflow was moved to a team project @auth:owner', async ({
|
||||
n8n,
|
||||
}) => {
|
||||
await n8n.navigate.toCredentials();
|
||||
await n8n.credentials.emptyListCreateCredentialButton.click();
|
||||
|
||||
await n8n.credentials.createCredentialFromCredentialPicker(
|
||||
'Notion API',
|
||||
{
|
||||
apiKey: NOTION_API_KEY,
|
||||
},
|
||||
{
|
||||
name: 'Credential in Home project',
|
||||
},
|
||||
);
|
||||
|
||||
await n8n.navigate.toWorkflows();
|
||||
await expect(n8n.workflows.cards.getWorkflows()).toHaveCount(0);
|
||||
|
||||
await n8n.navigate.toWorkflow('new');
|
||||
|
||||
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
|
||||
await n8n.canvas.addNode(NOTION_NODE_NAME, { action: 'Append a block', closeNDV: true });
|
||||
await n8n.canvas.waitForSaveWorkflowCompleted();
|
||||
|
||||
const { projectId, projectName } = await n8n.projectComposer.createProject('Project 1');
|
||||
|
||||
await n8n.api.projects.addUserToProjectByEmail(
|
||||
projectId,
|
||||
INSTANCE_MEMBER_CREDENTIALS[0].email,
|
||||
'project:editor',
|
||||
);
|
||||
|
||||
await n8n.sideBar.clickPersonalMenuItem();
|
||||
await n8n.sideBar.clickWorkflowsLink();
|
||||
|
||||
await expect(n8n.workflows.cards.getWorkflows()).toHaveCount(1);
|
||||
|
||||
await n8n.workflowComposer.moveToProject('My workflow', projectName);
|
||||
|
||||
await expect(n8n.workflows.cards.getWorkflows()).toHaveCount(0);
|
||||
|
||||
await n8n.sideBar.clickProjectMenuItem(projectName);
|
||||
await n8n.navigate.toWorkflows();
|
||||
|
||||
await expect(n8n.workflows.cards.getWorkflows()).toHaveCount(1);
|
||||
await expect(
|
||||
n8n.workflows.cards.getWorkflow('My workflow').getByText('Personal'),
|
||||
).toBeHidden();
|
||||
|
||||
await n8n.sideBar.clickSignout();
|
||||
await n8n.page.waitForURL(/\/signin/);
|
||||
await n8n.signIn.loginWithEmailAndPassword(
|
||||
INSTANCE_MEMBER_CREDENTIALS[0].email,
|
||||
INSTANCE_MEMBER_CREDENTIALS[0].password,
|
||||
);
|
||||
|
||||
await expect(n8n.workflows.getProjectName()).toBeVisible();
|
||||
|
||||
await n8n.sideBar.clickProjectMenuItem(projectName);
|
||||
await n8n.navigate.toWorkflows();
|
||||
|
||||
await expect(n8n.workflows.cards.getWorkflows()).toHaveCount(1);
|
||||
|
||||
await n8n.workflows.cards.clickWorkflowCard('My workflow');
|
||||
|
||||
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(2);
|
||||
|
||||
await n8n.canvas.openNode('Append a block');
|
||||
|
||||
await expect(n8n.ndv.getCredentialSelectInput()).toBeEnabled();
|
||||
});
|
||||
|
||||
test('should filter credentials by project ID when creating new workflow or hard reloading an opened workflow', async ({
|
||||
n8n,
|
||||
}) => {
|
||||
const { projectName, projectId } = await n8n.projectComposer.createProject();
|
||||
await n8n.projectComposer.addCredentialToProject(
|
||||
projectName,
|
||||
'Notion API',
|
||||
'apiKey',
|
||||
NOTION_API_KEY,
|
||||
);
|
||||
|
||||
const credentials = await n8n.api.credentials.getCredentialsByProject(projectId);
|
||||
expect(credentials).toHaveLength(1);
|
||||
|
||||
const { projectId: project2Id } = await n8n.projectComposer.createProject();
|
||||
const credentials2 = await n8n.api.credentials.getCredentialsByProject(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.waitForSaveWorkflowCompleted();
|
||||
|
||||
await n8n.canvas.addNode(EXECUTE_WORKFLOW_NODE_NAME, { action: 'Execute A Sub Workflow' });
|
||||
|
||||
const subn8n = await n8n.start.fromNewPage(() =>
|
||||
n8n.ndv.selectWorkflowResource(`Create a Sub-Workflow in '${projectName}'`),
|
||||
);
|
||||
|
||||
await subn8n.ndv.clickBackToCanvasButton();
|
||||
|
||||
await subn8n.canvas.deleteNodeByName('Replace me with your logic');
|
||||
await subn8n.canvas.addNode(NOTION_NODE_NAME, { action: 'Append a block' });
|
||||
await subn8n.credentialsComposer.createFromNdv({
|
||||
apiKey: NOTION_API_KEY,
|
||||
});
|
||||
|
||||
await subn8n.ndv.clickBackToCanvasButton();
|
||||
await subn8n.canvas.waitForSaveWorkflowCompleted();
|
||||
|
||||
await subn8n.navigate.toWorkflows();
|
||||
await subn8n.sideBar.clickProjectMenuItem(projectName);
|
||||
await subn8n.navigate.toWorkflows();
|
||||
|
||||
await expect(subn8n.workflows.cards.getWorkflows()).toHaveCount(2);
|
||||
|
||||
await expect(subn8n.page.getByRole('heading', { name: 'My Sub-Workflow' })).toBeVisible();
|
||||
|
||||
await subn8n.navigate.toCredentials();
|
||||
|
||||
await expect(subn8n.credentials.cards.getCredentials()).toHaveCount(1);
|
||||
await expect(subn8n.page.getByRole('heading', { name: 'Notion account' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('should create credential from workflow in the correct project after editor page refresh @auth:owner', async ({
|
||||
n8n,
|
||||
}) => {
|
||||
const { projectName } = await n8n.projectComposer.createProject(`Dev ${nanoid(8)}`);
|
||||
|
||||
await n8n.sideBar.clickProjectMenuItem(projectName);
|
||||
await n8n.navigate.toWorkflows();
|
||||
|
||||
await n8n.workflows.clickNewWorkflowButtonFromOverview();
|
||||
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
|
||||
await n8n.canvas.waitForSaveWorkflowCompleted();
|
||||
|
||||
// Wait for URL to update with workflow ID after save
|
||||
await n8n.page.waitForURL(/\/workflow\/[^/]+$/);
|
||||
|
||||
await n8n.page.reload();
|
||||
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(1);
|
||||
|
||||
await n8n.canvas.addNode(NOTION_NODE_NAME, { action: 'Append a block' });
|
||||
await n8n.credentialsComposer.createFromNdv({
|
||||
apiKey: NOTION_API_KEY,
|
||||
});
|
||||
await n8n.ndv.close();
|
||||
await n8n.canvas.waitForSaveWorkflowCompleted();
|
||||
|
||||
await n8n.sideBar.clickProjectMenuItem(projectName);
|
||||
await n8n.navigate.toCredentials();
|
||||
|
||||
await expect(n8n.credentials.cards.getCredentials()).toHaveCount(1);
|
||||
});
|
||||
|
||||
test('should set and update project icon @auth:admin', async ({ n8n }) => {
|
||||
const DEFAULT_ICON = 'layers';
|
||||
const NEW_PROJECT_NAME = `Test Project ${nanoid(8)}`;
|
||||
|
||||
await n8n.projectComposer.createProject(NEW_PROJECT_NAME);
|
||||
|
||||
await expect(n8n.projectSettings.getIconPickerButton().locator('svg')).toHaveAttribute(
|
||||
'data-icon',
|
||||
DEFAULT_ICON,
|
||||
);
|
||||
|
||||
await n8n.projectSettings.clickIconPickerButton();
|
||||
await n8n.projectSettings.selectIconTab('Emojis');
|
||||
|
||||
await n8n.projectSettings.selectFirstEmoji();
|
||||
|
||||
await expect(
|
||||
n8n.notifications.getNotificationByTitle('Project icon updated successfully'),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(n8n.projectSettings.getIconPickerButton()).toContainText('😀');
|
||||
|
||||
await n8n.sideBar.expand();
|
||||
await expect(
|
||||
n8n.sideBar.getProjectMenuItems().filter({ hasText: NEW_PROJECT_NAME }),
|
||||
).toContainText('😀');
|
||||
});
|
||||
|
||||
test('should be able to create a workflow when in the workflow editor @auth:owner', async ({
|
||||
n8n,
|
||||
}) => {
|
||||
await n8n.navigate.toWorkflow('new');
|
||||
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
|
||||
await n8n.canvas.addNode(EDIT_FIELDS_SET_NODE_NAME, { closeNDV: true });
|
||||
await n8n.canvas.waitForSaveWorkflowCompleted();
|
||||
|
||||
// Wait for URL to be updated (new=true removed after save)
|
||||
await n8n.page.waitForURL(/\/workflow\/[^?]+$/);
|
||||
const savedWorkflowUrl = n8n.page.url();
|
||||
|
||||
await n8n.sideBar.addWorkflowFromUniversalAdd('Personal');
|
||||
|
||||
// Close dropdown/menu
|
||||
await n8n.page.locator('body').click();
|
||||
|
||||
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(0);
|
||||
|
||||
await n8n.page.goBack();
|
||||
|
||||
expect(n8n.page.url()).toBe(savedWorkflowUrl);
|
||||
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(2);
|
||||
|
||||
await n8n.sideBar.addWorkflowFromUniversalAdd('Personal');
|
||||
|
||||
// New workflows redirect to /workflow/<id>?new=true
|
||||
await n8n.page.waitForURL(/\/workflow\/[a-zA-Z0-9_-]+\?.*new=true/);
|
||||
|
||||
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user