From 6b056871068cb4b5bcd6626fee7fd03bf7fbe591 Mon Sep 17 00:00:00 2001 From: Milorad Filipovic Date: Mon, 19 Aug 2024 16:46:53 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A8=20Adding=20dedicated=20aiAssistant?= =?UTF-8?q?=20page,=20more=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cypress/e2e/45-ai-assistant.cy.ts | 157 ++++++++++-------- .../aiAssistant/end_session_response.json | 16 ++ cypress/pages/features/ai-assistant.ts | 29 ++++ cypress/pages/ndv.ts | 2 - cypress/pages/workflow.ts | 18 -- 5 files changed, 131 insertions(+), 91 deletions(-) create mode 100644 cypress/fixtures/aiAssistant/end_session_response.json create mode 100644 cypress/pages/features/ai-assistant.ts diff --git a/cypress/e2e/45-ai-assistant.cy.ts b/cypress/e2e/45-ai-assistant.cy.ts index 8e05854e64574..e835ddeecf828 100644 --- a/cypress/e2e/45-ai-assistant.cy.ts +++ b/cypress/e2e/45-ai-assistant.cy.ts @@ -1,5 +1,6 @@ import { overrideFeatureFlag } from '../composables/featureFlags'; import { NDV, WorkflowPage } from '../pages'; +import { AIAssistant } from '../pages/features/ai-assistant'; const AI_ASSISTANT_FEATURE = { flagName: '021_ai_debug_helper', @@ -9,6 +10,7 @@ const AI_ASSISTANT_FEATURE = { const wf = new WorkflowPage(); const ndv = new NDV(); +const aiAssistant = new AIAssistant(); describe('AI Assistant::disabled', () => { beforeEach(() => { @@ -17,7 +19,7 @@ describe('AI Assistant::disabled', () => { }); it('does not show assistant button if feature is disabled', () => { - wf.getters.askAssistantFloatingButton().should('not.exist'); + aiAssistant.getters.askAssistantFloatingButton().should('not.exist'); }); }); @@ -27,29 +29,31 @@ describe('AI Assistant::enabled', () => { wf.actions.visit(); }); + after(() => { + overrideFeatureFlag(AI_ASSISTANT_FEATURE.flagName, AI_ASSISTANT_FEATURE.disabledFor); + }); + it('renders placeholder UI', () => { - wf.getters.askAssistantFloatingButton().should('be.visible'); - wf.getters.askAssistantFloatingButton().click(); - wf.getters.askAssistantChat().should('be.visible'); - wf.getters.aiAssistantPlaceholderMessage().should('be.visible'); - wf.getters.aiAssistantChatInputWrapper().should('not.exist'); - wf.getters.aiAssistantCloseButton().should('be.visible'); - wf.getters.aiAssistantCloseButton().click(); - wf.getters.askAssistantChat().should('not.exist'); + aiAssistant.getters.askAssistantFloatingButton().should('be.visible'); + aiAssistant.getters.askAssistantFloatingButton().click(); + aiAssistant.getters.askAssistantChat().should('be.visible'); + aiAssistant.getters.placeholderMessage().should('be.visible'); + aiAssistant.getters.chatInputWrapper().should('not.exist'); + aiAssistant.getters.closeChatButton().should('be.visible'); + aiAssistant.getters.closeChatButton().click(); + aiAssistant.getters.askAssistantChat().should('not.exist'); }); it('should resize assistant chat up', () => { - wf.getters.askAssistantFloatingButton().should('be.visible'); - wf.getters.askAssistantFloatingButton().click(); - wf.getters.askAssistantChat().should('be.visible'); - wf.getters.askAssistantSidebarResizer().should('be.visible'); - wf.getters.askAssistantChat().then((element) => { + aiAssistant.getters.askAssistantFloatingButton().click(); + aiAssistant.getters.askAssistantSidebarResizer().should('be.visible'); + aiAssistant.getters.askAssistantChat().then((element) => { const { width, left } = element[0].getBoundingClientRect(); - cy.drag(wf.getters.askAssistantSidebarResizer(), [left - 10, 0], { + cy.drag(aiAssistant.getters.askAssistantSidebarResizer(), [left - 10, 0], { abs: true, clickToFinish: true, }); - wf.getters.askAssistantChat().then((newElement) => { + aiAssistant.getters.askAssistantChat().then((newElement) => { const newWidth = newElement[0].getBoundingClientRect().width; expect(newWidth).to.be.greaterThan(width); }); @@ -57,17 +61,15 @@ describe('AI Assistant::enabled', () => { }); it('should resize assistant chat down', () => { - wf.getters.askAssistantFloatingButton().should('be.visible'); - wf.getters.askAssistantFloatingButton().click(); - wf.getters.askAssistantChat().should('be.visible'); - wf.getters.askAssistantSidebarResizer().should('be.visible'); - wf.getters.askAssistantChat().then((element) => { + aiAssistant.getters.askAssistantFloatingButton().click(); + aiAssistant.getters.askAssistantSidebarResizer().should('be.visible'); + aiAssistant.getters.askAssistantChat().then((element) => { const { width, left } = element[0].getBoundingClientRect(); - cy.drag(wf.getters.askAssistantSidebarResizer(), [left + 10, 0], { + cy.drag(aiAssistant.getters.askAssistantSidebarResizer(), [left + 10, 0], { abs: true, clickToFinish: true, }); - wf.getters.askAssistantChat().then((newElement) => { + aiAssistant.getters.askAssistantChat().then((newElement) => { const newWidth = newElement[0].getBoundingClientRect().width; expect(newWidth).to.be.lessThan(width); }); @@ -82,15 +84,14 @@ describe('AI Assistant::enabled', () => { cy.createFixtureWorkflow('aiAssistant/test_workflow.json'); wf.actions.openNode('Stop and Error'); ndv.getters.nodeExecuteButton().click(); - ndv.getters.nodeErrorViewAssistantButton().should('be.visible'); - ndv.getters.nodeErrorViewAssistantButton().click(); + aiAssistant.getters.nodeErrorViewAssistantButton().click(); cy.wait('@chatRequest'); - wf.getters.aiAssistantChatMessagesAll().should('have.length', 1); - wf.getters - .aiAssistantChatMessagesAll() + aiAssistant.getters.chatMessagesAll().should('have.length', 1); + aiAssistant.getters + .chatMessagesAll() .eq(0) .should('contain.text', 'Hey, this is an assistant message'); - ndv.getters.nodeErrorViewAssistantButton().should('be.disabled'); + aiAssistant.getters.nodeErrorViewAssistantButton().should('be.disabled'); }); it('should render chat input correctly', () => { @@ -101,28 +102,27 @@ describe('AI Assistant::enabled', () => { cy.createFixtureWorkflow('aiAssistant/test_workflow.json'); wf.actions.openNode('Stop and Error'); ndv.getters.nodeExecuteButton().click(); - ndv.getters.nodeErrorViewAssistantButton().should('be.visible'); - ndv.getters.nodeErrorViewAssistantButton().click(); + aiAssistant.getters.nodeErrorViewAssistantButton().click(); cy.wait('@chatRequest'); // Send button should be disabled when input is empty - wf.getters.aiAssistantSendButton().should('be.disabled'); - wf.getters.aiAssistantChatInput().type('Yo '); - wf.getters.aiAssistantSendButton().should('not.be.disabled'); - wf.getters.aiAssistantChatInput().then((element) => { + aiAssistant.getters.sendMessageButton().should('be.disabled'); + aiAssistant.getters.chatInput().type('Yo '); + aiAssistant.getters.sendMessageButton().should('not.be.disabled'); + aiAssistant.getters.chatInput().then((element) => { const { height } = element[0].getBoundingClientRect(); // Shift + Enter should add a new line - wf.getters.aiAssistantChatInput().type('Hello{shift+enter}there'); - wf.getters.aiAssistantChatInput().then((newElement) => { + aiAssistant.getters.chatInput().type('Hello{shift+enter}there'); + aiAssistant.getters.chatInput().then((newElement) => { const newHeight = newElement[0].getBoundingClientRect().height; // Chat input should grow as user adds new lines expect(newHeight).to.be.greaterThan(height); - wf.getters.aiAssistantSendButton().click(); + aiAssistant.getters.sendMessageButton().click(); cy.wait('@chatRequest'); // New lines should be rendered as
in the chat - wf.getters.aiAssistantUserMessages().should('have.length', 1); - wf.getters.aiAssistantUserMessages().eq(0).find('br').should('have.length', 1); + aiAssistant.getters.chatMessagesUser().should('have.length', 1); + aiAssistant.getters.chatMessagesUser().eq(0).find('br').should('have.length', 1); // Chat input should be cleared now - wf.getters.aiAssistantChatInput().should('have.value', ''); + aiAssistant.getters.chatInput().should('have.value', ''); }); }); }); @@ -135,14 +135,13 @@ describe('AI Assistant::enabled', () => { cy.createFixtureWorkflow('aiAssistant/test_workflow.json'); wf.actions.openNode('Stop and Error'); ndv.getters.nodeExecuteButton().click(); - ndv.getters.nodeErrorViewAssistantButton().should('be.visible'); - ndv.getters.nodeErrorViewAssistantButton().click(); + aiAssistant.getters.nodeErrorViewAssistantButton().click(); cy.wait('@chatRequest'); - wf.getters.aiAssistantQuickReplies().should('have.length', 2); - wf.getters.aiAssistantQuickReplies().eq(0).click(); + aiAssistant.getters.quickReplies().should('have.length', 2); + aiAssistant.getters.quickReplies().eq(0).click(); cy.wait('@chatRequest'); - wf.getters.aiAssistantUserMessages().should('have.length', 1); - wf.getters.aiAssistantUserMessages().eq(0).should('contain.text', "Sure, let's do it"); + aiAssistant.getters.chatMessagesUser().should('have.length', 1); + aiAssistant.getters.chatMessagesUser().eq(0).should('contain.text', "Sure, let's do it"); }); it('should send message to assistant when node is executed', () => { @@ -153,14 +152,13 @@ describe('AI Assistant::enabled', () => { cy.createFixtureWorkflow('aiAssistant/test_workflow.json'); wf.actions.openNode('Edit Fields'); ndv.getters.nodeExecuteButton().click(); - ndv.getters.nodeErrorViewAssistantButton().should('be.visible'); - ndv.getters.nodeErrorViewAssistantButton().click(); + aiAssistant.getters.nodeErrorViewAssistantButton().click(); cy.wait('@chatRequest'); - wf.getters.aiAssistantChatMessagesAssistant().should('have.length', 1); + aiAssistant.getters.chatMessagesAssistant().should('have.length', 1); // Executing the same node should sende a new message to the assistant automatically ndv.getters.nodeExecuteButton().click(); cy.wait('@chatRequest'); - wf.getters.aiAssistantChatMessagesAssistant().should('have.length', 2); + aiAssistant.getters.chatMessagesAssistant().should('have.length', 2); }); it('should warn before starting a new session', () => { @@ -171,20 +169,23 @@ describe('AI Assistant::enabled', () => { cy.createFixtureWorkflow('aiAssistant/test_workflow.json'); wf.actions.openNode('Edit Fields'); ndv.getters.nodeExecuteButton().click(); - ndv.getters.nodeErrorViewAssistantButton().should('be.visible'); - ndv.getters.nodeErrorViewAssistantButton().click(); + aiAssistant.getters.nodeErrorViewAssistantButton().click(); cy.wait('@chatRequest'); - wf.getters.aiAssistantCloseButton().click(); + aiAssistant.getters.closeChatButton().click(); ndv.getters.backToCanvas().click(); wf.actions.openNode('Stop and Error'); ndv.getters.nodeExecuteButton().click(); - ndv.getters.nodeErrorViewAssistantButton().click(); + aiAssistant.getters.nodeErrorViewAssistantButton().click(); // Since we already have an active session, a warning should be shown - wf.getters.newAssistantSessionModal().should('be.visible'); - wf.getters.newAssistantSessionModal().find('button').contains('Start new session').click(); + aiAssistant.getters.newAssistantSessionModal().should('be.visible'); + aiAssistant.getters + .newAssistantSessionModal() + .find('button') + .contains('Start new session') + .click(); cy.wait('@chatRequest'); // New session should start with initial assistant message - wf.getters.aiAssistantChatMessagesAll().should('have.length', 1); + aiAssistant.getters.chatMessagesAll().should('have.length', 1); }); it('should apply code diff to code node', () => { @@ -199,25 +200,25 @@ describe('AI Assistant::enabled', () => { cy.createFixtureWorkflow('aiAssistant/test_workflow.json'); wf.actions.openNode('Code'); ndv.getters.nodeExecuteButton().click(); - ndv.getters.nodeErrorViewAssistantButton().click({ force: true }); + aiAssistant.getters.nodeErrorViewAssistantButton().click({ force: true }); cy.wait('@chatRequest'); // Should have two assistant messages - wf.getters.aiAssistantChatMessagesAll().should('have.length', 2); - wf.getters.aiAssistantCodeDiffs().should('have.length', 1); - wf.getters.aiAssistantApplyCodeDiffButtons().should('have.length', 1); - wf.getters.aiAssistantApplyCodeDiffButtons().first().click(); + aiAssistant.getters.chatMessagesAll().should('have.length', 2); + aiAssistant.getters.codeDiffs().should('have.length', 1); + aiAssistant.getters.applyCodeDiffButtons().should('have.length', 1); + aiAssistant.getters.applyCodeDiffButtons().first().click(); cy.wait('@applySuggestion'); - wf.getters.aiAssistantApplyCodeDiffButtons().should('have.length', 0); - wf.getters.aiAssistantUndoReplaceCodeButtons().should('have.length', 1); - wf.getters.aiAssistantCodeReplacedMessage().should('be.visible'); + aiAssistant.getters.applyCodeDiffButtons().should('have.length', 0); + aiAssistant.getters.undoReplaceCodeButtons().should('have.length', 1); + aiAssistant.getters.codeReplacedMessage().should('be.visible'); ndv.getters .parameterInput('jsCode') .get('.cm-content') .should('contain.text', 'item.json.myNewField = 1'); // Clicking undo should revert the code back but not call the assistant - wf.getters.aiAssistantUndoReplaceCodeButtons().first().click(); - wf.getters.aiAssistantApplyCodeDiffButtons().should('have.length', 1); - wf.getters.aiAssistantCodeReplacedMessage().should('not.exist'); + aiAssistant.getters.undoReplaceCodeButtons().first().click(); + aiAssistant.getters.applyCodeDiffButtons().should('have.length', 1); + aiAssistant.getters.codeReplacedMessage().should('not.exist'); cy.get('@applySuggestion.all').then((interceptions) => { expect(interceptions).to.have.length(1); }); @@ -229,11 +230,25 @@ describe('AI Assistant::enabled', () => { cy.get('@applySuggestion.all').then((interceptions) => { expect(interceptions).to.have.length(1); }); - wf.getters.aiAssistantApplyCodeDiffButtons().should('have.length', 1); - wf.getters.aiAssistantApplyCodeDiffButtons().first().click(); + aiAssistant.getters.applyCodeDiffButtons().should('have.length', 1); + aiAssistant.getters.applyCodeDiffButtons().first().click(); ndv.getters .parameterInput('jsCode') .get('.cm-content') .should('contain.text', 'item.json.myNewField = 1'); }); + + it('should end chat session when `end_session` event is received', () => { + cy.intercept('POST', '/rest/ai-assistant/chat', { + statusCode: 200, + fixture: 'aiAssistant/end_session_response.json', + }).as('chatRequest'); + cy.createFixtureWorkflow('aiAssistant/test_workflow.json'); + wf.actions.openNode('Stop and Error'); + ndv.getters.nodeExecuteButton().click(); + aiAssistant.getters.nodeErrorViewAssistantButton().click(); + cy.wait('@chatRequest'); + aiAssistant.getters.chatMessagesSystem().should('have.length', 1); + aiAssistant.getters.chatMessagesSystem().first().should('contain.text', 'session has ended'); + }); }); diff --git a/cypress/fixtures/aiAssistant/end_session_response.json b/cypress/fixtures/aiAssistant/end_session_response.json new file mode 100644 index 0000000000000..c53574d93a6ca --- /dev/null +++ b/cypress/fixtures/aiAssistant/end_session_response.json @@ -0,0 +1,16 @@ +{ + "sessionId": "f9130bd7-c078-4862-a38a-369b27b0ff20-e96eb9f7-d581-4684-b6a9-fd3dfe9fe1fb-XCldJLlusGrEVku5I9cYT", + "messages": [ + { + "role": "assistant", + "type": "agent-suggestion", + "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" + } + ] +} diff --git a/cypress/pages/features/ai-assistant.ts b/cypress/pages/features/ai-assistant.ts new file mode 100644 index 0000000000000..ed3f114035428 --- /dev/null +++ b/cypress/pages/features/ai-assistant.ts @@ -0,0 +1,29 @@ +import { BasePage } from '../base'; + +export class AIAssistant extends BasePage { + url = '/workflows/new'; + + getters = { + askAssistantFloatingButton: () => cy.getByTestId('ask-assistant-floating-button'), + askAssistantSidebar: () => cy.getByTestId('ask-assistant-sidebar'), + askAssistantSidebarResizer: () => this.getters.askAssistantSidebar().find('[class^=_resizer][data-dir=left]').first(), + askAssistantChat: () => cy.getByTestId('ask-assistant-chat'), + placeholderMessage: () => cy.getByTestId('placeholder-message'), + closeChatButton: () => cy.getByTestId('close-chat-button'), + chatInputWrapper: () => cy.getByTestId('chat-input-wrapper'), + chatInput: () => cy.getByTestId('chat-input'), + sendMessageButton: () => cy.getByTestId('send-message-button'), + chatMessagesAll: () => cy.get('[data-test-id^=chat-message]'), + chatMessagesAssistant: () => cy.getByTestId('chat-message-assistant'), + chatMessagesUser: () => cy.getByTestId('chat-message-user'), + chatMessagesSystem: () => cy.getByTestId('chat-message-system'), + quickReplies: () => cy.getByTestId('quick-replies').find('button'), + newAssistantSessionModal: () => cy.getByTestId('new-assistant-session-modal'), + codeDiffs: () => cy.getByTestId('code-diff-suggestion'), + applyCodeDiffButtons: () => cy.getByTestId('replace-code-button'), + undoReplaceCodeButtons: () => cy.getByTestId('undo-replace-button'), + codeReplacedMessage: () => cy.getByTestId('code-replaced-message'), + nodeErrorViewAssistantButton: () => + cy.getByTestId('node-error-view-ask-assistant-button').find('button').first(), + }; +} diff --git a/cypress/pages/ndv.ts b/cypress/pages/ndv.ts index aa5b30ad13423..3bb9eb0fe0907 100644 --- a/cypress/pages/ndv.ts +++ b/cypress/pages/ndv.ts @@ -138,8 +138,6 @@ export class NDV extends BasePage { cy.getByTestId(`fixed-collection-${paramName}`), schemaViewNode: () => cy.getByTestId('run-data-schema-node'), schemaViewNodeName: () => cy.getByTestId('run-data-schema-node-name'), - nodeErrorViewAssistantButton: () => - cy.getByTestId('node-error-view-ask-assistant-button').find('button').first(), }; actions = { diff --git a/cypress/pages/workflow.ts b/cypress/pages/workflow.ts index 2af7e24e7fe76..0c2a26960768d 100644 --- a/cypress/pages/workflow.ts +++ b/cypress/pages/workflow.ts @@ -128,24 +128,6 @@ export class WorkflowPage extends BasePage { workflowHistoryButton: () => cy.getByTestId('workflow-history-button'), colors: () => cy.getByTestId('color'), contextMenuAction: (action: string) => cy.getByTestId(`context-menu-item-${action}`), - askAssistantFloatingButton: () => cy.getByTestId('ask-assistant-floating-button'), - askAssistantSidebar: () => cy.getByTestId('ask-assistant-sidebar'), - askAssistantSidebarResizer: () => this.getters.askAssistantSidebar().find('[class^=_resizer][data-dir=left]').first(), - askAssistantChat: () => cy.getByTestId('ask-assistant-chat'), - aiAssistantPlaceholderMessage: () => cy.getByTestId('placeholder-message'), - aiAssistantCloseButton: () => cy.getByTestId('close-chat-button'), - aiAssistantChatInputWrapper: () => cy.getByTestId('chat-input-wrapper'), - aiAssistantChatInput: () => cy.getByTestId('chat-input'), - aiAssistantSendButton: () => cy.getByTestId('send-message-button'), - aiAssistantChatMessagesAll: () => cy.get('[data-test-id^=chat-message]'), - aiAssistantChatMessagesAssistant: () => cy.getByTestId('chat-message-assistant'), - aiAssistantUserMessages: () => cy.getByTestId('chat-message-user'), - aiAssistantQuickReplies: () => cy.getByTestId('quick-replies').find('button'), - newAssistantSessionModal: () => cy.getByTestId('new-assistant-session-modal'), - aiAssistantCodeDiffs: () => cy.getByTestId('code-diff-suggestion'), - aiAssistantApplyCodeDiffButtons: () => cy.getByTestId('replace-code-button'), - aiAssistantUndoReplaceCodeButtons: () => cy.getByTestId('undo-replace-button'), - aiAssistantCodeReplacedMessage: () => cy.getByTestId('code-replaced-message'), }; actions = {