import { describe, it, expect, vi, beforeEach } from 'vitest'; import { isExtensionValid, safeStorageGet, safeStorageSet, safeSendMessage, } from '@lib/messaging'; // Rebuild the chrome mock per test so each case controls runtime.id presence. function mockChrome({ valid = true }: { valid?: boolean } = {}) { const local = { get: vi.fn((_keys: string[], cb: (r: Record) => void) => cb({})), set: vi.fn((_data: Record, cb?: () => void) => cb && cb()), }; const session = { get: vi.fn((_keys: string[], cb: (r: Record) => void) => cb({})), set: vi.fn((_data: Record, cb?: () => void) => cb && cb()), }; global.chrome = { storage: { local, session }, runtime: { id: valid ? 'test-extension-id' : undefined, sendMessage: vi.fn().mockResolvedValue({ result: 'ok' }), }, } as any; return { local, session }; } beforeEach(() => { vi.restoreAllMocks(); }); describe('isExtensionValid', () => { it('is true when chrome.runtime.id exists', () => { mockChrome({ valid: true }); expect(isExtensionValid()).toBe(true); }); it('is false when the extension context is invalidated (no runtime.id)', () => { mockChrome({ valid: false }); expect(isExtensionValid()).toBe(false); }); }); describe('safeStorageGet', () => { it('reads from chrome.storage.local by default', () => { const { local } = mockChrome(); const cb = vi.fn(); safeStorageGet(['provider', 'model'], cb); expect(local.get).toHaveBeenCalledWith(['provider', 'model'], cb); expect(cb).toHaveBeenCalledWith({}); }); it('reads from an explicitly passed storage area (e.g. session)', () => { const { local, session } = mockChrome(); const cb = vi.fn(); safeStorageGet(['lexai_popup_input'], cb, (global.chrome as any).storage.session); expect(session.get).toHaveBeenCalled(); expect(local.get).not.toHaveBeenCalled(); }); it('is a silent no-op when the extension context is gone', () => { const { local } = mockChrome({ valid: false }); const cb = vi.fn(); safeStorageGet(['provider'], cb); expect(local.get).not.toHaveBeenCalled(); expect(cb).not.toHaveBeenCalled(); }); it('swallows a throwing storage call with a warning', () => { mockChrome(); (global.chrome as any).storage.local.get = vi.fn(() => { throw new Error('Extension context invalidated.'); }); const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); expect(() => safeStorageGet(['provider'], vi.fn())).not.toThrow(); expect(warn).toHaveBeenCalled(); }); }); describe('safeStorageSet', () => { it('writes to chrome.storage.local by default and honors the callback', () => { const { local } = mockChrome(); const done = vi.fn(); safeStorageSet({ provider: 'openai' }, done); expect(local.set).toHaveBeenCalledWith({ provider: 'openai' }, done); expect(done).toHaveBeenCalled(); }); it('writes without a callback', () => { const { local } = mockChrome(); safeStorageSet({ writingStyle: 'Formal' }); expect(local.set).toHaveBeenCalledWith({ writingStyle: 'Formal' }); }); it('writes to an explicitly passed storage area', () => { const { session } = mockChrome(); safeStorageSet({ lexai_popup_input: 'draft' }, undefined, (global.chrome as any).storage.session); expect(session.set).toHaveBeenCalledWith({ lexai_popup_input: 'draft' }); }); it('is a no-op when the extension context is gone', () => { const { local } = mockChrome({ valid: false }); safeStorageSet({ provider: 'openai' }); expect(local.set).not.toHaveBeenCalled(); }); }); describe('safeSendMessage', () => { it('resolves with the background response', async () => { mockChrome(); const res = await safeSendMessage({ type: 'ANALYZE_TEXT', payload: { text: 'hi', action: 'grammar' } }); expect(res).toEqual({ result: 'ok' }); expect((global.chrome as any).runtime.sendMessage).toHaveBeenCalledWith({ type: 'ANALYZE_TEXT', payload: { text: 'hi', action: 'grammar' }, }); }); it('returns null and fires onContextInvalidated when the context is gone up front', async () => { mockChrome({ valid: false }); const onInvalid = vi.fn(); const res = await safeSendMessage({ type: 'LIST_MODELS' }, onInvalid); expect(res).toBeNull(); expect(onInvalid).toHaveBeenCalledTimes(1); }); it('returns null and fires onContextInvalidated when sendMessage rejects with an invalidation error', async () => { mockChrome(); (global.chrome as any).runtime.sendMessage = vi .fn() .mockRejectedValue(new Error('Extension context invalidated.')); const onInvalid = vi.fn(); const res = await safeSendMessage({ type: 'COPY_AS', text: 'x', format: 'markdown' }, onInvalid); expect(res).toBeNull(); expect(onInvalid).toHaveBeenCalledTimes(1); }); it('returns null and warns (no invalidation callback) on other send failures', async () => { mockChrome(); (global.chrome as any).runtime.sendMessage = vi.fn().mockRejectedValue(new Error('boom')); const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); const onInvalid = vi.fn(); const res = await safeSendMessage({ type: 'LIST_MODELS' }, onInvalid); expect(res).toBeNull(); expect(onInvalid).not.toHaveBeenCalled(); expect(warn).toHaveBeenCalled(); }); });