mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-27 23:07:12 +02:00
refactor(editor): Move workflow diff utils to workflow package (#21286)
This commit is contained in:
parent
918ef5fd57
commit
8a3ce4e9bf
|
|
@ -1,7 +1,7 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import DiffBadge from '@/features/workflows/workflowDiff/DiffBadge.vue';
|
||||
import { NodeDiffStatus } from '@/features/workflows/workflowDiff/useWorkflowDiff';
|
||||
import { NodeDiffStatus } from 'n8n-workflow';
|
||||
|
||||
const renderComponent = createComponentRenderer(DiffBadge);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { NodeDiffStatus } from '@/features/workflows/workflowDiff/useWorkflowDiff';
|
||||
import { NodeDiffStatus } from 'n8n-workflow';
|
||||
import { computed } from 'vue';
|
||||
const props = defineProps<{
|
||||
type: NodeDiffStatus;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import DiffBadge from '@/features/workflows/workflowDiff/DiffBadge.vue';
|
|||
import NodeDiff from '@/features/workflows/workflowDiff/NodeDiff.vue';
|
||||
import SyncedWorkflowCanvas from '@/features/workflows/workflowDiff/SyncedWorkflowCanvas.vue';
|
||||
import { useProvideViewportSync } from '@/features/workflows/workflowDiff/useViewportSync';
|
||||
import { NodeDiffStatus, useWorkflowDiff } from '@/features/workflows/workflowDiff/useWorkflowDiff';
|
||||
import { useWorkflowDiff } from '@/features/workflows/workflowDiff/useWorkflowDiff';
|
||||
import type { IWorkflowDb } from '@/Interface';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import { useSourceControlStore } from '@/features/integrations/sourceControl.ee/sourceControl.store';
|
||||
|
|
@ -19,7 +19,7 @@ import type { BaseTextKey } from '@n8n/i18n';
|
|||
import { useI18n } from '@n8n/i18n';
|
||||
import type { EventBus } from '@n8n/utils/event-bus';
|
||||
import { useAsyncState } from '@vueuse/core';
|
||||
import type { IWorkflowSettings } from 'n8n-workflow';
|
||||
import { NodeDiffStatus, type IWorkflowSettings } from 'n8n-workflow';
|
||||
import { computed, onMounted, onUnmounted, ref, useCssModule } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import HighlightedEdge from './HighlightedEdge.vue';
|
||||
|
|
|
|||
|
|
@ -1,19 +1,13 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { ref, computed } from 'vue';
|
||||
import {
|
||||
NodeDiffStatus,
|
||||
compareNodes,
|
||||
compareWorkflowsNodes,
|
||||
mapConnections,
|
||||
useWorkflowDiff,
|
||||
} from './useWorkflowDiff';
|
||||
import { mapConnections, useWorkflowDiff } from './useWorkflowDiff';
|
||||
import type {
|
||||
CanvasConnection,
|
||||
CanvasNode,
|
||||
ExecutionOutputMap,
|
||||
} from '@/features/workflows/canvas/canvas.types';
|
||||
import type { INodeUi, IWorkflowDb } from '@/Interface';
|
||||
import type { IConnections } from 'n8n-workflow';
|
||||
import { NodeDiffStatus, type IConnections } from 'n8n-workflow';
|
||||
import { useCanvasMapping } from '@/features/workflows/canvas/composables/useCanvasMapping';
|
||||
|
||||
// Mock modules at top level
|
||||
|
|
@ -48,268 +42,6 @@ vi.mock('@/features/workflows/canvas/composables/useCanvasMapping', () => ({
|
|||
}));
|
||||
|
||||
describe('useWorkflowDiff', () => {
|
||||
describe('NodeDiffStatus', () => {
|
||||
it('should have correct enum values', () => {
|
||||
expect(NodeDiffStatus.Eq).toBe('equal');
|
||||
expect(NodeDiffStatus.Modified).toBe('modified');
|
||||
expect(NodeDiffStatus.Added).toBe('added');
|
||||
expect(NodeDiffStatus.Deleted).toBe('deleted');
|
||||
});
|
||||
});
|
||||
|
||||
describe('compareNodes', () => {
|
||||
const createTestNode = (overrides: Partial<TestNode> = {}): TestNode => ({
|
||||
id: 'test-node-1',
|
||||
name: 'Test Node',
|
||||
type: 'test-type',
|
||||
typeVersion: 1,
|
||||
webhookId: 'webhook-123',
|
||||
credentials: { test: 'credential' },
|
||||
parameters: { param1: 'value1' },
|
||||
position: [100, 200],
|
||||
disabled: false,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
type TestNode = {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
typeVersion: number;
|
||||
webhookId: string;
|
||||
credentials: Record<string, unknown>;
|
||||
parameters: Record<string, unknown>;
|
||||
position: [number, number];
|
||||
disabled: boolean;
|
||||
};
|
||||
|
||||
it('should return true for identical nodes', () => {
|
||||
const node1 = createTestNode();
|
||||
const node2 = createTestNode();
|
||||
|
||||
const result = compareNodes(node1, node2);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when nodes have different names', () => {
|
||||
const node1 = createTestNode({ name: 'Node 1' });
|
||||
const node2 = createTestNode({ name: 'Node 2' });
|
||||
|
||||
const result = compareNodes(node1, node2);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when nodes have different types', () => {
|
||||
const node1 = createTestNode({ type: 'type1' });
|
||||
const node2 = createTestNode({ type: 'type2' });
|
||||
|
||||
const result = compareNodes(node1, node2);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when nodes have different typeVersions', () => {
|
||||
const node1 = createTestNode({ typeVersion: 1 });
|
||||
const node2 = createTestNode({ typeVersion: 2 });
|
||||
|
||||
const result = compareNodes(node1, node2);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when nodes have different webhookIds', () => {
|
||||
const node1 = createTestNode({ webhookId: 'webhook1' });
|
||||
const node2 = createTestNode({ webhookId: 'webhook2' });
|
||||
|
||||
const result = compareNodes(node1, node2);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when nodes have different credentials', () => {
|
||||
const node1 = createTestNode({ credentials: { test: 'cred1' } });
|
||||
const node2 = createTestNode({ credentials: { test: 'cred2' } });
|
||||
|
||||
const result = compareNodes(node1, node2);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when nodes have different parameters', () => {
|
||||
const node1 = createTestNode({ parameters: { param1: 'value1' } });
|
||||
const node2 = createTestNode({ parameters: { param1: 'value2' } });
|
||||
|
||||
const result = compareNodes(node1, node2);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should ignore properties not in comparison list', () => {
|
||||
const node1 = createTestNode({ position: [100, 200], disabled: false });
|
||||
const node2 = createTestNode({ position: [300, 400], disabled: true });
|
||||
|
||||
const result = compareNodes(node1, node2);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle undefined base node', () => {
|
||||
const node2 = createTestNode();
|
||||
|
||||
const result = compareNodes(undefined, node2);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle undefined target node', () => {
|
||||
const node1 = createTestNode();
|
||||
|
||||
const result = compareNodes(node1, undefined);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle both nodes being undefined', () => {
|
||||
const result = compareNodes(undefined, undefined);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('compareWorkflowsNodes', () => {
|
||||
const createTestNode = (id: string, overrides: Partial<TestNode> = {}): TestNode => ({
|
||||
id,
|
||||
name: `Node ${id}`,
|
||||
type: 'test-type',
|
||||
typeVersion: 1,
|
||||
webhookId: `webhook-${id}`,
|
||||
credentials: {},
|
||||
parameters: {},
|
||||
...overrides,
|
||||
});
|
||||
|
||||
type TestNode = {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
typeVersion: number;
|
||||
webhookId: string;
|
||||
credentials: Record<string, unknown>;
|
||||
parameters: Record<string, unknown>;
|
||||
};
|
||||
|
||||
it('should detect equal nodes', () => {
|
||||
const baseNodes = [createTestNode('1'), createTestNode('2')];
|
||||
const targetNodes = [createTestNode('1'), createTestNode('2')];
|
||||
|
||||
const diff = compareWorkflowsNodes(baseNodes, targetNodes);
|
||||
|
||||
expect(diff.size).toBe(2);
|
||||
expect(diff.get('1')?.status).toBe(NodeDiffStatus.Eq);
|
||||
expect(diff.get('2')?.status).toBe(NodeDiffStatus.Eq);
|
||||
});
|
||||
|
||||
it('should detect modified nodes', () => {
|
||||
const baseNodes = [createTestNode('1', { name: 'Original Name' })];
|
||||
const targetNodes = [createTestNode('1', { name: 'Modified Name' })];
|
||||
|
||||
const diff = compareWorkflowsNodes(baseNodes, targetNodes);
|
||||
|
||||
expect(diff.size).toBe(1);
|
||||
expect(diff.get('1')?.status).toBe(NodeDiffStatus.Modified);
|
||||
expect(diff.get('1')?.node).toEqual(baseNodes[0]);
|
||||
});
|
||||
|
||||
it('should detect added nodes', () => {
|
||||
const baseNodes = [createTestNode('1')];
|
||||
const targetNodes = [createTestNode('1'), createTestNode('2')];
|
||||
|
||||
const diff = compareWorkflowsNodes(baseNodes, targetNodes);
|
||||
|
||||
expect(diff.size).toBe(2);
|
||||
expect(diff.get('1')?.status).toBe(NodeDiffStatus.Eq);
|
||||
expect(diff.get('2')?.status).toBe(NodeDiffStatus.Added);
|
||||
expect(diff.get('2')?.node).toEqual(targetNodes[1]);
|
||||
});
|
||||
|
||||
it('should detect deleted nodes', () => {
|
||||
const baseNodes = [createTestNode('1'), createTestNode('2')];
|
||||
const targetNodes = [createTestNode('1')];
|
||||
|
||||
const diff = compareWorkflowsNodes(baseNodes, targetNodes);
|
||||
|
||||
expect(diff.size).toBe(2);
|
||||
expect(diff.get('1')?.status).toBe(NodeDiffStatus.Eq);
|
||||
expect(diff.get('2')?.status).toBe(NodeDiffStatus.Deleted);
|
||||
expect(diff.get('2')?.node).toEqual(baseNodes[1]);
|
||||
});
|
||||
|
||||
it('should handle empty base array', () => {
|
||||
const baseNodes: TestNode[] = [];
|
||||
const targetNodes = [createTestNode('1'), createTestNode('2')];
|
||||
|
||||
const diff = compareWorkflowsNodes(baseNodes, targetNodes);
|
||||
|
||||
expect(diff.size).toBe(2);
|
||||
expect(diff.get('1')?.status).toBe(NodeDiffStatus.Added);
|
||||
expect(diff.get('2')?.status).toBe(NodeDiffStatus.Added);
|
||||
});
|
||||
|
||||
it('should handle empty target array', () => {
|
||||
const baseNodes = [createTestNode('1'), createTestNode('2')];
|
||||
const targetNodes: TestNode[] = [];
|
||||
|
||||
const diff = compareWorkflowsNodes(baseNodes, targetNodes);
|
||||
|
||||
expect(diff.size).toBe(2);
|
||||
expect(diff.get('1')?.status).toBe(NodeDiffStatus.Deleted);
|
||||
expect(diff.get('2')?.status).toBe(NodeDiffStatus.Deleted);
|
||||
});
|
||||
|
||||
it('should handle both arrays being empty', () => {
|
||||
const baseNodes: TestNode[] = [];
|
||||
const targetNodes: TestNode[] = [];
|
||||
|
||||
const diff = compareWorkflowsNodes(baseNodes, targetNodes);
|
||||
|
||||
expect(diff.size).toBe(0);
|
||||
});
|
||||
|
||||
it('should use custom comparison function when provided', () => {
|
||||
const baseNodes = [createTestNode('1', { name: 'Original' })];
|
||||
const targetNodes = [createTestNode('1', { name: 'Modified' })];
|
||||
|
||||
const customCompare = vi.fn().mockReturnValue(true);
|
||||
const diff = compareWorkflowsNodes(baseNodes, targetNodes, customCompare);
|
||||
|
||||
expect(customCompare).toHaveBeenCalledWith(baseNodes[0], targetNodes[0]);
|
||||
expect(diff.get('1')?.status).toBe(NodeDiffStatus.Eq);
|
||||
});
|
||||
|
||||
it('should handle complex workflow comparison', () => {
|
||||
const baseNodes = [
|
||||
createTestNode('1', { name: 'Node 1' }),
|
||||
createTestNode('2', { name: 'Node 2' }),
|
||||
createTestNode('3', { name: 'Node 3' }),
|
||||
];
|
||||
const targetNodes = [
|
||||
createTestNode('1', { name: 'Node 1' }), // Equal
|
||||
createTestNode('2', { name: 'Node 2 Modified' }), // Modified
|
||||
createTestNode('4', { name: 'Node 4' }), // Added
|
||||
];
|
||||
|
||||
const diff = compareWorkflowsNodes(baseNodes, targetNodes);
|
||||
|
||||
expect(diff.size).toBe(4);
|
||||
expect(diff.get('1')?.status).toBe(NodeDiffStatus.Eq);
|
||||
expect(diff.get('2')?.status).toBe(NodeDiffStatus.Modified);
|
||||
expect(diff.get('3')?.status).toBe(NodeDiffStatus.Deleted);
|
||||
expect(diff.get('4')?.status).toBe(NodeDiffStatus.Added);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapConnections', () => {
|
||||
const createTestConnection = (
|
||||
id: string,
|
||||
|
|
|
|||
|
|
@ -1,76 +1,13 @@
|
|||
import _pick from 'lodash-es/pick';
|
||||
import _isEqual from 'lodash-es/isEqual';
|
||||
import type { CanvasConnection, CanvasNode } from '@/features/workflows/canvas/canvas.types';
|
||||
import type { INodeUi, IWorkflowDb } from '@/Interface';
|
||||
import type { MaybeRefOrGetter, Ref, ComputedRef } from 'vue';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { toValue, computed, ref, watchEffect, shallowRef } from 'vue';
|
||||
import { useCanvasMapping } from '@/features/workflows/canvas/composables/useCanvasMapping';
|
||||
import type { Workflow, IConnections, INodeTypeDescription } from 'n8n-workflow';
|
||||
import type { Workflow, IConnections, INodeTypeDescription, NodeDiff } from 'n8n-workflow';
|
||||
import { compareWorkflowsNodes, NodeDiffStatus } from 'n8n-workflow';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
|
||||
export const enum NodeDiffStatus {
|
||||
Eq = 'equal',
|
||||
Modified = 'modified',
|
||||
Added = 'added',
|
||||
Deleted = 'deleted',
|
||||
}
|
||||
|
||||
export type NodeDiff<T> = {
|
||||
status: NodeDiffStatus;
|
||||
node: T;
|
||||
};
|
||||
|
||||
export type WorkflowDiff<T> = Map<string, NodeDiff<T>>;
|
||||
|
||||
export function compareNodes<T extends { id: string }>(
|
||||
base: T | undefined,
|
||||
target: T | undefined,
|
||||
): boolean {
|
||||
const propsToCompare = ['name', 'type', 'typeVersion', 'webhookId', 'credentials', 'parameters'];
|
||||
|
||||
const baseNode = _pick(base, propsToCompare);
|
||||
const targetNode = _pick(target, propsToCompare);
|
||||
|
||||
return _isEqual(baseNode, targetNode);
|
||||
}
|
||||
|
||||
export function compareWorkflowsNodes<T extends { id: string }>(
|
||||
base: T[],
|
||||
target: T[],
|
||||
nodesEqual: (base: T | undefined, target: T | undefined) => boolean = compareNodes,
|
||||
): WorkflowDiff<T> {
|
||||
const baseNodes = base.reduce<Map<string, T>>((acc, node) => {
|
||||
acc.set(node.id, node);
|
||||
return acc;
|
||||
}, new Map());
|
||||
|
||||
const targetNodes = target.reduce<Map<string, T>>((acc, node) => {
|
||||
acc.set(node.id, node);
|
||||
return acc;
|
||||
}, new Map());
|
||||
|
||||
const diff: WorkflowDiff<T> = new Map();
|
||||
|
||||
for (const [id, node] of baseNodes.entries()) {
|
||||
if (!targetNodes.has(id)) {
|
||||
diff.set(id, { status: NodeDiffStatus.Deleted, node });
|
||||
} else if (!nodesEqual(baseNodes.get(id), targetNodes.get(id))) {
|
||||
diff.set(id, { status: NodeDiffStatus.Modified, node });
|
||||
} else {
|
||||
diff.set(id, { status: NodeDiffStatus.Eq, node });
|
||||
}
|
||||
}
|
||||
|
||||
for (const [id, node] of targetNodes.entries()) {
|
||||
if (!baseNodes.has(id)) {
|
||||
diff.set(id, { status: NodeDiffStatus.Added, node });
|
||||
}
|
||||
}
|
||||
|
||||
return diff;
|
||||
}
|
||||
|
||||
export function mapConnections(connections: CanvasConnection[]) {
|
||||
return connections.reduce(
|
||||
(acc, connection) => {
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ export * from './node-parameters/filter-parameter';
|
|||
export * from './node-parameters/parameter-type-validation';
|
||||
export * from './node-parameters/path-utils';
|
||||
export * from './evaluation-helpers';
|
||||
export * from './workflow-diff';
|
||||
|
||||
export type {
|
||||
DocMetadata,
|
||||
|
|
|
|||
64
packages/workflow/src/workflow-diff.ts
Normal file
64
packages/workflow/src/workflow-diff.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import isEqual from 'lodash/isEqual';
|
||||
import pick from 'lodash/pick';
|
||||
|
||||
export const enum NodeDiffStatus {
|
||||
Eq = 'equal',
|
||||
Modified = 'modified',
|
||||
Added = 'added',
|
||||
Deleted = 'deleted',
|
||||
}
|
||||
|
||||
export type NodeDiff<T> = {
|
||||
status: NodeDiffStatus;
|
||||
node: T;
|
||||
};
|
||||
|
||||
export type WorkflowDiff<T> = Map<string, NodeDiff<T>>;
|
||||
|
||||
export function compareNodes<T extends { id: string }>(
|
||||
base: T | undefined,
|
||||
target: T | undefined,
|
||||
): boolean {
|
||||
const propsToCompare = ['name', 'type', 'typeVersion', 'webhookId', 'credentials', 'parameters'];
|
||||
|
||||
const baseNode = pick(base, propsToCompare);
|
||||
const targetNode = pick(target, propsToCompare);
|
||||
|
||||
return isEqual(baseNode, targetNode);
|
||||
}
|
||||
|
||||
export function compareWorkflowsNodes<T extends { id: string }>(
|
||||
base: T[],
|
||||
target: T[],
|
||||
nodesEqual: (base: T | undefined, target: T | undefined) => boolean = compareNodes,
|
||||
): WorkflowDiff<T> {
|
||||
const baseNodes = base.reduce<Map<string, T>>((acc, node) => {
|
||||
acc.set(node.id, node);
|
||||
return acc;
|
||||
}, new Map());
|
||||
|
||||
const targetNodes = target.reduce<Map<string, T>>((acc, node) => {
|
||||
acc.set(node.id, node);
|
||||
return acc;
|
||||
}, new Map());
|
||||
|
||||
const diff: WorkflowDiff<T> = new Map();
|
||||
|
||||
for (const [id, node] of baseNodes.entries()) {
|
||||
if (!targetNodes.has(id)) {
|
||||
diff.set(id, { status: NodeDiffStatus.Deleted, node });
|
||||
} else if (!nodesEqual(baseNodes.get(id), targetNodes.get(id))) {
|
||||
diff.set(id, { status: NodeDiffStatus.Modified, node });
|
||||
} else {
|
||||
diff.set(id, { status: NodeDiffStatus.Eq, node });
|
||||
}
|
||||
}
|
||||
|
||||
for (const [id, node] of targetNodes.entries()) {
|
||||
if (!baseNodes.has(id)) {
|
||||
diff.set(id, { status: NodeDiffStatus.Added, node });
|
||||
}
|
||||
}
|
||||
|
||||
return diff;
|
||||
}
|
||||
263
packages/workflow/test/workflow-diff.test.ts
Normal file
263
packages/workflow/test/workflow-diff.test.ts
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
import { compareNodes, compareWorkflowsNodes, NodeDiffStatus } from '../src/workflow-diff';
|
||||
|
||||
describe('NodeDiffStatus', () => {
|
||||
it('should have correct enum values', () => {
|
||||
expect(NodeDiffStatus.Eq).toBe('equal');
|
||||
expect(NodeDiffStatus.Modified).toBe('modified');
|
||||
expect(NodeDiffStatus.Added).toBe('added');
|
||||
expect(NodeDiffStatus.Deleted).toBe('deleted');
|
||||
});
|
||||
});
|
||||
|
||||
describe('compareNodes', () => {
|
||||
const createTestNode = (overrides: Partial<TestNode> = {}): TestNode => ({
|
||||
id: 'test-node-1',
|
||||
name: 'Test Node',
|
||||
type: 'test-type',
|
||||
typeVersion: 1,
|
||||
webhookId: 'webhook-123',
|
||||
credentials: { test: 'credential' },
|
||||
parameters: { param1: 'value1' },
|
||||
position: [100, 200],
|
||||
disabled: false,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
type TestNode = {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
typeVersion: number;
|
||||
webhookId: string;
|
||||
credentials: Record<string, unknown>;
|
||||
parameters: Record<string, unknown>;
|
||||
position: [number, number];
|
||||
disabled: boolean;
|
||||
};
|
||||
|
||||
it('should return true for identical nodes', () => {
|
||||
const node1 = createTestNode();
|
||||
const node2 = createTestNode();
|
||||
|
||||
const result = compareNodes(node1, node2);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when nodes have different names', () => {
|
||||
const node1 = createTestNode({ name: 'Node 1' });
|
||||
const node2 = createTestNode({ name: 'Node 2' });
|
||||
|
||||
const result = compareNodes(node1, node2);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when nodes have different types', () => {
|
||||
const node1 = createTestNode({ type: 'type1' });
|
||||
const node2 = createTestNode({ type: 'type2' });
|
||||
|
||||
const result = compareNodes(node1, node2);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when nodes have different typeVersions', () => {
|
||||
const node1 = createTestNode({ typeVersion: 1 });
|
||||
const node2 = createTestNode({ typeVersion: 2 });
|
||||
|
||||
const result = compareNodes(node1, node2);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when nodes have different webhookIds', () => {
|
||||
const node1 = createTestNode({ webhookId: 'webhook1' });
|
||||
const node2 = createTestNode({ webhookId: 'webhook2' });
|
||||
|
||||
const result = compareNodes(node1, node2);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when nodes have different credentials', () => {
|
||||
const node1 = createTestNode({ credentials: { test: 'cred1' } });
|
||||
const node2 = createTestNode({ credentials: { test: 'cred2' } });
|
||||
|
||||
const result = compareNodes(node1, node2);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when nodes have different parameters', () => {
|
||||
const node1 = createTestNode({ parameters: { param1: 'value1' } });
|
||||
const node2 = createTestNode({ parameters: { param1: 'value2' } });
|
||||
|
||||
const result = compareNodes(node1, node2);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should ignore properties not in comparison list', () => {
|
||||
const node1 = createTestNode({ position: [100, 200], disabled: false });
|
||||
const node2 = createTestNode({ position: [300, 400], disabled: true });
|
||||
|
||||
const result = compareNodes(node1, node2);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle undefined base node', () => {
|
||||
const node2 = createTestNode();
|
||||
|
||||
const result = compareNodes(undefined, node2);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle undefined target node', () => {
|
||||
const node1 = createTestNode();
|
||||
|
||||
const result = compareNodes(node1, undefined);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle both nodes being undefined', () => {
|
||||
const result = compareNodes(undefined, undefined);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('compareWorkflowsNodes', () => {
|
||||
const createTestNode = (id: string, overrides: Partial<TestNode> = {}): TestNode => ({
|
||||
id,
|
||||
name: `Node ${id}`,
|
||||
type: 'test-type',
|
||||
typeVersion: 1,
|
||||
webhookId: `webhook-${id}`,
|
||||
credentials: {},
|
||||
parameters: {},
|
||||
...overrides,
|
||||
});
|
||||
|
||||
type TestNode = {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
typeVersion: number;
|
||||
webhookId: string;
|
||||
credentials: Record<string, unknown>;
|
||||
parameters: Record<string, unknown>;
|
||||
};
|
||||
|
||||
it('should detect equal nodes', () => {
|
||||
const baseNodes = [createTestNode('1'), createTestNode('2')];
|
||||
const targetNodes = [createTestNode('1'), createTestNode('2')];
|
||||
|
||||
const diff = compareWorkflowsNodes(baseNodes, targetNodes);
|
||||
|
||||
expect(diff.size).toBe(2);
|
||||
expect(diff.get('1')?.status).toBe(NodeDiffStatus.Eq);
|
||||
expect(diff.get('2')?.status).toBe(NodeDiffStatus.Eq);
|
||||
});
|
||||
|
||||
it('should detect modified nodes', () => {
|
||||
const baseNodes = [createTestNode('1', { name: 'Original Name' })];
|
||||
const targetNodes = [createTestNode('1', { name: 'Modified Name' })];
|
||||
|
||||
const diff = compareWorkflowsNodes(baseNodes, targetNodes);
|
||||
|
||||
expect(diff.size).toBe(1);
|
||||
expect(diff.get('1')?.status).toBe(NodeDiffStatus.Modified);
|
||||
expect(diff.get('1')?.node).toEqual(baseNodes[0]);
|
||||
});
|
||||
|
||||
it('should detect added nodes', () => {
|
||||
const baseNodes = [createTestNode('1')];
|
||||
const targetNodes = [createTestNode('1'), createTestNode('2')];
|
||||
|
||||
const diff = compareWorkflowsNodes(baseNodes, targetNodes);
|
||||
|
||||
expect(diff.size).toBe(2);
|
||||
expect(diff.get('1')?.status).toBe(NodeDiffStatus.Eq);
|
||||
expect(diff.get('2')?.status).toBe(NodeDiffStatus.Added);
|
||||
expect(diff.get('2')?.node).toEqual(targetNodes[1]);
|
||||
});
|
||||
|
||||
it('should detect deleted nodes', () => {
|
||||
const baseNodes = [createTestNode('1'), createTestNode('2')];
|
||||
const targetNodes = [createTestNode('1')];
|
||||
|
||||
const diff = compareWorkflowsNodes(baseNodes, targetNodes);
|
||||
|
||||
expect(diff.size).toBe(2);
|
||||
expect(diff.get('1')?.status).toBe(NodeDiffStatus.Eq);
|
||||
expect(diff.get('2')?.status).toBe(NodeDiffStatus.Deleted);
|
||||
expect(diff.get('2')?.node).toEqual(baseNodes[1]);
|
||||
});
|
||||
|
||||
it('should handle empty base array', () => {
|
||||
const baseNodes: TestNode[] = [];
|
||||
const targetNodes = [createTestNode('1'), createTestNode('2')];
|
||||
|
||||
const diff = compareWorkflowsNodes(baseNodes, targetNodes);
|
||||
|
||||
expect(diff.size).toBe(2);
|
||||
expect(diff.get('1')?.status).toBe(NodeDiffStatus.Added);
|
||||
expect(diff.get('2')?.status).toBe(NodeDiffStatus.Added);
|
||||
});
|
||||
|
||||
it('should handle empty target array', () => {
|
||||
const baseNodes = [createTestNode('1'), createTestNode('2')];
|
||||
const targetNodes: TestNode[] = [];
|
||||
|
||||
const diff = compareWorkflowsNodes(baseNodes, targetNodes);
|
||||
|
||||
expect(diff.size).toBe(2);
|
||||
expect(diff.get('1')?.status).toBe(NodeDiffStatus.Deleted);
|
||||
expect(diff.get('2')?.status).toBe(NodeDiffStatus.Deleted);
|
||||
});
|
||||
|
||||
it('should handle both arrays being empty', () => {
|
||||
const baseNodes: TestNode[] = [];
|
||||
const targetNodes: TestNode[] = [];
|
||||
|
||||
const diff = compareWorkflowsNodes(baseNodes, targetNodes);
|
||||
|
||||
expect(diff.size).toBe(0);
|
||||
});
|
||||
|
||||
it('should use custom comparison function when provided', () => {
|
||||
const baseNodes = [createTestNode('1', { name: 'Original' })];
|
||||
const targetNodes = [createTestNode('1', { name: 'Modified' })];
|
||||
|
||||
const customCompare = vi.fn().mockReturnValue(true);
|
||||
const diff = compareWorkflowsNodes(baseNodes, targetNodes, customCompare);
|
||||
|
||||
expect(customCompare).toHaveBeenCalledWith(baseNodes[0], targetNodes[0]);
|
||||
expect(diff.get('1')?.status).toBe(NodeDiffStatus.Eq);
|
||||
});
|
||||
|
||||
it('should handle complex workflow comparison', () => {
|
||||
const baseNodes = [
|
||||
createTestNode('1', { name: 'Node 1' }),
|
||||
createTestNode('2', { name: 'Node 2' }),
|
||||
createTestNode('3', { name: 'Node 3' }),
|
||||
];
|
||||
const targetNodes = [
|
||||
createTestNode('1', { name: 'Node 1' }), // Equal
|
||||
createTestNode('2', { name: 'Node 2 Modified' }), // Modified
|
||||
createTestNode('4', { name: 'Node 4' }), // Added
|
||||
];
|
||||
|
||||
const diff = compareWorkflowsNodes(baseNodes, targetNodes);
|
||||
|
||||
expect(diff.size).toBe(4);
|
||||
expect(diff.get('1')?.status).toBe(NodeDiffStatus.Eq);
|
||||
expect(diff.get('2')?.status).toBe(NodeDiffStatus.Modified);
|
||||
expect(diff.get('3')?.status).toBe(NodeDiffStatus.Deleted);
|
||||
expect(diff.get('4')?.status).toBe(NodeDiffStatus.Added);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user