123import { beforeEach, describe, expect, it, vi } from "vitest";45import { fillFields, fillCardForm, fillTOTPCode, formatCardNumber } from "./fill";67describe("content fill", () => {8 beforeEach(() => {9 document.body.innerHTML = "";10 });1112 function makeVisibleInput(type: string): HTMLInputElement {13 const input = document.createElement("input");14 input.type = type;15 Object.defineProperty(input, "getBoundingClientRect", {16 configurable: true,17 value: () => ({18 width: 100,19 height: 20,20 top: 0,21 left: 0,22 right: 100,23 bottom: 20,24 x: 0,25 y: 0,26 toJSON: () => ({}),27 }),28 });29 document.body.appendChild(input);30 return input;31 }3233 it("fillFields writes username and password into matching inputs", () => {34 const username = makeVisibleInput("text");35 const password = makeVisibleInput("password");3637 fillFields(username, password, "june", "hunter2");3839 expect(username.value).toBe("june");40 expect(password.value).toBe("hunter2");41 });4243 it("fillFields refuses to fill when the form action domain does not match", () => {44 const form = document.createElement("form");45 form.action = "https://evil.com/login";46 const username = makeVisibleInput("text");47 const password = makeVisibleInput("password");48 form.append(username, password);49 document.body.appendChild(form);5051 fillFields(username, password, "june", "hunter2", form, "github.com");5253 expect(username.value).toBe("");54 expect(password.value).toBe("");55 });5657 58 59 it("fillFields refuses a plain-http form action (PB-26-008)", () => {60 const form = document.createElement("form");61 form.action = "http://github.com/login";62 const username = makeVisibleInput("text");63 const password = makeVisibleInput("password");64 form.append(username, password);65 document.body.appendChild(form);6667 fillFields(username, password, "june", "hunter2", form, "github.com");6869 expect(username.value).toBe("");70 expect(password.value).toBe("");71 });7273 it("fillFields allows an https form action on a subdomain of the expected domain (PB-26-008)", () => {74 const form = document.createElement("form");75 form.action = "https://auth.github.com/session";76 const username = makeVisibleInput("text");77 const password = makeVisibleInput("password");78 form.append(username, password);79 document.body.appendChild(form);8081 fillFields(username, password, "june", "hunter2", form, "github.com");8283 expect(username.value).toBe("june");84 expect(password.value).toBe("hunter2");85 });8687 it("fillFields still fills the username when the username field is hidden", () => {88 const username = makeVisibleInput("text");89 const password = makeVisibleInput("password");90 Object.defineProperty(username, "getBoundingClientRect", {91 value: () => ({92 width: 0,93 height: 0,94 top: 0,95 left: 0,96 right: 0,97 bottom: 0,98 x: 0,99 y: 0,100 toJSON: () => ({}),101 }),102 });103104 fillFields(username, password, "june", "hunter2");105106 expect(username.value).toBe("june");107 expect(password.value).toBe("hunter2");108 });109110 it("fillTOTPCode respects the form-action gate (PB-26-105)", () => {111 const form = document.createElement("form");112 form.action = "https://evil.com/login";113 const totpEl = makeVisibleInput("text");114 form.appendChild(totpEl);115 document.body.appendChild(form);116117 const filled = fillTOTPCode(totpEl, "123456", form, "github.com");118119 expect(filled).toBe(false);120 expect(totpEl.value).toBe("");121 });122123 it("fillTOTPCode allows https form action on expected domain (PB-26-105)", () => {124 const form = document.createElement("form");125 form.action = "https://github.com/login";126 const totpEl = makeVisibleInput("text");127 form.appendChild(totpEl);128 document.body.appendChild(form);129130 const filled = fillTOTPCode(totpEl, "123456", form, "github.com");131132 expect(filled).toBe(true);133 expect(totpEl.value).toBe("123456");134 });135136 it("fillFields dispatches standard input/change events rather than custom password events", () => {137 const username = makeVisibleInput("text");138 const password = makeVisibleInput("password");139 const usernameDispatch = vi.spyOn(username, "dispatchEvent");140 const passwordDispatch = vi.spyOn(password, "dispatchEvent");141142 fillFields(username, password, "june", "hunter2");143144 expect(usernameDispatch.mock.calls.map(([event]) => event.type)).toEqual(["input", "change"]);145 expect(passwordDispatch.mock.calls.map(([event]) => event.type)).toEqual(["input", "change"]);146 expect(passwordDispatch.mock.calls.every(([event]) => !(event instanceof CustomEvent))).toBe(true);147 });148});149150describe("card fill", () => {151 beforeEach(() => {152 document.body.innerHTML = "";153 });154155 function makeInput(attrs: Record<string, string> = {}): HTMLInputElement {156 const input = document.createElement("input");157 for (const [k, v] of Object.entries(attrs)) input.setAttribute(k, v);158 159 160 Object.defineProperty(input, "getBoundingClientRect", {161 configurable: true,162 value: () => ({163 width: 100,164 height: 20,165 top: 0,166 left: 0,167 right: 100,168 bottom: 20,169 x: 0,170 y: 0,171 toJSON: () => ({}),172 }),173 });174 document.body.appendChild(input);175 return input;176 }177178 function makeHiddenInput(attrs: Record<string, string> = {}): HTMLInputElement {179 const input = makeInput(attrs);180 Object.defineProperty(input, "getBoundingClientRect", {181 configurable: true,182 value: () => ({183 width: 0,184 height: 0,185 top: 0,186 left: 0,187 right: 0,188 bottom: 0,189 x: 0,190 y: 0,191 toJSON: () => ({}),192 }),193 });194 return input;195 }196197 198 199 200 it("fillCardForm — skips hidden fields (PB-26-007)", () => {201 const number = makeHiddenInput({ type: "text" });202 const cvc = makeHiddenInput({ type: "text" });203 const name = makeInput({ type: "text" });204205 fillCardForm(206 { number, expMonth: null, expYear: null, expiryCombo: null, cvc, name },207 { cardNumber: "4111111111111111", expMonth: "05", expYear: "2028", cvc: "123", cardholderName: "Jane Smith" }208 );209210 expect(number.value).toBe("");211 expect(cvc.value).toBe("");212 expect(name.value).toBe("Jane Smith");213 });214215 it("fillCardForm — fills number, expiry, CVC, and name fields", () => {216 const number = makeInput({ type: "text" });217 const expMonth = makeInput({ type: "text" });218 const expYear = makeInput({ type: "text" });219 const cvc = makeInput({ type: "text" });220 const name = makeInput({ type: "text" });221222 fillCardForm(223 { number, expMonth, expYear, expiryCombo: null, cvc, name },224 { cardNumber: "4111111111111111", expMonth: "05", expYear: "2028", cvc: "123", cardholderName: "Jane Smith" }225 );226227 expect(number.value).toBe("4111 1111 1111 1111");228 expect(expMonth.value).toBe("05");229 expect(expYear.value).toBe("2028");230 expect(cvc.value).toBe("123");231 expect(name.value).toBe("Jane Smith");232 });233234 it("fillCardForm — handles combined expiry field (MM/YY)", () => {235 const number = makeInput({ type: "text" });236 const expiryCombo = makeInput({ type: "text" });237 const cvc = makeInput({ type: "text" });238239 fillCardForm(240 { number, expMonth: null, expYear: null, expiryCombo, cvc, name: null },241 { cardNumber: "5555555555554444", expMonth: "12", expYear: "2026", cvc: "456", cardholderName: "John Doe" }242 );243244 expect(expiryCombo.value).toBe("12/26");245 });246247 it("fillCardForm — dispatches input and change events for React compatibility", () => {248 const number = makeInput({ type: "text" });249 const numberDispatch = vi.spyOn(number, "dispatchEvent");250251 fillCardForm(252 { number, expMonth: null, expYear: null, expiryCombo: null, cvc: null, name: null },253 { cardNumber: "4111111111111111", expMonth: "05", expYear: "2028", cvc: "123", cardholderName: "Jane" }254 );255256 expect(numberDispatch.mock.calls.map(([event]) => event.type)).toEqual(["input", "change"]);257 });258259 it("formatCardNumber — formats Visa as 4-4-4-4", () => {260 expect(formatCardNumber("4111111111111111")).toBe("4111 1111 1111 1111");261 });262263 it("formatCardNumber — formats Amex as 4-6-5", () => {264 expect(formatCardNumber("378282246310005")).toBe("3782 822463 10005");265 });266});