Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { act, fireEvent, render, screen } from '@testing-library/react';
import type { Node, NodeProps } from '@uipath/apollo-react/canvas/xyflow/react';
import { Position } from '@uipath/apollo-react/canvas/xyflow/react';
import { describe, expect, it, vi } from 'vitest';
Expand All @@ -12,6 +12,7 @@ const DEFAULT_MANIFEST = {
// Hoisted mocks — available inside vi.mock factories
const {
mockUpdateNode,
mockUpdateNodeData,
mockGetNode,
mockNodeState,
mockHandleConfigs,
Expand All @@ -26,6 +27,7 @@ const {
mockReadOnlyNodeIds,
} = vi.hoisted(() => ({
mockUpdateNode: vi.fn(),
mockUpdateNodeData: vi.fn(),
mockGetNode: vi.fn(),
// Simulates React Flow's stored node: updateNode writes height here, getNode reads it.
mockNodeState: { current: { height: undefined as number | undefined } },
Expand Down Expand Up @@ -64,7 +66,7 @@ vi.mock('@uipath/apollo-react/canvas/xyflow/react', async (importOriginal) => ({
useStore: () => mockIsConnecting.current,
useUpdateNodeInternals: () => vi.fn(),
useReactFlow: () => ({
updateNodeData: vi.fn(),
updateNodeData: mockUpdateNodeData,
updateNode: mockUpdateNode,
getNode: mockGetNode,
}),
Expand Down Expand Up @@ -186,6 +188,7 @@ const makeHandles = (position: Position, count: number): HandleGroupManifest[] =
describe('BaseNode', () => {
afterEach(() => {
mockUpdateNode.mockClear();
mockUpdateNodeData.mockClear();
mockGetNode.mockClear();
mockNodeState.current = { height: undefined };
mockUseButtonHandles.mockClear();
Expand Down Expand Up @@ -736,4 +739,38 @@ describe('BaseNode', () => {
expect(opts.showAddButton).toBe(false);
});
});

// NodeLabel is module-mocked above, so the inline edit is driven through the
// onChange it receives, as useInlineLabelEditing would call it on commit.
describe('Inline label editing', () => {
const commitLabel = (values: { label: string; subLabel: string }) =>
act(() => mockNodeLabel.mock.calls.at(-1)?.[0]?.onChange(values));

it('stores an empty label when the name is cleared', () => {
render(
<BaseNode {...defaultProps} id="script1" data={{ display: { label: 'Fetch orders' } }} />
);
commitLabel({ label: '', subLabel: '' });
expect(mockUpdateNodeData).toHaveBeenCalledWith('script1', { display: { label: '' } });
});

it('drops an empty subLabel', () => {
render(<BaseNode {...defaultProps} data={{ display: { label: 'A', subLabel: 'B' } }} />);
commitLabel({ label: 'A', subLabel: '' });
expect(mockUpdateNodeData).toHaveBeenCalledWith('test-node', { display: { label: 'A' } });
});

it('keeps the other display fields', () => {
render(
<BaseNode
{...defaultProps}
data={{ display: { label: 'A', icon: 'mail', color: 'red' } }}
/>
);
commitLabel({ label: 'B', subLabel: 'Details' });
expect(mockUpdateNodeData).toHaveBeenCalledWith('test-node', {
display: { label: 'B', subLabel: 'Details', icon: 'mail', color: 'red' },
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -439,15 +439,15 @@ const BaseNodeComponent = (props: NodeProps<Node<BaseNodeData>>) => {
const handleBlur = useCallback(() => setIsFocused(false), []);

const handleLabelChange = useCallback(
(values: { label: string; subLabel: string }) => {
const newDisplay = { ...data.display };
// Ensure all label fields are updated or removed appropriately.
for (const labelKey of Object.keys(values) as (keyof typeof values)[]) {
if (values[labelKey]) {
newDisplay[labelKey] = values[labelKey];
} else {
delete newDisplay[labelKey];
}
({ label, subLabel }: { label: string; subLabel: string }) => {
// A cleared label is stored as '' so the node stays unnamed; an unset
// label would fall back to the manifest label. An empty subLabel is
// dropped so a label-only edit doesn't add `subLabel: ''` to the node.
const newDisplay = { ...data.display, label };
if (subLabel) {
newDisplay.subLabel = subLabel;
} else {
delete newDisplay.subLabel;
}
updateNodeData(id, {
display: newDisplay,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,13 @@ describe('resolveDisplay', () => {
);
expect(result.label).toBe('Notify Ops');
});

it('keeps an empty instance.label instead of falling back to the manifest', () => {
// A cleared name is stored as label: '', meaning "unnamed". Only an unset
// label falls back to the manifest.
const result = resolveDisplay({ label: 'Script', icon: 'code' }, { display: { label: '' } });
expect(result.label).toBe('');
});
});

// ============================================================================
Expand Down
Loading