Skip to content

Commit 1a47570

Browse files
ft(#4):ft-reset-password-flow (#45)
1 parent 2c20b54 commit 1a47570

File tree

13 files changed

+3178
-137
lines changed

13 files changed

+3178
-137
lines changed

Diff for: __tests__/auth/login/UserLogin.test.tsx

+57-26
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,79 @@
1+
import { render, fireEvent, waitFor } from '@testing-library/react-native';
12
import UserLogin from '@/components/Login/UserLogin';
2-
import { fireEvent, render, waitFor } from '@testing-library/react-native';
3+
import { router } from 'expo-router';
4+
import AsyncStorage from '@react-native-async-storage/async-storage';
35

4-
describe('<UserLogin />', () => {
5-
const onSubmitMock = jest.fn();
6+
jest.mock('expo-router', () => ({
7+
router: {
8+
push: jest.fn(),
9+
},
10+
}));
11+
12+
jest.mock('@react-native-async-storage/async-storage', () => ({
13+
getItem: jest.fn(() => Promise.resolve('Test Organization')),
14+
}));
15+
16+
describe('UserLogin Component', () => {
17+
const mockOnSubmit = jest.fn().mockResolvedValue({});
618

719
beforeEach(() => {
820
jest.clearAllMocks();
921
});
1022

11-
test('renders text correctly', () => {
12-
const { getByText } = render(<UserLogin onSubmit={onSubmitMock} />);
23+
it('renders correctly and fetches organization name from AsyncStorage', async () => {
24+
const { getByText } = render(<UserLogin onSubmit={mockOnSubmit} />);
1325

14-
getByText('Welcome to your_organization_name');
15-
getByText('Switch your organization');
16-
getByText('Sign In');
17-
getByText('Forgot Password?');
18-
getByText('Remember me.');
26+
await waitFor(() => expect(AsyncStorage.getItem).toHaveBeenCalledWith('orgName'));
27+
expect(getByText('Welcome to Test Organization')).toBeTruthy();
1928
});
2029

21-
test('displays error messages when validation fails', async () => {
22-
const { getByText, getByPlaceholderText } = render(<UserLogin onSubmit={onSubmitMock} />);
30+
it('submits form successfully when fields are valid', async () => {
31+
const { getByPlaceholderText, getByText } = render(<UserLogin onSubmit={mockOnSubmit} />);
2332

33+
fireEvent.changeText(getByPlaceholderText('Email'), '[email protected]');
34+
fireEvent.changeText(getByPlaceholderText('Password'), 'ValidPassword');
2435
fireEvent.press(getByText('Sign In'));
2536

26-
await waitFor(() => {
27-
getByText('Please provide your email address');
28-
getByText('Please provide a password');
29-
});
30-
});
37+
await waitFor(() => expect(mockOnSubmit).toHaveBeenCalledWith({
38+
39+
password: 'ValidPassword',
40+
}));
3141

32-
test('calls onSubmit with correct values', async () => {
33-
const { getByPlaceholderText, getByText } = render(<UserLogin onSubmit={onSubmitMock} />);
42+
expect(mockOnSubmit).toHaveBeenCalledTimes(1);
43+
});
3444

35-
fireEvent.changeText(getByPlaceholderText('Email'), '[email protected]');
36-
fireEvent.changeText(getByPlaceholderText('Password'), 'password123');
45+
it('shows validation error messages for empty fields', async () => {
46+
const { getByText } = render(<UserLogin onSubmit={mockOnSubmit} />);
3747

3848
fireEvent.press(getByText('Sign In'));
3949

4050
await waitFor(() => {
41-
expect(onSubmitMock).toHaveBeenCalledWith({
42-
43-
password: 'password123',
44-
});
45-
expect(onSubmitMock).toHaveBeenCalledTimes(1);
51+
expect(getByText('Email is required')).toBeTruthy();
52+
expect(getByText('Password is required')).toBeTruthy();
4653
});
54+
55+
expect(mockOnSubmit).not.toHaveBeenCalled();
56+
});
57+
58+
it('navigates to reset password when "Forgot Password?" is pressed', () => {
59+
const { getByText } = render(<UserLogin onSubmit={mockOnSubmit} />);
60+
61+
fireEvent.press(getByText('Forgot Password?'));
62+
expect(router.push).toHaveBeenCalledWith('/auth/forgot-password');
63+
});
64+
65+
it('toggles password visibility when eye icon is pressed', () => {
66+
const { getByPlaceholderText, getByTestId } = render(<UserLogin onSubmit={mockOnSubmit} />);
67+
68+
const passwordInput = getByPlaceholderText('Password');
69+
const toggleIcon = getByTestId('password-toggle');
70+
71+
expect(passwordInput.props.secureTextEntry).toBe(true);
72+
73+
fireEvent.press(toggleIcon);
74+
expect(passwordInput.props.secureTextEntry).toBe(false);
75+
76+
fireEvent.press(toggleIcon);
77+
expect(passwordInput.props.secureTextEntry).toBe(true);
4778
});
4879
});

Diff for: __tests__/forgetpassword/forgot-password.test.tsx

+116
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import React from 'react';
2+
import { render, fireEvent, waitFor } from '@testing-library/react-native';
3+
import { MockedProvider } from '@apollo/client/testing';
4+
import { useToast } from 'react-native-toast-notifications';
5+
import ResetPassword from '@/app/auth/forgot-password';
6+
import { RESET_PASSWORD_EMAIL } from '@/graphql/mutations/resetPassword';
7+
8+
jest.mock('react-native-toast-notifications', () => ({
9+
useToast: jest.fn(),
10+
}));
11+
12+
const mockShowToast = jest.fn();
13+
useToast.mockReturnValue({ show: mockShowToast });
14+
15+
const mocks = [
16+
{
17+
request: {
18+
query: RESET_PASSWORD_EMAIL,
19+
variables: { email: '[email protected]' },
20+
},
21+
result: {
22+
data: {},
23+
},
24+
},
25+
];
26+
27+
describe('<ResetPassword />', () => {
28+
beforeEach(() => {
29+
jest.clearAllMocks();
30+
});
31+
32+
test('renders correctly', () => {
33+
const { getByText } = render(
34+
<MockedProvider mocks={mocks} addTypename={false}>
35+
<ResetPassword />
36+
</MockedProvider>
37+
);
38+
39+
getByText('Reset Password');
40+
getByText('You will receive an email to proceed with resetting password');
41+
});
42+
43+
test('shows success message after successful email submission', async () => {
44+
const { getByPlaceholderText, getByText } = render(
45+
<MockedProvider mocks={mocks} addTypename={false}>
46+
<ResetPassword />
47+
</MockedProvider>
48+
);
49+
50+
fireEvent.changeText(getByPlaceholderText('Enter Email'), '[email protected]');
51+
fireEvent.press(getByText('Continue'));
52+
53+
await waitFor(() =>
54+
expect(mockShowToast).toHaveBeenCalledWith('Check your email to proceed!', expect.anything())
55+
);
56+
57+
// Check if the success message is displayed
58+
expect(getByText('Password reset request successful!')).toBeTruthy();
59+
expect(getByText('Please check your email for a link to reset your password!')).toBeTruthy();
60+
});
61+
62+
test('shows error message when email submission fails', async () => {
63+
const errorMock = [
64+
{
65+
request: {
66+
query: RESET_PASSWORD_EMAIL,
67+
variables: { email: '[email protected]' },
68+
},
69+
error: new Error('Failed to send reset email'),
70+
},
71+
];
72+
73+
const { getByPlaceholderText, getByText } = render(
74+
<MockedProvider mocks={errorMock} addTypename={false}>
75+
<ResetPassword />
76+
</MockedProvider>
77+
);
78+
79+
fireEvent.changeText(getByPlaceholderText('Enter Email'), '[email protected]');
80+
fireEvent.press(getByText('Continue'));
81+
82+
await waitFor(() =>
83+
expect(mockShowToast).toHaveBeenCalledWith('Failed to send reset email', expect.anything())
84+
);
85+
});
86+
87+
test('displays validation error when submitting empty email', async () => {
88+
const { getByText } = render(
89+
<MockedProvider mocks={mocks} addTypename={false}>
90+
<ResetPassword />
91+
</MockedProvider>
92+
);
93+
94+
fireEvent.press(getByText('Continue'));
95+
96+
// Assuming the validation schema requires an email
97+
await waitFor(() => expect(getByText(/email/i)).toBeTruthy());
98+
});
99+
100+
test('displays loading state while submitting', async () => {
101+
const { getByPlaceholderText, getByText } = render(
102+
<MockedProvider mocks={mocks} addTypename={false}>
103+
<ResetPassword />
104+
</MockedProvider>
105+
);
106+
107+
fireEvent.changeText(getByPlaceholderText('Enter Email'), '[email protected]');
108+
109+
// Press the button and check if it shows loading state
110+
fireEvent.press(getByText('Continue'));
111+
112+
expect(getByText('Continue').props.state).toBe('Loading');
113+
114+
await waitFor(() => expect(getByText('Check your email to proceed!')).toBeTruthy());
115+
});
116+
});

Diff for: __tests__/forgetpassword/reset-password.test.tsx

+126
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import React from 'react';
2+
import { render, fireEvent, waitFor } from '@testing-library/react-native';
3+
import { useToast } from 'react-native-toast-notifications';
4+
import SetNewPassword from '@/app/auth/reset-password';
5+
import { MockedProvider } from '@apollo/client/testing';
6+
import { FORGOT_PASSWORD, VERIFY_RESET_PASSWORD_TOKEN } from '@/graphql/mutations/resetPassword';
7+
8+
jest.mock('react-native-toast-notifications', () => ({
9+
useToast: jest.fn(),
10+
}));
11+
12+
const mockShowToast = jest.fn();
13+
useToast.mockReturnValue({ show: mockShowToast });
14+
15+
const mocks = [
16+
{
17+
request: {
18+
query: VERIFY_RESET_PASSWORD_TOKEN,
19+
variables: { token: 'mockedToken' },
20+
},
21+
result: {
22+
data: {},
23+
},
24+
},
25+
{
26+
request: {
27+
query: FORGOT_PASSWORD,
28+
variables: {
29+
password: 'newPassword123',
30+
confirmPassword: 'newPassword123',
31+
token: 'mockedToken',
32+
},
33+
},
34+
result: {
35+
data: {},
36+
},
37+
},
38+
];
39+
40+
describe('<SetNewPassword />', () => {
41+
beforeEach(() => {
42+
jest.clearAllMocks();
43+
});
44+
45+
test('renders correctly', () => {
46+
const { getByText } = render(
47+
<MockedProvider mocks={mocks} addTypename={false}>
48+
<SetNewPassword />
49+
</MockedProvider>
50+
);
51+
52+
getByText('Reset Password');
53+
});
54+
55+
test('toggles password visibility', () => {
56+
const { getByPlaceholderText, getByRole } = render(
57+
<MockedProvider mocks={mocks} addTypename={false}>
58+
<SetNewPassword />
59+
</MockedProvider>
60+
);
61+
62+
const passwordInput = getByPlaceholderText('Password');
63+
const toggleButton = getByRole('button', { name: /eye/i });
64+
65+
expect(passwordInput.props.secureTextEntry).toBe(true);
66+
67+
fireEvent.press(toggleButton);
68+
69+
expect(passwordInput.props.secureTextEntry).toBe(false);
70+
71+
fireEvent.press(toggleButton);
72+
73+
expect(passwordInput.props.secureTextEntry).toBe(true);
74+
});
75+
76+
test('shows error message when form submission fails', async () => {
77+
const errorMock = [
78+
{
79+
request: {
80+
query: FORGOT_PASSWORD,
81+
variables: {
82+
password: 'newPassword123',
83+
confirmPassword: 'newPassword123',
84+
token: 'mockedToken',
85+
},
86+
},
87+
error: new Error('Failed to reset password'),
88+
},
89+
];
90+
91+
const { getByPlaceholderText, getByText } = render(
92+
<MockedProvider mocks={errorMock} addTypename={false}>
93+
<SetNewPassword />
94+
</MockedProvider>
95+
);
96+
97+
fireEvent.changeText(getByPlaceholderText('Password'), 'newPassword123');
98+
fireEvent.changeText(getByPlaceholderText('Confirm Password'), 'newPassword123');
99+
100+
fireEvent.press(getByText('Continue'));
101+
102+
await waitFor(() =>
103+
expect(mockShowToast).toHaveBeenCalledWith('Failed to reset password', expect.anything())
104+
);
105+
});
106+
107+
test('submits the form with valid inputs', async () => {
108+
const { getByPlaceholderText, getByText } = render(
109+
<MockedProvider mocks={mocks} addTypename={false}>
110+
<SetNewPassword />
111+
</MockedProvider>
112+
);
113+
114+
fireEvent.changeText(getByPlaceholderText('Password'), 'newPassword123');
115+
fireEvent.changeText(getByPlaceholderText('Confirm Password'), 'newPassword123');
116+
117+
fireEvent.press(getByText('Continue'));
118+
119+
await waitFor(() =>
120+
expect(mockShowToast).toHaveBeenCalledWith(
121+
'You have successfully reset your password!',
122+
expect.anything()
123+
)
124+
);
125+
});
126+
});

Diff for: app/(onboarding)/index.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ export default function AppOnboarding() {
7979
</View>
8080
<View className={`flex-1 flex-row justify-center items-center ${bgColor}`}>
8181
<TouchableOpacity>
82-
<Text className="text-lg font-Inter-Medium" onPress={() => router.push('/auth/login')}>
82+
<Text className={`text-lg font-Inter-Medium ${textColor}`} onPress={() => router.push('/auth/login')}>
8383
Get Started
8484
</Text>
8585
</TouchableOpacity>

Diff for: app/_layout.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { Stack } from 'expo-router';
1616
import * as SplashScreen from 'expo-splash-screen';
1717
import { useEffect } from 'react';
1818
import 'react-native-reanimated';
19+
import { ToastProvider } from 'react-native-toast-notifications';
1920
export { ErrorBoundary } from 'expo-router';
2021

2122
export const unstable_settings = {
@@ -56,13 +57,15 @@ function RootLayoutNav() {
5657

5758
return (
5859
<ApolloProvider client={client}>
60+
<ToastProvider>
5961
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
6062
<Stack>
6163
<Stack.Screen name="(onboarding)" options={{ headerShown: false }} />
6264
<Stack.Screen name="auth" options={{ headerShown: false }} />
6365
<Stack.Screen name="dashboard" options={{ headerShown: false }}/>
6466
</Stack>
6567
</ThemeProvider>
68+
</ToastProvider>
6669
</ApolloProvider>
6770
);
6871
}

0 commit comments

Comments
 (0)