feat(web): ChangeMyPasswordPage + ResetPasswordModal — hooks, pages, modal [UDT-008]
This commit is contained in:
10
src/web/src/features/profile/api/changeMyPassword.ts
Normal file
10
src/web/src/features/profile/api/changeMyPassword.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { axiosClient } from '@/api/axiosClient'
|
||||
|
||||
export interface ChangeMyPasswordRequest {
|
||||
oldPassword: string
|
||||
newPassword: string
|
||||
}
|
||||
|
||||
export async function changeMyPassword(payload: ChangeMyPasswordRequest): Promise<void> {
|
||||
await axiosClient.put('/api/v1/users/me/password', payload)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { changeMyPassword } from '../api/changeMyPassword'
|
||||
import type { ChangeMyPasswordRequest } from '../api/changeMyPassword'
|
||||
|
||||
export function useChangeMyPassword() {
|
||||
return useMutation({
|
||||
mutationFn: (payload: ChangeMyPasswordRequest) => changeMyPassword(payload),
|
||||
})
|
||||
}
|
||||
121
src/web/src/features/profile/pages/ChangeMyPasswordPage.tsx
Normal file
121
src/web/src/features/profile/pages/ChangeMyPasswordPage.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import { useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { isAxiosError } from 'axios'
|
||||
import { toast } from 'sonner'
|
||||
import { AlertCircle } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { useChangeMyPassword } from '../hooks/useChangeMyPassword'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
|
||||
export function ChangeMyPasswordPage() {
|
||||
const navigate = useNavigate()
|
||||
const { mutate, isPending } = useChangeMyPassword()
|
||||
const updateUser = useAuthStore((s) => s.updateUser)
|
||||
|
||||
const [oldPassword, setOldPassword] = useState('')
|
||||
const [newPassword, setNewPassword] = useState('')
|
||||
const [confirmPassword, setConfirmPassword] = useState('')
|
||||
const [clientError, setClientError] = useState<string | null>(null)
|
||||
const [serverError, setServerError] = useState<string | null>(null)
|
||||
|
||||
function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault()
|
||||
setClientError(null)
|
||||
setServerError(null)
|
||||
|
||||
if (newPassword !== confirmPassword) {
|
||||
setClientError('Las contraseñas no coinciden')
|
||||
return
|
||||
}
|
||||
|
||||
mutate(
|
||||
{ oldPassword, newPassword },
|
||||
{
|
||||
onSuccess: () => {
|
||||
// Clear mustChangePassword flag in store
|
||||
updateUser({ mustChangePassword: false })
|
||||
toast.success('Contraseña actualizada correctamente')
|
||||
navigate('/')
|
||||
},
|
||||
onError: (err) => {
|
||||
if (isAxiosError(err) && err.response?.data) {
|
||||
const data = err.response.data as { error?: string; title?: string }
|
||||
if (data.error === 'invalid-old-password' || data.title === 'invalid-old-password') {
|
||||
setServerError('La contraseña actual es incorrecta')
|
||||
return
|
||||
}
|
||||
}
|
||||
setServerError('Error al cambiar la contraseña. Intentá nuevamente.')
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-md mx-auto space-y-6">
|
||||
<h1 className="text-xl font-semibold">Cambiar contraseña</h1>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4" noValidate>
|
||||
{clientError && (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>{clientError}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{serverError && (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>{serverError}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="oldPassword">Contraseña actual</Label>
|
||||
<Input
|
||||
id="oldPassword"
|
||||
type="password"
|
||||
value={oldPassword}
|
||||
onChange={(e) => setOldPassword(e.target.value)}
|
||||
disabled={isPending}
|
||||
autoComplete="current-password"
|
||||
aria-label="Contraseña actual"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="newPassword">Nueva contraseña</Label>
|
||||
<Input
|
||||
id="newPassword"
|
||||
type="password"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
disabled={isPending}
|
||||
autoComplete="new-password"
|
||||
aria-label="Nueva contraseña"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="confirmPassword">Confirmar nueva contraseña</Label>
|
||||
<Input
|
||||
id="confirmPassword"
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
disabled={isPending}
|
||||
autoComplete="new-password"
|
||||
aria-label="Confirmar contraseña"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button type="submit" disabled={isPending} className="w-full">
|
||||
{isPending ? 'Cambiando...' : 'Cambiar contraseña'}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
13
src/web/src/features/users/api/resetUserPassword.ts
Normal file
13
src/web/src/features/users/api/resetUserPassword.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { axiosClient } from '@/api/axiosClient'
|
||||
|
||||
export interface ResetPasswordResponse {
|
||||
tempPassword: string
|
||||
mustChangeOnLogin: boolean
|
||||
}
|
||||
|
||||
export async function resetUserPassword(userId: number): Promise<ResetPasswordResponse> {
|
||||
const response = await axiosClient.post<ResetPasswordResponse>(
|
||||
`/api/v1/users/${userId}/password/reset`,
|
||||
)
|
||||
return response.data
|
||||
}
|
||||
157
src/web/src/features/users/components/ResetPasswordModal.tsx
Normal file
157
src/web/src/features/users/components/ResetPasswordModal.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
import { useState } from 'react'
|
||||
import * as Dialog from '@radix-ui/react-dialog'
|
||||
import { Copy, X } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { AlertCircle } from 'lucide-react'
|
||||
import { useResetUserPassword } from '../hooks/useResetUserPassword'
|
||||
|
||||
interface ResetPasswordModalProps {
|
||||
userId: number
|
||||
}
|
||||
|
||||
type ModalState = 'idle' | 'confirming' | 'showing-password' | 'error'
|
||||
|
||||
export function ResetPasswordModal({ userId }: ResetPasswordModalProps) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [modalState, setModalState] = useState<ModalState>('idle')
|
||||
const [tempPassword, setTempPassword] = useState<string | null>(null)
|
||||
const [copyDone, setCopyDone] = useState(false)
|
||||
const [errorMsg, setErrorMsg] = useState<string | null>(null)
|
||||
|
||||
const { mutate, isPending } = useResetUserPassword()
|
||||
|
||||
function handleOpen() {
|
||||
setModalState('confirming')
|
||||
setTempPassword(null)
|
||||
setCopyDone(false)
|
||||
setErrorMsg(null)
|
||||
setOpen(true)
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
setOpen(false)
|
||||
setModalState('idle')
|
||||
}
|
||||
|
||||
function handleConfirm() {
|
||||
mutate(userId, {
|
||||
onSuccess: (data) => {
|
||||
setTempPassword(data.tempPassword)
|
||||
setModalState('showing-password')
|
||||
},
|
||||
onError: () => {
|
||||
setErrorMsg('Error al resetear la contraseña. Intentá de nuevo.')
|
||||
setModalState('error')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function handleCopy() {
|
||||
if (tempPassword) {
|
||||
await navigator.clipboard.writeText(tempPassword)
|
||||
setCopyDone(true)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog.Root open={open} onOpenChange={setOpen}>
|
||||
<Dialog.Trigger asChild>
|
||||
<Button variant="outline" size="sm" onClick={handleOpen}>
|
||||
Resetear contraseña
|
||||
</Button>
|
||||
</Dialog.Trigger>
|
||||
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay className="fixed inset-0 bg-black/50 z-40" />
|
||||
<Dialog.Content
|
||||
className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 bg-background border border-border rounded-lg shadow-xl p-6 z-50 w-full max-w-md space-y-4"
|
||||
aria-describedby="reset-pwd-desc"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<Dialog.Title className="text-base font-semibold">
|
||||
Resetear contraseña
|
||||
</Dialog.Title>
|
||||
<Dialog.Close asChild>
|
||||
<Button variant="ghost" size="sm" onClick={handleCancel} aria-label="Cerrar">
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</Dialog.Close>
|
||||
</div>
|
||||
|
||||
<Dialog.Description id="reset-pwd-desc" className="sr-only">
|
||||
Resetear contraseña del usuario
|
||||
</Dialog.Description>
|
||||
|
||||
{modalState === 'confirming' && (
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
¿Estás seguro que querés resetear la contraseña de este usuario?
|
||||
Se generará una contraseña temporal y se invalidarán todas sus sesiones activas.
|
||||
</p>
|
||||
<div className="flex gap-2 justify-end">
|
||||
<Button variant="outline" onClick={handleCancel} disabled={isPending}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button onClick={handleConfirm} disabled={isPending}>
|
||||
{isPending ? 'Reseteando...' : 'Confirmar'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{modalState === 'showing-password' && tempPassword && (
|
||||
<div className="space-y-4">
|
||||
<Alert>
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
Esta es la única vez que verás esta contraseña. Copiála ahora.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div className="rounded-md border border-border bg-muted p-3">
|
||||
<p className="font-mono text-base tracking-widest select-all">{tempPassword}</p>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={handleCopy}
|
||||
>
|
||||
<Copy className="h-4 w-4 mr-2" />
|
||||
{copyDone ? '¡Copiado!' : 'Copiar al portapapeles'}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
setOpen(false)
|
||||
setModalState('idle')
|
||||
}}
|
||||
>
|
||||
Cerrar
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{modalState === 'error' && (
|
||||
<div className="space-y-4">
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>{errorMsg}</AlertDescription>
|
||||
</Alert>
|
||||
<div className="flex gap-2 justify-end">
|
||||
<Button variant="outline" onClick={handleCancel}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button onClick={() => { setModalState('confirming'); setErrorMsg(null) }}>
|
||||
Reintentar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
)
|
||||
}
|
||||
8
src/web/src/features/users/hooks/useResetUserPassword.ts
Normal file
8
src/web/src/features/users/hooks/useResetUserPassword.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { resetUserPassword } from '../api/resetUserPassword'
|
||||
|
||||
export function useResetUserPassword() {
|
||||
return useMutation({
|
||||
mutationFn: (userId: number) => resetUserPassword(userId),
|
||||
})
|
||||
}
|
||||
139
src/web/src/tests/features/profile/ChangeMyPasswordPage.test.tsx
Normal file
139
src/web/src/tests/features/profile/ChangeMyPasswordPage.test.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
import { describe, it, expect, beforeAll, afterAll, afterEach, vi } from 'vitest'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { http, HttpResponse } from 'msw'
|
||||
import { setupServer } from 'msw/node'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { MemoryRouter, Routes, Route } from 'react-router-dom'
|
||||
import { ChangeMyPasswordPage } from '../../../features/profile/pages/ChangeMyPasswordPage'
|
||||
import { useAuthStore } from '../../../stores/authStore'
|
||||
|
||||
const API_URL = 'http://localhost:5000'
|
||||
|
||||
const mockNavigate = vi.fn()
|
||||
vi.mock('react-router-dom', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('react-router-dom')>()
|
||||
return { ...actual, useNavigate: () => mockNavigate }
|
||||
})
|
||||
|
||||
const authUser = {
|
||||
id: 1, username: 'admin', nombre: 'Admin', rol: 'admin',
|
||||
permisos: [], mustChangePassword: true,
|
||||
}
|
||||
|
||||
const server = setupServer()
|
||||
|
||||
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
|
||||
afterEach(() => {
|
||||
server.resetHandlers()
|
||||
useAuthStore.getState().clearAuth()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
afterAll(() => server.close())
|
||||
|
||||
function renderPage() {
|
||||
useAuthStore.setState({ user: authUser })
|
||||
const qc = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false } } })
|
||||
return render(
|
||||
<QueryClientProvider client={qc}>
|
||||
<MemoryRouter initialEntries={['/perfil/contrasena']}>
|
||||
<Routes>
|
||||
<Route path="/perfil/contrasena" element={<ChangeMyPasswordPage />} />
|
||||
<Route path="/" element={<div>Home</div>} />
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>,
|
||||
)
|
||||
}
|
||||
|
||||
// Helper: get form fields by their input id
|
||||
function getOldPasswordInput() { return screen.getByLabelText('Contraseña actual') }
|
||||
function getNewPasswordInput() { return screen.getByLabelText('Nueva contraseña') }
|
||||
function getConfirmPasswordInput() { return screen.getByLabelText('Confirmar nueva contraseña') }
|
||||
function getSubmitButton() { return screen.getByRole('button', { name: /cambiar contraseña/i }) }
|
||||
|
||||
describe('ChangeMyPasswordPage', () => {
|
||||
it('shows validation error when passwords do not match', async () => {
|
||||
server.use(
|
||||
http.put(`${API_URL}/api/v1/users/me/password`, () => {
|
||||
throw new Error('Should not be called')
|
||||
}),
|
||||
)
|
||||
|
||||
renderPage()
|
||||
|
||||
await userEvent.type(getOldPasswordInput(), 'current123')
|
||||
await userEvent.type(getNewPasswordInput(), 'NewPass123')
|
||||
await userEvent.type(getConfirmPasswordInput(), 'DifferentPass456')
|
||||
|
||||
await userEvent.click(getSubmitButton())
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText(/no coinciden/i)).toBeInTheDocument(),
|
||||
)
|
||||
expect(mockNavigate).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('no HTTP call when passwords do not match', async () => {
|
||||
let httpCalled = false
|
||||
server.use(
|
||||
http.put(`${API_URL}/api/v1/users/me/password`, () => {
|
||||
httpCalled = true
|
||||
return HttpResponse.json({}, { status: 204 })
|
||||
}),
|
||||
)
|
||||
|
||||
renderPage()
|
||||
|
||||
await userEvent.type(getOldPasswordInput(), 'current123')
|
||||
await userEvent.type(getNewPasswordInput(), 'NewPass123')
|
||||
await userEvent.type(getConfirmPasswordInput(), 'WrongConfirm')
|
||||
|
||||
await userEvent.click(getSubmitButton())
|
||||
|
||||
await new Promise((r) => setTimeout(r, 100))
|
||||
expect(httpCalled).toBe(false)
|
||||
})
|
||||
|
||||
it('submit success → updates authStore mustChangePassword to false + navigate home', async () => {
|
||||
server.use(
|
||||
http.put(`${API_URL}/api/v1/users/me/password`, () =>
|
||||
new HttpResponse(null, { status: 204 }),
|
||||
),
|
||||
)
|
||||
|
||||
renderPage()
|
||||
|
||||
await userEvent.type(getOldPasswordInput(), 'current123')
|
||||
await userEvent.type(getNewPasswordInput(), 'NewPass123')
|
||||
await userEvent.type(getConfirmPasswordInput(), 'NewPass123')
|
||||
|
||||
await userEvent.click(getSubmitButton())
|
||||
|
||||
await waitFor(() => expect(mockNavigate).toHaveBeenCalledWith('/'))
|
||||
|
||||
const state = useAuthStore.getState()
|
||||
expect(state.user?.mustChangePassword).toBe(false)
|
||||
})
|
||||
|
||||
it('shows invalid-old-password error message on 400', async () => {
|
||||
server.use(
|
||||
http.put(`${API_URL}/api/v1/users/me/password`, () =>
|
||||
HttpResponse.json({ error: 'invalid-old-password' }, { status: 400 }),
|
||||
),
|
||||
)
|
||||
|
||||
renderPage()
|
||||
|
||||
await userEvent.type(getOldPasswordInput(), 'wrongpassword')
|
||||
await userEvent.type(getNewPasswordInput(), 'NewPass123')
|
||||
await userEvent.type(getConfirmPasswordInput(), 'NewPass123')
|
||||
|
||||
await userEvent.click(getSubmitButton())
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText(/contraseña actual es incorrecta/i)).toBeInTheDocument(),
|
||||
)
|
||||
expect(mockNavigate).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
117
src/web/src/tests/features/users/ResetPasswordModal.test.tsx
Normal file
117
src/web/src/tests/features/users/ResetPasswordModal.test.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import { describe, it, expect, beforeAll, afterAll, afterEach, vi } from 'vitest'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { http, HttpResponse } from 'msw'
|
||||
import { setupServer } from 'msw/node'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { MemoryRouter } from 'react-router-dom'
|
||||
import { ResetPasswordModal } from '../../../features/users/components/ResetPasswordModal'
|
||||
import { useAuthStore } from '../../../stores/authStore'
|
||||
|
||||
const API_URL = 'http://localhost:5000'
|
||||
|
||||
const adminUser = {
|
||||
id: 1, username: 'admin', nombre: 'Admin', rol: 'admin',
|
||||
permisos: ['administracion:usuarios:gestionar'],
|
||||
mustChangePassword: false,
|
||||
}
|
||||
|
||||
const server = setupServer()
|
||||
|
||||
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
|
||||
afterEach(() => {
|
||||
server.resetHandlers()
|
||||
useAuthStore.getState().clearAuth()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
afterAll(() => server.close())
|
||||
|
||||
function renderModal(userId = 5) {
|
||||
useAuthStore.setState({ user: adminUser })
|
||||
const qc = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false } } })
|
||||
return render(
|
||||
<QueryClientProvider client={qc}>
|
||||
<MemoryRouter>
|
||||
<ResetPasswordModal userId={userId} />
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>,
|
||||
)
|
||||
}
|
||||
|
||||
describe('ResetPasswordModal', () => {
|
||||
it('shows trigger button and modal closed by default', () => {
|
||||
renderModal()
|
||||
expect(screen.getByRole('button', { name: /resetear|reset.*contraseña/i })).toBeInTheDocument()
|
||||
expect(screen.queryByText(/confirmar|advertencia|única vez/i)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('trigger button → modal opens with confirmation', async () => {
|
||||
renderModal()
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: /resetear contraseña/i }))
|
||||
|
||||
// Modal should now show the confirm button
|
||||
expect(screen.getByRole('button', { name: /confirmar/i })).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: /cancelar/i })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('cancel → modal closes without HTTP call', async () => {
|
||||
let httpCalled = false
|
||||
server.use(
|
||||
http.post(`${API_URL}/api/v1/users/5/password/reset`, () => {
|
||||
httpCalled = true
|
||||
return HttpResponse.json({ tempPassword: 'Ax!k9mQ3@rT2', mustChangeOnLogin: true })
|
||||
}),
|
||||
)
|
||||
|
||||
renderModal()
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: /resetear|reset.*contraseña/i }))
|
||||
await userEvent.click(screen.getByRole('button', { name: /cancelar|cancel/i }))
|
||||
|
||||
await new Promise((r) => setTimeout(r, 100))
|
||||
expect(httpCalled).toBe(false)
|
||||
expect(screen.queryByText(/contraseña temporal|tempPassword/i)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('confirm → calls POST and shows tempPassword + warning', async () => {
|
||||
server.use(
|
||||
http.post(`${API_URL}/api/v1/users/5/password/reset`, () =>
|
||||
HttpResponse.json({ tempPassword: 'Ax!k9mQ3@rT2', mustChangeOnLogin: true }),
|
||||
),
|
||||
)
|
||||
|
||||
renderModal()
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: /resetear|reset.*contraseña/i }))
|
||||
await userEvent.click(screen.getByRole('button', { name: /confirmar|confirm/i }))
|
||||
|
||||
await waitFor(() => expect(screen.getByText('Ax!k9mQ3@rT2')).toBeInTheDocument())
|
||||
expect(screen.getByText(/única vez|solo una vez|this is the only time/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('copy button calls clipboard.writeText with tempPassword', async () => {
|
||||
const clipboardWriteText = vi.fn().mockResolvedValue(undefined)
|
||||
Object.defineProperty(navigator, 'clipboard', {
|
||||
value: { writeText: clipboardWriteText },
|
||||
writable: true,
|
||||
})
|
||||
|
||||
server.use(
|
||||
http.post(`${API_URL}/api/v1/users/5/password/reset`, () =>
|
||||
HttpResponse.json({ tempPassword: 'Ax!k9mQ3@rT2', mustChangeOnLogin: true }),
|
||||
),
|
||||
)
|
||||
|
||||
renderModal()
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: /resetear|reset.*contraseña/i }))
|
||||
await userEvent.click(screen.getByRole('button', { name: /confirmar|confirm/i }))
|
||||
|
||||
await waitFor(() => expect(screen.getByText('Ax!k9mQ3@rT2')).toBeInTheDocument())
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: /copiar|copy/i }))
|
||||
|
||||
expect(clipboardWriteText).toHaveBeenCalledWith('Ax!k9mQ3@rT2')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user