Skip to content

Commit 339f115

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 339f115

1 file changed

Lines changed: 182 additions & 7 deletions

File tree

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

Lines changed: 182 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,20 @@ jest.mock('../tasks-utils', () => {
4444
});
4545

4646
jest.mock('@/components/ui/multi-select', () => ({
47-
MultiSelectFilter: jest.fn(({ title, completionStats }) => (
48-
<div data-testid={`multi-select-${title.toLowerCase()}`}>
47+
MultiSelectFilter: jest.fn(({ id, title, completionStats }) => (
48+
<button
49+
id={id}
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,178 @@ 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+
1748+
fireEvent.keyDown(window, { key: 'ArrowDown' });
1749+
fireEvent.keyDown(window, { key: 'Enter' });
1750+
1751+
const dialog = await screen.findByRole('dialog');
1752+
expect(within(dialog).getByText('Deleted Task 1')).toBeInTheDocument();
1753+
});
1754+
1755+
test('ArrowUp moves selection back to previous task', async () => {
1756+
render(<Tasks {...mockProps} />);
1757+
await screen.findByText('Task 1');
1758+
1759+
fireEvent.keyDown(window, { key: 'ArrowDown' });
1760+
fireEvent.keyDown(window, { key: 'ArrowDown' });
1761+
fireEvent.keyDown(window, { key: 'ArrowUp' });
1762+
fireEvent.keyDown(window, { key: 'Enter' });
1763+
1764+
const dialog = await screen.findByRole('dialog');
1765+
expect(within(dialog).getByText('Deleted Task 1')).toBeInTheDocument();
1766+
});
1767+
1768+
test('ArrowDown stops at last task on page', async () => {
1769+
render(<Tasks {...mockProps} />);
1770+
await screen.findByText('Task 1');
1771+
1772+
for (let i = 0; i < 20; i++) {
1773+
fireEvent.keyDown(window, { key: 'ArrowDown' });
1774+
}
1775+
1776+
fireEvent.keyDown(window, { key: 'Enter' });
1777+
1778+
const dialog = await screen.findByRole('dialog');
1779+
expect(within(dialog).getByText('Task 9')).toBeInTheDocument();
1780+
});
1781+
1782+
test('ArrowUp stops at index zero task', async () => {
1783+
render(<Tasks {...mockProps} />);
1784+
await screen.findByText('Task 1');
1785+
1786+
for (let i = 0; i < 5; i++) {
1787+
fireEvent.keyDown(window, { key: 'ArrowUp' });
1788+
}
1789+
1790+
fireEvent.keyDown(window, { key: 'Enter' });
1791+
1792+
const dialog = await screen.findByRole('dialog');
1793+
expect(within(dialog).getByText('tag1')).toBeInTheDocument();
1794+
expect(within(dialog).getByText('Overdue')).toBeInTheDocument();
1795+
});
1796+
});
1797+
1798+
describe('Hotkey Shortcuts', () => {
1799+
test('pressing "a" opens the Add Task dialog', async () => {
1800+
render(<Tasks {...mockProps} />);
1801+
await screen.findByText('Task 1');
1802+
1803+
fireEvent.keyDown(window, { key: 'a' });
1804+
1805+
expect(
1806+
await screen.findByText(
1807+
/fill in the details below to add a new task/i
1808+
)
1809+
).toBeInTheDocument();
1810+
});
1811+
1812+
test.each([
1813+
['c', 'complete'],
1814+
['d', 'delete'],
1815+
])(
1816+
'pressing %s attempts to open task dialog and trigger %s action',
1817+
async (key, _action) => {
1818+
jest.useFakeTimers();
1819+
1820+
render(<Tasks {...mockProps} />);
1821+
await screen.findByText('Task 1');
1822+
1823+
fireEvent.keyDown(window, { key });
1824+
1825+
expect(await screen.findByText('Tags:')).toBeInTheDocument();
1826+
1827+
act(() => {
1828+
jest.advanceTimersByTime(200);
1829+
});
1830+
1831+
expect(await screen.findByText('Are you')).toBeInTheDocument();
1832+
1833+
jest.useRealTimers();
1834+
}
1835+
);
1836+
1837+
test('pressing "Enter" key opens the selected task dialog', async () => {
1838+
render(<Tasks {...mockProps} />);
1839+
await screen.findByText('Task 1');
1840+
1841+
fireEvent.keyDown(window, { key: 'Enter' });
1842+
1843+
expect(await screen.findByText('Description:')).toBeInTheDocument();
1844+
});
1845+
1846+
test('pressing "f" focuses the search input', async () => {
1847+
render(<Tasks {...mockProps} />);
1848+
await screen.findByText('Task 1');
1849+
1850+
fireEvent.keyDown(window, { key: 'f' });
1851+
1852+
const searchInput = screen.getByPlaceholderText('Search tasks...');
1853+
expect(document.activeElement).toBe(searchInput);
1854+
});
1855+
1856+
test('pressing "r" triggers sync', async () => {
1857+
render(<Tasks {...mockProps} />);
1858+
await screen.findByText('Task 1');
1859+
1860+
fireEvent.keyDown(window, { key: 'r' });
1861+
1862+
expect(mockProps.setIsLoading).toHaveBeenCalledWith(true);
1863+
});
1864+
1865+
test.each([
1866+
['p', 'projects'],
1867+
['s', 'status'],
1868+
['t', 'tags'],
1869+
])('pressing "%s" opens the %s filter', async (key, filterName) => {
1870+
render(<Tasks {...mockProps} />);
1871+
await screen.findByText('Task 1');
1872+
1873+
const filterButton = screen.getByTestId(`multi-select-${filterName}`);
1874+
expect(filterButton).toHaveAttribute('aria-expanded', 'false');
1875+
1876+
fireEvent.keyDown(window, { key });
1877+
1878+
expect(filterButton).toHaveAttribute('aria-expanded', 'true');
1879+
});
1880+
1881+
test('hotkeys are disabled when input is focused', async () => {
1882+
render(<Tasks {...mockProps} />);
1883+
await screen.findByText('Task 1');
1884+
1885+
const searchInput = screen.getByPlaceholderText('Search tasks...');
1886+
searchInput.focus();
1887+
1888+
fireEvent.keyDown(searchInput, { key: 'r' });
1889+
1890+
expect(mockProps.setIsLoading).not.toHaveBeenCalledWith(true);
1891+
});
1892+
});
1893+
1894+
describe('Complete Hotkey When Dialog Open', () => {
1895+
test.each([
1896+
['c', 'complete'],
1897+
['d', 'delete'],
1898+
])(
1899+
'pressing "%s" when dialog is already open triggers %s confirmation',
1900+
async (key, _action) => {
1901+
render(<Tasks {...mockProps} />);
1902+
await screen.findByText('Task 1');
1903+
1904+
fireEvent.click(screen.getByText('Task 1'));
1905+
1906+
expect(await screen.findByRole('dialog')).toBeInTheDocument();
1907+
1908+
fireEvent.keyDown(window, { key });
1909+
1910+
expect(await screen.findByText('Are you')).toBeInTheDocument();
1911+
}
1912+
);
1913+
});
1914+
});
17401915
});

0 commit comments

Comments
 (0)