content.selector-ui.test.ts

3.6 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 externally-observable contract: the selector mounts14// a host into the document and the returned dismiss function tears it down.15// Browser-level behavior is covered through observable effects rather than by16// adding a test-only seam into the closed shadow root.1718import "./_setup"; // chrome mock + ResizeObserver polyfill19import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";20import { showAutofillSelector } from "./inject";21import { makeInput } from "./_setup";2223const ENTRIES = [24  { id: "e1", title: "Acme", username: "[email protected]" },25  { id: "e2", title: "Acme Admin", username: "[email protected]" },26];2728// The host is a position:fixed div with the max z-index; closed shadow means we29// can only see the host, never its contents.30function selectorHost(): HTMLElement | null {31  return document.body.querySelector<HTMLElement>('div[style*="2147483647"]');32}3334beforeEach(() => {35  document.body.innerHTML = "";36});37afterEach(() => {38  document.body.innerHTML = "";39});4041describe("T-I01: mount + dismiss (the observable contract)", () => {42  // T-I01: opening the selector mounts a host into the document and returns a43  // dismiss function.44  it("T-I01: showAutofillSelector mounts a host and returns a dismiss fn", () => {45    const anchor = makeInput({ type: "password" });46    const dismiss = showAutofillSelector(anchor, ENTRIES, vi.fn());47    expect(typeof dismiss).toBe("function");48    expect(selectorHost()).not.toBeNull();49    dismiss();50  });5152  // Dismiss tears the host back down — the closed shadow goes with it. Teardown53  // is animated: dismiss() schedules host.remove() ~130ms later, so we wait it54  // out rather than asserting synchronously.55  it("T-I05/T-I06 (teardown half): the returned dismiss removes the host", async () => {56    const anchor = makeInput({ type: "password" });57    const dismiss = showAutofillSelector(anchor, ENTRIES, vi.fn());58    expect(selectorHost()).not.toBeNull();59    dismiss();60    await new Promise((r) => setTimeout(r, 180));61    expect(selectorHost()).toBeNull();62  });6364  // The host mounts even in locked mode (the unlock prompt is also closed-shadow).65  it("T-I01b: locked-mode open also mounts a host", () => {66    const anchor = makeInput({ type: "password" });67    const dismiss = showAutofillSelector(anchor, [], vi.fn(), { locked: true });68    expect(selectorHost()).not.toBeNull();69    dismiss();70  });7172  // Re-opening dismisses any previous host the caller forgot — but the caller73  // (index.ts) owns currentDismiss, so here we just confirm two opens leave two74  // hosts unless dismissed, proving each open is independent.75  it("T-I01c: each open creates its own host (caller owns single-instance policy)", () => {76    const anchor = makeInput({ type: "password" });77    const d1 = showAutofillSelector(anchor, ENTRIES, vi.fn());78    const d2 = showAutofillSelector(anchor, ENTRIES, vi.fn());79    expect(document.body.querySelectorAll('div[style*="2147483647"]').length).toBe(2);80    d1();81    d2();82  });83});