refactor(editor): Migrate workflows store to setup function with composition API (no-changelog) (#9270)

This commit is contained in:
Alex Grozav 2024-05-08 14:35:29 +03:00 committed by GitHub
parent 6b6e8dfc33
commit f64a41d617
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 2002 additions and 1543 deletions

View file

@ -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

View file

@ -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}`,
);
}

View file

@ -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,
});
};
});

View file

@ -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();

View file

@ -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) => {

View file

@ -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,
},
);
});
});
});

View file

@ -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);
});
});
});

View 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