Skip to content

Commit 5201a4b

Browse files
committed
Add useSearchResults tests
1 parent 57c0536 commit 5201a4b

1 file changed

Lines changed: 145 additions & 0 deletions

File tree

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import {act, renderHook} from '@testing-library/react-native';
2+
import useSearchResults from '@hooks/useSearchResults';
3+
import CONST from '@src/CONST';
4+
5+
type Item = {id: number; name: string};
6+
7+
const buildItems = (count: number, prefix = 'item'): Item[] => Array.from({length: count}, (_, i) => ({id: i, name: `${prefix}-${i}`}));
8+
9+
const filterByName = (item: Item, query: string) => item.name.toLowerCase().includes(query);
10+
11+
describe('useSearchResults', () => {
12+
beforeEach(() => {
13+
jest.useFakeTimers();
14+
});
15+
16+
afterEach(() => {
17+
jest.clearAllTimers();
18+
jest.useRealTimers();
19+
});
20+
21+
it('returns the full list when the input is empty', () => {
22+
const data = buildItems(5);
23+
const {result} = renderHook(() => useSearchResults(data, filterByName));
24+
25+
expect(result.current[0]).toBe('');
26+
expect(result.current[2]).toEqual(data);
27+
});
28+
29+
it('updates the input value immediately but defers filtering until the debounce settles', () => {
30+
const data = buildItems(5);
31+
const {result} = renderHook(() => useSearchResults(data, filterByName));
32+
33+
act(() => {
34+
result.current[1]('item-2');
35+
});
36+
37+
// The typed value is visible right away but the filtered result still reflects the full list.
38+
expect(result.current[0]).toBe('item-2');
39+
expect(result.current[2]).toEqual(data);
40+
41+
act(() => {
42+
jest.advanceTimersByTime(CONST.TIMING.SEARCH_OPTION_LIST_DEBOUNCE_TIME);
43+
});
44+
45+
expect(result.current[2]).toEqual([{id: 2, name: 'item-2'}]);
46+
});
47+
48+
it('restores the full list instantly when the input is cleared, without waiting for the debounce', () => {
49+
const data = buildItems(5);
50+
const {result} = renderHook(() => useSearchResults(data, filterByName));
51+
52+
act(() => {
53+
result.current[1]('item-2');
54+
jest.advanceTimersByTime(CONST.TIMING.SEARCH_OPTION_LIST_DEBOUNCE_TIME);
55+
});
56+
expect(result.current[2]).toHaveLength(1);
57+
58+
act(() => {
59+
result.current[1]('');
60+
});
61+
expect(result.current[2]).toEqual(data);
62+
});
63+
64+
it('applies the sort comparator to the filtered results', () => {
65+
const data: Item[] = [
66+
{id: 1, name: 'banana'},
67+
{id: 2, name: 'apple'},
68+
{id: 3, name: 'cherry'},
69+
];
70+
const sortAlpha = (items: Item[]) => [...items].sort((a, b) => a.name.localeCompare(b.name));
71+
const {result} = renderHook(() => useSearchResults(data, filterByName, sortAlpha));
72+
73+
expect(result.current[2].map((i) => i.name)).toEqual(['apple', 'banana', 'cherry']);
74+
});
75+
76+
it('applies the preFilter before the text filter', () => {
77+
const data: Item[] = [
78+
{id: 1, name: 'apple-good'},
79+
{id: 2, name: 'apple-bad'},
80+
{id: 3, name: 'banana-good'},
81+
];
82+
const preFilter = (item: Item) => item.name.endsWith('-good');
83+
const {result} = renderHook(() => useSearchResults(data, filterByName, undefined, preFilter));
84+
85+
expect(result.current[2].map((i) => i.id)).toEqual([1, 3]);
86+
87+
act(() => {
88+
result.current[1]('apple');
89+
jest.advanceTimersByTime(CONST.TIMING.SEARCH_OPTION_LIST_DEBOUNCE_TIME);
90+
});
91+
expect(result.current[2].map((i) => i.id)).toEqual([1]);
92+
});
93+
94+
describe('auto-clear when data crosses SEARCH_ITEM_LIMIT', () => {
95+
const aboveLimit = CONST.SEARCH_ITEM_LIMIT + 5;
96+
const belowLimit = CONST.SEARCH_ITEM_LIMIT - 5;
97+
98+
it('clears the input when the dataset shrinks from above the limit to at or below the limit', () => {
99+
const {result, rerender} = renderHook(({data}) => useSearchResults(data, filterByName), {
100+
initialProps: {data: buildItems(aboveLimit)},
101+
});
102+
103+
act(() => {
104+
result.current[1]('item');
105+
jest.advanceTimersByTime(CONST.TIMING.SEARCH_OPTION_LIST_DEBOUNCE_TIME);
106+
});
107+
expect(result.current[0]).toBe('item');
108+
109+
rerender({data: buildItems(belowLimit)});
110+
111+
// The search bar is no longer shown for small datasets, so any leftover query must be wiped.
112+
expect(result.current[0]).toBe('');
113+
});
114+
115+
it('does not clear the input when the dataset stays above the limit', () => {
116+
const {result, rerender} = renderHook(({data}) => useSearchResults(data, filterByName), {
117+
initialProps: {data: buildItems(aboveLimit)},
118+
});
119+
120+
act(() => {
121+
result.current[1]('item');
122+
jest.advanceTimersByTime(CONST.TIMING.SEARCH_OPTION_LIST_DEBOUNCE_TIME);
123+
});
124+
125+
rerender({data: buildItems(aboveLimit + 1)});
126+
127+
expect(result.current[0]).toBe('item');
128+
});
129+
130+
it('does not clear the input when the dataset stays at or below the limit', () => {
131+
const {result, rerender} = renderHook(({data}) => useSearchResults(data, filterByName), {
132+
initialProps: {data: buildItems(belowLimit)},
133+
});
134+
135+
act(() => {
136+
result.current[1]('item');
137+
jest.advanceTimersByTime(CONST.TIMING.SEARCH_OPTION_LIST_DEBOUNCE_TIME);
138+
});
139+
140+
rerender({data: buildItems(belowLimit - 1)});
141+
142+
expect(result.current[0]).toBe('item');
143+
});
144+
});
145+
});

0 commit comments

Comments
 (0)