From 8fff83032cb0a47a05460fcbbf8bd54bfd8956ce Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Thu, 10 Jul 2025 09:30:27 +0200 Subject: [PATCH] fix(editor): Open failed node in failed execution from sub-workflow node (#17076) Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> --- .../frontend/@n8n/i18n/src/locales/en.json | 1 + .../components/Error/NodeErrorView.test.ts | 162 +++++++++++++++--- .../src/components/Error/NodeErrorView.vue | 41 ++++- .../editor-ui/src/components/RunData.test.ts | 41 +++-- .../editor-ui/src/components/RunData.vue | 6 +- .../src/components/WorkflowPreview.vue | 3 + .../workflow/WorkflowExecutionsPreview.vue | 2 + .../composables/useCanvasOperations.test.ts | 55 ++++++ .../src/composables/useCanvasOperations.ts | 14 +- packages/frontend/editor-ui/src/router.ts | 2 +- .../frontend/editor-ui/src/views/NodeView.vue | 6 +- 11 files changed, 284 insertions(+), 49 deletions(-) diff --git a/packages/frontend/@n8n/i18n/src/locales/en.json b/packages/frontend/@n8n/i18n/src/locales/en.json index 588a68dc71..3a8218267c 100644 --- a/packages/frontend/@n8n/i18n/src/locales/en.json +++ b/packages/frontend/@n8n/i18n/src/locales/en.json @@ -1523,6 +1523,7 @@ "nodeView.showError.mounted2.message": "There was a problem initializing the workflow", "nodeView.showError.mounted2.title": "Init Problem", "nodeView.showError.openExecution.title": "Problem loading execution", + "nodeView.showError.openExecution.node": "Problem opening node in execution", "nodeView.showError.openWorkflow.title": "Problem opening workflow", "nodeView.showError.stopExecution.title": "Problem stopping execution", "nodeView.showError.stopWaitingForWebhook.title": "Problem deleting test webhook", diff --git a/packages/frontend/editor-ui/src/components/Error/NodeErrorView.test.ts b/packages/frontend/editor-ui/src/components/Error/NodeErrorView.test.ts index bdea24f545..1e15f545cf 100644 --- a/packages/frontend/editor-ui/src/components/Error/NodeErrorView.test.ts +++ b/packages/frontend/editor-ui/src/components/Error/NodeErrorView.test.ts @@ -1,30 +1,51 @@ -import { createComponentRenderer } from '@/__tests__/render'; - -import NodeErrorView from '@/components/Error/NodeErrorView.vue'; - +import { reactive } from 'vue'; import { createTestingPinia } from '@pinia/testing'; +import userEvent from '@testing-library/user-event'; import type { NodeError } from 'n8n-workflow'; +import { mockedStore } from '@/__tests__/utils'; +import { createComponentRenderer } from '@/__tests__/render'; +import type { IExecutionResponse } from '@/Interface'; +import NodeErrorView from '@/components/Error/NodeErrorView.vue'; import { useAssistantStore } from '@/stores/assistant.store'; import { useNodeTypesStore } from '@/stores/nodeTypes.store'; -import { mockedStore } from '@/__tests__/utils'; -import userEvent from '@testing-library/user-event'; import { useNDVStore } from '@/stores/ndv.store'; +import { useWorkflowsStore } from '@/stores/workflows.store'; -const renderComponent = createComponentRenderer(NodeErrorView); +const mockRouterResolve = vi.fn(() => ({ + href: '', +})); + +vi.mock('vue-router', () => ({ + useRouter: () => ({ + resolve: mockRouterResolve, + }), + useRoute: () => reactive({ meta: {} }), + RouterLink: vi.fn(), +})); + +// Mock window.open +Object.defineProperty(window, 'open', { + value: vi.fn(), + writable: true, +}); let mockAiAssistantStore: ReturnType>; let mockNodeTypeStore: ReturnType>; -let mockNdvStore: ReturnType>; +let mockNDVStore: ReturnType>; +let mockWorkflowsStore: ReturnType>; + +const renderComponent = createComponentRenderer(NodeErrorView); describe('NodeErrorView.vue', () => { let error: NodeError; beforeEach(() => { createTestingPinia(); - mockAiAssistantStore = mockedStore(useAssistantStore); mockNodeTypeStore = mockedStore(useNodeTypesStore); - mockNdvStore = mockedStore(useNDVStore); + mockNDVStore = mockedStore(useNDVStore); + mockWorkflowsStore = mockedStore(useWorkflowsStore); + //@ts-expect-error error = { name: 'NodeOperationError', @@ -59,7 +80,7 @@ describe('NodeErrorView.vue', () => { vi.clearAllMocks(); }); - it('renders an Error with a messages array', async () => { + it('renders an Error with a messages array', () => { const { getByTestId } = renderComponent({ props: { error: { @@ -74,7 +95,7 @@ describe('NodeErrorView.vue', () => { expect(errorMessage).toHaveTextContent('Unexpected identifier [line 1]'); }); - it('renders an Error with a message string', async () => { + it('renders an Error with a message string', () => { const { getByTestId } = renderComponent({ props: { error: { @@ -89,8 +110,8 @@ describe('NodeErrorView.vue', () => { expect(errorMessage).toHaveTextContent('Unexpected identifier [line 1]'); }); - it('should not render AI assistant button when error happens in deprecated function node', async () => { - //@ts-expect-error + it('should not render AI assistant button when error happens in deprecated function node', () => { + // @ts-expect-error - Mock node type store method mockNodeTypeStore.getNodeType = vi.fn(() => ({ type: 'n8n-nodes-base.function', typeVersion: 1, @@ -168,20 +189,111 @@ describe('NodeErrorView.vue', () => { expect(getByTestId('ask-assistant-button')).toBeInTheDocument(); }); - it('open error node details when open error node is clicked', async () => { - const { getByTestId, emitted } = renderComponent({ - props: { - error: { - ...error, - name: 'NodeOperationError', - functionality: 'configuration-node', + describe('onOpenErrorNodeDetailClick', () => { + it('does nothing when error has no node', async () => { + const errorWithoutNode = { + name: 'NodeOperationError', + functionality: 'configuration-node', + message: 'Error without node', + node: undefined, + }; + + const { queryByTestId } = renderComponent({ + props: { + error: errorWithoutNode, }, - }, + }); + + const button = queryByTestId('node-error-view-open-node-button'); + + // If there's no node, button should not render or if it does, clicking it should do nothing + if (button) { + await userEvent.click(button); + } + + expect(window.open).not.toHaveBeenCalled(); + expect(mockNDVStore.activeNodeName).toBeNull(); }); - await userEvent.click(getByTestId('node-error-view-open-node-button')); + it('opens new window when error has different workflow and execution IDs', async () => { + mockWorkflowsStore.workflowId = 'current-workflow-id'; + mockWorkflowsStore.getWorkflowExecution = { + id: 'current-execution-id', + } as IExecutionResponse; - expect(emitted().click).toHaveLength(1); - expect(mockNdvStore.activeNodeName).toBe(error.node.name); + const testError = { + ...error, + name: 'NodeOperationError', + functionality: 'configuration-node', + workflowId: 'different-workflow-id', + executionId: 'different-execution-id', + }; + + const { getByTestId } = renderComponent({ + props: { + error: testError, + }, + }); + + const button = getByTestId('node-error-view-open-node-button'); + await userEvent.click(button); + + expect(mockRouterResolve).toHaveBeenCalledWith({ + name: 'ExecutionPreview', + params: { + name: 'different-workflow-id', + executionId: 'different-execution-id', + nodeId: 'd1ce5dc9-f9ae-4ac6-84e5-0696ba175dd9', + }, + }); + expect(window.open).toHaveBeenCalled(); + }); + + it('sets active node name when error is in current workflow/execution', async () => { + mockWorkflowsStore.workflowId = 'current-workflow-id'; + mockWorkflowsStore.getWorkflowExecution = { + id: 'current-execution-id', + } as IExecutionResponse; + + const testError = { + ...error, + name: 'NodeOperationError', + functionality: 'configuration-node', + workflowId: 'current-workflow-id', + executionId: 'current-execution-id', + }; + + const { getByTestId } = renderComponent({ + props: { + error: testError, + }, + }); + + const button = getByTestId('node-error-view-open-node-button'); + await userEvent.click(button); + + expect(window.open).not.toHaveBeenCalled(); + expect(mockNDVStore.activeNodeName).toBe('ErrorCode'); + }); + + it('sets active node name when error has no workflow/execution IDs', async () => { + const testError = { + ...error, + name: 'NodeOperationError', + functionality: 'configuration-node', + }; + + const { getByTestId } = renderComponent({ + props: { + error: testError, + }, + }); + + const button = getByTestId('node-error-view-open-node-button'); + await userEvent.click(button); + + expect(window.open).not.toHaveBeenCalled(); + expect(mockNDVStore.activeNodeName).toBe('ErrorCode'); + }); }); }); diff --git a/packages/frontend/editor-ui/src/components/Error/NodeErrorView.vue b/packages/frontend/editor-ui/src/components/Error/NodeErrorView.vue index 9531796340..dcf4e17d1d 100644 --- a/packages/frontend/editor-ui/src/components/Error/NodeErrorView.vue +++ b/packages/frontend/editor-ui/src/components/Error/NodeErrorView.vue @@ -1,10 +1,12 @@