From 813d33372c47983798cd6ed1ea9cce8b7b45add7 Mon Sep 17 00:00:00 2001 From: Artem Sorokin <38620398+seemewalkin@users.noreply.github.com> Date: Thu, 4 Dec 2025 21:43:07 +0100 Subject: [PATCH] test: Move AI features tests to separate folder (#22727) --- .../playwright/tests/ui/30-langchain.spec.ts | 561 ---------------- .../tests/ui/45-ai-assistant.spec.ts | 618 ------------------ .../tests/ui/ai/assistant-basic.spec.ts | 288 ++++++++ .../tests/ui/ai/assistant-code-help.spec.ts | 97 +++ .../ui/ai/assistant-credential-help.spec.ts | 147 +++++ .../ui/ai/assistant-support-chat.spec.ts | 114 ++++ .../chat-session.spec.ts} | 2 +- .../tests/ui/{ => ai}/evaluations.spec.ts | 2 +- .../tests/ui/ai/langchain-agents.spec.ts | 244 +++++++ .../tests/ui/ai/langchain-chains.spec.ts | 119 ++++ .../tests/ui/ai/langchain-memory.spec.ts | 178 +++++ .../tests/ui/ai/langchain-tools.spec.ts | 111 ++++ .../ui/ai/langchain-vectorstores.spec.ts | 109 +++ .../rag-callout.spec.ts} | 2 +- .../workflow-builder.spec.ts} | 6 +- 15 files changed, 1413 insertions(+), 1185 deletions(-) delete mode 100644 packages/testing/playwright/tests/ui/30-langchain.spec.ts delete mode 100644 packages/testing/playwright/tests/ui/45-ai-assistant.spec.ts create mode 100644 packages/testing/playwright/tests/ui/ai/assistant-basic.spec.ts create mode 100644 packages/testing/playwright/tests/ui/ai/assistant-code-help.spec.ts create mode 100644 packages/testing/playwright/tests/ui/ai/assistant-credential-help.spec.ts create mode 100644 packages/testing/playwright/tests/ui/ai/assistant-support-chat.spec.ts rename packages/testing/playwright/tests/ui/{chat-session-id-reset.spec.ts => ai/chat-session.spec.ts} (97%) rename packages/testing/playwright/tests/ui/{ => ai}/evaluations.spec.ts (98%) create mode 100644 packages/testing/playwright/tests/ui/ai/langchain-agents.spec.ts create mode 100644 packages/testing/playwright/tests/ui/ai/langchain-chains.spec.ts create mode 100644 packages/testing/playwright/tests/ui/ai/langchain-memory.spec.ts create mode 100644 packages/testing/playwright/tests/ui/ai/langchain-tools.spec.ts create mode 100644 packages/testing/playwright/tests/ui/ai/langchain-vectorstores.spec.ts rename packages/testing/playwright/tests/ui/{52-rag-callout-experiment.spec.ts => ai/rag-callout.spec.ts} (95%) rename packages/testing/playwright/tests/ui/{55-workflow-builder.spec.ts => ai/workflow-builder.spec.ts} (95%) diff --git a/packages/testing/playwright/tests/ui/30-langchain.spec.ts b/packages/testing/playwright/tests/ui/30-langchain.spec.ts deleted file mode 100644 index 0629ffc1769..00000000000 --- a/packages/testing/playwright/tests/ui/30-langchain.spec.ts +++ /dev/null @@ -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(); - }); -}); diff --git a/packages/testing/playwright/tests/ui/45-ai-assistant.spec.ts b/packages/testing/playwright/tests/ui/45-ai-assistant.spec.ts deleted file mode 100644 index bffdee651d9..00000000000 --- a/packages/testing/playwright/tests/ui/45-ai-assistant.spec.ts +++ /dev/null @@ -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; - }; -}; - -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 - >; - 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 - >; - 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); - }); - }); -}); diff --git a/packages/testing/playwright/tests/ui/ai/assistant-basic.spec.ts b/packages/testing/playwright/tests/ui/ai/assistant-basic.spec.ts new file mode 100644 index 00000000000..e7c6d6c7338 --- /dev/null +++ b/packages/testing/playwright/tests/ui/ai/assistant-basic.spec.ts @@ -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; + }; +}; + +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); + }); +}); diff --git a/packages/testing/playwright/tests/ui/ai/assistant-code-help.spec.ts b/packages/testing/playwright/tests/ui/ai/assistant-code-help.spec.ts new file mode 100644 index 00000000000..18710a0a93a --- /dev/null +++ b/packages/testing/playwright/tests/ui/ai/assistant-code-help.spec.ts @@ -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?', + ); + }); + }); +}); diff --git a/packages/testing/playwright/tests/ui/ai/assistant-credential-help.spec.ts b/packages/testing/playwright/tests/ui/ai/assistant-credential-help.spec.ts new file mode 100644 index 00000000000..c3d830c5fc2 --- /dev/null +++ b/packages/testing/playwright/tests/ui/ai/assistant-credential-help.spec.ts @@ -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 + >; + 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 + >; + 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); + }); + }); +}); diff --git a/packages/testing/playwright/tests/ui/ai/assistant-support-chat.spec.ts b/packages/testing/playwright/tests/ui/ai/assistant-support-chat.spec.ts new file mode 100644 index 00000000000..ff9d50f7890 --- /dev/null +++ b/packages/testing/playwright/tests/ui/ai/assistant-support-chat.spec.ts @@ -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; + }; +}; + +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(); + }); + }); +}); diff --git a/packages/testing/playwright/tests/ui/chat-session-id-reset.spec.ts b/packages/testing/playwright/tests/ui/ai/chat-session.spec.ts similarity index 97% rename from packages/testing/playwright/tests/ui/chat-session-id-reset.spec.ts rename to packages/testing/playwright/tests/ui/ai/chat-session.spec.ts index abadbe848c0..aa77ff5d367 100644 --- a/packages/testing/playwright/tests/ui/chat-session-id-reset.spec.ts +++ b/packages/testing/playwright/tests/ui/ai/chat-session.spec.ts @@ -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 }) => { diff --git a/packages/testing/playwright/tests/ui/evaluations.spec.ts b/packages/testing/playwright/tests/ui/ai/evaluations.spec.ts similarity index 98% rename from packages/testing/playwright/tests/ui/evaluations.spec.ts rename to packages/testing/playwright/tests/ui/ai/evaluations.spec.ts index 83456021ba5..f0c0074e984 100644 --- a/packages/testing/playwright/tests/ui/evaluations.spec.ts +++ b/packages/testing/playwright/tests/ui/ai/evaluations.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from '../../fixtures/base'; +import { expect, test } from '../../../fixtures/base'; test.use({ addContainerCapability: { diff --git a/packages/testing/playwright/tests/ui/ai/langchain-agents.spec.ts b/packages/testing/playwright/tests/ui/ai/langchain-agents.spec.ts new file mode 100644 index 00000000000..4c1533e8fd8 --- /dev/null +++ b/packages/testing/playwright/tests/ui/ai/langchain-agents.spec.ts @@ -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(); + }); +}); diff --git a/packages/testing/playwright/tests/ui/ai/langchain-chains.spec.ts b/packages/testing/playwright/tests/ui/ai/langchain-chains.spec.ts new file mode 100644 index 00000000000..71d7899ca9d --- /dev/null +++ b/packages/testing/playwright/tests/ui/ai/langchain-chains.spec.ts @@ -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(); + }); + }); +}); diff --git a/packages/testing/playwright/tests/ui/ai/langchain-memory.spec.ts b/packages/testing/playwright/tests/ui/ai/langchain-memory.spec.ts new file mode 100644 index 00000000000..49e4bde048a --- /dev/null +++ b/packages/testing/playwright/tests/ui/ai/langchain-memory.spec.ts @@ -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); + }); + }); +}); diff --git a/packages/testing/playwright/tests/ui/ai/langchain-tools.spec.ts b/packages/testing/playwright/tests/ui/ai/langchain-tools.spec.ts new file mode 100644 index 00000000000..86ba1860b4b --- /dev/null +++ b/packages/testing/playwright/tests/ui/ai/langchain-tools.spec.ts @@ -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(); + }); + }); +}); diff --git a/packages/testing/playwright/tests/ui/ai/langchain-vectorstores.spec.ts b/packages/testing/playwright/tests/ui/ai/langchain-vectorstores.spec.ts new file mode 100644 index 00000000000..e1ef48947dd --- /dev/null +++ b/packages/testing/playwright/tests/ui/ai/langchain-vectorstores.spec.ts @@ -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'); + }); + }); +}); diff --git a/packages/testing/playwright/tests/ui/52-rag-callout-experiment.spec.ts b/packages/testing/playwright/tests/ui/ai/rag-callout.spec.ts similarity index 95% rename from packages/testing/playwright/tests/ui/52-rag-callout-experiment.spec.ts rename to packages/testing/playwright/tests/ui/ai/rag-callout.spec.ts index afe115d40db..fa11ec195b9 100644 --- a/packages/testing/playwright/tests/ui/52-rag-callout-experiment.spec.ts +++ b/packages/testing/playwright/tests/ui/ai/rag-callout.spec.ts @@ -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', () => { diff --git a/packages/testing/playwright/tests/ui/55-workflow-builder.spec.ts b/packages/testing/playwright/tests/ui/ai/workflow-builder.spec.ts similarity index 95% rename from packages/testing/playwright/tests/ui/55-workflow-builder.spec.ts rename to packages/testing/playwright/tests/ui/ai/workflow-builder.spec.ts index 18a051255a2..1d3772a0eba 100644 --- a/packages/testing/playwright/tests/ui/55-workflow-builder.spec.ts +++ b/packages/testing/playwright/tests/ui/ai/workflow-builder.spec.ts @@ -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) {