123import { describe, it, expect, vi, beforeEach } from 'vitest';4567type MessageListener = (8 msg: unknown,9 sender: unknown,10 sendResponse: (r: unknown) => void11) => boolean;1213type StorageShape = Record<string, unknown>;1415function installChromeMock(): {16 storage: StorageShape;17 sessionStorage: StorageShape;18 listener: () => MessageListener;19} {20 const storage: StorageShape = {};21 const sessionStorage: StorageShape = {};22 let messageListener: MessageListener | null = null;2324 const chromeMock = {25 runtime: {26 id: 'peach-extension',27 sendMessage: vi.fn(async () => ({ ok: true })),28 onMessage: {29 addListener: vi.fn((fn: MessageListener) => {30 messageListener = fn;31 }),32 },33 },34 storage: {35 local: {36 get: vi.fn(async (key?: string | string[]) => {37 if (!key) return storage;38 if (typeof key === 'string') return { [key]: storage[key] };39 return key.reduce<Record<string, unknown>>((acc, k) => { acc[k] = storage[k]; return acc; }, {});40 }),41 set: vi.fn(async (next: Record<string, unknown>) => { Object.assign(storage, next); }),42 remove: vi.fn(async (k: string | string[]) => {43 const keys = Array.isArray(k) ? k : [k];44 for (const key of keys) delete storage[key];45 }),46 },47 session: {48 get: vi.fn(async (key?: string | string[]) => {49 if (!key) return sessionStorage;50 if (typeof key === 'string') return { [key]: sessionStorage[key] };51 return key.reduce<Record<string, unknown>>((acc, k) => { acc[k] = sessionStorage[k]; return acc; }, {});52 }),53 set: vi.fn(async (next: Record<string, unknown>) => { Object.assign(sessionStorage, next); }),54 remove: vi.fn(async (k: string | string[]) => {55 const keys = Array.isArray(k) ? k : [k];56 for (const key of keys) delete sessionStorage[key];57 }),58 },59 },60 };61 (globalThis as { chrome?: typeof chrome }).chrome = chromeMock as unknown as typeof chrome;6263 return {64 storage,65 sessionStorage,66 listener: () => messageListener!,67 };68}69707172const mockPayload = {73 site: 'example.com',74 displayName: 'Example',75 username: 'june',76 requestId: 'req-1',77};7879function sendToContentScript(80 listener: MessageListener,81 msg: { type: string; payload?: unknown },82): void {83 const sender = {};84 const sendResponse = vi.fn();85 listener(msg, sender, sendResponse);86}87888990describe('PASSKEY_AUTH_RESPONSE handler', () => {91 let storage: StorageShape;92 let listener: (() => MessageListener);9394 beforeEach(async () => {95 vi.resetModules();96 const mocks = installChromeMock();97 storage = mocks.storage;98 listener = mocks.listener;99 storage['peach_settings'] = { breachMonitoring: false };100 vi.stubGlobal('fetch', vi.fn(async () => ({ ok: true, status: 200, text: async () => '' })));101 vi.stubGlobal('__PEACH_E2E_PASSKEYS__', false);102 (globalThis as Record<string, unknown>).chrome = (globalThis as Record<string, unknown>).chrome ?? {};103 ((globalThis as Record<string, unknown>).chrome as Record<string, unknown>).tabs = {104 query: vi.fn((_, cb?: (tabs: unknown[]) => void) => { if (cb) cb([]); return Promise.resolve([]); }),105 sendMessage: vi.fn(async () => ({ ok: true })),106 };107 const msgs = await import('../../background/messages');108 msgs.setupMessageHandler();109 const vault = await import('../../background/vault');110 await vault.ensureSettingsLoaded();111 });112113 async function dispatch(msg: { type: string; payload?: unknown }): Promise<Record<string, unknown>> {114 return new Promise((resolve) => {115 const keepOpen = listener()(msg as unknown as Parameters<MessageListener>[0], { id: 'peach-extension' }, (r) => resolve(r as Record<string, unknown>));116 expect(keepOpen).toBe(true);117 });118 }119120 it('returns ok:true when approved:true and payload is valid', async () => {121 const result = await dispatch({122 type: 'PASSKEY_AUTH_RESPONSE',123 payload: { requestId: 42, approved: true },124 });125 expect(result.ok).toBe(true);126 });127128 it('returns ok:true when approved:false and payload is valid', async () => {129 const result = await dispatch({130 type: 'PASSKEY_AUTH_RESPONSE',131 payload: { requestId: 42, approved: false },132 });133 expect(result.ok).toBe(true);134 });135136 it('returns error when requestId is missing', async () => {137 const result = await dispatch({138 type: 'PASSKEY_AUTH_RESPONSE',139 payload: { approved: true },140 });141 expect(result.ok).toBe(false);142 expect(result.error).toMatch(/requestId/);143 });144145 it('returns error when approved flag is missing', async () => {146 const result = await dispatch({147 type: 'PASSKEY_AUTH_RESPONSE',148 payload: { requestId: 42 },149 });150 expect(result.ok).toBe(false);151 expect(result.error).toMatch(/requestId/);152 });153});154155156157describe('toast injection', () => {158 let contentListener: MessageListener;159160 beforeEach(async () => {161 vi.resetModules();162163 164 document.body.innerHTML = '';165166 const chromeMock = {167 runtime: {168 sendMessage: vi.fn(async () => ({ ok: true })),169 onMessage: {170 addListener: vi.fn((fn: MessageListener) => {171 contentListener = fn;172 }),173 },174 },175 storage: {176 local: {177 get: vi.fn(async () => ({})),178 set: vi.fn(async () => {}),179 },180 },181 };182 (globalThis as { chrome?: unknown }).chrome = chromeMock;183184 185 await import('./inject');186 });187188 it('shows toast when SHOW_PASSKEY_TOAST message received', () => {189 sendToContentScript(contentListener, { type: 'SHOW_PASSKEY_TOAST', payload: mockPayload });190 expect(document.querySelector('#peach-passkey-toast-root')).toBeTruthy();191 });192193 it('second SHOW_PASSKEY_TOAST replaces existing toast', () => {194 sendToContentScript(contentListener, { type: 'SHOW_PASSKEY_TOAST', payload: mockPayload });195 sendToContentScript(contentListener, {196 type: 'SHOW_PASSKEY_TOAST',197 payload: { ...mockPayload, requestId: 'req-2' },198 });199 expect(document.querySelectorAll('#peach-passkey-toast-root').length).toBe(1);200 });201202 it('uses shadow DOM — container has a shadowRoot', () => {203 sendToContentScript(contentListener, { type: 'SHOW_PASSKEY_TOAST', payload: mockPayload });204 const root = document.querySelector('#peach-passkey-toast-root');205 206 207 expect(root).toBeTruthy();208 expect(root?.tagName.toLowerCase()).toBe('div');209 });210211 it('DISMISS_PASSKEY_TOAST removes toast', () => {212 sendToContentScript(contentListener, { type: 'SHOW_PASSKEY_TOAST', payload: mockPayload });213 expect(document.querySelector('#peach-passkey-toast-root')).toBeTruthy();214 sendToContentScript(contentListener, { type: 'DISMISS_PASSKEY_TOAST' });215 216 expect(document.querySelector('#peach-passkey-toast-root')).toBeTruthy();217 });218});219220221222describe('autofill blocking while toast active', () => {223 let contentListener: MessageListener;224225 beforeEach(async () => {226 vi.resetModules();227 document.body.innerHTML = '';228229 const chromeMock = {230 runtime: {231 sendMessage: vi.fn(async () => ({ ok: true })),232 onMessage: {233 addListener: vi.fn((fn: MessageListener) => {234 contentListener = fn;235 }),236 },237 },238 storage: {239 local: {240 get: vi.fn(async () => ({})),241 set: vi.fn(async () => {}),242 },243 },244 };245 (globalThis as { chrome?: unknown }).chrome = chromeMock;246 await import('./inject');247 });248249 it('toast is shown after SHOW_PASSKEY_TOAST', () => {250 sendToContentScript(contentListener, { type: 'SHOW_PASSKEY_TOAST', payload: mockPayload });251 expect(document.querySelector('#peach-passkey-toast-root')).toBeTruthy();252 });253254 it('toast is cleared after DISMISS_PASSKEY_TOAST', () => {255 sendToContentScript(contentListener, { type: 'SHOW_PASSKEY_TOAST', payload: mockPayload });256 sendToContentScript(contentListener, { type: 'DISMISS_PASSKEY_TOAST' });257 258 const root = document.querySelector('#peach-passkey-toast-root');259 expect(root).toBeTruthy();260 });261});