diff --git a/cypress/composables/executions.ts b/cypress/composables/executions.ts index cd07eb7e6a..1da9792675 100644 --- a/cypress/composables/executions.ts +++ b/cypress/composables/executions.ts @@ -9,6 +9,9 @@ export const getWorkflowExecutionPreviewIframe = () => cy.getByTestId('workflow- export const getExecutionPreviewBody = () => getWorkflowExecutionPreviewIframe() .its('0.contentDocument.body') + .should((body) => { + expect(body.querySelector('[data-test-id="canvas-wrapper"]')).to.exist; + }) .then((el) => cy.wrap(el)); export const getExecutionPreviewBodyNodes = () => @@ -21,9 +24,23 @@ export function getExecutionPreviewOutputPanelRelatedExecutionLink() { return getExecutionPreviewBody().findChildByTestId('related-execution-link'); } +export function getLogsOverviewStatus() { + return getExecutionPreviewBody().findChildByTestId('logs-overview-status'); +} + +export function getLogEntries() { + return getExecutionPreviewBody().findChildByTestId('logs-overview-body').find('[role=treeitem]'); +} + +export function getManualChatMessages() { + return getExecutionPreviewBody().find('.chat-messages-list .chat-message'); +} + /** * Actions */ export const openExecutionPreviewNode = (name: string) => getExecutionPreviewBodyNodesByName(name).dblclick(); + +export const toggleAutoRefresh = () => cy.getByTestId('auto-refresh-checkbox').click(); diff --git a/cypress/composables/logs.ts b/cypress/composables/logs.ts index 3131913ca4..2510e2c0ee 100644 --- a/cypress/composables/logs.ts +++ b/cypress/composables/logs.ts @@ -2,8 +2,20 @@ * Accessors */ -export function getLogEntryAtRow(rowIndex: number) { - return cy.getByTestId('logs-overview-body').find('[role=treeitem]').eq(rowIndex); +export function getOverviewStatus() { + return cy.getByTestId('logs-overview-status'); +} + +export function getLogEntries() { + return cy.getByTestId('logs-overview-body').find('[role=treeitem]'); +} + +export function getSelectedLogEntry() { + return cy.getByTestId('logs-overview-body').find('[role=treeitem][aria-selected=true]'); +} + +export function getInputPanel() { + return cy.getByTestId('log-details-input'); } export function getInputTableRows() { @@ -14,6 +26,22 @@ export function getInputTbodyCell(row: number, col: number) { return cy.getByTestId('log-details-input').find('table tr').eq(row).find('td').eq(col); } +export function getNodeErrorMessageHeader() { + return cy.getByTestId('log-details-output').findChildByTestId('node-error-message'); +} + +export function getOutputPanel() { + return cy.getByTestId('log-details-output'); +} + +export function getOutputTableRows() { + return cy.getByTestId('log-details-output').find('table tr'); +} + +export function getOutputTbodyCell(row: number, col: number) { + return cy.getByTestId('log-details-output').find('table tr').eq(row).find('td').eq(col); +} + /** * Actions */ @@ -22,8 +50,12 @@ export function openLogsPanel() { cy.getByTestId('logs-overview-header').click(); } +export function pressClearExecutionButton() { + cy.getByTestId('logs-overview-header').find('button').contains('Clear execution').click(); +} + export function clickLogEntryAtRow(rowIndex: number) { - getLogEntryAtRow(rowIndex).click(); + getLogEntries().eq(rowIndex).click(); } export function toggleInputPanel() { @@ -31,11 +63,21 @@ export function toggleInputPanel() { } export function clickOpenNdvAtRow(rowIndex: number) { - getLogEntryAtRow(rowIndex).realHover(); - getLogEntryAtRow(rowIndex).find('[aria-label="Open..."]').click(); + getLogEntries().eq(rowIndex).realHover(); + getLogEntries().eq(rowIndex).find('[aria-label="Open..."]').click(); } -export function setInputDisplayMode(mode: 'table') { +export function clickTriggerPartialExecutionAtRow(rowIndex: number) { + getLogEntries().eq(rowIndex).realHover(); + getLogEntries().eq(rowIndex).find('[aria-label="Test step"]').click(); +} + +export function setInputDisplayMode(mode: 'table' | 'ai' | 'json' | 'schema') { cy.getByTestId('log-details-input').realHover(); cy.getByTestId('log-details-input').findChildByTestId(`radio-button-${mode}`).click(); } + +export function setOutputDisplayMode(mode: 'table' | 'ai' | 'json' | 'schema') { + cy.getByTestId('log-details-output').realHover(); + cy.getByTestId('log-details-output').findChildByTestId(`radio-button-${mode}`).click(); +} diff --git a/cypress/composables/workflow.ts b/cypress/composables/workflow.ts index 7344362e97..791e101984 100644 --- a/cypress/composables/workflow.ts +++ b/cypress/composables/workflow.ts @@ -25,14 +25,6 @@ export type EndpointType = * Getters */ -export function executeWorkflowAndWait(waitForSuccessBannerToDisappear = true) { - cy.get('[data-test-id="execute-workflow-button"]').click(); - cy.contains('Workflow executed successfully', { timeout: 4000 }).should('be.visible'); - if (waitForSuccessBannerToDisappear) { - cy.contains('Workflow executed successfully', { timeout: 10000 }).should('not.exist'); - } -} - export function getCanvas() { return cy.getByTestId('canvas'); } @@ -141,7 +133,7 @@ export function getWorkflowHistoryCloseButton() { export function disableNode(name: string) { const target = getNodeByName(name); - target.rightclick(name ? 'center' : 'topLeft', { force: true }); + target.trigger('contextmenu'); cy.getByTestId('context-menu-item-toggle_activation').click(); } @@ -201,6 +193,22 @@ export function getNodeIssuesByName(nodeName: string) { * Actions */ +export function executeWorkflow() { + cy.get('[data-test-id="execute-workflow-button"]').click(); +} + +export function waitForSuccessBannerToAppear() { + cy.contains(/(Workflow|Node) executed successfully/, { timeout: 4000 }).should('be.visible'); +} + +export function executeWorkflowAndWait(waitForSuccessBannerToDisappear = true) { + executeWorkflow(); + waitForSuccessBannerToAppear(); + if (waitForSuccessBannerToDisappear) { + cy.contains('Workflow executed successfully', { timeout: 10000 }).should('not.exist'); + } +} + export function addNodeToCanvas( nodeDisplayName: string, plusButtonClick = true, @@ -373,3 +381,7 @@ export function openContextMenu( export function clickContextMenuAction(action: string) { getContextMenuAction(action).click({ force: true }); } + +export function openExecutions() { + cy.getByTestId('radio-button-executions').click(); +} diff --git a/cypress/e2e/50-logs.cy.ts b/cypress/e2e/50-logs.cy.ts index 87c123c5b1..e31ff557b5 100644 --- a/cypress/e2e/50-logs.cy.ts +++ b/cypress/e2e/50-logs.cy.ts @@ -1,19 +1,120 @@ +import * as executions from '../composables/executions'; import * as logs from '../composables/logs'; +import * as chat from '../composables/modals/chat-modal'; import * as ndv from '../composables/ndv'; import * as workflow from '../composables/workflow'; -import Workflow from '../fixtures/Workflow_if.json'; +import Workflow_chat from '../fixtures/Workflow_ai_agent.json'; +import Workflow_if from '../fixtures/Workflow_if.json'; +import Workflow_loop from '../fixtures/Workflow_loop.json'; describe('Logs', () => { beforeEach(() => { cy.overrideSettings({ logsView: { enabled: true } }); }); - it('should show input and output data of correct run index and branch', () => { + it('should populate logs as manual execution progresses', () => { workflow.navigateToNewWorkflowPage(); - workflow.pasteWorkflow(Workflow); + workflow.pasteWorkflow(Workflow_loop); workflow.clickZoomToFit(); logs.openLogsPanel(); + logs.getLogEntries().should('have.length', 0); + + workflow.executeWorkflow(); + logs.getOverviewStatus().contains('Running').should('exist'); + + logs.getLogEntries().should('have.length', 4); + logs.getLogEntries().eq(0).should('contain.text', 'When clicking ‘Test workflow’'); + logs.getLogEntries().eq(1).should('contain.text', 'Code'); + logs.getLogEntries().eq(2).should('contain.text', 'Loop Over Items'); + logs.getLogEntries().eq(3).should('contain.text', 'Wait'); + + logs.getLogEntries().should('have.length', 6); + logs.getLogEntries().eq(4).should('contain.text', 'Loop Over Items'); + logs.getLogEntries().eq(5).should('contain.text', 'Wait'); + + logs.getLogEntries().should('have.length', 8); + logs.getLogEntries().eq(6).should('contain.text', 'Loop Over Items'); + logs.getLogEntries().eq(7).should('contain.text', 'Wait'); + + logs.getLogEntries().should('have.length', 10); + logs.getLogEntries().eq(8).should('contain.text', 'Loop Over Items'); + logs.getLogEntries().eq(9).should('contain.text', 'Code1'); + logs + .getOverviewStatus() + .contains(/Error in [\d\.]+s/) + .should('exist'); + logs.getSelectedLogEntry().should('contain.text', 'Code1'); // Errored node is automatically selected + logs.getNodeErrorMessageHeader().should('contain.text', 'test!!! [line 1]'); + workflow.getNodeIssuesByName('Code1').should('exist'); + + logs.pressClearExecutionButton(); + logs.getLogEntries().should('have.length', 0); + workflow.getNodeIssuesByName('Code1').should('not.exist'); + }); + + it('should allow to trigger partial execution', () => { + workflow.navigateToNewWorkflowPage(); + workflow.pasteWorkflow(Workflow_if); + workflow.clickZoomToFit(); + logs.openLogsPanel(); + workflow.executeWorkflowAndWait(false); + logs.getLogEntries().should('have.length', 6); + logs.getLogEntries().eq(0).should('contain.text', 'Schedule Trigger'); + logs.getLogEntries().eq(1).should('contain.text', 'Code'); + logs.getLogEntries().eq(2).should('contain.text', 'Edit Fields'); + logs.getLogEntries().eq(3).should('contain.text', 'If'); + logs.getLogEntries().eq(4).should('contain.text', 'Edit Fields'); + logs.getLogEntries().eq(5).should('contain.text', 'Edit Fields'); + + logs.clickTriggerPartialExecutionAtRow(3); + logs.getLogEntries().should('have.length', 3); + logs.getLogEntries().eq(0).should('contain.text', 'Schedule Trigger'); + logs.getLogEntries().eq(1).should('contain.text', 'Code'); + logs.getLogEntries().eq(2).should('contain.text', 'If'); + }); + + // TODO: make it possible to test workflows with AI model end-to-end + // eslint-disable-next-line n8n-local-rules/no-skipped-tests + it.skip('should show input and output data in the selected display mode', () => { + workflow.navigateToNewWorkflowPage(); + workflow.pasteWorkflow(Workflow_chat); + workflow.clickZoomToFit(); + logs.openLogsPanel(); + chat.sendManualChatMessage('Hi!'); + workflow.waitForSuccessBannerToAppear(); + chat.getManualChatMessages().eq(0).should('contain.text', 'Hi!'); + chat.getManualChatMessages().eq(1).should('contain.text', 'Hello from e2e model!!!'); + logs.getLogEntries().eq(2).should('have.text', 'E2E Chat Model'); + logs.getLogEntries().eq(2).click(); + + logs.getOutputPanel().should('contain.text', 'Hello from e2e model!!!'); + logs.setOutputDisplayMode('table'); + logs.getOutputTbodyCell(1, 0).should('contain.text', 'text:Hello from **e2e** model!!!'); + logs.getOutputTbodyCell(1, 1).should('contain.text', 'completionTokens:20'); + logs.setOutputDisplayMode('schema'); + logs.getOutputPanel().should('contain.text', 'generations[0]'); + logs.getOutputPanel().should('contain.text', 'Hello from **e2e** model!!!'); + logs.setOutputDisplayMode('json'); + logs.getOutputPanel().should('contain.text', '[{"response": {"generations": ['); + + logs.toggleInputPanel(); + logs.getInputPanel().should('contain.text', 'Human: Hi!'); + logs.setInputDisplayMode('table'); + logs.getInputTbodyCell(1, 0).should('contain.text', '0:Human: Hi!'); + logs.setInputDisplayMode('schema'); + logs.getInputPanel().should('contain.text', 'messages[0]'); + logs.getInputPanel().should('contain.text', 'Human: Hi!'); + logs.setInputDisplayMode('json'); + logs.getInputPanel().should('contain.text', '[{"messages": ["Human: Hi!"],'); + }); + + it('should show input and output data of correct run index and branch', () => { + workflow.navigateToNewWorkflowPage(); + workflow.pasteWorkflow(Workflow_if); + workflow.clickZoomToFit(); + logs.openLogsPanel(); + workflow.executeWorkflow(); logs.clickLogEntryAtRow(2); // Run #1 of 'Edit Fields' node; input is 'Code' node logs.toggleInputPanel(); @@ -55,4 +156,41 @@ describe('Logs', () => { ndv.getInputTbodyCell(5, 0).should('contain.text', '4'); ndv.getOutputRunSelectorInput().should('have.value', '3 of 3 (5 items)'); }); + + it('should keep populated logs unchanged when workflow get edits after the execution', () => { + workflow.navigateToNewWorkflowPage(); + workflow.pasteWorkflow(Workflow_if); + workflow.clickZoomToFit(); + logs.openLogsPanel(); + + workflow.executeWorkflowAndWait(false); + logs.getLogEntries().should('have.length', 6); + workflow.disableNode('Edit Fields'); + logs.getLogEntries().should('have.length', 6); + workflow.deleteNode('If'); + logs.getLogEntries().should('have.length', 6); + }); + + // TODO: make it possible to test workflows with AI model end-to-end + // eslint-disable-next-line n8n-local-rules/no-skipped-tests + it.skip('should show logs for a past execution', () => { + workflow.navigateToNewWorkflowPage(); + workflow.pasteWorkflow(Workflow_chat); + workflow.clickZoomToFit(); + logs.openLogsPanel(); + chat.sendManualChatMessage('Hi!'); + workflow.waitForSuccessBannerToAppear(); + workflow.openExecutions(); + executions.toggleAutoRefresh(); // Stop unnecessary background requests + executions.getManualChatMessages().eq(0).should('contain.text', 'Hi!'); + executions.getManualChatMessages().eq(1).should('contain.text', 'Hello from e2e model!!!'); + executions + .getLogsOverviewStatus() + .contains(/Success in [\d\.]+m?s/) + .should('exist'); + executions.getLogEntries().should('have.length', 3); + executions.getLogEntries().eq(0).should('contain.text', 'When chat message received'); + executions.getLogEntries().eq(1).should('contain.text', 'AI Agent'); + executions.getLogEntries().eq(2).should('contain.text', 'E2E Chat Model'); + }); }); diff --git a/cypress/fixtures/Workflow_ai_agent.json b/cypress/fixtures/Workflow_ai_agent.json new file mode 100644 index 0000000000..30e4cb3478 --- /dev/null +++ b/cypress/fixtures/Workflow_ai_agent.json @@ -0,0 +1,66 @@ +{ + "nodes": [ + { + "parameters": { + "options": {} + }, + "id": "5eb6b347-b34e-4112-9601-f7aa94f26575", + "name": "When chat message received", + "type": "@n8n/n8n-nodes-langchain.chatTrigger", + "typeVersion": 1.1, + "position": [0, 0], + "webhookId": "4fb58136-3481-494a-a30f-d9e064dac186" + }, + { + "parameters": { + "options": {} + }, + "type": "@n8n/n8n-nodes-langchain.agent", + "typeVersion": 1.9, + "position": [220, 0], + "id": "32534841-9474-4890-9998-65d6a56bdf0c", + "name": "AI Agent" + }, + { + "parameters": { + "response": "Hello from **e2e** model!!!" + }, + "type": "@n8n/n8n-nodes-langchain.lmChatE2eTest", + "typeVersion": 1, + "position": [308, 220], + "id": "2f239d5b-95ef-4949-92b6-5a7541e1029f", + "name": "E2E Chat Model" + } + ], + "connections": { + "When chat message received": { + "main": [ + [ + { + "node": "AI Agent", + "type": "main", + "index": 0 + } + ] + ] + }, + "AI Agent": { + "main": [[]] + }, + "E2E Chat Model": { + "ai_languageModel": [ + [ + { + "node": "AI Agent", + "type": "ai_languageModel", + "index": 0 + } + ] + ] + } + }, + "pinData": {}, + "meta": { + "instanceId": "14f5bd03485879885c6d92999d35d4d24556536fa2b675f932eb27193691e2b2" + } +} diff --git a/cypress/fixtures/Workflow_if.json b/cypress/fixtures/Workflow_if.json index 21dacb3538..af90f958f0 100644 --- a/cypress/fixtures/Workflow_if.json +++ b/cypress/fixtures/Workflow_if.json @@ -8,8 +8,8 @@ }, "type": "n8n-nodes-base.scheduleTrigger", "typeVersion": 1.2, - "position": [-900, 60], - "id": "e6b8fc7c-442e-4283-a0cd-604dc7c9e816", + "position": [0, 0], + "id": "4c4f02e3-f0cc-4e1a-b084-9888dd75ccaf", "name": "Schedule Trigger" }, { @@ -38,8 +38,8 @@ }, "type": "n8n-nodes-base.if", "typeVersion": 2.2, - "position": [-460, 135], - "id": "f5c96b5b-9e22-4348-a258-fdb0417f5ff5", + "position": [660, 80], + "id": "3273dcdb-845c-43cc-8ed0-ffaef7b68b1c", "name": "If" }, { @@ -58,8 +58,8 @@ }, "type": "n8n-nodes-base.set", "typeVersion": 3.4, - "position": [-240, 60], - "id": "2a6fc40d-5d8c-4c35-bf53-ee910267619f", + "position": [880, 0], + "id": "0522012f-fe99-41e0-ba19-7e18f22758d9", "name": "Edit Fields" }, { @@ -68,9 +68,20 @@ }, "type": "n8n-nodes-base.code", "typeVersion": 2, - "position": [-680, 60], - "id": "12ae07e7-be34-43b6-806b-4c24be169ee6", + "position": [440, 0], + "id": "268c2da4-23c8-46c5-8992-37a92b8e4aad", "name": "Code" + }, + { + "parameters": { + "options": {} + }, + "type": "n8n-nodes-base.set", + "typeVersion": 3.4, + "position": [220, 0], + "id": "0e2d1621-607d-4d53-98a3-07afc0d6230d", + "name": "Edit Fields1", + "disabled": true } ], "connections": { @@ -78,7 +89,7 @@ "main": [ [ { - "node": "Code", + "node": "Edit Fields1", "type": "main", "index": 0 } @@ -118,10 +129,21 @@ } ] ] + }, + "Edit Fields1": { + "main": [ + [ + { + "node": "Code", + "type": "main", + "index": 0 + } + ] + ] } }, "pinData": {}, "meta": { - "instanceId": "db1f26b45a71ad9a8df79dde8d35bf1be13616c3b23eb55be8ecf642dd31500c" + "instanceId": "ddb4b2493e5f30d4af3bb43dd3c9acf1f60cbaa91c89e84d2967725474533c77" } } diff --git a/cypress/fixtures/Workflow_loop.json b/cypress/fixtures/Workflow_loop.json new file mode 100644 index 0000000000..5055ef0983 --- /dev/null +++ b/cypress/fixtures/Workflow_loop.json @@ -0,0 +1,110 @@ +{ + "nodes": [ + { + "parameters": {}, + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [0, -10], + "id": "cc68bd44-e150-403c-afaf-bd0bac0459dd", + "name": "When clicking ‘Test workflow’" + }, + { + "parameters": { + "jsCode": "return [{data:1},{data:2},{data:3}]" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [220, -10], + "id": "461b130c-0efb-4201-8210-d5e794e88ed8", + "name": "Code" + }, + { + "parameters": { + "options": {} + }, + "type": "n8n-nodes-base.splitInBatches", + "typeVersion": 3, + "position": [440, -10], + "id": "16860d82-1b2c-4882-ad76-43cc2042d695", + "name": "Loop Over Items" + }, + { + "parameters": { + "amount": 2 + }, + "type": "n8n-nodes-base.wait", + "typeVersion": 1.1, + "position": [660, 40], + "id": "9ede6c97-a3c5-4f42-adcc-dfe66fc7a2a8", + "name": "Wait", + "webhookId": "36d32e2d-a9cf-4dc7-b138-70d7966c96d7" + }, + { + "parameters": { + "jsCode": "throw Error('test!!!')" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [660, -160], + "id": "05ad4f4d-10fc-4bec-9145-0e5c20f455c4", + "name": "Code1" + } + ], + "connections": { + "When clicking ‘Test workflow’": { + "main": [ + [ + { + "node": "Code", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code": { + "main": [ + [ + { + "node": "Loop Over Items", + "type": "main", + "index": 0 + } + ] + ] + }, + "Loop Over Items": { + "main": [ + [ + { + "node": "Code1", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Wait", + "type": "main", + "index": 0 + } + ] + ] + }, + "Wait": { + "main": [ + [ + { + "node": "Loop Over Items", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "pinData": {}, + "meta": { + "instanceId": "74f18f142a6ddf6880a6f8c5b30685f621743a7a66d8d94c8f164937f4dd5515" + } +} diff --git a/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogsOverviewPanel.vue b/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogsOverviewPanel.vue index 0848f36d94..2181b5a9f4 100644 --- a/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogsOverviewPanel.vue +++ b/packages/frontend/editor-ui/src/components/CanvasChat/future/components/LogsOverviewPanel.vue @@ -167,6 +167,7 @@ watch(