@@ -15,61 +15,105 @@ import type { ReactElement, ReactNode } from "react"
1515import { useForm } from "react-hook-form"
1616import { type Mock , beforeEach , describe , expect , it , vi } from "vitest"
1717
18+ import type { ApiError } from "@/client"
1819import { loginLoginRouterRecoverPassword } from "@/client"
1920import { RecoverPassword } from "@/routes/recover-password"
2021import { emailPattern } from "@/utils"
2122
2223// region Mocks
2324
24- const mockShowApiErrorToast : Mock = vi . fn ( )
25+ // Mock useCustomToast to spy on its methods.
26+ const mockShowApiErrorToast : Mock = vi . fn ( ) // Simple spy mock
2527vi . mock ( "@/hooks/useCustomToast" , ( ) => ( {
26- default : ( ) => ( {
27- showApiErrorToast : mockShowApiErrorToast ,
28- } ) ,
28+ /**
29+ * Mock implementation of the useCustomToast hook.
30+ * @returns {object } An object with the mocked `showApiErrorToast` method.
31+ */
32+ default : ( ) : object => ( { showApiErrorToast : mockShowApiErrorToast } ) ,
2933} ) )
3034
35+ // Mock useAuth to always return a non-authenticated state.
3136vi . mock ( "@/hooks/useAuth" , ( ) => ( {
32- isLoggedIn : ( ) => false ,
37+ /**
38+ * Mock implementation of isLoggedIn.
39+ * @returns {boolean } Always returns false for testing purposes.
40+ */
41+ isLoggedIn : ( ) : boolean => false ,
3342} ) )
3443
44+ // Mock @tanstack /react-router to provide simplified implementations.
3545vi . mock ( "@tanstack/react-router" , async ( importOriginal ) => {
3646 const original = await importOriginal < Record < string , unknown > > ( )
3747 return {
3848 ...original ,
39- createFileRoute : ( ) => ( options : any ) => ( { ...options } ) ,
40- Link : ( { children, to, ...rest } : { children : ReactNode ; to : string ; [ key : string ] : any } ) => (
49+ /**
50+ * Mock for createFileRoute that returns the options object.
51+ * @param {string } _path - The route path (ignored in mock).
52+ * @returns {function(object): object } A function that returns its options.
53+ */
54+ createFileRoute :
55+ ( _path : string ) : ( ( arg0 : object ) => object ) =>
56+ ( options : any ) => ( { ...options } ) ,
57+ /**
58+ * Mock for the Link component that renders a simple anchor tag.
59+ * @param {object } props - The component props.
60+ * @returns {ReactElement } A mocked anchor tag.
61+ */
62+ Link : ( { children, to, ...rest } : { children : ReactNode ; to : string ; [ key : string ] : any } ) : ReactElement => (
4163 < a href = { to } { ...rest } >
4264 { children }
4365 </ a >
4466 ) ,
4567 }
4668} )
4769
70+ // Mock react-icons/fi for a simple icon placeholder.
4871vi . mock ( "react-icons/fi" , ( ) => ( {
72+ /**
73+ * Mock for the FiMail icon.
74+ * @returns {ReactElement } A span with placeholder text.
75+ */
4976 FiMail : ( ) : ReactElement => < span > mail-icon</ span > ,
5077} ) )
5178
79+ // Mock the API client module.
5280vi . mock ( "@/client" )
81+
82+ // Mock custom UI components to be simple, non-styled elements.
5383vi . mock ( "@/components/ui/button" , ( ) => ( {
54- Button : ( { children, loading, ...rest } : { children : ReactNode ; loading ?: boolean ; [ key : string ] : any } ) => (
84+ /**
85+ * Mock for the Button component.
86+ * @param {object } props - Component props.
87+ * @returns {ReactElement } A mocked button element.
88+ */
89+ Button : ( {
90+ children,
91+ loading,
92+ ...rest
93+ } : { children : ReactNode ; loading ?: boolean ; [ key : string ] : any } ) : ReactElement => (
5594 < button disabled = { loading } { ...rest } >
5695 { children }
5796 </ button >
5897 ) ,
5998} ) )
6099
61100vi . mock ( "@/components/ui/field" , ( ) => ( {
101+ /**
102+ * Mock for the Field component.
103+ * @param {object } props - Component props.
104+ * @returns {ReactElement } A mocked div element with optional error text.
105+ */
62106 Field : ( {
63107 children,
64108 errorText,
65- invalid,
109+ invalid, // Consume the 'invalid' prop to prevent React warnings.
66110 ...rest
67111 } : {
68112 children : ReactNode
69113 errorText ?: string
70114 invalid ?: boolean
71115 [ key : string ] : any
72- } ) => (
116+ } ) : ReactElement => (
73117 < div { ...rest } >
74118 { children }
75119 { errorText && < span > { errorText } </ span > }
@@ -78,15 +122,26 @@ vi.mock("@/components/ui/field", () => ({
78122} ) )
79123
80124vi . mock ( "@/components/ui/input-group" , ( ) => ( {
81- InputGroup : ( { children, startElement } : { children : ReactNode ; startElement ?: ReactNode } ) => (
125+ /**
126+ * Mock for the InputGroup component.
127+ * @param {object } props - Component props.
128+ * @returns {ReactElement } A mocked div element.
129+ */
130+ InputGroup : ( { children, startElement } : { children : ReactNode ; startElement ?: ReactNode } ) : ReactElement => (
82131 < div >
83132 { startElement }
84133 { children }
85134 </ div >
86135 ) ,
87136} ) )
88137
138+ // Mock Chakra UI components to be basic HTML elements and consume their specific props.
89139vi . mock ( "@chakra-ui/react" , ( ) => ( {
140+ /**
141+ * Mock for the Container component.
142+ * @param {object } props - Component props.
143+ * @returns {ReactElement } A mocked form element.
144+ */
90145 Container : ( {
91146 children,
92147 as,
@@ -100,141 +155,159 @@ vi.mock("@chakra-ui/react", () => ({
100155 } : {
101156 children : ReactNode
102157 [ key : string ] : any
103- } ) => < form { ...rest } > { children } </ form > ,
158+ } ) : ReactElement => < form { ...rest } > { children } </ form > ,
159+ /**
160+ * Mock for the Heading component.
161+ * @param {object } props - Component props.
162+ * @returns {ReactElement } A mocked h1 element.
163+ */
104164 Heading : ( {
105165 children,
106166 size,
107167 color,
108168 textAlign,
109169 mb,
110170 ...rest
111- } : {
112- children : ReactNode
113- [ key : string ] : any
114- } ) => < h1 { ... rest } > { children } </ h1 > ,
115- // ИЗМЕНЕНИЕ: Деструктурируем `textAlign` и `mt`, чтобы они не попадали в DOM .
116- Text : ( {
117- children,
118- textAlign ,
119- mt ,
120- ... rest
121- } : {
122- children : ReactNode
123- [ key : string ] : any
124- } ) => < p { ... rest } > { children } </ p > ,
125- Input : ( props : any ) => < input { ...props } /> ,
171+ } : { children : ReactNode ; [ key : string ] : any } ) : ReactElement => < h1 { ... rest } > { children } </ h1 > ,
172+ /**
173+ * Mock for the Text component.
174+ * @param { object } props - Component props.
175+ * @returns { ReactElement } A mocked p element .
176+ */
177+ Text : ( { children, textAlign , mt , ... rest } : { children : ReactNode ; [ key : string ] : any } ) : ReactElement => (
178+ < p { ... rest } > { children } </ p >
179+ ) ,
180+ /**
181+ * Mock for the Input component.
182+ * @param { object } props - Component props.
183+ * @returns { ReactElement } A mocked input element.
184+ */
185+ Input : ( props : any ) : ReactElement => < input { ...props } /> ,
126186} ) )
127187
188+ // Mock the main hooks at the top level.
128189vi . mock ( "react-hook-form" )
129190vi . mock ( "@tanstack/react-query" )
130191
131192// endregion
132193
133- describe ( "RecoverPassword Component" , ( ) => {
194+ describe ( "RecoverPassword Component" , ( ) : void => {
195+ // region Test Setup
134196 let user : UserEvent
197+ // Use 'any' for the mock result to avoid complex type definitions and focus on logic.
135198 let mockMutationResult : any
136199
137- beforeEach ( ( ) => {
200+ beforeEach ( ( ) : void => {
138201 user = userEvent . setup ( )
139202 vi . clearAllMocks ( )
140-
141- mockMutationResult = {
142- isPending : false ,
143- isSuccess : false ,
144- mutate : vi . fn ( ) ,
145- }
146-
203+ mockMutationResult = { isPending : false , isSuccess : false , mutate : vi . fn ( ) }
147204 vi . mocked ( useForm ) . mockReturnValue ( {
148205 register : vi . fn ( ) ,
149- handleSubmit : ( fn : any ) => ( e : any ) => {
150- if ( e ) e . preventDefault ( )
151- fn ( { email : "test@example.com" } )
152- } ,
206+ /**
207+ * Handles form submission.
208+ * @description Prevents the default form submission, then calls the provided callback function
209+ * with a mock form data object containing a test email address.
210+ * @param {Function } fn - The callback function to be invoked with form data.
211+ * @returns {Function } A function that takes an event as an argument and processes the form submission.
212+ */
213+ handleSubmit :
214+ ( fn : any ) : any =>
215+ ( e : any ) : void => {
216+ if ( e ) e . preventDefault ( )
217+ fn ( { email : "test@example.com" } )
218+ } ,
153219 formState : { errors : { } } ,
154220 } as any )
155221
156- vi . mocked ( useMutation ) . mockImplementation ( ( options : any ) => {
157- mockMutationResult . mutate = vi . fn ( async ( data ) => {
222+ vi . mocked ( useMutation ) . mockImplementation ( ( options : any ) : any => {
223+ mockMutationResult . mutate = vi . fn ( async ( data ) : Promise < void > => {
158224 try {
159- const result = await options . mutationFn ( data )
160- if ( options . onSuccess ) options . onSuccess ( result , data , undefined )
225+ await options . mutationFn ( data )
226+ // The component logic for success is now based on `isSuccess`, so onSuccess callback is less critical.
161227 } catch ( error ) {
162228 if ( options . onError ) options . onError ( error , data , undefined )
163229 }
164230 } )
165231 return mockMutationResult
166232 } )
167233 } )
234+ // endregion
168235
169- it ( "should render the initial form correctly" , ( ) => {
236+ // region Render Tests
237+ it ( "should render the initial form correctly" , ( ) : void => {
170238 render ( < RecoverPassword /> )
171239 expect ( screen . getByRole ( "heading" , { name : "Password Recovery" } ) ) . toBeInTheDocument ( )
172240 expect ( screen . getByText ( "A password recovery email will be sent to the registered account." ) ) . toBeInTheDocument ( )
173241 expect ( screen . getByPlaceholderText ( "Email" ) ) . toBeInTheDocument ( )
174242 expect ( screen . getByRole ( "button" , { name : "Continue" } ) ) . toBeInTheDocument ( )
175243 expect ( screen . getByRole ( "link" , { name : "Back to Log In" } ) ) . toBeInTheDocument ( )
176244 } )
245+ // endregion
177246
178- it ( "should display validation error if email is invalid" , async ( ) => {
247+ // region Validation Tests
248+ it ( "should display validation error if email is invalid" , async ( ) : Promise < void > => {
179249 vi . mocked ( useForm ) . mockReturnValue ( {
180250 register : vi . fn ( ) ,
181- handleSubmit : ( fn : any ) => fn ,
251+ /**
252+ * A mock implementation of `useForm`'s `handleSubmit` that simply calls the
253+ * provided function with no arguments.
254+ *
255+ * @param {Function } fn The function to call on form submission.
256+ * @returns {Function } The mocked `handleSubmit` function.
257+ */
258+ handleSubmit : ( fn : any ) : any => fn ,
182259 formState : { errors : { email : { message : emailPattern . message } } } ,
183260 } as any )
184-
185261 render ( < RecoverPassword /> )
186262 expect ( screen . getByText ( emailPattern . message ) ) . toBeInTheDocument ( )
187263 } )
264+ // endregion
188265
189- it ( "should call the mutation on valid form submission" , async ( ) => {
266+ // region State and Submission Tests
267+ it ( "should call the mutation on valid form submission" , async ( ) : Promise < void > => {
190268 render ( < RecoverPassword /> )
191269 await user . click ( screen . getByRole ( "button" , { name : "Continue" } ) )
192270
193- await waitFor ( ( ) => {
271+ await waitFor ( ( ) : void => {
194272 expect ( mockMutationResult . mutate ) . toHaveBeenCalledWith ( { email : "test@example.com" } )
195273 } )
196274 } )
197275
198- it ( "should disable the button and input when pending" , ( ) => {
276+ it ( "should disable the button and input when pending" , ( ) : void => {
199277 mockMutationResult . isPending = true
200278 render ( < RecoverPassword /> )
201-
202279 expect ( screen . getByRole ( "button" , { name : "Continue" } ) ) . toBeDisabled ( )
203280 expect ( screen . getByPlaceholderText ( "Email" ) ) . toBeDisabled ( )
204281 } )
205282
206- it ( "should show success view when submission is successful" , ( ) => {
283+ it ( "should show success view when submission is successful" , ( ) : void => {
207284 mockMutationResult . isSuccess = true
208285 render ( < RecoverPassword /> )
209-
210286 expect ( screen . getByRole ( "heading" , { name : "Check your email" } ) ) . toBeInTheDocument ( )
211287 expect ( screen . queryByRole ( "button" , { name : "Continue" } ) ) . not . toBeInTheDocument ( )
212288 } )
213289
214- it ( "should show success view even when API returns a 404" , async ( ) => {
290+ it ( "should show success view even when API returns a 404 to prevent user enumeration" , async ( ) : Promise < void > => {
291+ // Simulate the API call failing with a 404 status.
215292 vi . mocked ( loginLoginRouterRecoverPassword ) . mockRejectedValue ( { status : 404 } )
216-
217- render ( < RecoverPassword /> )
218-
293+ const { rerender } = render ( < RecoverPassword /> )
219294 await user . click ( screen . getByRole ( "button" , { name : "Continue" } ) )
220-
295+ // The component logic catches the 404 and transitions `useMutation` to a success state.
221296 mockMutationResult . isSuccess = true
222- render ( < RecoverPassword /> )
223-
297+ rerender ( < RecoverPassword /> ) // Rerender to reflect the new state
224298 expect ( screen . getByRole ( "heading" , { name : "Check your email" } ) ) . toBeInTheDocument ( )
225299 expect ( mockShowApiErrorToast ) . not . toHaveBeenCalled ( )
226300 } )
227301
228- it ( "should show an API error toast for non-404 errors" , async ( ) => {
229- const serverError = { status : 500 , body : { detail : "Server error" } }
302+ it ( "should show an API error toast for non-404 errors" , async ( ) : Promise < void > => {
303+ // Use a Partial<ApiError> for a simpler mock object.
304+ const serverError : Partial < ApiError > = { status : 500 , body : { detail : "Server error" } }
230305 vi . mocked ( loginLoginRouterRecoverPassword ) . mockRejectedValue ( serverError )
231-
232306 render ( < RecoverPassword /> )
233-
234307 await user . click ( screen . getByRole ( "button" , { name : "Continue" } ) )
235-
236- await waitFor ( ( ) => {
308+ await waitFor ( ( ) : void => {
237309 expect ( mockShowApiErrorToast ) . toHaveBeenCalledWith ( serverError )
238310 } )
239311 } )
312+ // endregion
240313} )
0 commit comments