Skip to content

Commit cf49bdf

Browse files
committed
test(Tasks): Add comprehensive test suite for Tasks component
- Add tests for Keyboard Navigation (ArrowUp, ArrowDown, boundary stops) - Add tests for Hotkey Shortcuts ('a', 'c', 'd', 'f', 'p', 'r', 's', 't', Enter) - Add tests for complete/delete hotkeys when dialog is already open - Add test for hotkeys disabled when input is focused - Update MultiSelectFilter mock to use aria-expanded for behavior testing - Remove unnecessary mock override in overdue filter test
1 parent 5b33b58 commit cf49bdf

1 file changed

Lines changed: 200 additions & 6 deletions

File tree

frontend/src/components/HomeComponents/Tasks/__tests__/Tasks.test.tsx

Lines changed: 200 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,19 @@ jest.mock('../tasks-utils', () => {
4545

4646
jest.mock('@/components/ui/multi-select', () => ({
4747
MultiSelectFilter: jest.fn(({ title, completionStats }) => (
48-
<div data-testid={`multi-select-${title.toLowerCase()}`}>
48+
<button
49+
id={title.toLowerCase()}
50+
data-testid={`multi-select-${title.toLowerCase()}`}
51+
aria-expanded="false"
52+
onClick={(e) => e.currentTarget.setAttribute('aria-expanded', 'true')}
53+
>
4954
Mocked MultiSelect: {title}
5055
{completionStats && (
5156
<span data-testid={`stats-${title.toLowerCase()}`}>
5257
{JSON.stringify(completionStats)}
5358
</span>
5459
)}
55-
</div>
60+
</button>
5661
)),
5762
}));
5863

@@ -863,10 +868,6 @@ describe('Tasks Component', () => {
863868
const MultiSelectFilter =
864869
require('@/components/ui/multi-select').MultiSelectFilter;
865870

866-
MultiSelectFilter.mockImplementation(({ title }: { title: string }) => {
867-
return <div data-testid={`ms-${title}`}>Mocked MultiSelect: {title}</div>;
868-
});
869-
870871
render(<Tasks {...mockProps} />);
871872

872873
await waitFor(async () => {
@@ -1737,4 +1738,197 @@ describe('Tasks Component', () => {
17371738
expect(task1Row).toBeInTheDocument();
17381739
});
17391740
});
1741+
1742+
describe('Keyboard Navigation', () => {
1743+
describe('Arrow Key Navigation', () => {
1744+
test('ArrowDown key moves selection to next task', async () => {
1745+
render(<Tasks {...mockProps} />);
1746+
await screen.findByText('Task 1');
1747+
const taskRows = screen.getAllByTestId(/task-row-/);
1748+
1749+
expect(taskRows[0]).toHaveAttribute('data-selected', 'true');
1750+
expect(taskRows[1]).toHaveAttribute('data-selected', 'false');
1751+
1752+
fireEvent.keyDown(window, { key: 'ArrowDown' });
1753+
1754+
expect(taskRows[0]).toHaveAttribute('data-selected', 'false');
1755+
expect(taskRows[1]).toHaveAttribute('data-selected', 'true');
1756+
});
1757+
1758+
test('ArrowUp moves selection back to previous task', async () => {
1759+
render(<Tasks {...mockProps} />);
1760+
await screen.findByText('Task 1');
1761+
const taskRows = screen.getAllByTestId(/task-row-/);
1762+
1763+
fireEvent.keyDown(window, { key: 'ArrowDown' });
1764+
fireEvent.keyDown(window, { key: 'ArrowDown' });
1765+
1766+
expect(taskRows[1]).toHaveAttribute('data-selected', 'false');
1767+
expect(taskRows[2]).toHaveAttribute('data-selected', 'true');
1768+
1769+
fireEvent.keyDown(window, { key: 'ArrowUp' });
1770+
1771+
expect(taskRows[1]).toHaveAttribute('data-selected', 'true');
1772+
expect(taskRows[2]).toHaveAttribute('data-selected', 'false');
1773+
});
1774+
1775+
test('ArrowDown stops at last task on page', async () => {
1776+
render(<Tasks {...mockProps} />);
1777+
await screen.findByText('Task 1');
1778+
1779+
const taskRows = screen.getAllByTestId(/task-row-/);
1780+
1781+
for (let i = 0; i < taskRows.length + 2; i++) {
1782+
fireEvent.keyDown(window, { key: 'ArrowDown' });
1783+
}
1784+
1785+
expect(taskRows[taskRows.length - 1]).toHaveAttribute(
1786+
'data-selected',
1787+
'true'
1788+
);
1789+
});
1790+
1791+
test('ArrowUp stops at first task', async () => {
1792+
render(<Tasks {...mockProps} />);
1793+
await screen.findByText('Task 1');
1794+
const taskRows = screen.getAllByTestId(/task-row-/);
1795+
const middleIndex = Math.floor(taskRows.length / 2);
1796+
1797+
for (let i = 0; i < middleIndex; i++) {
1798+
fireEvent.keyDown(window, { key: 'ArrowDown' });
1799+
}
1800+
for (let i = 0; i < middleIndex + 5; i++) {
1801+
fireEvent.keyDown(window, { key: 'ArrowUp' });
1802+
}
1803+
1804+
expect(taskRows[0]).toHaveAttribute('data-selected', 'true');
1805+
});
1806+
});
1807+
1808+
describe('Hotkey Shortcuts', () => {
1809+
test('pressing "a" opens the Add Task dialog', async () => {
1810+
render(<Tasks {...mockProps} />);
1811+
await screen.findByText('Task 1');
1812+
1813+
fireEvent.keyDown(window, { key: 'a' });
1814+
1815+
const dialog = await screen.findByRole('dialog');
1816+
expect(within(dialog).getByText(/add a new task/i)).toBeInTheDocument();
1817+
});
1818+
1819+
test.each([
1820+
['c', 'complete', 'markTaskAsCompleted'],
1821+
['d', 'delete', 'markTaskAsDeleted'],
1822+
])(
1823+
'pressing %s attempts to open task dialog and trigger %s action',
1824+
async (key, _action, fn) => {
1825+
render(<Tasks {...mockProps} />);
1826+
await screen.findByText('Task 1');
1827+
1828+
fireEvent.keyDown(window, { key });
1829+
1830+
const yesButton = await screen.findByRole('button', {
1831+
name: /^yes$/i,
1832+
});
1833+
fireEvent.click(yesButton);
1834+
1835+
expect(jest.requireMock('../tasks-utils')[fn]).toHaveBeenCalled();
1836+
}
1837+
);
1838+
1839+
test('pressing "Enter" key opens the selected task dialog', async () => {
1840+
render(<Tasks {...mockProps} />);
1841+
await screen.findByText('Task 1');
1842+
1843+
const taskRows = screen.getAllByTestId(/task-row-/);
1844+
const selectedRow = taskRows.find(
1845+
(row) => row.getAttribute('data-selected') === 'true'
1846+
);
1847+
const selectedTaskId = selectedRow
1848+
?.getAttribute('data-testid')
1849+
?.replace('task-row-', '');
1850+
1851+
fireEvent.keyDown(window, { key: 'Enter' });
1852+
1853+
const dialog = await screen.findByRole('dialog');
1854+
const idCell = within(dialog).getByText('ID:').closest('tr');
1855+
expect(within(idCell!).getByText(selectedTaskId!)).toBeInTheDocument();
1856+
});
1857+
1858+
test('pressing "f" focuses the search input', async () => {
1859+
render(<Tasks {...mockProps} />);
1860+
await screen.findByText('Task 1');
1861+
1862+
fireEvent.keyDown(window, { key: 'f' });
1863+
1864+
const searchInput = screen.getByPlaceholderText('Search tasks...');
1865+
expect(document.activeElement).toBe(searchInput);
1866+
});
1867+
1868+
test('pressing "r" triggers sync', async () => {
1869+
render(<Tasks {...mockProps} />);
1870+
await screen.findByText('Task 1');
1871+
1872+
fireEvent.keyDown(window, { key: 'r' });
1873+
1874+
expect(mockProps.setIsLoading).toHaveBeenCalledWith(true);
1875+
expect(
1876+
jest.requireMock('../hooks').fetchTaskwarriorTasks
1877+
).toHaveBeenCalled();
1878+
});
1879+
1880+
test.each([
1881+
['p', 'projects'],
1882+
['s', 'status'],
1883+
['t', 'tags'],
1884+
])('pressing "%s" opens the %s filter', async (key, filterName) => {
1885+
render(<Tasks {...mockProps} />);
1886+
await screen.findByText('Task 1');
1887+
1888+
const filterButton = screen.getByTestId(`multi-select-${filterName}`);
1889+
expect(filterButton).toHaveAttribute('aria-expanded', 'false');
1890+
1891+
fireEvent.keyDown(window, { key });
1892+
1893+
expect(filterButton).toHaveAttribute('aria-expanded', 'true');
1894+
});
1895+
1896+
test('hotkeys are disabled when input is focused', async () => {
1897+
render(<Tasks {...mockProps} />);
1898+
await screen.findByText('Task 1');
1899+
1900+
const searchInput = screen.getByPlaceholderText('Search tasks...');
1901+
searchInput.focus();
1902+
1903+
fireEvent.keyDown(searchInput, { key: 'r' });
1904+
1905+
expect(mockProps.setIsLoading).not.toHaveBeenCalledWith(true);
1906+
});
1907+
});
1908+
1909+
describe('Complete/Delete Hotkeys When Dialog Open', () => {
1910+
test.each([
1911+
['c', 'complete', 'markTaskAsCompleted'],
1912+
['d', 'delete', 'markTaskAsDeleted'],
1913+
])(
1914+
'pressing "%s" with dialog open triggers %s action on confirmation',
1915+
async (key, _action, fn) => {
1916+
render(<Tasks {...mockProps} />);
1917+
await screen.findByText('Task 1');
1918+
1919+
fireEvent.click(screen.getByText('Task 1'));
1920+
await screen.findByRole('dialog');
1921+
1922+
fireEvent.keyDown(window, { key });
1923+
1924+
const yesButton = await screen.findByRole('button', {
1925+
name: /^yes$/i,
1926+
});
1927+
fireEvent.click(yesButton);
1928+
1929+
expect(jest.requireMock('../tasks-utils')[fn]).toHaveBeenCalled();
1930+
}
1931+
);
1932+
});
1933+
});
17401934
});

0 commit comments

Comments
 (0)