Skip to content

Commit c199ab2

Browse files
authored
test: add missing unit tests - [CU-869bgfmvj] (#250)
1 parent 90a277b commit c199ab2

12 files changed

Lines changed: 629 additions & 15 deletions

babel.config.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,10 @@ module.exports = function (api) {
22
api.cache(true);
33
return {
44
presets: [['babel-preset-expo', { jsxImportSource: 'nativewind' }], 'nativewind/babel'],
5+
env: {
6+
test: {
7+
plugins: ['dynamic-import-node'],
8+
},
9+
},
510
};
611
};

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@
108108
"@wdio/spec-reporter": "^9.20.0",
109109
"appium": "^2.19.0",
110110
"appium-uiautomator2-driver": "^4.2.9",
111+
"babel-plugin-dynamic-import-node": "^2.3.3",
111112
"babel-preset-expo": "^54.0.5",
112113
"eslint": "^9.25.0",
113114
"eslint-config-expo": "~10.0.0",

pnpm-lock.yaml

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

src/__tests__/components/TweetComposer.test.tsx

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ jest.mock('expo-file-system', () => ({
4545
getInfoAsync: jest.fn().mockResolvedValue({ exists: true, size: 1024 * 1024 }), // 1MB mock
4646
}));
4747

48+
jest.mock('expo-file-system/legacy', () => ({
49+
getInfoAsync: jest.fn().mockResolvedValue({ exists: true, size: 1024 * 1024 }),
50+
}));
51+
4852
jest.mock('expo-video', () => {
4953
const addListener = jest.fn(() => ({ remove: jest.fn() }));
5054
const React = jest.requireActual('react');
@@ -1136,4 +1140,79 @@ describe('TweetComposer', () => {
11361140
});
11371141
});
11381142
});
1143+
1144+
it('handles navigation to author profile', () => {
1145+
const onPressAuthor = jest.fn();
1146+
const onClose = jest.fn();
1147+
1148+
const mockReplyToTweet = {
1149+
author: { username: 'author', displayName: 'Author', avatarUrl: null },
1150+
createdAt: new Date().toISOString(),
1151+
content: 'Original tweet',
1152+
};
1153+
1154+
const { getByTestId } = renderWithProviders(
1155+
<TweetComposer
1156+
visible
1157+
onClose={onClose}
1158+
onPressAuthor={onPressAuthor}
1159+
replyToAuthor={{ username: 'author', displayName: 'Author' }}
1160+
replyToTweet={mockReplyToTweet}
1161+
/>
1162+
);
1163+
1164+
const authorLink = getByTestId('replying-to-username');
1165+
fireEvent.press(authorLink);
1166+
expect(onPressAuthor).toHaveBeenCalledWith('author');
1167+
expect(onClose).toHaveBeenCalled();
1168+
});
1169+
1170+
it('alerts when camera permission is denied', async () => {
1171+
const mockRequestPermission = ImagePicker.requestCameraPermissionsAsync as jest.Mock;
1172+
mockRequestPermission.mockResolvedValueOnce({ status: 'denied' });
1173+
const mockAlert = jest.spyOn(Alert, 'alert');
1174+
1175+
const { getByTestId } = renderWithProviders(<TweetComposer visible onClose={() => {}} />);
1176+
const cameraIcon = getByTestId('camera-icon');
1177+
1178+
fireEvent.press(cameraIcon);
1179+
1180+
await waitFor(() => {
1181+
expect(mockAlert).toHaveBeenCalledWith('Permission needed', expect.any(String));
1182+
});
1183+
mockAlert.mockRestore();
1184+
});
1185+
1186+
it('alerts when file is too large', async () => {
1187+
const mockLaunchImageLibrary = ImagePicker.launchImageLibraryAsync as jest.Mock;
1188+
mockLaunchImageLibrary.mockResolvedValue({
1189+
canceled: false,
1190+
assets: [
1191+
{
1192+
uri: 'file://large-image.jpg',
1193+
fileName: 'large-image.jpg',
1194+
width: 1000,
1195+
height: 1000,
1196+
},
1197+
],
1198+
});
1199+
1200+
const mockGetInfo = require('expo-file-system/legacy').getInfoAsync;
1201+
mockGetInfo.mockResolvedValue({ exists: true, size: 10 * 1024 * 1024 }); // 10MB > 5MB limit
1202+
1203+
const mockAlert = jest.spyOn(Alert, 'alert');
1204+
1205+
const { getByTestId } = renderWithProviders(<TweetComposer visible onClose={() => {}} />);
1206+
const pictureIcon = getByTestId('media-icon');
1207+
1208+
fireEvent.press(pictureIcon);
1209+
1210+
await waitFor(() => {
1211+
expect(mockAlert).toHaveBeenCalledWith(
1212+
'File Too Large',
1213+
expect.stringContaining('exceed size limits')
1214+
);
1215+
});
1216+
mockAlert.mockRestore();
1217+
});
11391218
});

src/__tests__/components/notifications/FollowNotificationItem.test.tsx

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Alert } from 'react-native';
22

33
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
4-
import { fireEvent, render } from '@testing-library/react-native';
4+
import { act, fireEvent, render } from '@testing-library/react-native';
55
import { SafeAreaProvider } from 'react-native-safe-area-context';
66

77
import { FollowNotificationItem } from '@/components/notifications/types/FollowNotificationItem';
@@ -123,7 +123,6 @@ describe('FollowNotificationItem', () => {
123123
</Wrapper>
124124
);
125125

126-
// Button text is rendered with lowercase from nativewind
127126
const followButton = getByText('Follow back');
128127
expect(followButton).toBeTruthy();
129128
});
@@ -185,12 +184,13 @@ describe('FollowNotificationItem', () => {
185184
const followButton = getByText('Follow back');
186185
fireEvent.press(followButton);
187186

188-
// Simulate error by calling the error callback
189187
const mutateCall = mockFollowMutation.mutate.mock.calls[0];
190188
const errorCallback = mutateCall[1]?.onError;
191189

192190
if (errorCallback) {
193-
errorCallback(new Error('Network error'));
191+
act(() => {
192+
errorCallback(new Error('Network error'));
193+
});
194194
}
195195

196196
expect(mockAlert).toHaveBeenCalledWith('Unable to update follow', 'Network error');
@@ -292,9 +292,46 @@ describe('FollowNotificationItem', () => {
292292

293293
const nameText = getByText('Follower One');
294294

295-
// The name text is within a pressable container
296295
fireEvent.press(nameText.parent?.parent || nameText);
297296

298297
expect(mockOnPress).toHaveBeenCalled();
299298
});
299+
300+
it('navigates to user profile when avatar is pressed', () => {
301+
const { getByTestId } = render(
302+
<Wrapper>
303+
<FollowNotificationItem notification={mockNotification} onPress={mockOnPress} />
304+
</Wrapper>
305+
);
306+
307+
const avatarGroup = getByTestId('actor-avatar-group');
308+
fireEvent.press(avatarGroup);
309+
310+
expect(mockOnPress).toHaveBeenCalled();
311+
});
312+
313+
it('should use default error message if error has no message', () => {
314+
const mockAlert = jest.spyOn(Alert, 'alert');
315+
316+
const { getByText } = render(
317+
<Wrapper>
318+
<FollowNotificationItem notification={mockNotification} onPress={mockOnPress} />
319+
</Wrapper>
320+
);
321+
322+
const followButton = getByText('Follow back');
323+
fireEvent.press(followButton);
324+
325+
const mutateCall = mockFollowMutation.mutate.mock.calls[0];
326+
const errorCallback = mutateCall[1]?.onError;
327+
328+
if (errorCallback) {
329+
act(() => {
330+
errorCallback({});
331+
});
332+
}
333+
334+
expect(mockAlert).toHaveBeenCalledWith('Unable to update follow', 'Please try again.');
335+
mockAlert.mockRestore();
336+
});
300337
});

src/__tests__/components/notifications/TweetQuoteNotificationItem.test.tsx

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,16 @@ jest.mock('@/hooks/navigation/useRootNavigation', () => ({
2323
}));
2424

2525
jest.mock('@/components/ui/ActorAvatarGroup', () => 'ActorAvatarGroup');
26-
jest.mock('@/components/notifications/types/TweetActionBar', () => ({
27-
TweetActionBar: 'TweetActionBar',
28-
}));
26+
jest.mock('@/components/notifications/types/TweetActionBar', () => {
27+
const { Pressable, Text } = require('react-native');
28+
return {
29+
TweetActionBar: ({ onReplyPress }: { onReplyPress: () => void }) => (
30+
<Pressable testID="reply-button" onPress={onReplyPress}>
31+
<Text>Reply</Text>
32+
</Pressable>
33+
),
34+
};
35+
});
2936

3037
describe('TweetQuoteNotificationItem', () => {
3138
const mockOnPress = jest.fn((callback) => callback());
@@ -184,4 +191,22 @@ describe('TweetQuoteNotificationItem', () => {
184191
fireEvent.press(getByText('User One'));
185192
expect(mockOnPress).toHaveBeenCalled();
186193
});
194+
195+
it('handles reply press', () => {
196+
const notification = createNotification();
197+
const { getByTestId } = render(
198+
<TweetQuoteNotificationItem notification={notification} onPress={mockOnPress} />
199+
);
200+
201+
const replyButton = getByTestId('reply-button');
202+
fireEvent.press(replyButton);
203+
204+
expect(mockRootNavigate).toHaveBeenCalledWith(ROOT.TWEET, {
205+
screen: TWEET.DETAIL,
206+
params: {
207+
tweetId: 'quoted-tweet1',
208+
initialOpenComposer: true,
209+
},
210+
});
211+
});
187212
});

0 commit comments

Comments
 (0)