Skip to content

Commit 3ead6c8

Browse files
committed
test: recover-password.tsx
1 parent 0e97db0 commit 3ead6c8

File tree

2 files changed

+141
-68
lines changed

2 files changed

+141
-68
lines changed

frontend/src/routes/recover-password.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export const Route = createFileRoute("/recover-password")({
6363
* message and a link back to the login page.
6464
* @returns {ReactElement} The rendered password recovery component.
6565
*/
66-
function RecoverPassword(): ReactElement {
66+
export function RecoverPassword(): ReactElement {
6767
const {
6868
register,
6969
handleSubmit,

frontend/tests/routes/recover-password.test.tsx

Lines changed: 140 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -15,61 +15,105 @@ import type { ReactElement, ReactNode } from "react"
1515
import { useForm } from "react-hook-form"
1616
import { type Mock, beforeEach, describe, expect, it, vi } from "vitest"
1717

18+
import type { ApiError } from "@/client"
1819
import { loginLoginRouterRecoverPassword } from "@/client"
1920
import { RecoverPassword } from "@/routes/recover-password"
2021
import { 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
2527
vi.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.
3136
vi.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.
3545
vi.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.
4871
vi.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.
5280
vi.mock("@/client")
81+
82+
// Mock custom UI components to be simple, non-styled elements.
5383
vi.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

61100
vi.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

80124
vi.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.
89139
vi.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.
128189
vi.mock("react-hook-form")
129190
vi.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

Comments
 (0)