diff --git a/src/core/public/plugins/plugin.test.mocks.ts b/src/core/public/plugins/plugin.test.mocks.ts index b877847aaa90e..422442c9ca4d2 100644 --- a/src/core/public/plugins/plugin.test.mocks.ts +++ b/src/core/public/plugins/plugin.test.mocks.ts @@ -24,8 +24,8 @@ export const mockPlugin = { }; export const mockInitializer = jest.fn(() => mockPlugin); -export const mockPluginLoader = jest.fn().mockResolvedValue(mockInitializer); +export const mockPluginReader = jest.fn(() => mockInitializer); -jest.mock('./plugin_loader', () => ({ - loadPluginBundle: mockPluginLoader, +jest.mock('./plugin_reader', () => ({ + read: mockPluginReader, })); diff --git a/src/core/public/plugins/plugin.test.ts b/src/core/public/plugins/plugin.test.ts index 4ed2106d542d5..8fe745db9554d 100644 --- a/src/core/public/plugins/plugin.test.ts +++ b/src/core/public/plugins/plugin.test.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { mockInitializer, mockPlugin, mockPluginLoader } from './plugin.test.mocks'; +import { mockInitializer, mockPlugin, mockPluginReader } from './plugin.test.mocks'; import { DiscoveredPlugin } from '../../server'; import { coreMock } from '../mocks'; @@ -40,7 +40,7 @@ const opaqueId = Symbol(); const initializerContext = coreMock.createPluginInitializerContext(); beforeEach(() => { - mockPluginLoader.mockClear(); + mockPluginReader.mockClear(); mockPlugin.setup.mockClear(); mockPlugin.start.mockClear(); mockPlugin.stop.mockClear(); @@ -48,12 +48,6 @@ beforeEach(() => { }); describe('PluginWrapper', () => { - test('`setup` fails if load is not called first', async () => { - await expect(plugin.setup({} as any, {} as any)).rejects.toThrowErrorMatchingInlineSnapshot( - `"Plugin \\"plugin-a\\" can't be setup since its bundle isn't loaded."` - ); - }); - test('`setup` fails if plugin.setup is not a function', async () => { mockInitializer.mockReturnValueOnce({ start: jest.fn() } as any); await expect(plugin.setup({} as any, {} as any)).rejects.toThrowErrorMatchingInlineSnapshot( @@ -102,19 +96,21 @@ describe('PluginWrapper', () => { }; let startDependenciesResolved = false; - mockPluginLoader.mockResolvedValueOnce(() => ({ - setup: jest.fn(), - start: async () => { - // Add small delay to ensure startDependencies is not resolved until after the plugin instance's start resolves. - await new Promise(resolve => setTimeout(resolve, 10)); - expect(startDependenciesResolved).toBe(false); - return pluginStartContract; - }, - })); + mockPluginReader.mockReturnValueOnce( + jest.fn(() => ({ + setup: jest.fn(), + start: jest.fn(async () => { + // Add small delay to ensure startDependencies is not resolved until after the plugin instance's start resolves. + await new Promise(resolve => setTimeout(resolve, 10)); + expect(startDependenciesResolved).toBe(false); + return pluginStartContract; + }), + stop: jest.fn(), + })) + ); await plugin.setup({} as any, {} as any); const context = { any: 'thing' } as any; const deps = { otherDep: 'value' }; - // Add promise callback prior to calling `start` to ensure calls in `setup` will not resolve before `start` is // called. const startDependenciesCheck = plugin.startDependencies.then(res => { diff --git a/src/core/public/plugins/plugin.ts b/src/core/public/plugins/plugin.ts index 8957b7f4f1508..591165fcd2839 100644 --- a/src/core/public/plugins/plugin.ts +++ b/src/core/public/plugins/plugin.ts @@ -21,24 +21,9 @@ import { Subject } from 'rxjs'; import { first } from 'rxjs/operators'; import { DiscoveredPlugin, PluginOpaqueId } from '../../server'; import { PluginInitializerContext } from './plugin_context'; +import { read } from './plugin_reader'; import { CoreStart, CoreSetup } from '..'; -/** - * Unknown variant for internal use only for when plugins are not known. - * @internal - */ -export type UnknownPluginInitializer = PluginInitializer>; - -/** - * Custom window type for loading bundles. Do not extend global Window to avoid leaking these types. - * @internal - */ -export interface CoreWindow { - __kbnBundles__: { - [pluginBundleName: string]: { plugin: UnknownPluginInitializer } | undefined; - }; -} - /** * The interface that should be returned by a `PluginInitializer`. * @@ -100,25 +85,6 @@ export class PluginWrapper< this.optionalPlugins = discoveredPlugin.optionalPlugins; } - /** - * Reads the plugin's bundle declared in the global context. - */ - private read() { - const coreWindow = (window as unknown) as CoreWindow; - const exportId = `plugin/${this.name}`; - const pluginExport = coreWindow.__kbnBundles__[exportId]; - if (typeof pluginExport!.plugin !== 'function') { - throw new Error(`Definition of plugin "${this.name}" should be a function.`); - } else { - return pluginExport!.plugin as PluginInitializer< - TSetup, - TStart, - TPluginsSetup, - TPluginsStart - >; - } - } - /** * Instantiates plugin and calls `setup` function exposed by the plugin initializer. * @param setupContext Context that consists of various core services tailored specifically @@ -167,7 +133,13 @@ export class PluginWrapper< } private async createPluginInstance() { - const initializer = this.read(); + const initializer = read(this.name) as PluginInitializer< + TSetup, + TStart, + TPluginsSetup, + TPluginsStart + >; + const instance = initializer(this.initializerContext); if (typeof instance.setup !== 'function') { diff --git a/src/core/public/plugins/plugin_reader.ts b/src/core/public/plugins/plugin_reader.ts new file mode 100644 index 0000000000000..55271c6e3af6a --- /dev/null +++ b/src/core/public/plugins/plugin_reader.ts @@ -0,0 +1,50 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PluginInitializer } from './plugin'; + +/** + * Unknown variant for internal use only for when plugins are not known. + * @internal + */ +export type UnknownPluginInitializer = PluginInitializer>; + +/** + * Custom window type for loading bundles. Do not extend global Window to avoid leaking these types. + * @internal + */ +export interface CoreWindow { + __kbnBundles__: { + [pluginBundleName: string]: { plugin: UnknownPluginInitializer } | undefined; + }; +} + +/** + * Reads the plugin's bundle declared in the global context. + */ +export function read(name: string) { + const coreWindow = (window as unknown) as CoreWindow; + const exportId = `plugin/${name}`; + const pluginExport = coreWindow.__kbnBundles__[exportId]; + if (typeof pluginExport!.plugin !== 'function') { + throw new Error(`Definition of plugin "${name}" should be a function.`); + } else { + return pluginExport!.plugin; + } +} diff --git a/src/core/public/plugins/plugins_service.test.mocks.ts b/src/core/public/plugins/plugins_service.test.mocks.ts index 46879c8133366..85b84e561056a 100644 --- a/src/core/public/plugins/plugins_service.test.mocks.ts +++ b/src/core/public/plugins/plugins_service.test.mocks.ts @@ -25,4 +25,10 @@ export type MockedPluginInitializer = jest.Mock = jest.fn().mockRejectedValue(new Error('No provider specified')); +> = jest.fn().mockImplementation(() => () => { + throw new Error('No provider specified'); +}); + +jest.mock('./plugin_reader', () => ({ + read: mockPluginInitializerProvider, +})); diff --git a/src/core/public/plugins/plugins_service.test.ts b/src/core/public/plugins/plugins_service.test.ts index d8b627747e4df..6d71844bc19c8 100644 --- a/src/core/public/plugins/plugins_service.test.ts +++ b/src/core/public/plugins/plugins_service.test.ts @@ -31,6 +31,7 @@ import { PluginsServiceStartDeps, PluginsServiceSetupDeps, } from './plugins_service'; + import { InjectedPluginMetadata } from '../injected_metadata'; import { notificationServiceMock } from '../notifications/notifications_service.mock'; import { applicationServiceMock } from '../application/application_service.mock'; @@ -151,10 +152,6 @@ describe('PluginsService', () => { ] as unknown) as [[PluginName, any]]); }); - afterEach(() => { - // mockLoadPluginBundle.mockClear(); - }); - describe('#getOpaqueIds()', () => { it('returns dependency tree of symbols', () => { const pluginsService = new PluginsService(mockCoreContext, plugins); @@ -173,16 +170,6 @@ describe('PluginsService', () => { }); describe('#setup()', () => { - it('fails if any bundle cannot be loaded', async () => { - // TODO - // mockLoadPluginBundle.mockRejectedValueOnce(new Error('Could not load bundle')); - - const pluginsService = new PluginsService(mockCoreContext, plugins); - await expect(pluginsService.setup(mockSetupDeps)).rejects.toThrowErrorMatchingInlineSnapshot( - `"Could not load bundle"` - ); - }); - it('fails if any plugin instance does not have a setup function', async () => { mockPluginInitializers.set('pluginA', (() => ({})) as any); const pluginsService = new PluginsService(mockCoreContext, plugins); @@ -283,7 +270,6 @@ describe('PluginsService', () => { const pluginsService = new PluginsService(mockCoreContext, plugins); const promise = pluginsService.setup(mockSetupDeps); - jest.runAllTimers(); // load plugin bundles await flushPromises(); jest.runAllTimers(); // setup plugins