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 ( <> setPassword(event.target.value)} /> ); } function ruleStates(): Record { 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(); 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(); 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); }); it("announces only the rules whose state changed through a polite status region", async () => { const user = userEvent.setup(); render(); 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", ); }); it("marks a rule unmet again when the user deletes the character that satisfied it", async () => { const user = userEvent.setup(); render(); const input = screen.getByLabelText("Password"); await user.type(input, "Abcde1!"); await user.type(input, "{Backspace}"); expect(ruleStates()).toMatchObject({ special: "false", length: "true", number: "true" }); }); });