1234567891011121314import { beforeEach, describe, expect, it } from "vitest";1516import { detectPasswordPolicy, extractNearbyText } from "./policy";1718beforeEach(() => {19 document.body.innerHTML = "";20});2122function pwField(html: string): void {23 document.body.innerHTML = html;24}2526describe("T-PO01–T-PO04: attribute / pattern / text extraction", () => {27 28 29 30 it("T-PO01: minLength comes from the pattern, not the minlength attribute", () => {31 pwField('<input type="password" minlength="12" />');32 expect(detectPasswordPolicy().minLength).toBeUndefined();3334 pwField('<input type="password" pattern=".{8,}" />');35 expect(detectPasswordPolicy().minLength).toBe(8);36 });3738 39 it("T-PO02: maxlength attribute → policy.maxLength", () => {40 pwField('<input type="password" maxlength="64" />');41 expect(detectPasswordPolicy().maxLength).toBe(64);42 });4344 45 46 it("T-PO03: range pattern → min+max; negation text → forbidden chars", () => {47 pwField('<input type="password" pattern=".{10,32}" />');48 const p1 = detectPasswordPolicy();49 expect(p1.minLength).toBe(10);50 expect(p1.maxLength).toBe(32);5152 pwField(53 '<div><input type="password" id="pw" aria-describedby="rule" />' +54 '<p id="rule">Password cannot contain ` ~ < > characters.</p></div>'55 );56 const p2 = detectPasswordPolicy();57 expect(p2.forbiddenChars).toBeTruthy();58 for (const ch of ["`", "~", "<", ">"]) expect(p2.forbiddenChars).toContain(ch);59 });6061 62 63 it("T-PO04: no attributes or hints → empty policy", () => {64 pwField('<input type="password" />');65 expect(detectPasswordPolicy()).toEqual({});66 });67});6869describe("T-PO01b: human-readable length ranges in nearby text", () => {70 71 it("dash range '8-20 characters' → min 8, max 20", () => {72 pwField(73 '<div><input type="password" id="pw" aria-describedby="h" />' +74 '<span id="h">Use 8-20 characters.</span></div>'75 );76 const p = detectPasswordPolicy();77 expect(p.minLength).toBe(8);78 expect(p.maxLength).toBe(20);79 });8081 it("'at least 12 characters' → min 12", () => {82 pwField(83 '<div><input type="password" id="pw" aria-describedby="h" />' +84 '<small id="h">Must be at least 12 characters.</small></div>'85 );86 expect(detectPasswordPolicy().minLength).toBe(12);87 });88});8990describe("T-PO05–T-PO06: documented scope boundaries", () => {91 92 93 94 it("T-PO05: produced policy uses the PasswordPolicy field names the generator reads", () => {95 pwField('<input type="password" pattern=".{6,18}" maxlength="18" />');96 const p = detectPasswordPolicy();97 expect(Object.keys(p).sort()).toEqual(["maxLength", "minLength"]);98 });99100 101 102 103 it("T-PO06: reads the first matching password field (new-password preferred)", () => {104 pwField(105 '<input type="password" autocomplete="new-password" pattern=".{16,}" />' +106 '<input type="password" autocomplete="current-password" />'107 );108 expect(detectPasswordPolicy().minLength).toBe(16);109 });110});111112describe("extractNearbyText", () => {113 114 it("returns empty string for a null field", () => {115 expect(extractNearbyText(null)).toBe("");116 });117118 it("pulls aria-describedby, title, placeholder, and aria-label together", () => {119 document.body.innerHTML =120 '<input type="password" id="pw" title="t-text" placeholder="p-text" ' +121 'aria-label="a-text" aria-describedby="d" /><span id="d">d-text</span>';122 const el = document.querySelector<HTMLInputElement>("#pw")!;123 const text = extractNearbyText(el);124 for (const s of ["t-text", "p-text", "a-text", "d-text"]) expect(text).toContain(s);125 });126});