Skip to content
Open
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
27 changes: 27 additions & 0 deletions frontend/jest.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
114 changes: 114 additions & 0 deletions frontend/src/components/ui/__tests__/date-time-picker.test.tsx
Original file line number Diff line number Diff line change
@@ -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(
<DateTimePicker
date={undefined}
onDateTimeChange={mockOnDateTimeChange}
/>
);
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(
<DateTimePicker
date={undefined}
onDateTimeChange={mockOnDateTimeChange}
/>
);

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(
<DateTimePicker
date={undefined}
onDateTimeChange={mockOnDateTimeChange}
/>
);

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(
<DateTimePicker
date={initialDate}
onDateTimeChange={mockOnDateTimeChange}
/>
);

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
});
});
4 changes: 4 additions & 0 deletions frontend/src/components/ui/date-time-picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion frontend/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,6 @@
"@/*": ["./src/*"]
}
},
"include": ["src"],
"include": ["src", "../../extra/multi-select-filter.test.tsx"],
"references": [{ "path": "./tsconfig.node.json" }]
}
Loading