Skip to content

Commit e969b4f

Browse files
Merge pull request #964 from Nimatstar/fix/issues-901-900-897-896
Add tests for favoritesStore, certificateStore, walletStore and useDebouncedSearch
2 parents 89bd311 + bae120a commit e969b4f

4 files changed

Lines changed: 569 additions & 0 deletions

File tree

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import { renderHook, act } from '@testing-library/react';
2+
import { useDebouncedSearch } from '../useDebouncedSearch';
3+
4+
describe('useDebouncedSearch', () => {
5+
beforeEach(() => {
6+
jest.useFakeTimers();
7+
});
8+
9+
afterEach(() => {
10+
jest.useRealTimers();
11+
});
12+
13+
it('starts with an empty query and no results', () => {
14+
const searchFn = jest.fn().mockResolvedValue(['a']);
15+
const { result } = renderHook(() =>
16+
useDebouncedSearch({ searchFn, delay: 300 }),
17+
);
18+
19+
expect(result.current.query).toBe('');
20+
expect(result.current.results).toBeUndefined();
21+
expect(result.current.isLoading).toBe(false);
22+
expect(result.current.error).toBeNull();
23+
});
24+
25+
it('does not fire the search until the debounce window elapses', () => {
26+
const searchFn = jest.fn().mockResolvedValue(['a']);
27+
const { result } = renderHook(() =>
28+
useDebouncedSearch({ searchFn, delay: 300 }),
29+
);
30+
31+
act(() => {
32+
result.current.setQuery('hello');
33+
});
34+
35+
expect(searchFn).not.toHaveBeenCalled();
36+
37+
act(() => {
38+
jest.advanceTimersByTime(300);
39+
});
40+
41+
expect(searchFn).toHaveBeenCalledTimes(1);
42+
expect(searchFn).toHaveBeenCalledWith('hello', expect.any(AbortSignal));
43+
});
44+
45+
it('collapses rapid input into a single search with the latest query', () => {
46+
const searchFn = jest.fn().mockResolvedValue(['a']);
47+
const { result } = renderHook(() =>
48+
useDebouncedSearch({ searchFn, delay: 300 }),
49+
);
50+
51+
act(() => {
52+
result.current.setQuery('h');
53+
jest.advanceTimersByTime(100);
54+
result.current.setQuery('he');
55+
jest.advanceTimersByTime(100);
56+
result.current.setQuery('hel');
57+
jest.advanceTimersByTime(100);
58+
result.current.setQuery('hello');
59+
jest.advanceTimersByTime(300);
60+
});
61+
62+
expect(searchFn).toHaveBeenCalledTimes(1);
63+
expect(searchFn).toHaveBeenCalledWith('hello', expect.any(AbortSignal));
64+
});
65+
66+
it('sets results once the async search resolves', async () => {
67+
const searchFn = jest.fn().mockResolvedValue(['result-1']);
68+
const { result } = renderHook(() =>
69+
useDebouncedSearch({ searchFn, delay: 300 }),
70+
);
71+
72+
act(() => {
73+
result.current.setQuery('hello');
74+
});
75+
76+
await act(async () => {
77+
jest.advanceTimersByTime(300);
78+
await Promise.resolve();
79+
await Promise.resolve();
80+
});
81+
82+
expect(result.current.results).toEqual(['result-1']);
83+
expect(result.current.isLoading).toBe(false);
84+
});
85+
86+
it('surfaces errors from the search function', async () => {
87+
const searchFn = jest.fn().mockRejectedValue(new Error('Network error'));
88+
const { result } = renderHook(() =>
89+
useDebouncedSearch({ searchFn, delay: 300 }),
90+
);
91+
92+
act(() => {
93+
result.current.setQuery('hello');
94+
});
95+
96+
await act(async () => {
97+
jest.advanceTimersByTime(300);
98+
await Promise.resolve();
99+
await Promise.resolve();
100+
});
101+
102+
expect(result.current.error).toEqual(new Error('Network error'));
103+
expect(result.current.isLoading).toBe(false);
104+
});
105+
106+
it('respects minLength and does not search short queries', () => {
107+
const searchFn = jest.fn().mockResolvedValue(['a']);
108+
const { result } = renderHook(() =>
109+
useDebouncedSearch({ searchFn, delay: 300, minLength: 3, initialResults: ['init'] }),
110+
);
111+
112+
act(() => {
113+
result.current.setQuery('hi');
114+
jest.advanceTimersByTime(300);
115+
});
116+
117+
expect(searchFn).not.toHaveBeenCalled();
118+
expect(result.current.results).toEqual(['init']);
119+
expect(result.current.isLoading).toBe(false);
120+
});
121+
122+
it('clear resets query, results and error', () => {
123+
const searchFn = jest.fn().mockResolvedValue(['a']);
124+
const { result } = renderHook(() =>
125+
useDebouncedSearch({ searchFn, delay: 300, initialResults: ['init'] }),
126+
);
127+
128+
act(() => {
129+
result.current.setQuery('hello');
130+
result.current.clear();
131+
});
132+
133+
expect(result.current.query).toBe('');
134+
expect(result.current.results).toEqual(['init']);
135+
expect(result.current.error).toBeNull();
136+
});
137+
138+
it('cleans up pending timers on unmount', () => {
139+
const searchFn = jest.fn().mockResolvedValue(['a']);
140+
const { result, unmount } = renderHook(() =>
141+
useDebouncedSearch({ searchFn, delay: 300 }),
142+
);
143+
144+
act(() => {
145+
result.current.setQuery('hello');
146+
});
147+
148+
unmount();
149+
150+
act(() => {
151+
jest.advanceTimersByTime(300);
152+
});
153+
154+
// The pending search must not fire after unmount.
155+
expect(searchFn).not.toHaveBeenCalled();
156+
});
157+
});
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { act, renderHook } from '@testing-library/react';
2+
import { useCertificateStore } from '../certificateStore';
3+
import type { NFTCertificate } from '@/types/certificate';
4+
5+
const mockCertificate = (overrides: Partial<NFTCertificate> = {}): NFTCertificate => ({
6+
id: 'cert-1',
7+
propertyId: 'prop-1',
8+
propertyName: 'Test Property',
9+
propertyAddress: '1 Test St',
10+
propertyImage: null,
11+
tokenAmount: 10,
12+
tokenSymbol: 'TST',
13+
walletAddress: '0xAbCd...W1',
14+
purchaseDate: '2024-01-15T10:00:00Z',
15+
transactionHash: '0xtxhash123',
16+
network: 'ethereum',
17+
contractAddress: '0x1234...5678',
18+
ownershipPercentage: 1,
19+
...overrides,
20+
});
21+
22+
describe('certificateStore', () => {
23+
beforeEach(() => {
24+
localStorage.clear();
25+
useCertificateStore.setState({ certificates: [] });
26+
});
27+
28+
it('starts with no certificates', () => {
29+
const { result } = renderHook(() => useCertificateStore());
30+
expect(result.current.certificates).toEqual([]);
31+
expect(
32+
result.current.getCertificate('prop-1', '0xAbCd...W1'),
33+
).toBeUndefined();
34+
});
35+
36+
it('adds a certificate', () => {
37+
const { result } = renderHook(() => useCertificateStore());
38+
39+
act(() => {
40+
result.current.addCertificate(mockCertificate());
41+
});
42+
43+
expect(result.current.certificates).toHaveLength(1);
44+
expect(result.current.certificates[0].id).toBe('cert-1');
45+
});
46+
47+
it('replaces a certificate for the same property and wallet', () => {
48+
const { result } = renderHook(() => useCertificateStore());
49+
50+
act(() => {
51+
result.current.addCertificate(mockCertificate({ id: 'cert-1', tokenAmount: 10 }));
52+
result.current.addCertificate(mockCertificate({ id: 'cert-2', tokenAmount: 25 }));
53+
});
54+
55+
expect(result.current.certificates).toHaveLength(1);
56+
expect(result.current.certificates[0].id).toBe('cert-2');
57+
expect(result.current.certificates[0].tokenAmount).toBe(25);
58+
});
59+
60+
it('keeps separate certificates for different wallets', () => {
61+
const { result } = renderHook(() => useCertificateStore());
62+
63+
act(() => {
64+
result.current.addCertificate(mockCertificate({ walletAddress: '0xAbCd...W1' }));
65+
result.current.addCertificate(mockCertificate({ walletAddress: '0xEfGh...W2' }));
66+
});
67+
68+
expect(result.current.certificates).toHaveLength(2);
69+
});
70+
71+
it('keeps separate certificates for different properties', () => {
72+
const { result } = renderHook(() => useCertificateStore());
73+
74+
act(() => {
75+
result.current.addCertificate(mockCertificate({ propertyId: 'prop-1' }));
76+
result.current.addCertificate(mockCertificate({ propertyId: 'prop-2' }));
77+
});
78+
79+
expect(result.current.certificates).toHaveLength(2);
80+
});
81+
82+
it('getCertificate returns the matching certificate', () => {
83+
const { result } = renderHook(() => useCertificateStore());
84+
85+
act(() => {
86+
result.current.addCertificate(
87+
mockCertificate({ propertyId: 'prop-1', walletAddress: '0xAbCd...W1' }),
88+
);
89+
result.current.addCertificate(
90+
mockCertificate({ propertyId: 'prop-2', walletAddress: '0xAbCd...W1' }),
91+
);
92+
});
93+
94+
const found = result.current.getCertificate('prop-2', '0xAbCd...W1');
95+
expect(found?.propertyId).toBe('prop-2');
96+
});
97+
98+
it('getCertificate returns undefined when nothing matches', () => {
99+
const { result } = renderHook(() => useCertificateStore());
100+
101+
act(() => {
102+
result.current.addCertificate(mockCertificate());
103+
});
104+
105+
expect(result.current.getCertificate('prop-9', '0xAbCd...W1')).toBeUndefined();
106+
expect(result.current.getCertificate('prop-1', '0xOther...W9')).toBeUndefined();
107+
});
108+
109+
it('persists certificates across store instances', () => {
110+
const { result } = renderHook(() => useCertificateStore());
111+
112+
act(() => {
113+
result.current.addCertificate(mockCertificate());
114+
});
115+
116+
const { result: result2 } = renderHook(() => useCertificateStore());
117+
expect(result2.current.certificates).toHaveLength(1);
118+
expect(result2.current.certificates[0].id).toBe('cert-1');
119+
});
120+
});

0 commit comments

Comments
 (0)