Skip to content

Commit e93ac8b

Browse files
author
Guillermo Machado
committed
chore: add tests
1 parent 6463dcd commit e93ac8b

File tree

7 files changed

+218
-110
lines changed

7 files changed

+218
-110
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
"@lukemorales/query-key-factory": "^1.3.4",
7070
"@shopify/flash-list": "1.6.4",
7171
"@tanstack/react-query": "^5.52.1",
72+
"@testing-library/react-hooks": "^8.0.1",
7273
"app-icon-badge": "^0.0.15",
7374
"axios": "^1.7.5",
7475
"dayjs": "^1.11.13",

pnpm-lock.yaml

Lines changed: 40 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/app/(app)/_layout.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,8 @@ export default function TabLayout() {
1818
}, []);
1919

2020
useEffect(() => {
21-
const TIMEOUT = 1000;
2221
if (!ready) {
23-
setTimeout(() => {
24-
hideSplash();
25-
}, TIMEOUT);
22+
hideSplash();
2623
}
2724
}, [hideSplash, ready]);
2825

src/app/(app)/settings.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
/* eslint-disable max-lines-per-function */
2-
import { Env } from '@env';
32
import { Link } from 'expo-router';
43
import { useColorScheme } from 'nativewind';
54
import React from 'react';
@@ -11,6 +10,7 @@ import { ItemsContainer } from '@/components/settings/items-container';
1110
import { LanguageItem } from '@/components/settings/language-item';
1211
import { ThemeItem } from '@/components/settings/theme-item';
1312
import { translate } from '@/core';
13+
import { Env } from '@/core/env';
1414
import { colors, FocusAwareStatusBar, ScrollView, Text, View } from '@/ui';
1515
import { Website } from '@/ui/icons';
1616

@@ -24,7 +24,6 @@ export default function Settings() {
2424
return (
2525
<>
2626
<FocusAwareStatusBar />
27-
2827
<ScrollView>
2928
<View className="flex-1 gap-2 p-4">
3029
<Text className="text-xl font-bold">
Lines changed: 154 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,127 +1,184 @@
1-
/* eslint-disable max-lines-per-function */
2-
import { act, screen, waitFor } from '@testing-library/react-native';
3-
import React from 'react';
4-
5-
import { render } from '@/core/test-utils';
6-
7-
import { AuthProvider, authStorage, HEADER_KEYS, useAuth } from './auth';
8-
9-
jest.mock('@/api', () => {
10-
const originalModule = jest.requireActual('@/api'); // Import the original module
11-
const mockStore: Record<string, string> = {};
12-
1+
import { act, renderHook } from '@testing-library/react-hooks';
2+
import { waitFor } from '@testing-library/react-native';
3+
import dayjs from 'dayjs';
4+
5+
import { fireEvent, render, screen } from '@/core/test-utils';
6+
import { Text, TouchableOpacity, View } from '@/ui';
7+
8+
import {
9+
AuthProvider,
10+
clearTokens,
11+
getTokenDetails,
12+
storeTokens,
13+
useAuth,
14+
} from './auth';
15+
16+
// Mock MMKV Storage
17+
jest.mock('react-native-mmkv', () => {
18+
const mockStorage = new Map();
1319
return {
14-
...originalModule, // Spread the original module to keep other exports
15-
authStorage: {
16-
getString: jest.fn((key: string) => mockStore[key] || null),
17-
set: jest.fn((key: string, value: string) => {
18-
mockStore[key] = value;
19-
}),
20-
delete: jest.fn((key: string) => {
21-
delete mockStore[key];
22-
}),
23-
},
24-
client: {
25-
interceptors: {
26-
request: { use: jest.fn(), eject: jest.fn() },
27-
response: { use: jest.fn(), eject: jest.fn() },
28-
},
29-
},
20+
MMKV: jest.fn().mockImplementation(() => ({
21+
set: (key: string, value: string) => mockStorage.set(key, value),
22+
getString: (key: string) => mockStorage.get(key) || null,
23+
delete: (key: string) => mockStorage.delete(key),
24+
})),
3025
};
3126
});
3227

33-
const TestComponent: React.FC = () => {
34-
const { token, isAuthenticated, loading, ready, logout } = useAuth();
28+
// Mock API client interceptors
29+
jest.mock('@/api', () => ({
30+
client: {
31+
interceptors: {
32+
request: { use: jest.fn(), eject: jest.fn() },
33+
response: { use: jest.fn(), eject: jest.fn() },
34+
},
35+
},
36+
}));
3537

38+
const TestComponent = () => {
39+
const { token, isAuthenticated, loading, ready, logout } = useAuth();
3640
return (
37-
<div>
38-
<p data-testid="token">{token}</p>
39-
<p data-testid="isAuthenticated">{isAuthenticated ? 'true' : 'false'}</p>
40-
<p data-testid="loading">{loading ? 'true' : 'false'}</p>
41-
<p data-testid="ready">{ready ? 'true' : 'false'}</p>
42-
<button data-testid="logout" onClick={logout}>
43-
Logout
44-
</button>
45-
</div>
41+
<View>
42+
<Text testID="token">{token}</Text>
43+
<Text testID="isAuthenticated">{isAuthenticated ? 'true' : 'false'}</Text>
44+
<Text testID="loading">{loading ? 'true' : 'false'}</Text>
45+
<Text testID="ready">{ready ? 'true' : 'false'}</Text>
46+
<TouchableOpacity testID="logout" onPress={logout}>
47+
<Text>Logout</Text>
48+
</TouchableOpacity>
49+
</View>
4650
);
4751
};
4852

49-
describe('AuthProvider', () => {
50-
render(
51-
<AuthProvider>
52-
<TestComponent />
53-
</AuthProvider>,
54-
);
55-
53+
describe('Auth Utilities', () => {
5654
afterEach(() => {
57-
jest.clearAllMocks();
55+
clearTokens();
5856
});
5957

60-
it('should initialize with loading and ready states', async () => {
61-
(authStorage.getString as jest.Mock).mockImplementation((key) => {
62-
if (key === HEADER_KEYS.ACCESS_TOKEN) {
63-
return 'mockToken';
64-
}
65-
if (key === HEADER_KEYS.EXPIRY) {
66-
return '2100-01-01T00:00:00.000Z';
67-
}
68-
return null;
58+
it('stores tokens correctly', () => {
59+
storeTokens({
60+
accessToken: 'access-token',
61+
refreshToken: 'refresh-token',
62+
userId: 'user-id',
63+
expiration: '2025-01-17T00:00:00Z',
6964
});
7065

71-
expect(screen.getByTestId('loading').textContent).toBe('true');
72-
expect(screen.getByTestId('ready').textContent).toBe('false');
66+
const tokens = getTokenDetails();
67+
expect(tokens).toEqual({
68+
accessToken: 'access-token',
69+
refreshToken: 'refresh-token',
70+
userId: 'user-id',
71+
expiration: '2025-01-17T00:00:00Z',
72+
});
73+
});
7374

74-
await waitFor(() =>
75-
expect(screen.getByTestId('loading').textContent).toBe('false'),
76-
);
77-
expect(screen.getByTestId('ready').textContent).toBe('true');
78-
expect(screen.getByTestId('isAuthenticated').textContent).toBe('true');
79-
expect(screen.getByTestId('token').textContent).toBe('mockToken');
75+
it('clears tokens correctly', () => {
76+
storeTokens({
77+
accessToken: 'access-token',
78+
refreshToken: 'refresh-token',
79+
userId: 'user-id',
80+
expiration: '2025-01-17T00:00:00Z',
81+
});
82+
clearTokens();
83+
84+
const tokens = getTokenDetails();
85+
expect(tokens).toEqual({
86+
accessToken: '',
87+
refreshToken: '',
88+
userId: '',
89+
expiration: '',
90+
});
8091
});
92+
});
8193

82-
it('should handle expired token', async () => {
83-
(authStorage.getString as jest.Mock).mockImplementation((key) => {
84-
if (key === HEADER_KEYS.ACCESS_TOKEN) {
85-
return 'expiredToken';
86-
}
87-
if (key === HEADER_KEYS.EXPIRY) {
88-
return '2000-01-01T00:00:00.000Z';
89-
}
90-
return null;
94+
describe('AuthProvider', () => {
95+
it('provides initial state correctly', () => {
96+
const { result } = renderHook(() => useAuth(), {
97+
wrapper: AuthProvider,
9198
});
9299

93-
await waitFor(() =>
94-
expect(screen.getByTestId('loading').textContent).toBe('false'),
95-
);
96-
expect(screen.getByTestId('isAuthenticated').textContent).toBe('false');
97-
expect(screen.getByTestId('token').textContent).toBe('');
100+
expect(result.current).toEqual({
101+
token: null,
102+
isAuthenticated: false,
103+
loading: false,
104+
ready: true,
105+
logout: expect.any(Function),
106+
});
98107
});
99108

100-
it('should clear storage and state on logout', async () => {
101-
(authStorage.getString as jest.Mock).mockImplementation((key) => {
102-
if (key === HEADER_KEYS.ACCESS_TOKEN) {
103-
return 'mockToken';
104-
}
105-
if (key === HEADER_KEYS.EXPIRY) {
106-
return '2100-01-01T00:00:00.000Z';
107-
}
108-
return null;
109+
it('handles token state correctly', async () => {
110+
storeTokens({
111+
accessToken: 'valid-token',
112+
refreshToken: 'refresh-token',
113+
userId: 'user-id',
114+
expiration: dayjs().add(1, 'hour').toISOString(),
109115
});
110116

111-
await waitFor(() =>
112-
expect(screen.getByTestId('loading').textContent).toBe('false'),
113-
);
117+
const { result } = renderHook(() => useAuth(), {
118+
wrapper: AuthProvider,
119+
});
120+
121+
await waitFor(() => {
122+
expect(result.current.isAuthenticated).toBe(true);
123+
});
124+
125+
expect(result.current.token).toBe('valid-token');
126+
expect(result.current.loading).toBe(false);
127+
expect(result.current.ready).toBe(true);
128+
});
129+
130+
it('logs out correctly', () => {
131+
const { result } = renderHook(() => useAuth(), {
132+
wrapper: AuthProvider,
133+
});
114134

115135
act(() => {
116-
screen.getByTestId('logout').click();
136+
result.current.logout();
137+
});
138+
139+
expect(getTokenDetails()).toEqual({
140+
accessToken: '',
141+
refreshToken: '',
142+
userId: '',
143+
expiration: '',
144+
});
145+
expect(result.current.isAuthenticated).toBe(false);
146+
});
147+
});
148+
describe('TestComponent', () => {
149+
afterEach(() => {
150+
clearTokens();
151+
});
152+
153+
it('renders correctly and handles logout', async () => {
154+
// Set initial tokens
155+
storeTokens({
156+
accessToken: 'valid-token',
157+
refreshToken: 'refresh-token',
158+
userId: 'user-id',
159+
expiration: dayjs().add(1, 'hour').toISOString(),
117160
});
118161

119-
expect(authStorage.delete).toHaveBeenCalledWith(HEADER_KEYS.ACCESS_TOKEN);
120-
expect(authStorage.delete).toHaveBeenCalledWith(HEADER_KEYS.REFRESH_TOKEN);
121-
expect(authStorage.delete).toHaveBeenCalledWith(HEADER_KEYS.USER_ID);
122-
expect(authStorage.delete).toHaveBeenCalledWith(HEADER_KEYS.EXPIRY);
162+
// Render the component with AuthProvider
163+
render(
164+
<AuthProvider>
165+
<TestComponent />
166+
</AuthProvider>,
167+
);
168+
169+
// Verify initial state
170+
expect(screen.getByTestId('token')).toHaveTextContent('valid-token');
171+
expect(screen.getByTestId('isAuthenticated')).toHaveTextContent('true');
172+
expect(screen.getByTestId('loading')).toHaveTextContent('false');
173+
expect(screen.getByTestId('ready')).toHaveTextContent('true');
174+
175+
// Simulate logout action
176+
fireEvent.press(screen.getByTestId('logout'));
123177

124-
expect(screen.getByTestId('isAuthenticated').textContent).toBe('false');
125-
expect(screen.getByTestId('token').textContent).toBe('');
178+
// Verify state after logout
179+
expect(screen.getByTestId('token')).toHaveTextContent('');
180+
expect(screen.getByTestId('isAuthenticated')).toHaveTextContent('false');
181+
expect(screen.getByTestId('loading')).toHaveTextContent('false');
182+
expect(screen.getByTestId('ready')).toHaveTextContent('true');
126183
});
127184
});

0 commit comments

Comments
 (0)