123456789101112import "./_setup"; 13import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";14import { injectFieldIcon } from "./inject";15import { makeInput, makeFieldVisible } from "./_setup";1617const ICON_SELECTOR = 'button[aria-label="Peach autofill"]';1819beforeEach(() => {20 document.body.innerHTML = "";21});22afterEach(() => {23 document.body.innerHTML = "";24});2526describe("T-IC01, T-IC08: rendering and accessibility", () => {27 28 it("T-IC01: injects a position:fixed button into the document", () => {29 const field = makeInput({ type: "password" });30 const cleanup = injectFieldIcon(field, () => {});31 const btn = document.querySelector<HTMLButtonElement>(ICON_SELECTOR);32 expect(btn).not.toBeNull();33 expect(btn!.style.position).toBe("fixed");34 cleanup();35 });3637 38 it("T-IC08: button has an aria-label and title", () => {39 const field = makeInput({ type: "password" });40 const cleanup = injectFieldIcon(field, () => {});41 const btn = document.querySelector<HTMLButtonElement>(ICON_SELECTOR)!;42 expect(btn.getAttribute("aria-label")).toBe("Peach autofill");43 expect(btn.title).toBeTruthy();44 cleanup();45 });46});4748describe("T-IC02: click opens the selector", () => {49 50 51 it("T-IC02: clicking the icon calls onClick exactly once", () => {52 const field = makeInput({ type: "password" });53 const onClick = vi.fn();54 const cleanup = injectFieldIcon(field, onClick);55 const btn = document.querySelector<HTMLButtonElement>(ICON_SELECTOR)!;56 btn.dispatchEvent(new MouseEvent("click", { bubbles: true }));57 expect(onClick).toHaveBeenCalledTimes(1);58 cleanup();59 });60});6162describe("T-IC04: teardown", () => {63 64 65 it("T-IC04: cleanup() removes the injected button", () => {66 const field = makeInput({ type: "password" });67 const cleanup = injectFieldIcon(field, () => {});68 expect(document.querySelector(ICON_SELECTOR)).not.toBeNull();69 cleanup();70 expect(document.querySelector(ICON_SELECTOR)).toBeNull();71 });72});7374describe("T-IC06: size / narrow-field hide rule", () => {75 76 77 78 it("T-IC06: a <40px-wide field hides the icon", () => {79 const field = makeInput({ type: "password" });80 Object.defineProperty(field, "getBoundingClientRect", {81 configurable: true,82 value: () => ({ width: 30, height: 24, top: 10, left: 10, right: 40, bottom: 34, x: 10, y: 10, toJSON: () => ({}) }),83 });84 const cleanup = injectFieldIcon(field, () => {});85 const btn = document.querySelector<HTMLButtonElement>(ICON_SELECTOR)!;86 87 expect(btn.style.opacity).toBe("0");88 expect(btn.style.pointerEvents).toBe("none");89 cleanup();90 });9192 it("T-IC06b: a normally-wide field shows the icon", () => {93 const field = makeInput({ type: "password" });94 makeFieldVisible(field); 95 const cleanup = injectFieldIcon(field, () => {});96 const btn = document.querySelector<HTMLButtonElement>(ICON_SELECTOR)!;97 expect(btn.style.opacity).toBe("1");98 cleanup();99 });100});101102103describe("T-IC03 / T-IC05 / T-IC07: documented boundaries", () => {104 105 106 107 108 it("T-IC03: injectFieldIcon itself is not idempotent (dedup is the caller's job)", () => {109 const field = makeInput({ type: "password" });110 const c1 = injectFieldIcon(field, () => {});111 const c2 = injectFieldIcon(field, () => {});112 expect(document.querySelectorAll(ICON_SELECTOR).length).toBe(2);113 c1();114 c2();115 });116117 118 119 120 121 it.todo("T-IC05: icon repositions on scroll (e2e — needs real layout)");122123 124 125 126 127 it.todo("T-IC07: suppress on autocomplete=current-password (no such policy in inject today)");128});