From 58cb4cf012b7293c703abf619ee1f376358b6d0d Mon Sep 17 00:00:00 2001 From: codomposer Date: Tue, 4 Nov 2025 10:54:52 -0500 Subject: [PATCH 1/3] add unit test to missing components --- src/test/calendar_container.test.tsx | 90 ++++++++ src/test/click_outside_wrapper.test.tsx | 169 ++++++++++++++ src/test/input_time.test.tsx | 214 +++++++++++++++++ src/test/popper_component.test.tsx | 292 ++++++++++++++++++++++++ src/test/portal.test.tsx | 157 +++++++++++++ src/test/tab_loop.test.tsx | 250 ++++++++++++++++++++ src/test/with_floating.test.tsx | 221 ++++++++++++++++++ 7 files changed, 1393 insertions(+) create mode 100644 src/test/calendar_container.test.tsx create mode 100644 src/test/click_outside_wrapper.test.tsx create mode 100644 src/test/input_time.test.tsx create mode 100644 src/test/popper_component.test.tsx create mode 100644 src/test/portal.test.tsx create mode 100644 src/test/tab_loop.test.tsx create mode 100644 src/test/with_floating.test.tsx diff --git a/src/test/calendar_container.test.tsx b/src/test/calendar_container.test.tsx new file mode 100644 index 000000000..a79514a2c --- /dev/null +++ b/src/test/calendar_container.test.tsx @@ -0,0 +1,90 @@ +import { render } from "@testing-library/react"; +import React from "react"; + +import CalendarContainer from "../calendar_container"; + +describe("CalendarContainer", () => { + it("renders with default props", () => { + const { container } = render( + +
Test Content
+
, + ); + + const dialog = container.querySelector('[role="dialog"]'); + expect(dialog).toBeTruthy(); + expect(dialog?.getAttribute("aria-label")).toBe("Choose Date"); + expect(dialog?.getAttribute("aria-modal")).toBe("true"); + expect(dialog?.textContent).toBe("Test Content"); + }); + + it("renders with showTimeSelectOnly prop", () => { + const { container } = render( + +
Time Content
+
, + ); + + const dialog = container.querySelector('[role="dialog"]'); + expect(dialog?.getAttribute("aria-label")).toBe("Choose Time"); + }); + + it("renders with showTime prop", () => { + const { container } = render( + +
Date and Time Content
+
, + ); + + const dialog = container.querySelector('[role="dialog"]'); + expect(dialog?.getAttribute("aria-label")).toBe("Choose Date and Time"); + }); + + it("renders with both showTime and showTimeSelectOnly props", () => { + const { container } = render( + +
Content
+
, + ); + + const dialog = container.querySelector('[role="dialog"]'); + // showTimeSelectOnly takes precedence + expect(dialog?.getAttribute("aria-label")).toBe("Choose Time"); + }); + + it("applies custom className", () => { + const { container } = render( + +
Content
+
, + ); + + const dialog = container.querySelector('[role="dialog"]'); + expect(dialog?.className).toBe("custom-class"); + }); + + it("renders children correctly", () => { + const { container } = render( + +
Child 1
+
Child 2
+
, + ); + + expect(container.querySelector('[data-testid="child-1"]')).toBeTruthy(); + expect(container.querySelector('[data-testid="child-2"]')).toBeTruthy(); + }); + + it("renders with proper ARIA attributes", () => { + const { container } = render( + +
Content
+
, + ); + + const dialog = container.querySelector('[role="dialog"]'); + expect(dialog?.getAttribute("role")).toBe("dialog"); + expect(dialog?.getAttribute("aria-modal")).toBe("true"); + expect(dialog?.getAttribute("aria-label")).toBe("Choose Date"); + }); +}); diff --git a/src/test/click_outside_wrapper.test.tsx b/src/test/click_outside_wrapper.test.tsx new file mode 100644 index 000000000..c5967d51b --- /dev/null +++ b/src/test/click_outside_wrapper.test.tsx @@ -0,0 +1,169 @@ +import { render, fireEvent } from "@testing-library/react"; +import React from "react"; + +import { ClickOutsideWrapper } from "../click_outside_wrapper"; + +describe("ClickOutsideWrapper", () => { + let onClickOutsideMock: jest.Mock; + + beforeEach(() => { + onClickOutsideMock = jest.fn(); + }); + + afterEach(() => { + onClickOutsideMock.mockClear(); + }); + + it("renders children correctly", () => { + const { container } = render( + +
Test Content
+
, + ); + + expect(container.querySelector('[data-testid="child"]')).toBeTruthy(); + }); + + it("calls onClickOutside when clicking outside the wrapper", () => { + const { container } = render( +
+ +
Inside
+
+
Outside
+
, + ); + + const outsideElement = container.querySelector( + '[data-testid="outside"]', + ) as HTMLElement; + fireEvent.mouseDown(outsideElement); + + expect(onClickOutsideMock).toHaveBeenCalledTimes(1); + }); + + it("does not call onClickOutside when clicking inside the wrapper", () => { + const { container } = render( + +
Inside
+
, + ); + + const insideElement = container.querySelector( + '[data-testid="inside"]', + ) as HTMLElement; + fireEvent.mouseDown(insideElement); + + expect(onClickOutsideMock).not.toHaveBeenCalled(); + }); + + it("applies custom className", () => { + const { container } = render( + +
Content
+
, + ); + + const wrapper = container.firstChild as HTMLElement; + expect(wrapper.className).toBe("custom-class"); + }); + + it("applies custom style", () => { + const customStyle = { backgroundColor: "red", padding: "10px" }; + const { container } = render( + +
Content
+
, + ); + + const wrapper = container.firstChild as HTMLElement; + expect(wrapper.style.backgroundColor).toBe("red"); + expect(wrapper.style.padding).toBe("10px"); + }); + + it("does not call onClickOutside when clicking on element with ignoreClass", () => { + const { container } = render( +
+ +
Inside
+
+
+ Ignored +
+
, + ); + + const ignoredElement = container.querySelector( + '[data-testid="ignored"]', + ) as HTMLElement; + fireEvent.mouseDown(ignoredElement); + + expect(onClickOutsideMock).not.toHaveBeenCalled(); + }); + + it("calls onClickOutside when clicking on element without ignoreClass", () => { + const { container } = render( +
+ +
Inside
+
+
+ Not Ignored +
+
, + ); + + const notIgnoredElement = container.querySelector( + '[data-testid="not-ignored"]', + ) as HTMLElement; + fireEvent.mouseDown(notIgnoredElement); + + expect(onClickOutsideMock).toHaveBeenCalledTimes(1); + }); + + it("uses containerRef when provided", () => { + const containerRef = React.createRef(); + render( + +
Content
+
, + ); + + expect(containerRef.current).toBeTruthy(); + expect(containerRef.current?.tagName).toBe("DIV"); + }); + + it("cleans up event listener on unmount", () => { + const removeEventListenerSpy = jest.spyOn(document, "removeEventListener"); + + const { unmount } = render( + +
Content
+
, + ); + + unmount(); + + expect(removeEventListenerSpy).toHaveBeenCalledWith( + "mousedown", + expect.any(Function), + ); + + removeEventListenerSpy.mockRestore(); + }); +}); diff --git a/src/test/input_time.test.tsx b/src/test/input_time.test.tsx new file mode 100644 index 000000000..401b9ff8a --- /dev/null +++ b/src/test/input_time.test.tsx @@ -0,0 +1,214 @@ +import { render, fireEvent } from "@testing-library/react"; +import React from "react"; + +import InputTime from "../input_time"; + +describe("InputTime", () => { + it("renders with default props", () => { + const { container } = render(); + + const timeInput = container.querySelector( + 'input[type="time"]', + ) as HTMLInputElement; + expect(timeInput).toBeTruthy(); + expect(timeInput.className).toBe("react-datepicker-time__input"); + expect(timeInput.placeholder).toBe("Time"); + }); + + it("renders with timeString prop", () => { + const { container } = render(); + + const timeInput = container.querySelector( + 'input[type="time"]', + ) as HTMLInputElement; + expect(timeInput.value).toBe("14:30"); + }); + + it("renders with timeInputLabel prop", () => { + const { container } = render(); + + const label = container.querySelector( + ".react-datepicker-time__caption", + ) as HTMLElement; + expect(label.textContent).toBe("Select Time"); + }); + + it("calls onChange when time is changed", () => { + const onChangeMock = jest.fn(); + const { container } = render( + , + ); + + const timeInput = container.querySelector( + 'input[type="time"]', + ) as HTMLInputElement; + fireEvent.change(timeInput, { target: { value: "15:45" } }); + + expect(onChangeMock).toHaveBeenCalledTimes(1); + const calledDate = onChangeMock.mock.calls[0][0]; + expect(calledDate.getHours()).toBe(15); + expect(calledDate.getMinutes()).toBe(45); + }); + + it("updates state when timeString prop changes", () => { + const { container, rerender } = render(); + + let timeInput = container.querySelector( + 'input[type="time"]', + ) as HTMLInputElement; + expect(timeInput.value).toBe("10:00"); + + rerender(); + + timeInput = container.querySelector( + 'input[type="time"]', + ) as HTMLInputElement; + expect(timeInput.value).toBe("16:30"); + }); + + it("uses provided date when onChange is called", () => { + const onChangeMock = jest.fn(); + const testDate = new Date(2023, 5, 15, 10, 30); + + const { container } = render( + , + ); + + const timeInput = container.querySelector( + 'input[type="time"]', + ) as HTMLInputElement; + fireEvent.change(timeInput, { target: { value: "14:45" } }); + + expect(onChangeMock).toHaveBeenCalledTimes(1); + const calledDate = onChangeMock.mock.calls[0][0]; + expect(calledDate.getFullYear()).toBe(2023); + expect(calledDate.getMonth()).toBe(5); + expect(calledDate.getDate()).toBe(15); + expect(calledDate.getHours()).toBe(14); + expect(calledDate.getMinutes()).toBe(45); + }); + + it("creates new date when no date prop is provided", () => { + const onChangeMock = jest.fn(); + const { container } = render( + , + ); + + const timeInput = container.querySelector( + 'input[type="time"]', + ) as HTMLInputElement; + fireEvent.change(timeInput, { target: { value: "14:45" } }); + + expect(onChangeMock).toHaveBeenCalledTimes(1); + const calledDate = onChangeMock.mock.calls[0][0]; + expect(calledDate).toBeInstanceOf(Date); + expect(calledDate.getHours()).toBe(14); + expect(calledDate.getMinutes()).toBe(45); + }); + + it("renders custom time input when provided", () => { + const CustomTimeInput = ({ + value, + onChange, + }: { + value: string; + onChange: (time: string) => void; + }) => ( + onChange(e.target.value)} + /> + ); + + const { container } = render( + {}} />} + timeString="12:00" + />, + ); + + const customInput = container.querySelector( + '[data-testid="custom-time-input"]', + ) as HTMLInputElement; + expect(customInput).toBeTruthy(); + expect(customInput.value).toBe("12:00"); + }); + + it("calls onChange with custom time input", () => { + const onChangeMock = jest.fn(); + const CustomTimeInput = ({ + value, + onChange, + }: { + value: string; + onChange: (time: string) => void; + }) => ( + onChange(e.target.value)} + /> + ); + + const { container } = render( + {}} />} + timeString="12:00" + />, + ); + + const customInput = container.querySelector( + '[data-testid="custom-time-input"]', + ) as HTMLInputElement; + fireEvent.change(customInput, { target: { value: "18:30" } }); + + expect(onChangeMock).toHaveBeenCalledTimes(1); + const calledDate = onChangeMock.mock.calls[0][0]; + expect(calledDate.getHours()).toBe(18); + expect(calledDate.getMinutes()).toBe(30); + }); + + it("focuses input when clicked", () => { + const { container } = render(); + + const timeInput = container.querySelector( + 'input[type="time"]', + ) as HTMLInputElement; + const focusSpy = jest.spyOn(timeInput, "focus"); + + fireEvent.click(timeInput); + + expect(focusSpy).toHaveBeenCalled(); + focusSpy.mockRestore(); + }); + + it("uses timeString as fallback when onChange value is empty", () => { + const onChangeMock = jest.fn(); + const { container } = render( + , + ); + + const timeInput = container.querySelector( + 'input[type="time"]', + ) as HTMLInputElement; + fireEvent.change(timeInput, { target: { value: "" } }); + + expect(onChangeMock).toHaveBeenCalledTimes(1); + }); + + it("renders container with correct class names", () => { + const { container } = render(); + + expect( + container.querySelector(".react-datepicker__input-time-container"), + ).toBeTruthy(); + expect( + container.querySelector(".react-datepicker-time__input-container"), + ).toBeTruthy(); + expect( + container.querySelector(".react-datepicker-time__input"), + ).toBeTruthy(); + }); +}); diff --git a/src/test/popper_component.test.tsx b/src/test/popper_component.test.tsx new file mode 100644 index 000000000..07786b47c --- /dev/null +++ b/src/test/popper_component.test.tsx @@ -0,0 +1,292 @@ +import { render, fireEvent } from "@testing-library/react"; +import React from "react"; + +import { PopperComponent } from "../popper_component"; + +// Mock the withFloating HOC +jest.mock("../with_floating", () => ({ + __esModule: true, + default: (Component: React.ComponentType) => Component, +})); + +// Mock FloatingArrow component +jest.mock("@floating-ui/react", () => ({ + FloatingArrow: ({ className }: { className: string }) => ( +
+ ), +})); + +describe("PopperComponent", () => { + const mockPopperProps = { + refs: { + reference: { current: null }, + floating: { current: null }, + setFloating: jest.fn(), + setReference: jest.fn(), + setPositionReference: jest.fn(), + }, + floatingStyles: { position: "absolute" as const, top: 0, left: 0 }, + placement: "bottom" as const, + strategy: "absolute" as const, + x: 0, + y: 0, + middlewareData: {}, + isPositioned: true, + update: jest.fn(), + elements: { + reference: null, + floating: null, + domReference: null, + }, + context: {} as any, + arrowRef: { current: null }, + } as any; + + const defaultProps = { + popperComponent:
Popper Content
, + targetComponent:
Target
, + popperOnKeyDown: jest.fn(), + popperProps: mockPopperProps, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("renders target component", () => { + const { container } = render(); + + expect(container.querySelector('[data-testid="target"]')).toBeTruthy(); + }); + + it("renders target component with wrapper class", () => { + const { container } = render(); + + const wrapper = container.querySelector(".react-datepicker-wrapper"); + expect(wrapper).toBeTruthy(); + expect(wrapper?.querySelector('[data-testid="target"]')).toBeTruthy(); + }); + + it("applies custom wrapperClassName", () => { + const { container } = render( + , + ); + + const wrapper = container.querySelector( + ".react-datepicker-wrapper.custom-wrapper", + ); + expect(wrapper).toBeTruthy(); + }); + + it("hides popper when hidePopper is true", () => { + const { container } = render( + , + ); + + expect(container.querySelector('[data-testid="popper-content"]')).toBe( + null, + ); + }); + + it("shows popper when hidePopper is false", () => { + const { container } = render( + , + ); + + expect( + container.querySelector('[data-testid="popper-content"]'), + ).toBeTruthy(); + }); + + it("applies popper className", () => { + const { container } = render( + , + ); + + const popper = container.querySelector( + ".react-datepicker-popper.custom-popper", + ); + expect(popper).toBeTruthy(); + }); + + it("applies data-placement attribute", () => { + const { container } = render( + , + ); + + const popper = container.querySelector(".react-datepicker-popper"); + expect(popper?.getAttribute("data-placement")).toBe("bottom"); + }); + + it("calls popperOnKeyDown when key is pressed in popper", () => { + const onKeyDownMock = jest.fn(); + const { container } = render( + , + ); + + const popper = container.querySelector( + ".react-datepicker-popper", + ) as HTMLElement; + fireEvent.keyDown(popper, { key: "Escape" }); + + expect(onKeyDownMock).toHaveBeenCalledTimes(1); + }); + + it("renders arrow when showArrow is true", () => { + const { container } = render( + , + ); + + expect( + container.querySelector('[data-testid="floating-arrow"]'), + ).toBeTruthy(); + }); + + it("does not render arrow when showArrow is false", () => { + const { container } = render( + , + ); + + expect(container.querySelector('[data-testid="floating-arrow"]')).toBe( + null, + ); + }); + + it("wraps popper in TabLoop when enableTabLoop is true", () => { + const { container } = render( + , + ); + + expect(container.querySelector(".react-datepicker__tab-loop")).toBeTruthy(); + }); + + it("renders in portal when portalId is provided", () => { + const { container } = render( + , + ); + + // Popper should not be in the main container + expect(container.querySelector('[data-testid="popper-content"]')).toBe( + null, + ); + + // Popper should be in the portal + const portalRoot = document.getElementById("test-portal"); + expect(portalRoot).toBeTruthy(); + expect( + portalRoot?.querySelector('[data-testid="popper-content"]'), + ).toBeTruthy(); + + // Cleanup + portalRoot?.remove(); + }); + + it("does not render in portal when hidePopper is true even with portalId", () => { + const { container } = render( + , + ); + + expect(container.querySelector('[data-testid="popper-content"]')).toBe( + null, + ); + expect(document.getElementById("test-portal-2")).toBe(null); + }); + + it("wraps popper in custom container when popperContainer is provided", () => { + const CustomContainer: React.FC<{ children?: React.ReactNode }> = ({ + children, + }) =>
{children}
; + + const { container } = render( + , + ); + + expect( + container.querySelector('[data-testid="custom-container"]'), + ).toBeTruthy(); + expect( + container + .querySelector('[data-testid="custom-container"]') + ?.querySelector('[data-testid="popper-content"]'), + ).toBeTruthy(); + }); + + it("applies floating styles to popper", () => { + const customStyles = { + position: "absolute" as const, + top: 100, + left: 200, + }; + + const customPopperProps = { + ...mockPopperProps, + floatingStyles: customStyles, + }; + + const { container } = render( + , + ); + + const popper = container.querySelector( + ".react-datepicker-popper", + ) as HTMLElement; + expect(popper.style.position).toBe("absolute"); + expect(popper.style.top).toBe("100px"); + expect(popper.style.left).toBe("200px"); + }); + + it("renders with shadow DOM when portalHost is provided", () => { + const shadowHost = document.createElement("div"); + document.body.appendChild(shadowHost); + const shadowRoot = shadowHost.attachShadow({ mode: "open" }); + + render( + , + ); + + const portalRoot = shadowRoot.getElementById("shadow-portal"); + expect(portalRoot).toBeTruthy(); + expect( + portalRoot?.querySelector('[data-testid="popper-content"]'), + ).toBeTruthy(); + + shadowHost.remove(); + }); +}); diff --git a/src/test/portal.test.tsx b/src/test/portal.test.tsx new file mode 100644 index 000000000..5793e3587 --- /dev/null +++ b/src/test/portal.test.tsx @@ -0,0 +1,157 @@ +import { render } from "@testing-library/react"; +import React from "react"; + +import Portal from "../portal"; + +describe("Portal", () => { + afterEach(() => { + // Clean up any portal elements created during tests + const portalElements = document.querySelectorAll('[id^="test-portal"]'); + portalElements.forEach((el) => el.remove()); + }); + + it("renders children into a portal", () => { + const { container } = render( + +
Portal Content
+
, + ); + + // Content should not be in the original container + expect(container.querySelector('[data-testid="portal-content"]')).toBe( + null, + ); + + // Content should be in the portal + const portalRoot = document.getElementById("test-portal-1"); + expect(portalRoot).toBeTruthy(); + expect( + portalRoot?.querySelector('[data-testid="portal-content"]'), + ).toBeTruthy(); + }); + + it("creates portal root if it doesn't exist", () => { + expect(document.getElementById("test-portal-2")).toBe(null); + + render( + +
Content
+
, + ); + + const portalRoot = document.getElementById("test-portal-2"); + expect(portalRoot).toBeTruthy(); + expect(portalRoot?.parentElement).toBe(document.body); + }); + + it("uses existing portal root if it exists", () => { + const existingPortal = document.createElement("div"); + existingPortal.id = "test-portal-3"; + document.body.appendChild(existingPortal); + + render( + +
Content
+
, + ); + + const portalRoot = document.getElementById("test-portal-3"); + expect(portalRoot).toBe(existingPortal); + expect(portalRoot?.querySelector('[data-testid="content"]')).toBeTruthy(); + + existingPortal.remove(); + }); + + it("removes portal content on unmount", () => { + const { unmount } = render( + +
Portal Content
+
, + ); + + const portalRoot = document.getElementById("test-portal-4"); + expect( + portalRoot?.querySelector('[data-testid="portal-content"]'), + ).toBeTruthy(); + + unmount(); + + expect(portalRoot?.querySelector('[data-testid="portal-content"]')).toBe( + null, + ); + }); + + it("renders multiple children correctly", () => { + render( + +
Child 1
+
Child 2
+ Child 3 +
, + ); + + const portalRoot = document.getElementById("test-portal-5"); + expect(portalRoot?.querySelector('[data-testid="child-1"]')).toBeTruthy(); + expect(portalRoot?.querySelector('[data-testid="child-2"]')).toBeTruthy(); + expect(portalRoot?.querySelector('[data-testid="child-3"]')).toBeTruthy(); + }); + + it("works with shadow DOM when portalHost is provided", () => { + const shadowHost = document.createElement("div"); + document.body.appendChild(shadowHost); + const shadowRoot = shadowHost.attachShadow({ mode: "open" }); + + render( + +
Shadow Content
+
, + ); + + const portalRoot = shadowRoot.getElementById("test-portal-shadow"); + expect(portalRoot).toBeTruthy(); + expect( + portalRoot?.querySelector('[data-testid="shadow-content"]'), + ).toBeTruthy(); + + shadowHost.remove(); + }); + + it("appends to portalHost instead of document.body when provided", () => { + const customHost = document.createElement("div"); + customHost.id = "custom-host"; + document.body.appendChild(customHost); + const shadowRoot = customHost.attachShadow({ mode: "open" }); + + render( + +
Custom Host Content
+
, + ); + + const portalRoot = shadowRoot.getElementById("test-portal-custom-host"); + expect(portalRoot).toBeTruthy(); + expect(portalRoot?.parentNode).toBe(shadowRoot); + + customHost.remove(); + }); + + it("handles re-renders correctly", () => { + const { rerender } = render( + +
Content 1
+
, + ); + + const portalRoot = document.getElementById("test-portal-6"); + expect(portalRoot?.querySelector('[data-testid="content-1"]')).toBeTruthy(); + + rerender( + +
Content 2
+
, + ); + + expect(portalRoot?.querySelector('[data-testid="content-1"]')).toBe(null); + expect(portalRoot?.querySelector('[data-testid="content-2"]')).toBeTruthy(); + }); +}); diff --git a/src/test/tab_loop.test.tsx b/src/test/tab_loop.test.tsx new file mode 100644 index 000000000..bb818d67c --- /dev/null +++ b/src/test/tab_loop.test.tsx @@ -0,0 +1,250 @@ +import { render, fireEvent } from "@testing-library/react"; +import React from "react"; + +import TabLoop from "../tab_loop"; + +describe("TabLoop", () => { + it("renders children when enableTabLoop is true", () => { + const { container } = render( + +
Test Content
+
, + ); + + expect(container.querySelector('[data-testid="child"]')).toBeTruthy(); + }); + + it("renders children when enableTabLoop is false", () => { + const { container } = render( + +
Test Content
+
, + ); + + expect(container.querySelector('[data-testid="child"]')).toBeTruthy(); + }); + + it("renders tab loop wrapper when enableTabLoop is true", () => { + const { container } = render( + +
Content
+
, + ); + + expect(container.querySelector(".react-datepicker__tab-loop")).toBeTruthy(); + expect( + container.querySelector(".react-datepicker__tab-loop__start"), + ).toBeTruthy(); + expect( + container.querySelector(".react-datepicker__tab-loop__end"), + ).toBeTruthy(); + }); + + it("does not render tab loop wrapper when enableTabLoop is false", () => { + const { container } = render( + +
Content
+
, + ); + + expect(container.querySelector(".react-datepicker__tab-loop")).toBe(null); + expect(container.querySelector(".react-datepicker__tab-loop__start")).toBe( + null, + ); + expect(container.querySelector(".react-datepicker__tab-loop__end")).toBe( + null, + ); + }); + + it("uses default enableTabLoop value when not provided", () => { + const { container } = render( + +
Content
+
, + ); + + // Default is true + expect(container.querySelector(".react-datepicker__tab-loop")).toBeTruthy(); + }); + + it("focuses last tabbable element when start sentinel is focused", () => { + const { container } = render( + + + + + , + ); + + const startSentinel = container.querySelector( + ".react-datepicker__tab-loop__start", + ) as HTMLElement; + const lastButton = container.querySelector( + '[data-testid="button-3"]', + ) as HTMLButtonElement; + + const focusSpy = jest.spyOn(lastButton, "focus"); + fireEvent.focus(startSentinel); + + expect(focusSpy).toHaveBeenCalled(); + focusSpy.mockRestore(); + }); + + it("focuses first tabbable element when end sentinel is focused", () => { + const { container } = render( + + + + + , + ); + + const endSentinel = container.querySelector( + ".react-datepicker__tab-loop__end", + ) as HTMLElement; + const firstButton = container.querySelector( + '[data-testid="button-1"]', + ) as HTMLButtonElement; + + const focusSpy = jest.spyOn(firstButton, "focus"); + fireEvent.focus(endSentinel); + + expect(focusSpy).toHaveBeenCalled(); + focusSpy.mockRestore(); + }); + + it("handles multiple tabbable element types", () => { + const { container } = render( + + + + +