一些检查失败了
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
258 行
9.5 KiB
TypeScript
258 行
9.5 KiB
TypeScript
import Setting from '../../models/Setting';
|
|
import PluginService, { defaultPluginSetting } from '../../services/plugins/PluginService';
|
|
import { setupDatabaseAndSynchronizer, switchClient, withWarningSilenced } from '../../testing/test-utils';
|
|
import loadPlugins, { Props as LoadPluginsProps } from './loadPlugins';
|
|
import MockPluginRunner from './testing/MockPluginRunner';
|
|
import reducer, { State, defaultState } from '../../reducer';
|
|
import { Action, createStore } from 'redux';
|
|
import MockPlatformImplementation from './testing/MockPlatformImplementation';
|
|
import createTestPlugin from '../../testing/plugins/createTestPlugin';
|
|
import Plugin from './Plugin';
|
|
import shim from '../../shim';
|
|
import { PluginManifest } from './utils/types';
|
|
import { writeFile, mkdirp } from 'fs-extra';
|
|
import { join } from 'path';
|
|
|
|
const createMockReduxStore = () => {
|
|
return createStore((state: State = defaultState, action: Action<string>) => {
|
|
return reducer(state, action);
|
|
});
|
|
};
|
|
|
|
const defaultManifestProperties = {
|
|
manifest_version: 1,
|
|
version: '0.1.0',
|
|
app_min_version: '2.3.4',
|
|
platforms: ['desktop', 'mobile'],
|
|
};
|
|
|
|
const platformImplementation = new MockPlatformImplementation();
|
|
|
|
describe('loadPlugins', () => {
|
|
beforeEach(async () => {
|
|
await setupDatabaseAndSynchronizer(1);
|
|
await switchClient(1);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
for (const id of PluginService.instance().pluginIds) {
|
|
await PluginService.instance().unloadPlugin(id);
|
|
}
|
|
await PluginService.instance().destroy();
|
|
});
|
|
|
|
test('should load only enabled plugins', async () => {
|
|
await createTestPlugin({
|
|
...defaultManifestProperties,
|
|
id: 'this.is.a.test.1',
|
|
name: 'Disabled Plugin',
|
|
}, { enabled: false });
|
|
|
|
const enabledPluginId = 'this.is.a.test.2';
|
|
await createTestPlugin({
|
|
...defaultManifestProperties,
|
|
id: enabledPluginId,
|
|
name: 'Enabled Plugin',
|
|
});
|
|
|
|
const pluginRunner = new MockPluginRunner();
|
|
const store = createMockReduxStore();
|
|
|
|
const loadPluginsOptions: LoadPluginsProps = {
|
|
pluginRunner,
|
|
pluginSettings: Setting.value('plugins.states'),
|
|
platformImplementation,
|
|
store,
|
|
reloadAll: false,
|
|
cancelEvent: { cancelled: false },
|
|
};
|
|
expect(Object.keys(PluginService.instance().plugins)).toHaveLength(0);
|
|
|
|
await loadPlugins(loadPluginsOptions);
|
|
await pluginRunner.waitForAllToBeRunning([enabledPluginId]);
|
|
|
|
expect(pluginRunner.runningPluginIds).toMatchObject([enabledPluginId]);
|
|
// No plugins were running before, so none should be stopped.
|
|
expect(pluginRunner.stopCalledTimes).toBe(0);
|
|
|
|
// Loading again should not re-run plugins
|
|
await loadPlugins(loadPluginsOptions);
|
|
|
|
// Should have tried to stop at most the disabled plugin (which is a no-op).
|
|
expect(pluginRunner.stopCalledTimes).toBe(1);
|
|
expect(pluginRunner.runningPluginIds).toMatchObject([enabledPluginId]);
|
|
});
|
|
|
|
test('should reload all plugins when reloadAll is true', async () => {
|
|
const enabledCount = 3;
|
|
for (let i = 0; i < enabledCount; i++) {
|
|
await createTestPlugin({
|
|
...defaultManifestProperties,
|
|
id: `joplin.test.plugin.${i}`,
|
|
name: `Enabled Plugin ${i}`,
|
|
});
|
|
}
|
|
|
|
const disabledCount = 6;
|
|
const disabledPlugins = [];
|
|
for (let i = 0; i < disabledCount; i++) {
|
|
disabledPlugins.push(
|
|
await createTestPlugin({
|
|
...defaultManifestProperties,
|
|
id: `joplin.test.plugin.disabled.${i}`,
|
|
name: `Disabled Plugin ${i}`,
|
|
}, { enabled: false }),
|
|
);
|
|
}
|
|
|
|
const pluginRunner = new MockPluginRunner();
|
|
const store = createMockReduxStore();
|
|
const loadPluginsOptions: LoadPluginsProps = {
|
|
pluginRunner,
|
|
pluginSettings: Setting.value('plugins.states'),
|
|
platformImplementation,
|
|
store,
|
|
reloadAll: true,
|
|
cancelEvent: { cancelled: false },
|
|
};
|
|
await loadPlugins(loadPluginsOptions);
|
|
let expectedRunningIds = ['joplin.test.plugin.0', 'joplin.test.plugin.1', 'joplin.test.plugin.2'];
|
|
await pluginRunner.waitForAllToBeRunning(expectedRunningIds);
|
|
|
|
// No additional plugins should be running.
|
|
expect([...pluginRunner.runningPluginIds].sort()).toMatchObject(expectedRunningIds);
|
|
|
|
// No plugins were running before -- there were no plugins to stop
|
|
expect(pluginRunner.stopCalledTimes).toBe(0);
|
|
|
|
const testPlugin = disabledPlugins[2];
|
|
expect(testPlugin.manifest.id).toBe('joplin.test.plugin.disabled.2');
|
|
|
|
// Enabling a plugin and reloading it should cause all plugins to load.
|
|
testPlugin.setEnabled(true);
|
|
await loadPlugins({ ...loadPluginsOptions, pluginSettings: Setting.value('plugins.states') });
|
|
expectedRunningIds = ['joplin.test.plugin.0', 'joplin.test.plugin.1', 'joplin.test.plugin.2', 'joplin.test.plugin.disabled.2'];
|
|
await pluginRunner.waitForAllToBeRunning(expectedRunningIds);
|
|
|
|
// Reloading all should stop all plugins and rerun enabled plugins, even
|
|
// if not enabled previously.
|
|
expect(pluginRunner.stopCalledTimes).toBe(disabledCount + enabledCount);
|
|
expect([...pluginRunner.runningPluginIds].sort()).toMatchObject(expectedRunningIds);
|
|
});
|
|
|
|
test('should skip extraction when jpl has not changed', async () => {
|
|
const pluginId = 'joplin.test.plugin.packed';
|
|
await createTestPlugin({
|
|
...defaultManifestProperties,
|
|
id: pluginId,
|
|
name: 'Test JPL Plugin',
|
|
}, { format: 'jpl' });
|
|
|
|
const pluginRunner = new MockPluginRunner();
|
|
const store = createMockReduxStore();
|
|
const service = PluginService.instance();
|
|
service.initialize('2.3.4', platformImplementation, pluginRunner, store);
|
|
|
|
const tarExtractSpy = jest.spyOn(shim.fsDriver(), 'tarExtract');
|
|
|
|
// First load should extract
|
|
await service.loadAndRunPlugins(Setting.value('pluginDir'), Setting.value('plugins.states'));
|
|
expect(tarExtractSpy).toHaveBeenCalledTimes(1);
|
|
|
|
// Second load with same file should skip extraction
|
|
await service.unloadPlugin(pluginId);
|
|
await service.loadAndRunPlugins(Setting.value('pluginDir'), Setting.value('plugins.states'));
|
|
expect(tarExtractSpy).toHaveBeenCalledTimes(1);
|
|
|
|
// Recreating the jpl (different mtime/size) should trigger re-extraction
|
|
await service.unloadPlugin(pluginId);
|
|
await createTestPlugin({
|
|
...defaultManifestProperties,
|
|
id: pluginId,
|
|
name: 'Test JPL Plugin',
|
|
}, { format: 'jpl', onStart: '/* changed */' });
|
|
|
|
await service.loadAndRunPlugins(Setting.value('pluginDir'), Setting.value('plugins.states'));
|
|
expect(tarExtractSpy).toHaveBeenCalledTimes(2);
|
|
|
|
tarExtractSpy.mockRestore();
|
|
});
|
|
|
|
test('should not load the script for disabled plugins', async () => {
|
|
const createDirPlugin = async (manifest: PluginManifest, enabled: boolean) => {
|
|
const dir = join(Setting.value('pluginDir'), manifest.id);
|
|
await mkdirp(dir);
|
|
await writeFile(join(dir, 'manifest.json'), JSON.stringify(manifest), 'utf-8');
|
|
await writeFile(join(dir, 'index.js'), 'joplin.plugins.register({ onStart: async function() {} });', 'utf-8');
|
|
const newStates = {
|
|
...Setting.value('plugins.states'),
|
|
[manifest.id]: { ...defaultPluginSetting(), enabled },
|
|
};
|
|
Setting.setValue('plugins.states', newStates);
|
|
};
|
|
|
|
const enabledId = 'joplin.test.plugin.enabled';
|
|
const disabledId = 'joplin.test.plugin.disabled';
|
|
await createDirPlugin({ ...defaultManifestProperties, id: enabledId, name: 'Enabled' }, true);
|
|
await createDirPlugin({ ...defaultManifestProperties, id: disabledId, name: 'Disabled' }, false);
|
|
|
|
const pluginRunner = new MockPluginRunner();
|
|
const store = createMockReduxStore();
|
|
PluginService.instance().initialize('2.3.4', platformImplementation, pluginRunner, store);
|
|
await PluginService.instance().loadAndRunPlugins(Setting.value('pluginDir'), Setting.value('plugins.states'));
|
|
|
|
expect(PluginService.instance().plugins[disabledId].scriptText).toBe('');
|
|
expect(PluginService.instance().plugins[enabledId].scriptText).not.toBe('');
|
|
});
|
|
|
|
test('should not block allPluginsStarted when a plugin fails to start', async () => {
|
|
// This tests the fix for https://github.com/laurent22/joplin/issues/12793
|
|
// When a plugin crashes before calling register(), it should not block
|
|
// other plugins from working.
|
|
|
|
const goodPluginId = 'joplin.test.good.plugin';
|
|
await createTestPlugin({
|
|
...defaultManifestProperties,
|
|
id: goodPluginId,
|
|
name: 'Good Plugin',
|
|
});
|
|
|
|
const badPluginId = 'joplin.test.bad.plugin';
|
|
await createTestPlugin({
|
|
...defaultManifestProperties,
|
|
id: badPluginId,
|
|
name: 'Bad Plugin',
|
|
});
|
|
|
|
// Create a mock runner that throws for the bad plugin
|
|
// and emits 'started' for good plugins (matching real behavior).
|
|
const failingPluginRunner = new MockPluginRunner();
|
|
const originalRun = failingPluginRunner.run.bind(failingPluginRunner);
|
|
failingPluginRunner.run = async (plugin: Plugin) => {
|
|
if (plugin.manifest.id === badPluginId) {
|
|
throw new Error('Simulated plugin crash before register()');
|
|
}
|
|
await originalRun(plugin);
|
|
// Simulate what JoplinPlugins.register() does after onStart()
|
|
plugin.emit('started');
|
|
};
|
|
|
|
const store = createMockReduxStore();
|
|
const service = PluginService.instance();
|
|
service.initialize('2.3.4', platformImplementation, failingPluginRunner, store);
|
|
|
|
await withWarningSilenced(/Plugin "joplin.test.bad.plugin" failed to start: Error: Simulated plugin crash/, async () => {
|
|
await service.loadAndRunPlugins(Setting.value('pluginDir'), Setting.value('plugins.states'));
|
|
});
|
|
|
|
// The bad plugin failed, but allPluginsStarted should still be true
|
|
// because the fix marks failed plugins as "started".
|
|
expect(service.allPluginsStarted).toBe(true);
|
|
|
|
// The good plugin should still be running
|
|
expect(failingPluginRunner.runningPluginIds).toContain(goodPluginId);
|
|
// The bad plugin should not be running
|
|
expect(failingPluginRunner.runningPluginIds).not.toContain(badPluginId);
|
|
});
|
|
});
|