You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-08-10 22:11:50 +02:00
@@ -739,6 +739,7 @@ packages/app-mobile/components/getResponsiveValue.js
|
||||
packages/app-mobile/components/global-style.js
|
||||
packages/app-mobile/components/plugins/PluginNotification.js
|
||||
packages/app-mobile/components/plugins/PluginRunner.js
|
||||
packages/app-mobile/components/plugins/PluginRunnerWebView.test.js
|
||||
packages/app-mobile/components/plugins/PluginRunnerWebView.js
|
||||
packages/app-mobile/components/plugins/backgroundPage/initializeDialogWebView.js
|
||||
packages/app-mobile/components/plugins/backgroundPage/initializePluginBackgroundIframe.js
|
||||
@@ -1584,6 +1585,7 @@ packages/lib/shim-init-node.js
|
||||
packages/lib/shim.js
|
||||
packages/lib/string-utils.test.js
|
||||
packages/lib/string-utils.js
|
||||
packages/lib/testing/plugins/createTestPlugin.js
|
||||
packages/lib/testing/share/makeMockShareInvitation.js
|
||||
packages/lib/testing/share/mockShareService.js
|
||||
packages/lib/testing/syncTargetUtils.js
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -712,6 +712,7 @@ packages/app-mobile/components/getResponsiveValue.js
|
||||
packages/app-mobile/components/global-style.js
|
||||
packages/app-mobile/components/plugins/PluginNotification.js
|
||||
packages/app-mobile/components/plugins/PluginRunner.js
|
||||
packages/app-mobile/components/plugins/PluginRunnerWebView.test.js
|
||||
packages/app-mobile/components/plugins/PluginRunnerWebView.js
|
||||
packages/app-mobile/components/plugins/backgroundPage/initializeDialogWebView.js
|
||||
packages/app-mobile/components/plugins/backgroundPage/initializePluginBackgroundIframe.js
|
||||
@@ -1557,6 +1558,7 @@ packages/lib/shim-init-node.js
|
||||
packages/lib/shim.js
|
||||
packages/lib/string-utils.test.js
|
||||
packages/lib/string-utils.js
|
||||
packages/lib/testing/plugins/createTestPlugin.js
|
||||
packages/lib/testing/share/makeMockShareInvitation.js
|
||||
packages/lib/testing/share/mockShareService.js
|
||||
packages/lib/testing/syncTargetUtils.js
|
||||
|
@@ -15,7 +15,7 @@ const logger = Logger.create('ExtendedWebView');
|
||||
const ExtendedWebView = (props: Props, ref: Ref<WebViewControl>) => {
|
||||
const dom = useMemo(() => {
|
||||
// Note: Adding `runScripts: 'dangerously'` to allow running inline <script></script>s.
|
||||
// Use with caution.
|
||||
// Use with caution -- don't load untrusted WebView HTML while testing.
|
||||
return new JSDOM(props.html, { runScripts: 'dangerously', pretendToBeVisual: true });
|
||||
}, [props.html]);
|
||||
|
||||
@@ -57,6 +57,43 @@ const ExtendedWebView = (props: Props, ref: Ref<WebViewControl>) => {
|
||||
// JSDOM polyfills
|
||||
dom.window.eval(`
|
||||
window.scrollBy = (_amount) => { };
|
||||
|
||||
// JSDOM iframes are missing certain functionality required by Joplin,
|
||||
// including:
|
||||
// - MessageEvent.source: Should point to the window that created a message.
|
||||
// Joplin uses this to determine the source of messages in iframe-related IPC.
|
||||
// - iframe.srcdoc: Used by Joplin to create plugin windows.
|
||||
const polyfillIframeContentWindow = (contentWindow) => {
|
||||
contentWindow.addEventListener('message', event => {
|
||||
// Work around a missing ".source" property on events.
|
||||
// See https://github.com/jsdom/jsdom/issues/2745#issuecomment-1207414024
|
||||
if (!event.source) {
|
||||
contentWindow.dispatchEvent(new MessageEvent('message', {
|
||||
source: window,
|
||||
data: event.data,
|
||||
}));
|
||||
event.stopImmediatePropagation();
|
||||
}
|
||||
});
|
||||
|
||||
contentWindow.parent.postMessage = (message) => {
|
||||
window.dispatchEvent(new MessageEvent('message', {
|
||||
data: message,
|
||||
source: contentWindow,
|
||||
}));
|
||||
};
|
||||
};
|
||||
|
||||
Object.defineProperty(HTMLIFrameElement.prototype, 'srcdoc', {
|
||||
set(value) {
|
||||
this.src = 'about:blank';
|
||||
setTimeout(() => {
|
||||
this.contentDocument.write(value);
|
||||
|
||||
polyfillIframeContentWindow(this.contentWindow);
|
||||
}, 0);
|
||||
},
|
||||
});
|
||||
`);
|
||||
|
||||
dom.window.eval(`
|
||||
@@ -71,7 +108,12 @@ const ExtendedWebView = (props: Props, ref: Ref<WebViewControl>) => {
|
||||
},
|
||||
});
|
||||
|
||||
dom.window.eval(injectedJavaScriptRef.current);
|
||||
// Wrap the injected JavaScript in (() => {...})() to more closely
|
||||
// match the behavior of injectedJavaScript on Android -- variables
|
||||
// declared with "var" or "const" should not become global variables.
|
||||
dom.window.eval(`(() => {
|
||||
${injectedJavaScriptRef.current}
|
||||
})()`);
|
||||
}, [dom]);
|
||||
|
||||
const onLoadEndRef = useRef(props.onLoadEnd);
|
||||
|
@@ -0,0 +1,71 @@
|
||||
import * as React from 'react';
|
||||
import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
|
||||
import { AppState } from '../../utils/types';
|
||||
import { Store } from 'redux';
|
||||
import createMockReduxStore from '../../utils/testing/createMockReduxStore';
|
||||
import setupGlobalStore from '../../utils/testing/setupGlobalStore';
|
||||
import PluginRunnerWebView from './PluginRunnerWebView';
|
||||
import TestProviderStack from '../testing/TestProviderStack';
|
||||
import { render, waitFor } from '../../utils/testing/testingLibrary';
|
||||
import createTestPlugin from '@joplin/lib/testing/plugins/createTestPlugin';
|
||||
import getWebViewDomById from '../../utils/testing/getWebViewDomById';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||
|
||||
let store: Store<AppState>;
|
||||
|
||||
interface WrapperProps { }
|
||||
|
||||
const WrappedPluginRunnerWebView: React.FC<WrapperProps> = _props => {
|
||||
return <TestProviderStack store={store}>
|
||||
<PluginRunnerWebView/>
|
||||
</TestProviderStack>;
|
||||
};
|
||||
|
||||
const defaultManifestProperties = {
|
||||
manifest_version: 1,
|
||||
version: '0.1.0',
|
||||
app_min_version: '2.3.4',
|
||||
platforms: ['desktop', 'mobile'],
|
||||
name: 'Some plugin name',
|
||||
};
|
||||
|
||||
describe('PluginRunnerWebView', () => {
|
||||
beforeEach(async () => {
|
||||
await setupDatabaseAndSynchronizer(0);
|
||||
await switchClient(0);
|
||||
|
||||
store = createMockReduxStore();
|
||||
setupGlobalStore(store);
|
||||
Setting.setValue('plugins.pluginSupportEnabled', true);
|
||||
});
|
||||
|
||||
test('should load a plugin that shows a dialog', async () => {
|
||||
const testPlugin = await createTestPlugin({
|
||||
...defaultManifestProperties,
|
||||
id: 'org.joplinapp.dialog-test',
|
||||
}, {
|
||||
onStart: `
|
||||
const dialogs = joplin.views.dialogs;
|
||||
const dialogHandle = await dialogs.create('test-dialog');
|
||||
await dialogs.setHtml(
|
||||
dialogHandle,
|
||||
'<h1>Test!</h1>',
|
||||
);
|
||||
await joplin.views.dialogs.open(dialogHandle)
|
||||
`,
|
||||
});
|
||||
render(<WrappedPluginRunnerWebView/>);
|
||||
|
||||
// Should load the plugin
|
||||
await waitFor(async () => {
|
||||
expect(PluginService.instance().pluginById(testPlugin.manifest.id)).toBeTruthy();
|
||||
});
|
||||
|
||||
// Should show the dialog
|
||||
await waitFor(async () => {
|
||||
const dom = await getWebViewDomById('joplin__PluginDialogWebView');
|
||||
expect(dom.querySelector('h1').textContent).toBe('Test!');
|
||||
});
|
||||
});
|
||||
});
|
@@ -158,6 +158,7 @@ const PluginRunnerWebViewComponent: React.FC<Props> = props => {
|
||||
const injectedJs = `
|
||||
if (!window.loadedBackgroundPage) {
|
||||
${shim.injectedJs('pluginBackgroundPage')}
|
||||
window.pluginBackgroundPage = pluginBackgroundPage;
|
||||
console.log('Loaded PluginRunnerWebView.');
|
||||
|
||||
// Necessary, because React Native WebView can re-run injectedJs
|
||||
|
@@ -101,6 +101,8 @@ const PluginUserWebView = (props: Props) => {
|
||||
return `
|
||||
if (!window.backgroundPageLoaded) {
|
||||
${shim.injectedJs('pluginBackgroundPage')}
|
||||
window.pluginBackgroundPage = pluginBackgroundPage;
|
||||
|
||||
pluginBackgroundPage.initializeDialogWebView(
|
||||
${JSON.stringify(messageChannelId)}
|
||||
);
|
||||
@@ -120,6 +122,7 @@ const PluginUserWebView = (props: Props) => {
|
||||
<ExtendedWebView
|
||||
style={props.style}
|
||||
baseDirectory={plugin.baseDir}
|
||||
testID='joplin__PluginDialogWebView'
|
||||
webviewInstanceId='joplin__PluginDialogWebView'
|
||||
html={html}
|
||||
hasPluginScripts={true}
|
||||
|
@@ -1,14 +1,12 @@
|
||||
import Setting from '../../models/Setting';
|
||||
import PluginService, { defaultPluginSetting } from '../../services/plugins/PluginService';
|
||||
import { PluginManifest } from '../../services/plugins/utils/types';
|
||||
import PluginService from '../../services/plugins/PluginService';
|
||||
import { setupDatabaseAndSynchronizer, switchClient } from '../../testing/test-utils';
|
||||
import { writeFile } from 'fs-extra';
|
||||
import { join } from 'path';
|
||||
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';
|
||||
|
||||
const createMockReduxStore = () => {
|
||||
return createStore((state: State = defaultState, action: Action<string>) => {
|
||||
@@ -16,33 +14,6 @@ const createMockReduxStore = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const setPluginEnabled = (id: string, enabled: boolean) => {
|
||||
const newPluginStates = {
|
||||
...Setting.value('plugins.states'),
|
||||
[id]: {
|
||||
...defaultPluginSetting(),
|
||||
enabled,
|
||||
},
|
||||
};
|
||||
Setting.setValue('plugins.states', newPluginStates);
|
||||
};
|
||||
|
||||
const addPluginWithManifest = async (manifest: PluginManifest, enabled: boolean) => {
|
||||
const pluginSource = `
|
||||
/* joplin-manifest:
|
||||
${JSON.stringify(manifest)}
|
||||
*/
|
||||
|
||||
joplin.plugins.register({
|
||||
onStart: async function() { },
|
||||
});
|
||||
`;
|
||||
const pluginPath = join(Setting.value('pluginDir'), `${manifest.id}.js`);
|
||||
await writeFile(pluginPath, pluginSource, 'utf-8');
|
||||
|
||||
setPluginEnabled(manifest.id, enabled);
|
||||
};
|
||||
|
||||
const defaultManifestProperties = {
|
||||
manifest_version: 1,
|
||||
version: '0.1.0',
|
||||
@@ -66,18 +37,18 @@ describe('loadPlugins', () => {
|
||||
});
|
||||
|
||||
test('should load only enabled plugins', async () => {
|
||||
await addPluginWithManifest({
|
||||
await createTestPlugin({
|
||||
...defaultManifestProperties,
|
||||
id: 'this.is.a.test.1',
|
||||
name: 'Disabled Plugin',
|
||||
}, false);
|
||||
}, { enabled: false });
|
||||
|
||||
const enabledPluginId = 'this.is.a.test.2';
|
||||
await addPluginWithManifest({
|
||||
await createTestPlugin({
|
||||
...defaultManifestProperties,
|
||||
id: enabledPluginId,
|
||||
name: 'Enabled Plugin',
|
||||
}, true);
|
||||
});
|
||||
|
||||
const pluginRunner = new MockPluginRunner();
|
||||
const store = createMockReduxStore();
|
||||
@@ -110,20 +81,23 @@ describe('loadPlugins', () => {
|
||||
test('should reload all plugins when reloadAll is true', async () => {
|
||||
const enabledCount = 3;
|
||||
for (let i = 0; i < enabledCount; i++) {
|
||||
await addPluginWithManifest({
|
||||
await createTestPlugin({
|
||||
...defaultManifestProperties,
|
||||
id: `joplin.test.plugin.${i}`,
|
||||
name: `Enabled Plugin ${i}`,
|
||||
}, true);
|
||||
});
|
||||
}
|
||||
|
||||
const disabledCount = 6;
|
||||
const disabledPlugins = [];
|
||||
for (let i = 0; i < disabledCount; i++) {
|
||||
await addPluginWithManifest({
|
||||
...defaultManifestProperties,
|
||||
id: `joplin.test.plugin.disabled.${i}`,
|
||||
name: `Disabled Plugin ${i}`,
|
||||
}, false);
|
||||
disabledPlugins.push(
|
||||
await createTestPlugin({
|
||||
...defaultManifestProperties,
|
||||
id: `joplin.test.plugin.disabled.${i}`,
|
||||
name: `Disabled Plugin ${i}`,
|
||||
}, { enabled: false }),
|
||||
);
|
||||
}
|
||||
|
||||
const pluginRunner = new MockPluginRunner();
|
||||
@@ -146,8 +120,11 @@ describe('loadPlugins', () => {
|
||||
// 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.
|
||||
setPluginEnabled('joplin.test.plugin.disabled.2', true);
|
||||
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);
|
||||
|
49
packages/lib/testing/plugins/createTestPlugin.ts
Normal file
49
packages/lib/testing/plugins/createTestPlugin.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { join } from 'path';
|
||||
import { PluginManifest } from '../../services/plugins/utils/types';
|
||||
import Setting from '../../models/Setting';
|
||||
import { writeFile } from 'fs-extra';
|
||||
import { defaultPluginSetting } from '../../services/plugins/PluginService';
|
||||
|
||||
|
||||
const setPluginEnabled = (id: string, enabled: boolean) => {
|
||||
const newPluginStates = {
|
||||
...Setting.value('plugins.states'),
|
||||
[id]: {
|
||||
...defaultPluginSetting(),
|
||||
enabled,
|
||||
},
|
||||
};
|
||||
Setting.setValue('plugins.states', newPluginStates);
|
||||
};
|
||||
|
||||
interface Options {
|
||||
onStart?: string;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
const createTestPlugin = async (manifest: PluginManifest, { onStart = '', enabled = true }: Options = {}) => {
|
||||
const pluginSource = `
|
||||
/* joplin-manifest:
|
||||
${JSON.stringify(manifest)}
|
||||
*/
|
||||
|
||||
joplin.plugins.register({
|
||||
onStart: async function() {
|
||||
${onStart}
|
||||
},
|
||||
});
|
||||
`;
|
||||
const pluginPath = join(Setting.value('pluginDir'), `${manifest.id}.js`);
|
||||
await writeFile(pluginPath, pluginSource, 'utf-8');
|
||||
|
||||
setPluginEnabled(manifest.id, enabled);
|
||||
|
||||
return {
|
||||
manifest,
|
||||
setEnabled: (enabled: boolean) => {
|
||||
setPluginEnabled(manifest.id, enabled);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default createTestPlugin;
|
Reference in New Issue
Block a user