Merge "Add event interface to Gerrit" into stable-2.16
This commit is contained in:
		@@ -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);
 | 
				
			||||||
@@ -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>
 | 
				
			||||||
@@ -30,6 +30,7 @@ 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">
 | 
				
			||||||
 | 
					  <script src="../gr-event-interface/gr-event-interface.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>
 | 
				
			||||||
  <script src="gr-change-actions-js-api.js"></script>
 | 
					  <script src="gr-change-actions-js-api.js"></script>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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;
 | 
					  window.Gerrit = Gerrit;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Preloaded plugins should be installed after Gerrit.install() is set,
 | 
					  // Preloaded plugins should be installed after Gerrit.install() is set,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -149,6 +149,7 @@ limitations under the License.
 | 
				
			|||||||
    'settings/gr-settings-view/gr-settings-view_test.html',
 | 
					    'settings/gr-settings-view/gr-settings-view_test.html',
 | 
				
			||||||
    'settings/gr-ssh-editor/gr-ssh-editor_test.html',
 | 
					    'settings/gr-ssh-editor/gr-ssh-editor_test.html',
 | 
				
			||||||
    'settings/gr-watched-projects-editor/gr-watched-projects-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-label/gr-account-label_test.html',
 | 
				
			||||||
    'shared/gr-account-link/gr-account-link_test.html',
 | 
					    'shared/gr-account-link/gr-account-link_test.html',
 | 
				
			||||||
    'shared/gr-alert/gr-alert_test.html',
 | 
					    'shared/gr-alert/gr-alert_test.html',
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user