import { describe, it, expect, vi, beforeEach } from 'vitest'; import { createPluginAPI, setSlotRegistrationBridge } from '../plugin-api'; import type { InstalledPlugin } from '../plugin-types'; import { clearAllHooks } from '../plugin-hooks'; function makePlugin(overrides: Partial = {}): InstalledPlugin { return { id: 'test-plugin', name: 'Test Plugin', version: '1.0.0', author: 'Test', description: '', type: 'ui-extension', entrypoint: 'index.js', permissions: [], enabled: true, status: 'running', settings: {}, ...overrides, }; } beforeEach(() => { clearAllHooks(); localStorage.clear(); setSlotRegistrationBridge(null); }); describe('createPluginAPI', () => { it('exposes plugin info', () => { const plugin = makePlugin(); const api = createPluginAPI(plugin); expect(api.plugin.id).toBe('test-plugin'); expect(api.plugin.version).toBe('1.0.0'); }); it('returns a frozen copy of settings', () => { const plugin = makePlugin({ settings: { key: 'val' } }); const api = createPluginAPI(plugin); expect(api.plugin.settings).toEqual({ key: 'val' }); }); }); describe('plugin storage (scoped localStorage)', () => { it('set and get a value', () => { const api = createPluginAPI(makePlugin()); api.storage.set('foo', 42); expect(api.storage.get('foo')).toBe(42); }); it('scopes to plugin id', () => { const api1 = createPluginAPI(makePlugin({ id: 'p1' })); const api2 = createPluginAPI(makePlugin({ id: 'p2' })); api1.storage.set('key', 'a'); api2.storage.set('key', 'b'); expect(api1.storage.get('key')).toBe('a'); expect(api2.storage.get('key')).toBe('b'); }); it('remove deletes a value', () => { const api = createPluginAPI(makePlugin()); api.storage.set('x', 10); api.storage.remove('x'); expect(api.storage.get('x')).toBeNull(); }); it('keys lists only plugin-scoped keys', () => { const api = createPluginAPI(makePlugin({ id: 'kp' })); api.storage.set('a', 1); api.storage.set('b', 2); localStorage.setItem('unrelated', 'val'); expect(api.storage.keys()).toContain('a'); expect(api.storage.keys()).toContain('b'); expect(api.storage.keys()).not.toContain('unrelated'); }); }); describe('plugin logger', () => { it('prefixes log messages with plugin id', () => { const infoSpy = vi.spyOn(console, 'info').mockImplementation(() => {}); const api = createPluginAPI(makePlugin({ id: 'log-test' })); api.log.info('hello'); expect(infoSpy).toHaveBeenCalledWith('[plugin:log-test]', 'hello'); infoSpy.mockRestore(); }); }); describe('hooks permission gating', () => { it('returns no-op disposable without permission', () => { const plugin = makePlugin({ permissions: [] }); // no email:read const api = createPluginAPI(plugin); const d = api.hooks.onEmailOpen(vi.fn()); expect(d).toBeDefined(); expect(d.dispose).toBeInstanceOf(Function); }); it('registers handler when permission is granted', () => { const plugin = makePlugin({ permissions: ['email:read'] }); const api = createPluginAPI(plugin); const fn = vi.fn(); const d = api.hooks.onEmailOpen(fn); expect(d).toBeDefined(); d.dispose(); // should not throw }); }); describe('ui permission requirement', () => { it('throws without ui:toolbar permission', () => { const plugin = makePlugin({ permissions: [] }); const api = createPluginAPI(plugin); expect(() => api.ui.registerToolbarAction({ id: 'test', label: 'Test', onClick: () => {}, })).toThrow('lacks permission'); }); it('does not throw with correct permission (slot bridge not set, returns no-op)', () => { const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); const plugin = makePlugin({ permissions: ['ui:toolbar'] }); const api = createPluginAPI(plugin); const d = api.ui.registerToolbarAction({ id: 'test', label: 'Test', onClick: () => {} }); expect(d.dispose).toBeInstanceOf(Function); consoleSpy.mockRestore(); }); }); describe('slot registration bridge', () => { it('calls bridge when set', () => { const bridge = vi.fn((_name, _reg) => ({ dispose: () => {} })); setSlotRegistrationBridge(bridge); const plugin = makePlugin({ permissions: ['ui:email-footer'] }); const api = createPluginAPI(plugin); const DummyComponent = () => null; api.ui.registerEmailFooter(DummyComponent); expect(bridge).toHaveBeenCalled(); }); }); describe('toast bridge', () => { it('exposes success/error/info/warning methods', () => { const plugin = makePlugin(); const api = createPluginAPI(plugin); expect(api.toast.success).toBeInstanceOf(Function); expect(api.toast.error).toBeInstanceOf(Function); expect(api.toast.info).toBeInstanceOf(Function); expect(api.toast.warning).toBeInstanceOf(Function); }); });