mirror of
https://github.com/nowcommunity/nowdatabase.git
synced 2026-10-12 19:13:52 +03:00
Merge pull request #1000 from nowcommunity/codex/plan-feature-to-disable-change-password-button-exaaef
Disable change password submit until inputs valid
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import { useNotify } from '@/hooks/notification'
|
||||
import { removeFirstLogin, useChangePasswordMutation } from '@/redux/userReducer'
|
||||
import { Button, Grid, TextField } from '@mui/material'
|
||||
import { useState } from 'react'
|
||||
import { Button, Grid, List, ListItem, ListItemText, TextField, Typography } from '@mui/material'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { usePasswordValidation } from '@/hooks/usePasswordValidation'
|
||||
|
||||
type ChangePasswordError = { status: number; data: { error: string } }
|
||||
|
||||
@@ -15,11 +16,25 @@ export const ChangePasswordForm = () => {
|
||||
const sx = { display: 'flex', alignItems: 'center', width: '10em', fontWeight: 'bold' }
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const { passwordRequirements, validatePassword } = usePasswordValidation()
|
||||
const isPasswordValid = useMemo(() => validatePassword(newPassword).isValid, [newPassword, validatePassword])
|
||||
const isFormValid =
|
||||
oldPassword.length > 0 &&
|
||||
newPassword.length > 0 &&
|
||||
verifyPassword.length > 0 &&
|
||||
isPasswordValid &&
|
||||
newPassword === verifyPassword
|
||||
|
||||
const changePassword = async () => {
|
||||
if (newPassword !== verifyPassword) {
|
||||
notify('New password was not the same in both fields.', 'error')
|
||||
return
|
||||
}
|
||||
const passwordValidationResult = validatePassword(newPassword)
|
||||
if (!passwordValidationResult.isValid) {
|
||||
notify(passwordValidationResult.error ?? 'Password does not meet requirements', 'error')
|
||||
return
|
||||
}
|
||||
if (newPassword.length === 0 || oldPassword.length === 0) {
|
||||
notify('Please fill all fields.', 'error')
|
||||
return
|
||||
@@ -48,6 +63,7 @@ export const ChangePasswordForm = () => {
|
||||
type="password"
|
||||
value={oldPassword}
|
||||
onChange={event => setOldPassword(event.target.value)}
|
||||
inputProps={{ 'data-testid': 'old-password-input' }}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid container direction="row">
|
||||
@@ -59,6 +75,7 @@ export const ChangePasswordForm = () => {
|
||||
type="password"
|
||||
value={newPassword}
|
||||
onChange={event => setNewPassword(event.target.value)}
|
||||
inputProps={{ 'data-testid': 'new-password-input' }}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid container direction="row">
|
||||
@@ -70,8 +87,19 @@ export const ChangePasswordForm = () => {
|
||||
type="password"
|
||||
value={verifyPassword}
|
||||
onChange={event => setVerifyPassword(event.target.value)}
|
||||
inputProps={{ 'data-testid': 'verify-password-input' }}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid container direction="column" sx={{ rowGap: '0.5em' }}>
|
||||
<Typography variant="subtitle1">Password requirements</Typography>
|
||||
<List dense sx={{ listStyleType: 'disc', pl: 4 }}>
|
||||
{passwordRequirements.map(requirement => (
|
||||
<ListItem key={requirement} sx={{ display: 'list-item', py: 0 }}>
|
||||
<ListItemText primary={requirement} />
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Grid>
|
||||
<Grid container direction="row">
|
||||
<Grid item width="27em">
|
||||
<Button
|
||||
@@ -80,6 +108,7 @@ export const ChangePasswordForm = () => {
|
||||
sx={{ height: '4em' }}
|
||||
variant="contained"
|
||||
onClick={() => void changePassword()}
|
||||
disabled={!isFormValid}
|
||||
>
|
||||
Change password
|
||||
</Button>
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { useCallback, useMemo } from 'react'
|
||||
|
||||
export interface PasswordValidationResult {
|
||||
isValid: boolean
|
||||
error?: string
|
||||
}
|
||||
|
||||
const passwordRequirements = [
|
||||
'Password must be at least 8 characters long',
|
||||
'Password must only contain characters a-z, A-Z, 0-9 and ^?$%&~!',
|
||||
]
|
||||
|
||||
export const usePasswordValidation = () => {
|
||||
const requirements = useMemo(() => passwordRequirements, [])
|
||||
|
||||
const validatePassword = useCallback(
|
||||
(password: string): PasswordValidationResult => {
|
||||
if (password.length < 8) {
|
||||
return { isValid: false, error: requirements[0] }
|
||||
}
|
||||
|
||||
if (!/^[0-9A-Za-z$%&~!]+$/.test(password)) {
|
||||
return { isValid: false, error: requirements[1] }
|
||||
}
|
||||
|
||||
return { isValid: true }
|
||||
},
|
||||
[requirements]
|
||||
)
|
||||
|
||||
return { passwordRequirements: requirements, validatePassword }
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { describe, expect, it, jest } from '@jest/globals'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
|
||||
import '@testing-library/jest-dom'
|
||||
import { ChangePasswordForm } from '@/components/Person/Tabs/ChangePasswordForm'
|
||||
|
||||
jest.mock('@/hooks/notification', () => ({
|
||||
useNotify: () => ({
|
||||
notify: jest.fn(),
|
||||
setMessage: jest.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
jest.mock('@/redux/userReducer', () => ({
|
||||
useChangePasswordMutation: () => [jest.fn(), {}],
|
||||
removeFirstLogin: jest.fn(),
|
||||
}))
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
useDispatch: () => jest.fn(),
|
||||
}))
|
||||
|
||||
describe('ChangePasswordForm', () => {
|
||||
it('shows password requirements before submission', () => {
|
||||
render(<ChangePasswordForm />)
|
||||
|
||||
const requirementsHeading = screen.getByText('Password requirements')
|
||||
const minLengthRequirement = screen.getByText('Password must be at least 8 characters long')
|
||||
const allowedCharactersRequirement = screen.getByText(
|
||||
'Password must only contain characters a-z, A-Z, 0-9 and ^?$%&~!'
|
||||
)
|
||||
|
||||
expect(requirementsHeading).toBeDefined()
|
||||
expect(minLengthRequirement).toBeDefined()
|
||||
expect(allowedCharactersRequirement).toBeDefined()
|
||||
})
|
||||
|
||||
it('disables submit when required fields are missing', async () => {
|
||||
render(<ChangePasswordForm />)
|
||||
|
||||
const user = userEvent.setup()
|
||||
const submitButton = screen.getByRole('button', { name: /change password/i })
|
||||
expect((submitButton as HTMLButtonElement).disabled).toBe(true)
|
||||
|
||||
await user.type(screen.getByTestId('old-password-input'), 'oldPassword123')
|
||||
await user.type(screen.getByTestId('new-password-input'), 'ValidPass1')
|
||||
|
||||
expect((submitButton as HTMLButtonElement).disabled).toBe(true)
|
||||
})
|
||||
|
||||
it('disables submit when confirmation does not match', async () => {
|
||||
render(<ChangePasswordForm />)
|
||||
|
||||
const user = userEvent.setup()
|
||||
const submitButton = screen.getByRole('button', { name: /change password/i })
|
||||
|
||||
await user.type(screen.getByTestId('old-password-input'), 'oldPassword123')
|
||||
await user.type(screen.getByTestId('new-password-input'), 'ValidPass1')
|
||||
await user.type(screen.getByTestId('verify-password-input'), 'DifferentPass1')
|
||||
|
||||
expect((submitButton as HTMLButtonElement).disabled).toBe(true)
|
||||
})
|
||||
|
||||
it('enables submit when all requirements are met', async () => {
|
||||
render(<ChangePasswordForm />)
|
||||
|
||||
const user = userEvent.setup()
|
||||
const submitButton = screen.getByRole('button', { name: /change password/i })
|
||||
|
||||
await user.type(screen.getByTestId('old-password-input'), 'oldPassword123')
|
||||
await user.type(screen.getByTestId('new-password-input'), 'ValidPass1')
|
||||
await user.type(screen.getByTestId('verify-password-input'), 'ValidPass1')
|
||||
|
||||
expect((submitButton as HTMLButtonElement).disabled).toBe(false)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user