123456789101112131415import { vi } from "vitest";1617const { predictBatch, dismissSpy } = vi.hoisted(() => ({18 predictBatch: vi.fn(() => Promise.resolve([] as unknown[])),19 dismissSpy: vi.fn(),20}));2122vi.mock("./inject", () => ({23 injectFieldIcon: vi.fn(() => vi.fn()),24 showAutofillSelector: vi.fn(() => dismissSpy),25 showTOTPSelector: vi.fn(() => dismissSpy),26 showCardSelector: vi.fn(() => dismissSpy),27 showRememberSiteToast: vi.fn(),28 showPasskeyAvailable: vi.fn(),29 showPasskeyProGate: vi.fn(),30 showPasskeyRegistrationOffer: vi.fn(),31}));32vi.mock("./content-ml-detector", () => ({33 contentMLDetector: { predictBatch },34}));3536import { afterEach, beforeEach, describe, expect, it } from "vitest";37import "./_setup";38import { resetChromeMock, setMessageResponder, sentMessages, messageListeners, createLoginForm, makeInput } from "./_setup";39import "./index"; 40import * as inject from "./inject";4142const showSelector = inject.showAutofillSelector as unknown as ReturnType<typeof vi.fn>;43const showCardSelector = inject.showCardSelector as unknown as ReturnType<typeof vi.fn>;44const showPasskeyProGate = inject.showPasskeyProGate as unknown as ReturnType<typeof vi.fn>;45const SELF_SENDER = { id: "peach-test-extension-id" };46const ENTRIES = [{ id: "e1", title: "Acme", username: "alice" }];47484950function dispatch(msg: any, sender: any = SELF_SENDER) {51 const sendResponse = vi.fn();52 for (const l of messageListeners) l(msg, sender, sendResponse);53 return sendResponse;54}5556function withEntries(): void {57 setMessageResponder((msg) =>58 msg.type === "AUTOFILL_QUERY" ? { ok: true, data: ENTRIES } : { ok: true, data: null }59 );60}6162let urlCounter = 0;63beforeEach(async () => {64 try { history.pushState({}, "", `/m${++urlCounter}`); } catch { }65 document.body.innerHTML = "";66 await new Promise((r) => setTimeout(r, 0));67 resetChromeMock();68 showSelector.mockClear();69 showCardSelector.mockClear();70 showPasskeyProGate.mockClear();71 predictBatch.mockReset();72 predictBatch.mockReturnValue(Promise.resolve([]));73});7475afterEach(async () => {76 await new Promise((r) => setTimeout(r, 0));77});7879describe("T-M01–T-M02: SUMMON_AUTOFILL", () => {80 81 it("T-M01: SUMMON_AUTOFILL with a focused login field → selector opens", async () => {82 const { password } = createLoginForm();83 password.focus();84 withEntries();85 const send = dispatch({ type: "SUMMON_AUTOFILL", source: "shortcut" });86 await vi.waitFor(() => expect(showSelector).toHaveBeenCalled());87 expect(send).toHaveBeenCalled(); 88 });8990 91 92 it("T-M02: SUMMON_AUTOFILL with no login form → no crash, no credential selector", () => {93 expect(() => dispatch({ type: "SUMMON_AUTOFILL", source: "shortcut" })).not.toThrow();94 expect(showSelector).not.toHaveBeenCalled();95 });9697 it("PB-26-201: no-form command carries capability from inventory to selected fill", async () => {98 const originalAttachShadow = Element.prototype.attachShadow;99 const attachSpy = vi.spyOn(Element.prototype, "attachShadow").mockImplementation(function (this: Element) {100 return originalAttachShadow.call(this, { mode: "open" });101 });102 setMessageResponder((msg) => {103 if (msg.type === "AUTOFILL_QUERY") return { ok: true, data: ENTRIES };104 if (msg.type === "GET_ENTRY_FOR_FILL") return { ok: true, data: { ...ENTRIES[0], password: "secret" } };105 return { ok: true, data: null };106 });107108 try {109 dispatch({ type: "SUMMON_AUTOFILL", source: "shortcut", capability: "no-form-capability" });110 await vi.waitFor(() => {111 expect(sentMessages.find((msg) => msg.type === "AUTOFILL_QUERY")).toMatchObject({112 payload: { includeAll: true, capability: "no-form-capability" },113 });114 });115 const findRow = () => Array.from(document.body.children)116 .map((host) => host.shadowRoot?.querySelector<HTMLElement>(".e"))117 .find((candidate): candidate is HTMLElement => candidate !== null && candidate !== undefined);118 await vi.waitFor(() => expect(findRow()).toBeDefined());119 const row = findRow();120 row?.click();121 await vi.waitFor(() => {122 expect(sentMessages.find((msg) => msg.type === "GET_ENTRY_FOR_FILL")).toMatchObject({123 payload: { id: "e1", capability: "no-form-capability" },124 });125 });126 } finally {127 attachSpy.mockRestore();128 }129 });130131 it("PB-26-201: card command carries capability into inventory and selector actions", async () => {132 const form = document.createElement("form");133 document.body.appendChild(form);134 const number = makeInput({ type: "text", autocomplete: "cc-number" }, form);135 makeInput({ type: "text", autocomplete: "cc-exp" }, form);136 makeInput({ type: "text", autocomplete: "cc-csc" }, form);137 number.focus();138 setMessageResponder((msg) =>139 msg.type === "AUTOFILL_QUERY"140 ? { ok: true, data: [{ id: "card-1", title: "Personal Visa", last4: "1111" }] }141 : { ok: true, data: null },142 );143144 dispatch({ type: "SUMMON_AUTOFILL", source: "shortcut", capability: "card-capability" });145146 await vi.waitFor(() => expect(showCardSelector).toHaveBeenCalled());147 expect(sentMessages.find((msg) => msg.type === "AUTOFILL_QUERY" && msg.payload?.kind === "card")).toMatchObject({148 payload: { capability: "card-capability" },149 });150 expect(showCardSelector.mock.calls.at(-1)![2]).toMatchObject({ capability: "card-capability" });151 });152});153154describe("T-M03–T-M05: VAULT_UNLOCKED", () => {155 156 it("T-M03: VAULT_UNLOCKED with a pending anchor → re-triggers and reopens", async () => {157 const { password } = createLoginForm();158 password.focus();159 withEntries();160 dispatch({ type: "SUMMON_AUTOFILL", source: "shortcut" }); 161 await vi.waitFor(() => expect(showSelector).toHaveBeenCalled());162 showSelector.mockClear();163164 dispatch({ type: "VAULT_UNLOCKED" });165 await vi.waitFor(() => expect(showSelector).toHaveBeenCalled()); 166 });167168 169 170 it("T-M05: VAULT_UNLOCKED with no pending interaction → no-op, no crash", async () => {171 (document.activeElement as HTMLElement | null)?.blur?.();172 const send = dispatch({ type: "VAULT_UNLOCKED" });173 await new Promise((r) => setTimeout(r, 20));174 expect(send).toHaveBeenCalledWith({ ok: true });175 expect(showSelector).not.toHaveBeenCalled();176 });177178 179 180 181 182 183 it("T-M04: VAULT_UNLOCKED while passkeySummonActive → no re-trigger", async () => {184 185 186 187 const anchor = makeInput({ type: "text", name: "search-box", placeholder: "Search" });188 anchor.focus();189 dispatch({ type: "SUMMON_AUTOFILL", source: "passkey-unlock" }); 190 await vi.waitFor(() => expect(showSelector).toHaveBeenCalled());191 showSelector.mockClear();192193 dispatch({ type: "VAULT_UNLOCKED" }); 194 await new Promise((r) => setTimeout(r, 20));195 expect(showSelector).not.toHaveBeenCalled();196 });197});198199describe("T-M08–T-M10: PING, passkey gate, robustness", () => {200 201 202 it("PING → sendResponse({ ok: true })", () => {203 const send = dispatch({ type: "PING" });204 expect(send).toHaveBeenCalledWith({ ok: true });205 });206207 208 it("GET_SITE_PASSWORD_POLICY → returns a policy object", () => {209 document.body.innerHTML = '<input type="password" maxlength="32" />';210 const send = dispatch({ type: "GET_SITE_PASSWORD_POLICY" });211 expect(send).toHaveBeenCalledTimes(1);212 const arg = send.mock.calls[0]![0];213 expect(arg.ok).toBe(true);214 expect(arg.data.maxLength).toBe(32);215 });216217 218 it("PASSKEY_PRO_GATE → showPasskeyProGate called", () => {219 dispatch({ type: "PASSKEY_PRO_GATE" });220 expect(showPasskeyProGate).toHaveBeenCalled();221 });222223 224 225 it("T-M09: unknown message type → handled gracefully", () => {226 expect(() => dispatch({ type: "TOTALLY_UNKNOWN_MESSAGE" })).not.toThrow();227 expect(showSelector).not.toHaveBeenCalled();228 });229230 231 232 it("T-M10: message from a foreign sender id is ignored", () => {233 const send = dispatch({ type: "PING" }, { id: "some-other-extension" });234 expect(send).not.toHaveBeenCalled();235 });236});