content.field-icon.test.ts

4.9 KB · raw

1// @vitest-environment jsdom2//3// Test File 6 — inject.ts injectFieldIcon (T-IC01–T-IC08).4//5// injectFieldIcon renders the small peach button into the LIGHT DOM (appended to6// document.body, position:fixed), so unlike the selector it is fully observable7// from a unit test. We import the REAL inject.ts and assert on the button it8// produces: presence, accessibility, click wiring, teardown, and the9// too-narrow-field hide rule. Where the brief assumed behavior that lives10// elsewhere (idempotency, autocomplete policy), it is documented, not faked.1112import "./_setup"; // chrome mock + ResizeObserver polyfill (observeAnchorPosition needs it)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  // T-IC01: the icon button is injected into the document for a normal field.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  // T-IC08: the button carries an aria-label and title for screen readers.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  // T-IC02: clicking the icon invokes the onClick callback (which, in index.ts,50  // is what summons the selector).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  // T-IC04: the returned cleanup removes the button — index.ts calls it when the64  // field leaves the DOM, so a stale icon never lingers.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  // T-IC06: on a field too narrow to host the icon cleanly (<40px wide), the76  // button is hidden (opacity 0, pointer-events none) so it never overlaps a77  // tiny input or an adjacent control.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    // Read synchronously, before the post-mount requestAnimationFrame fires.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); // 160×32 → wide enough95    const cleanup = injectFieldIcon(field, () => {});96    const btn = document.querySelector<HTMLButtonElement>(ICON_SELECTOR)!;97    expect(btn.style.opacity).toBe("1");98    cleanup();99  });100});101102describe("T-IC03: renderer idempotency boundary", () => {103  // T-IC03: injectFieldIcon is NOT idempotent on its own — each call appends a104  // button. Idempotency is enforced by the caller (index.ts processedInputs),105  // covered by content.initial-scan.test.ts T-S10. We pin the real behavior so106  // the contract is explicit.107  it("T-IC03: injectFieldIcon itself is not idempotent (dedup is the caller's job)", () => {108    const field = makeInput({ type: "password" });109    const c1 = injectFieldIcon(field, () => {});110    const c2 = injectFieldIcon(field, () => {});111    expect(document.querySelectorAll(ICON_SELECTOR).length).toBe(2);112    c1();113    c2();114  });115});