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 getURL: (path: string) => `chrome-extension://peach-extension/${path}`,28 sendMessage: vi.fn(async () => ({ ok: true })),29 onMessage: {30 addListener: vi.fn((fn: MessageListener) => {31 messageListener = fn;32 }),33 },34 },35 storage: {36 local: {37 get: vi.fn(async (key?: string | string[]) => {38 if (!key) return storage;39 if (typeof key === 'string') return { [key]: storage[key] };40 return key.reduce<Record<string, unknown>>((acc, k) => { acc[k] = storage[k]; return acc; }, {});41 }),42 set: vi.fn(async (next: Record<string, unknown>) => { Object.assign(storage, next); }),43 remove: vi.fn(async (k: string | string[]) => {44 const keys = Array.isArray(k) ? k : [k];45 for (const key of keys) delete storage[key];46 }),47 },48 session: {49 get: vi.fn(async (key?: string | string[]) => {50 if (!key) return sessionStorage;51 if (typeof key === 'string') return { [key]: sessionStorage[key] };52 return key.reduce<Record<string, unknown>>((acc, k) => { acc[k] = sessionStorage[k]; return acc; }, {});53 }),54 set: vi.fn(async (next: Record<string, unknown>) => { Object.assign(sessionStorage, next); }),55 remove: vi.fn(async (k: string | string[]) => {56 const keys = Array.isArray(k) ? k : [k];57 for (const key of keys) delete sessionStorage[key];58 }),59 },60 },61 };62 (globalThis as { chrome?: typeof chrome }).chrome = chromeMock as unknown as typeof chrome;6364 return {65 storage,66 sessionStorage,67 listener: () => messageListener!,68 };69}70717273const mockPayload = {74 site: 'example.com',75 displayName: 'Example',76 username: 'june',77 requestId: 'req-1',78 toastToken: 'extension-token',79};8081function sendToContentScript(82 listener: MessageListener,83 msg: { type: string; payload?: unknown },84): void {85 const sender = {};86 const sendResponse = vi.fn();87 listener(msg, sender, sendResponse);88}89909192describe('PASSKEY_AUTH_RESPONSE handler', () => {93 let storage: StorageShape;94 let listener: (() => MessageListener);9596 beforeEach(async () => {97 vi.resetModules();98 const mocks = installChromeMock();99 storage = mocks.storage;100 listener = mocks.listener;101 storage['peach_settings'] = { breachMonitoring: false };102 vi.stubGlobal('fetch', vi.fn(async () => ({ ok: true, status: 200, text: async () => '' })));103 vi.stubGlobal('__PEACH_E2E_PASSKEYS__', false);104 (globalThis as Record<string, unknown>).chrome = (globalThis as Record<string, unknown>).chrome ?? {};105 ((globalThis as Record<string, unknown>).chrome as Record<string, unknown>).tabs = {106 query: vi.fn((_, cb?: (tabs: unknown[]) => void) => { if (cb) cb([]); return Promise.resolve([]); }),107 sendMessage: vi.fn(async () => ({ ok: true })),108 };109 const msgs = await import('../../background/messages');110 msgs.setupMessageHandler();111 const vault = await import('../../background/vault');112 await vault.ensureSettingsLoaded();113 });114115 async function dispatch(msg: { type: string; payload?: unknown }): Promise<Record<string, unknown>> {116 return new Promise((resolve) => {117 const keepOpen = listener()(msg as unknown as Parameters<MessageListener>[0], { id: 'peach-extension' }, (r) => resolve(r as Record<string, unknown>));118 expect(keepOpen).toBe(true);119 });120 }121122 it('returns ok:true when approved:true and payload is valid', async () => {123 const result = await dispatch({124 type: 'PASSKEY_AUTH_RESPONSE',125 payload: { requestId: 42, approved: true },126 });127 expect(result.ok).toBe(true);128 });129130 it('returns ok:true when approved:false and payload is valid', async () => {131 const result = await dispatch({132 type: 'PASSKEY_AUTH_RESPONSE',133 payload: { requestId: 42, approved: false },134 });135 expect(result.ok).toBe(true);136 });137138 it('returns error when requestId is missing', async () => {139 const result = await dispatch({140 type: 'PASSKEY_AUTH_RESPONSE',141 payload: { approved: true },142 });143 expect(result.ok).toBe(false);144 expect(result.error).toMatch(/requestId/);145 });146147 it('returns error when approved flag is missing', async () => {148 const result = await dispatch({149 type: 'PASSKEY_AUTH_RESPONSE',150 payload: { requestId: 42 },151 });152 expect(result.ok).toBe(false);153 expect(result.error).toMatch(/requestId/);154 });155});156157158159describe('toast injection', () => {160 let contentListener: MessageListener;161162 beforeEach(async () => {163 vi.resetModules();164165 166 document.body.innerHTML = '';167168 const chromeMock = {169 runtime: {170 sendMessage: vi.fn(async () => ({ ok: true })),171 onMessage: {172 addListener: vi.fn((fn: MessageListener) => {173 contentListener = fn;174 }),175 },176 },177 storage: {178 local: {179 get: vi.fn(async () => ({})),180 set: vi.fn(async () => {}),181 },182 },183 };184 (globalThis as { chrome?: unknown }).chrome = chromeMock;185186 187 await import('./inject');188 });189190 it('shows toast when SHOW_PASSKEY_TOAST message received', () => {191 sendToContentScript(contentListener, { type: 'SHOW_PASSKEY_TOAST', payload: mockPayload });192 expect(document.querySelector('#peach-passkey-toast-root')).toBeTruthy();193 });194195 it('second SHOW_PASSKEY_TOAST replaces existing toast', () => {196 sendToContentScript(contentListener, { type: 'SHOW_PASSKEY_TOAST', payload: mockPayload });197 sendToContentScript(contentListener, {198 type: 'SHOW_PASSKEY_TOAST',199 payload: { ...mockPayload, requestId: 'req-2' },200 });201 expect(document.querySelectorAll('#peach-passkey-toast-root').length).toBe(1);202 });203204 it('uses shadow DOM — container has a shadowRoot', () => {205 sendToContentScript(contentListener, { type: 'SHOW_PASSKEY_TOAST', payload: mockPayload });206 const root = document.querySelector('#peach-passkey-toast-root');207 208 209 expect(root).toBeTruthy();210 expect(root?.tagName.toLowerCase()).toBe('div');211 });212213 it('DISMISS_PASSKEY_TOAST removes toast', () => {214 sendToContentScript(contentListener, { type: 'SHOW_PASSKEY_TOAST', payload: mockPayload });215 expect(document.querySelector('#peach-passkey-toast-root')).toBeTruthy();216 sendToContentScript(contentListener, { type: 'DISMISS_PASSKEY_TOAST' });217 218 expect(document.querySelector('#peach-passkey-toast-root')).toBeTruthy();219 });220});221222223224describe('autofill blocking while toast active', () => {225 let contentListener: MessageListener;226227 beforeEach(async () => {228 vi.resetModules();229 document.body.innerHTML = '';230231 const chromeMock = {232 runtime: {233 sendMessage: vi.fn(async () => ({ ok: true })),234 onMessage: {235 addListener: vi.fn((fn: MessageListener) => {236 contentListener = fn;237 }),238 },239 },240 storage: {241 local: {242 get: vi.fn(async () => ({})),243 set: vi.fn(async () => {}),244 },245 },246 };247 (globalThis as { chrome?: unknown }).chrome = chromeMock;248 await import('./inject');249 });250251 it('toast is shown after SHOW_PASSKEY_TOAST', () => {252 sendToContentScript(contentListener, { type: 'SHOW_PASSKEY_TOAST', payload: mockPayload });253 expect(document.querySelector('#peach-passkey-toast-root')).toBeTruthy();254 });255256 it('toast is cleared after DISMISS_PASSKEY_TOAST', () => {257 sendToContentScript(contentListener, { type: 'SHOW_PASSKEY_TOAST', payload: mockPayload });258 sendToContentScript(contentListener, { type: 'DISMISS_PASSKEY_TOAST' });259 260 const root = document.querySelector('#peach-passkey-toast-root');261 expect(root).toBeTruthy();262 });263});