detect.test.ts

6.7 KB · raw

1// @vitest-environment jsdom23import { 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  // PCH-01-025: ids may legally contain CSS-special chars; field detection122  // must not throw a SyntaxError out of the label[for=...] lookup.123  it("field_detection_does_not_throw_on_id_with_css_special_chars", () => {124    const label = document.createElement("label");125    label.setAttribute("for", "user[email]");126    label.textContent = "Email";127    document.body.appendChild(label);128    const email = visibleInput({ type: "email", name: "email", id: "user[email]" });129    visibleInput({ type: "password", autocomplete: "current-password" });130131    let fields: ReturnType<typeof detectLoginFields> = [];132    expect(() => {133      fields = detectLoginFields(document);134    }).not.toThrow();135    expect(fields.some((field) => field.el === email)).toBe(true);136  });137138  it("field_detection_does_not_throw_on_id_with_quotes", () => {139    const email = visibleInput({ type: "email", name: "email", id: "it's-me" });140    visibleInput({ type: "password", autocomplete: "current-password" });141142    let fields: ReturnType<typeof detectLoginFields> = [];143    expect(() => {144      fields = detectLoginFields(document);145    }).not.toThrow();146    expect(fields.some((field) => field.el === email)).toBe(true);147  });148});149150describe("card form detection", () => {151  beforeEach(() => {152    document.body.innerHTML = "";153  });154155  it("detectCardForm — finds number field by autocomplete attribute", () => {156    document.body.innerHTML = '<input autocomplete="cc-number" /><input autocomplete="cc-exp-month" /><input autocomplete="cc-exp-year" /><input autocomplete="cc-csc" />';157    const result = detectCardForm(document);158    expect(result).not.toBeNull();159    expect(result!.number).toBeTruthy();160    expect(result!.expMonth).toBeTruthy();161  });162163  it("detectCardForm — finds fields by name/id heuristics when no autocomplete", () => {164    document.body.innerHTML = '<input name="cardnum" /><input name="expiry" /><input name="cvv" />';165    const result = detectCardForm(document);166    expect(result).not.toBeNull();167    expect(result!.number).toBeTruthy();168  });169170  it("detectCardForm — returns null when no card number field found", () => {171    document.body.innerHTML = '<input type="text" name="username" />';172    expect(detectCardForm(document)).toBeNull();173  });174175  it("hasPaymentProcessorIframe — returns true when Stripe iframe present", () => {176    document.body.innerHTML = '<iframe src="https://js.stripe.com/v3/"></iframe>';177    expect(hasPaymentProcessorIframe()).toBe(true);178  });179180  it("hasPaymentProcessorIframe — returns false on standard checkout page", () => {181    document.body.innerHTML = '<input name="cardnum" />';182    expect(hasPaymentProcessorIframe()).toBe(false);183  });184});