diff --git a/frontend/src/components/HomeComponents/Tasks/__tests__/Tasks.test.tsx b/frontend/src/components/HomeComponents/Tasks/__tests__/Tasks.test.tsx
index d650a64b..acc4bab2 100644
--- a/frontend/src/components/HomeComponents/Tasks/__tests__/Tasks.test.tsx
+++ b/frontend/src/components/HomeComponents/Tasks/__tests__/Tasks.test.tsx
@@ -45,14 +45,19 @@ jest.mock('../tasks-utils', () => {
jest.mock('@/components/ui/multi-select', () => ({
MultiSelectFilter: jest.fn(({ title, completionStats }) => (
-
+
+
)),
}));
@@ -863,10 +868,6 @@ describe('Tasks Component', () => {
const MultiSelectFilter =
require('@/components/ui/multi-select').MultiSelectFilter;
- MultiSelectFilter.mockImplementation(({ title }: { title: string }) => {
- return Mocked MultiSelect: {title}
;
- });
-
render();
await waitFor(async () => {
@@ -1737,4 +1738,197 @@ 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');
+ const taskRows = screen.getAllByTestId(/task-row-/);
+
+ expect(taskRows[0]).toHaveAttribute('data-selected', 'true');
+ expect(taskRows[1]).toHaveAttribute('data-selected', 'false');
+
+ fireEvent.keyDown(window, { key: 'ArrowDown' });
+
+ expect(taskRows[0]).toHaveAttribute('data-selected', 'false');
+ expect(taskRows[1]).toHaveAttribute('data-selected', 'true');
+ });
+
+ test('ArrowUp moves selection back to previous task', async () => {
+ render();
+ await screen.findByText('Task 1');
+ const taskRows = screen.getAllByTestId(/task-row-/);
+
+ fireEvent.keyDown(window, { key: 'ArrowDown' });
+ fireEvent.keyDown(window, { key: 'ArrowDown' });
+
+ expect(taskRows[1]).toHaveAttribute('data-selected', 'false');
+ expect(taskRows[2]).toHaveAttribute('data-selected', 'true');
+
+ fireEvent.keyDown(window, { key: 'ArrowUp' });
+
+ expect(taskRows[1]).toHaveAttribute('data-selected', 'true');
+ expect(taskRows[2]).toHaveAttribute('data-selected', 'false');
+ });
+
+ test('ArrowDown stops at last task on page', async () => {
+ render();
+ await screen.findByText('Task 1');
+
+ const taskRows = screen.getAllByTestId(/task-row-/);
+
+ for (let i = 0; i < taskRows.length + 2; i++) {
+ fireEvent.keyDown(window, { key: 'ArrowDown' });
+ }
+
+ expect(taskRows[taskRows.length - 1]).toHaveAttribute(
+ 'data-selected',
+ 'true'
+ );
+ });
+
+ test('ArrowUp stops at first task', async () => {
+ render();
+ await screen.findByText('Task 1');
+ const taskRows = screen.getAllByTestId(/task-row-/);
+ const middleIndex = Math.floor(taskRows.length / 2);
+
+ for (let i = 0; i < middleIndex; i++) {
+ fireEvent.keyDown(window, { key: 'ArrowDown' });
+ }
+ for (let i = 0; i < middleIndex + 5; i++) {
+ fireEvent.keyDown(window, { key: 'ArrowUp' });
+ }
+
+ expect(taskRows[0]).toHaveAttribute('data-selected', 'true');
+ });
+ });
+
+ describe('Hotkey Shortcuts', () => {
+ test('pressing "a" opens the Add Task dialog', async () => {
+ render();
+ await screen.findByText('Task 1');
+
+ fireEvent.keyDown(window, { key: 'a' });
+
+ const dialog = await screen.findByRole('dialog');
+ expect(within(dialog).getByText(/add a new task/i)).toBeInTheDocument();
+ });
+
+ test.each([
+ ['c', 'complete', 'markTaskAsCompleted'],
+ ['d', 'delete', 'markTaskAsDeleted'],
+ ])(
+ 'pressing %s attempts to open task dialog and trigger %s action',
+ async (key, _action, fn) => {
+ render();
+ await screen.findByText('Task 1');
+
+ fireEvent.keyDown(window, { key });
+
+ const yesButton = await screen.findByRole('button', {
+ name: /^yes$/i,
+ });
+ fireEvent.click(yesButton);
+
+ expect(jest.requireMock('../tasks-utils')[fn]).toHaveBeenCalled();
+ }
+ );
+
+ test('pressing "Enter" key opens the selected task dialog', async () => {
+ render();
+ await screen.findByText('Task 1');
+
+ const taskRows = screen.getAllByTestId(/task-row-/);
+ const selectedRow = taskRows.find(
+ (row) => row.getAttribute('data-selected') === 'true'
+ );
+ const selectedTaskId = selectedRow
+ ?.getAttribute('data-testid')
+ ?.replace('task-row-', '');
+
+ fireEvent.keyDown(window, { key: 'Enter' });
+
+ const dialog = await screen.findByRole('dialog');
+ const idCell = within(dialog).getByText('ID:').closest('tr');
+ expect(within(idCell!).getByText(selectedTaskId!)).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);
+ expect(
+ jest.requireMock('../hooks').fetchTaskwarriorTasks
+ ).toHaveBeenCalled();
+ });
+
+ 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/Delete Hotkeys When Dialog Open', () => {
+ test.each([
+ ['c', 'complete', 'markTaskAsCompleted'],
+ ['d', 'delete', 'markTaskAsDeleted'],
+ ])(
+ 'pressing "%s" with dialog open triggers %s action on confirmation',
+ async (key, _action, fn) => {
+ render();
+ await screen.findByText('Task 1');
+
+ fireEvent.click(screen.getByText('Task 1'));
+ await screen.findByRole('dialog');
+
+ fireEvent.keyDown(window, { key });
+
+ const yesButton = await screen.findByRole('button', {
+ name: /^yes$/i,
+ });
+ fireEvent.click(yesButton);
+
+ expect(jest.requireMock('../tasks-utils')[fn]).toHaveBeenCalled();
+ }
+ );
+ });
+ });
});