Skip to content
Open
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
6 changes: 3 additions & 3 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
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