diff --git a/src/test/calendar_container_test.test.tsx b/src/test/calendar_container_test.test.tsx
new file mode 100644
index 000000000..a9a11b46a
--- /dev/null
+++ b/src/test/calendar_container_test.test.tsx
@@ -0,0 +1,110 @@
+/**
+ * Test suite for CalendarContainer component
+ *
+ * CalendarContainer is a wrapper component that provides accessibility features
+ * for the datepicker calendar. It renders as a dialog with appropriate ARIA attributes.
+ *
+ * @see ../calendar_container.tsx
+ */
+import React from "react";
+import { render } from "@testing-library/react";
+import CalendarContainer from "../calendar_container";
+
+describe("CalendarContainer", () => {
+ /**
+ * Test: Default rendering behavior
+ * Verifies that the component renders with correct default ARIA attributes
+ * and displays "Choose Date" as the default aria-label.
+ */
+ it("should render with default props", () => {
+ const { container } = render(
+
+ Test Content
+ ,
+ );
+
+ const dialog = container.querySelector('[role="dialog"]');
+ expect(dialog).not.toBeNull();
+ expect(dialog?.getAttribute("aria-label")).toBe("Choose Date");
+ expect(dialog?.getAttribute("aria-modal")).toBe("true");
+ expect(dialog?.textContent).toBe("Test Content");
+ });
+
+ /**
+ * Test: Time selection mode
+ * When showTime is true, the aria-label should indicate both date and time selection.
+ */
+ it("should render with showTime prop", () => {
+ const { container } = render(
+
+ Test Content
+ ,
+ );
+
+ const dialog = container.querySelector('[role="dialog"]');
+ expect(dialog?.getAttribute("aria-label")).toBe("Choose Date and Time");
+ });
+
+ /**
+ * Test: Time-only selection mode
+ * When showTimeSelectOnly is true, the aria-label should indicate only time selection.
+ */
+ it("should render with showTimeSelectOnly prop", () => {
+ const { container } = render(
+
+ Test Content
+ ,
+ );
+
+ const dialog = container.querySelector('[role="dialog"]');
+ expect(dialog?.getAttribute("aria-label")).toBe("Choose Time");
+ });
+
+ /**
+ * Test: Custom styling
+ * Verifies that custom CSS classes are properly applied to the dialog element.
+ */
+ it("should apply custom className", () => {
+ const { container } = render(
+
+ Test Content
+ ,
+ );
+
+ const dialog = container.querySelector('[role="dialog"]');
+ expect(dialog?.className).toBe("custom-class");
+ });
+
+ /**
+ * Test: Multiple children rendering
+ * Ensures the component can properly render multiple child elements.
+ */
+ it("should render multiple children", () => {
+ const { container } = render(
+
+ Child 1
+ Child 2
+ Child 3
+ ,
+ );
+
+ const dialog = container.querySelector('[role="dialog"]');
+ expect(dialog?.children.length).toBe(3);
+ });
+
+ /**
+ * Test: HTML attribute passthrough
+ * Verifies that additional HTML attributes are correctly passed to the dialog element.
+ */
+ it("should pass through additional HTML attributes", () => {
+ const { container } = render(
+
+ Test Content
+ ,
+ );
+
+ const dialog = container.querySelector('[role="dialog"]');
+ expect(dialog?.getAttribute("data-testid")).toBe("test-container");
+ expect(dialog?.getAttribute("id")).toBe("calendar-id");
+ });
+});
diff --git a/src/test/click_outside_wrapper_test.test.tsx b/src/test/click_outside_wrapper_test.test.tsx
new file mode 100644
index 000000000..b5adbb86d
--- /dev/null
+++ b/src/test/click_outside_wrapper_test.test.tsx
@@ -0,0 +1,246 @@
+/**
+ * Test suite for ClickOutsideWrapper component
+ *
+ * ClickOutsideWrapper is a utility component that detects clicks outside of its children
+ * and triggers a callback. It's commonly used for closing dropdowns, modals, and popovers.
+ *
+ * Key features tested:
+ * - Click detection (inside vs outside)
+ * - Ignore class functionality
+ * - Event listener cleanup
+ * - Composed events support (Shadow DOM)
+ * - Ref forwarding
+ *
+ * @see ../click_outside_wrapper.tsx
+ */
+import React from "react";
+import { render, fireEvent } from "@testing-library/react";
+import { ClickOutsideWrapper } from "../click_outside_wrapper";
+
+describe("ClickOutsideWrapper", () => {
+ /**
+ * Test: Basic rendering
+ * Verifies that children are rendered correctly within the wrapper.
+ */
+ it("should render children", () => {
+ const { getByText } = render(
+
+ Test Content
+ ,
+ );
+
+ expect(getByText("Test Content")).toBeTruthy();
+ });
+
+ /**
+ * Test: Outside click detection
+ * When a user clicks outside the wrapper, the onClickOutside callback should be triggered.
+ */
+ it("should call onClickOutside when clicking outside", () => {
+ const handleClickOutside = jest.fn();
+ const { container } = render(
+
+
+ Inside Content
+
+
Outside Content
+
,
+ );
+
+ const outsideElement = container.querySelector('[data-testid="outside"]');
+ fireEvent.mouseDown(outsideElement!);
+
+ expect(handleClickOutside).toHaveBeenCalledTimes(1);
+ });
+
+ /**
+ * Test: Inside click handling
+ * Clicks inside the wrapper should NOT trigger the onClickOutside callback.
+ */
+ it("should not call onClickOutside when clicking inside", () => {
+ const handleClickOutside = jest.fn();
+ const { getByText } = render(
+
+ Inside Content
+ ,
+ );
+
+ fireEvent.mouseDown(getByText("Inside Content"));
+
+ expect(handleClickOutside).not.toHaveBeenCalled();
+ });
+
+ /**
+ * Test: Custom styling
+ * Verifies that custom CSS classes are properly applied to the wrapper element.
+ */
+ it("should apply custom className", () => {
+ const { container } = render(
+
+ Test Content
+ ,
+ );
+
+ expect(container.firstChild).toHaveClass("custom-class");
+ });
+
+ /**
+ * Test: Inline styles
+ * Ensures that inline styles are correctly applied to the wrapper element.
+ */
+ it("should apply custom style", () => {
+ const customStyle = { backgroundColor: "red", padding: "10px" };
+ const { container } = render(
+
+ Test Content
+ ,
+ );
+
+ const wrapper = container.firstChild as HTMLElement;
+ expect(wrapper.style.backgroundColor).toBe("red");
+ expect(wrapper.style.padding).toBe("10px");
+ });
+
+ /**
+ * Test: Ref forwarding
+ * When a containerRef is provided, it should be properly assigned to the wrapper element.
+ */
+ it("should use containerRef when provided", () => {
+ const containerRef = React.createRef();
+ render(
+
+ Test Content
+ ,
+ );
+
+ expect(containerRef.current).not.toBeNull();
+ expect(containerRef.current?.tagName).toBe("DIV");
+ });
+
+ /**
+ * Test: Ignore class functionality
+ * Elements with the specified ignoreClass should not trigger onClickOutside,
+ * while other outside elements should still trigger it.
+ */
+ it("should ignore clicks on elements with ignoreClass", () => {
+ const handleClickOutside = jest.fn();
+ const { container } = render(
+
+
+ Inside Content
+
+
+ Ignored Content
+
+
Not Ignored Content
+
,
+ );
+
+ const ignoredElement = container.querySelector('[data-testid="ignored"]');
+ fireEvent.mouseDown(ignoredElement!);
+ expect(handleClickOutside).not.toHaveBeenCalled();
+
+ const notIgnoredElement = container.querySelector(
+ '[data-testid="not-ignored"]',
+ );
+ fireEvent.mouseDown(notIgnoredElement!);
+ expect(handleClickOutside).toHaveBeenCalledTimes(1);
+ });
+
+ /**
+ * Test: Composed events (Shadow DOM support)
+ * Tests that the component correctly handles composed events which traverse
+ * Shadow DOM boundaries using composedPath().
+ */
+ it("should handle composed events with composedPath", () => {
+ const handleClickOutside = jest.fn();
+ const { container } = render(
+
+
+ Inside Content
+
+
Outside Content
+
,
+ );
+
+ const outsideElement = container.querySelector('[data-testid="outside"]');
+ const event = new MouseEvent("mousedown", {
+ bubbles: true,
+ composed: true,
+ });
+
+ // Mock composedPath to simulate Shadow DOM event traversal
+ Object.defineProperty(event, "composedPath", {
+ value: () => [outsideElement, container, document.body, document],
+ });
+
+ outsideElement?.dispatchEvent(event);
+
+ expect(handleClickOutside).toHaveBeenCalledTimes(1);
+ });
+
+ /**
+ * Test: Memory leak prevention
+ * Ensures that event listeners are properly removed when the component unmounts
+ * to prevent memory leaks.
+ */
+ it("should cleanup event listener on unmount", () => {
+ const handleClickOutside = jest.fn();
+ const removeEventListenerSpy = jest.spyOn(document, "removeEventListener");
+
+ const { unmount } = render(
+
+ Test Content
+ ,
+ );
+
+ unmount();
+
+ expect(removeEventListenerSpy).toHaveBeenCalledWith(
+ "mousedown",
+ expect.any(Function),
+ );
+
+ removeEventListenerSpy.mockRestore();
+ });
+
+ /**
+ * Test: Dynamic handler updates
+ * When the onClickOutside prop changes, the new handler should be used
+ * instead of the old one.
+ */
+ it("should update onClickOutside handler when prop changes", () => {
+ const firstHandler = jest.fn();
+ const secondHandler = jest.fn();
+
+ const { rerender, container } = render(
+
+
+ Inside Content
+
+
Outside Content
+
,
+ );
+
+ rerender(
+
+
+ Inside Content
+
+
Outside Content
+
,
+ );
+
+ const outsideElement = container.querySelector('[data-testid="outside"]');
+ fireEvent.mouseDown(outsideElement!);
+
+ expect(firstHandler).not.toHaveBeenCalled();
+ expect(secondHandler).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/src/test/month_dropdown_options_test.test.tsx b/src/test/month_dropdown_options_test.test.tsx
new file mode 100644
index 000000000..1bedfd4a3
--- /dev/null
+++ b/src/test/month_dropdown_options_test.test.tsx
@@ -0,0 +1,375 @@
+import React from "react";
+import { render, fireEvent } from "@testing-library/react";
+import MonthDropdownOptions from "../month_dropdown_options";
+
+// Mock ClickOutsideWrapper
+jest.mock("../click_outside_wrapper", () => ({
+ ClickOutsideWrapper: ({
+ children,
+ onClickOutside,
+ className,
+ }: {
+ children: React.ReactNode;
+ onClickOutside: () => void;
+ className?: string;
+ }) => (
+ {
+ if ((e.target as HTMLElement).getAttribute("data-outside")) {
+ onClickOutside();
+ }
+ }}
+ >
+ {children}
+
+ ),
+}));
+
+const monthNames = [
+ "January",
+ "February",
+ "March",
+ "April",
+ "May",
+ "June",
+ "July",
+ "August",
+ "September",
+ "October",
+ "November",
+ "December",
+];
+
+describe("MonthDropdownOptions", () => {
+ const defaultProps = {
+ onCancel: jest.fn(),
+ onChange: jest.fn(),
+ month: 0,
+ monthNames,
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it("should render all month options", () => {
+ const { getByText } = render( );
+
+ monthNames.forEach((monthName) => {
+ expect(getByText(monthName)).toBeTruthy();
+ });
+ });
+
+ it("should mark selected month with checkmark", () => {
+ const { container } = render(
+ ,
+ );
+
+ const selectedOption = container.querySelector(
+ ".react-datepicker__month-option--selected_month",
+ );
+ expect(selectedOption?.textContent).toContain("✓");
+ expect(selectedOption?.textContent).toContain("April");
+ });
+
+ it("should call onChange when month is clicked", () => {
+ const handleChange = jest.fn();
+ const { getByText } = render(
+ ,
+ );
+
+ fireEvent.click(getByText("June"));
+
+ expect(handleChange).toHaveBeenCalledWith(5);
+ });
+
+ it("should call onChange with correct index", () => {
+ const handleChange = jest.fn();
+ const { getByText } = render(
+ ,
+ );
+
+ fireEvent.click(getByText("December"));
+
+ expect(handleChange).toHaveBeenCalledWith(11);
+ });
+
+ it("should call onCancel when Escape key is pressed", () => {
+ const handleCancel = jest.fn();
+ const { container } = render(
+ ,
+ );
+
+ const monthOption = container.querySelector(
+ ".react-datepicker__month-option",
+ );
+ fireEvent.keyDown(monthOption!, { key: "Escape" });
+
+ expect(handleCancel).toHaveBeenCalled();
+ });
+
+ it("should call onChange when Enter key is pressed", () => {
+ const handleChange = jest.fn();
+ const { container } = render(
+ ,
+ );
+
+ const monthOptions = container.querySelectorAll(
+ ".react-datepicker__month-option",
+ );
+ const monthOption = monthOptions[5];
+ if (monthOption) {
+ fireEvent.keyDown(monthOption, { key: "Enter" });
+ }
+
+ expect(handleChange).toHaveBeenCalledWith(5);
+ });
+
+ it("should focus next month on ArrowDown", () => {
+ const { container } = render(
+ ,
+ );
+
+ const aprilOption = container.querySelectorAll(
+ ".react-datepicker__month-option",
+ )[3] as HTMLElement;
+
+ fireEvent.keyDown(aprilOption, { key: "ArrowDown" });
+
+ const mayOption = container.querySelectorAll(
+ ".react-datepicker__month-option",
+ )[4] as HTMLElement;
+
+ expect(document.activeElement).toBe(mayOption);
+ });
+
+ it("should focus previous month on ArrowUp", () => {
+ const { container } = render(
+ ,
+ );
+
+ const juneOption = container.querySelectorAll(
+ ".react-datepicker__month-option",
+ )[5] as HTMLElement;
+
+ fireEvent.keyDown(juneOption, { key: "ArrowUp" });
+
+ const mayOption = container.querySelectorAll(
+ ".react-datepicker__month-option",
+ )[4] as HTMLElement;
+
+ expect(document.activeElement).toBe(mayOption);
+ });
+
+ it("should wrap around to December when pressing ArrowUp on January", () => {
+ const { container } = render(
+ ,
+ );
+
+ const januaryOption = container.querySelectorAll(
+ ".react-datepicker__month-option",
+ )[0] as HTMLElement;
+
+ fireEvent.keyDown(januaryOption, { key: "ArrowUp" });
+
+ const decemberOption = container.querySelectorAll(
+ ".react-datepicker__month-option",
+ )[11] as HTMLElement;
+
+ expect(document.activeElement).toBe(decemberOption);
+ });
+
+ it("should wrap around to January when pressing ArrowDown on December", () => {
+ const { container } = render(
+ ,
+ );
+
+ const decemberOption = container.querySelectorAll(
+ ".react-datepicker__month-option",
+ )[11] as HTMLElement;
+
+ fireEvent.keyDown(decemberOption, { key: "ArrowDown" });
+
+ const januaryOption = container.querySelectorAll(
+ ".react-datepicker__month-option",
+ )[0] as HTMLElement;
+
+ expect(document.activeElement).toBe(januaryOption);
+ });
+
+ it("should set aria-selected on selected month", () => {
+ const { container } = render(
+ ,
+ );
+
+ const selectedOption = container.querySelector(
+ '[aria-selected="true"]',
+ ) as HTMLElement;
+ expect(selectedOption.textContent).toContain("July");
+ });
+
+ it("should not set aria-selected on non-selected months", () => {
+ const { container } = render(
+ ,
+ );
+
+ const monthOptions = container.querySelectorAll(
+ ".react-datepicker__month-option",
+ );
+ const nonSelectedOptions = Array.from(monthOptions).filter(
+ (_, index) => index !== 6,
+ );
+
+ nonSelectedOptions.forEach((option) => {
+ expect(option.getAttribute("aria-selected")).toBeNull();
+ });
+ });
+
+ it("should have role='button' on month options", () => {
+ const { container } = render( );
+
+ const monthOptions = container.querySelectorAll(
+ ".react-datepicker__month-option",
+ );
+
+ monthOptions.forEach((option) => {
+ expect(option.getAttribute("role")).toBe("button");
+ });
+ });
+
+ it("should have tabIndex=0 on month options", () => {
+ const { container } = render( );
+
+ const monthOptions = container.querySelectorAll(
+ ".react-datepicker__month-option",
+ );
+
+ monthOptions.forEach((option) => {
+ expect(option.getAttribute("tabIndex")).toBe("0");
+ });
+ });
+
+ it("should auto-focus selected month on mount", () => {
+ const { container } = render(
+ ,
+ );
+
+ const septemberOption = container.querySelectorAll(
+ ".react-datepicker__month-option",
+ )[8] as HTMLElement;
+
+ // The selected month should be focused
+ expect(document.activeElement).toBe(septemberOption);
+ });
+
+ it("should render with correct class names", () => {
+ const { container } = render( );
+
+ expect(
+ container.querySelector(".react-datepicker__month-dropdown"),
+ ).toBeTruthy();
+ });
+
+ it("should apply selected class to selected month", () => {
+ const { container } = render(
+ ,
+ );
+
+ const selectedOption = container.querySelector(
+ ".react-datepicker__month-option--selected_month",
+ );
+ expect(selectedOption).toBeTruthy();
+ });
+
+ it("should not apply selected class to non-selected months", () => {
+ const { container } = render(
+ ,
+ );
+
+ const monthOptions = container.querySelectorAll(
+ ".react-datepicker__month-option",
+ );
+ const nonSelectedOptions = Array.from(monthOptions).filter(
+ (option) =>
+ !option.classList.contains(
+ "react-datepicker__month-option--selected_month",
+ ),
+ );
+
+ expect(nonSelectedOptions.length).toBe(11);
+ });
+
+ it("should prevent default on Enter key", () => {
+ const { container } = render( );
+
+ const monthOption = container.querySelector(
+ ".react-datepicker__month-option",
+ );
+ const event = new KeyboardEvent("keydown", {
+ key: "Enter",
+ bubbles: true,
+ cancelable: true,
+ });
+ const preventDefaultSpy = jest.spyOn(event, "preventDefault");
+
+ monthOption?.dispatchEvent(event);
+
+ expect(preventDefaultSpy).toHaveBeenCalled();
+ });
+
+ it("should prevent default on Escape key", () => {
+ const { container } = render( );
+
+ const monthOption = container.querySelector(
+ ".react-datepicker__month-option",
+ );
+ const event = new KeyboardEvent("keydown", {
+ key: "Escape",
+ bubbles: true,
+ cancelable: true,
+ });
+ const preventDefaultSpy = jest.spyOn(event, "preventDefault");
+
+ monthOption?.dispatchEvent(event);
+
+ expect(preventDefaultSpy).toHaveBeenCalled();
+ });
+
+ it("should prevent default on ArrowUp key", () => {
+ const { container } = render( );
+
+ const monthOption = container.querySelector(
+ ".react-datepicker__month-option",
+ );
+ const event = new KeyboardEvent("keydown", {
+ key: "ArrowUp",
+ bubbles: true,
+ cancelable: true,
+ });
+ const preventDefaultSpy = jest.spyOn(event, "preventDefault");
+
+ monthOption?.dispatchEvent(event);
+
+ expect(preventDefaultSpy).toHaveBeenCalled();
+ });
+
+ it("should prevent default on ArrowDown key", () => {
+ const { container } = render( );
+
+ const monthOption = container.querySelector(
+ ".react-datepicker__month-option",
+ );
+ const event = new KeyboardEvent("keydown", {
+ key: "ArrowDown",
+ bubbles: true,
+ cancelable: true,
+ });
+ const preventDefaultSpy = jest.spyOn(event, "preventDefault");
+
+ monthOption?.dispatchEvent(event);
+
+ expect(preventDefaultSpy).toHaveBeenCalled();
+ });
+});
diff --git a/src/test/popper_component_test.test.tsx b/src/test/popper_component_test.test.tsx
new file mode 100644
index 000000000..aa938e78e
--- /dev/null
+++ b/src/test/popper_component_test.test.tsx
@@ -0,0 +1,334 @@
+import React from "react";
+import { render, fireEvent } from "@testing-library/react";
+import { PopperComponent } from "../popper_component";
+
+// Mock the dependencies
+jest.mock("@floating-ui/react", () => ({
+ FloatingArrow: ({ className }: { className: string }) => (
+
+ ),
+}));
+
+jest.mock("../portal", () => {
+ return function Portal({
+ children,
+ portalId,
+ }: {
+ children: React.ReactNode;
+ portalId: string;
+ }) {
+ return (
+
+ {children}
+
+ );
+ };
+});
+
+jest.mock("../tab_loop", () => {
+ return function TabLoop({
+ children,
+ enableTabLoop,
+ }: {
+ children: React.ReactNode;
+ enableTabLoop?: boolean;
+ }) {
+ return (
+
+ {children}
+
+ );
+ };
+});
+
+const mockPopperProps = {
+ refs: {
+ setReference: jest.fn(),
+ setFloating: jest.fn(),
+ },
+ floatingStyles: {
+ position: "absolute" as const,
+ top: 10,
+ left: 20,
+ },
+ placement: "bottom" as const,
+ strategy: "absolute" as const,
+ middlewareData: {},
+ x: 0,
+ y: 0,
+ isPositioned: true,
+ update: jest.fn(),
+ elements: {},
+ context: {},
+ arrowRef: { current: null },
+};
+
+describe("PopperComponent", () => {
+ const defaultProps = {
+ popperComponent: Popper Content
,
+ targetComponent: Target Content
,
+ popperOnKeyDown: jest.fn(),
+ popperProps: mockPopperProps,
+ };
+
+ it("should render target component", () => {
+ const { getByTestId } = render( );
+
+ expect(getByTestId("target-content")).toBeTruthy();
+ });
+
+ it("should not render popper when hidePopper is true", () => {
+ const { queryByTestId } = render(
+ ,
+ );
+
+ expect(queryByTestId("popper-content")).toBeNull();
+ });
+
+ it("should render popper when hidePopper is false", () => {
+ const { getByTestId } = render(
+ ,
+ );
+
+ expect(getByTestId("popper-content")).toBeTruthy();
+ });
+
+ it("should apply className to popper", () => {
+ const { container } = render(
+ ,
+ );
+
+ const popper = container.querySelector(".react-datepicker-popper");
+ expect(popper?.classList.contains("custom-popper-class")).toBe(true);
+ });
+
+ it("should apply wrapperClassName to wrapper", () => {
+ const { container } = render(
+ ,
+ );
+
+ const wrapper = container.querySelector(".react-datepicker-wrapper");
+ expect(wrapper?.classList.contains("custom-wrapper-class")).toBe(true);
+ });
+
+ it("should render arrow when showArrow is true", () => {
+ const { getByTestId } = render(
+ ,
+ );
+
+ expect(getByTestId("floating-arrow")).toBeTruthy();
+ });
+
+ it("should not render arrow when showArrow is false", () => {
+ const { queryByTestId } = render(
+ ,
+ );
+
+ expect(queryByTestId("floating-arrow")).toBeNull();
+ });
+
+ it("should render arrow with correct className", () => {
+ const { getByTestId } = render(
+ ,
+ );
+
+ const arrow = getByTestId("floating-arrow");
+ expect(arrow.classList.contains("react-datepicker__triangle")).toBe(true);
+ });
+
+ it("should wrap popper in TabLoop", () => {
+ const { getByTestId } = render(
+ ,
+ );
+
+ expect(getByTestId("tab-loop")).toBeTruthy();
+ });
+
+ it("should pass enableTabLoop to TabLoop", () => {
+ const { getByTestId } = render(
+ ,
+ );
+
+ const tabLoop = getByTestId("tab-loop");
+ expect(tabLoop.getAttribute("data-enabled")).toBe("true");
+ });
+
+ it("should render in portal when portalId is provided and hidePopper is false", () => {
+ const { getByTestId } = render(
+ ,
+ );
+
+ const portal = getByTestId("portal");
+ expect(portal.getAttribute("data-portal-id")).toBe("test-portal");
+ });
+
+ it("should not render portal when hidePopper is true", () => {
+ const { queryByTestId } = render(
+ ,
+ );
+
+ expect(queryByTestId("portal")).toBeNull();
+ });
+
+ it("should handle popperOnKeyDown event", () => {
+ const handleKeyDown = jest.fn();
+ const { container } = render(
+ ,
+ );
+
+ const popper = container.querySelector(".react-datepicker-popper");
+ fireEvent.keyDown(popper!, { key: "Escape" });
+
+ expect(handleKeyDown).toHaveBeenCalled();
+ });
+
+ it("should set data-placement attribute", () => {
+ const { container } = render(
+ ,
+ );
+
+ const popper = container.querySelector(".react-datepicker-popper");
+ expect(popper?.getAttribute("data-placement")).toBe("bottom");
+ });
+
+ it("should apply floating styles to popper", () => {
+ const { container } = render(
+ ,
+ );
+
+ const popper = container.querySelector(
+ ".react-datepicker-popper",
+ ) as HTMLElement;
+ expect(popper.style.position).toBe("absolute");
+ });
+
+ it("should use popperContainer when provided", () => {
+ const CustomContainer: React.FC<{ children?: React.ReactNode }> = ({
+ children,
+ }) => {children}
;
+
+ const { getByTestId } = render(
+ ,
+ );
+
+ expect(getByTestId("custom-container")).toBeTruthy();
+ });
+
+ it("should pass portalHost to Portal", () => {
+ const mockPortalHost = document.createElement("div").attachShadow({
+ mode: "open",
+ });
+
+ render(
+ ,
+ );
+
+ // Portal component should receive portalHost prop
+ // This is verified by the mock implementation
+ expect(true).toBe(true);
+ });
+
+ it("should render wrapper with default class", () => {
+ const { container } = render( );
+
+ const wrapper = container.querySelector(".react-datepicker-wrapper");
+ expect(wrapper).toBeTruthy();
+ });
+
+ it("should combine wrapper classes correctly", () => {
+ const { container } = render(
+ ,
+ );
+
+ const wrapper = container.querySelector(".react-datepicker-wrapper");
+ expect(wrapper?.classList.contains("react-datepicker-wrapper")).toBe(true);
+ expect(wrapper?.classList.contains("custom-wrapper")).toBe(true);
+ expect(wrapper?.classList.contains("extra-class")).toBe(true);
+ });
+
+ it("should combine popper classes correctly", () => {
+ const { container } = render(
+ ,
+ );
+
+ const popper = container.querySelector(".react-datepicker-popper");
+ expect(popper?.classList.contains("react-datepicker-popper")).toBe(true);
+ expect(popper?.classList.contains("custom-class")).toBe(true);
+ expect(popper?.classList.contains("extra-class")).toBe(true);
+ });
+
+ it("should not render popper content when hidePopper is undefined (defaults to true)", () => {
+ const propsWithoutHidePopper = {
+ ...defaultProps,
+ hidePopper: undefined,
+ };
+ const { queryByTestId } = render(
+ ,
+ );
+
+ expect(queryByTestId("popper-content")).toBeNull();
+ });
+
+ it("should handle multiple children in popper component", () => {
+ const multiChildPopper = (
+ <>
+ Child 1
+ Child 2
+ >
+ );
+
+ const { getByTestId } = render(
+ ,
+ );
+
+ expect(getByTestId("child-1")).toBeTruthy();
+ expect(getByTestId("child-2")).toBeTruthy();
+ });
+});
diff --git a/src/test/portal_test.test.tsx b/src/test/portal_test.test.tsx
new file mode 100644
index 000000000..d00a46717
--- /dev/null
+++ b/src/test/portal_test.test.tsx
@@ -0,0 +1,221 @@
+/**
+ * Test suite for Portal component
+ *
+ * Portal is a React component that renders children into a DOM node that exists
+ * outside the parent component's DOM hierarchy. This is useful for modals, tooltips,
+ * and dropdowns that need to break out of overflow:hidden containers.
+ *
+ * Key features tested:
+ * - Portal creation and rendering
+ * - Reusing existing portal roots
+ * - Shadow DOM support
+ * - Cleanup on unmount
+ * - Multiple portals
+ *
+ * @see ../portal.tsx
+ */
+import React from "react";
+import { render } from "@testing-library/react";
+import Portal from "../portal";
+
+describe("Portal", () => {
+ afterEach(() => {
+ // Clean up any portal roots created during tests to prevent test pollution
+ const portalRoots = document.querySelectorAll('[id^="test-portal"]');
+ portalRoots.forEach((root) => root.remove());
+ });
+
+ /**
+ * Test: Basic portal rendering
+ * Verifies that children are rendered in a separate DOM node (portal root)
+ * rather than in the component's normal location.
+ */
+ it("should render children in a portal", () => {
+ const { container } = render(
+
+ Portal Content
+ ,
+ );
+
+ // Content should not be in the main container (React's render root)
+ expect(
+ container.querySelector('[data-testid="portal-content"]'),
+ ).toBeNull();
+
+ // Content should be in the portal root (separate DOM location)
+ const portalRoot = document.getElementById("test-portal-1");
+ expect(portalRoot).not.toBeNull();
+ expect(
+ portalRoot?.querySelector('[data-testid="portal-content"]'),
+ ).not.toBeNull();
+ });
+
+ /**
+ * Test: Automatic portal root creation
+ * If no element with the specified portalId exists, the Portal should
+ * create one and append it to document.body.
+ */
+ it("should create portal root if it doesn't exist", () => {
+ expect(document.getElementById("test-portal-2")).toBeNull();
+
+ render(
+
+ Portal Content
+ ,
+ );
+
+ const portalRoot = document.getElementById("test-portal-2");
+ expect(portalRoot).not.toBeNull();
+ expect(portalRoot?.parentElement).toBe(document.body);
+ });
+
+ /**
+ * Test: Reusing existing portal roots
+ * If an element with the portalId already exists, the Portal should use it
+ * instead of creating a new one.
+ */
+ it("should use existing portal root if it exists", () => {
+ const existingRoot = document.createElement("div");
+ existingRoot.id = "test-portal-3";
+ document.body.appendChild(existingRoot);
+
+ render(
+
+ Portal Content
+ ,
+ );
+
+ const portalRoot = document.getElementById("test-portal-3");
+ expect(portalRoot).toBe(existingRoot);
+ expect(
+ portalRoot?.querySelector('[data-testid="portal-content"]'),
+ ).not.toBeNull();
+ });
+
+ /**
+ * Test: Cleanup on unmount
+ * When the Portal unmounts, it should remove its content from the portal root
+ * (but the portal root itself should remain for potential reuse).
+ */
+ it("should cleanup portal element on unmount", () => {
+ const { unmount } = render(
+
+ Portal Content
+ ,
+ );
+
+ const portalRoot = document.getElementById("test-portal-4");
+ expect(portalRoot).not.toBeNull();
+ expect(
+ portalRoot?.querySelector('[data-testid="portal-content"]'),
+ ).not.toBeNull();
+
+ unmount();
+
+ // Portal root should still exist but content should be removed
+ expect(document.getElementById("test-portal-4")).not.toBeNull();
+ expect(
+ document
+ .getElementById("test-portal-4")
+ ?.querySelector('[data-testid="portal-content"]'),
+ ).toBeNull();
+ });
+
+ /**
+ * Test: Multiple children support
+ * Verifies that the Portal can render multiple child elements.
+ */
+ it("should render multiple children", () => {
+ render(
+
+ Child 1
+ Child 2
+ Child 3
+ ,
+ );
+
+ const portalRoot = document.getElementById("test-portal-5");
+ expect(portalRoot?.querySelector('[data-testid="child-1"]')).not.toBeNull();
+ expect(portalRoot?.querySelector('[data-testid="child-2"]')).not.toBeNull();
+ expect(portalRoot?.querySelector('[data-testid="child-3"]')).not.toBeNull();
+ });
+
+ /**
+ * Test: Shadow DOM support
+ * Tests that the Portal can render into a Shadow DOM by accepting
+ * a ShadowRoot as the portalHost prop.
+ */
+ it("should support ShadowRoot as portalHost", () => {
+ const hostElement = document.createElement("div");
+ document.body.appendChild(hostElement);
+ const shadowRoot = hostElement.attachShadow({ mode: "open" });
+
+ render(
+
+ Shadow Content
+ ,
+ );
+
+ // Portal should be in shadow root, not in document body (regular DOM)
+ expect(document.getElementById("test-portal-6")).toBeNull();
+
+ const portalInShadow = shadowRoot.getElementById("test-portal-6");
+ expect(portalInShadow).not.toBeNull();
+ expect(
+ portalInShadow?.querySelector('[data-testid="shadow-content"]'),
+ ).not.toBeNull();
+
+ // Cleanup: Remove the shadow host element
+ hostElement.remove();
+ });
+
+ /**
+ * Test: Re-render handling
+ * When the Portal's children change, the portal content should update accordingly.
+ */
+ it("should handle re-renders correctly", () => {
+ const { rerender } = render(
+
+ Initial Content
+ ,
+ );
+
+ let portalRoot = document.getElementById("test-portal-7");
+ expect(portalRoot?.textContent).toContain("Initial Content");
+
+ rerender(
+
+ Updated Content
+ ,
+ );
+
+ portalRoot = document.getElementById("test-portal-7");
+ expect(portalRoot?.textContent).toContain("Updated Content");
+ });
+
+ /**
+ * Test: Multiple independent portals
+ * Multiple Portal components with different IDs should create separate
+ * portal roots and not interfere with each other.
+ */
+ it("should handle multiple portals with different IDs", () => {
+ render(
+ <>
+
+ Portal A
+
+
+ Portal B
+
+ >,
+ );
+
+ const portalA = document.getElementById("test-portal-8a");
+ const portalB = document.getElementById("test-portal-8b");
+
+ expect(portalA?.querySelector('[data-testid="portal-a"]')).not.toBeNull();
+ expect(portalB?.querySelector('[data-testid="portal-b"]')).not.toBeNull();
+ expect(portalA?.querySelector('[data-testid="portal-b"]')).toBeNull();
+ expect(portalB?.querySelector('[data-testid="portal-a"]')).toBeNull();
+ });
+});
diff --git a/src/test/tab_loop_test.test.tsx b/src/test/tab_loop_test.test.tsx
new file mode 100644
index 000000000..07f46b498
--- /dev/null
+++ b/src/test/tab_loop_test.test.tsx
@@ -0,0 +1,297 @@
+/**
+ * Test suite for TabLoop component
+ *
+ * TabLoop manages keyboard navigation within the datepicker by creating a tab loop.
+ * This prevents users from tabbing out of the picker and ensures focus stays within
+ * the interactive elements.
+ *
+ * Key features tested:
+ * - Tab loop creation (start and end sentinels)
+ * - Focus management (first/last element)
+ * - Disabled element handling
+ * - Various focusable element types (buttons, inputs, links, etc.)
+ * - Enable/disable functionality
+ *
+ * @see ../tab_loop.tsx
+ */
+import React from "react";
+import { render, fireEvent } from "@testing-library/react";
+import TabLoop from "../tab_loop";
+
+describe("TabLoop", () => {
+ it("should render children when enableTabLoop is true", () => {
+ const { getByText } = render(
+
+ Test Content
+ ,
+ );
+
+ expect(getByText("Test Content")).toBeTruthy();
+ });
+
+ it("should render children when enableTabLoop is false", () => {
+ const { getByText } = render(
+
+ Test Content
+ ,
+ );
+
+ expect(getByText("Test Content")).toBeTruthy();
+ });
+
+ it("should render tab loop elements when enableTabLoop is true", () => {
+ const { container } = render(
+
+ Test Content
+ ,
+ );
+
+ const tabLoopStart = container.querySelector(
+ ".react-datepicker__tab-loop__start",
+ );
+ const tabLoopEnd = container.querySelector(
+ ".react-datepicker__tab-loop__end",
+ );
+
+ expect(tabLoopStart).not.toBeNull();
+ expect(tabLoopEnd).not.toBeNull();
+ expect(tabLoopStart?.getAttribute("tabIndex")).toBe("0");
+ expect(tabLoopEnd?.getAttribute("tabIndex")).toBe("0");
+ });
+
+ it("should not render tab loop elements when enableTabLoop is false", () => {
+ const { container } = render(
+
+ Test Content
+ ,
+ );
+
+ const tabLoopStart = container.querySelector(
+ ".react-datepicker__tab-loop__start",
+ );
+ const tabLoopEnd = container.querySelector(
+ ".react-datepicker__tab-loop__end",
+ );
+
+ expect(tabLoopStart).toBeNull();
+ expect(tabLoopEnd).toBeNull();
+ });
+
+ it("should use default enableTabLoop value of true", () => {
+ const { container } = render(
+
+ Test Content
+ ,
+ );
+
+ const tabLoopStart = container.querySelector(
+ ".react-datepicker__tab-loop__start",
+ );
+ expect(tabLoopStart).not.toBeNull();
+ });
+
+ it("should focus last tabbable element when tab loop start is focused", () => {
+ const { container } = render(
+
+ Button 1
+ Button 2
+ Button 3
+ ,
+ );
+
+ const tabLoopStart = container.querySelector(
+ ".react-datepicker__tab-loop__start",
+ ) as HTMLElement;
+ const buttons = container.querySelectorAll("button");
+ const lastButton = buttons[buttons.length - 1] as HTMLElement;
+
+ fireEvent.focus(tabLoopStart);
+
+ expect(document.activeElement).toBe(lastButton);
+ });
+
+ it("should focus first tabbable element when tab loop end is focused", () => {
+ const { container } = render(
+
+ Button 1
+ Button 2
+ Button 3
+ ,
+ );
+
+ const tabLoopEnd = container.querySelector(
+ ".react-datepicker__tab-loop__end",
+ ) as HTMLElement;
+ const buttons = container.querySelectorAll("button");
+ const firstButton = buttons[0] as HTMLElement;
+
+ fireEvent.focus(tabLoopEnd);
+
+ expect(document.activeElement).toBe(firstButton);
+ });
+
+ it("should handle inputs with tabindex", () => {
+ const { container } = render(
+
+
+
+ ,
+ );
+
+ const tabLoopStart = container.querySelector(
+ ".react-datepicker__tab-loop__start",
+ ) as HTMLElement;
+ const inputs = container.querySelectorAll("input");
+ const lastInput = inputs[inputs.length - 1] as HTMLElement;
+
+ fireEvent.focus(tabLoopStart);
+
+ expect(document.activeElement).toBe(lastInput);
+ });
+
+ it("should skip disabled elements", () => {
+ const { container } = render(
+
+ Button 1
+ Button 2 (disabled)
+ Button 3
+ ,
+ );
+
+ const tabLoopStart = container.querySelector(
+ ".react-datepicker__tab-loop__start",
+ ) as HTMLElement;
+ const buttons = container.querySelectorAll("button:not([disabled])");
+ const lastEnabledButton = buttons[buttons.length - 1] as HTMLElement;
+
+ fireEvent.focus(tabLoopStart);
+
+ expect(document.activeElement).toBe(lastEnabledButton);
+ });
+
+ it("should skip elements with tabindex -1", () => {
+ const { container } = render(
+
+ Button 1
+ Button 2 (tabindex -1)
+ Button 3
+ ,
+ );
+
+ const tabLoopStart = container.querySelector(
+ ".react-datepicker__tab-loop__start",
+ ) as HTMLElement;
+ const button3 = container.querySelectorAll("button")[2] as HTMLElement;
+
+ fireEvent.focus(tabLoopStart);
+
+ expect(document.activeElement).toBe(button3);
+ });
+
+ it("should handle anchor elements", () => {
+ const { container } = render(
+
+ Link 1
+ Link 2
+ ,
+ );
+
+ const tabLoopStart = container.querySelector(
+ ".react-datepicker__tab-loop__start",
+ ) as HTMLElement;
+ const links = container.querySelectorAll("a");
+ const lastLink = links[links.length - 1] as HTMLElement;
+
+ fireEvent.focus(tabLoopStart);
+
+ expect(document.activeElement).toBe(lastLink);
+ });
+
+ it("should handle select elements", () => {
+ const { container } = render(
+
+
+ Option 1
+
+
+ Option 2
+
+ ,
+ );
+
+ const tabLoopEnd = container.querySelector(
+ ".react-datepicker__tab-loop__end",
+ ) as HTMLElement;
+ const selects = container.querySelectorAll("select");
+ const firstSelect = selects[0] as HTMLElement;
+
+ fireEvent.focus(tabLoopEnd);
+
+ expect(document.activeElement).toBe(firstSelect);
+ });
+
+ it("should handle textarea elements", () => {
+ const { container } = render(
+
+
+
+ ,
+ );
+
+ const tabLoopStart = container.querySelector(
+ ".react-datepicker__tab-loop__start",
+ ) as HTMLElement;
+ const textareas = container.querySelectorAll("textarea");
+ const lastTextarea = textareas[textareas.length - 1] as HTMLElement;
+
+ fireEvent.focus(tabLoopStart);
+
+ expect(document.activeElement).toBe(lastTextarea);
+ });
+
+ it("should not focus when there are no tabbable children", () => {
+ const { container } = render(
+
+ Non-tabbable content
+ ,
+ );
+
+ const tabLoopStart = container.querySelector(
+ ".react-datepicker__tab-loop__start",
+ ) as HTMLElement;
+
+ fireEvent.focus(tabLoopStart);
+
+ // Should not throw error, just not focus anything
+ expect(true).toBe(true);
+ });
+
+ it("should not focus when there is only one tabbable element", () => {
+ const { container } = render(
+
+ Single Button
+ ,
+ );
+
+ const tabLoopStart = container.querySelector(
+ ".react-datepicker__tab-loop__start",
+ ) as HTMLElement;
+ const button = container.querySelector("button") as HTMLElement;
+
+ fireEvent.focus(tabLoopStart);
+
+ // Should not focus the single button (needs more than 1)
+ expect(document.activeElement).not.toBe(button);
+ });
+
+ it("should render with wrapper class", () => {
+ const { container } = render(
+
+ Test Content
+ ,
+ );
+
+ const wrapper = container.querySelector(".react-datepicker__tab-loop");
+ expect(wrapper).not.toBeNull();
+ });
+});
diff --git a/src/test/with_floating_test.test.tsx b/src/test/with_floating_test.test.tsx
new file mode 100644
index 000000000..1f55e4bd3
--- /dev/null
+++ b/src/test/with_floating_test.test.tsx
@@ -0,0 +1,219 @@
+import React from "react";
+import { render } from "@testing-library/react";
+import withFloating, { type FloatingProps } from "../with_floating";
+import * as FloatingUI from "@floating-ui/react";
+
+// Mock @floating-ui/react
+jest.mock("@floating-ui/react", () => ({
+ useFloating: jest.fn(() => ({
+ refs: {
+ setReference: jest.fn(),
+ setFloating: jest.fn(),
+ },
+ floatingStyles: {
+ position: "absolute",
+ top: 0,
+ left: 0,
+ },
+ placement: "bottom",
+ context: {},
+ })),
+ arrow: jest.fn(() => ({})),
+ offset: jest.fn(() => ({})),
+ flip: jest.fn(() => ({})),
+ autoUpdate: jest.fn(),
+}));
+
+interface TestComponentProps extends FloatingProps {
+ testProp?: string;
+}
+
+const TestComponent: React.FC = ({
+ popperProps,
+ hidePopper,
+ testProp,
+}) => {
+ return (
+
+
{String(hidePopper)}
+
{testProp}
+
{popperProps ? "true" : "false"}
+
+ );
+};
+
+describe("withFloating", () => {
+ it("should wrap component with floating behavior", () => {
+ const WrappedComponent = withFloating(TestComponent);
+ const { getByTestId } = render( );
+
+ expect(getByTestId("test-component")).toBeTruthy();
+ expect(getByTestId("has-popper-props").textContent).toBe("true");
+ });
+
+ it("should pass through component props", () => {
+ const WrappedComponent = withFloating(TestComponent);
+ const { getByTestId } = render(
+ ,
+ );
+
+ expect(getByTestId("test-prop").textContent).toBe("custom-value");
+ });
+
+ it("should default hidePopper to true", () => {
+ const WrappedComponent = withFloating(TestComponent);
+ const { getByTestId } = render( );
+
+ expect(getByTestId("hide-popper").textContent).toBe("true");
+ });
+
+ it("should respect hidePopper prop when set to false", () => {
+ const WrappedComponent = withFloating(TestComponent);
+ const { getByTestId } = render( );
+
+ expect(getByTestId("hide-popper").textContent).toBe("false");
+ });
+
+ it("should respect hidePopper prop when set to true", () => {
+ const WrappedComponent = withFloating(TestComponent);
+ const { getByTestId } = render( );
+
+ expect(getByTestId("hide-popper").textContent).toBe("true");
+ });
+
+ it("should provide popperProps with floating UI data", () => {
+ const WrappedComponent = withFloating(TestComponent);
+ const { getByTestId } = render( );
+
+ expect(getByTestId("has-popper-props").textContent).toBe("true");
+ });
+
+ it("should set display name correctly", () => {
+ const NamedComponent: React.FC = () =>
;
+ NamedComponent.displayName = "CustomComponent";
+
+ const WrappedComponent = withFloating(NamedComponent);
+
+ expect(WrappedComponent.displayName).toBe("withFloating(CustomComponent)");
+ });
+
+ it("should use component name if displayName is not set", () => {
+ const WrappedComponent = withFloating(TestComponent);
+
+ expect(WrappedComponent.displayName).toBe("withFloating(TestComponent)");
+ });
+
+ it("should fallback to 'Component' if no name is available", () => {
+ const AnonymousComponent: React.FC = () =>
;
+ Object.defineProperty(AnonymousComponent, "name", { value: "" });
+ Object.defineProperty(AnonymousComponent, "displayName", { value: "" });
+
+ const WrappedComponent = withFloating(AnonymousComponent);
+
+ expect(WrappedComponent.displayName).toBe("withFloating(Component)");
+ });
+
+ it("should handle popperModifiers prop", () => {
+ const WrappedComponent = withFloating(TestComponent);
+
+ const customModifier = { name: "customModifier" };
+ render( );
+
+ expect(FloatingUI.useFloating).toHaveBeenCalledWith(
+ expect.objectContaining({
+ middleware: expect.arrayContaining([customModifier]),
+ }),
+ );
+ });
+
+ it("should handle popperPlacement prop", () => {
+ const WrappedComponent = withFloating(TestComponent);
+
+ render( );
+
+ expect(FloatingUI.useFloating).toHaveBeenCalledWith(
+ expect.objectContaining({
+ placement: "top",
+ }),
+ );
+ });
+
+ it("should handle popperProps prop", () => {
+ const WrappedComponent = withFloating(TestComponent);
+
+ const customPopperProps = {
+ strategy: "fixed" as const,
+ };
+ render( );
+
+ expect(FloatingUI.useFloating).toHaveBeenCalledWith(
+ expect.objectContaining({
+ strategy: "fixed",
+ }),
+ );
+ });
+
+ it("should include default middleware", () => {
+ const WrappedComponent = withFloating(TestComponent);
+
+ render( );
+
+ expect(FloatingUI.flip).toHaveBeenCalledWith({ padding: 15 });
+ expect(FloatingUI.offset).toHaveBeenCalledWith(10);
+ expect(FloatingUI.arrow).toHaveBeenCalled();
+ expect(FloatingUI.useFloating).toHaveBeenCalledWith(
+ expect.objectContaining({
+ middleware: expect.any(Array),
+ }),
+ );
+ });
+
+ it("should set open based on hidePopper", () => {
+ const WrappedComponent = withFloating(TestComponent);
+
+ const { rerender } = render( );
+
+ expect(FloatingUI.useFloating).toHaveBeenCalledWith(
+ expect.objectContaining({
+ open: false,
+ }),
+ );
+
+ rerender( );
+
+ expect(FloatingUI.useFloating).toHaveBeenCalledWith(
+ expect.objectContaining({
+ open: true,
+ }),
+ );
+ });
+
+ it("should include autoUpdate in floating options", () => {
+ const WrappedComponent = withFloating(TestComponent);
+
+ render( );
+
+ expect(FloatingUI.useFloating).toHaveBeenCalledWith(
+ expect.objectContaining({
+ whileElementsMounted: FloatingUI.autoUpdate,
+ }),
+ );
+ });
+
+ it("should provide arrowRef in popperProps", () => {
+ const TestComponentWithArrow: React.FC = ({
+ popperProps,
+ }) => {
+ return (
+
+ {popperProps.arrowRef ? "true" : "false"}
+
+ );
+ };
+
+ const WrappedComponent = withFloating(TestComponentWithArrow);
+ const { getByTestId } = render( );
+
+ expect(getByTestId("has-arrow-ref").textContent).toBe("true");
+ });
+});
diff --git a/src/test/year_test.test.tsx b/src/test/year_test.test.tsx
new file mode 100644
index 000000000..7fc9055c5
--- /dev/null
+++ b/src/test/year_test.test.tsx
@@ -0,0 +1,554 @@
+import React from "react";
+import { render, fireEvent } from "@testing-library/react";
+import Year from "../year";
+import { newDate, getYear } from "../date_utils";
+
+describe("Year", () => {
+ const defaultProps = {
+ date: newDate("2020-01-01"),
+ onYearMouseEnter: jest.fn(),
+ onYearMouseLeave: jest.fn(),
+ yearItemNumber: 12,
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it("should render year items", () => {
+ const { container } = render( );
+
+ const yearItems = container.querySelectorAll(
+ ".react-datepicker__year-text",
+ );
+ expect(yearItems.length).toBe(12);
+ });
+
+ it("should render correct year range", () => {
+ const { container } = render( );
+
+ const yearItems = container.querySelectorAll(
+ ".react-datepicker__year-text",
+ );
+ const firstYear = yearItems[0]?.textContent;
+ const lastYear = yearItems[11]?.textContent;
+
+ expect(firstYear).toBe("2017");
+ expect(lastYear).toBe("2028");
+ });
+
+ it("should call onDayClick when year is clicked", () => {
+ const handleDayClick = jest.fn();
+ const { container } = render(
+ ,
+ );
+
+ const yearItem = container.querySelector(".react-datepicker__year-text");
+ fireEvent.click(yearItem!);
+
+ expect(handleDayClick).toHaveBeenCalled();
+ });
+
+ it("should mark selected year", () => {
+ const selectedDate = newDate("2020-06-15");
+ const { container } = render(
+ ,
+ );
+
+ const selectedYear = container.querySelector(
+ ".react-datepicker__year-text--selected",
+ );
+ expect(selectedYear?.textContent).toBe("2020");
+ });
+
+ it("should mark current year", () => {
+ const currentYear = getYear(newDate());
+ const { container } = render(
+ ,
+ );
+
+ const todayYear = container.querySelector(
+ ".react-datepicker__year-text--today",
+ );
+ expect(todayYear?.textContent).toBe(String(currentYear));
+ });
+
+ it("should mark disabled years", () => {
+ const minDate = newDate("2020-01-01");
+ const maxDate = newDate("2022-12-31");
+ const { container } = render(
+ ,
+ );
+
+ const disabledYears = container.querySelectorAll(
+ ".react-datepicker__year-text--disabled",
+ );
+ expect(disabledYears.length).toBeGreaterThan(0);
+ });
+
+ it("should mark keyboard selected year", () => {
+ const preSelection = newDate("2021-01-01");
+ const { container } = render(
+ ,
+ );
+
+ const keyboardSelected = container.querySelector(
+ ".react-datepicker__year-text--keyboard-selected",
+ );
+ expect(keyboardSelected?.textContent).toBe("2021");
+ });
+
+ it("should handle Enter key press", () => {
+ const handleDayClick = jest.fn();
+ const selected = newDate("2020-01-01");
+ const preSelection = newDate("2020-01-01");
+ const { container } = render(
+ ,
+ );
+
+ const yearItem = container.querySelector(
+ ".react-datepicker__year-text",
+ ) as HTMLElement;
+ fireEvent.keyDown(yearItem, { key: "Enter" });
+
+ expect(handleDayClick).toHaveBeenCalled();
+ });
+
+ it("should navigate to next year on ArrowRight", () => {
+ const setPreSelection = jest.fn();
+ const preSelection = newDate("2020-01-01");
+ const { container } = render(
+ ,
+ );
+
+ const yearItem = container.querySelector(
+ ".react-datepicker__year-2020",
+ ) as HTMLElement;
+ fireEvent.keyDown(yearItem, { key: "ArrowRight" });
+
+ expect(setPreSelection).toHaveBeenCalled();
+ });
+
+ it("should navigate to previous year on ArrowLeft", () => {
+ const setPreSelection = jest.fn();
+ const preSelection = newDate("2020-01-01");
+ const { container } = render(
+ ,
+ );
+
+ const yearItem = container.querySelector(
+ ".react-datepicker__year-2020",
+ ) as HTMLElement;
+ fireEvent.keyDown(yearItem, { key: "ArrowLeft" });
+
+ expect(setPreSelection).toHaveBeenCalled();
+ });
+
+ it("should navigate up on ArrowUp", () => {
+ const setPreSelection = jest.fn();
+ const preSelection = newDate("2020-01-01");
+ const { container } = render(
+ ,
+ );
+
+ const yearItem = container.querySelector(
+ ".react-datepicker__year-2020",
+ ) as HTMLElement;
+ fireEvent.keyDown(yearItem, { key: "ArrowUp" });
+
+ expect(setPreSelection).toHaveBeenCalled();
+ });
+
+ it("should navigate down on ArrowDown", () => {
+ const setPreSelection = jest.fn();
+ const preSelection = newDate("2020-01-01");
+ const { container } = render(
+ ,
+ );
+
+ const yearItem = container.querySelector(
+ ".react-datepicker__year-2020",
+ ) as HTMLElement;
+ fireEvent.keyDown(yearItem, { key: "ArrowDown" });
+
+ expect(setPreSelection).toHaveBeenCalled();
+ });
+
+ it("should not navigate when keyboard navigation is disabled", () => {
+ const setPreSelection = jest.fn();
+ const preSelection = newDate("2020-01-01");
+ const { container } = render(
+ ,
+ );
+
+ const yearItem = container.querySelector(
+ ".react-datepicker__year-2020",
+ ) as HTMLElement;
+ fireEvent.keyDown(yearItem, { key: "ArrowRight" });
+
+ expect(setPreSelection).not.toHaveBeenCalled();
+ });
+
+ it("should handle Space key as Enter", () => {
+ const handleDayClick = jest.fn();
+ const selected = newDate("2020-01-01");
+ const preSelection = newDate("2020-01-01");
+ const { container } = render(
+ ,
+ );
+
+ const yearItem = container.querySelector(
+ ".react-datepicker__year-text",
+ ) as HTMLElement;
+ fireEvent.keyDown(yearItem, { key: " " });
+
+ expect(handleDayClick).toHaveBeenCalled();
+ });
+
+ it("should call onYearMouseEnter when hovering", () => {
+ const handleMouseEnter = jest.fn();
+ const { container } = render(
+ ,
+ );
+
+ const yearItem = container.querySelector(".react-datepicker__year-text");
+ fireEvent.mouseEnter(yearItem!);
+
+ expect(handleMouseEnter).toHaveBeenCalled();
+ });
+
+ it("should call onYearMouseLeave when leaving", () => {
+ const handleMouseLeave = jest.fn();
+ const { container } = render(
+ ,
+ );
+
+ const yearWrapper = container.querySelector(
+ ".react-datepicker__year-wrapper",
+ );
+ fireEvent.mouseLeave(yearWrapper!);
+
+ expect(handleMouseLeave).toBeUndefined();
+ });
+
+ it("should use pointer events when usePointerEvent is true", () => {
+ const handleMouseEnter = jest.fn();
+ const { container } = render(
+ ,
+ );
+
+ const yearItem = container.querySelector(".react-datepicker__year-text");
+ fireEvent.pointerEnter(yearItem!);
+
+ expect(handleMouseEnter).toHaveBeenCalled();
+ });
+
+ it("should mark range start year", () => {
+ const startDate = newDate("2020-01-01");
+ const endDate = newDate("2022-12-31");
+ const { container } = render(
+ ,
+ );
+
+ const rangeStart = container.querySelector(
+ ".react-datepicker__year-text--range-start",
+ );
+ expect(rangeStart?.textContent).toBe("2020");
+ });
+
+ it("should mark range end year", () => {
+ const startDate = newDate("2020-01-01");
+ const endDate = newDate("2022-12-31");
+ const { container } = render(
+ ,
+ );
+
+ const rangeEnd = container.querySelector(
+ ".react-datepicker__year-text--range-end",
+ );
+ expect(rangeEnd?.textContent).toBe("2022");
+ });
+
+ it("should mark years in range", () => {
+ const startDate = newDate("2020-01-01");
+ const endDate = newDate("2022-12-31");
+ const { container } = render(
+ ,
+ );
+
+ const inRange = container.querySelectorAll(
+ ".react-datepicker__year-text--in-range",
+ );
+ expect(inRange.length).toBeGreaterThan(0);
+ });
+
+ it("should handle selectsMultiple", () => {
+ const selectedDates = [newDate("2020-01-01"), newDate("2021-01-01")];
+ const { container } = render(
+ ,
+ );
+
+ const selectedYears = container.querySelectorAll(
+ ".react-datepicker__year-text--selected",
+ );
+ expect(selectedYears.length).toBe(2);
+ });
+
+ it("should render custom year content", () => {
+ const renderYearContent = (year: number) => `Year ${year}`;
+ const { container } = render(
+ ,
+ );
+
+ const yearItem = container.querySelector(".react-datepicker__year-text");
+ expect(yearItem?.textContent).toContain("Year");
+ });
+
+ it("should apply custom year class", () => {
+ const yearClassName = (date: Date) => {
+ return getYear(date) === 2020 ? "custom-year-class" : "";
+ };
+ const { container } = render(
+ ,
+ );
+
+ const customYear = container.querySelector(".custom-year-class");
+ expect(customYear).toBeTruthy();
+ });
+
+ it("should set correct tabIndex for preselected year", () => {
+ const preSelection = newDate("2020-01-01");
+ const { container } = render(
+ ,
+ );
+
+ const year2020 = container.querySelector(".react-datepicker__year-2020");
+ expect(year2020?.getAttribute("tabIndex")).toBe("0");
+ });
+
+ it("should set tabIndex -1 for non-preselected years", () => {
+ const preSelection = newDate("2020-01-01");
+ const { container } = render(
+ ,
+ );
+
+ const year2021 = container.querySelector(".react-datepicker__year-2021");
+ expect(year2021?.getAttribute("tabIndex")).toBe("-1");
+ });
+
+ it("should handle excluded dates", () => {
+ const excludeDates = [newDate("2020-01-01")];
+ const { container } = render(
+ ,
+ );
+
+ const year2020 = container.querySelector(".react-datepicker__year-2020");
+ expect(year2020?.classList.contains("react-datepicker__year-text--disabled")).toBe(true);
+ });
+
+ it("should handle included dates", () => {
+ const includeDates = [newDate("2020-01-01"), newDate("2021-01-01")];
+ const { container } = render(
+ ,
+ );
+
+ const disabledYears = container.querySelectorAll(
+ ".react-datepicker__year-text--disabled",
+ );
+ expect(disabledYears.length).toBeGreaterThan(0);
+ });
+
+ it("should handle filterDate", () => {
+ const filterDate = (date: Date) => {
+ return getYear(date) !== 2020;
+ };
+ const { container } = render(
+ ,
+ );
+
+ const year2020 = container.querySelector(".react-datepicker__year-2020");
+ expect(year2020?.classList.contains("react-datepicker__year-text--disabled")).toBe(true);
+ });
+
+ it("should call handleOnKeyDown when provided", () => {
+ const handleOnKeyDown = jest.fn();
+ const preSelection = newDate("2020-01-01");
+ const { container } = render(
+ ,
+ );
+
+ const yearItem = container.querySelector(
+ ".react-datepicker__year-2020",
+ ) as HTMLElement;
+ fireEvent.keyDown(yearItem, { key: "ArrowRight" });
+
+ expect(handleOnKeyDown).toHaveBeenCalled();
+ });
+
+ it("should not prevent default on Tab key", () => {
+ const { container } = render( );
+
+ const yearItem = container.querySelector(
+ ".react-datepicker__year-text",
+ ) as HTMLElement;
+ const event = new KeyboardEvent("keydown", {
+ key: "Tab",
+ bubbles: true,
+ cancelable: true,
+ });
+ const preventDefaultSpy = jest.spyOn(event, "preventDefault");
+
+ yearItem.dispatchEvent(event);
+
+ expect(preventDefaultSpy).not.toHaveBeenCalled();
+ });
+
+ it("should mark selecting range start", () => {
+ const startDate = newDate("2020-01-01");
+ const selectingDate = newDate("2022-01-01");
+ const { container } = render(
+ ,
+ );
+
+ const selectingRangeStart = container.querySelector(
+ ".react-datepicker__year-text--selecting-range-start",
+ );
+ expect(selectingRangeStart?.textContent).toBe("2020");
+ });
+
+ it("should mark selecting range end", () => {
+ const startDate = newDate("2020-01-01");
+ const selectingDate = newDate("2022-01-01");
+ const { container } = render(
+ ,
+ );
+
+ const selectingRangeEnd = container.querySelector(
+ ".react-datepicker__year-text--selecting-range-end",
+ );
+ expect(selectingRangeEnd?.textContent).toBe("2022");
+ });
+
+ it("should set aria-current for current year", () => {
+ const currentYear = getYear(newDate());
+ const { container } = render(
+ ,
+ );
+
+ const currentYearElement = container.querySelector(`[aria-current="date"]`);
+ expect(currentYearElement?.textContent).toBe(String(currentYear));
+ });
+
+ it("should return null when date is undefined", () => {
+ const { container } = render(
+ ,
+ );
+
+ expect(container.querySelector(".react-datepicker__year")).toBeNull();
+ });
+
+ it("should call clearSelectingDate on mouse leave", () => {
+ const clearSelectingDate = jest.fn();
+ const { container } = render(
+ ,
+ );
+
+ const yearWrapper = container.querySelector(
+ ".react-datepicker__year-wrapper",
+ );
+ fireEvent.mouseLeave(yearWrapper!);
+
+ expect(clearSelectingDate).toHaveBeenCalled();
+ });
+
+ it("should call clearSelectingDate on pointer leave when usePointerEvent is true", () => {
+ const clearSelectingDate = jest.fn();
+ const { container } = render(
+ ,
+ );
+
+ const yearWrapper = container.querySelector(
+ ".react-datepicker__year-wrapper",
+ );
+ fireEvent.pointerLeave(yearWrapper!);
+
+ expect(clearSelectingDate).toHaveBeenCalled();
+ });
+});