Merge "Add reset button to my menu in settings"
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.js b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.js
index 1982b9a..853e316 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.js
@@ -297,7 +297,8 @@
      */
     _recursivelyRemoveDeleted(obj) {
       for (const k in obj) {
-        if (!obj.hasOwnProperty(k)) { return; }
+        if (!obj.hasOwnProperty(k)) { continue; }
+
         if (typeof obj[k] == 'object') {
           if (obj[k].deleted) {
             delete obj[k];
@@ -310,7 +311,7 @@
 
     _recursivelyUpdateAddRemoveObj(obj, addRemoveObj, path = []) {
       for (const k in obj) {
-        if (!obj.hasOwnProperty(k)) { return; }
+        if (!obj.hasOwnProperty(k)) { continue; }
         if (typeof obj[k] == 'object') {
           const updatedId = obj[k].updatedId;
           const ref = updatedId ? updatedId : k;
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html
index 8035df59..a9c3c6f 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html
@@ -79,6 +79,7 @@
       <gr-change-list
           account="[[account]]"
           changes="{{_changes}}"
+          preferences="[[preferences]]"
           selected-index="{{viewState.selectedChangeIndex}}"
           show-star="[[_loggedIn]]"></gr-change-list>
       <nav class$="[[_computeNavClass(_loading)]]">
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
index e4af24d..1728bc1 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
@@ -76,6 +76,8 @@
         value() { return {}; },
       },
 
+      preferences: Object,
+
       _changesPerPage: Number,
 
       /**
@@ -120,7 +122,7 @@
     },
 
     attached() {
-      this.fire('title-change', {title: this._query});
+      this._loadPreferences();
     },
 
     _paramsChanged(value) {
@@ -158,6 +160,18 @@
       });
     },
 
+    _loadPreferences() {
+      return this.$.restAPI.getLoggedIn().then(loggedIn => {
+        if (loggedIn) {
+          this._getPreferences().then(preferences => {
+            this.preferences = preferences;
+          });
+        } else {
+          this.preferences = {};
+        }
+      });
+    },
+
     _replaceCurrentLocation(url) {
       window.location.replace(url);
     },
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html
index 1e0622e..d917423 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html
@@ -24,7 +24,6 @@
 <link rel="import" href="../../../styles/gr-change-list-styles.html">
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
 <link rel="import" href="../../shared/gr-cursor-manager/gr-cursor-manager.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../gr-change-list-item/gr-change-list-item.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
@@ -92,7 +91,6 @@
         </template>
       </template>
     </table>
-    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
     <gr-cursor-manager
         id="cursor"
         index="{{selectedIndex}}"
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
index 89c6577..dc41f59 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
@@ -94,6 +94,7 @@
       },
       changeTableColumns: Array,
       visibleChangeTableColumns: Array,
+      preferences: Object,
     },
 
     behaviors: [
@@ -120,6 +121,7 @@
 
     observers: [
       '_sectionsChanged(sections.*)',
+      '_computePreferences(account, preferences)',
     ],
 
     /**
@@ -136,38 +138,23 @@
       }
     },
 
-    attached() {
-      this._loadPreferences();
-    },
-
     _lowerCase(column) {
       return column.toLowerCase();
     },
 
-    _loadPreferences() {
-      return this._getLoggedIn().then(loggedIn => {
-        this.changeTableColumns = this.columnNames;
+    _computePreferences(account, preferences) {
+      this.changeTableColumns = this.columnNames;
 
-        if (!loggedIn) {
-          this.showNumber = false;
-          this.visibleChangeTableColumns = this.columnNames;
-          return;
-        }
-        return this._getPreferences().then(preferences => {
-          this.showNumber = !!(preferences &&
-              preferences.legacycid_in_change_table);
-          this.visibleChangeTableColumns = preferences.change_table.length > 0 ?
-              preferences.change_table : this.columnNames;
-        });
-      });
-    },
-
-    _getLoggedIn() {
-      return this.$.restAPI.getLoggedIn();
-    },
-
-    _getPreferences() {
-      return this.$.restAPI.getPreferences();
+      if (account) {
+        this.showNumber = !!(preferences &&
+            preferences.legacycid_in_change_table);
+        this.visibleChangeTableColumns = preferences.change_table.length > 0 ?
+            preferences.change_table : this.columnNames;
+      } else {
+        // Not logged in.
+        this.showNumber = false;
+        this.visibleChangeTableColumns = this.columnNames;
+      }
     },
 
     _computeColspan(changeTableColumns, labelNames) {
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
index e72830c..3dd8c9d 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
@@ -52,22 +52,11 @@
 
     teardown(() => { sandbox.restore(); });
 
-    function stubRestAPI(preferences) {
-      const loggedInPromise = Promise.resolve(preferences !== null);
-      const preferencesPromise = Promise.resolve(preferences);
-      stub('gr-rest-api-interface', {
-        getLoggedIn: sinon.stub().returns(loggedInPromise),
-        getPreferences: sinon.stub().returns(preferencesPromise),
-      });
-      return Promise.all([loggedInPromise, preferencesPromise]);
-    }
-
     suite('test show change number not logged in', () => {
       setup(() => {
-        return stubRestAPI(null).then(() => {
-          element = fixture('basic');
-          return element._loadPreferences();
-        });
+        element = fixture('basic');
+        element.account = null;
+        element.preferences = null;
       });
 
       test('show number disabled', () => {
@@ -77,13 +66,14 @@
 
     suite('test show change number preference enabled', () => {
       setup(() => {
-        return stubRestAPI({legacycid_in_change_table: true,
+        element = fixture('basic');
+        element.preferences = {
+          legacycid_in_change_table: true,
           time_format: 'HHMM_12',
           change_table: [],
-        }).then(() => {
-          element = fixture('basic');
-          return element._loadPreferences();
-        });
+        };
+        element.account = {_account_id: 1001};
+        flushAsynchronousOperations();
       });
 
       test('show number enabled', () => {
@@ -93,12 +83,14 @@
 
     suite('test show change number preference disabled', () => {
       setup(() => {
+        element = fixture('basic');
         // legacycid_in_change_table is not set when false.
-        return stubRestAPI({time_format: 'HHMM_12', change_table: []}).then(
-            () => {
-              element = fixture('basic');
-              return element._loadPreferences();
-            });
+        element.preferences = {
+          time_format: 'HHMM_12',
+          change_table: [],
+        };
+        element.account = {_account_id: 1001};
+        flushAsynchronousOperations();
       });
 
       test('show number disabled', () => {
@@ -275,16 +267,16 @@
     suite('empty column preference', () => {
       let element;
 
-      setup(() =>
-        stubRestAPI({
+      setup(() => {
+        element = fixture('basic');
+        element.account = {_account_id: 1001};
+        element.preferences = {
           legacycid_in_change_table: true,
           time_format: 'HHMM_12',
           change_table: [],
-        }).then(() => {
-          element = fixture('basic');
-          return element._loadPreferences();
-        })
-      );
+        };
+        flushAsynchronousOperations();
+      });
 
       test('show number enabled', () => {
         assert.isTrue(element.showNumber);
@@ -302,7 +294,9 @@
       let element;
 
       setup(() => {
-        return stubRestAPI({
+        element = fixture('basic');
+        element.account = {_account_id: 1001};
+        element.preferences = {
           legacycid_in_change_table: true,
           time_format: 'HHMM_12',
           change_table: [
@@ -315,10 +309,8 @@
             'Updated',
             'Size',
           ],
-        }).then(() => {
-          element = fixture('basic');
-          return element._loadPreferences();
-        });
+        };
+        flushAsynchronousOperations();
       });
 
       test('all columns visible', () => {
@@ -333,7 +325,9 @@
       let element;
 
       setup(() => {
-        return stubRestAPI({
+        element = fixture('basic');
+        element.account = {_account_id: 1001};
+        element.preferences = {
           legacycid_in_change_table: true,
           time_format: 'HHMM_12',
           change_table: [
@@ -345,10 +339,8 @@
             'Updated',
             'Size',
           ],
-        }).then(() => {
-          element = fixture('basic');
-          return element._loadPreferences();
-        });
+        };
+        flushAsynchronousOperations();
       });
 
       test('all columns except project visible', () => {
@@ -369,16 +361,16 @@
       /* This would only exist if somebody manually updated the config
       file. */
       setup(() => {
-        return stubRestAPI({
+        element = fixture('basic');
+        element.account = {_account_id: 1001};
+        element.preferences = {
           legacycid_in_change_table: true,
           time_format: 'HHMM_12',
           change_table: [
             'Bad',
           ],
-        }).then(() => {
-          element = fixture('basic');
-          return element._loadPreferences();
-        });
+        };
+        flushAsynchronousOperations();
       });
 
       test('bad column does not exist', () => {
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.html b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.html
index 33cd39e..d97e7f4 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.html
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.html
@@ -57,6 +57,7 @@
           show-star
           show-reviewed-state
           account="[[account]]"
+          preferences="[[preferences]]"
           selected-index="{{viewState.selectedChangeIndex}}"
           sections="[[_results]]"></gr-change-list>
     </div>
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
index 6be2331..b6e06d8 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
@@ -77,6 +77,7 @@
         type: Object,
         value: null,
       },
+      preferences: Object,
       /** @type {{ selectedChangeIndex: number }} */
       viewState: Object,
 
@@ -116,6 +117,22 @@
       );
     },
 
+    attached() {
+      this._loadPreferences();
+    },
+
+    _loadPreferences() {
+      return this.$.restAPI.getLoggedIn().then(loggedIn => {
+        if (loggedIn) {
+          this.$.restAPI.getPreferences().then(preferences => {
+            this.preferences = preferences;
+          });
+        } else {
+          this.preferences = {};
+        }
+      });
+    },
+
     _getProjectDashboard(project, dashboard) {
       const errFn = response => {
         this.fire('page-error', {response});
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
index ff4562b..1827ef6 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
@@ -250,12 +250,8 @@
         max-width: 15rem;
         --paper-tab-ink: var(--color-link);
       }
-      #threadList,
-      #messageList {
-        display: none;
-      }
-      #threadList.visible,
-      #messageList.visible {
+      gr-thread-list,
+      gr-messages-list {
         display: block;
       }
       #includedInOverlay {
@@ -562,24 +558,26 @@
             <span>Comment Threads</span></gr-tooltip-content>
         </paper-tab>
       </paper-tabs>
-      <gr-messages-list id="messageList"
-          class$="hideOnMobileOverlay [[_computeShowMessages(_showMessagesView)]]"
-          change-num="[[_changeNum]]"
-          labels="[[_change.labels]]"
-          messages="[[_change.messages]]"
-          reviewer-updates="[[_change.reviewer_updates]]"
-          change-comments="[[_changeComments]]"
-          project-name="[[_change.project]]"
-          show-reply-buttons="[[_loggedIn]]"
-          on-reply="_handleMessageReply"></gr-messages-list>
-      <gr-thread-list
-          id="threadList"
-          threads="[[_commentThreads]]"
-          change="[[_change]]"
-          change-num="[[_changeNum]]"
-          class$="[[_computeShowThreads(_showMessagesView)]]"
-          logged-in="[[_loggedIn]]"
-          on-thread-list-modified="_handleReloadDiffComments"></gr-thread-list>
+      <template is="dom-if" if="[[_showMessagesView]]">
+        <gr-messages-list
+            class="hideOnMobileOverlay"
+            change-num="[[_changeNum]]"
+            labels="[[_change.labels]]"
+            messages="[[_change.messages]]"
+            reviewer-updates="[[_change.reviewer_updates]]"
+            change-comments="[[_changeComments]]"
+            project-name="[[_change.project]]"
+            show-reply-buttons="[[_loggedIn]]"
+            on-reply="_handleMessageReply"></gr-messages-list>
+      </template>
+      <template is="dom-if" if="[[!_showMessagesView]]">
+        <gr-thread-list
+            threads="[[_commentThreads]]"
+            change="[[_change]]"
+            change-num="[[_changeNum]]"
+            logged-in="[[_loggedIn]]"
+            on-thread-list-modified="_handleReloadDiffComments"></gr-thread-list>
+      </template>
     </div>
     <gr-overlay id="downloadOverlay" with-backdrop>
       <gr-download-dialog
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index ebce594..e999911 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -308,6 +308,14 @@
       }
     },
 
+    get messagesList() {
+      return this.$$('gr-messages-list');
+    },
+
+    get threadList() {
+      return this.$$('gr-thread-list');
+    },
+
     /**
      * @param {boolean=} opt_reset
      */
@@ -341,14 +349,6 @@
       this._showMessagesView = this.$.commentTabs.selected === 0;
     },
 
-    _computeShowMessages(showSection) {
-      return showSection ? 'visible' : '';
-    },
-
-    _computeShowThreads(showSection) {
-      return !showSection ? 'visible' : '';
-    },
-
     _handleEditCommitMessage(e) {
       this._editingCommitMessage = true;
       this.$.commitMessageEditor.focusTextarea();
@@ -706,7 +706,7 @@
     _maybeScrollToMessage(hash) {
       const msgPrefix = '#message-';
       if (hash.startsWith(msgPrefix)) {
-        this.$.messageList.scrollToMessage(hash.substr(msgPrefix.length));
+        this.messagesList.scrollToMessage(hash.substr(msgPrefix.length));
       }
     },
 
@@ -937,7 +937,7 @@
           this.modifierPressed(e)) { return; }
 
       e.preventDefault();
-      this.$.messageList.handleExpandCollapse(true);
+      this.messagesList.handleExpandCollapse(true);
     },
 
     _handleZKey(e) {
@@ -945,7 +945,7 @@
           this.modifierPressed(e)) { return; }
 
       e.preventDefault();
-      this.$.messageList.handleExpandCollapse(false);
+      this.messagesList.handleExpandCollapse(false);
     },
 
     _handleCommaKey(e) {
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
index 4d33fab..f377277 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
@@ -205,18 +205,24 @@
         assert.isTrue(handleCollapse.called);
       });
 
-      test('X should expand all messages', () => {
-        const handleExpand =
-            sandbox.stub(element.$.messageList, 'handleExpandCollapse');
-        MockInteractions.pressAndReleaseKeyOn(element, 88, null, 'x');
-        assert(handleExpand.calledWith(true));
+      test('X should expand all messages', done => {
+        flush(() => {
+          const handleExpand = sandbox.stub(element.messagesList,
+              'handleExpandCollapse');
+          MockInteractions.pressAndReleaseKeyOn(element, 88, null, 'x');
+          assert(handleExpand.calledWith(true));
+          done();
+        });
       });
 
-      test('Z should collapse all messages', () => {
-        const handleExpand =
-            sandbox.stub(element.$.messageList, 'handleExpandCollapse');
-        MockInteractions.pressAndReleaseKeyOn(element, 90, null, 'z');
-        assert(handleExpand.calledWith(false));
+      test('Z should collapse all messages', done => {
+        flush(() => {
+          const handleExpand = sandbox.stub(element.messagesList,
+              'handleExpandCollapse');
+          MockInteractions.pressAndReleaseKeyOn(element, 90, null, 'z');
+          assert(handleExpand.calledWith(false));
+          done();
+        });
       });
 
       test('shift + R should fetch and navigate to the latest patch set',
@@ -295,10 +301,7 @@
       };
       setup(() => {
         reloadStub = sandbox.stub(element.$.commentAPI, 'reloadDrafts')
-          .returns(Promise.resolve({
-            drafts,
-          }
-        ));
+            .returns(Promise.resolve({drafts}));
       });
 
       test('drafts are reloaded when reload-drafts fired', done => {
@@ -327,14 +330,13 @@
 
     test('thread list modified', () => {
       sandbox.spy(element, '_handleReloadDiffComments');
-      return element._reloadComments().then(() => {
-        element.$.threadList.fire('thread-list-modified');
-        assert.isTrue(element._handleReloadDiffComments.called);
-      });
-    });
+      element._showMessagesView = false;
+      flushAsynchronousOperations();
 
-    test('thread list modified', () => {
       return element._reloadComments().then(() => {
+        element.threadList.fire('thread-list-modified');
+        assert.isTrue(element._handleReloadDiffComments.called);
+
         let draftStub = sinon.stub(element._changeComments, 'computeDraftCount')
             .returns(1);
         assert.equal(element._computeTotalCommentCounts(5,
@@ -395,27 +397,18 @@
       // Wait for tab to get selected
       flush(() => {
         assert.equal(element.$.commentTabs.selected, 0);
-        assert.notEqual(getComputedStyle(element.$.messageList).display,
-            'none');
-        assert.equal(getComputedStyle(element.$.threadList).display, 'none');
-
+        assert.isTrue(element._showMessagesView);
         // Switch to comment thread tab
         MockInteractions.tap(element.$$('paper-tab.commentThreads'));
         assert.equal(element.$.commentTabs.selected, 1);
-        assert.equal(getComputedStyle(element.$.messageList).display,
-            'none');
-        assert.notEqual(getComputedStyle(element.$.threadList).display,
-            'none');
+        assert.isFalse(element._showMessagesView);
 
         // When the change is partially reloaded (ex: Shift+R), the content
         // is swapped out before the tab, so messages list will display even
         // though the tab for comment threads is still temporarily selected.
         element._paramsChanged(element.params);
         assert.equal(element.$.commentTabs.selected, 1);
-        assert.notEqual(getComputedStyle(element.$.messageList).display,
-            'none');
-        assert.equal(getComputedStyle(element.$.threadList).display,
-            'none');
+        assert.isTrue(element._showMessagesView);
         done();
       });
     });
@@ -1021,13 +1014,17 @@
         });
 
     test('_openReplyDialog called with `BODY` when coming from message reply' +
-        'event', () => {
-      const openStub = sandbox.stub(element, '_openReplyDialog');
-      element.$.messageList.fire('reply', {message: {message: 'text'}});
-      assert(openStub.lastCall.calledWithExactly(
-          element.$.replyDialog.FocusTarget.BODY),
-          '_openReplyDialog should have been passed BODY');
-      assert.equal(openStub.callCount, 1);
+        'event', done => {
+      flush(() => {
+        const openStub = sandbox.stub(element, '_openReplyDialog');
+        element.messagesList.fire('reply',
+            {message: {message: 'text'}});
+        assert(openStub.lastCall.calledWithExactly(
+            element.$.replyDialog.FocusTarget.BODY),
+            '_openReplyDialog should have been passed BODY');
+        assert.equal(openStub.callCount, 1);
+        done();
+      });
     });
 
     test('reply dialog focus can be controlled', () => {
@@ -1437,18 +1434,20 @@
       assert.equal(element._computeHeaderClass(true), 'header editMode');
     });
 
-    test('_maybeScrollToMessage', () => {
-      const scrollStub = sandbox.stub(element.$.messageList, 'scrollToMessage');
+    test('_maybeScrollToMessage', done => {
+      flush(() => {
+        const scrollStub = sandbox.stub(element.messagesList,
+            'scrollToMessage');
 
-      element._maybeScrollToMessage('');
-      assert.isFalse(scrollStub.called);
-
-      element._maybeScrollToMessage('message');
-      assert.isFalse(scrollStub.called);
-
-      element._maybeScrollToMessage('#message-TEST');
-      assert.isTrue(scrollStub.called);
-      assert.equal(scrollStub.lastCall.args[0], 'TEST');
+        element._maybeScrollToMessage('');
+        assert.isFalse(scrollStub.called);
+        element._maybeScrollToMessage('message');
+        assert.isFalse(scrollStub.called);
+        element._maybeScrollToMessage('#message-TEST');
+        assert.isTrue(scrollStub.called);
+        assert.equal(scrollStub.lastCall.args[0], 'TEST');
+        done();
+      });
     });
 
     test('topic update reloads related changes', () => {
@@ -1602,8 +1601,7 @@
 
       setup(() => {
         fireEdit = () => {
-          element.$.actions
-              .dispatchEvent(new CustomEvent('edit-tap', {bubbles: false}));
+          element.$.actions.dispatchEvent(new CustomEvent('edit-tap'));
         };
         sandbox.stub(element.$.metadata, '_computeShowLabelStatus');
         sandbox.stub(element.$.metadata, '_computeLabelNames');
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.html b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.html
index 4b36607..c3e65ab 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.html
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.html
@@ -80,7 +80,12 @@
       <template is="dom-if" if="[[!threads.length]]">
         There are no inline comment threads on any diff for this change.
       </template>
-      <template is="dom-repeat" items="[[_sortedThreads]]" as="thread">
+      <template
+          is="dom-repeat"
+          items="[[_sortedThreads]]"
+          as="thread"
+          initial-count="5"
+          target-framerate="60">
         <gr-diff-comment-thread
             show-file-path
             change-num="[[changeNum]]"
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.html b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.html
index 3181a0b..2970a26 100644
--- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.html
+++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.html
@@ -15,7 +15,6 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
 
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
index 49edbd5..8ab5adf3 100644
--- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
+++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
@@ -40,9 +40,6 @@
 
   const INTERACTION_TYPE = 'interaction';
 
-  const CHANGE_VIEW_REGEX = /^\/c\/\d+\/?\d*$/;
-  const DIFF_VIEW_REGEX = /^\/c\/\d+\/\d+\/.+$/;
-
   const pending = [];
 
   const onError = function(oldOnError, msg, url, line, column, error) {
@@ -89,10 +86,6 @@
       },
     },
 
-    behaviors: [
-      Gerrit.BaseUrlBehavior,
-    ],
-
     get performanceTiming() {
       return window.performance.timing;
     },
@@ -164,19 +157,7 @@
       }
     },
 
-    locationChanged() {
-      let page = '';
-      const pathname = this._getPathname();
-      if (pathname.startsWith('/q/')) {
-        page = this.getBaseUrl() + '/q/';
-      } else if (pathname.match(CHANGE_VIEW_REGEX)) { // change view
-        page = this.getBaseUrl() + '/c/';
-      } else if (pathname.match(DIFF_VIEW_REGEX)) { // diff view
-        page = this.getBaseUrl() + '/c//COMMIT_MSG';
-      } else {
-        // Ignore other page changes.
-        return;
-      }
+    locationChanged(page) {
       this.reporter(
           NAVIGATION.TYPE, NAVIGATION.CATEGORY, NAVIGATION.PAGE, page);
     },
@@ -185,10 +166,6 @@
       this.timeEnd('PluginsLoaded');
     },
 
-    _getPathname() {
-      return '/' + window.location.pathname.substring(this.getBaseUrl().length);
-    },
-
     /**
      * Reset named timer.
      */
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
index c3208ab..62ef2d6 100644
--- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
+++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
@@ -135,43 +135,10 @@
       });
     });
 
-    suite('location changed', () => {
-      let pathnameStub;
-      setup(() => {
-        pathnameStub = sinon.stub(element, '_getPathname');
-      });
-
-      teardown(() => {
-        pathnameStub.restore();
-      });
-
-      test('search', () => {
-        pathnameStub.returns('/q/foo');
-        element.locationChanged();
-        assert.isTrue(element.reporter.calledWithExactly(
-            'nav-report', 'Location Changed', 'Page', '/q/'));
-      });
-
-      test('change view', () => {
-        pathnameStub.returns('/c/42/');
-        element.locationChanged();
-        assert.isTrue(element.reporter.calledWithExactly(
-            'nav-report', 'Location Changed', 'Page', '/c/'));
-      });
-
-      test('change view', () => {
-        pathnameStub.returns('/c/41/2');
-        element.locationChanged();
-        assert.isTrue(element.reporter.calledWithExactly(
-            'nav-report', 'Location Changed', 'Page', '/c/'));
-      });
-
-      test('diff view', () => {
-        pathnameStub.returns('/c/41/2/file.txt');
-        element.locationChanged();
-        assert.isTrue(element.reporter.calledWithExactly(
-            'nav-report', 'Location Changed', 'Page', '/c//COMMIT_MSG'));
-      });
+    test('search', () => {
+      element.locationChanged('_handleSomeRoute');
+      assert.isTrue(element.reporter.calledWithExactly(
+          'nav-report', 'Location Changed', 'Page', '_handleSomeRoute'));
     });
 
     suite('exception logging', () => {
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.html b/polygerrit-ui/app/elements/core/gr-router/gr-router.html
index 0d05bf1..7186b52 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.html
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.html
@@ -26,6 +26,7 @@
 <dom-module id="gr-router">
   <template>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+    <gr-reporting id="reporting"></gr-reporting>
   </template>
   <script src="../../../bower_components/page/page.js"></script>
   <script src="gr-router.js"></script>
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.js b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
index 4ee00fb..f6079a5 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -178,21 +178,18 @@
     console.log('No gr-app found (running tests)');
   }
 
-  let _reporting;
-  function getReporting() {
-    if (!_reporting) {
-      _reporting = document.createElement('gr-reporting');
-    }
-    return _reporting;
-  }
+  // Setup listeners outside of the router component initialization.
+  (function() {
+    const reporting = document.createElement('gr-reporting');
 
-  document.onload = function() {
-    getReporting().pageLoaded();
-  };
+    document.onload = function() {
+      reporting.pageLoaded();
+    };
 
-  window.addEventListener('WebComponentsReady', () => {
-    getReporting().timeEnd('WebComponentsReady');
-  });
+    window.addEventListener('WebComponentsReady', () => {
+      reporting.timeEnd('WebComponentsReady');
+    });
+  })();
 
   Polymer({
     is: 'gr-router',
@@ -643,6 +640,7 @@
         return;
       }
       page(pattern, this._loadUserMiddleware.bind(this), data => {
+        this.$.reporting.locationChanged(handlerName);
         const promise = opt_authRedirect ?
           this._redirectIfNotLoggedIn(data) : Promise.resolve();
         promise.then(() => { this[handlerName](data); });
@@ -655,8 +653,6 @@
         page.base(base);
       }
 
-      const reporting = getReporting();
-
       Gerrit.Nav.setup(
           url => { page.show(url); },
           this._generateUrl.bind(this),
@@ -682,7 +678,6 @@
             hash: window.location.hash,
             pathname: window.location.pathname,
           });
-          reporting.locationChanged();
         }, 1);
         next();
       });
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
index b6409792..f1bce4c 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.html
@@ -273,14 +273,16 @@
             [[comment.robot_id]]
           </div>
         </template>
-        <gr-textarea
-            id="editTextarea"
-            class="editMessage"
-            autocomplete="on"
-            monospace
-            disabled="{{disabled}}"
-            rows="4"
-            text="{{_messageText}}"></gr-textarea>
+        <template is="dom-if" if="[[editing]]">
+          <gr-textarea
+              id="editTextarea"
+              class="editMessage"
+              autocomplete="on"
+              monospace
+              disabled="{{disabled}}"
+              rows="4"
+              text="{{_messageText}}"></gr-textarea>
+        </template>
         <!--The message class is needed to ensure selectability from
         gr-diff-selection.-->
         <gr-formatted-text class="message"
@@ -339,27 +341,29 @@
         </div>
       </div>
     </div>
-    <gr-overlay id="confirmDeleteOverlay" with-backdrop>
-      <gr-confirm-delete-comment-dialog id="confirmDeleteComment"
-          on-confirm="_handleConfirmDeleteComment"
-          on-cancel="_handleCancelDeleteComment">
-      </gr-confirm-delete-comment-dialog>
-    </gr-overlay>
-    <gr-overlay id="confirmDiscardOverlay" with-backdrop>
-      <gr-confirm-dialog
-          id="confirmDiscardDialog"
-          confirm-label="Discard"
-          confirm-on-enter
-          on-confirm="_handleConfirmDiscard"
-          on-cancel="_closeConfirmDiscardOverlay">
-        <div class="header" slot="header">
-          Discard comment
-        </div>
-        <div class="main" slot="main">
-          Are you sure you want to discard this draft comment?
-        </div>
-      </gr-confirm-dialog>
-    </gr-overlay>
+    <template is="dom-if" if="[[_enableOverlay]]">
+      <gr-overlay id="confirmDeleteOverlay" with-backdrop>
+        <gr-confirm-delete-comment-dialog id="confirmDeleteComment"
+            on-confirm="_handleConfirmDeleteComment"
+            on-cancel="_handleCancelDeleteComment">
+        </gr-confirm-delete-comment-dialog>
+      </gr-overlay>
+      <gr-overlay id="confirmDiscardOverlay" with-backdrop>
+        <gr-confirm-dialog
+            id="confirmDiscardDialog"
+            confirm-label="Discard"
+            confirm-on-enter
+            on-confirm="_handleConfirmDiscard"
+            on-cancel="_closeConfirmDiscardOverlay">
+          <div class="header" slot="header">
+            Discard comment
+          </div>
+          <div class="main" slot="main">
+            Are you sure you want to discard this draft comment?
+          </div>
+        </gr-confirm-dialog>
+      </gr-overlay>
+    </template>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
     <gr-storage id="storage"></gr-storage>
   </template>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
index 9e5ae87..8612011 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment.js
@@ -124,6 +124,22 @@
       },
 
       _savingMessage: String,
+
+      _enableOverlay: {
+        type: Boolean,
+        value: false,
+      },
+
+      /**
+       * Property for storing references to overlay elements. When the overlays
+       * are moved to Gerrit.getRootElement() to be shown they are no-longer
+       * children, so they can't be queried along the tree, so they are stored
+       * here.
+       */
+      _overlays: {
+        type: Object,
+        value: () => ({}),
+      },
     },
 
     observers: [
@@ -155,7 +171,31 @@
 
     detached() {
       this.cancelDebouncer('fire-update');
-      this.$.editTextarea.closeDropdown();
+      if (this.textarea) {
+        this.textarea.closeDropdown();
+      }
+    },
+
+    get textarea() {
+      return this.$$('#editTextarea');
+    },
+
+    get confirmDeleteOverlay() {
+      if (!this._overlays.confirmDelete) {
+        this._enableOverlay = true;
+        Polymer.dom.flush();
+        this._overlays.confirmDelete = this.$$('#confirmDeleteOverlay');
+      }
+      return this._overlays.confirmDelete;
+    },
+
+    get confirmDiscardOverlay() {
+      if (!this._overlays.confirmDiscard) {
+        this._enableOverlay = true;
+        Polymer.dom.flush();
+        this._overlays.confirmDiscard = this.$$('#confirmDiscardOverlay');
+      }
+      return this._overlays.confirmDiscard;
     },
 
     _computeShowHideText(collapsed) {
@@ -272,9 +312,6 @@
 
     _editingChanged(editing, previousValue) {
       this.$.container.classList.toggle('editing', editing);
-      if (editing) {
-        this.$.editTextarea.putCursorAtEnd();
-      }
       if (this.comment && this.comment.id) {
         this.$$('.cancel').hidden = !editing;
       }
@@ -285,6 +322,12 @@
         // To prevent event firing on comment creation.
         this._fireUpdate();
       }
+      if (editing) {
+        this.async(() => {
+          Polymer.dom.flush();
+          this.textarea.putCursorAtEnd();
+        }, 1);
+      }
     },
 
     _computeLinkToComment(comment) {
@@ -437,7 +480,7 @@
         this._discardDraft();
         return;
       }
-      this._openOverlay(this.$.confirmDiscardOverlay);
+      this._openOverlay(this.confirmDiscardOverlay);
     },
 
     _handleConfirmDiscard(e) {
@@ -475,7 +518,7 @@
     },
 
     _closeConfirmDiscardOverlay() {
-      this._closeOverlay(this.$.confirmDiscardOverlay);
+      this._closeOverlay(this.confirmDiscardOverlay);
     },
 
     _getSavingMessage(numPending) {
@@ -593,11 +636,11 @@
     },
 
     _handleCommentDelete() {
-      this._openOverlay(this.$.confirmDeleteOverlay);
+      this._openOverlay(this.confirmDeleteOverlay);
     },
 
     _handleCancelDeleteComment() {
-      this._closeOverlay(this.$.confirmDeleteOverlay);
+      this._closeOverlay(this.confirmDeleteOverlay);
     },
 
     _openOverlay(overlay) {
@@ -613,9 +656,11 @@
     },
 
     _handleConfirmDeleteComment() {
+      const dialog =
+          this.confirmDeleteOverlay.querySelector('#confirmDeleteComment');
       this.$.restAPI.deleteComment(
-          this.changeNum, this.patchNum, this.comment.id,
-          this.$.confirmDeleteComment.message).then(newComment => {
+          this.changeNum, this.patchNum, this.comment.id, dialog.message)
+          .then(newComment => {
             this._handleCancelDeleteComment();
             this.comment = newComment;
           });
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
index d124764..5b43430 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment/gr-diff-comment_test.html
@@ -80,8 +80,7 @@
           'gr-formatted-text is not visible');
       assert.isFalse(isVisible(element.$$('.actions')),
           'actions are not visible');
-      assert.isFalse(isVisible(element.$$('gr-textarea')),
-          'textarea is not visible');
+      assert.isNotOk(element.textarea, 'textarea is not visible');
 
       // The header middle content is only visible when comments are collapsed.
       // It shows the message in a condensed way, and limits to a single line.
@@ -95,8 +94,7 @@
           'gr-formatted-text is visible');
       assert.isTrue(isVisible(element.$$('.actions')),
           'actions are visible');
-      assert.isFalse(isVisible(element.$$('gr-textarea')),
-          'textarea is not visible');
+      assert.isNotOk(element.textarea, 'textarea is not visible');
       assert.isFalse(isVisible(element.$$('.collapsedContent')),
           'header middle content is not visible');
     });
@@ -167,8 +165,7 @@
           'gr-formatted-text is not visible');
       assert.isFalse(isVisible(element.$$('.actions')),
           'actions are not visible');
-      assert.isFalse(isVisible(element.$$('gr-textarea')),
-          'textarea is not visible');
+      assert.isNotOk(element.textarea, 'textarea is not visible');
       assert.isTrue(isVisible(element.$$('.collapsedContent')),
           'header middle content is visible');
 
@@ -178,8 +175,7 @@
           'gr-formatted-text is visible');
       assert.isTrue(isVisible(element.$$('.actions')),
           'actions are visible');
-      assert.isFalse(isVisible(element.$$('gr-textarea')),
-          'textarea is not visible');
+      assert.isNotOk(element.textarea, 'textarea is not visible');
       assert.isFalse(isVisible(element.$$('.collapsedContent')),
           'header middle content is is not visible');
     });
@@ -201,50 +197,50 @@
 
         test('esc closes comment when text is empty', () => {
           MockInteractions.pressAndReleaseKeyOn(
-              element.$.editTextarea, 27); // esc
+              element.textarea, 27); // esc
           assert.isTrue(element._handleCancel.called);
         });
 
         test('ctrl+enter does not save', () => {
           MockInteractions.pressAndReleaseKeyOn(
-              element.$.editTextarea, 13, 'ctrl'); // ctrl + enter
+              element.textarea, 13, 'ctrl'); // ctrl + enter
           assert.isFalse(element._handleSave.called);
         });
 
         test('meta+enter does not save', () => {
           MockInteractions.pressAndReleaseKeyOn(
-              element.$.editTextarea, 13, 'meta'); // meta + enter
+              element.textarea, 13, 'meta'); // meta + enter
           assert.isFalse(element._handleSave.called);
         });
 
         test('ctrl+s does not save', () => {
           MockInteractions.pressAndReleaseKeyOn(
-              element.$.editTextarea, 83, 'ctrl'); // ctrl + s
+              element.textarea, 83, 'ctrl'); // ctrl + s
           assert.isFalse(element._handleSave.called);
         });
       });
 
       test('esc does not close comment that has content', () => {
         MockInteractions.pressAndReleaseKeyOn(
-            element.$.editTextarea, 27); // esc
+            element.textarea, 27); // esc
         assert.isFalse(element._handleCancel.called);
       });
 
       test('ctrl+enter saves', () => {
         MockInteractions.pressAndReleaseKeyOn(
-            element.$.editTextarea, 13, 'ctrl'); // ctrl + enter
+            element.textarea, 13, 'ctrl'); // ctrl + enter
         assert.isTrue(element._handleSave.called);
       });
 
       test('meta+enter saves', () => {
         MockInteractions.pressAndReleaseKeyOn(
-            element.$.editTextarea, 13, 'meta'); // meta + enter
+            element.textarea, 13, 'meta'); // meta + enter
         assert.isTrue(element._handleSave.called);
       });
 
       test('ctrl+s saves', () => {
         MockInteractions.pressAndReleaseKeyOn(
-            element.$.editTextarea, 83, 'ctrl'); // ctrl + s
+            element.textarea, 83, 'ctrl'); // ctrl + s
         assert.isTrue(element._handleSave.called);
       });
     });
@@ -264,7 +260,7 @@
     test('delete comment', done => {
       sandbox.stub(
           element.$.restAPI, 'deleteComment').returns(Promise.resolve({}));
-      sandbox.spy(element.$.confirmDeleteOverlay, 'open');
+      sandbox.spy(element.confirmDeleteOverlay, 'open');
       element.changeNum = 42;
       element.patchNum = 0xDEADBEEF;
       element._isAdmin = true;
@@ -272,8 +268,10 @@
           .classList.contains('showDeleteButtons'));
       MockInteractions.tap(element.$$('.action.delete'));
       flush(() => {
-        element.$.confirmDeleteOverlay.open.lastCall.returnValue.then(() => {
-          element.$.confirmDeleteComment.message = 'removal reason';
+        element.confirmDeleteOverlay.open.lastCall.returnValue.then(() => {
+          const dialog =
+              this.confirmDeleteOverlay.querySelector('#confirmDeleteComment');
+          dialog.message = 'removal reason';
           element._handleConfirmDeleteComment();
           assert.isTrue(element.$.restAPI.deleteComment.calledWith(
               42, 0xDEADBEEF, 'baf0414d_60047215', 'removal reason'));
@@ -351,6 +349,7 @@
       assert.isTrue(element.$$('.robotActions').hasAttribute('hidden'));
 
       element.editing = true;
+      flushAsynchronousOperations();
       assert.isFalse(isVisible(element.$$('.edit')), 'edit is not visible');
       assert.isFalse(isVisible(element.$$('.discard')), 'discard not visible');
       assert.isTrue(isVisible(element.$$('.save')), 'save is visible');
@@ -361,6 +360,7 @@
 
       element.draft = false;
       element.editing = false;
+      flushAsynchronousOperations();
       assert.isFalse(isVisible(element.$$('.edit')), 'edit is not visible');
       assert.isFalse(isVisible(element.$$('.discard')),
           'discard is not visible');
@@ -372,6 +372,7 @@
       element.comment.id = 'foo';
       element.draft = true;
       element.editing = true;
+      flushAsynchronousOperations();
       assert.isTrue(isVisible(element.$$('.cancel')), 'cancel is visible');
       assert.isFalse(element.$$('.humanActions').hasAttribute('hidden'));
       assert.isTrue(element.$$('.robotActions').hasAttribute('hidden'));
@@ -411,8 +412,7 @@
           'gr-formatted-text is not visible');
       assert.isFalse(isVisible(element.$$('.actions')),
           'actions are not visible');
-      assert.isFalse(isVisible(element.$$('gr-textarea')),
-          'textarea is not visible');
+      assert.isNotOk(element.textarea, 'textarea is not visible');
       assert.isTrue(isVisible(element.$$('.collapsedContent')),
           'header middle content is visible');
 
@@ -422,21 +422,20 @@
           'gr-formatted-text is visible');
       assert.isTrue(isVisible(element.$$('.actions')),
           'actions are visible');
-      assert.isFalse(isVisible(element.$$('gr-textarea')),
-          'textarea is not visible');
+      assert.isNotOk(element.textarea, 'textarea is not visible');
       assert.isFalse(isVisible(element.$$('.collapsedContent')),
           'header middle content is is not visible');
 
       // When the edit button is pressed, should still see the actions
       // and also textarea
       MockInteractions.tap(element.$$('.edit'));
+      flushAsynchronousOperations();
       assert.isFalse(element.collapsed);
       assert.isFalse(isVisible(element.$$('gr-formatted-text')),
           'gr-formatted-text is not visible');
       assert.isTrue(isVisible(element.$$('.actions')),
           'actions are visible');
-      assert.isTrue(isVisible(element.$$('gr-textarea')),
-          'textarea is visible');
+      assert.isTrue(isVisible(element.textarea), 'textarea is visible');
       assert.isFalse(isVisible(element.$$('.collapsedContent')),
           'header middle content is not visible');
 
@@ -460,8 +459,7 @@
           'gr-formatted-text is not visible');
       assert.isTrue(isVisible(element.$$('.actions')),
           'actions are visible');
-      assert.isTrue(isVisible(element.$$('gr-textarea')),
-          'textarea is visible');
+      assert.isTrue(isVisible(element.textarea), 'textarea is visible');
       assert.isFalse(isVisible(element.$$('.collapsedContent')),
           'header middle content is not visible');
     });
@@ -496,7 +494,8 @@
       MockInteractions.tap(element.$$('.cancel'));
       element.flushDebouncer('fire-update');
       element._messageText = '';
-      MockInteractions.pressAndReleaseKeyOn(element.$.editTextarea, 27); // esc
+      flushAsynchronousOperations();
+      MockInteractions.pressAndReleaseKeyOn(element.textarea, 27); // esc
     });
 
     test('draft discard removes message from storage', done => {
@@ -562,7 +561,7 @@
       test('confirms discard of comments with message text', () => {
         element._messageText = 'test';
         element._handleDiscard(mockEvent);
-        assert.isTrue(overlayStub.calledWith(element.$.confirmDiscardOverlay));
+        assert.isTrue(overlayStub.calledWith(element.confirmDiscardOverlay));
         assert.isFalse(discardStub.called);
       });
 
@@ -581,8 +580,10 @@
         done();
       });
       element._messageText = 'is that the horse from horsing around??';
+      element.editing = true;
+      flushAsynchronousOperations();
       MockInteractions.pressAndReleaseKeyOn(
-          element.$.editTextarea.$.textarea.textarea,
+          element.textarea.$.textarea.textarea,
           83, 'ctrl'); // 'ctrl + s'
     });
 
diff --git a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.js b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.js
index f34328e..c771332 100644
--- a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.js
+++ b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.js
@@ -132,9 +132,9 @@
     // then hides the text box and submit button.
     _computeHideAgreementClass(name, config) {
       for (const key in config) {
-        if (!config.hasOwnProperty(key)) { return; }
+        if (!config.hasOwnProperty(key)) { continue; }
         for (const prop in config[key]) {
-          if (!config[key].hasOwnProperty(prop)) { return; }
+          if (!config[key].hasOwnProperty(prop)) { continue; }
           if (name === config[key].name &&
               !config[key].auto_verify_group) {
             return 'hideAgreementsTextBox';
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html
index ca9c2baa..7f6cded 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html
@@ -93,7 +93,7 @@
         link="[[link]]"
         class="dropdown-trigger" id="trigger"
         down-arrow="[[downArrow]]"
-        on-tap="_showDropdownTapHandler">
+        on-tap="_dropdownTriggerTapHandler">
       <content></content>
     </gr-button>
     <iron-dropdown id="dropdown"
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
index 3e05c2b..f8edc83 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
@@ -153,21 +153,21 @@
      * @param {!Event} e
      */
     _handleDropdownTap(e) {
-      // async is needed so that that the click event is fired before the
-      // dropdown closes (This was a bug for touch devices).
-      this.async(() => {
-        this.$.dropdown.close();
-      }, 1);
+      this._close();
     },
 
     /**
      * Hanlde a click on the button to open the dropdown.
      * @param {!Event} e
      */
-    _showDropdownTapHandler(e) {
+    _dropdownTriggerTapHandler(e) {
       e.preventDefault();
       e.stopPropagation();
-      this._open();
+      if (this.$.dropdown.opened) {
+        this._close();
+      } else {
+        this._open();
+      }
     },
 
     /**
@@ -180,6 +180,14 @@
       this.$.cursor.target.focus();
     },
 
+    _close() {
+      // async is needed so that that the click event is fired before the
+      // dropdown closes (This was a bug for touch devices).
+      this.async(() => {
+        this.$.dropdown.close();
+      }, 1);
+    },
+
     /**
      * Get the class for a top-content item based on the given boolean.
      * @param {boolean} bold Whether the item is bold.
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html
index adb89d8..d4d21b0 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html
@@ -49,11 +49,14 @@
       sandbox.restore();
     });
 
-    test('tap on trigger opens menu', () => {
+    test('tap on trigger opens menu, then closes', () => {
       sandbox.stub(element, '_open', () => { element.$.dropdown.open(); });
+      sandbox.stub(element, '_close', () => { element.$.dropdown.close(); });
       assert.isFalse(element.$.dropdown.opened);
       MockInteractions.tap(element.$.trigger);
       assert.isTrue(element.$.dropdown.opened);
+      MockInteractions.tap(element.$.trigger);
+      assert.isFalse(element.$.dropdown.opened);
     });
 
     test('_computeURLHelper', () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index 2c061f6..bb9b627 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -189,6 +189,7 @@
 
       const params = [];
       for (const p in opt_params) {
+        if (!opt_params.hasOwnProperty(p)) { continue; }
         if (opt_params[p] == null) {
           params.push(encodeURIComponent(p));
           continue;
diff --git a/tools/js/bower2bazel.py b/tools/js/bower2bazel.py
index ccdf2df..171ab55 100755
--- a/tools/js/bower2bazel.py
+++ b/tools/js/bower2bazel.py
@@ -118,10 +118,15 @@
     json.dump(bower_json, f, indent=2)
   return ret
 
+def decode(input):
+  try:
+    return input.decode("utf-8")
+  except TypeError:
+    return input
 
 def bower_command(args):
   base = subprocess.check_output(["bazel", "info", "output_base"]).strip()
-  exp = os.path.join(base, "external", "bower", "*npm_binary.tgz")
+  exp = os.path.join(decode(base), "external", "bower", "*npm_binary.tgz")
   fs = sorted(glob.glob(exp))
   assert len(fs) == 1, "bower tarball not found or have multiple versions %s" % fs
   return ["python", os.getcwd() + "/tools/js/run_npm_binary.py", sorted(fs)[0]] + args
@@ -137,8 +142,8 @@
     "bazel", "query", "kind(bower_component_bundle, //polygerrit-ui/...)"])
   seed_str = subprocess.check_output([
     "bazel", "query", "attr(seed, 1, kind(bower_component, deps(//polygerrit-ui/...)))"])
-  targets = [s for s in target_str.split('\n') if s]
-  seeds = [s for s in seed_str.split('\n') if s]
+  targets = [s for s in decode(target_str).split('\n') if s]
+  seeds = [s for s in decode(seed_str).split('\n') if s]
   prefix = "//lib/js:"
   non_seeds = [s for s in seeds if not s.startswith(prefix)]
   assert not non_seeds, non_seeds
@@ -223,7 +228,7 @@
   out = subprocess.check_output(["find", "bower_components/", "-name", ".bower.json"])
 
   data = []
-  for f in sorted(out.split('\n')):
+  for f in sorted(decode(out).split('\n')):
     if not f:
       continue
     pkg = json.load(open(f))