// @vitest-environment jsdom // // Test File 5 — inject.ts showAutofillSelector (T-I01–T-I16). // // IMPORTANT ARCHITECTURE NOTE: showAutofillSelector mounts its UI inside a // CLOSED shadow root (host.attachShadow({ mode: "closed" })). Everything the // brief wants to assert about the selector's interior — the entry list, arrow/ // Enter/Escape keyboard navigation, the search filter, the lazy GET_ENTRY load, // the TOTP chip, focus-trapping, above/below placement, isTrusted gating — lives // behind that closed boundary and is, by design, unreachable from any external // script (including this test and even a page-context Playwright script). // // So this file asserts the externally-observable contract: the selector mounts // a host into the document and the returned dismiss function tears it down. // Browser-level behavior is covered through observable effects rather than by // adding a test-only seam into the closed shadow root. import "./_setup"; // chrome mock + ResizeObserver polyfill import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { showAutofillSelector } from "./inject"; import { makeInput } from "./_setup"; const ENTRIES = [ { id: "e1", title: "Acme", username: "alice@example.com" }, { id: "e2", title: "Acme Admin", username: "root@example.com" }, ]; // The host is a position:fixed div with the max z-index; closed shadow means we // can only see the host, never its contents. function selectorHost(): HTMLElement | null { return document.body.querySelector('div[style*="2147483647"]'); } beforeEach(() => { document.body.innerHTML = ""; }); afterEach(() => { document.body.innerHTML = ""; }); describe("T-I01: mount + dismiss (the observable contract)", () => { // T-I01: opening the selector mounts a host into the document and returns a // dismiss function. it("T-I01: showAutofillSelector mounts a host and returns a dismiss fn", () => { const anchor = makeInput({ type: "password" }); const dismiss = showAutofillSelector(anchor, ENTRIES, vi.fn()); expect(typeof dismiss).toBe("function"); expect(selectorHost()).not.toBeNull(); dismiss(); }); // Dismiss tears the host back down — the closed shadow goes with it. Teardown // is animated: dismiss() schedules host.remove() ~130ms later, so we wait it // out rather than asserting synchronously. it("T-I05/T-I06 (teardown half): the returned dismiss removes the host", async () => { const anchor = makeInput({ type: "password" }); const dismiss = showAutofillSelector(anchor, ENTRIES, vi.fn()); expect(selectorHost()).not.toBeNull(); dismiss(); await new Promise((r) => setTimeout(r, 180)); expect(selectorHost()).toBeNull(); }); // The host mounts even in locked mode (the unlock prompt is also closed-shadow). it("T-I01b: locked-mode open also mounts a host", () => { const anchor = makeInput({ type: "password" }); const dismiss = showAutofillSelector(anchor, [], vi.fn(), { locked: true }); expect(selectorHost()).not.toBeNull(); dismiss(); }); // Re-opening dismisses any previous host the caller forgot — but the caller // (index.ts) owns currentDismiss, so here we just confirm two opens leave two // hosts unless dismissed, proving each open is independent. it("T-I01c: each open creates its own host (caller owns single-instance policy)", () => { const anchor = makeInput({ type: "password" }); const d1 = showAutofillSelector(anchor, ENTRIES, vi.fn()); const d2 = showAutofillSelector(anchor, ENTRIES, vi.fn()); expect(document.body.querySelectorAll('div[style*="2147483647"]').length).toBe(2); d1(); d2(); }); });