Skip to content

Commit 86b0de2

Browse files
committed
test: add unit tests with documentation for 8 previously untested components (CalendarContainer, ClickOutsideWrapper, Portal, TabLoop, withFloating, PopperComponent, MonthDropdownOptions, Year)
1 parent a1618ce commit 86b0de2

8 files changed

+2354
-0
lines changed
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/**
2+
* Test suite for CalendarContainer component
3+
*
4+
* CalendarContainer is a wrapper component that provides accessibility features
5+
* for the datepicker calendar. It renders as a dialog with appropriate ARIA attributes.
6+
*
7+
* @see ../calendar_container.tsx
8+
*/
9+
import React from "react";
10+
import { render } from "@testing-library/react";
11+
import CalendarContainer from "../calendar_container";
12+
13+
describe("CalendarContainer", () => {
14+
/**
15+
* Test: Default rendering behavior
16+
* Verifies that the component renders with correct default ARIA attributes
17+
* and displays "Choose Date" as the default aria-label.
18+
*/
19+
it("should render with default props", () => {
20+
const { container } = render(
21+
<CalendarContainer>
22+
<div>Test Content</div>
23+
</CalendarContainer>,
24+
);
25+
26+
const dialog = container.querySelector('[role="dialog"]');
27+
expect(dialog).not.toBeNull();
28+
expect(dialog?.getAttribute("aria-label")).toBe("Choose Date");
29+
expect(dialog?.getAttribute("aria-modal")).toBe("true");
30+
expect(dialog?.textContent).toBe("Test Content");
31+
});
32+
33+
/**
34+
* Test: Time selection mode
35+
* When showTime is true, the aria-label should indicate both date and time selection.
36+
*/
37+
it("should render with showTime prop", () => {
38+
const { container } = render(
39+
<CalendarContainer showTime>
40+
<div>Test Content</div>
41+
</CalendarContainer>,
42+
);
43+
44+
const dialog = container.querySelector('[role="dialog"]');
45+
expect(dialog?.getAttribute("aria-label")).toBe("Choose Date and Time");
46+
});
47+
48+
/**
49+
* Test: Time-only selection mode
50+
* When showTimeSelectOnly is true, the aria-label should indicate only time selection.
51+
*/
52+
it("should render with showTimeSelectOnly prop", () => {
53+
const { container } = render(
54+
<CalendarContainer showTimeSelectOnly>
55+
<div>Test Content</div>
56+
</CalendarContainer>,
57+
);
58+
59+
const dialog = container.querySelector('[role="dialog"]');
60+
expect(dialog?.getAttribute("aria-label")).toBe("Choose Time");
61+
});
62+
63+
/**
64+
* Test: Custom styling
65+
* Verifies that custom CSS classes are properly applied to the dialog element.
66+
*/
67+
it("should apply custom className", () => {
68+
const { container } = render(
69+
<CalendarContainer className="custom-class">
70+
<div>Test Content</div>
71+
</CalendarContainer>,
72+
);
73+
74+
const dialog = container.querySelector('[role="dialog"]');
75+
expect(dialog?.className).toBe("custom-class");
76+
});
77+
78+
/**
79+
* Test: Multiple children rendering
80+
* Ensures the component can properly render multiple child elements.
81+
*/
82+
it("should render multiple children", () => {
83+
const { container } = render(
84+
<CalendarContainer>
85+
<div>Child 1</div>
86+
<div>Child 2</div>
87+
<div>Child 3</div>
88+
</CalendarContainer>,
89+
);
90+
91+
const dialog = container.querySelector('[role="dialog"]');
92+
expect(dialog?.children.length).toBe(3);
93+
});
94+
95+
/**
96+
* Test: HTML attribute passthrough
97+
* Verifies that additional HTML attributes are correctly passed to the dialog element.
98+
*/
99+
it("should pass through additional HTML attributes", () => {
100+
const { container } = render(
101+
<CalendarContainer data-testid="test-container" id="calendar-id">
102+
<div>Test Content</div>
103+
</CalendarContainer>,
104+
);
105+
106+
const dialog = container.querySelector('[role="dialog"]');
107+
expect(dialog?.getAttribute("data-testid")).toBe("test-container");
108+
expect(dialog?.getAttribute("id")).toBe("calendar-id");
109+
});
110+
});
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
/**
2+
* Test suite for ClickOutsideWrapper component
3+
*
4+
* ClickOutsideWrapper is a utility component that detects clicks outside of its children
5+
* and triggers a callback. It's commonly used for closing dropdowns, modals, and popovers.
6+
*
7+
* Key features tested:
8+
* - Click detection (inside vs outside)
9+
* - Ignore class functionality
10+
* - Event listener cleanup
11+
* - Composed events support (Shadow DOM)
12+
* - Ref forwarding
13+
*
14+
* @see ../click_outside_wrapper.tsx
15+
*/
16+
import React from "react";
17+
import { render, fireEvent } from "@testing-library/react";
18+
import { ClickOutsideWrapper } from "../click_outside_wrapper";
19+
20+
describe("ClickOutsideWrapper", () => {
21+
/**
22+
* Test: Basic rendering
23+
* Verifies that children are rendered correctly within the wrapper.
24+
*/
25+
it("should render children", () => {
26+
const { getByText } = render(
27+
<ClickOutsideWrapper onClickOutside={jest.fn()}>
28+
<div>Test Content</div>
29+
</ClickOutsideWrapper>,
30+
);
31+
32+
expect(getByText("Test Content")).toBeTruthy();
33+
});
34+
35+
/**
36+
* Test: Outside click detection
37+
* When a user clicks outside the wrapper, the onClickOutside callback should be triggered.
38+
*/
39+
it("should call onClickOutside when clicking outside", () => {
40+
const handleClickOutside = jest.fn();
41+
const { container } = render(
42+
<div>
43+
<ClickOutsideWrapper onClickOutside={handleClickOutside}>
44+
<div>Inside Content</div>
45+
</ClickOutsideWrapper>
46+
<div data-testid="outside">Outside Content</div>
47+
</div>,
48+
);
49+
50+
const outsideElement = container.querySelector('[data-testid="outside"]');
51+
fireEvent.mouseDown(outsideElement!);
52+
53+
expect(handleClickOutside).toHaveBeenCalledTimes(1);
54+
});
55+
56+
/**
57+
* Test: Inside click handling
58+
* Clicks inside the wrapper should NOT trigger the onClickOutside callback.
59+
*/
60+
it("should not call onClickOutside when clicking inside", () => {
61+
const handleClickOutside = jest.fn();
62+
const { getByText } = render(
63+
<ClickOutsideWrapper onClickOutside={handleClickOutside}>
64+
<div>Inside Content</div>
65+
</ClickOutsideWrapper>,
66+
);
67+
68+
fireEvent.mouseDown(getByText("Inside Content"));
69+
70+
expect(handleClickOutside).not.toHaveBeenCalled();
71+
});
72+
73+
/**
74+
* Test: Custom styling
75+
* Verifies that custom CSS classes are properly applied to the wrapper element.
76+
*/
77+
it("should apply custom className", () => {
78+
const { container } = render(
79+
<ClickOutsideWrapper
80+
onClickOutside={jest.fn()}
81+
className="custom-class"
82+
>
83+
<div>Test Content</div>
84+
</ClickOutsideWrapper>,
85+
);
86+
87+
expect(container.firstChild).toHaveClass("custom-class");
88+
});
89+
90+
/**
91+
* Test: Inline styles
92+
* Ensures that inline styles are correctly applied to the wrapper element.
93+
*/
94+
it("should apply custom style", () => {
95+
const customStyle = { backgroundColor: "red", padding: "10px" };
96+
const { container } = render(
97+
<ClickOutsideWrapper onClickOutside={jest.fn()} style={customStyle}>
98+
<div>Test Content</div>
99+
</ClickOutsideWrapper>,
100+
);
101+
102+
const wrapper = container.firstChild as HTMLElement;
103+
expect(wrapper.style.backgroundColor).toBe("red");
104+
expect(wrapper.style.padding).toBe("10px");
105+
});
106+
107+
/**
108+
* Test: Ref forwarding
109+
* When a containerRef is provided, it should be properly assigned to the wrapper element.
110+
*/
111+
it("should use containerRef when provided", () => {
112+
const containerRef = React.createRef<HTMLDivElement>();
113+
render(
114+
<ClickOutsideWrapper
115+
onClickOutside={jest.fn()}
116+
containerRef={containerRef}
117+
>
118+
<div>Test Content</div>
119+
</ClickOutsideWrapper>,
120+
);
121+
122+
expect(containerRef.current).not.toBeNull();
123+
expect(containerRef.current?.tagName).toBe("DIV");
124+
});
125+
126+
/**
127+
* Test: Ignore class functionality
128+
* Elements with the specified ignoreClass should not trigger onClickOutside,
129+
* while other outside elements should still trigger it.
130+
*/
131+
it("should ignore clicks on elements with ignoreClass", () => {
132+
const handleClickOutside = jest.fn();
133+
const { container } = render(
134+
<div>
135+
<ClickOutsideWrapper
136+
onClickOutside={handleClickOutside}
137+
ignoreClass="ignore-me"
138+
>
139+
<div>Inside Content</div>
140+
</ClickOutsideWrapper>
141+
<div className="ignore-me" data-testid="ignored">
142+
Ignored Content
143+
</div>
144+
<div data-testid="not-ignored">Not Ignored Content</div>
145+
</div>,
146+
);
147+
148+
const ignoredElement = container.querySelector('[data-testid="ignored"]');
149+
fireEvent.mouseDown(ignoredElement!);
150+
expect(handleClickOutside).not.toHaveBeenCalled();
151+
152+
const notIgnoredElement = container.querySelector(
153+
'[data-testid="not-ignored"]',
154+
);
155+
fireEvent.mouseDown(notIgnoredElement!);
156+
expect(handleClickOutside).toHaveBeenCalledTimes(1);
157+
});
158+
159+
/**
160+
* Test: Composed events (Shadow DOM support)
161+
* Tests that the component correctly handles composed events which traverse
162+
* Shadow DOM boundaries using composedPath().
163+
*/
164+
it("should handle composed events with composedPath", () => {
165+
const handleClickOutside = jest.fn();
166+
const { container } = render(
167+
<div>
168+
<ClickOutsideWrapper onClickOutside={handleClickOutside}>
169+
<div>Inside Content</div>
170+
</ClickOutsideWrapper>
171+
<div data-testid="outside">Outside Content</div>
172+
</div>,
173+
);
174+
175+
const outsideElement = container.querySelector('[data-testid="outside"]');
176+
const event = new MouseEvent("mousedown", {
177+
bubbles: true,
178+
composed: true,
179+
});
180+
181+
// Mock composedPath to simulate Shadow DOM event traversal
182+
Object.defineProperty(event, "composedPath", {
183+
value: () => [outsideElement, container, document.body, document],
184+
});
185+
186+
outsideElement?.dispatchEvent(event);
187+
188+
expect(handleClickOutside).toHaveBeenCalledTimes(1);
189+
});
190+
191+
/**
192+
* Test: Memory leak prevention
193+
* Ensures that event listeners are properly removed when the component unmounts
194+
* to prevent memory leaks.
195+
*/
196+
it("should cleanup event listener on unmount", () => {
197+
const handleClickOutside = jest.fn();
198+
const removeEventListenerSpy = jest.spyOn(
199+
document,
200+
"removeEventListener",
201+
);
202+
203+
const { unmount } = render(
204+
<ClickOutsideWrapper onClickOutside={handleClickOutside}>
205+
<div>Test Content</div>
206+
</ClickOutsideWrapper>,
207+
);
208+
209+
unmount();
210+
211+
expect(removeEventListenerSpy).toHaveBeenCalledWith(
212+
"mousedown",
213+
expect.any(Function),
214+
);
215+
216+
removeEventListenerSpy.mockRestore();
217+
});
218+
219+
/**
220+
* Test: Dynamic handler updates
221+
* When the onClickOutside prop changes, the new handler should be used
222+
* instead of the old one.
223+
*/
224+
it("should update onClickOutside handler when prop changes", () => {
225+
const firstHandler = jest.fn();
226+
const secondHandler = jest.fn();
227+
228+
const { rerender, container } = render(
229+
<div>
230+
<ClickOutsideWrapper onClickOutside={firstHandler}>
231+
<div>Inside Content</div>
232+
</ClickOutsideWrapper>
233+
<div data-testid="outside">Outside Content</div>
234+
</div>,
235+
);
236+
237+
rerender(
238+
<div>
239+
<ClickOutsideWrapper onClickOutside={secondHandler}>
240+
<div>Inside Content</div>
241+
</ClickOutsideWrapper>
242+
<div data-testid="outside">Outside Content</div>
243+
</div>,
244+
);
245+
246+
const outsideElement = container.querySelector('[data-testid="outside"]');
247+
fireEvent.mouseDown(outsideElement!);
248+
249+
expect(firstHandler).not.toHaveBeenCalled();
250+
expect(secondHandler).toHaveBeenCalledTimes(1);
251+
});
252+
});

0 commit comments

Comments
 (0)