Merge branch 'stable-2.16' into stable-3.0

* stable-2.16:
  Add event interface to Gerrit

Change-Id: I3d65950598cc04922c19188ce020d504d109e817
This commit is contained in:
David Pursehouse
2019-12-11 08:20:11 +09:00
5 changed files with 327 additions and 0 deletions

View File

@@ -0,0 +1,142 @@
/**
* @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';
// Avoid duplicate registeration
if (window.EventEmitter) return;
/**
* An lite implementation of
* https://nodejs.org/api/events.html#events_class_eventemitter.
*
* This is unrelated to the native DOM events, you should use it when you want
* to enable EventEmitter interface on any class.
*
* @example
*
* class YourClass extends EventEmitter {
* // now all instance of YourClass will have this EventEmitter interface
* }
*
*/
class EventEmitter {
constructor() {
/**
* Shared events map from name to the listeners.
* @type {!Object<string, Array<eventCallback>>}
*/
this._listenersMap = new Map();
}
/**
* Register an event listener to an event.
*
* @param {string} eventName
* @param {eventCallback} cb
* @returns {Function} Unsubscribe method
*/
addListener(eventName, cb) {
if (!eventName || !cb) {
console.warn('A valid eventname and callback is required!');
return;
}
const listeners = this._listenersMap.get(eventName) || [];
listeners.push(cb);
this._listenersMap.set(eventName, listeners);
return () => {
this.off(eventName, cb);
};
}
// Alias for addListener.
on(eventName, cb) {
return this.addListener(eventName, cb);
}
// Attach event handler only once. Automatically removed.
once(eventName, cb) {
const onceWrapper = (...args) => {
cb(...args);
this.off(eventName, onceWrapper);
};
return this.on(eventName, onceWrapper);
}
/**
* De-register an event listener to an event.
*
* @param {string} eventName
* @param {eventCallback} cb
*/
removeListener(eventName, cb) {
let listeners = this._listenersMap.get(eventName) || [];
listeners = listeners.filter(listener => listener !== cb);
this._listenersMap.set(eventName, listeners);
}
// Alias to removeListener
off(eventName, cb) {
this.removeListener(eventName, cb);
}
/**
* Synchronously calls each of the listeners registered for
* the event named eventName, in the order they were registered,
* passing the supplied detail to each.
*
* Returns true if the event had listeners, false otherwise.
*
* @param {string} eventName
* @param {*} detail
*/
emit(eventName, detail) {
const listeners = this._listenersMap.get(eventName) || [];
for (const listener of listeners) {
try {
listener(detail);
} catch (e) {
console.error(e);
}
}
return listeners.length !== 0;
}
// Alias to emit.
dispatch(eventName, detail) {
return this.emit(eventName, detail);
}
/**
* Remove listeners for a specific event or all.
*
* @param {string} eventName if not provided, will remove all
*/
removeAllListeners(eventName) {
if (eventName) {
this._listenersMap.set(eventName, []);
} else {
this._listenersMap = new Map();
}
}
}
window.EventEmitter = EventEmitter;
})(window);

View File

@@ -0,0 +1,146 @@
<!DOCTYPE html>
<!--
@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.
-->
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-api-interface</title>
<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.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/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>
suite('gr-event-interface tests', () => {
let sandbox;
setup(() => {
sandbox = sinon.sandbox.create();
});
teardown(() => {
sandbox.restore();
});
suite('test on Gerrit', () => {
setup(() => {
fixture('basic');
Gerrit.removeAllListeners();
});
test('communicate between plugin and Gerrit', done => {
const eventName = 'test-plugin-event';
let p;
Gerrit.on(eventName, e => {
assert.equal(e.value, 'test');
assert.equal(e.plugin, p);
done();
});
Gerrit.install(plugin => {
p = plugin;
Gerrit.emit(eventName, {value: 'test', plugin});
}, '0.1',
'http://test.com/plugins/testplugin/static/test.js');
});
test('listen on events from core', done => {
const eventName = 'test-plugin-event';
Gerrit.on(eventName, e => {
assert.equal(e.value, 'test');
done();
});
Gerrit.emit(eventName, {value: 'test'});
});
test('communicate across plugins', done => {
const eventName = 'test-plugin-event';
Gerrit.install(plugin => {
Gerrit.on(eventName, e => {
assert.equal(e.plugin.getPluginName(), 'testB');
done();
});
}, '0.1',
'http://test.com/plugins/testA/static/testA.js');
Gerrit.install(plugin => {
Gerrit.emit(eventName, {plugin});
}, '0.1',
'http://test.com/plugins/testB/static/testB.js');
});
});
suite('test on interfaces', () => {
let testObj;
class TestClass extends EventEmitter {
}
setup(() => {
testObj = new TestClass();
});
test('on', () => {
const cbStub = sinon.stub();
testObj.on('test', cbStub);
testObj.emit('test');
testObj.emit('test');
assert.isTrue(cbStub.calledTwice);
});
test('once', () => {
const cbStub = sinon.stub();
testObj.once('test', cbStub);
testObj.emit('test');
testObj.emit('test');
assert.isTrue(cbStub.calledOnce);
});
test('unsubscribe', () => {
const cbStub = sinon.stub();
const unsubscribe = testObj.on('test', cbStub);
testObj.emit('test');
unsubscribe();
testObj.emit('test');
assert.isTrue(cbStub.calledOnce);
});
test('off', () => {
const cbStub = sinon.stub();
testObj.on('test', cbStub);
testObj.emit('test');
testObj.off('test', cbStub);
testObj.emit('test');
assert.isTrue(cbStub.calledOnce);
});
test('removeAllListeners', () => {
const cbStub = sinon.stub();
testObj.on('test', cbStub);
testObj.removeAllListeners('test');
testObj.emit('test');
assert.isTrue(cbStub.notCalled);
});
});
});
</script>

View File

@@ -30,6 +30,7 @@ limitations under the License.
<link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html">
<dom-module id="gr-js-api-interface">
<script src="../gr-event-interface/gr-event-interface.js"></script>
<script src="gr-annotation-actions-context.js"></script>
<script src="gr-annotation-actions-js-api.js"></script>
<script src="gr-change-actions-js-api.js"></script>

View File

@@ -668,6 +668,43 @@
}
};
// TODO(taoalpha): List all internal supported event names.
// Also convert this to inherited class once we move Gerrit to class.
Gerrit._eventEmitter = new EventEmitter();
['addListener',
'dispatch',
'emit',
'off',
'on',
'once',
'removeAllListeners',
'removeListener',
].forEach(method => {
/**
* Enabling EventEmitter interface on Gerrit.
*
* This will enable to signal across different parts of js code without relying on DOM,
* including core to core, plugin to plugin and also core to plugin.
*
* @example
*
* // Emit this event from pluginA
* Gerrit.install(pluginA => {
* fetch("some-api").then(() => {
* Gerrit.on("your-special-event", {plugin: pluginA});
* });
* });
*
* // Listen on your-special-event from pluignB
* Gerrit.install(pluginB => {
* Gerrit.on("your-special-event", ({plugin}) => {
* // do something, plugin is pluginA
* });
* });
*/
Gerrit[method] = Gerrit._eventEmitter[method].bind(Gerrit._eventEmitter);
});
window.Gerrit = Gerrit;
// Preloaded plugins should be installed after Gerrit.install() is set,

View File

@@ -148,6 +148,7 @@ limitations under the License.
'settings/gr-settings-view/gr-settings-view_test.html',
'settings/gr-ssh-editor/gr-ssh-editor_test.html',
'settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html',
'shared/gr-event-interface/gr-event-interface_test.html',
'shared/gr-account-label/gr-account-label_test.html',
'shared/gr-account-link/gr-account-link_test.html',
'shared/gr-alert/gr-alert_test.html',