Skip to content

Commit 8cba100

Browse files
author
Ivan Atanasov
authored
fix(editor): Show only error title and 'Open errored node' button; hide 'Ask Assistant' in root for sub-node errors (#11573)
1 parent 40c8882 commit 8cba100

File tree

3 files changed

+155
-56
lines changed

3 files changed

+155
-56
lines changed
Lines changed: 121 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,69 @@
11
import { createComponentRenderer } from '@/__tests__/render';
2-
import { SETTINGS_STORE_DEFAULT_STATE } from '@/__tests__/utils';
2+
33
import NodeErrorView from '@/components/Error/NodeErrorView.vue';
4-
import { STORES } from '@/constants';
4+
55
import { createTestingPinia } from '@pinia/testing';
6-
import { type INode } from 'n8n-workflow';
6+
import type { NodeError } from 'n8n-workflow';
77
import { useAssistantStore } from '@/stores/assistant.store';
88
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
9+
import { mockedStore } from '@/__tests__/utils';
10+
import userEvent from '@testing-library/user-event';
11+
import { useNDVStore } from '@/stores/ndv.store';
912

10-
const DEFAULT_SETUP = {
11-
pinia: createTestingPinia({
12-
initialState: {
13-
[STORES.SETTINGS]: SETTINGS_STORE_DEFAULT_STATE,
14-
},
15-
}),
16-
};
13+
const renderComponent = createComponentRenderer(NodeErrorView);
1714

18-
const renderComponent = createComponentRenderer(NodeErrorView, DEFAULT_SETUP);
15+
let mockAiAssistantStore: ReturnType<typeof mockedStore<typeof useAssistantStore>>;
16+
let mockNodeTypeStore: ReturnType<typeof mockedStore<typeof useNodeTypesStore>>;
17+
let mockNdvStore: ReturnType<typeof mockedStore<typeof useNDVStore>>;
1918

2019
describe('NodeErrorView.vue', () => {
21-
let mockNode: INode;
22-
afterEach(() => {
23-
mockNode = {
24-
parameters: {
25-
mode: 'runOnceForAllItems',
26-
language: 'javaScript',
27-
jsCode: 'cons error = 9;',
28-
notice: '',
20+
let error: NodeError;
21+
22+
beforeEach(() => {
23+
createTestingPinia();
24+
25+
mockAiAssistantStore = mockedStore(useAssistantStore);
26+
mockNodeTypeStore = mockedStore(useNodeTypesStore);
27+
mockNdvStore = mockedStore(useNDVStore);
28+
//@ts-expect-error
29+
error = {
30+
name: 'NodeOperationError',
31+
message: 'Test error message',
32+
description: 'Test error description',
33+
context: {
34+
descriptionKey: 'noInputConnection',
35+
nodeCause: 'Test node cause',
36+
runIndex: '1',
37+
itemIndex: '2',
38+
parameter: 'testParameter',
39+
data: { key: 'value' },
40+
causeDetailed: 'Detailed cause',
2941
},
30-
id: 'd1ce5dc9-f9ae-4ac6-84e5-0696ba175dd9',
31-
name: 'Code',
32-
type: 'n8n-nodes-base.code',
33-
typeVersion: 2,
34-
position: [940, 240],
42+
node: {
43+
parameters: {
44+
mode: 'runOnceForAllItems',
45+
language: 'javaScript',
46+
jsCode: 'cons error = 9;',
47+
notice: '',
48+
},
49+
id: 'd1ce5dc9-f9ae-4ac6-84e5-0696ba175dd9',
50+
name: 'ErrorCode',
51+
type: 'n8n-nodes-base.code',
52+
typeVersion: 2,
53+
position: [940, 240],
54+
},
55+
stack: 'Test stack trace',
3556
};
57+
});
58+
afterEach(() => {
3659
vi.clearAllMocks();
3760
});
3861

3962
it('renders an Error with a messages array', async () => {
4063
const { getByTestId } = renderComponent({
4164
props: {
4265
error: {
43-
node: mockNode,
66+
node: error.node,
4467
messages: ['Unexpected identifier [line 1]'],
4568
},
4669
},
@@ -55,7 +78,7 @@ describe('NodeErrorView.vue', () => {
5578
const { getByTestId } = renderComponent({
5679
props: {
5780
error: {
58-
node: mockNode,
81+
node: error.node,
5982
message: 'Unexpected identifier [line 1]',
6083
},
6184
},
@@ -67,24 +90,20 @@ describe('NodeErrorView.vue', () => {
6790
});
6891

6992
it('should not render AI assistant button when error happens in deprecated function node', async () => {
70-
const aiAssistantStore = useAssistantStore(DEFAULT_SETUP.pinia);
71-
const nodeTypeStore = useNodeTypesStore(DEFAULT_SETUP.pinia);
72-
7393
//@ts-expect-error
74-
nodeTypeStore.getNodeType = vi.fn(() => ({
94+
mockNodeTypeStore.getNodeType = vi.fn(() => ({
7595
type: 'n8n-nodes-base.function',
7696
typeVersion: 1,
7797
hidden: true,
7898
}));
7999

80-
//@ts-expect-error
81-
aiAssistantStore.canShowAssistantButtonsOnCanvas = true;
100+
mockAiAssistantStore.canShowAssistantButtonsOnCanvas = true;
82101

83102
const { queryByTestId } = renderComponent({
84103
props: {
85104
error: {
86105
node: {
87-
...mockNode,
106+
...error.node,
88107
type: 'n8n-nodes-base.function',
89108
typeVersion: 1,
90109
},
@@ -96,4 +115,73 @@ describe('NodeErrorView.vue', () => {
96115

97116
expect(aiAssistantButton).toBeNull();
98117
});
118+
119+
it('renders error message', () => {
120+
const { getByTestId } = renderComponent({
121+
props: { error },
122+
});
123+
expect(getByTestId('node-error-message').textContent).toContain('Test error message');
124+
});
125+
126+
it('renders error description', () => {
127+
const { getByTestId } = renderComponent({
128+
props: { error },
129+
});
130+
expect(getByTestId('node-error-description').innerHTML).toContain(
131+
'This node has no input data. Please make sure this node is connected to another node.',
132+
);
133+
});
134+
135+
it('renders stack trace', () => {
136+
const { getByText } = renderComponent({
137+
props: { error },
138+
});
139+
expect(getByText('Test stack trace')).toBeTruthy();
140+
});
141+
142+
it('renders open node button when the error is in sub node', () => {
143+
const { getByTestId, queryByTestId } = renderComponent({
144+
props: {
145+
error: {
146+
...error,
147+
name: 'NodeOperationError',
148+
functionality: 'configuration-node',
149+
},
150+
},
151+
});
152+
153+
expect(getByTestId('node-error-view-open-node-button')).toHaveTextContent('Open errored node');
154+
155+
expect(queryByTestId('ask-assistant-button')).not.toBeInTheDocument();
156+
});
157+
158+
it('does not renders open node button when the error is in sub node', () => {
159+
mockAiAssistantStore.canShowAssistantButtonsOnCanvas = true;
160+
const { getByTestId, queryByTestId } = renderComponent({
161+
props: {
162+
error,
163+
},
164+
});
165+
166+
expect(queryByTestId('node-error-view-open-node-button')).not.toBeInTheDocument();
167+
168+
expect(getByTestId('ask-assistant-button')).toBeInTheDocument();
169+
});
170+
171+
it('open error node details when open error node is clicked', async () => {
172+
const { getByTestId, emitted } = renderComponent({
173+
props: {
174+
error: {
175+
...error,
176+
name: 'NodeOperationError',
177+
functionality: 'configuration-node',
178+
},
179+
},
180+
});
181+
182+
await userEvent.click(getByTestId('node-error-view-open-node-button'));
183+
184+
expect(emitted().click).toHaveLength(1);
185+
expect(mockNdvStore.activeNodeName).toBe(error.node.name);
186+
});
99187
});

packages/editor-ui/src/components/Error/NodeErrorView.vue

Lines changed: 33 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ const prepareRawMessages = computed(() => {
117117
});
118118
119119
const isAskAssistantAvailable = computed(() => {
120-
if (!node.value) {
120+
if (!node.value || isSubNodeError.value) {
121121
return false;
122122
}
123123
const isCustomNode = node.value.type === undefined || isCommunityPackageName(node.value.type);
@@ -132,6 +132,13 @@ const assistantAlreadyAsked = computed(() => {
132132
});
133133
});
134134
135+
const isSubNodeError = computed(() => {
136+
return (
137+
props.error.name === 'NodeOperationError' &&
138+
(props.error as NodeOperationError).functionality === 'configuration-node'
139+
);
140+
});
141+
135142
function nodeVersionTag(nodeType: NodeError['node']): string {
136143
if (!nodeType || ('hidden' in nodeType && nodeType.hidden)) {
137144
return i18n.baseText('nodeSettings.deprecated');
@@ -153,19 +160,6 @@ function prepareDescription(description: string): string {
153160
}
154161
155162
function getErrorDescription(): string {
156-
const isSubNodeError =
157-
props.error.name === 'NodeOperationError' &&
158-
(props.error as NodeOperationError).functionality === 'configuration-node';
159-
160-
if (isSubNodeError) {
161-
return prepareDescription(
162-
props.error.description +
163-
i18n.baseText('pushConnection.executionError.openNode', {
164-
interpolate: { node: props.error.node.name },
165-
}),
166-
);
167-
}
168-
169163
if (props.error.context?.descriptionKey) {
170164
const interpolate = {
171165
nodeCause: props.error.context.nodeCause as string,
@@ -205,13 +199,10 @@ function addItemIndexSuffix(message: string): string {
205199
function getErrorMessage(): string {
206200
let message = '';
207201
208-
const isSubNodeError =
209-
props.error.name === 'NodeOperationError' &&
210-
(props.error as NodeOperationError).functionality === 'configuration-node';
211202
const isNonEmptyString = (value?: unknown): value is string =>
212203
!!value && typeof value === 'string';
213204
214-
if (isSubNodeError) {
205+
if (isSubNodeError.value) {
215206
message = i18n.baseText('nodeErrorView.errorSubNode', {
216207
interpolate: { node: props.error.node.name },
217208
});
@@ -390,6 +381,10 @@ function nodeIsHidden() {
390381
return nodeType?.hidden ?? false;
391382
}
392383
384+
const onOpenErrorNodeDetailClick = () => {
385+
ndvStore.activeNodeName = props.error.node.name;
386+
};
387+
393388
async function onAskAssistantClick() {
394389
const { message, lineNumber, description } = props.error;
395390
const sessionInProgress = !assistantStore.isSessionEnded;
@@ -428,14 +423,25 @@ async function onAskAssistantClick() {
428423
</div>
429424
</div>
430425
<div
431-
v-if="error.description || error.context?.descriptionKey"
426+
v-if="(error.description || error.context?.descriptionKey) && !isSubNodeError"
432427
data-test-id="node-error-description"
433428
class="node-error-view__header-description"
434429
v-n8n-html="getErrorDescription()"
435430
></div>
431+
432+
<div v-if="isSubNodeError">
433+
<n8n-button
434+
icon="arrow-right"
435+
type="secondary"
436+
:label="i18n.baseText('pushConnection.executionError.openNode')"
437+
class="node-error-view__button"
438+
data-test-id="node-error-view-open-node-button"
439+
@click="onOpenErrorNodeDetailClick"
440+
/>
441+
</div>
436442
<div
437443
v-if="isAskAssistantAvailable"
438-
class="node-error-view__assistant-button"
444+
class="node-error-view__button"
439445
data-test-id="node-error-view-ask-assistant-button"
440446
>
441447
<InlineAskAssistantButton :asked="assistantAlreadyAsked" @click="onAskAssistantClick" />
@@ -696,9 +702,14 @@ async function onAskAssistantClick() {
696702
}
697703
}
698704
699-
&__assistant-button {
705+
&__button {
700706
margin-left: var(--spacing-s);
701707
margin-bottom: var(--spacing-xs);
708+
flex-direction: row-reverse;
709+
span {
710+
margin-right: var(--spacing-5xs);
711+
margin-left: var(--spacing-5xs);
712+
}
702713
}
703714
704715
&__debugging {
@@ -831,7 +842,7 @@ async function onAskAssistantClick() {
831842
}
832843
}
833844
834-
.node-error-view__assistant-button {
845+
.node-error-view__button {
835846
margin-top: var(--spacing-xs);
836847
}
837848
</style>

packages/editor-ui/src/plugins/i18n/locales/en.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1498,7 +1498,7 @@
14981498
"pushConnection.executionFailed": "Execution failed",
14991499
"pushConnection.executionFailed.message": "There might not be enough memory to finish the execution. Tips for avoiding this <a target=\"_blank\" href=\"https://docs.n8n.io/flow-logic/error-handling/memory-errors/\">here</a>",
15001500
"pushConnection.executionError": "There was a problem executing the workflow{error}",
1501-
"pushConnection.executionError.openNode": " <a data-action='openNodeDetail' data-action-parameter-node='{node}'>Open node</a>",
1501+
"pushConnection.executionError.openNode": "Open errored node",
15021502
"pushConnection.executionError.details": "<br /><strong>{details}</strong>",
15031503
"prompts.productTeamMessage": "Our product team will get in touch personally",
15041504
"prompts.npsSurvey.recommendationQuestion": "How likely are you to recommend n8n to a friend or colleague?",

0 commit comments

Comments
 (0)