Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
8 changes: 4 additions & 4 deletions .secrets.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -1403,23 +1403,23 @@
"filename": "src/frontend/src/constants/constants.ts",
"hashed_secret": "19a2fbd0dd38b4097f419c962342ef5e109eab07",
"is_verified": false,
"line_number": 737,
"line_number": 751,
"is_secret": false
},
{
"type": "Secret Keyword",
"filename": "src/frontend/src/constants/constants.ts",
"hashed_secret": "3806954324550e26ef5de85d007f1746825a073c",
"is_verified": false,
"line_number": 738,
"line_number": 752,
"is_secret": false
},
{
"type": "Secret Keyword",
"filename": "src/frontend/src/constants/constants.ts",
"hashed_secret": "c04f8fbf55c9096907a982750b1c6b0e4c1dd658",
"is_verified": false,
"line_number": 913,
"line_number": 926,
"is_secret": false
}
],
Expand Down Expand Up @@ -1528,5 +1528,5 @@
}
]
},
"generated_at": "2025-12-03T21:24:04Z"
"generated_at": "2025-12-17T16:48:17Z"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
/**
* Unit tests for NoteNode shrink/resize behavior
* Validates that sticky notes can shrink to the correct minimum size
*/

import { render, screen } from "@testing-library/react";
import {
DEFAULT_NOTE_SIZE,
NOTE_NODE_MIN_HEIGHT,
NOTE_NODE_MIN_WIDTH,
} from "@/constants/constants";
import type { NoteDataType } from "@/types/flow";

// Mock dependencies
const mockSetNode = jest.fn();
const mockCurrentFlow = {
data: {
nodes: [] as Array<{ id: string; width?: number; height?: number }>,
},
};

jest.mock("@/stores/flowStore", () => ({
__esModule: true,
default: (selector: (state: any) => any) =>
selector({
currentFlow: mockCurrentFlow,
setNode: mockSetNode,
}),
}));

jest.mock("@xyflow/react", () => ({
NodeResizer: ({
minWidth,
minHeight,
onResize,
isVisible,
}: {
minWidth: number;
minHeight: number;
onResize: (event: any, params: { width: number; height: number }) => void;
isVisible?: boolean;
}) => (
<div
data-testid="node-resizer"
data-min-width={minWidth}
data-min-height={minHeight}
data-is-visible={isVisible}
/>
),
}));

jest.mock("@/shared/hooks/use-alternate", () => ({
useAlternate: (initial: boolean) => [initial, jest.fn()],
}));

jest.mock("../NoteToolbarComponent", () => ({
__esModule: true,
default: () => <div data-testid="note-toolbar" />,
}));

jest.mock("../../GenericNode/components/NodeDescription", () => ({
__esModule: true,
default: () => <div data-testid="node-description" />,
}));

jest.mock("@/utils/utils", () => ({
cn: (...classes: any[]) => classes.filter(Boolean).join(" "),
}));

// Import component after mocks are set up
import NoteNode from "../index";

describe("NoteNode Shrink Behavior", () => {
const createMockData = (
id: string = "test-note",
backgroundColor: string = "amber",
): NoteDataType =>
({
id,
type: "noteNode",
node: {
description: "Test note content",
template: { backgroundColor },
},
}) as NoteDataType;

beforeEach(() => {
jest.clearAllMocks();
mockCurrentFlow.data.nodes = [];
});

describe("Minimum Size Constraints", () => {
it("should configure NodeResizer with correct minimum width", () => {
const data = createMockData();
render(<NoteNode data={data} selected={true} />);

const resizer = screen.getByTestId("node-resizer");
expect(Number(resizer.dataset.minWidth)).toBe(NOTE_NODE_MIN_WIDTH);
expect(NOTE_NODE_MIN_WIDTH).toBe(260);
});

it("should configure NodeResizer with correct minimum height", () => {
const data = createMockData();
render(<NoteNode data={data} selected={true} />);

const resizer = screen.getByTestId("node-resizer");
expect(Number(resizer.dataset.minHeight)).toBe(NOTE_NODE_MIN_HEIGHT);
expect(NOTE_NODE_MIN_HEIGHT).toBe(100);
});

it("should show resizer only when selected", () => {
const data = createMockData();

// When selected
const { rerender } = render(<NoteNode data={data} selected={true} />);
let resizer = screen.getByTestId("node-resizer");
expect(resizer.dataset.isVisible).toBe("true");

// When not selected
rerender(<NoteNode data={data} selected={false} />);
resizer = screen.getByTestId("node-resizer");
expect(resizer.dataset.isVisible).toBe("false");
});
});

describe("Default Size Behavior", () => {
it("should use DEFAULT_NOTE_SIZE when no dimensions are stored", () => {
const data = createMockData("note-1");
mockCurrentFlow.data.nodes = [];

render(<NoteNode data={data} selected={false} />);

const noteNode = screen.getByTestId("note_node");
expect(noteNode.style.width).toBe(`${DEFAULT_NOTE_SIZE}px`);
expect(noteNode.style.height).toBe(`${DEFAULT_NOTE_SIZE}px`);
expect(DEFAULT_NOTE_SIZE).toBe(324);
});

it("should use stored dimensions from flow state", () => {
const data = createMockData("note-1");
const customWidth = 400;
const customHeight = 300;

mockCurrentFlow.data.nodes = [
{ id: "note-1", width: customWidth, height: customHeight },
];

render(<NoteNode data={data} selected={false} />);

const noteNode = screen.getByTestId("note_node");
expect(noteNode.style.width).toBe(`${customWidth}px`);
expect(noteNode.style.height).toBe(`${customHeight}px`);
});
});

describe("Shrink to Minimum Size", () => {
it("should allow shrinking to minimum dimensions", () => {
const data = createMockData("note-1");

// Simulate a note that has been shrunk to minimum size
mockCurrentFlow.data.nodes = [
{
id: "note-1",
width: NOTE_NODE_MIN_WIDTH,
height: NOTE_NODE_MIN_HEIGHT,
},
];

render(<NoteNode data={data} selected={true} />);

const noteNode = screen.getByTestId("note_node");
expect(noteNode.style.width).toBe(`${NOTE_NODE_MIN_WIDTH}px`);
expect(noteNode.style.height).toBe(`${NOTE_NODE_MIN_HEIGHT}px`);
});

it("should render correctly at minimum width", () => {
const data = createMockData("note-1");
mockCurrentFlow.data.nodes = [
{ id: "note-1", width: NOTE_NODE_MIN_WIDTH, height: DEFAULT_NOTE_SIZE },
];

render(<NoteNode data={data} selected={false} />);

const noteNode = screen.getByTestId("note_node");
expect(noteNode.style.width).toBe(`${NOTE_NODE_MIN_WIDTH}px`);
});

it("should render correctly at minimum height", () => {
const data = createMockData("note-1");
mockCurrentFlow.data.nodes = [
{
id: "note-1",
width: DEFAULT_NOTE_SIZE,
height: NOTE_NODE_MIN_HEIGHT,
},
];

render(<NoteNode data={data} selected={false} />);

const noteNode = screen.getByTestId("note_node");
expect(noteNode.style.height).toBe(`${NOTE_NODE_MIN_HEIGHT}px`);
});
});

describe("Size Constraints Validation", () => {
it("should have minimum width less than default size", () => {
expect(NOTE_NODE_MIN_WIDTH).toBeLessThan(DEFAULT_NOTE_SIZE);
});

it("should have minimum height less than default size", () => {
expect(NOTE_NODE_MIN_HEIGHT).toBeLessThan(DEFAULT_NOTE_SIZE);
});

it("should have reasonable minimum dimensions for usability", () => {
// Minimum width should be at least 200px for readability
expect(NOTE_NODE_MIN_WIDTH).toBeGreaterThanOrEqual(200);
// Minimum height should be at least 80px for content
expect(NOTE_NODE_MIN_HEIGHT).toBeGreaterThanOrEqual(80);
});
});
});
14 changes: 7 additions & 7 deletions src/frontend/src/CustomNodes/NoteNode/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { debounce } from "lodash";
import { useMemo, useRef, useState } from "react";
import {
COLOR_OPTIONS,
DEFAULT_NOTE_SIZE,
NOTE_NODE_MIN_HEIGHT,
NOTE_NODE_MIN_WIDTH,
} from "@/constants/constants";
Expand All @@ -14,7 +15,6 @@ import NodeDescription from "../GenericNode/components/NodeDescription";
import NoteToolbarComponent from "./NoteToolbarComponent";

const CHAR_LIMIT = 2500;
const DEFAULT_NOTE_SIZE = 324;
const TRANSPARENT_COLOR = "#00000000";

/**
Expand Down Expand Up @@ -103,8 +103,8 @@ function NoteNode({
() => currentFlow?.data?.nodes.find((node) => node.id === data.id),
[currentFlow, data.id],
);
const nodeWidth = nodeData?.measured?.width ?? DEFAULT_NOTE_SIZE;
const nodeHeight = nodeData?.measured?.height ?? DEFAULT_NOTE_SIZE;
const nodeWidth = nodeData?.width ?? DEFAULT_NOTE_SIZE;
const nodeHeight = nodeData?.height ?? DEFAULT_NOTE_SIZE;

// Debounced resize handler to avoid excessive state updates during drag
const debouncedResize = useMemo(
Expand Down Expand Up @@ -146,8 +146,8 @@ function NoteNode({
return (
<>
<NodeResizer
minWidth={Math.max(DEFAULT_NOTE_SIZE, NOTE_NODE_MIN_WIDTH)}
minHeight={Math.max(DEFAULT_NOTE_SIZE, NOTE_NODE_MIN_HEIGHT)}
minWidth={NOTE_NODE_MIN_WIDTH}
minHeight={NOTE_NODE_MIN_HEIGHT}
onResize={(_, { width, height }) => debouncedResize(width, height)}
isVisible={selected}
lineClassName="!border !border-muted-foreground"
Expand All @@ -162,8 +162,8 @@ function NoteNode({
ref={nodeRef}
data-testid="note_node"
style={{
minWidth: nodeWidth,
minHeight: nodeHeight,
width: nodeWidth,
height: nodeHeight,
backgroundColor: resolvedBgColor,
}}
className={cn(
Expand Down
5 changes: 3 additions & 2 deletions src/frontend/src/constants/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -882,8 +882,9 @@ export const DRAG_EVENTS_CUSTOM_TYPESS = {
"text/plain": "text/plain",
};

export const NOTE_NODE_MIN_WIDTH = 324;
export const NOTE_NODE_MIN_HEIGHT = 324;
export const NOTE_NODE_MIN_WIDTH = 260;
export const NOTE_NODE_MIN_HEIGHT = 100;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these min numbers come from product.

export const DEFAULT_NOTE_SIZE = 324;

export const COLOR_OPTIONS = {
amber: "hsl(var(--note-amber))",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import NoteNode from "@/CustomNodes/NoteNode";
import FlowToolbar from "@/components/core/flowToolbarComponent";
import {
COLOR_OPTIONS,
DEFAULT_NOTE_SIZE,
NOTE_NODE_MIN_HEIGHT,
NOTE_NODE_MIN_WIDTH,
} from "@/constants/constants";
Expand Down Expand Up @@ -161,8 +162,8 @@ export default function Page({
const addComponent = useAddComponent();

const zoomLevel = reactFlowInstance?.getZoom();
const shadowBoxWidth = NOTE_NODE_MIN_WIDTH * (zoomLevel || 1);
const shadowBoxHeight = NOTE_NODE_MIN_HEIGHT * (zoomLevel || 1);
const shadowBoxWidth = DEFAULT_NOTE_SIZE * (zoomLevel || 1);
const shadowBoxHeight = DEFAULT_NOTE_SIZE * (zoomLevel || 1);
const shadowBoxBackgroundColor = COLOR_OPTIONS[Object.keys(COLOR_OPTIONS)[0]];

const handleGroupNode = useCallback(() => {
Expand Down Expand Up @@ -656,6 +657,8 @@ export default function Page({
id: newId,
type: "noteNode",
position: position || { x: 0, y: 0 },
width: DEFAULT_NOTE_SIZE,
height: DEFAULT_NOTE_SIZE,
data: {
...data,
id: newId,
Expand Down
Loading