diff --git a/frontend/jest.setup.ts b/frontend/jest.setup.ts index dcc614ba..6bc5b584 100644 --- a/frontend/jest.setup.ts +++ b/frontend/jest.setup.ts @@ -2,6 +2,33 @@ import '@testing-library/jest-dom'; import { expect } from '@jest/globals'; import type { Plugin } from 'pretty-format'; +// ResizeObserver polyfill +global.ResizeObserver = class ResizeObserver { + observe() {} + unobserve() {} + disconnect() {} +}; + +// ScrollIntoView mock +window.HTMLElement.prototype.scrollIntoView = jest.fn(); + +// PointerEvent mock +class MockPointerEvent extends Event { + button: number; + ctrlKey: boolean; + pointerType: string; + + constructor(type: string, props: PointerEventInit) { + super(type, props); + this.button = props.button || 0; + this.ctrlKey = props.ctrlKey || false; + this.pointerType = props.pointerType || 'mouse'; + } +} +window.PointerEvent = MockPointerEvent as any; +window.HTMLElement.prototype.hasPointerCapture = jest.fn(); +window.HTMLElement.prototype.releasePointerCapture = jest.fn(); + let isSerializing = false; const radixSnapshotSerializer: Plugin = { diff --git a/frontend/src/components/ui/__tests__/date-time-picker.test.tsx b/frontend/src/components/ui/__tests__/date-time-picker.test.tsx new file mode 100644 index 00000000..73f7feb5 --- /dev/null +++ b/frontend/src/components/ui/__tests__/date-time-picker.test.tsx @@ -0,0 +1,114 @@ +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { DateTimePicker } from '../date-time-picker'; +import '@testing-library/jest-dom'; + +describe('DateTimePicker', () => { + it('renders without crashing', () => { + const mockOnDateTimeChange = jest.fn(); + render( + + ); + expect( + screen.getByRole('button', { name: /calender-button/i }) + ).toBeInTheDocument(); + }); + + it('opens and closes the popover when the trigger button is clicked', async () => { + const user = userEvent.setup(); + const mockOnDateTimeChange = jest.fn(); + render( + + ); + + const triggerButton = screen.getByRole('button', { + name: /calender-button/i, + }); + + // Open popover + await user.click(triggerButton); + expect(screen.getByRole('dialog')).toBeInTheDocument(); // Popover content is a dialog + expect(screen.getByText(/February 2026/)).toBeInTheDocument(); // Check for specific content inside the calendar + + // Close popover using Escape key + fireEvent.keyDown(document, { key: 'Escape' }); + await waitFor(() => { + expect(screen.queryByText(/February 2026/)).not.toBeInTheDocument(); // Check for absence of specific content + }); + }); + + it('allows selecting a date from the calendar', async () => { + const user = userEvent.setup(); + const mockOnDateTimeChange = jest.fn(); + render( + + ); + + await user.click(screen.getByRole('button', { name: /calender-button/i })); // Open popover + + // Find a date in the current month (e.g., the 15th) + const dateToSelect = screen.getByRole('gridcell', { name: '15' }); + await user.click(dateToSelect); + + // Expect the popover to close after selecting a date + await waitFor(() => { + expect(screen.queryByText(/February 2026/)).not.toBeInTheDocument(); + }); + + // Check if onDateTimeChange was called with the correct date (year, month, and day) + expect(mockOnDateTimeChange).toHaveBeenCalledTimes(1); + const calledDate = mockOnDateTimeChange.mock.calls[0][0]; + expect(calledDate).toBeInstanceOf(Date); + expect(calledDate.getDate()).toBe(15); + expect(calledDate.getMonth()).toBe(new Date().getMonth()); // Assuming current month for simplicity + expect(calledDate.getFullYear()).toBe(new Date().getFullYear()); // Assuming current year for simplicity + expect(calledDate.getHours()).toBe(0); // Should reset time to 00:00:00 + expect(mockOnDateTimeChange.mock.calls[0][1]).toBe(false); // hasTime should be false + }); + + it('allows selecting an hour, minute, and AM/PM', async () => { + const user = userEvent.setup(); + const mockOnDateTimeChange = jest.fn(); + const initialDate = new Date(2024, 0, 15, 10, 30); // Jan 15, 2024, 10:30 AM + render( + + ); + + await user.click(screen.getByRole('button', { name: /calender-button/i })); // Open popover + + // Verify time selection elements are present + expect(screen.getByText('AM')).toBeInTheDocument(); + expect(screen.getByText('PM')).toBeInTheDocument(); + + // Select an hour (e.g., 2 PM) + await user.click(screen.getByRole('button', { name: '2' })); // Select hour 2 + expect(mockOnDateTimeChange).toHaveBeenCalledTimes(1); // One call for hour selection + let calledDate = mockOnDateTimeChange.mock.calls[0][0]; + expect(calledDate.getHours()).toBe(2); // Should be 2 AM initially before PM is clicked + + await user.click(screen.getByRole('button', { name: 'PM' })); // Select PM + expect(mockOnDateTimeChange).toHaveBeenCalledTimes(2); // Second call for AM/PM selection + calledDate = mockOnDateTimeChange.mock.calls[1][0]; + expect(calledDate.getHours()).toBe(14); // 2 PM + expect(mockOnDateTimeChange.mock.calls[1][1]).toBe(true); // hasTime should be true + + // Select a minute (e.g., 45 minutes) + await user.click(screen.getByRole('button', { name: '45' })); + expect(mockOnDateTimeChange).toHaveBeenCalledTimes(3); // Third call for minute selection + calledDate = mockOnDateTimeChange.mock.calls[2][0]; + expect(calledDate.getMinutes()).toBe(45); + expect(mockOnDateTimeChange.mock.calls[2][1]).toBe(true); // hasTime should be true + }); +}); diff --git a/frontend/src/components/ui/date-time-picker.tsx b/frontend/src/components/ui/date-time-picker.tsx index 0ee83ab1..d7e43f50 100644 --- a/frontend/src/components/ui/date-time-picker.tsx +++ b/frontend/src/components/ui/date-time-picker.tsx @@ -58,6 +58,10 @@ export const DateTimePicker = React.forwardRef< isInternalUpdate.current = true; onDateTimeChange(newDate, false); + setIsOpen((prev) => { + console.log('Closing popover, prev was:', prev); + return false; + }); } else { setInternalDate(undefined); setHasTime(false); diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index a575d8bd..952983f9 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -27,6 +27,6 @@ "@/*": ["./src/*"] } }, - "include": ["src"], + "include": ["src", "../../extra/multi-select-filter.test.tsx"], "references": [{ "path": "./tsconfig.node.json" }] }