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:
Kari Lintulaakso
2025-12-01 17:33:03 +02:00
committed by GitHub
3 changed files with 140 additions and 2 deletions
@@ -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)
})
})