content.interaction.test.ts

5.3 KB · raw

1// @vitest-environment jsdom2//3// Test File 12 — interaction.ts target resolution (T-IT01–T-IT06).4//5// resolveAutofillInteractionTargets is the small, pure decision that turns a set6// of detected fields + the clicked anchor into "what do we fill, and do we even7// open?". It is deliberately narrow: it does NOT scope by form (the caller hands8// it a pre-scoped field list) and it does NOT inspect the live DOM. The tests9// below pin its real contract; where the brief assumed broader behavior10// (form-scoping, email-first shape), the deviation is noted inline.1112import { beforeEach, describe, expect, it } from "vitest";1314import type { DetectedField } from "./detect";15import { resolveAutofillInteractionTargets } from "./interaction";16import { makeInput } from "./_setup";1718beforeEach(() => {19  document.body.innerHTML = "";20});2122function field(type: DetectedField["type"], el: HTMLInputElement): DetectedField {23  return { type, el };24}2526describe("resolveAutofillInteractionTargets", () => {27  // T-IT01: a standard form resolves both the username and password elements and28  // opens.29  it("T-IT01: standard form → {usernameEl, passwordEl} and shouldOpen", () => {30    const u = makeInput({ type: "text" });31    const p = makeInput({ type: "password" });32    const targets = resolveAutofillInteractionTargets(p, [field("username", u), field("password", p)]);33    expect(targets.usernameEl).toBe(u);34    expect(targets.passwordEl).toBe(p);35    expect(targets.emailEl).toBeNull();36    expect(targets.shouldOpen).toBe(true);37  });3839  // T-IT02: email-first step has no password. The function treats a lone email40  // as the identity field (usernameEl), so the caller fills entry.username into41  // it; emailEl stays null because there is no *separate* username field. (The42  // brief's {emailEl, passwordEl:null} shape predates this consolidation.)43  it("T-IT02: email-first form → email becomes usernameEl, passwordEl null", () => {44    const e = makeInput({ type: "email" });45    const targets = resolveAutofillInteractionTargets(e, [field("email", e)]);46    expect(targets.usernameEl).toBe(e);47    expect(targets.passwordEl).toBeNull();48    expect(targets.emailEl).toBeNull();49    // shouldOpen depends on isAnyCredentialForm; an email field with no auth50    // context on a "/" path is not yet a credential form, so passive open is51    // gated. An explicit summon overrides (next test).52  });5354  // T-IT02b: distinct username AND email → both tracked separately.55  it("T-IT02b: distinct username + email → emailEl is tracked alongside usernameEl", () => {56    const u = makeInput({ type: "text", autocomplete: "username" });57    const e = makeInput({ type: "email", autocomplete: "email" });58    const p = makeInput({ type: "password" });59    const targets = resolveAutofillInteractionTargets(60      u,61      [field("username", u), field("email", e), field("password", p)]62    );63    expect(targets.usernameEl).toBe(u);64    expect(targets.emailEl).toBe(e);65    expect(targets.passwordEl).toBe(p);66  });6768  // T-IT03: password-only step → passwordEl set, usernameEl null, and it opens69  // (isPasswordStepForm).70  it("T-IT03: password-only form → passwordEl set, usernameEl null, shouldOpen", () => {71    const p = makeInput({ type: "password", autocomplete: "current-password" });72    const targets = resolveAutofillInteractionTargets(p, [field("password", p)]);73    expect(targets.passwordEl).toBe(p);74    expect(targets.usernameEl).toBeNull();75    expect(targets.shouldOpen).toBe(true);76  });7778  // T-IT04: no qualifying fields → do not open (passive).79  it("T-IT04: empty field set + passive → shouldOpen false", () => {80    const anchor = makeInput({ type: "text" });81    const targets = resolveAutofillInteractionTargets(anchor, []);82    expect(targets.shouldOpen).toBe(false);83    expect(targets.usernameEl).toBeNull();84    expect(targets.passwordEl).toBeNull();85  });8687  // T-IT05: the function resolves targets from the field list it is GIVEN — form88  // scoping is the caller's job (index.ts calls detectLoginFields per the focused89  // field's context). With explicitSummon, a bare anchor still resolves as a90  // fallback target so an icon click always opens something.91  it("T-IT05: explicit summon on a bare password anchor falls back to the anchor", () => {92    const anchor = makeInput({ type: "password" });93    const targets = resolveAutofillInteractionTargets(anchor, [], true);94    expect(targets.shouldOpen).toBe(true);95    expect(targets.passwordEl).toBe(anchor);96  });9798  it("T-IT05b: explicit summon on a bare username anchor falls back to the anchor", () => {99    const anchor = makeInput({ type: "text" });100    const targets = resolveAutofillInteractionTargets(anchor, [], true);101    expect(targets.shouldOpen).toBe(true);102    expect(targets.usernameEl).toBe(anchor);103  });104105  // T-IT06: a detached anchor with no detected fields resolves to a non-opening106  // result rather than throwing — the graceful-abort path index.ts relies on.107  it("T-IT06: detached anchor + empty fields → shouldOpen false, no throw", () => {108    const detached = document.createElement("input");109    detached.type = "text"; // never appended to the document110    let targets!: ReturnType<typeof resolveAutofillInteractionTargets>;111    expect(() => { targets = resolveAutofillInteractionTargets(detached, []); }).not.toThrow();112    expect(targets.shouldOpen).toBe(false);113  });114});