2021-05-21 15:17:21 +02:00
|
|
|
import PluginRunner from '../../../app/services/plugins/PluginRunner';
|
2024-01-18 13:24:44 +02:00
|
|
|
import PluginService, { PluginSettings } from '@joplin/lib/services/plugins/PluginService';
|
2020-11-07 17:59:37 +02:00
|
|
|
import { ContentScriptType } from '@joplin/lib/services/plugins/api/types';
|
|
|
|
import MdToHtml from '@joplin/renderer/MdToHtml';
|
|
|
|
import shim from '@joplin/lib/shim';
|
2020-11-20 01:46:04 +02:00
|
|
|
import Setting from '@joplin/lib/models/Setting';
|
2021-01-24 17:51:35 +02:00
|
|
|
import * as fs from 'fs-extra';
|
2021-01-22 19:41:11 +02:00
|
|
|
import Note from '@joplin/lib/models/Note';
|
|
|
|
import Folder from '@joplin/lib/models/Folder';
|
2021-05-21 15:17:21 +02:00
|
|
|
import { expectNotThrow, setupDatabaseAndSynchronizer, switchClient, expectThrow, createTempDir, supportDir } from '@joplin/lib/testing/test-utils';
|
2021-05-25 17:50:51 +02:00
|
|
|
import { newPluginScript } from '../../testUtils';
|
2020-10-09 19:35:46 +02:00
|
|
|
|
2021-05-21 15:17:21 +02:00
|
|
|
const testPluginDir = `${supportDir}/plugins`;
|
2020-10-09 19:35:46 +02:00
|
|
|
|
2023-06-30 10:11:26 +02:00
|
|
|
function newPluginService(appVersion = '1.4') {
|
2020-10-09 19:35:46 +02:00
|
|
|
const runner = new PluginRunner();
|
|
|
|
const service = new PluginService();
|
|
|
|
service.initialize(
|
2020-11-15 16:18:46 +02:00
|
|
|
appVersion,
|
2020-10-09 19:35:46 +02:00
|
|
|
{
|
2020-12-01 16:08:41 +02:00
|
|
|
joplin: {},
|
2020-10-09 19:35:46 +02:00
|
|
|
},
|
|
|
|
runner,
|
|
|
|
{
|
|
|
|
dispatch: () => {},
|
|
|
|
getState: () => {},
|
2023-08-22 12:58:53 +02:00
|
|
|
},
|
2020-10-09 19:35:46 +02:00
|
|
|
);
|
|
|
|
return service;
|
|
|
|
}
|
|
|
|
|
2023-02-20 17:02:29 +02:00
|
|
|
describe('services_PluginService', () => {
|
2020-10-09 19:35:46 +02:00
|
|
|
|
2022-11-15 12:23:50 +02:00
|
|
|
beforeEach(async () => {
|
2020-10-09 19:35:46 +02:00
|
|
|
await setupDatabaseAndSynchronizer(1);
|
|
|
|
await switchClient(1);
|
|
|
|
});
|
|
|
|
|
2020-12-01 20:05:24 +02:00
|
|
|
it('should load and run a simple plugin', (async () => {
|
2020-10-09 19:35:46 +02:00
|
|
|
const service = newPluginService();
|
2020-11-19 14:34:49 +02:00
|
|
|
await service.loadAndRunPlugins([`${testPluginDir}/simple`], {});
|
2020-10-09 19:35:46 +02:00
|
|
|
|
2020-11-18 12:17:27 +02:00
|
|
|
expect(() => service.pluginById('org.joplinapp.plugins.Simple')).not.toThrowError();
|
2020-11-14 00:03:10 +02:00
|
|
|
|
2020-10-09 19:35:46 +02:00
|
|
|
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);
|
|
|
|
}));
|
|
|
|
|
2020-12-01 20:05:24 +02:00
|
|
|
it('should load and run a simple plugin and handle trailing slash', (async () => {
|
2020-11-14 00:03:10 +02:00
|
|
|
const service = newPluginService();
|
2020-11-19 14:34:49 +02:00
|
|
|
await service.loadAndRunPlugins([`${testPluginDir}/simple/`], {});
|
2020-11-18 12:17:27 +02:00
|
|
|
expect(() => service.pluginById('org.joplinapp.plugins.Simple')).not.toThrowError();
|
2020-11-14 00:03:10 +02:00
|
|
|
}));
|
|
|
|
|
2020-12-01 20:05:24 +02:00
|
|
|
it('should load and run a plugin that uses external packages', (async () => {
|
2020-10-09 19:35:46 +02:00
|
|
|
const service = newPluginService();
|
2020-11-19 14:34:49 +02:00
|
|
|
await service.loadAndRunPlugins([`${testPluginDir}/withExternalModules`], {});
|
2020-11-18 12:17:27 +02:00
|
|
|
expect(() => service.pluginById('org.joplinapp.plugins.ExternalModuleDemo')).not.toThrowError();
|
2020-10-09 19:35:46 +02:00
|
|
|
|
|
|
|
const allFolders = await Folder.all();
|
|
|
|
expect(allFolders.length).toBe(1);
|
2020-10-22 15:51:59 +02:00
|
|
|
|
|
|
|
// If you have an error here, it might mean you need to run `npm i` from
|
|
|
|
// the "withExternalModules" folder. Not clear exactly why.
|
2020-10-09 19:35:46 +02:00
|
|
|
expect(allFolders[0].title).toBe(' foo');
|
|
|
|
}));
|
|
|
|
|
2020-12-01 20:05:24 +02:00
|
|
|
it('should load multiple plugins from a directory', (async () => {
|
2020-10-09 19:35:46 +02:00
|
|
|
const service = newPluginService();
|
2020-11-19 14:34:49 +02:00
|
|
|
await service.loadAndRunPlugins(`${testPluginDir}/multi_plugins`, {});
|
2020-10-09 19:35:46 +02:00
|
|
|
|
2020-11-18 12:17:27 +02:00
|
|
|
const plugin1 = service.pluginById('org.joplinapp.plugins.MultiPluginDemo1');
|
|
|
|
const plugin2 = service.pluginById('org.joplinapp.plugins.MultiPluginDemo2');
|
2020-10-09 19:35:46 +02:00
|
|
|
expect(!!plugin1).toBe(true);
|
|
|
|
expect(!!plugin2).toBe(true);
|
|
|
|
|
|
|
|
const allFolders = await Folder.all();
|
|
|
|
expect(allFolders.length).toBe(2);
|
2020-11-12 21:13:28 +02:00
|
|
|
expect(allFolders.map((f: any) => f.title).sort().join(', ')).toBe('multi - simple1, multi - simple2');
|
2020-10-09 19:35:46 +02:00
|
|
|
}));
|
|
|
|
|
2020-12-01 20:05:24 +02:00
|
|
|
it('should load plugins from JS bundles', (async () => {
|
2020-10-13 12:16:36 +02:00
|
|
|
const service = newPluginService();
|
|
|
|
|
2020-11-18 12:17:27 +02:00
|
|
|
const plugin = await service.loadPluginFromJsBundle('/tmp', `
|
2020-10-13 12:16:36 +02:00
|
|
|
/* joplin-manifest:
|
|
|
|
{
|
2020-11-18 12:17:27 +02:00
|
|
|
"id": "org.joplinapp.plugins.JsBundleTest",
|
2020-10-13 12:16:36 +02:00
|
|
|
"manifest_version": 1,
|
2020-11-15 16:18:46 +02:00
|
|
|
"app_min_version": "1.4",
|
2020-10-13 12:16:36 +02:00
|
|
|
"name": "JS Bundle test",
|
|
|
|
"description": "JS Bundle Test plugin",
|
|
|
|
"version": "1.0.0",
|
|
|
|
"author": "Laurent Cozic",
|
|
|
|
"homepage_url": "https://joplinapp.org"
|
|
|
|
}
|
|
|
|
*/
|
2021-05-21 15:17:21 +02:00
|
|
|
|
2020-10-13 12:16:36 +02:00
|
|
|
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);
|
|
|
|
}));
|
|
|
|
|
2020-12-01 20:05:24 +02:00
|
|
|
it('should load plugins from JS bundle files', (async () => {
|
2020-10-13 12:16:36 +02:00
|
|
|
const service = newPluginService();
|
2020-11-19 14:34:49 +02:00
|
|
|
await service.loadAndRunPlugins(`${testPluginDir}/jsbundles`, {});
|
2020-11-18 12:17:27 +02:00
|
|
|
expect(!!service.pluginById('org.joplinapp.plugins.JsBundleDemo')).toBe(true);
|
2020-10-13 12:16:36 +02:00
|
|
|
expect((await Folder.all()).length).toBe(1);
|
|
|
|
}));
|
|
|
|
|
2020-12-01 20:05:24 +02:00
|
|
|
it('should load plugins from JPL archive', (async () => {
|
2020-11-17 20:26:24 +02:00
|
|
|
const service = newPluginService();
|
2020-11-19 14:34:49 +02:00
|
|
|
await service.loadAndRunPlugins([`${testPluginDir}/jpl_test/org.joplinapp.FirstJplPlugin.jpl`], {});
|
2020-11-17 20:26:24 +02:00
|
|
|
expect(!!service.pluginById('org.joplinapp.FirstJplPlugin')).toBe(true);
|
|
|
|
expect((await Folder.all()).length).toBe(1);
|
|
|
|
}));
|
|
|
|
|
2020-12-01 20:05:24 +02:00
|
|
|
it('should validate JS bundles', (async () => {
|
2020-10-13 12:16:36 +02:00
|
|
|
const invalidJsBundles = [
|
|
|
|
`
|
|
|
|
/* joplin-manifest:
|
|
|
|
{
|
|
|
|
"not_a_valid_manifest_at_all": 1
|
|
|
|
}
|
|
|
|
*/
|
2021-05-21 15:17:21 +02:00
|
|
|
|
2020-10-13 12:16:36 +02:00
|
|
|
joplin.plugins.register({
|
|
|
|
onStart: async function() {},
|
|
|
|
});
|
|
|
|
`, `
|
|
|
|
/* joplin-manifest:
|
|
|
|
*/
|
2021-05-21 15:17:21 +02:00
|
|
|
|
2020-10-13 12:16:36 +02:00
|
|
|
joplin.plugins.register({
|
|
|
|
onStart: async function() {},
|
|
|
|
});
|
|
|
|
`, `
|
|
|
|
joplin.plugins.register({
|
|
|
|
onStart: async function() {},
|
|
|
|
});
|
|
|
|
`, '',
|
|
|
|
];
|
|
|
|
|
|
|
|
const service = newPluginService();
|
|
|
|
|
|
|
|
for (const jsBundle of invalidJsBundles) {
|
2020-11-18 12:17:27 +02:00
|
|
|
await expectThrow(async () => await service.loadPluginFromJsBundle('/tmp', jsBundle));
|
2020-10-13 12:16:36 +02:00
|
|
|
}
|
|
|
|
}));
|
|
|
|
|
2020-12-01 20:05:24 +02:00
|
|
|
it('should register a Markdown-it plugin', (async () => {
|
2020-10-22 16:55:29 +02:00
|
|
|
const tempDir = await createTempDir();
|
|
|
|
|
|
|
|
const contentScriptPath = `${tempDir}/markdownItTestPlugin.js`;
|
2020-12-10 18:09:31 +02:00
|
|
|
const contentScriptCssPath = `${tempDir}/markdownItTestPlugin.css`;
|
2021-01-03 15:21:48 +02:00
|
|
|
await shim.fsDriver().copy(`${testPluginDir}/markdownItTestPlugin.js`, contentScriptPath);
|
2020-12-10 18:09:31 +02:00
|
|
|
await shim.fsDriver().copy(`${testPluginDir}/content_script/src/markdownItTestPlugin.css`, contentScriptCssPath);
|
2020-10-21 01:23:55 +02:00
|
|
|
|
|
|
|
const service = newPluginService();
|
|
|
|
|
2020-11-18 12:17:27 +02:00
|
|
|
const plugin = await service.loadPluginFromJsBundle(tempDir, `
|
2020-10-21 01:23:55 +02:00
|
|
|
/* joplin-manifest:
|
|
|
|
{
|
2020-11-18 12:17:27 +02:00
|
|
|
"id": "org.joplinapp.plugin.MarkdownItPluginTest",
|
2020-10-21 01:23:55 +02:00
|
|
|
"manifest_version": 1,
|
2020-11-15 16:18:46 +02:00
|
|
|
"app_min_version": "1.4",
|
2020-10-21 01:23:55 +02:00
|
|
|
"name": "JS Bundle test",
|
|
|
|
"description": "JS Bundle Test plugin",
|
|
|
|
"version": "1.0.0",
|
|
|
|
"author": "Laurent Cozic",
|
|
|
|
"homepage_url": "https://joplinapp.org"
|
|
|
|
}
|
|
|
|
*/
|
2021-05-21 15:17:21 +02:00
|
|
|
|
2020-10-21 01:23:55 +02:00
|
|
|
joplin.plugins.register({
|
|
|
|
onStart: async function() {
|
2021-01-12 01:33:10 +02:00
|
|
|
await joplin.contentScripts.register('markdownItPlugin', 'justtesting', './markdownItTestPlugin.js');
|
2020-10-21 01:23:55 +02:00
|
|
|
},
|
|
|
|
});
|
|
|
|
`);
|
|
|
|
|
|
|
|
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;
|
2023-11-03 21:45:21 +02:00
|
|
|
mdToHtml.loadExtraRendererRule(contentScript.id, tempDir, module({}), '');
|
2020-10-21 01:23:55 +02:00
|
|
|
|
|
|
|
const result = await mdToHtml.render([
|
|
|
|
'```justtesting',
|
|
|
|
'something',
|
|
|
|
'```',
|
|
|
|
].join('\n'));
|
|
|
|
|
2020-12-10 18:09:31 +02:00
|
|
|
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);
|
2020-12-11 18:03:55 +02:00
|
|
|
expect(assetContent.includes('background-color: rgb(202, 255, 255)')).toBe(true);
|
2020-10-21 01:23:55 +02:00
|
|
|
expect(result.html.includes('JUST TESTING: something')).toBe(true);
|
|
|
|
|
2020-10-22 16:55:29 +02:00
|
|
|
await shim.fsDriver().remove(tempDir);
|
2020-10-21 01:23:55 +02:00
|
|
|
}));
|
|
|
|
|
2020-12-01 20:05:24 +02:00
|
|
|
it('should enable and disable plugins depending on what app version they support', (async () => {
|
2020-11-15 16:18:46 +02:00
|
|
|
const pluginScript = `
|
|
|
|
/* joplin-manifest:
|
|
|
|
{
|
2020-11-18 12:17:27 +02:00
|
|
|
"id": "org.joplinapp.plugins.PluginTest",
|
2020-11-15 16:18:46 +02:00
|
|
|
"manifest_version": 1,
|
|
|
|
"app_min_version": "1.4",
|
|
|
|
"name": "JS Bundle test",
|
|
|
|
"version": "1.0.0"
|
|
|
|
}
|
|
|
|
*/
|
2021-05-21 15:17:21 +02:00
|
|
|
|
2020-11-15 16:18:46 +02:00
|
|
|
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) {
|
2020-11-19 14:34:49 +02:00
|
|
|
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));
|
|
|
|
}
|
2020-11-15 16:18:46 +02:00
|
|
|
}
|
|
|
|
}));
|
|
|
|
|
2020-12-01 20:05:24 +02:00
|
|
|
it('should install a plugin', (async () => {
|
2020-11-20 01:46:04 +02:00
|
|
|
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`;
|
2021-01-24 17:51:35 +02:00
|
|
|
expect(await fs.pathExists(installedPluginPath)).toBe(true);
|
2020-11-20 01:46:04 +02:00
|
|
|
}));
|
|
|
|
|
2020-12-01 20:05:24 +02:00
|
|
|
it('should rename the plugin archive to the right name', (async () => {
|
2020-11-20 01:46:04 +02:00
|
|
|
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);
|
2021-01-24 17:51:35 +02:00
|
|
|
expect(await fs.pathExists(installedPluginPath)).toBe(true);
|
|
|
|
}));
|
|
|
|
|
|
|
|
it('should create the data directory', (async () => {
|
2021-05-21 15:17:21 +02:00
|
|
|
const pluginScript = newPluginScript(`
|
2021-01-24 17:51:35 +02:00
|
|
|
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);
|
2020-11-20 01:46:04 +02:00
|
|
|
}));
|
|
|
|
|
2024-01-18 13:24:44 +02:00
|
|
|
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);
|
|
|
|
});
|
2020-10-09 19:35:46 +02:00
|
|
|
});
|