// @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, showCardSelector } from "./inject"; import { makeInput, resetChromeMock, sentMessages, setMessageResponder } 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 = ""; resetChromeMock(); }); afterEach(() => { document.body.innerHTML = ""; }); describe("T-I01: mount + dismiss (the observable contract)", () => { it("PB-26-707: shows the full security origin instead of the registrable domain", () => { const originalAttachShadow = Element.prototype.attachShadow; const attachSpy = vi.spyOn(Element.prototype, "attachShadow").mockImplementation(function (this: Element) { return originalAttachShadow.call(this, { mode: "open" }); }); const anchor = makeInput({ type: "password" }); try { const dismiss = showAutofillSelector(anchor, ENTRIES, vi.fn(), { domain: "example.com", securityOrigin: "https://login.accounts.example.com:8443", }); expect(selectorHost()?.shadowRoot?.querySelector(".domain-name")?.textContent) .toBe("https://login.accounts.example.com:8443"); dismiss(); } finally { attachSpy.mockRestore(); } }); // 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(); }); }); describe("PB-26-201: content-safe privileged actions", () => { it("opens new-entry creation inline without rechecking an already-unlocked selector", async () => { const originalAttachShadow = Element.prototype.attachShadow; const attachSpy = vi.spyOn(Element.prototype, "attachShadow").mockImplementation(function (this: Element) { return originalAttachShadow.call(this, { mode: "open" }); }); const anchor = makeInput({ type: "password" }); setMessageResponder((message) => message.type === "GET_VAULT_STATUS" ? { ok: true, unlocked: false } : { ok: true, data: null }, ); try { const dismiss = showAutofillSelector(anchor, [ENTRIES[0]!], vi.fn(), { domain: "example.com" }); const host = selectorHost(); host?.shadowRoot?.querySelector(".domain-add-btn")?.click(); await vi.waitFor(() => { expect(host?.shadowRoot?.querySelector(".create-form-wrap")).not.toBeNull(); }); expect(host?.shadowRoot?.querySelector(".locked-wrap")).toBeNull(); expect(sentMessages.some((message) => message.type === "GET_VAULT_STATUS")).toBe(false); dismiss(); } finally { attachSpy.mockRestore(); } }); it("uses a command capability for exactly one login-with-TOTP secret fetch", async () => { const originalAttachShadow = Element.prototype.attachShadow; const attachSpy = vi.spyOn(Element.prototype, "attachShadow").mockImplementation(function (this: Element) { return originalAttachShadow.call(this, { mode: "open" }); }); const anchor = makeInput({ type: "password" }); const onSelect = vi.fn(async (id: string) => { await chrome.runtime.sendMessage({ type: "GET_ENTRY_FOR_FILL", payload: { id, capability: "login-capability" }, }); }); setMessageResponder((message) => message.type === "GET_ENTRY_FOR_FILL" ? { ok: true, data: { password: "secret", totpCode: "123456" } } : { ok: true, data: null }, ); try { const dismiss = showAutofillSelector( anchor, [{ id: "e1", title: "Acme", username: "alice", hasTotp: true }], onSelect, { domain: "example.com", capability: "login-capability" }, ); const host = selectorHost(); expect(sentMessages.some((message) => message.type === "GET_ENTRY_FOR_FILL")).toBe(false); host?.shadowRoot?.querySelector(".sp-fill-btn")?.click(); await vi.waitFor(() => expect(onSelect).toHaveBeenCalledWith("e1")); expect(sentMessages.filter((message) => message.type === "GET_ENTRY_FOR_FILL")).toHaveLength(1); dismiss(); } finally { attachSpy.mockRestore(); } }); it("passes the command capability into a selected password copy", async () => { const originalAttachShadow = Element.prototype.attachShadow; const attachSpy = vi.spyOn(Element.prototype, "attachShadow").mockImplementation(function (this: Element) { return originalAttachShadow.call(this, { mode: "open" }); }); const anchor = makeInput({ type: "password" }); setMessageResponder((message) => message.type === "GET_ENTRY_FOR_FILL" ? { ok: true, data: { password: "correct horse battery staple" } } : { ok: true, data: null }, ); try { const dismiss = showAutofillSelector(anchor, [ENTRIES[0]!], vi.fn(), { domain: "example.com", capability: "login-capability", } as Parameters[3]); const host = selectorHost(); host?.shadowRoot?.querySelector('button[title="Copy password"]')?.click(); await vi.waitFor(() => { expect(sentMessages.find((message) => message.type === "GET_ENTRY_FOR_FILL")).toMatchObject({ payload: { id: "e1", capability: "login-capability" }, }); }); dismiss(); } finally { attachSpy.mockRestore(); } }); it("opens the selected entry editor inside the inline helper", async () => { const originalAttachShadow = Element.prototype.attachShadow; const attachSpy = vi.spyOn(Element.prototype, "attachShadow").mockImplementation(function (this: Element) { return originalAttachShadow.call(this, { mode: "open" }); }); const anchor = makeInput({ type: "password" }); setMessageResponder((message) => message.type === "GET_ENTRY_FOR_FILL" ? { ok: true, data: { title: "Acme", username: "alice@example.com", password: "current-password", url: "https://example.com", }, } : { ok: true, data: null }, ); try { const dismiss = showAutofillSelector(anchor, [ENTRIES[0]!], vi.fn(), { domain: "example.com" }); const host = selectorHost(); const edit = host?.shadowRoot?.querySelector(".sp-action-edit"); expect(edit).not.toBeNull(); edit?.click(); await vi.waitFor(() => { expect(host?.shadowRoot?.querySelector('.create-form-wrap input[type="password"]')?.value) .toBe("current-password"); }); expect(sentMessages).toContainEqual({ type: "GET_ENTRY_FOR_FILL", payload: { id: "e1", capability: undefined }, }); expect(sentMessages.some((message) => message.type === "OPEN_POPUP")).toBe(false); expect(sentMessages.some((message) => message.type === "GET_ENTRY_FOR_EDIT")).toBe(false); const title = host?.shadowRoot?.querySelector('.create-form-wrap input[type="text"]'); if (title) title.value = "Acme Updated"; host?.shadowRoot?.querySelector(".create-form-wrap .save-btn")?.click(); await vi.waitFor(() => { expect(sentMessages.find((message) => message.type === "UPDATE_ENTRY")).toMatchObject({ payload: { id: "e1", patch: { title: "Acme Updated", password: "current-password" }, }, }); }); const patch = sentMessages.find((message) => message.type === "UPDATE_ENTRY")?.payload?.patch; expect(patch).not.toHaveProperty("totp"); expect(patch).not.toHaveProperty("passwordHistory"); dismiss(); } finally { attachSpy.mockRestore(); } }); it("passes the card command capability into the selected card fill", async () => { const originalAttachShadow = Element.prototype.attachShadow; const attachSpy = vi.spyOn(Element.prototype, "attachShadow").mockImplementation(function (this: Element) { return originalAttachShadow.call(this, { mode: "open" }); }); const anchor = makeInput({ type: "text", autocomplete: "cc-number" }); const onFill = vi.fn(); setMessageResponder((message) => message.type === "GET_ENTRY_FOR_FILL" ? { ok: true, data: { id: "card-1", type: "card", title: "Personal Visa", cardNumber: "4111111111111111" } } : { ok: true, data: null }, ); try { const dismiss = showCardSelector( anchor, [{ id: "card-1", title: "Personal Visa", last4: "1111" }], { domain: "shop.example", capability: "card-capability", onFill } as Parameters[2], ); const host = selectorHost(); host?.shadowRoot?.querySelector(".sp-fill-btn")?.click(); await vi.waitFor(() => { expect(sentMessages.find((message) => message.type === "GET_ENTRY_FOR_FILL")).toMatchObject({ payload: { id: "card-1", capability: "card-capability" }, }); }); dismiss(); } finally { attachSpy.mockRestore(); } }); it("allows only one card secret action per command capability", async () => { const originalAttachShadow = Element.prototype.attachShadow; const attachSpy = vi.spyOn(Element.prototype, "attachShadow").mockImplementation(function (this: Element) { return originalAttachShadow.call(this, { mode: "open" }); }); const anchor = makeInput({ type: "text", autocomplete: "cc-number" }); setMessageResponder((message) => message.type === "GET_ENTRY_FOR_FILL" ? { ok: true, data: { id: "card-1", type: "card", cardNumber: "4111111111111111", cvc: "123" } } : { ok: true, data: null }, ); try { const dismiss = showCardSelector( anchor, [{ id: "card-1", title: "Personal Visa", last4: "1111" }], { domain: "shop.example", capability: "card-capability", onFill: vi.fn() }, ); const host = selectorHost(); const copyNumber = host?.shadowRoot?.querySelector('button[title="Copy card number"]'); const fill = host?.shadowRoot?.querySelector(".sp-fill-btn"); copyNumber?.click(); fill?.click(); await vi.waitFor(() => { expect(sentMessages.filter((message) => message.type === "GET_ENTRY_FOR_FILL")).toHaveLength(1); }); dismiss(); } finally { attachSpy.mockRestore(); } }); });