123456789101112import { vi } from "vitest";1314const { injectIcon } = vi.hoisted(() => ({ injectIcon: vi.fn(() => vi.fn()) }));1516vi.mock("./inject", () => ({17 injectFieldIcon: injectIcon,18 showAutofillSelector: vi.fn(() => vi.fn()),19 showTOTPSelector: vi.fn(() => vi.fn()),20 showCardSelector: vi.fn(() => vi.fn()),21 showRememberSiteToast: vi.fn(),22 showPasskeyAvailable: vi.fn(),23 showPasskeyProGate: vi.fn(),24 showPasskeyRegistrationOffer: vi.fn(),25}));26vi.mock("./content-ml-detector", () => ({27 contentMLDetector: { predictBatch: vi.fn(() => Promise.resolve([])) },28}));2930import { afterEach, beforeEach, describe, expect, it } from "vitest";31import "./_setup";32import { resetChromeMock, makeInput, createLoginForm, createEmailFirstForm } from "./_setup";33import { initialScan } from "./index";3435363738beforeEach(() => {39 document.body.innerHTML = "";40 resetChromeMock();41 injectIcon.mockClear();42});4344afterEach(async () => {45 await new Promise((r) => setTimeout(r, 0));46});4748describe("T-S01–T-S02: level 0 is conservative", () => {49 50 it("T-S01: no forms at all + level 0 → no icon", () => {51 initialScan(0);52 expect(injectIcon).not.toHaveBeenCalled();53 });5455 56 it("T-S02: non-matching form + level 0 → no icon", () => {57 const f = document.createElement("form");58 document.body.appendChild(f);59 makeInput({ type: "search", name: "q", placeholder: "Search" }, f);60 initialScan(0);61 expect(injectIcon).not.toHaveBeenCalled();62 });63});6465describe("T-S03–T-S06: icons get injected as aggression rises", () => {66 67 68 it("T-S03: standard login form + level 1 → icon injected", () => {69 createLoginForm();70 initialScan(1);71 expect(injectIcon).toHaveBeenCalled();72 });7374 75 it("T-S04: email-first form + level 1 → icon injected", () => {76 createEmailFirstForm();77 initialScan(1);78 expect(injectIcon).toHaveBeenCalled();79 });8081 82 83 84 it("T-S05: bare password + level 2 final-pass → icon injected", () => {85 makeInput({ type: "password", autocomplete: "current-password" });86 initialScan(2, true);87 expect(injectIcon).toHaveBeenCalled();88 });8990 91 92 it("T-S07: two login forms → an icon per form's password field", () => {93 createLoginForm();94 createLoginForm();95 initialScan(1);96 97 98 expect(injectIcon.mock.calls.length).toBeGreaterThanOrEqual(2);99 });100});101102describe("T-S08–T-S10: visibility, dynamic forms, idempotency", () => {103 104 105 it("T-S08: hidden (display:none) form → no icon", () => {106 const f = document.createElement("form");107 f.style.display = "none";108 document.body.appendChild(f);109 const u = makeInput({ type: "text", name: "username", autocomplete: "username" }, f);110 const p = makeInput({ type: "password", autocomplete: "current-password" }, f);111 112 113 for (const el of [u, p]) {114 Object.defineProperty(el, "getBoundingClientRect", {115 configurable: true,116 value: () => ({ width: 0, height: 0, top: 0, left: 0, right: 0, bottom: 0, x: 0, y: 0, toJSON: () => ({}) }),117 });118 }119 initialScan(1);120 expect(injectIcon).not.toHaveBeenCalled();121 });122123 124 125 126 127 it("T-S09: a form inserted after the first scan is picked up on rescan", () => {128 initialScan(1);129 expect(injectIcon).not.toHaveBeenCalled(); 130 createLoginForm(); 131 initialScan(1);132 expect(injectIcon).toHaveBeenCalled();133 });134135 136 137 it("T-S10: scanning the same form twice → icon injected once per field", () => {138 createLoginForm();139 initialScan(1);140 const afterFirst = injectIcon.mock.calls.length;141 initialScan(1);142 expect(injectIcon.mock.calls.length).toBe(afterFirst); 143 });144});