1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-30 10:36:35 +02:00
joplin/packages/app-cli/tests/services/plugins/PluginService.ts

476 lines
15 KiB
TypeScript

import PluginRunner from '../../../app/services/plugins/PluginRunner';
import PluginService, { PluginSettings, defaultPluginSetting } from '@joplin/lib/services/plugins/PluginService';
import { ContentScriptType } from '@joplin/lib/services/plugins/api/types';
import MdToHtml from '@joplin/renderer/MdToHtml';
import shim from '@joplin/lib/shim';
import Setting from '@joplin/lib/models/Setting';
import * as fs from 'fs-extra';
import Note from '@joplin/lib/models/Note';
import Folder from '@joplin/lib/models/Folder';
import { expectNotThrow, setupDatabaseAndSynchronizer, switchClient, expectThrow, createTempDir, supportDir, mockMobilePlatform } from '@joplin/lib/testing/test-utils';
import { newPluginScript } from '../../testUtils';
import { join } from 'path';
const testPluginDir = `${supportDir}/plugins`;
function newPluginService(appVersion = '1.4') {
const runner = new PluginRunner();
const service = new PluginService();
service.initialize(
appVersion,
{
joplin: {},
},
runner,
{
dispatch: () => {},
getState: () => {},
},
);
return service;
}
describe('services_PluginService', () => {
beforeEach(async () => {
await setupDatabaseAndSynchronizer(1);
await switchClient(1);
});
it('should load and run a simple plugin', (async () => {
const service = newPluginService();
await service.loadAndRunPlugins([`${testPluginDir}/simple`], {});
expect(() => service.pluginById('org.joplinapp.plugins.Simple')).not.toThrowError();
const allFolders = await Folder.all();
expect(allFolders.length).toBe(1);
expect(allFolders[0].title).toBe('my plugin folder');
const allNotes = await Note.all();
expect(allNotes.length).toBe(1);
expect(allNotes[0].title).toBe('testing plugin!');
expect(allNotes[0].parent_id).toBe(allFolders[0].id);
}));
it('should load and run a simple plugin and handle trailing slash', (async () => {
const service = newPluginService();
await service.loadAndRunPlugins([`${testPluginDir}/simple/`], {});
expect(() => service.pluginById('org.joplinapp.plugins.Simple')).not.toThrowError();
}));
it('should load and run a plugin that uses external packages', (async () => {
const service = newPluginService();
await service.loadAndRunPlugins([`${testPluginDir}/withExternalModules`], {});
expect(() => service.pluginById('org.joplinapp.plugins.ExternalModuleDemo')).not.toThrowError();
const allFolders = await Folder.all();
expect(allFolders.length).toBe(1);
// If you have an error here, it might mean you need to run `npm i` from
// the "withExternalModules" folder. Not clear exactly why.
expect(allFolders[0].title).toBe(' foo');
}));
it('should load multiple plugins from a directory', (async () => {
const service = newPluginService();
await service.loadAndRunPlugins(`${testPluginDir}/multi_plugins`, {});
const plugin1 = service.pluginById('org.joplinapp.plugins.MultiPluginDemo1');
const plugin2 = service.pluginById('org.joplinapp.plugins.MultiPluginDemo2');
expect(!!plugin1).toBe(true);
expect(!!plugin2).toBe(true);
const allFolders = await Folder.all();
expect(allFolders.length).toBe(2);
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
expect(allFolders.map((f: any) => f.title).sort().join(', ')).toBe('multi - simple1, multi - simple2');
}));
it('should load plugins from JS bundles', (async () => {
const service = newPluginService();
const plugin = await service.loadPluginFromJsBundle('/tmp', `
/* joplin-manifest:
{
"id": "org.joplinapp.plugins.JsBundleTest",
"manifest_version": 1,
"app_min_version": "1.4",
"name": "JS Bundle test",
"description": "JS Bundle Test plugin",
"version": "1.0.0",
"author": "Laurent Cozic",
"homepage_url": "https://joplinapp.org"
}
*/
joplin.plugins.register({
onStart: async function() {
await joplin.data.post(['folders'], null, { title: "my plugin folder" });
},
});
`);
await service.runPlugin(plugin);
expect(plugin.manifest.manifest_version).toBe(1);
expect(plugin.manifest.name).toBe('JS Bundle test');
const allFolders = await Folder.all();
expect(allFolders.length).toBe(1);
}));
it('should load plugins from JS bundle files', (async () => {
const service = newPluginService();
await service.loadAndRunPlugins(`${testPluginDir}/jsbundles`, {});
expect(!!service.pluginById('org.joplinapp.plugins.JsBundleDemo')).toBe(true);
expect((await Folder.all()).length).toBe(1);
}));
it('should load plugins from JPL archive', (async () => {
const service = newPluginService();
await service.loadAndRunPlugins([`${testPluginDir}/jpl_test/org.joplinapp.FirstJplPlugin.jpl`], {});
expect(!!service.pluginById('org.joplinapp.FirstJplPlugin')).toBe(true);
expect((await Folder.all()).length).toBe(1);
}));
it('should validate JS bundles', (async () => {
const invalidJsBundles = [
`
/* joplin-manifest:
{
"not_a_valid_manifest_at_all": 1
}
*/
joplin.plugins.register({
onStart: async function() {},
});
`, `
/* joplin-manifest:
*/
joplin.plugins.register({
onStart: async function() {},
});
`, `
joplin.plugins.register({
onStart: async function() {},
});
`, '',
];
const service = newPluginService();
for (const jsBundle of invalidJsBundles) {
await expectThrow(async () => await service.loadPluginFromJsBundle('/tmp', jsBundle));
}
}));
it('should register a Markdown-it plugin', (async () => {
const tempDir = await createTempDir();
const contentScriptPath = `${tempDir}/markdownItTestPlugin.js`;
const contentScriptCssPath = `${tempDir}/markdownItTestPlugin.css`;
await shim.fsDriver().copy(`${testPluginDir}/markdownItTestPlugin.js`, contentScriptPath);
await shim.fsDriver().copy(`${testPluginDir}/content_script/src/markdownItTestPlugin.css`, contentScriptCssPath);
const service = newPluginService();
const plugin = await service.loadPluginFromJsBundle(tempDir, `
/* joplin-manifest:
{
"id": "org.joplinapp.plugin.MarkdownItPluginTest",
"manifest_version": 1,
"app_min_version": "1.4",
"name": "JS Bundle test",
"description": "JS Bundle Test plugin",
"version": "1.0.0",
"author": "Laurent Cozic",
"homepage_url": "https://joplinapp.org"
}
*/
joplin.plugins.register({
onStart: async function() {
await joplin.contentScripts.register('markdownItPlugin', 'justtesting', './markdownItTestPlugin.js');
},
});
`);
await service.runPlugin(plugin);
const contentScripts = plugin.contentScriptsByType(ContentScriptType.MarkdownItPlugin);
expect(contentScripts.length).toBe(1);
expect(!!contentScripts[0].path).toBe(true);
const contentScript = contentScripts[0];
const mdToHtml = new MdToHtml();
const module = require(contentScript.path).default;
mdToHtml.loadExtraRendererRule(contentScript.id, tempDir, module({}), '');
const result = await mdToHtml.render([
'```justtesting',
'something',
'```',
].join('\n'));
const asset = result.pluginAssets.find(a => a.name === 'justtesting/markdownItTestPlugin.css');
const assetContent: string = await shim.fsDriver().readFile(asset.path, 'utf8');
expect(assetContent.includes('.just-testing')).toBe(true);
expect(assetContent.includes('background-color: rgb(202, 255, 255)')).toBe(true);
expect(result.html.includes('JUST TESTING: something')).toBe(true);
await shim.fsDriver().remove(tempDir);
}));
it('should enable and disable plugins depending on what app version they support', (async () => {
const pluginScript = `
/* joplin-manifest:
{
"id": "org.joplinapp.plugins.PluginTest",
"manifest_version": 1,
"app_min_version": "1.4",
"name": "JS Bundle test",
"version": "1.0.0"
}
*/
joplin.plugins.register({
onStart: async function() { },
});
`;
const testCases = [
['1.4', true],
['1.5', true],
['2.0', true],
['1.3', false],
['0.9', false],
];
for (const testCase of testCases) {
const [appVersion, hasNoError] = testCase;
const service = newPluginService(appVersion as string);
const plugin = await service.loadPluginFromJsBundle('', pluginScript);
if (hasNoError) {
await expectNotThrow(() => service.runPlugin(plugin));
} else {
await expectThrow(() => service.runPlugin(plugin));
}
}
}));
it.each([
{
manifestPlatforms: ['desktop'],
isDesktop: true,
appVersion: '3.0.0',
shouldRun: true,
},
{
manifestPlatforms: ['desktop'],
isDesktop: false,
appVersion: '3.0.6',
shouldRun: false,
},
{
manifestPlatforms: ['desktop', 'mobile'],
isDesktop: false,
appVersion: '3.0.6',
shouldRun: true,
},
{
// Should default to desktop-only
manifestPlatforms: [],
isDesktop: false,
appVersion: '3.0.8',
shouldRun: false,
},
])('should enable and disable plugins depending on what platform(s) they support (case %#: %j)', async ({ manifestPlatforms, isDesktop, appVersion, shouldRun }) => {
const pluginScript = `
/* joplin-manifest:
{
"id": "org.joplinapp.plugins.PluginTest",
"manifest_version": 1,
"app_min_version": "1.0.0",
"platforms": ${JSON.stringify(manifestPlatforms)},
"name": "JS Bundle test",
"version": "1.0.0"
}
*/
joplin.plugins.register({
onStart: async function() { },
});
`;
let resetPlatformMock = () => {};
if (!isDesktop) {
resetPlatformMock = mockMobilePlatform('android').reset;
}
try {
const service = newPluginService(appVersion);
const plugin = await service.loadPluginFromJsBundle('', pluginScript);
if (shouldRun) {
await expect(service.runPlugin(plugin)).resolves.toBeUndefined();
} else {
await expect(service.runPlugin(plugin)).rejects.toThrow(/disabled/);
}
} finally {
resetPlatformMock();
}
});
it('should install a plugin', (async () => {
const service = newPluginService();
const pluginPath = `${testPluginDir}/jpl_test/org.joplinapp.FirstJplPlugin.jpl`;
await service.installPlugin(pluginPath);
const installedPluginPath = `${Setting.value('pluginDir')}/org.joplinapp.FirstJplPlugin.jpl`;
expect(await fs.pathExists(installedPluginPath)).toBe(true);
}));
it('should rename the plugin archive to the right name', (async () => {
const tempDir = await createTempDir();
const service = newPluginService();
const pluginPath = `${testPluginDir}/jpl_test/org.joplinapp.FirstJplPlugin.jpl`;
const tempPath = `${tempDir}/something.jpl`;
await shim.fsDriver().copy(pluginPath, tempPath);
const installedPluginPath = `${Setting.value('pluginDir')}/org.joplinapp.FirstJplPlugin.jpl`;
await service.installPlugin(tempPath);
expect(await fs.pathExists(installedPluginPath)).toBe(true);
}));
it('should create the data directory', (async () => {
const pluginScript = newPluginScript(`
joplin.plugins.register({
onStart: async function() {
const dataDir = await joplin.plugins.dataDir();
joplin.data.post(['folders'], null, { title: JSON.stringify(dataDir) });
},
});
`);
const expectedPath = `${Setting.value('pluginDataDir')}/org.joplinapp.plugins.PluginTest`;
expect(await fs.pathExists(expectedPath)).toBe(false);
const service = newPluginService();
const plugin = await service.loadPluginFromJsBundle('', pluginScript);
await service.runPlugin(plugin);
expect(await fs.pathExists(expectedPath)).toBe(true);
const folders = await Folder.all();
expect(JSON.parse(folders[0].title)).toBe(expectedPath);
}));
it('should uninstall multiple plugins', async () => {
const service = newPluginService();
const pluginId1 = 'org.joplinapp.FirstJplPlugin';
const pluginId2 = 'org.joplinapp.plugins.TocDemo';
const pluginPath1 = `${testPluginDir}/jpl_test/${pluginId1}.jpl`;
const pluginPath2 = `${testPluginDir}/toc/${pluginId2}.jpl`;
await service.installPlugin(pluginPath1);
await service.installPlugin(pluginPath2);
// Both should be installed
expect(await fs.pathExists(`${Setting.value('pluginDir')}/${pluginId1}.jpl`)).toBe(true);
expect(await fs.pathExists(`${Setting.value('pluginDir')}/${pluginId2}.jpl`)).toBe(true);
const pluginSettings: PluginSettings = {
[pluginId1]: { enabled: true, deleted: true, hasBeenUpdated: false },
[pluginId2]: { enabled: true, deleted: true, hasBeenUpdated: false },
};
const newPluginSettings = await service.uninstallPlugins(pluginSettings);
// Should have deleted plugins
expect(await fs.pathExists(`${Setting.value('pluginDir')}/${pluginId1}.jpl`)).toBe(false);
expect(await fs.pathExists(`${Setting.value('pluginDir')}${pluginId2}.jpl`)).toBe(false);
// Should clear deleted plugins from settings
expect(newPluginSettings[pluginId1]).toBe(undefined);
expect(newPluginSettings[pluginId2]).toBe(undefined);
});
it('re-running loadAndRunPlugins should reload plugins that have changed but keep unchanged plugins running', async () => {
const testDir = await createTempDir();
try {
const loadCounterNote = await Note.save({ title: 'Log of plugin loads' });
const readLoadCounterNote = async () => {
return (await Note.load(loadCounterNote.id)).body;
};
expect(await readLoadCounterNote()).toBe('');
const writePluginScript = async (version: string, id: string) => {
const script = `
/* joplin-manifest:
{
"id": ${JSON.stringify(id)},
"manifest_version": 1,
"app_min_version": "1.0.0",
"name": "JS Bundle test",
"version": ${JSON.stringify(version)}
}
*/
joplin.plugins.register({
onStart: async function() {
const noteId = ${JSON.stringify(loadCounterNote.id)};
const pluginId = ${JSON.stringify(id)};
const note = await joplin.data.get(['notes', noteId], { fields: ['body'] });
const newBody = note.body + '\\n' + pluginId;
await joplin.data.put(['notes', noteId], null, { body: newBody.trim() });
},
});
`;
await fs.writeFile(join(testDir, `${id}.bundle.js`), script);
};
const service = newPluginService();
const pluginId1 = 'org.joplinapp.testPlugin1';
await writePluginScript('0.0.1', pluginId1);
const pluginId2 = 'org.joplinapp.testPlugin2';
await writePluginScript('0.0.1', pluginId2);
let pluginSettings: PluginSettings = {
[pluginId1]: defaultPluginSetting(),
[pluginId2]: defaultPluginSetting(),
};
await service.loadAndRunPlugins(testDir, pluginSettings);
// Plugins should initially load once
expect(service.pluginIds).toHaveLength(2);
expect(service.pluginById(pluginId1).running).toBe(true);
expect(service.pluginById(pluginId2).running).toBe(true);
expect(await readLoadCounterNote()).toBe(`${pluginId1}\n${pluginId2}`);
// Updating just plugin 1 reload just plugin 1.
await writePluginScript('0.0.2', pluginId1);
await service.loadAndRunPlugins(testDir, pluginSettings);
expect(service.pluginById(pluginId1).running).toBe(true);
expect(service.pluginById(pluginId2).running).toBe(true);
expect(await readLoadCounterNote()).toBe(`${pluginId1}\n${pluginId2}\n${pluginId1}`);
// Disabling plugin 1 should not reload plugin 2
pluginSettings = { ...pluginSettings, [pluginId1]: { ...defaultPluginSetting(), enabled: false } };
await service.loadAndRunPlugins(testDir, pluginSettings);
expect(service.pluginById(pluginId1).running).toBe(false);
expect(service.pluginById(pluginId2).running).toBe(true);
expect(await readLoadCounterNote()).toBe(`${pluginId1}\n${pluginId2}\n${pluginId1}`);
await service.destroy();
} finally {
await fs.remove(testDir);
}
});
});