Skip to content

Commit

Permalink
Merge pull request #112 from xmtp-labs/ar/test-setup-services-mock
Browse files Browse the repository at this point in the history
feat: Test setup
  • Loading branch information
alexrisch authored May 30, 2024
2 parents d003e33 + ee012a2 commit 6a35be0
Show file tree
Hide file tree
Showing 17 changed files with 775 additions and 12 deletions.
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

0 comments on commit 6a35be0

Please sign in to comment.