2026-09-25 11:16:59 -03:00
import { useState } from "react" ;
import { render , screen , within } from "@testing-library/react" ;
import userEvent from "@testing-library/user-event" ;
import { describe , expect , it } from "vitest" ;
import { PasswordChecklist } from "@/components/auth/password-checklist" ;
function ChecklistHarness() {
const [ password , setPassword ] = useState ( "" ) ;
return (
< >
< label htmlFor = "pw" > Password < / label >
< input id = "pw" value = { password } onChange = { ( event ) = > setPassword ( event . target . value ) } / >
< PasswordChecklist password = { password } / >
< / >
) ;
}
function ruleStates ( ) : Record < string , string | null > {
const list = screen . getByRole ( "list" , { name : "Password requirements" } ) ;
return Object . fromEntries (
within ( list )
. getAllByRole ( "listitem" )
. map ( ( item ) = > [ item . getAttribute ( "data-rule" ) ? ? "" , item . getAttribute ( "data-met" ) ] ) ,
) ;
}
describe ( "PasswordChecklist" , ( ) = > {
it ( "starts with every rule unmet" , ( ) = > {
render ( < ChecklistHarness / > ) ;
expect ( ruleStates ( ) ) . toEqual ( {
length : "false" ,
uppercase : "false" ,
number : "false" ,
special : "false" ,
} ) ;
expect ( screen . getAllByText ( "requirement not met" ) ) . toHaveLength ( 4 ) ;
} ) ;
it ( "marks each rule met live as the user types" , async ( ) = > {
const user = userEvent . setup ( ) ;
render ( < ChecklistHarness / > ) ;
const input = screen . getByLabelText ( "Password" ) ;
await user . type ( input , "A" ) ;
expect ( ruleStates ( ) ) . toMatchObject ( { uppercase : "true" , number : "false" , special : "false" } ) ;
await user . type ( input , "1" ) ;
expect ( ruleStates ( ) ) . toMatchObject ( { uppercase : "true" , number : "true" , special : "false" } ) ;
await user . type ( input , "!" ) ;
expect ( ruleStates ( ) ) . toMatchObject ( { special : "true" , length : "false" } ) ;
await user . type ( input , "xyz" ) ;
expect ( ruleStates ( ) ) . toEqual ( {
length : "true" ,
uppercase : "true" ,
number : "true" ,
special : "true" ,
} ) ;
expect ( screen . getAllByText ( "requirement met" ) ) . toHaveLength ( 4 ) ;
} ) ;
2026-09-25 11:52:10 -03:00
it ( "announces only the rules whose state changed through a polite status region" , async ( ) = > {
const user = userEvent . setup ( ) ;
render ( < ChecklistHarness / > ) ;
const input = screen . getByLabelText ( "Password" ) ;
const status = screen . getByRole ( "status" ) ;
expect ( status ) . toHaveAttribute ( "aria-live" , "polite" ) ;
expect ( status ) . toHaveTextContent ( /^$/ ) ;
await user . type ( input , "A" ) ;
expect ( status ) . toHaveTextContent ( /^One uppercase letter: requirement met$/ ) ;
await user . type ( input , "b" ) ;
expect ( status ) . toHaveTextContent ( /^One uppercase letter: requirement met$/ ) ;
await user . type ( input , "1!xy" ) ;
expect ( status ) . toHaveTextContent ( /^At least 6 characters: requirement met$/ ) ;
await user . clear ( input ) ;
expect ( status ) . toHaveTextContent (
"At least 6 characters: requirement not met. One uppercase letter: requirement not met. One number: requirement not met. One special character: requirement not met" ,
) ;
} ) ;
2026-09-25 11:16:59 -03:00
it ( "marks a rule unmet again when the user deletes the character that satisfied it" , async ( ) = > {
const user = userEvent . setup ( ) ;
render ( < ChecklistHarness / > ) ;
const input = screen . getByLabelText ( "Password" ) ;
await user . type ( input , "Abcde1!" ) ;
await user . type ( input , "{Backspace}" ) ;
expect ( ruleStates ( ) ) . toMatchObject ( { special : "false" , length : "true" , number : "true" } ) ;
} ) ;
} ) ;