mirror of
https://github.com/mattermost/mattermost.git
synced 2026-02-18 18:18:23 -05:00
MM-67538 Add ability for plugins to load asynchronously (#35238)
Some checks are pending
API / build (push) Waiting to run
Server CI / Compute Go Version (push) Waiting to run
Server CI / Check mocks (push) Blocked by required conditions
Server CI / Check go mod tidy (push) Blocked by required conditions
Server CI / check-style (push) Blocked by required conditions
Server CI / Check serialization methods for hot structs (push) Blocked by required conditions
Server CI / Vet API (push) Blocked by required conditions
Server CI / Check migration files (push) Blocked by required conditions
Server CI / Generate email templates (push) Blocked by required conditions
Server CI / Check store layers (push) Blocked by required conditions
Server CI / Check mmctl docs (push) Blocked by required conditions
Server CI / Postgres with binary parameters (push) Blocked by required conditions
Server CI / Postgres (push) Blocked by required conditions
Server CI / Postgres (FIPS) (push) Blocked by required conditions
Server CI / Generate Test Coverage (push) Blocked by required conditions
Server CI / Run mmctl tests (push) Blocked by required conditions
Server CI / Run mmctl tests (FIPS) (push) Blocked by required conditions
Server CI / Build mattermost server app (push) Blocked by required conditions
Web App CI / check-lint (push) Waiting to run
Web App CI / check-i18n (push) Blocked by required conditions
Web App CI / check-types (push) Blocked by required conditions
Web App CI / test (platform) (push) Blocked by required conditions
Web App CI / test (mattermost-redux) (push) Blocked by required conditions
Web App CI / test (channels shard 1/4) (push) Blocked by required conditions
Web App CI / test (channels shard 2/4) (push) Blocked by required conditions
Web App CI / test (channels shard 3/4) (push) Blocked by required conditions
Web App CI / test (channels shard 4/4) (push) Blocked by required conditions
Web App CI / upload-coverage (push) Blocked by required conditions
Web App CI / build (push) Blocked by required conditions
Some checks are pending
API / build (push) Waiting to run
Server CI / Compute Go Version (push) Waiting to run
Server CI / Check mocks (push) Blocked by required conditions
Server CI / Check go mod tidy (push) Blocked by required conditions
Server CI / check-style (push) Blocked by required conditions
Server CI / Check serialization methods for hot structs (push) Blocked by required conditions
Server CI / Vet API (push) Blocked by required conditions
Server CI / Check migration files (push) Blocked by required conditions
Server CI / Generate email templates (push) Blocked by required conditions
Server CI / Check store layers (push) Blocked by required conditions
Server CI / Check mmctl docs (push) Blocked by required conditions
Server CI / Postgres with binary parameters (push) Blocked by required conditions
Server CI / Postgres (push) Blocked by required conditions
Server CI / Postgres (FIPS) (push) Blocked by required conditions
Server CI / Generate Test Coverage (push) Blocked by required conditions
Server CI / Run mmctl tests (push) Blocked by required conditions
Server CI / Run mmctl tests (FIPS) (push) Blocked by required conditions
Server CI / Build mattermost server app (push) Blocked by required conditions
Web App CI / check-lint (push) Waiting to run
Web App CI / check-i18n (push) Blocked by required conditions
Web App CI / check-types (push) Blocked by required conditions
Web App CI / test (platform) (push) Blocked by required conditions
Web App CI / test (mattermost-redux) (push) Blocked by required conditions
Web App CI / test (channels shard 1/4) (push) Blocked by required conditions
Web App CI / test (channels shard 2/4) (push) Blocked by required conditions
Web App CI / test (channels shard 3/4) (push) Blocked by required conditions
Web App CI / test (channels shard 4/4) (push) Blocked by required conditions
Web App CI / upload-coverage (push) Blocked by required conditions
Web App CI / build (push) Blocked by required conditions
Automatic Merge
This commit is contained in:
parent
a711b22717
commit
1a4de869b3
2 changed files with 40 additions and 81 deletions
|
|
@ -957,14 +957,12 @@ describe('handleCloudSubscriptionChanged', () => {
|
|||
});
|
||||
|
||||
describe('handlePluginEnabled/handlePluginDisabled', () => {
|
||||
const origLog = console.log;
|
||||
const origError = console.error;
|
||||
const origCreateElement = document.createElement;
|
||||
const origGetElementsByTagName = document.getElementsByTagName;
|
||||
const origWindowPlugins = window.plugins;
|
||||
|
||||
afterEach(() => {
|
||||
console.log = origLog;
|
||||
console.error = origError;
|
||||
document.createElement = origCreateElement;
|
||||
document.getElementsByTagName = origGetElementsByTagName;
|
||||
|
|
@ -990,8 +988,7 @@ describe('handlePluginEnabled/handlePluginDisabled', () => {
|
|||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
console.log = jest.fn();
|
||||
console.error = jest.fn();
|
||||
console.error = jest.fn((...args) => origError(...args));
|
||||
|
||||
document.createElement = jest.fn();
|
||||
document.getElementsByTagName = jest.fn();
|
||||
|
|
@ -1000,60 +997,36 @@ describe('handlePluginEnabled/handlePluginDisabled', () => {
|
|||
}]);
|
||||
});
|
||||
|
||||
test('when a plugin is enabled', () => {
|
||||
test('when a plugin is enabled', async () => {
|
||||
const manifest = {
|
||||
...baseManifest,
|
||||
id: 'com.mattermost.demo-plugin',
|
||||
};
|
||||
const initialize = jest.fn();
|
||||
window.plugins = {
|
||||
[manifest.id]: {
|
||||
initialize,
|
||||
},
|
||||
};
|
||||
|
||||
const mockScript = {};
|
||||
document.createElement.mockReturnValue(mockScript);
|
||||
|
||||
expect(mockScript.onload).toBeUndefined();
|
||||
handlePluginEnabled({data: {manifest}});
|
||||
|
||||
expect(document.createElement).toHaveBeenCalledWith('script');
|
||||
expect(document.getElementsByTagName).toHaveBeenCalledTimes(1);
|
||||
expect(document.getElementsByTagName()[0].appendChild).toHaveBeenCalledTimes(1);
|
||||
expect(mockScript.onload).toBeInstanceOf(Function);
|
||||
|
||||
// Pretend to be a browser, invoke onload
|
||||
mockScript.onload();
|
||||
expect(initialize).toHaveBeenCalledWith(expect.anything(), store);
|
||||
const registery = initialize.mock.calls[0][0];
|
||||
const mockComponent = 'mockRootComponent';
|
||||
registery.registerRootComponent(mockComponent);
|
||||
expect(store.dispatch).toHaveBeenCalledTimes(1);
|
||||
|
||||
let dispatchArg = store.dispatch.mock.calls[0][0];
|
||||
expect(dispatchArg.type).toBe(ActionTypes.RECEIVED_WEBAPP_PLUGIN);
|
||||
expect(dispatchArg.data).toBe(manifest);
|
||||
|
||||
dispatchArg = store.dispatch.mock.calls[1][0];
|
||||
|
||||
expect(dispatchArg.type).toBe(ActionTypes.RECEIVED_PLUGIN_COMPONENT);
|
||||
expect(dispatchArg.name).toBe('Root');
|
||||
expect(dispatchArg.data.component).toBe(mockComponent);
|
||||
expect(dispatchArg.data.pluginId).toBe(manifest.id);
|
||||
// Assert handlePluginEnabled is idempotent
|
||||
handlePluginEnabled({data: {manifest}});
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledTimes(2);
|
||||
|
||||
// Assert handlePluginEnabled is idempotent
|
||||
mockScript.onload = undefined;
|
||||
handlePluginEnabled({data: {manifest}});
|
||||
expect(mockScript.onload).toBeUndefined();
|
||||
|
||||
dispatchArg = store.dispatch.mock.calls[2][0];
|
||||
dispatchArg = store.dispatch.mock.calls[1][0];
|
||||
expect(dispatchArg.type).toBe(ActionTypes.RECEIVED_WEBAPP_PLUGIN);
|
||||
expect(dispatchArg.data).toBe(manifest);
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledTimes(3);
|
||||
|
||||
expect(console.error).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
|
|
@ -1062,12 +1035,6 @@ describe('handlePluginEnabled/handlePluginDisabled', () => {
|
|||
...baseManifest,
|
||||
id: 'com.mattermost.demo-2-plugin',
|
||||
};
|
||||
const initialize = jest.fn();
|
||||
window.plugins = {
|
||||
[manifest.id]: {
|
||||
initialize,
|
||||
},
|
||||
};
|
||||
|
||||
const manifestv2 = {
|
||||
...manifest,
|
||||
|
|
@ -1080,69 +1047,39 @@ describe('handlePluginEnabled/handlePluginDisabled', () => {
|
|||
const mockScript = {};
|
||||
document.createElement.mockReturnValue(mockScript);
|
||||
|
||||
expect(mockScript.onload).toBeUndefined();
|
||||
handlePluginEnabled({data: {manifest}});
|
||||
|
||||
expect(document.createElement).toHaveBeenCalledWith('script');
|
||||
expect(document.getElementsByTagName).toHaveBeenCalledTimes(1);
|
||||
expect(document.getElementsByTagName()[0].appendChild).toHaveBeenCalledTimes(1);
|
||||
expect(mockScript.onload).toBeInstanceOf(Function);
|
||||
|
||||
// Pretend to be a browser, invoke onload
|
||||
mockScript.onload();
|
||||
expect(initialize).toHaveBeenCalledWith(expect.anything(), store);
|
||||
const registry = initialize.mock.calls[0][0];
|
||||
const mockComponent = 'mockRootComponent';
|
||||
registry.registerRootComponent(mockComponent);
|
||||
|
||||
let dispatchArg = store.dispatch.mock.calls[0][0];
|
||||
expect(dispatchArg.type).toBe(ActionTypes.RECEIVED_WEBAPP_PLUGIN);
|
||||
expect(dispatchArg.data).toBe(manifest);
|
||||
|
||||
dispatchArg = store.dispatch.mock.calls[1][0];
|
||||
expect(dispatchArg.type).toBe(ActionTypes.RECEIVED_PLUGIN_COMPONENT);
|
||||
expect(dispatchArg.name).toBe('Root');
|
||||
expect(dispatchArg.data.component).toBe(mockComponent);
|
||||
expect(dispatchArg.data.pluginId).toBe(manifest.id);
|
||||
|
||||
// Upgrade plugin
|
||||
mockScript.onload = undefined;
|
||||
handlePluginEnabled({data: {manifest: manifestv2}});
|
||||
|
||||
// Assert upgrade is idempotent
|
||||
handlePluginEnabled({data: {manifest: manifestv2}});
|
||||
|
||||
expect(mockScript.onload).toBeInstanceOf(Function);
|
||||
expect(document.createElement).toHaveBeenCalledTimes(2);
|
||||
|
||||
mockScript.onload();
|
||||
expect(initialize).toHaveBeenCalledWith(expect.anything(), store);
|
||||
expect(initialize).toHaveBeenCalledTimes(2);
|
||||
const registry2 = initialize.mock.calls[0][0];
|
||||
const mockComponent2 = 'mockRootComponent2';
|
||||
registry2.registerRootComponent(mockComponent2);
|
||||
dispatchArg = store.dispatch.mock.calls[1][0];
|
||||
expect(dispatchArg.type).toBe(ActionTypes.RECEIVED_WEBAPP_PLUGIN);
|
||||
expect(dispatchArg.data).toBe(manifestv2);
|
||||
|
||||
dispatchArg = store.dispatch.mock.calls[2][0];
|
||||
expect(store.dispatch).toHaveBeenCalledTimes(4);
|
||||
const dispatchRemovedArg = store.dispatch.mock.calls[2][0];
|
||||
expect(typeof dispatchRemovedArg).toBe('function');
|
||||
dispatchRemovedArg(store.dispatch);
|
||||
|
||||
dispatchArg = store.dispatch.mock.calls[3][0];
|
||||
expect(dispatchArg.type).toBe(ActionTypes.RECEIVED_WEBAPP_PLUGIN);
|
||||
expect(dispatchArg.data).toBe(manifestv2);
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledTimes(6);
|
||||
const dispatchRemovedArg = store.dispatch.mock.calls[3][0];
|
||||
expect(typeof dispatchRemovedArg).toBe('function');
|
||||
dispatchRemovedArg(store.dispatch);
|
||||
|
||||
dispatchArg = store.dispatch.mock.calls[4][0];
|
||||
expect(dispatchArg.type).toBe(ActionTypes.RECEIVED_WEBAPP_PLUGIN);
|
||||
expect(dispatchArg.data).toBe(manifestv2);
|
||||
|
||||
const dispatchReceivedArg2 = store.dispatch.mock.calls[5][0];
|
||||
expect(dispatchReceivedArg2.type).toBe(ActionTypes.RECEIVED_PLUGIN_COMPONENT);
|
||||
expect(dispatchReceivedArg2.name).toBe('Root');
|
||||
expect(dispatchReceivedArg2.data.component).toBe(mockComponent2);
|
||||
expect(dispatchReceivedArg2.data.pluginId).toBe(manifest.id);
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledTimes(8);
|
||||
const dispatchReceivedArg4 = store.dispatch.mock.calls[7][0];
|
||||
const dispatchReceivedArg4 = store.dispatch.mock.calls[5][0];
|
||||
|
||||
expect(dispatchReceivedArg4.type).toBe(ActionTypes.REMOVED_WEBAPP_PLUGIN);
|
||||
expect(dispatchReceivedArg4.data).toBe(manifestv2);
|
||||
|
|
@ -1170,7 +1107,6 @@ describe('handlePluginEnabled/handlePluginDisabled', () => {
|
|||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
console.log = jest.fn();
|
||||
console.error = jest.fn();
|
||||
|
||||
document.createElement = jest.fn();
|
||||
|
|
|
|||
|
|
@ -67,6 +67,8 @@ function registerPlugin(id: string, plugin: Plugin): void {
|
|||
}
|
||||
|
||||
window.plugins[id] = plugin;
|
||||
|
||||
onPluginRegistered(id);
|
||||
}
|
||||
window.registerPlugin = registerPlugin;
|
||||
|
||||
|
|
@ -168,6 +170,8 @@ export function loadPlugin(manifest: PluginManifest): Promise<void> {
|
|||
bundlePath = bundlePath.replace('/static/', '/static/plugins/');
|
||||
}
|
||||
|
||||
addPluginRegisteredHandler(manifest.id, onLoad);
|
||||
|
||||
console.log('Loading ' + describePlugin(manifest)); //eslint-disable-line no-console
|
||||
|
||||
const script = document.createElement('script');
|
||||
|
|
@ -175,7 +179,6 @@ export function loadPlugin(manifest: PluginManifest): Promise<void> {
|
|||
script.type = 'text/javascript';
|
||||
script.src = getSiteURL() + bundlePath;
|
||||
script.defer = true;
|
||||
script.onload = onLoad;
|
||||
script.onerror = onError;
|
||||
|
||||
document.getElementsByTagName('head')[0].appendChild(script);
|
||||
|
|
@ -227,6 +230,26 @@ export function removePlugin(manifest: PluginManifest): void {
|
|||
console.log('Removed ' + describePlugin(manifest)); //eslint-disable-line no-console
|
||||
}
|
||||
|
||||
type PluginRegisteredListener = () => void;
|
||||
|
||||
const pluginRegisteredHandlers = new Map<string, PluginRegisteredListener>();
|
||||
|
||||
function addPluginRegisteredHandler(pluginId: string, listener: PluginRegisteredListener) {
|
||||
pluginRegisteredHandlers.set(pluginId, listener);
|
||||
}
|
||||
|
||||
function onPluginRegistered(pluginId: string) {
|
||||
const listener = pluginRegisteredHandlers.get(pluginId);
|
||||
|
||||
if (listener) {
|
||||
pluginRegisteredHandlers.delete(pluginId);
|
||||
listener();
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('A plugin was registered, but no listener has been registered for it. It won\'t be loaded correctly.');
|
||||
}
|
||||
}
|
||||
|
||||
// loadPluginsIfNecessary synchronizes the current state of loaded plugins with that of the server,
|
||||
// loading any newly added plugins and unloading any removed ones.
|
||||
export async function loadPluginsIfNecessary(): Promise<void> {
|
||||
|
|
|
|||
Loading…
Reference in a new issue