// @vitest-environment jsdom // // Test File 11 — policy.ts site password policy (T-PO01–T-PO06). // // detectPasswordPolicy scrapes the page so Peach's generator can produce a // password the site will actually accept. Its real inputs are: the password // field's maxLength attribute, its pattern attribute, and nearby descriptive // text (aria-describedby, hints, labels). It does NOT read a minlength attribute // and it inspects only the first password field — where the brief assumed // otherwise (T-PO01 minlength, T-PO05 generator wiring, T-PO06 multi-field // "most restrictive"), the deviation is pinned inline so the gap remains // explicit rather than silently "passing". import { beforeEach, describe, expect, it } from "vitest"; import { detectPasswordPolicy, extractNearbyText } from "./policy"; beforeEach(() => { document.body.innerHTML = ""; }); function pwField(html: string): void { document.body.innerHTML = html; } describe("T-PO01–T-PO04: attribute / pattern / text extraction", () => { // T-PO01: minLength is NOT read from a minlength attribute — only from the // pattern or descriptive text. This test pins the real source (pattern) and // documents that a bare minlength attribute yields nothing. it("T-PO01: minLength comes from the pattern, not the minlength attribute", () => { pwField(''); expect(detectPasswordPolicy().minLength).toBeUndefined(); pwField(''); expect(detectPasswordPolicy().minLength).toBe(8); }); // T-PO02: maxLength IS read directly from the attribute. it("T-PO02: maxlength attribute → policy.maxLength", () => { pwField(''); expect(detectPasswordPolicy().maxLength).toBe(64); }); // T-PO03: a range pattern yields both bounds; forbidden characters are derived // from negation TEXT (not the pattern). Pin both real mechanisms. it("T-PO03: range pattern → min+max; negation text → forbidden chars", () => { pwField(''); const p1 = detectPasswordPolicy(); expect(p1.minLength).toBe(10); expect(p1.maxLength).toBe(32); pwField( '
' + '

Password cannot contain ` ~ < > characters.

' ); const p2 = detectPasswordPolicy(); expect(p2.forbiddenChars).toBeTruthy(); for (const ch of ["`", "~", "<", ">"]) expect(p2.forbiddenChars).toContain(ch); }); // T-PO04: no policy signals anywhere → an empty policy object (the generator // then uses its own defaults). it("T-PO04: no attributes or hints → empty policy", () => { pwField(''); expect(detectPasswordPolicy()).toEqual({}); }); }); describe("T-PO01b: human-readable length ranges in nearby text", () => { // The most common real-world phrasing is "8-20 characters" in a hint element. it("dash range '8-20 characters' → min 8, max 20", () => { pwField( '
' + 'Use 8-20 characters.
' ); const p = detectPasswordPolicy(); expect(p.minLength).toBe(8); expect(p.maxLength).toBe(20); }); it("'at least 12 characters' → min 12", () => { pwField( '
' + 'Must be at least 12 characters.
' ); expect(detectPasswordPolicy().minLength).toBe(12); }); }); describe("T-PO05–T-PO06: documented scope boundaries", () => { // T-PO05 (generator integration) lives in the generator's own suite, not here: // detectPasswordPolicy only PRODUCES the policy. We pin the shape the generator // consumes so a field rename would break a real contract. it("T-PO05: produced policy uses the PasswordPolicy field names the generator reads", () => { pwField(''); const p = detectPasswordPolicy(); expect(Object.keys(p).sort()).toEqual(["maxLength", "minLength"]); }); // T-PO06: only the FIRST new-password/password field is inspected. "Most // restrictive across multiple fields" is not implemented — pin the actual // single-field behavior (new-password is preferred by selector order). it("T-PO06: reads the first matching password field (new-password preferred)", () => { pwField( '' + '' ); expect(detectPasswordPolicy().minLength).toBe(16); }); }); describe("extractNearbyText", () => { // extractNearbyText is the scraping primitive; null input must be safe. it("returns empty string for a null field", () => { expect(extractNearbyText(null)).toBe(""); }); it("pulls aria-describedby, title, placeholder, and aria-label together", () => { document.body.innerHTML = 'd-text'; const el = document.querySelector("#pw")!; const text = extractNearbyText(el); for (const s of ["t-text", "p-text", "a-text", "d-text"]) expect(text).toContain(s); }); });