diff --git a/cypress/composables/modals/chat-modal.ts b/cypress/composables/modals/chat-modal.ts index 254d811a184..220c363dd17 100644 --- a/cypress/composables/modals/chat-modal.ts +++ b/cypress/composables/modals/chat-modal.ts @@ -3,7 +3,7 @@ */ export function getManualChatModal() { - return cy.getByTestId('lmChat-modal'); + return cy.getByTestId('canvas-chat'); } export function getManualChatInput() { @@ -19,11 +19,11 @@ export function getManualChatMessages() { } export function getManualChatModalCloseButton() { - return getManualChatModal().get('.el-dialog__close'); + return cy.getByTestId('workflow-chat-button'); } export function getManualChatModalLogs() { - return getManualChatModal().getByTestId('lm-chat-logs'); + return cy.getByTestId('canvas-chat-logs'); } export function getManualChatDialog() { return getManualChatModal().getByTestId('workflow-lm-chat-dialog'); diff --git a/cypress/composables/projects.ts b/cypress/composables/projects.ts index da9c6fcc656..52a28cba621 100644 --- a/cypress/composables/projects.ts +++ b/cypress/composables/projects.ts @@ -11,6 +11,7 @@ export const getAddProjectButton = () => export const getProjectTabs = () => cy.getByTestId('project-tabs').find('a'); export const getProjectTabWorkflows = () => getProjectTabs().filter('a[href$="/workflows"]'); export const getProjectTabCredentials = () => getProjectTabs().filter('a[href$="/credentials"]'); +export const getProjectTabExecutions = () => getProjectTabs().filter('a[href$="/executions"]'); export const getProjectTabSettings = () => getProjectTabs().filter('a[href$="/settings"]'); export const getProjectSettingsNameInput = () => cy.getByTestId('project-settings-name-input').find('input'); diff --git a/cypress/e2e/30-langchain.cy.ts b/cypress/e2e/30-langchain.cy.ts index 43f6e0453c1..78934c3ce5d 100644 --- a/cypress/e2e/30-langchain.cy.ts +++ b/cypress/e2e/30-langchain.cy.ts @@ -14,7 +14,6 @@ import { } from './../constants'; import { closeManualChatModal, - getManualChatDialog, getManualChatMessages, getManualChatModal, getManualChatModalLogs, @@ -168,7 +167,7 @@ describe('Langchain Integration', () => { lastNodeExecuted: BASIC_LLM_CHAIN_NODE_NAME, }); - getManualChatDialog().should('contain', outputMessage); + getManualChatMessages().should('contain', outputMessage); }); it('should be able to open and execute Agent node', () => { @@ -208,7 +207,7 @@ describe('Langchain Integration', () => { lastNodeExecuted: AGENT_NODE_NAME, }); - getManualChatDialog().should('contain', outputMessage); + getManualChatMessages().should('contain', outputMessage); }); it('should add and use Manual Chat Trigger node together with Agent node', () => { @@ -229,8 +228,6 @@ describe('Langchain Integration', () => { clickManualChatButton(); - getManualChatModalLogs().should('not.exist'); - const inputMessage = 'Hello!'; const outputMessage = 'Hi there! How can I assist you today?'; const runData = [ @@ -335,6 +332,8 @@ describe('Langchain Integration', () => { getManualChatModalLogsEntries().should('have.length', 1); closeManualChatModal(); + getManualChatModalLogs().should('not.exist'); + getManualChatModal().should('not.exist'); }); it('should auto-add chat trigger and basic LLM chain when adding LLM node', () => { diff --git a/cypress/e2e/39-projects.cy.ts b/cypress/e2e/39-projects.cy.ts index 84d062fff5b..0d4154e6462 100644 --- a/cypress/e2e/39-projects.cy.ts +++ b/cypress/e2e/39-projects.cy.ts @@ -51,7 +51,7 @@ describe('Projects', { disableAutoLogin: true }, () => { }); projects.getHomeButton().click(); - projects.getProjectTabs().should('have.length', 2); + projects.getProjectTabs().should('have.length', 3); projects.getProjectTabCredentials().click(); credentialsPage.getters.credentialCards().should('not.have.length'); @@ -101,7 +101,7 @@ describe('Projects', { disableAutoLogin: true }, () => { projects.getMenuItems().first().click(); workflowsPage.getters.workflowCards().should('not.have.length'); - projects.getProjectTabs().should('have.length', 3); + projects.getProjectTabs().should('have.length', 4); workflowsPage.getters.newWorkflowButtonCard().click(); @@ -441,9 +441,7 @@ describe('Projects', { disableAutoLogin: true }, () => { .should('contain.text', 'Notion account personal project'); }); - // Skip flaky test - // eslint-disable-next-line n8n-local-rules/no-skipped-tests - it.skip('should move resources between projects', () => { + it('should move resources between projects', () => { cy.signinAsOwner(); cy.visit(workflowsPage.url); @@ -686,9 +684,7 @@ describe('Projects', { disableAutoLogin: true }, () => { .should('have.length', 1); }); - // Skip flaky test - // eslint-disable-next-line n8n-local-rules/no-skipped-tests - it.skip('should allow to change inaccessible credential when the workflow was moved to a team project', () => { + it('should allow to change inaccessible credential when the workflow was moved to a team project', () => { cy.signinAsOwner(); cy.visit(workflowsPage.url); @@ -701,9 +697,7 @@ describe('Projects', { disableAutoLogin: true }, () => { projects.getHomeButton().click(); workflowsPage.getters.workflowCards().should('not.have.length'); workflowsPage.getters.newWorkflowButtonCard().click(); - workflowsPage.getters.workflowCards().should('not.have.length'); - workflowsPage.getters.newWorkflowButtonCard().click(); workflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true); ndv.getters.backToCanvas().click(); @@ -789,7 +783,8 @@ describe('Projects', { disableAutoLogin: true }, () => { cy.get('input[name="password"]').type(INSTANCE_MEMBERS[0].password); cy.getByTestId('form-submit-button').click(); - mainSidebar.getters.executions().click(); + projects.getMenuItems().last().click(); + projects.getProjectTabExecutions().click(); cy.getByTestId('global-execution-list-item').first().find('td:last button').click(); getVisibleDropdown() .find('li') diff --git a/cypress/e2e/45-ai-assistant.cy.ts b/cypress/e2e/45-ai-assistant.cy.ts index 9c69269b331..9b50fef44a0 100644 --- a/cypress/e2e/45-ai-assistant.cy.ts +++ b/cypress/e2e/45-ai-assistant.cy.ts @@ -224,6 +224,54 @@ describe('AI Assistant::enabled', () => { .should('contain.text', 'item.json.myNewField = 1'); }); + it('Should ignore node execution success and error messages after the node run successfully once', () => { + const getParameter = () => ndv.getters.parameterInput('jsCode').should('be.visible'); + + const getEditor = () => getParameter().find('.cm-content').should('exist'); + + cy.intercept('POST', '/rest/ai/chat', { + statusCode: 200, + fixture: 'aiAssistant/responses/code_diff_suggestion_response.json', + }).as('chatRequest'); + + cy.createFixtureWorkflow('aiAssistant/workflows/test_workflow.json'); + wf.actions.openNode('Code'); + ndv.getters.nodeExecuteButton().click(); + aiAssistant.getters.nodeErrorViewAssistantButton().click({ force: true }); + cy.wait('@chatRequest'); + + cy.intercept('POST', '/rest/ai/chat', { + statusCode: 200, + fixture: 'aiAssistant/responses/node_execution_succeeded_response.json', + }).as('chatRequest2'); + + getEditor() + .type('{selectall}') + .paste( + 'for (const item of $input.all()) {\n item.json.myNewField = 1;\n}\n\nreturn $input.all();', + ); + + ndv.getters.nodeExecuteButton().click(); + + getEditor() + .type('{selectall}') + .paste( + 'for (const item of $input.all()) {\n item.json.myNewField = 1aaaa!;\n}\n\nreturn $input.all();', + ); + + ndv.getters.nodeExecuteButton().click(); + + aiAssistant.getters.chatMessagesAssistant().should('have.length', 3); + + aiAssistant.getters + .chatMessagesAssistant() + .eq(2) + .should( + 'contain.text', + 'Code node ran successfully, did my solution help resolve your issue?\nQuick reply ๐Ÿ‘‡Yes, thanksNo, I am still stuck', + ); + }); + it('should end chat session when `end_session` event is received', () => { cy.intercept('POST', '/rest/ai/chat', { statusCode: 200, diff --git a/cypress/e2e/5-ndv.cy.ts b/cypress/e2e/5-ndv.cy.ts index 70a62bb2447..96b917d4c3d 100644 --- a/cypress/e2e/5-ndv.cy.ts +++ b/cypress/e2e/5-ndv.cy.ts @@ -81,7 +81,7 @@ describe('NDV', () => { ndv.getters.backToCanvas().click(); workflowPage.actions.executeWorkflow(); workflowPage.actions.openNode('Merge'); - ndv.getters.outputPanel().contains('1 item').should('exist'); + ndv.getters.outputPanel().contains('2 items').should('exist'); cy.contains('span', 'zero').should('exist'); }); diff --git a/cypress/fixtures/aiAssistant/responses/node_execution_succeeded_response.json b/cypress/fixtures/aiAssistant/responses/node_execution_succeeded_response.json new file mode 100644 index 00000000000..62caf2a6b5d --- /dev/null +++ b/cypress/fixtures/aiAssistant/responses/node_execution_succeeded_response.json @@ -0,0 +1,22 @@ +{ + "sessionId": "1", + "messages": [ + { + "role": "assistant", + "type": "message", + "text": "**Code** node ran successfully, did my solution help resolve your issue?", + "quickReplies": [ + { + "text": "Yes, thanks", + "type": "all-good", + "isFeedback": true + }, + { + "text": "No, I am still stuck", + "type": "still-stuck", + "isFeedback": true + } + ] + } + ] +} diff --git a/package.json b/package.json index 1b35d0384f3..c7d4512b2e5 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "build:nodes": "turbo run build:nodes", "typecheck": "turbo typecheck", "dev": "turbo run dev --parallel --env-mode=loose --filter=!n8n-design-system --filter=!@n8n/chat --filter=!@n8n/task-runner", + "dev:be": "turbo run dev --parallel --env-mode=loose --filter=!n8n-design-system --filter=!@n8n/chat --filter=!@n8n/task-runner --filter=!n8n-editor-ui", "dev:ai": "turbo run dev --parallel --env-mode=loose --filter=@n8n/nodes-langchain --filter=n8n --filter=n8n-core", "clean": "turbo run clean --parallel", "reset": "node scripts/ensure-zx.mjs && zx scripts/reset.mjs", diff --git a/packages/@n8n/chat/src/__tests__/setup.ts b/packages/@n8n/chat/src/__tests__/setup.ts index 7b0828bfa80..33e89fb68b2 100644 --- a/packages/@n8n/chat/src/__tests__/setup.ts +++ b/packages/@n8n/chat/src/__tests__/setup.ts @@ -1 +1,13 @@ import '@testing-library/jest-dom'; +import '@testing-library/jest-dom'; +import { configure } from '@testing-library/vue'; + +configure({ testIdAttribute: 'data-test-id' }); + +window.ResizeObserver = + window.ResizeObserver || + vi.fn().mockImplementation(() => ({ + disconnect: vi.fn(), + observe: vi.fn(), + unobserve: vi.fn(), + })); diff --git a/packages/@n8n/chat/src/components/ChatFile.vue b/packages/@n8n/chat/src/components/ChatFile.vue index d3c3c2db7d5..52b4acf789a 100644 --- a/packages/@n8n/chat/src/components/ChatFile.vue +++ b/packages/@n8n/chat/src/components/ChatFile.vue @@ -30,22 +30,23 @@ const TypeIcon = computed(() => { }); function onClick() { - if (props.isRemovable) { - emit('remove', props.file); - } - if (props.isPreviewable) { window.open(URL.createObjectURL(props.file)); } } +function onDelete() { + emit('remove', props.file); +} @@ -80,12 +81,25 @@ function onClick() { .chat-file-preview { background: none; border: none; - display: none; + display: block; cursor: pointer; flex-shrink: 0; +} - .chat-file:hover & { - display: block; +.chat-file-delete { + position: relative; + &:hover { + color: red; + } + + /* Increase hit area for better clickability */ + &:before { + content: ''; + position: absolute; + top: -10px; + right: -10px; + bottom: -10px; + left: -10px; } } diff --git a/packages/@n8n/chat/src/components/Input.vue b/packages/@n8n/chat/src/components/Input.vue index 3e823917e0c..4abfc76849c 100644 --- a/packages/@n8n/chat/src/components/Input.vue +++ b/packages/@n8n/chat/src/components/Input.vue @@ -1,6 +1,6 @@