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 and in11// INTEGRATION-CONTRACT.md.1213import { beforeEach, describe, expect, it } from "vitest";1415import type { DetectedField } from "./detect";16import { resolveAutofillInteractionTargets } from "./interaction";17import { makeInput } from "./_setup";1819beforeEach(() => {20  document.body.innerHTML = "";21});2223function field(type: DetectedField["type"], el: HTMLInputElement): DetectedField {24  return { type, el };25}2627describe("resolveAutofillInteractionTargets", () => {28  // T-IT01: a standard form resolves both the username and password elements and29  // opens.30  it("T-IT01: standard form → {usernameEl, passwordEl} and shouldOpen", () => {31    const u = makeInput({ type: "text" });32    const p = makeInput({ type: "password" });33    const targets = resolveAutofillInteractionTargets(p, [field("username", u), field("password", p)]);34    expect(targets.usernameEl).toBe(u);35    expect(targets.passwordEl).toBe(p);36    expect(targets.emailEl).toBeNull();37    expect(targets.shouldOpen).toBe(true);38  });3940  // T-IT02: email-first step has no password. The function treats a lone email41  // as the identity field (usernameEl), so the caller fills entry.username into42  // it; emailEl stays null because there is no *separate* username field. (The43  // brief's {emailEl, passwordEl:null} shape predates this consolidation.)44  it("T-IT02: email-first form → email becomes usernameEl, passwordEl null", () => {45    const e = makeInput({ type: "email" });46    const targets = resolveAutofillInteractionTargets(e, [field("email", e)]);47    expect(targets.usernameEl).toBe(e);48    expect(targets.passwordEl).toBeNull();49    expect(targets.emailEl).toBeNull();50    // shouldOpen depends on isAnyCredentialForm; an email field with no auth51    // context on a "/" path is not yet a credential form, so passive open is52    // gated. An explicit summon overrides (next test).53  });5455  // T-IT02b: distinct username AND email → both tracked separately.56  it("T-IT02b: distinct username + email → emailEl is tracked alongside usernameEl", () => {57    const u = makeInput({ type: "text", autocomplete: "username" });58    const e = makeInput({ type: "email", autocomplete: "email" });59    const p = makeInput({ type: "password" });60    const targets = resolveAutofillInteractionTargets(61      u,62      [field("username", u), field("email", e), field("password", p)]63    );64    expect(targets.usernameEl).toBe(u);65    expect(targets.emailEl).toBe(e);66    expect(targets.passwordEl).toBe(p);67  });6869  // T-IT03: password-only step → passwordEl set, usernameEl null, and it opens70  // (isPasswordStepForm).71  it("T-IT03: password-only form → passwordEl set, usernameEl null, shouldOpen", () => {72    const p = makeInput({ type: "password", autocomplete: "current-password" });73    const targets = resolveAutofillInteractionTargets(p, [field("password", p)]);74    expect(targets.passwordEl).toBe(p);75    expect(targets.usernameEl).toBeNull();76    expect(targets.shouldOpen).toBe(true);77  });7879  // T-IT04: no qualifying fields → do not open (passive).80  it("T-IT04: empty field set + passive → shouldOpen false", () => {81    const anchor = makeInput({ type: "text" });82    const targets = resolveAutofillInteractionTargets(anchor, []);83    expect(targets.shouldOpen).toBe(false);84    expect(targets.usernameEl).toBeNull();85    expect(targets.passwordEl).toBeNull();86  });8788  // T-IT05: the function resolves targets from the field list it is GIVEN — form89  // scoping is the caller's job (index.ts calls detectLoginFields per the focused90  // field's context). With explicitSummon, a bare anchor still resolves as a91  // fallback target so an icon click always opens something.92  it("T-IT05: explicit summon on a bare password anchor falls back to the anchor", () => {93    const anchor = makeInput({ type: "password" });94    const targets = resolveAutofillInteractionTargets(anchor, [], true);95    expect(targets.shouldOpen).toBe(true);96    expect(targets.passwordEl).toBe(anchor);97  });9899  it("T-IT05b: explicit summon on a bare username anchor falls back to the anchor", () => {100    const anchor = makeInput({ type: "text" });101    const targets = resolveAutofillInteractionTargets(anchor, [], true);102    expect(targets.shouldOpen).toBe(true);103    expect(targets.usernameEl).toBe(anchor);104  });105106  // T-IT06: a detached anchor with no detected fields resolves to a non-opening107  // result rather than throwing — the graceful-abort path index.ts relies on.108  it("T-IT06: detached anchor + empty fields → shouldOpen false, no throw", () => {109    const detached = document.createElement("input");110    detached.type = "text"; // never appended to the document111    let targets!: ReturnType<typeof resolveAutofillInteractionTargets>;112    expect(() => { targets = resolveAutofillInteractionTargets(detached, []); }).not.toThrow();113    expect(targets.shouldOpen).toBe(false);114  });115});