123456789101112import { vi } from "vitest";131415const { predictBatch, dismissSpy } = vi.hoisted(() => ({16 predictBatch: vi.fn(() => Promise.resolve([] as unknown[])),17 dismissSpy: vi.fn(),18}));192021vi.mock("./inject", () => ({22 injectFieldIcon: vi.fn(() => vi.fn()),23 showAutofillSelector: vi.fn(() => dismissSpy),24 showTOTPSelector: vi.fn(() => dismissSpy),25 showCardSelector: vi.fn(() => dismissSpy),26 showRememberSiteToast: vi.fn(),27 showPasskeyAvailable: vi.fn(),28 showPasskeyProGate: vi.fn(),29 showPasskeyRegistrationOffer: vi.fn(),30}));313233vi.mock("./content-ml-detector", () => ({34 contentMLDetector: { predictBatch },35}));3637import { afterEach, beforeEach, describe, expect, it } from "vitest";38import "./_setup";39import { setMessageResponder, resetChromeMock, makeInput, createLoginForm, createEmailFirstForm } from "./_setup";40import { handleFieldInteraction } from "./index";41import { showAutofillSelector, showTOTPSelector } from "./inject";4243const showSelector = showAutofillSelector as unknown as ReturnType<typeof vi.fn>;44const showTOTP = showTOTPSelector as unknown as ReturnType<typeof vi.fn>;454647484950beforeEach(() => {51 document.body.innerHTML = "";52 resetChromeMock();53 showSelector.mockClear();54 showTOTP.mockClear();55 predictBatch.mockReset();56 predictBatch.mockReturnValue(Promise.resolve([]));57});5859afterEach(async () => {60 vi.useRealTimers();61 62 63 64 65 await new Promise((r) => setTimeout(r, 0));66});676869function lastSelectorOptions(): any {70 const calls = showSelector.mock.calls;71 return calls.length ? calls[calls.length - 1]![3] : undefined;72}7374describe("T-F01–T-F02: ML predict timeout race (150ms)", () => {75 76 77 it("T-F02: ML resolves fast with a suppressing prediction → no selector", async () => {78 const { username } = createLoginForm();79 predictBatch.mockReturnValue(Promise.resolve([{ type: "search", confidence: 0.9 }]));80 await handleFieldInteraction(username); 81 82 await new Promise((r) => setTimeout(r, 20));83 expect(showSelector).not.toHaveBeenCalled();84 });8586 87 88 89 90 it("T-F01: ML slower than 150ms → timeout path taken, selector still opens", async () => {91 const { username } = createLoginForm();92 predictBatch.mockReturnValue(93 new Promise((resolve) => setTimeout(() => resolve([{ type: "search", confidence: 0.9 }]), 250))94 );95 setMessageResponder((msg) =>96 msg.type === "AUTOFILL_QUERY" ? { ok: true, data: [{ id: "e1", title: "Acme", username: "alice" }] } : { ok: true, data: null }97 );98 await handleFieldInteraction(username);99 await vi.waitFor(() => expect(showSelector).toHaveBeenCalled(), { timeout: 1000 });100 });101});102103describe("T-F03–T-F05: vault state + form shape", () => {104 105 106 it("T-F03: locked vault → selector opened in locked mode", async () => {107 const { password } = createLoginForm();108 setMessageResponder((msg) =>109 msg.type === "AUTOFILL_QUERY" ? { ok: false, error: "vault_locked" } : { ok: true, data: null }110 );111 await handleFieldInteraction(password);112 await vi.waitFor(() => expect(showSelector).toHaveBeenCalled());113 expect(lastSelectorOptions()?.locked).toBe(true);114 });115116 117 118 it("T-F04: no domain entries → signup/identity surface", async () => {119 const { password } = createLoginForm();120 setMessageResponder((msg) =>121 msg.type === "AUTOFILL_QUERY" ? { ok: true, data: [] } : { ok: true, data: null }122 );123 await handleFieldInteraction(password);124 await vi.waitFor(() => expect(showSelector).toHaveBeenCalled());125 expect(lastSelectorOptions()?.isSignup).toBe(true);126 expect(lastSelectorOptions()?.locked).toBeFalsy();127 });128129 130 131 it("T-F05: email-first form opens on the email field", async () => {132 const { email } = createEmailFirstForm();133 setMessageResponder((msg) =>134 msg.type === "AUTOFILL_QUERY" ? { ok: true, data: [{ id: "e1", title: "Acme", username: "alice" }] } : { ok: true, data: null }135 );136 await handleFieldInteraction(email);137 await vi.waitFor(() => expect(showSelector).toHaveBeenCalled());138 });139});140141describe("T-F07: TOTP field path", () => {142 143 144 it("T-F07: standalone one-time-code field → TOTP selector", async () => {145 const totp = makeInput({ type: "text", autocomplete: "one-time-code", name: "otp" });146 setMessageResponder((msg) =>147 msg.type === "AUTOFILL_QUERY" ? { ok: true, data: [{ id: "t1", title: "Acme 2FA" }] } : { ok: true, data: null }148 );149 await handleFieldInteraction(totp);150 await vi.waitFor(() => expect(showTOTP).toHaveBeenCalled());151 expect(showSelector).not.toHaveBeenCalled();152 });153});154155describe("T-F09: Peach respects the user, not the page's autocomplete=off", () => {156 157 158 it("T-F09: form with autocomplete=off still offers", async () => {159 const form = document.createElement("form");160 form.setAttribute("autocomplete", "off");161 document.body.appendChild(form);162 makeInput({ type: "text", name: "username", autocomplete: "username" }, form);163 const password = makeInput({ type: "password", autocomplete: "current-password" }, form);164 setMessageResponder((msg) =>165 msg.type === "AUTOFILL_QUERY" ? { ok: true, data: [{ id: "e1", title: "Acme", username: "alice" }] } : { ok: true, data: null }166 );167 await handleFieldInteraction(password);168 await vi.waitFor(() => expect(showSelector).toHaveBeenCalled());169 });170});171172describe("T-F10–T-F12: robustness", () => {173 174 175 176 it("T-F10/T-F12: detached field mid-interaction → clean abort, no throw, no selector", async () => {177 const detached = document.createElement("input");178 detached.type = "password"; 179 await expect(handleFieldInteraction(detached)).resolves.toBeUndefined();180 expect(showSelector).not.toHaveBeenCalled();181 });182183 184 185 it("T-F11: concurrent calls for the same field → selector opens at most once", async () => {186 const { password } = createLoginForm();187 setMessageResponder((msg) =>188 msg.type === "AUTOFILL_QUERY" ? { ok: true, data: [{ id: "e1", title: "Acme", username: "alice" }] } : { ok: true, data: null }189 );190 await Promise.all([handleFieldInteraction(password), handleFieldInteraction(password)]);191 await vi.waitFor(() => expect(showSelector).toHaveBeenCalled());192 expect(showSelector).toHaveBeenCalledTimes(1);193 });194});