import { describe, it, expect, vi, beforeEach } from 'vitest'; import { PluginManager } from '../src/core/PluginManager'; import type { LightPlugin, LightClient, SentryEvent } from '../src/types'; describe('PluginManager', () => { let mockClient: LightClient; let consoleSpy: ReturnType; beforeEach(() => { mockClient = { config: { dsn: 'https://abc@sentry.io/123' }, dsn: { protocol: 'https', publicKey: 'abc', host: 'sentry.io', projectId: '123' }, on: vi.fn(), off: vi.fn(), emit: vi.fn(), captureException: vi.fn(), captureMessage: vi.fn(), captureEvent: vi.fn(), setUser: vi.fn(), setAnonymousId: vi.fn(), setTag: vi.fn(), setTags: vi.fn(), setExtra: vi.fn(), addBreadcrumb: vi.fn(), startTransaction: vi.fn(), getCurrentTransaction: vi.fn(), getCurrentSpan: vi.fn(), setSpan: vi.fn(), startSpan: vi.fn(), flush: vi.fn(), disable: vi.fn(), enable: vi.fn(), use: vi.fn(), init: vi.fn(), destroy: vi.fn(), } as unknown as LightClient; consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); }); describe('constructor', () => { it('should create a PluginManager instance', () => { const manager = new PluginManager(mockClient); expect(manager).toBeDefined(); }); }); describe('add', () => { it('should add a plugin', () => { const manager = new PluginManager(mockClient); const plugin: LightPlugin = { name: 'test-plugin', version: '1.0.0', setup: vi.fn(), }; manager.add(plugin); expect(manager.has('test-plugin')).toBe(true); }); it('should not add duplicate plugin', () => { const manager = new PluginManager(mockClient); const setup1 = vi.fn(); const setup2 = vi.fn(); const plugin1: LightPlugin = { name: 'test-plugin', version: '1.0.0', setup: setup1, }; const plugin2: LightPlugin = { name: 'test-plugin', version: '2.0.0', setup: setup2, }; manager.add(plugin1); manager.add(plugin2); expect(manager.has('test-plugin')).toBe(true); expect(setup1).toHaveBeenCalledWith(mockClient); expect(setup2).not.toHaveBeenCalled(); }); it('should call plugin setup with client', () => { const manager = new PluginManager(mockClient); const setupFn = vi.fn(); const plugin: LightPlugin = { name: 'setup-test', version: '1.0.0', setup: setupFn, }; manager.add(plugin); expect(setupFn).toHaveBeenCalledWith(mockClient); }); it('should not throw if setup throws', () => { const manager = new PluginManager(mockClient); const plugin: LightPlugin = { name: 'error-plugin', version: '1.0.0', setup: () => { throw new Error('Setup error'); }, }; expect(() => manager.add(plugin)).not.toThrow(); expect(consoleSpy).toHaveBeenCalled(); }); }); describe('remove', () => { it('should remove a plugin', () => { const manager = new PluginManager(mockClient); const plugin: LightPlugin = { name: 'removable', version: '1.0.0', setup: vi.fn(), }; manager.add(plugin); expect(manager.has('removable')).toBe(true); manager.remove('removable'); expect(manager.has('removable')).toBe(false); }); it('should call plugin destroy when removing', () => { const manager = new PluginManager(mockClient); const destroyFn = vi.fn(); const plugin: LightPlugin = { name: 'destroyable', version: '1.0.0', setup: vi.fn(), destroy: destroyFn, }; manager.add(plugin); manager.remove('destroyable'); expect(destroyFn).toHaveBeenCalled(); }); it('should not throw if destroy throws', () => { const manager = new PluginManager(mockClient); const plugin: LightPlugin = { name: 'destroy-error', version: '1.0.0', setup: vi.fn(), destroy: () => { throw new Error('Destroy error'); }, }; manager.add(plugin); expect(() => manager.remove('destroy-error')).not.toThrow(); expect(consoleSpy).toHaveBeenCalled(); }); it('should do nothing if plugin does not exist', () => { const manager = new PluginManager(mockClient); manager.remove('non-existent'); expect(consoleSpy).not.toHaveBeenCalled(); }); it('should do nothing if plugin has no destroy method', () => { const manager = new PluginManager(mockClient); const plugin: LightPlugin = { name: 'no-destroy', version: '1.0.0', setup: vi.fn(), }; manager.add(plugin); expect(() => manager.remove('no-destroy')).not.toThrow(); }); }); describe('get', () => { it('should return plugin by name', () => { const manager = new PluginManager(mockClient); const plugin: LightPlugin = { name: 'gettable', version: '1.0.0', setup: vi.fn(), }; manager.add(plugin); const retrieved = manager.get('gettable'); expect(retrieved).toBe(plugin); }); it('should return undefined for non-existent plugin', () => { const manager = new PluginManager(mockClient); const retrieved = manager.get('non-existent'); expect(retrieved).toBeUndefined(); }); }); describe('has', () => { it('should return true for added plugin', () => { const manager = new PluginManager(mockClient); const plugin: LightPlugin = { name: 'checkable', version: '1.0.0', setup: vi.fn(), }; manager.add(plugin); expect(manager.has('checkable')).toBe(true); }); it('should return false for non-existent plugin', () => { const manager = new PluginManager(mockClient); expect(manager.has('non-existent')).toBe(false); }); }); describe('applyBeforeReport', () => { it('should pass event through all plugins', () => { const manager = new PluginManager(mockClient); const plugin1: LightPlugin = { name: 'before-1', version: '1.0.0', setup: vi.fn(), beforeReport: (event) => ({ ...event, extra: { ...event.extra, from1: true } }), }; const plugin2: LightPlugin = { name: 'before-2', version: '1.0.0', setup: vi.fn(), beforeReport: (event) => ({ ...event, extra: { ...event.extra, from2: true } }), }; manager.add(plugin1); manager.add(plugin2); const event = { type: 'error', level: 'error', timestamp: Date.now() } as SentryEvent; const result = manager.applyBeforeReport(event); expect(result.extra).toEqual({ from1: true, from2: true }); }); it('should return null if any plugin filters the event', () => { const manager = new PluginManager(mockClient); const plugin1: LightPlugin = { name: 'filter-1', version: '1.0.0', setup: vi.fn(), beforeReport: (event) => event, }; const plugin2: LightPlugin = { name: 'filter-2', version: '1.0.0', setup: vi.fn(), beforeReport: () => null, }; manager.add(plugin1); manager.add(plugin2); const event = { type: 'error', level: 'error', timestamp: Date.now() } as SentryEvent; const result = manager.applyBeforeReport(event); expect(result).toBeNull(); }); it('should continue if plugin beforeReport throws', () => { const manager = new PluginManager(mockClient); const plugin1: LightPlugin = { name: 'error-before', version: '1.0.0', setup: vi.fn(), beforeReport: () => { throw new Error('BeforeReport error'); }, }; const plugin2: LightPlugin = { name: 'ok-after', version: '1.0.0', setup: vi.fn(), beforeReport: (event) => ({ ...event, extra: { passed: true } }), }; manager.add(plugin1); manager.add(plugin2); const event = { type: 'error', level: 'error', timestamp: Date.now() } as SentryEvent; const result = manager.applyBeforeReport(event); expect(result?.extra).toEqual({ passed: true }); expect(consoleSpy).toHaveBeenCalled(); }); it('should skip plugin without beforeReport', () => { const manager = new PluginManager(mockClient); const plugin1: LightPlugin = { name: 'no-before', version: '1.0.0', setup: vi.fn(), }; const plugin2: LightPlugin = { name: 'modify-after', version: '1.0.0', setup: vi.fn(), beforeReport: (event) => ({ ...event, extra: { modified: true } }), }; manager.add(plugin1); manager.add(plugin2); const event = { type: 'error', level: 'error', timestamp: Date.now() } as SentryEvent; const result = manager.applyBeforeReport(event); expect(result?.extra).toEqual({ modified: true }); }); it('should return event when no plugins', () => { const manager = new PluginManager(mockClient); const event = { type: 'error', level: 'error', timestamp: Date.now() } as SentryEvent; const result = manager.applyBeforeReport(event); expect(result).toBe(event); }); }); describe('applyAfterReport', () => { it('should call afterReport on all plugins', () => { const manager = new PluginManager(mockClient); const after1 = vi.fn(); const after2 = vi.fn(); const plugin1: LightPlugin = { name: 'after-1', version: '1.0.0', setup: vi.fn(), afterReport: after1, }; const plugin2: LightPlugin = { name: 'after-2', version: '1.0.0', setup: vi.fn(), afterReport: after2, }; manager.add(plugin1); manager.add(plugin2); const event = { type: 'error', level: 'error', timestamp: Date.now() } as SentryEvent; manager.applyAfterReport(event); expect(after1).toHaveBeenCalledWith(event); expect(after2).toHaveBeenCalledWith(event); }); it('should continue if afterReport throws', () => { const manager = new PluginManager(mockClient); const plugin1: LightPlugin = { name: 'error-after', version: '1.0.0', setup: vi.fn(), afterReport: () => { throw new Error('AfterReport error'); }, }; const plugin2: LightPlugin = { name: 'ok-after', version: '1.0.0', setup: vi.fn(), afterReport: vi.fn(), }; manager.add(plugin1); manager.add(plugin2); const event = { type: 'error', level: 'error', timestamp: Date.now() } as SentryEvent; expect(() => manager.applyAfterReport(event)).not.toThrow(); expect(consoleSpy).toHaveBeenCalled(); }); it('should skip plugin without afterReport', () => { const manager = new PluginManager(mockClient); const plugin: LightPlugin = { name: 'no-after', version: '1.0.0', setup: vi.fn(), }; manager.add(plugin); const event = { type: 'error', level: 'error', timestamp: Date.now() } as SentryEvent; expect(() => manager.applyAfterReport(event)).not.toThrow(); }); }); describe('destroy', () => { it('should call destroy on all plugins', () => { const manager = new PluginManager(mockClient); const destroy1 = vi.fn(); const destroy2 = vi.fn(); const plugin1: LightPlugin = { name: 'destroy-1', version: '1.0.0', setup: vi.fn(), destroy: destroy1, }; const plugin2: LightPlugin = { name: 'destroy-2', version: '1.0.0', setup: vi.fn(), destroy: destroy2, }; manager.add(plugin1); manager.add(plugin2); manager.destroy(); expect(destroy1).toHaveBeenCalled(); expect(destroy2).toHaveBeenCalled(); }); it('should clear plugins after destroy', () => { const manager = new PluginManager(mockClient); const plugin: LightPlugin = { name: 'clear-me', version: '1.0.0', setup: vi.fn(), }; manager.add(plugin); expect(manager.has('clear-me')).toBe(true); manager.destroy(); expect(manager.has('clear-me')).toBe(false); }); it('should continue even if destroy throws', () => { const manager = new PluginManager(mockClient); const plugin1: LightPlugin = { name: 'destroy-error', version: '1.0.0', setup: vi.fn(), destroy: () => { throw new Error('Destroy error'); }, }; const plugin2: LightPlugin = { name: 'destroy-ok', version: '1.0.0', setup: vi.fn(), destroy: vi.fn(), }; manager.add(plugin1); manager.add(plugin2); manager.destroy(); expect(consoleSpy).toHaveBeenCalled(); expect((plugin2 as any).destroy).toHaveBeenCalled(); }); it('should not throw if plugin has no destroy method', () => { const manager = new PluginManager(mockClient); const plugin: LightPlugin = { name: 'no-destroy', version: '1.0.0', setup: vi.fn(), }; manager.add(plugin); expect(() => manager.destroy()).not.toThrow(); }); }); });