mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
refactor(editor): Migrate workflows store to setup function with composition API (no-changelog) (#9270)
This commit is contained in:
parent
6b6e8dfc33
commit
f64a41d617
|
@ -253,6 +253,12 @@ export interface IWorkflowToShare extends IWorkflowDataUpdate {
|
|||
meta?: WorkflowMetadata;
|
||||
}
|
||||
|
||||
export interface NewWorkflowResponse {
|
||||
name: string;
|
||||
onboardingFlowEnabled?: boolean;
|
||||
defaultSettings: IWorkflowSettings;
|
||||
}
|
||||
|
||||
export interface IWorkflowTemplateNode
|
||||
extends Pick<INodeUi, 'name' | 'type' | 'position' | 'parameters' | 'typeVersion' | 'webhookId'> {
|
||||
// The credentials in a template workflow have a different type than in a regular workflow
|
||||
|
|
|
@ -1,9 +1,20 @@
|
|||
import type { IExecutionsCurrentSummaryExtended, IRestApiContext } from '@/Interface';
|
||||
import type {
|
||||
IExecutionResponse,
|
||||
IExecutionsCurrentSummaryExtended,
|
||||
IRestApiContext,
|
||||
IWorkflowDb,
|
||||
NewWorkflowResponse,
|
||||
} from '@/Interface';
|
||||
import type { ExecutionFilters, ExecutionOptions, IDataObject } from 'n8n-workflow';
|
||||
import { makeRestApiRequest } from '@/utils/apiUtils';
|
||||
|
||||
export async function getNewWorkflow(context: IRestApiContext, name?: string) {
|
||||
const response = await makeRestApiRequest(context, 'GET', '/workflows/new', name ? { name } : {});
|
||||
const response = await makeRestApiRequest<NewWorkflowResponse>(
|
||||
context,
|
||||
'GET',
|
||||
'/workflows/new',
|
||||
name ? { name } : {},
|
||||
);
|
||||
return {
|
||||
name: response.name,
|
||||
onboardingFlowEnabled: response.onboardingFlowEnabled === true,
|
||||
|
@ -14,17 +25,17 @@ export async function getNewWorkflow(context: IRestApiContext, name?: string) {
|
|||
export async function getWorkflow(context: IRestApiContext, id: string, filter?: object) {
|
||||
const sendData = filter ? { filter } : undefined;
|
||||
|
||||
return await makeRestApiRequest(context, 'GET', `/workflows/${id}`, sendData);
|
||||
return await makeRestApiRequest<IWorkflowDb>(context, 'GET', `/workflows/${id}`, sendData);
|
||||
}
|
||||
|
||||
export async function getWorkflows(context: IRestApiContext, filter?: object) {
|
||||
const sendData = filter ? { filter } : undefined;
|
||||
|
||||
return await makeRestApiRequest(context, 'GET', '/workflows', sendData);
|
||||
return await makeRestApiRequest<IWorkflowDb[]>(context, 'GET', '/workflows', sendData);
|
||||
}
|
||||
|
||||
export async function getActiveWorkflows(context: IRestApiContext) {
|
||||
return await makeRestApiRequest(context, 'GET', '/active-workflows');
|
||||
return await makeRestApiRequest<string[]>(context, 'GET', '/active-workflows');
|
||||
}
|
||||
|
||||
export async function getActiveExecutions(context: IRestApiContext, filter: IDataObject) {
|
||||
|
@ -42,5 +53,9 @@ export async function getExecutions(
|
|||
}
|
||||
|
||||
export async function getExecutionData(context: IRestApiContext, executionId: string) {
|
||||
return await makeRestApiRequest(context, 'GET', `/executions/${executionId}`);
|
||||
return await makeRestApiRequest<IExecutionResponse | null>(
|
||||
context,
|
||||
'GET',
|
||||
`/executions/${executionId}`,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -6,7 +6,20 @@ import RunData from '@/components/RunData.vue';
|
|||
import { STORES, VIEWS } from '@/constants';
|
||||
import { SETTINGS_STORE_DEFAULT_STATE } from '@/__tests__/utils';
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import type { IRunDataDisplayMode } from '@/Interface';
|
||||
import type { INodeUi, IRunDataDisplayMode } from '@/Interface';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { setActivePinia } from 'pinia';
|
||||
|
||||
const nodes = [
|
||||
{
|
||||
id: '1',
|
||||
typeVersion: 1,
|
||||
name: 'Test Node',
|
||||
position: [0, 0],
|
||||
type: 'test',
|
||||
parameters: {},
|
||||
},
|
||||
] as INodeUi[];
|
||||
|
||||
describe('RunData', () => {
|
||||
it('should render data correctly even when "item.json" has another "json" key', async () => {
|
||||
|
@ -81,8 +94,64 @@ describe('RunData', () => {
|
|||
expect(getByTestId('ndv-binary-data_0')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const render = (outputData: unknown[], displayMode: IRunDataDisplayMode) =>
|
||||
createComponentRenderer(RunData, {
|
||||
const render = (outputData: unknown[], displayMode: IRunDataDisplayMode) => {
|
||||
const pinia = createTestingPinia({
|
||||
initialState: {
|
||||
[STORES.SETTINGS]: {
|
||||
settings: merge({}, SETTINGS_STORE_DEFAULT_STATE.settings),
|
||||
},
|
||||
[STORES.NDV]: {
|
||||
output: {
|
||||
displayMode,
|
||||
},
|
||||
activeNodeName: 'Test Node',
|
||||
},
|
||||
[STORES.WORKFLOWS]: {
|
||||
workflow: {
|
||||
nodes,
|
||||
},
|
||||
workflowExecutionData: {
|
||||
id: '1',
|
||||
finished: true,
|
||||
mode: 'trigger',
|
||||
startedAt: new Date(),
|
||||
workflowData: {
|
||||
id: '1',
|
||||
name: 'Test Workflow',
|
||||
versionId: '1',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
active: false,
|
||||
nodes: [],
|
||||
connections: {},
|
||||
},
|
||||
data: {
|
||||
resultData: {
|
||||
runData: {
|
||||
'Test Node': [
|
||||
{
|
||||
startTime: new Date().getTime(),
|
||||
executionTime: new Date().getTime(),
|
||||
data: {
|
||||
main: [outputData],
|
||||
},
|
||||
source: [null],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
setActivePinia(pinia);
|
||||
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
vi.mocked(workflowsStore).getNodeByName.mockReturnValue(nodes[0]);
|
||||
|
||||
return createComponentRenderer(RunData, {
|
||||
props: {
|
||||
node: {
|
||||
name: 'Test Node',
|
||||
|
@ -114,64 +183,7 @@ describe('RunData', () => {
|
|||
mappingEnabled: true,
|
||||
distanceFromActive: 0,
|
||||
},
|
||||
pinia: createTestingPinia({
|
||||
initialState: {
|
||||
[STORES.SETTINGS]: {
|
||||
settings: merge({}, SETTINGS_STORE_DEFAULT_STATE.settings),
|
||||
},
|
||||
[STORES.NDV]: {
|
||||
output: {
|
||||
displayMode,
|
||||
},
|
||||
activeNodeName: 'Test Node',
|
||||
},
|
||||
[STORES.WORKFLOWS]: {
|
||||
workflow: {
|
||||
nodes: [
|
||||
{
|
||||
id: '1',
|
||||
typeVersion: 1,
|
||||
name: 'Test Node',
|
||||
position: [0, 0],
|
||||
type: 'test',
|
||||
parameters: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
workflowExecutionData: {
|
||||
id: '1',
|
||||
finished: true,
|
||||
mode: 'trigger',
|
||||
startedAt: new Date(),
|
||||
workflowData: {
|
||||
id: '1',
|
||||
name: 'Test Workflow',
|
||||
versionId: '1',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
active: false,
|
||||
nodes: [],
|
||||
connections: {},
|
||||
},
|
||||
data: {
|
||||
resultData: {
|
||||
runData: {
|
||||
'Test Node': [
|
||||
{
|
||||
startTime: new Date().getTime(),
|
||||
executionTime: new Date().getTime(),
|
||||
data: {
|
||||
main: [outputData],
|
||||
},
|
||||
source: [null],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
pinia,
|
||||
});
|
||||
};
|
||||
});
|
||||
|
|
|
@ -8,6 +8,8 @@ import { renderComponent } from '@/__tests__/render';
|
|||
import { waitFor } from '@testing-library/vue';
|
||||
import { setActivePinia } from 'pinia';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import type { INodeUi } from '@/Interface';
|
||||
|
||||
const EXPRESSION_OUTPUT_TEST_ID = 'inline-expression-editor-output';
|
||||
|
||||
|
@ -18,43 +20,50 @@ const DEFAULT_SETUP = {
|
|||
},
|
||||
};
|
||||
|
||||
const nodes = [
|
||||
{
|
||||
id: '1',
|
||||
typeVersion: 1,
|
||||
name: 'Test Node',
|
||||
position: [0, 0],
|
||||
type: 'test',
|
||||
parameters: {},
|
||||
},
|
||||
] as INodeUi[];
|
||||
|
||||
const mockResolveExpression = () => {
|
||||
const mock = vi.fn();
|
||||
vi.spyOn(workflowHelpers, 'useWorkflowHelpers').mockReturnValueOnce({
|
||||
...workflowHelpers.useWorkflowHelpers({ router: useRouter() }),
|
||||
resolveExpression: mock,
|
||||
});
|
||||
|
||||
return mock;
|
||||
};
|
||||
|
||||
describe('SqlEditor.vue', () => {
|
||||
const pinia = createTestingPinia({
|
||||
initialState: {
|
||||
[STORES.SETTINGS]: {
|
||||
settings: SETTINGS_STORE_DEFAULT_STATE.settings,
|
||||
},
|
||||
[STORES.NDV]: {
|
||||
activeNodeName: 'Test Node',
|
||||
},
|
||||
[STORES.WORKFLOWS]: {
|
||||
workflow: {
|
||||
nodes: [
|
||||
{
|
||||
id: '1',
|
||||
typeVersion: 1,
|
||||
name: 'Test Node',
|
||||
position: [0, 0],
|
||||
type: 'test',
|
||||
parameters: {},
|
||||
},
|
||||
],
|
||||
connections: {},
|
||||
beforeEach(() => {
|
||||
const pinia = createTestingPinia({
|
||||
initialState: {
|
||||
[STORES.SETTINGS]: {
|
||||
settings: SETTINGS_STORE_DEFAULT_STATE.settings,
|
||||
},
|
||||
[STORES.NDV]: {
|
||||
activeNodeName: 'Test Node',
|
||||
},
|
||||
[STORES.WORKFLOWS]: {
|
||||
workflow: {
|
||||
nodes,
|
||||
connections: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
setActivePinia(pinia);
|
||||
|
||||
const mockResolveExpression = () => {
|
||||
const mock = vi.fn();
|
||||
vi.spyOn(workflowHelpers, 'useWorkflowHelpers').mockReturnValueOnce({
|
||||
...workflowHelpers.useWorkflowHelpers({ router: useRouter() }),
|
||||
resolveExpression: mock,
|
||||
});
|
||||
setActivePinia(pinia);
|
||||
|
||||
return mock;
|
||||
};
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
vi.mocked(workflowsStore).getNodeByName.mockReturnValue(nodes[0]);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
vi.clearAllMocks();
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import type { INodeUi } from '@/Interface';
|
||||
import { useContextMenu } from '@/composables/useContextMenu';
|
||||
import { BASIC_CHAIN_NODE_TYPE, NO_OP_NODE_TYPE, STICKY_NODE_TYPE, STORES } from '@/constants';
|
||||
import { BASIC_CHAIN_NODE_TYPE, NO_OP_NODE_TYPE, STICKY_NODE_TYPE } from '@/constants';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { setActivePinia } from 'pinia';
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
|
@ -27,21 +26,18 @@ describe('useContextMenu', () => {
|
|||
const selectedNodes = nodes.slice(0, 2);
|
||||
|
||||
beforeAll(() => {
|
||||
setActivePinia(
|
||||
createTestingPinia({
|
||||
initialState: {
|
||||
[STORES.UI]: { selectedNodes },
|
||||
[STORES.WORKFLOWS]: { workflow: { nodes } },
|
||||
},
|
||||
}),
|
||||
);
|
||||
setActivePinia(createPinia());
|
||||
sourceControlStore = useSourceControlStore();
|
||||
uiStore = useUIStore();
|
||||
workflowsStore = useWorkflowsStore();
|
||||
vi.spyOn(uiStore, 'isReadOnlyView', 'get').mockReturnValue(false);
|
||||
vi.spyOn(sourceControlStore, 'preferences', 'get').mockReturnValue({
|
||||
branchReadOnly: false,
|
||||
} as never);
|
||||
|
||||
uiStore = useUIStore();
|
||||
uiStore.selectedNodes = selectedNodes;
|
||||
vi.spyOn(uiStore, 'isReadOnlyView', 'get').mockReturnValue(false);
|
||||
|
||||
workflowsStore = useWorkflowsStore();
|
||||
workflowsStore.workflow.nodes = nodes;
|
||||
vi.spyOn(workflowsStore, 'getCurrentWorkflow').mockReturnValue({
|
||||
nodes,
|
||||
getNode: (_: string) => {
|
||||
|
|
|
@ -1,101 +0,0 @@
|
|||
import { setActivePinia, createPinia } from 'pinia';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import type { IWorkflowDataUpdate } from '@/Interface';
|
||||
import { makeRestApiRequest } from '@/utils/apiUtils';
|
||||
import { useRootStore } from '../n8nRoot.store';
|
||||
|
||||
vi.mock('@/utils/apiUtils', () => ({
|
||||
makeRestApiRequest: vi.fn(),
|
||||
}));
|
||||
|
||||
const MOCK_WORKFLOW_SIMPLE: IWorkflowDataUpdate = {
|
||||
id: '1',
|
||||
name: 'test',
|
||||
nodes: [
|
||||
{
|
||||
parameters: {
|
||||
path: '21a77783-e050-4e0f-9915-2d2dd5b53cde',
|
||||
options: {},
|
||||
},
|
||||
id: '2dbf9369-2eec-42e7-9b89-37e50af12289',
|
||||
name: 'Webhook',
|
||||
type: 'n8n-nodes-base.webhook',
|
||||
typeVersion: 1,
|
||||
position: [340, 240],
|
||||
webhookId: '21a77783-e050-4e0f-9915-2d2dd5b53cde',
|
||||
},
|
||||
{
|
||||
parameters: {
|
||||
table: 'product',
|
||||
columns: 'name,ean',
|
||||
additionalFields: {},
|
||||
},
|
||||
name: 'Insert Rows1',
|
||||
type: 'n8n-nodes-base.postgres',
|
||||
position: [580, 240],
|
||||
typeVersion: 1,
|
||||
id: 'a10ba62a-8792-437c-87df-0762fa53e157',
|
||||
credentials: {
|
||||
postgres: {
|
||||
id: 'iEFl08xIegmR8xF6',
|
||||
name: 'Postgres account',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
connections: {
|
||||
Webhook: {
|
||||
main: [
|
||||
[
|
||||
{
|
||||
node: 'Insert Rows1',
|
||||
type: 'main',
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('worklfows store', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
});
|
||||
|
||||
describe('createNewWorkflow', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
it('creates new workflow', async () => {
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
await workflowsStore.createNewWorkflow(MOCK_WORKFLOW_SIMPLE);
|
||||
|
||||
expect(makeRestApiRequest).toHaveBeenCalledWith(
|
||||
useRootStore().getRestApiContext,
|
||||
'POST',
|
||||
'/workflows',
|
||||
{
|
||||
...MOCK_WORKFLOW_SIMPLE,
|
||||
active: false,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('sets active to false', async () => {
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
await workflowsStore.createNewWorkflow({ ...MOCK_WORKFLOW_SIMPLE, active: true });
|
||||
|
||||
expect(makeRestApiRequest).toHaveBeenCalledWith(
|
||||
useRootStore().getRestApiContext,
|
||||
'POST',
|
||||
'/workflows',
|
||||
{
|
||||
...MOCK_WORKFLOW_SIMPLE,
|
||||
active: false,
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,31 +0,0 @@
|
|||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
|
||||
let pinia: ReturnType<typeof createTestingPinia>;
|
||||
beforeAll(() => {
|
||||
pinia = createTestingPinia();
|
||||
});
|
||||
|
||||
describe('Workflows Store', () => {
|
||||
describe('shouldReplaceInputDataWithPinData', () => {
|
||||
beforeEach(() => {
|
||||
pinia.state.value = {
|
||||
workflows: useWorkflowsStore(),
|
||||
};
|
||||
});
|
||||
|
||||
it('should return true if no active execution is set', () => {
|
||||
expect(useWorkflowsStore().shouldReplaceInputDataWithPinData).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if active execution is set and mode is manual', () => {
|
||||
pinia.state.value.workflows.activeWorkflowExecution = { mode: 'manual' };
|
||||
expect(useWorkflowsStore().shouldReplaceInputDataWithPinData).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if active execution is set and mode is not manual', () => {
|
||||
pinia.state.value.workflows.activeWorkflowExecution = { mode: 'webhook' };
|
||||
expect(useWorkflowsStore().shouldReplaceInputDataWithPinData).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
444
packages/editor-ui/src/stores/workflows.store.spec.ts
Normal file
444
packages/editor-ui/src/stores/workflows.store.spec.ts
Normal file
|
@ -0,0 +1,444 @@
|
|||
import { setActivePinia, createPinia } from 'pinia';
|
||||
import * as workflowsApi from '@/api/workflows';
|
||||
import {
|
||||
DUPLICATE_POSTFFIX,
|
||||
MAX_WORKFLOW_NAME_LENGTH,
|
||||
PLACEHOLDER_EMPTY_WORKFLOW_ID,
|
||||
} from '@/constants';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import type { IExecutionResponse, INodeUi, IWorkflowDb, IWorkflowSettings } from '@/Interface';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import type { ExecutionSummary, IConnection, INodeExecutionData } from 'n8n-workflow';
|
||||
import { stringSizeInBytes } from '@/utils/typesUtils';
|
||||
import { dataPinningEventBus } from '@/event-bus';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
|
||||
vi.mock('@/api/workflows', () => ({
|
||||
getWorkflows: vi.fn(),
|
||||
getWorkflow: vi.fn(),
|
||||
getNewWorkflow: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/stores/nodeTypes.store', () => ({
|
||||
useNodeTypesStore: vi.fn(() => ({
|
||||
getNodeType: vi.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('useWorkflowsStore', () => {
|
||||
let workflowsStore: ReturnType<typeof useWorkflowsStore>;
|
||||
let uiStore: ReturnType<typeof useUIStore>;
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
workflowsStore = useWorkflowsStore();
|
||||
uiStore = useUIStore();
|
||||
});
|
||||
|
||||
it('should initialize with default state', () => {
|
||||
expect(workflowsStore.workflow.name).toBe('');
|
||||
expect(workflowsStore.workflow.id).toBe(PLACEHOLDER_EMPTY_WORKFLOW_ID);
|
||||
});
|
||||
|
||||
describe('allWorkflows', () => {
|
||||
it('should return sorted workflows by name', () => {
|
||||
workflowsStore.setWorkflows([
|
||||
{ id: '3', name: 'Zeta' },
|
||||
{ id: '1', name: 'Alpha' },
|
||||
{ id: '2', name: 'Beta' },
|
||||
] as IWorkflowDb[]);
|
||||
|
||||
const allWorkflows = workflowsStore.allWorkflows;
|
||||
expect(allWorkflows[0].name).toBe('Alpha');
|
||||
expect(allWorkflows[1].name).toBe('Beta');
|
||||
expect(allWorkflows[2].name).toBe('Zeta');
|
||||
});
|
||||
|
||||
it('should return empty array when no workflows are set', () => {
|
||||
workflowsStore.setWorkflows([]);
|
||||
|
||||
const allWorkflows = workflowsStore.allWorkflows;
|
||||
expect(allWorkflows).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isNewWorkflow', () => {
|
||||
it('should return true for a new workflow', () => {
|
||||
expect(workflowsStore.isNewWorkflow).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for an existing workflow', () => {
|
||||
workflowsStore.setWorkflowId('123');
|
||||
expect(workflowsStore.isNewWorkflow).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('workflowTriggerNodes', () => {
|
||||
it('should return only nodes that are triggers', () => {
|
||||
vi.mocked(useNodeTypesStore).mockReturnValueOnce({
|
||||
getNodeType: vi.fn(() => ({
|
||||
group: ['trigger'],
|
||||
})),
|
||||
} as unknown as ReturnType<typeof useNodeTypesStore>);
|
||||
|
||||
workflowsStore.workflow.nodes = [
|
||||
{ type: 'triggerNode', typeVersion: '1' },
|
||||
{ type: 'nonTriggerNode', typeVersion: '1' },
|
||||
] as unknown as IWorkflowDb['nodes'];
|
||||
|
||||
expect(workflowsStore.workflowTriggerNodes).toHaveLength(1);
|
||||
expect(workflowsStore.workflowTriggerNodes[0].type).toBe('triggerNode');
|
||||
});
|
||||
|
||||
it('should return empty array when no nodes are triggers', () => {
|
||||
workflowsStore.workflow.nodes = [
|
||||
{ type: 'nonTriggerNode1', typeVersion: '1' },
|
||||
{ type: 'nonTriggerNode2', typeVersion: '1' },
|
||||
] as unknown as IWorkflowDb['nodes'];
|
||||
|
||||
expect(workflowsStore.workflowTriggerNodes).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('currentWorkflowHasWebhookNode', () => {
|
||||
it('should return true when a node has a webhookId', () => {
|
||||
workflowsStore.workflow.nodes = [
|
||||
{ name: 'Node1', webhookId: 'webhook1' },
|
||||
{ name: 'Node2' },
|
||||
] as unknown as IWorkflowDb['nodes'];
|
||||
|
||||
const hasWebhookNode = workflowsStore.currentWorkflowHasWebhookNode;
|
||||
expect(hasWebhookNode).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when no nodes have a webhookId', () => {
|
||||
workflowsStore.workflow.nodes = [
|
||||
{ name: 'Node1' },
|
||||
{ name: 'Node2' },
|
||||
] as unknown as IWorkflowDb['nodes'];
|
||||
|
||||
const hasWebhookNode = workflowsStore.currentWorkflowHasWebhookNode;
|
||||
expect(hasWebhookNode).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when there are no nodes', () => {
|
||||
workflowsStore.workflow.nodes = [];
|
||||
|
||||
const hasWebhookNode = workflowsStore.currentWorkflowHasWebhookNode;
|
||||
expect(hasWebhookNode).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getWorkflowRunData', () => {
|
||||
it('should return null when no execution data is present', () => {
|
||||
workflowsStore.workflowExecutionData = null;
|
||||
|
||||
const runData = workflowsStore.getWorkflowRunData;
|
||||
expect(runData).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null when execution data does not contain resultData', () => {
|
||||
workflowsStore.workflowExecutionData = { data: {} } as IExecutionResponse;
|
||||
|
||||
const runData = workflowsStore.getWorkflowRunData;
|
||||
expect(runData).toBeNull();
|
||||
});
|
||||
|
||||
it('should return runData when execution data contains resultData', () => {
|
||||
const expectedRunData = { node1: [{}, {}], node2: [{}] };
|
||||
workflowsStore.workflowExecutionData = {
|
||||
data: { resultData: { runData: expectedRunData } },
|
||||
} as unknown as IExecutionResponse;
|
||||
|
||||
const runData = workflowsStore.getWorkflowRunData;
|
||||
expect(runData).toEqual(expectedRunData);
|
||||
});
|
||||
});
|
||||
|
||||
describe('nodesIssuesExist', () => {
|
||||
it('should return true when a node has issues', () => {
|
||||
workflowsStore.workflow.nodes = [
|
||||
{ name: 'Node1', issues: { error: ['Error message'] } },
|
||||
{ name: 'Node2' },
|
||||
] as unknown as IWorkflowDb['nodes'];
|
||||
|
||||
const hasIssues = workflowsStore.nodesIssuesExist;
|
||||
expect(hasIssues).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when no nodes have issues', () => {
|
||||
workflowsStore.workflow.nodes = [
|
||||
{ name: 'Node1' },
|
||||
{ name: 'Node2' },
|
||||
] as unknown as IWorkflowDb['nodes'];
|
||||
|
||||
const hasIssues = workflowsStore.nodesIssuesExist;
|
||||
expect(hasIssues).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when there are no nodes', () => {
|
||||
workflowsStore.workflow.nodes = [];
|
||||
|
||||
const hasIssues = workflowsStore.nodesIssuesExist;
|
||||
expect(hasIssues).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('shouldReplaceInputDataWithPinData', () => {
|
||||
it('should return true when no active workflow execution', () => {
|
||||
workflowsStore.activeWorkflowExecution = null;
|
||||
|
||||
expect(workflowsStore.shouldReplaceInputDataWithPinData).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when active workflow execution mode is manual', () => {
|
||||
workflowsStore.activeWorkflowExecution = { mode: 'manual' } as unknown as ExecutionSummary;
|
||||
|
||||
expect(workflowsStore.shouldReplaceInputDataWithPinData).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when active workflow execution mode is not manual', () => {
|
||||
workflowsStore.activeWorkflowExecution = { mode: 'automatic' } as unknown as ExecutionSummary;
|
||||
|
||||
expect(workflowsStore.shouldReplaceInputDataWithPinData).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getWorkflowResultDataByNodeName()', () => {
|
||||
it('should return null when no workflow run data is present', () => {
|
||||
workflowsStore.workflowExecutionData = null;
|
||||
|
||||
const resultData = workflowsStore.getWorkflowResultDataByNodeName('Node1');
|
||||
expect(resultData).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null when node name is not present in workflow run data', () => {
|
||||
workflowsStore.workflowExecutionData = {
|
||||
data: { resultData: { runData: {} } },
|
||||
} as unknown as IExecutionResponse;
|
||||
|
||||
const resultData = workflowsStore.getWorkflowResultDataByNodeName('Node1');
|
||||
expect(resultData).toBeNull();
|
||||
});
|
||||
|
||||
it('should return result data when node name is present in workflow run data', () => {
|
||||
const expectedData = [{}, {}];
|
||||
workflowsStore.workflowExecutionData = {
|
||||
data: { resultData: { runData: { Node1: expectedData } } },
|
||||
} as unknown as IExecutionResponse;
|
||||
|
||||
const resultData = workflowsStore.getWorkflowResultDataByNodeName('Node1');
|
||||
expect(resultData).toEqual(expectedData);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isNodeInOutgoingNodeConnections()', () => {
|
||||
it('should return false when no outgoing connections from root node', () => {
|
||||
workflowsStore.workflow.connections = {};
|
||||
|
||||
const result = workflowsStore.isNodeInOutgoingNodeConnections('RootNode', 'SearchNode');
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when search node is directly connected to root node', () => {
|
||||
workflowsStore.workflow.connections = {
|
||||
RootNode: { main: [[{ node: 'SearchNode' } as IConnection]] },
|
||||
};
|
||||
|
||||
const result = workflowsStore.isNodeInOutgoingNodeConnections('RootNode', 'SearchNode');
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when search node is indirectly connected to root node', () => {
|
||||
workflowsStore.workflow.connections = {
|
||||
RootNode: { main: [[{ node: 'IntermediateNode' } as IConnection]] },
|
||||
IntermediateNode: { main: [[{ node: 'SearchNode' } as IConnection]] },
|
||||
};
|
||||
|
||||
const result = workflowsStore.isNodeInOutgoingNodeConnections('RootNode', 'SearchNode');
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when search node is not connected to root node', () => {
|
||||
workflowsStore.workflow.connections = {
|
||||
RootNode: { main: [[{ node: 'IntermediateNode' } as IConnection]] },
|
||||
IntermediateNode: { main: [[{ node: 'AnotherNode' } as IConnection]] },
|
||||
};
|
||||
|
||||
const result = workflowsStore.isNodeInOutgoingNodeConnections('RootNode', 'SearchNode');
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPinDataSize()', () => {
|
||||
it('returns zero when pinData is empty', () => {
|
||||
const pinData = {};
|
||||
const result = workflowsStore.getPinDataSize(pinData);
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
|
||||
it('returns correct size when pinData contains string values', () => {
|
||||
const pinData = {
|
||||
key1: 'value1',
|
||||
key2: 'value2',
|
||||
} as Record<string, string | INodeExecutionData[]>;
|
||||
const result = workflowsStore.getPinDataSize(pinData);
|
||||
expect(result).toBe(stringSizeInBytes(pinData.key1) + stringSizeInBytes(pinData.key2));
|
||||
});
|
||||
|
||||
it('returns correct size when pinData contains array values', () => {
|
||||
const pinData = {
|
||||
key1: [{ parameters: 'value1', data: null }],
|
||||
key2: [{ parameters: 'value2', data: null }],
|
||||
} as unknown as Record<string, string | INodeExecutionData[]>;
|
||||
const result = workflowsStore.getPinDataSize(pinData);
|
||||
expect(result).toBe(stringSizeInBytes(pinData.key1) + stringSizeInBytes(pinData.key2));
|
||||
});
|
||||
|
||||
it('returns correct size when pinData contains mixed string and array values', () => {
|
||||
const pinData = {
|
||||
key1: 'value1',
|
||||
key2: [{ parameters: 'value2', data: null }],
|
||||
} as unknown as Record<string, string | INodeExecutionData[]>;
|
||||
const result = workflowsStore.getPinDataSize(pinData);
|
||||
expect(result).toBe(stringSizeInBytes(pinData.key1) + stringSizeInBytes(pinData.key2));
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchAllWorkflows()', () => {
|
||||
it('should fetch workflows successfully', async () => {
|
||||
const mockWorkflows = [{ id: '1', name: 'Test Workflow' }] as IWorkflowDb[];
|
||||
vi.mocked(workflowsApi).getWorkflows.mockResolvedValue(mockWorkflows);
|
||||
|
||||
await workflowsStore.fetchAllWorkflows();
|
||||
|
||||
expect(workflowsApi.getWorkflows).toHaveBeenCalled();
|
||||
expect(Object.values(workflowsStore.workflowsById)).toEqual(mockWorkflows);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setWorkflowName()', () => {
|
||||
it('should set the workflow name correctly', () => {
|
||||
workflowsStore.setWorkflowName({ newName: 'New Workflow Name', setStateDirty: false });
|
||||
expect(workflowsStore.workflow.name).toBe('New Workflow Name');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setWorkflowActive()', () => {
|
||||
it('should set workflow as active when it is not already active', () => {
|
||||
workflowsStore.workflowsById = { '1': { active: false } as IWorkflowDb };
|
||||
workflowsStore.workflow.id = '1';
|
||||
|
||||
workflowsStore.setWorkflowActive('1');
|
||||
|
||||
expect(workflowsStore.activeWorkflows).toContain('1');
|
||||
expect(workflowsStore.workflowsById['1'].active).toBe(true);
|
||||
expect(workflowsStore.workflow.active).toBe(true);
|
||||
});
|
||||
|
||||
it('should not modify active workflows when workflow is already active', () => {
|
||||
workflowsStore.activeWorkflows = ['1'];
|
||||
workflowsStore.workflowsById = { '1': { active: true } as IWorkflowDb };
|
||||
workflowsStore.workflow.id = '1';
|
||||
|
||||
workflowsStore.setWorkflowActive('1');
|
||||
|
||||
expect(workflowsStore.activeWorkflows).toEqual(['1']);
|
||||
expect(workflowsStore.workflowsById['1'].active).toBe(true);
|
||||
expect(workflowsStore.workflow.active).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setWorkflowInactive()', () => {
|
||||
it('should set workflow as inactive when it exists', () => {
|
||||
workflowsStore.activeWorkflows = ['1', '2'];
|
||||
workflowsStore.workflowsById = { '1': { active: true } as IWorkflowDb };
|
||||
workflowsStore.setWorkflowInactive('1');
|
||||
expect(workflowsStore.workflowsById['1'].active).toBe(false);
|
||||
expect(workflowsStore.activeWorkflows).toEqual(['2']);
|
||||
});
|
||||
|
||||
it('should not modify active workflows when workflow is not active', () => {
|
||||
workflowsStore.workflowsById = { '2': { active: true } as IWorkflowDb };
|
||||
workflowsStore.activeWorkflows = ['2'];
|
||||
workflowsStore.setWorkflowInactive('1');
|
||||
expect(workflowsStore.activeWorkflows).toEqual(['2']);
|
||||
expect(workflowsStore.workflowsById['2'].active).toBe(true);
|
||||
});
|
||||
|
||||
it('should set current workflow as inactive when it is the target', () => {
|
||||
workflowsStore.workflow.id = '1';
|
||||
workflowsStore.workflow.active = true;
|
||||
workflowsStore.setWorkflowInactive('1');
|
||||
expect(workflowsStore.workflow.active).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDuplicateCurrentWorkflowName()', () => {
|
||||
it('should return the same name if appending postfix exceeds max length', async () => {
|
||||
const longName = 'a'.repeat(MAX_WORKFLOW_NAME_LENGTH - DUPLICATE_POSTFFIX.length + 1);
|
||||
const newName = await workflowsStore.getDuplicateCurrentWorkflowName(longName);
|
||||
expect(newName).toBe(longName);
|
||||
});
|
||||
|
||||
it('should append postfix to the name if it does not exceed max length', async () => {
|
||||
const name = 'TestWorkflow';
|
||||
const expectedName = `${name}${DUPLICATE_POSTFFIX}`;
|
||||
vi.mocked(workflowsApi).getNewWorkflow.mockResolvedValue({
|
||||
name: expectedName,
|
||||
onboardingFlowEnabled: false,
|
||||
settings: {} as IWorkflowSettings,
|
||||
});
|
||||
const newName = await workflowsStore.getDuplicateCurrentWorkflowName(name);
|
||||
expect(newName).toBe(expectedName);
|
||||
});
|
||||
|
||||
it('should handle API failure gracefully', async () => {
|
||||
const name = 'TestWorkflow';
|
||||
const expectedName = `${name}${DUPLICATE_POSTFFIX}`;
|
||||
vi.mocked(workflowsApi).getNewWorkflow.mockRejectedValue(new Error('API Error'));
|
||||
const newName = await workflowsStore.getDuplicateCurrentWorkflowName(name);
|
||||
expect(newName).toBe(expectedName);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pinData', () => {
|
||||
it('should create pinData object if it does not exist', async () => {
|
||||
workflowsStore.workflow.pinData = undefined;
|
||||
const node = { name: 'TestNode' } as INodeUi;
|
||||
const data = [{ json: 'testData' }] as unknown as INodeExecutionData[];
|
||||
workflowsStore.pinData({ node, data });
|
||||
expect(workflowsStore.workflow.pinData).toBeDefined();
|
||||
});
|
||||
|
||||
it('should convert data to array if it is not', async () => {
|
||||
const node = { name: 'TestNode' } as INodeUi;
|
||||
const data = { json: 'testData' } as unknown as INodeExecutionData;
|
||||
workflowsStore.pinData({ node, data: data as unknown as INodeExecutionData[] });
|
||||
expect(Array.isArray(workflowsStore.workflow.pinData?.[node.name])).toBe(true);
|
||||
});
|
||||
|
||||
it('should store pinData correctly', async () => {
|
||||
const node = { name: 'TestNode' } as INodeUi;
|
||||
const data = [{ json: 'testData' }] as unknown as INodeExecutionData[];
|
||||
workflowsStore.pinData({ node, data });
|
||||
expect(workflowsStore.workflow.pinData?.[node.name]).toEqual(data);
|
||||
});
|
||||
|
||||
it('should emit pin-data event', async () => {
|
||||
const node = { name: 'TestNode' } as INodeUi;
|
||||
const data = [{ json: 'testData' }] as unknown as INodeExecutionData[];
|
||||
const emitSpy = vi.spyOn(dataPinningEventBus, 'emit');
|
||||
workflowsStore.pinData({ node, data });
|
||||
expect(emitSpy).toHaveBeenCalledWith('pin-data', { [node.name]: data });
|
||||
});
|
||||
|
||||
it('should set stateIsDirty to true', async () => {
|
||||
uiStore.stateIsDirty = false;
|
||||
const node = { name: 'TestNode' } as INodeUi;
|
||||
const data = [{ json: 'testData' }] as unknown as INodeExecutionData[];
|
||||
workflowsStore.pinData({ node, data });
|
||||
expect(uiStore.stateIsDirty).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue