123456789101112import { beforeEach, describe, expect, it, vi } from "vitest";1314import {15 detectCardNetwork,16 extractLast4,17 fillCardForm,18 fillFields,19 fillTOTPCode,20 formatCardNumber,21 passesLuhn,22 setNativeValue,23} from "./fill";24import { makeInput } from "./_setup";2526beforeEach(() => {27 document.body.innerHTML = "";28});2930describe("T-FL01–T-FL03: fillFields + event dispatch", () => {31 32 it("T-FL01: fills username and password into standard inputs", () => {33 const u = makeInput({ type: "text" });34 const p = makeInput({ type: "password" });35 fillFields(u, p, "alice", "s3cret");36 expect(u.value).toBe("alice");37 expect(p.value).toBe("s3cret");38 });3940 41 it("T-FL02: dispatches an input event on filled fields", () => {42 const p = makeInput({ type: "password" });43 const onInput = vi.fn();44 p.addEventListener("input", onInput);45 fillFields(null, p, "", "pw");46 expect(onInput).toHaveBeenCalledTimes(1);47 });4849 50 it("T-FL03: dispatches a change event on filled fields", () => {51 const p = makeInput({ type: "password" });52 const onChange = vi.fn();53 p.addEventListener("change", onChange);54 fillFields(null, p, "", "pw");55 expect(onChange).toHaveBeenCalledTimes(1);56 });57});5859describe("T-FL04–T-FL05: framework-controlled inputs (setNativeValue)", () => {60 61 62 63 64 65 it("T-FL04: setNativeValue bypasses a React-style instance setter via the prototype setter", () => {66 const el = makeInput({ type: "text" });67 let trackerSawValue: string | null = null;68 el.addEventListener("input", () => { trackerSawValue = el.value; });6970 71 72 73 74 const protoDesc = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")!;75 let instanceSetterCalled = false;76 Object.defineProperty(el, "value", {77 configurable: true,78 get() { return protoDesc.get!.call(el); },79 set() { instanceSetterCalled = true; },80 });8182 setNativeValue(el, "from-peach");8384 expect(instanceSetterCalled).toBe(false); 85 expect(el.value).toBe("from-peach"); 86 expect(trackerSawValue).toBe("from-peach"); 87 });8889 90 it("T-FL05: setNativeValue emits input then change for Vue-style v-model", () => {91 const el = makeInput({ type: "text" });92 const seq: string[] = [];93 el.addEventListener("input", () => seq.push("input"));94 el.addEventListener("change", () => seq.push("change"));95 setNativeValue(el, "x");96 expect(seq).toEqual(["input", "change"]);97 });98});99100describe("T-FL06: cross-origin / wrong-domain guard", () => {101 102 103 104 105 it("T-FL06: form action on a foreign domain → fields are not filled, no crash", () => {106 const formEl = document.createElement("form");107 formEl.action = "https://evil.example.org/collect";108 document.body.appendChild(formEl);109 const u = makeInput({ type: "text" }, formEl);110 const p = makeInput({ type: "password" }, formEl);111 expect(() =>112 fillFields(u, p, "alice", "s3cret", formEl, "mybank.example.com")113 ).not.toThrow();114 expect(u.value).toBe("");115 expect(p.value).toBe("");116 });117});118119describe("T-FL07–T-FL09: card fill + validation", () => {120 121 it("T-FL07: fills number, expiry combo, cvc, and cardholder name", () => {122 const number = makeInput({ autocomplete: "cc-number" });123 const expiryCombo = makeInput({ autocomplete: "cc-exp" });124 const cvc = makeInput({ autocomplete: "cc-csc" });125 const name = makeInput({ autocomplete: "cc-name" });126 fillCardForm(127 { number, expMonth: null, expYear: null, expiryCombo, cvc, name },128 { cardNumber: "4111111111111111", expMonth: "08", expYear: "2027", cvc: "123", cardholderName: "Alice Wong" }129 );130 expect(number.value).toBe("4111 1111 1111 1111"); 131 expect(expiryCombo.value).toBe("08/27");132 expect(cvc.value).toBe("123");133 expect(name.value).toBe("Alice Wong");134 });135136 137 it("T-FL08: passesLuhn accepts valid PANs and rejects invalid ones", () => {138 expect(passesLuhn("4111111111111111")).toBe(true); 139 expect(passesLuhn("4111 1111 1111 1111")).toBe(true); 140 expect(passesLuhn("4111111111111112")).toBe(false); 141 });142143 144 it("T-FL09: detectCardNetwork keys off the issuer prefix", () => {145 expect(detectCardNetwork("4111111111111111")).toBe("visa");146 expect(detectCardNetwork("5500005555555559")).toBe("mastercard");147 expect(detectCardNetwork("340000000000009")).toBe("amex");148 expect(detectCardNetwork("6011000000000004")).toBe("discover");149 expect(detectCardNetwork("9999999999999999")).toBe("unknown");150 });151152 it("T-FL09b: amex (15-digit) numbers format in 4-6-5 groups", () => {153 expect(formatCardNumber("340000000000009")).toBe("3400 000000 00009");154 expect(extractLast4("3400 000000 00009")).toBe("0009");155 });156});157158describe("T-FL10–T-FL12: TOTP fill, preservation, and the return contract", () => {159 160 it("T-FL10: fillTOTPCode inserts the code into an empty visible field", () => {161 const totp = makeInput({ type: "text", autocomplete: "one-time-code" });162 expect(fillTOTPCode(totp, "123456")).toBe(true);163 expect(totp.value).toBe("123456");164 });165166 it("T-FL10b: fillTOTPCode refuses to overwrite a non-empty field", () => {167 const totp = makeInput({ type: "text", autocomplete: "one-time-code" });168 setNativeValue(totp, "000000");169 expect(fillTOTPCode(totp, "123456")).toBe(false);170 expect(totp.value).toBe("000000");171 });172173 174 175 it("T-FL11: fields not passed to fillFields are left untouched", () => {176 const u = makeInput({ type: "text" });177 const p = makeInput({ type: "password" });178 const other = makeInput({ type: "text" });179 setNativeValue(other, "do-not-touch");180 fillFields(u, p, "alice", "s3cret");181 expect(other.value).toBe("do-not-touch");182 });183184 185 186 187 188 it("T-FL12: fillFields reports gate permission and skips an invisible password field", () => {189 const u = makeInput({ type: "text" });190 const p = makeInput({ type: "password" });191 p.style.display = "none"; 192 const result = fillFields(u, p, "alice", "s3cret");193 expect(result).toBe(true);194 expect(u.value).toBe("alice");195 expect(p.value).toBe(""); 196 });197198 it("T-FL12b: fillFields reports false when the form-action gate refuses", () => {199 const form = document.createElement("form");200 form.action = "https://evil.example/login";201 document.body.appendChild(form);202 const u = makeInput({ type: "text" }, form);203 const p = makeInput({ type: "password" }, form);204 expect(fillFields(u, p, "alice", "s3cret", form, "localhost")).toBe(false);205 expect(u.value).toBe("");206 expect(p.value).toBe("");207 });208});