Skip to content

Commit de8b953

Browse files
HelanaNadyim-saifExo1i
authored
feat: edit profile - [CU-869atcbxt] (#60)
Co-authored-by: Saif <saifeldin402@gmail.com> Co-authored-by: hxed <121557650+Exo1i@users.noreply.github.com>
1 parent 2bf4ec1 commit de8b953

32 files changed

Lines changed: 1245 additions & 219 deletions

src/__tests__/base_navigation.test.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ describe('Base Navigation', () => {
3737
jest.clearAllMocks();
3838
});
3939

40+
afterAll(() => {
41+
jest.clearAllMocks();
42+
});
43+
4044
it('renders home screen', async () => {
4145
const { getByText } = render(
4246
<ThemeProvider>
Lines changed: 332 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,332 @@
1+
// --- Adjusted Mocks ---
2+
3+
import { Alert, Platform } from 'react-native';
4+
5+
import * as DateTimePickerModule from '@react-native-community/datetimepicker';
6+
import { NavigationContainer } from '@react-navigation/native';
7+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
8+
import { act, fireEvent, render, waitFor } from '@testing-library/react-native';
9+
import * as ImagePicker from 'expo-image-picker';
10+
11+
import { formatBirthDate } from '@/components/utils/profile';
12+
import { mockUsers } from '@/mocks/data/userMock';
13+
import EditProfileScreen from '@/screens/profile/EditProfileScreen';
14+
import { useUserStore } from '@/stores/userStore';
15+
16+
jest.mock('@/hooks/useTheme', () => ({
17+
useTheme: () => ({ theme: 'light' }),
18+
}));
19+
20+
jest.mock('@react-navigation/elements', () => ({
21+
useHeaderHeight: () => 0,
22+
}));
23+
24+
const mockSetOptions = jest.fn();
25+
const mockGoBack = jest.fn();
26+
const mockAddListener = jest.fn();
27+
28+
jest.mock('@react-navigation/native', () => {
29+
const actual = jest.requireActual('@react-navigation/native');
30+
return {
31+
...actual,
32+
useNavigation: () => ({
33+
navigate: jest.fn(),
34+
goBack: mockGoBack,
35+
setOptions: mockSetOptions,
36+
addListener: mockAddListener,
37+
}),
38+
};
39+
});
40+
41+
jest.mock('@/services/user', () => ({
42+
updateUserProfile: jest.fn(),
43+
}));
44+
45+
jest.mock('@/stores/userStore', () => ({
46+
useUserStore: jest.fn(),
47+
updateUser: jest.fn(),
48+
}));
49+
50+
jest.mock('expo-image-picker', () => ({
51+
requestMediaLibraryPermissionsAsync: jest.fn(),
52+
requestCameraPermissionsAsync: jest.fn(),
53+
launchImageLibraryAsync: jest.fn(),
54+
launchCameraAsync: jest.fn(),
55+
}));
56+
57+
jest.mock('@react-native-community/datetimepicker', () => {
58+
let lastOnChange: ((event?: unknown, date?: Date) => void) | null = null;
59+
const MockDateTimePicker = ({
60+
onChange,
61+
}: {
62+
onChange: (event?: unknown, date?: Date) => void;
63+
}) => {
64+
lastOnChange = onChange;
65+
return null;
66+
};
67+
const __triggerChange = (date: Date) => {
68+
if (lastOnChange) lastOnChange(undefined, date);
69+
};
70+
return { __esModule: true, default: MockDateTimePicker, __triggerChange };
71+
});
72+
73+
const triggerPickerChange = (date: Date) =>
74+
(DateTimePickerModule as unknown as { __triggerChange: (d: Date) => void }).__triggerChange(date);
75+
76+
const _initialOS = Platform.OS as 'ios' | 'android';
77+
const setPlatformOS = (os: typeof _initialOS) => {
78+
Object.defineProperty(Platform, 'OS', {
79+
configurable: true,
80+
value: os,
81+
});
82+
};
83+
84+
jest.spyOn(console, 'error').mockImplementation(() => {});
85+
86+
const mockQueryClient = new QueryClient({
87+
defaultOptions: {
88+
queries: { retry: false },
89+
},
90+
});
91+
92+
const mockUsername = 'johndoe';
93+
const mockUser = mockUsers[mockUsername];
94+
95+
describe('EditProfileScreen (adjusted)', () => {
96+
const mockUpdateUser = jest.fn();
97+
beforeEach(() => {
98+
jest.clearAllMocks();
99+
100+
(useUserStore as unknown as jest.Mock).mockReturnValue({
101+
user: mockUsers[mockUsername],
102+
updateUser: mockUpdateUser,
103+
});
104+
});
105+
106+
afterAll(() => {
107+
jest.clearAllMocks();
108+
jest.clearAllTimers();
109+
});
110+
111+
it('renders correctly with user data', () => {
112+
const { getByTestId } = render(
113+
<NavigationContainer>
114+
<QueryClientProvider client={mockQueryClient}>
115+
<EditProfileScreen />
116+
</QueryClientProvider>
117+
</NavigationContainer>
118+
);
119+
expect(getByTestId('edit-display-name-input').props.value).toBe(mockUser.displayName);
120+
expect(getByTestId('edit-bio-input').props.value).toBe(mockUser.bio);
121+
expect(getByTestId('edit-location-input').props.value).toBe(mockUser.location);
122+
expect(getByTestId('edit-website-input').props.value).toBe(mockUser.websiteUrl);
123+
});
124+
125+
it('updates all form fields and sets formDataChanged', async () => {
126+
const { getByTestId } = render(
127+
<NavigationContainer>
128+
<QueryClientProvider client={mockQueryClient}>
129+
<EditProfileScreen />
130+
</QueryClientProvider>
131+
</NavigationContainer>
132+
);
133+
134+
await act(async () => {
135+
fireEvent.changeText(getByTestId('edit-display-name-input'), 'New Name');
136+
fireEvent.changeText(getByTestId('edit-bio-input'), 'New Bio');
137+
fireEvent.changeText(getByTestId('edit-location-input'), 'Mars');
138+
fireEvent.changeText(getByTestId('edit-website-input'), 'https://newsite.com');
139+
});
140+
141+
expect(getByTestId('edit-display-name-input').props.value).toBe('New Name');
142+
expect(getByTestId('edit-bio-input').props.value).toBe('New Bio');
143+
expect(getByTestId('edit-location-input').props.value).toBe('Mars');
144+
expect(getByTestId('edit-website-input').props.value).toBe('https://newsite.com');
145+
});
146+
147+
it('handles invalid website URL and shows error', async () => {
148+
const { getByTestId, getByText } = render(
149+
<NavigationContainer>
150+
<QueryClientProvider client={mockQueryClient}>
151+
<EditProfileScreen />
152+
</QueryClientProvider>
153+
</NavigationContainer>
154+
);
155+
await act(async () => {
156+
fireEvent.changeText(getByTestId('edit-website-input'), 'invalid');
157+
});
158+
expect(getByText('Please enter a valid website URL')).toBeTruthy();
159+
});
160+
161+
it('handles image picking from gallery', async () => {
162+
(ImagePicker.requestMediaLibraryPermissionsAsync as jest.Mock).mockResolvedValueOnce({
163+
status: 'granted',
164+
});
165+
(ImagePicker.launchImageLibraryAsync as jest.Mock).mockResolvedValueOnce({
166+
canceled: false,
167+
assets: [{ uri: 'file://mock-gallery.jpg' }],
168+
});
169+
170+
const { getByText, getByTestId } = render(
171+
<NavigationContainer>
172+
<QueryClientProvider client={mockQueryClient}>
173+
<EditProfileScreen />
174+
</QueryClientProvider>
175+
</NavigationContainer>
176+
);
177+
178+
const bannerEdit = getByTestId('edit-banner');
179+
await act(async () => {
180+
fireEvent.press(bannerEdit);
181+
});
182+
183+
await act(async () => {
184+
fireEvent.press(getByText('Choose Existing Photo'));
185+
});
186+
187+
await waitFor(() => {
188+
expect(ImagePicker.launchImageLibraryAsync).toHaveBeenCalled();
189+
});
190+
});
191+
192+
it('handles denied camera permission', async () => {
193+
(ImagePicker.requestCameraPermissionsAsync as jest.Mock).mockResolvedValueOnce({
194+
status: 'denied',
195+
});
196+
jest.spyOn(Alert, 'alert').mockImplementation(() => {});
197+
const { getByText, getByTestId } = render(
198+
<NavigationContainer>
199+
<QueryClientProvider client={mockQueryClient}>
200+
<EditProfileScreen />
201+
</QueryClientProvider>
202+
</NavigationContainer>
203+
);
204+
205+
const bannerEdit = getByTestId('edit-banner');
206+
await act(async () => {
207+
fireEvent.press(bannerEdit);
208+
});
209+
210+
await act(async () => {
211+
fireEvent.press(getByText('Take Photo'));
212+
});
213+
214+
expect(Alert.alert).toHaveBeenCalledWith(
215+
'Permission needed',
216+
'Please grant permission to access your camera'
217+
);
218+
});
219+
220+
it('removes banner via modal option', async () => {
221+
const { getByTestId } = render(
222+
<NavigationContainer>
223+
<QueryClientProvider client={mockQueryClient}>
224+
<EditProfileScreen />
225+
</QueryClientProvider>
226+
</NavigationContainer>
227+
);
228+
229+
const bannerEdit = getByTestId('edit-banner');
230+
await act(async () => {
231+
fireEvent.press(bannerEdit);
232+
});
233+
234+
const removeOption = getByTestId('remove-header');
235+
236+
await act(async () => {
237+
fireEvent.press(removeOption);
238+
});
239+
240+
expect(getByTestId('banner-image').props.uri).toBeUndefined();
241+
});
242+
243+
it('choose birthdate via date picker on iOS', async () => {
244+
setPlatformOS('ios');
245+
246+
const { getByTestId } = render(
247+
<NavigationContainer>
248+
<QueryClientProvider client={mockQueryClient}>
249+
<EditProfileScreen />
250+
</QueryClientProvider>
251+
</NavigationContainer>
252+
);
253+
254+
const birthDateButton = getByTestId('edit-birthdate-button');
255+
256+
await act(async () => {
257+
fireEvent.press(birthDateButton);
258+
});
259+
260+
const newBirthdate = new Date();
261+
newBirthdate.setFullYear(200, 3, 15);
262+
263+
const newBirthInput = formatBirthDate(newBirthdate.toISOString());
264+
fireEvent.changeText(getByTestId('edit-birthdate-input'), newBirthInput);
265+
266+
act(() => {
267+
triggerPickerChange(newBirthdate);
268+
});
269+
270+
expect(getByTestId('edit-birthdate-input').props.value).toBe(newBirthInput);
271+
});
272+
273+
it('handles denied gallery permission with alert', async () => {
274+
(ImagePicker.requestMediaLibraryPermissionsAsync as jest.Mock).mockResolvedValueOnce({
275+
status: 'denied',
276+
});
277+
jest.spyOn(Alert, 'alert').mockImplementation(() => {});
278+
279+
const { getByTestId, getByText } = render(
280+
<NavigationContainer>
281+
<QueryClientProvider client={mockQueryClient}>
282+
<EditProfileScreen />
283+
</QueryClientProvider>
284+
</NavigationContainer>
285+
);
286+
287+
const bannerEdit = getByTestId('edit-banner');
288+
await act(async () => {
289+
fireEvent.press(bannerEdit);
290+
});
291+
292+
await act(async () => {
293+
fireEvent.press(getByText('Choose Existing Photo'));
294+
});
295+
296+
expect(Alert.alert).toHaveBeenCalledWith(
297+
'Permission needed',
298+
'Please grant permission to access your photo library'
299+
);
300+
});
301+
302+
it('handles granted gallery permission correctly', async () => {
303+
(ImagePicker.requestMediaLibraryPermissionsAsync as jest.Mock).mockResolvedValueOnce({
304+
status: 'granted',
305+
});
306+
(ImagePicker.launchImageLibraryAsync as jest.Mock).mockResolvedValueOnce({
307+
canceled: false,
308+
assets: [{ uri: 'file://mock-gallery-image.jpg' }],
309+
});
310+
311+
const { getByTestId, getByText } = render(
312+
<NavigationContainer>
313+
<QueryClientProvider client={mockQueryClient}>
314+
<EditProfileScreen />
315+
</QueryClientProvider>
316+
</NavigationContainer>
317+
);
318+
319+
const bannerEdit = getByTestId('edit-banner');
320+
await act(async () => {
321+
fireEvent.press(bannerEdit);
322+
});
323+
324+
await act(async () => {
325+
fireEvent.press(getByText('Choose Existing Photo'));
326+
});
327+
328+
await waitFor(() => {
329+
expect(ImagePicker.launchImageLibraryAsync).toHaveBeenCalled();
330+
});
331+
});
332+
});

src/__tests__/profile/profile.test.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import ProfileInfo from '@/components/profile/ProfileInfo';
88
import * as profileHook from '@/hooks/profile/useProfile';
99
import { mockUsers } from '@/mocks/data/userMock';
1010
import { useUserStore } from '@/stores/userStore';
11-
import { UserData } from '@/types/user';
11+
import { UserProfile } from '@/types/user';
1212

1313
const mockUsername = 'johndoe';
1414

@@ -76,20 +76,20 @@ jest.mock('@/services/user', () => ({
7676
},
7777
}));
7878

79-
const mockErrorResult: UseQueryResult<UserData, Error> = {
79+
const mockErrorResult: UseQueryResult<UserProfile, Error> = {
8080
data: undefined,
8181
error: new Error('Something went wrong'),
8282
isError: true,
8383
isSuccess: false,
84-
} as unknown as UseQueryResult<UserData, Error>;
84+
} as unknown as UseQueryResult<UserProfile, Error>;
8585

86-
const mockLoadingResult: UseQueryResult<UserData, Error> = {
86+
const mockLoadingResult: UseQueryResult<UserProfile, Error> = {
8787
data: undefined,
8888
error: null,
8989
isError: false,
9090
isSuccess: false,
9191
isLoading: true,
92-
} as unknown as UseQueryResult<UserData, Error>;
92+
} as unknown as UseQueryResult<UserProfile, Error>;
9393

9494
// Mock useTheme like in step1.test.tsx
9595
jest.mock('@/hooks/useTheme', () => ({

0 commit comments

Comments
 (0)