mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +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.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 CODE_NODE_NAME = 'Code';
|
||||
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 IF_NODE_NAME = 'If';
|
||||
export const MERGE_NODE_NAME = 'Merge';
|
||||
|
||||
@@ -9,14 +9,6 @@ export class CanvasPage extends BasePage {
|
||||
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 {
|
||||
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> {
|
||||
await this.clickButtonByName('Save');
|
||||
await this.saveWorkflowButton().click();
|
||||
}
|
||||
|
||||
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> {
|
||||
@@ -69,14 +61,14 @@ export class CanvasPage extends BasePage {
|
||||
|
||||
async addNodeAndCloseNDV(text: string, subItemText?: string): Promise<void> {
|
||||
if (subItemText) {
|
||||
await this.addNodeToCanvasWithSubItem(text, subItemText);
|
||||
await this.addNodeWithSubItem(text, subItemText);
|
||||
} else {
|
||||
await this.addNode(text);
|
||||
}
|
||||
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.nodeCreatorSubItem(subItemText).click();
|
||||
}
|
||||
@@ -97,12 +89,12 @@ export class CanvasPage extends BasePage {
|
||||
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.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.page.getByText('Unpin').click();
|
||||
}
|
||||
@@ -215,6 +207,10 @@ export class CanvasPage extends BasePage {
|
||||
return tags;
|
||||
}
|
||||
|
||||
getWorkflowSaveButton(): Locator {
|
||||
return this.page.getByTestId('workflow-save-button');
|
||||
}
|
||||
|
||||
// Production Checklist methods
|
||||
getProductionChecklistButton(): Locator {
|
||||
return this.page.getByTestId('suggested-action-count');
|
||||
@@ -260,7 +256,159 @@ export class CanvasPage extends BasePage {
|
||||
await this.getProductionChecklistActionItem(actionText).click();
|
||||
}
|
||||
|
||||
getCanvasNodes() {
|
||||
getCanvasNodes(): Locator {
|
||||
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 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 }),
|
||||
).toBeVisible();
|
||||
|
||||
await n8n.canvas.addNodeToCanvasWithSubItem(
|
||||
EXECUTE_WORKFLOW_NODE_NAME,
|
||||
'Execute A Sub Workflow',
|
||||
);
|
||||
await n8n.canvas.addNodeWithSubItem(EXECUTE_WORKFLOW_NODE_NAME, 'Execute A Sub Workflow');
|
||||
|
||||
const subWorkflowPagePromise = n8n.page.waitForEvent('popup');
|
||||
|
||||
@@ -77,7 +74,7 @@ test.describe('Projects @db:reset', () => {
|
||||
await subn8n.ndv.clickBackToCanvasButton();
|
||||
|
||||
await subn8n.canvas.deleteNodeByName('Replace me with your logic');
|
||||
await subn8n.canvas.addNodeToCanvasWithSubItem(NOTION_NODE_NAME, 'Append a block');
|
||||
await subn8n.canvas.addNodeWithSubItem(NOTION_NODE_NAME, 'Append a block');
|
||||
|
||||
await subn8n.credentials.createAndSaveNewCredential('apiKey', NOTION_API_KEY);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user