mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +00:00
test: Migrate 12-canvas-actions tests to Playwright (#18442)
This commit is contained in:
@@ -1,270 +0,0 @@
|
|||||||
# Cypress to Playwright Migration Guide
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
This guide outlines the systematic approach for migrating Cypress tests to Playwright in the n8n codebase, based on successful migrations and lessons learned.
|
|
||||||
|
|
||||||
## 🎯 Migration Principles
|
|
||||||
|
|
||||||
### 1. **Architecture First**
|
|
||||||
- Follow the established 4-layer architecture: Tests → Composables → Page Objects → BasePage
|
|
||||||
- Use existing composables and page objects before creating new ones
|
|
||||||
- Maintain separation of concerns: business logic in composables, UI interactions in page objects
|
|
||||||
|
|
||||||
### 2. **Search Existing Patterns First**
|
|
||||||
- **ALWAYS** search for existing Playwright patterns before implementing new functionality
|
|
||||||
- Look for working examples in existing test files (e.g., `39-projects.spec.ts`)
|
|
||||||
- Check composables and page objects for existing methods
|
|
||||||
- Framework-specific patterns may differ (Cypress display names vs Playwright field names)
|
|
||||||
|
|
||||||
### 3. **Idempotent Test Design**
|
|
||||||
- Design tests to work regardless of initial state
|
|
||||||
- Use fresh project creation for tests that need empty states
|
|
||||||
- Create test prerequisites within the test when needed
|
|
||||||
- Avoid `@db:reset` dependencies in favor of project-based isolation
|
|
||||||
|
|
||||||
## 📋 Pre-Migration Checklist
|
|
||||||
|
|
||||||
### 1. **Environment Setup**
|
|
||||||
```bash
|
|
||||||
# Start isolated test environment
|
|
||||||
cd packages/testing/playwright
|
|
||||||
pnpm start:isolated
|
|
||||||
|
|
||||||
# Run tests with proper environment
|
|
||||||
N8N_BASE_URL=http://localhost:5679 npx playwright test --reporter=list
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. **Study Existing Patterns**
|
|
||||||
- Review `CONTRIBUTING.md` for architecture guidelines
|
|
||||||
- Examine working test files (e.g., `1-workflows.spec.ts`, `39-projects.spec.ts`)
|
|
||||||
- Check available composables in `composables/` directory
|
|
||||||
- Review page objects in `pages/` directory
|
|
||||||
|
|
||||||
### 3. **Understand Framework Differences**
|
|
||||||
- **Cypress**: Uses display names (`'Internal Integration Secret'`)
|
|
||||||
- **Playwright**: Uses field names (`'apiKey'`)
|
|
||||||
- **Navigation**: Direct page navigation often more reliable than complex UI interactions
|
|
||||||
- **Selectors**: Prefer `data-test-id` over text-based selectors
|
|
||||||
|
|
||||||
## 🔄 Migration Process
|
|
||||||
|
|
||||||
### Step 1: Scaffold the Test File
|
|
||||||
```typescript
|
|
||||||
// 1. Create test file with proper imports
|
|
||||||
import { test, expect } from '../fixtures/base';
|
|
||||||
import {
|
|
||||||
// Import constants from existing patterns
|
|
||||||
NOTION_NODE_NAME,
|
|
||||||
NEW_NOTION_ACCOUNT_NAME,
|
|
||||||
// ... other constants
|
|
||||||
} from '../config/constants';
|
|
||||||
|
|
||||||
// 2. Add beforeEach setup if needed
|
|
||||||
test.describe('Feature Name', () => {
|
|
||||||
test.beforeEach(async ({ api, n8n }) => {
|
|
||||||
await api.enableFeature('sharing');
|
|
||||||
await api.enableFeature('folders');
|
|
||||||
// ... other feature flags
|
|
||||||
await n8n.goHome();
|
|
||||||
});
|
|
||||||
|
|
||||||
// 3. Scaffold all tests from Cypress file
|
|
||||||
test('should do something', async ({ n8n }) => {
|
|
||||||
// TODO: Implement based on Cypress version
|
|
||||||
console.log('Test scaffolded - ready for implementation');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 2: Research Existing Patterns
|
|
||||||
```bash
|
|
||||||
# Search for existing implementations
|
|
||||||
grep -r "addCredentialToProject" packages/testing/playwright/
|
|
||||||
grep -r "createProject" packages/testing/playwright/
|
|
||||||
grep -r "workflowComposer" packages/testing/playwright/
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 3: Implement Working Tests First
|
|
||||||
- Start with tests that have clear existing patterns
|
|
||||||
- Use composables for high-level operations (project creation, navigation)
|
|
||||||
- Use direct DOM interactions for form filling when composables don't match
|
|
||||||
- Implement one test at a time and verify it works
|
|
||||||
|
|
||||||
### Step 4: Handle Complex UI Interactions
|
|
||||||
- **Node Creation Issues**: Close NDV after adding first node to prevent overlay blocking
|
|
||||||
- **Universal Add Button**: Use direct navigation when button interactions fail
|
|
||||||
- **Modal Overlays**: Use route interception for error testing
|
|
||||||
- **Multiple Elements**: Use specific selectors to avoid strict mode violations
|
|
||||||
|
|
||||||
## 🛠️ Common Patterns
|
|
||||||
|
|
||||||
### Project-Based Testing
|
|
||||||
```typescript
|
|
||||||
// ✅ Good: Use existing composable
|
|
||||||
const { projectName } = await n8n.projectComposer.createProject();
|
|
||||||
await n8n.projectComposer.addCredentialToProject(
|
|
||||||
projectName,
|
|
||||||
'Notion API',
|
|
||||||
'apiKey', // Use field name, not display name
|
|
||||||
'test_value'
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Direct Navigation
|
|
||||||
```typescript
|
|
||||||
// ✅ Good: Direct navigation when UI interactions fail
|
|
||||||
await n8n.page.goto('/home/credentials/create');
|
|
||||||
await n8n.page.goto('/workflow/new');
|
|
||||||
```
|
|
||||||
|
|
||||||
### Error Testing with Route Interception
|
|
||||||
```typescript
|
|
||||||
// ✅ Good: Force errors for notification testing
|
|
||||||
await n8n.page.route('**/rest/credentials', route => {
|
|
||||||
route.abort();
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### Node Creation with NDV Handling
|
|
||||||
```typescript
|
|
||||||
// ✅ Good: Handle NDV auto-opening
|
|
||||||
await n8n.canvas.addNode(SCHEDULE_TRIGGER_NODE_NAME);
|
|
||||||
await n8n.ndv.close(); // Close NDV that opens automatically
|
|
||||||
await n8n.canvas.addNode(NOTION_NODE_NAME);
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🚨 Common Pitfalls
|
|
||||||
|
|
||||||
### 1. **Not Checking Existing Patterns**
|
|
||||||
```typescript
|
|
||||||
// ❌ Bad: Implementing without checking existing patterns
|
|
||||||
await n8n.page.getByText('Internal Integration Secret').fill('value');
|
|
||||||
|
|
||||||
// ✅ Good: Use existing composable with correct field name
|
|
||||||
await n8n.projectComposer.addCredentialToProject(
|
|
||||||
projectName, 'Notion API', 'apiKey', 'value'
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. **Ignoring Framework Differences**
|
|
||||||
```typescript
|
|
||||||
// ❌ Bad: Assuming Cypress patterns work in Playwright
|
|
||||||
await n8n.credentialsModal.connectionParameter('Internal Integration Secret').fill('value');
|
|
||||||
|
|
||||||
// ✅ Good: Use Playwright field names
|
|
||||||
await n8n.page.getByTestId('parameter-input-field').fill('value');
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. **Complex UI Interactions When Simple Navigation Works**
|
|
||||||
```typescript
|
|
||||||
// ❌ Bad: Complex button clicking when direct navigation works
|
|
||||||
await n8n.workflows.clickAddWorkflowButton();
|
|
||||||
await n8n.page.waitForLoadState();
|
|
||||||
|
|
||||||
// ✅ Good: Direct navigation
|
|
||||||
await n8n.page.goto('/workflow/new');
|
|
||||||
await n8n.page.waitForLoadState('networkidle');
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. **Not Handling UI Blocking**
|
|
||||||
```typescript
|
|
||||||
// ❌ Bad: Not handling NDV auto-opening
|
|
||||||
await n8n.canvas.addNode(SCHEDULE_TRIGGER_NODE_NAME);
|
|
||||||
await n8n.canvas.addNode(NOTION_NODE_NAME); // This will fail
|
|
||||||
|
|
||||||
// ✅ Good: Close NDV after first node
|
|
||||||
await n8n.canvas.addNode(SCHEDULE_TRIGGER_NODE_NAME);
|
|
||||||
await n8n.ndv.close();
|
|
||||||
await n8n.canvas.addNode(NOTION_NODE_NAME);
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📝 Testing Strategy
|
|
||||||
|
|
||||||
### 1. **Start Simple**
|
|
||||||
- Begin with basic navigation and page verification tests
|
|
||||||
- Use existing composables for common operations
|
|
||||||
- Verify each test works before moving to complex scenarios
|
|
||||||
|
|
||||||
### 2. **Incremental Implementation**
|
|
||||||
- Scaffold all tests first with placeholders
|
|
||||||
- Implement one test at a time
|
|
||||||
- Use `console.log` for placeholder tests to maintain passing test suite
|
|
||||||
|
|
||||||
### 3. **Debugging Approach**
|
|
||||||
```typescript
|
|
||||||
// Add pauses for debugging
|
|
||||||
await n8n.page.pause();
|
|
||||||
|
|
||||||
// Use headed mode for visual debugging
|
|
||||||
SHOW_BROWSER=true npx playwright test
|
|
||||||
|
|
||||||
// Use specific test selection
|
|
||||||
npx playwright test -g "test name" --reporter=list
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. **Verification Strategy**
|
|
||||||
- Run individual tests during development
|
|
||||||
- Run full test suite after each major change
|
|
||||||
- Use `--reporter=list` for clear output during development
|
|
||||||
|
|
||||||
## 🔧 Environment Configuration
|
|
||||||
|
|
||||||
### VS Code Settings
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"playwright.env": {
|
|
||||||
"N8N_BASE_URL": "http://localhost:5679",
|
|
||||||
"SHOW_BROWSER": "true",
|
|
||||||
"RESET_E2E_DB": "true"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Package.json Scripts
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"scripts": {
|
|
||||||
"start:isolated": "cd ..; N8N_PORT=5679 N8N_USER_FOLDER=/tmp/n8n-test-$(date +%s) E2E_TESTS=true pnpm start",
|
|
||||||
"test:local": "RESET_E2E_DB=true N8N_BASE_URL=http://localhost:5679 start-server-and-test 'pnpm start:isolated' http://localhost:5679/favicon.ico 'sleep 1 && pnpm test:standard --workers 4 --repeat-each 5'"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📊 Success Metrics
|
|
||||||
|
|
||||||
### Migration Complete When:
|
|
||||||
- [ ] All tests from Cypress file are scaffolded
|
|
||||||
- [ ] All tests pass consistently
|
|
||||||
- [ ] Tests use existing composables where appropriate
|
|
||||||
- [ ] Tests follow established patterns
|
|
||||||
- [ ] No `@db:reset` dependencies (unless absolutely necessary)
|
|
||||||
- [ ] Tests are idempotent and can run in any order
|
|
||||||
- [ ] Complex UI interactions are handled properly
|
|
||||||
|
|
||||||
### Quality Checklist:
|
|
||||||
- [ ] Tests use proper error handling
|
|
||||||
- [ ] Tests include appropriate assertions
|
|
||||||
- [ ] Tests follow naming conventions
|
|
||||||
- [ ] Tests include proper comments
|
|
||||||
- [ ] Tests use constants for repeated values
|
|
||||||
- [ ] Tests handle dynamic data properly
|
|
||||||
|
|
||||||
## 🎯 Best Practices Summary
|
|
||||||
|
|
||||||
1. **Search First**: Always look for existing patterns before implementing
|
|
||||||
2. **Use Composables**: Leverage existing business logic composables
|
|
||||||
3. **Direct Navigation**: Prefer direct page navigation over complex UI interactions
|
|
||||||
4. **Handle UI Blocking**: Close modals/NDV when adding multiple nodes
|
|
||||||
5. **Framework Awareness**: Understand differences between Cypress and Playwright
|
|
||||||
6. **Incremental Approach**: Implement one test at a time
|
|
||||||
7. **Idempotent Design**: Make tests work regardless of initial state
|
|
||||||
8. **Proper Debugging**: Use pauses and headed mode for troubleshooting
|
|
||||||
|
|
||||||
## 📚 Resources
|
|
||||||
|
|
||||||
- [Playwright Test Documentation](https://playwright.dev/docs/intro)
|
|
||||||
- [n8n Playwright Contributing Guide](./packages/testing/playwright/CONTRIBUTING.md)
|
|
||||||
- [Existing Test Examples](./packages/testing/playwright/tests/)
|
|
||||||
- [Composables Reference](./packages/testing/playwright/composables/)
|
|
||||||
- [Page Objects Reference](./packages/testing/playwright/pages/)
|
|
||||||
@@ -1,202 +0,0 @@
|
|||||||
import {
|
|
||||||
MANUAL_TRIGGER_NODE_NAME,
|
|
||||||
MANUAL_TRIGGER_NODE_DISPLAY_NAME,
|
|
||||||
CODE_NODE_NAME,
|
|
||||||
SCHEDULE_TRIGGER_NODE_NAME,
|
|
||||||
EDIT_FIELDS_SET_NODE_NAME,
|
|
||||||
HTTP_REQUEST_NODE_NAME,
|
|
||||||
} from './../constants';
|
|
||||||
import { getCanvasPane } from '../composables/workflow';
|
|
||||||
import { successToast } from '../pages/notifications';
|
|
||||||
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
|
||||||
|
|
||||||
const WorkflowPage = new WorkflowPageClass();
|
|
||||||
describe('Canvas Actions', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
WorkflowPage.actions.visit();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should add first step', () => {
|
|
||||||
WorkflowPage.getters.canvasPlusButton().should('be.visible');
|
|
||||||
WorkflowPage.actions.addInitialNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
|
||||||
WorkflowPage.getters.canvasNodes().should('have.length', 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should add a connected node using plus endpoint', () => {
|
|
||||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
|
||||||
WorkflowPage.getters.canvasNodePlusEndpointByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
|
||||||
WorkflowPage.getters.nodeCreatorSearchBar().should('be.visible');
|
|
||||||
WorkflowPage.getters.nodeCreatorSearchBar().type(CODE_NODE_NAME);
|
|
||||||
WorkflowPage.getters.nodeCreatorSearchBar().type('{enter}');
|
|
||||||
cy.get('body').type('{esc}');
|
|
||||||
WorkflowPage.getters.canvasNodes().should('have.length', 2);
|
|
||||||
WorkflowPage.getters.nodeConnections().should('have.length', 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should add a connected node dragging from node creator', () => {
|
|
||||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
|
||||||
WorkflowPage.getters.canvasNodePlusEndpointByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
|
||||||
WorkflowPage.getters.nodeCreatorSearchBar().should('be.visible');
|
|
||||||
WorkflowPage.getters.nodeCreatorSearchBar().type(CODE_NODE_NAME);
|
|
||||||
cy.drag(WorkflowPage.getters.nodeCreatorNodeItems().first(), [100, 100], {
|
|
||||||
realMouse: true,
|
|
||||||
abs: true,
|
|
||||||
});
|
|
||||||
cy.get('body').type('{esc}');
|
|
||||||
WorkflowPage.getters.canvasNodes().should('have.length', 2);
|
|
||||||
WorkflowPage.getters.nodeConnections().should('have.length', 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should open a category when trying to drag and drop it on the canvas', () => {
|
|
||||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
|
||||||
WorkflowPage.getters.canvasNodePlusEndpointByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
|
||||||
WorkflowPage.getters.nodeCreatorSearchBar().should('be.visible');
|
|
||||||
WorkflowPage.getters.nodeCreatorSearchBar().type(CODE_NODE_NAME);
|
|
||||||
cy.drag(WorkflowPage.getters.nodeCreatorActionItems().first(), [100, 100], {
|
|
||||||
realMouse: true,
|
|
||||||
abs: true,
|
|
||||||
});
|
|
||||||
WorkflowPage.getters.nodeCreatorCategoryItems().its('length').should('be.gt', 0);
|
|
||||||
WorkflowPage.getters.canvasNodes().should('have.length', 1);
|
|
||||||
WorkflowPage.getters.nodeConnections().should('have.length', 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should add disconnected node if nothing is selected', () => {
|
|
||||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
|
||||||
// Deselect nodes
|
|
||||||
getCanvasPane().click({ force: true });
|
|
||||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
|
||||||
WorkflowPage.getters.canvasNodes().should('have.length', 2);
|
|
||||||
WorkflowPage.getters.nodeConnections().should('have.length', 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should add node between two connected nodes', () => {
|
|
||||||
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
|
||||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
|
||||||
WorkflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME);
|
|
||||||
WorkflowPage.actions.zoomToFit();
|
|
||||||
WorkflowPage.getters.canvasNodes().should('have.length', 3);
|
|
||||||
WorkflowPage.getters.nodeConnections().should('have.length', 2);
|
|
||||||
WorkflowPage.actions.addNodeBetweenNodes(
|
|
||||||
CODE_NODE_NAME,
|
|
||||||
EDIT_FIELDS_SET_NODE_NAME,
|
|
||||||
HTTP_REQUEST_NODE_NAME,
|
|
||||||
);
|
|
||||||
WorkflowPage.getters.canvasNodes().should('have.length', 4);
|
|
||||||
WorkflowPage.getters.nodeConnections().should('have.length', 3);
|
|
||||||
|
|
||||||
WorkflowPage.getters.canvasNodeByName(EDIT_FIELDS_SET_NODE_NAME).then(($editFieldsNode) => {
|
|
||||||
const editFieldsNodeLeft = WorkflowPage.getters.getNodeLeftPosition($editFieldsNode);
|
|
||||||
|
|
||||||
WorkflowPage.getters.canvasNodeByName(HTTP_REQUEST_NODE_NAME).then(($httpNode) => {
|
|
||||||
const httpNodeLeft = WorkflowPage.getters.getNodeLeftPosition($httpNode);
|
|
||||||
expect(httpNodeLeft).to.be.lessThan(editFieldsNodeLeft);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should delete node by pressing keyboard backspace', () => {
|
|
||||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
|
||||||
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
|
||||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
|
||||||
WorkflowPage.getters.canvasNodeByName(CODE_NODE_NAME).click();
|
|
||||||
cy.get('body').type('{backspace}');
|
|
||||||
WorkflowPage.getters.nodeConnections().should('have.length', 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should delete connections by clicking on the delete button', () => {
|
|
||||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
|
||||||
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
|
||||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
|
||||||
WorkflowPage.getters.nodeConnections().first().realHover();
|
|
||||||
WorkflowPage.actions.deleteNodeBetweenNodes(MANUAL_TRIGGER_NODE_DISPLAY_NAME, CODE_NODE_NAME);
|
|
||||||
|
|
||||||
WorkflowPage.getters.nodeConnections().should('have.length', 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Node hover actions', () => {
|
|
||||||
it('should execute node', () => {
|
|
||||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
|
||||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
|
||||||
WorkflowPage.getters
|
|
||||||
.canvasNodes()
|
|
||||||
.last()
|
|
||||||
.findChildByTestId('execute-node-button')
|
|
||||||
.click({ force: true });
|
|
||||||
|
|
||||||
successToast().should('have.length', 1);
|
|
||||||
|
|
||||||
WorkflowPage.actions.executeNode(CODE_NODE_NAME);
|
|
||||||
|
|
||||||
successToast().should('have.length', 2);
|
|
||||||
successToast().should('contain.text', 'Node executed successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should disable and enable node', () => {
|
|
||||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
|
||||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
|
||||||
const disableButton = WorkflowPage.getters
|
|
||||||
.canvasNodes()
|
|
||||||
.last()
|
|
||||||
.findChildByTestId('disable-node-button');
|
|
||||||
disableButton.click({ force: true });
|
|
||||||
WorkflowPage.getters.disabledNodes().should('have.length', 1);
|
|
||||||
disableButton.click({ force: true });
|
|
||||||
WorkflowPage.getters.disabledNodes().should('have.length', 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should delete node', () => {
|
|
||||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
|
||||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
|
||||||
WorkflowPage.getters
|
|
||||||
.canvasNodes()
|
|
||||||
.last()
|
|
||||||
.find('[data-test-id="delete-node-button"]')
|
|
||||||
.click({ force: true });
|
|
||||||
WorkflowPage.getters.canvasNodes().should('have.length', 1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should copy selected nodes', () => {
|
|
||||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
|
||||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
|
||||||
WorkflowPage.actions.hitSelectAll();
|
|
||||||
|
|
||||||
WorkflowPage.actions.hitCopy();
|
|
||||||
successToast().should('contain', 'Copied to clipboard');
|
|
||||||
|
|
||||||
WorkflowPage.actions.copyNode(CODE_NODE_NAME);
|
|
||||||
successToast().should('contain', 'Copied to clipboard');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should select/deselect all nodes', () => {
|
|
||||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
|
||||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
|
||||||
WorkflowPage.actions.hitSelectAll();
|
|
||||||
WorkflowPage.getters.selectedNodes().should('have.length', 2);
|
|
||||||
WorkflowPage.actions.deselectAll();
|
|
||||||
WorkflowPage.getters.selectedNodes().should('have.length', 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should select nodes using arrow keys', () => {
|
|
||||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
|
||||||
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
|
||||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
|
||||||
cy.wait(500);
|
|
||||||
cy.get('body').type('{leftArrow}');
|
|
||||||
const selectedCanvasNodes = () => WorkflowPage.getters.canvasNodes().parent();
|
|
||||||
|
|
||||||
selectedCanvasNodes().first().should('have.class', 'selected');
|
|
||||||
cy.get('body').type('{rightArrow}');
|
|
||||||
selectedCanvasNodes().last().should('have.class', 'selected');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should select nodes using shift and arrow keys', () => {
|
|
||||||
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
|
||||||
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
|
||||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
|
||||||
cy.wait(500);
|
|
||||||
cy.get('body').type('{shift}', { release: false }).type('{leftArrow}');
|
|
||||||
WorkflowPage.getters.selectedNodes().should('have.length', 2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -12,4 +12,29 @@ export class CanvasComposer {
|
|||||||
await this.n8n.ndv.togglePinData();
|
await this.n8n.ndv.togglePinData();
|
||||||
await this.n8n.ndv.close();
|
await this.n8n.ndv.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a node and wait for success toast notification
|
||||||
|
* @param nodeName - The node to execute
|
||||||
|
*/
|
||||||
|
async executeNodeAndWaitForToast(nodeName: string): Promise<void> {
|
||||||
|
await this.n8n.canvas.executeNode(nodeName);
|
||||||
|
await this.n8n.notifications.waitForNotificationAndClose('Node executed successfully');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy selected nodes and verify success toast
|
||||||
|
*/
|
||||||
|
async copySelectedNodesWithToast(): Promise<void> {
|
||||||
|
await this.n8n.canvas.copyNodes();
|
||||||
|
await this.n8n.notifications.waitForNotificationAndClose('Copied to clipboard');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select all nodes and copy them
|
||||||
|
*/
|
||||||
|
async selectAllAndCopy(): Promise<void> {
|
||||||
|
await this.n8n.canvas.selectAll();
|
||||||
|
await this.copySelectedNodesWithToast();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export const CHAT_TRIGGER_NODE_DISPLAY_NAME = 'When chat message received';
|
|||||||
export const SCHEDULE_TRIGGER_NODE_NAME = 'Schedule Trigger';
|
export const SCHEDULE_TRIGGER_NODE_NAME = 'Schedule Trigger';
|
||||||
export const CODE_NODE_NAME = 'Code';
|
export const CODE_NODE_NAME = 'Code';
|
||||||
export const SET_NODE_NAME = 'Set';
|
export const SET_NODE_NAME = 'Set';
|
||||||
export const EDIT_FIELDS_SET_NODE_NAME = 'Edit Fields';
|
export const EDIT_FIELDS_SET_NODE_NAME = 'Edit Fields (Set)';
|
||||||
export const LOOP_OVER_ITEMS_NODE_NAME = 'Loop Over Items';
|
export const LOOP_OVER_ITEMS_NODE_NAME = 'Loop Over Items';
|
||||||
export const IF_NODE_NAME = 'If';
|
export const IF_NODE_NAME = 'If';
|
||||||
export const MERGE_NODE_NAME = 'Merge';
|
export const MERGE_NODE_NAME = 'Merge';
|
||||||
|
|||||||
@@ -9,14 +9,6 @@ export class CanvasPage extends BasePage {
|
|||||||
return this.page.getByRole('button', { name: 'Save' });
|
return this.page.getByRole('button', { name: 'Save' });
|
||||||
}
|
}
|
||||||
|
|
||||||
workflowSaveButton(): Locator {
|
|
||||||
return this.page.getByTestId('workflow-save-button');
|
|
||||||
}
|
|
||||||
|
|
||||||
canvasAddButton(): Locator {
|
|
||||||
return this.page.getByTestId('canvas-add-button');
|
|
||||||
}
|
|
||||||
|
|
||||||
nodeCreatorItemByName(text: string): Locator {
|
nodeCreatorItemByName(text: string): Locator {
|
||||||
return this.page.getByTestId('node-creator-item-name').getByText(text, { exact: true });
|
return this.page.getByTestId('node-creator-item-name').getByText(text, { exact: true });
|
||||||
}
|
}
|
||||||
@@ -50,11 +42,11 @@ export class CanvasPage extends BasePage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async clickSaveWorkflowButton(): Promise<void> {
|
async clickSaveWorkflowButton(): Promise<void> {
|
||||||
await this.clickButtonByName('Save');
|
await this.saveWorkflowButton().click();
|
||||||
}
|
}
|
||||||
|
|
||||||
async fillNodeCreatorSearchBar(text: string): Promise<void> {
|
async fillNodeCreatorSearchBar(text: string): Promise<void> {
|
||||||
await this.fillByTestId('node-creator-search-bar', text);
|
await this.nodeCreatorSearchBar().fill(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
async clickNodeCreatorItemName(text: string): Promise<void> {
|
async clickNodeCreatorItemName(text: string): Promise<void> {
|
||||||
@@ -69,14 +61,14 @@ export class CanvasPage extends BasePage {
|
|||||||
|
|
||||||
async addNodeAndCloseNDV(text: string, subItemText?: string): Promise<void> {
|
async addNodeAndCloseNDV(text: string, subItemText?: string): Promise<void> {
|
||||||
if (subItemText) {
|
if (subItemText) {
|
||||||
await this.addNodeToCanvasWithSubItem(text, subItemText);
|
await this.addNodeWithSubItem(text, subItemText);
|
||||||
} else {
|
} else {
|
||||||
await this.addNode(text);
|
await this.addNode(text);
|
||||||
}
|
}
|
||||||
await this.page.keyboard.press('Escape');
|
await this.page.keyboard.press('Escape');
|
||||||
}
|
}
|
||||||
|
|
||||||
async addNodeToCanvasWithSubItem(searchText: string, subItemText: string): Promise<void> {
|
async addNodeWithSubItem(searchText: string, subItemText: string): Promise<void> {
|
||||||
await this.addNode(searchText);
|
await this.addNode(searchText);
|
||||||
await this.nodeCreatorSubItem(subItemText).click();
|
await this.nodeCreatorSubItem(subItemText).click();
|
||||||
}
|
}
|
||||||
@@ -97,12 +89,12 @@ export class CanvasPage extends BasePage {
|
|||||||
await this.page.getByRole('button', { name: 'Debug in editor' }).click();
|
await this.page.getByRole('button', { name: 'Debug in editor' }).click();
|
||||||
}
|
}
|
||||||
|
|
||||||
async pinNodeByNameUsingContextMenu(nodeName: string): Promise<void> {
|
async pinNode(nodeName: string): Promise<void> {
|
||||||
await this.nodeByName(nodeName).click({ button: 'right' });
|
await this.nodeByName(nodeName).click({ button: 'right' });
|
||||||
await this.page.getByTestId('context-menu').getByText('Pin').click();
|
await this.page.getByTestId('context-menu').getByText('Pin').click();
|
||||||
}
|
}
|
||||||
|
|
||||||
async unpinNodeByNameUsingContextMenu(nodeName: string): Promise<void> {
|
async unpinNode(nodeName: string): Promise<void> {
|
||||||
await this.nodeByName(nodeName).click({ button: 'right' });
|
await this.nodeByName(nodeName).click({ button: 'right' });
|
||||||
await this.page.getByText('Unpin').click();
|
await this.page.getByText('Unpin').click();
|
||||||
}
|
}
|
||||||
@@ -215,6 +207,10 @@ export class CanvasPage extends BasePage {
|
|||||||
return tags;
|
return tags;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getWorkflowSaveButton(): Locator {
|
||||||
|
return this.page.getByTestId('workflow-save-button');
|
||||||
|
}
|
||||||
|
|
||||||
// Production Checklist methods
|
// Production Checklist methods
|
||||||
getProductionChecklistButton(): Locator {
|
getProductionChecklistButton(): Locator {
|
||||||
return this.page.getByTestId('suggested-action-count');
|
return this.page.getByTestId('suggested-action-count');
|
||||||
@@ -260,7 +256,159 @@ export class CanvasPage extends BasePage {
|
|||||||
await this.getProductionChecklistActionItem(actionText).click();
|
await this.getProductionChecklistActionItem(actionText).click();
|
||||||
}
|
}
|
||||||
|
|
||||||
getCanvasNodes() {
|
getCanvasNodes(): Locator {
|
||||||
return this.page.getByTestId('canvas-node');
|
return this.page.getByTestId('canvas-node');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nodeConnections(): Locator {
|
||||||
|
return this.page.locator('[data-test-id="edge"]');
|
||||||
|
}
|
||||||
|
|
||||||
|
canvasNodePlusEndpointByName(nodeName: string): Locator {
|
||||||
|
return this.page
|
||||||
|
.locator(
|
||||||
|
`[data-test-id="canvas-node-output-handle"][data-node-name="${nodeName}"] [data-test-id="canvas-handle-plus"]`,
|
||||||
|
)
|
||||||
|
.first();
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeCreatorSearchBar(): Locator {
|
||||||
|
return this.page.getByTestId('node-creator-search-bar');
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeCreatorNodeItems(): Locator {
|
||||||
|
return this.page.getByTestId('node-creator-node-item');
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeCreatorActionItems(): Locator {
|
||||||
|
return this.page.getByTestId('node-creator-action-item');
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeCreatorCategoryItems(): Locator {
|
||||||
|
return this.page.getByTestId('node-creator-category-item');
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedNodes(): Locator {
|
||||||
|
return this.page
|
||||||
|
.locator('[data-test-id="canvas-node"]')
|
||||||
|
.locator('xpath=..')
|
||||||
|
.locator('.selected');
|
||||||
|
}
|
||||||
|
|
||||||
|
disabledNodes(): Locator {
|
||||||
|
return this.page.locator('[data-canvas-node-render-type][class*="disabled"]');
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeExecuteButton(nodeName: string): Locator {
|
||||||
|
return this.nodeToolbar(nodeName).getByTestId('execute-node-button');
|
||||||
|
}
|
||||||
|
|
||||||
|
canvasPane(): Locator {
|
||||||
|
return this.page.getByTestId('canvas-wrapper');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
|
||||||
|
async addInitialNodeToCanvas(nodeName: string): Promise<void> {
|
||||||
|
await this.clickCanvasPlusButton();
|
||||||
|
await this.fillNodeCreatorSearchBar(nodeName);
|
||||||
|
await this.clickNodeCreatorItemName(nodeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
async clickNodePlusEndpoint(nodeName: string): Promise<void> {
|
||||||
|
await this.canvasNodePlusEndpointByName(nodeName).click();
|
||||||
|
}
|
||||||
|
|
||||||
|
async executeNode(nodeName: string): Promise<void> {
|
||||||
|
await this.nodeByName(nodeName).hover();
|
||||||
|
await this.nodeExecuteButton(nodeName).click();
|
||||||
|
}
|
||||||
|
|
||||||
|
async selectAll(): Promise<void> {
|
||||||
|
await this.page.keyboard.press('ControlOrMeta+a');
|
||||||
|
}
|
||||||
|
|
||||||
|
async copyNodes(): Promise<void> {
|
||||||
|
await this.page.keyboard.press('ControlOrMeta+c');
|
||||||
|
}
|
||||||
|
|
||||||
|
async deselectAll(): Promise<void> {
|
||||||
|
await this.canvasPane().click({ position: { x: 10, y: 10 } });
|
||||||
|
}
|
||||||
|
|
||||||
|
getNodeLeftPosition(nodeLocator: Locator): Promise<number> {
|
||||||
|
return nodeLocator.evaluate((el) => el.getBoundingClientRect().left);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connection helpers
|
||||||
|
connectionBetweenNodes(sourceNodeName: string, targetNodeName: string): Locator {
|
||||||
|
return this.page.locator(
|
||||||
|
`[data-test-id="edge"][data-source-node-name="${sourceNodeName}"][data-target-node-name="${targetNodeName}"]`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
connectionToolbarBetweenNodes(sourceNodeName: string, targetNodeName: string): Locator {
|
||||||
|
return this.page.locator(
|
||||||
|
`[data-test-id="edge-label"][data-source-node-name="${sourceNodeName}"][data-target-node-name="${targetNodeName}"] [data-test-id="canvas-edge-toolbar"]`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Canvas action helpers
|
||||||
|
async addNodeBetweenNodes(
|
||||||
|
sourceNodeName: string,
|
||||||
|
targetNodeName: string,
|
||||||
|
newNodeName: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const specificConnection = this.connectionBetweenNodes(sourceNodeName, targetNodeName);
|
||||||
|
// eslint-disable-next-line playwright/no-force-option
|
||||||
|
await specificConnection.hover({ force: true });
|
||||||
|
|
||||||
|
const addNodeButton = this.connectionToolbarBetweenNodes(
|
||||||
|
sourceNodeName,
|
||||||
|
targetNodeName,
|
||||||
|
).getByTestId('add-connection-button');
|
||||||
|
|
||||||
|
await addNodeButton.click();
|
||||||
|
await this.fillNodeCreatorSearchBar(newNodeName);
|
||||||
|
await this.clickNodeCreatorItemName(newNodeName);
|
||||||
|
await this.page.keyboard.press('Escape');
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteConnectionBetweenNodes(
|
||||||
|
sourceNodeName: string,
|
||||||
|
targetNodeName: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const specificConnection = this.connectionBetweenNodes(sourceNodeName, targetNodeName);
|
||||||
|
// eslint-disable-next-line playwright/no-force-option
|
||||||
|
await specificConnection.hover({ force: true });
|
||||||
|
|
||||||
|
const deleteButton = this.connectionToolbarBetweenNodes(
|
||||||
|
sourceNodeName,
|
||||||
|
targetNodeName,
|
||||||
|
).getByTestId('delete-connection-button');
|
||||||
|
|
||||||
|
await deleteButton.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
async navigateNodesWithArrows(direction: 'left' | 'right' | 'up' | 'down'): Promise<void> {
|
||||||
|
const keyMap = {
|
||||||
|
left: 'ArrowLeft',
|
||||||
|
right: 'ArrowRight',
|
||||||
|
up: 'ArrowUp',
|
||||||
|
down: 'ArrowDown',
|
||||||
|
};
|
||||||
|
await this.canvasPane().focus();
|
||||||
|
await this.page.keyboard.press(keyMap[direction]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async extendSelectionWithArrows(direction: 'left' | 'right' | 'up' | 'down'): Promise<void> {
|
||||||
|
const keyMap = {
|
||||||
|
left: 'Shift+ArrowLeft',
|
||||||
|
right: 'Shift+ArrowRight',
|
||||||
|
up: 'Shift+ArrowUp',
|
||||||
|
down: 'Shift+ArrowDown',
|
||||||
|
};
|
||||||
|
await this.canvasPane().focus();
|
||||||
|
await this.page.keyboard.press(keyMap[direction]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
186
packages/testing/playwright/tests/ui/12-canvas-actions.spec.ts
Normal file
186
packages/testing/playwright/tests/ui/12-canvas-actions.spec.ts
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
import {
|
||||||
|
MANUAL_TRIGGER_NODE_NAME,
|
||||||
|
MANUAL_TRIGGER_NODE_DISPLAY_NAME,
|
||||||
|
CODE_NODE_NAME,
|
||||||
|
HTTP_REQUEST_NODE_NAME,
|
||||||
|
} from '../../config/constants';
|
||||||
|
import { test, expect } from '../../fixtures/base';
|
||||||
|
|
||||||
|
test.describe('Canvas Actions', () => {
|
||||||
|
test.beforeEach(async ({ n8n }) => {
|
||||||
|
await n8n.goHome();
|
||||||
|
await n8n.workflows.clickAddWorkflowButton();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should add first step', async ({ n8n }) => {
|
||||||
|
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
|
||||||
|
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should add a connected node using plus endpoint', async ({ n8n }) => {
|
||||||
|
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
|
||||||
|
await n8n.canvas.clickNodePlusEndpoint(MANUAL_TRIGGER_NODE_DISPLAY_NAME);
|
||||||
|
await n8n.canvas.fillNodeCreatorSearchBar(CODE_NODE_NAME);
|
||||||
|
await n8n.page.keyboard.press('Enter');
|
||||||
|
await n8n.page.keyboard.press('Escape');
|
||||||
|
|
||||||
|
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(2);
|
||||||
|
await expect(n8n.canvas.nodeConnections()).toHaveCount(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should add a connected node dragging from node creator', async ({ n8n }) => {
|
||||||
|
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
|
||||||
|
await n8n.canvas.clickNodePlusEndpoint(MANUAL_TRIGGER_NODE_DISPLAY_NAME);
|
||||||
|
await n8n.canvas.fillNodeCreatorSearchBar(CODE_NODE_NAME);
|
||||||
|
|
||||||
|
const sourceElement = n8n.canvas
|
||||||
|
.nodeCreatorNodeItems()
|
||||||
|
.filter({ hasText: CODE_NODE_NAME })
|
||||||
|
.first();
|
||||||
|
await sourceElement.dragTo(n8n.canvas.canvasPane(), { targetPosition: { x: 100, y: 100 } });
|
||||||
|
|
||||||
|
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(2);
|
||||||
|
await expect(n8n.canvas.nodeConnections()).toHaveCount(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should open a category when trying to drag and drop it on the canvas', async ({ n8n }) => {
|
||||||
|
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
|
||||||
|
await n8n.canvas.clickNodePlusEndpoint(MANUAL_TRIGGER_NODE_DISPLAY_NAME);
|
||||||
|
await n8n.canvas.fillNodeCreatorSearchBar(CODE_NODE_NAME);
|
||||||
|
|
||||||
|
const categoryItem = n8n.canvas.nodeCreatorActionItems().first();
|
||||||
|
await categoryItem.dragTo(n8n.canvas.canvasPane(), {
|
||||||
|
targetPosition: { x: 100, y: 100 },
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(n8n.canvas.nodeCreatorCategoryItems()).toHaveCount(1);
|
||||||
|
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(1);
|
||||||
|
await expect(n8n.canvas.nodeConnections()).toHaveCount(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should add disconnected node if nothing is selected', async ({ n8n }) => {
|
||||||
|
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
|
||||||
|
await n8n.canvas.deselectAll();
|
||||||
|
await n8n.canvas.addNodeAndCloseNDV(CODE_NODE_NAME);
|
||||||
|
|
||||||
|
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(2);
|
||||||
|
await expect(n8n.canvas.nodeConnections()).toHaveCount(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should add node between two connected nodes', async ({ n8n }) => {
|
||||||
|
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
|
||||||
|
await n8n.canvas.nodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||||
|
await n8n.canvas.addNodeAndCloseNDV(CODE_NODE_NAME);
|
||||||
|
|
||||||
|
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(2);
|
||||||
|
await expect(n8n.canvas.nodeConnections()).toHaveCount(1);
|
||||||
|
|
||||||
|
await n8n.canvas.addNodeBetweenNodes(
|
||||||
|
MANUAL_TRIGGER_NODE_DISPLAY_NAME,
|
||||||
|
CODE_NODE_NAME,
|
||||||
|
HTTP_REQUEST_NODE_NAME,
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(3);
|
||||||
|
await expect(n8n.canvas.nodeConnections()).toHaveCount(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should delete node by pressing keyboard backspace', async ({ n8n }) => {
|
||||||
|
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
|
||||||
|
await n8n.canvas.nodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||||
|
await n8n.page.keyboard.press('Backspace');
|
||||||
|
|
||||||
|
await expect(n8n.canvas.nodeConnections()).toHaveCount(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should delete connections by clicking on the delete button', async ({ n8n }) => {
|
||||||
|
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
|
||||||
|
await n8n.canvas.nodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||||
|
await n8n.canvas.addNodeAndCloseNDV(CODE_NODE_NAME);
|
||||||
|
await n8n.canvas.deleteConnectionBetweenNodes(MANUAL_TRIGGER_NODE_DISPLAY_NAME, CODE_NODE_NAME);
|
||||||
|
|
||||||
|
await expect(n8n.canvas.nodeConnections()).toHaveCount(0);
|
||||||
|
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Node hover actions', () => {
|
||||||
|
test('should execute node', async ({ n8n }) => {
|
||||||
|
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
|
||||||
|
await n8n.canvas.executeNode(MANUAL_TRIGGER_NODE_DISPLAY_NAME);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
n8n.notifications.notificationContainerByText('Node executed successfully'),
|
||||||
|
).toHaveCount(1);
|
||||||
|
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should disable and enable node', async ({ n8n }) => {
|
||||||
|
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
|
||||||
|
await n8n.canvas.addNodeAndCloseNDV(CODE_NODE_NAME);
|
||||||
|
|
||||||
|
const disableButton = n8n.canvas.nodeDisableButton(CODE_NODE_NAME);
|
||||||
|
await disableButton.click();
|
||||||
|
|
||||||
|
await expect(n8n.canvas.disabledNodes()).toHaveCount(1);
|
||||||
|
|
||||||
|
await disableButton.click();
|
||||||
|
|
||||||
|
await expect(n8n.canvas.disabledNodes()).toHaveCount(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should delete node', async ({ n8n }) => {
|
||||||
|
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
|
||||||
|
await n8n.canvas.addNodeAndCloseNDV(CODE_NODE_NAME);
|
||||||
|
await n8n.canvas.deleteNodeByName(CODE_NODE_NAME);
|
||||||
|
|
||||||
|
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(1);
|
||||||
|
await expect(n8n.canvas.nodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME)).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should copy selected nodes', async ({ n8n }) => {
|
||||||
|
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
|
||||||
|
await n8n.canvas.addNodeAndCloseNDV(CODE_NODE_NAME);
|
||||||
|
await n8n.canvasComposer.selectAllAndCopy();
|
||||||
|
await n8n.canvas.nodeByName(CODE_NODE_NAME).click();
|
||||||
|
await n8n.canvasComposer.copySelectedNodesWithToast();
|
||||||
|
|
||||||
|
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should select/deselect all nodes', async ({ n8n }) => {
|
||||||
|
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
|
||||||
|
await n8n.canvas.addNodeAndCloseNDV(CODE_NODE_NAME);
|
||||||
|
await n8n.canvas.selectAll();
|
||||||
|
|
||||||
|
await expect(n8n.canvas.selectedNodes()).toHaveCount(2);
|
||||||
|
|
||||||
|
await n8n.canvas.deselectAll();
|
||||||
|
await expect(n8n.canvas.selectedNodes()).toHaveCount(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should select nodes using arrow keys', async ({ n8n }) => {
|
||||||
|
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
|
||||||
|
await n8n.canvas.nodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||||
|
await n8n.canvas.addNodeAndCloseNDV(CODE_NODE_NAME);
|
||||||
|
await n8n.canvas.getCanvasNodes().first().waitFor();
|
||||||
|
await n8n.canvas.navigateNodesWithArrows('left');
|
||||||
|
|
||||||
|
const selectedNodes = n8n.canvas.selectedNodes();
|
||||||
|
await expect(selectedNodes.first()).toHaveClass(/selected/);
|
||||||
|
|
||||||
|
await n8n.canvas.navigateNodesWithArrows('right');
|
||||||
|
|
||||||
|
await expect(selectedNodes.last()).toHaveClass(/selected/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should select nodes using shift and arrow keys', async ({ n8n }) => {
|
||||||
|
await n8n.canvas.addNode(MANUAL_TRIGGER_NODE_NAME);
|
||||||
|
await n8n.canvas.nodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
|
||||||
|
await n8n.canvas.addNodeAndCloseNDV(CODE_NODE_NAME);
|
||||||
|
await n8n.canvas.getCanvasNodes().first().waitFor();
|
||||||
|
await n8n.canvas.extendSelectionWithArrows('left');
|
||||||
|
|
||||||
|
await expect(n8n.canvas.selectedNodes()).toHaveCount(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -15,6 +15,6 @@ test.describe('ADO-2270 Save button resets on webhook node open', () => {
|
|||||||
|
|
||||||
await n8n.ndv.clickBackToCanvasButton();
|
await n8n.ndv.clickBackToCanvasButton();
|
||||||
|
|
||||||
await expect(n8n.canvas.workflowSaveButton()).toContainText('Saved');
|
await expect(n8n.canvas.getWorkflowSaveButton()).toContainText('Saved');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -63,10 +63,7 @@ test.describe('Projects @db:reset', () => {
|
|||||||
n8n.page.getByText('Workflow successfully created', { exact: false }),
|
n8n.page.getByText('Workflow successfully created', { exact: false }),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
await n8n.canvas.addNodeToCanvasWithSubItem(
|
await n8n.canvas.addNodeWithSubItem(EXECUTE_WORKFLOW_NODE_NAME, 'Execute A Sub Workflow');
|
||||||
EXECUTE_WORKFLOW_NODE_NAME,
|
|
||||||
'Execute A Sub Workflow',
|
|
||||||
);
|
|
||||||
|
|
||||||
const subWorkflowPagePromise = n8n.page.waitForEvent('popup');
|
const subWorkflowPagePromise = n8n.page.waitForEvent('popup');
|
||||||
|
|
||||||
@@ -77,7 +74,7 @@ test.describe('Projects @db:reset', () => {
|
|||||||
await subn8n.ndv.clickBackToCanvasButton();
|
await subn8n.ndv.clickBackToCanvasButton();
|
||||||
|
|
||||||
await subn8n.canvas.deleteNodeByName('Replace me with your logic');
|
await subn8n.canvas.deleteNodeByName('Replace me with your logic');
|
||||||
await subn8n.canvas.addNodeToCanvasWithSubItem(NOTION_NODE_NAME, 'Append a block');
|
await subn8n.canvas.addNodeWithSubItem(NOTION_NODE_NAME, 'Append a block');
|
||||||
|
|
||||||
await subn8n.credentials.createAndSaveNewCredential('apiKey', NOTION_API_KEY);
|
await subn8n.credentials.createAndSaveNewCredential('apiKey', NOTION_API_KEY);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user