Merge "Move plugin loading related methods to PluginLoader." into stable-3.1

This commit is contained in:
David Pursehouse
2019-11-09 09:37:57 +00:00
committed by Gerrit Code Review
25 changed files with 1033 additions and 519 deletions

View File

@@ -89,6 +89,7 @@ limitations under the License.
teardown(() => { teardown(() => {
sandbox.restore(); sandbox.restore();
Gerrit._testOnly_resetPlugins();
}); });
suite('by default', () => { suite('by default', () => {
@@ -141,7 +142,7 @@ limitations under the License.
new URL('test/plugin.html?' + Math.random(), new URL('test/plugin.html?' + Math.random(),
window.location.href).toString()); window.location.href).toString());
sandbox.stub(Gerrit, '_arePluginsLoaded').returns(true); sandbox.stub(Gerrit, '_arePluginsLoaded').returns(true);
Gerrit._setPluginsPending([]); Gerrit._loadPlugins([]);
element = createElement(); element = createElement();
}); });

View File

@@ -725,7 +725,7 @@ limitations under the License.
}, },
'0.1', '0.1',
'http://some/plugins/url.html'); 'http://some/plugins/url.html');
Gerrit._setPluginsCount(0); Gerrit._loadPlugins([]);
flush(() => { flush(() => {
assert.strictEqual(hookEl.plugin, plugin); assert.strictEqual(hookEl.plugin, plugin);
assert.strictEqual(hookEl.change, element.change); assert.strictEqual(hookEl.change, element.change);

View File

@@ -81,7 +81,7 @@ limitations under the License.
}); });
element = fixture('basic'); element = fixture('basic');
sandbox.stub(element.$.actions, 'reload').returns(Promise.resolve()); sandbox.stub(element.$.actions, 'reload').returns(Promise.resolve());
Gerrit._setPluginsCount(0); Gerrit._loadPlugins([]);
}); });
teardown(done => { teardown(done => {

View File

@@ -426,7 +426,8 @@
}, },
_computePluginScreenName({plugin, screen}) { _computePluginScreenName({plugin, screen}) {
return Gerrit._getPluginScreenName(plugin, screen); if (!plugin || !screen) return '';
return `${plugin}-screen-${screen}`;
}, },
_logWelcome() { _logWelcome() {

View File

@@ -39,7 +39,7 @@ limitations under the License.
let plugin; let plugin;
Gerrit.install(p => { plugin = p; }, '0.1', Gerrit.install(p => { plugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js'); 'http://test.com/plugins/testplugin/static/test.js');
sandbox.stub(Gerrit, '_arePluginsLoaded').returns(true); Gerrit._loadPlugins([]);
adminApi = plugin.admin(); adminApi = plugin.admin();
}); });

View File

@@ -67,7 +67,7 @@ limitations under the License.
replacementHook = plugin.registerCustomComponent( replacementHook = plugin.registerCustomComponent(
'second', 'other-module', {replace: true}); 'second', 'other-module', {replace: true});
// Mimic all plugins loaded. // Mimic all plugins loaded.
Gerrit._setPluginsPending([]); Gerrit._loadPlugins([]);
flush(done); flush(done);
}); });

View File

@@ -27,33 +27,29 @@
}, },
}, },
behaviors: [
Gerrit.BaseUrlBehavior,
],
_configChanged(config) { _configChanged(config) {
const plugins = config.plugin; const plugins = config.plugin;
const htmlPlugins = (plugins.html_resource_paths || []) const htmlPlugins = (plugins.html_resource_paths || []);
.map(p => this._urlFor(p))
.filter(p => !Gerrit._isPluginPreloaded(p));
const jsPlugins = const jsPlugins =
this._handleMigrations(plugins.js_resource_paths || [], htmlPlugins) this._handleMigrations(plugins.js_resource_paths || [], htmlPlugins);
.map(p => this._urlFor(p))
.filter(p => !Gerrit._isPluginPreloaded(p));
const shouldLoadTheme = config.default_theme && const shouldLoadTheme = config.default_theme &&
!Gerrit._isPluginPreloaded('preloaded:gerrit-theme'); !Gerrit._isPluginPreloaded('preloaded:gerrit-theme');
const defaultTheme = const themeToLoad =
shouldLoadTheme ? this._urlFor(config.default_theme) : null; shouldLoadTheme ? [config.default_theme] : [];
// Theme should be loaded first if has one to have better UX
const pluginsPending = const pluginsPending =
[].concat(jsPlugins, htmlPlugins, defaultTheme || []); themeToLoad.concat(jsPlugins, htmlPlugins);
Gerrit._setPluginsPending(pluginsPending);
if (defaultTheme) { const pluginOpts = {};
// Make theme first to be first to load.
// Load sync to work around rare theme loading race condition. if (shouldLoadTheme) {
this._importHtmlPlugins([defaultTheme], true); // Theme needs to be loaded synchronous.
pluginOpts[config.default_theme] = {sync: true};
} }
this._loadJsPlugins(jsPlugins);
this._importHtmlPlugins(htmlPlugins); Gerrit._loadPlugins(pluginsPending, pluginOpts);
}, },
/** /**
@@ -66,53 +62,5 @@
return !htmlPlugins.includes(counterpart); return !htmlPlugins.includes(counterpart);
}); });
}, },
/**
* @suppress {checkTypes}
* States that it expects no more than 3 parameters, but that's not true.
* @todo (beckysiegel) check Polymer annotations and submit change.
* @param {Array} plugins
* @param {boolean=} opt_sync
*/
_importHtmlPlugins(plugins, opt_sync) {
const async = !opt_sync;
for (const url of plugins) {
// onload (second param) needs to be a function. When null or undefined
// were passed, plugins were not loaded correctly.
(this.importHref || Polymer.importHref)(
this._urlFor(url), () => {},
Gerrit._pluginInstallError.bind(null, `${url} import error`),
async);
}
},
_loadJsPlugins(plugins) {
for (const url of plugins) {
this._createScriptTag(this._urlFor(url));
}
},
_createScriptTag(url) {
const el = document.createElement('script');
el.defer = true;
el.src = url;
el.onerror = Gerrit._pluginInstallError.bind(null, `${url} load error`);
return document.body.appendChild(el);
},
_urlFor(pathOrUrl) {
if (!pathOrUrl) {
return pathOrUrl;
}
if (pathOrUrl.startsWith('preloaded:') ||
pathOrUrl.startsWith('http')) {
// Plugins are loaded from another domain or preloaded.
return pathOrUrl;
}
if (!pathOrUrl.startsWith('/')) {
pathOrUrl = '/' + pathOrUrl;
}
return window.location.origin + this.getBaseUrl() + pathOrUrl;
},
}); });
})(); })();

View File

@@ -38,195 +38,57 @@ limitations under the License.
suite('gr-plugin-host tests', () => { suite('gr-plugin-host tests', () => {
let element; let element;
let sandbox; let sandbox;
let url;
setup(() => { setup(() => {
element = fixture('basic'); element = fixture('basic');
sandbox = sinon.sandbox.create(); sandbox = sinon.sandbox.create();
sandbox.stub(document.body, 'appendChild'); sandbox.stub(document.body, 'appendChild');
sandbox.stub(element, 'importHref'); sandbox.stub(element, 'importHref');
url = window.location.origin;
}); });
teardown(() => { teardown(() => {
sandbox.restore(); sandbox.restore();
}); });
test('counts plugins', () => { test('load plugins should be called', () => {
sandbox.stub(Gerrit, '_setPluginsCount'); sandbox.stub(Gerrit, '_loadPlugins');
element.config = { element.config = {
plugin: { plugin: {
html_resource_paths: ['plugins/foo/bar', 'plugins/baz'], html_resource_paths: ['plugins/foo/bar', 'plugins/baz'],
js_resource_paths: ['plugins/42'], js_resource_paths: ['plugins/42'],
}, },
}; };
assert.isTrue(Gerrit._setPluginsCount.calledWith(3)); assert.isTrue(Gerrit._loadPlugins.calledOnce);
assert.isTrue(Gerrit._loadPlugins.calledWith([
'plugins/42', 'plugins/foo/bar', 'plugins/baz',
], {}));
}); });
test('imports relative html plugins from config', () => { test('theme plugins should be loaded if enabled', () => {
sandbox.stub(Gerrit, '_pluginInstallError'); sandbox.stub(Gerrit, '_loadPlugins');
element.config = {
plugin: {html_resource_paths: ['foo/bar', 'baz']},
};
assert.equal(element.importHref.firstCall.args[0], url + '/foo/bar');
assert.isTrue(element.importHref.firstCall.args[3]);
assert.equal(element.importHref.secondCall.args[0], url + '/baz');
assert.isTrue(element.importHref.secondCall.args[3]);
assert.equal(Gerrit._pluginInstallError.callCount, 0);
element.importHref.firstCall.args[2]();
assert.equal(Gerrit._pluginInstallError.callCount, 1);
element.importHref.secondCall.args[2]();
assert.equal(Gerrit._pluginInstallError.callCount, 2);
});
test('imports relative html plugins from config with a base url', () => {
sandbox.stub(Gerrit, '_pluginInstallError');
sandbox.stub(element, 'getBaseUrl').returns('/the-base');
element.config = {
plugin: {html_resource_paths: ['foo/bar', 'baz']}};
assert.equal(element.importHref.firstCall.args[0],
url + '/the-base/foo/bar');
assert.isTrue(element.importHref.firstCall.args[3]);
assert.equal(element.importHref.secondCall.args[0],
url + '/the-base/baz');
assert.isTrue(element.importHref.secondCall.args[3]);
assert.equal(Gerrit._pluginInstallError.callCount, 0);
element.importHref.firstCall.args[2]();
assert.equal(Gerrit._pluginInstallError.callCount, 1);
element.importHref.secondCall.args[2]();
assert.equal(Gerrit._pluginInstallError.callCount, 2);
});
test('importHref is not called with null callback functions', () => {
const plugins = ['path/to/plugin'];
element._importHtmlPlugins(plugins);
assert.isTrue(element.importHref.calledOnce);
assert.isFunction(element.importHref.lastCall.args[1]);
assert.isFunction(element.importHref.lastCall.args[2]);
});
test('imports absolute html plugins from config', () => {
sandbox.stub(Gerrit, '_pluginInstallError');
element.config = { element.config = {
default_theme: 'gerrit-theme.html',
plugin: { plugin: {
html_resource_paths: [ html_resource_paths: ['plugins/foo/bar', 'plugins/baz'],
'http://example.com/foo/bar', js_resource_paths: ['plugins/42'],
'https://example.com/baz',
],
}, },
}; };
assert.equal(element.importHref.firstCall.args[0], assert.isTrue(Gerrit._loadPlugins.calledOnce);
'http://example.com/foo/bar'); assert.isTrue(Gerrit._loadPlugins.calledWith([
assert.isTrue(element.importHref.firstCall.args[3]); 'gerrit-theme.html', 'plugins/42', 'plugins/foo/bar', 'plugins/baz',
], {'gerrit-theme.html': {sync: true}}));
assert.equal(element.importHref.secondCall.args[0],
'https://example.com/baz');
assert.isTrue(element.importHref.secondCall.args[3]);
assert.equal(Gerrit._pluginInstallError.callCount, 0);
element.importHref.firstCall.args[2]();
assert.equal(Gerrit._pluginInstallError.callCount, 1);
element.importHref.secondCall.args[2]();
assert.equal(Gerrit._pluginInstallError.callCount, 2);
}); });
test('adds js plugins from config to the body', () => { test('skip theme if preloaded', () => {
element.config = {plugin: {js_resource_paths: ['foo/bar', 'baz']}};
assert.isTrue(document.body.appendChild.calledTwice);
});
test('imports relative js plugins from config', () => {
sandbox.stub(element, '_createScriptTag');
element.config = {plugin: {js_resource_paths: ['foo/bar', 'baz']}};
assert.isTrue(element._createScriptTag.calledWith(url + '/foo/bar'));
assert.isTrue(element._createScriptTag.calledWith(url + '/baz'));
});
test('imports relative html plugins from config with a base url', () => {
sandbox.stub(element, '_createScriptTag');
sandbox.stub(element, 'getBaseUrl').returns('/the-base');
element.config = {plugin: {js_resource_paths: ['foo/bar', 'baz']}};
assert.isTrue(element._createScriptTag.calledWith(
url + '/the-base/foo/bar'));
assert.isTrue(element._createScriptTag.calledWith(
url + '/the-base/baz'));
});
test('imports absolute html plugins from config', () => {
sandbox.stub(element, '_createScriptTag');
element.config = {
plugin: {
js_resource_paths: [
'http://example.com/foo/bar',
'https://example.com/baz',
],
},
};
assert.isTrue(element._createScriptTag.calledWith(
'http://example.com/foo/bar'));
assert.isTrue(element._createScriptTag.calledWith(
'https://example.com/baz'));
});
test('default theme is loaded with html plugins', () => {
sandbox.stub(Gerrit, '_pluginInstallError');
element.config = {
default_theme: '/oof',
plugin: {
html_resource_paths: ['some'],
},
};
assert.equal(element.importHref.firstCall.args[0], url + '/oof');
assert.isFalse(element.importHref.firstCall.args[3]);
assert.equal(element.importHref.secondCall.args[0], url + '/some');
assert.isTrue(element.importHref.secondCall.args[3]);
assert.equal(Gerrit._pluginInstallError.callCount, 0);
element.importHref.firstCall.args[2]();
assert.equal(Gerrit._pluginInstallError.callCount, 1);
element.importHref.secondCall.args[2]();
assert.equal(Gerrit._pluginInstallError.callCount, 2);
});
test('default theme is loaded with html plugins', () => {
sandbox.stub(Gerrit, '_setPluginsPending');
element.config = {
default_theme: '/oof',
plugin: {},
};
assert.isTrue(Gerrit._setPluginsPending.calledWith([url + '/oof']));
});
test('skips default theme loading if preloaded', () => {
sandbox.stub(Gerrit, '_isPluginPreloaded') sandbox.stub(Gerrit, '_isPluginPreloaded')
.withArgs('preloaded:gerrit-theme').returns(true); .withArgs('preloaded:gerrit-theme').returns(true);
sandbox.stub(Gerrit, '_setPluginsPending'); sandbox.stub(Gerrit, '_loadPlugins');
element.config = { element.config = {
default_theme: '/oof', default_theme: '/oof',
plugin: {}, plugin: {},
}; };
assert.isFalse(element.importHref.calledWith(url + '/oof')); assert.isTrue(Gerrit._loadPlugins.calledOnce);
}); assert.isTrue(Gerrit._loadPlugins.calledWith([], {}));
test('skips preloaded plugins', () => {
sandbox.stub(Gerrit, '_isPluginPreloaded')
.withArgs(url + '/plugins/foo/bar').returns(true)
.withArgs(url + '/plugins/42').returns(true);
sandbox.stub(Gerrit, '_setPluginsCount');
sandbox.stub(Gerrit, '_setPluginsPending');
sandbox.stub(element, '_createScriptTag');
element.config = {
plugin: {
html_resource_paths: ['plugins/foo/bar', 'plugins/baz'],
js_resource_paths: ['plugins/42'],
},
};
assert.isTrue(
Gerrit._setPluginsPending.calledWith([url + '/plugins/baz']));
assert.equal(element._createScriptTag.callCount, 0);
assert.isTrue(element.importHref.calledWith(url + '/plugins/baz'));
}); });
}); });
</script> </script>

View File

@@ -46,7 +46,7 @@ limitations under the License.
let plugin; let plugin;
Gerrit.install(p => { plugin = p; }, '0.1', Gerrit.install(p => { plugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js'); 'http://test.com/plugins/testplugin/static/test.js');
sandbox.stub(Gerrit, '_arePluginsLoaded').returns(true); Gerrit._loadPlugins([]);
repoApi = plugin.project(); repoApi = plugin.project();
}); });

View File

@@ -48,7 +48,7 @@ limitations under the License.
let plugin; let plugin;
Gerrit.install(p => { plugin = p; }, '0.1', Gerrit.install(p => { plugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js'); 'http://test.com/plugins/testplugin/static/test.js');
sandbox.stub(Gerrit, '_arePluginsLoaded').returns(true); Gerrit._loadPlugins([]);
settingsApi = plugin.settings(); settingsApi = plugin.settings();
}); });

View File

@@ -46,7 +46,7 @@ limitations under the License.
let plugin; let plugin;
Gerrit.install(p => { plugin = p; }, '0.1', Gerrit.install(p => { plugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js'); 'http://test.com/plugins/testplugin/static/test.js');
sandbox.stub(Gerrit, '_arePluginsLoaded').returns(true); Gerrit._loadPlugins([]);
stylesApi = plugin.styles(); stylesApi = plugin.styles();
}); });
@@ -76,7 +76,7 @@ limitations under the License.
let plugin; let plugin;
Gerrit.install(p => { plugin = p; }, '0.1', Gerrit.install(p => { plugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js'); 'http://test.com/plugins/testplugin/static/test.js');
sandbox.stub(Gerrit, '_arePluginsLoaded').returns(true); Gerrit._loadPlugins([]);
stylesApi = plugin.styles(); stylesApi = plugin.styles();
displayInlineStyle = stylesApi.css('display: inline'); displayInlineStyle = stylesApi.css('display: inline');
displayNoneStyle = stylesApi.css('display: none'); displayNoneStyle = stylesApi.css('display: none');

View File

@@ -67,7 +67,7 @@ limitations under the License.
stub('gr-custom-plugin-header', { stub('gr-custom-plugin-header', {
ready() { customHeader = this; }, ready() { customHeader = this; },
}); });
Gerrit._setPluginsPending([]); Gerrit._loadPlugins([]);
}); });
test('sets logo and title', done => { test('sets logo and title', done => {

View File

@@ -117,7 +117,7 @@ limitations under the License.
assert.strictEqual(element.style.backgroundImage, ''); assert.strictEqual(element.style.backgroundImage, '');
// Emulate plugins loaded. // Emulate plugins loaded.
Gerrit._setPluginsPending([]); Gerrit._loadPlugins([]);
Promise.all([ Promise.all([
element.$.restAPI.getConfig(), element.$.restAPI.getConfig(),
@@ -155,7 +155,7 @@ limitations under the License.
assert.isFalse(element.hasAttribute('hidden')); assert.isFalse(element.hasAttribute('hidden'));
// Emulate plugins loaded. // Emulate plugins loaded.
Gerrit._setPluginsPending([]); Gerrit._loadPlugins([]);
return Promise.all([ return Promise.all([
element.$.restAPI.getConfig(), element.$.restAPI.getConfig(),
@@ -197,7 +197,7 @@ limitations under the License.
_account_id: 123, _account_id: 123,
}; };
// Emulate plugins loaded. // Emulate plugins loaded.
Gerrit._setPluginsPending([]); Gerrit._loadPlugins([]);
return Promise.all([ return Promise.all([
element.$.restAPI.getConfig(), element.$.restAPI.getConfig(),

View File

@@ -19,6 +19,7 @@
'use strict'; 'use strict';
const PRELOADED_PROTOCOL = 'preloaded:'; const PRELOADED_PROTOCOL = 'preloaded:';
const PLUGIN_LOADING_TIMEOUT_MS = 10000;
let _restAPI; let _restAPI;
function getRestAPI() { function getRestAPI() {
@@ -28,6 +29,10 @@
return _restAPI; return _restAPI;
} }
function getBaseUrl() {
return Gerrit.BaseUrlBehavior.getBaseUrl();
}
/** /**
* Retrieves the name of the plugin base on the url. * Retrieves the name of the plugin base on the url.
* @param {string|URL} url * @param {string|URL} url
@@ -96,6 +101,9 @@
getPluginNameFromUrl, getPluginNameFromUrl,
send, send,
getRestAPI, getRestAPI,
getBaseUrl,
PRELOADED_PROTOCOL,
PLUGIN_LOADING_TIMEOUT_MS,
// TEST only methods // TEST only methods
testOnly_resetInternalState, testOnly_resetInternalState,

View File

@@ -58,13 +58,14 @@ breaking changes to gr-change-actions wont be noticed.
Gerrit.install(p => { plugin = p; }, '0.1', Gerrit.install(p => { plugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js'); 'http://test.com/plugins/testplugin/static/test.js');
// Mimic all plugins loaded. // Mimic all plugins loaded.
Gerrit._setPluginsPending([]); Gerrit._loadPlugins([]);
changeActions = plugin.changeActions(); changeActions = plugin.changeActions();
element = fixture('basic'); element = fixture('basic');
}); });
teardown(() => { teardown(() => {
changeActions = null; changeActions = null;
Gerrit._testOnly_resetPlugins();
}); });
test('does not throw', ()=> { test('does not throw', ()=> {
@@ -85,11 +86,12 @@ breaking changes to gr-change-actions wont be noticed.
'http://test.com/plugins/testplugin/static/test.js'); 'http://test.com/plugins/testplugin/static/test.js');
changeActions = plugin.changeActions(); changeActions = plugin.changeActions();
// Mimic all plugins loaded. // Mimic all plugins loaded.
Gerrit._setPluginsPending([]); Gerrit._loadPlugins([]);
}); });
teardown(() => { teardown(() => {
changeActions = null; changeActions = null;
Gerrit._testOnly_resetPlugins();
}); });
test('property existence', () => { test('property existence', () => {

View File

@@ -23,42 +23,12 @@
(function(window) { (function(window) {
'use strict'; 'use strict';
/**
* Hash of loaded and installed plugins, name to Plugin object.
*/
const _plugins = {};
/**
* Array of plugin URLs to be loaded, name to url.
*/
let _pluginsPending = {};
let _pluginsInstalled = [];
let _pluginsPendingCount = -1;
const UNKNOWN_PLUGIN = 'unknown';
const PRELOADED_PROTOCOL = 'preloaded:';
const PLUGIN_LOADING_TIMEOUT_MS = 10000;
let _reporting;
const getReporting = () => {
if (!_reporting) {
_reporting = document.createElement('gr-reporting');
}
return _reporting;
};
// Import utils methods // Import utils methods
const { const {
getPluginNameFromUrl,
send, send,
getRestAPI, getRestAPI,
} = window._apiUtils; } = window._apiUtils;
const API_VERSION = '0.1';
/** /**
* Trigger the preinstalls for bundled plugins. * Trigger the preinstalls for bundled plugins.
* This needs to happen before Gerrit as plugin bundle overrides the Gerrit. * This needs to happen before Gerrit as plugin bundle overrides the Gerrit.
@@ -72,9 +42,7 @@
window.Gerrit = window.Gerrit || {}; window.Gerrit = window.Gerrit || {};
const Gerrit = window.Gerrit; const Gerrit = window.Gerrit;
Gerrit._pluginLoader = new PluginLoader();
let _resolveAllPluginsLoaded = null;
let _allPluginsPromise = null;
Gerrit._endpoints = new GrPluginEndpoints(); Gerrit._endpoints = new GrPluginEndpoints();
@@ -85,20 +53,13 @@
const { const {
testOnly_resetInternalState, testOnly_resetInternalState,
} = window._apiUtils; } = window._apiUtils;
Gerrit._testOnly_installPreloadedPlugins = installPreloadedPlugins; Gerrit._testOnly_installPreloadedPlugins = (...args) => Gerrit._pluginLoader
.installPreloadedPlugins(...args);
Gerrit._testOnly_flushPreinstalls = flushPreinstalls; Gerrit._testOnly_flushPreinstalls = flushPreinstalls;
Gerrit._testOnly_resetPlugins = () => { Gerrit._testOnly_resetPlugins = () => {
_allPluginsPromise = null;
_pluginsInstalled = [];
_pluginsPending = {};
_pluginsPendingCount = -1;
_reporting = null;
_resolveAllPluginsLoaded = null;
testOnly_resetInternalState(); testOnly_resetInternalState();
Gerrit._endpoints = new GrPluginEndpoints(); Gerrit._endpoints = new GrPluginEndpoints();
for (const k of Object.keys(_plugins)) { Gerrit._pluginLoader = new PluginLoader();
delete _plugins[k];
}
}; };
} }
@@ -122,36 +83,7 @@
}; };
Gerrit.install = function(callback, opt_version, opt_src) { Gerrit.install = function(callback, opt_version, opt_src) {
// HTML import polyfill adds __importElement pointing to the import tag. Gerrit._pluginLoader.install(callback, opt_version, opt_src);
const script = document.currentScript &&
(document.currentScript.__importElement || document.currentScript);
let src = opt_src || (script && script.src);
if (!src || src.startsWith('data:')) {
src = script && script.baseURI;
}
const name = getPluginNameFromUrl(src);
if (opt_version && opt_version !== API_VERSION) {
Gerrit._pluginInstallError(`Plugin ${name} install error: only version ` +
API_VERSION + ' is supported in PolyGerrit. ' + opt_version +
' was given.');
return;
}
const existingPlugin = _plugins[name];
const plugin = existingPlugin || new Plugin(src);
try {
callback(plugin);
if (name) {
_plugins[name] = plugin;
}
if (!existingPlugin) {
Gerrit._pluginInstalled(src);
}
} catch (e) {
Gerrit._pluginInstallError(`${e.name}: ${e.message}`);
}
}; };
Gerrit.getLoggedIn = function() { Gerrit.getLoggedIn = function() {
@@ -195,96 +127,33 @@
}; };
Gerrit.awaitPluginsLoaded = function() { Gerrit.awaitPluginsLoaded = function() {
if (!_allPluginsPromise) { return Gerrit._pluginLoader.awaitPluginsLoaded();
if (Gerrit._arePluginsLoaded()) {
_allPluginsPromise = Promise.resolve();
} else {
let timeoutId;
_allPluginsPromise =
Promise.race([
new Promise(resolve => _resolveAllPluginsLoaded = resolve),
new Promise(resolve => timeoutId = setTimeout(
Gerrit._pluginLoadingTimeout, PLUGIN_LOADING_TIMEOUT_MS)),
]).then(() => clearTimeout(timeoutId));
}
}
return _allPluginsPromise;
}; };
Gerrit._pluginLoadingTimeout = function() { // TODO(taoalpha): consider removing these proxy methods
console.error(`Failed to load plugins: ${Object.keys(_pluginsPending)}`); // and using _pluginLoader directly
Gerrit._setPluginsPending([]);
};
Gerrit._setPluginsPending = function(plugins) { Gerrit._loadPlugins = function(plugins, opt_option) {
_pluginsPending = plugins.reduce((o, url) => { Gerrit._pluginLoader.loadPlugins(plugins, opt_option);
// TODO(viktard): Remove guard (@see Issue 8962)
o[getPluginNameFromUrl(url) || UNKNOWN_PLUGIN] = url;
return o;
}, {});
Gerrit._setPluginsCount(Object.keys(_pluginsPending).length);
};
Gerrit._setPluginsCount = function(count) {
_pluginsPendingCount = count;
if (Gerrit._arePluginsLoaded()) {
getReporting().pluginsLoaded(_pluginsInstalled);
if (_resolveAllPluginsLoaded) {
_resolveAllPluginsLoaded();
}
}
};
Gerrit._pluginInstallError = function(message) {
document.dispatchEvent(new CustomEvent('show-alert', {
detail: {
message: `Plugin install error: ${message}`,
},
}));
console.info(`Plugin install error: ${message}`);
Gerrit._setPluginsCount(_pluginsPendingCount - 1);
};
Gerrit._pluginInstalled = function(url) {
const name = getPluginNameFromUrl(url) || UNKNOWN_PLUGIN;
if (!_pluginsPending[name]) {
console.warn(`Unexpected plugin ${name} installed from ${url}.`);
} else {
delete _pluginsPending[name];
_pluginsInstalled.push(name);
Gerrit._setPluginsCount(_pluginsPendingCount - 1);
getReporting().pluginLoaded(name);
console.log(`Plugin ${name} installed.`);
}
}; };
Gerrit._arePluginsLoaded = function() { Gerrit._arePluginsLoaded = function() {
return _pluginsPendingCount === 0; return Gerrit._pluginLoader.arePluginsLoaded;
};
Gerrit._getPluginScreenName = function(pluginName, screenName) {
return `${pluginName}-screen-${screenName}`;
}; };
Gerrit._isPluginPreloaded = function(url) { Gerrit._isPluginPreloaded = function(url) {
const name = getPluginNameFromUrl(url); return Gerrit._pluginLoader.isPluginPreloaded(url);
if (name && Gerrit._preloadedPlugins) {
return name in Gerrit._preloadedPlugins;
} else {
return false;
}
}; };
function installPreloadedPlugins() { Gerrit._isPluginEnabled = function(pathOrUrl) {
if (!Gerrit._preloadedPlugins) { return; } return Gerrit._pluginLoader.isPluginEnabled(pathOrUrl);
for (const name in Gerrit._preloadedPlugins) { };
if (!Gerrit._preloadedPlugins.hasOwnProperty(name)) { continue; }
const callback = Gerrit._preloadedPlugins[name]; Gerrit._isPluginLoaded = function(pathOrUrl) {
Gerrit.install(callback, API_VERSION, PRELOADED_PROTOCOL + name); return Gerrit._pluginLoader.isPluginLoaded(pathOrUrl);
} };
}
// Preloaded plugins should be installed after Gerrit.install() is set, // Preloaded plugins should be installed after Gerrit.install() is set,
// since plugin preloader substitutes Gerrit.install() temporarily. // since plugin preloader substitutes Gerrit.install() temporarily.
installPreloadedPlugins(); Gerrit._pluginLoader.installPreloadedPlugins();
})(window); })(window);

View File

@@ -37,11 +37,11 @@ limitations under the License.
<script> <script>
suite('gr-gerrit tests', () => { suite('gr-gerrit tests', () => {
let element; let element;
let plugin;
let sandbox; let sandbox;
let sendStub; let sendStub;
setup(() => { setup(() => {
this.clock = sinon.useFakeTimers();
sandbox = sinon.sandbox.create(); sandbox = sinon.sandbox.create();
sendStub = sandbox.stub().returns(Promise.resolve({status: 200})); sendStub = sandbox.stub().returns(Promise.resolve({status: 200}));
stub('gr-rest-api-interface', { stub('gr-rest-api-interface', {
@@ -53,136 +53,48 @@ limitations under the License.
}, },
}); });
element = fixture('basic'); element = fixture('basic');
Gerrit.install(p => { plugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js');
Gerrit._setPluginsPending([]);
}); });
teardown(() => { teardown(() => {
this.clock.restore();
sandbox.restore(); sandbox.restore();
element._removeEventCallbacks(); element._removeEventCallbacks();
plugin = null; Gerrit._testOnly_resetPlugins();
}); });
test('reuse plugin for install calls', () => { suite('proxy methods', () => {
let otherPlugin; test('Gerrit._isPluginEnabled proxy to pluginLoader', () => {
Gerrit.install(p => { otherPlugin = p; }, '0.1', const stubFn = sandbox.stub();
'http://test.com/plugins/testplugin/static/test.js'); sandbox.stub(
assert.strictEqual(plugin, otherPlugin); Gerrit._pluginLoader,
'isPluginEnabled',
(...args) => stubFn(...args)
);
Gerrit._isPluginEnabled('test_plugin');
assert.isTrue(stubFn.calledWith('test_plugin'));
}); });
test('flushes preinstalls if provided', () => { test('Gerrit._isPluginLoaded proxy to pluginLoader', () => {
assert.doesNotThrow(() => { const stubFn = sandbox.stub();
Gerrit._testOnly_flushPreinstalls(); sandbox.stub(
}); Gerrit._pluginLoader,
window.Gerrit.flushPreinstalls = sandbox.stub(); 'isPluginLoaded',
Gerrit._testOnly_flushPreinstalls(); (...args) => stubFn(...args)
assert.isTrue(window.Gerrit.flushPreinstalls.calledOnce); );
delete window.Gerrit.flushPreinstalls; Gerrit._isPluginLoaded('test_plugin');
assert.isTrue(stubFn.calledWith('test_plugin'));
}); });
test('versioning', () => { test('Gerrit._isPluginPreloaded proxy to pluginLoader', () => {
const callback = sandbox.spy(); const stubFn = sandbox.stub();
Gerrit.install(callback, '0.0pre-alpha'); sandbox.stub(
assert(callback.notCalled); Gerrit._pluginLoader,
'isPluginPreloaded',
(...args) => stubFn(...args)
);
Gerrit._isPluginPreloaded('test_plugin');
assert.isTrue(stubFn.calledWith('test_plugin'));
}); });
test('_setPluginsCount', done => {
stub('gr-reporting', {
pluginsLoaded() {
done();
},
});
Gerrit._setPluginsCount(0);
});
test('_arePluginsLoaded', () => {
assert.isTrue(Gerrit._arePluginsLoaded());
Gerrit._setPluginsCount(1);
assert.isFalse(Gerrit._arePluginsLoaded());
Gerrit._setPluginsCount(0);
assert.isTrue(Gerrit._arePluginsLoaded());
});
test('_pluginInstalled', () => {
const pluginsLoadedStub = sandbox.stub();
stub('gr-reporting', {
pluginsLoaded: (...args) => pluginsLoadedStub(...args),
});
const plugins = [
'http://test.com/plugins/foo/static/test.js',
'http://test.com/plugins/bar/static/test.js',
];
Gerrit._setPluginsPending(plugins);
Gerrit._pluginInstalled(plugins[0]);
Gerrit._pluginInstalled(plugins[1]);
assert.isTrue(pluginsLoadedStub.calledWithExactly(['foo', 'bar']));
});
test('install calls _pluginInstalled', () => {
sandbox.stub(Gerrit, '_pluginInstalled');
Gerrit.install(p => { plugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js');
// testplugin has already been installed once (in setup).
assert.isFalse(Gerrit._pluginInstalled.called);
// testplugin2 plugin has not yet been installed.
Gerrit.install(p => { plugin = p; }, '0.1',
'http://test.com/plugins/testplugin2/static/test.js');
assert.isTrue(Gerrit._pluginInstalled.calledOnce);
});
test('plugin install errors mark plugins as loaded', () => {
Gerrit._setPluginsCount(1);
Gerrit.install(() => {}, '0.0pre-alpha');
return Gerrit.awaitPluginsLoaded();
});
test('multiple ui plugins per java plugin', () => {
const file1 = 'http://test.com/plugins/qaz/static/foo.nocache.js';
const file2 = 'http://test.com/plugins/qaz/static/bar.js';
Gerrit._setPluginsPending([file1, file2]);
Gerrit.install(() => {}, '0.1', file1);
Gerrit.install(() => {}, '0.1', file2);
return Gerrit.awaitPluginsLoaded();
});
test('plugin install errors shows toasts', () => {
const alertStub = sandbox.stub();
document.addEventListener('show-alert', alertStub);
Gerrit._setPluginsCount(1);
Gerrit.install(() => {}, '0.0pre-alpha');
return Gerrit.awaitPluginsLoaded().then(() => {
assert.isTrue(alertStub.calledOnce);
});
});
test('Gerrit._isPluginPreloaded', () => {
Gerrit._preloadedPlugins = {baz: ()=>{}};
assert.isFalse(Gerrit._isPluginPreloaded('plugins/foo/bar'));
assert.isFalse(Gerrit._isPluginPreloaded('http://a.com/42'));
assert.isTrue(Gerrit._isPluginPreloaded('preloaded:baz'));
Gerrit._preloadedPlugins = null;
});
test('preloaded plugins are installed', () => {
const installStub = sandbox.stub();
Gerrit._preloadedPlugins = {foo: installStub};
Gerrit._testOnly_installPreloadedPlugins();
assert.isTrue(installStub.called);
const pluginApi = installStub.lastCall.args[0];
assert.strictEqual(pluginApi.getPluginName(), 'foo');
});
test('installing preloaded plugin', () => {
let plugin;
window.ASSETS_PATH = 'http://blips.com/chitz';
Gerrit.install(p => { plugin = p; }, '0.1', 'preloaded:foo');
assert.strictEqual(plugin.getPluginName(), 'foo');
assert.strictEqual(plugin.url('/some/thing.html'),
'http://blips.com/chitz/plugins/foo/some/thing.html');
delete window.ASSETS_PATH;
}); });
}); });
</script> </script>

View File

@@ -31,6 +31,13 @@ limitations under the License.
<link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html"> <link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html">
<dom-module id="gr-js-api-interface"> <dom-module id="gr-js-api-interface">
<!--
Note: the order matters as files depend on each other.
1. gr-api-utils will be used in multiple files below.
2. gr-gerrit depends on gr-plugin-loader, gr-public-js-api and
also gr-plugin-endpoints
3. gr-public-js-api depends on gr-plugin-rest-api
-->
<script src="gr-api-utils.js"></script> <script src="gr-api-utils.js"></script>
<script src="gr-annotation-actions-context.js"></script> <script src="gr-annotation-actions-context.js"></script>
<script src="gr-annotation-actions-js-api.js"></script> <script src="gr-annotation-actions-js-api.js"></script>
@@ -41,5 +48,6 @@ limitations under the License.
<script src="gr-plugin-action-context.js"></script> <script src="gr-plugin-action-context.js"></script>
<script src="gr-plugin-rest-api.js"></script> <script src="gr-plugin-rest-api.js"></script>
<script src="gr-public-js-api.js"></script> <script src="gr-public-js-api.js"></script>
<script src="gr-plugin-loader.js"></script>
<script src="gr-gerrit.js"></script> <script src="gr-gerrit.js"></script>
</dom-module> </dom-module>

View File

@@ -35,6 +35,7 @@ limitations under the License.
</test-fixture> </test-fixture>
<script> <script>
const {PLUGIN_LOADING_TIMEOUT_MS} = window._apiUtils;
suite('gr-js-api-interface tests', () => { suite('gr-js-api-interface tests', () => {
let element; let element;
let plugin; let plugin;
@@ -48,6 +49,7 @@ limitations under the License.
}; };
setup(() => { setup(() => {
this.clock = sinon.useFakeTimers();
sandbox = sinon.sandbox.create(); sandbox = sinon.sandbox.create();
getResponseObjectStub = sandbox.stub().returns(Promise.resolve()); getResponseObjectStub = sandbox.stub().returns(Promise.resolve());
sendStub = sandbox.stub().returns(Promise.resolve({status: 200})); sendStub = sandbox.stub().returns(Promise.resolve({status: 200}));
@@ -64,10 +66,11 @@ limitations under the License.
errorStub = sandbox.stub(console, 'error'); errorStub = sandbox.stub(console, 'error');
Gerrit.install(p => { plugin = p; }, '0.1', Gerrit.install(p => { plugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js'); 'http://test.com/plugins/testplugin/static/test.js');
Gerrit._setPluginsPending([]); Gerrit._loadPlugins([]);
}); });
teardown(() => { teardown(() => {
this.clock.restore();
sandbox.restore(); sandbox.restore();
element._removeEventCallbacks(); element._removeEventCallbacks();
plugin = null; plugin = null;
@@ -194,12 +197,15 @@ limitations under the License.
revisions: {def: {_number: 2}, abc: {_number: 1}}, revisions: {def: {_number: 2}, abc: {_number: 1}},
}; };
const spy = sandbox.spy(); const spy = sandbox.spy();
Gerrit._setPluginsCount(1); Gerrit._loadPlugins(['plugins/test.html']);
plugin.on(element.EventType.SHOW_CHANGE, spy); plugin.on(element.EventType.SHOW_CHANGE, spy);
element.handleEvent(element.EventType.SHOW_CHANGE, element.handleEvent(element.EventType.SHOW_CHANGE,
{change: testChange, patchNum: 1}); {change: testChange, patchNum: 1});
assert.isFalse(spy.called); assert.isFalse(spy.called);
Gerrit._setPluginsCount(0);
// Timeout on loading plugins
this.clock.tick(PLUGIN_LOADING_TIMEOUT_MS * 2);
flush(() => { flush(() => {
assert.isTrue(spy.called); assert.isTrue(spy.called);
done(); done();
@@ -334,7 +340,6 @@ limitations under the License.
setup(() => { setup(() => {
sandbox.stub(Gerrit.BaseUrlBehavior, 'getBaseUrl').returns('/r'); sandbox.stub(Gerrit.BaseUrlBehavior, 'getBaseUrl').returns('/r');
Gerrit._setPluginsCount(1);
Gerrit.install(p => { baseUrlPlugin = p; }, '0.1', Gerrit.install(p => { baseUrlPlugin = p; }, '0.1',
'http://test.com/r/plugins/baseurlplugin/static/test.js'); 'http://test.com/r/plugins/baseurlplugin/static/test.js');
}); });

View File

@@ -42,7 +42,6 @@ limitations under the License.
setup(() => { setup(() => {
sandbox = sinon.sandbox.create(); sandbox = sinon.sandbox.create();
Gerrit._setPluginsCount(1);
Gerrit.install(p => { plugin = p; }, '0.1', Gerrit.install(p => { plugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js'); 'http://test.com/plugins/testplugin/static/test.js');
instance = new GrPluginActionContext(plugin); instance = new GrPluginActionContext(plugin);

View File

@@ -0,0 +1,393 @@
/**
* @license
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed 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.
*/
(function(window) {
'use strict';
// Import utils methods
const {
PLUGIN_LOADING_TIMEOUT_MS,
PRELOADED_PROTOCOL,
getPluginNameFromUrl,
getBaseUrl,
} = window._apiUtils;
/**
* @enum {string}
*/
const PluginState = {
/**
* State that indicates the plugin is pending to be loaded.
*/
PENDING: 'PENDING',
/**
* State that indicates the plugin is already loaded.
*/
LOADED: 'LOADED',
/**
* State that indicates the plugin is already loaded.
*/
PRE_LOADED: 'PRE_LOADED',
/**
* State that indicates the plugin failed to load.
*/
LOAD_FAILED: 'LOAD_FAILED',
};
// Prefix for any unrecognized plugin urls.
// Url should match following patterns:
// /plugins/PLUGINNAME/static/SCRIPTNAME.(html|js)
// /plugins/PLUGINNAME.(js|html)
const UNKNOWN_PLUGIN_PREFIX = '__$$__';
// Current API version for Plugin,
// plugins with incompatible version will not be laoded.
const API_VERSION = '0.1';
/**
* PluginLoader, responsible for:
*
* Loading all plugins and handling errors etc.
* Recording plugin state.
* Reporting on plugin loading status.
* Retrieve plugin.
* Check plugin status and if all plugins loaded.
*/
class PluginLoader {
constructor() {
this._pluginListLoaded = false;
/** @type {Map<string,PluginLoader.PluginObject>} */
this._plugins = new Map();
this._reporting = null;
// Promise that resolves when all plugins loaded
this._loadingPromise = null;
// Resolver to resolve _loadingPromise once all plugins loaded
this._loadingResolver = null;
}
_getReporting() {
if (!this._reporting) {
this._reporting = document.createElement('gr-reporting');
}
return this._reporting;
}
/**
* Use the plugin name or use the full url if not recognized.
* @see gr-api-utils#getPluginNameFromUrl
* @param {string|URL} url
*/
_getPluginKeyFromUrl(url) {
return getPluginNameFromUrl(url) ||
`${UNKNOWN_PLUGIN_PREFIX}${url}`;
}
/**
* Load multiple plugins with certain options.
*
* @param {Array<string>} plugins
* @param {Object<string, PluginLoader.PluginOption>} opts
*/
loadPlugins(plugins = [], opts = {}) {
this._pluginListLoaded = true;
plugins.forEach(path => {
const url = this._urlFor(path);
// Skip if preloaded, for bundling.
if (this.isPluginPreloaded(url)) return;
const pluginKey = this._getPluginKeyFromUrl(url);
// Skip if already installed.
if (this._plugins.has(pluginKey)) return;
this._plugins.set(pluginKey, {
name: pluginKey,
url,
state: PluginState.PENDING,
plugin: null,
});
if (this._isPathEndsWith(url, '.html')) {
this._importHtmlPlugin(url, opts && opts[path]);
} else if (this._isPathEndsWith(url, '.js')) {
this._loadJsPlugin(url);
} else {
this._failToLoad(`Unrecognized plugin url ${url}`, url);
}
});
this.awaitPluginsLoaded().then(() => {
console.info('Plugins loaded');
this._getReporting().pluginsLoaded(this._getAllInstalledPluginNames());
});
}
_isPathEndsWith(url, suffix) {
if (!(url instanceof URL)) {
try {
url = new URL(url);
} catch (e) {
console.warn(e);
return false;
}
}
return url.pathname && url.pathname.endsWith(suffix);
}
_getAllInstalledPluginNames() {
const installedPlugins = [];
for (const plugin of this._plugins.values()) {
if (plugin.state === PluginState.LOADED) {
installedPlugins.push(plugin.name);
}
}
return installedPlugins;
}
install(callback, opt_version, opt_src) {
// HTML import polyfill adds __importElement pointing to the import tag.
const script = document.currentScript &&
(document.currentScript.__importElement || document.currentScript);
let src = opt_src || (script && script.src);
if (!src || src.startsWith('data:')) {
src = script && script.baseURI;
}
if (opt_version && opt_version !== API_VERSION) {
this._failToLoad(`Plugin ${src} install error: only version ` +
API_VERSION + ' is supported in PolyGerrit. ' + opt_version +
' was given.', src);
return;
}
const pluginObject = this.getPlugin(src);
let plugin = pluginObject && pluginObject.plugin;
if (!plugin) {
plugin = new Plugin(src);
}
try {
callback(plugin);
this._pluginInstalled(src, plugin);
} catch (e) {
this._failToLoad(`${e.name}: ${e.message}`, src);
}
}
get arePluginsLoaded() {
// As the size of plugins is relatively small,
// so the performance of this check should be reasonable
if (!this._pluginListLoaded) return false;
for (const plugin of this._plugins.values()) {
if (plugin.state === PluginState.PENDING) return false;
}
return true;
}
_checkIfCompleted() {
if (this.arePluginsLoaded && this._loadingResolver) {
this._loadingResolver();
this._loadingResolver = null;
this._loadingPromise = null;
}
}
_timeout() {
const pendingPlugins = [];
for (const plugin of this._plugins.values()) {
if (plugin.state === PluginState.PENDING) {
this._updatePluginState(plugin.url, PluginState.LOAD_FAILED);
this._checkIfCompleted();
pendingPlugins.push(plugin.url);
}
}
return `Timeout when loading plugins: ${pendingPlugins.join(',')}`;
}
_failToLoad(message, pluginUrl) {
// Show an alert with the error
document.dispatchEvent(new CustomEvent('show-alert', {
detail: {
message: `Plugin install error: ${message} from ${pluginUrl}`,
},
}));
this._updatePluginState(pluginUrl, PluginState.LOAD_FAILED);
this._checkIfCompleted();
}
_updatePluginState(pluginUrl, state) {
const key = this._getPluginKeyFromUrl(pluginUrl);
if (this._plugins.has(key)) {
this._plugins.get(key).state = state;
} else {
// Plugin is not recorded for some reason.
console.warn(`Plugin loaded separately: ${pluginUrl}`);
this._plugins.set(key, {
name: key,
url: pluginUrl,
state,
plugin: null,
});
}
return this._plugins.get(key);
}
_pluginInstalled(url, plugin) {
const pluginObj = this._updatePluginState(url, PluginState.LOADED);
pluginObj.plugin = plugin;
this._getReporting().pluginLoaded(plugin.getPluginName() || url);
console.log(`Plugin ${plugin.getPluginName() || url} installed.`);
this._checkIfCompleted();
}
installPreloadedPlugins() {
if (!window.Gerrit || !window.Gerrit._preloadedPlugins) { return; }
const Gerrit = window.Gerrit;
for (const name in Gerrit._preloadedPlugins) {
if (!Gerrit._preloadedPlugins.hasOwnProperty(name)) { continue; }
const callback = Gerrit._preloadedPlugins[name];
this.install(callback, API_VERSION, PRELOADED_PROTOCOL + name);
}
}
isPluginPreloaded(pathOrUrl) {
const url = this._urlFor(pathOrUrl);
const name = getPluginNameFromUrl(url);
if (name && window.Gerrit._preloadedPlugins) {
return window.Gerrit._preloadedPlugins.hasOwnProperty(name);
} else {
return false;
}
}
/**
* Checks if given plugin path/url is enabled or not.
* @param {string} pathOrUrl
*/
isPluginEnabled(pathOrUrl) {
const url = this._urlFor(pathOrUrl);
if (this.isPluginPreloaded(url)) return true;
const key = this._getPluginKeyFromUrl(url);
return this._plugins.has(key);
}
/**
* Returns the plugin object with a given url.
* @param {string} pathOrUrl
*/
getPlugin(pathOrUrl) {
const key = this._getPluginKeyFromUrl(this._urlFor(pathOrUrl));
return this._plugins.get(key);
}
/**
* Checks if given plugin path/url is loaded or not.
* @param {string} pathOrUrl
*/
isPluginLoaded(pathOrUrl) {
const url = this._urlFor(pathOrUrl);
const key = this._getPluginKeyFromUrl(url);
return this._plugins.has(key) ?
this._plugins.get(key).state === PluginState.LOADED :
false;
}
_importHtmlPlugin(pluginUrl, opts = {}) {
// onload (second param) needs to be a function. When null or undefined
// were passed, plugins were not loaded correctly.
(Polymer.importHref || Polymer.Base.importHref)(
this._urlFor(pluginUrl), () => {},
() => this._failToLoad(`${pluginUrl} import error`, pluginUrl),
!opts.sync);
}
_loadJsPlugin(pluginUrl) {
this._createScriptTag(this._urlFor(pluginUrl));
}
_createScriptTag(url) {
const el = document.createElement('script');
el.defer = true;
el.src = url;
el.onerror = () => this._failToLoad(`${url} load error`, url);
return document.body.appendChild(el);
}
_urlFor(pathOrUrl) {
if (!pathOrUrl) {
return pathOrUrl;
}
if (pathOrUrl.startsWith(PRELOADED_PROTOCOL) ||
pathOrUrl.startsWith('http')) {
// Plugins are loaded from another domain or preloaded.
return pathOrUrl;
}
if (!pathOrUrl.startsWith('/')) {
pathOrUrl = '/' + pathOrUrl;
}
return window.location.origin + getBaseUrl() + pathOrUrl;
}
awaitPluginsLoaded() {
// Resolve if completed.
this._checkIfCompleted();
if (this.arePluginsLoaded) {
return Promise.resolve();
}
if (!this._loadingPromise) {
let timerId;
this._loadingPromise =
Promise.race([
new Promise(resolve => this._loadingResolver = resolve),
new Promise((_, reject) => timerId = setTimeout(
() => {
reject(this._timeout());
}, PLUGIN_LOADING_TIMEOUT_MS)),
]).then(() => {
if (timerId) clearTimeout(timerId);
});
}
return this._loadingPromise;
}
}
/**
* @typedef {{
* name:string,
* url:string,
* state:PluginState,
* plugin:Object
* }}
*/
PluginLoader.PluginObject;
/**
* @typedef {{
* sync:boolean,
* }}
*/
PluginLoader.PluginOption;
window.PluginLoader = PluginLoader;
})(window);

View File

@@ -0,0 +1,502 @@
<!DOCTYPE html>
<!--
@license
Copyright (C) 2017 The Android Open Source Project
Licensed 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.
-->
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-plugin-host</title>
<script src="/test/common-test-setup.js"></script>
<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="../../../test/common-test-setup.html"/>
<link rel="import" href="gr-js-api-interface.html">
<script>void(0);</script>
<test-fixture id="basic">
<template>
<gr-js-api-interface></gr-js-api-interface>
</template>
</test-fixture>
<script>
const {PRELOADED_PROTOCOL, PLUGIN_LOADING_TIMEOUT_MS} = window._apiUtils;
suite('gr-plugin-loader tests', () => {
let plugin;
let sandbox;
let url;
let sendStub;
setup(() => {
this.clock = sinon.useFakeTimers();
sandbox = sinon.sandbox.create();
sendStub = sandbox.stub().returns(Promise.resolve({status: 200}));
stub('gr-rest-api-interface', {
getAccount() {
return Promise.resolve({name: 'Judy Hopps'});
},
send(...args) {
return sendStub(...args);
},
});
sandbox.stub(document.body, 'appendChild');
fixture('basic');
url = window.location.origin;
});
teardown(() => {
sandbox.restore();
this.clock.restore();
Gerrit._testOnly_resetPlugins();
});
test('reuse plugin for install calls', () => {
Gerrit.install(p => { plugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js');
let otherPlugin;
Gerrit.install(p => { otherPlugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js');
assert.strictEqual(plugin, otherPlugin);
});
test('flushes preinstalls if provided', () => {
assert.doesNotThrow(() => {
Gerrit._testOnly_flushPreinstalls();
});
window.Gerrit.flushPreinstalls = sandbox.stub();
Gerrit._testOnly_flushPreinstalls();
assert.isTrue(window.Gerrit.flushPreinstalls.calledOnce);
delete window.Gerrit.flushPreinstalls;
});
test('versioning', () => {
const callback = sandbox.spy();
Gerrit.install(callback, '0.0pre-alpha');
assert(callback.notCalled);
});
test('report pluginsLoaded', done => {
stub('gr-reporting', {
pluginsLoaded() {
done();
},
});
Gerrit._loadPlugins([]);
});
test('arePluginsLoaded', done => {
assert.isFalse(Gerrit._arePluginsLoaded());
const plugins = [
'http://test.com/plugins/foo/static/test.js',
'http://test.com/plugins/bar/static/test.js',
];
Gerrit._loadPlugins(plugins);
assert.isFalse(Gerrit._arePluginsLoaded());
// Timeout on loading plugins
this.clock.tick(PLUGIN_LOADING_TIMEOUT_MS * 2);
flush(() => {
assert.isTrue(Gerrit._arePluginsLoaded());
done();
});
});
test('plugins installed successfully', done => {
sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
Gerrit.install(() => void 0, undefined, url);
});
const pluginsLoadedStub = sandbox.stub();
stub('gr-reporting', {
pluginsLoaded: (...args) => pluginsLoadedStub(...args),
});
const plugins = [
'http://test.com/plugins/foo/static/test.js',
'http://test.com/plugins/bar/static/test.js',
];
Gerrit._loadPlugins(plugins);
flush(() => {
assert.isTrue(pluginsLoadedStub.calledWithExactly(['foo', 'bar']));
assert.isTrue(Gerrit._arePluginsLoaded());
done();
});
});
test('isPluginEnabled and isPluginLoaded', done => {
sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
Gerrit.install(() => void 0, undefined, url);
});
const pluginsLoadedStub = sandbox.stub();
stub('gr-reporting', {
pluginsLoaded: (...args) => pluginsLoadedStub(...args),
});
const plugins = [
'http://test.com/plugins/foo/static/test.js',
'http://test.com/plugins/bar/static/test.js',
'bar/static/test.js',
];
Gerrit._loadPlugins(plugins);
assert.isTrue(
plugins.every(plugin => Gerrit._pluginLoader.isPluginEnabled(plugin))
);
flush(() => {
assert.isTrue(Gerrit._arePluginsLoaded());
assert.isTrue(
plugins.every(plugin => Gerrit._pluginLoader.isPluginLoaded(plugin))
);
done();
});
});
test('plugins installed mixed result, 1 fail 1 succeed', done => {
const plugins = [
'http://test.com/plugins/foo/static/test.js',
'http://test.com/plugins/bar/static/test.js',
];
const alertStub = sandbox.stub();
document.addEventListener('show-alert', alertStub);
sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
Gerrit.install(() => {
if (url === plugins[0]) {
throw new Error('failed');
}
}, undefined, url);
});
const pluginsLoadedStub = sandbox.stub();
stub('gr-reporting', {
pluginsLoaded: (...args) => pluginsLoadedStub(...args),
});
Gerrit._loadPlugins(plugins);
flush(() => {
assert.isTrue(pluginsLoadedStub.calledWithExactly(['bar']));
assert.isTrue(Gerrit._arePluginsLoaded());
assert.isTrue(alertStub.calledOnce);
done();
});
});
test('isPluginEnabled and isPluginLoaded for mixed results', done => {
const plugins = [
'http://test.com/plugins/foo/static/test.js',
'http://test.com/plugins/bar/static/test.js',
];
const alertStub = sandbox.stub();
document.addEventListener('show-alert', alertStub);
sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
Gerrit.install(() => {
if (url === plugins[0]) {
throw new Error('failed');
}
}, undefined, url);
});
const pluginsLoadedStub = sandbox.stub();
stub('gr-reporting', {
pluginsLoaded: (...args) => pluginsLoadedStub(...args),
});
Gerrit._loadPlugins(plugins);
assert.isTrue(
plugins.every(plugin => Gerrit._pluginLoader.isPluginEnabled(plugin))
);
flush(() => {
assert.isTrue(pluginsLoadedStub.calledWithExactly(['bar']));
assert.isTrue(Gerrit._arePluginsLoaded());
assert.isTrue(alertStub.calledOnce);
assert.isTrue(Gerrit._pluginLoader.isPluginLoaded(plugins[1]));
assert.isFalse(Gerrit._pluginLoader.isPluginLoaded(plugins[0]));
done();
});
});
test('plugins installed all failed', done => {
const plugins = [
'http://test.com/plugins/foo/static/test.js',
'http://test.com/plugins/bar/static/test.js',
];
const alertStub = sandbox.stub();
document.addEventListener('show-alert', alertStub);
sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
Gerrit.install(() => {
throw new Error('failed');
}, undefined, url);
});
const pluginsLoadedStub = sandbox.stub();
stub('gr-reporting', {
pluginsLoaded: (...args) => pluginsLoadedStub(...args),
});
Gerrit._loadPlugins(plugins);
flush(() => {
assert.isTrue(pluginsLoadedStub.calledWithExactly([]));
assert.isTrue(Gerrit._arePluginsLoaded());
assert.isTrue(alertStub.calledTwice);
done();
});
});
test('plugins installed failed becasue of wrong version', done => {
const plugins = [
'http://test.com/plugins/foo/static/test.js',
'http://test.com/plugins/bar/static/test.js',
];
const alertStub = sandbox.stub();
document.addEventListener('show-alert', alertStub);
sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
Gerrit.install(() => {
}, url === plugins[0] ? '' : 'alpha', url);
});
const pluginsLoadedStub = sandbox.stub();
stub('gr-reporting', {
pluginsLoaded: (...args) => pluginsLoadedStub(...args),
});
Gerrit._loadPlugins(plugins);
flush(() => {
assert.isTrue(pluginsLoadedStub.calledWithExactly(['foo']));
assert.isTrue(Gerrit._arePluginsLoaded());
assert.isTrue(alertStub.calledOnce);
done();
});
});
test('multiple assets for same plugin installed successfully', done => {
sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
Gerrit.install(() => void 0, undefined, url);
});
const pluginsLoadedStub = sandbox.stub();
stub('gr-reporting', {
pluginsLoaded: (...args) => pluginsLoadedStub(...args),
});
const plugins = [
'http://test.com/plugins/foo/static/test.js',
'http://test.com/plugins/foo/static/test2.js',
'http://test.com/plugins/bar/static/test.js',
];
Gerrit._loadPlugins(plugins);
flush(() => {
assert.isTrue(pluginsLoadedStub.calledWithExactly(['foo', 'bar']));
assert.isTrue(Gerrit._arePluginsLoaded());
done();
});
});
suite('plugin path and url', () => {
let importHtmlPluginStub;
let loadJsPluginStub;
setup(() => {
importHtmlPluginStub = sandbox.stub();
sandbox.stub(Gerrit._pluginLoader, '_importHtmlPlugin', url => {
importHtmlPluginStub(url);
});
loadJsPluginStub = sandbox.stub();
sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
loadJsPluginStub(url);
});
});
test('invalid plugin path', () => {
const failToLoadStub = sandbox.stub();
sandbox.stub(Gerrit._pluginLoader, '_failToLoad', (...args) => {
failToLoadStub(...args);
});
Gerrit._loadPlugins([
'foo/bar',
]);
assert.isTrue(failToLoadStub.calledOnce);
assert.isTrue(failToLoadStub.calledWithExactly(
`Unrecognized plugin url ${url}/foo/bar`,
`${url}/foo/bar`
));
});
test('relative path for plugins', () => {
Gerrit._loadPlugins([
'foo/bar.js',
'foo/bar.html',
]);
assert.isTrue(importHtmlPluginStub.calledOnce);
assert.isTrue(
importHtmlPluginStub.calledWithExactly(`${url}/foo/bar.html`)
);
assert.isTrue(loadJsPluginStub.calledOnce);
assert.isTrue(
loadJsPluginStub.calledWithExactly(`${url}/foo/bar.js`)
);
});
test('relative path should honor getBaseUrl', () => {
const testUrl = '/test';
sandbox.stub(Gerrit.BaseUrlBehavior, 'getBaseUrl', () => {
return testUrl;
});
Gerrit._loadPlugins([
'foo/bar.js',
'foo/bar.html',
]);
assert.isTrue(importHtmlPluginStub.calledOnce);
assert.isTrue(loadJsPluginStub.calledOnce);
assert.isTrue(
importHtmlPluginStub.calledWithExactly(
`${url}${testUrl}/foo/bar.html`
)
);
assert.isTrue(
loadJsPluginStub.calledWithExactly(`${url}${testUrl}/foo/bar.js`)
);
});
test('absolute path for plugins', () => {
Gerrit._loadPlugins([
'http://e.com/foo/bar.js',
'http://e.com/foo/bar.html',
]);
assert.isTrue(importHtmlPluginStub.calledOnce);
assert.isTrue(
importHtmlPluginStub.calledWithExactly(`http://e.com/foo/bar.html`)
);
assert.isTrue(loadJsPluginStub.calledOnce);
assert.isTrue(
loadJsPluginStub.calledWithExactly(`http://e.com/foo/bar.js`)
);
});
});
test('adds js plugins will call the body', () => {
Gerrit._loadPlugins([
'http://e.com/foo/bar.js',
'http://e.com/bar/foo.js',
]);
assert.isTrue(document.body.appendChild.calledTwice);
});
test('can call awaitPluginsLoaded multiple times', done => {
const plugins = [
'http://e.com/foo/bar.js',
'http://e.com/bar/foo.js',
];
let installed = false;
function pluginCallback(url) {
if (url === plugins[1]) {
installed = true;
}
}
sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
Gerrit.install(() => pluginCallback(url), undefined, url);
});
Gerrit._loadPlugins(plugins);
Gerrit.awaitPluginsLoaded().then(() => {
assert.isTrue(installed);
Gerrit.awaitPluginsLoaded().then(() => {
done();
});
});
});
suite('preloaded plugins', () => {
test('skips preloaded plugins when load plugins', () => {
const importHtmlPluginStub = sandbox.stub();
sandbox.stub(Gerrit._pluginLoader, '_importHtmlPlugin', url => {
importHtmlPluginStub(url);
});
const loadJsPluginStub = sandbox.stub();
sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => {
loadJsPluginStub(url);
});
Gerrit._preloadedPlugins = {
foo: () => void 0,
bar: () => void 0,
};
Gerrit._loadPlugins([
'http://e.com/plugins/foo.js',
'plugins/bar.html',
'http://e.com/plugins/test/foo.js',
]);
assert.isTrue(importHtmlPluginStub.notCalled);
assert.isTrue(loadJsPluginStub.calledOnce);
});
test('isPluginPreloaded', () => {
Gerrit._preloadedPlugins = {baz: ()=>{}};
assert.isFalse(Gerrit._pluginLoader.isPluginPreloaded('plugins/foo/bar'));
assert.isFalse(Gerrit._pluginLoader.isPluginPreloaded('http://a.com/42'));
assert.isTrue(
Gerrit._pluginLoader.isPluginPreloaded(PRELOADED_PROTOCOL + 'baz')
);
Gerrit._preloadedPlugins = null;
});
test('preloaded plugins are installed', () => {
const installStub = sandbox.stub();
Gerrit._preloadedPlugins = {foo: installStub};
Gerrit._pluginLoader.installPreloadedPlugins();
assert.isTrue(installStub.called);
const pluginApi = installStub.lastCall.args[0];
assert.strictEqual(pluginApi.getPluginName(), 'foo');
});
test('installing preloaded plugin', () => {
let plugin;
window.ASSETS_PATH = 'http://blips.com/chitz';
Gerrit.install(p => { plugin = p; }, '0.1', 'preloaded:foo');
assert.strictEqual(plugin.getPluginName(), 'foo');
assert.strictEqual(plugin.url('/some/thing.html'),
'http://blips.com/chitz/plugins/foo/some/thing.html');
delete window.ASSETS_PATH;
});
});
});
</script>

View File

@@ -50,7 +50,6 @@ limitations under the License.
a[k] = (...args) => restApiStub[k](...args); a[k] = (...args) => restApiStub[k](...args);
return a; return a;
}, {})); }, {}));
Gerrit._setPluginsCount(1);
Gerrit.install(p => { plugin = p; }, '0.1', Gerrit.install(p => { plugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js'); 'http://test.com/plugins/testplugin/static/test.js');
instance = new GrPluginRestApi(); instance = new GrPluginRestApi();

View File

@@ -267,10 +267,14 @@
return; return;
} }
return this.registerCustomComponent( return this.registerCustomComponent(
Gerrit._getPluginScreenName(this.getPluginName(), screenName), this._getScreenName(screenName),
opt_moduleName); opt_moduleName);
}; };
Plugin.prototype._getScreenName = function(screenName) {
return `${this.getPluginName()}-screen-${screenName}`;
};
const deprecatedAPI = { const deprecatedAPI = {
_loadedGwt: ()=> {}, _loadedGwt: ()=> {},
@@ -321,7 +325,7 @@
'Please use strings for patterns.'); 'Please use strings for patterns.');
return; return;
} }
this.hook(Gerrit._getPluginScreenName(this.getPluginName(), pattern)) this.hook(this._getScreenName(pattern))
.onAttached(el => { .onAttached(el => {
el.style.display = 'none'; el.style.display = 'none';
callback({ callback({

View File

@@ -188,6 +188,7 @@ limitations under the License.
'shared/gr-js-api-interface/gr-js-api-interface_test.html', 'shared/gr-js-api-interface/gr-js-api-interface_test.html',
'shared/gr-js-api-interface/gr-gerrit_test.html', 'shared/gr-js-api-interface/gr-gerrit_test.html',
'shared/gr-js-api-interface/gr-plugin-action-context_test.html', 'shared/gr-js-api-interface/gr-plugin-action-context_test.html',
'shared/gr-js-api-interface/gr-plugin-loader_test.html',
'shared/gr-js-api-interface/gr-plugin-endpoints_test.html', 'shared/gr-js-api-interface/gr-plugin-endpoints_test.html',
'shared/gr-js-api-interface/gr-plugin-rest-api_test.html', 'shared/gr-js-api-interface/gr-plugin-rest-api_test.html',
'shared/gr-fixed-panel/gr-fixed-panel_test.html', 'shared/gr-fixed-panel/gr-fixed-panel_test.html',