test: Migrate remaining folders tests from Cypress to Playwright (#20128)

This commit is contained in:
Declan Carroll
2025-09-29 15:27:22 +01:00
committed by GitHub
parent f72b9fa130
commit e248a50816
27 changed files with 644 additions and 503 deletions
@@ -1,163 +0,0 @@
import {
createFolderFromProjectHeader,
createFolderInsideFolder,
createNewProject,
createWorkflowFromProjectHeader,
dragAndDropToFolder,
dragAndDropToProjectRoot,
duplicateWorkflowFromCardActions,
duplicateWorkflowFromWorkflowPage,
getFolderCard,
getFolderCards,
getOverviewMenuItem,
getProjectMenuItem,
getWorkflowCard,
getWorkflowCards,
goToPersonalProject,
} from '../../composables/folders';
import { expandSidebar } from '../../composables/sidebar';
import { visitWorkflowsPage } from '../../composables/workflowsPage';
import { successToast } from '../../pages/notifications';
describe('Folders - Advanced Interactions', () => {
before(() => {
cy.resetDatabase();
cy.enableFeature('sharing');
cy.enableFeature('folders');
cy.enableFeature('advancedPermissions');
cy.enableFeature('projectRole:admin');
cy.enableFeature('projectRole:editor');
cy.changeQuota('maxTeamProjects', -1);
});
beforeEach(() => {
visitWorkflowsPage();
});
describe('Duplicate workflows', () => {
beforeEach(() => {
// Prevent the duplicated workflow from opening in a new tab
cy.window().then((win) => {
cy.stub(win, 'open').as('open');
});
});
it('should duplicate workflow within root folder from personal projects', () => {
goToPersonalProject();
createWorkflowFromProjectHeader(undefined, 'Duplicate Me From Root');
goToPersonalProject();
duplicateWorkflowFromCardActions('Duplicate Me From Root', 'Duplicate Me From Root (Copy)');
getWorkflowCard('Duplicate Me From Root (Copy)').should('exist');
});
it('should duplicate workflow within a folder from personal projects', () => {
goToPersonalProject();
createFolderFromProjectHeader('Parent folder for duplication');
getFolderCard('Parent folder for duplication').click();
createWorkflowFromProjectHeader(
'Parent folder for duplication',
'Duplicate Me From Personal',
);
goToPersonalProject();
getFolderCard('Parent folder for duplication').click();
duplicateWorkflowFromCardActions(
'Duplicate Me From Personal',
'Duplicate Me From Personal (Copy)',
);
getWorkflowCard('Duplicate Me From Personal (Copy)').should('exist');
});
it('should duplicate workflow within a folder from overview', () => {
expandSidebar();
goToPersonalProject();
getFolderCard('Parent folder for duplication').click();
createWorkflowFromProjectHeader(
'Parent folder for duplication',
'Duplicate Me From Overview',
);
getOverviewMenuItem().click();
duplicateWorkflowFromCardActions(
'Duplicate Me From Overview',
'Duplicate Me From Overview (Copy)',
);
getWorkflowCard('Duplicate Me From Overview (Copy)').should('exist');
goToPersonalProject();
getFolderCard('Parent folder for duplication').click();
getWorkflowCard('Duplicate Me From Overview (Copy)').should('exist');
});
it('should duplicate workflow within a folder from workflow', () => {
goToPersonalProject();
createFolderFromProjectHeader('Parent folder for duplication');
getFolderCard('Parent folder for duplication').click();
createWorkflowFromProjectHeader(
'Parent folder for duplication',
'Duplicate Me From Workflow',
);
duplicateWorkflowFromWorkflowPage('Duplicate Me From Workflow (Copy)');
goToPersonalProject();
getFolderCard('Parent folder for duplication').click();
getWorkflowCard('Duplicate Me From Workflow (Copy)').should('exist');
});
});
describe('Drag and drop', () => {
it('should drag and drop folders into folders', () => {
const PROJECT_NAME = 'Drag and Drop Test';
const TARGET_NAME = 'Drag me';
const DESTINATION_NAME = 'Folder Destination';
expandSidebar();
createNewProject(PROJECT_NAME, { openAfterCreate: true });
createFolderFromProjectHeader(TARGET_NAME);
createFolderFromProjectHeader(DESTINATION_NAME);
dragAndDropToFolder(TARGET_NAME, DESTINATION_NAME);
successToast().should('contain.text', `${TARGET_NAME} has been moved to ${DESTINATION_NAME}`);
// Only one folder card should remain
getFolderCards().should('have.length', 1);
// Check folder in the destination
getFolderCard(DESTINATION_NAME).click();
getFolderCard(TARGET_NAME).should('exist');
});
it('should drag and drop folders into project root breadcrumb', () => {
const PROJECT_NAME = 'Drag to root test';
const TARGET_NAME = 'To Project root';
const PARENT_NAME = 'Parent Folder';
expandSidebar();
createNewProject(PROJECT_NAME, { openAfterCreate: true });
createFolderFromProjectHeader(PARENT_NAME);
createFolderInsideFolder(TARGET_NAME, PARENT_NAME);
dragAndDropToProjectRoot(TARGET_NAME);
// No folder cards should be shown in the parent folder
getFolderCards().should('not.exist');
successToast().should('contain.text', `${TARGET_NAME} has been moved to ${PROJECT_NAME}`);
// Check folder in the project root
getProjectMenuItem(PROJECT_NAME).click();
getFolderCard(TARGET_NAME).should('exist');
});
it('should drag and drop workflows into folders', () => {
const PROJECT_NAME = 'Drag and Drop WF Test';
const TARGET_NAME = 'Drag me - WF';
const DESTINATION_NAME = 'Workflow Destination';
expandSidebar();
createNewProject(PROJECT_NAME, { openAfterCreate: true });
createFolderFromProjectHeader(DESTINATION_NAME);
createWorkflowFromProjectHeader(undefined, TARGET_NAME);
getProjectMenuItem(PROJECT_NAME).click();
dragAndDropToFolder(TARGET_NAME, DESTINATION_NAME);
// No workflow cards should be shown in the project root
getWorkflowCards().should('not.exist');
successToast().should('contain.text', `${TARGET_NAME} has been moved to ${DESTINATION_NAME}`);
// Check workflow in the destination
getFolderCard(DESTINATION_NAME).click();
getWorkflowCard(TARGET_NAME).should('exist');
});
});
});
@@ -1,247 +0,0 @@
import {
createFolderFromProjectHeader,
createFolderInsideFolder,
createWorkflowFromProjectHeader,
deleteAndTransferFolderContentsFromCardDropdown,
deleteAndTransferFolderContentsFromListDropdown,
deleteEmptyFolderFromCardDropdown,
deleteEmptyFolderFromListDropdown,
deleteFolderWithContentsFromCardDropdown,
deleteFolderWithContentsFromListDropdown,
getCurrentBreadcrumbText,
getFolderCard,
getFolderCards,
getFolderEmptyState,
getHomeProjectBreadcrumb,
getListBreadcrumbItem,
getListBreadcrumbs,
getPersonalProjectMenuItem,
getWorkflowCard,
getWorkflowCards,
goToPersonalProject,
moveFolderFromFolderCardActions,
moveFolderFromListActions,
moveWorkflowToFolder,
renameFolderFromCardActions,
renameFolderFromListActions,
} from '../../composables/folders';
import { visitWorkflowsPage } from '../../composables/workflowsPage';
import { successToast } from '../../pages/notifications';
describe('Folders - Operations', () => {
before(() => {
cy.resetDatabase();
cy.enableFeature('sharing');
cy.enableFeature('folders');
cy.enableFeature('advancedPermissions');
cy.enableFeature('projectRole:admin');
cy.enableFeature('projectRole:editor');
cy.changeQuota('maxTeamProjects', -1);
});
beforeEach(() => {
visitWorkflowsPage();
});
describe('Rename and delete folders', () => {
it('should rename folder from main dropdown', () => {
goToPersonalProject();
createFolderFromProjectHeader('Rename Me');
getFolderCard('Rename Me').should('exist');
renameFolderFromListActions('Rename Me', 'Renamed');
getCurrentBreadcrumbText().should('equal', 'Renamed');
});
it('should rename folder from card dropdown', () => {
goToPersonalProject();
createFolderFromProjectHeader('Rename Me 2');
renameFolderFromCardActions('Rename Me 2', 'Renamed 2');
getFolderCard('Renamed 2').should('exist');
});
it('should delete empty folder from card dropdown', () => {
goToPersonalProject();
createFolderFromProjectHeader('Delete Me');
getFolderCard('Delete Me').should('exist');
deleteEmptyFolderFromCardDropdown('Delete Me');
});
it('should delete empty folder from main dropdown', () => {
goToPersonalProject();
createFolderFromProjectHeader('Delete Me 2');
getFolderCard('Delete Me 2').should('exist');
deleteEmptyFolderFromListDropdown('Delete Me 2');
// Since we deleted the current folder, we should be back in the home project
getListBreadcrumbs().should('not.exist');
getPersonalProjectMenuItem().find('li').should('have.class', 'is-active');
});
it('should warn before deleting non-empty folder from list dropdown', () => {
goToPersonalProject();
createFolderFromProjectHeader('I have children');
createFolderInsideFolder('Child 1', 'I have children');
deleteFolderWithContentsFromListDropdown('I have children');
// Since we deleted the current folder, we should be back in the home project
getListBreadcrumbs().should('not.exist');
getPersonalProjectMenuItem().find('li').should('have.class', 'is-active');
});
it('should warn before deleting non-empty folder from card dropdown', () => {
goToPersonalProject();
createFolderFromProjectHeader('I also have family');
createFolderInsideFolder('Child 1', 'I also have family');
// Back to home
getHomeProjectBreadcrumb().click();
getFolderCard('I also have family').should('exist');
deleteFolderWithContentsFromCardDropdown('I also have family');
});
it('should transfer contents when deleting non-empty folder - from card dropdown', () => {
goToPersonalProject();
createFolderFromProjectHeader('Move my contents');
createFolderFromProjectHeader('Destination');
createFolderInsideFolder('Child 1', 'Move my contents');
getHomeProjectBreadcrumb().click();
getFolderCard('Move my contents').should('exist');
deleteAndTransferFolderContentsFromCardDropdown('Move my contents', 'Destination');
getFolderCard('Destination').click();
// Should show the contents of the moved folder
getFolderCard('Child 1').should('exist');
});
it('should transfer contents when deleting non-empty folder - from list breadcrumbs', () => {
goToPersonalProject();
createFolderFromProjectHeader('Move me too');
createFolderFromProjectHeader('Destination 2');
createFolderInsideFolder('Child 1', 'Move me too');
deleteAndTransferFolderContentsFromListDropdown('Destination 2');
getFolderCard('Destination').click();
// Should show the contents of the moved folder
getFolderCard('Child 1').should('exist');
});
});
describe('Move folders and workflows', () => {
it('should move empty folder to another folder - from folder card action', () => {
goToPersonalProject();
createFolderFromProjectHeader('Move me - I am empty');
createFolderFromProjectHeader('Destination 3');
moveFolderFromFolderCardActions('Move me - I am empty', 'Destination 3');
getFolderCard('Destination 3').click();
getFolderCard('Move me - I am empty').should('exist');
getFolderCard('Move me - I am empty').click();
getFolderEmptyState().should('exist');
successToast().should('contain.text', 'Move me - I am empty has been moved to Destination 3');
// Breadcrumbs should show the destination folder
getListBreadcrumbItem('Destination 3').should('exist');
});
it('should move folder with contents to another folder - from folder card action', () => {
goToPersonalProject();
createFolderFromProjectHeader('Move me - I have family');
createFolderFromProjectHeader('Destination 4');
// Create a workflow and a folder inside the folder
createFolderInsideFolder('Child 1', 'Move me - I have family');
createWorkflowFromProjectHeader('Move me - I have family');
goToPersonalProject();
// Move the folder
moveFolderFromFolderCardActions('Move me - I have family', 'Destination 4');
successToast().should(
'contain.text',
'Move me - I have family has been moved to Destination 4',
);
// Go to destination folder and check if contents are there
getFolderCard('Destination 4').click();
// Moved folder should be there
getFolderCard('Move me - I have family').should('exist').click();
// Both the workflow and the folder should be there
getFolderCards().should('have.length', 1);
getWorkflowCards().should('have.length', 1);
// Breadcrumbs should show the destination folder
getListBreadcrumbItem('Destination 4').should('exist');
});
it('should move empty folder to another folder - from list breadcrumbs', () => {
goToPersonalProject();
createFolderFromProjectHeader('Move me too - I am empty');
createFolderFromProjectHeader('Destination 5');
moveFolderFromListActions('Move me too - I am empty', 'Destination 5');
// Since we moved the current folder, we should be in the destination folder
getCurrentBreadcrumbText().should('equal', 'Destination 5');
});
it('should move folder with contents to another folder - from list dropdown', () => {
goToPersonalProject();
createFolderFromProjectHeader('Move me - I have family 2');
createFolderFromProjectHeader('Destination 6');
// Create a workflow and a folder inside the folder
createFolderInsideFolder('Child 1', 'Move me - I have family 2');
createWorkflowFromProjectHeader('Move me - I have family 2');
// Navigate back to folder
goToPersonalProject();
getFolderCard('Move me - I have family 2').should('exist');
// Move the folder
moveFolderFromListActions('Move me - I have family 2', 'Destination 6');
// Since we moved the current folder, we should be in the destination folder
getCurrentBreadcrumbText().should('equal', 'Destination 6');
// Moved folder should be there
getFolderCard('Move me - I have family 2').should('exist').click();
// After navigating to the moved folder, both the workflow and the folder should be there
getFolderCards().should('have.length', 1);
getWorkflowCards().should('have.length', 1);
// Breadcrumbs should show the destination folder
getListBreadcrumbItem('Destination 6').should('exist');
});
it('should move folder to project root - from folder card action', () => {
goToPersonalProject();
createFolderFromProjectHeader('Test parent');
createFolderInsideFolder('Move me to root', 'Test parent');
moveFolderFromFolderCardActions('Move me to root', 'No folder (project root)');
// Parent folder should be empty
getFolderEmptyState().should('exist');
// Child folder should be in the root
goToPersonalProject();
getFolderCard('Move me to root').should('exist');
// Navigate to the moved folder and check breadcrumbs
getFolderCard('Move me to root').click();
getHomeProjectBreadcrumb().should('contain.text', 'Personal');
getListBreadcrumbs().findChildByTestId('breadcrumbs-item').should('not.exist');
getCurrentBreadcrumbText().should('equal', 'Move me to root');
});
it('should move workflow from project root to folder', () => {
goToPersonalProject();
createWorkflowFromProjectHeader(undefined, 'Move me');
goToPersonalProject();
createFolderFromProjectHeader('Workflow destination');
moveWorkflowToFolder('Move me', 'Workflow destination');
successToast().should('contain.text', 'Move me has been moved to Workflow destination');
// Navigate to the destination folder
getFolderCard('Workflow destination').click();
// Moved workflow should be there
getWorkflowCards().should('have.length', 1);
getWorkflowCard('Move me').should('exist');
});
it('should move workflow to another folder', () => {
goToPersonalProject();
createFolderFromProjectHeader('Moving workflow from here');
createFolderFromProjectHeader('Moving workflow to here');
getFolderCard('Moving workflow from here').click();
createWorkflowFromProjectHeader(undefined, 'Move me');
goToPersonalProject();
getFolderCard('Moving workflow from here').click();
getWorkflowCard('Move me').should('exist');
moveWorkflowToFolder('Move me', 'Moving workflow to here');
// Now folder should be empty
getFolderEmptyState().should('exist');
// Navigate to the destination folder
getHomeProjectBreadcrumb().click();
getFolderCard('Moving workflow to here').click();
// Moved workflow should be there
getWorkflowCards().should('have.length', 1);
getWorkflowCard('Move me').should('exist');
});
});
});
@@ -47,6 +47,6 @@ export class CredentialsComposer {
* Create a credential directly via API. Returns created credential object.
*/
async createFromApi(payload: CreateCredentialDto & { projectId?: string }) {
return await this.n8n.api.credentialApi.createCredential(payload);
return await this.n8n.api.credentials.createCredential(payload);
}
}
@@ -1,3 +1,5 @@
import type { Page } from '@playwright/test';
import type { n8nPage } from '../pages/n8nPage';
/**
@@ -44,10 +46,7 @@ export class TestEntryComposer {
}
async fromNewProject() {
await this.withProjectFeatures();
// Create a project using the API
const response = await this.n8n.api.projects.createProject();
const projectId = response.id;
await this.n8n.navigate.toProject(projectId);
return projectId;
@@ -58,11 +57,26 @@ export class TestEntryComposer {
* Returns the workflow import result for use in the test
*/
async fromImportedWorkflow(workflowFile: string) {
const workflowImportResult = await this.n8n.api.workflowApi.importWorkflow(workflowFile);
const workflowImportResult = await this.n8n.api.workflows.importWorkflow(workflowFile);
await this.n8n.page.goto(`workflow/${workflowImportResult.workflowId}`);
return workflowImportResult;
}
/**
* Start UI test on a new page created by an action
* @param action - The action that will create a new page
* @returns n8nPage instance for the new page
*/
async fromNewPage(action: () => Promise<void>): Promise<n8nPage> {
const newPagePromise = this.n8n.page.waitForEvent('popup');
await action();
const newPage = await newPagePromise;
await newPage.waitForLoadState('domcontentloaded');
// Use the constructor from the current instance to avoid circular dependency
const n8nPageConstructor = this.n8n.constructor as new (page: Page) => n8nPage;
return new n8nPageConstructor(newPage);
}
/**
* Enable project feature set
* Allow project creation, sharing, and folder creation
@@ -138,6 +138,8 @@ export const test = base.extend<TestFixtures, WorkerFixtures>({
const page = await context.newPage();
const n8nInstance = new n8nPage(page);
await n8nInstance.api.setupFromTags(testInfo.tags);
// Enable project features for the tests, this is used in several tests, but is never disabled in tests, so we can have it on by default
await n8nInstance.start.withProjectFeatures();
await use(n8nInstance);
},
@@ -112,16 +112,14 @@ export class NavigationHelper {
* - Existing workflow: /workflow/{workflowId}
* - Project workflow: /projects/{projectId}/workflow/{workflowId}
*/
async toWorkflow(workflowId: string = 'new', projectId?: string): Promise<void> {
const url = projectId
? `/projects/${projectId}/workflow/${workflowId}`
: `/workflow/${workflowId}`;
async toWorkflow(workflowId: string = 'new'): Promise<void> {
const url = `/workflow/${workflowId}`;
await this.page.goto(url);
}
/**
* Navigate to a specific folder
* URL: /projects/{projectId}/folders/{folderId}
* URL: /projects/{projectId}/folders/{folderId}/workflows or /home/folders/{folderId}/workflows
*/
async toFolder(folderId: string, projectId?: string): Promise<void> {
const url = projectId
@@ -133,8 +131,8 @@ export class NavigationHelper {
/**
* Navigate to workflow canvas (alias for toWorkflow)
*/
async toCanvas(workflowId: string = 'new', projectId?: string): Promise<void> {
await this.toWorkflow(workflowId, projectId);
async toCanvas(workflowId: string = 'new'): Promise<void> {
await this.toWorkflow(workflowId);
}
/**
@@ -3,8 +3,11 @@ import type { Page } from '@playwright/test';
import { BaseModal } from './components/BaseModal';
export abstract class BasePage {
protected readonly baseModal = new BaseModal(this.page);
constructor(protected readonly page: Page) {}
protected readonly baseModal: BaseModal;
constructor(protected readonly page: Page) {
this.baseModal = new BaseModal(this.page);
}
protected async clickByTestId(testId: string) {
await this.page.getByTestId(testId).click();
@@ -752,4 +752,8 @@ export class CanvasPage extends BasePage {
getCanvasPlusButton(): Locator {
return this.page.getByTestId('canvas-plus-button');
}
getWorkflowName(): Locator {
return this.page.getByTestId('workflow-name-input');
}
}
@@ -18,6 +18,9 @@ export class WorkflowSettingsModal extends BasePage {
getErrorWorkflowField(): Locator {
return this.page.getByTestId('workflow-settings-error-workflow');
}
getDuplicateMenuItem(): Locator {
return this.page.getByTestId('workflow-menu-item-duplicate');
}
getSaveButton(): Locator {
return this.page.getByRole('button', { name: 'Save' });
@@ -154,7 +154,6 @@ export class WorkflowsPage extends BasePage {
async filterByTag(tag: string) {
await this.filterByTags([tag]);
}
getFolderBreadcrumbsActions() {
return this.page.getByTestId('folder-breadcrumbs-actions');
}
@@ -187,9 +186,51 @@ export class WorkflowsPage extends BasePage {
/**
* Fill the folder modal
* @param folderName - The name of the folder
* @param buttonText - The text of the button to click (default: 'Create')
*/
async fillFolderModal(folderName: string) {
async fillFolderModal(folderName: string, buttonText: string = 'Create') {
await this.baseModal.fillInput(folderName);
await this.baseModal.clickButton('Create');
await this.baseModal.clickButton(buttonText);
}
deleteFolderModal() {
return this.page.getByTestId('deleteFolder-modal');
}
deleteModalTransferRadioButton() {
return this.deleteFolderModal().getByTestId('transfer-content-radio');
}
deleteModalDeleteRadioButton() {
return this.deleteFolderModal().getByTestId('delete-content-radio');
}
deleteModalConfirmButton() {
return this.deleteFolderModal().getByTestId('confirm-delete-folder-button');
}
transferFolderDropdown() {
return this.deleteFolderModal().getByRole('combobox', { name: 'Select a folder' });
}
transferFolderOption(folderName: string) {
return this.page.getByTestId('move-to-folder-option').filter({ hasText: folderName });
}
// Move folder modal methods
moveFolderModal() {
return this.page.getByTestId('moveFolder-modal');
}
moveFolderDropdown() {
return this.moveFolderModal().getByTestId('move-to-folder-dropdown').getByRole('combobox');
}
moveFolderOption(folderName: string) {
return this.page.getByTestId('move-to-folder-option').filter({ hasText: folderName });
}
moveFolderConfirmButton() {
return this.moveFolderModal().getByTestId('confirm-move-folder-button');
}
}
@@ -29,4 +29,18 @@ export class Breadcrumbs {
getActionToggleDropdown(resourceName: string) {
return this.page.getByTestId('action-toggle-dropdown').getByTestId(`action-${resourceName}`);
}
}
getFolderBreadcrumbsActionToggle() {
return this.page.getByTestId('folder-breadcrumbs-actions');
}
/**
* Rename the current breadcrumb by activating inline edit mode
* @param newName - The new name for the breadcrumb item
*/
async renameCurrentBreadcrumb(newName: string) {
await this.getCurrentBreadcrumb().getByTestId('inline-edit-preview').click();
await this.getCurrentBreadcrumb().getByTestId('inline-edit-input').fill(newName);
await this.page.keyboard.press('Enter');
}
}
@@ -46,11 +46,14 @@ export class ResourceCards {
}
getCardActionToggle(card: Locator): Locator {
return card.getByTestId('card-append');
return card
.getByTestId('card-append')
.locator('[class*="action-toggle"]')
.filter({ visible: true });
}
getCardAction(actionName: string): Locator {
return this.page.getByTestId(`action-${actionName}`);
return this.page.getByTestId(`action-${actionName}`).filter({ visible: true });
}
async openCardActions(card: Locator): Promise<void> {
@@ -66,4 +69,9 @@ export class ResourceCards {
const folderCard = this.getFolder(folderName);
await this.clickCardAction(folderCard, 'open');
}
async deleteFolder(folderName: string): Promise<void> {
const folderCard = this.getFolder(folderName);
await this.clickCardAction(folderCard, 'delete');
}
}
@@ -35,17 +35,17 @@ const DB_TAGS = {
export class ApiHelpers {
request: APIRequestContext;
workflowApi: WorkflowApiHelper;
workflows: WorkflowApiHelper;
projects: ProjectApiHelper;
credentialApi: CredentialApiHelper;
variablesApi: VariablesApiHelper;
credentials: CredentialApiHelper;
variables: VariablesApiHelper;
constructor(requestContext: APIRequestContext) {
this.request = requestContext;
this.workflowApi = new WorkflowApiHelper(this);
this.workflows = new WorkflowApiHelper(this);
this.projects = new ProjectApiHelper(this);
this.credentialApi = new CredentialApiHelper(this);
this.variablesApi = new VariablesApiHelper(this);
this.credentials = new CredentialApiHelper(this);
this.variables = new VariablesApiHelper(this);
}
// ===== MAIN SETUP METHODS =====
@@ -35,6 +35,48 @@ export class WorkflowApiHelper {
return result.data ?? result;
}
/**
* Creates a workflow in a project with optional folder placement (Uses Internal API not public API)
* @param project - Required project ID where the workflow will be created
* @param options - Optional configuration for workflow creation
* @param options.folder - Optional folder ID to place the workflow in
* @param options.name - Optional workflow name. If not provided, generates a unique name using nanoid
* @returns Object containing the name and ID of the created workflow
*/
async createInProject(
project: string,
options?: {
folder?: string;
name?: string;
},
): Promise<{ name: string; id: string }> {
const workflowName = options?.name ?? `Test Workflow ${nanoid(8)}`;
const workflow = {
name: workflowName,
nodes: [],
connections: {},
settings: {},
active: false,
projectId: project,
...(options?.folder && { parentFolderId: options.folder }),
};
const response = await this.api.request.post('/rest/workflows', { data: workflow });
if (!response.ok()) {
throw new TestError(`Failed to create workflow: ${await response.text()}`);
}
const result = await response.json();
const workflowData = result.data ?? result;
return {
name: workflowName,
id: workflowData.id,
};
}
async setActive(workflowId: string, active: boolean) {
const response = await this.api.request.patch(`/rest/workflows/${workflowId}?forceSave=true`, {
data: { active },
@@ -11,12 +11,6 @@ const NOTIFICATIONS = {
test.describe('Workflows', () => {
test.beforeEach(async ({ n8n }) => {
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);
await n8n.goHome();
});
@@ -35,7 +29,7 @@ test.describe('Workflows', () => {
const uniqueIdForCreate = nanoid(8);
const workflowName = `Test Workflow ${uniqueIdForCreate}`;
await n8n.canvas.setWorkflowName(workflowName);
await n8n.canvas.clickSaveWorkflowButton();
await n8n.canvas.saveWorkflow();
await expect(n8n.notifications.getNotificationByTitle(NOTIFICATIONS.CREATED)).toBeVisible();
});
@@ -26,14 +26,14 @@ test.describe('Credentials', () => {
const credentialA = `A Credential ${nanoid()}`;
const credentialZ = `Z Credential ${nanoid()}`;
await n8n.api.credentialApi.createCredential({
await n8n.api.credentials.createCredential({
name: credentialA,
type: 'notionApi',
data: { apiKey: '1234567890' },
projectId,
});
await n8n.api.credentialApi.createCredential({
await n8n.api.credentials.createCredential({
name: credentialZ,
type: 'trelloApi',
data: { apiKey: 'test_api_key', apiToken: 'test_api_token' },
@@ -94,14 +94,14 @@ test.describe('Credentials', () => {
const serviceAccountCredentialName2 = `OAuth2 Credential ${nanoid()}`;
const serviceAccountCredentialName = `Service Account Credential ${nanoid()}`;
await n8n.api.credentialApi.createCredential({
await n8n.api.credentials.createCredential({
name: serviceAccountCredentialName2,
type: 'googleApi',
data: { email: 'test@service.com', privateKey: 'test_key' },
projectId,
});
await n8n.api.credentialApi.createCredential({
await n8n.api.credentials.createCredential({
name: serviceAccountCredentialName,
type: 'googleApi',
data: { email: 'test@service.com', privateKey: 'test_key' },
@@ -234,7 +234,7 @@ test.describe('Credentials', () => {
const projectId = await n8n.start.fromNewProjectBlankCanvas();
const credentialName = `My awesome Notion account ${nanoid()}`;
await n8n.api.credentialApi.createCredential({
await n8n.api.credentials.createCredential({
name: credentialName,
type: 'notionApi',
data: { apiKey: '1234567890' },
@@ -245,16 +245,16 @@ test.describe('Credentials', () => {
await n8n.canvas.addNode('Notion', { action: 'Append a block' });
await expect(n8n.ndv.getCredentialSelect()).toHaveValue(credentialName);
const credentials = await n8n.api.credentialApi.getCredentials();
const credentials = await n8n.api.credentials.getCredentials();
const credential = credentials.find((c) => c.name === credentialName);
await n8n.api.credentialApi.deleteCredential(credential!.id);
await n8n.api.credentials.deleteCredential(credential!.id);
});
test('should set a default credential when editing a node', async ({ n8n }) => {
const projectId = await n8n.start.fromNewProjectBlankCanvas();
const credentialName = `My awesome Notion account ${nanoid()}`;
await n8n.api.credentialApi.createCredential({
await n8n.api.credentials.createCredential({
name: credentialName,
type: 'notionApi',
data: { apiKey: '1234567890' },
@@ -268,9 +268,9 @@ test.describe('Credentials', () => {
await n8n.ndv.selectOptionInParameterDropdown('nodeCredentialType', 'Notion API');
await expect(n8n.ndv.getCredentialSelect()).toHaveValue(credentialName);
const credentials = await n8n.api.credentialApi.getCredentials();
const credentials = await n8n.api.credentials.getCredentials();
const credential = credentials.find((c) => c.name === credentialName);
await n8n.api.credentialApi.deleteCredential(credential!.id);
await n8n.api.credentials.deleteCredential(credential!.id);
});
test('should setup generic authentication for HTTP node', async ({ n8n }) => {
@@ -22,7 +22,7 @@ test.describe('Variables', () => {
test.describe('licensed', () => {
test.beforeEach(async ({ n8n }) => {
await n8n.api.enableFeature('variables');
await n8n.api.variablesApi.deleteAllVariables();
await n8n.api.variables.deleteAllVariables();
await n8n.navigate.toVariables();
});
@@ -1,5 +1,5 @@
import { test, expect } from '../../fixtures/base';
import { n8nPage } from '../../pages/n8nPage';
import type { n8nPage } from '../../pages/n8nPage';
const MANUAL_TRIGGER_NODE_NAME = 'Manual Trigger';
const EXECUTE_WORKFLOW_NODE_NAME = 'Execute Sub-workflow';
@@ -18,12 +18,6 @@ async function getCredentialsForProject(n8n: n8nPage, projectId?: string) {
test.describe('Projects', () => {
test.beforeEach(async ({ n8n }) => {
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);
await n8n.goHome();
});
@@ -64,11 +58,9 @@ test.describe('Projects', () => {
await n8n.canvas.addNode(EXECUTE_WORKFLOW_NODE_NAME, { action: '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);
const subn8n = await n8n.start.fromNewPage(() =>
n8n.ndv.selectWorkflowResource(`Create a Sub-Workflow in '${projectName}'`),
);
await subn8n.ndv.clickBackToCanvasButton();
@@ -14,7 +14,7 @@ test.describe('Workflow Selector Parameter @db:reset', () => {
];
for (const { file } of subWorkflows) {
await n8n.api.workflowApi.importWorkflow(file);
await n8n.api.workflows.importWorkflow(file);
}
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
@@ -0,0 +1,144 @@
import { test, expect } from '../../fixtures/base';
test.describe('Folders - Advanced Operations', () => {
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();
});
});
});
@@ -125,13 +125,15 @@ test.describe('Folders - Basic Operations', () => {
});
test('should create workflow in a folder', async ({ n8n }) => {
const projectId = await n8n.start.fromNewProject();
const { name: projectName, id: projectId } = await n8n.api.projects.createProject();
const folder = await n8n.api.projects.createFolder(projectId);
const folderName = folder.name;
await n8n.workflows.cards.openFolder(folderName);
await n8n.navigate.toFolder(folder.id, projectId);
await n8n.workflows.addResource.workflow();
await expect(n8n.breadcrumbs.getBreadcrumb(folderName)).toBeVisible();
await n8n.canvas.saveWorkflow();
const successMessage = `Workflow successfully created in "${projectName}", within "${folder.name}"`;
await expect(n8n.notifications.getNotificationByTitleOrContent(successMessage)).toBeVisible();
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 }) => {
@@ -0,0 +1,290 @@
import { test, expect } from '../../fixtures/base';
test.describe('Folders - Operations', () => {
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();
});
});
});
@@ -77,7 +77,7 @@ test.describe('04 - Credentials', () => {
const uniqueCredentialName = `credential-${nanoid()}`;
const projectId = await n8n.start.fromNewProjectBlankCanvas();
await n8n.api.credentialApi.createCredential({
await n8n.api.credentials.createCredential({
name: uniqueCredentialName,
type: 'notionApi',
data: {
@@ -15,25 +15,25 @@ test.describe('Credential API Operations', () => {
};
const { credentialId, createdCredential } =
await api.credentialApi.createCredentialFromDefinition(credentialData);
await api.credentials.createCredentialFromDefinition(credentialData);
expect(credentialId).toBeTruthy();
expect(createdCredential.type).toBe('httpBasicAuth');
expect(createdCredential.name).toContain('Test HTTP Basic Auth (Test');
const retrievedCredential = await api.credentialApi.getCredential(credentialId);
const retrievedCredential = await api.credentials.getCredential(credentialId);
expect(retrievedCredential.id).toBe(credentialId);
expect(retrievedCredential.type).toBe('httpBasicAuth');
expect(retrievedCredential.name).toBe(createdCredential.name);
const credentialWithData = await api.credentialApi.getCredential(credentialId, {
const credentialWithData = await api.credentials.getCredential(credentialId, {
includeData: true,
});
expect(credentialWithData.data).toBeDefined();
expect(credentialWithData.data?.user).toBe('test_user');
const updatedName = 'Updated HTTP Basic Auth';
const updatedCredential = await api.credentialApi.updateCredential(credentialId, {
const updatedCredential = await api.credentials.updateCredential(credentialId, {
name: updatedName,
data: {
user: 'updated_user',
@@ -42,47 +42,47 @@ test.describe('Credential API Operations', () => {
});
expect(updatedCredential.name).toBe(updatedName);
const verifyUpdated = await api.credentialApi.getCredential(credentialId, {
const verifyUpdated = await api.credentials.getCredential(credentialId, {
includeData: true,
});
expect(verifyUpdated.name).toBe(updatedName);
expect(verifyUpdated.data?.user).toBe('updated_user');
const deleteResult = await api.credentialApi.deleteCredential(credentialId);
const deleteResult = await api.credentials.deleteCredential(credentialId);
expect(deleteResult).toBe(true);
await expect(api.credentialApi.getCredential(credentialId)).rejects.toThrow();
await expect(api.credentials.getCredential(credentialId)).rejects.toThrow();
});
});
test.describe('Credential Listing', () => {
test('should list credentials with different query options', async ({ api }) => {
const credential1 = await api.credentialApi.createCredentialFromDefinition({
const credential1 = await api.credentials.createCredentialFromDefinition({
name: 'First Test Credential',
type: 'httpBasicAuth',
data: { user: 'user1', password: 'pass1' },
});
const credential2 = await api.credentialApi.createCredentialFromDefinition({
const credential2 = await api.credentials.createCredentialFromDefinition({
name: 'Second Test Credential',
type: 'httpHeaderAuth',
data: { name: 'Authorization', value: 'Bearer token' },
});
const allCredentials = await api.credentialApi.getCredentials();
const allCredentials = await api.credentials.getCredentials();
expect(allCredentials.length).toBeGreaterThanOrEqual(2);
const createdIds = [credential1.credentialId, credential2.credentialId];
const foundCredentials = allCredentials.filter((c) => createdIds.includes(c.id));
expect(foundCredentials).toHaveLength(2);
const credentialsWithScopes = await api.credentialApi.getCredentials({
const credentialsWithScopes = await api.credentials.getCredentials({
includeScopes: true,
});
expect(credentialsWithScopes[0].scopes).toBeDefined();
expect(Array.isArray(credentialsWithScopes[0].scopes)).toBe(true);
const credentialsWithData = await api.credentialApi.getCredentials({
const credentialsWithData = await api.credentials.getCredentials({
includeData: true,
});
const foundWithData = credentialsWithData.filter((c) => createdIds.includes(c.id));
@@ -98,14 +98,14 @@ test.describe('Credential API Operations', () => {
const project = await api.projects.createProject('Test Project for Credentials');
const credential = await api.credentialApi.createCredentialFromDefinition({
const credential = await api.credentials.createCredentialFromDefinition({
name: 'Project Credential',
type: 'httpBasicAuth',
data: { user: 'user', password: 'pass' },
projectId: project.id,
});
const projectCredentials = await api.credentialApi.getCredentialsForWorkflow({
const projectCredentials = await api.credentials.getCredentialsForWorkflow({
projectId: project.id,
});
@@ -124,22 +124,22 @@ test.describe('Credential API Operations', () => {
const sourceProject = await api.projects.createProject('Source Project');
const destinationProject = await api.projects.createProject('Destination Project');
const credential = await api.credentialApi.createCredentialFromDefinition({
const credential = await api.credentials.createCredentialFromDefinition({
name: 'Transfer Test Credential',
type: 'httpBasicAuth',
data: { user: 'user', password: 'pass' },
projectId: sourceProject.id,
});
const sourceCredentials = await api.credentialApi.getCredentialsForWorkflow({
const sourceCredentials = await api.credentials.getCredentialsForWorkflow({
projectId: sourceProject.id,
});
const foundInSource = sourceCredentials.find((c) => c.id === credential.credentialId);
expect(foundInSource).toBeDefined();
await api.credentialApi.transferCredential(credential.credentialId, destinationProject.id);
await api.credentials.transferCredential(credential.credentialId, destinationProject.id);
const destinationCredentials = await api.credentialApi.getCredentialsForWorkflow({
const destinationCredentials = await api.credentials.getCredentialsForWorkflow({
projectId: destinationProject.id,
});
const foundInDestination = destinationCredentials.find(
@@ -147,7 +147,7 @@ test.describe('Credential API Operations', () => {
);
expect(foundInDestination).toBeDefined();
const sourceCredentialsAfter = await api.credentialApi.getCredentialsForWorkflow({
const sourceCredentialsAfter = await api.credentials.getCredentialsForWorkflow({
projectId: sourceProject.id,
});
const stillInSource = sourceCredentialsAfter.find((c) => c.id === credential.credentialId);
@@ -166,27 +166,27 @@ test.describe('Credential API Operations', () => {
},
};
const { credentialId } = await api.credentialApi.createCredentialFromDefinition(originalData);
const { credentialId } = await api.credentials.createCredentialFromDefinition(originalData);
const afterCreate = await api.credentialApi.getCredential(credentialId, {
const afterCreate = await api.credentials.getCredential(credentialId, {
includeData: true,
});
expect(afterCreate.data?.user).toBe('persistent_user');
await api.credentialApi.updateCredential(credentialId, {
await api.credentials.updateCredential(credentialId, {
data: {
user: 'updated_persistent_user',
password: 'updated_persistent_password',
},
});
const afterUpdate = await api.credentialApi.getCredential(credentialId, {
const afterUpdate = await api.credentials.getCredential(credentialId, {
includeData: true,
});
expect(afterUpdate.data?.user).toBe('updated_persistent_user');
expect(afterUpdate.data?.password).toBeDefined();
const allCredentials = await api.credentialApi.getCredentials();
const allCredentials = await api.credentials.getCredentials();
const foundCredential = allCredentials.find((c) => c.id === credentialId);
expect(foundCredential).toBeDefined();
expect(foundCredential!.type).toBe('httpBasicAuth');
@@ -11,7 +11,7 @@ test.describe('Evaluations @capability:proxy', () => {
test('should load evaluations workflow and execute twice', async ({ n8n, proxyServer }) => {
await proxyServer.loadExpectations('evaluations');
await n8n.api.credentialApi.createCredentialFromDefinition({
await n8n.api.credentials.createCredentialFromDefinition({
name: 'Test Google Sheets',
type: 'googleApi',
data: {
@@ -4,7 +4,7 @@ test.describe('External Webhook Triggering', () => {
test('should create workflow via API, activate it, trigger webhook externally, and verify execution', async ({
api,
}) => {
const { webhookPath, workflowId } = await api.workflowApi.importWorkflow(
const { webhookPath, workflowId } = await api.workflows.importWorkflow(
'simple-webhook-test.json',
);
@@ -16,15 +16,15 @@ test.describe('External Webhook Triggering', () => {
expect(webhookResponse.ok()).toBe(true);
const execution = await api.workflowApi.waitForExecution(workflowId, 5000);
const execution = await api.workflows.waitForExecution(workflowId, 5000);
expect(execution.status).toBe('success');
const executionDetails = await api.workflowApi.getExecution(execution.id);
const executionDetails = await api.workflows.getExecution(execution.id);
expect(executionDetails.data).toContain('Hello from Playwright test');
});
test('should surface workflow configuration errors to the caller', async ({ api }) => {
const { webhookPath } = await api.workflowApi.importWorkflow(
const { webhookPath } = await api.workflows.importWorkflow(
'webhook-misconfiguration-test.json',
);
@@ -2,7 +2,7 @@ import { test, expect } from '../../fixtures/base';
test.describe('Webhook Origin Isolation', () => {
test.beforeAll(async ({ api }) => {
await api.workflowApi.importWorkflow('webhook-origin-isolation.json', { makeUnique: false });
await api.workflows.importWorkflow('webhook-origin-isolation.json', { makeUnique: false });
});
const webhookPaths = [