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" });
});
});