camofox-browser/tests/unit/plugins.test.js

180 lines
6.0 KiB
JavaScript

/**
* Tests for lib/plugins.js -- createPluginEvents, loadPlugins, and config reading.
*/
import { describe, test, expect, jest } from '@jest/globals';
import { createPluginEvents, loadPlugins } from '../../lib/plugins.js';
import fs from 'fs';
import path from 'path';
import os from 'os';
import { fileURLToPath } from 'url';
describe('lib/plugins', () => {
describe('createPluginEvents', () => {
test('returns an EventEmitter with high maxListeners', () => {
const events = createPluginEvents();
expect(events).toBeDefined();
expect(typeof events.on).toBe('function');
expect(typeof events.emit).toBe('function');
expect(events.getMaxListeners()).toBe(50);
});
test('basic emit/on works', () => {
const events = createPluginEvents();
const received = [];
events.on('test:event', (payload) => received.push(payload));
events.emit('test:event', { foo: 'bar' });
expect(received).toEqual([{ foo: 'bar' }]);
});
test('supports multiple listeners', () => {
const events = createPluginEvents();
const results = [];
events.on('multi', () => results.push('a'));
events.on('multi', () => results.push('b'));
events.on('multi', () => results.push('c'));
events.emit('multi');
expect(results).toEqual(['a', 'b', 'c']);
});
test('removeListener works', () => {
const events = createPluginEvents();
const results = [];
const handler = () => results.push('called');
events.on('removal', handler);
events.emit('removal');
expect(results).toEqual(['called']);
events.removeListener('removal', handler);
events.emit('removal');
expect(results).toEqual(['called']); // not called again
});
test('emitAsync awaits all listeners including async', async () => {
const events = createPluginEvents();
const results = [];
events.on('async:test', async (payload) => {
await new Promise((r) => setTimeout(r, 10));
results.push('async-' + payload.val);
});
events.on('async:test', (payload) => {
results.push('sync-' + payload.val);
});
await events.emitAsync('async:test', { val: 1 });
expect(results).toContain('async-1');
expect(results).toContain('sync-1');
expect(results.length).toBe(2);
});
test('emitAsync with no listeners resolves immediately', async () => {
const events = createPluginEvents();
await events.emitAsync('nonexistent', {});
// No error thrown
});
});
describe('loadPlugins', () => {
let tmpDir;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'camofox-plugin-test-'));
});
afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});
function makeMockCtx() {
return {
log: jest.fn(),
events: createPluginEvents(),
sessions: new Map(),
config: {},
};
}
test('returns empty array when plugins directory does not exist', async () => {
// loadPlugins checks the hardcoded PLUGINS_DIR, not tmpDir.
// We test by providing a mock ctx -- if no plugins/ dir exists
// relative to lib/, it would still load the real plugins.
// Instead, test via the actual project's plugin loader.
const ctx = makeMockCtx();
const app = {};
// This tests the real plugin loading -- should return the project's actual plugins
const loaded = await loadPlugins(app, ctx);
expect(Array.isArray(loaded)).toBe(true);
// Each loaded plugin should be a string
for (const name of loaded) {
expect(typeof name).toBe('string');
}
});
test('loadPlugins registers plugins and logs them', async () => {
const ctx = makeMockCtx();
const app = {};
const loaded = await loadPlugins(app, ctx);
// Verify that log was called for each loaded plugin
if (loaded.length > 0) {
const pluginLoadedCalls = ctx.log.mock.calls.filter(
([level, msg]) => level === 'info' && msg === 'plugin loaded'
);
expect(pluginLoadedCalls.length).toBe(loaded.length);
}
});
});
describe('readPluginConfig (tested indirectly via loadPlugins)', () => {
// readPluginConfig is not exported, so we test its behavior
// indirectly by verifying loadPlugins respects the config.
test('project camofox.config.json exists and is valid', () => {
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const configPath = path.join(__dirname, '../../camofox.config.json');
expect(fs.existsSync(configPath)).toBe(true);
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
expect(config).toHaveProperty('plugins');
// plugins can be array or object
const isArray = Array.isArray(config.plugins);
const isObject = typeof config.plugins === 'object' && !isArray;
expect(isArray || isObject).toBe(true);
});
test('array format plugins are string lists', () => {
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const configPath = path.join(__dirname, '../../camofox.config.json');
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
if (Array.isArray(config.plugins)) {
for (const name of config.plugins) {
expect(typeof name).toBe('string');
expect(name.length).toBeGreaterThan(0);
}
}
});
test('each configured plugin has an index.js', () => {
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const rootDir = path.join(__dirname, '../..');
const configPath = path.join(rootDir, 'camofox.config.json');
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
const pluginNames = Array.isArray(config.plugins)
? config.plugins
: Object.keys(config.plugins || {});
for (const name of pluginNames) {
const indexPath = path.join(rootDir, 'plugins', name, 'index.js');
expect(fs.existsSync(indexPath)).toBe(true);
}
});
});
});