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/global-style.js
|
||||||
packages/app-mobile/components/plugins/PluginNotification.js
|
packages/app-mobile/components/plugins/PluginNotification.js
|
||||||
packages/app-mobile/components/plugins/PluginRunner.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/PluginRunnerWebView.js
|
||||||
packages/app-mobile/components/plugins/backgroundPage/initializeDialogWebView.js
|
packages/app-mobile/components/plugins/backgroundPage/initializeDialogWebView.js
|
||||||
packages/app-mobile/components/plugins/backgroundPage/initializePluginBackgroundIframe.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/shim.js
|
||||||
packages/lib/string-utils.test.js
|
packages/lib/string-utils.test.js
|
||||||
packages/lib/string-utils.js
|
packages/lib/string-utils.js
|
||||||
|
packages/lib/testing/plugins/createTestPlugin.js
|
||||||
packages/lib/testing/share/makeMockShareInvitation.js
|
packages/lib/testing/share/makeMockShareInvitation.js
|
||||||
packages/lib/testing/share/mockShareService.js
|
packages/lib/testing/share/mockShareService.js
|
||||||
packages/lib/testing/syncTargetUtils.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/global-style.js
|
||||||
packages/app-mobile/components/plugins/PluginNotification.js
|
packages/app-mobile/components/plugins/PluginNotification.js
|
||||||
packages/app-mobile/components/plugins/PluginRunner.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/PluginRunnerWebView.js
|
||||||
packages/app-mobile/components/plugins/backgroundPage/initializeDialogWebView.js
|
packages/app-mobile/components/plugins/backgroundPage/initializeDialogWebView.js
|
||||||
packages/app-mobile/components/plugins/backgroundPage/initializePluginBackgroundIframe.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/shim.js
|
||||||
packages/lib/string-utils.test.js
|
packages/lib/string-utils.test.js
|
||||||
packages/lib/string-utils.js
|
packages/lib/string-utils.js
|
||||||
|
packages/lib/testing/plugins/createTestPlugin.js
|
||||||
packages/lib/testing/share/makeMockShareInvitation.js
|
packages/lib/testing/share/makeMockShareInvitation.js
|
||||||
packages/lib/testing/share/mockShareService.js
|
packages/lib/testing/share/mockShareService.js
|
||||||
packages/lib/testing/syncTargetUtils.js
|
packages/lib/testing/syncTargetUtils.js
|
||||||
|
@@ -15,7 +15,7 @@ const logger = Logger.create('ExtendedWebView');
|
|||||||
const ExtendedWebView = (props: Props, ref: Ref<WebViewControl>) => {
|
const ExtendedWebView = (props: Props, ref: Ref<WebViewControl>) => {
|
||||||
const dom = useMemo(() => {
|
const dom = useMemo(() => {
|
||||||
// Note: Adding `runScripts: 'dangerously'` to allow running inline <script></script>s.
|
// 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 });
|
return new JSDOM(props.html, { runScripts: 'dangerously', pretendToBeVisual: true });
|
||||||
}, [props.html]);
|
}, [props.html]);
|
||||||
|
|
||||||
@@ -57,6 +57,43 @@ const ExtendedWebView = (props: Props, ref: Ref<WebViewControl>) => {
|
|||||||
// JSDOM polyfills
|
// JSDOM polyfills
|
||||||
dom.window.eval(`
|
dom.window.eval(`
|
||||||
window.scrollBy = (_amount) => { };
|
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(`
|
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]);
|
}, [dom]);
|
||||||
|
|
||||||
const onLoadEndRef = useRef(props.onLoadEnd);
|
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 = `
|
const injectedJs = `
|
||||||
if (!window.loadedBackgroundPage) {
|
if (!window.loadedBackgroundPage) {
|
||||||
${shim.injectedJs('pluginBackgroundPage')}
|
${shim.injectedJs('pluginBackgroundPage')}
|
||||||
|
window.pluginBackgroundPage = pluginBackgroundPage;
|
||||||
console.log('Loaded PluginRunnerWebView.');
|
console.log('Loaded PluginRunnerWebView.');
|
||||||
|
|
||||||
// Necessary, because React Native WebView can re-run injectedJs
|
// Necessary, because React Native WebView can re-run injectedJs
|
||||||
|
@@ -101,6 +101,8 @@ const PluginUserWebView = (props: Props) => {
|
|||||||
return `
|
return `
|
||||||
if (!window.backgroundPageLoaded) {
|
if (!window.backgroundPageLoaded) {
|
||||||
${shim.injectedJs('pluginBackgroundPage')}
|
${shim.injectedJs('pluginBackgroundPage')}
|
||||||
|
window.pluginBackgroundPage = pluginBackgroundPage;
|
||||||
|
|
||||||
pluginBackgroundPage.initializeDialogWebView(
|
pluginBackgroundPage.initializeDialogWebView(
|
||||||
${JSON.stringify(messageChannelId)}
|
${JSON.stringify(messageChannelId)}
|
||||||
);
|
);
|
||||||
@@ -120,6 +122,7 @@ const PluginUserWebView = (props: Props) => {
|
|||||||
<ExtendedWebView
|
<ExtendedWebView
|
||||||
style={props.style}
|
style={props.style}
|
||||||
baseDirectory={plugin.baseDir}
|
baseDirectory={plugin.baseDir}
|
||||||
|
testID='joplin__PluginDialogWebView'
|
||||||
webviewInstanceId='joplin__PluginDialogWebView'
|
webviewInstanceId='joplin__PluginDialogWebView'
|
||||||
html={html}
|
html={html}
|
||||||
hasPluginScripts={true}
|
hasPluginScripts={true}
|
||||||
|
@@ -1,14 +1,12 @@
|
|||||||
import Setting from '../../models/Setting';
|
import Setting from '../../models/Setting';
|
||||||
import PluginService, { defaultPluginSetting } from '../../services/plugins/PluginService';
|
import PluginService from '../../services/plugins/PluginService';
|
||||||
import { PluginManifest } from '../../services/plugins/utils/types';
|
|
||||||
import { setupDatabaseAndSynchronizer, switchClient } from '../../testing/test-utils';
|
import { setupDatabaseAndSynchronizer, switchClient } from '../../testing/test-utils';
|
||||||
import { writeFile } from 'fs-extra';
|
|
||||||
import { join } from 'path';
|
|
||||||
import loadPlugins, { Props as LoadPluginsProps } from './loadPlugins';
|
import loadPlugins, { Props as LoadPluginsProps } from './loadPlugins';
|
||||||
import MockPluginRunner from './testing/MockPluginRunner';
|
import MockPluginRunner from './testing/MockPluginRunner';
|
||||||
import reducer, { State, defaultState } from '../../reducer';
|
import reducer, { State, defaultState } from '../../reducer';
|
||||||
import { Action, createStore } from 'redux';
|
import { Action, createStore } from 'redux';
|
||||||
import MockPlatformImplementation from './testing/MockPlatformImplementation';
|
import MockPlatformImplementation from './testing/MockPlatformImplementation';
|
||||||
|
import createTestPlugin from '../../testing/plugins/createTestPlugin';
|
||||||
|
|
||||||
const createMockReduxStore = () => {
|
const createMockReduxStore = () => {
|
||||||
return createStore((state: State = defaultState, action: Action<string>) => {
|
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 = {
|
const defaultManifestProperties = {
|
||||||
manifest_version: 1,
|
manifest_version: 1,
|
||||||
version: '0.1.0',
|
version: '0.1.0',
|
||||||
@@ -66,18 +37,18 @@ describe('loadPlugins', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should load only enabled plugins', async () => {
|
test('should load only enabled plugins', async () => {
|
||||||
await addPluginWithManifest({
|
await createTestPlugin({
|
||||||
...defaultManifestProperties,
|
...defaultManifestProperties,
|
||||||
id: 'this.is.a.test.1',
|
id: 'this.is.a.test.1',
|
||||||
name: 'Disabled Plugin',
|
name: 'Disabled Plugin',
|
||||||
}, false);
|
}, { enabled: false });
|
||||||
|
|
||||||
const enabledPluginId = 'this.is.a.test.2';
|
const enabledPluginId = 'this.is.a.test.2';
|
||||||
await addPluginWithManifest({
|
await createTestPlugin({
|
||||||
...defaultManifestProperties,
|
...defaultManifestProperties,
|
||||||
id: enabledPluginId,
|
id: enabledPluginId,
|
||||||
name: 'Enabled Plugin',
|
name: 'Enabled Plugin',
|
||||||
}, true);
|
});
|
||||||
|
|
||||||
const pluginRunner = new MockPluginRunner();
|
const pluginRunner = new MockPluginRunner();
|
||||||
const store = createMockReduxStore();
|
const store = createMockReduxStore();
|
||||||
@@ -110,20 +81,23 @@ describe('loadPlugins', () => {
|
|||||||
test('should reload all plugins when reloadAll is true', async () => {
|
test('should reload all plugins when reloadAll is true', async () => {
|
||||||
const enabledCount = 3;
|
const enabledCount = 3;
|
||||||
for (let i = 0; i < enabledCount; i++) {
|
for (let i = 0; i < enabledCount; i++) {
|
||||||
await addPluginWithManifest({
|
await createTestPlugin({
|
||||||
...defaultManifestProperties,
|
...defaultManifestProperties,
|
||||||
id: `joplin.test.plugin.${i}`,
|
id: `joplin.test.plugin.${i}`,
|
||||||
name: `Enabled Plugin ${i}`,
|
name: `Enabled Plugin ${i}`,
|
||||||
}, true);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const disabledCount = 6;
|
const disabledCount = 6;
|
||||||
|
const disabledPlugins = [];
|
||||||
for (let i = 0; i < disabledCount; i++) {
|
for (let i = 0; i < disabledCount; i++) {
|
||||||
await addPluginWithManifest({
|
disabledPlugins.push(
|
||||||
...defaultManifestProperties,
|
await createTestPlugin({
|
||||||
id: `joplin.test.plugin.disabled.${i}`,
|
...defaultManifestProperties,
|
||||||
name: `Disabled Plugin ${i}`,
|
id: `joplin.test.plugin.disabled.${i}`,
|
||||||
}, false);
|
name: `Disabled Plugin ${i}`,
|
||||||
|
}, { enabled: false }),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const pluginRunner = new MockPluginRunner();
|
const pluginRunner = new MockPluginRunner();
|
||||||
@@ -146,8 +120,11 @@ describe('loadPlugins', () => {
|
|||||||
// No plugins were running before -- there were no plugins to stop
|
// No plugins were running before -- there were no plugins to stop
|
||||||
expect(pluginRunner.stopCalledTimes).toBe(0);
|
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.
|
// 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') });
|
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'];
|
expectedRunningIds = ['joplin.test.plugin.0', 'joplin.test.plugin.1', 'joplin.test.plugin.2', 'joplin.test.plugin.disabled.2'];
|
||||||
await pluginRunner.waitForAllToBeRunning(expectedRunningIds);
|
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