fix(editor): Show error toast for failed executions (#15388)

This commit is contained in:
Raúl Gómez Morales
2025-05-16 10:53:55 +02:00
committed by GitHub
parent 954b66218f
commit e68149bbc7
5 changed files with 289 additions and 110 deletions

View File

@@ -1,9 +1,16 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { displayForm, executionFilterToQueryFilter, waitingNodeTooltip } from './executionUtils';
import type { INode, IRunData, IPinData } from 'n8n-workflow';
import {
displayForm,
executionFilterToQueryFilter,
waitingNodeTooltip,
getExecutionErrorMessage,
getExecutionErrorToastConfiguration,
} from './executionUtils';
import type { INode, IRunData, IPinData, ExecutionError } from 'n8n-workflow';
import { type INodeUi } from '../Interface';
import { CHAT_TRIGGER_NODE_TYPE, FORM_TRIGGER_NODE_TYPE, GITHUB_NODE_TYPE } from '@/constants';
import { createTestNode } from '@/__tests__/mocks';
import type { VNode } from 'vue';
const WAIT_NODE_TYPE = 'waitNode';
@@ -24,13 +31,15 @@ vi.mock('@/stores/workflows.store', () => ({
vi.mock('@/plugins/i18n', () => ({
i18n: {
baseText: (key: string) => {
baseText: (key: string, options?: { interpolate?: { error?: string; details?: string } }) => {
const texts: { [key: string]: string } = {
'ndv.output.waitNodeWaiting': 'Waiting for execution to resume...',
'ndv.output.waitNodeWaitingForFormSubmission': 'Waiting for form submission: ',
'ndv.output.waitNodeWaitingForWebhook': 'Waiting for webhook call: ',
'ndv.output.githubNodeWaitingForWebhook': 'Waiting for webhook call: ',
'ndv.output.sendAndWaitWaitingApproval': 'Waiting for approval...',
'pushConnection.executionError': `Execution error${options?.interpolate?.error}`,
'pushConnection.executionError.details': `Details: ${options?.interpolate?.details}`,
};
return texts[key] || key;
},
@@ -323,3 +332,123 @@ describe('waitingNodeTooltip', () => {
);
});
});
const executionErrorFactory = (error: Record<string, unknown>) =>
error as unknown as ExecutionError;
describe('getExecutionErrorMessage', () => {
it('returns error.message when lastNodeExecuted and error are present', () => {
const result = getExecutionErrorMessage({
error: executionErrorFactory({ message: 'Node failed' }),
lastNodeExecuted: 'Node1',
});
expect(result).toBe('Node failed');
});
it('uses fallback translation when only error.message is provided', () => {
const result = getExecutionErrorMessage({
error: executionErrorFactory({ message: 'Something went wrong' }),
});
expect(result).toBe('Execution error.Details: Something went wrong');
});
it('includes node name if error.node is a string', () => {
const result = getExecutionErrorMessage({
error: executionErrorFactory({ message: 'Failed', node: 'MyNode' }),
});
expect(result).toBe('Execution error.Details: MyNode: Failed');
});
it('includes node.name if error.node is an object', () => {
const result = getExecutionErrorMessage({
error: executionErrorFactory({ message: 'Crashed', node: { name: 'Step1' } }),
});
expect(result).toBe('Execution error.Details: Step1: Crashed');
});
it('uses default fallback when no error or lastNodeExecuted', () => {
const result = getExecutionErrorMessage({});
expect(result).toBe('Execution error!');
});
});
describe('getExecutionErrorToastConfiguration', () => {
it('returns config for SubworkflowOperationError', () => {
const result = getExecutionErrorToastConfiguration({
error: executionErrorFactory({
name: 'SubworkflowOperationError',
message: 'Subworkflow failed',
description: 'Workflow XYZ failed',
}),
lastNodeExecuted: 'NodeA',
});
expect(result).toEqual({
title: 'Subworkflow failed',
message: 'Workflow XYZ failed',
});
});
it('returns config for configuration-node error with node name', () => {
const result = getExecutionErrorToastConfiguration({
error: executionErrorFactory({
name: 'NodeOperationError',
message: 'Node failed',
description: 'Bad configuration',
functionality: 'configuration-node',
node: { name: 'TestNode' },
}),
});
expect(result.title).toBe('Error in sub-node TestNode');
expect((result.message as VNode).props).toEqual({
errorMessage: 'Bad configuration',
nodeName: 'TestNode',
});
});
it('returns config for configuration-node error without node name', () => {
const result = getExecutionErrorToastConfiguration({
error: executionErrorFactory({
name: 'NodeApiError',
message: 'API failed',
description: 'Missing credentials',
functionality: 'configuration-node',
node: {},
}),
});
expect(result.title).toBe('Problem executing workflow');
expect((result.message as VNode).props).toEqual({
errorMessage: 'Missing credentials',
nodeName: '',
});
});
it('returns generic config when error type is not special', () => {
const result = getExecutionErrorToastConfiguration({
error: executionErrorFactory({
name: 'UnknownError',
message: 'Something broke',
}),
lastNodeExecuted: 'NodeX',
});
expect(result).toEqual({
title: 'Problem in node NodeX',
message: 'Something broke',
});
});
it('returns generic config without lastNodeExecuted', () => {
const result = getExecutionErrorToastConfiguration({
error: executionErrorFactory({
name: 'UnknownError',
message: 'Something broke',
}),
});
expect(result).toEqual({
title: 'Problem executing workflow',
message: 'Execution error.Details: Something broke',
});
});
});