123import { beforeEach, describe, expect, it } from "vitest";4import { detectPasswordPolicy, extractNearbyText } from "./policy";56describe("detectPasswordPolicy", () => {7 beforeEach(() => {8 document.body.innerHTML = "";9 });1011 function setupPasswordField(attrs: Record<string, string> = {}, nearby = ""): HTMLInputElement {12 const wrap = document.createElement("div");13 const input = document.createElement("input");14 input.type = "password";15 Object.entries(attrs).forEach(([k, v]) => input.setAttribute(k, v));16 wrap.appendChild(input);17 if (nearby) {18 const hint = document.createElement("p");19 hint.className = "requirement";20 hint.textContent = nearby;21 wrap.appendChild(hint);22 }23 document.body.appendChild(wrap);24 return input;25 }2627 it("reads maxlength attribute", () => {28 setupPasswordField({ maxlength: "16" });29 expect(detectPasswordPolicy().maxLength).toBe(16);30 });3132 it("reads minLength/maxLength from pattern attribute", () => {33 setupPasswordField({ pattern: ".{8,32}" });34 const p = detectPasswordPolicy();35 expect(p.minLength).toBe(8);36 expect(p.maxLength).toBe(32);37 });3839 it("parses 'at least N' from nearby text", () => {40 setupPasswordField({}, "Password must contain at least 12 characters.");41 expect(detectPasswordPolicy().minLength).toBe(12);42 });4344 it("parses 'cannot exceed N characters'", () => {45 setupPasswordField({}, "Password cannot exceed 40 characters.");46 expect(detectPasswordPolicy().maxLength).toBe(40);47 });4849 it("detects requires/forbids symbols from text", () => {50 setupPasswordField({}, "Must contain a special character and an uppercase letter.");51 const p = detectPasswordPolicy();52 expect(p.requiresSymbols).toBe(true);53 expect(p.requiresUppercase).toBe(true);54 });5556 it("detects no-symbols phrasing", () => {57 setupPasswordField({}, "No special characters allowed in this password.");58 expect(detectPasswordPolicy().forbidsSymbols).toBe(true);59 });6061 it("extracts Oracle-style forbidden character list (space-separated)", () => {62 setupPasswordField(63 {},64 "Password cannot exceed 40 characters, contain the users first name, last name, email address, spaces, or ` ~ < > \\ characters."65 );66 const p = detectPasswordPolicy();67 expect(p.maxLength).toBe(40);68 const forbidden = p.forbiddenChars ?? "";69 for (const c of ["`", "~", "<", ">", "\\"]) {70 expect(forbidden).toContain(c);71 }72 });7374 it("extracts comma-separated forbidden chars", () => {75 setupPasswordField(76 {},77 "Your password must not contain < , > , or \\ symbols."78 );79 const forbidden = detectPasswordPolicy().forbiddenChars ?? "";80 expect(forbidden).toContain("<");81 expect(forbidden).toContain(">");82 expect(forbidden).toContain("\\");83 });8485 it("does not leak commas or periods from descriptive prose", () => {86 setupPasswordField(87 {},88 "Cannot contain your first name, last name, or email address."89 );90 const forbidden = detectPasswordPolicy().forbiddenChars ?? "";91 expect(forbidden).toBe("");92 });9394 it("returns empty policy when there is no password field", () => {95 expect(detectPasswordPolicy()).toEqual({});96 });9798 it("extractNearbyText honors aria-describedby", () => {99 document.body.innerHTML = `100 <input id="pw" type="password" aria-describedby="pw-help">101 <div id="pw-help">At least 12 characters required.</div>102 `;103 const el = document.getElementById("pw") as HTMLInputElement;104 expect(extractNearbyText(el)).toContain("At least 12");105 });106});