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
73 changes: 73 additions & 0 deletions packages/plexus/src/Digraph/MeasurableNodes.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright (c) 2026 Uber Technologies, Inc.
// SPDX-License-Identifier: Apache-2.0

import React from 'react';
import { render } from '@testing-library/react';
import MeasurableNodes, { areEqual } from './MeasurableNodes';
import { TLayerType } from './types';

describe('MeasurableNodes', () => {
const baseProps: any = {
getClassName: () => 'test',
layerType: 'html' as TLayerType,
layoutVertices: null,
nodeRefs: [],
renderUtils: { getGlobalId: () => 'id', getZoomTransform: () => '' } as any,
vertices: [{ key: 'v1' }] as any,
renderNode: () => <div />,
setOnNode: () => {},
};

it('renders without crashing', () => {
const { container } = render(<MeasurableNodes {...baseProps} />);
expect(container).toBeTruthy();
});

describe('areEqual comparator', () => {
it('returns true for identical props', () => {
expect(areEqual(baseProps, baseProps)).toBe(true);
});

it('returns false if renderNode changes', () => {
const nextProps = { ...baseProps, renderNode: () => <span /> };
expect(areEqual(baseProps, nextProps)).toBe(false);
});

it('returns false if getClassName changes', () => {
const nextProps = { ...baseProps, getClassName: () => 'other' };
expect(areEqual(baseProps, nextProps)).toBe(false);
});

it('returns false if layerType changes', () => {
const nextProps = { ...baseProps, layerType: 'svg' };
expect(areEqual(baseProps, nextProps)).toBe(false);
});

it('returns false if layoutVertices changes', () => {
const nextProps = { ...baseProps, layoutVertices: [] };
expect(areEqual(baseProps, nextProps)).toBe(false);
});

it('returns false if nodeRefs changes', () => {
const nextProps = { ...baseProps, nodeRefs: [{ current: null }] };
expect(areEqual(baseProps, nextProps)).toBe(false);
});

it('returns false if renderUtils changes', () => {
const nextProps = { ...baseProps, renderUtils: { getGlobalId: () => 'other' } };
expect(areEqual(baseProps, nextProps)).toBe(false);
});

it('returns false if vertices changes', () => {
const nextProps = { ...baseProps, vertices: [{ key: '1' }] };
expect(areEqual(baseProps, nextProps)).toBe(false);
});

it('returns false if setOnNode changes significantly', () => {
const nextProps = { ...baseProps, setOnNode: () => {} };
// isSamePropSetter compares setters by reference (or shallow array equality),
// so a new function identity will always cause areEqual to return false
expect(areEqual(baseProps, nextProps)).toBe(false);
});
});
});
79 changes: 38 additions & 41 deletions packages/plexus/src/Digraph/MeasurableNodes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,45 +17,42 @@ type TProps<T = {}> = Omit<TMeasurableNodeRenderer<T>, 'measurable' | 'measureNo
vertices: TVertex<T>[];
};

export default class MeasurableNodes<T = {}> extends React.Component<TProps<T>> {
shouldComponentUpdate(np: TProps<T>) {
const p = this.props;
return (
p.renderNode !== np.renderNode ||
p.getClassName !== np.getClassName ||
p.layerType !== np.layerType ||
p.layoutVertices !== np.layoutVertices ||
p.nodeRefs !== np.nodeRefs ||
p.renderUtils !== np.renderUtils ||
p.vertices !== np.vertices ||
!isSamePropSetter(p.setOnNode, np.setOnNode)
);
}

render() {
const {
getClassName,
nodeRefs,
layoutVertices,
renderUtils,
vertices,
layerType,
renderNode,
setOnNode,
} = this.props;
return vertices.map((vertex, i) => (
<MeasurableNode<T>
key={vertex.key}
getClassName={getClassName}
ref={nodeRefs[i]}
hidden={!layoutVertices}
layerType={layerType}
renderNode={renderNode}
renderUtils={renderUtils}
vertex={vertex}
layoutVertex={layoutVertices && layoutVertices[i]}
setOnNode={setOnNode}
/>
));
}
function MeasurableNodes<T = {}>(props: TProps<T>) {
const { getClassName, nodeRefs, layoutVertices, renderUtils, vertices, layerType, renderNode, setOnNode } =
props;
return (
<>
{vertices.map((vertex, i) => (
<MeasurableNode<T>
key={vertex.key}
getClassName={getClassName}
ref={nodeRefs[i]}
hidden={!layoutVertices}
layerType={layerType}
renderNode={renderNode}
renderUtils={renderUtils}
vertex={vertex}
layoutVertex={layoutVertices && layoutVertices[i]}
setOnNode={setOnNode}
/>
))}
</>
);
}

// Custom comparison function for React.memo
// Returns TRUE if props are EQUAL (opposite of shouldComponentUpdate logic)
export const areEqual = <T,>(prevProps: TProps<T>, nextProps: TProps<T>) => {
return (
prevProps.renderNode === nextProps.renderNode &&
prevProps.getClassName === nextProps.getClassName &&
prevProps.layerType === nextProps.layerType &&
prevProps.layoutVertices === nextProps.layoutVertices &&
prevProps.nodeRefs === nextProps.nodeRefs &&
prevProps.renderUtils === nextProps.renderUtils &&
prevProps.vertices === nextProps.vertices &&
isSamePropSetter(prevProps.setOnNode, nextProps.setOnNode)
);
};

export default React.memo(MeasurableNodes, areEqual) as typeof MeasurableNodes;