Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions src/test/calendar_container_test.test.tsx
Original file line number Diff line number Diff line change
@@ -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(
<CalendarContainer>
<div>Test Content</div>
</CalendarContainer>,
);

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(
<CalendarContainer showTime>
<div>Test Content</div>
</CalendarContainer>,
);

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(
<CalendarContainer showTimeSelectOnly>
<div>Test Content</div>
</CalendarContainer>,
);

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(
<CalendarContainer className="custom-class">
<div>Test Content</div>
</CalendarContainer>,
);

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(
<CalendarContainer>
<div>Child 1</div>
<div>Child 2</div>
<div>Child 3</div>
</CalendarContainer>,
);

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(
<CalendarContainer data-testid="test-container" id="calendar-id">
<div>Test Content</div>
</CalendarContainer>,
);

const dialog = container.querySelector('[role="dialog"]');
expect(dialog?.getAttribute("data-testid")).toBe("test-container");
expect(dialog?.getAttribute("id")).toBe("calendar-id");
});
});
246 changes: 246 additions & 0 deletions src/test/click_outside_wrapper_test.test.tsx
Original file line number Diff line number Diff line change
@@ -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(
<ClickOutsideWrapper onClickOutside={jest.fn()}>
<div>Test Content</div>
</ClickOutsideWrapper>,
);

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(
<div>
<ClickOutsideWrapper onClickOutside={handleClickOutside}>
<div>Inside Content</div>
</ClickOutsideWrapper>
<div data-testid="outside">Outside Content</div>
</div>,
);

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(
<ClickOutsideWrapper onClickOutside={handleClickOutside}>
<div>Inside Content</div>
</ClickOutsideWrapper>,
);

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(
<ClickOutsideWrapper onClickOutside={jest.fn()} className="custom-class">
<div>Test Content</div>
</ClickOutsideWrapper>,
);

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(
<ClickOutsideWrapper onClickOutside={jest.fn()} style={customStyle}>
<div>Test Content</div>
</ClickOutsideWrapper>,
);

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<HTMLDivElement>();
render(
<ClickOutsideWrapper
onClickOutside={jest.fn()}
containerRef={containerRef}
>
<div>Test Content</div>
</ClickOutsideWrapper>,
);

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(
<div>
<ClickOutsideWrapper
onClickOutside={handleClickOutside}
ignoreClass="ignore-me"
>
<div>Inside Content</div>
</ClickOutsideWrapper>
<div className="ignore-me" data-testid="ignored">
Ignored Content
</div>
<div data-testid="not-ignored">Not Ignored Content</div>
</div>,
);

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(
<div>
<ClickOutsideWrapper onClickOutside={handleClickOutside}>
<div>Inside Content</div>
</ClickOutsideWrapper>
<div data-testid="outside">Outside Content</div>
</div>,
);

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(
<ClickOutsideWrapper onClickOutside={handleClickOutside}>
<div>Test Content</div>
</ClickOutsideWrapper>,
);

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(
<div>
<ClickOutsideWrapper onClickOutside={firstHandler}>
<div>Inside Content</div>
</ClickOutsideWrapper>
<div data-testid="outside">Outside Content</div>
</div>,
);

rerender(
<div>
<ClickOutsideWrapper onClickOutside={secondHandler}>
<div>Inside Content</div>
</ClickOutsideWrapper>
<div data-testid="outside">Outside Content</div>
</div>,
);

const outsideElement = container.querySelector('[data-testid="outside"]');
fireEvent.mouseDown(outsideElement!);

expect(firstHandler).not.toHaveBeenCalled();
expect(secondHandler).toHaveBeenCalledTimes(1);
});
});
Loading
Loading