123import { beforeEach, describe, expect, it } from "vitest";45import {6 detectCardForm,7 detectLoginFields,8 getPageDomain,9 hasPaymentProcessorIframe,10 isLoginForm,11 isSignupForm,12 isTOTPField,13} from "./detect";1415describe("content detect", () => {16 beforeEach(() => {17 document.body.innerHTML = "";18 });1920 function visibleInput(21 attributes: Record<string, string> = {},22 tagName: "input" = "input"23 ): HTMLInputElement {24 const input = document.createElement(tagName);25 Object.entries(attributes).forEach(([key, value]) => input.setAttribute(key, value));26 Object.defineProperty(input, "getBoundingClientRect", {27 configurable: true,28 value: () => ({29 width: 100,30 height: 20,31 top: 0,32 left: 0,33 right: 100,34 bottom: 20,35 x: 0,36 y: 0,37 toJSON: () => ({}),38 }),39 });40 document.body.appendChild(input);41 return input as HTMLInputElement;42 }4344 it("isLoginForm returns true for a username/password pair", () => {45 const username = visibleInput({ type: "email", name: "email" });46 const password = visibleInput({ type: "password", autocomplete: "current-password" });4748 expect(49 isLoginForm([50 { type: "username", el: username },51 { type: "password", el: password },52 ])53 ).toBe(true);54 });5556 it("isLoginForm returns false for fields without a password", () => {57 const username = visibleInput({ type: "text", name: "username" });5859 expect(isLoginForm([{ type: "username", el: username }])).toBe(false);60 });6162 it("isSignupForm returns true for confirm-password flows", () => {63 const password = visibleInput({ type: "password", name: "password" });64 const confirm = visibleInput({ type: "password", name: "confirmPassword" });6566 expect(67 isSignupForm([68 { type: "password", el: password },69 { type: "password", el: confirm },70 ])71 ).toBe(true);72 });7374 it("isSignupForm returns false for a single current-password field", () => {75 const password = visibleInput({ type: "password", autocomplete: "current-password" });7677 expect(isSignupForm([{ type: "password", el: password }])).toBe(false);78 });7980 it("detectLoginFields matches email and username-style inputs but not tag-name fields", () => {81 const email = visibleInput({ type: "email", name: "email" });82 const username = visibleInput({ type: "text", name: "login-id" });83 const tagName = visibleInput({ type: "text", name: "tag-name" });84 const password = visibleInput({ type: "password", autocomplete: "current-password" });85 const hidden = visibleInput({ type: "hidden", name: "username" });8687 const fields = detectLoginFields(document);8889 expect(fields.some((field) => field.el === email && field.type === "email")).toBe(true);90 expect(fields.some((field) => field.el === username && field.type === "username")).toBe(true);91 expect(fields.some((field) => field.el === password && field.type === "password")).toBe(true);92 expect(fields.some((field) => field.el === tagName)).toBe(false);93 expect(fields.some((field) => field.el === hidden)).toBe(false);94 });9596 it("getPageDomain falls back to the current hostname when no registrable domain exists", () => {97 expect(getPageDomain()).toBe(window.location.hostname);98 });99100 it("isTOTPField detects one-time-code fields without matching generic code words", () => {101 const oneTimeCode = visibleInput({ autocomplete: "one-time-code" });102 const otpName = visibleInput({ name: "otp" });103 const verification = visibleInput({ placeholder: "Verification code" });104 const zipCode = visibleInput({ name: "zipcode" });105106 expect(isTOTPField(oneTimeCode)).toBe(true);107 expect(isTOTPField(otpName)).toBe(true);108 expect(isTOTPField(verification)).toBe(true);109 expect(isTOTPField(zipCode)).toBe(false);110 });111112 it("isTOTPField detects an adjacent 2FA label", () => {113 const label = document.createElement("label");114 label.textContent = "2FA Code";115 document.body.appendChild(label);116 const input = visibleInput({ placeholder: "If enabled" });117118 expect(isTOTPField(input)).toBe(true);119 });120121 122 123 124 125 it("isTOTPField ignores type=number split-OTP digit boxes", () => {126 for (let i = 0; i < 6; i++) {127 const box = visibleInput({ type: "number", maxlength: "1", name: "otp" });128 expect(isTOTPField(box)).toBe(false);129 }130 });131132 it("detectLoginFields skips type=number split-OTP digit boxes", () => {133 for (let i = 0; i < 6; i++) {134 visibleInput({ type: "number", maxlength: "1", name: "otp" });135 }136 expect(detectLoginFields(document)).toEqual([]);137 });138139 140 141 it("field_detection_does_not_throw_on_id_with_css_special_chars", () => {142 const label = document.createElement("label");143 label.setAttribute("for", "user[email]");144 label.textContent = "Email";145 document.body.appendChild(label);146 const email = visibleInput({ type: "email", name: "email", id: "user[email]" });147 visibleInput({ type: "password", autocomplete: "current-password" });148149 let fields: ReturnType<typeof detectLoginFields> = [];150 expect(() => {151 fields = detectLoginFields(document);152 }).not.toThrow();153 expect(fields.some((field) => field.el === email)).toBe(true);154 });155156 it("field_detection_does_not_throw_on_id_with_quotes", () => {157 const email = visibleInput({ type: "email", name: "email", id: "it's-me" });158 visibleInput({ type: "password", autocomplete: "current-password" });159160 let fields: ReturnType<typeof detectLoginFields> = [];161 expect(() => {162 fields = detectLoginFields(document);163 }).not.toThrow();164 expect(fields.some((field) => field.el === email)).toBe(true);165 });166});167168describe("card form detection", () => {169 beforeEach(() => {170 document.body.innerHTML = "";171 });172173 it("detectCardForm — finds number field by autocomplete attribute", () => {174 document.body.innerHTML = '<input autocomplete="cc-number" /><input autocomplete="cc-exp-month" /><input autocomplete="cc-exp-year" /><input autocomplete="cc-csc" />';175 const result = detectCardForm(document);176 expect(result).not.toBeNull();177 expect(result!.number).toBeTruthy();178 expect(result!.expMonth).toBeTruthy();179 });180181 it("detectCardForm — finds fields by name/id heuristics when no autocomplete", () => {182 document.body.innerHTML = '<input name="cardnum" /><input name="expiry" /><input name="cvv" />';183 const result = detectCardForm(document);184 expect(result).not.toBeNull();185 expect(result!.number).toBeTruthy();186 });187188 it("detectCardForm — returns null when no card number field found", () => {189 document.body.innerHTML = '<input type="text" name="username" />';190 expect(detectCardForm(document)).toBeNull();191 });192193 it("hasPaymentProcessorIframe — returns true when Stripe iframe present", () => {194 document.body.innerHTML = '<iframe src="https://js.stripe.com/v3/"></iframe>';195 expect(hasPaymentProcessorIframe()).toBe(true);196 });197198 it("hasPaymentProcessorIframe — returns false on standard checkout page", () => {199 document.body.innerHTML = '<input name="cardnum" />';200 expect(hasPaymentProcessorIframe()).toBe(false);201 });202});