// @vitest-environment jsdom import { describe, it, expect, vi, beforeEach } from 'vitest'; // ── chrome mock ────────────────────────────────────────────────────────────── type MessageListener = ( msg: unknown, sender: unknown, sendResponse: (r: unknown) => void ) => boolean; type StorageShape = Record; function installChromeMock(): { storage: StorageShape; sessionStorage: StorageShape; listener: () => MessageListener; } { const storage: StorageShape = {}; const sessionStorage: StorageShape = {}; let messageListener: MessageListener | null = null; const chromeMock = { runtime: { id: 'peach-extension', sendMessage: vi.fn(async () => ({ ok: true })), onMessage: { addListener: vi.fn((fn: MessageListener) => { messageListener = fn; }), }, }, storage: { local: { get: vi.fn(async (key?: string | string[]) => { if (!key) return storage; if (typeof key === 'string') return { [key]: storage[key] }; return key.reduce>((acc, k) => { acc[k] = storage[k]; return acc; }, {}); }), set: vi.fn(async (next: Record) => { Object.assign(storage, next); }), remove: vi.fn(async (k: string | string[]) => { const keys = Array.isArray(k) ? k : [k]; for (const key of keys) delete storage[key]; }), }, session: { get: vi.fn(async (key?: string | string[]) => { if (!key) return sessionStorage; if (typeof key === 'string') return { [key]: sessionStorage[key] }; return key.reduce>((acc, k) => { acc[k] = sessionStorage[k]; return acc; }, {}); }), set: vi.fn(async (next: Record) => { Object.assign(sessionStorage, next); }), remove: vi.fn(async (k: string | string[]) => { const keys = Array.isArray(k) ? k : [k]; for (const key of keys) delete sessionStorage[key]; }), }, }, }; (globalThis as { chrome?: typeof chrome }).chrome = chromeMock as unknown as typeof chrome; return { storage, sessionStorage, listener: () => messageListener!, }; } // ── helpers ────────────────────────────────────────────────────────────────── const mockPayload = { site: 'example.com', displayName: 'Example', username: 'june', requestId: 'req-1', }; function sendToContentScript( listener: MessageListener, msg: { type: string; payload?: unknown }, ): void { const sender = {}; const sendResponse = vi.fn(); listener(msg, sender, sendResponse); } // ── PASSKEY_AUTH_RESPONSE background handler ───────────────────────────────── describe('PASSKEY_AUTH_RESPONSE handler', () => { let storage: StorageShape; let listener: (() => MessageListener); beforeEach(async () => { vi.resetModules(); const mocks = installChromeMock(); storage = mocks.storage; listener = mocks.listener; storage['peach_settings'] = { breachMonitoring: false }; vi.stubGlobal('fetch', vi.fn(async () => ({ ok: true, status: 200, text: async () => '' }))); vi.stubGlobal('__PEACH_E2E_PASSKEYS__', false); (globalThis as Record).chrome = (globalThis as Record).chrome ?? {}; ((globalThis as Record).chrome as Record).tabs = { query: vi.fn((_, cb?: (tabs: unknown[]) => void) => { if (cb) cb([]); return Promise.resolve([]); }), sendMessage: vi.fn(async () => ({ ok: true })), }; const msgs = await import('../../background/messages'); msgs.setupMessageHandler(); const vault = await import('../../background/vault'); await vault.ensureSettingsLoaded(); }); async function dispatch(msg: { type: string; payload?: unknown }): Promise> { return new Promise((resolve) => { const keepOpen = listener()(msg as unknown as Parameters[0], { id: 'peach-extension' }, (r) => resolve(r as Record)); expect(keepOpen).toBe(true); }); } it('returns ok:true when approved:true and payload is valid', async () => { const result = await dispatch({ type: 'PASSKEY_AUTH_RESPONSE', payload: { requestId: 42, approved: true }, }); expect(result.ok).toBe(true); }); it('returns ok:true when approved:false and payload is valid', async () => { const result = await dispatch({ type: 'PASSKEY_AUTH_RESPONSE', payload: { requestId: 42, approved: false }, }); expect(result.ok).toBe(true); }); it('returns error when requestId is missing', async () => { const result = await dispatch({ type: 'PASSKEY_AUTH_RESPONSE', payload: { approved: true }, }); expect(result.ok).toBe(false); expect(result.error).toMatch(/requestId/); }); it('returns error when approved flag is missing', async () => { const result = await dispatch({ type: 'PASSKEY_AUTH_RESPONSE', payload: { requestId: 42 }, }); expect(result.ok).toBe(false); expect(result.error).toMatch(/requestId/); }); }); // ── Toast injection ─────────────────────────────────────────────────────────── describe('toast injection', () => { let contentListener: MessageListener; beforeEach(async () => { vi.resetModules(); // Reset DOM document.body.innerHTML = ''; const chromeMock = { runtime: { sendMessage: vi.fn(async () => ({ ok: true })), onMessage: { addListener: vi.fn((fn: MessageListener) => { contentListener = fn; }), }, }, storage: { local: { get: vi.fn(async () => ({})), set: vi.fn(async () => {}), }, }, }; (globalThis as { chrome?: unknown }).chrome = chromeMock; // Import the content script to register its listener await import('./inject'); }); it('shows toast when SHOW_PASSKEY_TOAST message received', () => { sendToContentScript(contentListener, { type: 'SHOW_PASSKEY_TOAST', payload: mockPayload }); expect(document.querySelector('#peach-passkey-toast-root')).toBeTruthy(); }); it('second SHOW_PASSKEY_TOAST replaces existing toast', () => { sendToContentScript(contentListener, { type: 'SHOW_PASSKEY_TOAST', payload: mockPayload }); sendToContentScript(contentListener, { type: 'SHOW_PASSKEY_TOAST', payload: { ...mockPayload, requestId: 'req-2' }, }); expect(document.querySelectorAll('#peach-passkey-toast-root').length).toBe(1); }); it('uses shadow DOM — container has a shadowRoot', () => { sendToContentScript(contentListener, { type: 'SHOW_PASSKEY_TOAST', payload: mockPayload }); const root = document.querySelector('#peach-passkey-toast-root'); // shadow mode is 'closed', so shadowRoot returns null from the outside; // we verify the element exists and is positioned correctly instead. expect(root).toBeTruthy(); expect(root?.tagName.toLowerCase()).toBe('div'); }); it('DISMISS_PASSKEY_TOAST removes toast', () => { sendToContentScript(contentListener, { type: 'SHOW_PASSKEY_TOAST', payload: mockPayload }); expect(document.querySelector('#peach-passkey-toast-root')).toBeTruthy(); sendToContentScript(contentListener, { type: 'DISMISS_PASSKEY_TOAST' }); // Container element stays in DOM; the toast content inside it is cleared expect(document.querySelector('#peach-passkey-toast-root')).toBeTruthy(); }); }); // ── Autofill blocking while toast active ────────────────────────────────────── describe('autofill blocking while toast active', () => { let contentListener: MessageListener; beforeEach(async () => { vi.resetModules(); document.body.innerHTML = ''; const chromeMock = { runtime: { sendMessage: vi.fn(async () => ({ ok: true })), onMessage: { addListener: vi.fn((fn: MessageListener) => { contentListener = fn; }), }, }, storage: { local: { get: vi.fn(async () => ({})), set: vi.fn(async () => {}), }, }, }; (globalThis as { chrome?: unknown }).chrome = chromeMock; await import('./inject'); }); it('toast is shown after SHOW_PASSKEY_TOAST', () => { sendToContentScript(contentListener, { type: 'SHOW_PASSKEY_TOAST', payload: mockPayload }); expect(document.querySelector('#peach-passkey-toast-root')).toBeTruthy(); }); it('toast is cleared after DISMISS_PASSKEY_TOAST', () => { sendToContentScript(contentListener, { type: 'SHOW_PASSKEY_TOAST', payload: mockPayload }); sendToContentScript(contentListener, { type: 'DISMISS_PASSKEY_TOAST' }); // The container persists; the toast content is cleaned up const root = document.querySelector('#peach-passkey-toast-root'); expect(root).toBeTruthy(); }); });