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, messageListeners, createLoginForm, makeInput } from "./_setup";39import "./index"; 40import * as inject from "./inject";4142const showSelector = inject.showAutofillSelector as unknown as ReturnType<typeof vi.fn>;43const showPasskeyProGate = inject.showPasskeyProGate as unknown as ReturnType<typeof vi.fn>;44const SELF_SENDER = { id: "peach-test-extension-id" };45const ENTRIES = [{ id: "e1", title: "Acme", username: "alice" }];46474849function dispatch(msg: any, sender: any = SELF_SENDER) {50 const sendResponse = vi.fn();51 for (const l of messageListeners) l(msg, sender, sendResponse);52 return sendResponse;53}5455function withEntries(): void {56 setMessageResponder((msg) =>57 msg.type === "AUTOFILL_QUERY" ? { ok: true, data: ENTRIES } : { ok: true, data: null }58 );59}6061let urlCounter = 0;62beforeEach(async () => {63 try { history.pushState({}, "", `/m${++urlCounter}`); } catch { }64 document.body.innerHTML = "";65 await new Promise((r) => setTimeout(r, 0));66 resetChromeMock();67 showSelector.mockClear();68 showPasskeyProGate.mockClear();69 predictBatch.mockReset();70 predictBatch.mockReturnValue(Promise.resolve([]));71});7273afterEach(async () => {74 await new Promise((r) => setTimeout(r, 0));75});7677describe("T-M01–T-M02: SUMMON_AUTOFILL", () => {78 79 it("T-M01: SUMMON_AUTOFILL with a focused login field → selector opens", async () => {80 const { password } = createLoginForm();81 password.focus();82 withEntries();83 const send = dispatch({ type: "SUMMON_AUTOFILL", source: "shortcut" });84 await vi.waitFor(() => expect(showSelector).toHaveBeenCalled());85 expect(send).toHaveBeenCalled(); 86 });8788 89 90 it("T-M02: SUMMON_AUTOFILL with no login form → no crash, no credential selector", () => {91 expect(() => dispatch({ type: "SUMMON_AUTOFILL", source: "shortcut" })).not.toThrow();92 expect(showSelector).not.toHaveBeenCalled();93 });94});9596describe("T-M03–T-M05: VAULT_UNLOCKED", () => {97 98 it("T-M03: VAULT_UNLOCKED with a pending anchor → re-triggers and reopens", async () => {99 const { password } = createLoginForm();100 password.focus();101 withEntries();102 dispatch({ type: "SUMMON_AUTOFILL", source: "shortcut" }); 103 await vi.waitFor(() => expect(showSelector).toHaveBeenCalled());104 showSelector.mockClear();105106 dispatch({ type: "VAULT_UNLOCKED" });107 await vi.waitFor(() => expect(showSelector).toHaveBeenCalled()); 108 });109110 111 112 it("T-M05: VAULT_UNLOCKED with no pending interaction → no-op, no crash", async () => {113 (document.activeElement as HTMLElement | null)?.blur?.();114 const send = dispatch({ type: "VAULT_UNLOCKED" });115 await new Promise((r) => setTimeout(r, 20));116 expect(send).toHaveBeenCalledWith({ ok: true });117 expect(showSelector).not.toHaveBeenCalled();118 });119120 121 122 123 124 125 it("T-M04: VAULT_UNLOCKED while passkeySummonActive → no re-trigger", async () => {126 127 128 129 const anchor = makeInput({ type: "text", name: "search-box", placeholder: "Search" });130 anchor.focus();131 dispatch({ type: "SUMMON_AUTOFILL", source: "passkey-unlock" }); 132 await vi.waitFor(() => expect(showSelector).toHaveBeenCalled());133 showSelector.mockClear();134135 dispatch({ type: "VAULT_UNLOCKED" }); 136 await new Promise((r) => setTimeout(r, 20));137 expect(showSelector).not.toHaveBeenCalled();138 });139});140141describe("T-M08–T-M10: PING, passkey gate, robustness", () => {142 143 144 it("PING → sendResponse({ ok: true })", () => {145 const send = dispatch({ type: "PING" });146 expect(send).toHaveBeenCalledWith({ ok: true });147 });148149 150 it("GET_SITE_PASSWORD_POLICY → returns a policy object", () => {151 document.body.innerHTML = '<input type="password" maxlength="32" />';152 const send = dispatch({ type: "GET_SITE_PASSWORD_POLICY" });153 expect(send).toHaveBeenCalledTimes(1);154 const arg = send.mock.calls[0]![0];155 expect(arg.ok).toBe(true);156 expect(arg.data.maxLength).toBe(32);157 });158159 160 it("PASSKEY_PRO_GATE → showPasskeyProGate called", () => {161 dispatch({ type: "PASSKEY_PRO_GATE" });162 expect(showPasskeyProGate).toHaveBeenCalled();163 });164165 166 167 it("T-M09: unknown message type → handled gracefully", () => {168 expect(() => dispatch({ type: "TOTALLY_UNKNOWN_MESSAGE" })).not.toThrow();169 expect(showSelector).not.toHaveBeenCalled();170 });171172 173 174 it("T-M10: message from a foreign sender id is ignored", () => {175 const send = dispatch({ type: "PING" }, { id: "some-other-extension" });176 expect(send).not.toHaveBeenCalled();177 });178});