feat: Initial clean release of CHM
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
这个提交包含在:
@@ -0,0 +1,489 @@
|
||||
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, { MobilePlatform } 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';
|
||||
import { PluginManifest } from '@joplin/lib/services/plugins/utils/types';
|
||||
|
||||
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);
|
||||
expect(allFolders.map(f => 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(MobilePlatform.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();
|
||||
await 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);
|
||||
}
|
||||
});
|
||||
|
||||
it('should report a missing app_min_version field specifically', () => {
|
||||
const service = newPluginService();
|
||||
const manifest = {
|
||||
manifest_version: 1,
|
||||
id: 'test.plugin',
|
||||
name: 'Test Plugin',
|
||||
version: '1.0.0',
|
||||
// Missing app_min_version
|
||||
};
|
||||
|
||||
const error = service.describeIncompatibility(manifest as unknown as PluginManifest);
|
||||
expect(error).toContain('Invalid plugin manifest: Missing required field: app_min_version');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
import { AppType } from '@joplin/lib/models/Setting';
|
||||
import RepositoryApi, { AppInfo, InstallMode } from '@joplin/lib/services/plugins/RepositoryApi';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import { setupDatabaseAndSynchronizer, switchClient, supportDir, createTempDir } from '@joplin/lib/testing/test-utils';
|
||||
import { remove } from 'fs-extra';
|
||||
|
||||
let tempDirs: string[] = [];
|
||||
async function newRepoApi(appInfo: AppInfo = { type: AppType.Desktop, version: '3.0.0' }): Promise<RepositoryApi> {
|
||||
const tempDir = await createTempDir();
|
||||
tempDirs.push(tempDir);
|
||||
const repo = new RepositoryApi(`${supportDir}/pluginRepo`, tempDir, appInfo, InstallMode.Default);
|
||||
await repo.initialize();
|
||||
return repo;
|
||||
}
|
||||
|
||||
describe('services_plugins_RepositoryApi', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
await setupDatabaseAndSynchronizer(1);
|
||||
await switchClient(1);
|
||||
});
|
||||
afterEach(async () => {
|
||||
for (const tempDir of tempDirs) {
|
||||
await remove(tempDir);
|
||||
}
|
||||
tempDirs = [];
|
||||
});
|
||||
|
||||
it('should get the manifests', (async () => {
|
||||
const api = await newRepoApi();
|
||||
const manifests = await api.manifests();
|
||||
expect(!!manifests.find(m => m.id === 'joplin.plugin.ambrt.backlinksToNote')).toBe(true);
|
||||
expect(!!manifests.find(m => m.id === 'org.joplinapp.plugins.ToggleSidebars')).toBe(true);
|
||||
}));
|
||||
|
||||
it('should search', (async () => {
|
||||
const api = await newRepoApi();
|
||||
|
||||
{
|
||||
const results = await api.search('to');
|
||||
expect(results.length).toBe(3);
|
||||
expect(!!results.find(m => m.id === 'joplin.plugin.ambrt.backlinksToNote')).toBe(true);
|
||||
expect(!!results.find(m => m.id === 'org.joplinapp.plugins.ToggleSidebars')).toBe(true);
|
||||
expect(!!results.find(m => m.id === 'org.joplinapp.plugins.AbcSheetMusic')).toBe(true);
|
||||
}
|
||||
|
||||
{
|
||||
const results = await api.search('backlink');
|
||||
expect(results.length).toBe(1);
|
||||
expect(!!results.find(m => m.id === 'joplin.plugin.ambrt.backlinksToNote')).toBe(true);
|
||||
}
|
||||
}));
|
||||
|
||||
it('should download a plugin', (async () => {
|
||||
const api = await newRepoApi();
|
||||
const pluginPath = await api.downloadPlugin('org.joplinapp.plugins.ToggleSidebars');
|
||||
expect(await shim.fsDriver().exists(pluginPath)).toBe(true);
|
||||
}));
|
||||
|
||||
it.each([
|
||||
{ id: 'org.joplinapp.plugins.ToggleSidebars', installedVersion: '1.0.0', appVersion: '3.0.0', shouldBeUpdatable: true },
|
||||
{ id: 'org.joplinapp.plugins.ToggleSidebars', installedVersion: '1.0.0', appVersion: '1.0.0', shouldBeUpdatable: false },
|
||||
{ id: 'org.joplinapp.plugins.ToggleSidebars', installedVersion: '1.0.2', appVersion: '3.0.0', shouldBeUpdatable: false },
|
||||
{ id: 'does.not.exist', installedVersion: '1.0.0', appVersion: '3.0.0', shouldBeUpdatable: false },
|
||||
])('should tell if a plugin can be updated (case %#)', (async ({ id, installedVersion, appVersion, shouldBeUpdatable }) => {
|
||||
const api = await newRepoApi({ version: appVersion, type: AppType.Desktop });
|
||||
expect(await api.pluginCanBeUpdated(id, installedVersion)).toBe(shouldBeUpdatable);
|
||||
}));
|
||||
|
||||
});
|
||||
@@ -0,0 +1,133 @@
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import { waitForFolderCount, setupDatabaseAndSynchronizer, switchClient, afterEachCleanUp } from '@joplin/lib/testing/test-utils';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
import { newPluginScript, newPluginService } from '../../../testUtils';
|
||||
import eventManager, { EventName } from '@joplin/lib/eventManager';
|
||||
|
||||
describe('JoplinSettings', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
await setupDatabaseAndSynchronizer(1);
|
||||
await switchClient(1);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await afterEachCleanUp();
|
||||
});
|
||||
|
||||
test('should listen to setting change event', async () => {
|
||||
const service = newPluginService();
|
||||
|
||||
const pluginScript = newPluginScript(`
|
||||
joplin.plugins.register({
|
||||
onStart: async function() {
|
||||
await joplin.settings.registerSettings({
|
||||
'myCustomSetting1': {
|
||||
value: 1,
|
||||
type: 1,
|
||||
public: true,
|
||||
label: 'My Custom Setting 1',
|
||||
},
|
||||
'myCustomSetting2': {
|
||||
value: 2,
|
||||
type: 1,
|
||||
public: true,
|
||||
label: 'My Custom Setting 2',
|
||||
}
|
||||
})
|
||||
|
||||
joplin.settings.onChange((event) => {
|
||||
joplin.data.post(['folders'], null, { title: JSON.stringify(event.keys) });
|
||||
});
|
||||
},
|
||||
});
|
||||
`);
|
||||
|
||||
const plugin = await service.loadPluginFromJsBundle('', pluginScript);
|
||||
await service.runPlugin(plugin);
|
||||
|
||||
Setting.setValue('plugin-org.joplinapp.plugins.PluginTest.myCustomSetting1', 111);
|
||||
Setting.setValue('plugin-org.joplinapp.plugins.PluginTest.myCustomSetting2', 222);
|
||||
|
||||
// Also change a global setting, to verify that the plugin doesn't get
|
||||
// notifications for non-plugin related events.
|
||||
Setting.setValue('locale', 'fr_FR');
|
||||
|
||||
Setting.emitScheduledChangeEvent();
|
||||
|
||||
await waitForFolderCount(1);
|
||||
|
||||
const folder = (await Folder.all())[0];
|
||||
|
||||
const settingNames: string[] = JSON.parse(folder.title);
|
||||
settingNames.sort();
|
||||
|
||||
expect(settingNames.join(',')).toBe('myCustomSetting1,myCustomSetting2');
|
||||
|
||||
await service.destroy();
|
||||
});
|
||||
|
||||
test('should de-register settings change listeners when a plugin is unloaded', async () => {
|
||||
const service = newPluginService();
|
||||
|
||||
const pluginScript = newPluginScript(`
|
||||
joplin.plugins.register({
|
||||
onStart: async function() {
|
||||
await joplin.settings.registerSettings({
|
||||
'test-setting': {
|
||||
value: 1234,
|
||||
type: 1,
|
||||
public: false,
|
||||
label: 'Test',
|
||||
}
|
||||
});
|
||||
|
||||
// Register 8 listeners to improve test reliability in the case
|
||||
// where listeners are added/removed from other sources.
|
||||
for (let i = 0; i < 8; i++) {
|
||||
await joplin.settings.onChange((event) => { });
|
||||
}
|
||||
},
|
||||
});
|
||||
`);
|
||||
|
||||
const plugin = await service.loadPluginFromJsBundle('', pluginScript);
|
||||
await service.runPlugin(plugin);
|
||||
|
||||
const listenerCounter = eventManager.listenerCounter_(EventName.SettingsChange);
|
||||
plugin.onUnload();
|
||||
expect(listenerCounter.getCountRemoved()).toBeGreaterThanOrEqual(8);
|
||||
});
|
||||
|
||||
test('should allow registering multiple settings', async () => {
|
||||
const service = newPluginService();
|
||||
|
||||
const pluginScript = newPluginScript(`
|
||||
joplin.plugins.register({
|
||||
onStart: async function() {
|
||||
await joplin.settings.registerSettings({
|
||||
'myCustomSetting1': {
|
||||
value: 1,
|
||||
type: 1,
|
||||
public: true,
|
||||
label: 'My Custom Setting 1',
|
||||
},
|
||||
'myCustomSetting2': {
|
||||
value: 2,
|
||||
type: 1,
|
||||
public: true,
|
||||
label: 'My Custom Setting 2',
|
||||
}
|
||||
})
|
||||
},
|
||||
});
|
||||
`);
|
||||
const plugin = await service.loadPluginFromJsBundle('', pluginScript);
|
||||
await service.runPlugin(plugin);
|
||||
|
||||
expect(Setting.value('plugin-org.joplinapp.plugins.PluginTest.myCustomSetting1')).toBe(1);
|
||||
expect(Setting.value('plugin-org.joplinapp.plugins.PluginTest.myCustomSetting2')).toBe(2);
|
||||
|
||||
await service.destroy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
import KeymapService from '@joplin/lib/services/KeymapService';
|
||||
import { setupDatabaseAndSynchronizer, switchClient, afterEachCleanUp } from '@joplin/lib/testing/test-utils';
|
||||
import { newPluginScript, newPluginService } from '../../../testUtils';
|
||||
|
||||
describe('JoplinViewMenuItem', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
await setupDatabaseAndSynchronizer(1);
|
||||
await switchClient(1);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await afterEachCleanUp();
|
||||
});
|
||||
|
||||
test('should register commands with the keymap service', async () => {
|
||||
const service = newPluginService();
|
||||
|
||||
KeymapService.instance().initialize();
|
||||
|
||||
const pluginScript = newPluginScript(`
|
||||
joplin.plugins.register({
|
||||
onStart: async function() {
|
||||
await joplin.commands.register({
|
||||
name: 'testCommand1',
|
||||
label: 'My Test Command 1',
|
||||
iconName: 'fas fa-music',
|
||||
execute: async () => {},
|
||||
});
|
||||
|
||||
await joplin.views.menuItems.create('myMenuItem1', 'testCommand1', 'tools', { accelerator: 'CmdOrCtrl+Alt+Shift+B' });
|
||||
|
||||
await joplin.commands.register({
|
||||
name: 'testCommand2',
|
||||
label: 'My Test Command 2',
|
||||
iconName: 'fas fa-music',
|
||||
execute: async () => {},
|
||||
});
|
||||
|
||||
await joplin.views.menuItems.create('myMenuItem2', 'testCommand2', 'tools');
|
||||
},
|
||||
});
|
||||
`);
|
||||
|
||||
const plugin = await service.loadPluginFromJsBundle('', pluginScript);
|
||||
await service.runPlugin(plugin);
|
||||
|
||||
const commandNames = KeymapService.instance().getCommandNames();
|
||||
|
||||
expect(commandNames.includes('testCommand1')).toBe(true);
|
||||
expect(commandNames.includes('testCommand2')).toBe(true);
|
||||
|
||||
await service.destroy();
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,112 @@
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import { setupDatabaseAndSynchronizer, switchClient, afterEachCleanUp } from '@joplin/lib/testing/test-utils';
|
||||
import Note from '@joplin/lib/models/Note';
|
||||
import Folder from '@joplin/lib/models/Folder';
|
||||
import ItemChange from '@joplin/lib/models/ItemChange';
|
||||
import { newPluginScript, newPluginService } from '../../../testUtils';
|
||||
import eventManager, { EventName } from '@joplin/lib/eventManager';
|
||||
|
||||
describe('JoplinWorkspace', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
await setupDatabaseAndSynchronizer(1);
|
||||
await switchClient(1);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await afterEachCleanUp();
|
||||
});
|
||||
|
||||
test('should listen to noteChange events', async () => {
|
||||
const appState: Record<string, string[]> = {
|
||||
selectedNoteIds: [],
|
||||
};
|
||||
|
||||
const service = newPluginService('1.4', {
|
||||
getState: () => {
|
||||
return appState;
|
||||
},
|
||||
});
|
||||
|
||||
const pluginScript = newPluginScript(`
|
||||
joplin.plugins.register({
|
||||
onStart: async function() {
|
||||
await joplin.workspace.onNoteChange(async (event) => {
|
||||
await joplin.data.post(['folders'], null, { title: JSON.stringify(event) });
|
||||
});
|
||||
},
|
||||
});
|
||||
`);
|
||||
|
||||
const note = await Note.save({});
|
||||
appState.selectedNoteIds.push(note.id);
|
||||
await ItemChange.waitForAllSaved();
|
||||
|
||||
const plugin = await service.loadPluginFromJsBundle('', pluginScript);
|
||||
await service.runPlugin(plugin);
|
||||
|
||||
await Note.save({ id: note.id, body: 'testing' });
|
||||
await ItemChange.waitForAllSaved();
|
||||
|
||||
const folder = (await Folder.all())[0];
|
||||
|
||||
const result: { id: string; event: number } = JSON.parse(folder.title);
|
||||
|
||||
expect(result.id).toBe(note.id);
|
||||
expect(result.event).toBe(ItemChange.TYPE_UPDATE);
|
||||
|
||||
await service.destroy();
|
||||
});
|
||||
|
||||
test('should return the selected folder', async () => {
|
||||
const service = newPluginService();
|
||||
|
||||
const pluginScript = newPluginScript(`
|
||||
joplin.plugins.register({
|
||||
onStart: async function() {
|
||||
const folder = await joplin.workspace.selectedFolder();
|
||||
await joplin.data.put(['folders', folder.id], null, { title: "changedtitle" });
|
||||
},
|
||||
});
|
||||
`);
|
||||
|
||||
const folder = await Folder.save({ title: 'folder' });
|
||||
Setting.setValue('activeFolderId', folder.id);
|
||||
|
||||
const plugin = await service.loadPluginFromJsBundle('', pluginScript);
|
||||
await service.runPlugin(plugin);
|
||||
|
||||
const modFolder = await Folder.load(folder.id);
|
||||
expect(modFolder.title).toBe('changedtitle');
|
||||
});
|
||||
|
||||
test('should remove event listeners when plugins are unloaded', async () => {
|
||||
const service = newPluginService();
|
||||
|
||||
const pluginScript = newPluginScript(`
|
||||
joplin.plugins.register({
|
||||
onStart: async () => {
|
||||
// Register each listener 8 times to improve test reliability -- it's possible
|
||||
// for listeners for the same events to be added/removed by other sources.
|
||||
for (let i = 0; i < 8; i++) {
|
||||
await joplin.workspace.onNoteChange(async (event) => { });
|
||||
await joplin.workspace.onResourceChange(async (event) => { });
|
||||
await joplin.workspace.filterEditorContextMenu(async (event) => { });
|
||||
}
|
||||
}
|
||||
})
|
||||
`);
|
||||
const plugin = await service.loadPluginFromJsBundle('', pluginScript);
|
||||
await service.runPlugin(plugin);
|
||||
|
||||
const itemChangeListenerCounter = eventManager.listenerCounter_(EventName.ItemChange);
|
||||
const resourceChangeListenerCounter = eventManager.listenerCounter_(EventName.ResourceChange);
|
||||
|
||||
plugin.onUnload();
|
||||
|
||||
expect(itemChangeListenerCounter.getCountRemoved()).toBeGreaterThanOrEqual(8);
|
||||
expect(resourceChangeListenerCounter.getCountRemoved()).toBeGreaterThanOrEqual(8);
|
||||
|
||||
await service.destroy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,264 @@
|
||||
import { afterDefaultPluginsLoaded, getDefaultPluginPathsAndSettings } from '@joplin/lib/services/plugins/defaultPlugins/defaultPluginsUtils';
|
||||
import PluginRunner from '../../../app/services/plugins/PluginRunner';
|
||||
import { checkThrow, setupDatabaseAndSynchronizer, supportDir, switchClient } from '@joplin/lib/testing/test-utils';
|
||||
import PluginService, { defaultPluginSetting, DefaultPluginsInfo } from '@joplin/lib/services/plugins/PluginService';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
|
||||
const testDefaultPluginsDir = `${supportDir}/testDefaultPlugins`;
|
||||
|
||||
function newPluginService(appVersion = '2.4') {
|
||||
const runner = new PluginRunner();
|
||||
const service = new PluginService();
|
||||
service.initialize(
|
||||
appVersion,
|
||||
{
|
||||
joplin: {},
|
||||
},
|
||||
runner,
|
||||
{
|
||||
dispatch: () => {},
|
||||
getState: () => {},
|
||||
},
|
||||
);
|
||||
return service;
|
||||
}
|
||||
|
||||
describe('defaultPluginsUtils', () => {
|
||||
|
||||
const pluginsId = ['joplin.plugin.ambrt.backlinksToNote', 'org.joplinapp.plugins.ToggleSidebars'];
|
||||
const defaultPluginsInfo = {
|
||||
'joplin.plugin.ambrt.backlinksToNote': {},
|
||||
'org.joplinapp.plugins.ToggleSidebars': {},
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
await setupDatabaseAndSynchronizer(1);
|
||||
await switchClient(1);
|
||||
});
|
||||
|
||||
it('should load default plugins when not previously installed', (async () => {
|
||||
Setting.setValue('installedDefaultPlugins', []);
|
||||
|
||||
const service = newPluginService('2.1');
|
||||
|
||||
const pluginSettings = service.unserializePluginSettings(Setting.value('plugins.states'));
|
||||
|
||||
for (const pluginId of pluginsId) {
|
||||
expect(pluginSettings[pluginId]).toBeFalsy();
|
||||
}
|
||||
|
||||
const pluginPathsAndNewSettings = await getDefaultPluginPathsAndSettings(
|
||||
testDefaultPluginsDir, defaultPluginsInfo, pluginSettings, service,
|
||||
);
|
||||
|
||||
for (const pluginId of pluginsId) {
|
||||
expect(
|
||||
pluginPathsAndNewSettings.pluginSettings[pluginId],
|
||||
).toMatchObject(defaultPluginSetting());
|
||||
}
|
||||
}));
|
||||
|
||||
it('should keep already created default plugins disabled with previous default plugins installed', (async () => {
|
||||
Setting.setValue('installedDefaultPlugins', ['org.joplinapp.plugins.ToggleSidebars']);
|
||||
Setting.setValue('plugins.states', {
|
||||
'org.joplinapp.plugins.ToggleSidebars': { ...defaultPluginSetting(), enabled: false },
|
||||
});
|
||||
|
||||
const service = newPluginService('2.1');
|
||||
|
||||
const pluginSettings = service.unserializePluginSettings(Setting.value('plugins.states'));
|
||||
const pluginPathsAndNewSettings = await getDefaultPluginPathsAndSettings(testDefaultPluginsDir, defaultPluginsInfo, pluginSettings, service);
|
||||
|
||||
// Should still be disabled
|
||||
expect(
|
||||
pluginPathsAndNewSettings.pluginSettings['org.joplinapp.plugins.ToggleSidebars'].enabled,
|
||||
).toBe(false);
|
||||
}));
|
||||
|
||||
const sampleJsBundlePlugin = `
|
||||
/* joplin-manifest:
|
||||
{
|
||||
"id": "io.github.jackgruber.backup",
|
||||
"manifest_version": 1,
|
||||
"app_min_version": "1.4",
|
||||
"name": "JS Bundle test",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
*/
|
||||
joplin.plugins.register({
|
||||
onStart: async function() {
|
||||
await joplin.settings.registerSettings({
|
||||
path: {
|
||||
value: "initial-path",
|
||||
type: 2,
|
||||
section: "backupSection",
|
||||
public: true,
|
||||
label: "Backup path",
|
||||
},
|
||||
})
|
||||
},
|
||||
});`;
|
||||
|
||||
it('should set initial settings for default plugins', async () => {
|
||||
const service = newPluginService();
|
||||
|
||||
|
||||
const plugin = await service.loadPluginFromJsBundle('', sampleJsBundlePlugin);
|
||||
plugin.builtIn = true;
|
||||
await service.runPlugin(plugin);
|
||||
const runningPlugins = { 'io.github.jackgruber.backup': plugin };
|
||||
|
||||
const defaultPluginsInfo: DefaultPluginsInfo = {
|
||||
'io.github.jackgruber.backup': {
|
||||
settings: {
|
||||
'path': `${Setting.value('profileDir')}`,
|
||||
},
|
||||
},
|
||||
'plugin.calebjohn.rich-markdown': {
|
||||
},
|
||||
};
|
||||
|
||||
// with pre-installed default plugin
|
||||
Setting.setValue('installedDefaultPlugins', ['io.github.jackgruber.backup']);
|
||||
const pluginSettings = { 'io.github.jackgruber.backup': defaultPluginSetting() };
|
||||
|
||||
await afterDefaultPluginsLoaded(
|
||||
runningPlugins,
|
||||
defaultPluginsInfo,
|
||||
pluginSettings,
|
||||
);
|
||||
expect(Setting.value('plugin-io.github.jackgruber.backup.path')).toBe('initial-path');
|
||||
|
||||
// with no pre-installed default plugin
|
||||
Setting.setValue('installedDefaultPlugins', ['']);
|
||||
await afterDefaultPluginsLoaded(
|
||||
runningPlugins,
|
||||
defaultPluginsInfo,
|
||||
pluginSettings,
|
||||
);
|
||||
expect(Setting.value('plugin-io.github.jackgruber.backup.path')).toBe(`${Setting.value('profileDir')}`);
|
||||
await service.destroy();
|
||||
});
|
||||
|
||||
it('should not overwrite existing settings for a user-installed version of a built-in plugin', async () => {
|
||||
const service = newPluginService();
|
||||
|
||||
const plugin = await service.loadPluginFromJsBundle('', sampleJsBundlePlugin);
|
||||
plugin.builtIn = false;
|
||||
await service.runPlugin(plugin);
|
||||
|
||||
const defaultPluginsInfo: DefaultPluginsInfo = {
|
||||
'io.github.jackgruber.backup': {
|
||||
settings: {
|
||||
'path': 'overwrite?',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// No pre-installed default plugins
|
||||
Setting.setValue('installedDefaultPlugins', []);
|
||||
|
||||
// The plugin is running and enabled
|
||||
const runningPlugins = { 'io.github.jackgruber.backup': plugin };
|
||||
const pluginSettings = { 'io.github.jackgruber.backup': defaultPluginSetting() };
|
||||
|
||||
await afterDefaultPluginsLoaded(
|
||||
runningPlugins,
|
||||
defaultPluginsInfo,
|
||||
pluginSettings,
|
||||
);
|
||||
|
||||
// Should not overwrite
|
||||
expect(Setting.value('plugin-io.github.jackgruber.backup.path')).toBe('initial-path');
|
||||
});
|
||||
|
||||
it('should support disabled-by-default plugins', async () => {
|
||||
const service = newPluginService();
|
||||
|
||||
const plugin = await service.loadPluginFromJsBundle('', sampleJsBundlePlugin);
|
||||
plugin.builtIn = false;
|
||||
await service.runPlugin(plugin);
|
||||
|
||||
const defaultPluginsInfo: DefaultPluginsInfo = {
|
||||
'joplin.plugin.ambrt.backlinksToNote': {
|
||||
enabled: false,
|
||||
},
|
||||
'org.joplinapp.plugins.ToggleSidebars': {},
|
||||
};
|
||||
Setting.setValue('installedDefaultPlugins', []);
|
||||
|
||||
const { pluginSettings } = await getDefaultPluginPathsAndSettings(
|
||||
testDefaultPluginsDir, defaultPluginsInfo, {}, service,
|
||||
);
|
||||
|
||||
expect(pluginSettings).toMatchObject({
|
||||
'joplin.plugin.ambrt.backlinksToNote': {
|
||||
enabled: false,
|
||||
deleted: false,
|
||||
},
|
||||
'org.joplinapp.plugins.ToggleSidebars': defaultPluginSetting(),
|
||||
});
|
||||
});
|
||||
|
||||
it('should not throw error on missing setting key', async () => {
|
||||
|
||||
const service = newPluginService();
|
||||
|
||||
const plugin = await service.loadPluginFromJsBundle('', sampleJsBundlePlugin);
|
||||
plugin.builtIn = true;
|
||||
await service.runPlugin(plugin);
|
||||
|
||||
const defaultPluginsInfo: DefaultPluginsInfo = {
|
||||
'io.github.jackgruber.backup': {
|
||||
settings: {
|
||||
'path': `${Setting.value('profileDir')}`,
|
||||
'missing-key1': 'someValue',
|
||||
},
|
||||
},
|
||||
'plugin.calebjohn.rich-markdown': {
|
||||
settings: {
|
||||
'missing-key2': 'someValue',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
Setting.setValue('installedDefaultPlugins', ['']);
|
||||
const pluginSettings = { 'io.github.jackgruber.backup': defaultPluginSetting() };
|
||||
const runningPlugins = { 'io.github.jackgruber.backup': plugin };
|
||||
|
||||
expect(checkThrow(() => afterDefaultPluginsLoaded(runningPlugins, defaultPluginsInfo, pluginSettings))).toBe(false);
|
||||
expect(Setting.value('plugin-io.github.jackgruber.backup.path')).toBe(`${Setting.value('profileDir')}`);
|
||||
await service.destroy();
|
||||
});
|
||||
|
||||
// Only returning not-yet-loaded plugins prevents non-default versions of built-in plugins
|
||||
// from being overwritten by PluginService.
|
||||
it('getDefaultPluginPathsAndSettings should return only plugins that haven\'t been loaded', async () => {
|
||||
const service = newPluginService();
|
||||
|
||||
const testPluginId = 'org.joplinapp.plugins.ToggleSidebars';
|
||||
const testPluginPath = `${supportDir}/pluginRepo/plugins/${testPluginId}/plugin.jpl`;
|
||||
|
||||
const pluginSettings = {
|
||||
[testPluginId]: defaultPluginSetting(),
|
||||
};
|
||||
|
||||
await service.loadAndRunPlugins([testPluginPath], pluginSettings, { devMode: false, builtIn: false });
|
||||
|
||||
// Should be running
|
||||
expect(service.isPluginLoaded(testPluginId)).toBe(true);
|
||||
|
||||
const testDefaultPluginsInfo = {
|
||||
[testPluginId]: {},
|
||||
'joplin.plugin.ambrt.backlinksToNote': {},
|
||||
};
|
||||
|
||||
const { pluginPaths } = await getDefaultPluginPathsAndSettings(
|
||||
testDefaultPluginsDir, testDefaultPluginsInfo, pluginSettings, service,
|
||||
);
|
||||
|
||||
// Should only return plugins that aren't loaded.
|
||||
expect(pluginPaths).toHaveLength(1);
|
||||
expect(pluginPaths[0]).toContain('joplin.plugin.ambrt.backlinksToNote');
|
||||
});
|
||||
});
|
||||
二进制文件未显示。
@@ -0,0 +1,56 @@
|
||||
const sandboxProxy = require('@joplin/lib/services/plugins/sandboxProxy');
|
||||
import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
|
||||
|
||||
describe('services_plugins_sandboxProxy', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
await setupDatabaseAndSynchronizer(1);
|
||||
await switchClient(1);
|
||||
});
|
||||
|
||||
it('should create a new sandbox proxy', (async () => {
|
||||
interface Result {
|
||||
path: string;
|
||||
args: unknown[];
|
||||
}
|
||||
|
||||
const results: Result[] = [];
|
||||
|
||||
const target = (path: string, args: unknown[]) => {
|
||||
results.push({ path, args });
|
||||
};
|
||||
|
||||
const proxy = sandboxProxy(target);
|
||||
|
||||
proxy.testing.bla.test('hello', '123');
|
||||
proxy.testing.test2();
|
||||
|
||||
expect(results[0].path).toBe('testing.bla.test');
|
||||
expect(results[0].args.join('_')).toBe('hello_123');
|
||||
expect(results[1].path).toBe('testing.test2');
|
||||
expect(results[1].args.join('_')).toBe('');
|
||||
}));
|
||||
|
||||
it('should allow importing a namespace', (async () => {
|
||||
interface Result {
|
||||
path: string;
|
||||
args: unknown[];
|
||||
}
|
||||
|
||||
const results: Result[] = [];
|
||||
|
||||
const target = (path: string, args: unknown[]) => {
|
||||
results.push({ path, args });
|
||||
};
|
||||
|
||||
const proxy = sandboxProxy(target);
|
||||
|
||||
const ns = proxy.testing.blabla;
|
||||
ns.test();
|
||||
ns.test2();
|
||||
|
||||
expect(results[0].path).toBe('testing.blabla.test');
|
||||
expect(results[1].path).toBe('testing.blabla.test2');
|
||||
}));
|
||||
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户