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};7980function sendToContentScript(81 listener: MessageListener,82 msg: { type: string; payload?: unknown },83): void {84 const sender = {};85 const sendResponse = vi.fn();86 listener(msg, sender, sendResponse);87}88899091describe('PASSKEY_AUTH_RESPONSE handler', () => {92 let storage: StorageShape;93 let listener: (() => MessageListener);9495 beforeEach(async () => {96 vi.resetModules();97 const mocks = installChromeMock();98 storage = mocks.storage;99 listener = mocks.listener;100 storage['peach_settings'] = { breachMonitoring: false };101 vi.stubGlobal('fetch', vi.fn(async () => ({ ok: true, status: 200, text: async () => '' })));102 vi.stubGlobal('__PEACH_E2E_PASSKEYS__', false);103 (globalThis as Record<string, unknown>).chrome = (globalThis as Record<string, unknown>).chrome ?? {};104 ((globalThis as Record<string, unknown>).chrome as Record<string, unknown>).tabs = {105 query: vi.fn((_, cb?: (tabs: unknown[]) => void) => { if (cb) cb([]); return Promise.resolve([]); }),106 sendMessage: vi.fn(async () => ({ ok: true })),107 };108 const msgs = await import('../../background/messages');109 msgs.setupMessageHandler();110 const vault = await import('../../background/vault');111 await vault.ensureSettingsLoaded();112 });113114 async function dispatch(msg: { type: string; payload?: unknown }): Promise<Record<string, unknown>> {115 return new Promise((resolve) => {116 const keepOpen = listener()(msg as unknown as Parameters<MessageListener>[0], { id: 'peach-extension' }, (r) => resolve(r as Record<string, unknown>));117 expect(keepOpen).toBe(true);118 });119 }120121 it('returns ok:true when approved:true and payload is valid', async () => {122 const result = await dispatch({123 type: 'PASSKEY_AUTH_RESPONSE',124 payload: { requestId: 42, approved: true },125 });126 expect(result.ok).toBe(true);127 });128129 it('returns ok:true when approved:false and payload is valid', async () => {130 const result = await dispatch({131 type: 'PASSKEY_AUTH_RESPONSE',132 payload: { requestId: 42, approved: false },133 });134 expect(result.ok).toBe(true);135 });136137 it('returns error when requestId is missing', async () => {138 const result = await dispatch({139 type: 'PASSKEY_AUTH_RESPONSE',140 payload: { approved: true },141 });142 expect(result.ok).toBe(false);143 expect(result.error).toMatch(/requestId/);144 });145146 it('returns error when approved flag is missing', async () => {147 const result = await dispatch({148 type: 'PASSKEY_AUTH_RESPONSE',149 payload: { requestId: 42 },150 });151 expect(result.ok).toBe(false);152 expect(result.error).toMatch(/requestId/);153 });154});155156157158describe('toast injection', () => {159 let contentListener: MessageListener;160161 beforeEach(async () => {162 vi.resetModules();163164 165 document.body.innerHTML = '';166167 const chromeMock = {168 runtime: {169 sendMessage: vi.fn(async () => ({ ok: true })),170 onMessage: {171 addListener: vi.fn((fn: MessageListener) => {172 contentListener = fn;173 }),174 },175 },176 storage: {177 local: {178 get: vi.fn(async () => ({})),179 set: vi.fn(async () => {}),180 },181 },182 };183 (globalThis as { chrome?: unknown }).chrome = chromeMock;184185 186 await import('./inject');187 });188189 it('shows toast when SHOW_PASSKEY_TOAST message received', () => {190 sendToContentScript(contentListener, { type: 'SHOW_PASSKEY_TOAST', payload: mockPayload });191 expect(document.querySelector('#peach-passkey-toast-root')).toBeTruthy();192 });193194 it('second SHOW_PASSKEY_TOAST replaces existing toast', () => {195 sendToContentScript(contentListener, { type: 'SHOW_PASSKEY_TOAST', payload: mockPayload });196 sendToContentScript(contentListener, {197 type: 'SHOW_PASSKEY_TOAST',198 payload: { ...mockPayload, requestId: 'req-2' },199 });200 expect(document.querySelectorAll('#peach-passkey-toast-root').length).toBe(1);201 });202203 it('uses shadow DOM — container has a shadowRoot', () => {204 sendToContentScript(contentListener, { type: 'SHOW_PASSKEY_TOAST', payload: mockPayload });205 const root = document.querySelector('#peach-passkey-toast-root');206 207 208 expect(root).toBeTruthy();209 expect(root?.tagName.toLowerCase()).toBe('div');210 });211212 it('DISMISS_PASSKEY_TOAST removes toast', () => {213 sendToContentScript(contentListener, { type: 'SHOW_PASSKEY_TOAST', payload: mockPayload });214 expect(document.querySelector('#peach-passkey-toast-root')).toBeTruthy();215 sendToContentScript(contentListener, { type: 'DISMISS_PASSKEY_TOAST' });216 217 expect(document.querySelector('#peach-passkey-toast-root')).toBeTruthy();218 });219});220221222223describe('autofill blocking while toast active', () => {224 let contentListener: MessageListener;225226 beforeEach(async () => {227 vi.resetModules();228 document.body.innerHTML = '';229230 const chromeMock = {231 runtime: {232 sendMessage: vi.fn(async () => ({ ok: true })),233 onMessage: {234 addListener: vi.fn((fn: MessageListener) => {235 contentListener = fn;236 }),237 },238 },239 storage: {240 local: {241 get: vi.fn(async () => ({})),242 set: vi.fn(async () => {}),243 },244 },245 };246 (globalThis as { chrome?: unknown }).chrome = chromeMock;247 await import('./inject');248 });249250 it('toast is shown after SHOW_PASSKEY_TOAST', () => {251 sendToContentScript(contentListener, { type: 'SHOW_PASSKEY_TOAST', payload: mockPayload });252 expect(document.querySelector('#peach-passkey-toast-root')).toBeTruthy();253 });254255 it('toast is cleared after DISMISS_PASSKEY_TOAST', () => {256 sendToContentScript(contentListener, { type: 'SHOW_PASSKEY_TOAST', payload: mockPayload });257 sendToContentScript(contentListener, { type: 'DISMISS_PASSKEY_TOAST' });258 259 const root = document.querySelector('#peach-passkey-toast-root');260 expect(root).toBeTruthy();261 });262});