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/__tests__/multi-select-filter.test.tsx b/frontend/src/components/ui/__tests__/multi-select-filter.test.tsx
new file mode 100644
index 00000000..b5026549
--- /dev/null
+++ b/frontend/src/components/ui/__tests__/multi-select-filter.test.tsx
@@ -0,0 +1,229 @@
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { MultiSelectFilter } from '../multi-select';
+import '@testing-library/jest-dom';
+
+describe('MultiSelectFilter', () => {
+ const mockOptions = ['Option A', 'Option B', 'Option C'];
+ const mockOnSelectionChange = jest.fn();
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('renders with title and placeholder when no options selected', () => {
+ render(
+
+ );
+
+ expect(screen.getByText('Test Filter')).toBeInTheDocument();
+ expect(screen.getByRole('combobox')).toBeInTheDocument(); // The button serves as a combobox trigger
+ });
+
+ it('renders with selected values', () => {
+ render(
+
+ );
+
+ // Since selected values are visually indicated by checkboxes in the popover (which is closed),
+ // and potentially by a badge or count on the button (depending on implementation),
+ // let's check if the component renders without error first.
+ // Inspecting the component, it doesn't seem to show selected count on the button in this version, just the title.
+ // So we rely on the internal logic validation in interaction tests or if we open the popover.
+ // For this render test, ensuring it mounts with props is the baseline.
+ expect(screen.getByRole('combobox')).toBeInTheDocument();
+ });
+
+ it('applies custom className', () => {
+ render(
+
+ );
+
+ expect(screen.getByRole('combobox')).toHaveClass('custom-class');
+ });
+
+ it('renders icon when provided', () => {
+ const icon = Icon;
+ render(
+
+ );
+
+ expect(screen.getByTestId('test-icon')).toBeInTheDocument();
+ });
+
+ it('displays completion stats correctly', async () => {
+ const user = userEvent.setup();
+ const completionStats = {
+ 'Option A': { completed: 5, total: 10, percentage: 50 },
+ };
+
+ render(
+
+ );
+
+ // Open popover to see options and stats
+ await user.click(screen.getByRole('combobox'));
+
+ expect(screen.getByText('5/10 tasks, 50%')).toBeInTheDocument();
+ });
+
+ it('opens and closes the popover', async () => {
+ const user = userEvent.setup();
+ render(
+
+ );
+
+ const button = screen.getByRole('combobox');
+
+ // Open
+ await user.click(button);
+ expect(screen.getByText('All Test Filter')).toBeInTheDocument();
+
+ // Close by clicking button again
+ await user.click(button);
+ expect(screen.queryByText('All Test Filter')).not.toBeInTheDocument();
+
+ // Open again
+ await user.click(button);
+ expect(screen.getByText('All Test Filter')).toBeInTheDocument();
+
+ // Close by Escape
+ await user.keyboard('{Escape}');
+ expect(screen.queryByText('All Test Filter')).not.toBeInTheDocument();
+ });
+
+ it('selects and deselects options', async () => {
+ const user = userEvent.setup();
+ render(
+
+ );
+
+ const button = screen.getByRole('combobox');
+ await user.click(button);
+
+ // Select unselected option
+ await user.click(screen.getByText('Option B'));
+ expect(mockOnSelectionChange).toHaveBeenCalledWith([
+ 'Option A',
+ 'Option B',
+ ]);
+
+ // Deselect selected option
+ await user.click(screen.getByText('Option A'));
+ expect(mockOnSelectionChange).toHaveBeenCalledWith([]);
+ });
+
+ it('clears selection when "All" option is clicked', async () => {
+ const user = userEvent.setup();
+ render(
+
+ );
+
+ const button = screen.getByRole('combobox');
+ await user.click(button);
+
+ await user.click(screen.getByText('All Test Filter'));
+ expect(mockOnSelectionChange).toHaveBeenCalledWith([]);
+ });
+
+ it('filters options based on search input', async () => {
+ const user = userEvent.setup();
+ render(
+
+ );
+
+ await user.click(screen.getByRole('combobox'));
+ const searchInput = screen.getByPlaceholderText('Search test filter...');
+
+ await user.type(searchInput, 'Option A');
+
+ expect(screen.getByText('Option A')).toBeInTheDocument();
+ expect(screen.queryByText('Option B')).not.toBeInTheDocument();
+ });
+
+ it('displays "No results found" for no matches', async () => {
+ const user = userEvent.setup();
+ render(
+
+ );
+
+ await user.click(screen.getByRole('combobox'));
+ const searchInput = screen.getByPlaceholderText('Search test filter...');
+
+ await user.type(searchInput, 'Non-existent Option');
+
+ expect(screen.getByText('No results found.')).toBeInTheDocument();
+ });
+
+ it('search is case-insensitive', async () => {
+ const user = userEvent.setup();
+ render(
+
+ );
+
+ await user.click(screen.getByRole('combobox'));
+ const searchInput = screen.getByPlaceholderText('Search test filter...');
+
+ await user.type(searchInput, 'option a');
+
+ expect(screen.getByText('Option A')).toBeInTheDocument();
+ });
+});
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..148afbb0 100644
--- a/frontend/tsconfig.json
+++ b/frontend/tsconfig.json
@@ -27,6 +27,9 @@
"@/*": ["./src/*"]
}
},
- "include": ["src"],
+ "include": [
+ "src",
+ "src/components/ui/__tests__/multi-select-filter.test.tsx"
+ ],
"references": [{ "path": "./tsconfig.node.json" }]
}