diff options
author | Ben Rohlfs <brohlfs@google.com> | 2019-12-10 14:29:29 +0000 |
---|---|---|
committer | Gerrit Code Review <noreply-gerritcodereview@google.com> | 2019-12-10 14:29:29 +0000 |
commit | bb1c614b7283958d2c3fa6e78e06bf481c48b631 (patch) | |
tree | 00276abee0effa7f4f919c935a1606b58de9464c | |
parent | 850b1c312ce8c283b2bfd503d7a142f7ca599e84 (diff) | |
parent | d936a7a0e0f49da3d0432a4f03e6a008c6e23c54 (diff) |
Merge "Add event interface to Gerrit" into stable-2.16
5 files changed, 327 insertions, 0 deletions
diff --git a/polygerrit-ui/app/elements/shared/gr-event-interface/gr-event-interface.js b/polygerrit-ui/app/elements/shared/gr-event-interface/gr-event-interface.js new file mode 100644 index 0000000000..d024bb2df3 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-event-interface/gr-event-interface.js @@ -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);
\ No newline at end of file diff --git a/polygerrit-ui/app/elements/shared/gr-event-interface/gr-event-interface_test.html b/polygerrit-ui/app/elements/shared/gr-event-interface/gr-event-interface_test.html new file mode 100644 index 0000000000..137ed25042 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-event-interface/gr-event-interface_test.html @@ -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>
\ No newline at end of file diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html index d8a662ec37..d95fd0afd6 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html @@ -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> diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js index 36a428d966..1311105572 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js @@ -662,6 +662,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, diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html index 5784eadec7..75ba7054d5 100644 --- a/polygerrit-ui/app/test/index.html +++ b/polygerrit-ui/app/test/index.html @@ -149,6 +149,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', |