Files
n8n-enterprise-unlocked/packages/frontend/editor-ui/src/composables/useRunWorkflow.test.ts

1103 lines
35 KiB
TypeScript

import { setActivePinia } from 'pinia';
import { createTestingPinia } from '@pinia/testing';
import { useRouter } from 'vue-router';
import type router from 'vue-router';
import { ExpressionError, NodeConnectionTypes } from 'n8n-workflow';
import type {
IPinData,
IRunData,
Workflow,
IExecuteData,
ITaskData,
INodeConnections,
INode,
} from 'n8n-workflow';
import { useRunWorkflow } from '@/composables/useRunWorkflow';
import type { IExecutionResponse, IStartRunData } from '@/Interface';
import type { WorkflowData } from '@n8n/rest-api-client/api/workflows';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { useUIStore } from '@/stores/ui.store';
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
import { useToast } from './useToast';
import { useI18n } from '@n8n/i18n';
import { captor, mock } from 'vitest-mock-extended';
import { useSettingsStore } from '@/stores/settings.store';
import { usePushConnectionStore } from '@/stores/pushConnection.store';
import { createTestNode, createTestWorkflow } from '@/__tests__/mocks';
import { waitFor } from '@testing-library/vue';
import { useAgentRequestStore } from '@n8n/stores/useAgentRequestStore';
import { SLACK_TRIGGER_NODE_TYPE, MANUAL_TRIGGER_NODE_TYPE } from '@/constants';
vi.mock('@/stores/workflows.store', () => {
const storeState: Partial<ReturnType<typeof useWorkflowsStore>> & {
activeExecutionId: string | null | undefined;
} = {
allNodes: [],
runWorkflow: vi.fn(),
subWorkflowExecutionError: null,
getWorkflowRunData: null,
workflowExecutionData: null,
setWorkflowExecutionData: vi.fn(),
activeExecutionId: undefined,
previousExecutionId: undefined,
nodesIssuesExist: false,
executionWaitingForWebhook: false,
getCurrentWorkflow: vi.fn().mockReturnValue({ id: '123' }),
getNodeByName: vi
.fn()
.mockImplementation((name) =>
name === 'Test node' ? { name: 'Test node', id: 'Test id' } : undefined,
),
getExecution: vi.fn(),
checkIfNodeHasChatParent: vi.fn(),
getParametersLastUpdate: vi.fn(),
getPinnedDataLastUpdate: vi.fn(),
getPinnedDataLastRemovedAt: vi.fn(),
incomingConnectionsByNodeName: vi.fn(),
outgoingConnectionsByNodeName: vi.fn(),
markExecutionAsStopped: vi.fn(),
setActiveExecutionId: vi.fn((id: string | null | undefined) => {
storeState.activeExecutionId = id;
}),
};
return {
useWorkflowsStore: vi.fn().mockReturnValue(storeState),
};
});
vi.mock('@/stores/parameterOverrides.store', () => {
const storeState: Partial<ReturnType<typeof useAgentRequestStore>> & {} = {
agentRequests: {},
getAgentRequest: vi.fn(),
};
return {
useAgentRequestStore: vi.fn().mockReturnValue(storeState),
};
});
vi.mock('@/stores/pushConnection.store', () => ({
usePushConnectionStore: vi.fn().mockReturnValue({
isConnected: true,
}),
}));
vi.mock('@/composables/useTelemetry', () => ({
useTelemetry: vi.fn().mockReturnValue({ track: vi.fn() }),
}));
vi.mock('@n8n/i18n', () => ({
useI18n: vi.fn().mockReturnValue({ baseText: vi.fn().mockImplementation((key) => key) }),
}));
vi.mock('@/composables/useExternalHooks', () => ({
useExternalHooks: vi.fn().mockReturnValue({
run: vi.fn(),
}),
}));
vi.mock('@/composables/useToast', () => ({
useToast: vi.fn().mockReturnValue({
clearAllStickyNotifications: vi.fn(),
showMessage: vi.fn(),
showError: vi.fn(),
}),
}));
vi.mock('@/composables/useWorkflowHelpers', () => ({
useWorkflowHelpers: vi.fn().mockReturnValue({
getCurrentWorkflow: vi.fn(),
saveCurrentWorkflow: vi.fn(),
getWorkflowDataToSave: vi.fn(),
setDocumentTitle: vi.fn(),
executeData: vi.fn(),
getNodeTypes: vi.fn().mockReturnValue([]),
}),
}));
vi.mock('@/composables/useNodeHelpers', () => ({
useNodeHelpers: vi.fn().mockReturnValue({
updateNodesExecutionIssues: vi.fn(),
}),
}));
vi.mock('vue-router', async (importOriginal) => {
const { RouterLink } = await importOriginal<typeof router>();
return {
RouterLink,
useRouter: vi.fn().mockReturnValue({
push: vi.fn(),
}),
useRoute: vi.fn(),
};
});
describe('useRunWorkflow({ router })', () => {
let pushConnectionStore: ReturnType<typeof usePushConnectionStore>;
let uiStore: ReturnType<typeof useUIStore>;
let workflowsStore: ReturnType<typeof useWorkflowsStore>;
let router: ReturnType<typeof useRouter>;
let workflowHelpers: ReturnType<typeof useWorkflowHelpers>;
let settingsStore: ReturnType<typeof useSettingsStore>;
let agentRequestStore: ReturnType<typeof useAgentRequestStore>;
beforeEach(() => {
const pinia = createTestingPinia({ stubActions: false });
setActivePinia(pinia);
pushConnectionStore = usePushConnectionStore();
uiStore = useUIStore();
workflowsStore = useWorkflowsStore();
settingsStore = useSettingsStore();
agentRequestStore = useAgentRequestStore();
router = useRouter();
workflowHelpers = useWorkflowHelpers();
});
afterEach(() => {
vi.mocked(workflowsStore).setActiveExecutionId(undefined);
vi.clearAllMocks();
});
describe('runWorkflowApi()', () => {
it('should throw an error if push connection is not active', async () => {
const { runWorkflowApi } = useRunWorkflow({ router });
vi.mocked(pushConnectionStore).isConnected = false;
await expect(runWorkflowApi({} as IStartRunData)).rejects.toThrow(
'workflowRun.noActiveConnectionToTheServer',
);
});
it('should successfully run a workflow', async () => {
const { runWorkflowApi } = useRunWorkflow({ router });
vi.mocked(pushConnectionStore).isConnected = true;
const mockResponse = { executionId: '123', waitingForWebhook: false };
vi.mocked(workflowsStore).runWorkflow.mockResolvedValue(mockResponse);
const response = await runWorkflowApi({} as IStartRunData);
expect(response).toEqual(mockResponse);
expect(workflowsStore.setActiveExecutionId).toHaveBeenNthCalledWith(1, null);
expect(workflowsStore.setActiveExecutionId).toHaveBeenNthCalledWith(2, '123');
expect(workflowsStore.executionWaitingForWebhook).toBe(false);
});
it('should prevent running a webhook-based workflow that has issues', async () => {
const { runWorkflowApi } = useRunWorkflow({ router });
vi.mocked(workflowsStore).nodesIssuesExist = true;
vi.mocked(workflowsStore).runWorkflow.mockResolvedValue({
executionId: '123',
waitingForWebhook: true,
});
await expect(runWorkflowApi({} as IStartRunData)).rejects.toThrow(
'workflowRun.showError.resolveOutstandingIssues',
);
vi.mocked(workflowsStore).nodesIssuesExist = false;
});
it('should handle workflow run failure', async () => {
const { runWorkflowApi } = useRunWorkflow({ router });
vi.mocked(pushConnectionStore).isConnected = true;
vi.mocked(workflowsStore).runWorkflow.mockRejectedValue(new Error('Failed to run workflow'));
await expect(runWorkflowApi({} as IStartRunData)).rejects.toThrow('Failed to run workflow');
expect(workflowsStore.setActiveExecutionId).toHaveBeenCalledWith(undefined);
});
it('should set waitingForWebhook if response indicates waiting', async () => {
const { runWorkflowApi } = useRunWorkflow({ router });
vi.mocked(pushConnectionStore).isConnected = true;
const mockResponse = { executionId: '123', waitingForWebhook: true };
vi.mocked(workflowsStore).runWorkflow.mockResolvedValue(mockResponse);
const response = await runWorkflowApi({} as IStartRunData);
expect(response).toEqual(mockResponse);
expect(workflowsStore.executionWaitingForWebhook).toBe(true);
});
});
describe('runWorkflow()', () => {
it('should prevent execution and show error message when workflow is active with single webhook trigger', async () => {
const pinia = createTestingPinia({ stubActions: false });
setActivePinia(pinia);
const toast = useToast();
const i18n = useI18n();
const { runWorkflow } = useRunWorkflow({ router });
vi.mocked(workflowsStore).isWorkflowActive = true;
vi.mocked(useWorkflowHelpers()).getWorkflowDataToSave.mockResolvedValue({
nodes: [
{
name: 'Slack',
type: SLACK_TRIGGER_NODE_TYPE,
disabled: false,
},
],
} as unknown as WorkflowData);
const result = await runWorkflow({});
expect(result).toBeUndefined();
expect(toast.showMessage).toHaveBeenCalledWith({
title: i18n.baseText('workflowRun.showError.deactivate'),
message: i18n.baseText('workflowRun.showError.productionActive', {
interpolate: { nodeName: 'Webhook' },
}),
type: 'error',
});
});
it('should execute the workflow if the single webhook trigger has pin data', async () => {
const pinia = createTestingPinia({ stubActions: false });
setActivePinia(pinia);
const toast = useToast();
const i18n = useI18n();
const { runWorkflow } = useRunWorkflow({ router });
vi.mocked(workflowsStore).isWorkflowActive = true;
vi.mocked(useWorkflowHelpers()).getWorkflowDataToSave.mockResolvedValue({
nodes: [
{
name: 'Slack',
type: SLACK_TRIGGER_NODE_TYPE,
disabled: false,
},
],
pinData: {
Slack: [{ json: { value: 'data2' } }],
},
} as unknown as WorkflowData);
const mockExecutionResponse = { executionId: '123' };
vi.mocked(uiStore).activeActions = [''];
vi.mocked(workflowHelpers).getCurrentWorkflow.mockReturnValue({
name: 'Test Workflow',
} as unknown as Workflow);
vi.mocked(workflowsStore).runWorkflow.mockResolvedValue(mockExecutionResponse);
vi.mocked(workflowsStore).nodesIssuesExist = true;
vi.mocked(workflowsStore).getWorkflowRunData = {
NodeName: [],
};
const result = await runWorkflow({});
expect(result).toEqual(mockExecutionResponse);
expect(toast.showMessage).not.toHaveBeenCalledWith({
title: i18n.baseText('workflowRun.showError.deactivate'),
message: i18n.baseText('workflowRun.showError.productionActive', {
interpolate: { nodeName: 'Webhook' },
}),
type: 'error',
});
});
it('should execute the workflow if there is a single webhook trigger, but another trigger is chosen', async () => {
// ARRANGE
const pinia = createTestingPinia({ stubActions: false });
setActivePinia(pinia);
const toast = useToast();
const i18n = useI18n();
const { runWorkflow } = useRunWorkflow({ router });
const mockExecutionResponse = { executionId: '123' };
const triggerNode = 'Manual';
vi.mocked(workflowsStore).isWorkflowActive = true;
vi.mocked(useWorkflowHelpers()).getWorkflowDataToSave.mockResolvedValue({
nodes: [
{
name: 'Slack',
type: SLACK_TRIGGER_NODE_TYPE,
disabled: false,
},
{
name: triggerNode,
type: MANUAL_TRIGGER_NODE_TYPE,
disabled: false,
},
],
} as unknown as WorkflowData);
vi.mocked(uiStore).activeActions = [''];
vi.mocked(workflowHelpers).getCurrentWorkflow.mockReturnValue({
name: 'Test Workflow',
} as unknown as Workflow);
vi.mocked(workflowsStore).runWorkflow.mockResolvedValue(mockExecutionResponse);
vi.mocked(workflowsStore).nodesIssuesExist = true;
vi.mocked(workflowsStore).getWorkflowRunData = { NodeName: [] };
// ACT
const result = await runWorkflow({ triggerNode });
// ASSERT
expect(result).toEqual(mockExecutionResponse);
expect(toast.showMessage).not.toHaveBeenCalledWith({
title: i18n.baseText('workflowRun.showError.deactivate'),
message: i18n.baseText('workflowRun.showError.productionActive', {
interpolate: { nodeName: 'Webhook' },
}),
type: 'error',
});
});
it('should prevent execution and show error message when workflow is active with multiple tirggers and a single webhook trigger is chosen', async () => {
// ARRANGE
const pinia = createTestingPinia({ stubActions: false });
setActivePinia(pinia);
const toast = useToast();
const i18n = useI18n();
const { runWorkflow } = useRunWorkflow({ router });
const mockExecutionResponse = { executionId: '123' };
const triggerNode = 'Slack';
vi.mocked(workflowsStore).isWorkflowActive = true;
vi.mocked(useWorkflowHelpers()).getWorkflowDataToSave.mockResolvedValue({
nodes: [
{
name: triggerNode,
type: SLACK_TRIGGER_NODE_TYPE,
disabled: false,
},
{
name: 'Manual',
type: MANUAL_TRIGGER_NODE_TYPE,
disabled: false,
},
],
} as unknown as WorkflowData);
vi.mocked(uiStore).activeActions = [''];
vi.mocked(workflowHelpers).getCurrentWorkflow.mockReturnValue({
name: 'Test Workflow',
} as unknown as Workflow);
vi.mocked(workflowsStore).runWorkflow.mockResolvedValue(mockExecutionResponse);
vi.mocked(workflowsStore).nodesIssuesExist = true;
vi.mocked(workflowsStore).getWorkflowRunData = { NodeName: [] };
// ACT
const result = await runWorkflow({ triggerNode });
// ASSERT
expect(result).toBeUndefined();
expect(toast.showMessage).toHaveBeenCalledWith({
title: i18n.baseText('workflowRun.showError.deactivate'),
message: i18n.baseText('workflowRun.showError.productionActive', {
interpolate: { nodeName: 'Webhook' },
}),
type: 'error',
});
});
it('should return undefined if UI action "workflowRunning" is active', async () => {
const { runWorkflow } = useRunWorkflow({ router });
vi.mocked(workflowsStore).setActiveExecutionId('123');
const result = await runWorkflow({});
expect(result).toBeUndefined();
});
it('should execute workflow even if it has issues', async () => {
const mockExecutionResponse = { executionId: '123' };
const { runWorkflow } = useRunWorkflow({ router });
vi.mocked(uiStore).activeActions = [''];
vi.mocked(workflowHelpers).getCurrentWorkflow.mockReturnValue({
name: 'Test Workflow',
} as unknown as Workflow);
vi.mocked(workflowsStore).runWorkflow.mockResolvedValue(mockExecutionResponse);
vi.mocked(workflowsStore).nodesIssuesExist = true;
vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue({
id: 'workflowId',
nodes: [],
} as unknown as WorkflowData);
vi.mocked(workflowsStore).getWorkflowRunData = {
NodeName: [],
};
const result = await runWorkflow({});
expect(result).toEqual(mockExecutionResponse);
});
it('should execute workflow successfully', async () => {
const mockExecutionResponse = { executionId: '123' };
const { runWorkflow } = useRunWorkflow({ router });
vi.mocked(pushConnectionStore).isConnected = true;
vi.mocked(workflowsStore).runWorkflow.mockResolvedValue(mockExecutionResponse);
vi.mocked(workflowsStore).nodesIssuesExist = false;
vi.mocked(workflowHelpers).getCurrentWorkflow.mockReturnValue({
name: 'Test Workflow',
} as Workflow);
vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue({
id: 'workflowId',
nodes: [],
} as unknown as WorkflowData);
vi.mocked(workflowsStore).getWorkflowRunData = {
NodeName: [],
};
const result = await runWorkflow({});
expect(result).toEqual(mockExecutionResponse);
});
it('should exclude destinationNode from startNodes when provided', async () => {
// ARRANGE
const mockExecutionResponse = { executionId: '123' };
const { runWorkflow } = useRunWorkflow({ router });
const dataCaptor = captor<IStartRunData>();
const parentNodeName = 'parentNode';
const destinationNodeName = 'destinationNode';
// Mock workflow with parent-child relationship
const workflow = {
name: 'Test Workflow',
id: 'workflowId',
getParentNodes: vi.fn().mockImplementation((nodeName: string) => {
if (nodeName === destinationNodeName) {
return [parentNodeName];
}
return [];
}),
nodes: {
[parentNodeName]: createTestNode({ name: parentNodeName }),
[destinationNodeName]: createTestNode({ name: destinationNodeName }),
},
} as unknown as Workflow;
vi.mocked(pushConnectionStore).isConnected = true;
vi.mocked(workflowsStore).runWorkflow.mockResolvedValue(mockExecutionResponse);
vi.mocked(workflowsStore).nodesIssuesExist = false;
vi.mocked(workflowHelpers).getCurrentWorkflow.mockReturnValue(workflow);
vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue({
id: 'workflowId',
nodes: [],
} as unknown as WorkflowData);
vi.mocked(workflowsStore).getWorkflowRunData = {
[parentNodeName]: [
{
startTime: 1,
executionTime: 0,
source: [],
data: { main: [[{ json: { test: 'data' } }]] },
},
],
} as unknown as IRunData;
// ACT
await runWorkflow({ destinationNode: destinationNodeName });
// ASSERT
expect(workflowsStore.runWorkflow).toHaveBeenCalledTimes(1);
expect(workflowsStore.runWorkflow).toHaveBeenCalledWith(dataCaptor);
const startNodes = dataCaptor.value.startNodes ?? [];
const destinationInStartNodes = startNodes.some((node) => node.name === destinationNodeName);
expect(destinationInStartNodes).toBe(false);
});
it('should send dirty nodes for partial executions v2', async () => {
vi.mocked(settingsStore).partialExecutionVersion = 2;
const composable = useRunWorkflow({ router });
const parentName = 'When clicking';
const executeName = 'Code';
vi.mocked(workflowsStore).allNodes = [
createTestNode({ name: parentName }),
createTestNode({ name: executeName }),
];
vi.mocked(workflowsStore).outgoingConnectionsByNodeName.mockImplementation((nodeName) =>
nodeName === parentName
? { main: [[{ node: executeName, type: NodeConnectionTypes.Main, index: 0 }]] }
: ({} as INodeConnections),
);
vi.mocked(workflowsStore).incomingConnectionsByNodeName.mockImplementation((nodeName) =>
nodeName === executeName
? { main: [[{ node: parentName, type: NodeConnectionTypes.Main, index: 0 }]] }
: ({} as INodeConnections),
);
vi.mocked(workflowsStore).getWorkflowRunData = {
[parentName]: [
{
startTime: 1,
executionIndex: 0,
executionTime: 0,
source: [],
},
],
[executeName]: [
{
startTime: 1,
executionIndex: 1,
executionTime: 8,
source: [
{
previousNode: parentName,
},
],
},
],
};
vi.mocked(workflowHelpers).getCurrentWorkflow.mockReturnValue({
name: 'Test Workflow',
getParentNodes: () => [parentName],
nodes: { [parentName]: {} },
} as unknown as Workflow);
vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue({
nodes: [],
} as unknown as WorkflowData);
vi.mocked(workflowHelpers).executeData.mockResolvedValue({
data: {},
node: {},
source: null,
} as IExecuteData);
vi.mocked(workflowsStore).checkIfNodeHasChatParent.mockReturnValue(false);
vi.mocked(workflowsStore).getParametersLastUpdate.mockImplementation((name: string) => {
if (name === executeName) return 2;
return undefined;
});
const { runWorkflow } = composable;
await runWorkflow({ destinationNode: 'Code 1', source: 'Node.executeNode' });
expect(workflowsStore.runWorkflow).toHaveBeenCalledWith(
expect.objectContaining({ dirtyNodeNames: [executeName] }),
);
});
it('should send triggerToStartFrom if triggerNode and nodeData are passed in', async () => {
// ARRANGE
const composable = useRunWorkflow({ router });
const triggerNode = 'Chat Trigger';
const nodeData = mock<ITaskData>();
vi.mocked(workflowHelpers).getCurrentWorkflow.mockReturnValue(
mock<Workflow>({ getChildNodes: vi.fn().mockReturnValue([]) }),
);
vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue(
mock<WorkflowData>({ nodes: [] }),
);
const { runWorkflow } = composable;
// ACT
await runWorkflow({ triggerNode, nodeData });
// ASSERT
expect(workflowsStore.runWorkflow).toHaveBeenCalledWith(
expect.objectContaining({
triggerToStartFrom: {
name: triggerNode,
data: nodeData,
},
startNodes: [],
}),
);
});
it('should retrigger workflow from child node if triggerNode and nodeData are passed in', async () => {
// ARRANGE
const composable = useRunWorkflow({ router });
const triggerNode = 'Chat Trigger';
const nodeData = mock<ITaskData>();
vi.mocked(workflowHelpers).getCurrentWorkflow.mockReturnValue(
mock<Workflow>({
getChildNodes: vi.fn().mockReturnValue([{ name: 'Child node', type: 'nodes.child' }]),
}),
);
vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue(
mock<WorkflowData>({ nodes: [] }),
);
const { runWorkflow } = composable;
// ACT
await runWorkflow({ triggerNode, nodeData });
// ASSERT
expect(workflowsStore.runWorkflow).toHaveBeenCalledWith(
expect.objectContaining({
triggerToStartFrom: {
name: triggerNode,
data: nodeData,
},
startNodes: [
{
name: {
name: 'Child node',
type: 'nodes.child',
},
sourceData: null,
},
],
}),
);
});
it('should retrigger workflow from trigger node if rerunTriggerNode is set', async () => {
// ARRANGE
const composable = useRunWorkflow({ router });
const triggerNode = 'Chat Trigger';
const nodeData = mock<ITaskData>();
vi.mocked(workflowHelpers).getCurrentWorkflow.mockReturnValue(
mock<Workflow>({
getChildNodes: vi.fn().mockReturnValue([{ name: 'Child node', type: 'nodes.child' }]),
}),
);
vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue(
mock<WorkflowData>({ nodes: [] }),
);
const { runWorkflow } = composable;
// ACT
await runWorkflow({ triggerNode, nodeData, rerunTriggerNode: true });
// ASSERT
expect(workflowsStore.runWorkflow).toHaveBeenCalledWith(
expect.objectContaining({
triggerToStartFrom: {
name: triggerNode,
data: nodeData,
},
startNodes: [],
}),
);
});
it('should send triggerToStartFrom if triggerNode is passed in without nodeData', async () => {
// ARRANGE
const { runWorkflow } = useRunWorkflow({ router });
const triggerNode = 'Chat Trigger';
vi.mocked(workflowHelpers).getCurrentWorkflow.mockReturnValue(
mock<Workflow>({ getChildNodes: vi.fn().mockReturnValue([]) }),
);
vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue(
mock<WorkflowData>({ nodes: [] }),
);
// ACT
await runWorkflow({ triggerNode });
// ASSERT
expect(workflowsStore.runWorkflow).toHaveBeenCalledWith(
expect.objectContaining({
triggerToStartFrom: {
name: triggerNode,
},
}),
);
});
it('does not use the original run data if `partialExecutionVersion` is set to 1', async () => {
// ARRANGE
const mockExecutionResponse = { executionId: '123' };
const mockRunData = { nodeName: [] };
const { runWorkflow } = useRunWorkflow({ router });
const dataCaptor = captor();
const workflow = mock<Workflow>({ name: 'Test Workflow' });
workflow.getParentNodes.mockReturnValue([]);
vi.mocked(settingsStore).partialExecutionVersion = 1;
vi.mocked(pushConnectionStore).isConnected = true;
vi.mocked(workflowsStore).runWorkflow.mockResolvedValue(mockExecutionResponse);
vi.mocked(workflowsStore).nodesIssuesExist = false;
vi.mocked(workflowHelpers).getCurrentWorkflow.mockReturnValue(workflow);
vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue(
mock<WorkflowData>({ id: 'workflowId', nodes: [] }),
);
vi.mocked(workflowsStore).getWorkflowRunData = mockRunData;
// ACT
const result = await runWorkflow({ destinationNode: 'some node name' });
// ASSERT
expect(result).toEqual(mockExecutionResponse);
expect(workflowsStore.setWorkflowExecutionData).toHaveBeenCalledTimes(1);
expect(workflowsStore.setWorkflowExecutionData).toHaveBeenCalledWith(dataCaptor);
expect(dataCaptor.value).toMatchObject({
data: { resultData: { runData: {} } },
});
});
it('sends agentRequest on partial execution if `partialExecutionVersion` is set to 2', async () => {
// ARRANGE
const mockExecutionResponse = { executionId: '123' };
const mockRunData = { nodeName: [] };
const { runWorkflow } = useRunWorkflow({ router });
const dataCaptor = captor();
const agentRequest = {
query: 'query',
toolName: 'tool',
};
const workflow = mock<Workflow>({
name: 'Test Workflow',
id: 'WorkflowId',
nodes: {
'Test node': {
id: 'Test id',
name: 'Test node',
parameters: {
param: '0',
},
},
},
});
const workflowData = {
id: 'workflowId',
nodes: [
{
id: 'Test id',
name: 'Test node',
parameters: {
param: '0',
},
position: [0, 0],
type: 'n8n-nodes-base.test',
typeVersion: 1,
} as INode,
],
connections: {},
};
workflow.getParentNodes.mockReturnValue([]);
vi.mocked(settingsStore).partialExecutionVersion = 2;
vi.mocked(pushConnectionStore).isConnected = true;
vi.mocked(workflowsStore).runWorkflow.mockResolvedValue(mockExecutionResponse);
vi.mocked(workflowsStore).nodesIssuesExist = false;
vi.mocked(workflowHelpers).getCurrentWorkflow.mockReturnValue(workflow);
vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue(workflowData);
vi.mocked(workflowsStore).getWorkflowRunData = mockRunData;
vi.mocked(agentRequestStore).getAgentRequest.mockReturnValue(agentRequest);
// ACT
const result = await runWorkflow({ destinationNode: 'Test node' });
// ASSERT
expect(agentRequestStore.getAgentRequest).toHaveBeenCalledWith('WorkflowId', 'Test id');
expect(workflowsStore.runWorkflow).toHaveBeenCalledWith({
agentRequest: {
query: 'query',
tool: {
name: 'tool',
},
},
destinationNode: 'Test node',
dirtyNodeNames: undefined,
runData: mockRunData,
startNodes: [
{
name: 'Test node',
sourceData: null,
},
],
triggerToStartFrom: undefined,
workflowData,
});
expect(result).toEqual(mockExecutionResponse);
expect(workflowsStore.setWorkflowExecutionData).toHaveBeenCalledTimes(1);
expect(workflowsStore.setWorkflowExecutionData).toHaveBeenCalledWith(dataCaptor);
expect(dataCaptor.value).toMatchObject({ data: { resultData: { runData: mockRunData } } });
});
it('retains the original run data if `partialExecutionVersion` is set to 2', async () => {
// ARRANGE
const mockExecutionResponse = { executionId: '123' };
const mockRunData = { nodeName: [] };
const { runWorkflow } = useRunWorkflow({ router });
const dataCaptor = captor();
const workflow = mock<Workflow>({ name: 'Test Workflow' });
workflow.getParentNodes.mockReturnValue([]);
vi.mocked(settingsStore).partialExecutionVersion = 2;
vi.mocked(pushConnectionStore).isConnected = true;
vi.mocked(workflowsStore).runWorkflow.mockResolvedValue(mockExecutionResponse);
vi.mocked(workflowsStore).nodesIssuesExist = false;
vi.mocked(workflowHelpers).getCurrentWorkflow.mockReturnValue(workflow);
vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue(
mock<WorkflowData>({ id: 'workflowId', nodes: [] }),
);
vi.mocked(workflowsStore).getWorkflowRunData = mockRunData;
// ACT
const result = await runWorkflow({ destinationNode: 'some node name' });
// ASSERT
expect(result).toEqual(mockExecutionResponse);
expect(workflowsStore.setWorkflowExecutionData).toHaveBeenCalledTimes(1);
expect(workflowsStore.setWorkflowExecutionData).toHaveBeenCalledWith(dataCaptor);
expect(dataCaptor.value).toMatchObject({ data: { resultData: { runData: mockRunData } } });
});
it("does not send run data if it's not a partial execution even if `partialExecutionVersion` is set to 2", async () => {
// ARRANGE
const mockExecutionResponse = { executionId: '123' };
const mockRunData = { nodeName: [] };
const { runWorkflow } = useRunWorkflow({ router });
const dataCaptor = captor();
const workflow = mock<Workflow>({ name: 'Test Workflow' });
workflow.getParentNodes.mockReturnValue([]);
vi.mocked(settingsStore).partialExecutionVersion = 2;
vi.mocked(pushConnectionStore).isConnected = true;
vi.mocked(workflowsStore).runWorkflow.mockResolvedValue(mockExecutionResponse);
vi.mocked(workflowsStore).nodesIssuesExist = false;
vi.mocked(workflowHelpers).getCurrentWorkflow.mockReturnValue(workflow);
vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue(
mock<WorkflowData>({ id: 'workflowId', nodes: [] }),
);
vi.mocked(workflowsStore).getWorkflowRunData = mockRunData;
// ACT
const result = await runWorkflow({});
// ASSERT
expect(result).toEqual(mockExecutionResponse);
expect(workflowsStore.runWorkflow).toHaveBeenCalledTimes(1);
expect(workflowsStore.runWorkflow).toHaveBeenCalledWith(dataCaptor);
expect(dataCaptor.value).toHaveProperty('runData', undefined);
});
it('should set execution data to null if the execution did not start successfully', async () => {
const { runWorkflow } = useRunWorkflow({ router });
const workflow = mock<Workflow>({ name: 'Test Workflow' });
vi.mocked(workflowHelpers).getCurrentWorkflow.mockReturnValue(workflow);
vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue({
id: workflow.id,
nodes: [],
} as unknown as WorkflowData);
// Simulate failed execution start
vi.mocked(workflowsStore).runWorkflow.mockRejectedValueOnce(new Error());
await runWorkflow({});
expect(workflowsStore.runWorkflow).toHaveBeenCalledTimes(1);
expect(workflowsStore.setWorkflowExecutionData).lastCalledWith(null);
});
});
describe('consolidateRunDataAndStartNodes()', () => {
it('should return empty runData and startNodeNames if runData is null', () => {
const { consolidateRunDataAndStartNodes } = useRunWorkflow({ router });
const workflowMock = {
getParentNodes: vi.fn(),
nodes: {},
} as unknown as Workflow;
const result = consolidateRunDataAndStartNodes([], null, undefined, workflowMock);
expect(result).toEqual({ runData: undefined, startNodeNames: [] });
});
it('should return correct startNodeNames and newRunData for given directParentNodes and runData', () => {
const { consolidateRunDataAndStartNodes } = useRunWorkflow({ router });
const directParentNodes = ['node1', 'node2'];
const runData = {
node2: [{ data: { main: [[{ json: { value: 'data2' } }]] } }],
node3: [{ data: { main: [[{ json: { value: 'data3' } }]] } }],
} as unknown as IRunData;
const pinData: IPinData = {
node2: [{ json: { value: 'data2' } }],
};
const workflowMock = {
getParentNodes: vi.fn().mockImplementation((node) => {
if (node === 'node1') return ['node3'];
return [];
}),
nodes: {
node1: { disabled: false },
node2: { disabled: false },
node3: { disabled: true },
},
} as unknown as Workflow;
const result = consolidateRunDataAndStartNodes(
directParentNodes,
runData,
pinData,
workflowMock,
);
expect(result.startNodeNames).toContain('node1');
expect(result.startNodeNames).not.toContain('node3');
expect(result.runData).toEqual(runData);
});
it('should include directParentNode in startNodeNames if it has no runData or pinData', () => {
const { consolidateRunDataAndStartNodes } = useRunWorkflow({ router });
const directParentNodes = ['node1'];
const runData = {
node2: [
{
data: {
main: [[{ json: { value: 'data2' } }]],
},
},
],
} as unknown as IRunData;
const workflowMock = {
getParentNodes: vi.fn().mockReturnValue([]),
nodes: { node1: { disabled: false } },
} as unknown as Workflow;
const result = consolidateRunDataAndStartNodes(
directParentNodes,
runData,
undefined,
workflowMock,
);
expect(result.startNodeNames).toContain('node1');
expect(result.runData).toBeUndefined();
});
it('should rerun failed parent nodes, adding them to the returned list of start nodes and not adding their result to runData', () => {
const { consolidateRunDataAndStartNodes } = useRunWorkflow({ router });
const directParentNodes = ['node1'];
const runData = {
node1: [
{
error: new ExpressionError('error'),
},
],
} as unknown as IRunData;
const workflowMock = {
getParentNodes: vi.fn().mockReturnValue([]),
nodes: {
node1: { disabled: false },
node2: { disabled: false },
},
} as unknown as Workflow;
const result = consolidateRunDataAndStartNodes(
directParentNodes,
runData,
undefined,
workflowMock,
);
expect(result.startNodeNames).toContain('node1');
expect(result.runData).toEqual(undefined);
});
});
describe('runEntireWorkflow()', () => {
it('should invoke runWorkflow with expected arguments', async () => {
const runWorkflowComposable = useRunWorkflow({ router });
vi.mocked(workflowHelpers).getCurrentWorkflow.mockReturnValue({
id: 'workflowId',
} as unknown as Workflow);
vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue({
id: 'workflowId',
nodes: [],
} as unknown as WorkflowData);
await runWorkflowComposable.runEntireWorkflow('main', 'foo');
expect(workflowsStore.runWorkflow).toHaveBeenCalledWith({
runData: undefined,
startNodes: [],
triggerToStartFrom: {
data: undefined,
name: 'foo',
},
workflowData: {
id: 'workflowId',
nodes: [],
},
});
});
});
describe('stopCurrentExecution()', () => {
it('should not prematurely call markExecutionAsStopped() while execution status is still "running"', async () => {
const runWorkflowComposable = useRunWorkflow({ router });
const executionData: IExecutionResponse = {
id: 'test-exec-id',
workflowData: createTestWorkflow({ id: 'test-wf-id' }),
finished: false,
mode: 'manual',
status: 'running',
startedAt: new Date('2025-04-01T00:00:00.000Z'),
createdAt: new Date('2025-04-01T00:00:00.000Z'),
};
const markStoppedSpy = vi.spyOn(workflowsStore, 'markExecutionAsStopped');
const getExecutionSpy = vi.spyOn(workflowsStore, 'getExecution');
workflowsStore.activeWorkflows = ['test-wf-id'];
workflowsStore.setActiveExecutionId('test-exec-id');
getExecutionSpy.mockResolvedValue(executionData);
// Exercise - don't wait for returned promise to resolve
void runWorkflowComposable.stopCurrentExecution();
// Assert that markExecutionAsStopped() isn't called yet after a simulated delay
await new Promise((resolve) => setTimeout(resolve, 10));
expect(markStoppedSpy).not.toHaveBeenCalled();
expect(getExecutionSpy).toHaveBeenCalledWith('test-exec-id');
// Simulated executionFinished event
getExecutionSpy.mockResolvedValue({
...executionData,
status: 'canceled',
stoppedAt: new Date('2025-04-01T00:00:99.000Z'),
});
// Assert that markExecutionAsStopped() is called eventually
await waitFor(() => expect(markStoppedSpy).toHaveBeenCalled());
expect(getExecutionSpy).toHaveBeenCalledWith('test-exec-id');
});
});
describe('sortNodesByYPosition()', () => {
const getNodeUi = (name: string, position: [number, number]) => {
return {
name,
position,
type: 'n8n-nodes-base.test',
typeVersion: 1,
id: name,
parameters: {},
};
};
it('should sort nodes by Y position in ascending order', () => {
const { sortNodesByYPosition } = useRunWorkflow({ router });
const topNode = 'topNode';
const middleNode = 'middleNode';
const bottomNode = 'bottomNode';
vi.mocked(workflowsStore.getNodeByName).mockImplementation((name) => {
if (name === topNode) return getNodeUi(topNode, [100, 50]);
if (name === middleNode) return getNodeUi(middleNode, [200, 200]);
if (name === bottomNode) return getNodeUi(bottomNode, [150, 350]);
return null;
});
// Test with different order of input nodes
const result = sortNodesByYPosition([bottomNode, topNode, middleNode]);
// Should be sorted by Y position (top to bottom)
expect(result).toEqual([topNode, middleNode, bottomNode]);
});
});
});