// @vitest-environment jsdom import { beforeEach, describe, expect, it } from "vitest"; import { detectCardForm, detectLoginFields, getPageDomain, hasPaymentProcessorIframe, isLoginForm, isSignupForm, isTOTPField, } from "./detect"; describe("content detect", () => { beforeEach(() => { document.body.innerHTML = ""; }); function visibleInput( attributes: Record = {}, tagName: "input" = "input" ): HTMLInputElement { const input = document.createElement(tagName); Object.entries(attributes).forEach(([key, value]) => input.setAttribute(key, value)); Object.defineProperty(input, "getBoundingClientRect", { configurable: true, value: () => ({ width: 100, height: 20, top: 0, left: 0, right: 100, bottom: 20, x: 0, y: 0, toJSON: () => ({}), }), }); document.body.appendChild(input); return input as HTMLInputElement; } it("isLoginForm returns true for a username/password pair", () => { const username = visibleInput({ type: "email", name: "email" }); const password = visibleInput({ type: "password", autocomplete: "current-password" }); expect( isLoginForm([ { type: "username", el: username }, { type: "password", el: password }, ]) ).toBe(true); }); it("isLoginForm returns false for fields without a password", () => { const username = visibleInput({ type: "text", name: "username" }); expect(isLoginForm([{ type: "username", el: username }])).toBe(false); }); it("isSignupForm returns true for confirm-password flows", () => { const password = visibleInput({ type: "password", name: "password" }); const confirm = visibleInput({ type: "password", name: "confirmPassword" }); expect( isSignupForm([ { type: "password", el: password }, { type: "password", el: confirm }, ]) ).toBe(true); }); it("isSignupForm returns false for a single current-password field", () => { const password = visibleInput({ type: "password", autocomplete: "current-password" }); expect(isSignupForm([{ type: "password", el: password }])).toBe(false); }); it("detectLoginFields matches email and username-style inputs but not tag-name fields", () => { const email = visibleInput({ type: "email", name: "email" }); const username = visibleInput({ type: "text", name: "login-id" }); const tagName = visibleInput({ type: "text", name: "tag-name" }); const password = visibleInput({ type: "password", autocomplete: "current-password" }); const hidden = visibleInput({ type: "hidden", name: "username" }); const fields = detectLoginFields(document); expect(fields.some((field) => field.el === email && field.type === "email")).toBe(true); expect(fields.some((field) => field.el === username && field.type === "username")).toBe(true); expect(fields.some((field) => field.el === password && field.type === "password")).toBe(true); expect(fields.some((field) => field.el === tagName)).toBe(false); expect(fields.some((field) => field.el === hidden)).toBe(false); }); it("getPageDomain falls back to the current hostname when no registrable domain exists", () => { expect(getPageDomain()).toBe(window.location.hostname); }); it("isTOTPField detects one-time-code fields without matching generic code words", () => { const oneTimeCode = visibleInput({ autocomplete: "one-time-code" }); const otpName = visibleInput({ name: "otp" }); const verification = visibleInput({ placeholder: "Verification code" }); const zipCode = visibleInput({ name: "zipcode" }); expect(isTOTPField(oneTimeCode)).toBe(true); expect(isTOTPField(otpName)).toBe(true); expect(isTOTPField(verification)).toBe(true); expect(isTOTPField(zipCode)).toBe(false); }); it("isTOTPField detects an adjacent 2FA label", () => { const label = document.createElement("label"); label.textContent = "2FA Code"; document.body.appendChild(label); const input = visibleInput({ placeholder: "If enabled" }); expect(isTOTPField(input)).toBe(true); }); // Regression: numeric 2FA boxes are usually type="number", where the maxlength // ATTRIBUTE is set to 1 but the .maxLength PROPERTY ignores it and reports a // large default. Detection must key on the attribute so each digit box is // recognised as a split-OTP box, not a full TOTP field getting its own icon. it("isTOTPField ignores type=number split-OTP digit boxes", () => { for (let i = 0; i < 6; i++) { const box = visibleInput({ type: "number", maxlength: "1", name: "otp" }); expect(isTOTPField(box)).toBe(false); } }); it("detectLoginFields skips type=number split-OTP digit boxes", () => { for (let i = 0; i < 6; i++) { visibleInput({ type: "number", maxlength: "1", name: "otp" }); } expect(detectLoginFields(document)).toEqual([]); }); // PCH-01-025: ids may legally contain CSS-special chars; field detection // must not throw a SyntaxError out of the label[for=...] lookup. it("field_detection_does_not_throw_on_id_with_css_special_chars", () => { const label = document.createElement("label"); label.setAttribute("for", "user[email]"); label.textContent = "Email"; document.body.appendChild(label); const email = visibleInput({ type: "email", name: "email", id: "user[email]" }); visibleInput({ type: "password", autocomplete: "current-password" }); let fields: ReturnType = []; expect(() => { fields = detectLoginFields(document); }).not.toThrow(); expect(fields.some((field) => field.el === email)).toBe(true); }); it("field_detection_does_not_throw_on_id_with_quotes", () => { const email = visibleInput({ type: "email", name: "email", id: "it's-me" }); visibleInput({ type: "password", autocomplete: "current-password" }); let fields: ReturnType = []; expect(() => { fields = detectLoginFields(document); }).not.toThrow(); expect(fields.some((field) => field.el === email)).toBe(true); }); }); describe("card form detection", () => { beforeEach(() => { document.body.innerHTML = ""; }); it("detectCardForm — finds number field by autocomplete attribute", () => { document.body.innerHTML = ''; const result = detectCardForm(document); expect(result).not.toBeNull(); expect(result!.number).toBeTruthy(); expect(result!.expMonth).toBeTruthy(); }); it("detectCardForm — finds fields by name/id heuristics when no autocomplete", () => { document.body.innerHTML = ''; const result = detectCardForm(document); expect(result).not.toBeNull(); expect(result!.number).toBeTruthy(); }); it("detectCardForm — returns null when no card number field found", () => { document.body.innerHTML = ''; expect(detectCardForm(document)).toBeNull(); }); it("hasPaymentProcessorIframe — returns true when Stripe iframe present", () => { document.body.innerHTML = ''; expect(hasPaymentProcessorIframe()).toBe(true); }); it("hasPaymentProcessorIframe — returns false on standard checkout page", () => { document.body.innerHTML = ''; expect(hasPaymentProcessorIframe()).toBe(false); }); });