Merge changes from topic "test-project-update-1"

* changes:
  TestProjectUpdate: Support granting global capabilities
  TestProjectUpdate: Support setting label permissions
  AbstractDaemonTest: Use ProjectOperations for block/deny/grant
  Introduce TestProjectUpdate to update project config
  Add ConfigSubject over JGit Configs
  ProjectOperations: Add methods for reading (Project)Config
  AbstractDaemonTest: Temporarily make projectOperations protected
diff --git a/javatests/com/google/gerrit/acceptance/ssh/QueryIT.java b/javatests/com/google/gerrit/acceptance/ssh/QueryIT.java
index 0f47a4a..78960bb 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/QueryIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/QueryIT.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.acceptance.ssh;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 
 import com.google.common.collect.Lists;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -294,8 +295,8 @@
     // computation while formatting the output, such as labels, reviewers etc.
     merge(r);
     for (ListChangesOption option : ListChangesOption.values()) {
-      assertThat(gApi.changes().query(r.getChangeId()).withOption(option).get())
-          .named("Option: " + option)
+      assertWithMessage("Option: " + option)
+          .that(gApi.changes().query(r.getChangeId()).withOption(option).get())
           .hasSize(1);
     }
   }
diff --git a/javatests/com/google/gerrit/metrics/proc/ProcMetricModuleTest.java b/javatests/com/google/gerrit/metrics/proc/ProcMetricModuleTest.java
index cd2e449..db75cd8 100644
--- a/javatests/com/google/gerrit/metrics/proc/ProcMetricModuleTest.java
+++ b/javatests/com/google/gerrit/metrics/proc/ProcMetricModuleTest.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.metrics.proc;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
 
 import com.codahale.metrics.Counter;
@@ -166,8 +167,8 @@
 
   private <M extends Metric> M get(String name, Class<M> type) {
     Metric m = registry.getMetrics().get(name);
-    assertThat(m).named(name).isNotNull();
-    assertThat(m).named(name).isInstanceOf(type);
+    assertWithMessage(name).that(m).isNotNull();
+    assertWithMessage(name).that(m).isInstanceOf(type);
 
     @SuppressWarnings("unchecked")
     M result = (M) m;
diff --git a/lib/jgit/jgit.bzl b/lib/jgit/jgit.bzl
index 6828807..b5c4dcc 100644
--- a/lib/jgit/jgit.bzl
+++ b/lib/jgit/jgit.bzl
@@ -1,6 +1,6 @@
 load("//tools/bzl:maven_jar.bzl", "MAVEN_CENTRAL", "maven_jar")
 
-_JGIT_VERS = "5.2.1.201812262042-r"
+_JGIT_VERS = "5.3.1.201904271842-r"
 
 _DOC_VERS = _JGIT_VERS  # Set to _JGIT_VERS unless using a snapshot
 
@@ -40,28 +40,28 @@
         name = "jgit-lib",
         artifact = "org.eclipse.jgit:org.eclipse.jgit:" + _JGIT_VERS,
         repository = _JGIT_REPO,
-        sha1 = "34914e63e1463e40ba40e2e28b0392993ea3b938",
-        src_sha1 = "b1c9e2ae01dd31ab4957de54756ec11acc99bb30",
+        sha1 = "dba85014483315fa426259bc1b8ccda9373a624b",
+        src_sha1 = "b2ddc76c39d81df716948a00d26faa35e11a0ddf",
         unsign = True,
     )
     maven_jar(
         name = "jgit-servlet",
         artifact = "org.eclipse.jgit:org.eclipse.jgit.http.server:" + _JGIT_VERS,
         repository = _JGIT_REPO,
-        sha1 = "18c8938c4d8966abed84fc9de6c09aaea8cc8d87",
+        sha1 = "3287341fca859340a00b51cb5dd3b78b8e532b39",
         unsign = True,
     )
     maven_jar(
         name = "jgit-archive",
         artifact = "org.eclipse.jgit:org.eclipse.jgit.archive:" + _JGIT_VERS,
         repository = _JGIT_REPO,
-        sha1 = "08c945bc664e4efe0d0e9a878f96505076da2ca9",
+        sha1 = "3585027e83fb44a5de2c10ae9ddbf976593bf080",
     )
     maven_jar(
         name = "jgit-junit",
         artifact = "org.eclipse.jgit:org.eclipse.jgit.junit:" + _JGIT_VERS,
         repository = _JGIT_REPO,
-        sha1 = "5a5fb36517cb05ca51cbb1f00a520142dc83f793",
+        sha1 = "3d9ba7e610d6ab5d08dcb1e4ba448b592a34de77",
         unsign = True,
     )
 
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..f096e2a
--- /dev/null
+++ b/package.json
@@ -0,0 +1,25 @@
+{
+  "name": "gerrit",
+  "version": "3.1.0-SNAPSHOT",
+  "description": "Gerrit Code Review",
+  "dependencies": {},
+  "devDependencies": {
+    "eslint": "^5.16.0",
+    "eslint-config-google": "^0.13.0",
+    "eslint-plugin-html": "^5.0.5",
+    "fried-twinkie": "^0.2.2",
+    "typescript": "^2.x.x",
+    "web-component-tester": "^6.5.0"
+  },
+  "scripts": {
+    "test": "WCT_HEADLESS_MODE=1 WCT_ARGS='--verbose -l chrome' ./polygerrit-ui/app/run_test.sh",
+    "eslint": "./node_modules/eslint/bin/eslint.js --ignore-pattern 'bower_components/' --ignore-pattern 'gr-linked-text' --ignore-pattern 'scripts/vendor' --ext .html,.js polygerrit-ui/app || exit 0",
+    "test-template": "./polygerrit-ui/app/run_template_test.sh"
+  },
+  "repository": {
+    "type": "git",
+    "url": "https://gerrit.googlesource.com/gerrit"
+  },
+  "author": "",
+  "license": "Apache-2.0"
+}
diff --git a/plugins/gitiles b/plugins/gitiles
index bf9313f..a58ae0b 160000
--- a/plugins/gitiles
+++ b/plugins/gitiles
@@ -1 +1 @@
-Subproject commit bf9313fc7dd5b913b4a6484799b899fde4ac3449
+Subproject commit a58ae0ba2c23576a68d457e00aaf0902f41e4bb9
diff --git a/plugins/replication b/plugins/replication
index aa07963..e3d669e 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit aa07963def69e4423444a078a662072e09629cec
+Subproject commit e3d669ed12a7771198d7a0442aa9202f76e577b6
diff --git a/polygerrit-ui/README.md b/polygerrit-ui/README.md
index c8c8e4f..e4e2994 100644
--- a/polygerrit-ui/README.md
+++ b/polygerrit-ui/README.md
@@ -31,14 +31,8 @@
 dependencies can be installed with:
 
 ```sh
-sudo npm install -g \
-  eslint \
-  eslint-config-google \
-  eslint-plugin-html \
-  typescript \
-  fried-twinkie \
-  polylint \
-  web-component-tester
+npm install
+sudo npm install -g polylint
 ```
 
 It may complain about a missing `typescript@2.3.4` peer dependency, which is
@@ -52,7 +46,7 @@
 simply execute:
 
 ```sh
-./run-server.sh
+./polygerrit-ui/run-server.sh
 ```
 
 Then visit <http://localhost:8081>.
@@ -65,7 +59,7 @@
 the command line:
 
 ```sh
-./run-server.sh --plugins=plugins/my_plugin/static/my_plugin.js,plugins/my_plugin/static/my_plugin.html
+./polygerrit-ui/run-server.sh --plugins=plugins/my_plugin/static/my_plugin.js,plugins/my_plugin/static/my_plugin.html
 ```
 
 The biggest draw back of this method is that you cannot log in, so cannot test
@@ -115,10 +109,10 @@
 changes are picked up on "reload".
 
 Our CI integration ensures that all tests are run when you upload a change to
-Gerrit, but you can also run all tests locally:
+Gerrit, but you can also run all tests locally in headless mode:
 
 ```sh
-./polygerrit-ui/app/run_test.sh
+npm test
 ```
 
 To allow the tests to run in Safari:
@@ -126,17 +120,10 @@
 * In the Advanced preferences tab, check "Show Develop menu in menu bar".
 * In the Develop menu, enable the "Allow Remote Automation" option.
 
-If you need to pass additional arguments to `wct`:
-
-```sh
-WCT_ARGS='-p --some-flag="foo bar"' ./polygerrit-ui/app/run_test.sh
-```
-
-
 To run Chrome tests in headless mode:
 
 ```sh
-WCT_HEADLESS_MODE=1 ./polygerrit-ui/app/run_test.sh
+WCT_HEADLESS_MODE=1 WCT_ARGS='--verbose -l chrome' ./polygerrit-ui/app/run_test.sh
 ```
 
 ## Style guide
@@ -162,11 +149,22 @@
 Some useful commands:
 
 * To run ESLint on the whole app, less some dependency code:
-`eslint --ignore-pattern 'bower_components/' --ignore-pattern 'gr-linked-text' --ignore-pattern 'scripts/vendor' --ext .html,.js polygerrit-ui/app`
+
+```sh
+npm run eslint
+```
+
 * To run ESLint on just the subdirectory you modified:
-`eslint --ext .html,.js polygerrit-ui/app/$YOUR_DIR_HERE`
+
+```sh
+node_modules/eslint/bin/eslint.js --ext .html,.js polygerrit-ui/app/$YOUR_DIR_HERE
+```
+
 * To run the linter on all of your local changes:
-`git diff --name-only master | xargs eslint --ext .html,.js`
+
+```sh
+git diff --name-only master | xargs node_modules/eslint/bin/eslint.js --ext .html,.js
+```
 
 We also use the `polylint` tool to lint use of Polymer. To install polylint,
 execute the following command.
@@ -194,6 +192,12 @@
 ./polygerrit-ui/app/run_template_test.sh
 ```
 
+or
+
+```sh
+npm run test-template
+```
+
 To run on a specific top level directory (ex: change-list)
 ```sh
 TEMPLATE_NO_DEFAULT=true ./polygerrit-ui/app/run_template_test.sh //polygerrit-ui/app:template_test_change-list
diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.js b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.js
index 6adf127..41ce201 100644
--- a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.js
+++ b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.js
@@ -95,7 +95,8 @@
         // For a new section, this is not fired because new permissions and
         // rules have to be added in order to save, modifying the ref is not
         // enough.
-        this.dispatchEvent(new CustomEvent('access-modified', {bubbles: true}));
+        this.dispatchEvent(new CustomEvent(
+            'access-modified', {bubbles: true, composed: true}));
       }
       this.section.value.updatedId = this.section.id;
     },
@@ -199,12 +200,13 @@
 
     _handleRemoveReference() {
       if (this.section.value.added) {
-        this.dispatchEvent(new CustomEvent('added-section-removed',
-            {bubbles: true}));
+        this.dispatchEvent(new CustomEvent(
+            'added-section-removed', {bubbles: true, composed: true}));
       }
       this._deleted = true;
       this.section.value.deleted = true;
-      this.dispatchEvent(new CustomEvent('access-modified', {bubbles: true}));
+      this.dispatchEvent(
+          new CustomEvent('access-modified', {bubbles: true, composed: true}));
     },
 
     _handleUndoRemove() {
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js
index 76bd558..8a262b8 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js
@@ -205,6 +205,7 @@
               this.dispatchEvent(new CustomEvent('show-alert', {
                 detail: {message: SAVING_ERROR_TEXT},
                 bubbles: true,
+                composed: true,
               }));
               return err;
             }
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js
index a131f4f..1c408df 100644
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js
@@ -137,17 +137,19 @@
     _handleValueChange() {
       this.permission.value.modified = true;
       // Allows overall access page to know a change has been made.
-      this.dispatchEvent(new CustomEvent('access-modified', {bubbles: true}));
+      this.dispatchEvent(
+          new CustomEvent('access-modified', {bubbles: true, composed: true}));
     },
 
     _handleRemovePermission() {
       if (this.permission.value.added) {
-        this.dispatchEvent(new CustomEvent('added-permission-removed',
-            {bubbles: true}));
+        this.dispatchEvent(new CustomEvent(
+            'added-permission-removed', {bubbles: true, composed: true}));
       }
       this._deleted = true;
       this.permission.value.deleted = true;
-      this.dispatchEvent(new CustomEvent('access-modified', {bubbles: true}));
+      this.dispatchEvent(
+          new CustomEvent('access-modified', {bubbles: true, composed: true}));
     },
 
     _handleRulesChanged(changeRecord) {
@@ -273,7 +275,8 @@
       const value = this._rules[this._rules.length - 1].value;
       value.added = true;
       this.set(['permission', 'value', 'rules', groupId], value);
-      this.dispatchEvent(new CustomEvent('access-modified', {bubbles: true}));
+      this.dispatchEvent(
+          new CustomEvent('access-modified', {bubbles: true, composed: true}));
     },
 
     _computeHasRange(name) {
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 713bf02..7d2890a 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
@@ -406,8 +406,11 @@
       if (!Object.keys(addRemoveObj.add).length &&
           !Object.keys(addRemoveObj.remove).length &&
           !addRemoveObj.parent) {
-        this.dispatchEvent(new CustomEvent('show-alert',
-            {detail: {message: NOTHING_TO_SAVE}, bubbles: true}));
+        this.dispatchEvent(new CustomEvent('show-alert', {
+          detail: {message: NOTHING_TO_SAVE},
+          bubbles: true,
+          composed: true,
+        }));
         return;
       }
       const obj = {
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command.js b/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command.js
index e0becaf..04d9781 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command.js
@@ -34,7 +34,8 @@
      */
 
     _onCommandTap() {
-      this.dispatchEvent(new CustomEvent('command-tap', {bubbles: true}));
+      this.dispatchEvent(
+          new CustomEvent('command-tap', {bubbles: true, composed: true}));
     },
   });
 })();
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.js b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.js
index 92b11fc..169672a 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.js
@@ -75,8 +75,9 @@
     _handleRunningGC() {
       return this.$.restAPI.runRepoGC(this.repo).then(response => {
         if (response.status === 200) {
-          this.dispatchEvent(new CustomEvent('show-alert',
-              {detail: {message: GC_MESSAGE}, bubbles: true}));
+          this.dispatchEvent(new CustomEvent(
+              'show-alert',
+              {detail: {message: GC_MESSAGE}, bubbles: true, composed: true}));
         }
       });
     },
@@ -100,8 +101,9 @@
             const message = change ?
                 CREATE_CHANGE_SUCCEEDED_MESSAGE :
                 CREATE_CHANGE_FAILED_MESSAGE;
-            this.dispatchEvent(new CustomEvent('show-alert',
-                {detail: {message}, bubbles: true}));
+            this.dispatchEvent(new CustomEvent(
+                'show-alert',
+                {detail: {message}, bubbles: true, composed: true}));
             if (!change) { return; }
 
             Gerrit.Nav.navigateToRelativeUrl(Gerrit.Nav.getEditUrlForDiff(
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.js b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.js
index 6d7677e..883a4e1 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.js
@@ -123,8 +123,8 @@
         notifyPath: `${name}.${notifyPath}`,
       };
 
-      this.dispatchEvent(new CustomEvent(this.PLUGIN_CONFIG_CHANGED,
-          {detail, bubbles: true}));
+      this.dispatchEvent(new CustomEvent(
+          this.PLUGIN_CONFIG_CHANGED, {detail, bubbles: true, composed: true}));
     },
   });
 })();
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_test.html b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_test.html
index 2af2043..37f43f4 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_test.html
@@ -151,7 +151,8 @@
         const select = element.$$('select');
         assert.ok(select);
         select.value = 'newTest';
-        select.dispatchEvent(new Event('change', {bubbles: true}));
+        select.dispatchEvent(new Event(
+            'change', {bubbles: true, composed: true}));
         flushAsynchronousOperations();
 
         assert.isTrue(buildStub.called);
diff --git a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js
index a45b391..4102747 100644
--- a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js
+++ b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js
@@ -210,12 +210,13 @@
 
     _handleRemoveRule() {
       if (this.rule.value.added) {
-        this.dispatchEvent(new CustomEvent('added-rule-removed',
-            {bubbles: true}));
+        this.dispatchEvent(new CustomEvent(
+            'added-rule-removed', {bubbles: true, composed: true}));
       }
       this._deleted = true;
       this.rule.value.deleted = true;
-      this.dispatchEvent(new CustomEvent('access-modified', {bubbles: true}));
+      this.dispatchEvent(
+          new CustomEvent('access-modified', {bubbles: true, composed: true}));
     },
 
     _handleUndoRemove() {
@@ -237,7 +238,8 @@
       if (!this._originalRuleValues) { return; }
       this.rule.value.modified = true;
       // Allows overall access page to know a change has been made.
-      this.dispatchEvent(new CustomEvent('access-modified', {bubbles: true}));
+      this.dispatchEvent(
+          new CustomEvent('access-modified', {bubbles: true, composed: true}));
     },
 
     _setOriginalRuleValues(value) {
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
index ecc7532..5d15d60 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
@@ -214,6 +214,7 @@
       this.set('change.reviewed', newVal);
       this.dispatchEvent(new CustomEvent('toggle-reviewed', {
         bubbles: true,
+        composed: true,
         detail: {change: this.change, reviewed: newVal},
       }));
     },
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.js b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.js
index 42d7bd7..b9df583b 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.js
+++ b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.js
@@ -29,7 +29,8 @@
 
     _handleCreateTap(e) {
       e.preventDefault();
-      this.dispatchEvent(new CustomEvent('create-tap', {bubbles: true}));
+      this.dispatchEvent(
+          new CustomEvent('create-tap', {bubbles: true, composed: true}));
     },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.js b/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.js
index fc0b633..147d1f2 100644
--- a/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.js
+++ b/polygerrit-ui/app/elements/change/gr-account-entry/gr-account-entry.js
@@ -118,8 +118,8 @@
 
     _inputTextChanged(text) {
       if (text.length && this.allowAnyInput) {
-        this.dispatchEvent(new CustomEvent('account-text-changed',
-            {bubbles: true}));
+        this.dispatchEvent(new CustomEvent(
+            'account-text-changed', {bubbles: true, composed: true}));
       }
     },
 
diff --git a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.js b/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.js
index 533ae09..c10f3d5 100644
--- a/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.js
+++ b/polygerrit-ui/app/elements/change/gr-account-list/gr-account-list.js
@@ -123,8 +123,11 @@
           // Repopulate the input with what the user tried to enter and have
           // a toast tell them why they can't enter it.
           this.$.entry.setText(reviewer);
-          this.dispatchEvent(new CustomEvent('show-alert',
-            {detail: {message: VALID_EMAIL_ALERT}, bubbles: true}));
+          this.dispatchEvent(new CustomEvent('show-alert', {
+            detail: {message: VALID_EMAIL_ALERT},
+            bubbles: true,
+            composed: true,
+          }));
           return false;
         } else {
           const account = {email: reviewer, _pendingAdd: true};
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
index 8ca8146..60f0dc2 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
@@ -221,8 +221,8 @@
             this._settingTopic = false;
             this.set(['change', 'topic'], newTopic);
             if (newTopic !== lastTopic) {
-              this.dispatchEvent(
-                  new CustomEvent('topic-changed', {bubbles: true}));
+              this.dispatchEvent(new CustomEvent(
+                  'topic-changed', {bubbles: true, composed: true}));
             }
           });
     },
@@ -246,8 +246,8 @@
           this.change._number, {add: [newHashtag]}).then(newHashtag => {
             this.set(['change', 'hashtags'], newHashtag);
             if (newHashtag !== lastHashtag) {
-              this.dispatchEvent(
-                  new CustomEvent('hashtag-changed', {bubbles: true}));
+              this.dispatchEvent(new CustomEvent(
+                  'hashtag-changed', {bubbles: true, composed: true}));
             }
           });
     },
@@ -378,7 +378,7 @@
         target.disabled = false;
         this.set(['change', 'topic'], '');
         this.dispatchEvent(
-            new CustomEvent('topic-changed', {bubbles: true}));
+            new CustomEvent('topic-changed', {bubbles: true, composed: true}));
       }).catch(err => {
         target.disabled = false;
         return;
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 a5adba7..89d799b 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
@@ -1599,32 +1599,44 @@
       sandbox.stub(Gerrit.Nav, 'navigateToRelativeUrl');
 
       // Delete
-      fileList.dispatchEvent(new CustomEvent('file-action-tap',
-          {detail: {action: Actions.DELETE.id, path: 'foo'}, bubbles: true}));
+      fileList.dispatchEvent(new CustomEvent('file-action-tap', {
+        detail: {action: Actions.DELETE.id, path: 'foo'},
+        bubbles: true,
+        composed: true,
+      }));
       flushAsynchronousOperations();
 
       assert.isTrue(controls.openDeleteDialog.called);
       assert.equal(controls.openDeleteDialog.lastCall.args[0], 'foo');
 
       // Restore
-      fileList.dispatchEvent(new CustomEvent('file-action-tap',
-          {detail: {action: Actions.RESTORE.id, path: 'foo'}, bubbles: true}));
+      fileList.dispatchEvent(new CustomEvent('file-action-tap', {
+        detail: {action: Actions.RESTORE.id, path: 'foo'},
+        bubbles: true,
+        composed: true,
+      }));
       flushAsynchronousOperations();
 
       assert.isTrue(controls.openRestoreDialog.called);
       assert.equal(controls.openRestoreDialog.lastCall.args[0], 'foo');
 
       // Rename
-      fileList.dispatchEvent(new CustomEvent('file-action-tap',
-          {detail: {action: Actions.RENAME.id, path: 'foo'}, bubbles: true}));
+      fileList.dispatchEvent(new CustomEvent('file-action-tap', {
+        detail: {action: Actions.RENAME.id, path: 'foo'},
+        bubbles: true,
+        composed: true,
+      }));
       flushAsynchronousOperations();
 
       assert.isTrue(controls.openRenameDialog.called);
       assert.equal(controls.openRenameDialog.lastCall.args[0], 'foo');
 
       // Open
-      fileList.dispatchEvent(new CustomEvent('file-action-tap',
-          {detail: {action: Actions.OPEN.id, path: 'foo'}, bubbles: true}));
+      fileList.dispatchEvent(new CustomEvent('file-action-tap', {
+        detail: {action: Actions.OPEN.id, path: 'foo'},
+        bubbles: true,
+        composed: true,
+      }));
       flushAsynchronousOperations();
 
       assert.isTrue(Gerrit.Nav.getEditUrlForDiff.called);
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js
index d9b961f..fe28a1d 100644
--- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js
+++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js
@@ -133,7 +133,8 @@
       const name = e.target.selectedItem.name;
       const value = e.target.selectedItem.getAttribute('value');
       this.dispatchEvent(new CustomEvent(
-        'labels-changed', {detail: {name, value}, bubbles: true}));
+          'labels-changed',
+          {detail: {name, value}, bubbles: true, composed: true}));
     },
 
     _computeAnyPermittedLabelValues(permittedLabels, label) {
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.js b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
index 837ffe0..a2ec28c 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.js
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
@@ -228,6 +228,7 @@
       e.preventDefault();
       this.dispatchEvent(new CustomEvent('message-anchor-tap', {
         bubbles: true,
+        composed: true,
         detail: {id: this.message.id},
       }));
     },
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html
index eb06b0f..7101249 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html
@@ -150,7 +150,8 @@
           flush(() => {
             const textarea = element.$.textarea.getNativeTextarea();
             textarea.value = 'LGTM';
-            textarea.dispatchEvent(new CustomEvent('input', {bubbles: true}));
+            textarea.dispatchEvent(new CustomEvent(
+                'input', {bubbles: true, composed: true}));
             const labelScoreRows = Polymer.dom(element.$.labelScores.root)
                 .querySelector('gr-label-score-row[name="Code-Review"]');
             const selectedBtn = Polymer.dom(labelScoreRows.root)
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
index 84c8cee..f775e1c 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
@@ -749,6 +749,7 @@
       if (this._sendDisabled) {
         this.dispatchEvent(new CustomEvent('show-alert', {
           bubbles: true,
+          composed: true,
           detail: {message: EMPTY_REPLY_MESSAGE},
         }));
         return;
@@ -756,9 +757,11 @@
       return this.send(this._includeComments, this.canBeStarted)
           .then(keepReviewers => {
             this._purgeReviewersPendingRemove(false, keepReviewers);
-          }).catch(err => {
+          })
+          .catch(err => {
             this.dispatchEvent(new CustomEvent('show-error', {
               bubbles: true,
+              composed: true,
               detail: {message: `Error submitting review ${err}`},
             }));
           });
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
index f9108c7..aec3491 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
@@ -415,7 +415,7 @@
       assert.isTrue(element.$$('#ccs').allowAnyInput);
       assert.isFalse(element.$$('#reviewers').allowAnyInput);
       element.$$('#ccs').dispatchEvent(new CustomEvent('account-text-changed',
-          {bubbles: true}));
+          {bubbles: true, composed: true}));
       assert.isTrue(element._reviewersMutated);
     });
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
index 7bfec00..42fb567 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.html
@@ -179,7 +179,8 @@
 
           const isBinary = !!(this.isImageDiff || this.diff.binary);
 
-          this.dispatchEvent(new CustomEvent('render-start', {bubbles: true}));
+          this.dispatchEvent(new CustomEvent(
+              'render-start', {bubbles: true, composed: true}));
           this._cancelableRenderPromise = util.makeCancelable(
               this.$.processor.process(this.diff.content, isBinary)
                   .then(() => {
@@ -187,7 +188,7 @@
                       this._builder.renderDiff();
                     }
                     this.dispatchEvent(new CustomEvent('render-content',
-                        {bubbles: true}));
+                        {bubbles: true, composed: true}));
 
                     if (this._diffTooLargeForSyntax()) {
                       this.$.syntaxLayer.enabled = false;
@@ -196,8 +197,8 @@
                     return this.$.syntaxLayer.process();
                   })
                   .then(() => {
-                    this.dispatchEvent(
-                        new CustomEvent('render-syntax', {bubbles: true}));
+                    this.dispatchEvent(new CustomEvent(
+                        'render-syntax', {bubbles: true, composed: true}));
                   }));
           return this._cancelableRenderPromise
               .finally(() => { this._cancelableRenderPromise = null; })
@@ -313,7 +314,7 @@
           this.dispatchEvent(new CustomEvent('show-alert', {
             detail: {
               message,
-            }, bubbles: true}));
+            }, bubbles: true, composed: true}));
           throw Error(`Invalid preference value: ${pref}`);
         },
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
index 9ec2b29..191d1d2 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
@@ -179,8 +179,8 @@
         element.commentRanges = [{side: 'right'}];
 
         sandbox.stub(element, 'set');
-        threadEl.dispatchEvent(
-            new CustomEvent('comment-thread-mouseenter', {bubbles: true}));
+        threadEl.dispatchEvent(new CustomEvent(
+            'comment-thread-mouseenter', {bubbles: true, composed: true}));
         assert.isFalse(element.set.called);
       });
 
@@ -204,8 +204,8 @@
         }}];
 
         sandbox.stub(element, 'set');
-        threadEl.dispatchEvent(
-            new CustomEvent('comment-thread-mouseenter', {bubbles: true}));
+        threadEl.dispatchEvent(new CustomEvent(
+            'comment-thread-mouseenter', {bubbles: true, composed: true}));
         assert.isTrue(element.set.called);
         const args = element.set.lastCall.args;
         assert.deepEqual(args[0], ['commentRanges', 0, 'hovering']);
@@ -221,8 +221,8 @@
         element.commentRanges = [{side: 'right'}];
 
         sandbox.stub(element, 'set');
-        threadEl.dispatchEvent(
-            new CustomEvent('comment-thread-mouseleave', {bubbles: true}));
+        threadEl.dispatchEvent(new CustomEvent(
+            'comment-thread-mouseleave', {bubbles: true, composed: true}));
         assert.isFalse(element.set.called);
       });
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js
index b848ed3..d47a61d 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js
@@ -110,7 +110,9 @@
       commitRange: Object,
       filesWeblinks: {
         type: Object,
-        value() { return {}; },
+        value() {
+          return {};
+        },
         notify: true,
       },
       hidden: {
@@ -309,7 +311,9 @@
     },
 
     _getFilesWeblinks(diff) {
-      if (!this.commitRange) { return {}; }
+      if (!this.commitRange) {
+        return {};
+      }
       return {
         meta_a: Gerrit.Nav.getFileWebLinks(
             this.projectName, this.commitRange.baseCommit, this.path,
@@ -435,7 +439,9 @@
      * Report info about the diff response.
      */
     _reportDiff(diff) {
-      if (!diff || !diff.content) { return; }
+      if (!diff || !diff.content) {
+        return;
+      }
 
       // Count the delta lines stemming from normal deltas, and from
       // due_to_rebase deltas.
@@ -663,7 +669,7 @@
      * @param {!Gerrit.Range=} range
      * @return {?Node}
      */
-    _getThreadEl(lineNum, commentSide, range=undefined) {
+    _getThreadEl(lineNum, commentSide, range = undefined) {
       let line;
       if (commentSide === GrDiffBuilder.Side.LEFT) {
         line = {beforeNumber: lineNum};
@@ -778,7 +784,8 @@
       return this.prefs.ignore_whitespace;
     },
 
-    _whitespaceChanged(preferredWhitespaceLevel, loadedWhitespaceLevel,
+    _whitespaceChanged(
+        preferredWhitespaceLevel, loadedWhitespaceLevel,
         noRenderOnPrefsChange) {
       if (preferredWhitespaceLevel !== loadedWhitespaceLevel &&
           !noRenderOnPrefsChange) {
@@ -831,8 +838,8 @@
     },
 
     _handleCommentSaveOrDiscard() {
-      this.dispatchEvent(new CustomEvent('diff-comments-modified',
-          {bubbles: true}));
+      this.dispatchEvent(new CustomEvent(
+          'diff-comments-modified', {bubbles: true, composed: true}));
     },
 
     _removeComment(comment) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html
index 7466ade..d27a0e5 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html
@@ -269,7 +269,7 @@
     suite('render reporting', () => {
       test('starts total and content timer on render-start', done => {
         element.dispatchEvent(
-            new CustomEvent('render-start', {bubbles: true}));
+            new CustomEvent('render-start', {bubbles: true, composed: true}));
         assert.isTrue(element.$.reporting.time.calledWithExactly(
             'Diff Total Render'));
         assert.isTrue(element.$.reporting.time.calledWithExactly(
@@ -279,7 +279,7 @@
 
       test('ends content and starts syntax timer on render-content', done => {
         element.dispatchEvent(
-            new CustomEvent('render-content', {bubbles: true}));
+            new CustomEvent('render-content', {bubbles: true, composed: true}));
         assert.isTrue(element.$.reporting.time.calledWithExactly(
             'Diff Syntax Render'));
         assert.isTrue(element.$.reporting.timeEnd.calledWithExactly(
@@ -289,7 +289,7 @@
 
       test('ends total and syntax timer on render-syntax', done => {
         element.dispatchEvent(
-            new CustomEvent('render-syntax', {bubbles: true}));
+            new CustomEvent('render-syntax', {bubbles: true, composed: true}));
         assert.isTrue(element.$.reporting.timeEnd.calledWithExactly(
             'Diff Total Render'));
         assert.isTrue(element.$.reporting.timeEnd.calledWithExactly(
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
index 40a7abe..d7e193c 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -390,12 +390,12 @@
     _redispatchHoverEvents(addedThreadEls) {
       for (const threadEl of addedThreadEls) {
         threadEl.addEventListener('mouseenter', () => {
-          threadEl.dispatchEvent(
-              new CustomEvent('comment-thread-mouseenter', {bubbles: true}));
+          threadEl.dispatchEvent(new CustomEvent(
+              'comment-thread-mouseenter', {bubbles: true, composed: true}));
         });
         threadEl.addEventListener('mouseleave', () => {
-          threadEl.dispatchEvent(
-              new CustomEvent('comment-thread-mouseleave', {bubbles: true}));
+          threadEl.dispatchEvent(new CustomEvent(
+              'comment-thread-mouseleave', {bubbles: true, composed: true}));
         });
       }
     },
@@ -551,6 +551,7 @@
           this._getIsParentCommentByLineAndContent(lineEl, contentEl);
       this.dispatchEvent(new CustomEvent('create-comment', {
         bubbles: true,
+        composed: true,
         detail: {
           lineNum,
           side,
@@ -727,14 +728,16 @@
     _renderDiffTable() {
       this._unobserveIncrementalNodes();
       if (!this.prefs) {
-        this.dispatchEvent(new CustomEvent('render', {bubbles: true}));
+        this.dispatchEvent(
+            new CustomEvent('render', {bubbles: true, composed: true}));
         return;
       }
       if (this.prefs.context === -1 &&
           this._diffLength >= LARGE_DIFF_THRESHOLD_LINES &&
           this._safetyBypass === null) {
         this._showWarning = true;
-        this.dispatchEvent(new CustomEvent('render', {bubbles: true}));
+        this.dispatchEvent(
+            new CustomEvent('render', {bubbles: true, composed: true}));
         return;
       }
 
@@ -744,7 +747,7 @@
       this.$.diffBuilder.render(keyLocations, this._getBypassPrefs())
           .then(() => {
             this.dispatchEvent(
-                new CustomEvent('render', {bubbles: true}));
+                new CustomEvent('render', {bubbles: true, composed: true}));
           });
     },
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
index 42f098b..762028a 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
@@ -762,7 +762,7 @@
             () => {
               Promise.resolve();
               element.$.diffBuilder.dispatchEvent(
-                  new CustomEvent('render', {bubbles: true}));
+                  new CustomEvent('render', {bubbles: true, composed: true}));
             });
         const mock = document.createElement('mock-diff-response');
         sandbox.stub(element.$.diffBuilder, 'getDiffLength').returns(10000);
diff --git a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.js b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.js
index e28142b..3c1f45f 100644
--- a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.js
+++ b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.js
@@ -188,6 +188,7 @@
               range.end = line.text.length;
               this.dispatchEvent(new CustomEvent('normalize-range', {
                 bubbles: true,
+                composed: true,
                 detail: {lineNum, side},
               }));
             }
diff --git a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.js b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.js
index 01cd9df..bae838e 100644
--- a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.js
+++ b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.js
@@ -32,8 +32,9 @@
     },
 
     _handleTextareaInput(e) {
-      this.dispatchEvent(new CustomEvent('content-change',
-          {detail: {value: e.target.value}, bubbles: true}));
+      this.dispatchEvent(new CustomEvent(
+          'content-change',
+          {detail: {value: e.target.value}, bubbles: true, composed: true}));
     },
   });
 })();
diff --git a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_test.html b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_test.html
index b79cd9d..423c493 100644
--- a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_test.html
+++ b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_test.html
@@ -50,7 +50,7 @@
       element.addEventListener('content-change', contentChangedHandler);
       textarea.value = 'test';
       textarea.dispatchEvent(new CustomEvent('input',
-          {target: textarea, bubbles: true}));
+          {target: textarea, bubbles: true, composed: true}));
     });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.js b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.js
index 9407f18..9c59a9a 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.js
+++ b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.js
@@ -46,8 +46,9 @@
     },
 
     _dispatchFileAction(action, path) {
-      this.dispatchEvent(new CustomEvent('file-action-tap',
-          {detail: {action, path}, bubbles: true}));
+      this.dispatchEvent(new CustomEvent(
+          'file-action-tap',
+          {detail: {action, path}, bubbles: true, composed: true}));
     },
 
     _computeFileActions(actions) {
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js
index 8e108bb..ec6f110 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js
@@ -104,7 +104,9 @@
     },
 
     _paramsChanged(value) {
-      if (value.view !== Gerrit.Nav.View.EDIT) { return; }
+      if (value.view !== Gerrit.Nav.View.EDIT) {
+        return;
+      }
 
       this._changeNum = value.changeNum;
       this._path = value.path;
@@ -134,7 +136,9 @@
 
     _handlePathChanged(e) {
       const path = e.detail;
-      if (path === this._path) { return Promise.resolve(); }
+      if (path === this._path) {
+        return Promise.resolve();
+      }
       return this.$.restAPI.renameFileInChangeEdit(this._changeNum,
           this._path, path).then(res => {
             if (!res.ok) { return; }
@@ -158,8 +162,11 @@
           .then(res => {
             if (storedContent && storedContent.message &&
                 storedContent.message !== res.content) {
-              this.dispatchEvent(new CustomEvent('show-alert',
-                  {detail: {message: RESTORED_MESSAGE}, bubbles: true}));
+              this.dispatchEvent(new CustomEvent('show-alert', {
+                detail: {message: RESTORED_MESSAGE},
+                bubbles: true,
+                composed: true,
+              }));
 
               this._newContent = storedContent.message;
             } else {
@@ -197,11 +204,14 @@
       this.dispatchEvent(new CustomEvent('show-alert', {
         detail: {message},
         bubbles: true,
+        composed: true,
       }));
     },
 
     _computeSaveDisabled(content, newContent, saving) {
-      if (saving) { return true; }
+      if (saving) {
+        return true;
+      }
       return content === newContent;
     },
 
@@ -224,7 +234,9 @@
 
     _handleSaveShortcut(e) {
       e.preventDefault();
-      if (!this._saveDisabled) { this._saveEdit(); }
+      if (!this._saveDisabled) {
+        this._saveEdit();
+      }
     },
   });
 })();
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.html b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.html
index 2f5332d..63f4314 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.html
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.html
@@ -121,7 +121,7 @@
     const storeStub = sandbox.spy(element.$.storage, 'setEditableContentItem');
     element._newContent = 'test';
     element.$.editorEndpoint.dispatchEvent(new CustomEvent('content-change', {
-      bubbles: true,
+      bubbles: true, composed: true,
       detail: {value: 'new content value'},
     }));
     element.flushDebouncer('store');
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 a59b886..62bec12 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
@@ -66,7 +66,9 @@
 
     _getAgreementsUrl(configUrl) {
       let url;
-      if (!configUrl) { return ''; }
+      if (!configUrl) {
+        return '';
+      }
       if (configUrl.startsWith('http:') || configUrl.startsWith('https:')) {
         url = configUrl;
       } else {
@@ -100,8 +102,8 @@
     },
 
     _createToast(message) {
-      this.dispatchEvent(new CustomEvent('show-alert',
-          {detail: {message}, bubbles: true}));
+      this.dispatchEvent(new CustomEvent(
+          'show-alert', {detail: {message}, bubbles: true, composed: true}));
     },
 
     _computeShowAgreementsClass(agreements) {
@@ -133,9 +135,13 @@
     // then hides the text box and submit button.
     _computeHideAgreementClass(name, config) {
       for (const key in config) {
-        if (!config.hasOwnProperty(key)) { continue; }
+        if (!config.hasOwnProperty(key)) {
+          continue;
+        }
         for (const prop in config[key]) {
-          if (!config[key].hasOwnProperty(prop)) { continue; }
+          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/settings/gr-settings-view/gr-settings-view.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
index 1f76a68..d776b54 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
@@ -414,6 +414,7 @@
       this.dispatchEvent(new CustomEvent('show-alert', {
         detail: {message: RELOAD_MESSAGE},
         bubbles: true,
+        composed: true,
       }));
       this.async(() => {
         window.location.reload();
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js
index 44f8c00..98fd94a 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js
+++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js
@@ -49,6 +49,7 @@
       this.set('change.starred', newVal);
       this.dispatchEvent(new CustomEvent('toggle-star', {
         bubbles: true,
+        composed: true,
         detail: {change: this.change, starred: newVal},
       }));
     },
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js
index b5481a9..bf7df71 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js
@@ -128,7 +128,8 @@
 
       _numPendingDraftRequests: {
         type: Object,
-        value: {number: 0}, // Intentional to share the object across instances.
+        value:
+            {number: 0}, // Intentional to share the object across instances.
       },
 
       _enableOverlay: {
@@ -230,7 +231,9 @@
      */
     save(opt_comment) {
       let comment = opt_comment;
-      if (!comment) { comment = this.comment; }
+      if (!comment) {
+        comment = this.comment;
+      }
 
       this.set('comment.message', this._messageText);
       this.editing = false;
@@ -340,7 +343,9 @@
 
     _computeSaveDisabled(draft, comment, resolved) {
       // If resolved state has changed and a msg exists, save should be enabled.
-      if (comment.unresolved === resolved && draft) { return false; }
+      if (comment.unresolved === resolved && draft) {
+        return false;
+      }
       return !draft || draft.trim() === '';
     },
 
@@ -376,7 +381,9 @@
     },
 
     _messageTextChanged(newValue, oldValue) {
-      if (!this.comment || (this.comment && this.comment.id)) { return; }
+      if (!this.comment || (this.comment && this.comment.id)) {
+        return;
+      }
 
       this.debounce('store', () => {
         const message = this._messageText;
@@ -400,9 +407,12 @@
 
     _handleAnchorTap(e) {
       e.preventDefault();
-      if (!this.comment.line) { return; }
+      if (!this.comment.line) {
+        return;
+      }
       this.dispatchEvent(new CustomEvent('comment-anchor-tap', {
         bubbles: true,
+        composed: true,
         detail: {
           number: this.comment.line || FILE,
           side: this.side,
@@ -421,7 +431,9 @@
       e.preventDefault();
 
       // Ignore saves started while already saving.
-      if (this.disabled) { return; }
+      if (this.disabled) {
+        return;
+      }
       const timingLabel = this.comment.id ?
           REPORT_UPDATE_DRAFT : REPORT_CREATE_DRAFT;
       const timer = this.$.reporting.getTimer(timingLabel);
@@ -450,6 +462,7 @@
     _handleFix() {
       this.dispatchEvent(new CustomEvent('create-fix-comment', {
         bubbles: true,
+        composed: true,
         detail: this._getEventPayload(),
       }));
     },
@@ -512,7 +525,9 @@
     },
 
     _getSavingMessage(numPending) {
-      if (numPending === 0) { return SAVED_MESSAGE; }
+      if (numPending === 0) {
+        return SAVED_MESSAGE;
+      }
       return [
         SAVING_MESSAGE,
         numPending,
@@ -544,8 +559,8 @@
         // Note: the event is fired on the body rather than this element because
         // this element may not be attached by the time this executes, in which
         // case the event would not bubble.
-        document.body.dispatchEvent(new CustomEvent('show-alert',
-            {detail: {message}, bubbles: true}));
+        document.body.dispatchEvent(new CustomEvent(
+            'show-alert', {detail: {message}, bubbles: true, composed: true}));
       }, TOAST_DEBOUNCE_INTERVAL);
     },
 
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.js b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.js
index 827bb71..dc945a2 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.js
+++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.js
@@ -96,8 +96,11 @@
             this.$.storage.getEditableContentItem(this.storageKey);
         if (storedContent && storedContent.message) {
           content = storedContent.message;
-          this.dispatchEvent(new CustomEvent('show-alert',
-              {detail: {message: RESTORED_MESSAGE}, bubbles: true}));
+          this.dispatchEvent(new CustomEvent('show-alert', {
+            detail: {message: RESTORED_MESSAGE},
+            bubbles: true,
+            composed: true,
+          }));
         }
       }
       if (!content) {
diff --git a/polygerrit-ui/app/run_template_test.sh b/polygerrit-ui/app/run_template_test.sh
index 4cd6e7f..d2b6989 100755
--- a/polygerrit-ui/app/run_template_test.sh
+++ b/polygerrit-ui/app/run_template_test.sh
@@ -3,7 +3,7 @@
 if [[ -z "${TEMPLATE_NO_DEFAULT}" ]]; then
 bazel test \
       --test_env="HOME=$HOME" \
-      //polygerrit-ui/app:all
+      //polygerrit-ui/app:all \
       --test_tag_filters=template \
       "$@" \
       --test_output errors \
diff --git a/polygerrit-ui/app/run_test.sh b/polygerrit-ui/app/run_test.sh
index df210b8..3d92e11 100755
--- a/polygerrit-ui/app/run_test.sh
+++ b/polygerrit-ui/app/run_test.sh
@@ -6,9 +6,29 @@
     exit 1
 fi
 
+# From https://www.linuxquestions.org/questions/programming-9/bash-script-return-full-path-and-filename-680368/page3.html
+function abs_path {
+  if [[ -d "$1" ]]
+  then
+      pushd "$1" >/dev/null
+      pwd
+      popd >/dev/null
+  elif [[ -e $1 ]]
+  then
+      pushd "$(dirname "$1")" >/dev/null
+      echo "$(pwd)/$(basename "$1")"
+      popd >/dev/null
+  else
+      echo "$1" does not exist! >&2
+      return 127
+  fi
+}
 wct_bin=$(which wct)
 if [[ -z "$wct_bin" ]]; then
-    echo "WCT must be on the path. (https://github.com/Polymer/web-component-tester)"
+  wct_bin=$(abs_path ./node_modules/web-component-tester/bin/wct);
+fi
+if [[ -z "$wct_bin" ]]; then
+    echo "wct_bin must be set or WCT locally installed (npm install wct)."
     exit 1
 fi
 
diff --git a/polygerrit-ui/app/template_test.sh b/polygerrit-ui/app/template_test.sh
index 7177e8a..b1a2380 100755
--- a/polygerrit-ui/app/template_test.sh
+++ b/polygerrit-ui/app/template_test.sh
@@ -14,19 +14,6 @@
     exit 1
 fi
 
-fried_twinkie_config=$(npm list -g | grep -c fried-twinkie)
-if [ -z "$npm_bin" ] || [ "$fried_twinkie_config" -eq "0" ]; then
-    echo "You must install fried twinkie and its dependencies from NPM."
-    echo "> npm install -g fried-twinkie"
-    exit 1
-fi
-
-twinkie_version=$(npm list -g fried-twinkie@\>0.1 | grep fried-twinkie || :)
-if [ -z "$twinkie_version" ]; then
-    echo "Outdated version of fried-twinkie found. Bypassing template check."
-    exit 0
-fi
-
 # Have to find where node_modules are installed and set the NODE_PATH
 
 get_node_path() {
diff --git a/polygerrit-ui/app/wct_test.sh b/polygerrit-ui/app/wct_test.sh
index 81dc109..f1b4666 100755
--- a/polygerrit-ui/app/wct_test.sh
+++ b/polygerrit-ui/app/wct_test.sh
@@ -59,9 +59,9 @@
     };
 EOF
 
-export PATH="$(dirname $WCT):$(dirname $NPM):$PATH"
+export PATH="$(dirname $NPM):$PATH"
 
 cd $t
 test -n "${WCT}"
 
-$(basename ${WCT}) ${WCT_ARGS}
+${WCT} ${WCT_ARGS}