Skip to content

Commit

Permalink
🔨 Adding dedicated aiAssistant page, more tests
Browse files Browse the repository at this point in the history
  • Loading branch information
MiloradFilipovic committed Aug 19, 2024
1 parent d182541 commit 6b05687
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 91 deletions.
157 changes: 86 additions & 71 deletions cypress/e2e/45-ai-assistant.cy.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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(() => {
Expand All @@ -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');
});
});

Expand All @@ -27,47 +29,47 @@ 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);
});
});
});

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);
});
Expand All @@ -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', () => {
Expand All @@ -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 <br> 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', '');
});
});
});
Expand All @@ -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', () => {
Expand All @@ -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', () => {
Expand All @@ -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', () => {
Expand All @@ -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);
});
Expand All @@ -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');
});
});
16 changes: 16 additions & 0 deletions cypress/fixtures/aiAssistant/end_session_response.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
29 changes: 29 additions & 0 deletions cypress/pages/features/ai-assistant.ts
Original file line number Diff line number Diff line change
@@ -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(),
};
}
2 changes: 0 additions & 2 deletions cypress/pages/ndv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
18 changes: 0 additions & 18 deletions cypress/pages/workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down

0 comments on commit 6b05687

Please sign in to comment.