Skip to content

Commit 8f30c83

Browse files
test(Tasks): Add comprehensive test suite for Tasks component (#434)
- 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 e27eb93 commit 8f30c83

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

@@ -865,10 +870,6 @@ describe('Tasks Component', () => {
865870
const MultiSelectFilter =
866871
require('@/components/ui/multi-select').MultiSelectFilter;
867872

868-
MultiSelectFilter.mockImplementation(({ title }: { title: string }) => {
869-
return <div data-testid={`ms-${title}`}>Mocked MultiSelect: {title}</div>;
870-
});
871-
872873
render(<Tasks {...mockProps} />);
873874

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

0 commit comments

Comments
 (0)