refactor: flowSidebarComponents folder structure refactor and add test coverage (#9383)
* refactor and test coverage * code issue cleanup * test and coverage fixes * fix test + pr comments * cleanup * test debugging * cleanup * try again * No logs * fix test
This commit is contained in:
parent
2c405f77e7
commit
590682d748
27 changed files with 5606 additions and 40 deletions
|
|
@ -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}",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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) => (
|
||||
<span data-testid={`icon-${name}`} className={className}>
|
||||
{name}
|
||||
</span>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock("@/components/ui/disclosure", () => ({
|
||||
Disclosure: ({ children, open, onOpenChange }: any) => (
|
||||
<div
|
||||
data-testid="disclosure"
|
||||
data-open={open}
|
||||
data-on-open-change={onOpenChange?.toString()}
|
||||
>
|
||||
{React.Children.map(children, (child, index) => {
|
||||
if (index === 0) {
|
||||
return React.cloneElement(child, { onOpenChange });
|
||||
}
|
||||
return child;
|
||||
})}
|
||||
</div>
|
||||
),
|
||||
DisclosureContent: ({ children }: any) => (
|
||||
<div data-testid="disclosure-content">{children}</div>
|
||||
),
|
||||
DisclosureTrigger: ({ children, className }: any) => (
|
||||
<div data-testid="disclosure-trigger" className={className}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock("@/components/ui/sidebar", () => ({
|
||||
SidebarMenuButton: ({ children, asChild }: any) => (
|
||||
<div data-testid="sidebar-menu-button">
|
||||
{asChild ? children : <button>{children}</button>}
|
||||
</div>
|
||||
),
|
||||
SidebarMenuItem: ({ children }: any) => (
|
||||
<div data-testid="sidebar-menu-item">{children}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock the SidebarItemsList component
|
||||
jest.mock("../sidebarItemsList", () => {
|
||||
return function MockSidebarItemsList(props: any) {
|
||||
return (
|
||||
<div data-testid="sidebar-items-list">Items for {props.item?.name}</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
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(<BundleItem {...defaultProps} />);
|
||||
|
||||
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(<BundleItem {...defaultProps} />);
|
||||
|
||||
expect(screen.getByTestId("icon-Package")).toBeInTheDocument();
|
||||
expect(screen.getByText("Test Bundle")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display chevron icon", () => {
|
||||
render(<BundleItem {...defaultProps} />);
|
||||
|
||||
expect(screen.getByTestId("icon-ChevronRight")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should include correct test id for disclosure", () => {
|
||||
render(<BundleItem {...defaultProps} />);
|
||||
|
||||
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(<BundleItem {...propsWithEmptyFilter} />);
|
||||
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(<BundleItem {...propsWithMissingBundle} />);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it("should render when dataFilter has items for bundle", () => {
|
||||
render(<BundleItem {...defaultProps} />);
|
||||
|
||||
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(<BundleItem {...defaultProps} />);
|
||||
|
||||
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(<BundleItem {...propsWithOpenCategory} />);
|
||||
|
||||
const disclosure = screen.getByTestId("disclosure");
|
||||
expect(disclosure).toHaveAttribute("data-open", "true");
|
||||
});
|
||||
|
||||
it("should toggle open state when disclosure changes", () => {
|
||||
const { rerender } = render(<BundleItem {...defaultProps} />);
|
||||
|
||||
// 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(<BundleItem {...propsWithHandlerCall} />);
|
||||
|
||||
// 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(<BundleItem {...propsWithOpenCategory} />);
|
||||
|
||||
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(<BundleItem {...defaultProps} />);
|
||||
|
||||
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(<BundleItem {...defaultProps} />);
|
||||
|
||||
const triggerDiv = screen.getByTestId("disclosure-bundles-test bundle");
|
||||
expect(triggerDiv).toHaveAttribute("tabIndex", "0");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Component Integration", () => {
|
||||
it("should render SidebarItemsList with correct props", () => {
|
||||
render(<BundleItem {...defaultProps} />);
|
||||
|
||||
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(<BundleItem {...defaultProps} />);
|
||||
|
||||
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(<BundleItem {...defaultProps} />);
|
||||
|
||||
const initialElement = screen.getByTestId("disclosure");
|
||||
|
||||
// Re-render with same props
|
||||
rerender(<BundleItem {...defaultProps} />);
|
||||
|
||||
// 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(<BundleItem {...defaultProps} />);
|
||||
|
||||
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(<BundleItem {...defaultProps} />);
|
||||
|
||||
const disclosureTrigger = screen.getByTestId("disclosure-trigger");
|
||||
expect(disclosureTrigger).toHaveClass("group/collapsible");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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) => (
|
||||
<span data-testid={`icon-${name}`} className={className}>
|
||||
{name}
|
||||
</span>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock("@/components/ui/disclosure", () => ({
|
||||
Disclosure: ({ children, open, onOpenChange }: any) => (
|
||||
<div
|
||||
data-testid="disclosure"
|
||||
data-open={open}
|
||||
data-on-open-change={onOpenChange?.toString()}
|
||||
>
|
||||
{React.Children.map(children, (child, index) => {
|
||||
if (index === 0) {
|
||||
return React.cloneElement(child, { onOpenChange });
|
||||
}
|
||||
return child;
|
||||
})}
|
||||
</div>
|
||||
),
|
||||
DisclosureContent: ({ children }: any) => (
|
||||
<div data-testid="disclosure-content">{children}</div>
|
||||
),
|
||||
DisclosureTrigger: ({ children, className }: any) => (
|
||||
<div data-testid="disclosure-trigger" className={className}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock("@/components/ui/sidebar", () => ({
|
||||
SidebarMenuButton: ({ children, asChild }: any) => (
|
||||
<div data-testid="sidebar-menu-button">
|
||||
{asChild ? children : <button>{children}</button>}
|
||||
</div>
|
||||
),
|
||||
SidebarMenuItem: ({ children }: any) => (
|
||||
<div data-testid="sidebar-menu-item">{children}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock the SidebarItemsList component
|
||||
jest.mock("../sidebarItemsList", () => {
|
||||
return function MockSidebarItemsList(props: any) {
|
||||
return (
|
||||
<div data-testid="sidebar-items-list">Items for {props.item?.name}</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
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(<CategoryDisclosure {...defaultProps} />);
|
||||
|
||||
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(<CategoryDisclosure {...defaultProps} />);
|
||||
|
||||
expect(screen.getByTestId("icon-Folder")).toBeInTheDocument();
|
||||
expect(screen.getByText("Test Category")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display chevron icon", () => {
|
||||
render(<CategoryDisclosure {...defaultProps} />);
|
||||
|
||||
expect(screen.getByTestId("icon-ChevronRight")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should include correct test id for disclosure", () => {
|
||||
render(<CategoryDisclosure {...defaultProps} />);
|
||||
|
||||
const disclosureDiv = screen.getByTestId("disclosure-test category");
|
||||
expect(disclosureDiv).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render disclosure content with SidebarItemsList", () => {
|
||||
render(<CategoryDisclosure {...defaultProps} />);
|
||||
|
||||
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(<CategoryDisclosure {...defaultProps} />);
|
||||
|
||||
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(<CategoryDisclosure {...propsWithOpenCategory} />);
|
||||
|
||||
const disclosure = screen.getByTestId("disclosure");
|
||||
expect(disclosure).toHaveAttribute("data-open", "true");
|
||||
});
|
||||
|
||||
it("should toggle open state when disclosure changes", () => {
|
||||
const { rerender } = render(<CategoryDisclosure {...defaultProps} />);
|
||||
|
||||
// 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(<CategoryDisclosure {...propsWithHandlerCall} />);
|
||||
|
||||
// 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(<CategoryDisclosure {...propsWithOpenCategory} />);
|
||||
|
||||
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(<CategoryDisclosure {...defaultProps} />);
|
||||
|
||||
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(<CategoryDisclosure {...defaultProps} />);
|
||||
|
||||
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(<CategoryDisclosure {...defaultProps} />);
|
||||
|
||||
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(<CategoryDisclosure {...defaultProps} />);
|
||||
|
||||
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(<CategoryDisclosure {...propsWithOpenCategory} />);
|
||||
|
||||
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(<CategoryDisclosure {...defaultProps} />);
|
||||
|
||||
const triggerDiv = screen.getByTestId("disclosure-test category");
|
||||
expect(triggerDiv).toHaveAttribute("tabIndex", "0");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Component Integration", () => {
|
||||
it("should render SidebarItemsList with correct props", () => {
|
||||
render(<CategoryDisclosure {...defaultProps} />);
|
||||
|
||||
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(<CategoryDisclosure {...defaultProps} />);
|
||||
|
||||
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(<CategoryDisclosure {...defaultProps} />);
|
||||
|
||||
// Get the keydown handler function
|
||||
const triggerDiv = screen.getByTestId("disclosure-test category");
|
||||
const initialKeyDownHandler = triggerDiv.getAttribute("onkeydown");
|
||||
|
||||
// Re-render with same props
|
||||
rerender(<CategoryDisclosure {...defaultProps} />);
|
||||
|
||||
// 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(<CategoryDisclosure {...defaultProps} />);
|
||||
|
||||
const disclosure = screen.getByTestId("disclosure");
|
||||
const initialOnOpenChangeAttr = disclosure.getAttribute(
|
||||
"data-on-open-change",
|
||||
);
|
||||
|
||||
// Re-render with same props
|
||||
rerender(<CategoryDisclosure {...defaultProps} />);
|
||||
|
||||
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(<CategoryDisclosure {...defaultProps} />);
|
||||
|
||||
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(<CategoryDisclosure {...defaultProps} />);
|
||||
|
||||
const disclosureTrigger = screen.getByTestId("disclosure-trigger");
|
||||
expect(disclosureTrigger).toHaveClass("group/collapsible");
|
||||
});
|
||||
|
||||
it("should apply accent-pink-foreground class to category icon", () => {
|
||||
render(<CategoryDisclosure {...defaultProps} />);
|
||||
|
||||
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(<CategoryDisclosure {...defaultProps} />);
|
||||
|
||||
const categoryNameSpan = screen.getByText("Test Category");
|
||||
expect(categoryNameSpan).toHaveClass(
|
||||
"group-aria-expanded/collapsible:font-semibold",
|
||||
);
|
||||
});
|
||||
|
||||
it("should apply rotation class to chevron icon", () => {
|
||||
render(<CategoryDisclosure {...defaultProps} />);
|
||||
|
||||
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(<CategoryDisclosure {...propsWithDifferentItem} />);
|
||||
|
||||
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(<CategoryDisclosure {...propsWithoutOpenCategories} />);
|
||||
}).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(<CategoryDisclosure {...propsWithSpecialName} />);
|
||||
|
||||
expect(
|
||||
screen.getByTestId("disclosure-test category & special"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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) => (
|
||||
<div data-testid="sidebar-group" className={className}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
SidebarGroupContent: ({ children }: any) => (
|
||||
<div data-testid="sidebar-group-content">{children}</div>
|
||||
),
|
||||
SidebarMenu: ({ children }: any) => (
|
||||
<div data-testid="sidebar-menu">{children}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock the CategoryDisclosure component
|
||||
jest.mock("../categoryDisclouse", () => ({
|
||||
CategoryDisclosure: ({ item, openCategories }: any) => (
|
||||
<div data-testid={`category-disclosure-${item.name}`}>
|
||||
CategoryDisclosure for {item.display_name} - Open:{" "}
|
||||
{openCategories.includes(item.name).toString()}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
// 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(<CategoryGroup {...defaultProps} />);
|
||||
|
||||
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(<CategoryGroup {...defaultProps} />);
|
||||
|
||||
expect(
|
||||
screen.getByTestId("category-disclosure-category1"),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByTestId("category-disclosure-category2"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display correct category names", () => {
|
||||
render(<CategoryGroup {...defaultProps} />);
|
||||
|
||||
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(<CategoryGroup {...defaultProps} />);
|
||||
|
||||
expect(
|
||||
screen.queryByTestId("category-disclosure-bundle1"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should exclude custom_component category", () => {
|
||||
render(<CategoryGroup {...defaultProps} />);
|
||||
|
||||
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(<CategoryGroup {...propsWithEmptyCategory} />);
|
||||
|
||||
expect(
|
||||
screen.queryByTestId("category-disclosure-emptyCategory"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should include categories with items", () => {
|
||||
render(<CategoryGroup {...defaultProps} />);
|
||||
|
||||
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(<CategoryGroup {...propsWithReorderedCategories} />);
|
||||
|
||||
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(<CategoryGroup {...propsWithSearch} />);
|
||||
|
||||
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(<CategoryGroup {...propsWithUnknownCategory} />);
|
||||
|
||||
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(<CategoryGroup {...propsWithOpenCategories} />);
|
||||
|
||||
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(<CategoryGroup {...propsWithMultipleOpen} />);
|
||||
|
||||
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(<CategoryGroup {...defaultProps} />);
|
||||
|
||||
// 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(<CategoryGroup {...defaultProps} />);
|
||||
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(<CategoryGroup {...propsWithNoValidCategories} />);
|
||||
|
||||
expect(
|
||||
screen.queryByTestId(/category-disclosure-/),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle empty dataFilter gracefully", () => {
|
||||
const propsWithEmptyDataFilter = {
|
||||
...defaultProps,
|
||||
dataFilter: {},
|
||||
};
|
||||
|
||||
render(<CategoryGroup {...propsWithEmptyDataFilter} />);
|
||||
|
||||
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(<CategoryGroup {...propsWithMissingCategories} />);
|
||||
|
||||
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(<CategoryGroup {...defaultProps} />);
|
||||
|
||||
const initialElement = screen.getByTestId("sidebar-group");
|
||||
|
||||
// Re-render with same props
|
||||
rerender(<CategoryGroup {...defaultProps} />);
|
||||
|
||||
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(<CategoryGroup {...propsWithComplexData} />);
|
||||
|
||||
// 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(<CategoryGroup {...propsWithSearchAndSort} />);
|
||||
|
||||
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",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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(<NoResultsMessage {...defaultProps} />);
|
||||
|
||||
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(<NoResultsMessage {...customProps} />);
|
||||
|
||||
expect(container.textContent).toContain("Custom no results message");
|
||||
});
|
||||
|
||||
it("should render with custom clear search text", () => {
|
||||
const customProps = {
|
||||
...defaultProps,
|
||||
clearSearchText: "Reset search",
|
||||
};
|
||||
|
||||
render(<NoResultsMessage {...customProps} />);
|
||||
|
||||
expect(screen.getByText("Reset search")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render with custom additional text", () => {
|
||||
const customProps = {
|
||||
...defaultProps,
|
||||
additionalText: "or try something else.",
|
||||
};
|
||||
|
||||
const { container } = render(<NoResultsMessage {...customProps} />);
|
||||
|
||||
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(<NoResultsMessage {...allCustomProps} />);
|
||||
|
||||
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(<NoResultsMessage {...defaultProps} />);
|
||||
|
||||
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(<NoResultsMessage {...customProps} />);
|
||||
|
||||
const clearLink = screen.getByText("Reset filters");
|
||||
await user.click(clearLink);
|
||||
|
||||
expect(mockOnClearSearch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should handle keyboard events on clear link", () => {
|
||||
render(<NoResultsMessage {...defaultProps} />);
|
||||
|
||||
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(<NoResultsMessage {...defaultProps} />);
|
||||
|
||||
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(<NoResultsMessage {...defaultProps} />);
|
||||
|
||||
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(<NoResultsMessage {...defaultProps} />);
|
||||
|
||||
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(<NoResultsMessage {...emptyProps} />);
|
||||
|
||||
// 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(<NoResultsMessage {...messageOnlyProps} />);
|
||||
|
||||
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(<NoResultsMessage {...minimalProps} />);
|
||||
|
||||
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(<NoResultsMessage {...overrideProps} />);
|
||||
|
||||
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(<NoResultsMessage {...propsWithoutCallback} />);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("should work with different callback functions", async () => {
|
||||
const user = userEvent.setup();
|
||||
const alternativeCallback = jest.fn();
|
||||
const alternativeProps = {
|
||||
onClearSearch: alternativeCallback,
|
||||
};
|
||||
|
||||
render(<NoResultsMessage {...alternativeProps} />);
|
||||
|
||||
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(
|
||||
<NoResultsMessage {...defaultProps} />,
|
||||
);
|
||||
|
||||
expect(container.textContent).toContain("No components found.");
|
||||
|
||||
const newProps = {
|
||||
...defaultProps,
|
||||
message: "Updated message",
|
||||
};
|
||||
|
||||
rerender(<NoResultsMessage {...newProps} />);
|
||||
|
||||
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(<NoResultsMessage {...defaultProps} />);
|
||||
|
||||
rerender(<NoResultsMessage {...defaultProps} />);
|
||||
|
||||
const clearLink = screen.getByText("Clear your search");
|
||||
await user.click(clearLink);
|
||||
|
||||
expect(mockOnClearSearch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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) => (
|
||||
<span
|
||||
data-testid={`badge-${variant}-${size}`}
|
||||
className={`badge-${variant} badge-${size}`}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock("@/components/ui/switch", () => ({
|
||||
Switch: ({ checked, onCheckedChange, "data-testid": testId }: any) => (
|
||||
<button
|
||||
data-testid={testId || "switch"}
|
||||
onClick={() => onCheckedChange(!checked)}
|
||||
aria-checked={checked}
|
||||
role="switch"
|
||||
>
|
||||
{checked ? "ON" : "OFF"}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
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(<FeatureToggles {...defaultProps} />);
|
||||
|
||||
// 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(<FeatureToggles {...defaultProps} />);
|
||||
|
||||
expect(screen.getByTestId("badge-pinkStatic-xq")).toHaveTextContent(
|
||||
"Beta",
|
||||
);
|
||||
expect(screen.getByTestId("sidebar-beta-switch")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render Legacy toggle with correct elements", () => {
|
||||
render(<FeatureToggles {...defaultProps} />);
|
||||
|
||||
expect(screen.getByTestId("badge-secondaryStatic-xq")).toHaveTextContent(
|
||||
"Legacy",
|
||||
);
|
||||
expect(screen.getByTestId("sidebar-legacy-switch")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render correct number of toggle sections", () => {
|
||||
render(<FeatureToggles {...defaultProps} />);
|
||||
|
||||
const toggleSections = screen.getAllByText("Show");
|
||||
expect(toggleSections).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Switch States", () => {
|
||||
it("should show Beta switch as OFF when showBeta is false", () => {
|
||||
render(<FeatureToggles {...defaultProps} />);
|
||||
|
||||
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(<FeatureToggles {...propsWithBetaOn} />);
|
||||
|
||||
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(<FeatureToggles {...defaultProps} />);
|
||||
|
||||
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(<FeatureToggles {...propsWithLegacyOn} />);
|
||||
|
||||
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(<FeatureToggles {...propsWithBothOn} />);
|
||||
|
||||
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(<FeatureToggles {...defaultProps} />);
|
||||
|
||||
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(<FeatureToggles {...propsWithBetaOn} />);
|
||||
|
||||
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(<FeatureToggles {...defaultProps} />);
|
||||
|
||||
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(<FeatureToggles {...defaultProps} />);
|
||||
|
||||
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(<FeatureToggles {...defaultProps} />);
|
||||
|
||||
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(<FeatureToggles {...propsWithLegacyOn} />);
|
||||
|
||||
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(<FeatureToggles {...defaultProps} />);
|
||||
|
||||
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(<FeatureToggles {...defaultProps} />);
|
||||
|
||||
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(<FeatureToggles {...defaultProps} />);
|
||||
|
||||
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 (
|
||||
<FeatureToggles
|
||||
showBeta={beta}
|
||||
setShowBeta={(value: boolean) => {
|
||||
setBeta(value);
|
||||
mockSetShowBeta(value);
|
||||
}}
|
||||
showLegacy={legacy}
|
||||
setShowLegacy={mockSetShowLegacy}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
render(<Controlled />);
|
||||
|
||||
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(<FeatureToggles {...defaultProps} />);
|
||||
|
||||
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(<FeatureToggles {...defaultProps} />);
|
||||
|
||||
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(<FeatureToggles {...defaultProps} />);
|
||||
|
||||
const badges = screen.getAllByTestId(/^badge-/);
|
||||
expect(badges[0]).toHaveTextContent("Beta");
|
||||
expect(badges[1]).toHaveTextContent("Legacy");
|
||||
});
|
||||
|
||||
it("should have switches with correct test IDs", () => {
|
||||
render(<FeatureToggles {...defaultProps} />);
|
||||
|
||||
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(<FeatureToggles {...propsWithoutCallbacks} />);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("should handle boolean state changes correctly", () => {
|
||||
const { rerender } = render(<FeatureToggles {...defaultProps} />);
|
||||
|
||||
let betaSwitch = screen.getByTestId("sidebar-beta-switch");
|
||||
expect(betaSwitch).toHaveAttribute("aria-checked", "false");
|
||||
|
||||
const newProps = { ...defaultProps, showBeta: true };
|
||||
rerender(<FeatureToggles {...newProps} />);
|
||||
|
||||
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(<FeatureToggles {...defaultProps} />);
|
||||
|
||||
// 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(<FeatureToggles {...defaultProps} />);
|
||||
|
||||
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(<FeatureToggles {...mixedStateProps} />);
|
||||
|
||||
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(<FeatureToggles {...defaultProps} />);
|
||||
|
||||
// Rapidly change states
|
||||
rerender(<FeatureToggles {...defaultProps} showBeta={true} />);
|
||||
rerender(<FeatureToggles {...defaultProps} showBeta={false} />);
|
||||
rerender(
|
||||
<FeatureToggles {...defaultProps} showBeta={true} showLegacy={true} />,
|
||||
);
|
||||
|
||||
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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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) => (
|
||||
<div data-testid="sidebar-group" className={className}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
SidebarGroupContent: ({ children }: any) => (
|
||||
<div data-testid="sidebar-group-content">{children}</div>
|
||||
),
|
||||
SidebarGroupLabel: ({ children, className }: any) => (
|
||||
<div data-testid="sidebar-group-label" className={className}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
SidebarMenu: ({ children }: any) => (
|
||||
<div data-testid="sidebar-menu">{children}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock the BundleItem component
|
||||
jest.mock("../bundleItems", () => ({
|
||||
BundleItem: ({ item, openCategories }: any) => (
|
||||
<div data-testid={`bundle-item-${item.name}`}>
|
||||
Bundle Item: {item.display_name} - Open:{" "}
|
||||
{openCategories.includes(item.name).toString()}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
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(<MemoizedSidebarGroup {...defaultProps} />);
|
||||
|
||||
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(<MemoizedSidebarGroup {...defaultProps} />);
|
||||
|
||||
expect(screen.getByTestId("sidebar-group-label")).toHaveTextContent(
|
||||
"Bundles",
|
||||
);
|
||||
});
|
||||
|
||||
it("should apply correct CSS classes", () => {
|
||||
render(<MemoizedSidebarGroup {...defaultProps} />);
|
||||
|
||||
expect(screen.getByTestId("sidebar-group")).toHaveClass("p-3");
|
||||
expect(screen.getByTestId("sidebar-group-label")).toHaveClass(
|
||||
"cursor-default",
|
||||
);
|
||||
});
|
||||
|
||||
it("should render all bundle items", () => {
|
||||
render(<MemoizedSidebarGroup {...defaultProps} />);
|
||||
|
||||
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(<MemoizedSidebarGroup {...defaultProps} />);
|
||||
|
||||
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(<MemoizedSidebarGroup {...propsWithReorderedBundles} />);
|
||||
|
||||
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(<MemoizedSidebarGroup {...propsWithSearch} />);
|
||||
|
||||
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(<MemoizedSidebarGroup {...propsWithPartialSort} />);
|
||||
|
||||
// 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(<MemoizedSidebarGroup {...propsWithEmptySort} />);
|
||||
|
||||
expect(screen.getAllByTestId(/bundle-item-/)).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Props Passing to BundleItem", () => {
|
||||
it("should pass all required props to each BundleItem", () => {
|
||||
render(<MemoizedSidebarGroup {...defaultProps} />);
|
||||
|
||||
// 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(<MemoizedSidebarGroup {...propsWithOpenCategories} />);
|
||||
|
||||
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(<MemoizedSidebarGroup {...propsWithMultipleOpen} />);
|
||||
|
||||
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(<MemoizedSidebarGroup {...defaultProps} />);
|
||||
|
||||
const initialElement = screen.getByTestId("sidebar-group");
|
||||
|
||||
// Re-render with same props
|
||||
rerender(<MemoizedSidebarGroup {...defaultProps} />);
|
||||
|
||||
expect(screen.getByTestId("sidebar-group")).toBe(initialElement);
|
||||
});
|
||||
|
||||
it("should re-render when BUNDLES prop changes", () => {
|
||||
const { rerender } = render(<MemoizedSidebarGroup {...defaultProps} />);
|
||||
|
||||
expect(screen.getAllByTestId(/bundle-item-/)).toHaveLength(3);
|
||||
|
||||
const newProps = {
|
||||
...defaultProps,
|
||||
BUNDLES: [
|
||||
{ name: "bundle1", display_name: "Bundle 1", icon: "Package" },
|
||||
],
|
||||
};
|
||||
|
||||
rerender(<MemoizedSidebarGroup {...newProps} />);
|
||||
|
||||
expect(screen.getAllByTestId(/bundle-item-/)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("should recalculate sorting when search changes", () => {
|
||||
const { rerender } = render(<MemoizedSidebarGroup {...defaultProps} />);
|
||||
|
||||
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(<MemoizedSidebarGroup {...propsWithSearch} />);
|
||||
|
||||
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(
|
||||
<MemoizedSidebarGroup {...propsWithSearch} />,
|
||||
);
|
||||
|
||||
let bundleItems = screen.getAllByTestId(/bundle-item-/);
|
||||
expect(bundleItems[0]).toHaveAttribute(
|
||||
"data-testid",
|
||||
"bundle-item-bundle1",
|
||||
);
|
||||
|
||||
const updatedProps = {
|
||||
...propsWithSearch,
|
||||
sortedCategories: ["bundle3", "bundle2", "bundle1"],
|
||||
};
|
||||
|
||||
rerender(<MemoizedSidebarGroup {...updatedProps} />);
|
||||
|
||||
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(<MemoizedSidebarGroup {...propsWithEmptyBundles} />);
|
||||
|
||||
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(<MemoizedSidebarGroup {...propsWithSingleBundle} />);
|
||||
|
||||
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(<MemoizedSidebarGroup {...propsWithDuplicateNames} />);
|
||||
|
||||
// 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(<MemoizedSidebarGroup {...propsWithoutDataFilter} />);
|
||||
|
||||
expect(screen.getAllByTestId(/bundle-item-/)).toHaveLength(3);
|
||||
});
|
||||
|
||||
it("should handle missing nodeColors gracefully", () => {
|
||||
const propsWithoutNodeColors = {
|
||||
...defaultProps,
|
||||
nodeColors: {},
|
||||
};
|
||||
|
||||
render(<MemoizedSidebarGroup {...propsWithoutNodeColors} />);
|
||||
|
||||
expect(screen.getAllByTestId(/bundle-item-/)).toHaveLength(3);
|
||||
});
|
||||
|
||||
it("should handle undefined search and sortedCategories", () => {
|
||||
const propsWithEmpty = {
|
||||
...defaultProps,
|
||||
search: "",
|
||||
sortedCategories: [],
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
render(<MemoizedSidebarGroup {...propsWithEmpty} />);
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Component Structure", () => {
|
||||
it("should have correct DOM hierarchy", () => {
|
||||
render(<MemoizedSidebarGroup {...defaultProps} />);
|
||||
|
||||
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(<MemoizedSidebarGroup {...defaultProps} />);
|
||||
|
||||
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(<MemoizedSidebarGroup {...propsWithNoBundles} />);
|
||||
|
||||
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(<MemoizedSidebarGroup {...propsWithCustomOrder} />);
|
||||
|
||||
// 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(<MemoizedSidebarGroup {...complexProps} />);
|
||||
|
||||
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(<MemoizedSidebarGroup {...propsWithoutCallbacks} />);
|
||||
}).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(<MemoizedSidebarGroup {...alternativeProps} />);
|
||||
|
||||
expect(screen.getAllByTestId(/bundle-item-/)).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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) => (
|
||||
<span data-testid={`badge-${variant}-${size}`} className={className}>
|
||||
{children}
|
||||
</span>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock("@/components/ui/button", () => ({
|
||||
Button: ({
|
||||
children,
|
||||
onClick,
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
tabIndex,
|
||||
...props
|
||||
}: any) => (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={className}
|
||||
tabIndex={tabIndex}
|
||||
data-variant={variant}
|
||||
data-size={size}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
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) => (
|
||||
<span data-testid={`icon-${name}`} className={className}>
|
||||
{name}
|
||||
</span>
|
||||
),
|
||||
ForwardedIconComponent: ({ name, className }: any) => (
|
||||
<span data-testid={`forwarded-icon-${name}`} className={className}>
|
||||
{name}
|
||||
</span>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock("@/components/common/shadTooltipComponent", () => ({
|
||||
__esModule: true,
|
||||
default: ({ children, content, styleClasses }: any) => (
|
||||
<div data-testid="tooltip" data-content={content} className={styleClasses}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock("@/components/ui/select-custom", () => ({
|
||||
Select: ({ children, onValueChange, onOpenChange, open, ...props }: any) => (
|
||||
<div
|
||||
data-testid="select"
|
||||
data-open={open}
|
||||
data-on-value-change={onValueChange?.toString()}
|
||||
data-on-open-change={onOpenChange?.toString()}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
SelectContent: ({ children, position, side, sideOffset, style }: any) => (
|
||||
<div
|
||||
data-testid="select-content"
|
||||
data-position={position}
|
||||
data-side={side}
|
||||
data-side-offset={sideOffset}
|
||||
style={style}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
SelectItem: ({ children, value, ...props }: any) => (
|
||||
<div data-testid={`select-item-${value}`} data-value={value} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
SelectTrigger: ({ children, tabIndex, ...props }: any) => (
|
||||
<div data-testid="select-trigger" tabIndex={tabIndex} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
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(<SidebarDraggableComponent {...defaultProps} />);
|
||||
|
||||
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(<SidebarDraggableComponent {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText("Test Component")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display component icon", () => {
|
||||
render(<SidebarDraggableComponent {...defaultProps} />);
|
||||
|
||||
expect(screen.getByTestId("forwarded-icon-TestIcon")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should have correct test id format", () => {
|
||||
render(<SidebarDraggableComponent {...defaultProps} />);
|
||||
|
||||
expect(
|
||||
screen.getByTestId("testsection_test component_draggable"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display grip vertical icon", () => {
|
||||
render(<SidebarDraggableComponent {...defaultProps} />);
|
||||
|
||||
expect(
|
||||
screen.getByTestId("forwarded-icon-GripVertical"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render add component button when not disabled", () => {
|
||||
render(<SidebarDraggableComponent {...defaultProps} />);
|
||||
|
||||
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(<SidebarDraggableComponent {...propsWithBeta} />);
|
||||
|
||||
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(<SidebarDraggableComponent {...propsWithLegacy} />);
|
||||
|
||||
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(<SidebarDraggableComponent {...propsWithBoth} />);
|
||||
|
||||
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(<SidebarDraggableComponent {...defaultProps} />);
|
||||
|
||||
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(<SidebarDraggableComponent {...propsWithDisabled} />);
|
||||
|
||||
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(<SidebarDraggableComponent {...propsWithDisabled} />);
|
||||
|
||||
expect(
|
||||
screen.queryByTestId("add-component-button-test-component"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show no tooltip content when not disabled", () => {
|
||||
render(<SidebarDraggableComponent {...defaultProps} />);
|
||||
|
||||
expect(screen.getAllByTestId("tooltip")[0]).not.toHaveAttribute(
|
||||
"data-content",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Error State", () => {
|
||||
it("should handle error state correctly", () => {
|
||||
const propsWithError = { ...defaultProps, error: true };
|
||||
|
||||
render(<SidebarDraggableComponent {...propsWithError} />);
|
||||
|
||||
const draggableDiv = screen.getByTestId(/testsectiontest component/i);
|
||||
expect(draggableDiv).toHaveAttribute("draggable", "false");
|
||||
});
|
||||
|
||||
it("should be draggable when no error", () => {
|
||||
render(<SidebarDraggableComponent {...defaultProps} />);
|
||||
|
||||
const draggableDiv = screen.getByTestId(/testsectiontest component/i);
|
||||
expect(draggableDiv).toHaveAttribute("draggable", "true");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Drag and Drop", () => {
|
||||
it("should call onDragStart when dragging starts", () => {
|
||||
render(<SidebarDraggableComponent {...defaultProps} />);
|
||||
|
||||
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(<SidebarDraggableComponent {...defaultProps} />);
|
||||
|
||||
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(<SidebarDraggableComponent {...propsWithError} />);
|
||||
|
||||
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(<SidebarDraggableComponent {...defaultProps} />);
|
||||
|
||||
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(<SidebarDraggableComponent {...defaultProps} />);
|
||||
|
||||
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(<SidebarDraggableComponent {...defaultProps} />);
|
||||
|
||||
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(<SidebarDraggableComponent {...defaultProps} />);
|
||||
|
||||
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(<SidebarDraggableComponent {...defaultProps} />);
|
||||
|
||||
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(<SidebarDraggableComponent {...defaultProps} />);
|
||||
|
||||
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(<SidebarDraggableComponent {...propsNotOfficial} />);
|
||||
|
||||
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(<SidebarDraggableComponent {...defaultProps} />);
|
||||
|
||||
expect(
|
||||
screen.queryByTestId("select-item-delete"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Style and Classes", () => {
|
||||
it("should apply correct border color", () => {
|
||||
render(<SidebarDraggableComponent {...defaultProps} />);
|
||||
|
||||
const draggableDiv = screen.getByTestId(/testsectiontest component/i);
|
||||
expect(draggableDiv).toHaveStyle({ borderLeftColor: "#FF0000" });
|
||||
});
|
||||
|
||||
it("should have correct tabIndex", () => {
|
||||
render(<SidebarDraggableComponent {...defaultProps} />);
|
||||
|
||||
const container = screen.getByTestId(
|
||||
"testsection_test component_draggable",
|
||||
);
|
||||
expect(container).toHaveAttribute("tabIndex", "0");
|
||||
});
|
||||
|
||||
it("should have add button with tabIndex -1", () => {
|
||||
render(<SidebarDraggableComponent {...defaultProps} />);
|
||||
|
||||
const addButton = screen.getByTestId(
|
||||
"add-component-button-test-component",
|
||||
);
|
||||
expect(addButton).toHaveAttribute("tabIndex", "-1");
|
||||
});
|
||||
|
||||
it("should have select trigger with tabIndex -1", () => {
|
||||
render(<SidebarDraggableComponent {...defaultProps} />);
|
||||
|
||||
expect(screen.getByTestId("select-trigger")).toHaveAttribute(
|
||||
"tabIndex",
|
||||
"-1",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Props Handling", () => {
|
||||
it("should handle different section names", () => {
|
||||
const propsWithDifferentSection = {
|
||||
...defaultProps,
|
||||
sectionName: "CustomSection",
|
||||
};
|
||||
|
||||
render(<SidebarDraggableComponent {...propsWithDifferentSection} />);
|
||||
|
||||
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(<SidebarDraggableComponent {...propsWithDifferentName} />);
|
||||
|
||||
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(<SidebarDraggableComponent {...propsWithDifferentIcon} />);
|
||||
|
||||
expect(
|
||||
screen.getByTestId("forwarded-icon-CustomIcon"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle different colors", () => {
|
||||
const propsWithDifferentColor = {
|
||||
...defaultProps,
|
||||
color: "#00FF00",
|
||||
};
|
||||
|
||||
render(<SidebarDraggableComponent {...propsWithDifferentColor} />);
|
||||
|
||||
const draggableDiv = screen.getByTestId(/testsectiontest component/i);
|
||||
expect(draggableDiv).toHaveStyle({ borderLeftColor: "#00FF00" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("Component Structure", () => {
|
||||
it("should have correct DOM hierarchy", () => {
|
||||
render(<SidebarDraggableComponent {...defaultProps} />);
|
||||
|
||||
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(<SidebarDraggableComponent {...defaultProps} />);
|
||||
|
||||
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(<SidebarDraggableComponent {...propsWithEmptyName} />);
|
||||
|
||||
expect(screen.getByTestId("display-name")).toHaveTextContent("");
|
||||
});
|
||||
|
||||
it("should handle missing color", () => {
|
||||
const propsWithoutColor = {
|
||||
...defaultProps,
|
||||
color: undefined as any,
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
render(<SidebarDraggableComponent {...propsWithoutColor} />);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("should handle missing onDragStart", () => {
|
||||
const propsWithoutDragStart = {
|
||||
...defaultProps,
|
||||
onDragStart: undefined as any,
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
render(<SidebarDraggableComponent {...propsWithoutDragStart} />);
|
||||
}).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(<SidebarDraggableComponent {...complexProps} />);
|
||||
|
||||
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(<SidebarDraggableComponent {...propsWithDisabledTooltip} />);
|
||||
|
||||
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(<SidebarDraggableComponent {...defaultProps} />);
|
||||
|
||||
const tooltips = screen.getAllByTestId("tooltip");
|
||||
expect(tooltips.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Select Positioning", () => {
|
||||
it("should render select content with correct positioning", () => {
|
||||
render(<SidebarDraggableComponent {...defaultProps} />);
|
||||
|
||||
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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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) => (
|
||||
<span data-testid={`icon-${name}`} className={className}>
|
||||
{name}
|
||||
</span>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock("@/components/common/shadTooltipComponent", () => ({
|
||||
__esModule: true,
|
||||
default: ({ children, content, side, styleClasses }: any) => (
|
||||
<div
|
||||
data-testid="tooltip"
|
||||
data-content={content}
|
||||
data-side={side}
|
||||
className={styleClasses}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock("@/components/ui/button", () => ({
|
||||
Button: ({ children, onClick, className, unstyled, ...props }: any) => (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={className}
|
||||
data-unstyled={unstyled}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
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(<SidebarFilterComponent {...defaultProps} />);
|
||||
|
||||
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(<SidebarFilterComponent {...defaultProps} />);
|
||||
|
||||
expect(screen.getByTestId("icon-ListFilter")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display X icon for reset button", () => {
|
||||
render(<SidebarFilterComponent {...defaultProps} />);
|
||||
|
||||
expect(screen.getByTestId("icon-X")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should have reset button with correct test id", () => {
|
||||
render(<SidebarFilterComponent {...defaultProps} />);
|
||||
|
||||
expect(screen.getByTestId("sidebar-filter-reset")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render tooltip with correct content", () => {
|
||||
render(<SidebarFilterComponent {...defaultProps} />);
|
||||
|
||||
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(<SidebarFilterComponent {...defaultProps} />);
|
||||
|
||||
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(<SidebarFilterComponent {...propsWithOutput} />);
|
||||
|
||||
expect(screen.getByText("Output:")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Input:")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display correct label for input", () => {
|
||||
render(<SidebarFilterComponent {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText("Input:")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display correct label for output", () => {
|
||||
const propsWithOutput = { ...defaultProps, isInput: false };
|
||||
render(<SidebarFilterComponent {...propsWithOutput} />);
|
||||
|
||||
expect(screen.getByText("Output:")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Type Display", () => {
|
||||
it("should display single type correctly", () => {
|
||||
render(<SidebarFilterComponent {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText("string")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display multiple types correctly", () => {
|
||||
const propsWithMultipleTypes = {
|
||||
...defaultProps,
|
||||
type: "string\nint\nboolean",
|
||||
};
|
||||
|
||||
render(<SidebarFilterComponent {...propsWithMultipleTypes} />);
|
||||
|
||||
expect(screen.getByText("string, int, boolean")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle empty type", () => {
|
||||
const propsWithEmptyType = { ...defaultProps, type: "" };
|
||||
render(<SidebarFilterComponent {...propsWithEmptyType} />);
|
||||
|
||||
// Should not crash
|
||||
expect(screen.getByTestId("icon-ListFilter")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle type with special characters", () => {
|
||||
const propsWithSpecialType = { ...defaultProps, type: "List[str]" };
|
||||
render(<SidebarFilterComponent {...propsWithSpecialType} />);
|
||||
|
||||
expect(screen.getByText("List[str]")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Pluralization", () => {
|
||||
it("should use singular form for single type", () => {
|
||||
render(<SidebarFilterComponent {...defaultProps} />);
|
||||
|
||||
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(<SidebarFilterComponent {...propsWithMultipleTypes} />);
|
||||
|
||||
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(<SidebarFilterComponent {...propsWithMultipleOutputTypes} />);
|
||||
|
||||
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(<SidebarFilterComponent {...propsWithEmptyLines} />);
|
||||
|
||||
// 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(<SidebarFilterComponent {...defaultProps} />);
|
||||
|
||||
// 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(<SidebarFilterComponent {...propsWithDifferentColor} />);
|
||||
|
||||
// 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(<SidebarFilterComponent {...propsWithCustomColor} />);
|
||||
|
||||
// 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(<SidebarFilterComponent {...defaultProps} />);
|
||||
|
||||
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(<SidebarFilterComponent {...defaultProps} />);
|
||||
|
||||
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(<SidebarFilterComponent {...defaultProps} />);
|
||||
|
||||
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(<SidebarFilterComponent {...propsWithDifferentCallback} />);
|
||||
|
||||
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(<SidebarFilterComponent {...defaultProps} />);
|
||||
|
||||
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(<SidebarFilterComponent {...defaultProps} />);
|
||||
|
||||
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(<SidebarFilterComponent {...defaultProps} />);
|
||||
|
||||
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(<SidebarFilterComponent {...defaultProps} />);
|
||||
|
||||
// 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(<SidebarFilterComponent {...defaultProps} />);
|
||||
|
||||
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(<SidebarFilterComponent {...defaultProps} />);
|
||||
|
||||
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(<SidebarFilterComponent {...defaultProps} />);
|
||||
|
||||
const tooltip = screen.getByTestId("tooltip");
|
||||
expect(tooltip).toHaveClass("max-w-full");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Props Handling", () => {
|
||||
it("should handle different isInput values", () => {
|
||||
const { rerender } = render(
|
||||
<SidebarFilterComponent {...defaultProps} isInput={true} />,
|
||||
);
|
||||
expect(screen.getByText("Input:")).toBeInTheDocument();
|
||||
|
||||
rerender(<SidebarFilterComponent {...defaultProps} isInput={false} />);
|
||||
expect(screen.getByText("Output:")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle different type values", () => {
|
||||
const { rerender } = render(
|
||||
<SidebarFilterComponent {...defaultProps} type="string" />,
|
||||
);
|
||||
expect(screen.getByText("string")).toBeInTheDocument();
|
||||
|
||||
rerender(<SidebarFilterComponent {...defaultProps} type="number" />);
|
||||
expect(screen.getByText("number")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle different color values", () => {
|
||||
const { rerender } = render(
|
||||
<SidebarFilterComponent {...defaultProps} color="blue" />,
|
||||
);
|
||||
expect(screen.getByText("string")).toBeInTheDocument();
|
||||
|
||||
rerender(<SidebarFilterComponent {...defaultProps} color="green" />);
|
||||
expect(screen.getByText("string")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle missing resetFilters function gracefully", () => {
|
||||
const propsWithoutCallback = {
|
||||
...defaultProps,
|
||||
resetFilters: undefined as any,
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
render(<SidebarFilterComponent {...propsWithoutCallback} />);
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Edge Cases", () => {
|
||||
it("should handle very long type names", () => {
|
||||
const propsWithLongType = {
|
||||
...defaultProps,
|
||||
type: "VeryLongTypeNameThatExceedsNormalLength",
|
||||
};
|
||||
|
||||
render(<SidebarFilterComponent {...propsWithLongType} />);
|
||||
|
||||
expect(
|
||||
screen.getByText("VeryLongTypeNameThatExceedsNormalLength"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle multiple very long type names", () => {
|
||||
const propsWithMultipleLongTypes = {
|
||||
...defaultProps,
|
||||
type: "FirstVeryLongTypeName\nSecondVeryLongTypeName\nThirdVeryLongTypeName",
|
||||
};
|
||||
|
||||
render(<SidebarFilterComponent {...propsWithMultipleLongTypes} />);
|
||||
|
||||
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(<SidebarFilterComponent {...propsWithComplexTypes} />);
|
||||
|
||||
expect(
|
||||
screen.getByText("List[str, int], Dict[str, Any]"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle empty color", () => {
|
||||
const propsWithEmptyColor = { ...defaultProps, color: "" };
|
||||
render(<SidebarFilterComponent {...propsWithEmptyColor} />);
|
||||
|
||||
// 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(<SidebarFilterComponent {...propsWithNewlineType} />);
|
||||
|
||||
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(<SidebarFilterComponent {...defaultProps} />);
|
||||
|
||||
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(<SidebarFilterComponent {...propsWithMultipleOutputTypes} />);
|
||||
|
||||
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(<SidebarFilterComponent {...propsWithMultipleTypes} />);
|
||||
|
||||
expect(
|
||||
screen.getByText("type1, type2, type3, type4"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Accessibility", () => {
|
||||
it("should have X icon with aria-hidden", () => {
|
||||
render(<SidebarFilterComponent {...defaultProps} />);
|
||||
|
||||
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(<SidebarFilterComponent {...defaultProps} />);
|
||||
|
||||
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(<SidebarFilterComponent {...propsWithCustomCallback} />);
|
||||
|
||||
const resetButton = screen.getByTestId("sidebar-filter-reset");
|
||||
await user.click(resetButton);
|
||||
|
||||
expect(customResetFilters).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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) => (
|
||||
<span data-testid={`icon-${name}`} className={className}>
|
||||
{name}
|
||||
</span>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock("@/components/ui/button", () => ({
|
||||
Button: ({
|
||||
children,
|
||||
onClick,
|
||||
className,
|
||||
disabled,
|
||||
unstyled,
|
||||
...props
|
||||
}: any) => (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={className}
|
||||
disabled={disabled}
|
||||
data-unstyled={unstyled}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock("@/components/ui/sidebar", () => ({
|
||||
SidebarMenuButton: ({ children, asChild }: any) => (
|
||||
<div data-testid="sidebar-menu-button" data-as-child={asChild}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock("@/customization/components/custom-link", () => ({
|
||||
CustomLink: ({ children, to, target, rel, className }: any) => (
|
||||
<a
|
||||
data-testid="custom-link"
|
||||
href={to}
|
||||
target={target}
|
||||
rel={rel}
|
||||
className={className}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
}));
|
||||
|
||||
// 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(<SidebarMenuButtons {...defaultProps} />);
|
||||
|
||||
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(<SidebarMenuButtons {...defaultProps} />);
|
||||
|
||||
expect(screen.getByTestId("sidebar-menu-button")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display correct text for custom component button", () => {
|
||||
render(<SidebarMenuButtons {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText("New Custom Component")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display Plus icon", () => {
|
||||
render(<SidebarMenuButtons {...defaultProps} />);
|
||||
|
||||
expect(screen.getByTestId("icon-Plus")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Store Link Rendering", () => {
|
||||
it("should not render store link when hasStore is false", () => {
|
||||
render(<SidebarMenuButtons {...defaultProps} />);
|
||||
|
||||
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(<SidebarMenuButtons {...propsWithStore} />);
|
||||
|
||||
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(<SidebarMenuButtons {...propsWithStore} />);
|
||||
|
||||
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(<SidebarMenuButtons {...propsWithStore} />);
|
||||
|
||||
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(<SidebarMenuButtons {...propsWithStore} />);
|
||||
|
||||
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(<SidebarMenuButtons {...defaultProps} />);
|
||||
|
||||
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(<SidebarMenuButtons {...propsWithoutCustomComponent} />);
|
||||
|
||||
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(<SidebarMenuButtons {...propsWithNullCustomComponent} />);
|
||||
|
||||
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(<SidebarMenuButtons {...defaultProps} />);
|
||||
|
||||
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(<SidebarMenuButtons {...propsWithLoading} />);
|
||||
|
||||
const customButton = screen.getByTestId(
|
||||
"sidebar-custom-component-button",
|
||||
);
|
||||
expect(customButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it("should not disable button when not loading", () => {
|
||||
render(<SidebarMenuButtons {...defaultProps} />);
|
||||
|
||||
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(<SidebarMenuButtons {...propsWithLoading} />);
|
||||
|
||||
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(<SidebarMenuButtons {...defaultProps} />);
|
||||
|
||||
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(<SidebarMenuButtons {...propsWithStore} />);
|
||||
|
||||
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(<SidebarMenuButtons {...defaultProps} />);
|
||||
|
||||
// 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(<SidebarMenuButtons {...defaultProps} />);
|
||||
|
||||
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(<SidebarMenuButtons {...propsWithStore} />);
|
||||
|
||||
const storeLink = screen.getByTestId("custom-link");
|
||||
expect(storeLink).toHaveClass("group/discover");
|
||||
});
|
||||
|
||||
it("should apply correct classes to icons", () => {
|
||||
const propsWithStore = { ...defaultProps, hasStore: true };
|
||||
render(<SidebarMenuButtons {...propsWithStore} />);
|
||||
|
||||
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(<SidebarMenuButtons {...minimalProps} />);
|
||||
|
||||
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(<SidebarMenuButtons {...fullProps} />);
|
||||
|
||||
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(<SidebarMenuButtons {...propsWithDifferentComponent} />);
|
||||
|
||||
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(<SidebarMenuButtons {...propsWithDifferentAddComponent} />);
|
||||
|
||||
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(<SidebarMenuButtons {...propsWithoutAddComponent} />);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("should handle boolean hasStore values", () => {
|
||||
const { rerender } = render(
|
||||
<SidebarMenuButtons {...defaultProps} hasStore={false} />,
|
||||
);
|
||||
expect(screen.queryByTestId("custom-link")).not.toBeInTheDocument();
|
||||
|
||||
rerender(<SidebarMenuButtons {...defaultProps} hasStore={true} />);
|
||||
expect(screen.getByTestId("custom-link")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle boolean isLoading values", () => {
|
||||
const { rerender } = render(
|
||||
<SidebarMenuButtons {...defaultProps} isLoading={false} />,
|
||||
);
|
||||
expect(
|
||||
screen.getByTestId("sidebar-custom-component-button"),
|
||||
).not.toBeDisabled();
|
||||
|
||||
rerender(<SidebarMenuButtons {...defaultProps} isLoading={true} />);
|
||||
expect(
|
||||
screen.getByTestId("sidebar-custom-component-button"),
|
||||
).toBeDisabled();
|
||||
});
|
||||
|
||||
it("should handle rapid prop changes", () => {
|
||||
const { rerender } = render(<SidebarMenuButtons {...defaultProps} />);
|
||||
|
||||
expect(screen.queryByTestId("custom-link")).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByTestId("sidebar-custom-component-button"),
|
||||
).not.toBeDisabled();
|
||||
|
||||
rerender(
|
||||
<SidebarMenuButtons
|
||||
{...defaultProps}
|
||||
hasStore={true}
|
||||
isLoading={true}
|
||||
/>,
|
||||
);
|
||||
|
||||
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(<SidebarMenuButtons {...propsWithStore} />);
|
||||
|
||||
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(<SidebarMenuButtons {...propsWithStore} />);
|
||||
|
||||
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(<SidebarMenuButtons {...propsWithStore} />);
|
||||
|
||||
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(<SidebarMenuButtons {...propsWithStore} />);
|
||||
|
||||
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(<SidebarMenuButtons {...defaultProps} />);
|
||||
|
||||
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(<SidebarMenuButtons {...propsWithThrowingFunction} />);
|
||||
|
||||
const customButton = screen.getByTestId(
|
||||
"sidebar-custom-component-button",
|
||||
);
|
||||
|
||||
// Should not crash the component
|
||||
expect(async () => {
|
||||
await user.click(customButton);
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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) => (
|
||||
<span data-testid={`forwarded-icon-${name}`} className={className}>
|
||||
{name}
|
||||
</span>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock("@/components/common/shadTooltipComponent", () => ({
|
||||
__esModule: true,
|
||||
default: ({ children, content, styleClasses }: any) => (
|
||||
<div data-testid="tooltip" data-content={content} className={styleClasses}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock("@/components/ui/button", () => ({
|
||||
Button: ({ children, onClick, variant, size, className, ...props }: any) => (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={className}
|
||||
data-variant={variant}
|
||||
data-size={size}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock("@/components/ui/disclosure", () => ({
|
||||
Disclosure: ({ children, open, onOpenChange }: any) => (
|
||||
<div
|
||||
data-testid="disclosure"
|
||||
data-open={open}
|
||||
data-on-open-change={onOpenChange?.toString()}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
DisclosureContent: ({ children }: any) => (
|
||||
<div data-testid="disclosure-content">{children}</div>
|
||||
),
|
||||
DisclosureTrigger: ({ children }: any) => (
|
||||
<div data-testid="disclosure-trigger">{children}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock("@/components/ui/sidebar", () => ({
|
||||
SidebarHeader: ({ children, className }: any) => (
|
||||
<div data-testid="sidebar-header" className={className}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
SidebarTrigger: ({ children, className }: any) => (
|
||||
<div data-testid="sidebar-trigger" className={className}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock("../featureTogglesComponent", () => ({
|
||||
__esModule: true,
|
||||
default: ({ showBeta, setShowBeta, showLegacy, setShowLegacy }: any) => (
|
||||
<div
|
||||
data-testid="feature-toggles"
|
||||
data-show-beta={showBeta}
|
||||
data-show-legacy={showLegacy}
|
||||
data-set-show-beta={setShowBeta?.toString()}
|
||||
data-set-show-legacy={setShowLegacy?.toString()}
|
||||
>
|
||||
Feature Toggles
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock("../searchInput", () => ({
|
||||
SearchInput: ({
|
||||
searchInputRef,
|
||||
isInputFocused,
|
||||
search,
|
||||
handleInputFocus,
|
||||
handleInputBlur,
|
||||
handleInputChange,
|
||||
}: any) => (
|
||||
<div
|
||||
data-testid="search-input"
|
||||
data-is-focused={isInputFocused}
|
||||
data-search={search}
|
||||
data-handle-focus={handleInputFocus?.toString()}
|
||||
data-handle-blur={handleInputBlur?.toString()}
|
||||
data-handle-change={handleInputChange?.toString()}
|
||||
>
|
||||
Search Input
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock("../sidebarFilterComponent", () => ({
|
||||
SidebarFilterComponent: ({ isInput, type, color, resetFilters }: any) => (
|
||||
<div
|
||||
data-testid="sidebar-filter"
|
||||
data-is-input={isInput}
|
||||
data-type={type}
|
||||
data-color={color}
|
||||
data-reset-filters={resetFilters?.toString()}
|
||||
>
|
||||
Filter Component
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
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(<SidebarHeaderComponent {...defaultProps} />);
|
||||
|
||||
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(<SidebarHeaderComponent {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText("Components")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render sidebar trigger with icon", () => {
|
||||
render(<SidebarHeaderComponent {...defaultProps} />);
|
||||
|
||||
expect(screen.getByTestId("sidebar-trigger")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByTestId("forwarded-icon-PanelLeftClose"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render settings button", () => {
|
||||
render(<SidebarHeaderComponent {...defaultProps} />);
|
||||
|
||||
expect(screen.getByTestId("sidebar-options-trigger")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByTestId("forwarded-icon-SlidersHorizontal"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render tooltip with correct content", () => {
|
||||
render(<SidebarHeaderComponent {...defaultProps} />);
|
||||
|
||||
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(<SidebarHeaderComponent {...defaultProps} />);
|
||||
|
||||
expect(screen.getByTestId("disclosure")).toHaveAttribute(
|
||||
"data-open",
|
||||
"false",
|
||||
);
|
||||
});
|
||||
|
||||
it("should render disclosure as open when showConfig is true", () => {
|
||||
const propsWithOpenConfig = { ...defaultProps, showConfig: true };
|
||||
render(<SidebarHeaderComponent {...propsWithOpenConfig} />);
|
||||
|
||||
expect(screen.getByTestId("disclosure")).toHaveAttribute(
|
||||
"data-open",
|
||||
"true",
|
||||
);
|
||||
});
|
||||
|
||||
it("should pass setShowConfig to disclosure", () => {
|
||||
render(<SidebarHeaderComponent {...defaultProps} />);
|
||||
|
||||
expect(screen.getByTestId("disclosure")).toHaveAttribute(
|
||||
"data-on-open-change",
|
||||
mockSetShowConfig.toString(),
|
||||
);
|
||||
});
|
||||
|
||||
it("should render disclosure content", () => {
|
||||
render(<SidebarHeaderComponent {...defaultProps} />);
|
||||
|
||||
expect(screen.getByTestId("disclosure-content")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render disclosure trigger", () => {
|
||||
render(<SidebarHeaderComponent {...defaultProps} />);
|
||||
|
||||
expect(screen.getByTestId("disclosure-trigger")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Settings Button Variants", () => {
|
||||
it("should show ghost variant when config is closed", () => {
|
||||
render(<SidebarHeaderComponent {...defaultProps} />);
|
||||
|
||||
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(<SidebarHeaderComponent {...propsWithOpenConfig} />);
|
||||
|
||||
const settingsButton = screen.getByTestId("sidebar-options-trigger");
|
||||
expect(settingsButton).toHaveAttribute("data-variant", "ghostActive");
|
||||
});
|
||||
|
||||
it("should have correct size", () => {
|
||||
render(<SidebarHeaderComponent {...defaultProps} />);
|
||||
|
||||
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(<SidebarHeaderComponent {...defaultProps} />);
|
||||
|
||||
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(<SidebarHeaderComponent {...propsWithToggles} />);
|
||||
|
||||
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(<SidebarHeaderComponent {...propsWithSearch} />);
|
||||
|
||||
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(<SidebarHeaderComponent {...defaultProps} />);
|
||||
|
||||
expect(screen.getByTestId("search-input")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Filter Component Conditional Rendering", () => {
|
||||
it("should not render filter component when filterType is null", () => {
|
||||
render(<SidebarHeaderComponent {...defaultProps} />);
|
||||
|
||||
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(<SidebarHeaderComponent {...propsWithFilter} />);
|
||||
|
||||
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(<SidebarHeaderComponent {...propsWithInputFilter} />);
|
||||
|
||||
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(<SidebarHeaderComponent {...propsWithOutputFilter} />);
|
||||
|
||||
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(<SidebarHeaderComponent {...propsWithFilter} />);
|
||||
|
||||
// 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(<SidebarHeaderComponent {...defaultProps} />);
|
||||
|
||||
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(<SidebarHeaderComponent {...defaultProps} />);
|
||||
|
||||
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(<SidebarHeaderComponent {...defaultProps} />);
|
||||
|
||||
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(<SidebarHeaderComponent {...defaultProps} />);
|
||||
|
||||
const sidebarTrigger = screen.getByTestId("sidebar-trigger");
|
||||
expect(sidebarTrigger).toHaveClass("text-muted-foreground");
|
||||
});
|
||||
|
||||
it("should apply correct classes to title", () => {
|
||||
render(<SidebarHeaderComponent {...defaultProps} />);
|
||||
|
||||
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(<SidebarHeaderComponent {...defaultProps} />);
|
||||
|
||||
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(
|
||||
<SidebarHeaderComponent {...defaultProps} showConfig={false} />,
|
||||
);
|
||||
expect(screen.getByTestId("disclosure")).toHaveAttribute(
|
||||
"data-open",
|
||||
"false",
|
||||
);
|
||||
expect(screen.getByTestId("sidebar-options-trigger")).toHaveAttribute(
|
||||
"data-variant",
|
||||
"ghost",
|
||||
);
|
||||
|
||||
rerender(<SidebarHeaderComponent {...defaultProps} showConfig={true} />);
|
||||
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(
|
||||
<SidebarHeaderComponent
|
||||
{...defaultProps}
|
||||
search=""
|
||||
isInputFocused={false}
|
||||
/>,
|
||||
);
|
||||
let searchInput = screen.getByTestId("search-input");
|
||||
expect(searchInput).toHaveAttribute("data-search", "");
|
||||
expect(searchInput).toHaveAttribute("data-is-focused", "false");
|
||||
|
||||
rerender(
|
||||
<SidebarHeaderComponent
|
||||
{...defaultProps}
|
||||
search="test"
|
||||
isInputFocused={true}
|
||||
/>,
|
||||
);
|
||||
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(
|
||||
<SidebarHeaderComponent
|
||||
{...defaultProps}
|
||||
setShowConfig={alternativeSetShowConfig}
|
||||
handleInputChange={alternativeHandleInputChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
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(<SidebarHeaderComponent {...propsWithoutCallbacks} />);
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it("should handle null filterType gracefully", () => {
|
||||
render(
|
||||
<SidebarHeaderComponent {...defaultProps} filterType={undefined} />,
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId("sidebar-filter")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle undefined filterType gracefully", () => {
|
||||
const propsWithUndefinedFilter = {
|
||||
...defaultProps,
|
||||
filterType: undefined as any,
|
||||
};
|
||||
|
||||
render(<SidebarHeaderComponent {...propsWithUndefinedFilter} />);
|
||||
|
||||
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(<SidebarHeaderComponent {...propsWithComplexFilter} />);
|
||||
|
||||
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(<SidebarHeaderComponent {...defaultProps} />);
|
||||
|
||||
// Component should render without issues
|
||||
expect(screen.getByTestId("sidebar-header")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle prop changes correctly", () => {
|
||||
const { rerender } = render(<SidebarHeaderComponent {...defaultProps} />);
|
||||
|
||||
expect(screen.getByTestId("disclosure")).toHaveAttribute(
|
||||
"data-open",
|
||||
"false",
|
||||
);
|
||||
|
||||
rerender(<SidebarHeaderComponent {...defaultProps} showConfig={true} />);
|
||||
|
||||
expect(screen.getByTestId("disclosure")).toHaveAttribute(
|
||||
"data-open",
|
||||
"true",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Accessibility", () => {
|
||||
it("should have proper heading structure", () => {
|
||||
render(<SidebarHeaderComponent {...defaultProps} />);
|
||||
|
||||
const title = screen.getByText("Components");
|
||||
expect(title.tagName).toBe("H3");
|
||||
});
|
||||
|
||||
it("should render tooltip for settings button", () => {
|
||||
render(<SidebarHeaderComponent {...defaultProps} />);
|
||||
|
||||
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(<SidebarHeaderComponent {...propsWithFilter} />);
|
||||
|
||||
expect(screen.getByTestId("feature-toggles")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("search-input")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("sidebar-filter")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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) => (
|
||||
<div data-testid="tooltip" data-content={content} data-side={side}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
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) => (
|
||||
<div
|
||||
data-testid={`draggable-${itemName}`}
|
||||
data-section-name={sectionName}
|
||||
data-icon={icon}
|
||||
data-color={color}
|
||||
data-item-name={itemName}
|
||||
data-error={error}
|
||||
data-display-name={display_name}
|
||||
data-official={official}
|
||||
data-beta={beta}
|
||||
data-legacy={legacy}
|
||||
data-disabled={disabled}
|
||||
data-disabled-tooltip={disabledTooltip}
|
||||
onClick={() =>
|
||||
onDragStart?.({ type: "dragstart" }, { type: itemName, node: apiClass })
|
||||
}
|
||||
>
|
||||
{display_name}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
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(<SidebarItemsList {...defaultProps} />);
|
||||
|
||||
const mainContainer = container.firstChild as HTMLElement;
|
||||
expect(mainContainer).toHaveClass("flex", "flex-col", "gap-1", "py-2");
|
||||
});
|
||||
|
||||
it("should render all components from dataFilter", () => {
|
||||
render(<SidebarItemsList {...defaultProps} />);
|
||||
|
||||
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(<SidebarItemsList {...defaultProps} />);
|
||||
|
||||
const tooltips = screen.getAllByTestId("tooltip");
|
||||
expect(tooltips).toHaveLength(4); // One for each component
|
||||
});
|
||||
|
||||
it("should display component names in tooltips", () => {
|
||||
render(<SidebarItemsList {...defaultProps} />);
|
||||
|
||||
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(<SidebarItemsList {...defaultProps} />);
|
||||
|
||||
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(<SidebarItemsList {...defaultProps} />);
|
||||
|
||||
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(<SidebarItemsList {...propsWithEqualPriorities} />);
|
||||
|
||||
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(<SidebarItemsList {...propsWithoutScores} />);
|
||||
|
||||
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(<SidebarItemsList {...propsWithMissingPriorities} />);
|
||||
|
||||
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(<SidebarItemsList {...defaultProps} />);
|
||||
|
||||
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(<SidebarItemsList {...defaultProps} />);
|
||||
|
||||
const webhook = screen.getByTestId("draggable-Webhook");
|
||||
expect(webhook).toHaveAttribute("data-error", "true");
|
||||
});
|
||||
|
||||
it("should handle component with beta flag", () => {
|
||||
render(<SidebarItemsList {...defaultProps} />);
|
||||
|
||||
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(<SidebarItemsList {...defaultProps} />);
|
||||
|
||||
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(<SidebarItemsList {...propsWithMissingIcon} />);
|
||||
|
||||
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(<SidebarItemsList {...propsWithNoIcon} />);
|
||||
|
||||
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(<SidebarItemsList {...defaultProps} />);
|
||||
|
||||
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(<SidebarItemsList {...defaultProps} />);
|
||||
|
||||
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(<SidebarItemsList {...defaultProps} />);
|
||||
|
||||
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(<SidebarItemsList {...defaultProps} />);
|
||||
|
||||
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(<SidebarItemsList {...propsWithCount} />);
|
||||
|
||||
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(<SidebarItemsList {...propsWithEmptyData} />);
|
||||
|
||||
expect(screen.queryByTestId(/^draggable-/)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle missing node colors", () => {
|
||||
const propsWithoutColors = {
|
||||
...defaultProps,
|
||||
nodeColors: {},
|
||||
};
|
||||
|
||||
render(<SidebarItemsList {...propsWithoutColors} />);
|
||||
|
||||
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(<SidebarItemsList {...propsWithUndefinedOfficial} />);
|
||||
|
||||
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(<SidebarItemsList {...propsWithMissingFlags} />);
|
||||
|
||||
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(<SidebarItemsList {...defaultProps} />);
|
||||
|
||||
const mainContainer = container.firstChild as HTMLElement;
|
||||
expect(mainContainer).toHaveClass("flex", "flex-col", "gap-1", "py-2");
|
||||
});
|
||||
|
||||
it("should contain all expected child elements", () => {
|
||||
render(<SidebarItemsList {...defaultProps} />);
|
||||
|
||||
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(<SidebarItemsList {...propsWithDifferentItem} />);
|
||||
|
||||
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(<SidebarItemsList {...propsWithDifferentCallbacks} />);
|
||||
|
||||
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(<SidebarItemsList {...defaultProps} />);
|
||||
|
||||
// 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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(
|
|||
<DisclosureTrigger className="group/collapsible">
|
||||
<SidebarMenuButton asChild>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => handleKeyDownInput(e, item.name)}
|
||||
className="user-select-none flex cursor-pointer items-center gap-2"
|
||||
|
|
@ -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({
|
|||
<SidebarMenuButton asChild>
|
||||
<div
|
||||
data-testid={`disclosure-${item.display_name.toLocaleLowerCase()}`}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={handleKeyDownInput}
|
||||
className="user-select-none flex cursor-pointer items-center gap-2"
|
||||
|
|
@ -5,8 +5,8 @@ import {
|
|||
SidebarMenu,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { SIDEBAR_BUNDLES } from "@/utils/styleUtils";
|
||||
import type { CategoryGroupProps } from "../../types";
|
||||
import { CategoryDisclosure } from "../categoryDisclouse";
|
||||
import type { CategoryGroupProps } from "../types";
|
||||
import { CategoryDisclosure } from "./categoryDisclouse";
|
||||
|
||||
export const CategoryGroup = memo(function CategoryGroup({
|
||||
dataFilter,
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { memo } from "react";
|
||||
import { ForwardedIconComponent } from "@/components/common/genericIconComponent";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import ShortcutDisplay from "../../../nodeToolbarComponent/shortcutDisplay";
|
||||
import ShortcutDisplay from "../../nodeToolbarComponent/shortcutDisplay";
|
||||
|
||||
export const SearchInput = memo(function SearchInput({
|
||||
searchInputRef,
|
||||
|
|
@ -5,8 +5,8 @@ import {
|
|||
SidebarGroupLabel,
|
||||
SidebarMenu,
|
||||
} from "@/components/ui/sidebar";
|
||||
import type { SidebarGroupProps } from "../../types";
|
||||
import { BundleItem } from "../bundleItems";
|
||||
import type { SidebarGroupProps } from "../types";
|
||||
import { BundleItem } from "./bundleItems";
|
||||
|
||||
export const MemoizedSidebarGroup = memo(
|
||||
({
|
||||
|
|
@ -1,28 +1,28 @@
|
|||
import { type DragEventHandler, forwardRef, useRef, useState } from "react";
|
||||
import IconComponent, {
|
||||
ForwardedIconComponent,
|
||||
} from "@/components/common/genericIconComponent";
|
||||
import ShadTooltip from "@/components/common/shadTooltipComponent";
|
||||
import { convertTestName } from "@/components/common/storeCardComponent/utils/convert-test-name";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import useDeleteFlow from "@/hooks/flows/use-delete-flow";
|
||||
import { useAddComponent } from "@/hooks/use-add-component";
|
||||
import IconComponent, {
|
||||
ForwardedIconComponent,
|
||||
} from "../../../../../../components/common/genericIconComponent";
|
||||
import ShadTooltip from "../../../../../../components/common/shadTooltipComponent";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
} from "../../../../../../components/ui/select-custom";
|
||||
import { useDarkStore } from "../../../../../../stores/darkStore";
|
||||
import useFlowsManagerStore from "../../../../../../stores/flowsManagerStore";
|
||||
import type { APIClassType } from "../../../../../../types/api";
|
||||
} from "@/components/ui/select-custom";
|
||||
import useDeleteFlow from "@/hooks/flows/use-delete-flow";
|
||||
import { useAddComponent } from "@/hooks/use-add-component";
|
||||
import { useDarkStore } from "@/stores/darkStore";
|
||||
import useFlowsManagerStore from "@/stores/flowsManagerStore";
|
||||
import type { APIClassType } from "@/types/api";
|
||||
import {
|
||||
createFlowComponent,
|
||||
downloadNode,
|
||||
getNodeId,
|
||||
} from "../../../../../../utils/reactflowUtils";
|
||||
import { cn, removeCountFromString } from "../../../../../../utils/utils";
|
||||
} from "@/utils/reactflowUtils";
|
||||
import { cn, removeCountFromString } from "@/utils/utils";
|
||||
|
||||
export const SidebarDraggableComponent = forwardRef(
|
||||
(
|
||||
|
|
@ -159,7 +159,10 @@ export const SidebarDraggableComponent = forwardRef(
|
|||
/>
|
||||
<div className="flex flex-1 items-center overflow-hidden">
|
||||
<ShadTooltip content={display_name} styleClasses="z-50">
|
||||
<span className="truncate text-sm font-normal">
|
||||
<span
|
||||
data-testid="display-name"
|
||||
className="truncate text-sm font-normal"
|
||||
>
|
||||
{display_name}
|
||||
</span>
|
||||
</ShadTooltip>
|
||||
|
|
@ -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,
|
||||
|
|
@ -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 (
|
||||
<UniqueInputsDraggableComponent
|
||||
key={SBItemName}
|
||||
item={item}
|
||||
currentItem={currentItem}
|
||||
SBItemName={SBItemName}
|
||||
idx={idx}
|
||||
onDragStart={onDragStart}
|
||||
nodeColors={nodeColors}
|
||||
/>
|
||||
|
|
@ -55,7 +55,7 @@ const SidebarItemsList = ({
|
|||
<ShadTooltip
|
||||
content={currentItem.display_name}
|
||||
side="right"
|
||||
key={idx}
|
||||
key={SBItemName}
|
||||
>
|
||||
<SidebarDraggableComponent
|
||||
sectionName={item.name}
|
||||
|
|
@ -71,7 +71,7 @@ const SidebarItemsList = ({
|
|||
itemName={SBItemName}
|
||||
error={!!currentItem.error}
|
||||
display_name={currentItem.display_name}
|
||||
official={currentItem.official === false ? false : true}
|
||||
official={currentItem.official !== false}
|
||||
beta={currentItem.beta ?? false}
|
||||
legacy={currentItem.legacy ?? false}
|
||||
disabled={false}
|
||||
|
|
@ -90,7 +90,6 @@ const UniqueInputsDraggableComponent = ({
|
|||
item,
|
||||
currentItem,
|
||||
SBItemName,
|
||||
idx,
|
||||
onDragStart,
|
||||
nodeColors,
|
||||
}) => {
|
||||
|
|
@ -105,7 +104,11 @@ const UniqueInputsDraggableComponent = ({
|
|||
}, [chatInputAdded, webhookInputAdded]);
|
||||
|
||||
return (
|
||||
<ShadTooltip content={currentItem.display_name} side="right" key={idx}>
|
||||
<ShadTooltip
|
||||
content={currentItem.display_name}
|
||||
side="right"
|
||||
key={SBItemName}
|
||||
>
|
||||
<SidebarDraggableComponent
|
||||
sectionName={item.name}
|
||||
apiClass={currentItem}
|
||||
|
|
@ -120,7 +123,7 @@ const UniqueInputsDraggableComponent = ({
|
|||
itemName={SBItemName}
|
||||
error={!!currentItem.error}
|
||||
display_name={currentItem.display_name}
|
||||
official={currentItem.official === false ? false : true}
|
||||
official={currentItem.official !== false}
|
||||
beta={currentItem.beta ?? false}
|
||||
legacy={currentItem.legacy ?? false}
|
||||
disabled={disableItem(SBItemName, uniqueInputsComponents)}
|
||||
55
src/frontend/src/utils/testUtils/mockData/mockAPIData.ts
Normal file
55
src/frontend/src/utils/testUtils/mockData/mockAPIData.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import type {
|
||||
APIClassType,
|
||||
APIDataType,
|
||||
APITemplateType,
|
||||
InputFieldType,
|
||||
} from "@/types/api";
|
||||
|
||||
const mockTemplate: APITemplateType = {
|
||||
api_key: {
|
||||
load_from_db: true,
|
||||
required: true,
|
||||
placeholder: "",
|
||||
show: true,
|
||||
list: false,
|
||||
readonly: false,
|
||||
name: "api_key",
|
||||
value: "",
|
||||
display_name: "API Key",
|
||||
advanced: false,
|
||||
input_types: [],
|
||||
dynamic: false,
|
||||
info: "Your Mock Component API key",
|
||||
title_case: false,
|
||||
password: true,
|
||||
type: "str",
|
||||
_input_type: "SecretStrInput",
|
||||
},
|
||||
};
|
||||
|
||||
const mockAPIData: APIDataType = {
|
||||
mockComponent: {
|
||||
MockComponent: {
|
||||
template: mockTemplate,
|
||||
description: "mocks the component",
|
||||
icon: "cirleQuestionMark",
|
||||
base_classes: ["Data"],
|
||||
display_name: "Mock Component",
|
||||
documentation: "https://docs.langflow.org/",
|
||||
minimized: false,
|
||||
custom_fields: {},
|
||||
output_types: [],
|
||||
pinned: false,
|
||||
conditional_paths: [],
|
||||
frozen: false,
|
||||
outputs: [],
|
||||
field_order: ["api_key"],
|
||||
beta: false,
|
||||
legacy: false,
|
||||
edited: false,
|
||||
tool_mode: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default mockAPIData;
|
||||
|
|
@ -141,9 +141,13 @@ test(
|
|||
numberOfOutdatedComponents++;
|
||||
}
|
||||
|
||||
await page.getByTestId("icon-ChevronLeft").click();
|
||||
await page.waitForSelector('[data-testid="mainpage_title"]', {
|
||||
timeout: 5000,
|
||||
await Promise.all([
|
||||
page.waitForURL((url) => url.pathname === "/", { timeout: 30000 }),
|
||||
page.getByTestId("icon-ChevronLeft").click(),
|
||||
]);
|
||||
|
||||
await expect(page.getByTestId("mainpage_title")).toBeVisible({
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
await page.waitForTimeout(500);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue