Wait for plugin endpoint params to be defined

Wait for all endpoint params to be set (not `undefined`) when attaching
plugin-provided element to the DOM at the plugin extension endpoint.

Practically, this ensures that all endpoint params are set for plugin
modules.

Change-Id: I8b2be9fe76eba27b9976d48867594c492e1b54e5
This commit is contained in:
Viktar Donich
2017-11-10 11:01:35 -08:00
parent 991d5646b7
commit 85268ba7e7
3 changed files with 132 additions and 88 deletions

View File

@@ -14,6 +14,8 @@
(function() {
'use strict';
const INIT_PROPERTIES_TIMEOUT_MS = 10000;
Polymer({
is: 'gr-endpoint-decorator',
@@ -40,59 +42,73 @@
_initDecoration(name, plugin) {
const el = document.createElement(name);
this._initProperties(el, plugin, this.getContentChildren().find(
el => el.nodeName !== 'GR-ENDPOINT-PARAM'));
this._appendChild(el);
return el;
return this._initProperties(el, plugin,
this.getContentChildren().find(
el => el.nodeName !== 'GR-ENDPOINT-PARAM'))
.then(el => this._appendChild(el));
},
_initReplacement(name, plugin) {
this.getContentChildNodes().forEach(node => node.remove());
this.getContentChildNodes()
.filter(node => node.nodeName !== 'GR-ENDPOINT-PARAM')
.forEach(node => node.remove());
const el = document.createElement(name);
this._initProperties(el, plugin);
this._appendChild(el);
return el;
return this._initProperties(el, plugin).then(
el => this._appendChild(el));
},
_getEndpointParams() {
return Polymer.dom(this).querySelectorAll('gr-endpoint-param').map(el => {
return {name: el.getAttribute('name'), value: el.value};
});
return Polymer.dom(this).querySelectorAll('gr-endpoint-param');
},
/**
* @param {!Element} el
* @param {!Object} plugin
* @param {!Element=} opt_content
* @return {!Promise<Element>}
*/
_initProperties(el, plugin, opt_content) {
el.plugin = plugin;
if (opt_content) {
el.content = opt_content;
}
for (const {name, value} of this._getEndpointParams()) {
el[name] = value;
}
const expectProperties = this._getEndpointParams().map(
paramEl => plugin.attributeHelper(paramEl).get('value')
.then(value => el[paramEl.getAttribute('name')] = value)
);
const timeout = new Promise(
resolve => setTimeout(() => {
console.warn(
'Timeout waiting for endpoint properties initialization.' +
`plugin ${plugin.getPluginName()}, endpoint ${this.name}`);
resolve();
}, INIT_PROPERTIES_TIMEOUT_MS));
return Promise.race([timeout, Promise.all(expectProperties)])
.then(() => el);
},
_appendChild(el) {
Polymer.dom(this.root).appendChild(el);
return Polymer.dom(this.root).appendChild(el);
},
_initModule({moduleName, plugin, type, domHook}) {
let el;
let initPromise;
switch (type) {
case 'decorate':
el = this._initDecoration(moduleName, plugin);
initPromise = this._initDecoration(moduleName, plugin);
break;
case 'replace':
el = this._initReplacement(moduleName, plugin);
initPromise = this._initReplacement(moduleName, plugin);
break;
}
if (el) {
domHook.handleInstanceAttached(el);
if (!initPromise) {
console.warn('Unable to initialize module' +
`${moduleName} from ${plugin.getPluginName()}`);
}
this._domHooks.set(el, domHook);
initPromise.then(el => {
domHook.handleInstanceAttached(el);
this._domHooks.set(el, domHook);
});
},
ready() {

View File

@@ -28,48 +28,45 @@ limitations under the License.
<test-fixture id="basic">
<template>
<gr-endpoint-decorator name="foo">
<gr-endpoint-param name="someparam" value="barbar"></gr-endpoint-param>
</gr-endpoint-decorator>
<div>
<gr-endpoint-decorator name="first">
<gr-endpoint-param name="someparam" value="barbar"></gr-endpoint-param>
</gr-endpoint-decorator>
<gr-endpoint-decorator name="second">
<gr-endpoint-param name="someparam" value="foofoo"></gr-endpoint-param>
</gr-endpoint-decorator>
<gr-endpoint-decorator name="banana">
<gr-endpoint-param name="someParam" value="yes"></gr-endpoint-param>
</gr-endpoint-decorator>
</div>
</template>
</test-fixture>
<script>
suite('gr-endpoint-decorator', () => {
let container;
let sandbox;
let element;
let plugin;
let domHookStub;
let decorationHook;
let replacementHook;
setup(done => {
Gerrit._endpoints = new GrPluginEndpoints();
sandbox = sinon.sandbox.create();
domHookStub = {
handleInstanceAttached: sandbox.stub(),
handleInstanceDetached: sandbox.stub(),
getPublicAPI: () => domHookStub,
};
sandbox.stub(
GrDomHooksManager.prototype, 'getDomHook').returns(domHookStub);
// NB: Order is important.
Gerrit.install(p => {
plugin = p;
plugin.registerCustomComponent('foo', 'some-module');
plugin.registerCustomComponent('foo', 'other-module', {replace: true});
plugin.registerCustomComponent('bar', 'some-module');
}, '0.1', 'http://some/plugin/url.html');
sandbox.stub(Gerrit, '_arePluginsLoaded').returns(true);
sandbox.stub(Gerrit, 'awaitPluginsLoaded').returns(Promise.resolve());
element = fixture('basic');
sandbox.stub(element, '_initDecoration').returns({});
sandbox.stub(element, '_initReplacement').returns({});
sandbox.stub(element, 'importHref', (url, resolve) => resolve());
stub('gr-endpoint-decorator', {
_import: sandbox.stub().returns(Promise.resolve()),
});
// Since _endpoints are global, must reset state.
Gerrit._endpoints = new GrPluginEndpoints();
container = fixture('basic');
Gerrit.install(p => plugin = p, '0.1', 'http://some/plugin/url.html');
hooks = [];
// Decoration
decorationHook = plugin.registerCustomComponent('first', 'some-module');
// Replacement
replacementHook = plugin.registerCustomComponent(
'second', 'other-module', {replace: true});
// Mimic all plugins loaded.
Gerrit._setPluginsCount(0);
flush(done);
});
@@ -77,51 +74,79 @@ limitations under the License.
sandbox.restore();
});
test('imports plugin-provided module', () => {
assert.isTrue(
element.importHref.calledWith(new URL('http://some/plugin/url.html')));
test('imports plugin-provided modules into endpoints', () => {
const endpoints =
Array.from(container.querySelectorAll('gr-endpoint-decorator'));
assert.equal(endpoints.length, 3);
endpoints.forEach(element => {
assert.isTrue(
element._import.calledWith(new URL('http://some/plugin/url.html')));
});
});
test('inits decoration dom hook', () => {
assert.strictEqual(
element._initDecoration.lastCall.args[0], 'some-module');
assert.strictEqual(
element._initDecoration.lastCall.args[1], plugin);
test('decoration', () => {
const element =
container.querySelector('gr-endpoint-decorator[name="first"]');
const module = Polymer.dom(element.root).children.find(
element => element.nodeName === 'SOME-MODULE');
assert.isOk(module);
assert.equal(module['someparam'], 'barbar');
return decorationHook.getLastAttached().then(element => {
assert.strictEqual(element, module);
}).then(() => {
element.remove();
assert.equal(decorationHook.getAllAttached().length, 0);
});
});
test('inits replacement dom hook', () => {
assert.strictEqual(
element._initReplacement.lastCall.args[0], 'other-module');
assert.strictEqual(
element._initReplacement.lastCall.args[1], plugin);
test('replacement', () => {
const element =
container.querySelector('gr-endpoint-decorator[name="second"]');
const module = Polymer.dom(element.root).children.find(
element => element.nodeName === 'OTHER-MODULE');
assert.isOk(module);
assert.equal(module['someparam'], 'foofoo');
return replacementHook.getLastAttached().then(element => {
assert.strictEqual(element, module);
}).then(() => {
element.remove();
assert.equal(replacementHook.getAllAttached().length, 0);
});
});
test('calls dom hook handleInstanceAttached', () => {
assert.equal(domHookStub.handleInstanceAttached.callCount, 2);
});
test('calls dom hook handleInstanceDetached', () => {
element.detached();
assert.equal(domHookStub.handleInstanceDetached.callCount, 2);
});
test('installs modules on late registration', done => {
domHookStub.handleInstanceAttached.reset();
plugin.registerCustomComponent('foo', 'noob-noob');
test('late registration', done => {
plugin.registerCustomComponent('banana', 'noob-noob');
flush(() => {
assert.equal(domHookStub.handleInstanceAttached.callCount, 1);
assert.strictEqual(
element._initDecoration.lastCall.args[0], 'noob-noob');
assert.strictEqual(
element._initDecoration.lastCall.args[1], plugin);
const element =
container.querySelector('gr-endpoint-decorator[name="banana"]');
const module = Polymer.dom(element.root).children.find(
element => element.nodeName === 'NOOB-NOOB');
assert.isOk(module);
done();
});
});
test('params', () => {
const instance = document.createElement('foo');
element._initProperties(instance, plugin);
assert.equal(instance.someparam, 'barbar');
test('late param setup', done => {
const element =
container.querySelector('gr-endpoint-decorator[name="banana"]');
const param = Polymer.dom(element).querySelector('gr-endpoint-param');
param['value'] = undefined;
plugin.registerCustomComponent('banana', 'noob-noob');
flush(() => {
let module = Polymer.dom(element.root).children.find(
element => element.nodeName === 'NOOB-NOOB');
// Module waits for param to be defined.
assert.isNotOk(module);
const value = {abc: 'def'};
param.value = value;
flush(() => {
module = Polymer.dom(element.root).children.find(
element => element.nodeName === 'NOOB-NOOB');
assert.isOk(module);
assert.strictEqual(module['someParam'], value);
done();
});
});
});
});
</script>

View File

@@ -18,7 +18,10 @@
is: 'gr-endpoint-param',
properties: {
name: String,
value: Object,
value: {
type: Object,
notify: true,
},
},
});
})();