content.selector-ui.test.ts

3.8 KB · raw

1// @vitest-environment jsdom2//3// Test File 5 — inject.ts showAutofillSelector (T-I01–T-I16).4//5// IMPORTANT ARCHITECTURE NOTE: showAutofillSelector mounts its UI inside a6// CLOSED shadow root (host.attachShadow({ mode: "closed" })). Everything the7// brief wants to assert about the selector's interior — the entry list, arrow/8// Enter/Escape keyboard navigation, the search filter, the lazy GET_ENTRY load,9// the TOTP chip, focus-trapping, above/below placement, isTrusted gating — lives10// behind that closed boundary and is, by design, unreachable from any external11// script (including this test and even a page-context Playwright script).12//13// So this file asserts the ONLY externally-observable contract: the selector14// mounts a host into the document and the returned dismiss function tears it15// down. The interior behaviors are marked it.todo and routed to the Playwright16// e2e, which verifies them through their effects (fields getting filled, the17// host disappearing) rather than by reading the closed tree. The recommendation18// to add a test-only "open shadow" seam is recorded in INTEGRATION-CONTRACT.md.1920import "./_setup"; // chrome mock + ResizeObserver polyfill21import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";22import { showAutofillSelector } from "./inject";23import { makeInput } from "./_setup";2425const ENTRIES = [26  { id: "e1", title: "Acme", username: "[email protected]" },27  { id: "e2", title: "Acme Admin", username: "[email protected]" },28];2930// The host is a position:fixed div with the max z-index; closed shadow means we31// can only see the host, never its contents.32function selectorHost(): HTMLElement | null {33  return document.body.querySelector<HTMLElement>('div[style*="2147483647"]');34}3536beforeEach(() => {37  document.body.innerHTML = "";38});39afterEach(() => {40  document.body.innerHTML = "";41});4243describe("T-I01: mount + dismiss (the observable contract)", () => {44  // T-I01: opening the selector mounts a host into the document and returns a45  // dismiss function.46  it("T-I01: showAutofillSelector mounts a host and returns a dismiss fn", () => {47    const anchor = makeInput({ type: "password" });48    const dismiss = showAutofillSelector(anchor, ENTRIES, vi.fn());49    expect(typeof dismiss).toBe("function");50    expect(selectorHost()).not.toBeNull();51    dismiss();52  });5354  // Dismiss tears the host back down — the closed shadow goes with it. Teardown55  // is animated: dismiss() schedules host.remove() ~130ms later, so we wait it56  // out rather than asserting synchronously.57  it("T-I05/T-I06 (teardown half): the returned dismiss removes the host", async () => {58    const anchor = makeInput({ type: "password" });59    const dismiss = showAutofillSelector(anchor, ENTRIES, vi.fn());60    expect(selectorHost()).not.toBeNull();61    dismiss();62    await new Promise((r) => setTimeout(r, 180));63    expect(selectorHost()).toBeNull();64  });6566  // The host mounts even in locked mode (the unlock prompt is also closed-shadow).67  it("T-I01b: locked-mode open also mounts a host", () => {68    const anchor = makeInput({ type: "password" });69    const dismiss = showAutofillSelector(anchor, [], vi.fn(), { locked: true });70    expect(selectorHost()).not.toBeNull();71    dismiss();72  });7374  // Re-opening dismisses any previous host the caller forgot — but the caller75  // (index.ts) owns currentDismiss, so here we just confirm two opens leave two76  // hosts unless dismissed, proving each open is independent.77  it("T-I01c: each open creates its own host (caller owns single-instance policy)", () => {78    const anchor = makeInput({ type: "password" });79    const d1 = showAutofillSelector(anchor, ENTRIES, vi.fn());80    const d2 = showAutofillSelector(anchor, ENTRIES, vi.fn());81    expect(document.body.querySelectorAll('div[style*="2147483647"]').length).toBe(2);82    d1();83    d2();84  });85});