test: Refactor node creator tests to Playwright (#19808)

This commit is contained in:
Declan Carroll 2025-09-22 11:32:40 +01:00 committed by GitHub
parent b6b3b7007e
commit c0b553e464
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 751 additions and 587 deletions

View File

@ -1,576 +0,0 @@
import { clickGetBackToCanvas } from '../../composables/ndv';
import {
addNodeToCanvas,
addRetrieverNodeToParent,
addVectorStoreNodeToParent,
addVectorStoreToolToParent,
getNodeCreatorItems,
} from '../../composables/workflow';
import { AGENT_NODE_NAME, IF_NODE_NAME, MANUAL_CHAT_TRIGGER_NODE_NAME } from '../../constants';
import { NodeCreator } from '../../pages/features/node-creator';
import { NDV } from '../../pages/ndv';
import { WorkflowPage as WorkflowPageClass } from '../../pages/workflow';
import { getVisibleSelect } from '../../utils';
const nodeCreatorFeature = new NodeCreator();
const WorkflowPage = new WorkflowPageClass();
const NDVModal = new NDV();
describe('Node Creator', () => {
beforeEach(() => {
WorkflowPage.actions.visit();
});
it('should open node creator on trigger tab if no trigger is on canvas', () => {
nodeCreatorFeature.getters.canvasAddButton().click();
nodeCreatorFeature.getters
.nodeCreator()
.contains('What triggers this workflow?')
.should('be.visible');
});
it('should navigate subcategory', () => {
nodeCreatorFeature.actions.openNodeCreator();
nodeCreatorFeature.getters.getCreatorItem('On app event').click();
nodeCreatorFeature.getters.activeSubcategory().should('have.text', 'On app event');
// Go back
nodeCreatorFeature.getters.activeSubcategory().find('button').click();
nodeCreatorFeature.getters.activeSubcategory().should('not.have.text', 'On app event');
});
it('should search for nodes', () => {
nodeCreatorFeature.actions.openNodeCreator();
nodeCreatorFeature.getters.searchBar().find('input').type('manual');
nodeCreatorFeature.getters.creatorItem().should('have.length', 1);
nodeCreatorFeature.getters.searchBar().find('input').clear().type('manual123');
nodeCreatorFeature.getters.creatorItem().should('have.length', 0);
nodeCreatorFeature.getters
.noResults()
.should('exist')
.should('contain.text', "We didn't make that... yet");
nodeCreatorFeature.getters.searchBar().find('input').clear().type('edit image');
nodeCreatorFeature.getters.creatorItem().should('have.length', 1);
nodeCreatorFeature.getters
.searchBar()
.find('input')
.clear()
.type('this node totally does not exist');
nodeCreatorFeature.getters.creatorItem().should('have.length', 0);
nodeCreatorFeature.getters.searchBar().find('input').clear();
nodeCreatorFeature.getters.getCreatorItem('On app event').click();
nodeCreatorFeature.getters.searchBar().find('input').clear().type('edit image');
nodeCreatorFeature.getters.getCategoryItem('Results in other categories').should('exist');
nodeCreatorFeature.getters.creatorItem().should('have.length', 1);
nodeCreatorFeature.getters.getCreatorItem('Edit Image').should('exist');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('edit image123123');
nodeCreatorFeature.getters.creatorItem().should('have.length', 0);
});
it('should check correct view panels', () => {
nodeCreatorFeature.getters.canvasAddButton().click();
WorkflowPage.actions.addNodeToCanvas('Manual', false);
nodeCreatorFeature.getters.canvasAddButton().should('not.exist');
nodeCreatorFeature.getters.nodeCreator().should('not.exist');
// TODO: Replace once we have canvas feature utils
cy.get('div').contains('Add first step').should('not.exist');
nodeCreatorFeature.actions.openNodeCreator();
nodeCreatorFeature.getters.nodeCreator().contains('What happens next?').should('be.visible');
nodeCreatorFeature.getters.getCreatorItem('Add another trigger').click();
nodeCreatorFeature.getters
.nodeCreator()
.contains('What triggers this workflow?')
.should('be.visible');
nodeCreatorFeature.getters.activeSubcategory().find('button').should('exist');
nodeCreatorFeature.getters.activeSubcategory().find('button').click();
nodeCreatorFeature.getters.nodeCreator().contains('What happens next?').should('be.visible');
});
it('should add node to canvas from actions panel', () => {
const editImageNode = 'Edit Image';
nodeCreatorFeature.actions.openNodeCreator();
nodeCreatorFeature.getters.searchBar().find('input').clear().type(editImageNode);
nodeCreatorFeature.getters.getCreatorItem(editImageNode).click();
nodeCreatorFeature.getters.activeSubcategory().should('have.text', editImageNode);
nodeCreatorFeature.getters.getCreatorItem('Crop Image').click();
NDVModal.getters.parameterInput('operation').find('input').should('have.value', 'Crop');
});
it('should search through actions and confirm added action', () => {
nodeCreatorFeature.actions.openNodeCreator();
nodeCreatorFeature.getters.searchBar().find('input').clear().type('ftp');
nodeCreatorFeature.getters.searchBar().find('input').type('{rightarrow}');
nodeCreatorFeature.getters.activeSubcategory().should('have.text', 'FTP');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('file');
// The 1st trigger is selected, up 1x to the collapsable header, up 2x to the last action (rename)
nodeCreatorFeature.getters.searchBar().find('input').type('{uparrow}{uparrow}{rightarrow}');
NDVModal.getters.parameterInput('operation').find('input').should('have.value', 'Rename');
});
it('should not show actions for single action nodes', () => {
const singleActionNodes = [
'DHL',
'Edit Fields',
'LingvaNex',
'Mailcheck',
'MSG91',
'OpenThesaurus',
'Spontit',
'Vonage',
'Toggl Trigger',
];
const doubleActionNode = 'OpenWeatherMap';
nodeCreatorFeature.actions.openNodeCreator();
singleActionNodes.forEach((node) => {
nodeCreatorFeature.getters.searchBar().find('input').clear().type(node);
nodeCreatorFeature.getters
.getCreatorItem(node)
.find('button[class*="panelIcon"]')
.should('not.exist');
});
nodeCreatorFeature.getters.searchBar().find('input').clear().type(doubleActionNode);
nodeCreatorFeature.getters.getCreatorItem(doubleActionNode).click();
nodeCreatorFeature.getters.creatorItem().should('have.length', 4);
});
it('should have "Actions" section collapsed when opening actions view from Trigger root view', () => {
nodeCreatorFeature.actions.openNodeCreator();
nodeCreatorFeature.getters.searchBar().find('input').clear().type('ActiveCampaign');
nodeCreatorFeature.getters.getCreatorItem('ActiveCampaign').click();
nodeCreatorFeature.getters.getCategoryItem('Actions').should('exist');
nodeCreatorFeature.getters.getCategoryItem('Triggers').should('exist');
nodeCreatorFeature.getters
.getCategoryItem('Triggers')
.parent()
.should('have.attr', 'data-category-collapsed', 'false');
nodeCreatorFeature.getters
.getCategoryItem('Actions')
.parent()
.should('have.attr', 'data-category-collapsed', 'true');
nodeCreatorFeature.getters.getCategoryItem('Actions').click();
nodeCreatorFeature.getters
.getCategoryItem('Actions')
.parent()
.should('have.attr', 'data-category-collapsed', 'false');
});
it('should have "Triggers" section collapsed when opening actions view from Regular root view', () => {
nodeCreatorFeature.actions.openNodeCreator();
nodeCreatorFeature.getters.getCreatorItem('Trigger manually').click();
nodeCreatorFeature.actions.openNodeCreator();
nodeCreatorFeature.getters.searchBar().find('input').clear().type('n8n');
nodeCreatorFeature.getters.getCreatorItem('n8n').click();
nodeCreatorFeature.getters
.getCategoryItem('Actions')
.parent()
.should('have.attr', 'data-category-collapsed', 'false');
nodeCreatorFeature.getters.getCategoryItem('Actions').click();
nodeCreatorFeature.getters
.getCategoryItem('Actions')
.parent()
.should('have.attr', 'data-category-collapsed', 'true');
nodeCreatorFeature.getters
.getCategoryItem('Triggers')
.parent()
.should('have.attr', 'data-category-collapsed', 'true');
nodeCreatorFeature.getters.getCategoryItem('Triggers').click();
nodeCreatorFeature.getters
.getCategoryItem('Triggers')
.parent()
.should('have.attr', 'data-category-collapsed', 'false');
});
it('should show callout and two suggested nodes if node has no trigger actions', () => {
nodeCreatorFeature.actions.openNodeCreator();
nodeCreatorFeature.getters
.searchBar()
.find('input')
.clear()
.type('Customer Datastore (n8n training)');
nodeCreatorFeature.getters.getCreatorItem('Customer Datastore (n8n training)').click();
cy.getByTestId('actions-panel-no-triggers-callout').should('be.visible');
nodeCreatorFeature.getters.getCreatorItem('On a Schedule').should('be.visible');
nodeCreatorFeature.getters.getCreatorItem('On a Webhook call').should('be.visible');
});
it('should show intro callout if user has not made a production execution', () => {
nodeCreatorFeature.actions.openNodeCreator();
nodeCreatorFeature.getters
.searchBar()
.find('input')
.clear()
.type('Customer Datastore (n8n training)');
nodeCreatorFeature.getters.getCreatorItem('Customer Datastore (n8n training)').click();
cy.getByTestId('actions-panel-activation-callout').should('be.visible');
nodeCreatorFeature.getters.activeSubcategory().find('button').click();
nodeCreatorFeature.getters.searchBar().find('input').clear();
nodeCreatorFeature.getters.getCreatorItem('On a schedule').click();
// Setup 1s interval execution
cy.getByTestId('parameter-input-field').click();
getVisibleSelect().find('.option-headline').contains('Seconds').click();
cy.getByTestId('parameter-input-secondsInterval').clear().type('1');
NDVModal.actions.close();
nodeCreatorFeature.actions.openNodeCreator();
nodeCreatorFeature.getters
.searchBar()
.find('input')
.clear()
.type('Customer Datastore (n8n training)');
nodeCreatorFeature.getters.getCreatorItem('Customer Datastore (n8n training)').click();
nodeCreatorFeature.getters.getCreatorItem('Get All People').click();
NDVModal.actions.close();
WorkflowPage.actions.saveWorkflowOnButtonClick();
WorkflowPage.actions.activateWorkflow();
WorkflowPage.getters.activatorSwitch().should('have.class', 'is-checked');
// Wait for schedule 1s execution to mark user as having made a production execution
cy.wait(1500);
cy.reload();
// Action callout should not be visible after user has made a production execution
nodeCreatorFeature.actions.openNodeCreator();
nodeCreatorFeature.getters
.searchBar()
.find('input')
.clear()
.type('Customer Datastore (n8n training)');
nodeCreatorFeature.getters.getCreatorItem('Customer Datastore (n8n training)').click();
cy.getByTestId('actions-panel-activation-callout').should('not.exist');
});
it('should show Trigger and Actions sections during search', () => {
nodeCreatorFeature.actions.openNodeCreator();
nodeCreatorFeature.getters
.searchBar()
.find('input')
.clear()
.type('Customer Datastore (n8n training)');
nodeCreatorFeature.getters.getCreatorItem('Customer Datastore (n8n training)').click();
nodeCreatorFeature.getters.searchBar().find('input').clear().type('Non existent action name');
nodeCreatorFeature.getters.getCategoryItem('Triggers').should('be.visible');
nodeCreatorFeature.getters.getCategoryItem('Actions').should('be.visible');
cy.getByTestId('actions-panel-no-triggers-callout').should('be.visible');
nodeCreatorFeature.getters.getCreatorItem('On a Schedule').should('be.visible');
nodeCreatorFeature.getters.getCreatorItem('On a Webhook call').should('be.visible');
});
describe('should correctly append manual trigger for regular actions', () => {
// For these sources, manual node should be added
const sourcesWithAppend = [
{
name: 'canvas add button',
handler: () => nodeCreatorFeature.getters.canvasAddButton().click(),
},
{
name: 'plus button',
handler: () => nodeCreatorFeature.getters.plusButton().click(),
},
// We can't test this one because it's not possible to trigger tab key in Cypress
// only way is to use `realPress` which is hanging the tests in Electron for some reason
// {
// name: 'tab key',
// handler: () => cy.realPress('Tab'),
// },
];
sourcesWithAppend.forEach((source) => {
it(`should append manual trigger when source is ${source.name}`, () => {
source.handler();
nodeCreatorFeature.getters.searchBar().find('input').clear().type('n8n');
nodeCreatorFeature.getters.getCreatorItem('n8n').click();
nodeCreatorFeature.getters.getCategoryItem('Actions').click();
nodeCreatorFeature.getters.getCreatorItem('Create a credential').click();
NDVModal.actions.close();
WorkflowPage.getters.canvasNodes().should('have.length', 2);
});
});
// @TODO FIX ADDING 2 NODES IN ONE GO
it('should not append manual trigger when source is canvas related', () => {
nodeCreatorFeature.getters.canvasAddButton().click();
nodeCreatorFeature.getters.searchBar().find('input').clear().type('n8n');
nodeCreatorFeature.getters.getCreatorItem('n8n').click();
nodeCreatorFeature.getters.getCategoryItem('Actions').click();
nodeCreatorFeature.getters.getCreatorItem('Create a credential').click();
NDVModal.actions.close();
WorkflowPage.actions.deleteNode('When clicking Execute workflow');
WorkflowPage.getters.canvasNodePlusEndpointByName('Create a credential').click();
nodeCreatorFeature.getters.searchBar().find('input').clear().type('n8n');
nodeCreatorFeature.getters.getCreatorItem('n8n').click();
nodeCreatorFeature.getters.getCategoryItem('Actions').click();
nodeCreatorFeature.getters.getCreatorItem('Create a credential').click();
NDVModal.actions.close();
WorkflowPage.getters.canvasNodes().should('have.length', 2);
WorkflowPage.actions.zoomToFit();
WorkflowPage.actions.addNodeBetweenNodes(
'Create a credential',
'Create a credential1',
'Summarize',
);
WorkflowPage.getters.canvasNodes().should('have.length', 3);
});
});
it('should correctly append a No Op node when Loop Over Items node is added (from add button)', () => {
nodeCreatorFeature.actions.openNodeCreator();
nodeCreatorFeature.getters.searchBar().find('input').type('Loop Over Items');
nodeCreatorFeature.getters.getCreatorItem('Loop Over Items').click();
NDVModal.actions.close();
WorkflowPage.getters.canvasNodes().should('have.length', 3);
WorkflowPage.getters.nodeConnections().should('have.length', 3);
WorkflowPage.getters.getConnectionBetweenNodes('Loop Over Items', 'Replace Me').should('exist');
WorkflowPage.getters.getConnectionBetweenNodes('Replace Me', 'Loop Over Items').should('exist');
});
it('should correctly append a No Op node when Loop Over Items node is added (from connection)', () => {
WorkflowPage.actions.addNodeToCanvas('Manual');
cy.getByTestId('canvas-handle-plus').click();
nodeCreatorFeature.getters.searchBar().find('input').type('Loop Over Items');
nodeCreatorFeature.getters.getCreatorItem('Loop Over Items').click();
NDVModal.actions.close();
WorkflowPage.getters.canvasNodes().should('have.length', 3);
WorkflowPage.getters.nodeConnections().should('have.length', 3);
WorkflowPage.getters.getConnectionBetweenNodes('Loop Over Items', 'Replace Me').should('exist');
WorkflowPage.getters.getConnectionBetweenNodes('Replace Me', 'Loop Over Items').should('exist');
});
// Skipping while we wait for a decision on how to handle the search results
// eslint-disable-next-line n8n-local-rules/no-skipped-tests
it.skip('should have most relevant nodes on top when searching', () => {
nodeCreatorFeature.getters.canvasAddButton().click();
nodeCreatorFeature.getters.searchBar().find('input').clear().type('email');
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Email Trigger (IMAP)');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('Set');
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Edit Fields (Set)');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('i');
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', IF_NODE_NAME);
nodeCreatorFeature.getters.nodeItemName().eq(1).should('have.text', 'Switch');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('sw');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('Edit F');
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Edit Fields (Set)');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('i');
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', IF_NODE_NAME);
nodeCreatorFeature.getters.nodeItemName().eq(1).should('have.text', 'Switch');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('IF');
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', IF_NODE_NAME);
nodeCreatorFeature.getters.nodeItemName().eq(1).should('have.text', 'Switch');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('sw');
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Switch');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('swit');
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Switch');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('red');
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Redis');
nodeCreatorFeature.getters.nodeItemName().eq(1).should('have.text', 'Reddit');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('redd');
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Reddit');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('wh');
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Webhook');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('web');
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Webflow');
nodeCreatorFeature.getters.nodeItemName().eq(1).should('have.text', 'Webhook');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('webh');
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Webhook');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('func');
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Code');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('cod');
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Coda');
nodeCreatorFeature.getters.nodeItemName().eq(1).should('have.text', 'Code');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('code');
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Code');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('js');
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Code');
nodeCreatorFeature.getters.nodeItemName().eq(1).should('have.text', 'Edit Fields (Set)');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('fi');
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Filter');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('filt');
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Filter');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('manu');
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Manual Trigger');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('sse');
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'SSE Trigger');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('cmpar');
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Compare Datasets');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('fb');
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Facebook Trigger');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('crn');
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Schedule Trigger');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('cron');
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Schedule Trigger');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('sch');
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Schedule Trigger');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('time');
nodeCreatorFeature.getters.nodeItemName().eq(1).should('have.text', 'Schedule Trigger');
nodeCreatorFeature.getters.nodeItemName().eq(2).should('have.text', 'Wait');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('mail');
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Mailgun');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('mailc');
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Mailcheck');
nodeCreatorFeature.getters.nodeItemName().eq(1).should('have.text', 'Mailchimp');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('api');
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'HTTP Request');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('s3');
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'S3');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('no op');
nodeCreatorFeature.getters
.nodeItemName()
.first()
.should('have.text', 'No Operation, do nothing');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('do no');
nodeCreatorFeature.getters
.nodeItemName()
.first()
.should('have.text', 'No Operation, do nothing');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('htt');
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'HTTP Request');
nodeCreatorFeature.getters.nodeItemName().eq(1).should('have.text', 'Webhook');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('http');
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'HTTP Request');
nodeCreatorFeature.getters.nodeItemName().eq(1).should('have.text', 'Webhook');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('wa');
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Wait');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('wait');
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Wait');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('spreadsheet');
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Convert to File');
nodeCreatorFeature.getters.nodeItemName().eq(1).should('have.text', 'Extract from File');
nodeCreatorFeature.getters.nodeItemName().eq(2).should('have.text', 'Google Sheets');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('sheets');
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Google Sheets');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('ggle she');
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Google Sheets');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('hub');
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'HubSpot');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('git');
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Git');
nodeCreatorFeature.getters.nodeItemName().eq(1).should('have.text', 'GitHub');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('gith');
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'GitHub');
});
it('should show vector stores actions', () => {
const actions = [
'Get ranked documents from vector store',
'Add documents to vector store',
'Retrieve documents for Chain/Tool as Vector Store',
];
nodeCreatorFeature.actions.openNodeCreator();
nodeCreatorFeature.getters.searchBar().find('input').clear().type('Vector Store');
getNodeCreatorItems().then((items) => {
const vectorStores = items.map((_i, el) => el.innerText);
// Loop over all vector stores and check if they have the three actions
vectorStores.each((_i, vectorStore) => {
if (vectorStore.includes('RAG starter template')) {
return;
}
nodeCreatorFeature.getters.getCreatorItem(vectorStore).click();
actions.forEach((action) => {
nodeCreatorFeature.getters.getCreatorItem(action).should('be.visible').realHover();
});
cy.realPress('ArrowLeft');
});
});
});
it('should add node directly for sub-connection as vector store', () => {
addNodeToCanvas('Question and Answer Chain', true);
addRetrieverNodeToParent('Vector Store Retriever', 'Question and Answer Chain');
cy.realPress('Escape');
addVectorStoreNodeToParent('Simple Vector Store', 'Vector Store Retriever');
cy.realPress('Escape');
WorkflowPage.getters.canvasNodes().should('have.length', 4);
});
it('should add node directly for sub-connection as tool', () => {
addNodeToCanvas(MANUAL_CHAT_TRIGGER_NODE_NAME, true, false, undefined, true);
addNodeToCanvas(AGENT_NODE_NAME, true, true);
clickGetBackToCanvas();
addVectorStoreToolToParent('Simple Vector Store', AGENT_NODE_NAME);
});
it('should insert node to canvas with sendAndWait operation selected', () => {
nodeCreatorFeature.getters.canvasAddButton().click();
WorkflowPage.actions.addNodeToCanvas('Manual', false);
nodeCreatorFeature.actions.openNodeCreator();
cy.contains('Human in the loop').click();
nodeCreatorFeature.getters.getCreatorItem('Slack').click();
cy.contains('Send and Wait for Response').should('exist');
});
});

View File

@ -0,0 +1,255 @@
# 🚀 n8n Playwright Test Writing Cheat Sheet
> **For AI Assistants**: This guide provides quick reference patterns for writing n8n Playwright tests using the established architecture.
## Quick Start Navigation Methods
### **n8n.start.*** Methods (Test Entry Points)
```typescript
// Start from home page
await n8n.start.fromHome();
// Start with blank canvas for new workflow
await n8n.start.fromBlankCanvas();
// Start with new project + blank canvas (returns projectId)
const projectId = await n8n.start.fromNewProjectBlankCanvas();
// Start with just a new project (no canvas)
const projectId = await n8n.start.fromNewProject();
// Import and start from existing workflow JSON
const result = await n8n.start.fromImportedWorkflow('simple-webhook-test.json');
const { workflowId, webhookPath } = result;
```
### **n8n.navigate.*** Methods (Page Navigation)
```typescript
// Basic navigation
await n8n.navigate.toHome();
await n8n.navigate.toWorkflow('new');
await n8n.navigate.toWorkflows(projectId);
// Settings & admin
await n8n.navigate.toVariables();
await n8n.navigate.toCredentials(projectId);
await n8n.navigate.toLogStreaming();
await n8n.navigate.toCommunityNodes();
// Project-specific navigation
await n8n.navigate.toProject(projectId);
await n8n.navigate.toProjectSettings(projectId);
```
## Common Test Patterns
### **Basic Workflow Test**
```typescript
test('should create and execute workflow', async ({ n8n }) => {
await n8n.start.fromBlankCanvas();
await n8n.canvas.addNode('Manual Trigger');
await n8n.canvas.addNode('Set');
await n8n.workflowComposer.executeWorkflowAndWaitForNotification('Success');
});
```
### **Imported Workflow Test**
```typescript
test('should import and test webhook', async ({ n8n }) => {
const { webhookPath } = await n8n.start.fromImportedWorkflow('webhook-test.json');
await n8n.canvas.clickExecuteWorkflowButton();
const response = await n8n.page.request.post(`/webhook-test/${webhookPath}`, {
data: { message: 'Hello' }
});
expect(response.ok()).toBe(true);
});
```
### **Project-Scoped Test**
```typescript
test('should create credential in project', async ({ n8n }) => {
const projectId = await n8n.start.fromNewProject();
await n8n.navigate.toCredentials(projectId);
await n8n.credentialsComposer.createFromList(
'Notion API',
{ apiKey: '12345' },
{ name: `cred-${nanoid()}` }
);
});
```
### **Node Configuration Test**
```typescript
test('should configure HTTP Request node', async ({ n8n }) => {
await n8n.start.fromBlankCanvas();
await n8n.canvas.addNode('Manual Trigger');
await n8n.canvas.addNode('HTTP Request');
await n8n.ndv.fillParameterInput('URL', 'https://api.example.com');
await n8n.ndv.close();
await n8n.canvas.saveWorkflow();
});
```
## Test Setup Patterns
### **Feature Flags Setup**
```typescript
test.beforeEach(async ({ n8n, api }) => {
await api.enableFeature('sharing');
await api.enableFeature('folders');
await api.enableFeature('projectRole:admin');
await api.setMaxTeamProjectsQuota(-1);
await n8n.goHome();
});
```
### **API + UI Combined Test**
```typescript
test('should use API-created credential in UI', async ({ n8n, api }) => {
const projectId = await n8n.start.fromNewProjectBlankCanvas();
// Create via API
await api.credentialApi.createCredential({
name: 'test-cred',
type: 'notionApi',
data: { apiKey: '12345' },
projectId
});
// Verify in UI
await n8n.canvas.addNode('Notion');
await expect(n8n.ndv.getCredentialSelect()).toHaveValue('test-cred');
});
```
### **Error/Edge Case Testing**
```typescript
test('should handle workflow execution error', async ({ n8n }) => {
await n8n.start.fromImportedWorkflow('failing-workflow.json');
await n8n.workflowComposer.executeWorkflowAndWaitForNotification('Problem in node');
await expect(n8n.canvas.getErrorIcon()).toBeVisible();
});
```
## Architecture Guidelines
### **Four-Layer UI Testing Architecture**
```
Tests (*.spec.ts)
↓ uses
Composables (*Composer.ts) - Business workflows
↓ orchestrates
Page Objects (*Page.ts) - UI interactions
↓ extends
BasePage - Common utilities
```
### **When to Use Each Layer**
- **Tests**: High-level scenarios, readable business logic
- **Composables**: Multi-step workflows (e.g., `executeWorkflowAndWaitForNotification`)
- **Page Objects**: Simple UI actions (e.g., `clickSaveButton`, `fillInput`)
- **BasePage**: Generic interactions (e.g., `clickByTestId`, `fillByTestId`)
### **Method Naming Conventions**
```typescript
// Page Object Getters (No async, return Locator)
getSearchBar() { return this.page.getByTestId('search'); }
// Page Object Actions (async, return void)
async clickSaveButton() { await this.clickButtonByName('Save'); }
// Page Object Queries (async, return data)
async getNotificationCount(): Promise<number> { /* ... */ }
```
## Quick Reference
### **Most Common Entry Points**
- `n8n.start.fromBlankCanvas()` - New workflow from scratch
- `n8n.start.fromImportedWorkflow('file.json')` - Test existing workflow
- `n8n.start.fromNewProjectBlankCanvas()` - Project-scoped testing
### **Most Common Navigation**
- `n8n.navigate.toCredentials(projectId)` - Credential management
- `n8n.navigate.toVariables()` - Environment variables
- `n8n.navigate.toWorkflow('new')` - New workflow canvas
### **Essential Assertions**
```typescript
// UI state verification
await expect(n8n.canvas.canvasPane()).toBeVisible();
await expect(n8n.notifications.getNotificationByTitle('Success')).toBeVisible();
await expect(n8n.ndv.getCredentialSelect()).toHaveValue(name);
// Node and workflow verification
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(2);
await expect(n8n.canvas.nodeByName('HTTP Request')).toBeVisible();
```
### **Common Composable Methods**
```typescript
// Workflow operations
await n8n.workflowComposer.executeWorkflowAndWaitForNotification('Success');
await n8n.workflowComposer.createWorkflow('My Workflow');
// Project operations
const { projectName, projectId } = await n8n.projectComposer.createProject();
// Credential operations
await n8n.credentialsComposer.createFromList('Notion API', { apiKey: '123' });
await n8n.credentialsComposer.createFromNdv({ apiKey: '123' });
```
### **Dynamic Data Patterns**
```typescript
// Use nanoid for unique identifiers
import { nanoid } from 'nanoid';
const workflowName = `Test Workflow ${nanoid()}`;
const credentialName = `cred-${nanoid()}`;
// Use timestamps for uniqueness
const projectName = `Project ${Date.now()}`;
```
## AI Guidelines
### **✅ DO**
- Always use `n8n.start.*` methods for test entry points
- Use composables for business workflows, not page objects directly in tests
- Use `nanoid()` or timestamps for unique test data
- Follow the 4-layer architecture pattern
- Use proper waiting with `expect().toBeVisible()` instead of `waitForTimeout`
### **❌ DON'T**
- Use raw `page.goto()` instead of navigation helpers
- Mix business logic in page objects (move to composables)
- Use hardcoded selectors in tests (use page object getters)
- Create overly specific methods (keep them reusable)
- Use `any` types or `waitForTimeout`
### **Test Structure Template**
```typescript
import { test, expect } from '../../fixtures/base';
test.describe('Feature Name', () => {
test.beforeEach(async ({ n8n, api }) => {
// Feature flags and setup
await api.enableFeature('requiredFeature');
await n8n.goHome();
});
test('should perform specific action', async ({ n8n }) => {
// 1. Setup/Navigation
await n8n.start.fromBlankCanvas();
// 2. Actions using composables
await n8n.workflowComposer.createBasicWorkflow();
// 3. Assertions
await expect(n8n.notifications.getNotificationByTitle('Success')).toBeVisible();
});
});
```

View File

@ -7,6 +7,7 @@ import { resolveFromRoot } from '../utils/path-helper';
import { CredentialModal } from './components/CredentialModal';
import { FocusPanel } from './components/FocusPanel';
import { LogsPanel } from './components/LogsPanel';
import { NodeCreator } from './components/NodeCreator';
import { StickyComponent } from './components/StickyComponent';
export class CanvasPage extends BasePage {
@ -14,6 +15,7 @@ export class CanvasPage extends BasePage {
readonly logsPanel = new LogsPanel(this.page.getByTestId('logs-panel'));
readonly focusPanel = new FocusPanel(this.page.getByTestId('focus-panel'));
readonly credentialModal = new CredentialModal(this.page.getByTestId('editCredential-modal'));
readonly nodeCreator = new NodeCreator(this.page);
saveWorkflowButton(): Locator {
return this.page.getByRole('button', { name: 'Save' });
@ -387,6 +389,10 @@ export class CanvasPage extends BasePage {
return this.page.locator('[data-test-id="edge"]');
}
getConnectionBetweenNodes(sourceNodeName: string, targetNodeName: string): Locator {
return this.connectionBetweenNodes(sourceNodeName, targetNodeName);
}
canvasNodePlusEndpointByName(nodeName: string): Locator {
return this.page
.locator(
@ -736,4 +742,8 @@ export class CanvasPage extends BasePage {
getNodeWarningStatusIndicator(nodeName: string): Locator {
return this.nodeByName(nodeName).getByTestId('canvas-node-status-warning');
}
getCanvasPlusButton(): Locator {
return this.page.getByTestId('canvas-plus-button');
}
}

View File

@ -0,0 +1,114 @@
import type { Locator, Page } from '@playwright/test';
import { expect } from '@playwright/test';
/**
* Node Creator component for adding nodes to workflows.
* Used within CanvasPage as `n8n.canvas.nodeCreator.*`
*
* @example
* // Access via canvas page
* await n8n.canvas.nodeCreator.open();
* await n8n.canvas.nodeCreator.searchFor('Gmail');
* await n8n.canvas.nodeCreator.selectItem('Gmail');
*/
export class NodeCreator {
constructor(private page: Page) {}
// Core locators
getRoot(): Locator {
return this.page.getByTestId('node-creator');
}
getSearchBar(): Locator {
return this.page.getByTestId('node-creator-search-bar');
}
getNodeItems(): Locator {
return this.page.getByTestId('item-iterator-item');
}
getActionItems(): Locator {
return this.page.getByTestId('node-creator-action-item');
}
getCategoryItems(): Locator {
return this.page.getByTestId('node-creator-category-item');
}
getTabs(): Locator {
return this.page.getByTestId('node-creator-type-selector');
}
getSelectedTab(): Locator {
return this.getTabs().locator('.is-active');
}
getActiveSubcategory(): Locator {
return this.page.getByTestId('nodes-list-header').first();
}
getNoResults(): Locator {
return this.page.getByTestId('node-creator-no-results');
}
getTriggerText(): Locator {
return this.page.getByText('What triggers this workflow?');
}
getNextText(): Locator {
return this.page.getByText('What happens next?');
}
// Item getters
getItem(text: string): Locator {
return this.getNodeItems().filter({ hasText: text }).first();
}
getCategoryItem(text: string): Locator {
return this.getCategoryItems().filter({ hasText: text });
}
getPanelIcon(nodeName: string): Locator {
return this.getItem(nodeName).locator('[class*="panelIcon"]');
}
// Actions
async open(): Promise<void> {
await this.page.getByTestId('canvas-plus-button').click();
await expect(this.getRoot()).toBeVisible();
}
async close(): Promise<void> {
await this.page.keyboard.press('Escape');
}
async searchFor(text: string): Promise<void> {
await this.getSearchBar().fill(text);
}
async clearSearch(): Promise<void> {
await this.getSearchBar().clear();
}
async selectItem(text: string): Promise<void> {
await this.getItem(text).click();
}
async selectCategoryItem(text: string): Promise<void> {
await this.getCategoryItem(text).click();
}
async navigateToSubcategory(category: string): Promise<void> {
await this.getItem(category).click();
await expect(this.getActiveSubcategory()).toContainText(category);
}
async goBackFromSubcategory(): Promise<void> {
await this.getActiveSubcategory().locator('button').click();
}
async selectWithKeyboard(direction: 'up' | 'down' | 'right'): Promise<void> {
const key = direction === 'up' ? 'ArrowUp' : direction === 'down' ? 'ArrowDown' : 'ArrowRight';
await this.page.keyboard.press(key);
}
}

View File

@ -182,7 +182,7 @@ test.describe('Projects', () => {
// Verify owner cannot change their own role
const ownerRow = memberRows.first();
const roleDropdown = ownerRow.getByTestId('project-member-role-dropdown');
await expect(roleDropdown).not.toBeVisible();
await expect(roleDropdown).toBeHidden();
});
test('should display role dropdown for members but not for current user @auth:owner', async ({
@ -197,7 +197,7 @@ test.describe('Projects', () => {
// 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')).not.toBeVisible();
await expect(currentUserRow.getByTestId('project-member-role-dropdown')).toBeHidden();
// The role should be displayed as static text for the current user
await expect(currentUserRow.getByText('Admin')).toBeVisible();
@ -246,7 +246,7 @@ test.describe('Projects', () => {
await n8n.projectSettings.fillProjectName('Valid Project Name');
// Save button should now be enabled
await expect(saveButton).not.toBeDisabled();
await expect(saveButton).toBeEnabled();
});
test('should handle unsaved changes state @auth:owner', async ({ n8n }) => {
@ -265,8 +265,8 @@ test.describe('Projects', () => {
await n8n.projectSettings.fillProjectName('Modified Name');
// Save and cancel buttons should now be enabled
await expect(n8n.page.getByTestId('project-settings-save-button')).not.toBeDisabled();
await expect(n8n.page.getByTestId('project-settings-cancel-button')).not.toBeDisabled();
await expect(n8n.page.getByTestId('project-settings-save-button')).toBeEnabled();
await expect(n8n.page.getByTestId('project-settings-cancel-button')).toBeEnabled();
// Unsaved changes message should be visible
await expect(n8n.page.getByText('You have unsaved changes')).toBeVisible();

View File

@ -26,14 +26,14 @@ test.describe('Focus panel', () => {
// Assert that mapper is closed but the Set node is still selected and shown in
await n8n.canvas.canvasBody().click({ position: { x: 0, y: 0 } });
await expect(n8n.canvas.focusPanel.getMapper()).not.toBeVisible();
await expect(n8n.canvas.focusPanel.getMapper()).toBeHidden();
await expect(n8n.canvas.focusPanel.getHeader()).toHaveText('Set');
await expect(n8n.canvas.selectedNodes()).toHaveCount(1);
// Assert that another click on canvas does de-select the Set node
await n8n.canvas.canvasBody().click({ position: { x: 0, y: 0 } });
await expect(n8n.canvas.focusPanel.getHeader()).not.toBeVisible();
await expect(n8n.canvas.focusPanel.getHeader()).toBeHidden();
await expect(n8n.canvas.selectedNodes()).toHaveCount(0);
});
});

View File

@ -86,12 +86,10 @@ test.describe('Canvas Node Actions', () => {
test('should filter nodes by search term', async ({ n8n }) => {
await n8n.canvas.clickCanvasPlusButton();
const initialCount = await n8n.canvas.nodeCreatorNodeItems().count();
await n8n.canvas.fillNodeCreatorSearchBar('HTTP');
const filteredCount = await n8n.canvas.nodeCreatorNodeItems().count();
expect(filteredCount).toBeLessThan(initialCount);
expect(filteredCount).toBeGreaterThan(0);
const filteredItems = n8n.canvas.nodeCreatorNodeItems();
await expect(filteredItems.first()).toContainText('HTTP');
});
});
});

View File

@ -0,0 +1,63 @@
import { test, expect } from '../../../fixtures/base';
test.describe('Node Creator Actions', () => {
test.beforeEach(async ({ n8n }) => {
await n8n.start.fromBlankCanvas();
});
test('should add node to canvas from actions panel', async ({ n8n }) => {
const editImageNode = 'Edit Image';
await n8n.canvas.nodeCreator.open();
await n8n.canvas.nodeCreator.searchFor(editImageNode);
await n8n.canvas.nodeCreator.selectItem(editImageNode);
await expect(n8n.canvas.nodeCreator.getActiveSubcategory()).toContainText(editImageNode);
await n8n.canvas.nodeCreator.selectItem('Crop Image');
await expect(n8n.ndv.getContainer()).toBeVisible();
await n8n.page.keyboard.press('Escape');
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(2);
});
test('should search through actions and confirm added action', async ({ n8n }) => {
await n8n.canvas.nodeCreator.open();
await n8n.canvas.nodeCreator.searchFor('ftp');
await n8n.canvas.nodeCreator.selectItem('FTP');
await expect(n8n.canvas.nodeCreator.getActiveSubcategory()).toContainText('FTP');
await n8n.canvas.nodeCreator.clearSearch();
await n8n.canvas.nodeCreator.searchFor('rename');
await n8n.canvas.nodeCreator.selectItem('Rename');
await expect(n8n.ndv.getContainer()).toBeVisible();
await n8n.page.keyboard.press('Escape');
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(2);
});
test('should show multiple actions for multi-action nodes', async ({ n8n }) => {
await n8n.canvas.nodeCreator.open();
await n8n.canvas.nodeCreator.searchFor('OpenWeatherMap');
await n8n.canvas.nodeCreator.selectItem('OpenWeatherMap');
await expect(n8n.canvas.nodeCreator.getActiveSubcategory()).toContainText('OpenWeatherMap');
await expect(n8n.canvas.nodeCreator.getNodeItems().first()).toBeVisible();
await expect(n8n.canvas.nodeCreator.getNodeItems().nth(1)).toBeVisible();
await n8n.canvas.nodeCreator.getNodeItems().first().click();
await n8n.page.keyboard.press('Escape');
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(2);
});
test('should add node with specific operation configuration', async ({ n8n }) => {
await n8n.canvas.nodeCreator.open();
await n8n.canvas.nodeCreator.searchFor('Slack');
await n8n.canvas.nodeCreator.selectItem('Slack');
await expect(n8n.canvas.nodeCreator.getActiveSubcategory()).toContainText('Slack');
await n8n.canvas.nodeCreator.getNodeItems().first().click();
await n8n.page.keyboard.press('Escape');
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(1);
});
});

View File

@ -0,0 +1,101 @@
import { MANUAL_TRIGGER_NODE_DISPLAY_NAME } from '../../../config/constants';
import { test, expect } from '../../../fixtures/base';
test.describe('Node Creator Categories', () => {
test.beforeEach(async ({ n8n }) => {
await n8n.start.fromBlankCanvas();
});
test('should have "Actions" section collapsed when opening actions view from Trigger root view', async ({
n8n,
}) => {
await n8n.canvas.nodeCreator.open();
await n8n.canvas.nodeCreator.searchFor('ActiveCampaign');
await n8n.canvas.nodeCreator.selectItem('ActiveCampaign');
await expect(n8n.canvas.nodeCreator.getCategoryItem('Actions')).toBeVisible();
await expect(n8n.canvas.nodeCreator.getCategoryItem('Triggers')).toBeVisible();
await expect(n8n.canvas.nodeCreator.getCategoryItem('Triggers').locator('..')).toHaveAttribute(
'data-category-collapsed',
'false',
);
await expect(n8n.canvas.nodeCreator.getCategoryItem('Actions').locator('..')).toHaveAttribute(
'data-category-collapsed',
'true',
);
await n8n.canvas.nodeCreator.selectCategoryItem('Actions');
await expect(n8n.canvas.nodeCreator.getCategoryItem('Actions').locator('..')).toHaveAttribute(
'data-category-collapsed',
'false',
);
});
test('should have "Triggers" section collapsed when opening actions view from Regular root view', async ({
n8n,
}) => {
await n8n.canvas.addNode('Manual Trigger');
await n8n.canvas.clickNodePlusEndpoint(MANUAL_TRIGGER_NODE_DISPLAY_NAME);
await n8n.canvas.nodeCreator.searchFor('n8n');
await n8n.canvas.nodeCreator.getNodeItems().filter({ hasText: 'n8n' }).first().click();
await expect(n8n.canvas.nodeCreator.getCategoryItem('Actions').locator('..')).toHaveAttribute(
'data-category-collapsed',
'false',
);
await n8n.canvas.nodeCreator.selectCategoryItem('Actions');
await expect(n8n.canvas.nodeCreator.getCategoryItem('Actions').locator('..')).toHaveAttribute(
'data-category-collapsed',
'true',
);
await expect(n8n.canvas.nodeCreator.getCategoryItem('Triggers').locator('..')).toHaveAttribute(
'data-category-collapsed',
'true',
);
await n8n.canvas.nodeCreator.selectCategoryItem('Triggers');
await expect(n8n.canvas.nodeCreator.getCategoryItem('Triggers').locator('..')).toHaveAttribute(
'data-category-collapsed',
'false',
);
});
test('should show callout and two suggested nodes if node has no trigger actions', async ({
n8n,
}) => {
await n8n.canvas.nodeCreator.open();
await n8n.canvas.nodeCreator.searchFor('Customer Datastore (n8n training)');
await n8n.canvas.nodeCreator.selectItem('Customer Datastore (n8n training)');
await expect(n8n.page.getByTestId('actions-panel-no-triggers-callout')).toBeVisible();
await expect(n8n.canvas.nodeCreator.getItem('On a Schedule')).toBeVisible();
await expect(n8n.canvas.nodeCreator.getItem('On a Webhook call')).toBeVisible();
});
test('should show intro callout if user has not made a production execution', async ({ n8n }) => {
await n8n.canvas.nodeCreator.open();
await n8n.canvas.nodeCreator.searchFor('Customer Datastore (n8n training)');
await n8n.canvas.nodeCreator.selectItem('Customer Datastore (n8n training)');
await expect(n8n.page.getByTestId('actions-panel-activation-callout')).toBeVisible();
});
test('should show Trigger and Actions sections during search', async ({ n8n }) => {
await n8n.canvas.nodeCreator.open();
await n8n.canvas.nodeCreator.searchFor('Customer Datastore (n8n training)');
await n8n.canvas.nodeCreator.selectItem('Customer Datastore (n8n training)');
await n8n.canvas.nodeCreator.searchFor('Non existent action name');
await expect(n8n.canvas.nodeCreator.getCategoryItem('Triggers')).toBeVisible();
await expect(n8n.canvas.nodeCreator.getCategoryItem('Actions')).toBeVisible();
await expect(n8n.page.getByTestId('actions-panel-no-triggers-callout')).toBeVisible();
await expect(n8n.canvas.nodeCreator.getItem('On a Schedule')).toBeVisible();
await expect(n8n.canvas.nodeCreator.getItem('On a Webhook call')).toBeVisible();
});
});

View File

@ -0,0 +1,71 @@
import { MANUAL_TRIGGER_NODE_DISPLAY_NAME } from '../../../config/constants';
import { test, expect } from '../../../fixtures/base';
test.describe('Node Creator Navigation', () => {
test.beforeEach(async ({ n8n }) => {
await n8n.start.fromBlankCanvas();
});
test('should open node creator on trigger tab if no trigger is on canvas', async ({ n8n }) => {
await n8n.canvas.clickCanvasPlusButton();
await expect(n8n.canvas.nodeCreator.getRoot()).toBeVisible();
await expect(n8n.canvas.nodeCreator.getTriggerText()).toBeVisible();
});
test('should navigate subcategory and return', async ({ n8n }) => {
await n8n.canvas.nodeCreator.open();
await n8n.canvas.nodeCreator.navigateToSubcategory('On app event');
await expect(n8n.canvas.nodeCreator.getActiveSubcategory()).toContainText('On app event');
await n8n.canvas.nodeCreator.goBackFromSubcategory();
await expect(n8n.canvas.nodeCreator.getActiveSubcategory()).not.toContainText('On app event');
});
test('should search for nodes with various queries', async ({ n8n }) => {
await n8n.canvas.nodeCreator.open();
await n8n.canvas.nodeCreator.searchFor('manual');
await expect(n8n.canvas.nodeCreator.getNodeItems()).toHaveCount(1);
await n8n.canvas.nodeCreator.clearSearch();
await n8n.canvas.nodeCreator.searchFor('manual123');
await expect(n8n.canvas.nodeCreator.getNodeItems()).toHaveCount(0);
await expect(n8n.canvas.nodeCreator.getNoResults()).toBeVisible();
await expect(n8n.canvas.nodeCreator.getNoResults()).toContainText("We didn't make that... yet");
await n8n.canvas.nodeCreator.clearSearch();
await n8n.canvas.nodeCreator.searchFor('edit image');
await expect(n8n.canvas.nodeCreator.getNodeItems()).toHaveCount(1);
await n8n.canvas.nodeCreator.clearSearch();
await n8n.canvas.nodeCreator.searchFor('this node totally does not exist');
await expect(n8n.canvas.nodeCreator.getNodeItems()).toHaveCount(0);
await n8n.canvas.nodeCreator.clearSearch();
await n8n.canvas.nodeCreator.navigateToSubcategory('On app event');
await n8n.canvas.nodeCreator.searchFor('edit image');
await expect(
n8n.canvas.nodeCreator.getCategoryItem('Results in other categories'),
).toBeVisible();
await expect(n8n.canvas.nodeCreator.getNodeItems()).toHaveCount(1);
await expect(n8n.canvas.nodeCreator.getItem('Edit Image')).toBeVisible();
await n8n.canvas.nodeCreator.clearSearch();
await n8n.canvas.nodeCreator.searchFor('edit image123123');
await expect(n8n.canvas.nodeCreator.getNodeItems()).toHaveCount(0);
});
test('should check correct view panels after adding manual trigger', async ({ n8n }) => {
await n8n.canvas.clickCanvasPlusButton();
await expect(n8n.canvas.nodeCreator.getTriggerText()).toBeVisible();
await n8n.canvas.nodeCreator.close();
await n8n.canvas.addNode('Manual Trigger');
await expect(n8n.canvas.getCanvasPlusButton()).toBeHidden();
await n8n.canvas.clickNodePlusEndpoint(MANUAL_TRIGGER_NODE_DISPLAY_NAME);
await expect(n8n.canvas.nodeCreator.getNextText()).toBeVisible();
});
});

View File

@ -0,0 +1,50 @@
import { MANUAL_TRIGGER_NODE_DISPLAY_NAME } from '../../../config/constants';
import { test, expect } from '../../../fixtures/base';
test.describe('Node Creator Special Nodes', () => {
test.beforeEach(async ({ n8n }) => {
await n8n.start.fromBlankCanvas();
});
test('should correctly append a No Op node when Loop Over Items node is added (from add button)', async ({
n8n,
}) => {
await n8n.canvas.nodeCreator.open();
await n8n.canvas.nodeCreator.searchFor('Loop Over Items');
await n8n.canvas.nodeCreator.selectItem('Loop Over Items');
await n8n.ndv.close();
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(3);
await expect(n8n.canvas.nodeConnections()).toHaveCount(3);
await expect(n8n.canvas.nodeByName('Loop Over Items')).toBeVisible();
await expect(n8n.canvas.nodeByName('Replace Me')).toBeVisible();
});
test('should correctly append a No Op node when Loop Over Items node is added (from connection)', async ({
n8n,
}) => {
await n8n.canvas.addNode('Manual Trigger');
await n8n.canvas.clickNodePlusEndpoint(MANUAL_TRIGGER_NODE_DISPLAY_NAME);
await n8n.canvas.nodeCreator.searchFor('Loop Over Items');
await n8n.canvas.nodeCreator.selectItem('Loop Over Items');
await n8n.ndv.close();
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(3);
await expect(n8n.canvas.nodeConnections()).toHaveCount(3);
await expect(n8n.canvas.nodeByName('Loop Over Items')).toBeVisible();
await expect(n8n.canvas.nodeByName('Replace Me')).toBeVisible();
});
test('should add a Send and Wait for Response node', async ({ n8n }) => {
await n8n.canvas.addNode('Manual Trigger');
await n8n.canvas.clickNodePlusEndpoint(MANUAL_TRIGGER_NODE_DISPLAY_NAME);
await n8n.canvas.nodeCreator.selectItem('Human in the loop');
await n8n.canvas.nodeCreator.selectItem('Slack');
await n8n.ndv.setupHelper.setParameter('operation', 'Send and Wait for Response');
await n8n.ndv.close();
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(2);
});
});

View File

@ -0,0 +1,46 @@
import { MANUAL_TRIGGER_NODE_DISPLAY_NAME } from '../../../config/constants';
import { test, expect } from '../../../fixtures/base';
test.describe('Node Creator Vector Stores', () => {
test.beforeEach(async ({ n8n }) => {
await n8n.start.fromBlankCanvas();
await n8n.canvas.addNode('Manual Trigger');
});
test('should show vector stores actions', async ({ n8n }) => {
const expectedActions = [
'Get ranked documents from vector store',
'Add documents to vector store',
'Retrieve documents for Chain/Tool as Vector Store',
'Retrieve documents for AI Agent as Tool',
];
await n8n.canvas.clickNodePlusEndpoint(MANUAL_TRIGGER_NODE_DISPLAY_NAME);
await n8n.canvas.nodeCreator.searchFor('Vector Store');
await expect(n8n.canvas.nodeCreator.getNodeItems().first()).toBeVisible();
await n8n.canvas.nodeCreator.getItem('Simple Vector Store').click();
for (const action of expectedActions) {
await expect(n8n.canvas.nodeCreator.getItem(action)).toBeVisible();
}
await n8n.canvas.nodeCreator.goBackFromSubcategory();
await expect(n8n.canvas.nodeCreator.getNodeItems().first()).toBeVisible();
});
test('should find vector store nodes in creator', async ({ n8n }) => {
await n8n.canvas.clickNodePlusEndpoint(MANUAL_TRIGGER_NODE_DISPLAY_NAME);
await n8n.canvas.nodeCreator.searchFor('Vector Store');
await expect(n8n.canvas.nodeCreator.getNodeItems().first()).toBeVisible();
});
test('should search for specific vector store nodes', async ({ n8n }) => {
await n8n.canvas.clickNodePlusEndpoint(MANUAL_TRIGGER_NODE_DISPLAY_NAME);
await n8n.canvas.nodeCreator.searchFor('Simple Vector Store');
await expect(n8n.canvas.nodeCreator.getItem('Simple Vector Store')).toBeVisible();
});
});

View File

@ -0,0 +1,32 @@
import { test, expect } from '../../../fixtures/base';
test.describe('Node Creator Workflow Building', () => {
test.beforeEach(async ({ n8n }) => {
await n8n.start.fromBlankCanvas();
});
test('should append manual trigger when adding action node from canvas add button', async ({
n8n,
}) => {
await n8n.canvas.clickCanvasPlusButton();
await n8n.canvas.nodeCreator.searchFor('n8n');
await n8n.canvas.nodeCreator.selectItem('n8n');
await n8n.canvas.nodeCreator.selectCategoryItem('Actions');
await n8n.canvas.nodeCreator.selectItem('Create a credential');
await n8n.page.keyboard.press('Escape');
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(2);
await expect(n8n.canvas.nodeConnections()).toHaveCount(1);
});
test('should append manual trigger when adding action node from plus button', async ({ n8n }) => {
await n8n.canvas.clickCanvasPlusButton();
await n8n.canvas.nodeCreator.searchFor('n8n');
await n8n.canvas.nodeCreator.selectItem('n8n');
await n8n.canvas.nodeCreator.selectCategoryItem('Actions');
await n8n.canvas.nodeCreator.selectItem('Create a credential');
await n8n.page.keyboard.press('Escape');
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(2);
});
});