From 058817bf5f2a40a5c7587eb37679e5168b468dfa Mon Sep 17 00:00:00 2001 From: cyo23 Date: Thu, 18 Apr 2024 23:20:24 +1000 Subject: [PATCH 1/5] feat: implement reset password functionality and tests Implemented three components for the reset password functionality, added unit tests for each component, and configured the necessary routing. This ensures users can securely reset their passwords through a user-friendly interface. Resolve CP-28 --- src/containers/Login/components/LogInForm.tsx | 2 +- .../ResetPasswordInitiationPage.test.tsx | 58 ++++++++ .../ResetPasswordInitiationPage.tsx | 102 ++++++++++++++ .../components/PasswordResetSuccess.test.tsx | 42 ++++++ .../components/PasswordResetSuccess.tsx | 43 ++++++ .../components/ResetPasswordForm.test.tsx | 121 ++++++++++++++++ .../components/ResetPasswordForm.tsx | 133 ++++++++++++++++++ src/containers/ResetPassword/index.ts | 3 + src/routes/routeConfig.ts | 28 ++++ 9 files changed, 531 insertions(+), 1 deletion(-) create mode 100644 src/containers/ResetPassword/ResetPasswordInitiationPage.test.tsx create mode 100644 src/containers/ResetPassword/ResetPasswordInitiationPage.tsx create mode 100644 src/containers/ResetPassword/components/PasswordResetSuccess.test.tsx create mode 100644 src/containers/ResetPassword/components/PasswordResetSuccess.tsx create mode 100644 src/containers/ResetPassword/components/ResetPasswordForm.test.tsx create mode 100644 src/containers/ResetPassword/components/ResetPasswordForm.tsx create mode 100644 src/containers/ResetPassword/index.ts diff --git a/src/containers/Login/components/LogInForm.tsx b/src/containers/Login/components/LogInForm.tsx index acd2c14..7430050 100644 --- a/src/containers/Login/components/LogInForm.tsx +++ b/src/containers/Login/components/LogInForm.tsx @@ -90,7 +90,7 @@ const LogInForm = ({ onSubmit, error = '' }: LogInFormProps) => { defaultValue="" /> - Forgot a password? + Forgot a password? diff --git a/src/containers/ResetPassword/ResetPasswordInitiationPage.test.tsx b/src/containers/ResetPassword/ResetPasswordInitiationPage.test.tsx new file mode 100644 index 0000000..df56f94 --- /dev/null +++ b/src/containers/ResetPassword/ResetPasswordInitiationPage.test.tsx @@ -0,0 +1,58 @@ +import { screen, render, fireEvent } from '@testing-library/react'; +import { BrowserRouter as Router } from 'react-router-dom'; +import ResetPasswordInitiationPage from './ResetPasswordInitiationPage'; +import { act } from 'react-dom/test-utils'; + +jest.mock('styled-theming', () => ({ + default: jest.fn().mockImplementation((_, values) => values.mode), +})); + +jest.mock('mdi-react/AccountOutlineIcon', () => { + return { + __esModule: true, + default: () =>
Mocked Account Outline Icon
, + }; +}); + +describe('ResetPasswordInitiationPage', () => { + it('should render the page and show the correct title', async () => { + render( + + + + ); + + const title = screen.getByText(/Enter Your Email/i); + expect(title).toBeInTheDocument(); + }); + + it('should allow user to enter email', async () => { + render( + + + + ); + const input = screen.getByPlaceholderText('Email'); + + await act(async () => { + fireEvent.change(input, { target: { value: 'user@example.com' } }); + }); + expect(screen.getByDisplayValue('user@example.com')).toBeInTheDocument(); + }); + + it('should navigate to the reset password form page on submit', async () => { + render( + + + + ); + + const input = screen.getByPlaceholderText('Email'); + const button = screen.getByRole('button', { name: 'Submit' }); + + await act(async () => { + fireEvent.change(input, { target: { value: 'user@example.com' } }); + fireEvent.click(button); + }); + }); +}); diff --git a/src/containers/ResetPassword/ResetPasswordInitiationPage.tsx b/src/containers/ResetPassword/ResetPasswordInitiationPage.tsx new file mode 100644 index 0000000..c4d96a4 --- /dev/null +++ b/src/containers/ResetPassword/ResetPasswordInitiationPage.tsx @@ -0,0 +1,102 @@ +import { useHistory } from 'react-router-dom'; +import { + FormGroup, + FormGroupField, + FormGroupIcon, + FormGroupLabel, +} from '@/shared/components/form/FormElements'; + +import { + AccountWrap, + AccountContent, + AccountCard, + AccountHead, + AccountLogo, + AccountLogoAccent, + AccountTitle, + AccountButton, +} from '@/shared/components/account/AccountElements'; + +import FormField from '@/shared/components/form/FormHookField'; +import AccountOutlineIcon from 'mdi-react/AccountOutlineIcon'; +import { useForm } from 'react-hook-form'; +import { emailPatter } from '@/shared/utils/helpers'; +import { EMAIL } from '@/shared/constants/storage'; + +// Form data interface +interface FormData { + email: string; +} + +const ResetPassword = () => { + const { + control, + formState: { errors }, + handleSubmit, + } = useForm(); // 使用泛型确保表单数据类型正确 + const history = useHistory(); + + const onSubmit = (data: FormData) => { + // 使用类型安全的 FormData + console.log(data.email); // 假设这里是处理数据的地方 + history.push('/reset-password-form'); + }; + + return ( +
+ + Email + + + + + + + + {/* @ts-ignore - Ignoring because of complex union types that are not correctly inferred */} + + Submit + +
+ ); +}; + +const ResetPasswordInitiationPage = () => { + return ( + + + + + + Enter Your Email +
+ + BeeQuant + AI + +
+

Trading smart, trading with BeeQuant AI

+
+ +
+
+
+ ); +}; + +export default ResetPasswordInitiationPage; diff --git a/src/containers/ResetPassword/components/PasswordResetSuccess.test.tsx b/src/containers/ResetPassword/components/PasswordResetSuccess.test.tsx new file mode 100644 index 0000000..a42e8f5 --- /dev/null +++ b/src/containers/ResetPassword/components/PasswordResetSuccess.test.tsx @@ -0,0 +1,42 @@ +import { render, screen, fireEvent } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; +import { useHistory } from 'react-router-dom'; +import PasswordResetSuccess from './PasswordResetSuccess'; + +// Correctly mocking react-router-dom's useHistory hook +jest.mock('react-router-dom', () => { + const originalModule = jest.requireActual('react-router-dom'); + + return { + ...originalModule, + useHistory: jest.fn(), // Create a mock function for useHistory + }; +}); + +jest.mock('styled-theming', () => ({ + default: jest.fn().mockImplementation((_, values) => values.mode), +})); + +jest.mock('@/containers/Dashboard/components/DashboardCardElements', () => ({ + WidgetTrendingIconUp: () =>
Mocked WidgetTrendingIconUp
, +})); + +describe('PasswordResetSuccess', () => { + it('navigates back to login on button click', () => { + const mockPush = jest.fn(); + (useHistory as jest.Mock).mockReturnValue({ push: mockPush }); // Correct usage to mock return value + + render( + + + + ); + + // Find the button by role and click it + const backButton = screen.getByRole('button', { name: /back to login/i }); + fireEvent.click(backButton); + + // Expect the mock push method to have been called with '/login' + expect(mockPush).toHaveBeenCalledWith('/login'); + }); +}); diff --git a/src/containers/ResetPassword/components/PasswordResetSuccess.tsx b/src/containers/ResetPassword/components/PasswordResetSuccess.tsx new file mode 100644 index 0000000..90db3ce --- /dev/null +++ b/src/containers/ResetPassword/components/PasswordResetSuccess.tsx @@ -0,0 +1,43 @@ +import { useHistory } from 'react-router-dom'; +import { + AccountWrap, + AccountContent, + AccountCard, + AccountHead, + AccountLogo, + AccountLogoAccent, + AccountTitle, + AccountButton, +} from '@/shared/components/account/AccountElements'; +import successImage from '@/shared/img/success.png'; + +const PasswordResetSuccess = () => { + const history = useHistory(); + const handleBackToLogin = () => { + history.push('/login'); + }; + + return ( + + + + + + Success + Cool! +
+ Your password has been reset + +
+
+ {/* @ts-ignore - Ignoring because of complex union types that are not correctly inferred */} + + Back to Login + +
+
+
+ ); +}; + +export default PasswordResetSuccess; diff --git a/src/containers/ResetPassword/components/ResetPasswordForm.test.tsx b/src/containers/ResetPassword/components/ResetPasswordForm.test.tsx new file mode 100644 index 0000000..d6b8b46 --- /dev/null +++ b/src/containers/ResetPassword/components/ResetPasswordForm.test.tsx @@ -0,0 +1,121 @@ +import { screen, render, fireEvent } from '@testing-library/react'; +import { BrowserRouter as Router } from 'react-router-dom'; +import ResetPasswordForm from './ResetPasswordForm'; +import { act } from 'react-dom/test-utils'; + +jest.mock('styled-theming', () => ({ + default: jest.fn().mockImplementation((_, values) => values.mode), +})); + +interface PasswordFieldProps { + input: { + name: string; + value: string; + onChange: (event: React.ChangeEvent) => void; + }; + meta: { + touched: boolean; + error?: string; + }; + placeholder: string; + keyIcon: boolean; +} + +jest.mock('@/shared/components/form/Password', () => { + const PasswordFieldMock: React.FC = ({ + input, + meta, + placeholder, + keyIcon, + }) => ( +
+ + + {meta.touched && meta.error && {meta.error}} +
Mocked Eye Icon
+
+ ); + + return { + __esModule: true, + default: PasswordFieldMock, + }; +}); + +describe('ResetPasswordForm', () => { + it('should render the page and show the correct title', () => { + render( + + + + ); + const title = screen.getByText(/Reset Password/i); + expect(title).toBeInTheDocument(); + }); + + it('should allow user to enter and confirm password', async () => { + render( + + + + ); + + const passwordInput = screen.getByPlaceholderText('Password'); + const confirmPasswordInput = screen.getByPlaceholderText('Confirm Password'); + + fireEvent.change(passwordInput, { target: { value: 'Password@123' } }); + fireEvent.change(confirmPasswordInput, { target: { value: 'Password@123' } }); + + const passwordInputs = screen.getAllByDisplayValue('Password@123'); + + expect(passwordInputs.length).toBe(2); + passwordInputs.forEach((input) => { + expect(input).toBeInTheDocument(); + }); + }); + + it('should show error if passwords do not match', async () => { + render( + + + + ); + const passwordInput = screen.getByPlaceholderText('Password'); + const confirmPasswordInput = screen.getByPlaceholderText('Confirm Password'); + const submitButton = screen.getByRole('button', { name: 'Submit' }); + + await act(async () => { + fireEvent.change(passwordInput, { target: { value: 'Password@123' } }); + fireEvent.change(confirmPasswordInput, { target: { value: 'DifferentPassword@123' } }); + fireEvent.click(submitButton); + }); + + const alert = screen.queryByText(/Passwords must match/i); + expect(alert).toBeInTheDocument(); + }); + + it('should not navigate if the passwords do not match', async () => { + render( + + + + ); + const passwordInput = screen.getByPlaceholderText('Password'); + const confirmPasswordInput = screen.getByPlaceholderText('Confirm Password'); + const submitButton = screen.getByRole('button', { name: 'Submit' }); + + await act(async () => { + fireEvent.change(passwordInput, { target: { value: 'Password@123' } }); + fireEvent.change(confirmPasswordInput, { target: { value: 'DifferentPassword@123' } }); + fireEvent.click(submitButton); + }); + + expect(screen.queryByText(/Password updated/i)).not.toBeInTheDocument(); + }); +}); diff --git a/src/containers/ResetPassword/components/ResetPasswordForm.tsx b/src/containers/ResetPassword/components/ResetPasswordForm.tsx new file mode 100644 index 0000000..588ba2f --- /dev/null +++ b/src/containers/ResetPassword/components/ResetPasswordForm.tsx @@ -0,0 +1,133 @@ +import { useHistory } from 'react-router-dom'; +import { FormGroup, FormGroupField, FormGroupLabel } from '@/shared/components/form/FormElements'; +import { + AccountWrap, + AccountContent, + AccountCard, + AccountHead, + AccountLogo, + AccountLogoAccent, + AccountTitle, + AccountButton, +} from '@/shared/components/account/AccountElements'; +import { SubmitHandler, useForm, Controller } from 'react-hook-form'; +import PasswordField from '@/shared/components/form/Password'; + +interface FormValues { + password: string; + confirmPassword: string; +} + +const ResetPasswordForm = () => { + const { handleSubmit, control, watch } = useForm({ + mode: 'onSubmit', + }); + const history = useHistory(); + + const onSubmit: SubmitHandler = (data) => { + if (data.password !== data.confirmPassword) { + alert('Passwords do not match.'); + return; + } + + console.log('Password updated:', data.password); + history.push('/password-reset-success'); + }; + + return ( + + + + + + Reset Password +
+ + BeeQuant + AI + +
+

Secure your account with a new password

+
+
+ + Password + + ( + + )} + rules={{ + required: 'This is required field', + minLength: { + value: 8, + message: 'Password must be at least 8 characters long', + }, + maxLength: { + value: 32, + message: 'Password must be no more than 32 characters long', + }, + pattern: { + value: /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,32}$/, + message: + 'Password must contain letters, numbers, and at least one special character', + }, + }} + defaultValue="" + /> + + + + Confirm Password + + { + const { password } = watch(); + return password === value || 'Passwords must match'; + }, + }, + }} + render={({ field, fieldState }) => ( + + )} + defaultValue="" + /> + + + {/* @ts-ignore - Ignoring because of complex union types that are not correctly inferred */} + + Submit + +
+
+
+
+ ); +}; + +export default ResetPasswordForm; diff --git a/src/containers/ResetPassword/index.ts b/src/containers/ResetPassword/index.ts new file mode 100644 index 0000000..059d688 --- /dev/null +++ b/src/containers/ResetPassword/index.ts @@ -0,0 +1,3 @@ +export { default as ResetPasswordInitiationPage } from './ResetPasswordInitiationPage'; +export { default as PasswordResetLinkSentPage } from './components/PasswordResetSuccess'; +export { default as ResetPasswordForm } from './components/ResetPasswordForm'; diff --git a/src/routes/routeConfig.ts b/src/routes/routeConfig.ts index d016898..f6648bd 100644 --- a/src/routes/routeConfig.ts +++ b/src/routes/routeConfig.ts @@ -13,6 +13,9 @@ import ExchangeDetails from '@/containers/Cryptoeconomy/ExchangeDetails'; import PriceDetails from '@/containers/Cryptoeconomy/PriceDetails'; import BotDetail from '@/containers/Bot/BotDetails'; import BotCreate from '@/containers/Bot/BotCreate'; +import ResetPasswordInitiationPage from '@/containers/ResetPassword/ResetPasswordInitiationPage'; +import ResetPasswordForm from '@/containers/ResetPassword/components/ResetPasswordForm'; +import PasswordResetSuccess from '@/containers/ResetPassword/components/PasswordResetSuccess'; interface IRoute { path: string; @@ -30,6 +33,9 @@ export const ROUTE_KEY = { LOGIN: 'login', REGISTER: 'register', SETTINGS: 'settings', + RESET_PASSWORD_INITIATION: 'reset_password_initiation', + RESET_PASSWORD_FORM: 'reset_password_form', + PASSWORD_RESET_SUCCESS: 'password_reset_success', BOT_DASHBOARD: 'bot_dashboard', BOT_MANAGEMENT: 'bot_management', BOT_CREATE: 'bot_create', @@ -53,6 +59,28 @@ export const PUBLIC_ROUTE_CONFIG: Record = { title: 'Register - BeeQuant', component: Register, }, + + [ROUTE_KEY.RESET_PASSWORD_INITIATION]: { + path: '/reset-password-initiation', + name: 'Reset Password', + title: 'Reset Password - BeeQuant', + component: ResetPasswordInitiationPage, + }, + + [ROUTE_KEY.RESET_PASSWORD_FORM]: { + path: '/reset-password-form', + name: 'Reset Password Form', + title: 'Enter New Password - BeeQuant', + component: ResetPasswordForm, + }, + + [ROUTE_KEY.PASSWORD_RESET_SUCCESS]: { + path: '/password-reset-success', + name: 'Password Reset Success', + title: 'Password Reset Success - BeeQuant', + component: PasswordResetSuccess, + }, + [ROUTE_KEY.PAGE_404]: { path: '/404', name: '404', From ddd292ca0a3e6c799476f7e1929b6af5afc0ac3d Mon Sep 17 00:00:00 2001 From: cyo23 Date: Thu, 9 May 2024 00:21:04 +1000 Subject: [PATCH 2/5] Update router settings, modify login page, and remove reset password components --- src/containers/Login/components/LogInForm.tsx | 2 +- src/containers/ResetPasswor/index.ts | 3 + .../ResetPasswor/init/init.test.tsx | 78 ++++++++++ src/containers/ResetPasswor/init/init.tsx | 104 +++++++++++++ .../ResetPasswor/reset/form.test.tsx | 141 ++++++++++++++++++ src/containers/ResetPasswor/reset/form.tsx | 125 ++++++++++++++++ .../ResetPasswor/success/success.test.tsx | 42 ++++++ .../ResetPasswor/success/success.tsx | 43 ++++++ src/routes/routeConfig.ts | 14 +- 9 files changed, 544 insertions(+), 8 deletions(-) create mode 100644 src/containers/ResetPasswor/index.ts create mode 100644 src/containers/ResetPasswor/init/init.test.tsx create mode 100644 src/containers/ResetPasswor/init/init.tsx create mode 100644 src/containers/ResetPasswor/reset/form.test.tsx create mode 100644 src/containers/ResetPasswor/reset/form.tsx create mode 100644 src/containers/ResetPasswor/success/success.test.tsx create mode 100644 src/containers/ResetPasswor/success/success.tsx diff --git a/src/containers/Login/components/LogInForm.tsx b/src/containers/Login/components/LogInForm.tsx index 7430050..fad1150 100644 --- a/src/containers/Login/components/LogInForm.tsx +++ b/src/containers/Login/components/LogInForm.tsx @@ -90,7 +90,7 @@ const LogInForm = ({ onSubmit, error = '' }: LogInFormProps) => { defaultValue="" /> - Forgot a password? + Forgot a password? diff --git a/src/containers/ResetPasswor/index.ts b/src/containers/ResetPasswor/index.ts new file mode 100644 index 0000000..76ba176 --- /dev/null +++ b/src/containers/ResetPasswor/index.ts @@ -0,0 +1,3 @@ +export { default as ResetPasswordInitiationPage } from './init/init'; +export { default as PasswordResetLinkSentPage } from './success/success'; +export { default as ResetPasswordForm } from './reset/form'; diff --git a/src/containers/ResetPasswor/init/init.test.tsx b/src/containers/ResetPasswor/init/init.test.tsx new file mode 100644 index 0000000..f9a26c2 --- /dev/null +++ b/src/containers/ResetPasswor/init/init.test.tsx @@ -0,0 +1,78 @@ +import { screen, render, fireEvent } from '@testing-library/react'; +import { BrowserRouter as Router } from 'react-router-dom'; +import ResetPasswordInitiationPage from './init'; +import { act } from 'react-dom/test-utils'; + +jest.mock('styled-theming', () => ({ + default: jest.fn().mockImplementation((_, values) => values.mode), +})); + +interface IconProps { + className?: string; +} + +jest.mock('mdi-react/TrendingUpIcon', () => { + return { + __esModule: true, + default: ({ className }: IconProps) =>
Mocked Trending Up Icon
, + }; +}); + +jest.mock('mdi-react/TrendingDownIcon', () => { + return { + __esModule: true, + default: ({ className }: IconProps) => ( +
Mocked Trending Down Icon
+ ), + }; +}); + +jest.mock('mdi-react/AccountOutlineIcon', () => { + return { + __esModule: true, + default: () =>
Mocked Account Outline Icon
, + }; +}); + +describe('ResetPasswordInitiationPage', () => { + it('should render the page and show the correct title', async () => { + render( + + + + ); + + const title = screen.getByText(/Enter Your Email/i); + expect(title).toBeInTheDocument(); + }); + + it('should allow user to enter email', async () => { + render( + + + + ); + const input = screen.getByPlaceholderText('Email'); + + await act(async () => { + fireEvent.change(input, { target: { value: 'user@example.com' } }); + }); + expect(screen.getByDisplayValue('user@example.com')).toBeInTheDocument(); + }); + + it('should navigate to the reset password form page on submit', async () => { + render( + + + + ); + + const input = screen.getByPlaceholderText('Email'); + const button = screen.getByRole('button', { name: 'Submit' }); + + await act(async () => { + fireEvent.change(input, { target: { value: 'user@example.com' } }); + fireEvent.click(button); + }); + }); +}); diff --git a/src/containers/ResetPasswor/init/init.tsx b/src/containers/ResetPasswor/init/init.tsx new file mode 100644 index 0000000..7c4c204 --- /dev/null +++ b/src/containers/ResetPasswor/init/init.tsx @@ -0,0 +1,104 @@ +import { + FormGroup, + FormGroupField, + FormGroupIcon, + FormGroupLabel, +} from '@/shared/components/form/FormElements'; +import { ROUTE_KEY } from '@/routes/routeConfig'; +import { useState } from 'react'; +import { + AccountWrap, + AccountContent, + AccountCard, + AccountHead, + AccountLogo, + AccountLogoAccent, + AccountTitle, + AccountButton, +} from '@/shared/components/account/AccountElements'; +import { useGoTo } from '@/hooks/useGoTo'; +import FormField from '@/shared/components/form/FormHookField'; +import AccountOutlineIcon from 'mdi-react/AccountOutlineIcon'; +import { useForm } from 'react-hook-form'; +import { emailPatter } from '@/shared/utils/helpers'; +import { EMAIL } from '@/shared/constants/storage'; + +// Form data interface +interface FormData { + email: string; +} + +const ResetPassword = () => { + const [showEmailHint, setShowEmailHint] = useState(false); + const { + control, + formState: { errors }, + handleSubmit, + } = useForm(); + const { go } = useGoTo(); + + const onSubmit = () => { + go(ROUTE_KEY.RESET_PASSWORD_FORM); + }; + + return ( +
+ + Email + + + + + setShowEmailHint(true)} + onBlur={() => setShowEmailHint(false)} + /> + {showEmailHint &&
Please enter a valid email address.
} +
+
+ {/* @ts-ignore - Ignoring because of complex union types that are not correctly inferred */} + + Submit + +
+ ); +}; + +const ResetPasswordInitiationPage = () => { + return ( + + + + + + Enter Your Email +
+ + BeeQuant + AI + +
+

Trading smart, trading with BeeQuant AI

+
+ +
+
+
+ ); +}; + +export default ResetPasswordInitiationPage; diff --git a/src/containers/ResetPasswor/reset/form.test.tsx b/src/containers/ResetPasswor/reset/form.test.tsx new file mode 100644 index 0000000..dec5e1d --- /dev/null +++ b/src/containers/ResetPasswor/reset/form.test.tsx @@ -0,0 +1,141 @@ +import { screen, render, fireEvent } from '@testing-library/react'; +import { BrowserRouter as Router } from 'react-router-dom'; +import ResetPasswordForm from './form'; +import { act } from 'react-dom/test-utils'; + +jest.mock('styled-theming', () => ({ + default: jest.fn().mockImplementation((_, values) => values.mode), +})); + +interface IconProps { + className?: string; +} + +jest.mock('mdi-react/TrendingUpIcon', () => { + return { + __esModule: true, + default: ({ className }: IconProps) =>
Mocked Trending Up Icon
, + }; +}); + +jest.mock('mdi-react/TrendingDownIcon', () => { + return { + __esModule: true, + default: ({ className }: IconProps) => ( +
Mocked Trending Down Icon
+ ), + }; +}); + +interface PasswordFieldProps { + input: { + name: string; + value: string; + onChange: (event: React.ChangeEvent) => void; + }; + meta: { + touched: boolean; + error?: string; + }; + placeholder: string; + keyIcon: boolean; +} + +jest.mock('@/shared/components/form/Password', () => { + const PasswordFieldMock: React.FC = ({ + input, + meta, + placeholder, + keyIcon, + }) => ( +
+ + + {meta.touched && meta.error && {meta.error}} +
Mocked Eye Icon
+
+ ); + + return { + __esModule: true, + default: PasswordFieldMock, + }; +}); + +describe('ResetPasswordForm', () => { + it('should render the page and show the correct title', () => { + render( + + + + ); + const title = screen.getByText(/Reset Password/i); + expect(title).toBeInTheDocument(); + }); + + it('should allow user to enter and confirm password', async () => { + render( + + + + ); + + const passwordInput = screen.getByPlaceholderText('Password'); + const confirmPasswordInput = screen.getByPlaceholderText('Confirm Password'); + + fireEvent.change(passwordInput, { target: { value: 'Password@123' } }); + fireEvent.change(confirmPasswordInput, { target: { value: 'Password@123' } }); + + const passwordInputs = screen.getAllByDisplayValue('Password@123'); + + expect(passwordInputs.length).toBe(2); + passwordInputs.forEach((input) => { + expect(input).toBeInTheDocument(); + }); + }); + + it('should show error if passwords do not match', async () => { + render( + + + + ); + const passwordInput = screen.getByPlaceholderText('Password'); + const confirmPasswordInput = screen.getByPlaceholderText('Confirm Password'); + const submitButton = screen.getByRole('button', { name: 'Submit' }); + + await act(async () => { + fireEvent.change(passwordInput, { target: { value: 'Password@123' } }); + fireEvent.change(confirmPasswordInput, { target: { value: 'DifferentPassword@123' } }); + fireEvent.click(submitButton); + }); + + const alert = screen.queryByText(/Passwords must match/i); + expect(alert).toBeInTheDocument(); + }); + + it('should not navigate if the passwords do not match', async () => { + render( + + + + ); + const passwordInput = screen.getByPlaceholderText('Password'); + const confirmPasswordInput = screen.getByPlaceholderText('Confirm Password'); + const submitButton = screen.getByRole('button', { name: 'Submit' }); + + await act(async () => { + fireEvent.change(passwordInput, { target: { value: 'Password@123' } }); + fireEvent.change(confirmPasswordInput, { target: { value: 'DifferentPassword@123' } }); + fireEvent.click(submitButton); + }); + + expect(screen.queryByText(/Password updated/i)).not.toBeInTheDocument(); + }); +}); diff --git a/src/containers/ResetPasswor/reset/form.tsx b/src/containers/ResetPasswor/reset/form.tsx new file mode 100644 index 0000000..14117c7 --- /dev/null +++ b/src/containers/ResetPasswor/reset/form.tsx @@ -0,0 +1,125 @@ +import { passwordPattern } from '@/shared/utils/helpers'; +import { useGoTo } from '@/hooks/useGoTo'; +import { FormGroup, FormGroupField, FormGroupLabel } from '@/shared/components/form/FormElements'; +import { + AccountWrap, + AccountContent, + AccountCard, + AccountHead, + AccountLogo, + AccountLogoAccent, + AccountTitle, + AccountButton, +} from '@/shared/components/account/AccountElements'; +import { SubmitHandler, useForm, Controller } from 'react-hook-form'; +import PasswordField from '@/shared/components/form/Password'; +import { ROUTE_KEY } from '@/routes/routeConfig'; + +interface FormValues { + password: string; + confirmPassword: string; +} + +const ResetPasswordForm = () => { + const { handleSubmit, control, watch } = useForm({ + mode: 'onSubmit', + }); + const { go } = useGoTo(); + + const onSubmit: SubmitHandler = (data) => { + if (data.password !== data.confirmPassword) { + alert('Passwords do not match.'); + return; + } + go(ROUTE_KEY.PASSWORD_RESET_SUCCESS); + }; + + return ( + + + + + + Reset Password +
+ + BeeQuant + AI + +
+

Secure your account with a new password

+
+
+ + Password + + ( + + )} + defaultValue="" + /> + + + + Confirm Password + + { + const { password } = watch(); + return password === value || 'Passwords must match'; + }, + }, + }} + render={({ field, fieldState }) => ( + + )} + defaultValue="" + /> + + + {/* @ts-ignore - Ignoring because of complex union types that are not correctly inferred */} + + Submit + +
+
+
+
+ ); +}; + +export default ResetPasswordForm; diff --git a/src/containers/ResetPasswor/success/success.test.tsx b/src/containers/ResetPasswor/success/success.test.tsx new file mode 100644 index 0000000..d450bf0 --- /dev/null +++ b/src/containers/ResetPasswor/success/success.test.tsx @@ -0,0 +1,42 @@ +import { render, screen, fireEvent } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; +import { useHistory } from 'react-router-dom'; +import PasswordResetSuccess from './success'; + +// Correctly mocking react-router-dom's useHistory hook +jest.mock('react-router-dom', () => { + const originalModule = jest.requireActual('react-router-dom'); + + return { + ...originalModule, + useHistory: jest.fn(), // Create a mock function for useHistory + }; +}); + +jest.mock('styled-theming', () => ({ + default: jest.fn().mockImplementation((_, values) => values.mode), +})); + +jest.mock('@/containers/Dashboard/components/DashboardCardElements', () => ({ + WidgetTrendingIconUp: () =>
Mocked WidgetTrendingIconUp
, +})); + +describe('PasswordResetSuccess', () => { + it('navigates back to login on button click', () => { + const mockPush = jest.fn(); + (useHistory as jest.Mock).mockReturnValue({ push: mockPush }); // Correct usage to mock return value + + render( + + + + ); + + // Find the button by role and click it + const backButton = screen.getByRole('button', { name: /back to login/i }); + fireEvent.click(backButton); + + // Expect the mock push method to have been called with '/login' + expect(mockPush).toHaveBeenCalledWith('/login'); + }); +}); diff --git a/src/containers/ResetPasswor/success/success.tsx b/src/containers/ResetPasswor/success/success.tsx new file mode 100644 index 0000000..90db3ce --- /dev/null +++ b/src/containers/ResetPasswor/success/success.tsx @@ -0,0 +1,43 @@ +import { useHistory } from 'react-router-dom'; +import { + AccountWrap, + AccountContent, + AccountCard, + AccountHead, + AccountLogo, + AccountLogoAccent, + AccountTitle, + AccountButton, +} from '@/shared/components/account/AccountElements'; +import successImage from '@/shared/img/success.png'; + +const PasswordResetSuccess = () => { + const history = useHistory(); + const handleBackToLogin = () => { + history.push('/login'); + }; + + return ( + + + + + + Success + Cool! +
+ Your password has been reset + +
+
+ {/* @ts-ignore - Ignoring because of complex union types that are not correctly inferred */} + + Back to Login + +
+
+
+ ); +}; + +export default PasswordResetSuccess; diff --git a/src/routes/routeConfig.ts b/src/routes/routeConfig.ts index f6648bd..01876fa 100644 --- a/src/routes/routeConfig.ts +++ b/src/routes/routeConfig.ts @@ -13,9 +13,9 @@ import ExchangeDetails from '@/containers/Cryptoeconomy/ExchangeDetails'; import PriceDetails from '@/containers/Cryptoeconomy/PriceDetails'; import BotDetail from '@/containers/Bot/BotDetails'; import BotCreate from '@/containers/Bot/BotCreate'; -import ResetPasswordInitiationPage from '@/containers/ResetPassword/ResetPasswordInitiationPage'; -import ResetPasswordForm from '@/containers/ResetPassword/components/ResetPasswordForm'; -import PasswordResetSuccess from '@/containers/ResetPassword/components/PasswordResetSuccess'; +import ResetPasswordInitiationPage from '@/containers/ResetPasswor/init/init'; +import ResetPasswordForm from '@/containers/ResetPasswor/reset/form'; +import PasswordResetLinkSentPage from '@/containers/ResetPasswor/success/success'; interface IRoute { path: string; @@ -61,24 +61,24 @@ export const PUBLIC_ROUTE_CONFIG: Record = { }, [ROUTE_KEY.RESET_PASSWORD_INITIATION]: { - path: '/reset-password-initiation', + path: '/password/reset/initiate', name: 'Reset Password', title: 'Reset Password - BeeQuant', component: ResetPasswordInitiationPage, }, [ROUTE_KEY.RESET_PASSWORD_FORM]: { - path: '/reset-password-form', + path: '/password/reset/form', name: 'Reset Password Form', title: 'Enter New Password - BeeQuant', component: ResetPasswordForm, }, [ROUTE_KEY.PASSWORD_RESET_SUCCESS]: { - path: '/password-reset-success', + path: '/password/reset/success', name: 'Password Reset Success', title: 'Password Reset Success - BeeQuant', - component: PasswordResetSuccess, + component: PasswordResetLinkSentPage, }, [ROUTE_KEY.PAGE_404]: { From 835dde1055803bb1740cc88fd80a74abbda450b2 Mon Sep 17 00:00:00 2001 From: cyo23 Date: Sat, 11 May 2024 18:48:46 +1000 Subject: [PATCH 3/5] merge all the commit together --- .../ResetPasswordInitiationPage.test.tsx | 58 -------- .../ResetPasswordInitiationPage.tsx | 102 -------------- .../components/PasswordResetSuccess.test.tsx | 42 ------ .../components/PasswordResetSuccess.tsx | 43 ------ .../components/ResetPasswordForm.test.tsx | 121 ---------------- .../components/ResetPasswordForm.tsx | 133 ------------------ src/containers/ResetPassword/index.ts | 3 - src/shared/utils/helpers.ts | 1 + 8 files changed, 1 insertion(+), 502 deletions(-) delete mode 100644 src/containers/ResetPassword/ResetPasswordInitiationPage.test.tsx delete mode 100644 src/containers/ResetPassword/ResetPasswordInitiationPage.tsx delete mode 100644 src/containers/ResetPassword/components/PasswordResetSuccess.test.tsx delete mode 100644 src/containers/ResetPassword/components/PasswordResetSuccess.tsx delete mode 100644 src/containers/ResetPassword/components/ResetPasswordForm.test.tsx delete mode 100644 src/containers/ResetPassword/components/ResetPasswordForm.tsx delete mode 100644 src/containers/ResetPassword/index.ts diff --git a/src/containers/ResetPassword/ResetPasswordInitiationPage.test.tsx b/src/containers/ResetPassword/ResetPasswordInitiationPage.test.tsx deleted file mode 100644 index df56f94..0000000 --- a/src/containers/ResetPassword/ResetPasswordInitiationPage.test.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { screen, render, fireEvent } from '@testing-library/react'; -import { BrowserRouter as Router } from 'react-router-dom'; -import ResetPasswordInitiationPage from './ResetPasswordInitiationPage'; -import { act } from 'react-dom/test-utils'; - -jest.mock('styled-theming', () => ({ - default: jest.fn().mockImplementation((_, values) => values.mode), -})); - -jest.mock('mdi-react/AccountOutlineIcon', () => { - return { - __esModule: true, - default: () =>
Mocked Account Outline Icon
, - }; -}); - -describe('ResetPasswordInitiationPage', () => { - it('should render the page and show the correct title', async () => { - render( - - - - ); - - const title = screen.getByText(/Enter Your Email/i); - expect(title).toBeInTheDocument(); - }); - - it('should allow user to enter email', async () => { - render( - - - - ); - const input = screen.getByPlaceholderText('Email'); - - await act(async () => { - fireEvent.change(input, { target: { value: 'user@example.com' } }); - }); - expect(screen.getByDisplayValue('user@example.com')).toBeInTheDocument(); - }); - - it('should navigate to the reset password form page on submit', async () => { - render( - - - - ); - - const input = screen.getByPlaceholderText('Email'); - const button = screen.getByRole('button', { name: 'Submit' }); - - await act(async () => { - fireEvent.change(input, { target: { value: 'user@example.com' } }); - fireEvent.click(button); - }); - }); -}); diff --git a/src/containers/ResetPassword/ResetPasswordInitiationPage.tsx b/src/containers/ResetPassword/ResetPasswordInitiationPage.tsx deleted file mode 100644 index c4d96a4..0000000 --- a/src/containers/ResetPassword/ResetPasswordInitiationPage.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import { useHistory } from 'react-router-dom'; -import { - FormGroup, - FormGroupField, - FormGroupIcon, - FormGroupLabel, -} from '@/shared/components/form/FormElements'; - -import { - AccountWrap, - AccountContent, - AccountCard, - AccountHead, - AccountLogo, - AccountLogoAccent, - AccountTitle, - AccountButton, -} from '@/shared/components/account/AccountElements'; - -import FormField from '@/shared/components/form/FormHookField'; -import AccountOutlineIcon from 'mdi-react/AccountOutlineIcon'; -import { useForm } from 'react-hook-form'; -import { emailPatter } from '@/shared/utils/helpers'; -import { EMAIL } from '@/shared/constants/storage'; - -// Form data interface -interface FormData { - email: string; -} - -const ResetPassword = () => { - const { - control, - formState: { errors }, - handleSubmit, - } = useForm(); // 使用泛型确保表单数据类型正确 - const history = useHistory(); - - const onSubmit = (data: FormData) => { - // 使用类型安全的 FormData - console.log(data.email); // 假设这里是处理数据的地方 - history.push('/reset-password-form'); - }; - - return ( -
- - Email - - - - - - - - {/* @ts-ignore - Ignoring because of complex union types that are not correctly inferred */} - - Submit - -
- ); -}; - -const ResetPasswordInitiationPage = () => { - return ( - - - - - - Enter Your Email -
- - BeeQuant - AI - -
-

Trading smart, trading with BeeQuant AI

-
- -
-
-
- ); -}; - -export default ResetPasswordInitiationPage; diff --git a/src/containers/ResetPassword/components/PasswordResetSuccess.test.tsx b/src/containers/ResetPassword/components/PasswordResetSuccess.test.tsx deleted file mode 100644 index a42e8f5..0000000 --- a/src/containers/ResetPassword/components/PasswordResetSuccess.test.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { render, screen, fireEvent } from '@testing-library/react'; -import { MemoryRouter } from 'react-router-dom'; -import { useHistory } from 'react-router-dom'; -import PasswordResetSuccess from './PasswordResetSuccess'; - -// Correctly mocking react-router-dom's useHistory hook -jest.mock('react-router-dom', () => { - const originalModule = jest.requireActual('react-router-dom'); - - return { - ...originalModule, - useHistory: jest.fn(), // Create a mock function for useHistory - }; -}); - -jest.mock('styled-theming', () => ({ - default: jest.fn().mockImplementation((_, values) => values.mode), -})); - -jest.mock('@/containers/Dashboard/components/DashboardCardElements', () => ({ - WidgetTrendingIconUp: () =>
Mocked WidgetTrendingIconUp
, -})); - -describe('PasswordResetSuccess', () => { - it('navigates back to login on button click', () => { - const mockPush = jest.fn(); - (useHistory as jest.Mock).mockReturnValue({ push: mockPush }); // Correct usage to mock return value - - render( - - - - ); - - // Find the button by role and click it - const backButton = screen.getByRole('button', { name: /back to login/i }); - fireEvent.click(backButton); - - // Expect the mock push method to have been called with '/login' - expect(mockPush).toHaveBeenCalledWith('/login'); - }); -}); diff --git a/src/containers/ResetPassword/components/PasswordResetSuccess.tsx b/src/containers/ResetPassword/components/PasswordResetSuccess.tsx deleted file mode 100644 index 90db3ce..0000000 --- a/src/containers/ResetPassword/components/PasswordResetSuccess.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { useHistory } from 'react-router-dom'; -import { - AccountWrap, - AccountContent, - AccountCard, - AccountHead, - AccountLogo, - AccountLogoAccent, - AccountTitle, - AccountButton, -} from '@/shared/components/account/AccountElements'; -import successImage from '@/shared/img/success.png'; - -const PasswordResetSuccess = () => { - const history = useHistory(); - const handleBackToLogin = () => { - history.push('/login'); - }; - - return ( - - - - - - Success - Cool! -
- Your password has been reset - -
-
- {/* @ts-ignore - Ignoring because of complex union types that are not correctly inferred */} - - Back to Login - -
-
-
- ); -}; - -export default PasswordResetSuccess; diff --git a/src/containers/ResetPassword/components/ResetPasswordForm.test.tsx b/src/containers/ResetPassword/components/ResetPasswordForm.test.tsx deleted file mode 100644 index d6b8b46..0000000 --- a/src/containers/ResetPassword/components/ResetPasswordForm.test.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import { screen, render, fireEvent } from '@testing-library/react'; -import { BrowserRouter as Router } from 'react-router-dom'; -import ResetPasswordForm from './ResetPasswordForm'; -import { act } from 'react-dom/test-utils'; - -jest.mock('styled-theming', () => ({ - default: jest.fn().mockImplementation((_, values) => values.mode), -})); - -interface PasswordFieldProps { - input: { - name: string; - value: string; - onChange: (event: React.ChangeEvent) => void; - }; - meta: { - touched: boolean; - error?: string; - }; - placeholder: string; - keyIcon: boolean; -} - -jest.mock('@/shared/components/form/Password', () => { - const PasswordFieldMock: React.FC = ({ - input, - meta, - placeholder, - keyIcon, - }) => ( -
- - - {meta.touched && meta.error && {meta.error}} -
Mocked Eye Icon
-
- ); - - return { - __esModule: true, - default: PasswordFieldMock, - }; -}); - -describe('ResetPasswordForm', () => { - it('should render the page and show the correct title', () => { - render( - - - - ); - const title = screen.getByText(/Reset Password/i); - expect(title).toBeInTheDocument(); - }); - - it('should allow user to enter and confirm password', async () => { - render( - - - - ); - - const passwordInput = screen.getByPlaceholderText('Password'); - const confirmPasswordInput = screen.getByPlaceholderText('Confirm Password'); - - fireEvent.change(passwordInput, { target: { value: 'Password@123' } }); - fireEvent.change(confirmPasswordInput, { target: { value: 'Password@123' } }); - - const passwordInputs = screen.getAllByDisplayValue('Password@123'); - - expect(passwordInputs.length).toBe(2); - passwordInputs.forEach((input) => { - expect(input).toBeInTheDocument(); - }); - }); - - it('should show error if passwords do not match', async () => { - render( - - - - ); - const passwordInput = screen.getByPlaceholderText('Password'); - const confirmPasswordInput = screen.getByPlaceholderText('Confirm Password'); - const submitButton = screen.getByRole('button', { name: 'Submit' }); - - await act(async () => { - fireEvent.change(passwordInput, { target: { value: 'Password@123' } }); - fireEvent.change(confirmPasswordInput, { target: { value: 'DifferentPassword@123' } }); - fireEvent.click(submitButton); - }); - - const alert = screen.queryByText(/Passwords must match/i); - expect(alert).toBeInTheDocument(); - }); - - it('should not navigate if the passwords do not match', async () => { - render( - - - - ); - const passwordInput = screen.getByPlaceholderText('Password'); - const confirmPasswordInput = screen.getByPlaceholderText('Confirm Password'); - const submitButton = screen.getByRole('button', { name: 'Submit' }); - - await act(async () => { - fireEvent.change(passwordInput, { target: { value: 'Password@123' } }); - fireEvent.change(confirmPasswordInput, { target: { value: 'DifferentPassword@123' } }); - fireEvent.click(submitButton); - }); - - expect(screen.queryByText(/Password updated/i)).not.toBeInTheDocument(); - }); -}); diff --git a/src/containers/ResetPassword/components/ResetPasswordForm.tsx b/src/containers/ResetPassword/components/ResetPasswordForm.tsx deleted file mode 100644 index 588ba2f..0000000 --- a/src/containers/ResetPassword/components/ResetPasswordForm.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import { useHistory } from 'react-router-dom'; -import { FormGroup, FormGroupField, FormGroupLabel } from '@/shared/components/form/FormElements'; -import { - AccountWrap, - AccountContent, - AccountCard, - AccountHead, - AccountLogo, - AccountLogoAccent, - AccountTitle, - AccountButton, -} from '@/shared/components/account/AccountElements'; -import { SubmitHandler, useForm, Controller } from 'react-hook-form'; -import PasswordField from '@/shared/components/form/Password'; - -interface FormValues { - password: string; - confirmPassword: string; -} - -const ResetPasswordForm = () => { - const { handleSubmit, control, watch } = useForm({ - mode: 'onSubmit', - }); - const history = useHistory(); - - const onSubmit: SubmitHandler = (data) => { - if (data.password !== data.confirmPassword) { - alert('Passwords do not match.'); - return; - } - - console.log('Password updated:', data.password); - history.push('/password-reset-success'); - }; - - return ( - - - - - - Reset Password -
- - BeeQuant - AI - -
-

Secure your account with a new password

-
-
- - Password - - ( - - )} - rules={{ - required: 'This is required field', - minLength: { - value: 8, - message: 'Password must be at least 8 characters long', - }, - maxLength: { - value: 32, - message: 'Password must be no more than 32 characters long', - }, - pattern: { - value: /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,32}$/, - message: - 'Password must contain letters, numbers, and at least one special character', - }, - }} - defaultValue="" - /> - - - - Confirm Password - - { - const { password } = watch(); - return password === value || 'Passwords must match'; - }, - }, - }} - render={({ field, fieldState }) => ( - - )} - defaultValue="" - /> - - - {/* @ts-ignore - Ignoring because of complex union types that are not correctly inferred */} - - Submit - -
-
-
-
- ); -}; - -export default ResetPasswordForm; diff --git a/src/containers/ResetPassword/index.ts b/src/containers/ResetPassword/index.ts deleted file mode 100644 index 059d688..0000000 --- a/src/containers/ResetPassword/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { default as ResetPasswordInitiationPage } from './ResetPasswordInitiationPage'; -export { default as PasswordResetLinkSentPage } from './components/PasswordResetSuccess'; -export { default as ResetPasswordForm } from './components/ResetPasswordForm'; diff --git a/src/shared/utils/helpers.ts b/src/shared/utils/helpers.ts index 26236f0..14a4e27 100644 --- a/src/shared/utils/helpers.ts +++ b/src/shared/utils/helpers.ts @@ -2,3 +2,4 @@ export const emailPatter = /^[\w-]+(\.[\w-]+)*@([a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*?\.[a-zA-Z]{2,6}|(\d{1,3}\.){3}\d{1,3})(:\d{4})?$/; export const urlPattern = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_.~#?&//=]*)/; +export const passwordPattern = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,32}$/; From 481cee5d983820d75128546022e885b3b34ac9a1 Mon Sep 17 00:00:00 2001 From: cyo23 Date: Thu, 13 Jun 2024 19:55:19 +1000 Subject: [PATCH 4/5] changed resetpasswor to next style --- .../(auth)/login/_components/LogInForm.tsx | 2 +- .../resetpasswor/_components}/form.test.tsx | 85 ++++++------ .../(auth)/resetpasswor/_components/form.tsx | 103 +++++++++++++++ .../resetpasswor/_components/success.test.tsx | 34 +++++ .../resetpasswor/_components/success.tsx | 36 +++++ .../(auth)/resetpasswor/page.test.tsx} | 48 ++++++- src/app/(auth)/resetpasswor/page.tsx | 119 +++++++++++++++++ src/containers/ResetPasswor/index.ts | 3 - src/containers/ResetPasswor/init/init.tsx | 104 --------------- src/containers/ResetPasswor/reset/form.tsx | 125 ------------------ .../ResetPasswor/success/success.test.tsx | 42 ------ .../ResetPasswor/success/success.tsx | 43 ------ src/graphql/auth.ts | 10 ++ src/routes/routeConfig.ts | 21 +-- 14 files changed, 401 insertions(+), 374 deletions(-) rename src/{containers/ResetPasswor/reset => app/(auth)/resetpasswor/_components}/form.test.tsx (61%) create mode 100644 src/app/(auth)/resetpasswor/_components/form.tsx create mode 100644 src/app/(auth)/resetpasswor/_components/success.test.tsx create mode 100644 src/app/(auth)/resetpasswor/_components/success.tsx rename src/{containers/ResetPasswor/init/init.test.tsx => app/(auth)/resetpasswor/page.test.tsx} (61%) create mode 100644 src/app/(auth)/resetpasswor/page.tsx delete mode 100644 src/containers/ResetPasswor/index.ts delete mode 100644 src/containers/ResetPasswor/init/init.tsx delete mode 100644 src/containers/ResetPasswor/reset/form.tsx delete mode 100644 src/containers/ResetPasswor/success/success.test.tsx delete mode 100644 src/containers/ResetPasswor/success/success.tsx diff --git a/src/app/(auth)/login/_components/LogInForm.tsx b/src/app/(auth)/login/_components/LogInForm.tsx index 6b936f4..ea89e11 100644 --- a/src/app/(auth)/login/_components/LogInForm.tsx +++ b/src/app/(auth)/login/_components/LogInForm.tsx @@ -95,7 +95,7 @@ const LogInForm = ({ onSubmit, error = '' }: LogInFormProps) => { defaultValue="" /> - Forgot a password? + Forgot a password? diff --git a/src/containers/ResetPasswor/reset/form.test.tsx b/src/app/(auth)/resetpasswor/_components/form.test.tsx similarity index 61% rename from src/containers/ResetPasswor/reset/form.test.tsx rename to src/app/(auth)/resetpasswor/_components/form.test.tsx index dec5e1d..a6c630f 100644 --- a/src/containers/ResetPasswor/reset/form.test.tsx +++ b/src/app/(auth)/resetpasswor/_components/form.test.tsx @@ -1,32 +1,8 @@ import { screen, render, fireEvent } from '@testing-library/react'; -import { BrowserRouter as Router } from 'react-router-dom'; +import { MemoryRouter as Router } from 'react-router-dom'; import ResetPasswordForm from './form'; import { act } from 'react-dom/test-utils'; -jest.mock('styled-theming', () => ({ - default: jest.fn().mockImplementation((_, values) => values.mode), -})); - -interface IconProps { - className?: string; -} - -jest.mock('mdi-react/TrendingUpIcon', () => { - return { - __esModule: true, - default: ({ className }: IconProps) =>
Mocked Trending Up Icon
, - }; -}); - -jest.mock('mdi-react/TrendingDownIcon', () => { - return { - __esModule: true, - default: ({ className }: IconProps) => ( -
Mocked Trending Down Icon
- ), - }; -}); - interface PasswordFieldProps { input: { name: string; @@ -49,9 +25,10 @@ jest.mock('@/shared/components/form/Password', () => { keyIcon, }) => (
- + { }); describe('ResetPasswordForm', () => { - it('should render the page and show the correct title', () => { + const mockOnSuccess = jest.fn(); + + it('should render the form and show password fields', () => { render( - + ); - const title = screen.getByText(/Reset Password/i); - expect(title).toBeInTheDocument(); + + const passwordInput = screen.getByPlaceholderText('Password'); + const confirmPasswordInput = screen.getByPlaceholderText('Confirm Password'); + + expect(passwordInput).toBeInTheDocument(); + expect(confirmPasswordInput).toBeInTheDocument(); }); it('should allow user to enter and confirm password', async () => { render( - + ); @@ -92,20 +75,17 @@ describe('ResetPasswordForm', () => { fireEvent.change(passwordInput, { target: { value: 'Password@123' } }); fireEvent.change(confirmPasswordInput, { target: { value: 'Password@123' } }); - const passwordInputs = screen.getAllByDisplayValue('Password@123'); - - expect(passwordInputs.length).toBe(2); - passwordInputs.forEach((input) => { - expect(input).toBeInTheDocument(); - }); + expect(passwordInput).toHaveValue('Password@123'); + expect(confirmPasswordInput).toHaveValue('Password@123'); }); it('should show error if passwords do not match', async () => { render( - + ); + const passwordInput = screen.getByPlaceholderText('Password'); const confirmPasswordInput = screen.getByPlaceholderText('Confirm Password'); const submitButton = screen.getByRole('button', { name: 'Submit' }); @@ -120,12 +100,13 @@ describe('ResetPasswordForm', () => { expect(alert).toBeInTheDocument(); }); - it('should not navigate if the passwords do not match', async () => { + it('should not call onSuccess if the passwords do not match', async () => { render( - + ); + const passwordInput = screen.getByPlaceholderText('Password'); const confirmPasswordInput = screen.getByPlaceholderText('Confirm Password'); const submitButton = screen.getByRole('button', { name: 'Submit' }); @@ -136,6 +117,26 @@ describe('ResetPasswordForm', () => { fireEvent.click(submitButton); }); - expect(screen.queryByText(/Password updated/i)).not.toBeInTheDocument(); + expect(mockOnSuccess).not.toHaveBeenCalled(); + }); + + it('should call onSuccess if the passwords match', async () => { + render( + + + + ); + + const passwordInput = screen.getByPlaceholderText('Password'); + const confirmPasswordInput = screen.getByPlaceholderText('Confirm Password'); + const submitButton = screen.getByRole('button', { name: 'Submit' }); + + await act(async () => { + fireEvent.change(passwordInput, { target: { value: 'Password@123' } }); + fireEvent.change(confirmPasswordInput, { target: { value: 'Password@123' } }); + fireEvent.click(submitButton); + }); + + expect(mockOnSuccess).toHaveBeenCalled(); }); }); diff --git a/src/app/(auth)/resetpasswor/_components/form.tsx b/src/app/(auth)/resetpasswor/_components/form.tsx new file mode 100644 index 0000000..1f3ed3e --- /dev/null +++ b/src/app/(auth)/resetpasswor/_components/form.tsx @@ -0,0 +1,103 @@ +import { passwordPatten } from '@/shared/utils/helpers'; +import PasswordField from '@/shared/components/form/Password'; +import { useForm, Controller, SubmitHandler } from 'react-hook-form'; +import { FormGroup, FormGroupField, FormGroupLabel } from '@/shared/components/form/FormElements'; +import { AccountButton } from '@/shared/components/account/AccountElements'; +import Link from 'next/link'; + +interface FormValues { + password: string; + confirmPassword: string; +} + +interface ResetPasswordFormProps { + onSuccess: () => void; +} + +const ResetPasswordForm: React.FC = ({ onSuccess }) => { + const { handleSubmit, control, watch } = useForm({ + mode: 'onSubmit', + }); + + const onSubmit: SubmitHandler = (data) => { + if (data.password !== data.confirmPassword) { + alert('Passwords do not match.'); + return; + } + onSuccess(); + }; + + return ( +
+ + Password + + ( + + )} + defaultValue="" + /> + + + + Confirm Password + + { + const { password } = watch(); + return password === value || 'Passwords must match'; + }, + }, + }} + render={({ field, fieldState }) => ( + + )} + defaultValue="" + /> + + + + Submit + + + Back to Login + +
+ ); +}; + +export default ResetPasswordForm; diff --git a/src/app/(auth)/resetpasswor/_components/success.test.tsx b/src/app/(auth)/resetpasswor/_components/success.test.tsx new file mode 100644 index 0000000..4cfbae4 --- /dev/null +++ b/src/app/(auth)/resetpasswor/_components/success.test.tsx @@ -0,0 +1,34 @@ +import { render, screen } from '@testing-library/react'; +import ResetPasswordSuccess from '../_components/success'; +import React, { ReactNode } from 'react'; + +jest.mock('styled-theming', () => ({ + theme: jest.fn().mockImplementation((_, values) => values.mode), +})); + +jest.mock('@/shared/components/account/AccountElements', () => ({ + AccountContent: ({ children }: { children: ReactNode }) =>
{children}
, + AccountHead: ({ children }: { children: ReactNode }) =>
{children}
, + AccountLogo: ({ children }: { children: ReactNode }) =>
{children}
, + AccountLogoAccent: ({ children }: { children: ReactNode }) => ( +
{children}
+ ), + AccountTitle: ({ children }: { children: ReactNode }) =>
{children}
, + AccountButton: ({ children, ...props }: { children: ReactNode }) => ( + + ), +})); + +jest.mock('next/link', () => { + return ({ children, ...props }: { children: ReactNode }) => {children}; +}); + +describe('PasswordResetSuccess', () => { + it('navigates back to login on button click', () => { + render(); + + // Find the link by text and check its href attribute + const backButtonLink = screen.getByRole('link', { name: /back to login/i }); + expect(backButtonLink).toHaveAttribute('href', '/login'); + }); +}); diff --git a/src/app/(auth)/resetpasswor/_components/success.tsx b/src/app/(auth)/resetpasswor/_components/success.tsx new file mode 100644 index 0000000..d1fda14 --- /dev/null +++ b/src/app/(auth)/resetpasswor/_components/success.tsx @@ -0,0 +1,36 @@ +import { + AccountContent, + AccountHead, + AccountLogo, + AccountLogoAccent, + AccountTitle, + AccountButton, +} from '@/shared/components/account/AccountElements'; +import successImage from '@/shared/img/success.png'; +import Link from 'next/link'; + +const ResetPasswordSuccess = () => { + return ( +
+ Success +
+ + + + + Cool! + +
+ Your password has been reset +
+
+
+ + + Back to Login + +
+ ); +}; + +export default ResetPasswordSuccess; diff --git a/src/containers/ResetPasswor/init/init.test.tsx b/src/app/(auth)/resetpasswor/page.test.tsx similarity index 61% rename from src/containers/ResetPasswor/init/init.test.tsx rename to src/app/(auth)/resetpasswor/page.test.tsx index f9a26c2..a479090 100644 --- a/src/containers/ResetPasswor/init/init.test.tsx +++ b/src/app/(auth)/resetpasswor/page.test.tsx @@ -1,12 +1,8 @@ import { screen, render, fireEvent } from '@testing-library/react'; import { BrowserRouter as Router } from 'react-router-dom'; -import ResetPasswordInitiationPage from './init'; +import ResetPasswordInitiationPage from './page'; import { act } from 'react-dom/test-utils'; -jest.mock('styled-theming', () => ({ - default: jest.fn().mockImplementation((_, values) => values.mode), -})); - interface IconProps { className?: string; } @@ -34,6 +30,21 @@ jest.mock('mdi-react/AccountOutlineIcon', () => { }; }); +jest.mock('./_components/form', () => ({ + __esModule: true, + default: ({ onSuccess }: { onSuccess: () => void }) => ( +
+ Mocked Reset Password Form + +
+ ), +})); + +jest.mock('./_components/success', () => ({ + __esModule: true, + default: () =>
Mocked Reset Password Success
, +})); + describe('ResetPasswordInitiationPage', () => { it('should render the page and show the correct title', async () => { render( @@ -74,5 +85,32 @@ describe('ResetPasswordInitiationPage', () => { fireEvent.change(input, { target: { value: 'user@example.com' } }); fireEvent.click(button); }); + + const form = screen.getByText('Mocked Reset Password Form'); + expect(form).toBeInTheDocument(); + }); + + it('should navigate to the success page after form submission', async () => { + render( + + + + ); + + const input = screen.getByPlaceholderText('Email'); + const button = screen.getByRole('button', { name: 'Submit' }); + + await act(async () => { + fireEvent.change(input, { target: { value: 'user@example.com' } }); + fireEvent.click(button); + }); + + const formButton = screen.getByText('Mock Submit'); + await act(async () => { + fireEvent.click(formButton); + }); + + const successMessage = screen.getByText('Mocked Reset Password Success'); + expect(successMessage).toBeInTheDocument(); }); }); diff --git a/src/app/(auth)/resetpasswor/page.tsx b/src/app/(auth)/resetpasswor/page.tsx new file mode 100644 index 0000000..25362ed --- /dev/null +++ b/src/app/(auth)/resetpasswor/page.tsx @@ -0,0 +1,119 @@ +'use client'; + +import { useState, Suspense } from 'react'; +import { + AccountWrap, + AccountContent, + AccountCard, + AccountHead, + AccountLogo, + AccountLogoAccent, + AccountTitle, + AccountButton, +} from '@/shared/components/account/AccountElements'; +import { + FormGroup, + FormGroupField, + FormGroupIcon, + FormGroupLabel, +} from '@/shared/components/form/FormElements'; +import AccountOutlineIcon from 'mdi-react/AccountOutlineIcon'; +import FormField from '@/shared/components/form/FormHookField'; +import { useForm } from 'react-hook-form'; +import { emailPattern } from '@/shared/utils/helpers'; +import { EMAIL } from '@/shared/constants/storage'; +import Link from 'next/link'; +import dynamic from 'next/dynamic'; + +const ResetPasswordForm = dynamic(() => import('./_components/form'), { ssr: false }); +const ResetPasswordSuccess = dynamic(() => import('./_components/success'), { ssr: false }); + +interface FormData { + email: string; +} + +const ResetPasswordInitiationPage = () => { + const [showEmailHint, setShowEmailHint] = useState(false); + const [step, setStep] = useState('init'); // 使用状态控制显示的步骤 + const { + control, + formState: { errors }, + handleSubmit, + } = useForm(); + + const onSubmit = (data: FormData) => { + setStep('form'); + }; + + return ( + + + + {step !== 'success' && ( + + + {step === 'init' ? 'Enter Your Email' : 'Reset Password'} +
+ + BeeQuant + AI + +
+

Trading smart, trading with BeeQuant AI

+
+ )} + {step === 'init' ? ( +
+ + Email + + + + + setShowEmailHint(true)} + onBlur={() => setShowEmailHint(false)} + /> + {showEmailHint && ( +
Please enter a valid email address.
+ )} +
+
+ + Submit + + + + Back to Login + +
+ ) : step === 'form' ? ( + Loading...
}> + setStep('success')} /> + + ) : ( + Loading...}> + + + )} + + + + ); +}; + +export default ResetPasswordInitiationPage; diff --git a/src/containers/ResetPasswor/index.ts b/src/containers/ResetPasswor/index.ts deleted file mode 100644 index 76ba176..0000000 --- a/src/containers/ResetPasswor/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { default as ResetPasswordInitiationPage } from './init/init'; -export { default as PasswordResetLinkSentPage } from './success/success'; -export { default as ResetPasswordForm } from './reset/form'; diff --git a/src/containers/ResetPasswor/init/init.tsx b/src/containers/ResetPasswor/init/init.tsx deleted file mode 100644 index 7c4c204..0000000 --- a/src/containers/ResetPasswor/init/init.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { - FormGroup, - FormGroupField, - FormGroupIcon, - FormGroupLabel, -} from '@/shared/components/form/FormElements'; -import { ROUTE_KEY } from '@/routes/routeConfig'; -import { useState } from 'react'; -import { - AccountWrap, - AccountContent, - AccountCard, - AccountHead, - AccountLogo, - AccountLogoAccent, - AccountTitle, - AccountButton, -} from '@/shared/components/account/AccountElements'; -import { useGoTo } from '@/hooks/useGoTo'; -import FormField from '@/shared/components/form/FormHookField'; -import AccountOutlineIcon from 'mdi-react/AccountOutlineIcon'; -import { useForm } from 'react-hook-form'; -import { emailPatter } from '@/shared/utils/helpers'; -import { EMAIL } from '@/shared/constants/storage'; - -// Form data interface -interface FormData { - email: string; -} - -const ResetPassword = () => { - const [showEmailHint, setShowEmailHint] = useState(false); - const { - control, - formState: { errors }, - handleSubmit, - } = useForm(); - const { go } = useGoTo(); - - const onSubmit = () => { - go(ROUTE_KEY.RESET_PASSWORD_FORM); - }; - - return ( -
- - Email - - - - - setShowEmailHint(true)} - onBlur={() => setShowEmailHint(false)} - /> - {showEmailHint &&
Please enter a valid email address.
} -
-
- {/* @ts-ignore - Ignoring because of complex union types that are not correctly inferred */} - - Submit - -
- ); -}; - -const ResetPasswordInitiationPage = () => { - return ( - - - - - - Enter Your Email -
- - BeeQuant - AI - -
-

Trading smart, trading with BeeQuant AI

-
- -
-
-
- ); -}; - -export default ResetPasswordInitiationPage; diff --git a/src/containers/ResetPasswor/reset/form.tsx b/src/containers/ResetPasswor/reset/form.tsx deleted file mode 100644 index 14117c7..0000000 --- a/src/containers/ResetPasswor/reset/form.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import { passwordPattern } from '@/shared/utils/helpers'; -import { useGoTo } from '@/hooks/useGoTo'; -import { FormGroup, FormGroupField, FormGroupLabel } from '@/shared/components/form/FormElements'; -import { - AccountWrap, - AccountContent, - AccountCard, - AccountHead, - AccountLogo, - AccountLogoAccent, - AccountTitle, - AccountButton, -} from '@/shared/components/account/AccountElements'; -import { SubmitHandler, useForm, Controller } from 'react-hook-form'; -import PasswordField from '@/shared/components/form/Password'; -import { ROUTE_KEY } from '@/routes/routeConfig'; - -interface FormValues { - password: string; - confirmPassword: string; -} - -const ResetPasswordForm = () => { - const { handleSubmit, control, watch } = useForm({ - mode: 'onSubmit', - }); - const { go } = useGoTo(); - - const onSubmit: SubmitHandler = (data) => { - if (data.password !== data.confirmPassword) { - alert('Passwords do not match.'); - return; - } - go(ROUTE_KEY.PASSWORD_RESET_SUCCESS); - }; - - return ( - - - - - - Reset Password -
- - BeeQuant - AI - -
-

Secure your account with a new password

-
-
- - Password - - ( - - )} - defaultValue="" - /> - - - - Confirm Password - - { - const { password } = watch(); - return password === value || 'Passwords must match'; - }, - }, - }} - render={({ field, fieldState }) => ( - - )} - defaultValue="" - /> - - - {/* @ts-ignore - Ignoring because of complex union types that are not correctly inferred */} - - Submit - -
-
-
-
- ); -}; - -export default ResetPasswordForm; diff --git a/src/containers/ResetPasswor/success/success.test.tsx b/src/containers/ResetPasswor/success/success.test.tsx deleted file mode 100644 index d450bf0..0000000 --- a/src/containers/ResetPasswor/success/success.test.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { render, screen, fireEvent } from '@testing-library/react'; -import { MemoryRouter } from 'react-router-dom'; -import { useHistory } from 'react-router-dom'; -import PasswordResetSuccess from './success'; - -// Correctly mocking react-router-dom's useHistory hook -jest.mock('react-router-dom', () => { - const originalModule = jest.requireActual('react-router-dom'); - - return { - ...originalModule, - useHistory: jest.fn(), // Create a mock function for useHistory - }; -}); - -jest.mock('styled-theming', () => ({ - default: jest.fn().mockImplementation((_, values) => values.mode), -})); - -jest.mock('@/containers/Dashboard/components/DashboardCardElements', () => ({ - WidgetTrendingIconUp: () =>
Mocked WidgetTrendingIconUp
, -})); - -describe('PasswordResetSuccess', () => { - it('navigates back to login on button click', () => { - const mockPush = jest.fn(); - (useHistory as jest.Mock).mockReturnValue({ push: mockPush }); // Correct usage to mock return value - - render( - - - - ); - - // Find the button by role and click it - const backButton = screen.getByRole('button', { name: /back to login/i }); - fireEvent.click(backButton); - - // Expect the mock push method to have been called with '/login' - expect(mockPush).toHaveBeenCalledWith('/login'); - }); -}); diff --git a/src/containers/ResetPasswor/success/success.tsx b/src/containers/ResetPasswor/success/success.tsx deleted file mode 100644 index 90db3ce..0000000 --- a/src/containers/ResetPasswor/success/success.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { useHistory } from 'react-router-dom'; -import { - AccountWrap, - AccountContent, - AccountCard, - AccountHead, - AccountLogo, - AccountLogoAccent, - AccountTitle, - AccountButton, -} from '@/shared/components/account/AccountElements'; -import successImage from '@/shared/img/success.png'; - -const PasswordResetSuccess = () => { - const history = useHistory(); - const handleBackToLogin = () => { - history.push('/login'); - }; - - return ( - - - - - - Success - Cool! -
- Your password has been reset - -
-
- {/* @ts-ignore - Ignoring because of complex union types that are not correctly inferred */} - - Back to Login - -
-
-
- ); -}; - -export default PasswordResetSuccess; diff --git a/src/graphql/auth.ts b/src/graphql/auth.ts index d874837..a67147d 100644 --- a/src/graphql/auth.ts +++ b/src/graphql/auth.ts @@ -19,3 +19,13 @@ export const USER_REGISTER = gql` } } `; + +export const USER_RESETPASSWOR = gql` + mutation Resetpasswor($input: CreateUserInput!) { + resetpasswor(input: $input) { + code + message + data + } + } +`; diff --git a/src/routes/routeConfig.ts b/src/routes/routeConfig.ts index 4f04a13..e947641 100644 --- a/src/routes/routeConfig.ts +++ b/src/routes/routeConfig.ts @@ -13,6 +13,9 @@ import ExchangeDetails from 'app/(protected)/crypto/exchange/details/page'; import PriceDetails from 'app/(protected)/crypto/price/details/page'; import BotDetail from 'app/(protected)/bot/details/page'; import BotCreate from 'app/(protected)/bot/create/page'; +import ResetPassword from 'app/(auth)/resetpasswor/page'; +import ResetPasswordForm from 'app/(auth)/resetpasswor/_components/form'; +import PasswordResetSuccess from 'app/(auth)/resetpasswor/_components/success'; interface IRoute { path: string; @@ -30,7 +33,7 @@ export const ROUTE_KEY = { LOGIN: 'login', REGISTER: 'register', SETTINGS: 'settings', - RESET_PASSWORD_INITIATION: 'reset_password_initiation', + RESET_PASSWORD: 'resetpasswor', RESET_PASSWORD_FORM: 'reset_password_form', PASSWORD_RESET_SUCCESS: 'password_reset_success', BOT_DASHBOARD: 'bot_dashboard', @@ -57,25 +60,25 @@ export const PUBLIC_ROUTE_CONFIG: Record = { component: Register, }, - [ROUTE_KEY.RESET_PASSWORD_INITIATION]: { - path: '/password/reset/initiate', - name: 'Reset Password', - title: 'Reset Password - BeeQuant', - component: ResetPasswordInitiationPage, + [ROUTE_KEY.RESET_PASSWORD]: { + path: '/resetpasswor', + name: 'resetpasswor', + title: 'Register - BeeQuant', + component: ResetPassword, }, [ROUTE_KEY.RESET_PASSWORD_FORM]: { - path: '/password/reset/form', + path: '/resetpasswor/form', name: 'Reset Password Form', title: 'Enter New Password - BeeQuant', component: ResetPasswordForm, }, [ROUTE_KEY.PASSWORD_RESET_SUCCESS]: { - path: '/password/reset/success', + path: '/resetpasswor/success', name: 'Password Reset Success', title: 'Password Reset Success - BeeQuant', - component: PasswordResetLinkSentPage, + component: PasswordResetSuccess, }, [ROUTE_KEY.PAGE_404]: { From dc4a6bd29b7013f8fc1cecc02b5c2511a60e7a33 Mon Sep 17 00:00:00 2001 From: cyo23 Date: Thu, 13 Jun 2024 20:03:24 +1000 Subject: [PATCH 5/5] fixed test bugs --- src/app/(auth)/resetpasswor/_components/form.test.tsx | 2 +- src/app/(auth)/resetpasswor/page.test.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/(auth)/resetpasswor/_components/form.test.tsx b/src/app/(auth)/resetpasswor/_components/form.test.tsx index a6c630f..54fa3c9 100644 --- a/src/app/(auth)/resetpasswor/_components/form.test.tsx +++ b/src/app/(auth)/resetpasswor/_components/form.test.tsx @@ -1,7 +1,7 @@ +import { act } from 'react-dom/test-utils'; import { screen, render, fireEvent } from '@testing-library/react'; import { MemoryRouter as Router } from 'react-router-dom'; import ResetPasswordForm from './form'; -import { act } from 'react-dom/test-utils'; interface PasswordFieldProps { input: { diff --git a/src/app/(auth)/resetpasswor/page.test.tsx b/src/app/(auth)/resetpasswor/page.test.tsx index a479090..0267e48 100644 --- a/src/app/(auth)/resetpasswor/page.test.tsx +++ b/src/app/(auth)/resetpasswor/page.test.tsx @@ -1,7 +1,7 @@ +import { act } from 'react-dom/test-utils'; import { screen, render, fireEvent } from '@testing-library/react'; import { BrowserRouter as Router } from 'react-router-dom'; import ResetPasswordInitiationPage from './page'; -import { act } from 'react-dom/test-utils'; interface IconProps { className?: string;