mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-08-02 10:00:17 +05:30
feat(plugins): add PluginContext, PluginRegistry, and SettingsStore
- PluginContext: scoped API with crash-safe command wrappers - PluginRegistry: lifecycle management with graceful init failure - SettingsStore: plugin-scoped key/value via IPC backend - Export hooks: pre/post hooks on registry for cross-plugin integration Amit Haridas
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
const { PluginContext } = require('../src/plugins/plugin-context');
|
||||
const { EventBus } = require('../src/plugins/event-bus');
|
||||
|
||||
describe('PluginContext', () => {
|
||||
let context;
|
||||
let mockDeps;
|
||||
|
||||
beforeEach(() => {
|
||||
mockDeps = {
|
||||
pluginId: 'test-plugin',
|
||||
sidebar: { registerPanel: jest.fn() },
|
||||
commands: { register: jest.fn() },
|
||||
statusBar: { registerIndicator: jest.fn() },
|
||||
eventBus: new EventBus(),
|
||||
settings: { get: jest.fn(), set: jest.fn(), onChanged: jest.fn() },
|
||||
editor: { getContent: jest.fn(), getSelection: jest.fn(), insertAtCursor: jest.fn(), onContentChanged: jest.fn() },
|
||||
ipc: { invoke: jest.fn(), on: jest.fn() },
|
||||
exportHooks: { preHooks: [], postHooks: [] }
|
||||
};
|
||||
context = new PluginContext(mockDeps);
|
||||
});
|
||||
|
||||
test('exposes sidebar.registerPanel with namespaced id', () => {
|
||||
const handler = jest.fn();
|
||||
context.sidebar.registerPanel('my-panel', { icon: 'test', title: 'Test', render: handler });
|
||||
expect(mockDeps.sidebar.registerPanel).toHaveBeenCalledWith('test-plugin:my-panel', { icon: 'test', title: 'Test', render: handler });
|
||||
});
|
||||
|
||||
test('exposes commands.register with crash-safe wrapper', () => {
|
||||
const badHandler = () => { throw new Error('boom'); };
|
||||
context.commands.register('bad-cmd', 'Bad', badHandler, 'Ctrl+Alt+T');
|
||||
const registeredHandler = mockDeps.commands.register.mock.calls[0][2];
|
||||
expect(() => registeredHandler()).not.toThrow();
|
||||
});
|
||||
|
||||
test('exposes settings.get/set scoped to plugin', () => {
|
||||
context.settings.get('myKey');
|
||||
context.settings.set('myKey', 'myValue');
|
||||
expect(mockDeps.settings.get).toHaveBeenCalledWith('plugins.test-plugin.myKey');
|
||||
expect(mockDeps.settings.set).toHaveBeenCalledWith('plugins.test-plugin.myKey', 'myValue');
|
||||
});
|
||||
|
||||
test('exposes editor methods', () => {
|
||||
context.editor.getContent();
|
||||
context.editor.getSelection();
|
||||
expect(mockDeps.editor.getContent).toHaveBeenCalled();
|
||||
expect(mockDeps.editor.getSelection).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('exposes events via event bus', () => {
|
||||
const handler = jest.fn();
|
||||
context.events.on('test:event', handler);
|
||||
context.events.emit('test:event', { data: 1 });
|
||||
expect(handler).toHaveBeenCalledWith({ data: 1 });
|
||||
});
|
||||
|
||||
test('exposes events.hasHandler', () => {
|
||||
expect(context.events.hasHandler('no:such')).toBe(false);
|
||||
context.events.on('exists:event', () => {});
|
||||
expect(context.events.hasHandler('exists:event')).toBe(true);
|
||||
});
|
||||
|
||||
test('exposes ipc.invoke', () => {
|
||||
context.ipc.invoke('test:channel', { a: 1 });
|
||||
expect(mockDeps.ipc.invoke).toHaveBeenCalledWith('test:channel', { a: 1 });
|
||||
});
|
||||
|
||||
test('exposes exports.registerPreHook', () => {
|
||||
const handler = jest.fn();
|
||||
context.exports.registerPreHook(handler);
|
||||
expect(mockDeps.exportHooks.preHooks).toContain(handler);
|
||||
});
|
||||
|
||||
test('exposes exports.registerPostHook', () => {
|
||||
const handler = jest.fn();
|
||||
context.exports.registerPostHook(handler);
|
||||
expect(mockDeps.exportHooks.postHooks).toContain(handler);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
const { PluginRegistry } = require('../src/plugins/plugin-registry');
|
||||
const { PluginAPI } = require('../src/plugins/plugin-api');
|
||||
const { EventBus } = require('../src/plugins/event-bus');
|
||||
|
||||
class TestPlugin extends PluginAPI {
|
||||
init(context) { this.initialized = true; this.ctx = context; }
|
||||
activate() { this.activated = true; }
|
||||
deactivate() { this.deactivated = true; }
|
||||
}
|
||||
|
||||
describe('PluginRegistry', () => {
|
||||
let registry;
|
||||
let mockDeps;
|
||||
|
||||
beforeEach(() => {
|
||||
mockDeps = {
|
||||
sidebar: { registerPanel: jest.fn() },
|
||||
commands: { register: jest.fn() },
|
||||
statusBar: { registerIndicator: jest.fn() },
|
||||
eventBus: new EventBus(),
|
||||
settings: { get: jest.fn(), set: jest.fn(), onChanged: jest.fn() },
|
||||
editor: { getContent: jest.fn(() => ''), getSelection: jest.fn(() => ''), insertAtCursor: jest.fn(), onContentChanged: jest.fn() },
|
||||
ipc: { invoke: jest.fn(), on: jest.fn() }
|
||||
};
|
||||
registry = new PluginRegistry(mockDeps);
|
||||
});
|
||||
|
||||
test('register — stores plugin and calls init', () => {
|
||||
registry.register({
|
||||
id: 'test', name: 'Test', version: '1.0.0', description: 'desc',
|
||||
manifest: {}, PluginClass: TestPlugin, dir: '/tmp/test'
|
||||
});
|
||||
const entry = registry.getPlugin('test');
|
||||
expect(entry).toBeDefined();
|
||||
expect(entry.instance.initialized).toBe(true);
|
||||
});
|
||||
|
||||
test('register — works without PluginClass', () => {
|
||||
registry.register({
|
||||
id: 'manifest-only', name: 'Manifest Only', version: '1.0.0', description: 'desc',
|
||||
manifest: {}, PluginClass: null, dir: '/tmp/test'
|
||||
});
|
||||
expect(registry.getPlugin('manifest-only')).toBeDefined();
|
||||
});
|
||||
|
||||
test('register — init error does not crash, plugin not registered', () => {
|
||||
class BadPlugin extends PluginAPI {
|
||||
init() { throw new Error('init fail'); }
|
||||
}
|
||||
expect(() => {
|
||||
registry.register({
|
||||
id: 'bad', name: 'Bad', version: '1.0.0', description: 'desc',
|
||||
manifest: {}, PluginClass: BadPlugin, dir: '/tmp/test'
|
||||
});
|
||||
}).not.toThrow();
|
||||
expect(registry.getPlugin('bad')).toBeUndefined();
|
||||
});
|
||||
|
||||
test('getPlugin — returns undefined for unknown', () => {
|
||||
expect(registry.getPlugin('nope')).toBeUndefined();
|
||||
});
|
||||
|
||||
test('getAll — returns all registered plugins', () => {
|
||||
registry.register({ id: 'a', name: 'A', version: '1', description: '', manifest: {}, PluginClass: null, dir: '' });
|
||||
registry.register({ id: 'b', name: 'B', version: '1', description: '', manifest: {}, PluginClass: null, dir: '' });
|
||||
expect(registry.getAll()).toHaveLength(2);
|
||||
});
|
||||
|
||||
test('activate — calls activate on plugin instance', () => {
|
||||
registry.register({ id: 'test', name: 'Test', version: '1', description: '', manifest: {}, PluginClass: TestPlugin, dir: '' });
|
||||
registry.activate('test');
|
||||
expect(registry.getPlugin('test').instance.activated).toBe(true);
|
||||
});
|
||||
|
||||
test('deactivate — calls deactivate on plugin instance', () => {
|
||||
registry.register({ id: 'test', name: 'Test', version: '1', description: '', manifest: {}, PluginClass: TestPlugin, dir: '' });
|
||||
registry.deactivate('test');
|
||||
expect(registry.getPlugin('test').instance.deactivated).toBe(true);
|
||||
});
|
||||
|
||||
test('exportHooks are available and populated', () => {
|
||||
registry.register({ id: 'test', name: 'Test', version: '1', description: '', manifest: {}, PluginClass: TestPlugin, dir: '' });
|
||||
const handler = jest.fn();
|
||||
registry.getPlugin('test').instance.ctx.exports.registerPreHook(handler);
|
||||
expect(registry.exportHooks.preHooks).toContain(handler);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
const { SettingsStore } = require('../src/plugins/settings-store');
|
||||
|
||||
describe('SettingsStore', () => {
|
||||
let store;
|
||||
let data;
|
||||
|
||||
beforeEach(() => {
|
||||
data = {};
|
||||
store = new SettingsStore({
|
||||
get: (key) => data[key],
|
||||
set: (key, value) => { data[key] = value; }
|
||||
});
|
||||
});
|
||||
|
||||
test('get returns value for full key', () => {
|
||||
data['plugins.my-plugin.myKey'] = 'myValue';
|
||||
expect(store.get('plugins.my-plugin.myKey')).toBe('myValue');
|
||||
});
|
||||
|
||||
test('get returns undefined for missing key', () => {
|
||||
expect(store.get('plugins.my-plugin.missing')).toBeUndefined();
|
||||
});
|
||||
|
||||
test('set stores value', () => {
|
||||
store.set('plugins.my-plugin.myKey', 42);
|
||||
expect(data['plugins.my-plugin.myKey']).toBe(42);
|
||||
});
|
||||
|
||||
test('set overwrites existing value', () => {
|
||||
data['plugins.my-plugin.myKey'] = 'old';
|
||||
store.set('plugins.my-plugin.myKey', 'new');
|
||||
expect(data['plugins.my-plugin.myKey']).toBe('new');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user