diff --git a/packages/editor-ui/src/__tests__/mocks.ts b/packages/editor-ui/src/__tests__/mocks.ts index dac9c9c478..1ab9d3a42f 100644 --- a/packages/editor-ui/src/__tests__/mocks.ts +++ b/packages/editor-ui/src/__tests__/mocks.ts @@ -26,6 +26,7 @@ import { STICKY_NODE_TYPE, } from '@/constants'; import type { INodeUi, IWorkflowDb } from '@/Interface'; +import { CanvasNodeRenderType } from '@/types'; export const mockNode = ({ id = uuid(), @@ -94,6 +95,7 @@ export const mockNodes = [ mockNode({ name: 'Chat Trigger', type: CHAT_TRIGGER_NODE_TYPE }), mockNode({ name: 'Agent', type: AGENT_NODE_TYPE }), mockNode({ name: 'Sticky', type: STICKY_NODE_TYPE }), + mockNode({ name: CanvasNodeRenderType.AddNodes, type: CanvasNodeRenderType.AddNodes }), mockNode({ name: 'End', type: NO_OP_NODE_TYPE }), ]; @@ -180,7 +182,7 @@ export function createTestNode(node: Partial = {}): INode { return { id: uuid(), name: 'Node', - type: 'n8n-nodes-base.test', + type: 'n8n-nodes-base.set', typeVersion: 1, position: [0, 0] as [number, number], parameters: {}, diff --git a/packages/editor-ui/src/components/canvas/WorkflowCanvas.spec.ts b/packages/editor-ui/src/components/canvas/WorkflowCanvas.spec.ts new file mode 100644 index 0000000000..75932803d3 --- /dev/null +++ b/packages/editor-ui/src/components/canvas/WorkflowCanvas.spec.ts @@ -0,0 +1,146 @@ +import { waitFor } from '@testing-library/vue'; +import { createPinia, setActivePinia } from 'pinia'; +import WorkflowCanvas from '@/components/canvas/WorkflowCanvas.vue'; +import { createEventBus } from 'n8n-design-system'; +import { createCanvasNodeElement, createCanvasConnection } from '@/__tests__/data'; +import type { Workflow } from 'n8n-workflow'; +import { createComponentRenderer } from '@/__tests__/render'; +import { STICKY_NODE_TYPE } from '@/constants'; +import { CanvasNodeRenderType } from '@/types'; +import { + createTestNode, + createTestWorkflow, + createTestWorkflowObject, + defaultNodeDescriptions, +} from '@/__tests__/mocks'; +import { useNodeTypesStore } from '@/stores/nodeTypes.store'; + +const renderComponent = createComponentRenderer(WorkflowCanvas, { + props: { + id: 'canvas', + workflow: { + id: '1', + name: 'Test Workflow', + nodes: [], + connections: [], + }, + workflowObject: {} as Workflow, + eventBus: createEventBus(), + }, +}); + +beforeEach(() => { + const pinia = createPinia(); + setActivePinia(pinia); + + const nodeTypesStore = useNodeTypesStore(); + nodeTypesStore.setNodeTypes(defaultNodeDescriptions); +}); + +afterEach(() => { + vi.clearAllMocks(); +}); + +describe('WorkflowCanvas', () => { + it('should initialize with default props', () => { + const { getByTestId } = renderComponent(); + + expect(getByTestId('canvas')).toBeVisible(); + }); + + it('should render nodes and connections', async () => { + const nodes = [ + createCanvasNodeElement({ id: '1', label: 'Node 1' }), + createCanvasNodeElement({ id: '2', label: 'Node 2' }), + ]; + const connections = [createCanvasConnection(nodes[0], nodes[1])]; + + const { container } = renderComponent({ + props: { + nodes, + connections, + }, + }); + + await waitFor(() => expect(container.querySelectorAll('.vue-flow__node')).toHaveLength(2)); + + expect(container.querySelector(`[data-id="${nodes[0].id}"]`)).toBeInTheDocument(); + expect(container.querySelector(`[data-id="${nodes[1].id}"]`)).toBeInTheDocument(); + expect(container.querySelector(`[data-id="${connections[0].id}"]`)).toBeInTheDocument(); + }); + + it('should handle empty nodes and connections gracefully', async () => { + const { container } = renderComponent(); + + await waitFor(() => expect(container.querySelectorAll('.vue-flow__node')).toHaveLength(0)); + expect(container.querySelectorAll('.vue-flow__connection')).toHaveLength(0); + }); + + it('should render fallback nodes when sticky nodes are present', async () => { + const stickyNodes = [createTestNode({ id: '2', name: 'Sticky Node', type: STICKY_NODE_TYPE })]; + const fallbackNodes = [ + createTestNode({ + id: CanvasNodeRenderType.AddNodes, + type: CanvasNodeRenderType.AddNodes, + name: CanvasNodeRenderType.AddNodes, + }), + ]; + + const workflow = createTestWorkflow({ + id: '1', + name: 'Test Workflow', + nodes: [...stickyNodes], + connections: {}, + }); + + const workflowObject = createTestWorkflowObject(workflow); + + const { container } = renderComponent({ + props: { + workflow, + workflowObject, + fallbackNodes, + }, + }); + + await waitFor(() => expect(container.querySelectorAll('.vue-flow__node')).toHaveLength(2)); + + expect(container.querySelector(`[data-id="${stickyNodes[0].id}"]`)).toBeInTheDocument(); + expect(container.querySelector(`[data-id="${fallbackNodes[0].id}"]`)).toBeInTheDocument(); + }); + + it('should not render fallback nodes when non-sticky nodes are present', async () => { + const nonStickyNodes = [createTestNode({ id: '1', name: 'Non-Sticky Node 1' })]; + const stickyNodes = [createTestNode({ id: '2', name: 'Sticky Node', type: STICKY_NODE_TYPE })]; + const fallbackNodes = [ + createTestNode({ + id: CanvasNodeRenderType.AddNodes, + type: CanvasNodeRenderType.AddNodes, + name: CanvasNodeRenderType.AddNodes, + }), + ]; + + const workflow = createTestWorkflow({ + id: '1', + name: 'Test Workflow', + nodes: [...nonStickyNodes, ...stickyNodes], + connections: {}, + }); + + const workflowObject = createTestWorkflowObject(workflow); + + const { container } = renderComponent({ + props: { + workflow, + workflowObject, + fallbackNodes, + }, + }); + + await waitFor(() => expect(container.querySelectorAll('.vue-flow__node')).toHaveLength(2)); + + expect(container.querySelector(`[data-id="${nonStickyNodes[0].id}"]`)).toBeInTheDocument(); + expect(container.querySelector(`[data-id="${stickyNodes[0].id}"]`)).toBeInTheDocument(); + expect(container.querySelector(`[data-id="${fallbackNodes[0].id}"]`)).not.toBeInTheDocument(); + }); +}); diff --git a/packages/editor-ui/src/components/canvas/WorkflowCanvas.vue b/packages/editor-ui/src/components/canvas/WorkflowCanvas.vue index 19482bf699..237b2e29e2 100644 --- a/packages/editor-ui/src/components/canvas/WorkflowCanvas.vue +++ b/packages/editor-ui/src/components/canvas/WorkflowCanvas.vue @@ -6,6 +6,7 @@ import type { IWorkflowDb } from '@/Interface'; import { useCanvasMapping } from '@/composables/useCanvasMapping'; import type { EventBus } from 'n8n-design-system'; import { createEventBus } from 'n8n-design-system'; +import { STICKY_NODE_TYPE } from '@/constants'; defineOptions({ inheritAttrs: false, @@ -32,9 +33,13 @@ const $style = useCssModule(); const workflow = toRef(props, 'workflow'); const workflowObject = toRef(props, 'workflowObject'); -const nodes = computed(() => - props.workflow.nodes.length > 0 ? props.workflow.nodes : props.fallbackNodes, -); +const nodes = computed(() => { + const stickyNoteNodes = props.workflow.nodes.filter((node) => node.type === STICKY_NODE_TYPE); + + return props.workflow.nodes.length > stickyNoteNodes.length + ? props.workflow.nodes + : [...props.fallbackNodes, ...stickyNoteNodes]; +}); const connections = computed(() => props.workflow.connections); const { nodes: mappedNodes, connections: mappedConnections } = useCanvasMapping({