diff --git a/frontend/src/components/HomeComponents/Tasks/__tests__/Tasks.test.tsx b/frontend/src/components/HomeComponents/Tasks/__tests__/Tasks.test.tsx
index f1dd1ed0..cb23d311 100644
--- a/frontend/src/components/HomeComponents/Tasks/__tests__/Tasks.test.tsx
+++ b/frontend/src/components/HomeComponents/Tasks/__tests__/Tasks.test.tsx
@@ -1992,4 +1992,193 @@ describe('Tasks Component', () => {
expect(screen.getByText('Show Reports')).toBeInTheDocument();
});
});
+
+ describe('Project, status, and tag filters', () => {
+ const getFilterChange = (title: string) => {
+ const { MultiSelectFilter } = require('@/components/ui/multi-select');
+ const call = MultiSelectFilter.mock.calls.find(
+ (entry: any[]) => entry[0].title === title
+ );
+ return call[0].onSelectionChange as (values: string[]) => void;
+ };
+
+ test('filters the list to the selected project', async () => {
+ render();
+ await screen.findByText('Task 12');
+
+ // Mock data: even indices (Task 1,3,5,...) are ProjectA; Task 12 is ProjectB.
+ act(() => {
+ getFilterChange('Projects')(['ProjectA']);
+ });
+
+ await waitFor(() => {
+ expect(screen.getByText('Task 11')).toBeInTheDocument();
+ expect(screen.getByText('Task 9')).toBeInTheDocument();
+ expect(screen.queryByText('Task 12')).not.toBeInTheDocument();
+ expect(screen.queryByText('Task 10')).not.toBeInTheDocument();
+ });
+ });
+
+ test('filters the list to the selected tag', async () => {
+ render();
+ await screen.findByText('Task 12');
+
+ // Mock data: tag1 is on Task 1,4,7,10 (i % 3 === 0); Task 12 has tag2.
+ act(() => {
+ getFilterChange('Tags')(['tag1']);
+ });
+
+ await waitFor(() => {
+ expect(screen.getByText('Task 10')).toBeInTheDocument();
+ expect(screen.getByText('Task 7')).toBeInTheDocument();
+ expect(screen.queryByText('Task 12')).not.toBeInTheDocument();
+ expect(screen.queryByText('Task 11')).not.toBeInTheDocument();
+ });
+ });
+
+ test('filters the list to completed status', async () => {
+ render();
+ await screen.findByText('Task 12');
+
+ const show = await screen.findByLabelText('Show:');
+ fireEvent.change(show, { target: { value: '20' } });
+
+ act(() => {
+ getFilterChange('Status')(['completed']);
+ });
+
+ await waitFor(() => {
+ expect(screen.getByText('Completed Task 1')).toBeInTheDocument();
+ expect(screen.queryByText('Task 12')).not.toBeInTheDocument();
+ expect(screen.queryByText('Deleted Task 1')).not.toBeInTheDocument();
+ });
+ });
+
+ test('clears a project filter when selection becomes empty', async () => {
+ render();
+ await screen.findByText('Task 12');
+
+ act(() => {
+ getFilterChange('Projects')(['ProjectA']);
+ });
+ await waitFor(() => {
+ expect(screen.queryByText('Task 12')).not.toBeInTheDocument();
+ });
+
+ act(() => {
+ getFilterChange('Projects')([]);
+ });
+ await waitFor(() => {
+ expect(screen.getByText('Task 11')).toBeInTheDocument();
+ expect(screen.getByText('Task 12')).toBeInTheDocument();
+ });
+ });
+ });
+
+ describe('Column sorting', () => {
+ test('clicking the ID header toggles ID sort order', async () => {
+ render();
+ await screen.findByText('Task 12');
+
+ const idHeader = screen.getByText('ID').closest('th') as HTMLElement;
+ // State starts as 'asc' while the loaded list is already desc, so the first
+ // click re-applies desc. The second click sorts ascending and reverses rows.
+ await act(async () => {
+ fireEvent.click(idHeader);
+ });
+ const afterFirst = screen
+ .getAllByTestId(/task-row-/)
+ .map((row) => row.getAttribute('data-testid'));
+
+ await act(async () => {
+ fireEvent.click(idHeader);
+ });
+
+ await waitFor(() => {
+ const afterSecond = screen
+ .getAllByTestId(/task-row-/)
+ .map((row) => row.getAttribute('data-testid'));
+ expect(afterSecond).not.toEqual(afterFirst);
+ });
+ });
+
+ test('clicking the Status header reorders tasks by status', async () => {
+ render();
+ const show = await screen.findByLabelText('Show:');
+ fireEvent.change(show, { target: { value: '20' } });
+ await screen.findByText('Completed Task 1');
+
+ const statusHeader = screen
+ .getByText('Status')
+ .closest('th') as HTMLElement;
+ const before = screen
+ .getAllByTestId(/task-row-/)
+ .map((row) => row.getAttribute('data-testid'));
+
+ await act(async () => {
+ fireEvent.click(statusHeader);
+ });
+
+ await waitFor(() => {
+ const after = screen
+ .getAllByTestId(/task-row-/)
+ .map((row) => row.getAttribute('data-testid'));
+ expect(after).not.toEqual(before);
+ });
+ });
+ });
+
+ describe('Empty search results', () => {
+ test('hides matching rows when search finds nothing', async () => {
+ render();
+ await screen.findByText('Task 12');
+
+ const search = screen.getByTestId('task-search-bar');
+ await act(async () => {
+ fireEvent.change(search, {
+ target: { value: 'zzzz-no-such-task-xyz' },
+ });
+ });
+
+ await waitFor(() => {
+ expect(screen.queryByText('Task 12')).not.toBeInTheDocument();
+ expect(screen.queryByText('Task 1')).not.toBeInTheDocument();
+ });
+ });
+ });
+
+ describe('Annotation edits from Tasks', () => {
+ test('persists annotations through the Tasks save handler', async () => {
+ const { editTaskOnBackend } = require('../hooks');
+ render();
+ await screen.findByText('Task 12');
+
+ fireEvent.click(screen.getByText('Task 12'));
+ await waitFor(() => {
+ expect(screen.getByText('Annotations:')).toBeInTheDocument();
+ });
+
+ const annotationsRow = screen
+ .getByText('Annotations:')
+ .closest('tr') as HTMLElement;
+ fireEvent.click(within(annotationsRow).getByLabelText('edit'));
+
+ const input = await screen.findByPlaceholderText(
+ 'Add an annotation (press enter to add)'
+ );
+ fireEvent.change(input, { target: { value: 'note from list test' } });
+ fireEvent.keyDown(input, { key: 'Enter' });
+ fireEvent.click(screen.getByLabelText('Save annotations'));
+
+ await waitFor(() => {
+ expect(editTaskOnBackend).toHaveBeenCalledWith(
+ expect.objectContaining({
+ annotations: [
+ expect.objectContaining({ description: 'note from list test' }),
+ ],
+ })
+ );
+ });
+ });
+ });
});