Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 157 additions & 0 deletions src/hooks/__tests__/useDebouncedSearch.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { renderHook, act } from '@testing-library/react';
import { useDebouncedSearch } from '../useDebouncedSearch';

describe('useDebouncedSearch', () => {
beforeEach(() => {
jest.useFakeTimers();
});

afterEach(() => {
jest.useRealTimers();
});

it('starts with an empty query and no results', () => {
const searchFn = jest.fn().mockResolvedValue(['a']);
const { result } = renderHook(() =>
useDebouncedSearch({ searchFn, delay: 300 }),
);

expect(result.current.query).toBe('');
expect(result.current.results).toBeUndefined();
expect(result.current.isLoading).toBe(false);
expect(result.current.error).toBeNull();
});

it('does not fire the search until the debounce window elapses', () => {
const searchFn = jest.fn().mockResolvedValue(['a']);
const { result } = renderHook(() =>
useDebouncedSearch({ searchFn, delay: 300 }),
);

act(() => {
result.current.setQuery('hello');
});

expect(searchFn).not.toHaveBeenCalled();

act(() => {
jest.advanceTimersByTime(300);
});

expect(searchFn).toHaveBeenCalledTimes(1);
expect(searchFn).toHaveBeenCalledWith('hello', expect.any(AbortSignal));
});

it('collapses rapid input into a single search with the latest query', () => {
const searchFn = jest.fn().mockResolvedValue(['a']);
const { result } = renderHook(() =>
useDebouncedSearch({ searchFn, delay: 300 }),
);

act(() => {
result.current.setQuery('h');
jest.advanceTimersByTime(100);
result.current.setQuery('he');
jest.advanceTimersByTime(100);
result.current.setQuery('hel');
jest.advanceTimersByTime(100);
result.current.setQuery('hello');
jest.advanceTimersByTime(300);
});

expect(searchFn).toHaveBeenCalledTimes(1);
expect(searchFn).toHaveBeenCalledWith('hello', expect.any(AbortSignal));
});

it('sets results once the async search resolves', async () => {
const searchFn = jest.fn().mockResolvedValue(['result-1']);
const { result } = renderHook(() =>
useDebouncedSearch({ searchFn, delay: 300 }),
);

act(() => {
result.current.setQuery('hello');
});

await act(async () => {
jest.advanceTimersByTime(300);
await Promise.resolve();
await Promise.resolve();
});

expect(result.current.results).toEqual(['result-1']);
expect(result.current.isLoading).toBe(false);
});

it('surfaces errors from the search function', async () => {
const searchFn = jest.fn().mockRejectedValue(new Error('Network error'));
const { result } = renderHook(() =>
useDebouncedSearch({ searchFn, delay: 300 }),
);

act(() => {
result.current.setQuery('hello');
});

await act(async () => {
jest.advanceTimersByTime(300);
await Promise.resolve();
await Promise.resolve();
});

expect(result.current.error).toEqual(new Error('Network error'));
expect(result.current.isLoading).toBe(false);
});

it('respects minLength and does not search short queries', () => {
const searchFn = jest.fn().mockResolvedValue(['a']);
const { result } = renderHook(() =>
useDebouncedSearch({ searchFn, delay: 300, minLength: 3, initialResults: ['init'] }),
);

act(() => {
result.current.setQuery('hi');
jest.advanceTimersByTime(300);
});

expect(searchFn).not.toHaveBeenCalled();
expect(result.current.results).toEqual(['init']);
expect(result.current.isLoading).toBe(false);
});

it('clear resets query, results and error', () => {
const searchFn = jest.fn().mockResolvedValue(['a']);
const { result } = renderHook(() =>
useDebouncedSearch({ searchFn, delay: 300, initialResults: ['init'] }),
);

act(() => {
result.current.setQuery('hello');
result.current.clear();
});

expect(result.current.query).toBe('');
expect(result.current.results).toEqual(['init']);
expect(result.current.error).toBeNull();
});

it('cleans up pending timers on unmount', () => {
const searchFn = jest.fn().mockResolvedValue(['a']);
const { result, unmount } = renderHook(() =>
useDebouncedSearch({ searchFn, delay: 300 }),
);

act(() => {
result.current.setQuery('hello');
});

unmount();

act(() => {
jest.advanceTimersByTime(300);
});

// The pending search must not fire after unmount.
expect(searchFn).not.toHaveBeenCalled();
});
});
120 changes: 120 additions & 0 deletions src/store/__tests__/certificateStore.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { act, renderHook } from '@testing-library/react';
import { useCertificateStore } from '../certificateStore';
import type { NFTCertificate } from '@/types/certificate';

const mockCertificate = (overrides: Partial<NFTCertificate> = {}): NFTCertificate => ({
id: 'cert-1',
propertyId: 'prop-1',
propertyName: 'Test Property',
propertyAddress: '1 Test St',
propertyImage: null,
tokenAmount: 10,
tokenSymbol: 'TST',
walletAddress: '0xAbCd...W1',
purchaseDate: '2024-01-15T10:00:00Z',
transactionHash: '0xtxhash123',
network: 'ethereum',
contractAddress: '0x1234...5678',
ownershipPercentage: 1,
...overrides,
});

describe('certificateStore', () => {
beforeEach(() => {
localStorage.clear();
useCertificateStore.setState({ certificates: [] });
});

it('starts with no certificates', () => {
const { result } = renderHook(() => useCertificateStore());
expect(result.current.certificates).toEqual([]);
expect(
result.current.getCertificate('prop-1', '0xAbCd...W1'),
).toBeUndefined();
});

it('adds a certificate', () => {
const { result } = renderHook(() => useCertificateStore());

act(() => {
result.current.addCertificate(mockCertificate());
});

expect(result.current.certificates).toHaveLength(1);
expect(result.current.certificates[0].id).toBe('cert-1');
});

it('replaces a certificate for the same property and wallet', () => {
const { result } = renderHook(() => useCertificateStore());

act(() => {
result.current.addCertificate(mockCertificate({ id: 'cert-1', tokenAmount: 10 }));
result.current.addCertificate(mockCertificate({ id: 'cert-2', tokenAmount: 25 }));
});

expect(result.current.certificates).toHaveLength(1);
expect(result.current.certificates[0].id).toBe('cert-2');
expect(result.current.certificates[0].tokenAmount).toBe(25);
});

it('keeps separate certificates for different wallets', () => {
const { result } = renderHook(() => useCertificateStore());

act(() => {
result.current.addCertificate(mockCertificate({ walletAddress: '0xAbCd...W1' }));
result.current.addCertificate(mockCertificate({ walletAddress: '0xEfGh...W2' }));
});

expect(result.current.certificates).toHaveLength(2);
});

it('keeps separate certificates for different properties', () => {
const { result } = renderHook(() => useCertificateStore());

act(() => {
result.current.addCertificate(mockCertificate({ propertyId: 'prop-1' }));
result.current.addCertificate(mockCertificate({ propertyId: 'prop-2' }));
});

expect(result.current.certificates).toHaveLength(2);
});

it('getCertificate returns the matching certificate', () => {
const { result } = renderHook(() => useCertificateStore());

act(() => {
result.current.addCertificate(
mockCertificate({ propertyId: 'prop-1', walletAddress: '0xAbCd...W1' }),
);
result.current.addCertificate(
mockCertificate({ propertyId: 'prop-2', walletAddress: '0xAbCd...W1' }),
);
});

const found = result.current.getCertificate('prop-2', '0xAbCd...W1');
expect(found?.propertyId).toBe('prop-2');
});

it('getCertificate returns undefined when nothing matches', () => {
const { result } = renderHook(() => useCertificateStore());

act(() => {
result.current.addCertificate(mockCertificate());
});

expect(result.current.getCertificate('prop-9', '0xAbCd...W1')).toBeUndefined();
expect(result.current.getCertificate('prop-1', '0xOther...W9')).toBeUndefined();
});

it('persists certificates across store instances', () => {
const { result } = renderHook(() => useCertificateStore());

act(() => {
result.current.addCertificate(mockCertificate());
});

const { result: result2 } = renderHook(() => useCertificateStore());
expect(result2.current.certificates).toHaveLength(1);
expect(result2.current.certificates[0].id).toBe('cert-1');
});
});
Loading
Loading