// @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);
});
});