content.policy.test.ts

5.3 KB · raw

1// @vitest-environment jsdom2//3// Test File 11 — policy.ts site password policy (T-PO01–T-PO06).4//5// detectPasswordPolicy scrapes the page so Peach's generator can produce a6// password the site will actually accept. Its real inputs are: the password7// field's maxLength attribute, its pattern attribute, and nearby descriptive8// text (aria-describedby, hints, labels). It does NOT read a minlength attribute9// and it inspects only the first password field — where the brief assumed10// otherwise (T-PO01 minlength, T-PO05 generator wiring, T-PO06 multi-field11// "most restrictive"), the deviation is pinned inline so the gap remains12// explicit rather than silently "passing".1314import { 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  // T-PO01: minLength is NOT read from a minlength attribute — only from the28  // pattern or descriptive text. This test pins the real source (pattern) and29  // documents that a bare minlength attribute yields nothing.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  // T-PO02: maxLength IS read directly from the attribute.39  it("T-PO02: maxlength attribute → policy.maxLength", () => {40    pwField('<input type="password" maxlength="64" />');41    expect(detectPasswordPolicy().maxLength).toBe(64);42  });4344  // T-PO03: a range pattern yields both bounds; forbidden characters are derived45  // from negation TEXT (not the pattern). Pin both real mechanisms.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  // T-PO04: no policy signals anywhere → an empty policy object (the generator62  // then uses its own defaults).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  // The most common real-world phrasing is "8-20 characters" in a hint element.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  // T-PO05 (generator integration) lives in the generator's own suite, not here:92  // detectPasswordPolicy only PRODUCES the policy. We pin the shape the generator93  // consumes so a field rename would break a real contract.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  // T-PO06: only the FIRST new-password/password field is inspected. "Most101  // restrictive across multiple fields" is not implemented — pin the actual102  // single-field behavior (new-password is preferred by selector order).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  // extractNearbyText is the scraping primitive; null input must be safe.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});