test(editor): Add more node execute button tests

This commit is contained in:
Csaba Tuncsik 2025-03-04 19:20:39 +01:00
parent ad2b36117f
commit eb62bce5d3
No known key found for this signature in database
2 changed files with 130 additions and 32 deletions

View file

@ -1,14 +1,20 @@
import { reactive } from 'vue'; import { reactive } from 'vue';
import { createTestingPinia } from '@pinia/testing'; import { createTestingPinia } from '@pinia/testing';
import { useRouter } from 'vue-router';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { createComponentRenderer } from '@/__tests__/render'; import { createComponentRenderer } from '@/__tests__/render';
import { type MockedStore, mockedStore } from '@/__tests__/utils'; import { type MockedStore, mockedStore } from '@/__tests__/utils';
import { mockNode } from '@/__tests__/mocks'; import { mockNode, mockNodeTypeDescription } from '@/__tests__/mocks';
import { CODE_NODE_TYPE } from '@/constants'; import { nodeViewEventBus } from '@/event-bus';
import { CHAT_TRIGGER_NODE_TYPE, SET_NODE_TYPE } from '@/constants';
import NodeExecuteButton from '@/components/NodeExecuteButton.vue'; import NodeExecuteButton from '@/components/NodeExecuteButton.vue';
import { useWorkflowsStore } from '@/stores/workflows.store'; import { useWorkflowsStore } from '@/stores/workflows.store';
import { useUIStore } from '@/stores/ui.store'; import { useUIStore } from '@/stores/ui.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store'; import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useNDVStore } from '@/stores/ndv.store';
import { useExecutionsStore } from '@/stores/executions.store';
import { useRunWorkflow } from '@/composables/useRunWorkflow';
import { useExternalHooks } from '@/composables/useExternalHooks';
vi.mock('vue-router', () => ({ vi.mock('vue-router', () => ({
useRouter: () => ({}), useRouter: () => ({}),
@ -16,44 +22,65 @@ vi.mock('vue-router', () => ({
RouterLink: vi.fn(), RouterLink: vi.fn(),
})); }));
vi.mock('@/composables/useRunWorkflow', () => ({
useRunWorkflow: () => ({
runWorkflow: vi.fn(),
stopCurrentExecution: vi.fn(),
}),
}));
vi.mock('@/composables/useExternalHooks', () => ({
useExternalHooks: () => ({
run: vi.fn(),
}),
}));
let renderComponent: ReturnType<typeof createComponentRenderer>; let renderComponent: ReturnType<typeof createComponentRenderer>;
let workflowsStore: MockedStore<typeof useWorkflowsStore>; let workflowsStore: MockedStore<typeof useWorkflowsStore>;
let uiStore: MockedStore<typeof useUIStore>; let uiStore: MockedStore<typeof useUIStore>;
let nodeTypesStore: MockedStore<typeof useNodeTypesStore>; let nodeTypesStore: MockedStore<typeof useNodeTypesStore>;
let ndvStore: MockedStore<typeof useNDVStore>;
let executionsStore: MockedStore<typeof useExecutionsStore>;
let runWorkflow: ReturnType<typeof useRunWorkflow>;
let externalHooks: ReturnType<typeof useExternalHooks>;
const nodeViewEventBusEmitSpy = vi.spyOn(nodeViewEventBus, 'emit');
describe('NodeExecuteButton', () => { describe('NodeExecuteButton', () => {
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks();
renderComponent = createComponentRenderer(NodeExecuteButton, { renderComponent = createComponentRenderer(NodeExecuteButton, {
pinia: createTestingPinia(), pinia: createTestingPinia(),
props: {
nodeName: 'test-node',
telemetrySource: 'test-source',
},
}); });
workflowsStore = mockedStore(useWorkflowsStore); workflowsStore = mockedStore(useWorkflowsStore);
uiStore = mockedStore(useUIStore); uiStore = mockedStore(useUIStore);
nodeTypesStore = mockedStore(useNodeTypesStore); nodeTypesStore = mockedStore(useNodeTypesStore);
ndvStore = mockedStore(useNDVStore);
executionsStore = mockedStore(useExecutionsStore);
runWorkflow = useRunWorkflow({ router: useRouter() });
externalHooks = useExternalHooks();
workflowsStore.workflowId = 'abc123';
}); });
it('renders without error', () => { it('renders without error', () => {
expect(() => expect(() => renderComponent()).not.toThrow();
renderComponent({
props: {
nodeName: 'test',
telemetrySource: 'test',
},
}),
).not.toThrow();
}); });
it('should be disabled if the node is disabled and show tooltip', async () => { it('should be disabled if the node is disabled and show tooltip', async () => {
workflowsStore.getNodeByName.mockReturnValue( workflowsStore.getNodeByName.mockReturnValue(
mockNode({ name: 'test', type: CODE_NODE_TYPE, disabled: true }), mockNode({ name: 'test', type: SET_NODE_TYPE, disabled: true }),
); );
const { getByRole, queryByRole } = renderComponent({ const { getByRole, queryByRole } = renderComponent();
props: {
nodeName: 'test',
telemetrySource: 'test',
},
});
const button = getByRole('button'); const button = getByRole('button');
expect(button).toBeDisabled(); expect(button).toBeDisabled();
@ -67,14 +94,11 @@ describe('NodeExecuteButton', () => {
it('should be disabled when workflow is running but node is not executing', async () => { it('should be disabled when workflow is running but node is not executing', async () => {
uiStore.isActionActive.workflowRunning = true; uiStore.isActionActive.workflowRunning = true;
workflowsStore.isNodeExecuting.mockReturnValue(false); workflowsStore.isNodeExecuting.mockReturnValue(false);
workflowsStore.getNodeByName.mockReturnValue(mockNode({ name: 'test', type: CODE_NODE_TYPE })); workflowsStore.getNodeByName.mockReturnValue(
mockNode({ name: 'test-node', type: SET_NODE_TYPE }),
);
const { getByRole, queryByRole } = renderComponent({ const { getByRole, queryByRole } = renderComponent();
props: {
nodeName: 'test',
telemetrySource: 'test',
},
});
const button = getByRole('button'); const button = getByRole('button');
expect(button).toBeDisabled(); expect(button).toBeDisabled();
@ -90,20 +114,94 @@ describe('NodeExecuteButton', () => {
workflowsStore.getNodeByName.mockReturnValue( workflowsStore.getNodeByName.mockReturnValue(
mockNode({ mockNode({
name: 'test', name: 'test',
type: CODE_NODE_TYPE, type: SET_NODE_TYPE,
issues: { issues: {
typeUnknown: true, typeUnknown: true,
}, },
}), }),
); );
const { getByRole } = renderComponent({ const { getByRole } = renderComponent();
props: {
nodeName: 'test',
telemetrySource: 'test',
},
});
expect(getByRole('button')).toBeDisabled(); expect(getByRole('button')).toBeDisabled();
}); });
it('stops webhook when clicking button while listening for events', async () => {
workflowsStore.executionWaitingForWebhook = true;
nodeTypesStore.isTriggerNode = () => true;
workflowsStore.getNodeByName.mockReturnValue(
mockNode({ name: 'test-node', type: SET_NODE_TYPE }),
);
const { getByRole } = renderComponent();
await userEvent.click(getByRole('button'));
expect(workflowsStore.removeTestWebhook).toHaveBeenCalledWith('abc123');
});
it('stops execution when clicking button while workflow is running', async () => {
uiStore.isActionActive.workflowRunning = true;
nodeTypesStore.isTriggerNode = () => true;
workflowsStore.activeExecutionId = 'test-execution-id';
workflowsStore.isNodeExecuting.mockReturnValue(true);
workflowsStore.getNodeByName.mockReturnValue(
mockNode({ name: 'test-node', type: SET_NODE_TYPE }),
);
const { getByRole, emitted } = renderComponent();
await userEvent.click(getByRole('button'));
expect(executionsStore.stopCurrentExecution).toHaveBeenCalledWith('test-execution-id');
expect(emitted().stopExecution).toBeTruthy();
});
it('runs workflow when clicking button normally', async () => {
workflowsStore.getNodeByName.mockReturnValue(
mockNode({ name: 'test-node', type: SET_NODE_TYPE }),
);
nodeTypesStore.getNodeType = () => mockNodeTypeDescription();
const { getByRole, emitted } = renderComponent();
await userEvent.click(getByRole('button'));
expect(externalHooks.run).toHaveBeenCalledWith('nodeExecuteButton.onClick', expect.any(Object));
expect(runWorkflow.runWorkflow).toHaveBeenCalledWith({
destinationNode: 'test-node',
source: 'RunData.ExecuteNodeButton',
});
expect(emitted().execute).toBeTruthy();
});
it('opens chat when clicking button for chat node', async () => {
workflowsStore.getNodeByName.mockReturnValue(
mockNode({ name: 'test-node', type: SET_NODE_TYPE }),
);
nodeTypesStore.getNodeType = () => mockNodeTypeDescription({ name: CHAT_TRIGGER_NODE_TYPE });
const { getByRole } = renderComponent();
await userEvent.click(getByRole('button'));
expect(ndvStore.setActiveNodeName).toHaveBeenCalledWith(null);
expect(workflowsStore.chatPartialExecutionDestinationNode).toBe('test-node');
expect(nodeViewEventBusEmitSpy).toHaveBeenCalledWith('openChat');
});
it('opens chat when clicking button for chat child node', async () => {
workflowsStore.getNodeByName.mockReturnValue(
mockNode({ name: 'test-node', type: SET_NODE_TYPE }),
);
workflowsStore.checkIfNodeHasChatParent.mockReturnValue(true);
const { getByRole } = renderComponent();
await userEvent.click(getByRole('button'));
expect(ndvStore.setActiveNodeName).toHaveBeenCalledWith(null);
expect(workflowsStore.chatPartialExecutionDestinationNode).toBe('test-node');
expect(nodeViewEventBusEmitSpy).toHaveBeenCalledWith('openChat');
});
}); });

View file

@ -85,7 +85,7 @@ const nodeType = computed((): INodeTypeDescription | null => {
}); });
const isNodeRunning = computed(() => { const isNodeRunning = computed(() => {
if (!uiStore.isActionActive['workflowRunning'] || codeGenerationInProgress.value) return false; if (!uiStore.isActionActive.workflowRunning || codeGenerationInProgress.value) return false;
const triggeredNode = workflowsStore.executedNode; const triggeredNode = workflowsStore.executedNode;
return ( return (
workflowsStore.isNodeExecuting(node.value?.name ?? '') || triggeredNode === node.value?.name workflowsStore.isNodeExecuting(node.value?.name ?? '') || triggeredNode === node.value?.name