diff --git a/frontend/src/components/HomeComponents/Tasks/__tests__/Tasks.test.tsx b/frontend/src/components/HomeComponents/Tasks/__tests__/Tasks.test.tsx index d650a64b..3c4a79fc 100644 --- a/frontend/src/components/HomeComponents/Tasks/__tests__/Tasks.test.tsx +++ b/frontend/src/components/HomeComponents/Tasks/__tests__/Tasks.test.tsx @@ -44,15 +44,20 @@ jest.mock('../tasks-utils', () => { }); jest.mock('@/components/ui/multi-select', () => ({ - MultiSelectFilter: jest.fn(({ title, completionStats }) => ( -
+ MultiSelectFilter: jest.fn(({ id, title, completionStats }) => ( +
+ )), })); @@ -509,27 +514,6 @@ describe('Tasks Component', () => { }); }); - describe('Overdue UI', () => { - test('shows red background on task ID and Overdue badge for overdue tasks', async () => { - render(); - - await screen.findByText('Task 12'); - - const dropdown = screen.getByLabelText('Show:'); - fireEvent.change(dropdown, { target: { value: '20' } }); - - const task1Description = screen.getByText('Task 1'); - const row = task1Description.closest('tr'); - const idElement = row?.querySelector('span'); - - expect(idElement).toHaveClass('bg-red-600/80'); - fireEvent.click(idElement!); - - const overdueBadge = await screen.findByText('Overdue'); - expect(overdueBadge).toBeInTheDocument(); - }); - }); - describe('Selection Logic', () => { it('adds a task UUID to selectedTaskUUIDs when an individual checkbox is clicked', async () => { render(); @@ -843,129 +827,150 @@ describe('Tasks Component', () => { }); }); - test('shows "overdue" in status filter options', async () => { - render(); + describe('Overdue', () => { + test('shows "overdue" in status filter options', async () => { + render(); - expect(await screen.findByText('Mocked BottomBar')).toBeInTheDocument(); + expect(await screen.findByText('Mocked BottomBar')).toBeInTheDocument(); - const multiSelectFilter = require('@/components/ui/multi-select'); + const multiSelectFilter = require('@/components/ui/multi-select'); - expect(multiSelectFilter.MultiSelectFilter).toHaveBeenCalledWith( - expect.objectContaining({ - title: 'Status', - options: expect.arrayContaining(['overdue']), - }), - {} - ); - }); + expect(multiSelectFilter.MultiSelectFilter).toHaveBeenCalledWith( + expect.objectContaining({ + title: 'Status', + options: expect.arrayContaining(['overdue']), + }), + {} + ); + }); - test('filters tasks to show only overdue tasks when status "overdue" is selected', async () => { - const MultiSelectFilter = - require('@/components/ui/multi-select').MultiSelectFilter; + test('filters tasks to show only overdue tasks when status "overdue" is selected', async () => { + const MultiSelectFilter = + require('@/components/ui/multi-select').MultiSelectFilter; - MultiSelectFilter.mockImplementation(({ title }: { title: string }) => { - return
Mocked MultiSelect: {title}
; - }); + render(); - render(); + await waitFor(async () => { + expect(await screen.findByText('Task 12')).toBeInTheDocument(); + }); - await waitFor(async () => { - expect(await screen.findByText('Task 12')).toBeInTheDocument(); - }); + const lastCall = MultiSelectFilter.mock.calls.find( + (call: any[]) => call[0].title === 'Status' + ); - const lastCall = MultiSelectFilter.mock.calls.find( - (call: any[]) => call[0].title === 'Status' - ); + const onSelectionChange = lastCall[0].onSelectionChange; - const onSelectionChange = lastCall[0].onSelectionChange; + act(() => { + onSelectionChange(['overdue']); + }); - act(() => { - onSelectionChange(['overdue']); + const overdueTask = screen.getByText('Task 1'); + expect(overdueTask).toBeInTheDocument(); + expect(screen.queryByText('Task 2')).not.toBeInTheDocument(); }); - const overdueTask = screen.getByText('Task 1'); - expect(overdueTask).toBeInTheDocument(); - expect(screen.queryByText('Task 2')).not.toBeInTheDocument(); - }); + test('shows red background on task ID and Overdue badge for overdue tasks', async () => { + render(); - test('shows "O" badge for overdue tasks in status column', async () => { - render(); + await screen.findByText('Task 12'); - await screen.findByText('Task 12'); + const dropdown = screen.getByLabelText('Show:'); + fireEvent.change(dropdown, { target: { value: '20' } }); - const dropdown = screen.getByLabelText('Show:'); - fireEvent.change(dropdown, { target: { value: '20' } }); + const task1Description = screen.getByText('Task 1'); + const row = task1Description.closest('tr'); + const idElement = row?.querySelector('span'); - const row = screen.getByText('Task 1').closest('tr')!; - const statusCell = within(row).getByText('O'); + expect(idElement).toHaveClass('bg-red-600/80'); + fireEvent.click(idElement!); - expect(statusCell).toBeInTheDocument(); - }); + const overdueBadge = await screen.findByText('Overdue'); + expect(overdueBadge).toBeInTheDocument(); + }); - test('does not show "O" badge for non-overdue pending tasks', async () => { - render(); + test('shows "O" badge for overdue tasks in status column', async () => { + render(); - await screen.findByText('Task 12'); + await screen.findByText('Task 12'); - const dropdown = screen.getByLabelText('Show:'); - fireEvent.change(dropdown, { target: { value: '20' } }); + const dropdown = screen.getByLabelText('Show:'); + fireEvent.change(dropdown, { target: { value: '20' } }); - expect(await screen.findByText('Task 2')).toBeInTheDocument(); + const row = screen.getByText('Task 1').closest('tr')!; + const statusCell = within(row).getByText('O'); - const row = screen.getByText('Task 2').closest('tr')!; - const statusCell = within(row).getByText('P'); + expect(statusCell).toBeInTheDocument(); + }); - expect(statusCell).toBeInTheDocument(); - }); + test('does not show "O" badge for non-overdue pending tasks', async () => { + render(); - test('overdue tasks appear at the top of the list', async () => { - render(); + await screen.findByText('Task 12'); - await screen.findByText('Task 12'); + const dropdown = screen.getByLabelText('Show:'); + fireEvent.change(dropdown, { target: { value: '20' } }); - const dropdown = screen.getByLabelText('Show:'); - fireEvent.change(dropdown, { target: { value: '20' } }); + expect(await screen.findByText('Task 2')).toBeInTheDocument(); - const firstRow = screen.getAllByRole('row')[1]; - expect(within(firstRow).getByText('Task 1')).toBeInTheDocument(); - }); + const row = screen.getByText('Task 2').closest('tr')!; + const statusCell = within(row).getByText('P'); - test('project dropdown lists existing projects and create-new option', async () => { - render(); + expect(statusCell).toBeInTheDocument(); + }); - expect(await screen.findByText('Task 1')).toBeInTheDocument(); + test('overdue tasks appear at the top of the list', async () => { + render(); - const addTaskButton = screen.getByRole('button', { name: /add task/i }); - fireEvent.click(addTaskButton); + await screen.findByText('Task 12'); - const projectSelect = await screen.findByTestId('project-select'); - expect( - within(projectSelect).getByText('Select a project') - ).toBeInTheDocument(); - expect(within(projectSelect).getByText('Engineering')).toBeInTheDocument(); - expect(within(projectSelect).getByText('ProjectA')).toBeInTheDocument(); - expect( - within(projectSelect).getByText('+ Create new project…') - ).toBeInTheDocument(); - }); + const dropdown = screen.getByLabelText('Show:'); + fireEvent.change(dropdown, { target: { value: '20' } }); - test('selecting "+ Create new project…" reveals inline input', async () => { - render(); + const firstRow = screen.getAllByRole('row')[1]; + expect(within(firstRow).getByText('Task 1')).toBeInTheDocument(); + }); + }); - expect(await screen.findByText('Task 1')).toBeInTheDocument(); + describe('Add Task Dialog', () => { + test('project dropdown lists existing projects and create-new option', async () => { + render(); - fireEvent.click(screen.getByRole('button', { name: /add task/i })); + expect(await screen.findByText('Task 1')).toBeInTheDocument(); - const projectSelect = await screen.findByTestId('project-select'); - fireEvent.change(projectSelect, { target: { value: '__CREATE_NEW__' } }); // Empty string triggers "create new project" mode + const addTaskButton = screen.getByRole('button', { name: /add task/i }); + fireEvent.click(addTaskButton); - const newProjectInput = - await screen.findByPlaceholderText('New project name'); - fireEvent.change(newProjectInput, { - target: { value: 'My Fresh Project' }, + const projectSelect = await screen.findByTestId('project-select'); + expect( + within(projectSelect).getByText('Select a project') + ).toBeInTheDocument(); + expect( + within(projectSelect).getByText('Engineering') + ).toBeInTheDocument(); + expect(within(projectSelect).getByText('ProjectA')).toBeInTheDocument(); + expect( + within(projectSelect).getByText('+ Create new project…') + ).toBeInTheDocument(); }); - expect(newProjectInput).toHaveValue('My Fresh Project'); + test('selecting "+ Create new project…" reveals inline input', async () => { + render(); + + expect(await screen.findByText('Task 1')).toBeInTheDocument(); + + fireEvent.click(screen.getByRole('button', { name: /add task/i })); + + const projectSelect = await screen.findByTestId('project-select'); + fireEvent.change(projectSelect, { target: { value: '__CREATE_NEW__' } }); // Empty string triggers "create new project" mode + + const newProjectInput = + await screen.findByPlaceholderText('New project name'); + fireEvent.change(newProjectInput, { + target: { value: 'My Fresh Project' }, + }); + + expect(newProjectInput).toHaveValue('My Fresh Project'); + }); }); // Task Dependencies Tests @@ -1006,574 +1011,585 @@ describe('Tasks Component', () => { }); }); - test('shows red border when task is marked as completed', async () => { - render(); + describe('Unsync', () => { + test('shows red border when task is marked as completed', async () => { + render(); - await waitFor(async () => { - expect(await screen.findByText('Task 12')).toBeInTheDocument(); - }); + await waitFor(async () => { + expect(await screen.findByText('Task 12')).toBeInTheDocument(); + }); - const task12 = screen.getByText('Task 12'); - fireEvent.click(task12); + const task12 = screen.getByText('Task 12'); + fireEvent.click(task12); - await waitFor(() => { - const completeButton = screen.getByLabelText('complete task'); - fireEvent.click(completeButton); - }); + await waitFor(() => { + const completeButton = screen.getByLabelText('complete task'); + fireEvent.click(completeButton); + }); - const yesButton = screen.getAllByText('Yes')[0]; - fireEvent.click(yesButton); + const yesButton = screen.getAllByText('Yes')[0]; + fireEvent.click(yesButton); - await waitFor(() => { - const row = screen.getByTestId('task-row-12'); - expect(row).toHaveClass('border-l-red-500'); + await waitFor(() => { + const row = screen.getByTestId('task-row-12'); + expect(row).toHaveClass('border-l-red-500'); + }); }); - }); - test('shows red border when task is deleted', async () => { - render(); + test('shows red border when task is deleted', async () => { + render(); - await waitFor(async () => { - expect(await screen.findByText('Task 12')).toBeInTheDocument(); - }); + await waitFor(async () => { + expect(await screen.findByText('Task 12')).toBeInTheDocument(); + }); - const task12 = screen.getByText('Task 12'); - fireEvent.click(task12); + const task12 = screen.getByText('Task 12'); + fireEvent.click(task12); - await waitFor(() => { - const deleteButton = screen.getByLabelText('delete task'); - fireEvent.click(deleteButton); - }); + await waitFor(() => { + const deleteButton = screen.getByLabelText('delete task'); + fireEvent.click(deleteButton); + }); - await waitFor(() => { - const yesButtons = screen.getAllByText('Yes'); - if (yesButtons.length > 0) fireEvent.click(yesButtons[0]); - }); + await waitFor(() => { + const yesButtons = screen.getAllByText('Yes'); + if (yesButtons.length > 0) fireEvent.click(yesButtons[0]); + }); - await waitFor(() => { - const row = screen.getByTestId('task-row-12'); - expect(row).toHaveClass('border-l-red-500'); + await waitFor(() => { + const row = screen.getByTestId('task-row-12'); + expect(row).toHaveClass('border-l-red-500'); + }); }); - }); - test('shows unsynced count after bulk delete', async () => { - render(); + test('shows unsynced count after bulk delete', async () => { + render(); - await screen.findByText('Task 1'); - const checkboxes = screen.getAllByRole('checkbox'); + await screen.findByText('Task 1'); + const checkboxes = screen.getAllByRole('checkbox'); - fireEvent.click(checkboxes[1]); - fireEvent.click(checkboxes[2]); + fireEvent.click(checkboxes[1]); + fireEvent.click(checkboxes[2]); - const deleteBtn = screen.getByText(/Delete 2 Tasks/i); - fireEvent.click(deleteBtn); + const deleteBtn = screen.getByText(/Delete 2 Tasks/i); + fireEvent.click(deleteBtn); - const yesButton = await screen.findByText('Yes'); - fireEvent.click(yesButton); + const yesButton = await screen.findByText('Yes'); + fireEvent.click(yesButton); - await waitFor(() => { - const syncButton = document.getElementById('sync-task'); - expect(within(syncButton!).getByText('2')).toBeInTheDocument(); + await waitFor(() => { + const syncButton = document.getElementById('sync-task'); + expect(within(syncButton!).getByText('2')).toBeInTheDocument(); + }); }); - }); - - test('shows unsynced count after bulk complete', async () => { - render(); - await screen.findByText('Task 1'); - const checkboxes = screen.getAllByRole('checkbox'); - - fireEvent.click(checkboxes[1]); - fireEvent.click(checkboxes[2]); + test('shows unsynced count after bulk complete', async () => { + render(); - const bulkButton = screen.getByTestId('bulk-complete-btn'); - fireEvent.click(bulkButton); + await screen.findByText('Task 1'); + const checkboxes = screen.getAllByRole('checkbox'); - const yesButton = await screen.findByText('Yes'); - fireEvent.click(yesButton); + fireEvent.click(checkboxes[1]); + fireEvent.click(checkboxes[2]); - await waitFor(() => { - const syncButton = document.getElementById('sync-task'); - expect(within(syncButton!).getByText('2')).toBeInTheDocument(); - }); - }); + const bulkButton = screen.getByTestId('bulk-complete-btn'); + fireEvent.click(bulkButton); - test('shows red border when task description is edited', async () => { - render(); + const yesButton = await screen.findByText('Yes'); + fireEvent.click(yesButton); - await waitFor(async () => { - expect(await screen.findByText('Task 12')).toBeInTheDocument(); + await waitFor(() => { + const syncButton = document.getElementById('sync-task'); + expect(within(syncButton!).getByText('2')).toBeInTheDocument(); + }); }); - const task12 = screen.getByText('Task 12'); + test('shows red border when task description is edited', async () => { + render(); - fireEvent.click(task12); + await waitFor(async () => { + expect(await screen.findByText('Task 12')).toBeInTheDocument(); + }); - await waitFor(() => { - expect(screen.getByText('Description:')).toBeInTheDocument(); - }); + const task12 = screen.getByText('Task 12'); - const descriptionLabel = screen.getByText('Description:'); - const descRow = descriptionLabel.closest('tr') as HTMLElement; - const editButton = within(descRow).getByLabelText('edit'); + fireEvent.click(task12); - fireEvent.click(editButton); + await waitFor(() => { + expect(screen.getByText('Description:')).toBeInTheDocument(); + }); - const input = await screen.findByDisplayValue('Task 12'); + const descriptionLabel = screen.getByText('Description:'); + const descRow = descriptionLabel.closest('tr') as HTMLElement; + const editButton = within(descRow).getByLabelText('edit'); - fireEvent.change(input, { target: { value: 'Updated Task 12' } }); + fireEvent.click(editButton); - const saveButton = screen.getByLabelText('save'); + const input = await screen.findByDisplayValue('Task 12'); - fireEvent.click(saveButton); + fireEvent.change(input, { target: { value: 'Updated Task 12' } }); - await waitFor(() => { - const row = screen.getByTestId('task-row-12'); - expect(row).toHaveClass('border-l-red-500'); - }); - }); + const saveButton = screen.getByLabelText('save'); - test('shows red border when task project is edited', async () => { - render(); + fireEvent.click(saveButton); - await waitFor(async () => { - expect(await screen.findByText('Task 12')).toBeInTheDocument(); + await waitFor(() => { + const row = screen.getByTestId('task-row-12'); + expect(row).toHaveClass('border-l-red-500'); + }); }); - const task12 = screen.getByText('Task 12'); - fireEvent.click(task12); + test('shows red border when task project is edited', async () => { + render(); - await waitFor(() => { - expect(screen.getByText('Project:')).toBeInTheDocument(); - }); + await waitFor(async () => { + expect(await screen.findByText('Task 12')).toBeInTheDocument(); + }); - const projectLabel = screen.getByText('Project:'); - const projectRow = projectLabel.closest('tr') as HTMLElement; - const editButton = within(projectRow).getByLabelText('edit'); - fireEvent.click(editButton); + const task12 = screen.getByText('Task 12'); + fireEvent.click(task12); - const projectSelect = await screen.findByTestId('project-select'); - fireEvent.change(projectSelect, { target: { value: 'ProjectA' } }); + await waitFor(() => { + expect(screen.getByText('Project:')).toBeInTheDocument(); + }); - fireEvent.keyDown(document.body, { key: 'Escape' }); + const projectLabel = screen.getByText('Project:'); + const projectRow = projectLabel.closest('tr') as HTMLElement; + const editButton = within(projectRow).getByLabelText('edit'); + fireEvent.click(editButton); - await waitFor(() => { - const row = screen.getByTestId('task-row-12'); - expect(row).toHaveClass('border-l-red-500'); - }); - }); + const projectSelect = await screen.findByTestId('project-select'); + fireEvent.change(projectSelect, { target: { value: 'ProjectA' } }); - test.each([ - ['Wait', 'Wait:', 'Select wait date and time'], - ['End', 'End:', 'Select end date and time'], - ['Due', 'Due:', 'Select due date and time'], - ['Start', 'Start:', 'Select start date and time'], - ['Entry', 'Entry:', 'Select entry date and time'], - ])('shows red when task %s date is edited', async (_, label, placeholder) => { - render(); + fireEvent.keyDown(document.body, { key: 'Escape' }); - await waitFor(async () => { - expect(await screen.findByText('Task 12')).toBeInTheDocument(); + await waitFor(() => { + const row = screen.getByTestId('task-row-12'); + expect(row).toHaveClass('border-l-red-500'); + }); }); - const task12 = screen.getByText('Task 12'); - fireEvent.click(task12); + test.each([ + ['Wait', 'Wait:', 'Select wait date and time'], + ['End', 'End:', 'Select end date and time'], + ['Due', 'Due:', 'Select due date and time'], + ['Start', 'Start:', 'Select start date and time'], + ['Entry', 'Entry:', 'Select entry date and time'], + ])( + 'shows red when task %s date is edited', + async (_, label, placeholder) => { + render(); - await waitFor(() => { - expect(screen.getByText(label)).toBeInTheDocument(); - }); + await waitFor(async () => { + expect(await screen.findByText('Task 12')).toBeInTheDocument(); + }); - const dateLabel = screen.getByText(label); - const dateRow = dateLabel.closest('tr') as HTMLElement; + const task12 = screen.getByText('Task 12'); + fireEvent.click(task12); - const editButton = within(dateRow).getByLabelText('edit'); - fireEvent.click(editButton); + await waitFor(() => { + expect(screen.getByText(label)).toBeInTheDocument(); + }); - const dateButton = within(dateRow).getByText(placeholder).closest('button'); - fireEvent.click(dateButton!); + const dateLabel = screen.getByText(label); + const dateRow = dateLabel.closest('tr') as HTMLElement; - await waitFor(() => { - expect(screen.getByRole('dialog')).toBeInTheDocument(); - }); + const editButton = within(dateRow).getByLabelText('edit'); + fireEvent.click(editButton); - const dialog = screen.getByRole('dialog'); - const day15 = within(dialog).getAllByText('15')[0]; - fireEvent.click(day15); + const dateButton = within(dateRow) + .getByText(placeholder) + .closest('button'); + fireEvent.click(dateButton!); - const saveButton = screen.getByLabelText('save'); - fireEvent.click(saveButton); + await waitFor(() => { + expect(screen.getByRole('dialog')).toBeInTheDocument(); + }); - await waitFor(() => { - const row = screen.getByTestId('task-row-12'); - expect(row).toHaveClass('border-l-red-500'); - }); - }); + const dialog = screen.getByRole('dialog'); + const day15 = within(dialog).getAllByText('15')[0]; + fireEvent.click(day15); - test('shows red border when task priority is edited', async () => { - render(); + const saveButton = screen.getByLabelText('save'); + fireEvent.click(saveButton); - await waitFor(async () => { - expect(await screen.findByText('Task 12')).toBeInTheDocument(); - }); + await waitFor(() => { + const row = screen.getByTestId('task-row-12'); + expect(row).toHaveClass('border-l-red-500'); + }); + } + ); - const task12 = screen.getByText('Task 12'); - fireEvent.click(task12); + test('shows red border when task priority is edited', async () => { + render(); - await waitFor(() => { - expect(screen.getByText('Priority:')).toBeInTheDocument(); - }); + await waitFor(async () => { + expect(await screen.findByText('Task 12')).toBeInTheDocument(); + }); - const priorityLabel = screen.getByText('Priority:'); - const priorityRow = priorityLabel.closest('tr') as HTMLElement; + const task12 = screen.getByText('Task 12'); + fireEvent.click(task12); - const editButton = within(priorityRow).getByLabelText('edit'); - fireEvent.click(editButton); + await waitFor(() => { + expect(screen.getByText('Priority:')).toBeInTheDocument(); + }); - const select = within(priorityRow).getByTestId('project-select'); - fireEvent.change(select, { target: { value: 'H' } }); + const priorityLabel = screen.getByText('Priority:'); + const priorityRow = priorityLabel.closest('tr') as HTMLElement; - const saveButton = screen.getByLabelText('save'); - fireEvent.click(saveButton); + const editButton = within(priorityRow).getByLabelText('edit'); + fireEvent.click(editButton); - await waitFor(() => { - const row = screen.getByTestId('task-row-12'); - expect(row).toHaveClass('border-l-red-500'); - }); - }); + const select = within(priorityRow).getByTestId('project-select'); + fireEvent.change(select, { target: { value: 'H' } }); - test('shows red border when task dependencies are edited', async () => { - render(); + const saveButton = screen.getByLabelText('save'); + fireEvent.click(saveButton); - await waitFor(async () => { - expect(await screen.findByText('Task 12')).toBeInTheDocument(); + await waitFor(() => { + const row = screen.getByTestId('task-row-12'); + expect(row).toHaveClass('border-l-red-500'); + }); }); - const task12 = screen.getByText('Task 12'); - fireEvent.click(task12); + test('shows red border when task dependencies are edited', async () => { + render(); - await waitFor(() => { - expect(screen.getByText('Depends:')).toBeInTheDocument(); - }); + await waitFor(async () => { + expect(await screen.findByText('Task 12')).toBeInTheDocument(); + }); - const dependsLabel = screen.getByText('Depends:'); - const dependsRow = dependsLabel.closest('tr') as HTMLElement; + const task12 = screen.getByText('Task 12'); + fireEvent.click(task12); - const editButton = within(dependsRow).getByLabelText('edit'); - fireEvent.click(editButton); + await waitFor(() => { + expect(screen.getByText('Depends:')).toBeInTheDocument(); + }); - const addDependecyButton = within(dependsRow) - .getByText('Add Dependency') - .closest('button'); - fireEvent.click(addDependecyButton!); + const dependsLabel = screen.getByText('Depends:'); + const dependsRow = dependsLabel.closest('tr') as HTMLElement; - const dropdown = within(dependsRow).getByTestId('dependency-dropdown'); + const editButton = within(dependsRow).getByLabelText('edit'); + fireEvent.click(editButton); - fireEvent.click(within(dropdown).getByText('Task 11')); - fireEvent.click(within(dropdown).getByText('Task 10')); + const addDependecyButton = within(dependsRow) + .getByText('Add Dependency') + .closest('button'); + fireEvent.click(addDependecyButton!); - const saveButton = screen.getByLabelText('save'); - fireEvent.click(saveButton); + const dropdown = within(dependsRow).getByTestId('dependency-dropdown'); - await waitFor(() => { - const row = screen.getByTestId('task-row-12'); - expect(row).toHaveClass('border-l-red-500'); - }); - }); + fireEvent.click(within(dropdown).getByText('Task 11')); + fireEvent.click(within(dropdown).getByText('Task 10')); - test('shows red border when task tags are edited', async () => { - render(); + const saveButton = screen.getByLabelText('save'); + fireEvent.click(saveButton); - await waitFor(async () => { - expect(await screen.findByText('Task 12')).toBeInTheDocument(); + await waitFor(() => { + const row = screen.getByTestId('task-row-12'); + expect(row).toHaveClass('border-l-red-500'); + }); }); - const task12 = screen.getByText('Task 12'); - fireEvent.click(task12); + test('shows red border when task tags are edited', async () => { + render(); - await waitFor(() => { - expect(screen.getByText('Tags:')).toBeInTheDocument(); - }); + await waitFor(async () => { + expect(await screen.findByText('Task 12')).toBeInTheDocument(); + }); - const tagsLabel = screen.getByText('Tags:'); - const tagsRow = tagsLabel.closest('tr') as HTMLElement; + const task12 = screen.getByText('Task 12'); + fireEvent.click(task12); - const editButton = within(tagsRow).getByLabelText('edit'); - fireEvent.click(editButton); + await waitFor(() => { + expect(screen.getByText('Tags:')).toBeInTheDocument(); + }); - const tagSelectButton = await screen.findByRole('button', { - name: /select items/i, - }); - fireEvent.click(tagSelectButton); + const tagsLabel = screen.getByText('Tags:'); + const tagsRow = tagsLabel.closest('tr') as HTMLElement; - const editInput = await screen.findByPlaceholderText('Search or create...'); + const editButton = within(tagsRow).getByLabelText('edit'); + fireEvent.click(editButton); - fireEvent.change(editInput, { target: { value: 'unsyncedtag' } }); - fireEvent.keyDown(editInput, { key: 'Enter', code: 'Enter' }); + const tagSelectButton = await screen.findByRole('button', { + name: /select items/i, + }); + fireEvent.click(tagSelectButton); - const saveButton = screen.getByLabelText('Save items'); - fireEvent.click(saveButton); + const editInput = await screen.findByPlaceholderText( + 'Search or create...' + ); - await waitFor(() => { - const row = screen.getByTestId('task-row-12'); - expect(row).toHaveClass('border-l-red-500'); - }); - }); + fireEvent.change(editInput, { target: { value: 'unsyncedtag' } }); + fireEvent.keyDown(editInput, { key: 'Enter', code: 'Enter' }); - test('shows red border when task recur is edited', async () => { - render(); + const saveButton = screen.getByLabelText('Save items'); + fireEvent.click(saveButton); - await waitFor(async () => { - expect(await screen.findByText('Task 12')).toBeInTheDocument(); + await waitFor(() => { + const row = screen.getByTestId('task-row-12'); + expect(row).toHaveClass('border-l-red-500'); + }); }); - const task12 = screen.getByText('Task 12'); - fireEvent.click(task12); + test('shows red border when task recur is edited', async () => { + render(); - await waitFor(() => { - expect(screen.getByText('Recur:')).toBeInTheDocument(); - }); + await waitFor(async () => { + expect(await screen.findByText('Task 12')).toBeInTheDocument(); + }); - const recurLabel = screen.getByText('Recur:'); - const recurRow = recurLabel.closest('tr') as HTMLElement; + const task12 = screen.getByText('Task 12'); + fireEvent.click(task12); - const editButton = within(recurRow).getByLabelText('edit'); - fireEvent.click(editButton); + await waitFor(() => { + expect(screen.getByText('Recur:')).toBeInTheDocument(); + }); - const select = within(recurRow).getByTestId('project-select'); - fireEvent.change(select, { target: { value: 'weekly' } }); + const recurLabel = screen.getByText('Recur:'); + const recurRow = recurLabel.closest('tr') as HTMLElement; - const saveButton = screen.getByLabelText('save'); - fireEvent.click(saveButton); + const editButton = within(recurRow).getByLabelText('edit'); + fireEvent.click(editButton); - await waitFor(() => { - const row = screen.getByTestId('task-row-12'); - expect(row).toHaveClass('border-l-red-500'); - }); - }); + const select = within(recurRow).getByTestId('project-select'); + fireEvent.change(select, { target: { value: 'weekly' } }); - test('shows and updates notification badge count on Sync button', async () => { - render(); + const saveButton = screen.getByLabelText('save'); + fireEvent.click(saveButton); - await waitFor(async () => { - expect(await screen.findByText('Task 12')).toBeInTheDocument(); + await waitFor(() => { + const row = screen.getByTestId('task-row-12'); + expect(row).toHaveClass('border-l-red-500'); + }); }); - const task12 = screen.getByText('Task 12'); - fireEvent.click(task12); + test('shows and updates notification badge count on Sync button', async () => { + render(); - await waitFor(() => { - const completeButton = screen.getByLabelText('complete task'); - fireEvent.click(completeButton); - }); + await waitFor(async () => { + expect(await screen.findByText('Task 12')).toBeInTheDocument(); + }); - const yesButton = screen.getAllByText('Yes')[0]; - fireEvent.click(yesButton); + const task12 = screen.getByText('Task 12'); + fireEvent.click(task12); - await waitFor(() => { - const row = screen.getByTestId('task-row-12'); - expect(row).toHaveClass('border-l-red-500'); - }); + await waitFor(() => { + const completeButton = screen.getByLabelText('complete task'); + fireEvent.click(completeButton); + }); - const syncButtons = screen.getAllByText('Sync'); - const syncBtnContainer = syncButtons[0].closest('button'); + const yesButton = screen.getAllByText('Yes')[0]; + fireEvent.click(yesButton); - if (syncBtnContainer) { - expect(within(syncBtnContainer).getByText('1')).toBeInTheDocument(); - } else { - throw new Error('Sync button not found'); - } - }); + await waitFor(() => { + const row = screen.getByTestId('task-row-12'); + expect(row).toHaveClass('border-l-red-500'); + }); - test('clears red border after sync', async () => { - render(); + const syncButtons = screen.getAllByText('Sync'); + const syncBtnContainer = syncButtons[0].closest('button'); - await waitFor(async () => { - expect(await screen.findByText('Task 12')).toBeInTheDocument(); + if (syncBtnContainer) { + expect(within(syncBtnContainer).getByText('1')).toBeInTheDocument(); + } else { + throw new Error('Sync button not found'); + } }); - const task12 = screen.getByText('Task 12'); - fireEvent.click(task12); - - await waitFor(() => { - const completeButton = screen.getByLabelText('complete task'); - fireEvent.click(completeButton); - }); + test('clears red border after sync', async () => { + render(); - const yesButton = screen.getAllByText('Yes')[0]; - fireEvent.click(yesButton); + await waitFor(async () => { + expect(await screen.findByText('Task 12')).toBeInTheDocument(); + }); - await waitFor(() => { - const row = screen.getByTestId('task-row-12'); - expect(row).toHaveClass('border-l-red-500'); - }); + const task12 = screen.getByText('Task 12'); + fireEvent.click(task12); - const hooks = require('../hooks'); - hooks.fetchTaskwarriorTasks.mockResolvedValueOnce([ - { - id: 12, - description: 'Task 12', - status: 'completed', - project: 'ProjectA', - tags: ['tag1'], - uuid: 'uuid-12', - }, - ]); + await waitFor(() => { + const completeButton = screen.getByLabelText('complete task'); + fireEvent.click(completeButton); + }); - const syncButtons = screen.getAllByText('Sync'); - fireEvent.click(syncButtons[0]); + const yesButton = screen.getAllByText('Yes')[0]; + fireEvent.click(yesButton); - await waitFor(() => { - const row = screen.getByTestId('task-row-12'); - expect(row).not.toHaveClass('border-l-red-500'); - }); - }); + await waitFor(() => { + const row = screen.getByTestId('task-row-12'); + expect(row).toHaveClass('border-l-red-500'); + }); - test('calculates and passes project completion stats to MultiSelectFilter', async () => { - render(); + const hooks = require('../hooks'); + hooks.fetchTaskwarriorTasks.mockResolvedValueOnce([ + { + id: 12, + description: 'Task 12', + status: 'completed', + project: 'ProjectA', + tags: ['tag1'], + uuid: 'uuid-12', + }, + ]); + + const syncButtons = screen.getAllByText('Sync'); + fireEvent.click(syncButtons[0]); - await waitFor(async () => { - expect(await screen.findByText('Task 1')).toBeInTheDocument(); + await waitFor(() => { + const row = screen.getByTestId('task-row-12'); + expect(row).not.toHaveClass('border-l-red-500'); + }); }); + }); - const { MultiSelectFilter } = require('@/components/ui/multi-select'); + describe('Completion Stats', () => { + test('calculates and passes project completion stats to MultiSelectFilter', async () => { + render(); - // Find the Projects filter call - const projectsFilterCall = MultiSelectFilter.mock.calls.find( - (call: any) => call[0].title === 'Projects' - ); + await waitFor(async () => { + expect(await screen.findByText('Task 1')).toBeInTheDocument(); + }); - expect(projectsFilterCall).toBeDefined(); - expect(projectsFilterCall[0].completionStats).toBeDefined(); + const { MultiSelectFilter } = require('@/components/ui/multi-select'); - const stats = projectsFilterCall[0].completionStats; + // Find the Projects filter call + const projectsFilterCall = MultiSelectFilter.mock.calls.find( + (call: any) => call[0].title === 'Projects' + ); - // ProjectA has tasks: 1,3,5,7,9,11 (pending) + task 16 (completed) = 1 completed out of 7 total - expect(stats['ProjectA']).toBeDefined(); - expect(stats['ProjectA'].completed).toBeGreaterThanOrEqual(1); - expect(stats['ProjectA'].total).toBeGreaterThanOrEqual(1); - expect(stats['ProjectA'].percentage).toBeGreaterThanOrEqual(0); - expect(stats['ProjectA'].percentage).toBeLessThanOrEqual(100); + expect(projectsFilterCall).toBeDefined(); + expect(projectsFilterCall[0].completionStats).toBeDefined(); - // ProjectB has tasks: 2,4,6,8,10,12 (pending) + task 17 (deleted) = 0 completed - expect(stats['ProjectB']).toBeDefined(); - expect(stats['ProjectB'].total).toBeGreaterThanOrEqual(1); - }); + const stats = projectsFilterCall[0].completionStats; - test('calculates and passes tag completion stats to MultiSelectFilter', async () => { - render(); + // ProjectA has tasks: 1,3,5,7,9,11 (pending) + task 16 (completed) = 1 completed out of 7 total + expect(stats['ProjectA']).toBeDefined(); + expect(stats['ProjectA'].completed).toBeGreaterThanOrEqual(1); + expect(stats['ProjectA'].total).toBeGreaterThanOrEqual(1); + expect(stats['ProjectA'].percentage).toBeGreaterThanOrEqual(0); + expect(stats['ProjectA'].percentage).toBeLessThanOrEqual(100); - await waitFor(async () => { - expect(await screen.findByText('Task 1')).toBeInTheDocument(); + // ProjectB has tasks: 2,4,6,8,10,12 (pending) + task 17 (deleted) = 0 completed + expect(stats['ProjectB']).toBeDefined(); + expect(stats['ProjectB'].total).toBeGreaterThanOrEqual(1); }); - const { MultiSelectFilter } = require('@/components/ui/multi-select'); + test('calculates and passes tag completion stats to MultiSelectFilter', async () => { + render(); - // Find the Tags filter call - const tagsFilterCall = MultiSelectFilter.mock.calls.find( - (call: any) => call[0].title === 'Tags' - ); + await waitFor(async () => { + expect(await screen.findByText('Task 1')).toBeInTheDocument(); + }); - expect(tagsFilterCall).toBeDefined(); - expect(tagsFilterCall[0].completionStats).toBeDefined(); + const { MultiSelectFilter } = require('@/components/ui/multi-select'); - const stats = tagsFilterCall[0].completionStats; + // Find the Tags filter call + const tagsFilterCall = MultiSelectFilter.mock.calls.find( + (call: any) => call[0].title === 'Tags' + ); - // Verify stats structure - Object.keys(stats).forEach((tag) => { - expect(stats[tag]).toHaveProperty('completed'); - expect(stats[tag]).toHaveProperty('total'); - expect(stats[tag]).toHaveProperty('percentage'); - expect(typeof stats[tag].completed).toBe('number'); - expect(typeof stats[tag].total).toBe('number'); - expect(typeof stats[tag].percentage).toBe('number'); - expect(stats[tag].percentage).toBeGreaterThanOrEqual(0); - expect(stats[tag].percentage).toBeLessThanOrEqual(100); + expect(tagsFilterCall).toBeDefined(); + expect(tagsFilterCall[0].completionStats).toBeDefined(); + + const stats = tagsFilterCall[0].completionStats; + + // Verify stats structure + Object.keys(stats).forEach((tag) => { + expect(stats[tag]).toHaveProperty('completed'); + expect(stats[tag]).toHaveProperty('total'); + expect(stats[tag]).toHaveProperty('percentage'); + expect(typeof stats[tag].completed).toBe('number'); + expect(typeof stats[tag].total).toBe('number'); + expect(typeof stats[tag].percentage).toBe('number'); + expect(stats[tag].percentage).toBeGreaterThanOrEqual(0); + expect(stats[tag].percentage).toBeLessThanOrEqual(100); + }); }); - }); - test('recalculates completion stats after sync', async () => { - const hooks = require('../hooks'); - - render(); + test('recalculates completion stats after sync', async () => { + const hooks = require('../hooks'); - await waitFor(async () => { - expect(await screen.findByText('Task 1')).toBeInTheDocument(); - }); + render(); - const { MultiSelectFilter } = require('@/components/ui/multi-select'); - - hooks.fetchTaskwarriorTasks.mockResolvedValueOnce([ - { - id: 1, - description: 'Task 1', - status: 'completed', - project: 'ProjectA', - tags: ['tag1'], - uuid: 'uuid-1', - }, - { - id: 2, - description: 'Task 2', - status: 'completed', - project: 'ProjectB', - tags: ['tag2'], - uuid: 'uuid-2', - }, - ]); - - MultiSelectFilter.mockClear(); - - const syncButtons = screen.getAllByText('Sync'); - fireEvent.click(syncButtons[0]); - - await waitFor(() => { - const projectsCall = MultiSelectFilter.mock.calls.find( - (call: any) => call[0].title === 'Projects' - ); - expect(projectsCall).toBeDefined(); - }); + await waitFor(async () => { + expect(await screen.findByText('Task 1')).toBeInTheDocument(); + }); - const updatedProjectsCall = MultiSelectFilter.mock.calls.find( - (call: any) => call[0].title === 'Projects' - ); + const { MultiSelectFilter } = require('@/components/ui/multi-select'); + + hooks.fetchTaskwarriorTasks.mockResolvedValueOnce([ + { + id: 1, + description: 'Task 1', + status: 'completed', + project: 'ProjectA', + tags: ['tag1'], + uuid: 'uuid-1', + }, + { + id: 2, + description: 'Task 2', + status: 'completed', + project: 'ProjectB', + tags: ['tag2'], + uuid: 'uuid-2', + }, + ]); + + MultiSelectFilter.mockClear(); + + const syncButtons = screen.getAllByText('Sync'); + fireEvent.click(syncButtons[0]); - expect(updatedProjectsCall).toBeDefined(); - expect(updatedProjectsCall[0].completionStats).toBeDefined(); + await waitFor(() => { + const projectsCall = MultiSelectFilter.mock.calls.find( + (call: any) => call[0].title === 'Projects' + ); + expect(projectsCall).toBeDefined(); + }); - const updatedStats = updatedProjectsCall[0].completionStats; - expect(updatedStats['ProjectA']).toBeDefined(); - expect(updatedStats['ProjectB']).toBeDefined(); - }); + const updatedProjectsCall = MultiSelectFilter.mock.calls.find( + (call: any) => call[0].title === 'Projects' + ); - test('completion stats structure is correct', async () => { - render(); + expect(updatedProjectsCall).toBeDefined(); + expect(updatedProjectsCall[0].completionStats).toBeDefined(); - await waitFor(async () => { - expect(await screen.findByText('Task 1')).toBeInTheDocument(); + const updatedStats = updatedProjectsCall[0].completionStats; + expect(updatedStats['ProjectA']).toBeDefined(); + expect(updatedStats['ProjectB']).toBeDefined(); }); - const { MultiSelectFilter } = require('@/components/ui/multi-select'); + test('completion stats structure is correct', async () => { + render(); - const projectsCall = MultiSelectFilter.mock.calls.find( - (call: any) => call[0].title === 'Projects' - ); + await waitFor(async () => { + expect(await screen.findByText('Task 1')).toBeInTheDocument(); + }); + + const { MultiSelectFilter } = require('@/components/ui/multi-select'); - expect(projectsCall).toBeDefined(); - const stats = projectsCall[0].completionStats; - - // Verify stats structure for any project that exists - Object.keys(stats).forEach((project) => { - expect(stats[project]).toHaveProperty('completed'); - expect(stats[project]).toHaveProperty('total'); - expect(stats[project]).toHaveProperty('percentage'); - expect(typeof stats[project].completed).toBe('number'); - expect(typeof stats[project].total).toBe('number'); - expect(typeof stats[project].percentage).toBe('number'); - expect(stats[project].completed).toBeLessThanOrEqual( - stats[project].total + const projectsCall = MultiSelectFilter.mock.calls.find( + (call: any) => call[0].title === 'Projects' ); - expect(stats[project].percentage).toBeGreaterThanOrEqual(0); - expect(stats[project].percentage).toBeLessThanOrEqual(100); + + expect(projectsCall).toBeDefined(); + const stats = projectsCall[0].completionStats; + + // Verify stats structure for any project that exists + Object.keys(stats).forEach((project) => { + expect(stats[project]).toHaveProperty('completed'); + expect(stats[project]).toHaveProperty('total'); + expect(stats[project]).toHaveProperty('percentage'); + expect(typeof stats[project].completed).toBe('number'); + expect(typeof stats[project].total).toBe('number'); + expect(typeof stats[project].percentage).toBe('number'); + expect(stats[project].completed).toBeLessThanOrEqual( + stats[project].total + ); + expect(stats[project].percentage).toBeGreaterThanOrEqual(0); + expect(stats[project].percentage).toBeLessThanOrEqual(100); + }); }); }); @@ -1737,4 +1753,178 @@ describe('Tasks Component', () => { expect(task1Row).toBeInTheDocument(); }); }); + + describe('Keyboard Navigation', () => { + describe('Arrow Key Navigation', () => { + test('ArrowDown key moves selection to next task', async () => { + render(); + await screen.findByText('Task 1'); + + fireEvent.keyDown(window, { key: 'ArrowDown' }); + fireEvent.keyDown(window, { key: 'Enter' }); + + const dialog = await screen.findByRole('dialog'); + expect(within(dialog).getByText('Deleted Task 1')).toBeInTheDocument(); + }); + + test('ArrowUp moves selection back to previous task', async () => { + render(); + await screen.findByText('Task 1'); + + fireEvent.keyDown(window, { key: 'ArrowDown' }); + fireEvent.keyDown(window, { key: 'ArrowDown' }); + fireEvent.keyDown(window, { key: 'ArrowUp' }); + fireEvent.keyDown(window, { key: 'Enter' }); + + const dialog = await screen.findByRole('dialog'); + expect(within(dialog).getByText('Deleted Task 1')).toBeInTheDocument(); + }); + + test('ArrowDown stops at last task on page', async () => { + render(); + await screen.findByText('Task 1'); + + for (let i = 0; i < 20; i++) { + fireEvent.keyDown(window, { key: 'ArrowDown' }); + } + + fireEvent.keyDown(window, { key: 'Enter' }); + + const dialog = await screen.findByRole('dialog'); + expect(within(dialog).getByText('Task 9')).toBeInTheDocument(); + }); + + test('ArrowUp stops at index zero task', async () => { + render(); + await screen.findByText('Task 1'); + + for (let i = 0; i < 5; i++) { + fireEvent.keyDown(window, { key: 'ArrowUp' }); + } + + fireEvent.keyDown(window, { key: 'Enter' }); + + const dialog = await screen.findByRole('dialog'); + expect(within(dialog).getByText('tag1')).toBeInTheDocument(); + expect(within(dialog).getByText('Overdue')).toBeInTheDocument(); + }); + }); + + describe('Hotkey Shortcuts', () => { + test('pressing "a" opens the Add Task dialog', async () => { + render(); + await screen.findByText('Task 1'); + + fireEvent.keyDown(window, { key: 'a' }); + + expect( + await screen.findByText( + /fill in the details below to add a new task/i + ) + ).toBeInTheDocument(); + }); + + test.each([ + ['c', 'complete'], + ['d', 'delete'], + ])( + 'pressing %s attempts to open task dialog and trigger %s action', + async (key, _action) => { + jest.useFakeTimers(); + + render(); + await screen.findByText('Task 1'); + + fireEvent.keyDown(window, { key }); + + expect(await screen.findByText('Tags:')).toBeInTheDocument(); + + act(() => { + jest.advanceTimersByTime(200); + }); + + expect(await screen.findByText('Are you')).toBeInTheDocument(); + + jest.useRealTimers(); + } + ); + + test('pressing "Enter" key opens the selected task dialog', async () => { + render(); + await screen.findByText('Task 1'); + + fireEvent.keyDown(window, { key: 'Enter' }); + + expect(await screen.findByText('Description:')).toBeInTheDocument(); + }); + + test('pressing "f" focuses the search input', async () => { + render(); + await screen.findByText('Task 1'); + + fireEvent.keyDown(window, { key: 'f' }); + + const searchInput = screen.getByPlaceholderText('Search tasks...'); + expect(document.activeElement).toBe(searchInput); + }); + + test('pressing "r" triggers sync', async () => { + render(); + await screen.findByText('Task 1'); + + fireEvent.keyDown(window, { key: 'r' }); + + expect(mockProps.setIsLoading).toHaveBeenCalledWith(true); + }); + + test.each([ + ['p', 'projects'], + ['s', 'status'], + ['t', 'tags'], + ])('pressing "%s" opens the %s filter', async (key, filterName) => { + render(); + await screen.findByText('Task 1'); + + const filterButton = screen.getByTestId(`multi-select-${filterName}`); + expect(filterButton).toHaveAttribute('aria-expanded', 'false'); + + fireEvent.keyDown(window, { key }); + + expect(filterButton).toHaveAttribute('aria-expanded', 'true'); + }); + + test('hotkeys are disabled when input is focused', async () => { + render(); + await screen.findByText('Task 1'); + + const searchInput = screen.getByPlaceholderText('Search tasks...'); + searchInput.focus(); + + fireEvent.keyDown(searchInput, { key: 'r' }); + + expect(mockProps.setIsLoading).not.toHaveBeenCalledWith(true); + }); + }); + + describe('Complete Hotkey When Dialog Open', () => { + test.each([ + ['c', 'complete'], + ['d', 'delete'], + ])( + 'pressing "%s" when dialog is already open triggers %s confirmation', + async (key, _action) => { + render(); + await screen.findByText('Task 1'); + + fireEvent.click(screen.getByText('Task 1')); + + expect(await screen.findByRole('dialog')).toBeInTheDocument(); + + fireEvent.keyDown(window, { key }); + + expect(await screen.findByText('Are you')).toBeInTheDocument(); + } + ); + }); + }); });