first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
+28
@@ -0,0 +1,28 @@
|
||||
import { test, expect } from '../../../fixtures/base';
|
||||
import type { TestRequirements } from '../../../Types';
|
||||
|
||||
const requirements: TestRequirements = {
|
||||
workflow: {
|
||||
'Test_ado_1338.json': 'Test Workflow ADO-1338',
|
||||
},
|
||||
};
|
||||
|
||||
test.describe('ADO-1338-ndv-missing-input-panel', {
|
||||
annotation: [
|
||||
{ type: 'owner', description: 'Adore' },
|
||||
],
|
||||
}, () => {
|
||||
test('should show the input and output panels when node is missing input and output data', async ({
|
||||
n8n,
|
||||
setupRequirements,
|
||||
}) => {
|
||||
await setupRequirements(requirements);
|
||||
await n8n.workflowComposer.executeWorkflowAndWaitForNotification(
|
||||
'Workflow successfully executed',
|
||||
);
|
||||
|
||||
await n8n.canvas.openNode('Discourse1');
|
||||
await expect(n8n.ndv.inputPanel.get()).toBeVisible();
|
||||
await expect(n8n.ndv.outputPanel.get()).toBeVisible();
|
||||
});
|
||||
});
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import { test, expect } from '../../../fixtures/base';
|
||||
|
||||
test.describe('ADO-2230 NDV Pagination Reset', {
|
||||
annotation: [
|
||||
{ type: 'owner', description: 'Adore' },
|
||||
],
|
||||
}, () => {
|
||||
test('should reset pagination if data size changes to less than current page', async ({
|
||||
n8n,
|
||||
}) => {
|
||||
await n8n.start.fromImportedWorkflow('NDV-debug-generate-data.json');
|
||||
|
||||
await n8n.canvas.openNode('DebugHelper');
|
||||
await n8n.ndv.execute();
|
||||
await n8n.notifications.quickCloseAll();
|
||||
|
||||
const outputPagination = n8n.ndv.getOutputPagination();
|
||||
await expect(outputPagination).toBeVisible();
|
||||
|
||||
await expect(n8n.ndv.getOutputPaginationPages()).toHaveCount(5);
|
||||
|
||||
await expect(n8n.ndv.outputPanel.getTbodyCell(0, 0)).not.toBeEmpty();
|
||||
const firstPageContent = await n8n.ndv.outputPanel.getTbodyCell(0, 0).textContent();
|
||||
|
||||
await n8n.ndv.navigateToOutputPage(4);
|
||||
|
||||
await expect(n8n.ndv.outputPanel.getTbodyCell(0, 0)).not.toHaveText(firstPageContent ?? '');
|
||||
|
||||
await n8n.ndv.setParameterInputValue('randomDataCount', '50');
|
||||
|
||||
await n8n.ndv.execute();
|
||||
await n8n.notifications.quickCloseAll();
|
||||
|
||||
await expect(n8n.ndv.getOutputPaginationPages()).toHaveCount(2);
|
||||
|
||||
await expect(n8n.ndv.outputPanel.getTbodyCell(0, 0)).not.toBeEmpty();
|
||||
});
|
||||
});
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
import { test, expect } from '../../../fixtures/base';
|
||||
|
||||
test.describe('ADO-2362 ADO-2350 NDV Prevent clipping long parameters and scrolling to expression', {
|
||||
annotation: [
|
||||
{ type: 'owner', description: 'Adore' },
|
||||
],
|
||||
}, () => {
|
||||
test('should show last parameters and open at scroll top of parameters', async ({ n8n }) => {
|
||||
await n8n.start.fromImportedWorkflow('Test-workflow-with-long-parameters.json');
|
||||
|
||||
await n8n.canvas.openNode('Schedule Trigger');
|
||||
|
||||
await expect(n8n.ndv.getInlineExpressionEditorInput().first()).toBeVisible();
|
||||
|
||||
await n8n.ndv.close();
|
||||
|
||||
await n8n.canvas.openNode('Edit Fields1');
|
||||
|
||||
await expect(n8n.ndv.getInputLabel().nth(0)).toContainText('Mode');
|
||||
await expect(n8n.ndv.getInputLabel().nth(0)).toBeVisible();
|
||||
|
||||
await expect(n8n.ndv.getInlineExpressionEditorInput()).toHaveCount(2);
|
||||
|
||||
await expect(n8n.ndv.getInlineExpressionEditorInput().nth(0)).toHaveText('should be visible!');
|
||||
await expect(n8n.ndv.getInlineExpressionEditorInput().nth(0)).toBeVisible();
|
||||
|
||||
await expect(n8n.ndv.getInlineExpressionEditorInput().nth(1)).toHaveText('not visible');
|
||||
await expect(n8n.ndv.getInlineExpressionEditorInput().nth(1)).toBeVisible();
|
||||
|
||||
await n8n.ndv.close();
|
||||
await n8n.canvas.openNode('Schedule Trigger');
|
||||
|
||||
await expect(n8n.ndv.getNthParameter(0)).toContainText(
|
||||
'This workflow will run on the schedule ',
|
||||
);
|
||||
await expect(n8n.ndv.getInputLabel().nth(0)).toBeVisible();
|
||||
|
||||
await expect(n8n.ndv.getInlineExpressionEditorInput()).toHaveCount(2);
|
||||
|
||||
await expect(n8n.ndv.getInlineExpressionEditorInput().nth(0)).toHaveText('should be visible');
|
||||
await expect(n8n.ndv.getInlineExpressionEditorInput().nth(0)).toBeVisible();
|
||||
|
||||
await expect(n8n.ndv.getInlineExpressionEditorInput().nth(1)).toHaveText('not visible');
|
||||
await expect(n8n.ndv.getInlineExpressionEditorInput().nth(1)).not.toBeInViewport();
|
||||
|
||||
await n8n.ndv.close();
|
||||
await n8n.canvas.openNode('Slack');
|
||||
|
||||
await expect(n8n.ndv.getCredentialsLabel()).toBeVisible();
|
||||
|
||||
await expect(n8n.ndv.getInlineExpressionEditorInput().nth(0)).toHaveText('should be visible');
|
||||
await expect(n8n.ndv.getInlineExpressionEditorInput().nth(0)).toBeVisible();
|
||||
|
||||
await expect(n8n.ndv.getInlineExpressionEditorInput().nth(1)).toHaveText('not visible');
|
||||
await expect(n8n.ndv.getInlineExpressionEditorInput().nth(1)).not.toBeInViewport();
|
||||
});
|
||||
|
||||
test('NODE-1272 ensure expressions scrolled to top, not middle', async ({ n8n }) => {
|
||||
await n8n.start.fromImportedWorkflow('Test-workflow-with-long-parameters.json');
|
||||
|
||||
await n8n.canvas.openNode('With long expression');
|
||||
|
||||
await expect(n8n.ndv.getInlineExpressionEditorInput().nth(0)).toBeVisible();
|
||||
|
||||
const editor = n8n.ndv.getInlineExpressionEditorInput().nth(0);
|
||||
await expect(editor.locator('.cm-line').nth(0)).toHaveText('1 visible!');
|
||||
await expect(editor.locator('.cm-line').nth(0)).toBeVisible();
|
||||
await expect(editor.locator('.cm-line').nth(6)).toHaveText('7 not visible!');
|
||||
await expect(editor.locator('.cm-line').nth(6)).not.toBeInViewport();
|
||||
});
|
||||
});
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import { test, expect } from '../../../fixtures/base';
|
||||
import type { TestRequirements } from '../../../Types';
|
||||
|
||||
const requirements: TestRequirements = {
|
||||
workflow: {
|
||||
'Switch_node_with_null_connection.json': 'Switch Node with Null Connection',
|
||||
},
|
||||
};
|
||||
|
||||
test.describe('ADO-2929 can load Switch nodes', {
|
||||
annotation: [
|
||||
{ type: 'owner', description: 'Adore' },
|
||||
],
|
||||
}, () => {
|
||||
test('can load workflows with Switch nodes with null at connection index @auth:owner', async ({
|
||||
n8n,
|
||||
setupRequirements,
|
||||
}) => {
|
||||
await setupRequirements(requirements);
|
||||
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(3);
|
||||
await n8n.canvas.deleteNodeByName('Switch');
|
||||
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(2);
|
||||
});
|
||||
});
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
import { readFileSync } from 'fs';
|
||||
|
||||
import { test, expect } from '../../../fixtures/base';
|
||||
import type { TestRequirements } from '../../../Types';
|
||||
import { resolveFromRoot } from '../../../utils/path-helper';
|
||||
|
||||
test.use({ capability: { env: { TEST_ISOLATION: 'template-setup-experiment' } } });
|
||||
|
||||
const TEMPLATE_HOSTNAME = 'custom.template.host';
|
||||
const TEMPLATE_HOST = `https://${TEMPLATE_HOSTNAME}/api`;
|
||||
const TEMPLATE_ID = 1205;
|
||||
|
||||
const testTemplate = JSON.parse(
|
||||
readFileSync(resolveFromRoot('workflows', 'Test_Template_1.json'), 'utf8'),
|
||||
);
|
||||
|
||||
function createTemplateRequirements(): TestRequirements {
|
||||
return {
|
||||
storage: {
|
||||
N8N_EXPERIMENT_OVERRIDES: JSON.stringify({
|
||||
'055_template_setup_experience': 'variant',
|
||||
'069_setup_panel': 'control',
|
||||
}),
|
||||
},
|
||||
config: {
|
||||
settings: {
|
||||
templates: {
|
||||
enabled: true,
|
||||
host: TEMPLATE_HOST,
|
||||
},
|
||||
},
|
||||
},
|
||||
intercepts: {
|
||||
health: {
|
||||
url: `${TEMPLATE_HOST}/health`,
|
||||
response: { status: 'OK' },
|
||||
},
|
||||
categories: {
|
||||
url: `${TEMPLATE_HOST}/templates/categories`,
|
||||
response: { categories: [] },
|
||||
},
|
||||
getTemplatePreview: {
|
||||
url: `${TEMPLATE_HOST}/templates/workflows/${TEMPLATE_ID}`,
|
||||
response: testTemplate,
|
||||
},
|
||||
getTemplate: {
|
||||
url: `${TEMPLATE_HOST}/workflows/templates/${TEMPLATE_ID}`,
|
||||
response: {
|
||||
id: TEMPLATE_ID,
|
||||
name: testTemplate.workflow.name,
|
||||
workflow: testTemplate.workflow.workflow,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test.describe('Template credentials setup @db:reset', {
|
||||
annotation: [
|
||||
{ type: 'owner', description: 'Adore' },
|
||||
],
|
||||
}, () => {
|
||||
test.beforeEach(async ({ setupRequirements, n8n }) => {
|
||||
await setupRequirements(createTemplateRequirements());
|
||||
await n8n.goHome();
|
||||
});
|
||||
|
||||
test('Should take users to canvas when importing template', async ({ n8n }) => {
|
||||
await n8n.navigate.toTemplateCredentialSetup(TEMPLATE_ID);
|
||||
await expect(n8n.canvas.getLoadingMask()).toBeHidden({ timeout: 30000 });
|
||||
|
||||
await expect(n8n.page).toHaveURL(/\/workflow\/.+\?templateId=.+&new=true/);
|
||||
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(3);
|
||||
await expect(n8n.templateCredentialSetup.getCanvasSetupButton()).toBeVisible();
|
||||
});
|
||||
|
||||
test('Loads template setup modal correctly', async ({ n8n }) => {
|
||||
await n8n.navigate.toTemplateCredentialSetup(TEMPLATE_ID);
|
||||
await expect(n8n.canvas.getLoadingMask()).toBeHidden({ timeout: 30000 });
|
||||
|
||||
await expect(n8n.page).toHaveURL(/\/workflow\/.+\?templateId=.+&new=true/);
|
||||
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(3);
|
||||
|
||||
await expect(n8n.templateCredentialSetup.getCanvasSetupButton()).toBeVisible();
|
||||
|
||||
// Open modal via button click
|
||||
await n8n.templateCredentialSetup.getCanvasSetupButton().click();
|
||||
await expect(n8n.templateCredentialSetup.getCanvasCredentialModal()).toBeVisible();
|
||||
|
||||
const modalSteps = n8n.templateCredentialSetup.getSetupCredentialModalSteps();
|
||||
await expect(modalSteps).toHaveCount(3);
|
||||
await expect(modalSteps.nth(0)).toContainText('Shopify');
|
||||
await expect(modalSteps.nth(1)).toContainText('X (Formerly Twitter)');
|
||||
await expect(modalSteps.nth(2)).toContainText('Telegram');
|
||||
});
|
||||
});
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import { test, expect } from '../../../fixtures/base';
|
||||
|
||||
test.describe('AI-1401 AI sub-nodes show node output with no path back in input', {
|
||||
annotation: [
|
||||
{ type: 'owner', description: 'AI' },
|
||||
],
|
||||
}, () => {
|
||||
test('should show correct root node for nested sub-nodes in input panel', async ({ n8n }) => {
|
||||
await n8n.start.fromImportedWorkflow('Test_ai_1401.json');
|
||||
|
||||
// Execute the workflow first to generate data
|
||||
await n8n.canvas.executeNode('Edit Fields');
|
||||
await n8n.notifications.waitForNotification('Node executed successfully');
|
||||
|
||||
for (const node of ['hackernews_top', 'hackernews_sub']) {
|
||||
await n8n.canvas.openNode(node);
|
||||
await expect(n8n.ndv.getContainer()).toBeVisible();
|
||||
await expect(n8n.ndv.inputPanel.get()).toBeVisible();
|
||||
|
||||
// Switch to JSON mode within the mapping view
|
||||
await n8n.ndv.inputPanel.switchDisplayMode('json');
|
||||
// Verify the input node dropdown shows the correct parent nodes
|
||||
const inputNodeSelect = n8n.ndv.inputPanel.get().locator('[data-test-id*="input-select"]');
|
||||
await expect(inputNodeSelect).toBeVisible();
|
||||
await inputNodeSelect.click();
|
||||
await expect(n8n.page.getByRole('option', { name: 'Edit Fields' })).toBeVisible();
|
||||
await expect(n8n.page.getByRole('option', { name: 'Manual Trigger' })).toBeVisible();
|
||||
await expect(n8n.page.getByRole('option', { name: 'No Operation, do nothing' })).toBeHidden();
|
||||
|
||||
await n8n.ndv.clickBackToCanvasButton();
|
||||
}
|
||||
});
|
||||
});
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { test, expect } from '../../../fixtures/base';
|
||||
|
||||
test.describe('AI-716 Correctly set up agent model shows error', {
|
||||
annotation: [
|
||||
{ type: 'owner', description: 'AI' },
|
||||
],
|
||||
}, () => {
|
||||
test('should not show error when adding a sub-node with credential set-up', async ({ n8n }) => {
|
||||
await n8n.start.fromBlankCanvas();
|
||||
|
||||
await n8n.canvas.addNode('AI Agent');
|
||||
|
||||
await n8n.page.keyboard.press('Escape');
|
||||
|
||||
await n8n.canvas.addNode('OpenAI Chat Model');
|
||||
|
||||
await n8n.credentialsComposer.createFromNdv({
|
||||
apiKey: 'sk-123',
|
||||
});
|
||||
|
||||
await n8n.page.keyboard.press('Escape');
|
||||
|
||||
await expect(n8n.canvas.getNodeIssuesByName('OpenAI Chat Model')).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
import { test, expect } from '../../../fixtures/base';
|
||||
|
||||
test.describe('AI-812-partial-execs-broken-when-using-chat-trigger', {
|
||||
annotation: [
|
||||
{ type: 'owner', description: 'AI' },
|
||||
],
|
||||
}, () => {
|
||||
test.beforeEach(async ({ n8n }) => {
|
||||
await n8n.start.fromImportedWorkflow('Test_chat_partial_execution.json');
|
||||
await n8n.notifications.quickCloseAll();
|
||||
await n8n.canvas.clickZoomToFitButton();
|
||||
await n8n.canvas.deselectAll();
|
||||
});
|
||||
|
||||
test.afterEach(async ({ n8n }) => {
|
||||
await n8n.notifications.quickCloseAll();
|
||||
await n8n.canvas.logsPanel.clearExecutionData();
|
||||
await n8n.canvas.logsPanel.sendManualChatMessage('Test Full Execution');
|
||||
|
||||
await expect(n8n.canvas.logsPanel.getManualChatMessages()).toHaveCount(4);
|
||||
|
||||
await expect(n8n.canvas.logsPanel.getManualChatMessages().last()).toContainText(
|
||||
'Set 3 with chatInput: Test Full Execution',
|
||||
);
|
||||
});
|
||||
|
||||
test('should do partial execution when using chat trigger and clicking NDV execute node', async ({
|
||||
n8n,
|
||||
}) => {
|
||||
await n8n.canvas.openNode('Edit Fields1');
|
||||
await n8n.ndv.execute();
|
||||
|
||||
await expect(n8n.canvas.logsPanel.getManualChatModal()).toBeVisible();
|
||||
await n8n.canvas.logsPanel.sendManualChatMessage('Test Partial Execution');
|
||||
|
||||
await expect(n8n.canvas.logsPanel.getManualChatMessages()).toHaveCount(2);
|
||||
await expect(n8n.canvas.logsPanel.getManualChatMessages().first()).toContainText(
|
||||
'Test Partial Execution',
|
||||
);
|
||||
await expect(n8n.canvas.logsPanel.getManualChatMessages().last()).toContainText(
|
||||
'Set 2 with chatInput: Test Partial Execution',
|
||||
);
|
||||
});
|
||||
|
||||
test('should do partial execution when using chat trigger and context-menu execute node', async ({
|
||||
n8n,
|
||||
}) => {
|
||||
// Workaround to prevent the context menu be blocked by the tabbar
|
||||
await n8n.canvas.dragNodeToRelativePosition('Edit Fields', 0, -100);
|
||||
await n8n.canvas.executeNodeFromContextMenu('Edit Fields');
|
||||
|
||||
await expect(n8n.canvas.logsPanel.getManualChatModal()).toBeVisible();
|
||||
await n8n.canvas.logsPanel.sendManualChatMessage('Test Partial Execution');
|
||||
|
||||
await expect(n8n.canvas.logsPanel.getManualChatMessages()).toHaveCount(2);
|
||||
await expect(n8n.canvas.logsPanel.getManualChatMessages().first()).toContainText(
|
||||
'Test Partial Execution',
|
||||
);
|
||||
await expect(n8n.canvas.logsPanel.getManualChatMessages().last()).toContainText(
|
||||
'Set 1 with chatInput: Test Partial Execution',
|
||||
);
|
||||
});
|
||||
});
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import { EDIT_FIELDS_SET_NODE_NAME } from '../../../config/constants';
|
||||
import { test, expect } from '../../../fixtures/base';
|
||||
|
||||
test.describe('CAT-726 Node connectors not rendered when nodes inserted on the canvas', {
|
||||
annotation: [
|
||||
{ type: 'owner', description: 'Catalysts' },
|
||||
],
|
||||
}, () => {
|
||||
test('should correctly append a No Op node when Loop Over Items node is added (from add button)', async ({
|
||||
n8n,
|
||||
}) => {
|
||||
await n8n.start.fromBlankCanvas();
|
||||
await n8n.canvas.addNode(EDIT_FIELDS_SET_NODE_NAME, { closeNDV: true });
|
||||
await n8n.workflowComposer.executeWorkflowAndWaitForNotification(
|
||||
'Workflow executed successfully',
|
||||
);
|
||||
|
||||
await n8n.canvas.addNodeBetweenNodes(
|
||||
'When clicking ‘Execute workflow’',
|
||||
'Edit Fields',
|
||||
'Loop Over Items (Split in Batches)',
|
||||
);
|
||||
|
||||
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(4);
|
||||
await expect(n8n.canvas.nodeConnections()).toHaveCount(4);
|
||||
|
||||
await expect
|
||||
.soft(n8n.canvas.connectionBetweenNodes('Loop Over Items', 'Replace Me'))
|
||||
.toBeVisible();
|
||||
await expect
|
||||
.soft(n8n.canvas.connectionBetweenNodes('Loop Over Items', 'Edit Fields'))
|
||||
.toBeVisible();
|
||||
await expect
|
||||
.soft(n8n.canvas.connectionBetweenNodes('Replace Me', 'Loop Over Items'))
|
||||
.toBeVisible();
|
||||
});
|
||||
});
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
import { test, expect } from '../../../fixtures/base';
|
||||
|
||||
/**
|
||||
* PAY-4367: Node shifting in cyclic workflows
|
||||
*
|
||||
* Bug: When inserting a node, ALL downstream nodes were shifted right,
|
||||
* including nodes to the LEFT of the insertion point (reachable via cycle).
|
||||
*
|
||||
* Workflow structure:
|
||||
* Trigger(-220) → Start(0) → Middle(220) → End(440)
|
||||
* ↑___________________________| (cycle)
|
||||
*
|
||||
* When inserting between Start and Middle:
|
||||
* - insertX ≈ 158 (midpoint between Start's right edge at 96 and Middle at 220)
|
||||
* - Start is "downstream" via cycle: Middle → End → Start
|
||||
* - But Start (x=0, rightEdge=96) is LEFT of insertX
|
||||
*
|
||||
* Fix filters downstream nodes by position:
|
||||
* overlapsOrIsToTheRight = (rightEdge > insertX) || (position >= insertX)
|
||||
*
|
||||
* Expected behavior:
|
||||
* - Start: should NOT shift (rightEdge 96 < insertX ~158)
|
||||
* - Middle, End: should shift right (position >= insertX)
|
||||
*/
|
||||
test.describe('PAY-4367: Node shifting in cyclic workflows', {
|
||||
annotation: [
|
||||
{ type: 'owner', description: 'Adore' },
|
||||
],
|
||||
}, () => {
|
||||
test('should not shift nodes to the left of insertion point in cyclic workflow', async ({
|
||||
n8n,
|
||||
}) => {
|
||||
// Workflow: Trigger → Start(x=0) → Middle(x=220) → End(x=440) → Start (cycle)
|
||||
await n8n.start.fromBlankCanvas();
|
||||
await n8n.canvas.importWorkflow('Cyclic_workflow_for_insertion_test.json', 'Cyclic Test');
|
||||
|
||||
// Record positions BEFORE insertion
|
||||
const posStartBefore = await n8n.canvas.getNodePosition('Start');
|
||||
const posMiddleBefore = await n8n.canvas.getNodePosition('Middle');
|
||||
const posEndBefore = await n8n.canvas.getNodePosition('End');
|
||||
|
||||
// ACT: Insert node between Start and Middle
|
||||
await n8n.canvas.addNodeBetweenNodes('Start', 'Middle', 'HTTP Request');
|
||||
|
||||
// Record positions AFTER insertion
|
||||
const posStartAfter = await n8n.canvas.getNodePosition('Start');
|
||||
const posMiddleAfter = await n8n.canvas.getNodePosition('Middle');
|
||||
const posEndAfter = await n8n.canvas.getNodePosition('End');
|
||||
|
||||
// ASSERT: Start should NOT have shifted (it's to the left of insertion)
|
||||
// This was the bug - Start would shift because it's "downstream" via the cycle
|
||||
expect(posStartAfter.x).toBe(posStartBefore.x);
|
||||
|
||||
// ASSERT: Middle and End should have shifted right
|
||||
expect(posMiddleAfter.x).toBeGreaterThan(posMiddleBefore.x);
|
||||
expect(posEndAfter.x).toBeGreaterThan(posEndBefore.x);
|
||||
|
||||
// Verify the new node was added (Trigger + Start + Middle + End + HTTP Request = 5)
|
||||
await expect(n8n.canvas.nodeByName('HTTP Request')).toBeVisible();
|
||||
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(5);
|
||||
});
|
||||
|
||||
test('should stretch sticky note when inserting node in front of it', async ({ n8n }) => {
|
||||
// Workflow with a pink sticky note ("Sticky Note14") between Edit Fields and A node
|
||||
// The sticky should stretch to encompass the new node when inserted close to it
|
||||
await n8n.start.fromBlankCanvas();
|
||||
await n8n.canvas.importWorkflow('Bug_node_insertions_sticky.json', 'Sticky Insert Test');
|
||||
|
||||
const pinkSticky = n8n.canvas.sticky.getStickies().filter({ hasText: 'Insert here' });
|
||||
await expect(pinkSticky).toBeVisible();
|
||||
|
||||
const stickyBefore = await pinkSticky.boundingBox();
|
||||
|
||||
// ACT: Insert node between Edit Fields and A (in front of the pink sticky)
|
||||
await n8n.canvas.addNodeBetweenNodes('Edit Fields', 'A', 'HTTP Request');
|
||||
|
||||
const stickyAfter = await pinkSticky.boundingBox();
|
||||
|
||||
// ASSERT: Sticky should have stretched (width increased) to encompass the new node
|
||||
await expect(n8n.canvas.nodeByName('HTTP Request')).toBeVisible();
|
||||
expect(stickyAfter?.width).toBeGreaterThan(stickyBefore?.width ?? 0);
|
||||
|
||||
const newNode = await n8n.canvas.nodeByName('HTTP Request').boundingBox();
|
||||
|
||||
// The new node should be horizontally between the sticky's left and right edges
|
||||
// (with some tolerance for padding/stretching)
|
||||
expect(newNode?.x).toBeGreaterThanOrEqual((stickyAfter?.x ?? 0) - 50);
|
||||
expect((newNode?.x ?? 0) + (newNode?.width ?? 0)).toBeLessThanOrEqual(
|
||||
(stickyAfter?.x ?? 0) + (stickyAfter?.width ?? 0) + 50,
|
||||
);
|
||||
});
|
||||
|
||||
test('should not associate node with stickies when inserting between two separate sticky notes', async ({
|
||||
n8n,
|
||||
}) => {
|
||||
// Workflow with two sticky notes: "Sticky Note20" (pink) and "Note for A5" (yellow)
|
||||
// Inserting a node between "Get a post3" and "A5" should place it in the gap between stickies
|
||||
// The node should NOT be associated with either sticky (no stretching)
|
||||
await n8n.start.fromBlankCanvas();
|
||||
await n8n.canvas.importWorkflow(
|
||||
'Bug_node_insertions_between_stickies.json',
|
||||
'Between Stickies Test',
|
||||
);
|
||||
|
||||
const pinkSticky = n8n.canvas.sticky.getStickies().filter({ hasText: 'Insert here' });
|
||||
const yellowSticky = n8n.canvas.sticky.getStickies().filter({ hasText: 'Note for A' });
|
||||
|
||||
await expect(pinkSticky).toBeVisible();
|
||||
await expect(yellowSticky).toBeVisible();
|
||||
|
||||
const pinkStickyBefore = await pinkSticky.boundingBox();
|
||||
const yellowStickyBefore = await yellowSticky.boundingBox();
|
||||
|
||||
// ACT: Insert node between "Get a post3" and "A5" (in the gap between the two stickies)
|
||||
await n8n.canvas.addNodeBetweenNodes('Get a post3', 'A5', 'HTTP Request');
|
||||
await expect(n8n.canvas.nodeByName('HTTP Request')).toBeVisible();
|
||||
|
||||
const pinkStickyAfter = await pinkSticky.boundingBox();
|
||||
const yellowStickyAfter = await yellowSticky.boundingBox();
|
||||
|
||||
//Stickies should both maintain their width (not stretch to include the new node)
|
||||
expect(pinkStickyAfter?.width).toBe(pinkStickyBefore?.width);
|
||||
expect(yellowStickyAfter?.width).toBe(yellowStickyBefore?.width);
|
||||
|
||||
const newNode = await n8n.canvas.nodeByName('HTTP Request').boundingBox();
|
||||
|
||||
// The new node should be between the pink and yellow stickies
|
||||
expect(newNode?.x).toBeGreaterThan((pinkStickyAfter?.x ?? 0) + (pinkStickyAfter?.width ?? 0));
|
||||
expect((newNode?.x ?? 0) + (newNode?.width ?? 0)).toBeLessThan(yellowStickyAfter?.x ?? 0);
|
||||
});
|
||||
});
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
import { test, expect } from '../../../fixtures/base';
|
||||
import type { TestRequirements } from '../../../Types';
|
||||
|
||||
const requirements: TestRequirements = {
|
||||
workflow: 'Test_workflow_1.json',
|
||||
storage: {
|
||||
N8N_EXPERIMENT_OVERRIDES: JSON.stringify({ ndv_in_focus_panel: 'variant' }),
|
||||
},
|
||||
};
|
||||
|
||||
test.describe('SUG-121 Fields reset after closing NDV', {
|
||||
annotation: [
|
||||
{ type: 'owner', description: 'Adore' },
|
||||
],
|
||||
}, () => {
|
||||
test('should preserve changes to parameters after closing NDV when focus panel is open', async ({
|
||||
n8n,
|
||||
setupRequirements,
|
||||
}) => {
|
||||
await setupRequirements(requirements);
|
||||
await n8n.canvas.clickZoomToFitButton();
|
||||
await n8n.canvas.toggleFocusPanelButton().click();
|
||||
await n8n.canvas.canvasPane().click();
|
||||
await n8n.canvas.nodeByName('Code').dblclick();
|
||||
await n8n.ndv.getParameterByLabel('JavaScript').getByRole('textbox').fill('alert(1)');
|
||||
await n8n.ndv.close();
|
||||
await n8n.canvas.nodeByName('Code').dblclick();
|
||||
await expect(n8n.ndv.getParameterByLabel('JavaScript').getByRole('textbox')).toHaveText(
|
||||
'alert(1)',
|
||||
);
|
||||
});
|
||||
});
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import { test, expect } from '../../../fixtures/base';
|
||||
import type { TestRequirements } from '../../../Types';
|
||||
|
||||
const requirements: TestRequirements = {
|
||||
workflow: {
|
||||
'Test_9999_SUG_38.json': 'SUG_38_Test_Workflow',
|
||||
},
|
||||
};
|
||||
|
||||
test.describe('SUG-38 Inline expression previews are not displayed in NDV', {
|
||||
annotation: [
|
||||
{ type: 'owner', description: 'Adore' },
|
||||
],
|
||||
}, () => {
|
||||
test("should show resolved inline expression preview in NDV if the node's input data is populated", async ({
|
||||
n8n,
|
||||
setupRequirements,
|
||||
}) => {
|
||||
await setupRequirements(requirements);
|
||||
|
||||
await n8n.canvas.clickZoomToFitButton();
|
||||
|
||||
await n8n.workflowComposer.executeWorkflowAndWaitForNotification(
|
||||
'Workflow executed successfully',
|
||||
);
|
||||
|
||||
await n8n.canvas.openNode('Repro1');
|
||||
|
||||
await expect(n8n.ndv.getParameterExpressionPreviewValue()).toBeVisible();
|
||||
|
||||
await expect(n8n.ndv.getParameterExpressionPreviewValue()).toHaveText('hello there');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user