12345678910111213141516import { vi } from "vitest";1718const { predictBatch, dismissSpy } = vi.hoisted(() => ({19 predictBatch: vi.fn(() => Promise.resolve([] as unknown[])),20 dismissSpy: vi.fn(),21}));2223vi.mock("./inject", () => ({24 injectFieldIcon: vi.fn(() => vi.fn()),25 showAutofillSelector: vi.fn(() => dismissSpy),26 showTOTPSelector: vi.fn(() => dismissSpy),27 showCardSelector: vi.fn(() => dismissSpy),28 showRememberSiteToast: vi.fn(),29 showPasskeyAvailable: vi.fn(),30 showPasskeyProGate: vi.fn(),31 showPasskeyRegistrationOffer: vi.fn(),32}));33vi.mock("./content-ml-detector", () => ({34 contentMLDetector: { predictBatch },35}));3637import { afterEach, beforeEach, describe, expect, it } from "vitest";38import "./_setup";39import { resetChromeMock, setMessageResponder, messageListeners, createLoginForm, makeInput } from "./_setup";40import "./index"; 41import * as inject from "./inject";4243const showSelector = inject.showAutofillSelector 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 showPasskeyProGate.mockClear();70 predictBatch.mockReset();71 predictBatch.mockReturnValue(Promise.resolve([]));72});7374afterEach(async () => {75 await new Promise((r) => setTimeout(r, 0));76});7778describe("T-M01–T-M02: SUMMON_AUTOFILL", () => {79 80 it("T-M01: SUMMON_AUTOFILL with a focused login field → selector opens", async () => {81 const { password } = createLoginForm();82 password.focus();83 withEntries();84 const send = dispatch({ type: "SUMMON_AUTOFILL", source: "shortcut" });85 await vi.waitFor(() => expect(showSelector).toHaveBeenCalled());86 expect(send).toHaveBeenCalled(); 87 });8889 90 91 it("T-M02: SUMMON_AUTOFILL with no login form → no crash, no credential selector", () => {92 expect(() => dispatch({ type: "SUMMON_AUTOFILL", source: "shortcut" })).not.toThrow();93 expect(showSelector).not.toHaveBeenCalled();94 });95});9697describe("T-M03–T-M05: VAULT_UNLOCKED", () => {98 99 it("T-M03: VAULT_UNLOCKED with a pending anchor → re-triggers and reopens", async () => {100 const { password } = createLoginForm();101 password.focus();102 withEntries();103 dispatch({ type: "SUMMON_AUTOFILL", source: "shortcut" }); 104 await vi.waitFor(() => expect(showSelector).toHaveBeenCalled());105 showSelector.mockClear();106107 dispatch({ type: "VAULT_UNLOCKED" });108 await vi.waitFor(() => expect(showSelector).toHaveBeenCalled()); 109 });110111 112 113 it("T-M05: VAULT_UNLOCKED with no pending interaction → no-op, no crash", async () => {114 (document.activeElement as HTMLElement | null)?.blur?.();115 const send = dispatch({ type: "VAULT_UNLOCKED" });116 await new Promise((r) => setTimeout(r, 20));117 expect(send).toHaveBeenCalledWith({ ok: true });118 expect(showSelector).not.toHaveBeenCalled();119 });120121 122 123 124 125 126 it("T-M04: VAULT_UNLOCKED while passkeySummonActive → no re-trigger", async () => {127 128 129 130 const anchor = makeInput({ type: "text", name: "search-box", placeholder: "Search" });131 anchor.focus();132 dispatch({ type: "SUMMON_AUTOFILL", source: "passkey-unlock" }); 133 await vi.waitFor(() => expect(showSelector).toHaveBeenCalled());134 showSelector.mockClear();135136 dispatch({ type: "VAULT_UNLOCKED" }); 137 await new Promise((r) => setTimeout(r, 20));138 expect(showSelector).not.toHaveBeenCalled();139 });140});141142describe("T-M08–T-M10: PING, passkey gate, robustness", () => {143 144 145 it("PING → sendResponse({ ok: true })", () => {146 const send = dispatch({ type: "PING" });147 expect(send).toHaveBeenCalledWith({ ok: true });148 });149150 151 it("GET_SITE_PASSWORD_POLICY → returns a policy object", () => {152 document.body.innerHTML = '<input type="password" maxlength="32" />';153 const send = dispatch({ type: "GET_SITE_PASSWORD_POLICY" });154 expect(send).toHaveBeenCalledTimes(1);155 const arg = send.mock.calls[0]![0];156 expect(arg.ok).toBe(true);157 expect(arg.data.maxLength).toBe(32);158 });159160 161 it("PASSKEY_PRO_GATE → showPasskeyProGate called", () => {162 dispatch({ type: "PASSKEY_PRO_GATE" });163 expect(showPasskeyProGate).toHaveBeenCalled();164 });165166 167 168 it("T-M09: unknown message type → handled gracefully", () => {169 expect(() => dispatch({ type: "TOTALLY_UNKNOWN_MESSAGE" })).not.toThrow();170 expect(showSelector).not.toHaveBeenCalled();171 });172173 174 175 it("T-M10: message from a foreign sender id is ignored", () => {176 const send = dispatch({ type: "PING" }, { id: "some-other-extension" });177 expect(send).not.toHaveBeenCalled();178 });179});180181182183184