mirror of
https://github.com/n8n-io/n8n.git
synced 2025-12-05 19:27:26 -06:00
test: Move AI features tests to separate folder (#22727)
This commit is contained in:
@@ -1,561 +0,0 @@
|
||||
import {
|
||||
AGENT_NODE_NAME,
|
||||
EDIT_FIELDS_SET_NODE_NAME,
|
||||
MANUAL_CHAT_TRIGGER_NODE_NAME,
|
||||
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
|
||||
AI_MEMORY_WINDOW_BUFFER_MEMORY_NODE_NAME,
|
||||
AI_MEMORY_POSTGRES_NODE_NAME,
|
||||
AI_TOOL_CALCULATOR_NODE_NAME,
|
||||
AI_OUTPUT_PARSER_AUTO_FIXING_NODE_NAME,
|
||||
AI_TOOL_CODE_NODE_NAME,
|
||||
AI_TOOL_WIKIPEDIA_NODE_NAME,
|
||||
BASIC_LLM_CHAIN_NODE_NAME,
|
||||
CHAT_TRIGGER_NODE_DISPLAY_NAME,
|
||||
SCHEDULE_TRIGGER_NODE_NAME,
|
||||
} from '../../config/constants';
|
||||
import { test, expect } from '../../fixtures/base';
|
||||
import type { n8nPage } from '../../pages/n8nPage';
|
||||
|
||||
// Helper functions for common operations
|
||||
async function addOpenAILanguageModelWithCredentials(
|
||||
n8n: n8nPage,
|
||||
parentNode: string,
|
||||
options: { exactMatch?: boolean; closeNDV?: boolean } = { exactMatch: true, closeNDV: false },
|
||||
) {
|
||||
await n8n.canvas.addSupplementalNodeToParent(
|
||||
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
|
||||
'ai_languageModel',
|
||||
parentNode,
|
||||
options,
|
||||
);
|
||||
|
||||
await n8n.credentialsComposer.createFromNdv({
|
||||
apiKey: 'abcd',
|
||||
});
|
||||
await n8n.ndv.clickBackToCanvasButton();
|
||||
}
|
||||
|
||||
async function waitForWorkflowSuccess(n8n: n8nPage, timeout = 3000) {
|
||||
await n8n.notifications.waitForNotificationAndClose('Workflow executed successfully', {
|
||||
timeout,
|
||||
});
|
||||
}
|
||||
|
||||
async function executeChatAndWaitForResponse(n8n: n8nPage, message: string) {
|
||||
await n8n.canvas.logsPanel.sendManualChatMessage(message);
|
||||
await waitForWorkflowSuccess(n8n);
|
||||
}
|
||||
|
||||
async function verifyChatMessages(n8n: n8nPage, expectedCount: number, inputMessage?: string) {
|
||||
const messages = n8n.canvas.getManualChatMessages();
|
||||
await expect(messages).toHaveCount(expectedCount);
|
||||
if (inputMessage) {
|
||||
await expect(messages.first()).toContainText(inputMessage);
|
||||
}
|
||||
await expect(messages.last()).toBeVisible();
|
||||
return messages;
|
||||
}
|
||||
|
||||
async function verifyLogsPanelEntries(n8n: n8nPage, expectedEntries: string[]) {
|
||||
await expect(n8n.canvas.logsPanel.getLogEntries().first()).toBeVisible();
|
||||
await expect(n8n.canvas.logsPanel.getLogEntries()).toHaveCount(expectedEntries.length);
|
||||
for (let i = 0; i < expectedEntries.length; i++) {
|
||||
await expect(n8n.canvas.logsPanel.getLogEntries().nth(i)).toHaveText(expectedEntries[i]);
|
||||
}
|
||||
}
|
||||
|
||||
async function setupBasicAgentWorkflow(n8n: n8nPage, additionalNodes: string[] = []) {
|
||||
await n8n.canvas.addNode(AGENT_NODE_NAME, { closeNDV: true });
|
||||
|
||||
// Add additional nodes if specified
|
||||
for (const nodeName of additionalNodes) {
|
||||
await n8n.canvas.addSupplementalNodeToParent(nodeName, 'ai_tool', AGENT_NODE_NAME, {
|
||||
closeNDV: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Always add OpenAI Language Model
|
||||
await addOpenAILanguageModelWithCredentials(n8n, AGENT_NODE_NAME);
|
||||
}
|
||||
|
||||
test.use({
|
||||
addContainerCapability: {
|
||||
proxyServerEnabled: true,
|
||||
},
|
||||
});
|
||||
test.describe('Langchain Integration @capability:proxy', () => {
|
||||
test.beforeEach(async ({ n8n, proxyServer }) => {
|
||||
await proxyServer.clearAllExpectations();
|
||||
await proxyServer.loadExpectations('langchain');
|
||||
await n8n.canvas.openNewWorkflow();
|
||||
});
|
||||
|
||||
test.describe('Workflow Execution Behavior', () => {
|
||||
test('should not open chat modal', async ({ n8n }) => {
|
||||
await n8n.canvas.addNode(EDIT_FIELDS_SET_NODE_NAME, { closeNDV: true });
|
||||
|
||||
await n8n.canvas.addNode(AGENT_NODE_NAME, { closeNDV: true });
|
||||
|
||||
await n8n.canvas.addSupplementalNodeToParent(
|
||||
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
|
||||
'ai_languageModel',
|
||||
AGENT_NODE_NAME,
|
||||
{ exactMatch: true, closeNDV: true },
|
||||
);
|
||||
|
||||
await n8n.canvas.clickExecuteWorkflowButton();
|
||||
await expect(n8n.canvas.getManualChatModal()).toBeHidden();
|
||||
});
|
||||
|
||||
test('should remove test workflow button', async ({ n8n }) => {
|
||||
await n8n.canvas.addNode(SCHEDULE_TRIGGER_NODE_NAME, { closeNDV: true });
|
||||
|
||||
await n8n.canvas.addNode(EDIT_FIELDS_SET_NODE_NAME, { closeNDV: true });
|
||||
|
||||
await n8n.canvas.addNode(AGENT_NODE_NAME, { closeNDV: true });
|
||||
|
||||
await n8n.canvas.addSupplementalNodeToParent(
|
||||
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
|
||||
'ai_languageModel',
|
||||
AGENT_NODE_NAME,
|
||||
{ exactMatch: true, closeNDV: true },
|
||||
);
|
||||
|
||||
await n8n.canvas.disableNodeFromContextMenu(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
await expect(n8n.canvas.getExecuteWorkflowButton()).toBeHidden();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Node Connection and Configuration', () => {
|
||||
test('should add nodes to all Agent node input types', async ({ n8n }) => {
|
||||
const agentSubNodes = [
|
||||
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
|
||||
AI_MEMORY_WINDOW_BUFFER_MEMORY_NODE_NAME,
|
||||
AI_TOOL_CALCULATOR_NODE_NAME,
|
||||
AI_OUTPUT_PARSER_AUTO_FIXING_NODE_NAME,
|
||||
];
|
||||
await n8n.canvas.addNode(AGENT_NODE_NAME, { closeNDV: false });
|
||||
|
||||
await n8n.ndv.checkParameterCheckboxInputByName('hasOutputParser');
|
||||
await n8n.ndv.clickBackToCanvasButton();
|
||||
await n8n.canvas.addSupplementalNodeToParent(
|
||||
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
|
||||
'ai_languageModel',
|
||||
AGENT_NODE_NAME,
|
||||
{ exactMatch: true, closeNDV: true },
|
||||
);
|
||||
|
||||
await n8n.canvas.addSupplementalNodeToParent(
|
||||
AI_MEMORY_WINDOW_BUFFER_MEMORY_NODE_NAME,
|
||||
'ai_memory',
|
||||
AGENT_NODE_NAME,
|
||||
{ closeNDV: true },
|
||||
);
|
||||
|
||||
await n8n.canvas.addSupplementalNodeToParent(
|
||||
AI_TOOL_CALCULATOR_NODE_NAME,
|
||||
'ai_tool',
|
||||
AGENT_NODE_NAME,
|
||||
{ closeNDV: true },
|
||||
);
|
||||
|
||||
await n8n.canvas.addSupplementalNodeToParent(
|
||||
AI_OUTPUT_PARSER_AUTO_FIXING_NODE_NAME,
|
||||
'ai_outputParser',
|
||||
AGENT_NODE_NAME,
|
||||
{ closeNDV: true },
|
||||
);
|
||||
for (const nodeName of agentSubNodes) {
|
||||
await expect(n8n.canvas.connectionBetweenNodes(nodeName, AGENT_NODE_NAME)).toBeAttached();
|
||||
}
|
||||
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(2 + agentSubNodes.length); // Chat Trigger + Agent + 4 inputs
|
||||
});
|
||||
|
||||
test('should add multiple tool nodes to Agent node tool input type', async ({ n8n }) => {
|
||||
await n8n.canvas.addNode(AGENT_NODE_NAME, { closeNDV: true });
|
||||
|
||||
const tools = [
|
||||
AI_TOOL_CALCULATOR_NODE_NAME,
|
||||
AI_TOOL_CODE_NODE_NAME,
|
||||
AI_TOOL_CODE_NODE_NAME,
|
||||
AI_TOOL_WIKIPEDIA_NODE_NAME,
|
||||
];
|
||||
|
||||
for (const tool of tools) {
|
||||
await n8n.canvas.addSupplementalNodeToParent(tool, 'ai_tool', AGENT_NODE_NAME, {
|
||||
closeNDV: true,
|
||||
});
|
||||
await expect(n8n.canvas.connectionBetweenNodes(tool, AGENT_NODE_NAME)).toBeAttached();
|
||||
}
|
||||
|
||||
// Chat Trigger + Agent + Tools
|
||||
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(2 + tools.length);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Auto-add Behavior', () => {
|
||||
test('should auto-add chat trigger and basic LLM chain when adding LLM node', async ({
|
||||
n8n,
|
||||
}) => {
|
||||
await n8n.canvas.addNode(AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, { closeNDV: true });
|
||||
|
||||
await expect(
|
||||
n8n.canvas.connectionBetweenNodes(
|
||||
CHAT_TRIGGER_NODE_DISPLAY_NAME,
|
||||
BASIC_LLM_CHAIN_NODE_NAME,
|
||||
),
|
||||
).toBeAttached();
|
||||
|
||||
await expect(
|
||||
n8n.canvas.connectionBetweenNodes(
|
||||
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
|
||||
BASIC_LLM_CHAIN_NODE_NAME,
|
||||
),
|
||||
).toBeAttached();
|
||||
|
||||
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(3);
|
||||
});
|
||||
|
||||
test('should not auto-add nodes if AI nodes are already present', async ({ n8n }) => {
|
||||
await n8n.canvas.addNode(AGENT_NODE_NAME, { closeNDV: true });
|
||||
|
||||
await n8n.canvas.addNode(AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, { closeNDV: true });
|
||||
|
||||
await expect(
|
||||
n8n.canvas.connectionBetweenNodes(CHAT_TRIGGER_NODE_DISPLAY_NAME, AGENT_NODE_NAME),
|
||||
).toBeAttached();
|
||||
|
||||
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(3);
|
||||
});
|
||||
|
||||
test('should not auto-add nodes if ChatTrigger is already present', async ({ n8n }) => {
|
||||
await n8n.canvas.addNode(MANUAL_CHAT_TRIGGER_NODE_NAME, { closeNDV: true });
|
||||
|
||||
await n8n.canvas.addNode(AGENT_NODE_NAME, { closeNDV: true });
|
||||
|
||||
await n8n.canvas.addNode(AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, { closeNDV: true });
|
||||
|
||||
await expect(
|
||||
n8n.canvas.connectionBetweenNodes(CHAT_TRIGGER_NODE_DISPLAY_NAME, AGENT_NODE_NAME),
|
||||
).toBeAttached();
|
||||
|
||||
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(3);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Chat Execution and Interaction', () => {
|
||||
test('should be able to open and execute Basic LLM Chain node', async ({ n8n }) => {
|
||||
await n8n.canvas.addNode(BASIC_LLM_CHAIN_NODE_NAME, { closeNDV: true });
|
||||
|
||||
await addOpenAILanguageModelWithCredentials(n8n, BASIC_LLM_CHAIN_NODE_NAME);
|
||||
|
||||
await n8n.canvas.openNode(BASIC_LLM_CHAIN_NODE_NAME);
|
||||
const inputMessage = 'Hello!';
|
||||
|
||||
await n8n.ndv.execute();
|
||||
await executeChatAndWaitForResponse(n8n, inputMessage);
|
||||
|
||||
// Verify chat message appears
|
||||
await expect(n8n.canvas.getManualChatLatestBotMessage()).toBeVisible();
|
||||
});
|
||||
test('should be able to open and execute Agent node', async ({ n8n }) => {
|
||||
await setupBasicAgentWorkflow(n8n);
|
||||
|
||||
const inputMessage = 'Hello!';
|
||||
await n8n.canvas.clickManualChatButton();
|
||||
await executeChatAndWaitForResponse(n8n, inputMessage);
|
||||
|
||||
// Verify chat message appears
|
||||
await expect(n8n.canvas.getManualChatLatestBotMessage()).toBeVisible();
|
||||
});
|
||||
test('should add and use Manual Chat Trigger node together with Agent node', async ({
|
||||
n8n,
|
||||
}) => {
|
||||
await setupBasicAgentWorkflow(n8n);
|
||||
|
||||
const inputMessage = 'Hello!';
|
||||
await n8n.canvas.clickManualChatButton();
|
||||
await executeChatAndWaitForResponse(n8n, inputMessage);
|
||||
|
||||
await verifyChatMessages(n8n, 2, inputMessage);
|
||||
await verifyLogsPanelEntries(n8n, [
|
||||
'When chat message received',
|
||||
'AI Agent',
|
||||
'OpenAI Chat Model',
|
||||
]);
|
||||
|
||||
await n8n.canvas.closeManualChatModal();
|
||||
await expect(n8n.canvas.logsPanel.getLogEntries()).toBeHidden();
|
||||
await expect(n8n.canvas.getManualChatInput()).toBeHidden();
|
||||
});
|
||||
});
|
||||
|
||||
// @AI team to look at this
|
||||
// eslint-disable-next-line playwright/no-skipped-test
|
||||
test.skip('Tool Usage Notifications', () => {
|
||||
test('should show tool info notice if no existing tools were used during execution', async ({
|
||||
n8n,
|
||||
}) => {
|
||||
await setupBasicAgentWorkflow(n8n, [AI_TOOL_CALCULATOR_NODE_NAME]);
|
||||
await n8n.canvas.openNode(AGENT_NODE_NAME);
|
||||
|
||||
const inputMessage = 'Hello!';
|
||||
await n8n.ndv.execute();
|
||||
await executeChatAndWaitForResponse(n8n, inputMessage);
|
||||
|
||||
await n8n.canvas.closeManualChatModal();
|
||||
await n8n.canvas.openNode(AGENT_NODE_NAME);
|
||||
|
||||
await expect(n8n.ndv.getRunDataInfoCallout()).toBeVisible();
|
||||
});
|
||||
test('should not show tool info notice if tools were used during execution', async ({
|
||||
n8n,
|
||||
}) => {
|
||||
await n8n.canvas.addNode(MANUAL_CHAT_TRIGGER_NODE_NAME, { closeNDV: true });
|
||||
await n8n.canvas.addNode(AGENT_NODE_NAME, { closeNDV: false });
|
||||
await expect(n8n.ndv.getRunDataInfoCallout()).toBeHidden();
|
||||
await n8n.ndv.clickBackToCanvasButton();
|
||||
|
||||
await addOpenAILanguageModelWithCredentials(n8n, AGENT_NODE_NAME);
|
||||
|
||||
await n8n.canvas.addSupplementalNodeToParent(
|
||||
AI_TOOL_CALCULATOR_NODE_NAME,
|
||||
'ai_tool',
|
||||
AGENT_NODE_NAME,
|
||||
{ closeNDV: true },
|
||||
);
|
||||
|
||||
const inputMessage = 'What is 1000 * 10?';
|
||||
await n8n.canvas.clickManualChatButton();
|
||||
await executeChatAndWaitForResponse(n8n, inputMessage);
|
||||
|
||||
await n8n.canvas.closeManualChatModal();
|
||||
await n8n.canvas.openNode(AGENT_NODE_NAME);
|
||||
|
||||
await expect(n8n.ndv.getRunDataInfoCallout()).toBeHidden();
|
||||
});
|
||||
});
|
||||
|
||||
// Create a ticket for this for AI team to fix
|
||||
// eslint-disable-next-line playwright/no-skipped-test
|
||||
test.skip('Error Handling and Logs Display', () => {
|
||||
// Helper function to set up the agent workflow with Postgres error configuration
|
||||
async function setupAgentWorkflowWithPostgresError(n8n: n8nPage) {
|
||||
await n8n.canvas.addNode(AGENT_NODE_NAME, { closeNDV: true });
|
||||
|
||||
// Add Calculator Tool (required for OpenAI model)
|
||||
await n8n.canvas.addSupplementalNodeToParent(
|
||||
AI_TOOL_CALCULATOR_NODE_NAME,
|
||||
'ai_tool',
|
||||
AGENT_NODE_NAME,
|
||||
{ closeNDV: true },
|
||||
);
|
||||
|
||||
// Add and configure Postgres Memory
|
||||
await n8n.canvas.addSupplementalNodeToParent(
|
||||
AI_MEMORY_POSTGRES_NODE_NAME,
|
||||
'ai_memory',
|
||||
AGENT_NODE_NAME,
|
||||
{ closeNDV: false },
|
||||
);
|
||||
|
||||
await n8n.credentialsComposer.createFromNdv({
|
||||
password: 'testtesttest',
|
||||
});
|
||||
|
||||
await n8n.ndv.getParameterInput('sessionIdType').click();
|
||||
await n8n.page.getByRole('option', { name: 'Define below' }).click();
|
||||
await n8n.ndv.getParameterInput('sessionKey').locator('input').fill('asdasd');
|
||||
await n8n.ndv.clickBackToCanvasButton();
|
||||
|
||||
// Add and configure OpenAI Language Model
|
||||
await addOpenAILanguageModelWithCredentials(n8n, AGENT_NODE_NAME);
|
||||
|
||||
await n8n.canvas.clickZoomToFitButton();
|
||||
}
|
||||
|
||||
// Helper function to assert logs tab is active
|
||||
async function assertLogsTabIsActive(n8n: n8nPage) {
|
||||
await expect(n8n.ndv.getOutputDataContainer()).toBeVisible();
|
||||
await expect(n8n.ndv.getAiOutputModeToggle()).toBeVisible();
|
||||
|
||||
const radioButtons = n8n.ndv.getAiOutputModeToggle().locator('[role="radio"]');
|
||||
await expect(radioButtons).toHaveCount(2);
|
||||
await expect(radioButtons.nth(1)).toHaveAttribute('aria-checked', 'true');
|
||||
}
|
||||
|
||||
// Helper function to assert error message is visible
|
||||
async function assertErrorMessageVisible(n8n: n8nPage) {
|
||||
await expect(
|
||||
n8n.ndv.getOutputPanel().getByTestId('node-error-message').first(),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
n8n.ndv.getOutputPanel().getByTestId('node-error-message').first(),
|
||||
).toContainText('Error in sub-node');
|
||||
}
|
||||
|
||||
test('should open logs tab by default when there was an error', async ({ n8n }) => {
|
||||
await setupAgentWorkflowWithPostgresError(n8n);
|
||||
|
||||
const inputMessage = 'Test the code tool';
|
||||
|
||||
// Execute workflow with chat trigger
|
||||
await n8n.canvas.clickManualChatButton();
|
||||
await executeChatAndWaitForResponse(n8n, inputMessage);
|
||||
|
||||
// Check that messages and logs are displayed
|
||||
const messages = await verifyChatMessages(n8n, 2, inputMessage);
|
||||
await expect(messages.last()).toContainText(
|
||||
'[ERROR: The service refused the connection - perhaps it is offline]',
|
||||
);
|
||||
|
||||
await expect(n8n.canvas.logsPanel.getLogEntries().first()).toBeVisible();
|
||||
await expect(n8n.canvas.logsPanel.getLogEntries()).toHaveCount(3);
|
||||
await expect(n8n.canvas.logsPanel.getSelectedLogEntry()).toHaveText('AI Agent');
|
||||
await expect(n8n.canvas.logsPanel.outputPanel.get()).toContainText(
|
||||
AI_MEMORY_POSTGRES_NODE_NAME,
|
||||
);
|
||||
|
||||
await n8n.canvas.closeManualChatModal();
|
||||
|
||||
// Open the AI Agent node to see the logs
|
||||
await n8n.canvas.openNode(AGENT_NODE_NAME);
|
||||
|
||||
// Assert that logs tab is active and error is displayed
|
||||
await assertLogsTabIsActive(n8n);
|
||||
await assertErrorMessageVisible(n8n);
|
||||
});
|
||||
|
||||
test('should switch to logs tab on error, when NDV is already opened', async ({ n8n }) => {
|
||||
// Remove the auto-added chat trigger
|
||||
await n8n.canvas.addNode(MANUAL_CHAT_TRIGGER_NODE_NAME, { closeNDV: false });
|
||||
|
||||
// Set manual trigger to output standard pinned data
|
||||
await n8n.ndv.getEditPinnedDataButton().click();
|
||||
await n8n.ndv.savePinnedData();
|
||||
await n8n.ndv.close();
|
||||
|
||||
// Set up the same workflow components but with manual trigger
|
||||
await setupAgentWorkflowWithPostgresError(n8n);
|
||||
|
||||
// Open the AI Agent node
|
||||
await n8n.canvas.openNode(AGENT_NODE_NAME);
|
||||
await n8n.ndv.getParameterInput('promptType').click();
|
||||
await n8n.page.getByRole('option', { name: 'Define below' }).click();
|
||||
await n8n.ndv.getParameterInput('text').locator('textarea').fill('Some text');
|
||||
await n8n.ndv.execute();
|
||||
await waitForWorkflowSuccess(n8n);
|
||||
|
||||
// Assert that logs tab is active and error is displayed
|
||||
await assertLogsTabIsActive(n8n);
|
||||
await assertErrorMessageVisible(n8n);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Advanced Workflow Features', () => {
|
||||
test('should render runItems for sub-nodes and allow switching between them', async ({
|
||||
n8n,
|
||||
}) => {
|
||||
await n8n.start.fromImportedWorkflow('In_memory_vector_store_fake_embeddings.json');
|
||||
await n8n.canvas.clickZoomToFitButton();
|
||||
await n8n.canvas.deselectAll();
|
||||
|
||||
await n8n.canvas.executeNode('Populate VS');
|
||||
await waitForWorkflowSuccess(n8n);
|
||||
|
||||
const assertInputOutputTextExists = async (text: string) => {
|
||||
await expect(n8n.ndv.getOutputPanel()).toContainText(text);
|
||||
await expect(n8n.ndv.getInputPanel()).toContainText(text);
|
||||
};
|
||||
|
||||
const assertInputOutputTextNotExists = async (text: string) => {
|
||||
await expect(n8n.ndv.getOutputPanel()).not.toContainText(text);
|
||||
await expect(n8n.ndv.getInputPanel()).not.toContainText(text);
|
||||
};
|
||||
|
||||
await n8n.canvas.openNode('Character Text Splitter');
|
||||
|
||||
await expect(n8n.ndv.getOutputRunSelector()).toBeVisible();
|
||||
await expect(n8n.ndv.getInputRunSelector()).toBeVisible();
|
||||
await expect(n8n.ndv.getInputRunSelectorInput()).toHaveValue('3 of 3');
|
||||
await expect(n8n.ndv.getOutputRunSelectorInput()).toHaveValue('3 of 3');
|
||||
await assertInputOutputTextExists('Kyiv');
|
||||
await assertInputOutputTextNotExists('Berlin');
|
||||
await assertInputOutputTextNotExists('Prague');
|
||||
|
||||
await n8n.ndv.changeOutputRunSelector('2 of 3');
|
||||
await assertInputOutputTextExists('Berlin');
|
||||
await assertInputOutputTextNotExists('Kyiv');
|
||||
await assertInputOutputTextNotExists('Prague');
|
||||
|
||||
await n8n.ndv.changeOutputRunSelector('1 of 3');
|
||||
await assertInputOutputTextExists('Prague');
|
||||
await assertInputOutputTextNotExists('Berlin');
|
||||
await assertInputOutputTextNotExists('Kyiv');
|
||||
|
||||
await n8n.ndv.toggleInputRunLinking();
|
||||
await n8n.ndv.changeOutputRunSelector('2 of 3');
|
||||
await expect(n8n.ndv.getInputRunSelectorInput()).toHaveValue('1 of 3');
|
||||
await expect(n8n.ndv.getOutputRunSelectorInput()).toHaveValue('2 of 3');
|
||||
await expect(n8n.ndv.getInputPanel()).toContainText('Prague');
|
||||
await expect(n8n.ndv.getInputPanel()).not.toContainText('Berlin');
|
||||
|
||||
await expect(n8n.ndv.getOutputPanel()).toContainText('Berlin');
|
||||
await expect(n8n.ndv.getOutputPanel()).not.toContainText('Prague');
|
||||
|
||||
await n8n.ndv.toggleInputRunLinking();
|
||||
await expect(n8n.ndv.getInputRunSelectorInput()).toHaveValue('1 of 3');
|
||||
await expect(n8n.ndv.getOutputRunSelectorInput()).toHaveValue('1 of 3');
|
||||
await assertInputOutputTextExists('Prague');
|
||||
await assertInputOutputTextNotExists('Berlin');
|
||||
await assertInputOutputTextNotExists('Kyiv');
|
||||
});
|
||||
|
||||
test('should execute up to Node 1 when using partial execution', async ({ n8n }) => {
|
||||
await n8n.start.fromImportedWorkflow('Test_workflow_chat_partial_execution.json');
|
||||
await n8n.canvas.clickZoomToFitButton();
|
||||
|
||||
// Check that chat modal is not initially visible
|
||||
await expect(n8n.canvas.getManualChatModal().locator('main')).toBeHidden();
|
||||
|
||||
// Open Node 1 and execute it
|
||||
await n8n.canvas.openNode('Node 1');
|
||||
await n8n.ndv.execute();
|
||||
// Chat modal should now be visible
|
||||
await expect(n8n.canvas.getManualChatModal().locator('main')).toBeVisible();
|
||||
|
||||
// Send first message
|
||||
await n8n.canvas.logsPanel.sendManualChatMessage('Test');
|
||||
await expect(n8n.canvas.getManualChatLatestBotMessage()).toContainText('this_my_field_1');
|
||||
|
||||
// Refresh session
|
||||
await n8n.page.getByTestId('refresh-session-button').click();
|
||||
await expect(n8n.canvas.getManualChatMessages()).not.toBeAttached();
|
||||
|
||||
// Send another message
|
||||
await n8n.canvas.logsPanel.sendManualChatMessage('Another test');
|
||||
await expect(n8n.canvas.getManualChatLatestBotMessage()).toContainText('this_my_field_3');
|
||||
await expect(n8n.canvas.getManualChatLatestBotMessage()).toContainText('this_my_field_4');
|
||||
});
|
||||
});
|
||||
|
||||
test('should keep the same session when switching tabs', async ({ n8n }) => {
|
||||
await n8n.start.fromImportedWorkflow('Test_workflow_chat_partial_execution.json');
|
||||
await n8n.canvas.clickZoomToFitButton();
|
||||
|
||||
await n8n.canvas.logsPanel.open();
|
||||
|
||||
// Send a message
|
||||
await n8n.canvas.logsPanel.sendManualChatMessage('Test');
|
||||
await expect(n8n.canvas.getManualChatLatestBotMessage()).toContainText('this_my_field');
|
||||
|
||||
await n8n.canvas.clickExecutionsTab();
|
||||
|
||||
await n8n.canvas.clickEditorTab();
|
||||
await expect(n8n.canvas.getManualChatLatestBotMessage()).toContainText('this_my_field');
|
||||
|
||||
// Refresh session
|
||||
await n8n.page.getByTestId('refresh-session-button').click();
|
||||
await expect(n8n.canvas.getManualChatMessages()).not.toBeAttached();
|
||||
});
|
||||
});
|
||||
@@ -1,618 +0,0 @@
|
||||
import {
|
||||
simpleAssistantResponse,
|
||||
codeDiffSuggestionResponse,
|
||||
applyCodeDiffResponse,
|
||||
nodeExecutionSucceededResponse,
|
||||
aiDisabledRequirements,
|
||||
aiEnabledRequirements,
|
||||
aiEnabledWorkflowBaseRequirements,
|
||||
aiEnabledWithWorkflowRequirements,
|
||||
aiEnabledWithQuickRepliesRequirements,
|
||||
aiEnabledWithEndSessionRequirements,
|
||||
aiEnabledWithCodeDiffRequirements,
|
||||
aiEnabledWithSimpleChatRequirements,
|
||||
aiEnabledWithCodeSnippetRequirements,
|
||||
aiEnabledWithHttpWorkflowRequirements,
|
||||
} from '../../config/ai-assistant-fixtures';
|
||||
import {
|
||||
GMAIL_NODE_NAME,
|
||||
HTTP_REQUEST_NODE_NAME,
|
||||
MANUAL_TRIGGER_NODE_NAME,
|
||||
SCHEDULE_TRIGGER_NODE_NAME,
|
||||
} from '../../config/constants';
|
||||
import { test, expect } from '../../fixtures/base';
|
||||
|
||||
type ChatRequestBody = {
|
||||
payload?: {
|
||||
type?: string;
|
||||
text?: string;
|
||||
question?: string;
|
||||
context?: Record<string, unknown>;
|
||||
};
|
||||
};
|
||||
|
||||
test.describe('AI Assistant::disabled', () => {
|
||||
test('does not show assistant button if feature is disabled', async ({
|
||||
n8n,
|
||||
setupRequirements,
|
||||
}) => {
|
||||
await setupRequirements(aiDisabledRequirements);
|
||||
await n8n.page.goto('/workflow/new');
|
||||
await expect(n8n.canvas.canvasPane()).toBeVisible();
|
||||
await expect(n8n.aiAssistant.getAskAssistantFloatingButton()).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('AI Assistant::enabled', () => {
|
||||
test('renders placeholder UI', async ({ n8n, setupRequirements }) => {
|
||||
await setupRequirements(aiEnabledRequirements);
|
||||
await n8n.page.goto('/workflow/new');
|
||||
|
||||
await expect(n8n.aiAssistant.getAskAssistantCanvasActionButton()).toBeVisible();
|
||||
|
||||
await n8n.aiAssistant.getAskAssistantCanvasActionButton().click();
|
||||
|
||||
await expect(n8n.aiAssistant.getAskAssistantChat()).toBeVisible();
|
||||
|
||||
await expect(n8n.aiAssistant.getPlaceholderMessage()).toBeVisible();
|
||||
|
||||
await expect(n8n.aiAssistant.getChatInput()).toBeVisible();
|
||||
|
||||
await expect(n8n.aiAssistant.getSendMessageButton()).toBeDisabled();
|
||||
|
||||
await expect(n8n.aiAssistant.getCloseChatButton()).toBeVisible();
|
||||
|
||||
await n8n.aiAssistant.getCloseChatButton().click();
|
||||
|
||||
await expect(n8n.aiAssistant.getAskAssistantChat()).toBeHidden();
|
||||
});
|
||||
|
||||
test('should show resizer when chat is open', async ({ n8n, setupRequirements }) => {
|
||||
await setupRequirements(aiEnabledRequirements);
|
||||
await n8n.page.goto('/workflow/new');
|
||||
|
||||
await n8n.aiAssistant.getAskAssistantCanvasActionButton().click();
|
||||
|
||||
await expect(n8n.aiAssistant.getAskAssistantSidebarResizer()).toBeVisible();
|
||||
|
||||
await expect(n8n.aiAssistant.getAskAssistantChat()).toBeVisible();
|
||||
|
||||
await n8n.aiAssistant.getAskAssistantSidebarResizer().hover();
|
||||
|
||||
await n8n.aiAssistant.getCloseChatButton().click();
|
||||
});
|
||||
|
||||
test('should start chat session from node error view', async ({ n8n, setupRequirements }) => {
|
||||
await setupRequirements(aiEnabledWithWorkflowRequirements);
|
||||
|
||||
await n8n.canvas.openNode('Stop and Error');
|
||||
|
||||
await n8n.ndv.execute();
|
||||
|
||||
await expect(n8n.aiAssistant.getNodeErrorViewAssistantButton()).toBeVisible();
|
||||
await expect(n8n.aiAssistant.getNodeErrorViewAssistantButton()).toBeEnabled();
|
||||
|
||||
await n8n.aiAssistant.getNodeErrorViewAssistantButton().click();
|
||||
|
||||
await expect(n8n.aiAssistant.getChatMessagesAll()).toHaveCount(1);
|
||||
|
||||
await expect(n8n.aiAssistant.getChatMessagesAll().first()).toContainText(
|
||||
'Hey, this is an assistant message',
|
||||
);
|
||||
});
|
||||
|
||||
test('should render chat input correctly', async ({ n8n, setupRequirements }) => {
|
||||
await setupRequirements(aiEnabledWithWorkflowRequirements);
|
||||
|
||||
await n8n.aiAssistant.getAskAssistantCanvasActionButton().click();
|
||||
|
||||
await expect(n8n.aiAssistant.getAskAssistantChat()).toBeVisible();
|
||||
await expect(n8n.aiAssistant.getChatInput()).toBeVisible();
|
||||
|
||||
await expect(n8n.aiAssistant.getSendMessageButton()).toBeDisabled();
|
||||
|
||||
await n8n.aiAssistant.getChatInput().fill('Test message');
|
||||
|
||||
await expect(n8n.aiAssistant.getChatInput()).toHaveValue('Test message');
|
||||
|
||||
await expect(n8n.aiAssistant.getSendMessageButton()).toBeEnabled();
|
||||
|
||||
await n8n.aiAssistant.getSendMessageButton().click();
|
||||
|
||||
await expect(n8n.aiAssistant.getChatMessagesUser()).toHaveCount(1);
|
||||
|
||||
await expect(n8n.aiAssistant.getChatMessagesUser()).toHaveCount(1);
|
||||
|
||||
await expect(n8n.aiAssistant.getChatInput()).toHaveValue('');
|
||||
});
|
||||
|
||||
test('should render and handle quick replies', async ({ n8n, setupRequirements }) => {
|
||||
await setupRequirements(aiEnabledWithQuickRepliesRequirements);
|
||||
|
||||
await n8n.canvas.openNode('Stop and Error');
|
||||
|
||||
await n8n.ndv.execute();
|
||||
|
||||
await n8n.aiAssistant.getNodeErrorViewAssistantButton().click();
|
||||
|
||||
await expect(n8n.aiAssistant.getQuickReplyButtons()).toHaveCount(2);
|
||||
|
||||
await expect(n8n.aiAssistant.getQuickReplyButtons()).toHaveCount(2);
|
||||
|
||||
await n8n.aiAssistant.getQuickReplyButtons().first().click();
|
||||
|
||||
await expect(n8n.aiAssistant.getChatMessagesUser()).toHaveCount(1);
|
||||
|
||||
await expect(n8n.aiAssistant.getChatMessagesUser()).toHaveCount(1);
|
||||
|
||||
await expect(n8n.aiAssistant.getChatMessagesUser().first()).toContainText("Sure, let's do it");
|
||||
});
|
||||
|
||||
test('should warn before starting a new session', async ({ n8n, setupRequirements }) => {
|
||||
await setupRequirements(aiEnabledWithWorkflowRequirements);
|
||||
|
||||
await n8n.canvas.openNode('Edit Fields');
|
||||
|
||||
await n8n.ndv.execute();
|
||||
|
||||
await n8n.aiAssistant.getNodeErrorViewAssistantButton().click();
|
||||
|
||||
await expect(n8n.aiAssistant.getChatMessagesAll()).toHaveCount(1);
|
||||
|
||||
await n8n.aiAssistant.getCloseChatButton().click();
|
||||
|
||||
await n8n.ndv.clickBackToCanvasButton();
|
||||
|
||||
await n8n.canvas.openNode('Stop and Error');
|
||||
|
||||
await n8n.ndv.execute();
|
||||
|
||||
await n8n.aiAssistant.getNodeErrorViewAssistantButton().click();
|
||||
|
||||
await expect(n8n.aiAssistant.getNewAssistantSessionModal()).toBeVisible();
|
||||
|
||||
await n8n.aiAssistant
|
||||
.getNewAssistantSessionModal()
|
||||
.getByRole('button', { name: 'Start new session' })
|
||||
.click();
|
||||
|
||||
await expect(n8n.aiAssistant.getChatMessagesAll()).toHaveCount(1);
|
||||
});
|
||||
|
||||
test('should end chat session when `end_session` event is received', async ({
|
||||
n8n,
|
||||
setupRequirements,
|
||||
}) => {
|
||||
await setupRequirements(aiEnabledWithEndSessionRequirements);
|
||||
|
||||
await n8n.canvas.openNode('Stop and Error');
|
||||
|
||||
await n8n.ndv.execute();
|
||||
|
||||
await n8n.aiAssistant.getNodeErrorViewAssistantButton().click();
|
||||
|
||||
await expect(n8n.aiAssistant.getChatMessagesSystem()).toHaveCount(1);
|
||||
|
||||
await expect(n8n.aiAssistant.getChatMessagesSystem()).toHaveCount(1);
|
||||
|
||||
await expect(n8n.aiAssistant.getChatMessagesSystem().first()).toContainText(
|
||||
'session has ended',
|
||||
);
|
||||
});
|
||||
|
||||
test('should reset session after it ended and sidebar is closed', async ({
|
||||
n8n,
|
||||
setupRequirements,
|
||||
}) => {
|
||||
await setupRequirements(aiEnabledRequirements);
|
||||
await n8n.page.goto('/workflow/new');
|
||||
|
||||
await n8n.page.route('**/rest/ai/chat', async (route) => {
|
||||
const requestBody = route.request().postDataJSON() as ChatRequestBody;
|
||||
const isInit = requestBody.payload?.type === 'init-support-chat';
|
||||
const response = isInit
|
||||
? simpleAssistantResponse
|
||||
: {
|
||||
sessionId: '1',
|
||||
messages: [
|
||||
{
|
||||
role: 'assistant',
|
||||
type: 'message',
|
||||
title: 'Glad to Help',
|
||||
text: "I'm glad I could help. If you have any more questions or need further assistance with your n8n workflows, feel free to ask!",
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
type: 'event',
|
||||
eventName: 'end-session',
|
||||
},
|
||||
],
|
||||
};
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(response),
|
||||
});
|
||||
});
|
||||
|
||||
await n8n.aiAssistant.getAskAssistantCanvasActionButton().click();
|
||||
await n8n.aiAssistant.sendMessage('Hello', 'enter-key');
|
||||
|
||||
await expect(n8n.aiAssistant.getChatMessagesAll()).toHaveCount(2);
|
||||
|
||||
await n8n.aiAssistant.getCloseChatButton().click();
|
||||
// Wait for sidebar to close
|
||||
await expect(n8n.aiAssistant.getAskAssistantChat()).toBeHidden();
|
||||
await n8n.aiAssistant.getAskAssistantCanvasActionButton().click();
|
||||
await expect(n8n.aiAssistant.getChatMessagesAll()).toHaveCount(2);
|
||||
|
||||
await n8n.aiAssistant.sendMessage('Thanks, bye', 'enter-key');
|
||||
|
||||
await expect(n8n.aiAssistant.getChatMessagesSystem()).toHaveCount(1);
|
||||
await expect(n8n.aiAssistant.getChatMessagesSystem().first()).toContainText(
|
||||
'session has ended',
|
||||
);
|
||||
|
||||
await n8n.aiAssistant.getCloseChatButton().click();
|
||||
await n8n.aiAssistant.getAskAssistantCanvasActionButton().click();
|
||||
await expect(n8n.aiAssistant.getPlaceholderMessage()).toBeVisible();
|
||||
});
|
||||
|
||||
test('should not reset assistant session when workflow is saved', async ({
|
||||
n8n,
|
||||
setupRequirements,
|
||||
}) => {
|
||||
await setupRequirements(aiEnabledWithSimpleChatRequirements);
|
||||
await n8n.page.goto('/workflow/new');
|
||||
|
||||
await n8n.canvas.addInitialNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
await n8n.ndv.clickBackToCanvasButton();
|
||||
|
||||
await n8n.aiAssistant.getAskAssistantCanvasActionButton().click();
|
||||
await n8n.aiAssistant.sendMessage('Hello', 'enter-key');
|
||||
|
||||
await expect(n8n.aiAssistant.getChatMessagesUser()).toHaveCount(1);
|
||||
|
||||
await n8n.canvas.openNode(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
await n8n.ndv.execute();
|
||||
|
||||
await expect(n8n.aiAssistant.getPlaceholderMessage()).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('should send message via shift + enter even with global NodeCreator panel opened', async ({
|
||||
n8n,
|
||||
setupRequirements,
|
||||
}) => {
|
||||
await setupRequirements(aiEnabledWithSimpleChatRequirements);
|
||||
await n8n.page.goto('/workflow/new');
|
||||
|
||||
await n8n.canvas.addInitialNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
await n8n.ndv.clickBackToCanvasButton();
|
||||
|
||||
await n8n.aiAssistant.getAskAssistantCanvasActionButton().click();
|
||||
await n8n.canvas.nodeCreator.open();
|
||||
|
||||
await n8n.aiAssistant.sendMessage('Hello', 'enter-key');
|
||||
|
||||
await expect(n8n.aiAssistant.getPlaceholderMessage()).toHaveCount(0);
|
||||
await expect(n8n.aiAssistant.getChatMessagesUser()).toHaveCount(1);
|
||||
});
|
||||
|
||||
test.describe('Support Chat', () => {
|
||||
test('assistant returns code snippet', async ({ n8n, setupRequirements }) => {
|
||||
await setupRequirements(aiEnabledWithCodeSnippetRequirements);
|
||||
await n8n.page.goto('/workflow/new');
|
||||
|
||||
await expect(n8n.aiAssistant.getAskAssistantCanvasActionButton()).toBeVisible();
|
||||
await n8n.aiAssistant.getAskAssistantCanvasActionButton().click();
|
||||
|
||||
await expect(n8n.aiAssistant.getAskAssistantChat()).toBeVisible();
|
||||
|
||||
await n8n.aiAssistant.sendMessage('Show me an expression');
|
||||
|
||||
await expect(n8n.aiAssistant.getChatMessagesAll()).toHaveCount(3);
|
||||
await expect(n8n.aiAssistant.getChatMessagesUser().first()).toContainText(
|
||||
'Show me an expression',
|
||||
);
|
||||
await expect(n8n.aiAssistant.getChatMessagesAssistant().first()).toContainText(
|
||||
'To use expressions in n8n, follow these steps:',
|
||||
);
|
||||
await expect(n8n.aiAssistant.getChatMessagesAssistant().first()).toContainText('New York');
|
||||
await expect(n8n.aiAssistant.getCodeSnippet()).toHaveText('{{$json.body.city}}');
|
||||
});
|
||||
|
||||
test('should send current context to support chat', async ({ n8n, setupRequirements }) => {
|
||||
await setupRequirements(aiEnabledWithHttpWorkflowRequirements);
|
||||
|
||||
const chatRequests: ChatRequestBody[] = [];
|
||||
await n8n.page.route('**/rest/ai/chat', async (route) => {
|
||||
const body = route.request().postDataJSON() as ChatRequestBody;
|
||||
chatRequests.push(body);
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(simpleAssistantResponse),
|
||||
});
|
||||
});
|
||||
|
||||
await n8n.aiAssistant.getAskAssistantCanvasActionButton().click();
|
||||
await n8n.aiAssistant.sendMessage('What is wrong with this workflow?');
|
||||
|
||||
const supportRequest = chatRequests.find(
|
||||
(request) => request.payload?.question === 'What is wrong with this workflow?',
|
||||
);
|
||||
expect(supportRequest).toBeDefined();
|
||||
const supportContext = supportRequest?.payload?.context;
|
||||
expect(supportContext).toBeDefined();
|
||||
expect(supportContext?.currentView).toBeDefined();
|
||||
expect(supportContext?.currentWorkflow).toBeDefined();
|
||||
});
|
||||
|
||||
test('should not send workflow context if nothing changed', async ({
|
||||
n8n,
|
||||
setupRequirements,
|
||||
}) => {
|
||||
await setupRequirements(aiEnabledWithHttpWorkflowRequirements);
|
||||
|
||||
const chatRequests: ChatRequestBody[] = [];
|
||||
await n8n.page.route('**/rest/ai/chat', async (route) => {
|
||||
const body = route.request().postDataJSON() as ChatRequestBody;
|
||||
chatRequests.push(body);
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(simpleAssistantResponse),
|
||||
});
|
||||
});
|
||||
|
||||
await n8n.aiAssistant.getAskAssistantCanvasActionButton().click();
|
||||
|
||||
await n8n.aiAssistant.sendMessage('What is wrong with this workflow?', 'enter-key');
|
||||
// Wait for message to be processed
|
||||
await expect(n8n.aiAssistant.getChatMessagesAssistant()).toHaveCount(1);
|
||||
|
||||
await n8n.aiAssistant.sendMessage('And now?', 'enter-key');
|
||||
await expect(n8n.aiAssistant.getChatMessagesAssistant()).toHaveCount(2);
|
||||
const secondRequest = chatRequests.find((request) => request.payload?.text === 'And now?');
|
||||
const secondContext = secondRequest?.payload?.context;
|
||||
expect(secondContext?.currentWorkflow).toBeUndefined();
|
||||
|
||||
await n8n.canvas.openNode(HTTP_REQUEST_NODE_NAME);
|
||||
await n8n.ndv.setParameterInputValue('url', 'https://example.com');
|
||||
await n8n.ndv.close();
|
||||
await n8n.canvas.clickExecuteWorkflowButton();
|
||||
|
||||
await n8n.aiAssistant.sendMessage('What about now?', 'enter-key');
|
||||
await expect(n8n.aiAssistant.getChatMessagesAssistant()).toHaveCount(3);
|
||||
|
||||
const thirdRequest = chatRequests.find(
|
||||
(request) => request.payload?.text === 'What about now?',
|
||||
);
|
||||
const thirdContext = thirdRequest?.payload?.context;
|
||||
expect(thirdContext?.currentWorkflow).toBeTruthy();
|
||||
expect(thirdContext?.executionData).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Code Node Error Help', () => {
|
||||
test('should apply code diff to code node', async ({ n8n, setupRequirements }) => {
|
||||
await setupRequirements(aiEnabledWithCodeDiffRequirements);
|
||||
|
||||
let applySuggestionCalls = 0;
|
||||
await n8n.page.route('**/rest/ai/chat/apply-suggestion', async (route) => {
|
||||
applySuggestionCalls += 1;
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(applyCodeDiffResponse),
|
||||
});
|
||||
});
|
||||
|
||||
await n8n.canvas.openNode('Code');
|
||||
|
||||
await n8n.ndv.execute();
|
||||
|
||||
await n8n.aiAssistant.getNodeErrorViewAssistantButton().click();
|
||||
|
||||
await expect(n8n.aiAssistant.getChatMessagesAll()).toHaveCount(2);
|
||||
await expect(n8n.aiAssistant.getCodeDiffs()).toHaveCount(1);
|
||||
await expect(n8n.aiAssistant.getApplyCodeDiffButtons()).toHaveCount(1);
|
||||
|
||||
await n8n.aiAssistant.getApplyCodeDiffButtons().first().click();
|
||||
|
||||
await expect(n8n.aiAssistant.getApplyCodeDiffButtons()).toHaveCount(0);
|
||||
await expect(n8n.aiAssistant.getUndoReplaceCodeButtons()).toHaveCount(1);
|
||||
await expect(n8n.aiAssistant.getCodeReplacedMessage()).toBeVisible();
|
||||
await expect(n8n.ndv.getCodeEditor()).toContainText('item.json.myNewField = 1');
|
||||
|
||||
await n8n.aiAssistant.getUndoReplaceCodeButtons().first().click();
|
||||
|
||||
await expect(n8n.aiAssistant.getApplyCodeDiffButtons()).toHaveCount(1);
|
||||
await expect(n8n.aiAssistant.getCodeReplacedMessage()).toHaveCount(0);
|
||||
expect(applySuggestionCalls).toBe(1);
|
||||
await expect(n8n.ndv.getCodeEditor()).toContainText('item.json.myNewField = 1aaa');
|
||||
|
||||
await n8n.aiAssistant.getApplyCodeDiffButtons().first().click();
|
||||
|
||||
await expect(n8n.ndv.getCodeEditor()).toContainText('item.json.myNewField = 1');
|
||||
});
|
||||
|
||||
test('should ignore node execution success and error messages after the node run successfully once', async ({
|
||||
n8n,
|
||||
setupRequirements,
|
||||
}) => {
|
||||
await setupRequirements(aiEnabledWorkflowBaseRequirements);
|
||||
|
||||
let chatRequestCount = 0;
|
||||
await n8n.page.route('**/rest/ai/chat', async (route) => {
|
||||
chatRequestCount += 1;
|
||||
const response =
|
||||
chatRequestCount === 1 ? codeDiffSuggestionResponse : nodeExecutionSucceededResponse;
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(response),
|
||||
});
|
||||
});
|
||||
|
||||
await n8n.canvas.openNode('Code');
|
||||
await n8n.ndv.execute();
|
||||
await n8n.aiAssistant.getNodeErrorViewAssistantButton().click();
|
||||
|
||||
await n8n.ndv
|
||||
.getCodeEditor()
|
||||
.fill(
|
||||
"// Loop over input items and add a new field called 'myNewField' to the JSON of each one\nfor (const item of $input.all()) {\n item.json.myNewField = 1;\n}\n\nreturn $input.all();",
|
||||
);
|
||||
|
||||
await n8n.ndv.execute();
|
||||
|
||||
await n8n.ndv
|
||||
.getCodeEditor()
|
||||
.fill(
|
||||
"// Loop over input items and add a new field called 'myNewField' to the JSON of each one\nfor (const item of $input.all()) {\n item.json.myNewField = 1aaaa!;\n}\n\nreturn $input.all();",
|
||||
);
|
||||
|
||||
await n8n.ndv.execute();
|
||||
|
||||
await expect(n8n.aiAssistant.getChatMessagesAssistant().nth(2)).toContainText(
|
||||
'Code node ran successfully, did my solution help resolve your issue?',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Credential Help', () => {
|
||||
test('should start credential help from node credential', async ({
|
||||
n8n,
|
||||
setupRequirements,
|
||||
}) => {
|
||||
await setupRequirements(aiEnabledWithSimpleChatRequirements);
|
||||
await n8n.page.goto('/workflow/new');
|
||||
|
||||
await n8n.canvas.addInitialNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
await n8n.ndv.clickBackToCanvasButton();
|
||||
await n8n.canvas.addNode(GMAIL_NODE_NAME, { action: 'Get many messages', closeNDV: false });
|
||||
|
||||
await n8n.ndv.clickCreateNewCredential();
|
||||
|
||||
await expect(n8n.canvas.credentialModal.getModal()).toBeVisible();
|
||||
|
||||
const assistantButton = n8n.aiAssistant.getCredentialEditAssistantButton().locator('button');
|
||||
await expect(assistantButton).toBeVisible();
|
||||
await assistantButton.click();
|
||||
|
||||
await expect(n8n.aiAssistant.getChatMessagesUser()).toHaveCount(1);
|
||||
await expect(n8n.aiAssistant.getChatMessagesUser().first()).toContainText(
|
||||
'How do I set up the credentials for Gmail OAuth2 API?',
|
||||
);
|
||||
await expect(n8n.aiAssistant.getChatMessagesAssistant().first()).toContainText(
|
||||
'Hey, this is an assistant message',
|
||||
);
|
||||
await expect(assistantButton).toBeDisabled();
|
||||
});
|
||||
|
||||
test('should start credential help from credential list', async ({
|
||||
n8n,
|
||||
setupRequirements,
|
||||
}) => {
|
||||
await setupRequirements(aiEnabledWithSimpleChatRequirements);
|
||||
|
||||
await n8n.navigate.toCredentials();
|
||||
|
||||
await n8n.workflows.addResource.credential();
|
||||
await n8n.credentials.selectCredentialType('Notion API');
|
||||
|
||||
const assistantButton = n8n.aiAssistant.getCredentialEditAssistantButton().locator('button');
|
||||
await expect(assistantButton).toBeVisible();
|
||||
await assistantButton.click();
|
||||
|
||||
await expect(n8n.aiAssistant.getChatMessagesUser()).toHaveCount(1);
|
||||
await expect(n8n.aiAssistant.getChatMessagesUser().first()).toContainText(
|
||||
'How do I set up the credentials for Notion API?',
|
||||
);
|
||||
await expect(n8n.aiAssistant.getChatMessagesAssistant().first()).toContainText(
|
||||
'Hey, this is an assistant message',
|
||||
);
|
||||
await expect(assistantButton).toBeDisabled();
|
||||
});
|
||||
|
||||
test('should not show assistant button if click to connect', async ({
|
||||
n8n,
|
||||
setupRequirements,
|
||||
}) => {
|
||||
await setupRequirements(aiEnabledRequirements);
|
||||
|
||||
await n8n.page.route('**/types/credentials.json', async (route) => {
|
||||
const response = await route.fetch();
|
||||
const credentials = (await response.json()) as Array<
|
||||
{ name?: string } & Record<string, unknown>
|
||||
>;
|
||||
const index = credentials.findIndex((c) => c.name === 'slackOAuth2Api');
|
||||
if (index >= 0) {
|
||||
credentials[index] = {
|
||||
...credentials[index],
|
||||
__overwrittenProperties: ['clientId', 'clientSecret'],
|
||||
};
|
||||
}
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(credentials),
|
||||
});
|
||||
});
|
||||
|
||||
await n8n.page.goto('/workflow/new');
|
||||
await n8n.canvas.addInitialNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||
await n8n.canvas.addNode('Slack', { action: 'Get a channel' });
|
||||
|
||||
await n8n.ndv.clickCreateNewCredential();
|
||||
|
||||
const authOptions = n8n.canvas.credentialModal.getAuthTypeRadioButtons();
|
||||
await authOptions.first().click();
|
||||
|
||||
await expect(n8n.canvas.credentialModal.oauthConnectButton).toHaveCount(1);
|
||||
await expect(n8n.canvas.credentialModal.getCredentialInputs()).toHaveCount(2);
|
||||
await expect(n8n.aiAssistant.getCredentialEditAssistantButton()).toHaveCount(0);
|
||||
|
||||
await authOptions.nth(1).click();
|
||||
await expect(n8n.canvas.credentialModal.getCredentialInputs()).toHaveCount(4);
|
||||
await expect(n8n.aiAssistant.getCredentialEditAssistantButton()).toHaveCount(1);
|
||||
});
|
||||
|
||||
test('should not show assistant button when click to connect with some fields', async ({
|
||||
n8n,
|
||||
setupRequirements,
|
||||
}) => {
|
||||
await setupRequirements(aiEnabledRequirements);
|
||||
|
||||
await n8n.page.route('**/types/credentials.json', async (route) => {
|
||||
const response = await route.fetch();
|
||||
const credentials = (await response.json()) as Array<
|
||||
{ name?: string } & Record<string, unknown>
|
||||
>;
|
||||
const index = credentials.findIndex((c) => c.name === 'microsoftOutlookOAuth2Api');
|
||||
if (index >= 0) {
|
||||
credentials[index] = {
|
||||
...credentials[index],
|
||||
__overwrittenProperties: ['authUrl', 'accessTokenUrl', 'clientId', 'clientSecret'],
|
||||
};
|
||||
}
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(credentials),
|
||||
});
|
||||
});
|
||||
|
||||
await n8n.page.goto('/workflow/new');
|
||||
await n8n.canvas.addInitialNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||
await n8n.canvas.addNode('Microsoft Outlook', { action: 'Get a calendar' });
|
||||
|
||||
await n8n.ndv.clickCreateNewCredential();
|
||||
|
||||
await expect(n8n.canvas.credentialModal.oauthConnectButton).toHaveCount(1);
|
||||
await expect(n8n.canvas.credentialModal.getCredentialInputs()).toHaveCount(2);
|
||||
await expect(n8n.aiAssistant.getCredentialEditAssistantButton()).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
288
packages/testing/playwright/tests/ui/ai/assistant-basic.spec.ts
Normal file
288
packages/testing/playwright/tests/ui/ai/assistant-basic.spec.ts
Normal file
@@ -0,0 +1,288 @@
|
||||
import {
|
||||
simpleAssistantResponse,
|
||||
aiDisabledRequirements,
|
||||
aiEnabledRequirements,
|
||||
aiEnabledWithWorkflowRequirements,
|
||||
aiEnabledWithQuickRepliesRequirements,
|
||||
aiEnabledWithEndSessionRequirements,
|
||||
aiEnabledWithSimpleChatRequirements,
|
||||
} from '../../../config/ai-assistant-fixtures';
|
||||
import { SCHEDULE_TRIGGER_NODE_NAME } from '../../../config/constants';
|
||||
import { test, expect } from '../../../fixtures/base';
|
||||
|
||||
type ChatRequestBody = {
|
||||
payload?: {
|
||||
type?: string;
|
||||
text?: string;
|
||||
question?: string;
|
||||
context?: Record<string, unknown>;
|
||||
};
|
||||
};
|
||||
|
||||
test.describe('AI Assistant::disabled', () => {
|
||||
test('does not show assistant button if feature is disabled', async ({
|
||||
n8n,
|
||||
setupRequirements,
|
||||
}) => {
|
||||
await setupRequirements(aiDisabledRequirements);
|
||||
await n8n.page.goto('/workflow/new');
|
||||
await expect(n8n.canvas.canvasPane()).toBeVisible();
|
||||
await expect(n8n.aiAssistant.getAskAssistantFloatingButton()).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('AI Assistant::enabled', () => {
|
||||
test('renders placeholder UI', async ({ n8n, setupRequirements }) => {
|
||||
await setupRequirements(aiEnabledRequirements);
|
||||
await n8n.page.goto('/workflow/new');
|
||||
|
||||
await expect(n8n.aiAssistant.getAskAssistantCanvasActionButton()).toBeVisible();
|
||||
|
||||
await n8n.aiAssistant.getAskAssistantCanvasActionButton().click();
|
||||
|
||||
await expect(n8n.aiAssistant.getAskAssistantChat()).toBeVisible();
|
||||
|
||||
await expect(n8n.aiAssistant.getPlaceholderMessage()).toBeVisible();
|
||||
|
||||
await expect(n8n.aiAssistant.getChatInput()).toBeVisible();
|
||||
|
||||
await expect(n8n.aiAssistant.getSendMessageButton()).toBeDisabled();
|
||||
|
||||
await expect(n8n.aiAssistant.getCloseChatButton()).toBeVisible();
|
||||
|
||||
await n8n.aiAssistant.getCloseChatButton().click();
|
||||
|
||||
await expect(n8n.aiAssistant.getAskAssistantChat()).toBeHidden();
|
||||
});
|
||||
|
||||
test('should show resizer when chat is open', async ({ n8n, setupRequirements }) => {
|
||||
await setupRequirements(aiEnabledRequirements);
|
||||
await n8n.page.goto('/workflow/new');
|
||||
|
||||
await n8n.aiAssistant.getAskAssistantCanvasActionButton().click();
|
||||
|
||||
await expect(n8n.aiAssistant.getAskAssistantSidebarResizer()).toBeVisible();
|
||||
|
||||
await expect(n8n.aiAssistant.getAskAssistantChat()).toBeVisible();
|
||||
|
||||
await n8n.aiAssistant.getAskAssistantSidebarResizer().hover();
|
||||
|
||||
await n8n.aiAssistant.getCloseChatButton().click();
|
||||
});
|
||||
|
||||
test('should start chat session from node error view', async ({ n8n, setupRequirements }) => {
|
||||
await setupRequirements(aiEnabledWithWorkflowRequirements);
|
||||
|
||||
await n8n.canvas.openNode('Stop and Error');
|
||||
|
||||
await n8n.ndv.execute();
|
||||
|
||||
await expect(n8n.aiAssistant.getNodeErrorViewAssistantButton()).toBeVisible();
|
||||
await expect(n8n.aiAssistant.getNodeErrorViewAssistantButton()).toBeEnabled();
|
||||
|
||||
await n8n.aiAssistant.getNodeErrorViewAssistantButton().click();
|
||||
|
||||
await expect(n8n.aiAssistant.getChatMessagesAll()).toHaveCount(1);
|
||||
|
||||
await expect(n8n.aiAssistant.getChatMessagesAll().first()).toContainText(
|
||||
'Hey, this is an assistant message',
|
||||
);
|
||||
});
|
||||
|
||||
test('should render chat input correctly', async ({ n8n, setupRequirements }) => {
|
||||
await setupRequirements(aiEnabledWithWorkflowRequirements);
|
||||
|
||||
await n8n.aiAssistant.getAskAssistantCanvasActionButton().click();
|
||||
|
||||
await expect(n8n.aiAssistant.getAskAssistantChat()).toBeVisible();
|
||||
await expect(n8n.aiAssistant.getChatInput()).toBeVisible();
|
||||
|
||||
await expect(n8n.aiAssistant.getSendMessageButton()).toBeDisabled();
|
||||
|
||||
await n8n.aiAssistant.getChatInput().fill('Test message');
|
||||
|
||||
await expect(n8n.aiAssistant.getChatInput()).toHaveValue('Test message');
|
||||
|
||||
await expect(n8n.aiAssistant.getSendMessageButton()).toBeEnabled();
|
||||
|
||||
await n8n.aiAssistant.getSendMessageButton().click();
|
||||
|
||||
await expect(n8n.aiAssistant.getChatMessagesUser()).toHaveCount(1);
|
||||
|
||||
await expect(n8n.aiAssistant.getChatMessagesUser()).toHaveCount(1);
|
||||
|
||||
await expect(n8n.aiAssistant.getChatInput()).toHaveValue('');
|
||||
});
|
||||
|
||||
test('should render and handle quick replies', async ({ n8n, setupRequirements }) => {
|
||||
await setupRequirements(aiEnabledWithQuickRepliesRequirements);
|
||||
|
||||
await n8n.canvas.openNode('Stop and Error');
|
||||
|
||||
await n8n.ndv.execute();
|
||||
|
||||
await n8n.aiAssistant.getNodeErrorViewAssistantButton().click();
|
||||
|
||||
await expect(n8n.aiAssistant.getQuickReplyButtons()).toHaveCount(2);
|
||||
|
||||
await expect(n8n.aiAssistant.getQuickReplyButtons()).toHaveCount(2);
|
||||
|
||||
await n8n.aiAssistant.getQuickReplyButtons().first().click();
|
||||
|
||||
await expect(n8n.aiAssistant.getChatMessagesUser()).toHaveCount(1);
|
||||
|
||||
await expect(n8n.aiAssistant.getChatMessagesUser()).toHaveCount(1);
|
||||
|
||||
await expect(n8n.aiAssistant.getChatMessagesUser().first()).toContainText("Sure, let's do it");
|
||||
});
|
||||
|
||||
test('should warn before starting a new session', async ({ n8n, setupRequirements }) => {
|
||||
await setupRequirements(aiEnabledWithWorkflowRequirements);
|
||||
|
||||
await n8n.canvas.openNode('Edit Fields');
|
||||
|
||||
await n8n.ndv.execute();
|
||||
|
||||
await n8n.aiAssistant.getNodeErrorViewAssistantButton().click();
|
||||
|
||||
await expect(n8n.aiAssistant.getChatMessagesAll()).toHaveCount(1);
|
||||
|
||||
await n8n.aiAssistant.getCloseChatButton().click();
|
||||
|
||||
await n8n.ndv.clickBackToCanvasButton();
|
||||
|
||||
await n8n.canvas.openNode('Stop and Error');
|
||||
|
||||
await n8n.ndv.execute();
|
||||
|
||||
await n8n.aiAssistant.getNodeErrorViewAssistantButton().click();
|
||||
|
||||
await expect(n8n.aiAssistant.getNewAssistantSessionModal()).toBeVisible();
|
||||
|
||||
await n8n.aiAssistant
|
||||
.getNewAssistantSessionModal()
|
||||
.getByRole('button', { name: 'Start new session' })
|
||||
.click();
|
||||
|
||||
await expect(n8n.aiAssistant.getChatMessagesAll()).toHaveCount(1);
|
||||
});
|
||||
|
||||
test('should end chat session when `end_session` event is received', async ({
|
||||
n8n,
|
||||
setupRequirements,
|
||||
}) => {
|
||||
await setupRequirements(aiEnabledWithEndSessionRequirements);
|
||||
|
||||
await n8n.canvas.openNode('Stop and Error');
|
||||
|
||||
await n8n.ndv.execute();
|
||||
|
||||
await n8n.aiAssistant.getNodeErrorViewAssistantButton().click();
|
||||
|
||||
await expect(n8n.aiAssistant.getChatMessagesSystem()).toHaveCount(1);
|
||||
|
||||
await expect(n8n.aiAssistant.getChatMessagesSystem()).toHaveCount(1);
|
||||
|
||||
await expect(n8n.aiAssistant.getChatMessagesSystem().first()).toContainText(
|
||||
'session has ended',
|
||||
);
|
||||
});
|
||||
|
||||
test('should reset session after it ended and sidebar is closed', async ({
|
||||
n8n,
|
||||
setupRequirements,
|
||||
}) => {
|
||||
await setupRequirements(aiEnabledRequirements);
|
||||
await n8n.page.goto('/workflow/new');
|
||||
|
||||
await n8n.page.route('**/rest/ai/chat', async (route) => {
|
||||
const requestBody = route.request().postDataJSON() as ChatRequestBody;
|
||||
const isInit = requestBody.payload?.type === 'init-support-chat';
|
||||
const response = isInit
|
||||
? simpleAssistantResponse
|
||||
: {
|
||||
sessionId: '1',
|
||||
messages: [
|
||||
{
|
||||
role: 'assistant',
|
||||
type: 'message',
|
||||
title: 'Glad to Help',
|
||||
text: "I'm glad I could help. If you have any more questions or need further assistance with your n8n workflows, feel free to ask!",
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
type: 'event',
|
||||
eventName: 'end-session',
|
||||
},
|
||||
],
|
||||
};
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(response),
|
||||
});
|
||||
});
|
||||
|
||||
await n8n.aiAssistant.getAskAssistantCanvasActionButton().click();
|
||||
await n8n.aiAssistant.sendMessage('Hello', 'enter-key');
|
||||
|
||||
await expect(n8n.aiAssistant.getChatMessagesAll()).toHaveCount(2);
|
||||
|
||||
await n8n.aiAssistant.getCloseChatButton().click();
|
||||
// Wait for sidebar to close
|
||||
await expect(n8n.aiAssistant.getAskAssistantChat()).toBeHidden();
|
||||
await n8n.aiAssistant.getAskAssistantCanvasActionButton().click();
|
||||
await expect(n8n.aiAssistant.getChatMessagesAll()).toHaveCount(2);
|
||||
|
||||
await n8n.aiAssistant.sendMessage('Thanks, bye', 'enter-key');
|
||||
|
||||
await expect(n8n.aiAssistant.getChatMessagesSystem()).toHaveCount(1);
|
||||
await expect(n8n.aiAssistant.getChatMessagesSystem().first()).toContainText(
|
||||
'session has ended',
|
||||
);
|
||||
|
||||
await n8n.aiAssistant.getCloseChatButton().click();
|
||||
await n8n.aiAssistant.getAskAssistantCanvasActionButton().click();
|
||||
await expect(n8n.aiAssistant.getPlaceholderMessage()).toBeVisible();
|
||||
});
|
||||
|
||||
test('should not reset assistant session when workflow is saved', async ({
|
||||
n8n,
|
||||
setupRequirements,
|
||||
}) => {
|
||||
await setupRequirements(aiEnabledWithSimpleChatRequirements);
|
||||
await n8n.page.goto('/workflow/new');
|
||||
|
||||
await n8n.canvas.addInitialNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
await n8n.ndv.clickBackToCanvasButton();
|
||||
|
||||
await n8n.aiAssistant.getAskAssistantCanvasActionButton().click();
|
||||
await n8n.aiAssistant.sendMessage('Hello', 'enter-key');
|
||||
|
||||
await expect(n8n.aiAssistant.getChatMessagesUser()).toHaveCount(1);
|
||||
|
||||
await n8n.canvas.openNode(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
await n8n.ndv.execute();
|
||||
|
||||
await expect(n8n.aiAssistant.getPlaceholderMessage()).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('should send message via shift + enter even with global NodeCreator panel opened', async ({
|
||||
n8n,
|
||||
setupRequirements,
|
||||
}) => {
|
||||
await setupRequirements(aiEnabledWithSimpleChatRequirements);
|
||||
await n8n.page.goto('/workflow/new');
|
||||
|
||||
await n8n.canvas.addInitialNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
await n8n.ndv.clickBackToCanvasButton();
|
||||
|
||||
await n8n.aiAssistant.getAskAssistantCanvasActionButton().click();
|
||||
await n8n.canvas.nodeCreator.open();
|
||||
|
||||
await n8n.aiAssistant.sendMessage('Hello', 'enter-key');
|
||||
|
||||
await expect(n8n.aiAssistant.getPlaceholderMessage()).toHaveCount(0);
|
||||
await expect(n8n.aiAssistant.getChatMessagesUser()).toHaveCount(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,97 @@
|
||||
import {
|
||||
codeDiffSuggestionResponse,
|
||||
applyCodeDiffResponse,
|
||||
nodeExecutionSucceededResponse,
|
||||
aiEnabledWorkflowBaseRequirements,
|
||||
aiEnabledWithCodeDiffRequirements,
|
||||
} from '../../../config/ai-assistant-fixtures';
|
||||
import { test, expect } from '../../../fixtures/base';
|
||||
|
||||
test.describe('AI Assistant::enabled', () => {
|
||||
test.describe('Code Node Error Help', () => {
|
||||
test('should apply code diff to code node', async ({ n8n, setupRequirements }) => {
|
||||
await setupRequirements(aiEnabledWithCodeDiffRequirements);
|
||||
|
||||
let applySuggestionCalls = 0;
|
||||
await n8n.page.route('**/rest/ai/chat/apply-suggestion', async (route) => {
|
||||
applySuggestionCalls += 1;
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(applyCodeDiffResponse),
|
||||
});
|
||||
});
|
||||
|
||||
await n8n.canvas.openNode('Code');
|
||||
|
||||
await n8n.ndv.execute();
|
||||
|
||||
await n8n.aiAssistant.getNodeErrorViewAssistantButton().click();
|
||||
|
||||
await expect(n8n.aiAssistant.getChatMessagesAll()).toHaveCount(2);
|
||||
await expect(n8n.aiAssistant.getCodeDiffs()).toHaveCount(1);
|
||||
await expect(n8n.aiAssistant.getApplyCodeDiffButtons()).toHaveCount(1);
|
||||
|
||||
await n8n.aiAssistant.getApplyCodeDiffButtons().first().click();
|
||||
|
||||
await expect(n8n.aiAssistant.getApplyCodeDiffButtons()).toHaveCount(0);
|
||||
await expect(n8n.aiAssistant.getUndoReplaceCodeButtons()).toHaveCount(1);
|
||||
await expect(n8n.aiAssistant.getCodeReplacedMessage()).toBeVisible();
|
||||
await expect(n8n.ndv.getCodeEditor()).toContainText('item.json.myNewField = 1');
|
||||
|
||||
await n8n.aiAssistant.getUndoReplaceCodeButtons().first().click();
|
||||
|
||||
await expect(n8n.aiAssistant.getApplyCodeDiffButtons()).toHaveCount(1);
|
||||
await expect(n8n.aiAssistant.getCodeReplacedMessage()).toHaveCount(0);
|
||||
expect(applySuggestionCalls).toBe(1);
|
||||
await expect(n8n.ndv.getCodeEditor()).toContainText('item.json.myNewField = 1aaa');
|
||||
|
||||
await n8n.aiAssistant.getApplyCodeDiffButtons().first().click();
|
||||
|
||||
await expect(n8n.ndv.getCodeEditor()).toContainText('item.json.myNewField = 1');
|
||||
});
|
||||
|
||||
test('should ignore node execution success and error messages after the node run successfully once', async ({
|
||||
n8n,
|
||||
setupRequirements,
|
||||
}) => {
|
||||
await setupRequirements(aiEnabledWorkflowBaseRequirements);
|
||||
|
||||
let chatRequestCount = 0;
|
||||
await n8n.page.route('**/rest/ai/chat', async (route) => {
|
||||
chatRequestCount += 1;
|
||||
const response =
|
||||
chatRequestCount === 1 ? codeDiffSuggestionResponse : nodeExecutionSucceededResponse;
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(response),
|
||||
});
|
||||
});
|
||||
|
||||
await n8n.canvas.openNode('Code');
|
||||
await n8n.ndv.execute();
|
||||
await n8n.aiAssistant.getNodeErrorViewAssistantButton().click();
|
||||
|
||||
await n8n.ndv
|
||||
.getCodeEditor()
|
||||
.fill(
|
||||
"// Loop over input items and add a new field called 'myNewField' to the JSON of each one\nfor (const item of $input.all()) {\n item.json.myNewField = 1;\n}\n\nreturn $input.all();",
|
||||
);
|
||||
|
||||
await n8n.ndv.execute();
|
||||
|
||||
await n8n.ndv
|
||||
.getCodeEditor()
|
||||
.fill(
|
||||
"// Loop over input items and add a new field called 'myNewField' to the JSON of each one\nfor (const item of $input.all()) {\n item.json.myNewField = 1aaaa!;\n}\n\nreturn $input.all();",
|
||||
);
|
||||
|
||||
await n8n.ndv.execute();
|
||||
|
||||
await expect(n8n.aiAssistant.getChatMessagesAssistant().nth(2)).toContainText(
|
||||
'Code node ran successfully, did my solution help resolve your issue?',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,147 @@
|
||||
import {
|
||||
aiEnabledRequirements,
|
||||
aiEnabledWithSimpleChatRequirements,
|
||||
} from '../../../config/ai-assistant-fixtures';
|
||||
import {
|
||||
GMAIL_NODE_NAME,
|
||||
MANUAL_TRIGGER_NODE_NAME,
|
||||
SCHEDULE_TRIGGER_NODE_NAME,
|
||||
} from '../../../config/constants';
|
||||
import { test, expect } from '../../../fixtures/base';
|
||||
|
||||
test.describe('AI Assistant::enabled', () => {
|
||||
test.describe('Credential Help', () => {
|
||||
test('should start credential help from node credential', async ({
|
||||
n8n,
|
||||
setupRequirements,
|
||||
}) => {
|
||||
await setupRequirements(aiEnabledWithSimpleChatRequirements);
|
||||
await n8n.page.goto('/workflow/new');
|
||||
|
||||
await n8n.canvas.addInitialNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
await n8n.ndv.clickBackToCanvasButton();
|
||||
await n8n.canvas.addNode(GMAIL_NODE_NAME, { action: 'Get many messages', closeNDV: false });
|
||||
|
||||
await n8n.ndv.clickCreateNewCredential();
|
||||
|
||||
await expect(n8n.canvas.credentialModal.getModal()).toBeVisible();
|
||||
|
||||
const assistantButton = n8n.aiAssistant.getCredentialEditAssistantButton().locator('button');
|
||||
await expect(assistantButton).toBeVisible();
|
||||
await assistantButton.click();
|
||||
|
||||
await expect(n8n.aiAssistant.getChatMessagesUser()).toHaveCount(1);
|
||||
await expect(n8n.aiAssistant.getChatMessagesUser().first()).toContainText(
|
||||
'How do I set up the credentials for Gmail OAuth2 API?',
|
||||
);
|
||||
await expect(n8n.aiAssistant.getChatMessagesAssistant().first()).toContainText(
|
||||
'Hey, this is an assistant message',
|
||||
);
|
||||
await expect(assistantButton).toBeDisabled();
|
||||
});
|
||||
|
||||
test('should start credential help from credential list', async ({
|
||||
n8n,
|
||||
setupRequirements,
|
||||
}) => {
|
||||
await setupRequirements(aiEnabledWithSimpleChatRequirements);
|
||||
|
||||
await n8n.navigate.toCredentials();
|
||||
|
||||
await n8n.workflows.addResource.credential();
|
||||
await n8n.credentials.selectCredentialType('Notion API');
|
||||
|
||||
const assistantButton = n8n.aiAssistant.getCredentialEditAssistantButton().locator('button');
|
||||
await expect(assistantButton).toBeVisible();
|
||||
await assistantButton.click();
|
||||
|
||||
await expect(n8n.aiAssistant.getChatMessagesUser()).toHaveCount(1);
|
||||
await expect(n8n.aiAssistant.getChatMessagesUser().first()).toContainText(
|
||||
'How do I set up the credentials for Notion API?',
|
||||
);
|
||||
await expect(n8n.aiAssistant.getChatMessagesAssistant().first()).toContainText(
|
||||
'Hey, this is an assistant message',
|
||||
);
|
||||
await expect(assistantButton).toBeDisabled();
|
||||
});
|
||||
|
||||
test('should not show assistant button if click to connect', async ({
|
||||
n8n,
|
||||
setupRequirements,
|
||||
}) => {
|
||||
await setupRequirements(aiEnabledRequirements);
|
||||
|
||||
await n8n.page.route('**/types/credentials.json', async (route) => {
|
||||
const response = await route.fetch();
|
||||
const credentials = (await response.json()) as Array<
|
||||
{ name?: string } & Record<string, unknown>
|
||||
>;
|
||||
const index = credentials.findIndex((c) => c.name === 'slackOAuth2Api');
|
||||
if (index >= 0) {
|
||||
credentials[index] = {
|
||||
...credentials[index],
|
||||
__overwrittenProperties: ['clientId', 'clientSecret'],
|
||||
};
|
||||
}
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(credentials),
|
||||
});
|
||||
});
|
||||
|
||||
await n8n.page.goto('/workflow/new');
|
||||
await n8n.canvas.addInitialNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||
await n8n.canvas.addNode('Slack', { action: 'Get a channel' });
|
||||
|
||||
await n8n.ndv.clickCreateNewCredential();
|
||||
|
||||
const authOptions = n8n.canvas.credentialModal.getAuthTypeRadioButtons();
|
||||
await authOptions.first().click();
|
||||
|
||||
await expect(n8n.canvas.credentialModal.oauthConnectButton).toHaveCount(1);
|
||||
await expect(n8n.canvas.credentialModal.getCredentialInputs()).toHaveCount(2);
|
||||
await expect(n8n.aiAssistant.getCredentialEditAssistantButton()).toHaveCount(0);
|
||||
|
||||
await authOptions.nth(1).click();
|
||||
await expect(n8n.canvas.credentialModal.getCredentialInputs()).toHaveCount(4);
|
||||
await expect(n8n.aiAssistant.getCredentialEditAssistantButton()).toHaveCount(1);
|
||||
});
|
||||
|
||||
test('should not show assistant button when click to connect with some fields', async ({
|
||||
n8n,
|
||||
setupRequirements,
|
||||
}) => {
|
||||
await setupRequirements(aiEnabledRequirements);
|
||||
|
||||
await n8n.page.route('**/types/credentials.json', async (route) => {
|
||||
const response = await route.fetch();
|
||||
const credentials = (await response.json()) as Array<
|
||||
{ name?: string } & Record<string, unknown>
|
||||
>;
|
||||
const index = credentials.findIndex((c) => c.name === 'microsoftOutlookOAuth2Api');
|
||||
if (index >= 0) {
|
||||
credentials[index] = {
|
||||
...credentials[index],
|
||||
__overwrittenProperties: ['authUrl', 'accessTokenUrl', 'clientId', 'clientSecret'],
|
||||
};
|
||||
}
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(credentials),
|
||||
});
|
||||
});
|
||||
|
||||
await n8n.page.goto('/workflow/new');
|
||||
await n8n.canvas.addInitialNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
|
||||
await n8n.canvas.addNode('Microsoft Outlook', { action: 'Get a calendar' });
|
||||
|
||||
await n8n.ndv.clickCreateNewCredential();
|
||||
|
||||
await expect(n8n.canvas.credentialModal.oauthConnectButton).toHaveCount(1);
|
||||
await expect(n8n.canvas.credentialModal.getCredentialInputs()).toHaveCount(2);
|
||||
await expect(n8n.aiAssistant.getCredentialEditAssistantButton()).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,114 @@
|
||||
import {
|
||||
simpleAssistantResponse,
|
||||
aiEnabledWithCodeSnippetRequirements,
|
||||
aiEnabledWithHttpWorkflowRequirements,
|
||||
} from '../../../config/ai-assistant-fixtures';
|
||||
import { HTTP_REQUEST_NODE_NAME } from '../../../config/constants';
|
||||
import { test, expect } from '../../../fixtures/base';
|
||||
|
||||
type ChatRequestBody = {
|
||||
payload?: {
|
||||
type?: string;
|
||||
text?: string;
|
||||
question?: string;
|
||||
context?: Record<string, unknown>;
|
||||
};
|
||||
};
|
||||
|
||||
test.describe('AI Assistant::enabled', () => {
|
||||
test.describe('Support Chat', () => {
|
||||
test('assistant returns code snippet', async ({ n8n, setupRequirements }) => {
|
||||
await setupRequirements(aiEnabledWithCodeSnippetRequirements);
|
||||
await n8n.page.goto('/workflow/new');
|
||||
|
||||
await expect(n8n.aiAssistant.getAskAssistantCanvasActionButton()).toBeVisible();
|
||||
await n8n.aiAssistant.getAskAssistantCanvasActionButton().click();
|
||||
|
||||
await expect(n8n.aiAssistant.getAskAssistantChat()).toBeVisible();
|
||||
|
||||
await n8n.aiAssistant.sendMessage('Show me an expression');
|
||||
|
||||
await expect(n8n.aiAssistant.getChatMessagesAll()).toHaveCount(3);
|
||||
await expect(n8n.aiAssistant.getChatMessagesUser().first()).toContainText(
|
||||
'Show me an expression',
|
||||
);
|
||||
await expect(n8n.aiAssistant.getChatMessagesAssistant().first()).toContainText(
|
||||
'To use expressions in n8n, follow these steps:',
|
||||
);
|
||||
await expect(n8n.aiAssistant.getChatMessagesAssistant().first()).toContainText('New York');
|
||||
await expect(n8n.aiAssistant.getCodeSnippet()).toHaveText('{{$json.body.city}}');
|
||||
});
|
||||
|
||||
test('should send current context to support chat', async ({ n8n, setupRequirements }) => {
|
||||
await setupRequirements(aiEnabledWithHttpWorkflowRequirements);
|
||||
|
||||
const chatRequests: ChatRequestBody[] = [];
|
||||
await n8n.page.route('**/rest/ai/chat', async (route) => {
|
||||
const body = route.request().postDataJSON() as ChatRequestBody;
|
||||
chatRequests.push(body);
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(simpleAssistantResponse),
|
||||
});
|
||||
});
|
||||
|
||||
await n8n.aiAssistant.getAskAssistantCanvasActionButton().click();
|
||||
await n8n.aiAssistant.sendMessage('What is wrong with this workflow?');
|
||||
|
||||
const supportRequest = chatRequests.find(
|
||||
(request) => request.payload?.question === 'What is wrong with this workflow?',
|
||||
);
|
||||
expect(supportRequest).toBeDefined();
|
||||
const supportContext = supportRequest?.payload?.context;
|
||||
expect(supportContext).toBeDefined();
|
||||
expect(supportContext?.currentView).toBeDefined();
|
||||
expect(supportContext?.currentWorkflow).toBeDefined();
|
||||
});
|
||||
|
||||
test('should not send workflow context if nothing changed', async ({
|
||||
n8n,
|
||||
setupRequirements,
|
||||
}) => {
|
||||
await setupRequirements(aiEnabledWithHttpWorkflowRequirements);
|
||||
|
||||
const chatRequests: ChatRequestBody[] = [];
|
||||
await n8n.page.route('**/rest/ai/chat', async (route) => {
|
||||
const body = route.request().postDataJSON() as ChatRequestBody;
|
||||
chatRequests.push(body);
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(simpleAssistantResponse),
|
||||
});
|
||||
});
|
||||
|
||||
await n8n.aiAssistant.getAskAssistantCanvasActionButton().click();
|
||||
|
||||
await n8n.aiAssistant.sendMessage('What is wrong with this workflow?', 'enter-key');
|
||||
// Wait for message to be processed
|
||||
await expect(n8n.aiAssistant.getChatMessagesAssistant()).toHaveCount(1);
|
||||
|
||||
await n8n.aiAssistant.sendMessage('And now?', 'enter-key');
|
||||
await expect(n8n.aiAssistant.getChatMessagesAssistant()).toHaveCount(2);
|
||||
const secondRequest = chatRequests.find((request) => request.payload?.text === 'And now?');
|
||||
const secondContext = secondRequest?.payload?.context;
|
||||
expect(secondContext?.currentWorkflow).toBeUndefined();
|
||||
|
||||
await n8n.canvas.openNode(HTTP_REQUEST_NODE_NAME);
|
||||
await n8n.ndv.setParameterInputValue('url', 'https://example.com');
|
||||
await n8n.ndv.close();
|
||||
await n8n.canvas.clickExecuteWorkflowButton();
|
||||
|
||||
await n8n.aiAssistant.sendMessage('What about now?', 'enter-key');
|
||||
await expect(n8n.aiAssistant.getChatMessagesAssistant()).toHaveCount(3);
|
||||
|
||||
const thirdRequest = chatRequests.find(
|
||||
(request) => request.payload?.text === 'What about now?',
|
||||
);
|
||||
const thirdContext = thirdRequest?.payload?.context;
|
||||
expect(thirdContext?.currentWorkflow).toBeTruthy();
|
||||
expect(thirdContext?.executionData).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { test, expect } from '../../fixtures/base';
|
||||
import { test, expect } from '../../../fixtures/base';
|
||||
|
||||
test.describe('Chat session ID reset', () => {
|
||||
test.beforeEach(async ({ n8n }) => {
|
||||
@@ -1,4 +1,4 @@
|
||||
import { expect, test } from '../../fixtures/base';
|
||||
import { expect, test } from '../../../fixtures/base';
|
||||
|
||||
test.use({
|
||||
addContainerCapability: {
|
||||
244
packages/testing/playwright/tests/ui/ai/langchain-agents.spec.ts
Normal file
244
packages/testing/playwright/tests/ui/ai/langchain-agents.spec.ts
Normal file
@@ -0,0 +1,244 @@
|
||||
import {
|
||||
AGENT_NODE_NAME,
|
||||
EDIT_FIELDS_SET_NODE_NAME,
|
||||
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
|
||||
AI_MEMORY_WINDOW_BUFFER_MEMORY_NODE_NAME,
|
||||
AI_TOOL_CALCULATOR_NODE_NAME,
|
||||
AI_OUTPUT_PARSER_AUTO_FIXING_NODE_NAME,
|
||||
AI_TOOL_CODE_NODE_NAME,
|
||||
AI_TOOL_WIKIPEDIA_NODE_NAME,
|
||||
SCHEDULE_TRIGGER_NODE_NAME,
|
||||
} from '../../../config/constants';
|
||||
import { test, expect } from '../../../fixtures/base';
|
||||
import type { n8nPage } from '../../../pages/n8nPage';
|
||||
|
||||
// Helper functions for common operations
|
||||
async function addOpenAILanguageModelWithCredentials(
|
||||
n8n: n8nPage,
|
||||
parentNode: string,
|
||||
options: { exactMatch?: boolean; closeNDV?: boolean } = { exactMatch: true, closeNDV: false },
|
||||
) {
|
||||
await n8n.canvas.addSupplementalNodeToParent(
|
||||
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
|
||||
'ai_languageModel',
|
||||
parentNode,
|
||||
options,
|
||||
);
|
||||
|
||||
await n8n.credentialsComposer.createFromNdv({
|
||||
apiKey: 'abcd',
|
||||
});
|
||||
await n8n.ndv.clickBackToCanvasButton();
|
||||
}
|
||||
|
||||
async function waitForWorkflowSuccess(n8n: n8nPage, timeout = 3000) {
|
||||
await n8n.notifications.waitForNotificationAndClose('Workflow executed successfully', {
|
||||
timeout,
|
||||
});
|
||||
}
|
||||
|
||||
async function executeChatAndWaitForResponse(n8n: n8nPage, message: string) {
|
||||
await n8n.canvas.logsPanel.sendManualChatMessage(message);
|
||||
await waitForWorkflowSuccess(n8n);
|
||||
}
|
||||
|
||||
async function verifyChatMessages(n8n: n8nPage, expectedCount: number, inputMessage?: string) {
|
||||
const messages = n8n.canvas.getManualChatMessages();
|
||||
await expect(messages).toHaveCount(expectedCount);
|
||||
if (inputMessage) {
|
||||
await expect(messages.first()).toContainText(inputMessage);
|
||||
}
|
||||
await expect(messages.last()).toBeVisible();
|
||||
return messages;
|
||||
}
|
||||
|
||||
async function verifyLogsPanelEntries(n8n: n8nPage, expectedEntries: string[]) {
|
||||
await expect(n8n.canvas.logsPanel.getLogEntries().first()).toBeVisible();
|
||||
await expect(n8n.canvas.logsPanel.getLogEntries()).toHaveCount(expectedEntries.length);
|
||||
for (let i = 0; i < expectedEntries.length; i++) {
|
||||
await expect(n8n.canvas.logsPanel.getLogEntries().nth(i)).toHaveText(expectedEntries[i]);
|
||||
}
|
||||
}
|
||||
|
||||
async function setupBasicAgentWorkflow(n8n: n8nPage, additionalNodes: string[] = []) {
|
||||
await n8n.canvas.addNode(AGENT_NODE_NAME, { closeNDV: true });
|
||||
|
||||
// Add additional nodes if specified
|
||||
for (const nodeName of additionalNodes) {
|
||||
await n8n.canvas.addSupplementalNodeToParent(nodeName, 'ai_tool', AGENT_NODE_NAME, {
|
||||
closeNDV: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Always add OpenAI Language Model
|
||||
await addOpenAILanguageModelWithCredentials(n8n, AGENT_NODE_NAME);
|
||||
}
|
||||
|
||||
test.use({
|
||||
addContainerCapability: {
|
||||
proxyServerEnabled: true,
|
||||
},
|
||||
});
|
||||
test.describe('Langchain Integration @capability:proxy', () => {
|
||||
test.beforeEach(async ({ n8n, proxyServer }) => {
|
||||
await proxyServer.clearAllExpectations();
|
||||
await proxyServer.loadExpectations('langchain');
|
||||
await n8n.canvas.openNewWorkflow();
|
||||
});
|
||||
|
||||
test.describe('Workflow Execution Behavior', () => {
|
||||
test('should not open chat modal', async ({ n8n }) => {
|
||||
await n8n.canvas.addNode(EDIT_FIELDS_SET_NODE_NAME, { closeNDV: true });
|
||||
|
||||
await n8n.canvas.addNode(AGENT_NODE_NAME, { closeNDV: true });
|
||||
|
||||
await n8n.canvas.addSupplementalNodeToParent(
|
||||
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
|
||||
'ai_languageModel',
|
||||
AGENT_NODE_NAME,
|
||||
{ exactMatch: true, closeNDV: true },
|
||||
);
|
||||
|
||||
await n8n.canvas.clickExecuteWorkflowButton();
|
||||
await expect(n8n.canvas.getManualChatModal()).toBeHidden();
|
||||
});
|
||||
|
||||
test('should remove test workflow button', async ({ n8n }) => {
|
||||
await n8n.canvas.addNode(SCHEDULE_TRIGGER_NODE_NAME, { closeNDV: true });
|
||||
|
||||
await n8n.canvas.addNode(EDIT_FIELDS_SET_NODE_NAME, { closeNDV: true });
|
||||
|
||||
await n8n.canvas.addNode(AGENT_NODE_NAME, { closeNDV: true });
|
||||
|
||||
await n8n.canvas.addSupplementalNodeToParent(
|
||||
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
|
||||
'ai_languageModel',
|
||||
AGENT_NODE_NAME,
|
||||
{ exactMatch: true, closeNDV: true },
|
||||
);
|
||||
|
||||
await n8n.canvas.disableNodeFromContextMenu(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
await expect(n8n.canvas.getExecuteWorkflowButton()).toBeHidden();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Node Connection and Configuration', () => {
|
||||
test('should add nodes to all Agent node input types', async ({ n8n }) => {
|
||||
const agentSubNodes = [
|
||||
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
|
||||
AI_MEMORY_WINDOW_BUFFER_MEMORY_NODE_NAME,
|
||||
AI_TOOL_CALCULATOR_NODE_NAME,
|
||||
AI_OUTPUT_PARSER_AUTO_FIXING_NODE_NAME,
|
||||
];
|
||||
await n8n.canvas.addNode(AGENT_NODE_NAME, { closeNDV: false });
|
||||
|
||||
await n8n.ndv.checkParameterCheckboxInputByName('hasOutputParser');
|
||||
await n8n.ndv.clickBackToCanvasButton();
|
||||
await n8n.canvas.addSupplementalNodeToParent(
|
||||
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
|
||||
'ai_languageModel',
|
||||
AGENT_NODE_NAME,
|
||||
{ exactMatch: true, closeNDV: true },
|
||||
);
|
||||
|
||||
await n8n.canvas.addSupplementalNodeToParent(
|
||||
AI_MEMORY_WINDOW_BUFFER_MEMORY_NODE_NAME,
|
||||
'ai_memory',
|
||||
AGENT_NODE_NAME,
|
||||
{ closeNDV: true },
|
||||
);
|
||||
|
||||
await n8n.canvas.addSupplementalNodeToParent(
|
||||
AI_TOOL_CALCULATOR_NODE_NAME,
|
||||
'ai_tool',
|
||||
AGENT_NODE_NAME,
|
||||
{ closeNDV: true },
|
||||
);
|
||||
|
||||
await n8n.canvas.addSupplementalNodeToParent(
|
||||
AI_OUTPUT_PARSER_AUTO_FIXING_NODE_NAME,
|
||||
'ai_outputParser',
|
||||
AGENT_NODE_NAME,
|
||||
{ closeNDV: true },
|
||||
);
|
||||
for (const nodeName of agentSubNodes) {
|
||||
await expect(n8n.canvas.connectionBetweenNodes(nodeName, AGENT_NODE_NAME)).toBeAttached();
|
||||
}
|
||||
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(2 + agentSubNodes.length); // Chat Trigger + Agent + 4 inputs
|
||||
});
|
||||
|
||||
test('should add multiple tool nodes to Agent node tool input type', async ({ n8n }) => {
|
||||
await n8n.canvas.addNode(AGENT_NODE_NAME, { closeNDV: true });
|
||||
|
||||
const tools = [
|
||||
AI_TOOL_CALCULATOR_NODE_NAME,
|
||||
AI_TOOL_CODE_NODE_NAME,
|
||||
AI_TOOL_CODE_NODE_NAME,
|
||||
AI_TOOL_WIKIPEDIA_NODE_NAME,
|
||||
];
|
||||
|
||||
for (const tool of tools) {
|
||||
await n8n.canvas.addSupplementalNodeToParent(tool, 'ai_tool', AGENT_NODE_NAME, {
|
||||
closeNDV: true,
|
||||
});
|
||||
await expect(n8n.canvas.connectionBetweenNodes(tool, AGENT_NODE_NAME)).toBeAttached();
|
||||
}
|
||||
|
||||
// Chat Trigger + Agent + Tools
|
||||
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(2 + tools.length);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Chat Execution and Interaction', () => {
|
||||
test('should be able to open and execute Agent node', async ({ n8n }) => {
|
||||
await setupBasicAgentWorkflow(n8n);
|
||||
|
||||
const inputMessage = 'Hello!';
|
||||
await n8n.canvas.clickManualChatButton();
|
||||
await executeChatAndWaitForResponse(n8n, inputMessage);
|
||||
|
||||
// Verify chat message appears
|
||||
await expect(n8n.canvas.getManualChatLatestBotMessage()).toBeVisible();
|
||||
});
|
||||
test('should add and use Manual Chat Trigger node together with Agent node', async ({
|
||||
n8n,
|
||||
}) => {
|
||||
await setupBasicAgentWorkflow(n8n);
|
||||
|
||||
const inputMessage = 'Hello!';
|
||||
await n8n.canvas.clickManualChatButton();
|
||||
await executeChatAndWaitForResponse(n8n, inputMessage);
|
||||
|
||||
await verifyChatMessages(n8n, 2, inputMessage);
|
||||
await verifyLogsPanelEntries(n8n, [
|
||||
'When chat message received',
|
||||
'AI Agent',
|
||||
'OpenAI Chat Model',
|
||||
]);
|
||||
|
||||
await n8n.canvas.closeManualChatModal();
|
||||
await expect(n8n.canvas.logsPanel.getLogEntries()).toBeHidden();
|
||||
await expect(n8n.canvas.getManualChatInput()).toBeHidden();
|
||||
});
|
||||
});
|
||||
|
||||
test('should keep the same session when switching tabs', async ({ n8n }) => {
|
||||
await n8n.start.fromImportedWorkflow('Test_workflow_chat_partial_execution.json');
|
||||
await n8n.canvas.clickZoomToFitButton();
|
||||
|
||||
await n8n.canvas.logsPanel.open();
|
||||
|
||||
// Send a message
|
||||
await n8n.canvas.logsPanel.sendManualChatMessage('Test');
|
||||
await expect(n8n.canvas.getManualChatLatestBotMessage()).toContainText('this_my_field');
|
||||
|
||||
await n8n.canvas.clickExecutionsTab();
|
||||
|
||||
await n8n.canvas.clickEditorTab();
|
||||
await expect(n8n.canvas.getManualChatLatestBotMessage()).toContainText('this_my_field');
|
||||
|
||||
// Refresh session
|
||||
await n8n.page.getByTestId('refresh-session-button').click();
|
||||
await expect(n8n.canvas.getManualChatMessages()).not.toBeAttached();
|
||||
});
|
||||
});
|
||||
119
packages/testing/playwright/tests/ui/ai/langchain-chains.spec.ts
Normal file
119
packages/testing/playwright/tests/ui/ai/langchain-chains.spec.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import {
|
||||
AGENT_NODE_NAME,
|
||||
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
|
||||
BASIC_LLM_CHAIN_NODE_NAME,
|
||||
CHAT_TRIGGER_NODE_DISPLAY_NAME,
|
||||
MANUAL_CHAT_TRIGGER_NODE_NAME,
|
||||
} from '../../../config/constants';
|
||||
import { test, expect } from '../../../fixtures/base';
|
||||
import type { n8nPage } from '../../../pages/n8nPage';
|
||||
|
||||
// Helper functions for common operations
|
||||
async function addOpenAILanguageModelWithCredentials(
|
||||
n8n: n8nPage,
|
||||
parentNode: string,
|
||||
options: { exactMatch?: boolean; closeNDV?: boolean } = { exactMatch: true, closeNDV: false },
|
||||
) {
|
||||
await n8n.canvas.addSupplementalNodeToParent(
|
||||
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
|
||||
'ai_languageModel',
|
||||
parentNode,
|
||||
options,
|
||||
);
|
||||
|
||||
await n8n.credentialsComposer.createFromNdv({
|
||||
apiKey: 'abcd',
|
||||
});
|
||||
await n8n.ndv.clickBackToCanvasButton();
|
||||
}
|
||||
|
||||
async function waitForWorkflowSuccess(n8n: n8nPage, timeout = 3000) {
|
||||
await n8n.notifications.waitForNotificationAndClose('Workflow executed successfully', {
|
||||
timeout,
|
||||
});
|
||||
}
|
||||
|
||||
async function executeChatAndWaitForResponse(n8n: n8nPage, message: string) {
|
||||
await n8n.canvas.logsPanel.sendManualChatMessage(message);
|
||||
await waitForWorkflowSuccess(n8n);
|
||||
}
|
||||
|
||||
test.use({
|
||||
addContainerCapability: {
|
||||
proxyServerEnabled: true,
|
||||
},
|
||||
});
|
||||
test.describe('Langchain Integration @capability:proxy', () => {
|
||||
test.beforeEach(async ({ n8n, proxyServer }) => {
|
||||
await proxyServer.clearAllExpectations();
|
||||
await proxyServer.loadExpectations('langchain');
|
||||
await n8n.canvas.openNewWorkflow();
|
||||
});
|
||||
|
||||
test.describe('Auto-add Behavior', () => {
|
||||
test('should auto-add chat trigger and basic LLM chain when adding LLM node', async ({
|
||||
n8n,
|
||||
}) => {
|
||||
await n8n.canvas.addNode(AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, { closeNDV: true });
|
||||
|
||||
await expect(
|
||||
n8n.canvas.connectionBetweenNodes(
|
||||
CHAT_TRIGGER_NODE_DISPLAY_NAME,
|
||||
BASIC_LLM_CHAIN_NODE_NAME,
|
||||
),
|
||||
).toBeAttached();
|
||||
|
||||
await expect(
|
||||
n8n.canvas.connectionBetweenNodes(
|
||||
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
|
||||
BASIC_LLM_CHAIN_NODE_NAME,
|
||||
),
|
||||
).toBeAttached();
|
||||
|
||||
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(3);
|
||||
});
|
||||
|
||||
test('should not auto-add nodes if AI nodes are already present', async ({ n8n }) => {
|
||||
await n8n.canvas.addNode(AGENT_NODE_NAME, { closeNDV: true });
|
||||
|
||||
await n8n.canvas.addNode(AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, { closeNDV: true });
|
||||
|
||||
await expect(
|
||||
n8n.canvas.connectionBetweenNodes(CHAT_TRIGGER_NODE_DISPLAY_NAME, AGENT_NODE_NAME),
|
||||
).toBeAttached();
|
||||
|
||||
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(3);
|
||||
});
|
||||
|
||||
test('should not auto-add nodes if ChatTrigger is already present', async ({ n8n }) => {
|
||||
await n8n.canvas.addNode(MANUAL_CHAT_TRIGGER_NODE_NAME, { closeNDV: true });
|
||||
|
||||
await n8n.canvas.addNode(AGENT_NODE_NAME, { closeNDV: true });
|
||||
|
||||
await n8n.canvas.addNode(AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, { closeNDV: true });
|
||||
|
||||
await expect(
|
||||
n8n.canvas.connectionBetweenNodes(CHAT_TRIGGER_NODE_DISPLAY_NAME, AGENT_NODE_NAME),
|
||||
).toBeAttached();
|
||||
|
||||
await expect(n8n.canvas.getCanvasNodes()).toHaveCount(3);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Chat Execution and Interaction', () => {
|
||||
test('should be able to open and execute Basic LLM Chain node', async ({ n8n }) => {
|
||||
await n8n.canvas.addNode(BASIC_LLM_CHAIN_NODE_NAME, { closeNDV: true });
|
||||
|
||||
await addOpenAILanguageModelWithCredentials(n8n, BASIC_LLM_CHAIN_NODE_NAME);
|
||||
|
||||
await n8n.canvas.openNode(BASIC_LLM_CHAIN_NODE_NAME);
|
||||
const inputMessage = 'Hello!';
|
||||
|
||||
await n8n.ndv.execute();
|
||||
await executeChatAndWaitForResponse(n8n, inputMessage);
|
||||
|
||||
// Verify chat message appears
|
||||
await expect(n8n.canvas.getManualChatLatestBotMessage()).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
178
packages/testing/playwright/tests/ui/ai/langchain-memory.spec.ts
Normal file
178
packages/testing/playwright/tests/ui/ai/langchain-memory.spec.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
import {
|
||||
AGENT_NODE_NAME,
|
||||
AI_TOOL_CALCULATOR_NODE_NAME,
|
||||
AI_MEMORY_POSTGRES_NODE_NAME,
|
||||
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
|
||||
MANUAL_CHAT_TRIGGER_NODE_NAME,
|
||||
} from '../../../config/constants';
|
||||
import { test, expect } from '../../../fixtures/base';
|
||||
import type { n8nPage } from '../../../pages/n8nPage';
|
||||
|
||||
// Helper functions for common operations
|
||||
async function addOpenAILanguageModelWithCredentials(
|
||||
n8n: n8nPage,
|
||||
parentNode: string,
|
||||
options: { exactMatch?: boolean; closeNDV?: boolean } = { exactMatch: true, closeNDV: false },
|
||||
) {
|
||||
await n8n.canvas.addSupplementalNodeToParent(
|
||||
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
|
||||
'ai_languageModel',
|
||||
parentNode,
|
||||
options,
|
||||
);
|
||||
|
||||
await n8n.credentialsComposer.createFromNdv({
|
||||
apiKey: 'abcd',
|
||||
});
|
||||
await n8n.ndv.clickBackToCanvasButton();
|
||||
}
|
||||
|
||||
async function waitForWorkflowSuccess(n8n: n8nPage, timeout = 3000) {
|
||||
await n8n.notifications.waitForNotificationAndClose('Workflow executed successfully', {
|
||||
timeout,
|
||||
});
|
||||
}
|
||||
|
||||
async function executeChatAndWaitForResponse(n8n: n8nPage, message: string) {
|
||||
await n8n.canvas.logsPanel.sendManualChatMessage(message);
|
||||
await waitForWorkflowSuccess(n8n);
|
||||
}
|
||||
|
||||
async function verifyChatMessages(n8n: n8nPage, expectedCount: number, inputMessage?: string) {
|
||||
const messages = n8n.canvas.getManualChatMessages();
|
||||
await expect(messages).toHaveCount(expectedCount);
|
||||
if (inputMessage) {
|
||||
await expect(messages.first()).toContainText(inputMessage);
|
||||
}
|
||||
await expect(messages.last()).toBeVisible();
|
||||
return messages;
|
||||
}
|
||||
|
||||
test.use({
|
||||
addContainerCapability: {
|
||||
proxyServerEnabled: true,
|
||||
},
|
||||
});
|
||||
test.describe('Langchain Integration @capability:proxy', () => {
|
||||
test.beforeEach(async ({ n8n, proxyServer }) => {
|
||||
await proxyServer.clearAllExpectations();
|
||||
await proxyServer.loadExpectations('langchain');
|
||||
await n8n.canvas.openNewWorkflow();
|
||||
});
|
||||
|
||||
// Create a ticket for this for AI team to fix
|
||||
// eslint-disable-next-line playwright/no-skipped-test
|
||||
test.skip('Error Handling and Logs Display', () => {
|
||||
// Helper function to set up the agent workflow with Postgres error configuration
|
||||
async function setupAgentWorkflowWithPostgresError(n8n: n8nPage) {
|
||||
await n8n.canvas.addNode(AGENT_NODE_NAME, { closeNDV: true });
|
||||
|
||||
// Add Calculator Tool (required for OpenAI model)
|
||||
await n8n.canvas.addSupplementalNodeToParent(
|
||||
AI_TOOL_CALCULATOR_NODE_NAME,
|
||||
'ai_tool',
|
||||
AGENT_NODE_NAME,
|
||||
{ closeNDV: true },
|
||||
);
|
||||
|
||||
// Add and configure Postgres Memory
|
||||
await n8n.canvas.addSupplementalNodeToParent(
|
||||
AI_MEMORY_POSTGRES_NODE_NAME,
|
||||
'ai_memory',
|
||||
AGENT_NODE_NAME,
|
||||
{ closeNDV: false },
|
||||
);
|
||||
|
||||
await n8n.credentialsComposer.createFromNdv({
|
||||
password: 'testtesttest',
|
||||
});
|
||||
|
||||
await n8n.ndv.getParameterInput('sessionIdType').click();
|
||||
await n8n.page.getByRole('option', { name: 'Define below' }).click();
|
||||
await n8n.ndv.getParameterInput('sessionKey').locator('input').fill('asdasd');
|
||||
await n8n.ndv.clickBackToCanvasButton();
|
||||
|
||||
// Add and configure OpenAI Language Model
|
||||
await addOpenAILanguageModelWithCredentials(n8n, AGENT_NODE_NAME);
|
||||
|
||||
await n8n.canvas.clickZoomToFitButton();
|
||||
}
|
||||
|
||||
// Helper function to assert logs tab is active
|
||||
async function assertLogsTabIsActive(n8n: n8nPage) {
|
||||
await expect(n8n.ndv.getOutputDataContainer()).toBeVisible();
|
||||
await expect(n8n.ndv.getAiOutputModeToggle()).toBeVisible();
|
||||
|
||||
const radioButtons = n8n.ndv.getAiOutputModeToggle().locator('[role="radio"]');
|
||||
await expect(radioButtons).toHaveCount(2);
|
||||
await expect(radioButtons.nth(1)).toHaveAttribute('aria-checked', 'true');
|
||||
}
|
||||
|
||||
// Helper function to assert error message is visible
|
||||
async function assertErrorMessageVisible(n8n: n8nPage) {
|
||||
await expect(
|
||||
n8n.ndv.getOutputPanel().getByTestId('node-error-message').first(),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
n8n.ndv.getOutputPanel().getByTestId('node-error-message').first(),
|
||||
).toContainText('Error in sub-node');
|
||||
}
|
||||
|
||||
test('should open logs tab by default when there was an error', async ({ n8n }) => {
|
||||
await setupAgentWorkflowWithPostgresError(n8n);
|
||||
|
||||
const inputMessage = 'Test the code tool';
|
||||
|
||||
// Execute workflow with chat trigger
|
||||
await n8n.canvas.clickManualChatButton();
|
||||
await executeChatAndWaitForResponse(n8n, inputMessage);
|
||||
|
||||
// Check that messages and logs are displayed
|
||||
const messages = await verifyChatMessages(n8n, 2, inputMessage);
|
||||
await expect(messages.last()).toContainText(
|
||||
'[ERROR: The service refused the connection - perhaps it is offline]',
|
||||
);
|
||||
|
||||
await expect(n8n.canvas.logsPanel.getLogEntries().first()).toBeVisible();
|
||||
await expect(n8n.canvas.logsPanel.getLogEntries()).toHaveCount(3);
|
||||
await expect(n8n.canvas.logsPanel.getSelectedLogEntry()).toHaveText('AI Agent');
|
||||
await expect(n8n.canvas.logsPanel.outputPanel.get()).toContainText(
|
||||
AI_MEMORY_POSTGRES_NODE_NAME,
|
||||
);
|
||||
|
||||
await n8n.canvas.closeManualChatModal();
|
||||
|
||||
// Open the AI Agent node to see the logs
|
||||
await n8n.canvas.openNode(AGENT_NODE_NAME);
|
||||
|
||||
// Assert that logs tab is active and error is displayed
|
||||
await assertLogsTabIsActive(n8n);
|
||||
await assertErrorMessageVisible(n8n);
|
||||
});
|
||||
|
||||
test('should switch to logs tab on error, when NDV is already opened', async ({ n8n }) => {
|
||||
// Remove the auto-added chat trigger
|
||||
await n8n.canvas.addNode(MANUAL_CHAT_TRIGGER_NODE_NAME, { closeNDV: false });
|
||||
|
||||
// Set manual trigger to output standard pinned data
|
||||
await n8n.ndv.getEditPinnedDataButton().click();
|
||||
await n8n.ndv.savePinnedData();
|
||||
await n8n.ndv.close();
|
||||
|
||||
// Set up the same workflow components but with manual trigger
|
||||
await setupAgentWorkflowWithPostgresError(n8n);
|
||||
|
||||
// Open the AI Agent node
|
||||
await n8n.canvas.openNode(AGENT_NODE_NAME);
|
||||
await n8n.ndv.getParameterInput('promptType').click();
|
||||
await n8n.page.getByRole('option', { name: 'Define below' }).click();
|
||||
await n8n.ndv.getParameterInput('text').locator('textarea').fill('Some text');
|
||||
await n8n.ndv.execute();
|
||||
await waitForWorkflowSuccess(n8n);
|
||||
|
||||
// Assert that logs tab is active and error is displayed
|
||||
await assertLogsTabIsActive(n8n);
|
||||
await assertErrorMessageVisible(n8n);
|
||||
});
|
||||
});
|
||||
});
|
||||
111
packages/testing/playwright/tests/ui/ai/langchain-tools.spec.ts
Normal file
111
packages/testing/playwright/tests/ui/ai/langchain-tools.spec.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import {
|
||||
AGENT_NODE_NAME,
|
||||
AI_TOOL_CALCULATOR_NODE_NAME,
|
||||
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
|
||||
MANUAL_CHAT_TRIGGER_NODE_NAME,
|
||||
} from '../../../config/constants';
|
||||
import { test, expect } from '../../../fixtures/base';
|
||||
import type { n8nPage } from '../../../pages/n8nPage';
|
||||
|
||||
// Helper functions for common operations
|
||||
async function addOpenAILanguageModelWithCredentials(
|
||||
n8n: n8nPage,
|
||||
parentNode: string,
|
||||
options: { exactMatch?: boolean; closeNDV?: boolean } = { exactMatch: true, closeNDV: false },
|
||||
) {
|
||||
await n8n.canvas.addSupplementalNodeToParent(
|
||||
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
|
||||
'ai_languageModel',
|
||||
parentNode,
|
||||
options,
|
||||
);
|
||||
|
||||
await n8n.credentialsComposer.createFromNdv({
|
||||
apiKey: 'abcd',
|
||||
});
|
||||
await n8n.ndv.clickBackToCanvasButton();
|
||||
}
|
||||
|
||||
async function waitForWorkflowSuccess(n8n: n8nPage, timeout = 3000) {
|
||||
await n8n.notifications.waitForNotificationAndClose('Workflow executed successfully', {
|
||||
timeout,
|
||||
});
|
||||
}
|
||||
|
||||
async function executeChatAndWaitForResponse(n8n: n8nPage, message: string) {
|
||||
await n8n.canvas.logsPanel.sendManualChatMessage(message);
|
||||
await waitForWorkflowSuccess(n8n);
|
||||
}
|
||||
|
||||
async function setupBasicAgentWorkflow(n8n: n8nPage, additionalNodes: string[] = []) {
|
||||
await n8n.canvas.addNode(AGENT_NODE_NAME, { closeNDV: true });
|
||||
|
||||
// Add additional nodes if specified
|
||||
for (const nodeName of additionalNodes) {
|
||||
await n8n.canvas.addSupplementalNodeToParent(nodeName, 'ai_tool', AGENT_NODE_NAME, {
|
||||
closeNDV: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Always add OpenAI Language Model
|
||||
await addOpenAILanguageModelWithCredentials(n8n, AGENT_NODE_NAME);
|
||||
}
|
||||
|
||||
test.use({
|
||||
addContainerCapability: {
|
||||
proxyServerEnabled: true,
|
||||
},
|
||||
});
|
||||
test.describe('Langchain Integration @capability:proxy', () => {
|
||||
test.beforeEach(async ({ n8n, proxyServer }) => {
|
||||
await proxyServer.clearAllExpectations();
|
||||
await proxyServer.loadExpectations('langchain');
|
||||
await n8n.canvas.openNewWorkflow();
|
||||
});
|
||||
|
||||
// @AI team to look at this
|
||||
// eslint-disable-next-line playwright/no-skipped-test
|
||||
test.skip('Tool Usage Notifications', () => {
|
||||
test('should show tool info notice if no existing tools were used during execution', async ({
|
||||
n8n,
|
||||
}) => {
|
||||
await setupBasicAgentWorkflow(n8n, [AI_TOOL_CALCULATOR_NODE_NAME]);
|
||||
await n8n.canvas.openNode(AGENT_NODE_NAME);
|
||||
|
||||
const inputMessage = 'Hello!';
|
||||
await n8n.ndv.execute();
|
||||
await executeChatAndWaitForResponse(n8n, inputMessage);
|
||||
|
||||
await n8n.canvas.closeManualChatModal();
|
||||
await n8n.canvas.openNode(AGENT_NODE_NAME);
|
||||
|
||||
await expect(n8n.ndv.getRunDataInfoCallout()).toBeVisible();
|
||||
});
|
||||
test('should not show tool info notice if tools were used during execution', async ({
|
||||
n8n,
|
||||
}) => {
|
||||
await n8n.canvas.addNode(MANUAL_CHAT_TRIGGER_NODE_NAME, { closeNDV: true });
|
||||
await n8n.canvas.addNode(AGENT_NODE_NAME, { closeNDV: false });
|
||||
await expect(n8n.ndv.getRunDataInfoCallout()).toBeHidden();
|
||||
await n8n.ndv.clickBackToCanvasButton();
|
||||
|
||||
await addOpenAILanguageModelWithCredentials(n8n, AGENT_NODE_NAME);
|
||||
|
||||
await n8n.canvas.addSupplementalNodeToParent(
|
||||
AI_TOOL_CALCULATOR_NODE_NAME,
|
||||
'ai_tool',
|
||||
AGENT_NODE_NAME,
|
||||
{ closeNDV: true },
|
||||
);
|
||||
|
||||
const inputMessage = 'What is 1000 * 10?';
|
||||
await n8n.canvas.clickManualChatButton();
|
||||
await executeChatAndWaitForResponse(n8n, inputMessage);
|
||||
|
||||
await n8n.canvas.closeManualChatModal();
|
||||
await n8n.canvas.openNode(AGENT_NODE_NAME);
|
||||
|
||||
await expect(n8n.ndv.getRunDataInfoCallout()).toBeHidden();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,109 @@
|
||||
import { test, expect } from '../../../fixtures/base';
|
||||
import type { n8nPage } from '../../../pages/n8nPage';
|
||||
|
||||
// Helper functions for common operations
|
||||
async function waitForWorkflowSuccess(n8n: n8nPage, timeout = 3000) {
|
||||
await n8n.notifications.waitForNotificationAndClose('Workflow executed successfully', {
|
||||
timeout,
|
||||
});
|
||||
}
|
||||
|
||||
test.use({
|
||||
addContainerCapability: {
|
||||
proxyServerEnabled: true,
|
||||
},
|
||||
});
|
||||
test.describe('Langchain Integration @capability:proxy', () => {
|
||||
test.beforeEach(async ({ n8n, proxyServer }) => {
|
||||
await proxyServer.clearAllExpectations();
|
||||
await proxyServer.loadExpectations('langchain');
|
||||
await n8n.canvas.openNewWorkflow();
|
||||
});
|
||||
|
||||
test.describe('Advanced Workflow Features', () => {
|
||||
test('should render runItems for sub-nodes and allow switching between them', async ({
|
||||
n8n,
|
||||
}) => {
|
||||
await n8n.start.fromImportedWorkflow('In_memory_vector_store_fake_embeddings.json');
|
||||
await n8n.canvas.clickZoomToFitButton();
|
||||
await n8n.canvas.deselectAll();
|
||||
|
||||
await n8n.canvas.executeNode('Populate VS');
|
||||
await waitForWorkflowSuccess(n8n);
|
||||
|
||||
const assertInputOutputTextExists = async (text: string) => {
|
||||
await expect(n8n.ndv.getOutputPanel()).toContainText(text);
|
||||
await expect(n8n.ndv.getInputPanel()).toContainText(text);
|
||||
};
|
||||
|
||||
const assertInputOutputTextNotExists = async (text: string) => {
|
||||
await expect(n8n.ndv.getOutputPanel()).not.toContainText(text);
|
||||
await expect(n8n.ndv.getInputPanel()).not.toContainText(text);
|
||||
};
|
||||
|
||||
await n8n.canvas.openNode('Character Text Splitter');
|
||||
|
||||
await expect(n8n.ndv.getOutputRunSelector()).toBeVisible();
|
||||
await expect(n8n.ndv.getInputRunSelector()).toBeVisible();
|
||||
await expect(n8n.ndv.getInputRunSelectorInput()).toHaveValue('3 of 3');
|
||||
await expect(n8n.ndv.getOutputRunSelectorInput()).toHaveValue('3 of 3');
|
||||
await assertInputOutputTextExists('Kyiv');
|
||||
await assertInputOutputTextNotExists('Berlin');
|
||||
await assertInputOutputTextNotExists('Prague');
|
||||
|
||||
await n8n.ndv.changeOutputRunSelector('2 of 3');
|
||||
await assertInputOutputTextExists('Berlin');
|
||||
await assertInputOutputTextNotExists('Kyiv');
|
||||
await assertInputOutputTextNotExists('Prague');
|
||||
|
||||
await n8n.ndv.changeOutputRunSelector('1 of 3');
|
||||
await assertInputOutputTextExists('Prague');
|
||||
await assertInputOutputTextNotExists('Berlin');
|
||||
await assertInputOutputTextNotExists('Kyiv');
|
||||
|
||||
await n8n.ndv.toggleInputRunLinking();
|
||||
await n8n.ndv.changeOutputRunSelector('2 of 3');
|
||||
await expect(n8n.ndv.getInputRunSelectorInput()).toHaveValue('1 of 3');
|
||||
await expect(n8n.ndv.getOutputRunSelectorInput()).toHaveValue('2 of 3');
|
||||
await expect(n8n.ndv.getInputPanel()).toContainText('Prague');
|
||||
await expect(n8n.ndv.getInputPanel()).not.toContainText('Berlin');
|
||||
|
||||
await expect(n8n.ndv.getOutputPanel()).toContainText('Berlin');
|
||||
await expect(n8n.ndv.getOutputPanel()).not.toContainText('Prague');
|
||||
|
||||
await n8n.ndv.toggleInputRunLinking();
|
||||
await expect(n8n.ndv.getInputRunSelectorInput()).toHaveValue('1 of 3');
|
||||
await expect(n8n.ndv.getOutputRunSelectorInput()).toHaveValue('1 of 3');
|
||||
await assertInputOutputTextExists('Prague');
|
||||
await assertInputOutputTextNotExists('Berlin');
|
||||
await assertInputOutputTextNotExists('Kyiv');
|
||||
});
|
||||
|
||||
test('should execute up to Node 1 when using partial execution', async ({ n8n }) => {
|
||||
await n8n.start.fromImportedWorkflow('Test_workflow_chat_partial_execution.json');
|
||||
await n8n.canvas.clickZoomToFitButton();
|
||||
|
||||
// Check that chat modal is not initially visible
|
||||
await expect(n8n.canvas.getManualChatModal().locator('main')).toBeHidden();
|
||||
|
||||
// Open Node 1 and execute it
|
||||
await n8n.canvas.openNode('Node 1');
|
||||
await n8n.ndv.execute();
|
||||
// Chat modal should now be visible
|
||||
await expect(n8n.canvas.getManualChatModal().locator('main')).toBeVisible();
|
||||
|
||||
// Send first message
|
||||
await n8n.canvas.logsPanel.sendManualChatMessage('Test');
|
||||
await expect(n8n.canvas.getManualChatLatestBotMessage()).toContainText('this_my_field_1');
|
||||
|
||||
// Refresh session
|
||||
await n8n.page.getByTestId('refresh-session-button').click();
|
||||
await expect(n8n.canvas.getManualChatMessages()).not.toBeAttached();
|
||||
|
||||
// Send another message
|
||||
await n8n.canvas.logsPanel.sendManualChatMessage('Another test');
|
||||
await expect(n8n.canvas.getManualChatLatestBotMessage()).toContainText('this_my_field_3');
|
||||
await expect(n8n.canvas.getManualChatLatestBotMessage()).toContainText('this_my_field_4');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { test, expect } from '../../fixtures/base';
|
||||
import { test, expect } from '../../../fixtures/base';
|
||||
|
||||
test.describe('RAG callout experiment', () => {
|
||||
test.describe('NDV callout', () => {
|
||||
@@ -1,6 +1,6 @@
|
||||
import { workflowBuilderEnabledRequirements } from '../../config/ai-builder-fixtures';
|
||||
import { test, expect } from '../../fixtures/base';
|
||||
import type { n8nPage } from '../../pages/n8nPage';
|
||||
import { workflowBuilderEnabledRequirements } from '../../../config/ai-builder-fixtures';
|
||||
import { test, expect } from '../../../fixtures/base';
|
||||
import type { n8nPage } from '../../../pages/n8nPage';
|
||||
|
||||
// Helper to open workflow builder and click a specific suggestion pill
|
||||
async function openBuilderAndClickSuggestion(n8n: n8nPage, suggestionText: string) {
|
||||
Reference in New Issue
Block a user