Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/sdk-self-loop-measured-height.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@workflowbuilder/sdk': patch
---

Self-connecting edges now use the measured source-node height consistently for loop geometry and label placement.
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { render, screen } from '@testing-library/react';
import type { EdgeProps } from '@xyflow/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';

import type { WorkflowBuilderEdge } from '../../../../node/node-data';
import { LabelEdge } from './label-edge';

const { nodeLookup } = vi.hoisted(() => ({ nodeLookup: new Map<string, unknown>() }));

vi.mock('@xyflow/react', () => ({
getSmoothStepPath: () => ['M 0 0', 0, 0],
useStore: (selector: (state: { nodeLookup: Map<string, unknown> }) => unknown) => selector({ nodeLookup }),
}));

vi.mock('../edge-label-renderer/edge-label-renderer', () => ({
EdgeLabel: ({ id, labelY }: { id: string; labelY: number }) => (
<span data-testid="edge-label" data-edge-label-id={id} data-label-y={labelY} />
),
}));

vi.mock('../enhanced-base-edge/enhanced-base-edge', () => ({
EnhancedBaseEdge: ({ id, path }: { id: string; path: string }) => (
<svg>
<path data-edge-id={id} d={path} />
</svg>
),
}));

vi.mock('./use-label-edge-hover', () => ({
useLabelEdgeHover: () => ({
style: {},
hovered: false,
onMouseEnter: vi.fn(),
onMouseLeave: vi.fn(),
}),
}));

const selfConnectingEdgeProps = {
id: 'self-loop',
source: 'node-1',
target: 'node-1',
sourceX: 100,
sourceY: 300,
targetX: 200,
targetY: 300,
sourcePosition: 'right',
targetPosition: 'left',
data: { label: 'Loop' },
} as EdgeProps<WorkflowBuilderEdge>;

beforeEach(() => {
nodeLookup.clear();
});

describe('LabelEdge', () => {
it('uses the measured source height for self-loop geometry and label placement', () => {
nodeLookup.set('node-1', { measured: { height: 80 } });

const { container } = render(<LabelEdge {...selfConnectingEdgeProps} />);

expect(container.querySelector('[data-edge-id="self-loop"]')?.getAttribute('d')).toContain('Q 125 120 109 120');
expect(screen.getByTestId('edge-label').dataset.labelY).toBe('120');
});

it('falls back to the explicit node height when no measurement exists', () => {
nodeLookup.set('node-1', { height: 40 });

render(<LabelEdge {...selfConnectingEdgeProps} />);

expect(screen.getByTestId('edge-label').dataset.labelY).toBe('160');
});

it('treats an unknown source node as zero height', () => {
render(<LabelEdge {...selfConnectingEdgeProps} />);

expect(screen.getByTestId('edge-label').dataset.labelY).toBe('200');
});
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type EdgeProps, getSmoothStepPath, useReactFlow } from '@xyflow/react';
import { type EdgeProps, getSmoothStepPath, useStore as useReactFlowStore } from '@xyflow/react';

import { Icon } from '@workflow-builder/icons';

Expand Down Expand Up @@ -34,7 +34,11 @@ export function LabelEdge({
source,
target,
}: EdgeProps<WorkflowBuilderEdge>) {
const { getNode } = useReactFlow();
const nodeHeight = useReactFlowStore((state) => {
if (source !== target) return 0;
const node = state.nodeLookup.get(source);
return node?.measured?.height ?? node?.height ?? 0;
});
const { style, hovered, onMouseEnter, onMouseLeave } = useLabelEdgeHover({
id,
isSelected: selected,
Expand Down Expand Up @@ -65,8 +69,6 @@ export function LabelEdge({
};

if (source === target) {
const sourceNode = getNode(source);
const nodeHeight = sourceNode?.height ?? 0;
const selfConnectingLabelY = sourceY - (nodeHeight + SELF_CONNECTING_EDGE_LABEL_OFFSET);

return (
Expand All @@ -83,6 +85,7 @@ export function LabelEdge({
target={target}
sourcePosition={sourcePosition}
targetPosition={targetPosition}
nodeHeight={nodeHeight}
/>
<EdgeLabel {...labelProps} labelX={labelX} labelY={selfConnectingLabelY} />
</>
Expand Down
Loading