Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Test setup #112

Merged
merged 4 commits into from
May 30, 2024
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
1 change: 1 addition & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ module.exports = {
preset: 'react-native',
transformIgnorePatterns: [`node_modules/(?!${esModules})`],
testPathIgnorePatterns: ['<rootDir>/node_modules/', '<rootDir>/e2e/'],
setupFiles: ['./jest.setup.js'],
};
3 changes: 3 additions & 0 deletions jest.setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import 'fastestsmallesttextencoderdecoder';
import 'react-native-get-random-values';
import 'react-native-url-polyfill/auto';
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@
"@react-native/eslint-config": "^0.73.1",
"@react-native/metro-config": "^0.73.2",
"@react-native/typescript-config": "^0.73.1",
"@testing-library/jest-native": "^5.4.3",
"@testing-library/react-hooks": "^8.0.1",
"@types/jest": "^29.5.11",
"@types/react": "^18.2.6",
"@types/react-native-push-notification": "^8.1.4",
Expand Down
33 changes: 33 additions & 0 deletions src/__mocks__/@xmtp/react-native-sdk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
function mockedClient() {
return {
conversations: {
streamAllMessages: jest.fn(() => {}),
cancelStreamAllMessages: jest.fn(() => {}),
cancelStream: jest.fn(() => {}),
},
exportKeyBundle: jest.fn(() => 'keybundle'),
canMessage: jest.fn(() => true),
};
}

module.exports = {
Client: {
createFromKeyBundle: jest.fn().mockImplementation(mockedClient),
createRandom: jest.fn().mockImplementation(mockedClient),
},
StaticAttachmentCodec: jest.fn().mockImplementation(() => {
return {};
}),
RemoteAttachmentCodec: jest.fn().mockImplementation(() => {
return {};
}),
JSContentCodec: jest.fn().mockImplementation(() => {
return {};
}),
GroupChangeCodec: jest.fn().mockImplementation(() => {
return {};
}),
ReplyCodec: jest.fn().mockImplementation(() => ({})),
ReactionCodec: jest.fn().mockImplementation(() => ({})),
emitter: {removeAllListeners: jest.fn(() => {})},
};
41 changes: 41 additions & 0 deletions src/hooks/useAddress.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {renderHook} from '@testing-library/react-hooks';
import {useAddress} from './useAddress';
import {useClient} from './useClient';

jest.mock('./useClient');

describe('useAddress', () => {
beforeEach(() => {
jest.clearAllMocks();
});

test('should return address and loading state from useClient', () => {
const mockClient = {address: '0x1234567890abcdef1234567890abcdef12345678'};
const mockLoading = false;

(useClient as jest.Mock).mockReturnValue({
client: mockClient,
loading: mockLoading,
});

const {result} = renderHook(() => useAddress());

expect(result.current.address).toBe(mockClient.address);
expect(result.current.loading).toBe(mockLoading);
});

test('should return undefined address if client is not available', () => {
const mockClient = null;
const mockLoading = true;

(useClient as jest.Mock).mockReturnValue({
client: mockClient,
loading: mockLoading,
});

const {result} = renderHook(() => useAddress());

expect(result.current.address).toBeUndefined();
expect(result.current.loading).toBe(mockLoading);
});
});
35 changes: 35 additions & 0 deletions src/hooks/useClient.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {renderHook} from '@testing-library/react-hooks';
import React from 'react';
import {ClientContext} from '../context/ClientContext';
import {useClient} from './useClient';

describe('useClient', () => {
test('should return the client context value', () => {
const mockContext = {
client: null,
setClient: () => {},
loading: false,
};

const wrapper = ({children}: {children: React.ReactNode}) => (
<ClientContext.Provider value={mockContext}>
{children}
</ClientContext.Provider>
);

const {result} = renderHook(() => useClient(), {wrapper});

expect(result.current).toBe(mockContext);
});

test('should return null if no client context value is provided', () => {
const wrapper = ({children}: {children: React.ReactNode}) => (
// @ts-ignore-next-line
<ClientContext.Provider value={null}>{children}</ClientContext.Provider>
);

const {result} = renderHook(() => useClient(), {wrapper});

expect(result.current).toBeNull();
});
});
124 changes: 124 additions & 0 deletions src/hooks/useContactInfo.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import {act, renderHook} from '@testing-library/react-hooks';
import {mmkvStorage} from '../services/mmkvStorage';
import {getEnsInfo} from '../utils/getEnsInfo';
import {useContactInfo} from './useContactInfo';

// Mock dependencies
jest.mock('../services/mmkvStorage', () => ({
mmkvStorage: {
getEnsName: jest.fn(),
getEnsAvatar: jest.fn(),
saveEnsName: jest.fn(),
saveEnsAvatar: jest.fn(),
clearEnsAvatar: jest.fn(),
},
}));

jest.mock('../utils/formatAddress', () => ({
formatAddress: jest.fn(address => `Formatted: ${address}`),
}));

jest.mock('../utils/getEnsInfo', () => ({
getEnsInfo: jest.fn(),
}));

describe('useContactInfo', () => {
const address = '0x1234567890abcdef1234567890abcdef12345678';

beforeEach(() => {
jest.clearAllMocks();
});

test('should return initial state when no address is provided', () => {
const {result} = renderHook(() => useContactInfo());

expect(result.current).toEqual({
displayName: null,
avatarUrl: null,
loading: true,
});
});

test('should return cached ENS name and avatar if available', () => {
(mmkvStorage.getEnsName as jest.Mock).mockReturnValue('cachedName');
(mmkvStorage.getEnsAvatar as jest.Mock).mockReturnValue('cachedAvatarUrl');
(getEnsInfo as jest.Mock).mockRejectedValueOnce('Failed to fetch ENS info');

const {result} = renderHook(() => useContactInfo(address));

expect(result.current).toEqual({
displayName: 'cachedName',
avatarUrl: 'cachedAvatarUrl',
loading: true,
});
});

test('should fetch ENS info and update state', async () => {
(mmkvStorage.getEnsName as jest.Mock).mockReturnValue(null);
(mmkvStorage.getEnsAvatar as jest.Mock).mockReturnValue(null);
(getEnsInfo as jest.Mock).mockResolvedValue({
ens: 'ensName',
avatarUrl: 'ensAvatarUrl',
});

const {result, waitForNextUpdate} = renderHook(() =>
useContactInfo(address),
);

await act(async () => {
await waitForNextUpdate();
});

expect(getEnsInfo).toHaveBeenCalledWith(address);
expect(mmkvStorage.saveEnsName).toHaveBeenCalledWith(address, 'ensName');
expect(mmkvStorage.saveEnsAvatar).toHaveBeenCalledWith(
address,
'ensAvatarUrl',
);
expect(result.current).toEqual({
displayName: 'ensName',
avatarUrl: 'ensAvatarUrl',
loading: false,
});
});

test('should handle error when fetching ENS info', async () => {
(mmkvStorage.getEnsName as jest.Mock).mockReturnValue(null);
(mmkvStorage.getEnsAvatar as jest.Mock).mockReturnValue(null);
(getEnsInfo as jest.Mock).mockRejectedValue(
new Error('Failed to fetch ENS info'),
);

const {result, waitForNextUpdate} = renderHook(() =>
useContactInfo(address),
);

await act(async () => {
await waitForNextUpdate();
});

expect(getEnsInfo).toHaveBeenCalledWith(address);
expect(result.current).toEqual({
displayName: `Formatted: ${address}`,
avatarUrl: null,
loading: false,
});
});

test('should save displayName to mmkvStorage when displayName changes', async () => {
(mmkvStorage.getEnsName as jest.Mock).mockReturnValue(null);
(mmkvStorage.getEnsAvatar as jest.Mock).mockReturnValue(null);
(getEnsInfo as jest.Mock).mockResolvedValue({
ens: 'ensName',
avatarUrl: 'ensAvatarUrl',
});

const {waitForNextUpdate} = renderHook(() => useContactInfo(address));

await act(async () => {
await waitForNextUpdate();
});

expect(mmkvStorage.saveEnsName).toHaveBeenCalledWith(address, 'ensName');
});
});
98 changes: 98 additions & 0 deletions src/hooks/useDebounce.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import {act, renderHook} from '@testing-library/react-hooks';
import {useDebounce} from './useDebounce';

jest.useFakeTimers();

describe('useDebounce', () => {
test('should return the initial value immediately', () => {
const {result} = renderHook(() => useDebounce('initial', 500));
expect(result.current).toBe('initial');
});

test('should debounce the value after the delay', () => {
const {result, rerender} = renderHook(
({value, delay}) => useDebounce(value, delay),
{
initialProps: {value: 'initial', delay: 500},
},
);

rerender({value: 'new value', delay: 500});

// Initially, the value should not be updated
expect(result.current).toBe('initial');

// Fast forward the timers by 500ms
act(() => {
jest.advanceTimersByTime(500);
});

// Now, the debounced value should be updated
expect(result.current).toBe('new value');
});

test('should update debounced value only after the specified delay', () => {
const {result, rerender} = renderHook(
({value, delay}) => useDebounce(value, delay),
{
initialProps: {value: 'initial', delay: 500},
},
);

rerender({value: 'new value', delay: 500});

// Fast forward the timers by less than the delay time
act(() => {
jest.advanceTimersByTime(300);
});

// Value should not have updated yet
expect(result.current).toBe('initial');

// Fast forward the remaining time
act(() => {
jest.advanceTimersByTime(200);
});

// Now, the debounced value should be updated
expect(result.current).toBe('new value');
});

test('should reset debounce timer if value changes within delay period', () => {
const {result, rerender} = renderHook(
({value, delay}) => useDebounce(value, delay),
{
initialProps: {value: 'initial', delay: 500},
},
);

rerender({value: 'new value', delay: 500});

// Fast forward the timers by 300ms
act(() => {
jest.advanceTimersByTime(300);
});

// Value should not have updated yet
expect(result.current).toBe('initial');

// Change the value again before the delay period completes
rerender({value: 'another value', delay: 500});

// Fast forward the timers by 300ms
act(() => {
jest.advanceTimersByTime(300);
});

// Value should still not have updated because the timer was reset
expect(result.current).toBe('initial');

// Fast forward the remaining time
act(() => {
jest.advanceTimersByTime(200);
});

// Now, the debounced value should be updated to the latest value
expect(result.current).toBe('another value');
});
});
Loading
Loading