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);