diff --git a/src/frontend/jest.config.js b/src/frontend/jest.config.js
index 9adc9062f..e85ae5e51 100644
--- a/src/frontend/jest.config.js
+++ b/src/frontend/jest.config.js
@@ -21,7 +21,6 @@ module.exports = {
transformIgnorePatterns: ["node_modules/(?!(.*\\.mjs$|@testing-library))"],
// Coverage configuration
- collectCoverage: true,
collectCoverageFrom: [
"src/**/*.{ts,tsx}",
"!src/**/*.{test,spec}.{ts,tsx}",
diff --git a/src/frontend/package.json b/src/frontend/package.json
index 985b6ed80..61fc0a6eb 100644
--- a/src/frontend/package.json
+++ b/src/frontend/package.json
@@ -95,6 +95,7 @@
"start": "vite",
"build": "vite build",
"test": "jest",
+ "test:coverage": "jest --coverage",
"test:watch": "jest --watch",
"serve": "vite preview",
"format": "npx @biomejs/biome check --write",
diff --git a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/__tests__/bundleItems.test.tsx b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/__tests__/bundleItems.test.tsx
new file mode 100644
index 000000000..b0f7200a7
--- /dev/null
+++ b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/__tests__/bundleItems.test.tsx
@@ -0,0 +1,311 @@
+import { fireEvent, render, screen } from "@testing-library/react";
+import React from "react";
+import { BundleItem } from "../bundleItems";
+
+// Mock the UI components
+jest.mock("@/components/common/genericIconComponent", () => ({
+ __esModule: true,
+ default: ({ name, className }: any) => (
+
+ {name}
+
+ ),
+}));
+
+jest.mock("@/components/ui/disclosure", () => ({
+ Disclosure: ({ children, open, onOpenChange }: any) => (
+
+ {React.Children.map(children, (child, index) => {
+ if (index === 0) {
+ return React.cloneElement(child, { onOpenChange });
+ }
+ return child;
+ })}
+
+ ),
+ DisclosureContent: ({ children }: any) => (
+ {children}
+ ),
+ DisclosureTrigger: ({ children, className }: any) => (
+
+ {children}
+
+ ),
+}));
+
+jest.mock("@/components/ui/sidebar", () => ({
+ SidebarMenuButton: ({ children, asChild }: any) => (
+
+ {asChild ? children : }
+
+ ),
+ SidebarMenuItem: ({ children }: any) => (
+ {children}
+ ),
+}));
+
+// Mock the SidebarItemsList component
+jest.mock("../sidebarItemsList", () => {
+ return function MockSidebarItemsList(props: any) {
+ return (
+ Items for {props.item?.name}
+ );
+ };
+});
+
+describe("BundleItem", () => {
+ const mockSetOpenCategories = jest.fn();
+ const mockOnDragStart = jest.fn();
+ const mockSensitiveSort = jest.fn();
+ const mockHandleKeyDownInput = jest.fn();
+
+ const mockAPIClass = {
+ description: "Test component",
+ template: {},
+ display_name: "Test Component",
+ documentation: "Test docs",
+ };
+
+ const defaultProps = {
+ item: {
+ name: "test-bundle",
+ display_name: "Test Bundle",
+ icon: "Package",
+ },
+ openCategories: [],
+ setOpenCategories: mockSetOpenCategories,
+ dataFilter: {
+ "test-bundle": {
+ component1: { ...mockAPIClass, display_name: "Component 1" },
+ component2: { ...mockAPIClass, display_name: "Component 2" },
+ },
+ },
+ nodeColors: {
+ "test-bundle": "#FF0000",
+ },
+ onDragStart: mockOnDragStart,
+ sensitiveSort: mockSensitiveSort,
+ handleKeyDownInput: mockHandleKeyDownInput,
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe("Basic Rendering", () => {
+ it("should render bundle item with correct structure", () => {
+ render();
+
+ expect(screen.getByTestId("disclosure")).toBeInTheDocument();
+ expect(screen.getByTestId("sidebar-menu-item")).toBeInTheDocument();
+ expect(screen.getByTestId("disclosure-trigger")).toBeInTheDocument();
+ expect(screen.getByTestId("sidebar-menu-button")).toBeInTheDocument();
+ });
+
+ it("should display bundle icon and name", () => {
+ render();
+
+ expect(screen.getByTestId("icon-Package")).toBeInTheDocument();
+ expect(screen.getByText("Test Bundle")).toBeInTheDocument();
+ });
+
+ it("should display chevron icon", () => {
+ render();
+
+ expect(screen.getByTestId("icon-ChevronRight")).toBeInTheDocument();
+ });
+
+ it("should include correct test id for disclosure", () => {
+ render();
+
+ const disclosureDiv = screen.getByTestId(
+ "disclosure-bundles-test bundle",
+ );
+ expect(disclosureDiv).toBeInTheDocument();
+ });
+ });
+
+ describe("Conditional Rendering", () => {
+ it("should return null when dataFilter has no items for bundle", () => {
+ const propsWithEmptyFilter = {
+ ...defaultProps,
+ dataFilter: {
+ "test-bundle": {},
+ },
+ };
+
+ const { container } = render();
+ expect(container.firstChild).toBeNull();
+ });
+
+ it("should return null when bundle not in dataFilter", () => {
+ const propsWithMissingBundle = {
+ ...defaultProps,
+ dataFilter: {
+ "other-bundle": {
+ component1: { ...mockAPIClass, display_name: "Component 1" },
+ },
+ },
+ };
+
+ const { container } = render();
+ expect(container.firstChild).toBeNull();
+ });
+
+ it("should render when dataFilter has items for bundle", () => {
+ render();
+
+ expect(screen.getByTestId("disclosure")).toBeInTheDocument();
+ expect(screen.getByTestId("sidebar-items-list")).toBeInTheDocument();
+ });
+ });
+
+ describe("Open/Closed State", () => {
+ it("should be closed by default when not in openCategories", () => {
+ render();
+
+ const disclosure = screen.getByTestId("disclosure");
+ expect(disclosure).toHaveAttribute("data-open", "false");
+ });
+
+ it("should be open when bundle is in openCategories", () => {
+ const propsWithOpenCategory = {
+ ...defaultProps,
+ openCategories: ["test-bundle"],
+ };
+
+ render();
+
+ const disclosure = screen.getByTestId("disclosure");
+ expect(disclosure).toHaveAttribute("data-open", "true");
+ });
+
+ it("should toggle open state when disclosure changes", () => {
+ const { rerender } = render();
+
+ // Simulate disclosure opening by rerendering with different props
+ const propsWithHandlerCall = {
+ ...defaultProps,
+ setOpenCategories: (updateFn: any) => {
+ const newCategories = updateFn(["other-category"]);
+ expect(newCategories).toEqual(["other-category", "test-bundle"]);
+ mockSetOpenCategories(updateFn);
+ },
+ };
+
+ // Test the handleOpenChange callback behavior directly
+ rerender();
+
+ // Trigger the callback by simulating a change from closed to open
+ const disclosure = screen.getByTestId("disclosure");
+ expect(disclosure).toHaveAttribute("data-open", "false");
+
+ // We can verify the callback is properly set up
+ expect(disclosure).toHaveAttribute("data-on-open-change");
+ });
+
+ it("should remove from open categories when closing", () => {
+ const propsWithOpenCategory = {
+ ...defaultProps,
+ openCategories: ["test-bundle", "other-bundle"],
+ setOpenCategories: (updateFn: any) => {
+ const newCategories = updateFn(["test-bundle", "other-bundle"]);
+ expect(newCategories).toEqual(["other-bundle"]);
+ mockSetOpenCategories(updateFn);
+ },
+ };
+
+ render();
+
+ const disclosure = screen.getByTestId("disclosure");
+ expect(disclosure).toHaveAttribute("data-open", "true");
+
+ // We can verify the callback is properly set up
+ expect(disclosure).toHaveAttribute("data-on-open-change");
+ });
+ });
+
+ describe("Keyboard Interaction", () => {
+ it("should call handleKeyDownInput on keydown", () => {
+ render();
+
+ const triggerDiv = screen.getByTestId("disclosure-bundles-test bundle");
+
+ fireEvent.keyDown(triggerDiv, { key: "Enter" });
+
+ expect(mockHandleKeyDownInput).toHaveBeenCalledWith(
+ expect.objectContaining({ key: "Enter" }),
+ "test-bundle",
+ );
+ });
+
+ it("should have correct tabIndex for keyboard navigation", () => {
+ render();
+
+ const triggerDiv = screen.getByTestId("disclosure-bundles-test bundle");
+ expect(triggerDiv).toHaveAttribute("tabIndex", "0");
+ });
+ });
+
+ describe("Component Integration", () => {
+ it("should render SidebarItemsList with correct props", () => {
+ render();
+
+ const sidebarItemsList = screen.getByTestId("sidebar-items-list");
+ expect(sidebarItemsList).toBeInTheDocument();
+ expect(sidebarItemsList).toHaveTextContent("Items for test-bundle");
+ });
+
+ it("should pass all required props to SidebarItemsList", () => {
+ // This test ensures the mocked component receives the right data
+ render();
+
+ expect(screen.getByTestId("sidebar-items-list")).toBeInTheDocument();
+ });
+ });
+
+ describe("Memoization", () => {
+ it("should be wrapped with memo for performance", () => {
+ // Test that component is memoized by checking displayName
+ expect(BundleItem.displayName).toBe("BundleItem");
+ });
+
+ it("should not re-render when props haven't changed", () => {
+ const { rerender } = render();
+
+ const initialElement = screen.getByTestId("disclosure");
+
+ // Re-render with same props
+ rerender();
+
+ // Component should remain the same due to memoization
+ expect(screen.getByTestId("disclosure")).toBe(initialElement);
+ });
+ });
+
+ describe("CSS Classes", () => {
+ it("should apply correct CSS classes to trigger element", () => {
+ render();
+
+ const triggerDiv = screen.getByTestId("disclosure-bundles-test bundle");
+ expect(triggerDiv).toHaveClass(
+ "user-select-none",
+ "flex",
+ "cursor-pointer",
+ "items-center",
+ "gap-2",
+ );
+ });
+
+ it("should apply group styling classes", () => {
+ render();
+
+ const disclosureTrigger = screen.getByTestId("disclosure-trigger");
+ expect(disclosureTrigger).toHaveClass("group/collapsible");
+ });
+ });
+});
diff --git a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/__tests__/categoryDisclouse.test.tsx b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/__tests__/categoryDisclouse.test.tsx
new file mode 100644
index 000000000..f5f17a77d
--- /dev/null
+++ b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/__tests__/categoryDisclouse.test.tsx
@@ -0,0 +1,425 @@
+import { fireEvent, render, screen } from "@testing-library/react";
+import React from "react";
+import { CategoryDisclosure } from "../categoryDisclouse";
+
+// Mock the UI components
+jest.mock("@/components/common/genericIconComponent", () => ({
+ ForwardedIconComponent: ({ name, className }: any) => (
+
+ {name}
+
+ ),
+}));
+
+jest.mock("@/components/ui/disclosure", () => ({
+ Disclosure: ({ children, open, onOpenChange }: any) => (
+
+ {React.Children.map(children, (child, index) => {
+ if (index === 0) {
+ return React.cloneElement(child, { onOpenChange });
+ }
+ return child;
+ })}
+
+ ),
+ DisclosureContent: ({ children }: any) => (
+ {children}
+ ),
+ DisclosureTrigger: ({ children, className }: any) => (
+
+ {children}
+
+ ),
+}));
+
+jest.mock("@/components/ui/sidebar", () => ({
+ SidebarMenuButton: ({ children, asChild }: any) => (
+
+ {asChild ? children : }
+
+ ),
+ SidebarMenuItem: ({ children }: any) => (
+ {children}
+ ),
+}));
+
+// Mock the SidebarItemsList component
+jest.mock("../sidebarItemsList", () => {
+ return function MockSidebarItemsList(props: any) {
+ return (
+ Items for {props.item?.name}
+ );
+ };
+});
+
+describe("CategoryDisclosure", () => {
+ const mockSetOpenCategories = jest.fn();
+ const mockOnDragStart = jest.fn();
+ const mockSensitiveSort = jest.fn();
+
+ const mockAPIClass = {
+ description: "Test component",
+ template: {},
+ display_name: "Test Component",
+ documentation: "Test docs",
+ };
+
+ const defaultProps = {
+ item: {
+ name: "test-category",
+ display_name: "Test Category",
+ icon: "Folder",
+ },
+ openCategories: [],
+ setOpenCategories: mockSetOpenCategories,
+ dataFilter: {
+ "test-category": {
+ component1: { ...mockAPIClass, display_name: "Component 1" },
+ component2: { ...mockAPIClass, display_name: "Component 2" },
+ },
+ },
+ nodeColors: {
+ "test-category": "#00FF00",
+ },
+ onDragStart: mockOnDragStart,
+ sensitiveSort: mockSensitiveSort,
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe("Basic Rendering", () => {
+ it("should render category disclosure with correct structure", () => {
+ render();
+
+ expect(screen.getByTestId("disclosure")).toBeInTheDocument();
+ expect(screen.getByTestId("sidebar-menu-item")).toBeInTheDocument();
+ expect(screen.getByTestId("disclosure-trigger")).toBeInTheDocument();
+ expect(screen.getByTestId("sidebar-menu-button")).toBeInTheDocument();
+ });
+
+ it("should display category icon and name", () => {
+ render();
+
+ expect(screen.getByTestId("icon-Folder")).toBeInTheDocument();
+ expect(screen.getByText("Test Category")).toBeInTheDocument();
+ });
+
+ it("should display chevron icon", () => {
+ render();
+
+ expect(screen.getByTestId("icon-ChevronRight")).toBeInTheDocument();
+ });
+
+ it("should include correct test id for disclosure", () => {
+ render();
+
+ const disclosureDiv = screen.getByTestId("disclosure-test category");
+ expect(disclosureDiv).toBeInTheDocument();
+ });
+
+ it("should render disclosure content with SidebarItemsList", () => {
+ render();
+
+ expect(screen.getByTestId("disclosure-content")).toBeInTheDocument();
+ expect(screen.getByTestId("sidebar-items-list")).toBeInTheDocument();
+ });
+ });
+
+ describe("Open/Closed State", () => {
+ it("should be closed by default when not in openCategories", () => {
+ render();
+
+ const disclosure = screen.getByTestId("disclosure");
+ expect(disclosure).toHaveAttribute("data-open", "false");
+ });
+
+ it("should be open when category is in openCategories", () => {
+ const propsWithOpenCategory = {
+ ...defaultProps,
+ openCategories: ["test-category"],
+ };
+
+ render();
+
+ const disclosure = screen.getByTestId("disclosure");
+ expect(disclosure).toHaveAttribute("data-open", "true");
+ });
+
+ it("should toggle open state when disclosure changes", () => {
+ const { rerender } = render();
+
+ // Simulate disclosure opening by rerendering with different props
+ const propsWithHandlerCall = {
+ ...defaultProps,
+ setOpenCategories: (updateFn: any) => {
+ const newCategories = updateFn(["other-category"]);
+ expect(newCategories).toEqual(["other-category", "test-category"]);
+ mockSetOpenCategories(updateFn);
+ },
+ };
+
+ // Test the handleOpenChange callback behavior directly
+ rerender();
+
+ // Trigger the callback by simulating a change from closed to open
+ const disclosure = screen.getByTestId("disclosure");
+ expect(disclosure).toHaveAttribute("data-open", "false");
+
+ // We can verify the callback is properly set up
+ expect(disclosure).toHaveAttribute("data-on-open-change");
+ });
+
+ it("should remove from open categories when closing", () => {
+ const propsWithOpenCategory = {
+ ...defaultProps,
+ openCategories: ["test-category", "other-category"],
+ setOpenCategories: (updateFn: any) => {
+ const newCategories = updateFn(["test-category", "other-category"]);
+ expect(newCategories).toEqual(["other-category"]);
+ mockSetOpenCategories(updateFn);
+ },
+ };
+
+ render();
+
+ const disclosure = screen.getByTestId("disclosure");
+ expect(disclosure).toHaveAttribute("data-open", "true");
+
+ // We can verify the callback is properly set up
+ expect(disclosure).toHaveAttribute("data-on-open-change");
+ });
+ });
+
+ describe("Keyboard Interaction", () => {
+ it("should toggle category on Enter key", () => {
+ render();
+
+ const triggerDiv = screen.getByTestId("disclosure-test category");
+
+ fireEvent.keyDown(triggerDiv, { key: "Enter" });
+
+ expect(mockSetOpenCategories).toHaveBeenCalledWith(expect.any(Function));
+
+ // Test the function for adding category (closed -> open)
+ const updateFunction = mockSetOpenCategories.mock.calls[0][0];
+ const newCategories = updateFunction([]);
+ expect(newCategories).toEqual(["test-category"]);
+ });
+
+ it("should toggle category on Space key", () => {
+ render();
+
+ const triggerDiv = screen.getByTestId("disclosure-test category");
+
+ fireEvent.keyDown(triggerDiv, { key: " " });
+
+ expect(mockSetOpenCategories).toHaveBeenCalledWith(expect.any(Function));
+ });
+
+ it("should respond to Enter and Space keys", () => {
+ render();
+
+ const triggerDiv = screen.getByTestId("disclosure-test category");
+
+ fireEvent.keyDown(triggerDiv, { key: "Enter" });
+ expect(mockSetOpenCategories).toHaveBeenCalledTimes(1);
+
+ fireEvent.keyDown(triggerDiv, { key: " " });
+ expect(mockSetOpenCategories).toHaveBeenCalledTimes(2);
+ });
+
+ it("should not trigger on other keys", () => {
+ render();
+
+ const triggerDiv = screen.getByTestId("disclosure-test category");
+
+ fireEvent.keyDown(triggerDiv, { key: "Escape" });
+
+ expect(mockSetOpenCategories).not.toHaveBeenCalled();
+ });
+
+ it("should handle toggle when category is already open", () => {
+ const propsWithOpenCategory = {
+ ...defaultProps,
+ openCategories: ["test-category"],
+ };
+
+ render();
+
+ const triggerDiv = screen.getByTestId("disclosure-test category");
+
+ fireEvent.keyDown(triggerDiv, { key: "Enter" });
+
+ expect(mockSetOpenCategories).toHaveBeenCalledWith(expect.any(Function));
+
+ // Test the function for removing category (open -> closed)
+ const updateFunction = mockSetOpenCategories.mock.calls[0][0];
+ const newCategories = updateFunction(["test-category"]);
+ expect(newCategories).toEqual([]);
+ });
+
+ it("should have correct tabIndex for keyboard navigation", () => {
+ render();
+
+ const triggerDiv = screen.getByTestId("disclosure-test category");
+ expect(triggerDiv).toHaveAttribute("tabIndex", "0");
+ });
+ });
+
+ describe("Component Integration", () => {
+ it("should render SidebarItemsList with correct props", () => {
+ render();
+
+ const sidebarItemsList = screen.getByTestId("sidebar-items-list");
+ expect(sidebarItemsList).toBeInTheDocument();
+ expect(sidebarItemsList).toHaveTextContent("Items for test-category");
+ });
+
+ it("should pass all required props to SidebarItemsList", () => {
+ // This test ensures the mocked component receives the right data
+ render();
+
+ expect(screen.getByTestId("sidebar-items-list")).toBeInTheDocument();
+ });
+ });
+
+ describe("Memoization and Performance", () => {
+ it("should be wrapped with memo for performance", () => {
+ // Test that component is memoized by checking displayName
+ expect(CategoryDisclosure.displayName).toBe("CategoryDisclosure");
+ });
+
+ it("should use useCallback for handleKeyDownInput", () => {
+ const { rerender } = render();
+
+ // Get the keydown handler function
+ const triggerDiv = screen.getByTestId("disclosure-test category");
+ const initialKeyDownHandler = triggerDiv.getAttribute("onkeydown");
+
+ // Re-render with same props
+ rerender();
+
+ // The handler should be the same due to useCallback
+ const rerenderKeyDownHandler = triggerDiv.getAttribute("onkeydown");
+ expect(rerenderKeyDownHandler).toBe(initialKeyDownHandler);
+ });
+
+ it("should use useCallback for handleOpenChange", () => {
+ const { rerender } = render();
+
+ const disclosure = screen.getByTestId("disclosure");
+ const initialOnOpenChangeAttr = disclosure.getAttribute(
+ "data-on-open-change",
+ );
+
+ // Re-render with same props
+ rerender();
+
+ const rerenderOnOpenChangeAttr = disclosure.getAttribute(
+ "data-on-open-change",
+ );
+ expect(rerenderOnOpenChangeAttr).toBe(initialOnOpenChangeAttr);
+ });
+ });
+
+ describe("CSS Classes and Styling", () => {
+ it("should apply correct CSS classes to trigger element", () => {
+ render();
+
+ const triggerDiv = screen.getByTestId("disclosure-test category");
+ expect(triggerDiv).toHaveClass(
+ "user-select-none",
+ "flex",
+ "cursor-pointer",
+ "items-center",
+ "gap-2",
+ );
+ });
+
+ it("should apply group styling classes to disclosure trigger", () => {
+ render();
+
+ const disclosureTrigger = screen.getByTestId("disclosure-trigger");
+ expect(disclosureTrigger).toHaveClass("group/collapsible");
+ });
+
+ it("should apply accent-pink-foreground class to category icon", () => {
+ render();
+
+ const categoryIcon = screen.getByTestId("icon-Folder");
+ expect(categoryIcon).toHaveClass(
+ "group-aria-expanded/collapsible:text-accent-pink-foreground",
+ );
+ });
+
+ it("should apply font-semibold class to category name span", () => {
+ render();
+
+ const categoryNameSpan = screen.getByText("Test Category");
+ expect(categoryNameSpan).toHaveClass(
+ "group-aria-expanded/collapsible:font-semibold",
+ );
+ });
+
+ it("should apply rotation class to chevron icon", () => {
+ render();
+
+ const chevronIcon = screen.getByTestId("icon-ChevronRight");
+ expect(chevronIcon).toHaveClass(
+ "group-aria-expanded/collapsible:rotate-90",
+ );
+ });
+ });
+
+ describe("Props Handling", () => {
+ it("should handle different item shapes", () => {
+ const propsWithDifferentItem = {
+ ...defaultProps,
+ item: {
+ name: "custom-category",
+ display_name: "Custom Category",
+ icon: "Star",
+ },
+ };
+
+ render();
+
+ expect(screen.getByTestId("icon-Star")).toBeInTheDocument();
+ expect(screen.getByText("Custom Category")).toBeInTheDocument();
+ });
+
+ it("should handle missing openCategories gracefully", () => {
+ const propsWithoutOpenCategories = {
+ ...defaultProps,
+ openCategories: [] as string[],
+ };
+
+ expect(() => {
+ render();
+ }).not.toThrow();
+ });
+
+ it("should handle item name with special characters", () => {
+ const propsWithSpecialName = {
+ ...defaultProps,
+ item: {
+ name: "test-category-special",
+ display_name: "Test Category & Special",
+ icon: "Hash",
+ },
+ };
+
+ render();
+
+ expect(
+ screen.getByTestId("disclosure-test category & special"),
+ ).toBeInTheDocument();
+ });
+ });
+});
diff --git a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/__tests__/categoryGroup.test.tsx b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/__tests__/categoryGroup.test.tsx
new file mode 100644
index 000000000..0b8a5130d
--- /dev/null
+++ b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/__tests__/categoryGroup.test.tsx
@@ -0,0 +1,426 @@
+import { render, screen } from "@testing-library/react";
+import React from "react";
+import { CategoryGroup } from "../categoryGroup";
+
+// Mock the UI components
+jest.mock("@/components/ui/sidebar", () => ({
+ SidebarGroup: ({ children, className }: any) => (
+
+ {children}
+
+ ),
+ SidebarGroupContent: ({ children }: any) => (
+ {children}
+ ),
+ SidebarMenu: ({ children }: any) => (
+ {children}
+ ),
+}));
+
+// Mock the CategoryDisclosure component
+jest.mock("../categoryDisclouse", () => ({
+ CategoryDisclosure: ({ item, openCategories }: any) => (
+
+ CategoryDisclosure for {item.display_name} - Open:{" "}
+ {openCategories.includes(item.name).toString()}
+
+ ),
+}));
+
+// Mock styleUtils
+jest.mock("@/utils/styleUtils", () => ({
+ SIDEBAR_BUNDLES: [
+ { name: "bundle1", display_name: "Bundle 1" },
+ { name: "bundle2", display_name: "Bundle 2" },
+ ],
+}));
+
+describe("CategoryGroup", () => {
+ const mockSetOpenCategories = jest.fn();
+ const mockOnDragStart = jest.fn();
+ const mockSensitiveSort = jest.fn();
+
+ const mockAPIClass = {
+ description: "Test component",
+ template: {},
+ display_name: "Test Component",
+ documentation: "Test docs",
+ };
+
+ const defaultProps = {
+ dataFilter: {
+ category1: {
+ component1: { ...mockAPIClass, display_name: "Component 1" },
+ component2: { ...mockAPIClass, display_name: "Component 2" },
+ },
+ category2: {
+ component3: { ...mockAPIClass, display_name: "Component 3" },
+ },
+ bundle1: {
+ bundleComponent: { ...mockAPIClass, display_name: "Bundle Component" },
+ },
+ custom_component: {
+ customComp: { ...mockAPIClass, display_name: "Custom Component" },
+ },
+ },
+ sortedCategories: ["category2", "category1"],
+ CATEGORIES: [
+ { display_name: "Category 1", name: "category1", icon: "Folder" },
+ { display_name: "Category 2", name: "category2", icon: "File" },
+ ],
+ openCategories: [],
+ setOpenCategories: mockSetOpenCategories,
+ search: "",
+ nodeColors: {
+ category1: "#FF0000",
+ category2: "#00FF00",
+ },
+ onDragStart: mockOnDragStart,
+ sensitiveSort: mockSensitiveSort,
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe("Basic Rendering", () => {
+ it("should render sidebar group with correct structure", () => {
+ render();
+
+ expect(screen.getByTestId("sidebar-group")).toBeInTheDocument();
+ expect(screen.getByTestId("sidebar-group-content")).toBeInTheDocument();
+ expect(screen.getByTestId("sidebar-menu")).toBeInTheDocument();
+ });
+
+ it("should render CategoryDisclosure for each valid category", () => {
+ render();
+
+ expect(
+ screen.getByTestId("category-disclosure-category1"),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByTestId("category-disclosure-category2"),
+ ).toBeInTheDocument();
+ });
+
+ it("should display correct category names", () => {
+ render();
+
+ expect(
+ screen.getByText("CategoryDisclosure for Category 1 - Open: false"),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText("CategoryDisclosure for Category 2 - Open: false"),
+ ).toBeInTheDocument();
+ });
+ });
+
+ describe("Category Filtering", () => {
+ it("should exclude bundle categories", () => {
+ render();
+
+ expect(
+ screen.queryByTestId("category-disclosure-bundle1"),
+ ).not.toBeInTheDocument();
+ });
+
+ it("should exclude custom_component category", () => {
+ render();
+
+ expect(
+ screen.queryByTestId("category-disclosure-custom_component"),
+ ).not.toBeInTheDocument();
+ });
+
+ it("should exclude categories with no items", () => {
+ const propsWithEmptyCategory = {
+ ...defaultProps,
+ dataFilter: {
+ ...defaultProps.dataFilter,
+ emptyCategory: {},
+ },
+ CATEGORIES: [
+ ...defaultProps.CATEGORIES,
+ {
+ display_name: "Empty Category",
+ name: "emptyCategory",
+ icon: "Empty",
+ },
+ ],
+ };
+
+ render();
+
+ expect(
+ screen.queryByTestId("category-disclosure-emptyCategory"),
+ ).not.toBeInTheDocument();
+ });
+
+ it("should include categories with items", () => {
+ render();
+
+ expect(
+ screen.getByTestId("category-disclosure-category1"),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByTestId("category-disclosure-category2"),
+ ).toBeInTheDocument();
+ });
+ });
+
+ describe("Category Sorting", () => {
+ it("should sort categories according to CATEGORIES order when search is empty", () => {
+ const propsWithReorderedCategories = {
+ ...defaultProps,
+ CATEGORIES: [
+ { display_name: "Category 2", name: "category2", icon: "File" },
+ { display_name: "Category 1", name: "category1", icon: "Folder" },
+ ],
+ };
+
+ render();
+
+ const disclosures = screen.getAllByTestId(/category-disclosure-/);
+ expect(disclosures[0]).toHaveAttribute(
+ "data-testid",
+ "category-disclosure-category2",
+ );
+ expect(disclosures[1]).toHaveAttribute(
+ "data-testid",
+ "category-disclosure-category1",
+ );
+ });
+
+ it("should sort categories according to sortedCategories when search is not empty", () => {
+ const propsWithSearch = {
+ ...defaultProps,
+ search: "test search",
+ sortedCategories: ["category1", "category2"],
+ };
+
+ render();
+
+ const disclosures = screen.getAllByTestId(/category-disclosure-/);
+ expect(disclosures[0]).toHaveAttribute(
+ "data-testid",
+ "category-disclosure-category1",
+ );
+ expect(disclosures[1]).toHaveAttribute(
+ "data-testid",
+ "category-disclosure-category2",
+ );
+ });
+
+ it("should handle categories not in CATEGORIES list", () => {
+ const propsWithUnknownCategory = {
+ ...defaultProps,
+ dataFilter: {
+ ...defaultProps.dataFilter,
+ unknownCategory: {
+ unknownComp: { ...mockAPIClass, display_name: "Unknown Component" },
+ },
+ },
+ };
+
+ render();
+
+ expect(
+ screen.getByTestId("category-disclosure-unknownCategory"),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText(
+ "CategoryDisclosure for unknownCategory - Open: false",
+ ),
+ ).toBeInTheDocument();
+ });
+ });
+
+ describe("Open Categories State", () => {
+ it("should pass correct open state to CategoryDisclosure", () => {
+ const propsWithOpenCategories = {
+ ...defaultProps,
+ openCategories: ["category1"],
+ };
+
+ render();
+
+ expect(
+ screen.getByText("CategoryDisclosure for Category 1 - Open: true"),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText("CategoryDisclosure for Category 2 - Open: false"),
+ ).toBeInTheDocument();
+ });
+
+ it("should handle multiple open categories", () => {
+ const propsWithMultipleOpen = {
+ ...defaultProps,
+ openCategories: ["category1", "category2"],
+ };
+
+ render();
+
+ expect(
+ screen.getByText("CategoryDisclosure for Category 1 - Open: true"),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText("CategoryDisclosure for Category 2 - Open: true"),
+ ).toBeInTheDocument();
+ });
+ });
+
+ describe("Props Passing", () => {
+ it("should pass all required props to CategoryDisclosure", () => {
+ const { rerender } = render();
+
+ // Verify components are rendered (props are passed correctly)
+ expect(
+ screen.getByTestId("category-disclosure-category1"),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByTestId("category-disclosure-category2"),
+ ).toBeInTheDocument();
+
+ // Re-render to ensure props are consistent
+ rerender();
+ expect(
+ screen.getByTestId("category-disclosure-category1"),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByTestId("category-disclosure-category2"),
+ ).toBeInTheDocument();
+ });
+ });
+
+ describe("Edge Cases", () => {
+ it("should render nothing when no valid categories exist", () => {
+ const propsWithNoValidCategories = {
+ ...defaultProps,
+ dataFilter: {
+ bundle1: {
+ bundleComp: { ...mockAPIClass, display_name: "Bundle Component" },
+ },
+ custom_component: {
+ customComp: { ...mockAPIClass, display_name: "Custom Component" },
+ },
+ },
+ };
+
+ render();
+
+ expect(
+ screen.queryByTestId(/category-disclosure-/),
+ ).not.toBeInTheDocument();
+ });
+
+ it("should handle empty dataFilter gracefully", () => {
+ const propsWithEmptyDataFilter = {
+ ...defaultProps,
+ dataFilter: {},
+ };
+
+ render();
+
+ expect(
+ screen.queryByTestId(/category-disclosure-/),
+ ).not.toBeInTheDocument();
+ });
+
+ it("should handle missing CATEGORIES gracefully", () => {
+ const propsWithMissingCategories = {
+ ...defaultProps,
+ CATEGORIES: [],
+ dataFilter: {
+ randomCategory: {
+ comp: { ...mockAPIClass, display_name: "Random Component" },
+ },
+ },
+ };
+
+ render();
+
+ expect(
+ screen.getByTestId("category-disclosure-randomCategory"),
+ ).toBeInTheDocument();
+ });
+ });
+
+ describe("Memoization", () => {
+ it("should be wrapped with memo for performance", () => {
+ expect(CategoryGroup.displayName).toBe("CategoryGroup");
+ });
+
+ it("should not re-render when props haven't changed", () => {
+ const { rerender } = render();
+
+ const initialElement = screen.getByTestId("sidebar-group");
+
+ // Re-render with same props
+ rerender();
+
+ expect(screen.getByTestId("sidebar-group")).toBe(initialElement);
+ });
+ });
+
+ describe("Complex Scenarios", () => {
+ it("should handle mixed bundle and category data", () => {
+ const complexDataFilter = {
+ category1: {
+ comp1: { ...mockAPIClass, display_name: "Component 1" },
+ },
+ bundle1: {
+ bundleComp: { ...mockAPIClass, display_name: "Bundle Component" },
+ },
+ category2: {
+ comp2: { ...mockAPIClass, display_name: "Component 2" },
+ },
+ custom_component: {
+ customComp: { ...mockAPIClass, display_name: "Custom Component" },
+ },
+ };
+
+ const propsWithComplexData = {
+ ...defaultProps,
+ dataFilter: complexDataFilter,
+ };
+
+ render();
+
+ // Should render only categories, not bundles or custom_component
+ expect(
+ screen.getByTestId("category-disclosure-category1"),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByTestId("category-disclosure-category2"),
+ ).toBeInTheDocument();
+ expect(
+ screen.queryByTestId("category-disclosure-bundle1"),
+ ).not.toBeInTheDocument();
+ expect(
+ screen.queryByTestId("category-disclosure-custom_component"),
+ ).not.toBeInTheDocument();
+ });
+
+ it("should handle search and sorting together", () => {
+ const propsWithSearchAndSort = {
+ ...defaultProps,
+ search: "search term",
+ sortedCategories: ["category2", "category1"],
+ };
+
+ render();
+
+ const disclosures = screen.getAllByTestId(/category-disclosure-/);
+ expect(disclosures).toHaveLength(2);
+ // Order should follow sortedCategories when search is present
+ expect(disclosures[0]).toHaveAttribute(
+ "data-testid",
+ "category-disclosure-category2",
+ );
+ expect(disclosures[1]).toHaveAttribute(
+ "data-testid",
+ "category-disclosure-category1",
+ );
+ });
+ });
+});
diff --git a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/__tests__/emptySearchComponent.test.tsx b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/__tests__/emptySearchComponent.test.tsx
new file mode 100644
index 000000000..216e3b001
--- /dev/null
+++ b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/__tests__/emptySearchComponent.test.tsx
@@ -0,0 +1,273 @@
+import { fireEvent, render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import React from "react";
+import NoResultsMessage from "../emptySearchComponent";
+
+describe("NoResultsMessage", () => {
+ const mockOnClearSearch = jest.fn();
+
+ const defaultProps = {
+ onClearSearch: mockOnClearSearch,
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe("Basic Rendering", () => {
+ it("should render with default message", () => {
+ const { container } = render();
+
+ expect(container.textContent).toContain("No components found.");
+ expect(screen.getByText("Clear your search")).toBeInTheDocument();
+ expect(container.textContent).toContain(
+ "or filter and try a different query.",
+ );
+ });
+
+ it("should render with custom message", () => {
+ const customProps = {
+ ...defaultProps,
+ message: "Custom no results message",
+ };
+
+ const { container } = render();
+
+ expect(container.textContent).toContain("Custom no results message");
+ });
+
+ it("should render with custom clear search text", () => {
+ const customProps = {
+ ...defaultProps,
+ clearSearchText: "Reset search",
+ };
+
+ render();
+
+ expect(screen.getByText("Reset search")).toBeInTheDocument();
+ });
+
+ it("should render with custom additional text", () => {
+ const customProps = {
+ ...defaultProps,
+ additionalText: "or try something else.",
+ };
+
+ const { container } = render();
+
+ expect(container.textContent).toContain("or try something else.");
+ });
+
+ it("should render with all custom props", () => {
+ const allCustomProps = {
+ ...defaultProps,
+ message: "Nothing here!",
+ clearSearchText: "Reset",
+ additionalText: "or adjust filters.",
+ };
+
+ const { container } = render();
+
+ expect(container.textContent).toContain("Nothing here!");
+ expect(screen.getByText("Reset")).toBeInTheDocument();
+ expect(container.textContent).toContain("or adjust filters.");
+ });
+ });
+
+ describe("Clear Search Functionality", () => {
+ it("should call onClearSearch when clear link is clicked", async () => {
+ const user = userEvent.setup();
+ render();
+
+ const clearLink = screen.getByText("Clear your search");
+ await user.click(clearLink);
+
+ expect(mockOnClearSearch).toHaveBeenCalledTimes(1);
+ });
+
+ it("should call onClearSearch when custom clear text is clicked", async () => {
+ const user = userEvent.setup();
+ const customProps = {
+ ...defaultProps,
+ clearSearchText: "Reset filters",
+ };
+
+ render();
+
+ const clearLink = screen.getByText("Reset filters");
+ await user.click(clearLink);
+
+ expect(mockOnClearSearch).toHaveBeenCalledTimes(1);
+ });
+
+ it("should handle keyboard events on clear link", () => {
+ render();
+
+ const clearLink = screen.getByText("Clear your search");
+ fireEvent.keyDown(clearLink, { key: "Enter" });
+ fireEvent.keyDown(clearLink, { key: " " });
+
+ // Note: onClick events in tests don't automatically handle keyboard events
+ // This test ensures the element is focusable and receives keyboard events
+ expect(clearLink).toBeInTheDocument();
+ });
+
+ it("should only call onClearSearch once per click", async () => {
+ const user = userEvent.setup();
+ render();
+
+ const clearLink = screen.getByText("Clear your search");
+ await user.click(clearLink);
+ await user.click(clearLink);
+
+ expect(mockOnClearSearch).toHaveBeenCalledTimes(2);
+ });
+ });
+
+ describe("Component Structure", () => {
+ it("should render clear link as clickable element", () => {
+ render();
+
+ const clearLink = screen.getByText("Clear your search");
+ expect(clearLink).toBeInTheDocument();
+ expect(clearLink.tagName).toBe("A");
+ });
+ });
+
+ describe("Text Concatenation", () => {
+ it("should properly concatenate message, clear text, and additional text", () => {
+ const { container } = render();
+
+ expect(container.textContent).toContain("No components found.");
+ expect(container.textContent).toContain("Clear your search");
+ expect(container.textContent).toContain(
+ "or filter and try a different query.",
+ );
+ });
+
+ it("should handle empty strings gracefully", () => {
+ const emptyProps = {
+ ...defaultProps,
+ message: "",
+ clearSearchText: "",
+ additionalText: "",
+ };
+
+ render();
+
+ // Should still render the structure even with empty strings
+ const paragraph = screen.getByRole("paragraph");
+ expect(paragraph).toBeInTheDocument();
+ });
+
+ it("should handle only message provided", () => {
+ const messageOnlyProps = {
+ ...defaultProps,
+ message: "Only message",
+ clearSearchText: "",
+ additionalText: "",
+ };
+
+ const { container } = render();
+
+ expect(container.textContent).toContain("Only message");
+ });
+ });
+
+ describe("Default Props Behavior", () => {
+ it("should use default values when props are undefined", () => {
+ const minimalProps = {
+ onClearSearch: mockOnClearSearch,
+ };
+
+ const { container } = render();
+
+ expect(container.textContent).toContain("No components found.");
+ expect(screen.getByText("Clear your search")).toBeInTheDocument();
+ expect(container.textContent).toContain(
+ "or filter and try a different query.",
+ );
+ });
+
+ it("should override default values when props are provided", () => {
+ const overrideProps = {
+ onClearSearch: mockOnClearSearch,
+ message: "Override message",
+ clearSearchText: "Override clear",
+ additionalText: "Override additional",
+ };
+
+ const { container } = render();
+
+ expect(container.textContent).not.toContain("No components found.");
+ expect(screen.queryByText("Clear your search")).not.toBeInTheDocument();
+ expect(container.textContent).not.toContain(
+ "or filter and try a different query.",
+ );
+
+ expect(container.textContent).toContain("Override message");
+ expect(screen.getByText("Override clear")).toBeInTheDocument();
+ expect(container.textContent).toContain("Override additional");
+ });
+ });
+
+ describe("Callback Function", () => {
+ it("should handle missing onClearSearch gracefully", () => {
+ const propsWithoutCallback = {
+ onClearSearch: undefined as any,
+ };
+
+ expect(() => {
+ render();
+ }).not.toThrow();
+ });
+
+ it("should work with different callback functions", async () => {
+ const user = userEvent.setup();
+ const alternativeCallback = jest.fn();
+ const alternativeProps = {
+ onClearSearch: alternativeCallback,
+ };
+
+ render();
+
+ const clearLink = screen.getByText("Clear your search");
+ await user.click(clearLink);
+
+ expect(alternativeCallback).toHaveBeenCalledTimes(1);
+ expect(mockOnClearSearch).not.toHaveBeenCalled();
+ });
+ });
+
+ describe("Re-rendering", () => {
+ it("should update when props change", () => {
+ const { rerender, container } = render(
+ ,
+ );
+
+ expect(container.textContent).toContain("No components found.");
+
+ const newProps = {
+ ...defaultProps,
+ message: "Updated message",
+ };
+
+ rerender();
+
+ expect(container.textContent).not.toContain("No components found.");
+ expect(container.textContent).toContain("Updated message");
+ });
+
+ it("should maintain functionality after re-render", async () => {
+ const user = userEvent.setup();
+ const { rerender } = render();
+
+ rerender();
+
+ const clearLink = screen.getByText("Clear your search");
+ await user.click(clearLink);
+
+ expect(mockOnClearSearch).toHaveBeenCalledTimes(1);
+ });
+ });
+});
diff --git a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/__tests__/featureTogglesComponent.test.tsx b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/__tests__/featureTogglesComponent.test.tsx
new file mode 100644
index 000000000..b6e830051
--- /dev/null
+++ b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/__tests__/featureTogglesComponent.test.tsx
@@ -0,0 +1,398 @@
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import React from "react";
+import FeatureToggles from "../featureTogglesComponent";
+
+// Mock the UI components
+jest.mock("@/components/ui/badge", () => ({
+ Badge: ({ children, variant, size }: any) => (
+
+ {children}
+
+ ),
+}));
+
+jest.mock("@/components/ui/switch", () => ({
+ Switch: ({ checked, onCheckedChange, "data-testid": testId }: any) => (
+
+ ),
+}));
+
+describe("FeatureToggles", () => {
+ const mockSetShowBeta = jest.fn();
+ const mockSetShowLegacy = jest.fn();
+
+ const defaultProps = {
+ showBeta: false,
+ setShowBeta: mockSetShowBeta,
+ showLegacy: false,
+ setShowLegacy: mockSetShowLegacy,
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe("Basic Rendering", () => {
+ it("should render both toggle sections", () => {
+ render();
+
+ // Allow multiple instances of "Show" to pass
+ expect(screen.getAllByText("Show").length).toBeGreaterThanOrEqual(1);
+ expect(screen.getByTestId("badge-pinkStatic-xq")).toHaveTextContent(
+ "Beta",
+ );
+ expect(screen.getByTestId("badge-secondaryStatic-xq")).toHaveTextContent(
+ "Legacy",
+ );
+ });
+
+ it("should render Beta toggle with correct elements", () => {
+ render();
+
+ expect(screen.getByTestId("badge-pinkStatic-xq")).toHaveTextContent(
+ "Beta",
+ );
+ expect(screen.getByTestId("sidebar-beta-switch")).toBeInTheDocument();
+ });
+
+ it("should render Legacy toggle with correct elements", () => {
+ render();
+
+ expect(screen.getByTestId("badge-secondaryStatic-xq")).toHaveTextContent(
+ "Legacy",
+ );
+ expect(screen.getByTestId("sidebar-legacy-switch")).toBeInTheDocument();
+ });
+
+ it("should render correct number of toggle sections", () => {
+ render();
+
+ const toggleSections = screen.getAllByText("Show");
+ expect(toggleSections).toHaveLength(2);
+ });
+ });
+
+ describe("Switch States", () => {
+ it("should show Beta switch as OFF when showBeta is false", () => {
+ render();
+
+ const betaSwitch = screen.getByTestId("sidebar-beta-switch");
+ expect(betaSwitch).toHaveAttribute("aria-checked", "false");
+ expect(betaSwitch).toHaveTextContent("OFF");
+ });
+
+ it("should show Beta switch as ON when showBeta is true", () => {
+ const propsWithBetaOn = { ...defaultProps, showBeta: true };
+ render();
+
+ const betaSwitch = screen.getByTestId("sidebar-beta-switch");
+ expect(betaSwitch).toHaveAttribute("aria-checked", "true");
+ expect(betaSwitch).toHaveTextContent("ON");
+ });
+
+ it("should show Legacy switch as OFF when showLegacy is false", () => {
+ render();
+
+ const legacySwitch = screen.getByTestId("sidebar-legacy-switch");
+ expect(legacySwitch).toHaveAttribute("aria-checked", "false");
+ expect(legacySwitch).toHaveTextContent("OFF");
+ });
+
+ it("should show Legacy switch as ON when showLegacy is true", () => {
+ const propsWithLegacyOn = { ...defaultProps, showLegacy: true };
+ render();
+
+ const legacySwitch = screen.getByTestId("sidebar-legacy-switch");
+ expect(legacySwitch).toHaveAttribute("aria-checked", "true");
+ expect(legacySwitch).toHaveTextContent("ON");
+ });
+
+ it("should handle both switches being ON", () => {
+ const propsWithBothOn = {
+ ...defaultProps,
+ showBeta: true,
+ showLegacy: true,
+ };
+ render();
+
+ const betaSwitch = screen.getByTestId("sidebar-beta-switch");
+ const legacySwitch = screen.getByTestId("sidebar-legacy-switch");
+
+ expect(betaSwitch).toHaveAttribute("aria-checked", "true");
+ expect(legacySwitch).toHaveAttribute("aria-checked", "true");
+ });
+ });
+
+ describe("Beta Toggle Functionality", () => {
+ it("should call setShowBeta with true when Beta switch is clicked while OFF", async () => {
+ const user = userEvent.setup();
+ render();
+
+ const betaSwitch = screen.getByTestId("sidebar-beta-switch");
+ await user.click(betaSwitch);
+
+ expect(mockSetShowBeta).toHaveBeenCalledWith(true);
+ });
+
+ it("should call setShowBeta with false when Beta switch is clicked while ON", async () => {
+ const user = userEvent.setup();
+ const propsWithBetaOn = { ...defaultProps, showBeta: true };
+ render();
+
+ const betaSwitch = screen.getByTestId("sidebar-beta-switch");
+ await user.click(betaSwitch);
+
+ expect(mockSetShowBeta).toHaveBeenCalledWith(false);
+ });
+
+ it("should only call setShowBeta once per click", async () => {
+ const user = userEvent.setup();
+ render();
+
+ const betaSwitch = screen.getByTestId("sidebar-beta-switch");
+ await user.click(betaSwitch);
+
+ expect(mockSetShowBeta).toHaveBeenCalledTimes(1);
+ });
+
+ it("should not affect Legacy switch when Beta is clicked", async () => {
+ const user = userEvent.setup();
+ render();
+
+ const betaSwitch = screen.getByTestId("sidebar-beta-switch");
+ await user.click(betaSwitch);
+
+ expect(mockSetShowLegacy).not.toHaveBeenCalled();
+ });
+ });
+
+ describe("Legacy Toggle Functionality", () => {
+ it("should call setShowLegacy with true when Legacy switch is clicked while OFF", async () => {
+ const user = userEvent.setup();
+ render();
+
+ const legacySwitch = screen.getByTestId("sidebar-legacy-switch");
+ await user.click(legacySwitch);
+
+ expect(mockSetShowLegacy).toHaveBeenCalledWith(true);
+ });
+
+ it("should call setShowLegacy with false when Legacy switch is clicked while ON", async () => {
+ const user = userEvent.setup();
+ const propsWithLegacyOn = { ...defaultProps, showLegacy: true };
+ render();
+
+ const legacySwitch = screen.getByTestId("sidebar-legacy-switch");
+ await user.click(legacySwitch);
+
+ expect(mockSetShowLegacy).toHaveBeenCalledWith(false);
+ });
+
+ it("should only call setShowLegacy once per click", async () => {
+ const user = userEvent.setup();
+ render();
+
+ const legacySwitch = screen.getByTestId("sidebar-legacy-switch");
+ await user.click(legacySwitch);
+
+ expect(mockSetShowLegacy).toHaveBeenCalledTimes(1);
+ });
+
+ it("should not affect Beta switch when Legacy is clicked", async () => {
+ const user = userEvent.setup();
+ render();
+
+ const legacySwitch = screen.getByTestId("sidebar-legacy-switch");
+ await user.click(legacySwitch);
+
+ expect(mockSetShowBeta).not.toHaveBeenCalled();
+ });
+ });
+
+ describe("Independent Toggle Behavior", () => {
+ it("should handle both switches being toggled independently", async () => {
+ const user = userEvent.setup();
+ render();
+
+ const betaSwitch = screen.getByTestId("sidebar-beta-switch");
+ const legacySwitch = screen.getByTestId("sidebar-legacy-switch");
+
+ await user.click(betaSwitch);
+ await user.click(legacySwitch);
+
+ expect(mockSetShowBeta).toHaveBeenCalledWith(true);
+ expect(mockSetShowLegacy).toHaveBeenCalledWith(true);
+ expect(mockSetShowBeta).toHaveBeenCalledTimes(1);
+ expect(mockSetShowLegacy).toHaveBeenCalledTimes(1);
+ });
+
+ it("should allow rapid toggling without interference", async () => {
+ const user = userEvent.setup();
+
+ const Controlled: React.FC = () => {
+ const [beta, setBeta] = React.useState(false);
+ const [legacy] = React.useState(false);
+ return (
+ {
+ setBeta(value);
+ mockSetShowBeta(value);
+ }}
+ showLegacy={legacy}
+ setShowLegacy={mockSetShowLegacy}
+ />
+ );
+ };
+
+ render();
+
+ const betaSwitch = screen.getByTestId("sidebar-beta-switch");
+
+ await user.click(betaSwitch);
+ await user.click(betaSwitch);
+ await user.click(betaSwitch);
+
+ expect(mockSetShowBeta).toHaveBeenCalledTimes(3);
+ expect(mockSetShowBeta).toHaveBeenNthCalledWith(1, true);
+ expect(mockSetShowBeta).toHaveBeenNthCalledWith(2, false);
+ expect(mockSetShowBeta).toHaveBeenNthCalledWith(3, true);
+ });
+ });
+
+ describe("Badge Configuration", () => {
+ it("should render Beta badge with correct variant and size", () => {
+ render();
+
+ const betaBadge = screen.getByTestId("badge-pinkStatic-xq");
+ expect(betaBadge).toHaveTextContent("Beta");
+ expect(betaBadge).toHaveClass("badge-pinkStatic", "badge-xq");
+ });
+
+ it("should render Legacy badge with correct variant and size", () => {
+ render();
+
+ const legacyBadge = screen.getByTestId("badge-secondaryStatic-xq");
+ expect(legacyBadge).toHaveTextContent("Legacy");
+ expect(legacyBadge).toHaveClass("badge-secondaryStatic", "badge-xq");
+ });
+ });
+
+ describe("Component Structure", () => {
+ it("should render toggles in correct order", () => {
+ render();
+
+ const badges = screen.getAllByTestId(/^badge-/);
+ expect(badges[0]).toHaveTextContent("Beta");
+ expect(badges[1]).toHaveTextContent("Legacy");
+ });
+
+ it("should have switches with correct test IDs", () => {
+ render();
+
+ expect(screen.getByTestId("sidebar-beta-switch")).toBeInTheDocument();
+ expect(screen.getByTestId("sidebar-legacy-switch")).toBeInTheDocument();
+ });
+ });
+
+ describe("Props Handling", () => {
+ it("should handle missing callback functions gracefully", () => {
+ const propsWithoutCallbacks = {
+ showBeta: false,
+ setShowBeta: undefined as any,
+ showLegacy: false,
+ setShowLegacy: undefined as any,
+ };
+
+ expect(() => {
+ render();
+ }).not.toThrow();
+ });
+
+ it("should handle boolean state changes correctly", () => {
+ const { rerender } = render();
+
+ let betaSwitch = screen.getByTestId("sidebar-beta-switch");
+ expect(betaSwitch).toHaveAttribute("aria-checked", "false");
+
+ const newProps = { ...defaultProps, showBeta: true };
+ rerender();
+
+ betaSwitch = screen.getByTestId("sidebar-beta-switch");
+ expect(betaSwitch).toHaveAttribute("aria-checked", "true");
+ });
+ });
+
+ describe("User Interaction", () => {
+ it("should respond to keyboard interactions on switches", async () => {
+ const user = userEvent.setup();
+ render();
+
+ // Tab to focus the switch and press Enter
+ await user.tab();
+ await user.keyboard("{Enter}");
+
+ expect(mockSetShowBeta).toHaveBeenCalledWith(true);
+ });
+
+ it("should maintain focus behavior for accessibility", async () => {
+ const user = userEvent.setup();
+ render();
+
+ const betaSwitch = screen.getByTestId("sidebar-beta-switch");
+ const legacySwitch = screen.getByTestId("sidebar-legacy-switch");
+
+ await user.tab();
+ expect(betaSwitch).toHaveFocus();
+
+ await user.tab();
+ expect(legacySwitch).toHaveFocus();
+ });
+ });
+
+ describe("Edge Cases", () => {
+ it("should render correctly with mixed initial states", () => {
+ const mixedStateProps = {
+ ...defaultProps,
+ showBeta: true,
+ showLegacy: false,
+ };
+
+ render();
+
+ const betaSwitch = screen.getByTestId("sidebar-beta-switch");
+ const legacySwitch = screen.getByTestId("sidebar-legacy-switch");
+
+ expect(betaSwitch).toHaveAttribute("aria-checked", "true");
+ expect(legacySwitch).toHaveAttribute("aria-checked", "false");
+ });
+
+ it("should handle rapid state changes", () => {
+ const { rerender } = render();
+
+ // Rapidly change states
+ rerender();
+ rerender();
+ rerender(
+ ,
+ );
+
+ const betaSwitch = screen.getByTestId("sidebar-beta-switch");
+ const legacySwitch = screen.getByTestId("sidebar-legacy-switch");
+
+ expect(betaSwitch).toHaveAttribute("aria-checked", "true");
+ expect(legacySwitch).toHaveAttribute("aria-checked", "true");
+ });
+ });
+});
diff --git a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/__tests__/sidebarBundles.test.tsx b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/__tests__/sidebarBundles.test.tsx
new file mode 100644
index 000000000..b9e026fb6
--- /dev/null
+++ b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/__tests__/sidebarBundles.test.tsx
@@ -0,0 +1,562 @@
+import { render, screen } from "@testing-library/react";
+import { MemoizedSidebarGroup } from "../sidebarBundles";
+
+// Mock the UI components
+jest.mock("@/components/ui/sidebar", () => ({
+ SidebarGroup: ({ children, className }: any) => (
+
+ {children}
+
+ ),
+ SidebarGroupContent: ({ children }: any) => (
+ {children}
+ ),
+ SidebarGroupLabel: ({ children, className }: any) => (
+
+ {children}
+
+ ),
+ SidebarMenu: ({ children }: any) => (
+ {children}
+ ),
+}));
+
+// Mock the BundleItem component
+jest.mock("../bundleItems", () => ({
+ BundleItem: ({ item, openCategories }: any) => (
+
+ Bundle Item: {item.display_name} - Open:{" "}
+ {openCategories.includes(item.name).toString()}
+
+ ),
+}));
+
+describe("MemoizedSidebarGroup (SidebarBundles)", () => {
+ const mockSetOpenCategories = jest.fn();
+ const mockOnDragStart = jest.fn();
+ const mockSensitiveSort = jest.fn();
+ const mockHandleKeyDownInput = jest.fn();
+
+ const mockAPIClass = {
+ description: "Test component",
+ template: {},
+ display_name: "Test Component",
+ documentation: "Test docs",
+ };
+
+ const defaultProps = {
+ BUNDLES: [
+ { name: "bundle1", display_name: "Bundle 1", icon: "Package" },
+ { name: "bundle2", display_name: "Bundle 2", icon: "Box" },
+ { name: "bundle3", display_name: "Bundle 3", icon: "Archive" },
+ ],
+ search: "",
+ sortedCategories: [],
+ dataFilter: {
+ bundle1: {
+ component1: { ...mockAPIClass, display_name: "Component 1" },
+ },
+ bundle2: {
+ component2: { ...mockAPIClass, display_name: "Component 2" },
+ },
+ bundle3: {
+ component3: { ...mockAPIClass, display_name: "Component 3" },
+ },
+ },
+ nodeColors: {
+ bundle1: "#FF0000",
+ bundle2: "#00FF00",
+ bundle3: "#0000FF",
+ },
+ onDragStart: mockOnDragStart,
+ sensitiveSort: mockSensitiveSort,
+ handleKeyDownInput: mockHandleKeyDownInput,
+ openCategories: [],
+ setOpenCategories: mockSetOpenCategories,
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe("Basic Rendering", () => {
+ it("should render sidebar group with correct structure", () => {
+ render();
+
+ expect(screen.getByTestId("sidebar-group")).toBeInTheDocument();
+ expect(screen.getByTestId("sidebar-group-content")).toBeInTheDocument();
+ expect(screen.getByTestId("sidebar-group-label")).toBeInTheDocument();
+ expect(screen.getByTestId("sidebar-menu")).toBeInTheDocument();
+ });
+
+ it("should display 'Bundles' label", () => {
+ render();
+
+ expect(screen.getByTestId("sidebar-group-label")).toHaveTextContent(
+ "Bundles",
+ );
+ });
+
+ it("should apply correct CSS classes", () => {
+ render();
+
+ expect(screen.getByTestId("sidebar-group")).toHaveClass("p-3");
+ expect(screen.getByTestId("sidebar-group-label")).toHaveClass(
+ "cursor-default",
+ );
+ });
+
+ it("should render all bundle items", () => {
+ render();
+
+ expect(screen.getByTestId("bundle-item-bundle1")).toBeInTheDocument();
+ expect(screen.getByTestId("bundle-item-bundle2")).toBeInTheDocument();
+ expect(screen.getByTestId("bundle-item-bundle3")).toBeInTheDocument();
+ });
+
+ it("should display correct bundle names", () => {
+ render();
+
+ expect(
+ screen.getByText("Bundle Item: Bundle 1 - Open: false"),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText("Bundle Item: Bundle 2 - Open: false"),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText("Bundle Item: Bundle 3 - Open: false"),
+ ).toBeInTheDocument();
+ });
+ });
+
+ describe("Bundle Sorting", () => {
+ it("should sort bundles according to BUNDLES order when search is empty", () => {
+ const propsWithReorderedBundles = {
+ ...defaultProps,
+ BUNDLES: [
+ { name: "bundle3", display_name: "Bundle 3", icon: "Archive" },
+ { name: "bundle1", display_name: "Bundle 1", icon: "Package" },
+ { name: "bundle2", display_name: "Bundle 2", icon: "Box" },
+ ],
+ };
+
+ render();
+
+ const bundleItems = screen.getAllByTestId(/bundle-item-/);
+ expect(bundleItems[0]).toHaveAttribute(
+ "data-testid",
+ "bundle-item-bundle3",
+ );
+ expect(bundleItems[1]).toHaveAttribute(
+ "data-testid",
+ "bundle-item-bundle1",
+ );
+ expect(bundleItems[2]).toHaveAttribute(
+ "data-testid",
+ "bundle-item-bundle2",
+ );
+ });
+
+ it("should sort bundles according to sortedCategories when search is not empty", () => {
+ const propsWithSearch = {
+ ...defaultProps,
+ search: "test search",
+ sortedCategories: ["bundle2", "bundle3", "bundle1"],
+ };
+
+ render();
+
+ const bundleItems = screen.getAllByTestId(/bundle-item-/);
+ expect(bundleItems[0]).toHaveAttribute(
+ "data-testid",
+ "bundle-item-bundle2",
+ );
+ expect(bundleItems[1]).toHaveAttribute(
+ "data-testid",
+ "bundle-item-bundle3",
+ );
+ expect(bundleItems[2]).toHaveAttribute(
+ "data-testid",
+ "bundle-item-bundle1",
+ );
+ });
+
+ it("should handle bundles not in sortedCategories gracefully", () => {
+ const propsWithPartialSort = {
+ ...defaultProps,
+ search: "test",
+ sortedCategories: ["bundle1"], // Only one bundle in sorted categories
+ };
+
+ render();
+
+ // All bundles should still render
+ expect(screen.getByTestId("bundle-item-bundle1")).toBeInTheDocument();
+ expect(screen.getByTestId("bundle-item-bundle2")).toBeInTheDocument();
+ expect(screen.getByTestId("bundle-item-bundle3")).toBeInTheDocument();
+ });
+
+ it("should handle empty sortedCategories with search", () => {
+ const propsWithEmptySort = {
+ ...defaultProps,
+ search: "test",
+ sortedCategories: [],
+ };
+
+ render();
+
+ expect(screen.getAllByTestId(/bundle-item-/)).toHaveLength(3);
+ });
+ });
+
+ describe("Props Passing to BundleItem", () => {
+ it("should pass all required props to each BundleItem", () => {
+ render();
+
+ // Verify that BundleItems are rendered (which means props are passed correctly)
+ expect(screen.getByTestId("bundle-item-bundle1")).toBeInTheDocument();
+ expect(screen.getByTestId("bundle-item-bundle2")).toBeInTheDocument();
+ expect(screen.getByTestId("bundle-item-bundle3")).toBeInTheDocument();
+ });
+
+ it("should pass openCategories state correctly", () => {
+ const propsWithOpenCategories = {
+ ...defaultProps,
+ openCategories: ["bundle2"],
+ };
+
+ render();
+
+ expect(
+ screen.getByText("Bundle Item: Bundle 1 - Open: false"),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText("Bundle Item: Bundle 2 - Open: true"),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText("Bundle Item: Bundle 3 - Open: false"),
+ ).toBeInTheDocument();
+ });
+
+ it("should handle multiple open categories", () => {
+ const propsWithMultipleOpen = {
+ ...defaultProps,
+ openCategories: ["bundle1", "bundle3"],
+ };
+
+ render();
+
+ expect(
+ screen.getByText("Bundle Item: Bundle 1 - Open: true"),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText("Bundle Item: Bundle 2 - Open: false"),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText("Bundle Item: Bundle 3 - Open: true"),
+ ).toBeInTheDocument();
+ });
+ });
+
+ describe("Memoization and Performance", () => {
+ it("should be memoized for performance", () => {
+ expect(MemoizedSidebarGroup.displayName).toBe("MemoizedSidebarGroup");
+ });
+
+ it("should not re-render when props haven't changed", () => {
+ const { rerender } = render();
+
+ const initialElement = screen.getByTestId("sidebar-group");
+
+ // Re-render with same props
+ rerender();
+
+ expect(screen.getByTestId("sidebar-group")).toBe(initialElement);
+ });
+
+ it("should re-render when BUNDLES prop changes", () => {
+ const { rerender } = render();
+
+ expect(screen.getAllByTestId(/bundle-item-/)).toHaveLength(3);
+
+ const newProps = {
+ ...defaultProps,
+ BUNDLES: [
+ { name: "bundle1", display_name: "Bundle 1", icon: "Package" },
+ ],
+ };
+
+ rerender();
+
+ expect(screen.getAllByTestId(/bundle-item-/)).toHaveLength(1);
+ });
+
+ it("should recalculate sorting when search changes", () => {
+ const { rerender } = render();
+
+ let bundleItems = screen.getAllByTestId(/bundle-item-/);
+ expect(bundleItems[0]).toHaveAttribute(
+ "data-testid",
+ "bundle-item-bundle1",
+ );
+
+ const propsWithSearch = {
+ ...defaultProps,
+ search: "test",
+ sortedCategories: ["bundle3", "bundle1", "bundle2"],
+ };
+
+ rerender();
+
+ bundleItems = screen.getAllByTestId(/bundle-item-/);
+ expect(bundleItems[0]).toHaveAttribute(
+ "data-testid",
+ "bundle-item-bundle3",
+ );
+ });
+
+ it("should recalculate sorting when sortedCategories changes", () => {
+ const propsWithSearch = {
+ ...defaultProps,
+ search: "test",
+ sortedCategories: ["bundle1", "bundle2", "bundle3"],
+ };
+
+ const { rerender } = render(
+ ,
+ );
+
+ let bundleItems = screen.getAllByTestId(/bundle-item-/);
+ expect(bundleItems[0]).toHaveAttribute(
+ "data-testid",
+ "bundle-item-bundle1",
+ );
+
+ const updatedProps = {
+ ...propsWithSearch,
+ sortedCategories: ["bundle3", "bundle2", "bundle1"],
+ };
+
+ rerender();
+
+ bundleItems = screen.getAllByTestId(/bundle-item-/);
+ expect(bundleItems[0]).toHaveAttribute(
+ "data-testid",
+ "bundle-item-bundle3",
+ );
+ });
+ });
+
+ describe("Edge Cases", () => {
+ it("should handle empty BUNDLES array", () => {
+ const propsWithEmptyBundles = {
+ ...defaultProps,
+ BUNDLES: [],
+ };
+
+ render();
+
+ expect(screen.getByTestId("sidebar-group-label")).toHaveTextContent(
+ "Bundles",
+ );
+ expect(screen.queryByTestId(/bundle-item-/)).not.toBeInTheDocument();
+ });
+
+ it("should handle single bundle", () => {
+ const propsWithSingleBundle = {
+ ...defaultProps,
+ BUNDLES: [
+ { name: "bundle1", display_name: "Bundle 1", icon: "Package" },
+ ],
+ };
+
+ render();
+
+ expect(screen.getAllByTestId(/bundle-item-/)).toHaveLength(1);
+ expect(screen.getByTestId("bundle-item-bundle1")).toBeInTheDocument();
+ });
+
+ it("should handle bundles with same names gracefully", () => {
+ const propsWithDuplicateNames = {
+ ...defaultProps,
+ BUNDLES: [
+ { name: "bundle1", display_name: "Bundle 1", icon: "Package" },
+ { name: "bundle1", display_name: "Bundle 1 Copy", icon: "Package" },
+ ],
+ };
+
+ render();
+
+ // Should render both (React will warn about duplicate keys, but still render)
+ const bundle1Items = screen.getAllByTestId("bundle-item-bundle1");
+ expect(bundle1Items.length).toBeGreaterThan(0);
+ });
+
+ it("should handle missing dataFilter gracefully", () => {
+ const propsWithoutDataFilter = {
+ ...defaultProps,
+ dataFilter: {},
+ };
+
+ render();
+
+ expect(screen.getAllByTestId(/bundle-item-/)).toHaveLength(3);
+ });
+
+ it("should handle missing nodeColors gracefully", () => {
+ const propsWithoutNodeColors = {
+ ...defaultProps,
+ nodeColors: {},
+ };
+
+ render();
+
+ expect(screen.getAllByTestId(/bundle-item-/)).toHaveLength(3);
+ });
+
+ it("should handle undefined search and sortedCategories", () => {
+ const propsWithEmpty = {
+ ...defaultProps,
+ search: "",
+ sortedCategories: [],
+ };
+
+ expect(() => {
+ render();
+ }).not.toThrow();
+ });
+ });
+
+ describe("Component Structure", () => {
+ it("should have correct DOM hierarchy", () => {
+ render();
+
+ const sidebarGroup = screen.getByTestId("sidebar-group");
+ const sidebarGroupLabel = screen.getByTestId("sidebar-group-label");
+ const sidebarGroupContent = screen.getByTestId("sidebar-group-content");
+ const sidebarMenu = screen.getByTestId("sidebar-menu");
+
+ expect(sidebarGroup).toContainElement(sidebarGroupLabel);
+ expect(sidebarGroup).toContainElement(sidebarGroupContent);
+ expect(sidebarGroupContent).toContainElement(sidebarMenu);
+ expect(sidebarMenu).toContainElement(
+ screen.getByTestId("bundle-item-bundle1"),
+ );
+ });
+
+ it("should render bundle items inside sidebar menu", () => {
+ render();
+
+ const sidebarMenu = screen.getByTestId("sidebar-menu");
+ const bundleItems = screen.getAllByTestId(/bundle-item-/);
+
+ bundleItems.forEach((item) => {
+ expect(sidebarMenu).toContainElement(item);
+ });
+ });
+
+ it("should maintain structure with no bundles", () => {
+ const propsWithNoBundles = {
+ ...defaultProps,
+ BUNDLES: [],
+ };
+
+ render();
+
+ expect(screen.getByTestId("sidebar-group")).toBeInTheDocument();
+ expect(screen.getByTestId("sidebar-group-label")).toBeInTheDocument();
+ expect(screen.getByTestId("sidebar-group-content")).toBeInTheDocument();
+ expect(screen.getByTestId("sidebar-menu")).toBeInTheDocument();
+ });
+ });
+
+ describe("Bundle Sorting Logic", () => {
+ it("should use toSorted method for bundle sorting", () => {
+ const propsWithCustomOrder = {
+ ...defaultProps,
+ BUNDLES: [
+ { name: "z", display_name: "Z Bundle", icon: "Package" },
+ { name: "a", display_name: "A Bundle", icon: "Package" },
+ { name: "m", display_name: "M Bundle", icon: "Package" },
+ ],
+ sortedCategories: [],
+ };
+
+ render();
+
+ // Should maintain original order when no search
+ const bundleItems = screen.getAllByTestId(/bundle-item-/);
+ expect(bundleItems[0]).toHaveAttribute("data-testid", "bundle-item-z");
+ expect(bundleItems[1]).toHaveAttribute("data-testid", "bundle-item-a");
+ expect(bundleItems[2]).toHaveAttribute("data-testid", "bundle-item-m");
+ });
+
+ it("should handle complex sorting scenarios", () => {
+ const complexProps = {
+ ...defaultProps,
+ BUNDLES: [
+ { name: "bundle1", display_name: "Bundle 1", icon: "Package" },
+ { name: "bundle2", display_name: "Bundle 2", icon: "Box" },
+ { name: "bundle3", display_name: "Bundle 3", icon: "Archive" },
+ { name: "bundle4", display_name: "Bundle 4", icon: "Package" },
+ ],
+ search: "test",
+ sortedCategories: ["bundle4", "bundle1", "bundle2", "bundle3"], // Include all bundles
+ };
+
+ render();
+
+ const bundleItems = screen.getAllByTestId(/bundle-item-/);
+ expect(bundleItems).toHaveLength(4);
+
+ // Should follow sortedCategories order when search is active
+ expect(bundleItems[0]).toHaveAttribute(
+ "data-testid",
+ "bundle-item-bundle4",
+ );
+ expect(bundleItems[1]).toHaveAttribute(
+ "data-testid",
+ "bundle-item-bundle1",
+ );
+ expect(bundleItems[2]).toHaveAttribute(
+ "data-testid",
+ "bundle-item-bundle2",
+ );
+ expect(bundleItems[3]).toHaveAttribute(
+ "data-testid",
+ "bundle-item-bundle3",
+ );
+ });
+ });
+
+ describe("Callback Functions", () => {
+ it("should handle missing callback functions gracefully", () => {
+ const propsWithoutCallbacks = {
+ ...defaultProps,
+ setOpenCategories: undefined as any,
+ onDragStart: undefined as any,
+ sensitiveSort: undefined as any,
+ handleKeyDownInput: undefined as any,
+ };
+
+ expect(() => {
+ render();
+ }).not.toThrow();
+ });
+
+ it("should work with different callback functions", () => {
+ const alternativeSetOpenCategories = jest.fn();
+ const alternativeOnDragStart = jest.fn();
+
+ const alternativeProps = {
+ ...defaultProps,
+ setOpenCategories: alternativeSetOpenCategories,
+ onDragStart: alternativeOnDragStart,
+ };
+
+ render();
+
+ expect(screen.getAllByTestId(/bundle-item-/)).toHaveLength(3);
+ });
+ });
+});
diff --git a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/__tests__/sidebarDraggableComponent.test.tsx b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/__tests__/sidebarDraggableComponent.test.tsx
new file mode 100644
index 000000000..f7c5dac5a
--- /dev/null
+++ b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/__tests__/sidebarDraggableComponent.test.tsx
@@ -0,0 +1,681 @@
+import { fireEvent, render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import React from "react";
+import { SidebarDraggableComponent } from "../sidebarDraggableComponent";
+
+// Mock all external dependencies
+jest.mock(
+ "@/components/common/storeCardComponent/utils/convert-test-name",
+ () => ({
+ convertTestName: (name: string) => name.toLowerCase().replace(/\s+/g, "-"),
+ }),
+);
+
+jest.mock("@/components/ui/badge", () => ({
+ Badge: ({ children, variant, size, className }: any) => (
+
+ {children}
+
+ ),
+}));
+
+jest.mock("@/components/ui/button", () => ({
+ Button: ({
+ children,
+ onClick,
+ className,
+ variant,
+ size,
+ tabIndex,
+ ...props
+ }: any) => (
+
+ ),
+}));
+
+jest.mock("@/hooks/flows/use-delete-flow", () => ({
+ __esModule: true,
+ default: () => ({
+ deleteFlow: jest.fn(),
+ }),
+}));
+
+const mockAddComponentFn = jest.fn();
+jest.mock("@/hooks/use-add-component", () => ({
+ useAddComponent: jest.fn(() => mockAddComponentFn),
+}));
+
+jest.mock("@/components/common/genericIconComponent", () => ({
+ __esModule: true,
+ default: ({ name, className }: any) => (
+
+ {name}
+
+ ),
+ ForwardedIconComponent: ({ name, className }: any) => (
+
+ {name}
+
+ ),
+}));
+
+jest.mock("@/components/common/shadTooltipComponent", () => ({
+ __esModule: true,
+ default: ({ children, content, styleClasses }: any) => (
+
+ {children}
+
+ ),
+}));
+
+jest.mock("@/components/ui/select-custom", () => ({
+ Select: ({ children, onValueChange, onOpenChange, open, ...props }: any) => (
+
+ {children}
+
+ ),
+ SelectContent: ({ children, position, side, sideOffset, style }: any) => (
+
+ {children}
+
+ ),
+ SelectItem: ({ children, value, ...props }: any) => (
+
+ {children}
+
+ ),
+ SelectTrigger: ({ children, tabIndex, ...props }: any) => (
+
+ {children}
+
+ ),
+}));
+
+jest.mock("@/stores/darkStore", () => ({
+ useDarkStore: () => ({
+ version: "1.0.0",
+ }),
+}));
+
+jest.mock("@/stores/flowsManagerStore", () => ({
+ __esModule: true,
+ default: () => ({
+ flows: [
+ { id: "flow1", name: "Test Flow" },
+ { id: "flow2", name: "Another Flow" },
+ ],
+ }),
+}));
+
+jest.mock("@/utils/reactflowUtils", () => ({
+ createFlowComponent: jest.fn(),
+ downloadNode: jest.fn(),
+ getNodeId: jest.fn().mockReturnValue("test-node-id"),
+}));
+
+jest.mock("@/utils/utils", () => ({
+ cn: (...classes: any[]) => classes.filter(Boolean).join(" "),
+ removeCountFromString: (str: string) => str.replace(/\d+$/, ""),
+}));
+
+describe("SidebarDraggableComponent", () => {
+ const mockOnDragStart = jest.fn();
+
+ const mockAPIClass = {
+ description: "Test component",
+ template: {},
+ display_name: "Test Component",
+ documentation: "Test docs",
+ icon: "TestIcon",
+ };
+
+ const defaultProps = {
+ sectionName: "TestSection",
+ display_name: "Test Component",
+ icon: "TestIcon",
+ itemName: "test-component",
+ error: false,
+ color: "#FF0000",
+ onDragStart: mockOnDragStart,
+ apiClass: mockAPIClass,
+ official: true,
+ beta: false,
+ legacy: false,
+ disabled: false,
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe("Basic Rendering", () => {
+ it("should render component with correct structure", () => {
+ render();
+
+ expect(screen.getByTestId("select")).toBeInTheDocument();
+ expect(screen.getAllByTestId("tooltip").length).toBeGreaterThan(0);
+ expect(
+ screen.getByTestId(/testsectiontest component/i),
+ ).toBeInTheDocument();
+ expect(screen.getByTestId("forwarded-icon-TestIcon")).toBeInTheDocument();
+ expect(screen.getByText("Test Component")).toBeInTheDocument();
+ });
+
+ it("should display component name", () => {
+ render();
+
+ expect(screen.getByText("Test Component")).toBeInTheDocument();
+ });
+
+ it("should display component icon", () => {
+ render();
+
+ expect(screen.getByTestId("forwarded-icon-TestIcon")).toBeInTheDocument();
+ });
+
+ it("should have correct test id format", () => {
+ render();
+
+ expect(
+ screen.getByTestId("testsection_test component_draggable"),
+ ).toBeInTheDocument();
+ });
+
+ it("should display grip vertical icon", () => {
+ render();
+
+ expect(
+ screen.getByTestId("forwarded-icon-GripVertical"),
+ ).toBeInTheDocument();
+ });
+
+ it("should render add component button when not disabled", () => {
+ render();
+
+ expect(
+ screen.getByTestId("add-component-button-test-component"),
+ ).toBeInTheDocument();
+ expect(screen.getByTestId("forwarded-icon-Plus")).toBeInTheDocument();
+ });
+ });
+
+ describe("Badge Rendering", () => {
+ it("should render Beta badge when beta prop is true", () => {
+ const propsWithBeta = { ...defaultProps, beta: true };
+
+ render();
+
+ expect(screen.getByTestId("badge-pinkStatic-xq")).toBeInTheDocument();
+ expect(screen.getByText("Beta")).toBeInTheDocument();
+ });
+
+ it("should render Legacy badge when legacy prop is true", () => {
+ const propsWithLegacy = { ...defaultProps, legacy: true };
+
+ render();
+
+ expect(
+ screen.getByTestId("badge-secondaryStatic-xq"),
+ ).toBeInTheDocument();
+ expect(screen.getByText("Legacy")).toBeInTheDocument();
+ });
+
+ it("should render both badges when both beta and legacy are true", () => {
+ const propsWithBoth = { ...defaultProps, beta: true, legacy: true };
+
+ render();
+
+ expect(screen.getByTestId("badge-pinkStatic-xq")).toBeInTheDocument();
+ expect(
+ screen.getByTestId("badge-secondaryStatic-xq"),
+ ).toBeInTheDocument();
+ expect(screen.getByText("Beta")).toBeInTheDocument();
+ expect(screen.getByText("Legacy")).toBeInTheDocument();
+ });
+
+ it("should not render badges when neither beta nor legacy are true", () => {
+ render();
+
+ expect(
+ screen.queryByTestId("badge-pinkStatic-xq"),
+ ).not.toBeInTheDocument();
+ expect(
+ screen.queryByTestId("badge-secondaryStatic-xq"),
+ ).not.toBeInTheDocument();
+ });
+ });
+
+ describe("Disabled State", () => {
+ it("should handle disabled state correctly", () => {
+ const propsWithDisabled = {
+ ...defaultProps,
+ disabled: true,
+ disabledTooltip: "This component is disabled",
+ };
+
+ render();
+
+ expect(screen.getAllByTestId("tooltip")[0]).toHaveAttribute(
+ "data-content",
+ "This component is disabled",
+ );
+ expect(
+ screen.queryByTestId("add-component-button-test-component"),
+ ).not.toBeInTheDocument();
+ });
+
+ it("should not show add button when disabled", () => {
+ const propsWithDisabled = { ...defaultProps, disabled: true };
+
+ render();
+
+ expect(
+ screen.queryByTestId("add-component-button-test-component"),
+ ).not.toBeInTheDocument();
+ });
+
+ it("should show no tooltip content when not disabled", () => {
+ render();
+
+ expect(screen.getAllByTestId("tooltip")[0]).not.toHaveAttribute(
+ "data-content",
+ );
+ });
+ });
+
+ describe("Error State", () => {
+ it("should handle error state correctly", () => {
+ const propsWithError = { ...defaultProps, error: true };
+
+ render();
+
+ const draggableDiv = screen.getByTestId(/testsectiontest component/i);
+ expect(draggableDiv).toHaveAttribute("draggable", "false");
+ });
+
+ it("should be draggable when no error", () => {
+ render();
+
+ const draggableDiv = screen.getByTestId(/testsectiontest component/i);
+ expect(draggableDiv).toHaveAttribute("draggable", "true");
+ });
+ });
+
+ describe("Drag and Drop", () => {
+ it("should call onDragStart when dragging starts", () => {
+ render();
+
+ const draggableDiv = screen.getByTestId(/testsectiontest component/i);
+ fireEvent.dragStart(draggableDiv);
+
+ expect(mockOnDragStart).toHaveBeenCalledTimes(1);
+ });
+
+ it("should handle drag end correctly", () => {
+ // Mock document methods
+ const mockRemoveChild = jest.fn();
+ const mockGetElementsByClassName = jest
+ .fn()
+ .mockReturnValue([document.createElement("div")]);
+ Object.defineProperty(document, "getElementsByClassName", {
+ value: mockGetElementsByClassName,
+ writable: true,
+ });
+ Object.defineProperty(document.body, "removeChild", {
+ value: mockRemoveChild,
+ writable: true,
+ });
+
+ render();
+
+ const draggableDiv = screen.getByTestId(/testsectiontest component/i);
+ fireEvent.dragEnd(draggableDiv);
+
+ expect(mockGetElementsByClassName).toHaveBeenCalledWith(
+ "cursor-grabbing",
+ );
+ });
+
+ it("should not be draggable when error is true", () => {
+ const propsWithError = { ...defaultProps, error: true };
+ render();
+
+ const draggableDiv = screen.getByTestId(/testsectiontest component/i);
+ expect(draggableDiv).toHaveAttribute("draggable", "false");
+ });
+ });
+
+ describe("Add Component Functionality", () => {
+ it("should call addComponent when add button is clicked", async () => {
+ const user = userEvent.setup();
+ render();
+
+ const addButton = screen.getByTestId(
+ "add-component-button-test-component",
+ );
+ await user.click(addButton);
+
+ expect(mockAddComponentFn).toHaveBeenCalledWith(
+ mockAPIClass,
+ "test-component",
+ );
+ });
+
+ it("should call addComponent when Enter key is pressed", () => {
+ render();
+
+ const container = screen.getByTestId(
+ "testsection_test component_draggable",
+ );
+ fireEvent.keyDown(container, { key: "Enter" });
+
+ expect(mockAddComponentFn).toHaveBeenCalledWith(
+ mockAPIClass,
+ "test-component",
+ );
+ });
+
+ it("should call addComponent when Space key is pressed", () => {
+ render();
+
+ const container = screen.getByTestId(
+ "testsection_test component_draggable",
+ );
+ fireEvent.keyDown(container, { key: " " });
+
+ expect(mockAddComponentFn).toHaveBeenCalledWith(
+ mockAPIClass,
+ "test-component",
+ );
+ });
+
+ it("should not call addComponent for other keys", () => {
+ render();
+
+ const container = screen.getByTestId(
+ "testsection_test component_draggable",
+ );
+ fireEvent.keyDown(container, { key: "Escape" });
+
+ expect(mockAddComponentFn).not.toHaveBeenCalled();
+ });
+ });
+
+ describe("Context Menu and Select", () => {
+ it("should handle context menu capture", () => {
+ render();
+
+ const container = screen.getByTestId(
+ "testsection_test component_draggable",
+ );
+ fireEvent.contextMenu(container);
+
+ // We can't easily verify the context menu state, but ensure no error occurs
+ expect(container).toBeInTheDocument();
+ });
+
+ it("should render select content with download option", () => {
+ render();
+
+ expect(screen.getByTestId("select-item-download")).toBeInTheDocument();
+ expect(screen.getByTestId("icon-Download")).toBeInTheDocument();
+ });
+
+ it("should render delete option when not official", () => {
+ const propsNotOfficial = { ...defaultProps, official: false };
+ render();
+
+ expect(screen.getByTestId("select-item-delete")).toBeInTheDocument();
+ expect(screen.getByText("Delete")).toBeInTheDocument();
+ expect(screen.getByTestId("icon-Trash2")).toBeInTheDocument();
+ });
+
+ it("should not render delete option when official", () => {
+ render();
+
+ expect(
+ screen.queryByTestId("select-item-delete"),
+ ).not.toBeInTheDocument();
+ });
+ });
+
+ describe("Style and Classes", () => {
+ it("should apply correct border color", () => {
+ render();
+
+ const draggableDiv = screen.getByTestId(/testsectiontest component/i);
+ expect(draggableDiv).toHaveStyle({ borderLeftColor: "#FF0000" });
+ });
+
+ it("should have correct tabIndex", () => {
+ render();
+
+ const container = screen.getByTestId(
+ "testsection_test component_draggable",
+ );
+ expect(container).toHaveAttribute("tabIndex", "0");
+ });
+
+ it("should have add button with tabIndex -1", () => {
+ render();
+
+ const addButton = screen.getByTestId(
+ "add-component-button-test-component",
+ );
+ expect(addButton).toHaveAttribute("tabIndex", "-1");
+ });
+
+ it("should have select trigger with tabIndex -1", () => {
+ render();
+
+ expect(screen.getByTestId("select-trigger")).toHaveAttribute(
+ "tabIndex",
+ "-1",
+ );
+ });
+ });
+
+ describe("Props Handling", () => {
+ it("should handle different section names", () => {
+ const propsWithDifferentSection = {
+ ...defaultProps,
+ sectionName: "CustomSection",
+ };
+
+ render();
+
+ expect(
+ screen.getByTestId(/customsectiontest component/i),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByTestId(/customsection_test component_draggable/i),
+ ).toBeInTheDocument();
+ });
+
+ it("should handle different display names", () => {
+ const propsWithDifferentName = {
+ ...defaultProps,
+ display_name: "My Custom Component",
+ };
+
+ render();
+
+ expect(screen.getByText("My Custom Component")).toBeInTheDocument();
+ expect(
+ screen.getByTestId("add-component-button-my-custom-component"),
+ ).toBeInTheDocument();
+ });
+
+ it("should handle different icons", () => {
+ const propsWithDifferentIcon = {
+ ...defaultProps,
+ icon: "CustomIcon",
+ };
+
+ render();
+
+ expect(
+ screen.getByTestId("forwarded-icon-CustomIcon"),
+ ).toBeInTheDocument();
+ });
+
+ it("should handle different colors", () => {
+ const propsWithDifferentColor = {
+ ...defaultProps,
+ color: "#00FF00",
+ };
+
+ render();
+
+ const draggableDiv = screen.getByTestId(/testsectiontest component/i);
+ expect(draggableDiv).toHaveStyle({ borderLeftColor: "#00FF00" });
+ });
+ });
+
+ describe("Component Structure", () => {
+ it("should have correct DOM hierarchy", () => {
+ render();
+
+ const select = screen.getByTestId("select");
+ const tooltips = screen.getAllByTestId("tooltip");
+ const draggableContainer = screen.getByTestId(
+ "testsection_test component_draggable",
+ );
+ const draggableDiv = screen.getByTestId(/testsectiontest component/i);
+
+ expect(select).toContainElement(tooltips[0]);
+ expect(tooltips[0]).toContainElement(draggableContainer);
+ expect(draggableContainer).toContainElement(draggableDiv);
+ });
+
+ it("should contain all expected child elements", () => {
+ render();
+
+ expect(screen.getByTestId("forwarded-icon-TestIcon")).toBeInTheDocument();
+ expect(screen.getByText("Test Component")).toBeInTheDocument();
+ expect(
+ screen.getByTestId("add-component-button-test-component"),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByTestId("forwarded-icon-GripVertical"),
+ ).toBeInTheDocument();
+ expect(screen.getByTestId("select-content")).toBeInTheDocument();
+ });
+ });
+
+ describe("Edge Cases", () => {
+ it("should handle empty display name", () => {
+ const propsWithEmptyName = {
+ ...defaultProps,
+ display_name: "",
+ };
+
+ render();
+
+ expect(screen.getByTestId("display-name")).toHaveTextContent("");
+ });
+
+ it("should handle missing color", () => {
+ const propsWithoutColor = {
+ ...defaultProps,
+ color: undefined as any,
+ };
+
+ expect(() => {
+ render();
+ }).not.toThrow();
+ });
+
+ it("should handle missing onDragStart", () => {
+ const propsWithoutDragStart = {
+ ...defaultProps,
+ onDragStart: undefined as any,
+ };
+
+ expect(() => {
+ render();
+ }).not.toThrow();
+ });
+
+ it("should handle complex component combinations", () => {
+ const complexProps = {
+ ...defaultProps,
+ beta: true,
+ legacy: true,
+ disabled: true,
+ error: true,
+ official: false,
+ disabledTooltip: "Complex disabled state",
+ };
+
+ render();
+
+ expect(screen.getByTestId("badge-pinkStatic-xq")).toBeInTheDocument();
+ expect(
+ screen.getByTestId("badge-secondaryStatic-xq"),
+ ).toBeInTheDocument();
+ expect(
+ screen.queryByTestId("add-component-button-test-component"),
+ ).not.toBeInTheDocument();
+ expect(screen.getByTestId("select-item-delete")).toBeInTheDocument();
+ });
+ });
+
+ describe("Tooltip Integration", () => {
+ it("should show disabled tooltip when disabled", () => {
+ const propsWithDisabledTooltip = {
+ ...defaultProps,
+ disabled: true,
+ disabledTooltip: "This feature is disabled",
+ };
+
+ render();
+
+ const tooltips = screen.getAllByTestId("tooltip");
+ expect(
+ tooltips.some(
+ (t) => t.getAttribute("data-content") === "This feature is disabled",
+ ),
+ ).toBe(true);
+ });
+
+ it("should show component name in inner tooltip", () => {
+ render();
+
+ const tooltips = screen.getAllByTestId("tooltip");
+ expect(tooltips.length).toBeGreaterThan(0);
+ });
+ });
+
+ describe("Select Positioning", () => {
+ it("should render select content with correct positioning", () => {
+ render();
+
+ const selectContent = screen.getByTestId("select-content");
+ expect(selectContent).toHaveAttribute("data-position", "popper");
+ expect(selectContent).toHaveAttribute("data-side", "bottom");
+ expect(selectContent).toHaveAttribute("data-side-offset", "-25");
+ });
+ });
+});
diff --git a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/__tests__/sidebarFilterComponent.test.tsx b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/__tests__/sidebarFilterComponent.test.tsx
new file mode 100644
index 000000000..2587c9b2d
--- /dev/null
+++ b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/__tests__/sidebarFilterComponent.test.tsx
@@ -0,0 +1,536 @@
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import React from "react";
+import { SidebarFilterComponent } from "../sidebarFilterComponent";
+
+// Mock the UI components
+jest.mock("@/components/common/genericIconComponent", () => ({
+ __esModule: true,
+ default: ({ name, className }: any) => (
+
+ {name}
+
+ ),
+}));
+
+jest.mock("@/components/common/shadTooltipComponent", () => ({
+ __esModule: true,
+ default: ({ children, content, side, styleClasses }: any) => (
+
+ {children}
+
+ ),
+}));
+
+jest.mock("@/components/ui/button", () => ({
+ Button: ({ children, onClick, className, unstyled, ...props }: any) => (
+
+ ),
+}));
+
+describe("SidebarFilterComponent", () => {
+ const mockResetFilters = jest.fn();
+
+ const defaultProps = {
+ isInput: true,
+ type: "string",
+ color: "blue",
+ resetFilters: mockResetFilters,
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe("Basic Rendering", () => {
+ it("should render filter component with correct structure", () => {
+ render();
+
+ expect(screen.getByTestId("icon-ListFilter")).toBeInTheDocument();
+ expect(screen.getByText("Input:")).toBeInTheDocument();
+ expect(screen.getByText("string")).toBeInTheDocument();
+ expect(screen.getByTestId("sidebar-filter-reset")).toBeInTheDocument();
+ expect(screen.getByTestId("icon-X")).toBeInTheDocument();
+ });
+
+ it("should display ListFilter icon", () => {
+ render();
+
+ expect(screen.getByTestId("icon-ListFilter")).toBeInTheDocument();
+ });
+
+ it("should display X icon for reset button", () => {
+ render();
+
+ expect(screen.getByTestId("icon-X")).toBeInTheDocument();
+ });
+
+ it("should have reset button with correct test id", () => {
+ render();
+
+ expect(screen.getByTestId("sidebar-filter-reset")).toBeInTheDocument();
+ });
+
+ it("should render tooltip with correct content", () => {
+ render();
+
+ expect(screen.getByTestId("tooltip")).toHaveAttribute(
+ "data-content",
+ "Remove filter",
+ );
+ expect(screen.getByTestId("tooltip")).toHaveAttribute(
+ "data-side",
+ "right",
+ );
+ });
+ });
+
+ describe("Input/Output Display", () => {
+ it("should display 'Input:' when isInput is true", () => {
+ render();
+
+ expect(screen.getByText("Input:")).toBeInTheDocument();
+ expect(screen.queryByText("Output:")).not.toBeInTheDocument();
+ });
+
+ it("should display 'Output:' when isInput is false", () => {
+ const propsWithOutput = { ...defaultProps, isInput: false };
+ render();
+
+ expect(screen.getByText("Output:")).toBeInTheDocument();
+ expect(screen.queryByText("Input:")).not.toBeInTheDocument();
+ });
+
+ it("should display correct label for input", () => {
+ render();
+
+ expect(screen.getByText("Input:")).toBeInTheDocument();
+ });
+
+ it("should display correct label for output", () => {
+ const propsWithOutput = { ...defaultProps, isInput: false };
+ render();
+
+ expect(screen.getByText("Output:")).toBeInTheDocument();
+ });
+ });
+
+ describe("Type Display", () => {
+ it("should display single type correctly", () => {
+ render();
+
+ expect(screen.getByText("string")).toBeInTheDocument();
+ });
+
+ it("should display multiple types correctly", () => {
+ const propsWithMultipleTypes = {
+ ...defaultProps,
+ type: "string\nint\nboolean",
+ };
+
+ render();
+
+ expect(screen.getByText("string, int, boolean")).toBeInTheDocument();
+ });
+
+ it("should handle empty type", () => {
+ const propsWithEmptyType = { ...defaultProps, type: "" };
+ render();
+
+ // Should not crash
+ expect(screen.getByTestId("icon-ListFilter")).toBeInTheDocument();
+ });
+
+ it("should handle type with special characters", () => {
+ const propsWithSpecialType = { ...defaultProps, type: "List[str]" };
+ render();
+
+ expect(screen.getByText("List[str]")).toBeInTheDocument();
+ });
+ });
+
+ describe("Pluralization", () => {
+ it("should use singular form for single type", () => {
+ render();
+
+ expect(screen.getByText("Input:")).toBeInTheDocument();
+ expect(screen.queryByText("Inputs:")).not.toBeInTheDocument();
+ });
+
+ it("should use plural form for multiple types", () => {
+ const propsWithMultipleTypes = {
+ ...defaultProps,
+ type: "string\nint",
+ };
+
+ render();
+
+ expect(screen.getByText("Inputs:")).toBeInTheDocument();
+ expect(screen.queryByText("Input:")).not.toBeInTheDocument();
+ });
+
+ it("should use plural form for output with multiple types", () => {
+ const propsWithMultipleOutputTypes = {
+ ...defaultProps,
+ isInput: false,
+ type: "string\nint\nfloat",
+ };
+
+ render();
+
+ expect(screen.getByText("Outputs:")).toBeInTheDocument();
+ expect(screen.queryByText("Output:")).not.toBeInTheDocument();
+ });
+
+ it("should handle edge case with empty lines in type", () => {
+ const propsWithEmptyLines = {
+ ...defaultProps,
+ type: "string\n\nint",
+ };
+
+ render();
+
+ // Should treat empty line as a separate type for pluralization
+ expect(screen.getByText("Inputs:")).toBeInTheDocument();
+ });
+ });
+
+ describe("Color Styling", () => {
+ it("should render with different color props", () => {
+ render();
+
+ // Just verify component renders with color prop - inline styles are complex to test
+ expect(screen.getByTestId("icon-ListFilter")).toBeInTheDocument();
+ });
+
+ it("should handle different colors", () => {
+ const propsWithDifferentColor = { ...defaultProps, color: "red" };
+ render();
+
+ // Verify component renders without errors with different color
+ expect(screen.getByTestId("icon-ListFilter")).toBeInTheDocument();
+ });
+
+ it("should handle custom color values", () => {
+ const propsWithCustomColor = { ...defaultProps, color: "custom-green" };
+ render();
+
+ // Verify component renders without errors with custom color
+ expect(screen.getByTestId("icon-ListFilter")).toBeInTheDocument();
+ });
+ });
+
+ describe("Reset Functionality", () => {
+ it("should call resetFilters when reset button is clicked", async () => {
+ const user = userEvent.setup();
+ render();
+
+ const resetButton = screen.getByTestId("sidebar-filter-reset");
+ await user.click(resetButton);
+
+ expect(mockResetFilters).toHaveBeenCalledTimes(1);
+ });
+
+ it("should call resetFilters only once per click", async () => {
+ const user = userEvent.setup();
+ render();
+
+ const resetButton = screen.getByTestId("sidebar-filter-reset");
+ await user.click(resetButton);
+ await user.click(resetButton);
+
+ expect(mockResetFilters).toHaveBeenCalledTimes(2);
+ });
+
+ it("should handle rapid clicks on reset button", async () => {
+ const user = userEvent.setup();
+ render();
+
+ const resetButton = screen.getByTestId("sidebar-filter-reset");
+ await user.click(resetButton);
+ await user.click(resetButton);
+ await user.click(resetButton);
+
+ expect(mockResetFilters).toHaveBeenCalledTimes(3);
+ });
+
+ it("should work with different callback functions", async () => {
+ const user = userEvent.setup();
+ const alternativeResetFilters = jest.fn();
+ const propsWithDifferentCallback = {
+ ...defaultProps,
+ resetFilters: alternativeResetFilters,
+ };
+
+ render();
+
+ const resetButton = screen.getByTestId("sidebar-filter-reset");
+ await user.click(resetButton);
+
+ expect(alternativeResetFilters).toHaveBeenCalledTimes(1);
+ expect(mockResetFilters).not.toHaveBeenCalled();
+ });
+ });
+
+ describe("Component Structure", () => {
+ it("should have correct DOM hierarchy", () => {
+ render();
+
+ const container =
+ screen.getByTestId("icon-ListFilter").parentElement?.parentElement
+ ?.parentElement;
+ const iconContainer =
+ screen.getByTestId("icon-ListFilter").parentElement?.parentElement;
+ const resetButton = screen.getByTestId("sidebar-filter-reset");
+ const tooltip = screen.getByTestId("tooltip");
+
+ expect(container).toBeInTheDocument();
+ expect(container).toContainElement(iconContainer);
+ expect(container).toContainElement(tooltip);
+ expect(tooltip).toContainElement(resetButton);
+ });
+
+ it("should contain all expected elements", () => {
+ render();
+
+ expect(screen.getByTestId("icon-ListFilter")).toBeInTheDocument();
+ expect(screen.getByText("Input:")).toBeInTheDocument();
+ expect(screen.getByText("string")).toBeInTheDocument();
+ expect(screen.getByTestId("sidebar-filter-reset")).toBeInTheDocument();
+ expect(screen.getByTestId("icon-X")).toBeInTheDocument();
+ expect(screen.getByTestId("tooltip")).toBeInTheDocument();
+ });
+
+ it("should render reset button inside tooltip", () => {
+ render();
+
+ const tooltip = screen.getByTestId("tooltip");
+ const resetButton = screen.getByTestId("sidebar-filter-reset");
+
+ expect(tooltip).toContainElement(resetButton);
+ });
+ });
+
+ describe("CSS Classes", () => {
+ it("should render component structure correctly", () => {
+ render();
+
+ // Verify component structure rather than specific CSS classes
+ expect(screen.getByTestId("icon-ListFilter")).toBeInTheDocument();
+ expect(screen.getByTestId("sidebar-filter-reset")).toBeInTheDocument();
+ expect(screen.getByTestId("tooltip")).toBeInTheDocument();
+ });
+
+ it("should apply correct classes to icons", () => {
+ render();
+
+ const listFilterIcon = screen.getByTestId("icon-ListFilter");
+ const xIcon = screen.getByTestId("icon-X");
+
+ expect(listFilterIcon).toHaveClass("h-4", "w-4", "shrink-0", "stroke-2");
+ expect(xIcon).toHaveClass("h-4", "w-4", "stroke-2");
+ });
+
+ it("should apply correct classes to button", () => {
+ render();
+
+ const resetButton = screen.getByTestId("sidebar-filter-reset");
+ expect(resetButton).toHaveClass("shrink-0");
+ expect(resetButton).toHaveAttribute("data-unstyled", "true");
+ });
+
+ it("should apply tooltip styling classes", () => {
+ render();
+
+ const tooltip = screen.getByTestId("tooltip");
+ expect(tooltip).toHaveClass("max-w-full");
+ });
+ });
+
+ describe("Props Handling", () => {
+ it("should handle different isInput values", () => {
+ const { rerender } = render(
+ ,
+ );
+ expect(screen.getByText("Input:")).toBeInTheDocument();
+
+ rerender();
+ expect(screen.getByText("Output:")).toBeInTheDocument();
+ });
+
+ it("should handle different type values", () => {
+ const { rerender } = render(
+ ,
+ );
+ expect(screen.getByText("string")).toBeInTheDocument();
+
+ rerender();
+ expect(screen.getByText("number")).toBeInTheDocument();
+ });
+
+ it("should handle different color values", () => {
+ const { rerender } = render(
+ ,
+ );
+ expect(screen.getByText("string")).toBeInTheDocument();
+
+ rerender();
+ expect(screen.getByText("string")).toBeInTheDocument();
+ });
+
+ it("should handle missing resetFilters function gracefully", () => {
+ const propsWithoutCallback = {
+ ...defaultProps,
+ resetFilters: undefined as any,
+ };
+
+ expect(() => {
+ render();
+ }).not.toThrow();
+ });
+ });
+
+ describe("Edge Cases", () => {
+ it("should handle very long type names", () => {
+ const propsWithLongType = {
+ ...defaultProps,
+ type: "VeryLongTypeNameThatExceedsNormalLength",
+ };
+
+ render();
+
+ expect(
+ screen.getByText("VeryLongTypeNameThatExceedsNormalLength"),
+ ).toBeInTheDocument();
+ });
+
+ it("should handle multiple very long type names", () => {
+ const propsWithMultipleLongTypes = {
+ ...defaultProps,
+ type: "FirstVeryLongTypeName\nSecondVeryLongTypeName\nThirdVeryLongTypeName",
+ };
+
+ render();
+
+ expect(
+ screen.getByText(
+ "FirstVeryLongTypeName, SecondVeryLongTypeName, ThirdVeryLongTypeName",
+ ),
+ ).toBeInTheDocument();
+ });
+
+ it("should handle types with newlines and commas", () => {
+ const propsWithComplexTypes = {
+ ...defaultProps,
+ type: "List[str, int]\nDict[str, Any]",
+ };
+
+ render();
+
+ expect(
+ screen.getByText("List[str, int], Dict[str, Any]"),
+ ).toBeInTheDocument();
+ });
+
+ it("should handle empty color", () => {
+ const propsWithEmptyColor = { ...defaultProps, color: "" };
+ render();
+
+ // Verify component renders without errors with empty color
+ expect(screen.getByTestId("icon-ListFilter")).toBeInTheDocument();
+ });
+
+ it("should handle single newline character as type", () => {
+ const propsWithNewlineType = { ...defaultProps, type: "\n" };
+ render();
+
+ expect(screen.getByText("Inputs:")).toBeInTheDocument(); // Should be plural due to split
+ });
+ });
+
+ describe("Text Content", () => {
+ it("should display correct text for input with single type", () => {
+ render();
+
+ expect(screen.getByText("Input:")).toBeInTheDocument();
+ expect(screen.getByText("string")).toBeInTheDocument();
+ });
+
+ it("should display correct text for output with multiple types", () => {
+ const propsWithMultipleOutputTypes = {
+ ...defaultProps,
+ isInput: false,
+ type: "str\nint\nfloat",
+ };
+
+ render();
+
+ expect(screen.getByText("Outputs:")).toBeInTheDocument();
+ expect(screen.getByText("str, int, float")).toBeInTheDocument();
+ });
+
+ it("should join multiple types with commas and spaces", () => {
+ const propsWithMultipleTypes = {
+ ...defaultProps,
+ type: "type1\ntype2\ntype3\ntype4",
+ };
+
+ render();
+
+ expect(
+ screen.getByText("type1, type2, type3, type4"),
+ ).toBeInTheDocument();
+ });
+ });
+
+ describe("Accessibility", () => {
+ it("should have X icon with aria-hidden", () => {
+ render();
+
+ const xIcon = screen.getByTestId("icon-X");
+ // The aria-hidden is applied to the ForwardedIconComponent, check it's present
+ expect(xIcon).toBeInTheDocument();
+ });
+
+ it("should render reset button as clickable element", () => {
+ render();
+
+ const resetButton = screen.getByTestId("sidebar-filter-reset");
+ expect(resetButton.tagName).toBe("BUTTON");
+ });
+ });
+
+ describe("Callback Functions", () => {
+ it("should work with different resetFilters implementations", async () => {
+ const user = userEvent.setup();
+ const customResetFilters = jest.fn((data) => {
+ // Custom implementation that might accept parameters
+ console.log("Custom reset", data);
+ });
+
+ const propsWithCustomCallback = {
+ ...defaultProps,
+ resetFilters: customResetFilters,
+ };
+
+ render();
+
+ const resetButton = screen.getByTestId("sidebar-filter-reset");
+ await user.click(resetButton);
+
+ expect(customResetFilters).toHaveBeenCalledTimes(1);
+ });
+ });
+});
diff --git a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/__tests__/sidebarFooterButtons.test.tsx b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/__tests__/sidebarFooterButtons.test.tsx
new file mode 100644
index 000000000..3d946873b
--- /dev/null
+++ b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/__tests__/sidebarFooterButtons.test.tsx
@@ -0,0 +1,574 @@
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import React from "react";
+import SidebarMenuButtons from "../sidebarFooterButtons";
+
+// Mock the UI components
+jest.mock("@/components/common/genericIconComponent", () => ({
+ __esModule: true,
+ default: ({ name, className }: any) => (
+
+ {name}
+
+ ),
+}));
+
+jest.mock("@/components/ui/button", () => ({
+ Button: ({
+ children,
+ onClick,
+ className,
+ disabled,
+ unstyled,
+ ...props
+ }: any) => (
+
+ ),
+}));
+
+jest.mock("@/components/ui/sidebar", () => ({
+ SidebarMenuButton: ({ children, asChild }: any) => (
+
+ {children}
+
+ ),
+}));
+
+jest.mock("@/customization/components/custom-link", () => ({
+ CustomLink: ({ children, to, target, rel, className }: any) => (
+
+ {children}
+
+ ),
+}));
+
+// Mock feature flag
+jest.mock("@/customization/feature-flags", () => ({
+ ENABLE_LANGFLOW_STORE: true,
+}));
+
+describe("SidebarMenuButtons", () => {
+ const mockAddComponent = jest.fn();
+ const mockCustomComponent = {
+ description: "Custom test component",
+ template: {},
+ display_name: "Custom Component",
+ documentation: "Custom docs",
+ };
+
+ const defaultProps = {
+ hasStore: false,
+ customComponent: mockCustomComponent,
+ addComponent: mockAddComponent,
+ isLoading: false,
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe("Basic Rendering", () => {
+ it("should render custom component button", () => {
+ render();
+
+ expect(
+ screen.getByTestId("sidebar-custom-component-button"),
+ ).toBeInTheDocument();
+ expect(screen.getByText("New Custom Component")).toBeInTheDocument();
+ expect(screen.getByTestId("icon-Plus")).toBeInTheDocument();
+ });
+
+ it("should render sidebar menu buttons", () => {
+ render();
+
+ expect(screen.getByTestId("sidebar-menu-button")).toBeInTheDocument();
+ });
+
+ it("should display correct text for custom component button", () => {
+ render();
+
+ expect(screen.getByText("New Custom Component")).toBeInTheDocument();
+ });
+
+ it("should display Plus icon", () => {
+ render();
+
+ expect(screen.getByTestId("icon-Plus")).toBeInTheDocument();
+ });
+ });
+
+ describe("Store Link Rendering", () => {
+ it("should not render store link when hasStore is false", () => {
+ render();
+
+ expect(screen.queryByTestId("custom-link")).not.toBeInTheDocument();
+ expect(
+ screen.queryByText("Discover more components"),
+ ).not.toBeInTheDocument();
+ });
+
+ it("should render store link when hasStore is true", () => {
+ const propsWithStore = { ...defaultProps, hasStore: true };
+ render();
+
+ expect(screen.getByTestId("custom-link")).toBeInTheDocument();
+ expect(screen.getByText("Discover more components")).toBeInTheDocument();
+ expect(screen.getByTestId("icon-Store")).toBeInTheDocument();
+ expect(
+ screen.getByTestId("icon-SquareArrowOutUpRight"),
+ ).toBeInTheDocument();
+ });
+
+ it("should render store link with correct attributes", () => {
+ const propsWithStore = { ...defaultProps, hasStore: true };
+ render();
+
+ const storeLink = screen.getByTestId("custom-link");
+ expect(storeLink).toHaveAttribute("href", "/store");
+ expect(storeLink).toHaveAttribute("target", "_blank");
+ expect(storeLink).toHaveAttribute("rel", "noopener noreferrer");
+ });
+
+ it("should render store link inside sidebar menu button", () => {
+ const propsWithStore = { ...defaultProps, hasStore: true };
+ render();
+
+ const sidebarMenuButtons = screen.getAllByTestId("sidebar-menu-button");
+ expect(sidebarMenuButtons).toHaveLength(2); // Store link + custom component button
+ expect(sidebarMenuButtons[0]).toContainElement(
+ screen.getByTestId("custom-link"),
+ );
+ });
+
+ it("should render store icons correctly", () => {
+ const propsWithStore = { ...defaultProps, hasStore: true };
+ render();
+
+ expect(screen.getByTestId("icon-Store")).toBeInTheDocument();
+ expect(
+ screen.getByTestId("icon-SquareArrowOutUpRight"),
+ ).toBeInTheDocument();
+ });
+ });
+
+ describe("Custom Component Button Functionality", () => {
+ it("should call addComponent when custom component button is clicked", async () => {
+ const user = userEvent.setup();
+ render();
+
+ const customButton = screen.getByTestId(
+ "sidebar-custom-component-button",
+ );
+ await user.click(customButton);
+
+ expect(mockAddComponent).toHaveBeenCalledWith(
+ mockCustomComponent,
+ "CustomComponent",
+ );
+ expect(mockAddComponent).toHaveBeenCalledTimes(1);
+ });
+
+ it("should not call addComponent when customComponent is undefined", async () => {
+ const user = userEvent.setup();
+ const propsWithoutCustomComponent = {
+ ...defaultProps,
+ customComponent: undefined,
+ };
+
+ render();
+
+ const customButton = screen.getByTestId(
+ "sidebar-custom-component-button",
+ );
+ await user.click(customButton);
+
+ expect(mockAddComponent).not.toHaveBeenCalled();
+ });
+
+ it("should not call addComponent when customComponent is null", async () => {
+ const user = userEvent.setup();
+ const propsWithNullCustomComponent = {
+ ...defaultProps,
+ customComponent: null,
+ };
+
+ render();
+
+ const customButton = screen.getByTestId(
+ "sidebar-custom-component-button",
+ );
+ await user.click(customButton);
+
+ expect(mockAddComponent).not.toHaveBeenCalled();
+ });
+
+ it("should handle multiple clicks correctly", async () => {
+ const user = userEvent.setup();
+ render();
+
+ const customButton = screen.getByTestId(
+ "sidebar-custom-component-button",
+ );
+ await user.click(customButton);
+ await user.click(customButton);
+ await user.click(customButton);
+
+ expect(mockAddComponent).toHaveBeenCalledTimes(3);
+ expect(mockAddComponent).toHaveBeenCalledWith(
+ mockCustomComponent,
+ "CustomComponent",
+ );
+ });
+ });
+
+ describe("Loading State", () => {
+ it("should disable custom component button when loading", () => {
+ const propsWithLoading = { ...defaultProps, isLoading: true };
+ render();
+
+ const customButton = screen.getByTestId(
+ "sidebar-custom-component-button",
+ );
+ expect(customButton).toBeDisabled();
+ });
+
+ it("should not disable button when not loading", () => {
+ render();
+
+ const customButton = screen.getByTestId(
+ "sidebar-custom-component-button",
+ );
+ expect(customButton).not.toBeDisabled();
+ });
+
+ it("should not call addComponent when button is disabled and clicked", async () => {
+ const user = userEvent.setup();
+ const propsWithLoading = { ...defaultProps, isLoading: true };
+ render();
+
+ const customButton = screen.getByTestId(
+ "sidebar-custom-component-button",
+ );
+ await user.click(customButton);
+
+ expect(mockAddComponent).not.toHaveBeenCalled();
+ });
+ });
+
+ describe("Component Structure", () => {
+ it("should have correct DOM hierarchy without store", () => {
+ render();
+
+ const sidebarMenuButtons = screen.getAllByTestId("sidebar-menu-button");
+ expect(sidebarMenuButtons).toHaveLength(1);
+ expect(sidebarMenuButtons[0]).toContainElement(
+ screen.getByTestId("sidebar-custom-component-button"),
+ );
+ });
+
+ it("should have correct DOM hierarchy with store", () => {
+ const propsWithStore = { ...defaultProps, hasStore: true };
+ render();
+
+ const sidebarMenuButtons = screen.getAllByTestId("sidebar-menu-button");
+ expect(sidebarMenuButtons).toHaveLength(2);
+ expect(sidebarMenuButtons[0]).toContainElement(
+ screen.getByTestId("custom-link"),
+ );
+ expect(sidebarMenuButtons[1]).toContainElement(
+ screen.getByTestId("sidebar-custom-component-button"),
+ );
+ });
+
+ it("should render fragments correctly", () => {
+ const { container } = render();
+
+ // Component should render without wrapper elements (using React fragment)
+ expect(container.children).toHaveLength(1);
+ });
+ });
+
+ describe("CSS Classes", () => {
+ it("should apply correct classes to custom component button", () => {
+ render();
+
+ const customButton = screen.getByTestId(
+ "sidebar-custom-component-button",
+ );
+ expect(customButton).toHaveClass("flex", "items-center", "gap-2");
+ expect(customButton).toHaveAttribute("data-unstyled", "true");
+ });
+
+ it("should apply correct classes to store link", () => {
+ const propsWithStore = { ...defaultProps, hasStore: true };
+ render();
+
+ const storeLink = screen.getByTestId("custom-link");
+ expect(storeLink).toHaveClass("group/discover");
+ });
+
+ it("should apply correct classes to icons", () => {
+ const propsWithStore = { ...defaultProps, hasStore: true };
+ render();
+
+ const storeIcon = screen.getByTestId("icon-Store");
+ const arrowIcon = screen.getByTestId("icon-SquareArrowOutUpRight");
+ const plusIcon = screen.getByTestId("icon-Plus");
+
+ expect(storeIcon).toHaveClass("h-4", "w-4", "text-muted-foreground");
+ expect(arrowIcon).toHaveClass(
+ "h-4",
+ "w-4",
+ "opacity-0",
+ "transition-all",
+ "group-hover/discover:opacity-100",
+ );
+ expect(plusIcon).toHaveClass("h-4", "w-4", "text-muted-foreground");
+ });
+ });
+
+ describe("Props Handling", () => {
+ it("should handle default props correctly", () => {
+ const minimalProps = {
+ addComponent: mockAddComponent,
+ };
+
+ render();
+
+ expect(
+ screen.getByTestId("sidebar-custom-component-button"),
+ ).not.toBeDisabled();
+ expect(screen.queryByTestId("custom-link")).not.toBeInTheDocument();
+ });
+
+ it("should handle all props provided", () => {
+ const fullProps = {
+ hasStore: true,
+ customComponent: mockCustomComponent,
+ addComponent: mockAddComponent,
+ isLoading: true,
+ };
+
+ render();
+
+ expect(screen.getByTestId("custom-link")).toBeInTheDocument();
+ expect(
+ screen.getByTestId("sidebar-custom-component-button"),
+ ).toBeDisabled();
+ });
+
+ it("should work with different customComponent objects", async () => {
+ const user = userEvent.setup();
+ const differentCustomComponent = {
+ description: "Different component",
+ template: { test: true },
+ display_name: "Different Component",
+ documentation: "Different docs",
+ };
+
+ const propsWithDifferentComponent = {
+ ...defaultProps,
+ customComponent: differentCustomComponent,
+ };
+
+ render();
+
+ const customButton = screen.getByTestId(
+ "sidebar-custom-component-button",
+ );
+ await user.click(customButton);
+
+ expect(mockAddComponent).toHaveBeenCalledWith(
+ differentCustomComponent,
+ "CustomComponent",
+ );
+ });
+
+ it("should work with different addComponent functions", async () => {
+ const user = userEvent.setup();
+ const alternativeAddComponent = jest.fn();
+ const propsWithDifferentAddComponent = {
+ ...defaultProps,
+ addComponent: alternativeAddComponent,
+ };
+
+ render();
+
+ const customButton = screen.getByTestId(
+ "sidebar-custom-component-button",
+ );
+ await user.click(customButton);
+
+ expect(alternativeAddComponent).toHaveBeenCalledWith(
+ mockCustomComponent,
+ "CustomComponent",
+ );
+ expect(mockAddComponent).not.toHaveBeenCalled();
+ });
+ });
+
+ describe("Edge Cases", () => {
+ it("should handle missing addComponent function gracefully", () => {
+ const propsWithoutAddComponent = {
+ ...defaultProps,
+ addComponent: undefined,
+ };
+
+ expect(() => {
+ render();
+ }).not.toThrow();
+ });
+
+ it("should handle boolean hasStore values", () => {
+ const { rerender } = render(
+ ,
+ );
+ expect(screen.queryByTestId("custom-link")).not.toBeInTheDocument();
+
+ rerender();
+ expect(screen.getByTestId("custom-link")).toBeInTheDocument();
+ });
+
+ it("should handle boolean isLoading values", () => {
+ const { rerender } = render(
+ ,
+ );
+ expect(
+ screen.getByTestId("sidebar-custom-component-button"),
+ ).not.toBeDisabled();
+
+ rerender();
+ expect(
+ screen.getByTestId("sidebar-custom-component-button"),
+ ).toBeDisabled();
+ });
+
+ it("should handle rapid prop changes", () => {
+ const { rerender } = render();
+
+ expect(screen.queryByTestId("custom-link")).not.toBeInTheDocument();
+ expect(
+ screen.getByTestId("sidebar-custom-component-button"),
+ ).not.toBeDisabled();
+
+ rerender(
+ ,
+ );
+
+ expect(screen.getByTestId("custom-link")).toBeInTheDocument();
+ expect(
+ screen.getByTestId("sidebar-custom-component-button"),
+ ).toBeDisabled();
+ });
+ });
+
+ describe("Text Content", () => {
+ it("should display correct text content", () => {
+ const propsWithStore = { ...defaultProps, hasStore: true };
+ render();
+
+ expect(screen.getByText("Discover more components")).toBeInTheDocument();
+ expect(screen.getByText("New Custom Component")).toBeInTheDocument();
+ });
+
+ it("should have spans with correct classes", () => {
+ const propsWithStore = { ...defaultProps, hasStore: true };
+ render();
+
+ const storeSpan = screen.getByText("Discover more components");
+ const customSpan = screen.getByText("New Custom Component");
+
+ expect(storeSpan).toHaveClass(
+ "flex-1",
+ "group-data-[state=open]/collapsible:font-semibold",
+ );
+ expect(customSpan).toHaveClass(
+ "group-data-[state=open]/collapsible:font-semibold",
+ );
+ });
+ });
+
+ describe("SidebarMenuButton Integration", () => {
+ it("should render SidebarMenuButton with asChild prop", () => {
+ const propsWithStore = { ...defaultProps, hasStore: true };
+ render();
+
+ const sidebarMenuButtons = screen.getAllByTestId("sidebar-menu-button");
+ sidebarMenuButtons.forEach((button) => {
+ expect(button).toHaveAttribute("data-as-child", "true");
+ });
+ });
+
+ it("should wrap both store link and custom button in SidebarMenuButton", () => {
+ const propsWithStore = { ...defaultProps, hasStore: true };
+ render();
+
+ const sidebarMenuButtons = screen.getAllByTestId("sidebar-menu-button");
+ expect(sidebarMenuButtons).toHaveLength(2);
+ });
+ });
+
+ describe("Callback Behavior", () => {
+ it("should call addComponent with exact arguments", async () => {
+ const user = userEvent.setup();
+ render();
+
+ const customButton = screen.getByTestId(
+ "sidebar-custom-component-button",
+ );
+ await user.click(customButton);
+
+ expect(mockAddComponent).toHaveBeenCalledWith(
+ mockCustomComponent,
+ "CustomComponent",
+ );
+ expect(mockAddComponent).toHaveBeenCalledTimes(1);
+
+ // Verify the exact call arguments
+ const [firstArg, secondArg] = mockAddComponent.mock.calls[0];
+ expect(firstArg).toBe(mockCustomComponent);
+ expect(secondArg).toBe("CustomComponent");
+ });
+
+ it("should handle addComponent throwing errors", async () => {
+ const user = userEvent.setup();
+ const throwingAddComponent = jest.fn(() => {
+ throw new Error("Test error");
+ });
+
+ const propsWithThrowingFunction = {
+ ...defaultProps,
+ addComponent: throwingAddComponent,
+ };
+
+ render();
+
+ const customButton = screen.getByTestId(
+ "sidebar-custom-component-button",
+ );
+
+ // Should not crash the component
+ expect(async () => {
+ await user.click(customButton);
+ }).not.toThrow();
+ });
+ });
+});
diff --git a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/__tests__/sidebarHeader.test.tsx b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/__tests__/sidebarHeader.test.tsx
new file mode 100644
index 000000000..296558f66
--- /dev/null
+++ b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/__tests__/sidebarHeader.test.tsx
@@ -0,0 +1,688 @@
+import { render, screen } from "@testing-library/react";
+import mockAPIData from "@/utils/testUtils/mockData/mockAPIData";
+import { SidebarHeaderComponentProps } from "../../types";
+import { SidebarHeaderComponent } from "../sidebarHeader";
+
+// Mock the UI components
+jest.mock("@/components/common/genericIconComponent", () => ({
+ ForwardedIconComponent: ({ name, className }: any) => (
+
+ {name}
+
+ ),
+}));
+
+jest.mock("@/components/common/shadTooltipComponent", () => ({
+ __esModule: true,
+ default: ({ children, content, styleClasses }: any) => (
+
+ {children}
+
+ ),
+}));
+
+jest.mock("@/components/ui/button", () => ({
+ Button: ({ children, onClick, variant, size, className, ...props }: any) => (
+
+ ),
+}));
+
+jest.mock("@/components/ui/disclosure", () => ({
+ Disclosure: ({ children, open, onOpenChange }: any) => (
+
+ {children}
+
+ ),
+ DisclosureContent: ({ children }: any) => (
+ {children}
+ ),
+ DisclosureTrigger: ({ children }: any) => (
+ {children}
+ ),
+}));
+
+jest.mock("@/components/ui/sidebar", () => ({
+ SidebarHeader: ({ children, className }: any) => (
+
+ {children}
+
+ ),
+ SidebarTrigger: ({ children, className }: any) => (
+
+ {children}
+
+ ),
+}));
+
+jest.mock("../featureTogglesComponent", () => ({
+ __esModule: true,
+ default: ({ showBeta, setShowBeta, showLegacy, setShowLegacy }: any) => (
+
+ Feature Toggles
+
+ ),
+}));
+
+jest.mock("../searchInput", () => ({
+ SearchInput: ({
+ searchInputRef,
+ isInputFocused,
+ search,
+ handleInputFocus,
+ handleInputBlur,
+ handleInputChange,
+ }: any) => (
+
+ Search Input
+
+ ),
+}));
+
+jest.mock("../sidebarFilterComponent", () => ({
+ SidebarFilterComponent: ({ isInput, type, color, resetFilters }: any) => (
+
+ Filter Component
+
+ ),
+}));
+
+describe("SidebarHeaderComponent", () => {
+ const mockSetShowConfig = jest.fn();
+ const mockSetShowBeta = jest.fn();
+ const mockSetShowLegacy = jest.fn();
+ const mockHandleInputFocus = jest.fn();
+ const mockHandleInputBlur = jest.fn();
+ const mockHandleInputChange = jest.fn();
+ const mockSetFilterEdge = jest.fn();
+ const mockSetFilterData = jest.fn();
+ const mockSearchInputRef = { current: null };
+
+ const defaultProps: SidebarHeaderComponentProps = {
+ showConfig: false,
+ setShowConfig: mockSetShowConfig,
+ showBeta: false,
+ setShowBeta: mockSetShowBeta,
+ showLegacy: false,
+ setShowLegacy: mockSetShowLegacy,
+ searchInputRef: mockSearchInputRef,
+ isInputFocused: false,
+ search: "",
+ handleInputFocus: mockHandleInputFocus,
+ handleInputBlur: mockHandleInputBlur,
+ handleInputChange: mockHandleInputChange,
+ filterType: undefined,
+ setFilterEdge: mockSetFilterEdge,
+ setFilterData: mockSetFilterData,
+ data: {},
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe("Basic Rendering", () => {
+ it("should render sidebar header with correct structure", () => {
+ render();
+
+ expect(screen.getByTestId("sidebar-header")).toBeInTheDocument();
+ expect(screen.getByTestId("disclosure")).toBeInTheDocument();
+ expect(screen.getByTestId("sidebar-trigger")).toBeInTheDocument();
+ expect(screen.getByText("Components")).toBeInTheDocument();
+ expect(screen.getByTestId("search-input")).toBeInTheDocument();
+ });
+
+ it("should display correct title", () => {
+ render();
+
+ expect(screen.getByText("Components")).toBeInTheDocument();
+ });
+
+ it("should render sidebar trigger with icon", () => {
+ render();
+
+ expect(screen.getByTestId("sidebar-trigger")).toBeInTheDocument();
+ expect(
+ screen.getByTestId("forwarded-icon-PanelLeftClose"),
+ ).toBeInTheDocument();
+ });
+
+ it("should render settings button", () => {
+ render();
+
+ expect(screen.getByTestId("sidebar-options-trigger")).toBeInTheDocument();
+ expect(
+ screen.getByTestId("forwarded-icon-SlidersHorizontal"),
+ ).toBeInTheDocument();
+ });
+
+ it("should render tooltip with correct content", () => {
+ render();
+
+ expect(screen.getByTestId("tooltip")).toHaveAttribute(
+ "data-content",
+ "Component settings",
+ );
+ expect(screen.getByTestId("tooltip")).toHaveClass("z-50");
+ });
+ });
+
+ describe("Disclosure Functionality", () => {
+ it("should render disclosure with correct open state", () => {
+ render();
+
+ expect(screen.getByTestId("disclosure")).toHaveAttribute(
+ "data-open",
+ "false",
+ );
+ });
+
+ it("should render disclosure as open when showConfig is true", () => {
+ const propsWithOpenConfig = { ...defaultProps, showConfig: true };
+ render();
+
+ expect(screen.getByTestId("disclosure")).toHaveAttribute(
+ "data-open",
+ "true",
+ );
+ });
+
+ it("should pass setShowConfig to disclosure", () => {
+ render();
+
+ expect(screen.getByTestId("disclosure")).toHaveAttribute(
+ "data-on-open-change",
+ mockSetShowConfig.toString(),
+ );
+ });
+
+ it("should render disclosure content", () => {
+ render();
+
+ expect(screen.getByTestId("disclosure-content")).toBeInTheDocument();
+ });
+
+ it("should render disclosure trigger", () => {
+ render();
+
+ expect(screen.getByTestId("disclosure-trigger")).toBeInTheDocument();
+ });
+ });
+
+ describe("Settings Button Variants", () => {
+ it("should show ghost variant when config is closed", () => {
+ render();
+
+ const settingsButton = screen.getByTestId("sidebar-options-trigger");
+ expect(settingsButton).toHaveAttribute("data-variant", "ghost");
+ });
+
+ it("should show ghostActive variant when config is open", () => {
+ const propsWithOpenConfig = { ...defaultProps, showConfig: true };
+ render();
+
+ const settingsButton = screen.getByTestId("sidebar-options-trigger");
+ expect(settingsButton).toHaveAttribute("data-variant", "ghostActive");
+ });
+
+ it("should have correct size", () => {
+ render();
+
+ const settingsButton = screen.getByTestId("sidebar-options-trigger");
+ expect(settingsButton).toHaveAttribute("data-size", "iconMd");
+ });
+ });
+
+ describe("Feature Toggles Integration", () => {
+ it("should render feature toggles with correct props", () => {
+ render();
+
+ const featureToggles = screen.getByTestId("feature-toggles");
+ expect(featureToggles).toHaveAttribute("data-show-beta", "false");
+ expect(featureToggles).toHaveAttribute("data-show-legacy", "false");
+ expect(featureToggles).toHaveAttribute(
+ "data-set-show-beta",
+ mockSetShowBeta.toString(),
+ );
+ expect(featureToggles).toHaveAttribute(
+ "data-set-show-legacy",
+ mockSetShowLegacy.toString(),
+ );
+ });
+
+ it("should pass different beta and legacy values", () => {
+ const propsWithToggles = {
+ ...defaultProps,
+ showBeta: true,
+ showLegacy: true,
+ };
+
+ render();
+
+ const featureToggles = screen.getByTestId("feature-toggles");
+ expect(featureToggles).toHaveAttribute("data-show-beta", "true");
+ expect(featureToggles).toHaveAttribute("data-show-legacy", "true");
+ });
+ });
+
+ describe("Search Input Integration", () => {
+ it("should render search input with correct props", () => {
+ const propsWithSearch = {
+ ...defaultProps,
+ isInputFocused: true,
+ search: "test search",
+ };
+
+ render();
+
+ const searchInput = screen.getByTestId("search-input");
+ expect(searchInput).toHaveAttribute("data-is-focused", "true");
+ expect(searchInput).toHaveAttribute("data-search", "test search");
+ expect(searchInput).toHaveAttribute(
+ "data-handle-focus",
+ mockHandleInputFocus.toString(),
+ );
+ expect(searchInput).toHaveAttribute(
+ "data-handle-blur",
+ mockHandleInputBlur.toString(),
+ );
+ expect(searchInput).toHaveAttribute(
+ "data-handle-change",
+ mockHandleInputChange.toString(),
+ );
+ });
+
+ it("should pass search input ref", () => {
+ render();
+
+ expect(screen.getByTestId("search-input")).toBeInTheDocument();
+ });
+ });
+
+ describe("Filter Component Conditional Rendering", () => {
+ it("should not render filter component when filterType is null", () => {
+ render();
+
+ expect(screen.queryByTestId("sidebar-filter")).not.toBeInTheDocument();
+ });
+
+ it("should render filter component when filterType is provided", () => {
+ const propsWithFilter = {
+ ...defaultProps,
+ filterType: {
+ source: "test_source",
+ sourceHandle: undefined,
+ target: undefined,
+ targetHandle: undefined,
+ type: "string",
+ color: "blue",
+ },
+ };
+
+ render();
+
+ expect(screen.getByTestId("sidebar-filter")).toBeInTheDocument();
+ });
+
+ it("should pass correct props to filter component for input", () => {
+ const propsWithInputFilter = {
+ ...defaultProps,
+ filterType: {
+ source: "test_source",
+ sourceHandle: undefined,
+ target: undefined,
+ targetHandle: undefined,
+ type: "string",
+ color: "blue",
+ },
+ };
+
+ render();
+
+ const filterComponent = screen.getByTestId("sidebar-filter");
+ expect(filterComponent).toHaveAttribute("data-is-input", "true");
+ expect(filterComponent).toHaveAttribute("data-type", "string");
+ expect(filterComponent).toHaveAttribute("data-color", "blue");
+ });
+
+ it("should pass correct props to filter component for output", () => {
+ const propsWithOutputFilter = {
+ ...defaultProps,
+ filterType: {
+ source: undefined,
+ sourceHandle: undefined,
+ target: "test_target",
+ targetHandle: undefined,
+ type: "number",
+ color: "red",
+ },
+ };
+
+ render();
+
+ const filterComponent = screen.getByTestId("sidebar-filter");
+ expect(filterComponent).toHaveAttribute("data-is-input", "false");
+ expect(filterComponent).toHaveAttribute("data-type", "number");
+ expect(filterComponent).toHaveAttribute("data-color", "red");
+ });
+
+ it("should handle filter reset correctly", async () => {
+ const propsWithFilter = {
+ ...defaultProps,
+ filterType: {
+ source: "test_source",
+ sourceHandle: undefined,
+ target: undefined,
+ targetHandle: undefined,
+ type: "string",
+ color: "blue",
+ },
+ data: mockAPIData,
+ };
+
+ render();
+
+ // Since resetFilters is passed as a function, we can't directly test it
+ // but we can verify the filter component receives the function
+ const filterComponent = screen.getByTestId("sidebar-filter");
+ expect(filterComponent).toHaveAttribute("data-reset-filters");
+ });
+ });
+
+ describe("Component Structure", () => {
+ it("should have correct DOM hierarchy", () => {
+ render();
+
+ const sidebarHeader = screen.getByTestId("sidebar-header");
+ const disclosure = screen.getByTestId("disclosure");
+ const searchInput = screen.getByTestId("search-input");
+
+ expect(sidebarHeader).toContainElement(disclosure);
+ expect(sidebarHeader).toContainElement(searchInput);
+ });
+
+ it("should apply correct CSS classes", () => {
+ render();
+
+ const sidebarHeader = screen.getByTestId("sidebar-header");
+ expect(sidebarHeader).toHaveClass(
+ "flex",
+ "w-full",
+ "flex-col",
+ "gap-4",
+ "p-4",
+ "pb-1",
+ );
+ });
+
+ it("should contain all expected child elements", () => {
+ render();
+
+ expect(screen.getByTestId("sidebar-trigger")).toBeInTheDocument();
+ expect(screen.getByText("Components")).toBeInTheDocument();
+ expect(screen.getByTestId("sidebar-options-trigger")).toBeInTheDocument();
+ expect(screen.getByTestId("feature-toggles")).toBeInTheDocument();
+ expect(screen.getByTestId("search-input")).toBeInTheDocument();
+ });
+ });
+
+ describe("CSS Classes", () => {
+ it("should apply correct classes to sidebar trigger", () => {
+ render();
+
+ const sidebarTrigger = screen.getByTestId("sidebar-trigger");
+ expect(sidebarTrigger).toHaveClass("text-muted-foreground");
+ });
+
+ it("should apply correct classes to title", () => {
+ render();
+
+ const title = screen.getByText("Components");
+ expect(title).toHaveClass(
+ "flex-1",
+ "cursor-default",
+ "text-sm",
+ "font-semibold",
+ );
+ });
+
+ it("should apply correct classes to settings icon", () => {
+ render();
+
+ const settingsIcon = screen.getByTestId(
+ "forwarded-icon-SlidersHorizontal",
+ );
+ expect(settingsIcon).toHaveClass("h-4", "w-4");
+ });
+ });
+
+ describe("Props Handling", () => {
+ it("should handle different showConfig values", () => {
+ const { rerender } = render(
+ ,
+ );
+ expect(screen.getByTestId("disclosure")).toHaveAttribute(
+ "data-open",
+ "false",
+ );
+ expect(screen.getByTestId("sidebar-options-trigger")).toHaveAttribute(
+ "data-variant",
+ "ghost",
+ );
+
+ rerender();
+ expect(screen.getByTestId("disclosure")).toHaveAttribute(
+ "data-open",
+ "true",
+ );
+ expect(screen.getByTestId("sidebar-options-trigger")).toHaveAttribute(
+ "data-variant",
+ "ghostActive",
+ );
+ });
+
+ it("should handle different search values", () => {
+ const { rerender } = render(
+ ,
+ );
+ let searchInput = screen.getByTestId("search-input");
+ expect(searchInput).toHaveAttribute("data-search", "");
+ expect(searchInput).toHaveAttribute("data-is-focused", "false");
+
+ rerender(
+ ,
+ );
+ searchInput = screen.getByTestId("search-input");
+ expect(searchInput).toHaveAttribute("data-search", "test");
+ expect(searchInput).toHaveAttribute("data-is-focused", "true");
+ });
+
+ it("should handle different callback functions", () => {
+ const alternativeSetShowConfig = jest.fn();
+ const alternativeHandleInputChange = jest.fn();
+
+ render(
+ ,
+ );
+
+ expect(screen.getByTestId("disclosure")).toHaveAttribute(
+ "data-on-open-change",
+ alternativeSetShowConfig.toString(),
+ );
+ expect(screen.getByTestId("search-input")).toHaveAttribute(
+ "data-handle-change",
+ alternativeHandleInputChange.toString(),
+ );
+ });
+ });
+
+ describe("Edge Cases", () => {
+ it("should handle missing callback functions gracefully", () => {
+ const propsWithoutCallbacks = {
+ ...defaultProps,
+ setShowConfig: undefined as any,
+ handleInputFocus: undefined as any,
+ handleInputBlur: undefined as any,
+ handleInputChange: undefined as any,
+ };
+
+ expect(() => {
+ render();
+ }).not.toThrow();
+ });
+
+ it("should handle null filterType gracefully", () => {
+ render(
+ ,
+ );
+
+ expect(screen.queryByTestId("sidebar-filter")).not.toBeInTheDocument();
+ });
+
+ it("should handle undefined filterType gracefully", () => {
+ const propsWithUndefinedFilter = {
+ ...defaultProps,
+ filterType: undefined as any,
+ };
+
+ render();
+
+ expect(screen.queryByTestId("sidebar-filter")).not.toBeInTheDocument();
+ });
+
+ it("should handle complex filterType objects", () => {
+ const complexFilterType = {
+ source: "test_source",
+ sourceHandle: undefined,
+ target: undefined,
+ targetHandle: undefined,
+ type: "List[str, int]",
+ color: "custom-color",
+ additionalProp: "ignored",
+ };
+
+ const propsWithComplexFilter = {
+ ...defaultProps,
+ filterType: complexFilterType,
+ };
+
+ render();
+
+ const filterComponent = screen.getByTestId("sidebar-filter");
+ expect(filterComponent).toHaveAttribute("data-type", "List[str, int]");
+ expect(filterComponent).toHaveAttribute("data-color", "custom-color");
+ });
+ });
+
+ describe("Memo Functionality", () => {
+ it("should render component name correctly", () => {
+ render();
+
+ // Component should render without issues
+ expect(screen.getByTestId("sidebar-header")).toBeInTheDocument();
+ });
+
+ it("should handle prop changes correctly", () => {
+ const { rerender } = render();
+
+ expect(screen.getByTestId("disclosure")).toHaveAttribute(
+ "data-open",
+ "false",
+ );
+
+ rerender();
+
+ expect(screen.getByTestId("disclosure")).toHaveAttribute(
+ "data-open",
+ "true",
+ );
+ });
+ });
+
+ describe("Accessibility", () => {
+ it("should have proper heading structure", () => {
+ render();
+
+ const title = screen.getByText("Components");
+ expect(title.tagName).toBe("H3");
+ });
+
+ it("should render tooltip for settings button", () => {
+ render();
+
+ expect(screen.getByTestId("tooltip")).toHaveAttribute(
+ "data-content",
+ "Component settings",
+ );
+ });
+ });
+
+ describe("Integration", () => {
+ it("should integrate all child components correctly", () => {
+ const propsWithFilter = {
+ ...defaultProps,
+ showConfig: true,
+ showBeta: true,
+ showLegacy: true,
+ isInputFocused: true,
+ search: "test search",
+ filterType: {
+ source: "test_source",
+ sourceHandle: undefined,
+ target: undefined,
+ targetHandle: undefined,
+ type: "string",
+ color: "blue",
+ },
+ };
+
+ render();
+
+ expect(screen.getByTestId("feature-toggles")).toBeInTheDocument();
+ expect(screen.getByTestId("search-input")).toBeInTheDocument();
+ expect(screen.getByTestId("sidebar-filter")).toBeInTheDocument();
+ });
+ });
+});
diff --git a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/__tests__/sidebarItemsList.test.tsx b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/__tests__/sidebarItemsList.test.tsx
new file mode 100644
index 000000000..cb8921365
--- /dev/null
+++ b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/__tests__/sidebarItemsList.test.tsx
@@ -0,0 +1,625 @@
+import { render, screen } from "@testing-library/react";
+import React from "react";
+import SidebarItemsList from "../sidebarItemsList";
+
+// Mock external dependencies
+jest.mock("@/components/common/shadTooltipComponent", () => ({
+ __esModule: true,
+ default: ({ children, content, side }: any) => (
+
+ {children}
+
+ ),
+}));
+
+jest.mock("@/stores/flowStore", () => ({
+ __esModule: true,
+ default: jest.fn((selector) =>
+ selector({
+ nodes: [
+ { id: "node1", type: "ChatInput" },
+ { id: "node2", type: "Text" },
+ ],
+ }),
+ ),
+}));
+
+jest.mock("@/utils/reactflowUtils", () => ({
+ checkChatInput: jest.fn((nodes) =>
+ nodes.some((node: any) => node.type === "ChatInput"),
+ ),
+ checkWebhookInput: jest.fn((nodes) =>
+ nodes.some((node: any) => node.type === "Webhook"),
+ ),
+}));
+
+jest.mock("@/utils/utils", () => ({
+ removeCountFromString: jest.fn((str) => str.replace(/\d+$/, "")),
+}));
+
+jest.mock("../../helpers/disable-item", () => ({
+ disableItem: jest.fn((itemName, uniqueInputs) => {
+ if (itemName === "ChatInput" && uniqueInputs.chatInput) return true;
+ if (itemName === "Webhook" && uniqueInputs.webhookInput) return true;
+ return false;
+ }),
+}));
+
+jest.mock("../../helpers/get-disabled-tooltip", () => ({
+ getDisabledTooltip: jest.fn((itemName, uniqueInputs) => {
+ if (itemName === "ChatInput" && uniqueInputs.chatInput)
+ return "Chat Input already added";
+ if (itemName === "Webhook" && uniqueInputs.webhookInput)
+ return "Webhook already added";
+ return "";
+ }),
+}));
+
+jest.mock("../sidebarDraggableComponent", () => ({
+ __esModule: true,
+ default: ({
+ sectionName,
+ apiClass,
+ icon,
+ onDragStart,
+ color,
+ itemName,
+ error,
+ display_name,
+ official,
+ beta,
+ legacy,
+ disabled,
+ disabledTooltip,
+ }: any) => (
+
+ onDragStart?.({ type: "dragstart" }, { type: itemName, node: apiClass })
+ }
+ >
+ {display_name}
+
+ ),
+}));
+
+describe("SidebarItemsList", () => {
+ const mockOnDragStart = jest.fn();
+ const mockSensitiveSort = jest.fn((a, b) => a.localeCompare(b));
+
+ const defaultItem = {
+ name: "TestCategory",
+ icon: "TestIcon",
+ };
+
+ const defaultDataFilter = {
+ TestCategory: {
+ Component1: {
+ display_name: "Component 1",
+ icon: "Component1Icon",
+ error: false,
+ official: true,
+ beta: false,
+ legacy: false,
+ priority: 1,
+ score: 10,
+ },
+ Component2: {
+ display_name: "Component 2",
+ icon: "Component2Icon",
+ error: false,
+ official: false,
+ beta: true,
+ legacy: false,
+ priority: 2,
+ score: 20,
+ },
+ ChatInput: {
+ display_name: "Chat Input",
+ icon: "ChatIcon",
+ error: false,
+ official: true,
+ beta: false,
+ legacy: false,
+ priority: 0,
+ score: 5,
+ },
+ Webhook: {
+ display_name: "Webhook",
+ icon: "WebhookIcon",
+ error: true,
+ official: true,
+ beta: false,
+ legacy: true,
+ priority: 3,
+ score: 30,
+ },
+ },
+ };
+
+ const defaultNodeColors = {
+ TestCategory: "#FF0000",
+ };
+
+ const defaultProps = {
+ item: defaultItem,
+ dataFilter: defaultDataFilter,
+ nodeColors: defaultNodeColors,
+ onDragStart: mockOnDragStart,
+ sensitiveSort: mockSensitiveSort,
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe("Basic Rendering", () => {
+ it("should render items list container", () => {
+ const { container } = render();
+
+ const mainContainer = container.firstChild as HTMLElement;
+ expect(mainContainer).toHaveClass("flex", "flex-col", "gap-1", "py-2");
+ });
+
+ it("should render all components from dataFilter", () => {
+ render();
+
+ expect(screen.getByTestId("draggable-Component1")).toBeInTheDocument();
+ expect(screen.getByTestId("draggable-Component2")).toBeInTheDocument();
+ expect(screen.getByTestId("draggable-ChatInput")).toBeInTheDocument();
+ expect(screen.getByTestId("draggable-Webhook")).toBeInTheDocument();
+ });
+
+ it("should wrap components in tooltips", () => {
+ render();
+
+ const tooltips = screen.getAllByTestId("tooltip");
+ expect(tooltips).toHaveLength(4); // One for each component
+ });
+
+ it("should display component names in tooltips", () => {
+ render();
+
+ const tooltips = screen.getAllByTestId("tooltip");
+ expect(tooltips[0]).toHaveAttribute("data-content", "Chat Input");
+ expect(tooltips[1]).toHaveAttribute("data-content", "Component 1");
+ expect(tooltips[2]).toHaveAttribute("data-content", "Component 2");
+ expect(tooltips[3]).toHaveAttribute("data-content", "Webhook");
+ });
+
+ it("should set tooltip side to right", () => {
+ render();
+
+ const tooltips = screen.getAllByTestId("tooltip");
+ tooltips.forEach((tooltip) => {
+ expect(tooltip).toHaveAttribute("data-side", "right");
+ });
+ });
+ });
+
+ describe("Component Sorting", () => {
+ it("should sort components by priority when available", () => {
+ render();
+
+ const draggables = screen.getAllByTestId(/^draggable-/);
+ expect(draggables[0]).toHaveAttribute("data-item-name", "ChatInput"); // priority 0
+ expect(draggables[1]).toHaveAttribute("data-item-name", "Component1"); // priority 1
+ expect(draggables[2]).toHaveAttribute("data-item-name", "Component2"); // priority 2
+ expect(draggables[3]).toHaveAttribute("data-item-name", "Webhook"); // priority 3
+ });
+
+ it("should fall back to score sorting when priorities are equal", () => {
+ const dataWithEqualPriorities = {
+ TestCategory: {
+ Component1: {
+ ...defaultDataFilter.TestCategory.Component1,
+ priority: 1,
+ score: 20,
+ },
+ Component2: {
+ ...defaultDataFilter.TestCategory.Component2,
+ priority: 1,
+ score: 10,
+ },
+ },
+ };
+
+ const propsWithEqualPriorities = {
+ ...defaultProps,
+ dataFilter: dataWithEqualPriorities,
+ };
+
+ render();
+
+ const draggables = screen.getAllByTestId(/^draggable-/);
+ expect(draggables[0]).toHaveAttribute("data-item-name", "Component2"); // score 10
+ expect(draggables[1]).toHaveAttribute("data-item-name", "Component1"); // score 20
+ });
+
+ it("should use sensitive sort when no score available", () => {
+ const dataWithoutScores = {
+ TestCategory: {
+ ZComponent: {
+ ...defaultDataFilter.TestCategory.Component1,
+ priority: 1,
+ score: undefined,
+ display_name: "Z Component",
+ },
+ AComponent: {
+ ...defaultDataFilter.TestCategory.Component2,
+ priority: 1,
+ score: undefined,
+ display_name: "A Component",
+ },
+ },
+ };
+
+ const propsWithoutScores = {
+ ...defaultProps,
+ dataFilter: dataWithoutScores,
+ };
+
+ render();
+
+ expect(mockSensitiveSort).toHaveBeenCalledWith(
+ "A Component",
+ "Z Component",
+ );
+ });
+
+ it("should handle missing priority as maximum value", () => {
+ const dataWithMissingPriorities = {
+ TestCategory: {
+ Component1: {
+ ...defaultDataFilter.TestCategory.Component1,
+ priority: 1,
+ },
+ Component2: {
+ ...defaultDataFilter.TestCategory.Component2,
+ priority: undefined,
+ },
+ },
+ };
+
+ const propsWithMissingPriorities = {
+ ...defaultProps,
+ dataFilter: dataWithMissingPriorities,
+ };
+
+ render();
+
+ const draggables = screen.getAllByTestId(/^draggable-/);
+ expect(draggables[0]).toHaveAttribute("data-item-name", "Component1"); // priority 1
+ expect(draggables[1]).toHaveAttribute("data-item-name", "Component2"); // priority undefined (max)
+ });
+ });
+
+ describe("Draggable Component Props", () => {
+ it("should pass correct props to regular components", () => {
+ render();
+
+ const component1 = screen.getByTestId("draggable-Component1");
+ expect(component1).toHaveAttribute("data-section-name", "TestCategory");
+ expect(component1).toHaveAttribute("data-icon", "Component1Icon");
+ expect(component1).toHaveAttribute("data-color", "#FF0000");
+ expect(component1).toHaveAttribute("data-display-name", "Component 1");
+ expect(component1).toHaveAttribute("data-official", "true");
+ expect(component1).toHaveAttribute("data-beta", "false");
+ expect(component1).toHaveAttribute("data-legacy", "false");
+ expect(component1).toHaveAttribute("data-disabled", "false");
+ expect(component1).toHaveAttribute("data-disabled-tooltip", "");
+ });
+
+ it("should handle component with error", () => {
+ render();
+
+ const webhook = screen.getByTestId("draggable-Webhook");
+ expect(webhook).toHaveAttribute("data-error", "true");
+ });
+
+ it("should handle component with beta flag", () => {
+ render();
+
+ const component2 = screen.getByTestId("draggable-Component2");
+ expect(component2).toHaveAttribute("data-beta", "true");
+ expect(component2).toHaveAttribute("data-official", "false");
+ });
+
+ it("should handle component with legacy flag", () => {
+ render();
+
+ const webhook = screen.getByTestId("draggable-Webhook");
+ expect(webhook).toHaveAttribute("data-legacy", "true");
+ });
+
+ it("should use fallback icon when component icon missing", () => {
+ const dataWithMissingIcon = {
+ TestCategory: {
+ Component1: {
+ ...defaultDataFilter.TestCategory.Component1,
+ icon: undefined,
+ },
+ },
+ };
+
+ const propsWithMissingIcon = {
+ ...defaultProps,
+ dataFilter: dataWithMissingIcon,
+ };
+
+ render();
+
+ const component1 = screen.getByTestId("draggable-Component1");
+ expect(component1).toHaveAttribute("data-icon", "TestIcon"); // Falls back to item.icon
+ });
+
+ it("should use Unknown icon when all icons missing", () => {
+ const dataWithNoIcon = {
+ TestCategory: {
+ Component1: {
+ ...defaultDataFilter.TestCategory.Component1,
+ icon: undefined,
+ },
+ },
+ };
+
+ const propsWithNoIcon = {
+ ...defaultProps,
+ item: { ...defaultItem, icon: undefined },
+ dataFilter: dataWithNoIcon,
+ };
+
+ render();
+
+ const component1 = screen.getByTestId("draggable-Component1");
+ expect(component1).toHaveAttribute("data-icon", "Unknown");
+ });
+ });
+
+ describe("Special Components (ChatInput/Webhook)", () => {
+ it("should handle ChatInput with special logic", () => {
+ render();
+
+ const chatInput = screen.getByTestId("draggable-ChatInput");
+ expect(chatInput).toHaveAttribute("data-disabled", "true"); // Should be disabled if already added
+ expect(chatInput).toHaveAttribute(
+ "data-disabled-tooltip",
+ "Chat Input already added",
+ );
+ });
+
+ it("should handle Webhook with special logic", () => {
+ render();
+
+ const webhook = screen.getByTestId("draggable-Webhook");
+ expect(webhook).toHaveAttribute("data-disabled", "false"); // Webhook not added in mock
+ expect(webhook).toHaveAttribute("data-disabled-tooltip", "");
+ });
+ });
+
+ describe("Drag and Drop", () => {
+ it("should handle drag start for regular components", () => {
+ render();
+
+ const component1 = screen.getByTestId("draggable-Component1");
+ component1.click(); // Simulate drag start
+
+ expect(mockOnDragStart).toHaveBeenCalledWith(
+ expect.objectContaining({ type: "dragstart" }),
+ expect.objectContaining({
+ type: "Component",
+ node: defaultDataFilter.TestCategory.Component1,
+ }),
+ );
+ });
+
+ it("should handle drag start for ChatInput", () => {
+ render();
+
+ const chatInput = screen.getByTestId("draggable-ChatInput");
+ chatInput.click(); // Simulate drag start
+
+ expect(mockOnDragStart).toHaveBeenCalledWith(
+ expect.objectContaining({ type: "dragstart" }),
+ expect.objectContaining({
+ type: "ChatInput",
+ node: defaultDataFilter.TestCategory.ChatInput,
+ }),
+ );
+ });
+
+ it("should remove count from string in drag start", () => {
+ const dataWithCount = {
+ TestCategory: {
+ Component1_2: {
+ ...defaultDataFilter.TestCategory.Component1,
+ display_name: "Component 1 Copy",
+ },
+ },
+ };
+
+ const propsWithCount = {
+ ...defaultProps,
+ dataFilter: dataWithCount,
+ };
+
+ render();
+
+ const component = screen.getByTestId("draggable-Component1_2");
+ component.click();
+
+ // removeCountFromString should be called to clean the name
+ expect(
+ require("@/utils/utils").removeCountFromString,
+ ).toHaveBeenCalledWith("Component1_2");
+ });
+ });
+
+ describe("Edge Cases", () => {
+ it("should handle empty dataFilter", () => {
+ const propsWithEmptyData = {
+ ...defaultProps,
+ dataFilter: { TestCategory: {} },
+ };
+
+ render();
+
+ expect(screen.queryByTestId(/^draggable-/)).not.toBeInTheDocument();
+ });
+
+ it("should handle missing node colors", () => {
+ const propsWithoutColors = {
+ ...defaultProps,
+ nodeColors: {},
+ };
+
+ render();
+
+ const component1 = screen.getByTestId("draggable-Component1");
+ expect(component1).not.toHaveAttribute("data-color"); // undefined color is not set as attribute
+ });
+
+ it("should handle components with undefined official property", () => {
+ const dataWithUndefinedOfficial = {
+ TestCategory: {
+ Component1: {
+ ...defaultDataFilter.TestCategory.Component1,
+ official: undefined,
+ },
+ },
+ };
+
+ const propsWithUndefinedOfficial = {
+ ...defaultProps,
+ dataFilter: dataWithUndefinedOfficial,
+ };
+
+ render();
+
+ const component1 = screen.getByTestId("draggable-Component1");
+ expect(component1).toHaveAttribute("data-official", "true"); // Defaults to true
+ });
+
+ it("should handle components with missing beta/legacy flags", () => {
+ const dataWithMissingFlags = {
+ TestCategory: {
+ Component1: {
+ ...defaultDataFilter.TestCategory.Component1,
+ beta: undefined,
+ legacy: undefined,
+ },
+ },
+ };
+
+ const propsWithMissingFlags = {
+ ...defaultProps,
+ dataFilter: dataWithMissingFlags,
+ };
+
+ render();
+
+ const component1 = screen.getByTestId("draggable-Component1");
+ expect(component1).toHaveAttribute("data-beta", "false"); // Defaults to false
+ expect(component1).toHaveAttribute("data-legacy", "false"); // Defaults to false
+ });
+ });
+
+ describe("Component Structure", () => {
+ it("should have correct container structure", () => {
+ const { container } = render();
+
+ const mainContainer = container.firstChild as HTMLElement;
+ expect(mainContainer).toHaveClass("flex", "flex-col", "gap-1", "py-2");
+ });
+
+ it("should contain all expected child elements", () => {
+ render();
+
+ expect(screen.getAllByTestId("tooltip")).toHaveLength(4);
+ expect(screen.getAllByTestId(/^draggable-/)).toHaveLength(4);
+ });
+ });
+
+ describe("Props Handling", () => {
+ it("should handle different item names", () => {
+ const propsWithDifferentItem = {
+ ...defaultProps,
+ item: { name: "CustomCategory", icon: "CustomIcon" },
+ dataFilter: {
+ CustomCategory: {
+ CustomComponent: {
+ display_name: "Custom Component",
+ icon: "CustomComponentIcon",
+ error: false,
+ official: true,
+ beta: false,
+ legacy: false,
+ priority: 1,
+ },
+ },
+ },
+ nodeColors: { CustomCategory: "#00FF00" },
+ };
+
+ render();
+
+ const customComponent = screen.getByTestId("draggable-CustomComponent");
+ expect(customComponent).toHaveAttribute(
+ "data-section-name",
+ "CustomCategory",
+ );
+ expect(customComponent).toHaveAttribute("data-color", "#00FF00");
+ });
+
+ it("should handle different callback functions", () => {
+ const alternativeOnDragStart = jest.fn();
+ const alternativeSensitiveSort = jest.fn((a, b) => b.localeCompare(a)); // Reverse sort
+
+ const propsWithDifferentCallbacks = {
+ ...defaultProps,
+ onDragStart: alternativeOnDragStart,
+ sensitiveSort: alternativeSensitiveSort,
+ };
+
+ render();
+
+ const component1 = screen.getByTestId("draggable-Component1");
+ component1.click();
+
+ expect(alternativeOnDragStart).toHaveBeenCalled();
+ expect(mockOnDragStart).not.toHaveBeenCalled();
+ });
+ });
+
+ describe("Integration", () => {
+ it("should integrate with all mocked dependencies correctly", () => {
+ render();
+
+ // Verify all mocked functions were called
+ expect(require("@/stores/flowStore").default).toHaveBeenCalled();
+ expect(
+ require("@/utils/reactflowUtils").checkChatInput,
+ ).toHaveBeenCalled();
+ expect(
+ require("@/utils/reactflowUtils").checkWebhookInput,
+ ).toHaveBeenCalled();
+ expect(
+ require("../../helpers/disable-item").disableItem,
+ ).toHaveBeenCalled();
+ expect(
+ require("../../helpers/get-disabled-tooltip").getDisabledTooltip,
+ ).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/bundleItems/index.tsx b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/bundleItems.tsx
similarity index 95%
rename from src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/bundleItems/index.tsx
rename to src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/bundleItems.tsx
index c1825db1a..e0547600d 100644
--- a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/bundleItems/index.tsx
+++ b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/bundleItems.tsx
@@ -6,8 +6,8 @@ import {
DisclosureTrigger,
} from "@/components/ui/disclosure";
import { SidebarMenuButton, SidebarMenuItem } from "@/components/ui/sidebar";
-import type { BundleItemProps } from "../../types";
-import SidebarItemsList from "../sidebarItemsList";
+import type { BundleItemProps } from "../types";
+import SidebarItemsList from "./sidebarItemsList";
export const BundleItem = memo(
({
@@ -46,6 +46,7 @@ export const BundleItem = memo(
handleKeyDownInput(e, item.name)}
className="user-select-none flex cursor-pointer items-center gap-2"
diff --git a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/categoryDisclouse/index.tsx b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/categoryDisclouse.tsx
similarity index 97%
rename from src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/categoryDisclouse/index.tsx
rename to src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/categoryDisclouse.tsx
index f1d7db283..a46f22404 100644
--- a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/categoryDisclouse/index.tsx
+++ b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/categoryDisclouse.tsx
@@ -7,7 +7,7 @@ import {
} from "@/components/ui/disclosure";
import { SidebarMenuButton, SidebarMenuItem } from "@/components/ui/sidebar";
import type { APIClassType } from "@/types/api";
-import SidebarItemsList from "../sidebarItemsList";
+import SidebarItemsList from "./sidebarItemsList";
export const CategoryDisclosure = memo(function CategoryDisclosure({
item,
@@ -59,6 +59,7 @@ export const CategoryDisclosure = memo(function CategoryDisclosure({
-
+
{display_name}
diff --git a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarFilterComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarFilterComponent.tsx
similarity index 100%
rename from src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarFilterComponent/index.tsx
rename to src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarFilterComponent.tsx
diff --git a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarFooterButtons/index.tsx b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarFooterButtons.tsx
similarity index 100%
rename from src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarFooterButtons/index.tsx
rename to src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarFooterButtons.tsx
diff --git a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarHeader/index.tsx b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarHeader.tsx
similarity index 92%
rename from src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarHeader/index.tsx
rename to src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarHeader.tsx
index 494b297ec..1347346c7 100644
--- a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarHeader/index.tsx
+++ b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarHeader.tsx
@@ -9,10 +9,10 @@ import {
DisclosureTrigger,
} from "@/components/ui/disclosure";
import { SidebarHeader, SidebarTrigger } from "@/components/ui/sidebar";
-import type { SidebarHeaderComponentProps } from "../../types";
-import FeatureToggles from "../featureTogglesComponent";
-import { SearchInput } from "../searchInput";
-import { SidebarFilterComponent } from "../sidebarFilterComponent";
+import type { SidebarHeaderComponentProps } from "../types";
+import FeatureToggles from "./featureTogglesComponent";
+import { SearchInput } from "./searchInput";
+import { SidebarFilterComponent } from "./sidebarFilterComponent";
export const SidebarHeaderComponent = memo(function SidebarHeaderComponent({
showConfig,
diff --git a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarItemsList/index.tsx b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarItemsList.tsx
similarity index 87%
rename from src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarItemsList/index.tsx
rename to src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarItemsList.tsx
index 452c8ece8..945b07c50 100644
--- a/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarItemsList/index.tsx
+++ b/src/frontend/src/pages/FlowPage/components/flowSidebarComponent/components/sidebarItemsList.tsx
@@ -3,10 +3,10 @@ import ShadTooltip from "@/components/common/shadTooltipComponent";
import useFlowStore from "@/stores/flowStore";
import { checkChatInput, checkWebhookInput } from "@/utils/reactflowUtils";
import { removeCountFromString } from "@/utils/utils";
-import { disableItem } from "../../helpers/disable-item";
-import { getDisabledTooltip } from "../../helpers/get-disabled-tooltip";
-import type { UniqueInputsComponents } from "../../types";
-import SidebarDraggableComponent from "../sidebarDraggableComponent";
+import { disableItem } from "../helpers/disable-item";
+import { getDisabledTooltip } from "../helpers/get-disabled-tooltip";
+import type { UniqueInputsComponents } from "../types";
+import SidebarDraggableComponent from "./sidebarDraggableComponent";
const SidebarItemsList = ({
item,
@@ -36,16 +36,16 @@ const SidebarItemsList = ({
? itemA.score - itemB.score
: sensitiveSort(itemA.display_name, itemB.display_name);
})
- .map((SBItemName, idx) => {
+ .map((SBItemName) => {
const currentItem = dataFilter[item.name][SBItemName];
if (SBItemName === "ChatInput" || SBItemName === "Webhook") {
return (
@@ -55,7 +55,7 @@ const SidebarItemsList = ({
{
@@ -105,7 +104,11 @@ const UniqueInputsDraggableComponent = ({
}, [chatInputAdded, webhookInputAdded]);
return (
-
+
url.pathname === "/", { timeout: 30000 }),
+ page.getByTestId("icon-ChevronLeft").click(),
+ ]);
+
+ await expect(page.getByTestId("mainpage_title")).toBeVisible({
+ timeout: 30000,
});
await page.waitForTimeout(500);