Merge branch 'stable-3.2'

* stable-3.2:
  CreateChange: Clarify the e2e test access modifier
  e2e-tests: Fix minor comma styling inconsistencies
  Set version to 2.16.19
  Set version to 3.2.0-SNAPSHOT

Change-Id: I2aa9688bc7dfbd46bf0a7364d67517c9ed8473bb
diff --git a/Documentation/dev-bazel.txt b/Documentation/dev-bazel.txt
index aa6e400..01cd494 100644
--- a/Documentation/dev-bazel.txt
+++ b/Documentation/dev-bazel.txt
@@ -21,7 +21,7 @@
 * A JDK for Java 8|9|10|11|...
 * Python 2 or 3
 * link:https://github.com/nodesource/distributions/blob/master/README.md[Node.js (including npm),role=external,window=_blank]
-* Bower (`sudo npm install -g bower`)
+* Bower (`npm install -g bower`)
 * link:https://docs.bazel.build/versions/master/install.html[Bazel,role=external,window=_blank] -launched with
 link:https://github.com/bazelbuild/bazelisk[Bazelisk,role=external,window=_blank]
 * Maven
@@ -32,12 +32,11 @@
 [[bazel]]
 === Bazel
 
-link:https://github.com/bazelbuild/bazelisk[Bazelisk,role=external,window=_blank] includes a
-link:https://bazel.build/[Bazel,role=external,window=_blank] version check and downloads the correct
-`bazel` version for the git project/repository. Bazelisk is the recommended
-`bazel` launcher for Gerrit. Once Bazelisk is installed locally, a `bazel`
-symlink can be created towards it. This is so that every `bazel` command
-seamlessly uses Bazelisk, which then runs the proper `bazel` binary version.
+link:https://github.com/bazelbuild/bazelisk[Bazelisk,role=external,window=_blank] is a version
+manager for link:https://bazel.build/[Bazel,role=external,window=_blank], similar to how `nvm`
+manages `npm` versions. It takes care of downloading and installing Bazel itself, so you don't have
+to worry about using the correct version of Bazel. Bazelisk can be installed in different
+ways: link:https://docs.bazel.build/install-bazelisk.html[Install,role=external,window=_blank]
 
 [[java]]
 === Java
diff --git a/Documentation/intro-gerrit-walkthrough.txt b/Documentation/intro-gerrit-walkthrough.txt
index 92732d0..6565ba4 100644
--- a/Documentation/intro-gerrit-walkthrough.txt
+++ b/Documentation/intro-gerrit-walkthrough.txt
@@ -296,6 +296,10 @@
 * Review the link:intro-project-owner.html[Project Owners guide] to learn more
   about configuring projects in Gerrit, including setting user permissions and
   configuring verification checks
+* Read through the Git and Gerrit training slides that explain concepts and
+  workflows in detail. They are meant for self-studying how Git and Gerrit work:
+** link:https://docs.google.com/presentation/d/1IQCRPHEIX-qKo7QFxsD3V62yhyGA9_5YsYXFOiBpgkk/edit?usp=sharing[Git explained: Git Concepts and Workflows]
+** link:https://docs.google.com/presentation/d/1C73UgQdzZDw0gzpaEqIC6SPujZJhqamyqO1XOHjH-uk/edit?usp=sharing[Gerrit explained: Gerrit Concepts and Workflows]
 
 GERRIT
 ------
diff --git a/Documentation/js_licenses.txt b/Documentation/js_licenses.txt
index bd26190..97c2548 100644
--- a/Documentation/js_licenses.txt
+++ b/Documentation/js_licenses.txt
@@ -778,39 +778,6 @@
 ----
 
 
-[[moment]]
-moment
-
-* moment
-
-[[moment_license]]
-----
-Copyright (c) JS Foundation and other contributors
-
-Permission is hereby granted, free of charge, to any person
-obtaining a copy of this software and associated documentation
-files (the "Software"), to deal in the Software without
-restriction, including without limitation the rights to use,
-copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the
-Software is furnished to do so, subject to the following
-conditions:
-
-The above copyright notice and this permission notice shall be
-included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
-OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
-HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
-WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
-OTHER DEALINGS IN THE SOFTWARE.
-
-----
-
-
 [[font-roboto-local-fonts-roboto]]
 font-roboto-local-fonts-roboto
 
diff --git a/Documentation/licenses.txt b/Documentation/licenses.txt
index 63c569a..c1dfb94 100644
--- a/Documentation/licenses.txt
+++ b/Documentation/licenses.txt
@@ -3720,39 +3720,6 @@
 ----
 
 
-[[moment]]
-moment
-
-* moment
-
-[[moment_license]]
-----
-Copyright (c) JS Foundation and other contributors
-
-Permission is hereby granted, free of charge, to any person
-obtaining a copy of this software and associated documentation
-files (the "Software"), to deal in the Software without
-restriction, including without limitation the rights to use,
-copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the
-Software is furnished to do so, subject to the following
-conditions:
-
-The above copyright notice and this permission notice shall be
-included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
-OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
-HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
-WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
-OTHER DEALINGS IN THE SOFTWARE.
-
-----
-
-
 [[font-roboto-local-fonts-roboto]]
 font-roboto-local-fonts-roboto
 
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 0badced..4006d1d 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -5731,6 +5731,11 @@
 The list of commits that are being integrated into the destination
 branch by submitting the merge commit.
 
+* `/PATCHSET_LEVEL`:
++
+This file path is used exclusively for posting and indicating
+patchset-level comments, thus not relevant for other use-cases.
+
 [[fix-id]]
 === \{fix-id\}
 UUID of a suggested fix.
@@ -6226,7 +6231,7 @@
 comments may be returned for multiple patch sets.
 |`id`          ||The URL encoded UUID of the comment.
 |`path`        |optional|
-The path of the file for which the inline comment was done. +
+link:#file-id[The file path] for which the inline comment was done. +
 Not set if returned in a map where the key is the file path.
 |`side`        |optional|
 The side on which the comment was added. +
@@ -6279,7 +6284,7 @@
 The URL encoded UUID of the comment if an existing draft comment should
 be updated.
 |`path`        |optional|
-The path of the file for which the inline comment should be added. +
+link:#file-id[The file path] for which the inline comment should be added. +
 Doesn't need to be set if contained in a map where the key is the file
 path.
 |`side`        |optional|
diff --git a/java/com/google/gerrit/entities/Patch.java b/java/com/google/gerrit/entities/Patch.java
index 50f86fe..3926d47 100644
--- a/java/com/google/gerrit/entities/Patch.java
+++ b/java/com/google/gerrit/entities/Patch.java
@@ -35,6 +35,12 @@
   public static final String MERGE_LIST = "/MERGE_LIST";
 
   /**
+   * Magical file name which doesn't represent a file. Used specifically for patchset-level
+   * comments.
+   */
+  public static final String PATCHSET_LEVEL = "/PATCHSET_LEVEL";
+
+  /**
    * Checks if the given path represents a magic file. A magic file is a generated file that is
    * automatically included into changes. It does not exist in the commit of the patch set.
    *
@@ -42,7 +48,7 @@
    * @return {@code true} if the path represents a magic file, otherwise {@code false}.
    */
   public static boolean isMagic(String path) {
-    return COMMIT_MSG.equals(path) || MERGE_LIST.equals(path);
+    return COMMIT_MSG.equals(path) || MERGE_LIST.equals(path) || PATCHSET_LEVEL.equals(path);
   }
 
   public static Key key(PatchSet.Id patchSetId, String fileName) {
diff --git a/java/com/google/gerrit/extensions/annotations/RemoveAfter.java b/java/com/google/gerrit/extensions/annotations/RemoveAfter.java
index aa31dd0..02f70e9 100644
--- a/java/com/google/gerrit/extensions/annotations/RemoveAfter.java
+++ b/java/com/google/gerrit/extensions/annotations/RemoveAfter.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.extensions.annotations;
 
-import static java.lang.annotation.RetentionPolicy.SOURCE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
 import com.google.inject.BindingAnnotation;
 import java.lang.annotation.ElementType;
@@ -26,7 +26,7 @@
  * period we promised to users.
  */
 @Target({ElementType.PARAMETER, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE})
-@Retention(SOURCE)
+@Retention(RUNTIME)
 @BindingAnnotation
 public @interface RemoveAfter {
   /**
diff --git a/java/com/google/gerrit/extensions/common/testing/RobotCommentInfoSubject.java b/java/com/google/gerrit/extensions/common/testing/RobotCommentInfoSubject.java
index 0698735..dd226ed 100644
--- a/java/com/google/gerrit/extensions/common/testing/RobotCommentInfoSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/RobotCommentInfoSubject.java
@@ -18,6 +18,7 @@
 import static com.google.gerrit.truth.ListSubject.elements;
 
 import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.StringSubject;
 import com.google.common.truth.Subject;
 import com.google.gerrit.extensions.common.FixSuggestionInfo;
 import com.google.gerrit.extensions.common.RobotCommentInfo;
@@ -53,6 +54,11 @@
         .thatCustom(robotCommentInfo.fixSuggestions, FixSuggestionInfoSubject.fixSuggestions());
   }
 
+  public StringSubject path() {
+    isNotNull();
+    return check("path").that(robotCommentInfo.path);
+  }
+
   public FixSuggestionInfoSubject onlyFixSuggestion() {
     return fixSuggestions().onlyElement();
   }
diff --git a/java/com/google/gerrit/server/CommentsUtil.java b/java/com/google/gerrit/server/CommentsUtil.java
index e9ba72d..450cbe0 100644
--- a/java/com/google/gerrit/server/CommentsUtil.java
+++ b/java/com/google/gerrit/server/CommentsUtil.java
@@ -242,7 +242,9 @@
    * @param changeMessages list of change messages
    */
   public static void linkCommentsToChangeMessages(
-      List<? extends CommentInfo> comments, List<ChangeMessage> changeMessages) {
+      List<? extends CommentInfo> comments,
+      List<ChangeMessage> changeMessages,
+      boolean skipAutoGeneratedMessages) {
     ArrayList<ChangeMessage> sortedChangeMessages =
         changeMessages.stream()
             .sorted(comparing(ChangeMessage::getWrittenOn))
@@ -257,7 +259,7 @@
       // message in timestamp
       while (cmItr < sortedChangeMessages.size()) {
         ChangeMessage cm = sortedChangeMessages.get(cmItr);
-        if (isAfter(comment, cm) || skipChangeMessage(cm)) {
+        if (isAfter(comment, cm) || (skipAutoGeneratedMessages && isAutoGenerated(cm))) {
           cmItr += 1;
         } else {
           break;
@@ -269,7 +271,7 @@
     }
   }
 
-  private static boolean skipChangeMessage(ChangeMessage cm) {
+  private static boolean isAutoGenerated(ChangeMessage cm) {
     return ChangeMessagesUtil.isAutogenerated(cm.getTag());
   }
 
diff --git a/java/com/google/gerrit/server/account/StoredPreferences.java b/java/com/google/gerrit/server/account/StoredPreferences.java
index 1b3ff40..573c619 100644
--- a/java/com/google/gerrit/server/account/StoredPreferences.java
+++ b/java/com/google/gerrit/server/account/StoredPreferences.java
@@ -15,19 +15,13 @@
 package com.google.gerrit.server.account;
 
 import static com.google.common.base.Preconditions.checkState;
-import static com.google.gerrit.server.config.ConfigUtil.loadSection;
-import static com.google.gerrit.server.config.ConfigUtil.skipField;
 import static com.google.gerrit.server.config.ConfigUtil.storeSection;
-import static com.google.gerrit.server.git.UserConfigSections.CHANGE_TABLE;
 import static com.google.gerrit.server.git.UserConfigSections.CHANGE_TABLE_COLUMN;
 import static com.google.gerrit.server.git.UserConfigSections.KEY_ID;
 import static com.google.gerrit.server.git.UserConfigSections.KEY_TARGET;
 import static com.google.gerrit.server.git.UserConfigSections.KEY_URL;
 import static java.util.Objects.requireNonNull;
 
-import com.google.common.base.Strings;
-import com.google.common.collect.Lists;
-import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
@@ -36,13 +30,12 @@
 import com.google.gerrit.extensions.client.MenuItem;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.PreferencesParserUtil;
 import com.google.gerrit.server.config.VersionedDefaultPreferences;
 import com.google.gerrit.server.git.UserConfigSections;
 import com.google.gerrit.server.git.ValidationError;
 import com.google.gerrit.server.git.meta.MetaDataUpdate;
 import java.io.IOException;
-import java.lang.reflect.Field;
-import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -76,8 +69,6 @@
  * <p>The preferences are lazily parsed.
  */
 public class StoredPreferences {
-  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
   public static final String PREFERENCES_CONFIG = "preferences.config";
 
   private final Account.Id accountId;
@@ -141,7 +132,7 @@
           UserConfigSections.GENERAL,
           null,
           mergedGeneralPreferencesInput,
-          parseDefaultGeneralPreferences(defaultCfg, null));
+          PreferencesParserUtil.parseDefaultGeneralPreferences(defaultCfg, null));
       setChangeTable(cfg, mergedGeneralPreferencesInput.changeTable);
       setMy(cfg, mergedGeneralPreferencesInput.my);
 
@@ -158,7 +149,7 @@
           UserConfigSections.DIFF,
           null,
           mergedDiffPreferencesInput,
-          parseDefaultDiffPreferences(defaultCfg, null));
+          PreferencesParserUtil.parseDefaultDiffPreferences(defaultCfg, null));
 
       // evict the cached diff preferences
       this.diffPreferences = null;
@@ -173,7 +164,7 @@
           UserConfigSections.EDIT,
           null,
           mergedEditPreferencesInput,
-          parseDefaultEditPreferences(defaultCfg, null));
+          PreferencesParserUtil.parseDefaultEditPreferences(defaultCfg, null));
 
       // evict the cached edit preferences
       this.editPreferences = null;
@@ -189,7 +180,7 @@
 
   private GeneralPreferencesInfo parseGeneralPreferences(@Nullable GeneralPreferencesInfo input) {
     try {
-      return parseGeneralPreferences(cfg, defaultCfg, input);
+      return PreferencesParserUtil.parseGeneralPreferences(cfg, defaultCfg, input);
     } catch (ConfigInvalidException e) {
       validationErrorSink.error(
           new ValidationError(
@@ -203,7 +194,7 @@
 
   private DiffPreferencesInfo parseDiffPreferences(@Nullable DiffPreferencesInfo input) {
     try {
-      return parseDiffPreferences(cfg, defaultCfg, input);
+      return PreferencesParserUtil.parseDiffPreferences(cfg, defaultCfg, input);
     } catch (ConfigInvalidException e) {
       validationErrorSink.error(
           new ValidationError(
@@ -216,7 +207,7 @@
 
   private EditPreferencesInfo parseEditPreferences(@Nullable EditPreferencesInfo input) {
     try {
-      return parseEditPreferences(cfg, defaultCfg, input);
+      return PreferencesParserUtil.parseEditPreferences(cfg, defaultCfg, input);
     } catch (ConfigInvalidException e) {
       validationErrorSink.error(
           new ValidationError(
@@ -227,218 +218,6 @@
     }
   }
 
-  /**
-   * Returns a {@link GeneralPreferencesInfo} that is the result of parsing {@code defaultCfg} for
-   * the server's default configs and {@code cfg} for the user's config. These configs are then
-   * overlaid to inherit values (default -> user -> input (if provided).
-   */
-  public static GeneralPreferencesInfo parseGeneralPreferences(
-      Config cfg, @Nullable Config defaultCfg, @Nullable GeneralPreferencesInfo input)
-      throws ConfigInvalidException {
-    GeneralPreferencesInfo r =
-        loadSection(
-            cfg,
-            UserConfigSections.GENERAL,
-            null,
-            new GeneralPreferencesInfo(),
-            defaultCfg != null
-                ? parseDefaultGeneralPreferences(defaultCfg, input)
-                : GeneralPreferencesInfo.defaults(),
-            input);
-    if (input != null) {
-      r.changeTable = input.changeTable;
-      r.my = input.my;
-    } else {
-      r.changeTable = parseChangeTableColumns(cfg, defaultCfg);
-      r.my = parseMyMenus(cfg, defaultCfg);
-    }
-    return r;
-  }
-
-  /**
-   * Returns a {@link DiffPreferencesInfo} that is the result of parsing {@code defaultCfg} for the
-   * server's default configs and {@code cfg} for the user's config. These configs are then overlaid
-   * to inherit values (default -> user -> input (if provided).
-   */
-  public static DiffPreferencesInfo parseDiffPreferences(
-      Config cfg, @Nullable Config defaultCfg, @Nullable DiffPreferencesInfo input)
-      throws ConfigInvalidException {
-    return loadSection(
-        cfg,
-        UserConfigSections.DIFF,
-        null,
-        new DiffPreferencesInfo(),
-        defaultCfg != null
-            ? parseDefaultDiffPreferences(defaultCfg, input)
-            : DiffPreferencesInfo.defaults(),
-        input);
-  }
-
-  /**
-   * Returns a {@link EditPreferencesInfo} that is the result of parsing {@code defaultCfg} for the
-   * server's default configs and {@code cfg} for the user's config. These configs are then overlaid
-   * to inherit values (default -> user -> input (if provided).
-   */
-  public static EditPreferencesInfo parseEditPreferences(
-      Config cfg, @Nullable Config defaultCfg, @Nullable EditPreferencesInfo input)
-      throws ConfigInvalidException {
-    return loadSection(
-        cfg,
-        UserConfigSections.EDIT,
-        null,
-        new EditPreferencesInfo(),
-        defaultCfg != null
-            ? parseDefaultEditPreferences(defaultCfg, input)
-            : EditPreferencesInfo.defaults(),
-        input);
-  }
-
-  private static GeneralPreferencesInfo parseDefaultGeneralPreferences(
-      Config defaultCfg, GeneralPreferencesInfo input) throws ConfigInvalidException {
-    GeneralPreferencesInfo allUserPrefs = new GeneralPreferencesInfo();
-    loadSection(
-        defaultCfg,
-        UserConfigSections.GENERAL,
-        null,
-        allUserPrefs,
-        GeneralPreferencesInfo.defaults(),
-        input);
-    return updateGeneralPreferencesDefaults(allUserPrefs);
-  }
-
-  private static DiffPreferencesInfo parseDefaultDiffPreferences(
-      Config defaultCfg, DiffPreferencesInfo input) throws ConfigInvalidException {
-    DiffPreferencesInfo allUserPrefs = new DiffPreferencesInfo();
-    loadSection(
-        defaultCfg,
-        UserConfigSections.DIFF,
-        null,
-        allUserPrefs,
-        DiffPreferencesInfo.defaults(),
-        input);
-    return updateDiffPreferencesDefaults(allUserPrefs);
-  }
-
-  private static EditPreferencesInfo parseDefaultEditPreferences(
-      Config defaultCfg, EditPreferencesInfo input) throws ConfigInvalidException {
-    EditPreferencesInfo allUserPrefs = new EditPreferencesInfo();
-    loadSection(
-        defaultCfg,
-        UserConfigSections.EDIT,
-        null,
-        allUserPrefs,
-        EditPreferencesInfo.defaults(),
-        input);
-    return updateEditPreferencesDefaults(allUserPrefs);
-  }
-
-  private static GeneralPreferencesInfo updateGeneralPreferencesDefaults(
-      GeneralPreferencesInfo input) {
-    GeneralPreferencesInfo result = GeneralPreferencesInfo.defaults();
-    try {
-      for (Field field : input.getClass().getDeclaredFields()) {
-        if (skipField(field)) {
-          continue;
-        }
-        Object newVal = field.get(input);
-        if (newVal != null) {
-          field.set(result, newVal);
-        }
-      }
-    } catch (IllegalAccessException e) {
-      logger.atSevere().withCause(e).log("Failed to apply default general preferences");
-      return GeneralPreferencesInfo.defaults();
-    }
-    return result;
-  }
-
-  private static DiffPreferencesInfo updateDiffPreferencesDefaults(DiffPreferencesInfo input) {
-    DiffPreferencesInfo result = DiffPreferencesInfo.defaults();
-    try {
-      for (Field field : input.getClass().getDeclaredFields()) {
-        if (skipField(field)) {
-          continue;
-        }
-        Object newVal = field.get(input);
-        if (newVal != null) {
-          field.set(result, newVal);
-        }
-      }
-    } catch (IllegalAccessException e) {
-      logger.atSevere().withCause(e).log("Failed to apply default diff preferences");
-      return DiffPreferencesInfo.defaults();
-    }
-    return result;
-  }
-
-  private static EditPreferencesInfo updateEditPreferencesDefaults(EditPreferencesInfo input) {
-    EditPreferencesInfo result = EditPreferencesInfo.defaults();
-    try {
-      for (Field field : input.getClass().getDeclaredFields()) {
-        if (skipField(field)) {
-          continue;
-        }
-        Object newVal = field.get(input);
-        if (newVal != null) {
-          field.set(result, newVal);
-        }
-      }
-    } catch (IllegalAccessException e) {
-      logger.atSevere().withCause(e).log("Failed to apply default edit preferences");
-      return EditPreferencesInfo.defaults();
-    }
-    return result;
-  }
-
-  private static List<String> parseChangeTableColumns(Config cfg, @Nullable Config defaultCfg) {
-    List<String> changeTable = changeTable(cfg);
-    if (changeTable == null && defaultCfg != null) {
-      changeTable = changeTable(defaultCfg);
-    }
-    return changeTable;
-  }
-
-  private static List<MenuItem> parseMyMenus(Config cfg, @Nullable Config defaultCfg) {
-    List<MenuItem> my = my(cfg);
-    if (my.isEmpty() && defaultCfg != null) {
-      my = my(defaultCfg);
-    }
-    if (my.isEmpty()) {
-      my.add(new MenuItem("Dashboard", "#/dashboard/self", null));
-      my.add(new MenuItem("Draft Comments", "#/q/has:draft", null));
-      my.add(new MenuItem("Edits", "#/q/has:edit", null));
-      my.add(new MenuItem("Watched Changes", "#/q/is:watched+is:open", null));
-      my.add(new MenuItem("Starred Changes", "#/q/is:starred", null));
-      my.add(new MenuItem("Groups", "#/settings/#Groups", null));
-    }
-    return my;
-  }
-
-  public static GeneralPreferencesInfo readDefaultGeneralPreferences(
-      AllUsersName allUsersName, Repository allUsersRepo)
-      throws IOException, ConfigInvalidException {
-    return parseGeneralPreferences(readDefaultConfig(allUsersName, allUsersRepo), null, null);
-  }
-
-  public static DiffPreferencesInfo readDefaultDiffPreferences(
-      AllUsersName allUsersName, Repository allUsersRepo)
-      throws IOException, ConfigInvalidException {
-    return parseDiffPreferences(readDefaultConfig(allUsersName, allUsersRepo), null, null);
-  }
-
-  public static EditPreferencesInfo readDefaultEditPreferences(
-      AllUsersName allUsersName, Repository allUsersRepo)
-      throws IOException, ConfigInvalidException {
-    return parseEditPreferences(readDefaultConfig(allUsersName, allUsersRepo), null, null);
-  }
-
-  static Config readDefaultConfig(AllUsersName allUsersName, Repository allUsersRepo)
-      throws IOException, ConfigInvalidException {
-    VersionedDefaultPreferences defaultPrefs = new VersionedDefaultPreferences();
-    defaultPrefs.load(allUsersName, allUsersRepo);
-    return defaultPrefs.getConfig();
-  }
-
   public static GeneralPreferencesInfo updateDefaultGeneralPreferences(
       MetaDataUpdate md, GeneralPreferencesInfo input) throws IOException, ConfigInvalidException {
     VersionedDefaultPreferences defaultPrefs = new VersionedDefaultPreferences();
@@ -453,7 +232,7 @@
     setChangeTable(defaultPrefs.getConfig(), input.changeTable);
     defaultPrefs.commit(md);
 
-    return parseGeneralPreferences(defaultPrefs.getConfig(), null, null);
+    return PreferencesParserUtil.parseGeneralPreferences(defaultPrefs.getConfig(), null, null);
   }
 
   public static DiffPreferencesInfo updateDefaultDiffPreferences(
@@ -468,7 +247,7 @@
         DiffPreferencesInfo.defaults());
     defaultPrefs.commit(md);
 
-    return parseDiffPreferences(defaultPrefs.getConfig(), null, null);
+    return PreferencesParserUtil.parseDiffPreferences(defaultPrefs.getConfig(), null, null);
   }
 
   public static EditPreferencesInfo updateDefaultEditPreferences(
@@ -483,11 +262,24 @@
         EditPreferencesInfo.defaults());
     defaultPrefs.commit(md);
 
-    return parseEditPreferences(defaultPrefs.getConfig(), null, null);
+    return PreferencesParserUtil.parseEditPreferences(defaultPrefs.getConfig(), null, null);
   }
 
-  private static List<String> changeTable(Config cfg) {
-    return Lists.newArrayList(cfg.getStringList(CHANGE_TABLE, null, CHANGE_TABLE_COLUMN));
+  public static void validateMy(List<MenuItem> my) throws BadRequestException {
+    if (my == null) {
+      return;
+    }
+    for (MenuItem item : my) {
+      checkRequiredMenuItemField(item.name, "name");
+      checkRequiredMenuItemField(item.url, "URL");
+    }
+  }
+
+  static Config readDefaultConfig(AllUsersName allUsersName, Repository allUsersRepo)
+      throws IOException, ConfigInvalidException {
+    VersionedDefaultPreferences defaultPrefs = new VersionedDefaultPreferences();
+    defaultPrefs.load(allUsersName, allUsersRepo);
+    return defaultPrefs.getConfig();
   }
 
   private static void setChangeTable(Config cfg, List<String> changeTable) {
@@ -497,21 +289,6 @@
     }
   }
 
-  private static List<MenuItem> my(Config cfg) {
-    List<MenuItem> my = new ArrayList<>();
-    for (String subsection : cfg.getSubsections(UserConfigSections.MY)) {
-      String url = my(cfg, subsection, KEY_URL, "#/");
-      String target = my(cfg, subsection, KEY_TARGET, url.startsWith("#") ? null : "_blank");
-      my.add(new MenuItem(subsection, url, target, my(cfg, subsection, KEY_ID, null)));
-    }
-    return my;
-  }
-
-  private static String my(Config cfg, String subsection, String key, String defaultValue) {
-    String val = cfg.getString(UserConfigSections.MY, subsection, key);
-    return !Strings.isNullOrEmpty(val) ? val : defaultValue;
-  }
-
   private static void setMy(Config cfg, List<MenuItem> my) {
     if (my != null) {
       unsetSection(cfg, UserConfigSections.MY);
@@ -526,16 +303,6 @@
     }
   }
 
-  public static void validateMy(List<MenuItem> my) throws BadRequestException {
-    if (my == null) {
-      return;
-    }
-    for (MenuItem item : my) {
-      checkRequiredMenuItemField(item.name, "name");
-      checkRequiredMenuItemField(item.url, "URL");
-    }
-  }
-
   private static void checkRequiredMenuItemField(String value, String name)
       throws BadRequestException {
     if (isNullOrEmpty(value)) {
diff --git a/java/com/google/gerrit/server/change/ChangeJson.java b/java/com/google/gerrit/server/change/ChangeJson.java
index 8c4f275..8f97b68 100644
--- a/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/java/com/google/gerrit/server/change/ChangeJson.java
@@ -140,7 +140,6 @@
           COMMIT_FOOTERS,
           CURRENT_ACTIONS,
           CURRENT_COMMIT,
-          DETAILED_LABELS, // may need to load ChangeNotes to check remove reviewer permissions
           MESSAGES);
 
   @Singleton
@@ -722,7 +721,10 @@
     // testRemoveReviewer check for a specific reviewer in the loop saving potentially many
     // permission checks.
     boolean canRemoveAnyReviewer =
-        permissionBackendForChange(userProvider.get(), cd).test(ChangePermission.REMOVE_REVIEWER);
+        permissionBackend
+            .user(userProvider.get())
+            .change(cd)
+            .test(ChangePermission.REMOVE_REVIEWER);
     for (LabelInfo label : labels) {
       if (label.all == null) {
         continue;
@@ -817,16 +819,4 @@
     }
     return map;
   }
-
-  /**
-   * @return {@link com.google.gerrit.server.permissions.PermissionBackend.ForChange} constructed
-   *     from either an index-backed or a database-backed {@link ChangeData} depending on {@code
-   *     lazyload}.
-   */
-  private PermissionBackend.ForChange permissionBackendForChange(CurrentUser user, ChangeData cd) {
-    PermissionBackend.WithUser withUser = permissionBackend.user(user);
-    return lazyLoad
-        ? withUser.change(cd)
-        : withUser.indexedChange(cd, notesFactory.createFromIndexedChange(cd.change()));
-  }
 }
diff --git a/java/com/google/gerrit/server/change/DeleteReviewerOp.java b/java/com/google/gerrit/server/change/DeleteReviewerOp.java
index b70b059..099334d 100644
--- a/java/com/google/gerrit/server/change/DeleteReviewerOp.java
+++ b/java/com/google/gerrit/server/change/DeleteReviewerOp.java
@@ -49,10 +49,8 @@
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.List;
 import java.util.Map;
 
 public class DeleteReviewerOp implements BatchUpdateOp {
@@ -134,12 +132,10 @@
     msg.append("Removed reviewer " + reviewer.account().fullName());
     StringBuilder removedVotesMsg = new StringBuilder();
     removedVotesMsg.append(" with the following votes:\n\n");
-    List<PatchSetApproval> del = new ArrayList<>();
     boolean votesRemoved = false;
     for (PatchSetApproval a : approvals(ctx, reviewerId)) {
       // Check if removing this vote is OK
       removeReviewerControl.checkRemoveReviewer(ctx.getNotes(), ctx.getUser(), a);
-      del.add(a);
       if (a.patchSetId().equals(currPs.id()) && a.value() != 0) {
         oldApprovals.put(a.label(), a.value());
         removedVotesMsg
diff --git a/java/com/google/gerrit/server/change/LabelsJson.java b/java/com/google/gerrit/server/change/LabelsJson.java
index c6f4969..2db17d6 100644
--- a/java/com/google/gerrit/server/change/LabelsJson.java
+++ b/java/com/google/gerrit/server/change/LabelsJson.java
@@ -131,7 +131,7 @@
 
     Map<String, Short> labels = null;
     Set<LabelPermission.WithValue> can =
-        permissionBackendForChange(filterApprovalsBy, cd).testLabels(toCheck.values());
+        permissionBackend.absentUser(filterApprovalsBy).change(cd).testLabels(toCheck.values());
     SetMultimap<String, String> permitted = LinkedHashMultimap.create();
     for (SubmitRecord rec : submitRecords(cd)) {
       if (rec.labels == null) {
@@ -452,7 +452,7 @@
 
     LabelTypes labelTypes = cd.getLabelTypes();
     for (Account.Id accountId : allUsers) {
-      PermissionBackend.ForChange perm = permissionBackendForChange(accountId, cd);
+      PermissionBackend.ForChange perm = permissionBackend.absentUser(accountId).change(cd);
       Map<String, VotingRangeInfo> pvr = getPermittedVotingRanges(permittedLabels(accountId, cd));
       for (Map.Entry<String, LabelWithStatus> e : labels.entrySet()) {
         LabelType lt = labelTypes.byLabel(e.getKey());
@@ -492,18 +492,6 @@
     }
   }
 
-  /**
-   * @return {@link com.google.gerrit.server.permissions.PermissionBackend.ForChange} constructed
-   *     from either an index-backed or a database-backed {@link ChangeData} depending on {@code
-   *     lazyload}.
-   */
-  private PermissionBackend.ForChange permissionBackendForChange(Account.Id user, ChangeData cd) {
-    PermissionBackend.WithUser withUser = permissionBackend.absentUser(user);
-    return lazyLoad
-        ? withUser.change(cd)
-        : withUser.indexedChange(cd, notesFactory.createFromIndexedChange(cd.change()));
-  }
-
   private List<SubmitRecord> submitRecords(ChangeData cd) {
     return cd.submitRecords(ChangeJson.SUBMIT_RULE_OPTIONS_LENIENT);
   }
diff --git a/java/com/google/gerrit/server/change/RevisionJson.java b/java/com/google/gerrit/server/change/RevisionJson.java
index 001a532..414107f 100644
--- a/java/com/google/gerrit/server/change/RevisionJson.java
+++ b/java/com/google/gerrit/server/change/RevisionJson.java
@@ -31,7 +31,6 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Sets;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Change;
@@ -60,7 +59,6 @@
 import com.google.gerrit.server.account.GpgApiAdapter;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MergeUtil;
-import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
@@ -107,8 +105,6 @@
   private final AnonymousUser anonymous;
   private final GitRepositoryManager repoManager;
   private final PermissionBackend permissionBackend;
-  private final ChangeNotes.Factory notesFactory;
-  private final boolean lazyLoad;
 
   @Inject
   RevisionJson(
@@ -128,7 +124,6 @@
       ChangeKindCache changeKindCache,
       GitRepositoryManager repoManager,
       PermissionBackend permissionBackend,
-      ChangeNotes.Factory notesFactory,
       @Assisted Iterable<ListChangesOption> options) {
     this.userProvider = userProvider;
     this.anonymous = anonymous;
@@ -145,10 +140,8 @@
     this.changeResourceFactory = changeResourceFactory;
     this.changeKindCache = changeKindCache;
     this.permissionBackend = permissionBackend;
-    this.notesFactory = notesFactory;
     this.repoManager = repoManager;
     this.options = ImmutableSet.copyOf(options);
-    this.lazyLoad = containsAnyOf(this.options, ChangeJson.REQUIRE_LAZY_LOAD);
   }
 
   /**
@@ -346,22 +339,9 @@
     return options.contains(option);
   }
 
-  /**
-   * @return {@link com.google.gerrit.server.permissions.PermissionBackend.ForChange} constructed
-   *     from either an index-backed or a database-backed {@link ChangeData} depending on {@code
-   *     lazyload}.
-   */
-  private PermissionBackend.ForChange permissionBackendForChange(
-      PermissionBackend.WithUser withUser, ChangeData cd) {
-    return lazyLoad
-        ? withUser.change(cd)
-        : withUser.indexedChange(cd, notesFactory.createFromIndexedChange(cd.change()));
-  }
-
   private boolean isWorldReadable(ChangeData cd) throws PermissionBackendException {
     try {
-      permissionBackendForChange(permissionBackend.user(anonymous), cd)
-          .check(ChangePermission.READ);
+      permissionBackend.user(anonymous).change(cd).check(ChangePermission.READ);
     } catch (AuthException ae) {
       return false;
     }
@@ -382,9 +362,4 @@
   private RevWalk newRevWalk(@Nullable Repository repo) {
     return repo != null ? new RevWalk(repo) : null;
   }
-
-  private static boolean containsAnyOf(
-      ImmutableSet<ListChangesOption> set, ImmutableSet<ListChangesOption> toFind) {
-    return !Sets.intersection(toFind, set).isEmpty();
-  }
 }
diff --git a/java/com/google/gerrit/server/config/CachedPreferences.java b/java/com/google/gerrit/server/config/CachedPreferences.java
index f4dcd10..388f58a 100644
--- a/java/com/google/gerrit/server/config/CachedPreferences.java
+++ b/java/com/google/gerrit/server/config/CachedPreferences.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.extensions.client.EditPreferencesInfo;
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
-import com.google.gerrit.server.account.StoredPreferences;
 import java.util.Optional;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Config;
@@ -52,7 +51,7 @@
   public static GeneralPreferencesInfo general(
       Optional<CachedPreferences> defaultPreferences, CachedPreferences userPreferences) {
     try {
-      return StoredPreferences.parseGeneralPreferences(
+      return PreferencesParserUtil.parseGeneralPreferences(
           userPreferences.asConfig(), configOrNull(defaultPreferences), null);
     } catch (ConfigInvalidException e) {
       return GeneralPreferencesInfo.defaults();
@@ -62,7 +61,7 @@
   public static EditPreferencesInfo edit(
       Optional<CachedPreferences> defaultPreferences, CachedPreferences userPreferences) {
     try {
-      return StoredPreferences.parseEditPreferences(
+      return PreferencesParserUtil.parseEditPreferences(
           userPreferences.asConfig(), configOrNull(defaultPreferences), null);
     } catch (ConfigInvalidException e) {
       return EditPreferencesInfo.defaults();
@@ -72,7 +71,7 @@
   public static DiffPreferencesInfo diff(
       Optional<CachedPreferences> defaultPreferences, CachedPreferences userPreferences) {
     try {
-      return StoredPreferences.parseDiffPreferences(
+      return PreferencesParserUtil.parseDiffPreferences(
           userPreferences.asConfig(), configOrNull(defaultPreferences), null);
     } catch (ConfigInvalidException e) {
       return DiffPreferencesInfo.defaults();
diff --git a/java/com/google/gerrit/server/config/PreferencesParserUtil.java b/java/com/google/gerrit/server/config/PreferencesParserUtil.java
new file mode 100644
index 0000000..69d75be
--- /dev/null
+++ b/java/com/google/gerrit/server/config/PreferencesParserUtil.java
@@ -0,0 +1,266 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.config;
+
+import static com.google.gerrit.server.config.ConfigUtil.loadSection;
+import static com.google.gerrit.server.config.ConfigUtil.skipField;
+import static com.google.gerrit.server.git.UserConfigSections.CHANGE_TABLE;
+import static com.google.gerrit.server.git.UserConfigSections.CHANGE_TABLE_COLUMN;
+import static com.google.gerrit.server.git.UserConfigSections.KEY_ID;
+import static com.google.gerrit.server.git.UserConfigSections.KEY_TARGET;
+import static com.google.gerrit.server.git.UserConfigSections.KEY_URL;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
+import com.google.gerrit.extensions.client.EditPreferencesInfo;
+import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
+import com.google.gerrit.extensions.client.MenuItem;
+import com.google.gerrit.server.git.UserConfigSections;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.List;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+
+/** Helper to read default or user preferences from Git-style config files. */
+public class PreferencesParserUtil {
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+  private PreferencesParserUtil() {}
+
+  /**
+   * Returns a {@link GeneralPreferencesInfo} that is the result of parsing {@code defaultCfg} for
+   * the server's default configs and {@code cfg} for the user's config. These configs are then
+   * overlaid to inherit values (default -> user -> input (if provided).
+   */
+  public static GeneralPreferencesInfo parseGeneralPreferences(
+      Config cfg, @Nullable Config defaultCfg, @Nullable GeneralPreferencesInfo input)
+      throws ConfigInvalidException {
+    GeneralPreferencesInfo r =
+        loadSection(
+            cfg,
+            UserConfigSections.GENERAL,
+            null,
+            new GeneralPreferencesInfo(),
+            defaultCfg != null
+                ? parseDefaultGeneralPreferences(defaultCfg, input)
+                : GeneralPreferencesInfo.defaults(),
+            input);
+    if (input != null) {
+      r.changeTable = input.changeTable;
+      r.my = input.my;
+    } else {
+      r.changeTable = parseChangeTableColumns(cfg, defaultCfg);
+      r.my = parseMyMenus(cfg, defaultCfg);
+    }
+    return r;
+  }
+
+  /**
+   * Returns a {@link GeneralPreferencesInfo} that is the result of parsing {@code defaultCfg} for
+   * the server's default configs. These configs are then overlaid to inherit values (default ->
+   * input (if provided).
+   */
+  public static GeneralPreferencesInfo parseDefaultGeneralPreferences(
+      Config defaultCfg, GeneralPreferencesInfo input) throws ConfigInvalidException {
+    GeneralPreferencesInfo allUserPrefs = new GeneralPreferencesInfo();
+    loadSection(
+        defaultCfg,
+        UserConfigSections.GENERAL,
+        null,
+        allUserPrefs,
+        GeneralPreferencesInfo.defaults(),
+        input);
+    return updateGeneralPreferencesDefaults(allUserPrefs);
+  }
+
+  /**
+   * Returns a {@link DiffPreferencesInfo} that is the result of parsing {@code defaultCfg} for the
+   * server's default configs and {@code cfg} for the user's config. These configs are then overlaid
+   * to inherit values (default -> user -> input (if provided).
+   */
+  public static DiffPreferencesInfo parseDiffPreferences(
+      Config cfg, @Nullable Config defaultCfg, @Nullable DiffPreferencesInfo input)
+      throws ConfigInvalidException {
+    return loadSection(
+        cfg,
+        UserConfigSections.DIFF,
+        null,
+        new DiffPreferencesInfo(),
+        defaultCfg != null
+            ? parseDefaultDiffPreferences(defaultCfg, input)
+            : DiffPreferencesInfo.defaults(),
+        input);
+  }
+
+  /**
+   * Returns a {@link DiffPreferencesInfo} that is the result of parsing {@code defaultCfg} for the
+   * server's default configs. These configs are then overlaid to inherit values (default -> input
+   * (if provided).
+   */
+  public static DiffPreferencesInfo parseDefaultDiffPreferences(
+      Config defaultCfg, DiffPreferencesInfo input) throws ConfigInvalidException {
+    DiffPreferencesInfo allUserPrefs = new DiffPreferencesInfo();
+    loadSection(
+        defaultCfg,
+        UserConfigSections.DIFF,
+        null,
+        allUserPrefs,
+        DiffPreferencesInfo.defaults(),
+        input);
+    return updateDiffPreferencesDefaults(allUserPrefs);
+  }
+
+  /**
+   * Returns a {@link EditPreferencesInfo} that is the result of parsing {@code defaultCfg} for the
+   * server's default configs and {@code cfg} for the user's config. These configs are then overlaid
+   * to inherit values (default -> user -> input (if provided).
+   */
+  public static EditPreferencesInfo parseEditPreferences(
+      Config cfg, @Nullable Config defaultCfg, @Nullable EditPreferencesInfo input)
+      throws ConfigInvalidException {
+    return loadSection(
+        cfg,
+        UserConfigSections.EDIT,
+        null,
+        new EditPreferencesInfo(),
+        defaultCfg != null
+            ? parseDefaultEditPreferences(defaultCfg, input)
+            : EditPreferencesInfo.defaults(),
+        input);
+  }
+
+  /**
+   * Returns a {@link EditPreferencesInfo} that is the result of parsing {@code defaultCfg} for the
+   * server's default configs. These configs are then overlaid to inherit values (default -> input
+   * (if provided).
+   */
+  public static EditPreferencesInfo parseDefaultEditPreferences(
+      Config defaultCfg, EditPreferencesInfo input) throws ConfigInvalidException {
+    EditPreferencesInfo allUserPrefs = new EditPreferencesInfo();
+    loadSection(
+        defaultCfg,
+        UserConfigSections.EDIT,
+        null,
+        allUserPrefs,
+        EditPreferencesInfo.defaults(),
+        input);
+    return updateEditPreferencesDefaults(allUserPrefs);
+  }
+
+  private static List<String> parseChangeTableColumns(Config cfg, @Nullable Config defaultCfg) {
+    List<String> changeTable = changeTable(cfg);
+    if (changeTable == null && defaultCfg != null) {
+      changeTable = changeTable(defaultCfg);
+    }
+    return changeTable;
+  }
+
+  private static List<MenuItem> parseMyMenus(Config cfg, @Nullable Config defaultCfg) {
+    List<MenuItem> my = my(cfg);
+    if (my.isEmpty() && defaultCfg != null) {
+      my = my(defaultCfg);
+    }
+    if (my.isEmpty()) {
+      my.add(new MenuItem("Dashboard", "#/dashboard/self", null));
+      my.add(new MenuItem("Draft Comments", "#/q/has:draft", null));
+      my.add(new MenuItem("Edits", "#/q/has:edit", null));
+      my.add(new MenuItem("Watched Changes", "#/q/is:watched+is:open", null));
+      my.add(new MenuItem("Starred Changes", "#/q/is:starred", null));
+      my.add(new MenuItem("Groups", "#/settings/#Groups", null));
+    }
+    return my;
+  }
+
+  private static GeneralPreferencesInfo updateGeneralPreferencesDefaults(
+      GeneralPreferencesInfo input) {
+    GeneralPreferencesInfo result = GeneralPreferencesInfo.defaults();
+    try {
+      for (Field field : input.getClass().getDeclaredFields()) {
+        if (skipField(field)) {
+          continue;
+        }
+        Object newVal = field.get(input);
+        if (newVal != null) {
+          field.set(result, newVal);
+        }
+      }
+    } catch (IllegalAccessException e) {
+      logger.atSevere().withCause(e).log("Failed to apply default general preferences");
+      return GeneralPreferencesInfo.defaults();
+    }
+    return result;
+  }
+
+  private static DiffPreferencesInfo updateDiffPreferencesDefaults(DiffPreferencesInfo input) {
+    DiffPreferencesInfo result = DiffPreferencesInfo.defaults();
+    try {
+      for (Field field : input.getClass().getDeclaredFields()) {
+        if (skipField(field)) {
+          continue;
+        }
+        Object newVal = field.get(input);
+        if (newVal != null) {
+          field.set(result, newVal);
+        }
+      }
+    } catch (IllegalAccessException e) {
+      logger.atSevere().withCause(e).log("Failed to apply default diff preferences");
+      return DiffPreferencesInfo.defaults();
+    }
+    return result;
+  }
+
+  private static EditPreferencesInfo updateEditPreferencesDefaults(EditPreferencesInfo input) {
+    EditPreferencesInfo result = EditPreferencesInfo.defaults();
+    try {
+      for (Field field : input.getClass().getDeclaredFields()) {
+        if (skipField(field)) {
+          continue;
+        }
+        Object newVal = field.get(input);
+        if (newVal != null) {
+          field.set(result, newVal);
+        }
+      }
+    } catch (IllegalAccessException e) {
+      logger.atSevere().withCause(e).log("Failed to apply default edit preferences");
+      return EditPreferencesInfo.defaults();
+    }
+    return result;
+  }
+
+  private static List<String> changeTable(Config cfg) {
+    return Lists.newArrayList(cfg.getStringList(CHANGE_TABLE, null, CHANGE_TABLE_COLUMN));
+  }
+
+  private static List<MenuItem> my(Config cfg) {
+    List<MenuItem> my = new ArrayList<>();
+    for (String subsection : cfg.getSubsections(UserConfigSections.MY)) {
+      String url = my(cfg, subsection, KEY_URL, "#/");
+      String target = my(cfg, subsection, KEY_TARGET, url.startsWith("#") ? null : "_blank");
+      my.add(new MenuItem(subsection, url, target, my(cfg, subsection, KEY_ID, null)));
+    }
+    return my;
+  }
+
+  private static String my(Config cfg, String subsection, String key, String defaultValue) {
+    String val = cfg.getString(UserConfigSections.MY, subsection, key);
+    return !Strings.isNullOrEmpty(val) ? val : defaultValue;
+  }
+}
diff --git a/java/com/google/gerrit/server/documentation/MarkdownFormatter.java b/java/com/google/gerrit/server/documentation/MarkdownFormatter.java
index 2eb46f1..2d5e708 100644
--- a/java/com/google/gerrit/server/documentation/MarkdownFormatter.java
+++ b/java/com/google/gerrit/server/documentation/MarkdownFormatter.java
@@ -21,7 +21,6 @@
 
 import com.google.common.base.Strings;
 import com.google.common.flogger.FluentLogger;
-import com.vladsch.flexmark.Extension;
 import com.vladsch.flexmark.ast.Block;
 import com.vladsch.flexmark.ast.Heading;
 import com.vladsch.flexmark.ast.Node;
@@ -36,7 +35,6 @@
 import java.io.UnsupportedEncodingException;
 import java.net.URL;
 import java.nio.charset.Charset;
-import java.util.ArrayList;
 import java.util.concurrent.atomic.AtomicBoolean;
 import org.apache.commons.lang.StringEscapeUtils;
 import org.eclipse.jgit.util.RawParseUtils;
@@ -95,11 +93,6 @@
                 options, MarkdownFormatterHeader.HeadingExtension.create())
             .toMutable();
 
-    ArrayList<Extension> extensions = new ArrayList<>();
-    for (Extension extension : optionsExt.get(com.vladsch.flexmark.parser.Parser.EXTENSIONS)) {
-      extensions.add(extension);
-    }
-
     return optionsExt;
   }
 
diff --git a/java/com/google/gerrit/server/git/SearchingChangeCacheImpl.java b/java/com/google/gerrit/server/git/SearchingChangeCacheImpl.java
index 196fc61..fed6541 100644
--- a/java/com/google/gerrit/server/git/SearchingChangeCacheImpl.java
+++ b/java/com/google/gerrit/server/git/SearchingChangeCacheImpl.java
@@ -165,7 +165,7 @@
         List<CachedChange> result = new ArrayList<>(cds.size());
         for (ChangeData cd : cds) {
           result.add(
-              new AutoValue_SearchingChangeCacheImpl_CachedChange(cd.change(), cd.getReviewers()));
+              new AutoValue_SearchingChangeCacheImpl_CachedChange(cd.change(), cd.reviewers()));
         }
         return Collections.unmodifiableList(result);
       }
diff --git a/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java b/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
index 0d762c7..2177485 100644
--- a/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/AsyncReceiveCommits.java
@@ -417,6 +417,16 @@
     metrics.latencyPerPush.record(pushType, deltaNanos, NANOSECONDS);
   }
 
+  /**
+   * Sends all messages which have been collected while processing the push to the client.
+   *
+   * @see ReceiveCommits#sendMessages()
+   */
+  @UsedAt(UsedAt.Project.GOOGLE)
+  public void sendMessages() {
+    receiveCommits.sendMessages();
+  }
+
   public ReceivePack getReceivePack() {
     return receivePack;
   }
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index a845e53..404fa3c 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -737,7 +737,7 @@
       Set<BranchNameKey> branches = new HashSet<>();
       for (ReceiveCommand c : cmds) {
         // Most post-update steps should happen in UpdateOneRefOp#postUpdate. The only steps that
-        // should happen in this loop are things that can't happen within one BatchUpdate because
+        // should happen in this loops are things that can't happen within one BatchUpdate because
         // they involve kicking off an additional BatchUpdate.
         if (c.getResult() != OK) {
           continue;
@@ -3238,6 +3238,13 @@
                       ObjectInserter ins = repo.newObjectInserter();
                       ObjectReader reader = ins.newReader();
                       RevWalk rw = new RevWalk(reader)) {
+                    if (ObjectId.zeroId().equals(cmd.getOldId())) {
+                      // The user is creating a new branch. The branch can't contain any changes, so
+                      // auto-closing doesn't apply. Exiting here early to spare any further,
+                      // potentially expensive computation that loop over all commits.
+                      return null;
+                    }
+
                     bu.setRepository(repo, rw, ins);
                     // TODO(dborowitz): Teach BatchUpdate to ignore missing changes.
 
@@ -3247,9 +3254,7 @@
                     rw.reset();
                     rw.sort(RevSort.REVERSE);
                     rw.markStart(newTip);
-                    if (!ObjectId.zeroId().equals(cmd.getOldId())) {
-                      rw.markUninteresting(rw.parseCommit(cmd.getOldId()));
-                    }
+                    rw.markUninteresting(rw.parseCommit(cmd.getOldId()));
 
                     Map<Change.Key, ChangeNotes> byKey = null;
                     List<ReplaceRequest> replaceAndClose = new ArrayList<>();
@@ -3261,6 +3266,8 @@
                     for (RevCommit c; (c = rw.next()) != null; ) {
                       rw.parseBody(c);
 
+                      // Check if change refs point to this commit. Usually there are 0-1 change
+                      // refs pointing to this commit.
                       for (Ref ref :
                           receivePackRefCache.tipsFromObjectId(c.copy(), RefNames.REFS_CHANGES)) {
                         PatchSet.Id psId = PatchSet.Id.fromRef(ref.getName());
@@ -3360,6 +3367,8 @@
         logger.atSevere().withCause(e).log("Can't insert patchset");
       } catch (UpdateException e) {
         logger.atSevere().withCause(e).log("Failed to auto-close changes");
+      } finally {
+        logger.atFine().log("Done auto-closing changes");
       }
     }
   }
diff --git a/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java b/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
index 4b538f3..07b58f2 100644
--- a/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
+++ b/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
@@ -19,7 +19,6 @@
 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
 
 import com.google.auto.value.AutoValue;
-import com.google.common.collect.Sets;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.Comment;
@@ -37,7 +36,6 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.ObjectId;
@@ -191,7 +189,6 @@
       RevWalk rw, ObjectInserter ins, ObjectId curr, CommitBuilder cb)
       throws ConfigInvalidException, IOException {
     RevisionNoteMap<ChangeRevisionNote> rnm = getRevisionNoteMap(rw, curr);
-    Set<ObjectId> updatedCommits = Sets.newHashSetWithExpectedSize(rnm.revisionNotes.size());
     RevisionNoteBuilder.Cache cache = new RevisionNoteBuilder.Cache(rnm);
 
     for (Comment c : put) {
@@ -207,7 +204,6 @@
     Map<ObjectId, RevisionNoteBuilder> builders = cache.getBuilders();
     boolean touchedAnyRevs = false;
     for (Map.Entry<ObjectId, RevisionNoteBuilder> e : builders.entrySet()) {
-      updatedCommits.add(e.getKey());
       ObjectId id = e.getKey();
       byte[] data = e.getValue().build(noteUtil.getChangeNoteJson());
       if (!Arrays.equals(data, e.getValue().baseRaw)) {
diff --git a/java/com/google/gerrit/server/patch/PatchList.java b/java/com/google/gerrit/server/patch/PatchList.java
index c9e45ba..28f61d3 100644
--- a/java/com/google/gerrit/server/patch/PatchList.java
+++ b/java/com/google/gerrit/server/patch/PatchList.java
@@ -45,23 +45,12 @@
 public class PatchList implements Serializable {
   private static final long serialVersionUID = PatchListKey.serialVersionUID;
 
-  private static final Comparator<PatchListEntry> PATCH_CMP =
-      Comparator.comparing(PatchListEntry::getNewName, PatchList::comparePaths);
-
   @VisibleForTesting
-  static int comparePaths(String a, String b) {
-    int m1 = Patch.isMagic(a) ? (a.equals(Patch.MERGE_LIST) ? 2 : 1) : 3;
-    int m2 = Patch.isMagic(b) ? (b.equals(Patch.MERGE_LIST) ? 2 : 1) : 3;
+  static final Comparator<String> FILE_PATH_CMP =
+      Comparator.comparing(Patch::isMagic).reversed().thenComparing(Comparator.naturalOrder());
 
-    if (m1 != m2) {
-      return m1 - m2;
-    } else if (m1 < 3) {
-      return 0;
-    }
-
-    // m1 == m2 == 3: normal names.
-    return a.compareTo(b);
-  }
+  private static final Comparator<PatchListEntry> PATCH_CMP =
+      Comparator.comparing(PatchListEntry::getNewName, FILE_PATH_CMP);
 
   @Nullable private transient ObjectId oldId;
   private transient ObjectId newId;
diff --git a/java/com/google/gerrit/server/permissions/ChangeControl.java b/java/com/google/gerrit/server/permissions/ChangeControl.java
index 143547b..9f216c0 100644
--- a/java/com/google/gerrit/server/permissions/ChangeControl.java
+++ b/java/com/google/gerrit/server/permissions/ChangeControl.java
@@ -19,21 +19,16 @@
 
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
-import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRange;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.Change;
-import com.google.gerrit.entities.Project;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.conditions.BooleanCondition;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.permissions.PermissionBackend.ForChange;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
 import java.util.Collection;
 import java.util.EnumSet;
 import java.util.Map;
@@ -41,39 +36,16 @@
 
 /** Access control management for a user accessing a single change. */
 class ChangeControl {
-  @Singleton
-  static class Factory {
-    private final ChangeData.Factory changeDataFactory;
-    private final ChangeNotes.Factory notesFactory;
-
-    @Inject
-    Factory(ChangeData.Factory changeDataFactory, ChangeNotes.Factory notesFactory) {
-      this.changeDataFactory = changeDataFactory;
-      this.notesFactory = notesFactory;
-    }
-
-    ChangeControl create(RefControl refControl, Project.NameKey project, Change.Id changeId) {
-      return create(refControl, notesFactory.create(project, changeId));
-    }
-
-    ChangeControl create(RefControl refControl, ChangeNotes notes) {
-      return new ChangeControl(changeDataFactory, refControl, notes);
-    }
-  }
-
-  private final ChangeData.Factory changeDataFactory;
   private final RefControl refControl;
-  private final ChangeNotes notes;
+  private final ChangeData changeData;
 
-  private ChangeControl(
-      ChangeData.Factory changeDataFactory, RefControl refControl, ChangeNotes notes) {
-    this.changeDataFactory = changeDataFactory;
+  ChangeControl(RefControl refControl, ChangeData changeData) {
     this.refControl = refControl;
-    this.notes = notes;
+    this.changeData = changeData;
   }
 
-  ForChange asForChange(@Nullable ChangeData cd) {
-    return new ForChangeImpl(cd);
+  ForChange asForChange() {
+    return new ForChangeImpl();
   }
 
   private CurrentUser getUser() {
@@ -85,7 +57,7 @@
   }
 
   private Change getChange() {
-    return notes.getChange();
+    return changeData.change();
   }
 
   /** Can this user see this change? */
@@ -224,19 +196,13 @@
   }
 
   private class ForChangeImpl extends ForChange {
-    private ChangeData cd;
     private Map<String, PermissionRange> labels;
     private String resourcePath;
 
-    ForChangeImpl(@Nullable ChangeData cd) {
-      this.cd = cd;
-    }
+    private ForChangeImpl() {}
 
     private ChangeData changeData() {
-      if (cd == null) {
-        cd = changeDataFactory.create(notes);
-      }
-      return cd;
+      return changeData;
     }
 
     @Override
diff --git a/java/com/google/gerrit/server/permissions/DefaultPermissionBackendModule.java b/java/com/google/gerrit/server/permissions/DefaultPermissionBackendModule.java
index f3a3c78..3f84dff 100644
--- a/java/com/google/gerrit/server/permissions/DefaultPermissionBackendModule.java
+++ b/java/com/google/gerrit/server/permissions/DefaultPermissionBackendModule.java
@@ -31,7 +31,6 @@
       // TODO(hiesel) Hide ProjectControl, RefControl, ChangeControl related bindings.
       factory(ProjectControl.Factory.class);
       factory(DefaultRefFilter.Factory.class);
-      bind(ChangeControl.Factory.class);
     }
   }
 }
diff --git a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
index 90d9d88..4e1a30c 100644
--- a/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
+++ b/java/com/google/gerrit/server/permissions/DefaultRefFilter.java
@@ -448,12 +448,11 @@
     try {
       Map<Change.Id, BranchNameKey> visibleChanges = new HashMap<>();
       for (ChangeData cd : changeCache.getChangeData(project)) {
-        ChangeNotes notes = changeNotesFactory.createFromIndexedChange(cd.change());
         if (!projectState.statePermitsRead()) {
           continue;
         }
         try {
-          permissionBackendForProject.indexedChange(cd, notes).check(ChangePermission.READ);
+          permissionBackendForProject.change(cd).check(ChangePermission.READ);
           visibleChanges.put(cd.getId(), cd.change().getDest());
         } catch (AuthException e) {
           // Do nothing.
diff --git a/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java b/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java
index 2344781..749ca6b 100644
--- a/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java
+++ b/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java
@@ -173,11 +173,6 @@
     }
 
     @Override
-    public ForChange indexedChange(ChangeData cd, ChangeNotes notes) {
-      return new FailedChange(message, cause);
-    }
-
-    @Override
     public void check(RefPermission perm) throws PermissionBackendException {
       throw new PermissionBackendException(message, cause);
     }
diff --git a/java/com/google/gerrit/server/permissions/PermissionBackend.java b/java/com/google/gerrit/server/permissions/PermissionBackend.java
index 653c3b5f..23145ba 100644
--- a/java/com/google/gerrit/server/permissions/PermissionBackend.java
+++ b/java/com/google/gerrit/server/permissions/PermissionBackend.java
@@ -173,15 +173,6 @@
       return ref(notes.getChange().getDest()).change(notes);
     }
 
-    /**
-     * Returns an instance scoped for the change loaded from index, and its destination ref and
-     * project. This method should only be used when database access is harmful and potentially
-     * stale data from the index is acceptable.
-     */
-    public ForChange indexedChange(ChangeData cd, ChangeNotes notes) {
-      return ref(notes.getChange().getDest()).indexedChange(cd, notes);
-    }
-
     /** Verify scoped user can {@code perm}, throwing if denied. */
     public abstract void check(GlobalOrPluginPermission perm)
         throws AuthException, PermissionBackendException;
@@ -289,15 +280,6 @@
       return ref(notes.getChange().getDest().branch()).change(notes);
     }
 
-    /**
-     * Returns an instance scoped for the change loaded from index, and its destination ref and
-     * project. This method should only be used when database access is harmful and potentially
-     * stale data from the index is acceptable.
-     */
-    public ForChange indexedChange(ChangeData cd, ChangeNotes notes) {
-      return ref(notes.getChange().getDest().branch()).indexedChange(cd, notes);
-    }
-
     /** Verify scoped user can {@code perm}, throwing if denied. */
     public abstract void check(CoreOrPluginProjectPermission perm)
         throws AuthException, PermissionBackendException;
@@ -386,12 +368,6 @@
     /** Returns an instance scoped to change. */
     public abstract ForChange change(ChangeNotes notes);
 
-    /**
-     * @return instance scoped to change loaded from index. This method should only be used when
-     *     database access is harmful and potentially stale data from the index is acceptable.
-     */
-    public abstract ForChange indexedChange(ChangeData cd, ChangeNotes notes);
-
     /** Verify scoped user can {@code perm}, throwing if denied. */
     public abstract void check(RefPermission perm) throws AuthException, PermissionBackendException;
 
diff --git a/java/com/google/gerrit/server/permissions/ProjectControl.java b/java/com/google/gerrit/server/permissions/ProjectControl.java
index 145e0b6..e6d66ee 100644
--- a/java/com/google/gerrit/server/permissions/ProjectControl.java
+++ b/java/com/google/gerrit/server/permissions/ProjectControl.java
@@ -70,9 +70,9 @@
   private final PermissionBackend permissionBackend;
   private final CurrentUser user;
   private final ProjectState state;
-  private final ChangeControl.Factory changeControlFactory;
   private final PermissionCollection.Factory permissionFilter;
   private final DefaultRefFilter.Factory refFilterFactory;
+  private final ChangeData.Factory changeDataFactory;
 
   private List<SectionMatcher> allSections;
   private Map<String, RefControl> refControls;
@@ -83,17 +83,17 @@
       @GitUploadPackGroups Set<AccountGroup.UUID> uploadGroups,
       @GitReceivePackGroups Set<AccountGroup.UUID> receiveGroups,
       PermissionCollection.Factory permissionFilter,
-      ChangeControl.Factory changeControlFactory,
       PermissionBackend permissionBackend,
       DefaultRefFilter.Factory refFilterFactory,
+      ChangeData.Factory changeDataFactory,
       @Assisted CurrentUser who,
       @Assisted ProjectState ps) {
-    this.changeControlFactory = changeControlFactory;
     this.uploadGroups = uploadGroups;
     this.receiveGroups = receiveGroups;
     this.permissionFilter = permissionFilter;
     this.permissionBackend = permissionBackend;
     this.refFilterFactory = refFilterFactory;
+    this.changeDataFactory = changeDataFactory;
     user = who;
     state = ps;
   }
@@ -102,13 +102,8 @@
     return new ForProjectImpl();
   }
 
-  ChangeControl controlFor(Change change) {
-    return changeControlFactory.create(
-        controlForRef(change.getDest()), change.getProject(), change.getId());
-  }
-
-  ChangeControl controlFor(ChangeNotes notes) {
-    return changeControlFactory.create(controlForRef(notes.getChange().getDest()), notes);
+  ChangeControl controlFor(ChangeData cd) {
+    return new ChangeControl(controlForRef(cd.change().getDest()), cd);
   }
 
   RefControl controlForRef(BranchNameKey ref) {
@@ -122,7 +117,7 @@
     RefControl ctl = refControls.get(refName);
     if (ctl == null) {
       PermissionCollection relevant = permissionFilter.filter(access(), refName, user);
-      ctl = new RefControl(this, refName, relevant);
+      ctl = new RefControl(changeDataFactory, this, refName, relevant);
       refControls.put(refName, ctl);
     }
     return ctl;
diff --git a/java/com/google/gerrit/server/permissions/RefControl.java b/java/com/google/gerrit/server/permissions/RefControl.java
index 7c5d6bd..5081116 100644
--- a/java/com/google/gerrit/server/permissions/RefControl.java
+++ b/java/com/google/gerrit/server/permissions/RefControl.java
@@ -43,6 +43,7 @@
 class RefControl {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
+  private final ChangeData.Factory changeDataFactory;
   private final ProjectControl projectControl;
   private final String refName;
 
@@ -58,7 +59,12 @@
   private Boolean canForgeCommitter;
   private Boolean isVisible;
 
-  RefControl(ProjectControl projectControl, String ref, PermissionCollection relevant) {
+  RefControl(
+      ChangeData.Factory changeDataFactory,
+      ProjectControl projectControl,
+      String ref,
+      PermissionCollection relevant) {
+    this.changeDataFactory = changeDataFactory;
     this.projectControl = projectControl;
     this.refName = ref;
     this.relevant = relevant;
@@ -444,7 +450,7 @@
     @Override
     public ForChange change(ChangeData cd) {
       try {
-        return getProjectControl().controlFor(cd.notes()).asForChange(cd);
+        return getProjectControl().controlFor(cd).asForChange();
       } catch (StorageException e) {
         return FailedPermissionBackend.change("unavailable", e);
       }
@@ -459,12 +465,9 @@
           "expected change in project %s, not %s",
           project,
           change.getProject());
-      return getProjectControl().controlFor(notes).asForChange(null);
-    }
-
-    @Override
-    public ForChange indexedChange(ChangeData cd, ChangeNotes notes) {
-      return getProjectControl().controlFor(notes).asForChange(cd);
+      // Having ChangeNotes means it's OK to load values from NoteDb if needed.
+      // ChangeData.Factory will allow lazyLoading
+      return getProjectControl().controlFor(changeDataFactory.create(notes)).asForChange();
     }
 
     @Override
diff --git a/java/com/google/gerrit/server/query/change/ChangeData.java b/java/com/google/gerrit/server/query/change/ChangeData.java
index 69f1a4e..01d2c31 100644
--- a/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -675,7 +675,9 @@
   public ReviewerSet reviewers() {
     if (reviewers == null) {
       if (!lazyLoad) {
-        return ReviewerSet.empty();
+        // We are not allowed to load values from NoteDb. Reviewers were not populated with values
+        // from the index. However, we need these values for permission checks.
+        throw new IllegalStateException("reviewers not populated");
       }
       reviewers = approvalsUtil.getReviewers(notes(), approvals().values());
     }
@@ -686,10 +688,6 @@
     this.reviewers = reviewers;
   }
 
-  public ReviewerSet getReviewers() {
-    return reviewers;
-  }
-
   public ReviewerByEmailSet reviewersByEmail() {
     if (reviewersByEmail == null) {
       if (!lazyLoad) {
diff --git a/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java b/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java
index 40a3a07..c6bcd60 100644
--- a/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ChangeIsVisibleToPredicate.java
@@ -73,7 +73,6 @@
       return false;
     }
 
-    ChangeNotes notes = notesFactory.createFromIndexedChange(change);
     Optional<ProjectState> projectState = projectCache.get(cd.project());
     if (!projectState.isPresent()) {
       logger.atFine().log("Filter out change %s of non-existing project %s", cd, cd.project());
@@ -92,7 +91,7 @@
                     .filter(u -> u instanceof SingleGroupUser || u instanceof InternalUser)
                     .orElseGet(anonymousUserProvider::get));
     try {
-      withUser.indexedChange(cd, notes).check(ChangePermission.READ);
+      withUser.change(cd).check(ChangePermission.READ);
     } catch (PermissionBackendException e) {
       Throwable cause = e.getCause();
       if (cause instanceof RepositoryNotFoundException) {
diff --git a/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java b/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java
index 5b7245d..42032f7 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import static com.google.gerrit.entities.Patch.PATCHSET_LEVEL;
 import static com.google.gerrit.server.CommentsUtil.setCommentCommitId;
 
 import com.google.common.base.Strings;
@@ -73,6 +74,9 @@
       throw new BadRequestException("path must be non-empty");
     } else if (in.message == null || in.message.trim().isEmpty()) {
       throw new BadRequestException("message must be non-empty");
+    } else if (in.path.equals(PATCHSET_LEVEL)
+        && (in.side != null || in.range != null || in.line != null)) {
+      throw new BadRequestException("patchset-level comments can't have side, range, or line");
     } else if (in.line != null && in.line < 0) {
       throw new BadRequestException("line must be >= 0");
     } else if (in.line != null && in.range != null && in.line != in.range.endLine) {
diff --git a/java/com/google/gerrit/server/restapi/change/ListChangeComments.java b/java/com/google/gerrit/server/restapi/change/ListChangeComments.java
index e544509..409b0d5 100644
--- a/java/com/google/gerrit/server/restapi/change/ListChangeComments.java
+++ b/java/com/google/gerrit/server/restapi/change/ListChangeComments.java
@@ -73,7 +73,7 @@
       throws PermissionBackendException {
     ImmutableList<CommentInfo> commentInfos = getCommentFormatter().formatAsList(comments);
     List<ChangeMessage> changeMessages = changeMessagesUtil.byChange(rsrc.getNotes());
-    CommentsUtil.linkCommentsToChangeMessages(commentInfos, changeMessages);
+    CommentsUtil.linkCommentsToChangeMessages(commentInfos, changeMessages, true);
     return commentInfos;
   }
 
@@ -83,7 +83,7 @@
     List<CommentInfo> commentInfos =
         commentInfosMap.values().stream().flatMap(List::stream).collect(toList());
     List<ChangeMessage> changeMessages = changeMessagesUtil.byChange(rsrc.getNotes());
-    CommentsUtil.linkCommentsToChangeMessages(commentInfos, changeMessages);
+    CommentsUtil.linkCommentsToChangeMessages(commentInfos, changeMessages, true);
     return commentInfosMap;
   }
 
diff --git a/java/com/google/gerrit/server/restapi/change/ListChangeRobotComments.java b/java/com/google/gerrit/server/restapi/change/ListChangeRobotComments.java
index 0ed7d60..d841183 100644
--- a/java/com/google/gerrit/server/restapi/change/ListChangeRobotComments.java
+++ b/java/com/google/gerrit/server/restapi/change/ListChangeRobotComments.java
@@ -63,7 +63,7 @@
     List<RobotCommentInfo> commentInfos =
         robotCommentsMap.values().stream().flatMap(List::stream).collect(toList());
     List<ChangeMessage> changeMessages = changeMessagesUtil.byChange(rsrc.getNotes());
-    CommentsUtil.linkCommentsToChangeMessages(commentInfos, changeMessages);
+    CommentsUtil.linkCommentsToChangeMessages(commentInfos, changeMessages, false);
     return Response.ok(robotCommentsMap);
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/change/ListRobotComments.java b/java/com/google/gerrit/server/restapi/change/ListRobotComments.java
index 742eaca..25f4005 100644
--- a/java/com/google/gerrit/server/restapi/change/ListRobotComments.java
+++ b/java/com/google/gerrit/server/restapi/change/ListRobotComments.java
@@ -64,7 +64,7 @@
       Iterable<RobotComment> comments, RevisionResource rsrc) throws PermissionBackendException {
     ImmutableList<RobotCommentInfo> commentInfos = getCommentFormatter().formatAsList(comments);
     List<ChangeMessage> changeMessages = changeMessagesUtil.byChange(rsrc.getNotes());
-    CommentsUtil.linkCommentsToChangeMessages(commentInfos, changeMessages);
+    CommentsUtil.linkCommentsToChangeMessages(commentInfos, changeMessages, false);
     return commentInfos;
   }
 
@@ -74,7 +74,7 @@
     List<RobotCommentInfo> commentInfos =
         commentInfosMap.values().stream().flatMap(List::stream).collect(toList());
     List<ChangeMessage> changeMessages = changeMessagesUtil.byChange(rsrc.getNotes());
-    CommentsUtil.linkCommentsToChangeMessages(commentInfos, changeMessages);
+    CommentsUtil.linkCommentsToChangeMessages(commentInfos, changeMessages, false);
     return commentInfosMap;
   }
 
diff --git a/java/com/google/gerrit/server/restapi/change/PostReview.java b/java/com/google/gerrit/server/restapi/change/PostReview.java
index 7008bb9..1e2e644 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReview.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReview.java
@@ -17,6 +17,7 @@
 import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.gerrit.entities.Patch.PATCHSET_LEVEL;
 import static com.google.gerrit.server.CommentsUtil.setCommentCommitId;
 import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
 import static com.google.gerrit.server.permissions.LabelPermission.ForUser.ON_BEHALF_OF;
@@ -606,6 +607,7 @@
         ensureLineIsNonNegative(comment.line, path);
         ensureCommentNotOnMagicFilesOfAutoMerge(path, comment);
         ensureRangeIsValid(path, comment.range);
+        ensureValidPatchsetLevelComment(path, comment);
       }
     }
   }
@@ -644,6 +646,14 @@
     }
   }
 
+  private static <T extends CommentInput> void ensureValidPatchsetLevelComment(
+      String path, T comment) throws BadRequestException {
+    if (path.equals(PATCHSET_LEVEL)
+        && (comment.side != null || comment.range != null || comment.line != null)) {
+      throw new BadRequestException("Patchset-level comments can't have side, range, or line");
+    }
+  }
+
   private void checkRobotComments(
       RevisionResource revision, Map<String, List<RobotCommentInput>> in)
       throws BadRequestException, PatchListNotAvailableException {
@@ -703,7 +713,7 @@
     ensureReplacementsArePresent(commentPath, fixReplacementInfos);
 
     for (FixReplacementInfo fixReplacementInfo : fixReplacementInfos) {
-      ensureReplacementPathIsSet(commentPath, fixReplacementInfo.path);
+      ensureReplacementPathIsSetAndNotPatchsetLevel(commentPath, fixReplacementInfo.path);
       ensureRangeIsSet(commentPath, fixReplacementInfo.range);
       ensureRangeIsValid(commentPath, fixReplacementInfo.range);
       ensureReplacementStringIsSet(commentPath, fixReplacementInfo.replacement);
@@ -727,14 +737,20 @@
     }
   }
 
-  private static void ensureReplacementPathIsSet(String commentPath, String replacementPath)
-      throws BadRequestException {
+  private static void ensureReplacementPathIsSetAndNotPatchsetLevel(
+      String commentPath, String replacementPath) throws BadRequestException {
     if (replacementPath == null) {
       throw new BadRequestException(
           String.format(
               "A file path must be given for the replacement of the robot comment on %s",
               commentPath));
     }
+    if (replacementPath.equals(PATCHSET_LEVEL)) {
+      throw new BadRequestException(
+          String.format(
+              "A file path must not be %s for the replacement of the robot comment on %s",
+              PATCHSET_LEVEL, commentPath));
+    }
   }
 
   private static void ensureRangeIsSet(String commentPath, Range range) throws BadRequestException {
diff --git a/java/com/google/gerrit/server/restapi/change/PutDraftComment.java b/java/com/google/gerrit/server/restapi/change/PutDraftComment.java
index 63cd7a3..ea58365 100644
--- a/java/com/google/gerrit/server/restapi/change/PutDraftComment.java
+++ b/java/com/google/gerrit/server/restapi/change/PutDraftComment.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.restapi.change;
 
+import static com.google.gerrit.entities.Patch.PATCHSET_LEVEL;
 import static com.google.gerrit.server.CommentsUtil.setCommentCommitId;
 
 import com.google.gerrit.entities.Comment;
@@ -79,6 +80,9 @@
       throw new BadRequestException("id must match URL");
     } else if (in.line != null && in.line < 0) {
       throw new BadRequestException("line must be >= 0");
+    } else if (in.path.equals(PATCHSET_LEVEL)
+        && (in.side != null || in.range != null || in.line != null)) {
+      throw new BadRequestException("patchset-level comments can't have side, range, or line");
     } else if (in.line != null && in.range != null && in.line != in.range.endLine) {
       throw new BadRequestException("range endLine must be on the same line as the comment");
     }
diff --git a/java/com/google/gerrit/server/restapi/config/GetDiffPreferences.java b/java/com/google/gerrit/server/restapi/config/GetDiffPreferences.java
index 83b0262..8185281 100644
--- a/java/com/google/gerrit/server/restapi/config/GetDiffPreferences.java
+++ b/java/com/google/gerrit/server/restapi/config/GetDiffPreferences.java
@@ -19,9 +19,9 @@
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.server.account.StoredPreferences;
 import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.config.DefaultPreferencesCache;
+import com.google.gerrit.server.config.PreferencesParserUtil;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -41,7 +41,7 @@
   public Response<DiffPreferencesInfo> apply(ConfigResource configResource)
       throws BadRequestException, ResourceConflictException, IOException, ConfigInvalidException {
     return Response.ok(
-        StoredPreferences.parseDiffPreferences(
+        PreferencesParserUtil.parseDiffPreferences(
             defaultPreferenceCache.get().asConfig(), null, null));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/config/GetEditPreferences.java b/java/com/google/gerrit/server/restapi/config/GetEditPreferences.java
index 95fc10e..bb9e483 100644
--- a/java/com/google/gerrit/server/restapi/config/GetEditPreferences.java
+++ b/java/com/google/gerrit/server/restapi/config/GetEditPreferences.java
@@ -19,9 +19,9 @@
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.server.account.StoredPreferences;
 import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.config.DefaultPreferencesCache;
+import com.google.gerrit.server.config.PreferencesParserUtil;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -40,7 +40,7 @@
   public Response<EditPreferencesInfo> apply(ConfigResource configResource)
       throws BadRequestException, ResourceConflictException, IOException, ConfigInvalidException {
     return Response.ok(
-        StoredPreferences.parseEditPreferences(
+        PreferencesParserUtil.parseEditPreferences(
             defaultPreferenceCache.get().asConfig(), null, null));
   }
 }
diff --git a/java/com/google/gerrit/server/restapi/config/GetPreferences.java b/java/com/google/gerrit/server/restapi/config/GetPreferences.java
index 8a28d55..288055b 100644
--- a/java/com/google/gerrit/server/restapi/config/GetPreferences.java
+++ b/java/com/google/gerrit/server/restapi/config/GetPreferences.java
@@ -17,9 +17,9 @@
 import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.server.account.StoredPreferences;
 import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.config.DefaultPreferencesCache;
+import com.google.gerrit.server.config.PreferencesParserUtil;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -38,7 +38,7 @@
   public Response<GeneralPreferencesInfo> apply(ConfigResource rsrc)
       throws IOException, ConfigInvalidException {
     return Response.ok(
-        StoredPreferences.parseGeneralPreferences(
+        PreferencesParserUtil.parseGeneralPreferences(
             defaultPreferenceCache.get().asConfig(), null, null));
   }
 }
diff --git a/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java b/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java
index 33c3584..edc3725 100644
--- a/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java
+++ b/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java
@@ -17,6 +17,7 @@
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.gerrit.server.submit.CommitMergeStatus.EMPTY_COMMIT;
 import static com.google.gerrit.server.submit.CommitMergeStatus.SKIPPED_IDENTICAL_TREE;
+import static java.util.stream.Collectors.toList;
 
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.entities.BooleanProjectConfig;
@@ -62,20 +63,42 @@
     } catch (IOException | StorageException e) {
       throw new StorageException("Commit sorting failed", e);
     }
-    List<SubmitStrategyOp> ops = new ArrayList<>(sorted.size());
-    boolean first = true;
 
+    // We cannot rebase merge commits. This is why we integrate merge changes into the target branch
+    // the same way as if MERGE_IF_NECESSARY was the submit strategy. This means if needed we create
+    // a merge commit that integrates the merge change into the target branch.
+    // If we integrate a change series that consists out of a normal change and a merge change,
+    // where the merge change depends on the normal change, we must skip rebasing the normal change,
+    // because it already gets integrated by merging the merge change. If the rebasing of the normal
+    // change is not skipped, it would appear twice in the history after the submit is done (once
+    // through its rebased commit, and once through its original commit which is a parent of the
+    // merge change that was merged into the target branch. To skip the rebasing of the normal
+    // change, we call MergeUtil#reduceToMinimalMerge, as it excludes commits which will be
+    // implicitly integrated by merging the series. Then we use the MergeIfNecessaryOp to integrate
+    // the whole series.
+    // If on the other hand, we integrate a change series that consists out of a merge change and a
+    // normal change, where the normal change depends on the merge change, we can first integrate
+    // the merge change by a merge and then integrate the normal change by a rebase. In this case we
+    // do not want to call MergeUtil#reduceToMinimalMerge as we are not intending to integrate the
+    // whole series by a merge, but rather do the integration of the commits one by one.
+    boolean foundNonMerge = false;
     for (CodeReviewCommit c : sorted) {
       if (c.getParentCount() > 1) {
-        // Since there is a merge commit, sort and prune again using
-        // MERGE_IF_NECESSARY semantics to avoid creating duplicate
-        // commits.
-        //
+        if (!foundNonMerge) {
+          // found a merge change, but it doesn't depend on a normal change, this means we are not
+          // required to merge the whole series at once
+          continue;
+        }
+        // found a merge commit that depends on a normal change, this means we are required to merge
+        // the whole series at once
         sorted = args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, sorted);
-        break;
+        return sorted.stream().map(n -> new MergeIfNecessaryOp(n)).collect(toList());
       }
+      foundNonMerge = true;
     }
 
+    List<SubmitStrategyOp> ops = new ArrayList<>(sorted.size());
+    boolean first = true;
     while (!sorted.isEmpty()) {
       CodeReviewCommit n = sorted.remove(0);
       if (first && args.mergeTip.getInitialTip() == null) {
@@ -87,7 +110,7 @@
       } else if (n.getParentCount() == 1) {
         ops.add(new RebaseOneOp(n));
       } else {
-        ops.add(new RebaseMultipleParentsOp(n));
+        ops.add(new MergeIfNecessaryOp(n));
       }
       first = false;
     }
@@ -254,8 +277,8 @@
     }
   }
 
-  private class RebaseMultipleParentsOp extends SubmitStrategyOp {
-    private RebaseMultipleParentsOp(CodeReviewCommit toMerge) {
+  private class MergeIfNecessaryOp extends SubmitStrategyOp {
+    private MergeIfNecessaryOp(CodeReviewCommit toMerge) {
       super(RebaseSubmitStrategy.this.args, toMerge);
     }
 
diff --git a/java/com/google/gerrit/testing/TestCommentHelper.java b/java/com/google/gerrit/testing/TestCommentHelper.java
index 5ce6d13..bd859db 100644
--- a/java/com/google/gerrit/testing/TestCommentHelper.java
+++ b/java/com/google/gerrit/testing/TestCommentHelper.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.extensions.client.Side;
 import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.common.FixSuggestionInfo;
+import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.inject.Inject;
 import java.util.Arrays;
 import java.util.Collection;
@@ -116,7 +117,6 @@
     RobotCommentInput in = new RobotCommentInput();
     in.robotId = "happyRobot";
     in.robotRunId = "1";
-    in.line = 1;
     in.message = "nit: trailing whitespace";
     in.path = path;
     return in;
@@ -144,6 +144,7 @@
     reviewInput.robotComments =
         Collections.singletonMap(robotCommentInput.path, ImmutableList.of(robotCommentInput));
     reviewInput.message = message;
+    reviewInput.tag = ChangeMessagesUtil.AUTOGENERATED_TAG_PREFIX;
     gApi.changes().id(targetChangeId).current().review(reviewInput);
   }
 }
diff --git a/java/com/google/gerrit/truth/MapSubject.java b/java/com/google/gerrit/truth/MapSubject.java
index 8217920..95a0e0c 100644
--- a/java/com/google/gerrit/truth/MapSubject.java
+++ b/java/com/google/gerrit/truth/MapSubject.java
@@ -1,18 +1,16 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
 
 package com.google.gerrit.truth;
 
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 5c786a5..82a19d4 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -4382,6 +4382,7 @@
           .message("Modify rules.pl")
           .create();
     }
+    projectCache.evict(project);
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java b/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java
index 24d08db..0ac7e20 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/RevertIT.java
@@ -1283,6 +1283,7 @@
           .message("Modify rules.pl")
           .create();
     }
+    projectCache.evict(project);
   }
 
   private List<ChangeApi> getChangeApis(RevertSubmissionInfo revertSubmissionInfo)
diff --git a/javatests/com/google/gerrit/acceptance/api/change/SubmitTypeRuleIT.java b/javatests/com/google/gerrit/acceptance/api/change/SubmitTypeRuleIT.java
index dab2d00..a9afcbc 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/SubmitTypeRuleIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/SubmitTypeRuleIT.java
@@ -106,6 +106,7 @@
       r.rule = rule;
       r.commit(md);
     }
+    projectCache.evict(project);
   }
 
   private static final String SUBMIT_TYPE_FROM_SUBJECT =
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/GetBlameIT.java b/javatests/com/google/gerrit/acceptance/api/revision/GetBlameIT.java
index 8dfebad..62140ed 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/GetBlameIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/GetBlameIT.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.acceptance.api.revision;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.entities.Patch.PATCHSET_LEVEL;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
@@ -35,6 +36,16 @@
   }
 
   @Test
+  public void forPatchsetLevelFile() throws Exception {
+    PushOneCommit.Result r = createChange("Test Change", "foo.txt", "FOO");
+    List<BlameInfo> blameInfos =
+        gApi.changes().id(r.getChangeId()).current().file(PATCHSET_LEVEL).blameRequest().get();
+
+    // File doesn't exist in commit.
+    assertThat(blameInfos).isEmpty();
+  }
+
+  @Test
   public void forNonExistingFileFromBase() throws Exception {
     PushOneCommit.Result r = createChange("Test Change", "foo.txt", "FOO");
     List<BlameInfo> blameInfos =
@@ -51,6 +62,22 @@
   }
 
   @Test
+  public void forPatchsetLevelFileFromBase() throws Exception {
+    PushOneCommit.Result r = createChange("Test Change", "foo.txt", "FOO");
+    List<BlameInfo> blameInfos =
+        gApi.changes()
+            .id(r.getChangeId())
+            .current()
+            .file(PATCHSET_LEVEL)
+            .blameRequest()
+            .forBase(true)
+            .get();
+
+    // File doesn't exist in base commit.
+    assertThat(blameInfos).isEmpty();
+  }
+
+  @Test
   public void forNewlyAddedFile() throws Exception {
     PushOneCommit.Result r = createChange("Test Change", "foo.txt", "FOO");
     List<BlameInfo> blameInfos =
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
index 717d3cc..5684b1f 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
@@ -18,6 +18,7 @@
 import static com.google.common.truth.TruthJUnit.assume;
 import static com.google.gerrit.entities.Patch.COMMIT_MSG;
 import static com.google.gerrit.entities.Patch.MERGE_LIST;
+import static com.google.gerrit.entities.Patch.PATCHSET_LEVEL;
 import static com.google.gerrit.extensions.common.testing.DiffInfoSubject.assertThat;
 import static com.google.gerrit.extensions.common.testing.FileInfoSubject.assertThat;
 import static com.google.gerrit.git.ObjectIds.abbreviateName;
@@ -121,6 +122,24 @@
   }
 
   @Test
+  public void patchsetLevelFileDiffIsEmpty() throws Exception {
+    PushOneCommit.Result result = createChange();
+    DiffInfo diffForPatchsetLevelFile =
+        gApi.changes()
+            .id(result.getChangeId())
+            .revision(result.getCommit().name())
+            .file(PATCHSET_LEVEL)
+            .diff();
+    // This behavior is the same as the behavior for non-existent files.
+    assertThat(diffForPatchsetLevelFile).binary().isNull();
+    assertThat(diffForPatchsetLevelFile).content().isEmpty();
+    assertThat(diffForPatchsetLevelFile).diffHeader().isNull();
+    assertThat(diffForPatchsetLevelFile).metaA().isNull();
+    assertThat(diffForPatchsetLevelFile).metaB().isNull();
+    assertThat(diffForPatchsetLevelFile).webLinks().isNull();
+  }
+
+  @Test
   public void deletedFileIsIncludedInDiff() throws Exception {
     gApi.changes().id(changeId).edit().deleteFile(FILE_NAME);
     gApi.changes().id(changeId).edit().publish();
@@ -353,6 +372,31 @@
   }
 
   @Test
+  public void copiedFileDetectedIfOriginalFileIsRenamedInDiff() throws Exception {
+    /*
+     * Copies are detected when a file is deleted and more than 1 file with the same content are
+     * added. In this case, the added file with the closest name to the original file is tagged as a
+     * rename and the remaining files are considered copies. This implementation is done by JGit in
+     * the RenameDetector component.
+     */
+    String renamedFileName = "renamed_some_file.txt";
+    String copyFileName1 = "copy1_with_different_name.txt";
+    String copyFileName2 = "copy2_with_different_name.txt";
+    gApi.changes().id(changeId).edit().modifyFile(copyFileName1, RawInputUtil.create(FILE_CONTENT));
+    gApi.changes().id(changeId).edit().modifyFile(copyFileName2, RawInputUtil.create(FILE_CONTENT));
+    gApi.changes().id(changeId).edit().renameFile(FILE_NAME, renamedFileName);
+    gApi.changes().id(changeId).edit().publish();
+
+    Map<String, FileInfo> changedFiles = gApi.changes().id(changeId).current().files();
+
+    assertThat(changedFiles.keySet())
+        .containsExactly("/COMMIT_MSG", renamedFileName, copyFileName1, copyFileName2);
+    assertThat(changedFiles.get(renamedFileName).status).isEqualTo('R');
+    assertThat(changedFiles.get(copyFileName1).status).isEqualTo('C');
+    assertThat(changedFiles.get(copyFileName2).status).isEqualTo('C');
+  }
+
+  @Test
   public void addedBinaryFileIsIncludedInDiff() throws Exception {
     String imageFileName = "an_image.png";
     byte[] imageBytes = createRgbImage(255, 0, 0);
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index 74f9134..dd13643 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -24,6 +24,7 @@
 import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
 import static com.google.gerrit.entities.Patch.COMMIT_MSG;
 import static com.google.gerrit.entities.Patch.MERGE_LIST;
+import static com.google.gerrit.entities.Patch.PATCHSET_LEVEL;
 import static com.google.gerrit.extensions.client.ListChangesOption.ALL_REVISIONS;
 import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
 import static com.google.gerrit.git.ObjectIds.abbreviateName;
@@ -1500,6 +1501,19 @@
   }
 
   @Test
+  public void patchsetLevelContentDoesNotExist() throws Exception {
+    PushOneCommit.Result change = createChange();
+    assertThrows(
+        ResourceNotFoundException.class,
+        () ->
+            gApi.changes()
+                .id(change.getChangeId())
+                .revision(change.getCommit().name())
+                .file(PATCHSET_LEVEL)
+                .content());
+  }
+
+  @Test
   public void cannotGetContentOfDirectory() throws Exception {
     Map<String, String> files = ImmutableMap.of("dir/file1.txt", "content 1");
     PushOneCommit.Result result =
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java
index 0b8f441..27b866b 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java
@@ -17,6 +17,7 @@
 import static com.google.common.collect.MoreCollectors.onlyElement;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.PushOneCommit.SUBJECT;
+import static com.google.gerrit.entities.Patch.PATCHSET_LEVEL;
 import static com.google.gerrit.extensions.client.ListChangesOption.MESSAGES;
 import static com.google.gerrit.extensions.common.testing.DiffInfoSubject.assertThat;
 import static com.google.gerrit.extensions.common.testing.EditInfoSubject.assertThat;
@@ -35,6 +36,7 @@
 import com.google.gerrit.extensions.api.changes.PublishChangeEditInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput.RobotCommentInput;
 import com.google.gerrit.extensions.client.Comment;
+import com.google.gerrit.extensions.client.Side;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.ChangeMessageInfo;
 import com.google.gerrit.extensions.common.ChangeType;
@@ -151,7 +153,7 @@
     TestTimeUtil.resetWithClockStep(0, TimeUnit.SECONDS);
     createChange();
     /* Advancing the time after creating the change so that the first robot comment is not in the same timestamp as with the change creation */
-    TestTimeUtil.incrementClock(5, TimeUnit.SECONDS);
+    TestTimeUtil.incrementClock(10, TimeUnit.SECONDS);
 
     RobotCommentInput c1 = TestCommentHelper.createRobotCommentInput(FILE_NAME);
     RobotCommentInput c2 = TestCommentHelper.createRobotCommentInput(FILE_NAME);
@@ -220,9 +222,9 @@
             .changeMessageId;
 
     /**
-     * Upload PS message, robot message 1 & robot comment 1 all have the same timestamp. The robot
-     * comment is matched to robot message 1 because the PS upload message is auto-generated and is
-     * ignored in matching
+     * All change messages have the auto-generated tag. Robot comments can be linked to
+     * auto-generated messages where each comment is linked to the next nearest change message in
+     * timestamp
      */
     assertThat(message1ChangeId).isEqualTo(comment1MessageId);
     assertThat(message2ChangeId).isEqualTo(comment2MessageId);
@@ -267,6 +269,57 @@
   }
 
   @Test
+  public void patchsetLevelRobotCommentCanBeAddedAndRetrieved() throws Exception {
+    RobotCommentInput input = TestCommentHelper.createRobotCommentInput(PATCHSET_LEVEL);
+    testCommentHelper.addRobotComment(changeId, input);
+
+    List<RobotCommentInfo> results = getRobotComments();
+    assertThatList(results).onlyElement().path().isEqualTo(PATCHSET_LEVEL);
+  }
+
+  @Test
+  public void patchsetLevelRobotCommentCantHaveLine() throws Exception {
+    RobotCommentInput input = TestCommentHelper.createRobotCommentInput(PATCHSET_LEVEL);
+    input.line = 1;
+    BadRequestException ex =
+        assertThrows(
+            BadRequestException.class, () -> testCommentHelper.addRobotComment(changeId, input));
+    assertThat(ex.getMessage()).contains("line");
+  }
+
+  @Test
+  public void patchsetLevelRobotCommentCantHaveRange() throws Exception {
+    RobotCommentInput input = TestCommentHelper.createRobotCommentInput(PATCHSET_LEVEL);
+    input.range = createRange(2, 9, 5, 10);
+    BadRequestException ex =
+        assertThrows(
+            BadRequestException.class, () -> testCommentHelper.addRobotComment(changeId, input));
+    assertThat(ex.getMessage()).contains("range");
+  }
+
+  @Test
+  public void patchsetLevelRobotCommentCantHaveSide() throws Exception {
+    RobotCommentInput input = TestCommentHelper.createRobotCommentInput(PATCHSET_LEVEL);
+    input.side = Side.REVISION;
+    BadRequestException ex =
+        assertThrows(
+            BadRequestException.class, () -> testCommentHelper.addRobotComment(changeId, input));
+    assertThat(ex.getMessage()).contains("side");
+  }
+
+  @Test
+  public void fixSuggestionCannotPointToPatchsetLevel() throws Exception {
+    RobotCommentInput input = TestCommentHelper.createRobotCommentInput(FILE_NAME);
+    FixReplacementInfo brokenFixReplacement = createFixReplacementInfo();
+    brokenFixReplacement.path = PATCHSET_LEVEL;
+    input.fixSuggestions = ImmutableList.of(createFixSuggestionInfo(brokenFixReplacement));
+    BadRequestException ex =
+        assertThrows(
+            BadRequestException.class, () -> testCommentHelper.addRobotComment(changeId, input));
+    assertThat(ex.getMessage()).contains("file path must not be " + PATCHSET_LEVEL);
+  }
+
+  @Test
   public void hugeRobotCommentIsRejected() {
     int defaultSizeLimit = 1 << 20;
     fixReplacementInfo.replacement = getStringFor(defaultSizeLimit + 1);
diff --git a/javatests/com/google/gerrit/acceptance/pgm/InitIT.java b/javatests/com/google/gerrit/acceptance/pgm/InitIT.java
index 4caee64..4db0177 100644
--- a/javatests/com/google/gerrit/acceptance/pgm/InitIT.java
+++ b/javatests/com/google/gerrit/acceptance/pgm/InitIT.java
@@ -17,6 +17,7 @@
 import static com.google.common.truth.Truth8.assertThat;
 
 import com.google.common.collect.ImmutableSet;
+import com.google.errorprone.annotations.MustBeClosed;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.StandaloneSiteTest;
 import com.google.gerrit.entities.Project;
@@ -82,27 +83,33 @@
 
   private void setProjectsIndexLastModifiedInThePast(Path indexDir, Instant time)
       throws IOException {
-    for (Path path : getAllProjectsIndexFiles(indexDir).collect(Collectors.toList())) {
-      FS.DETECTED.setLastModified(path, time);
+    try (Stream<Path> allprojectsIndexFiles = getAllProjectsIndexFiles(indexDir)) {
+      for (Path path : allprojectsIndexFiles.collect(Collectors.toList())) {
+        FS.DETECTED.setLastModified(path, time);
+      }
     }
   }
 
   private Optional<Instant> getProjectsIndexLastModified(Path indexDir) throws IOException {
-    return getAllProjectsIndexFiles(indexDir)
-        .map(FS.DETECTED::lastModifiedInstant)
-        .max(Comparator.comparingLong(Instant::toEpochMilli));
+    try (Stream<Path> allprojectsIndexFiles = getAllProjectsIndexFiles(indexDir)) {
+      return allprojectsIndexFiles
+          .map(FS.DETECTED::lastModifiedInstant)
+          .max(Comparator.comparingLong(Instant::toEpochMilli));
+    }
   }
 
+  @MustBeClosed
   private Stream<Path> getAllProjectsIndexFiles(Path indexDir) throws IOException {
-    Optional<Path> projectsPath =
-        Files.walk(indexDir, 1)
-            .filter(Files::isDirectory)
-            .filter(p -> p.getFileName().toString().startsWith("projects_"))
-            .findFirst();
-    if (!projectsPath.isPresent()) {
-      return Stream.empty();
+    try (Stream<Path> stream = Files.walk(indexDir, 1)) {
+      Optional<Path> projectsPath =
+          stream
+              .filter(Files::isDirectory)
+              .filter(p -> p.getFileName().toString().startsWith("projects_"))
+              .findFirst();
+      if (!projectsPath.isPresent()) {
+        return Stream.empty();
+      }
+      return Files.walk(projectsPath.get(), 1, FileVisitOption.FOLLOW_LINKS);
     }
-
-    return Files.walk(projectsPath.get(), 1, FileVisitOption.FOLLOW_LINKS);
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
index 72db9b3..2779284 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
@@ -299,6 +299,89 @@
     assertTrees(project, actual);
   }
 
+  /**
+   * Tests the following situation:
+   *
+   * <ul>
+   *   <li>1. create a change series, consisting out of a merge commit and a normal commit
+   *   <li>2. before submitting the change series, another non-conflicting change gets submitted
+   *   <li>3. when the change series gets submitted, Gerrit must perform a merge/rebase/cherry-pick
+   * </ul>
+   */
+  @Test
+  public void submitChangeSeriesWithMergeCommitThatIsBasedOnOldTip() throws Throwable {
+    RevCommit initialHead = projectOperations.project(project).getHead("master");
+
+    // create a commit which will become the first parent of a merge commit
+    PushOneCommit.Result parent1 =
+        pushFactory
+            .create(
+                admin.newIdent(),
+                testRepo,
+                "parent 2",
+                ImmutableMap.of("foo", "foo-2", "bar", "bar-2"))
+            .to("refs/heads/master");
+
+    // reset the testRepo in order to create a sibling of parent1
+    testRepo.reset(initialHead);
+
+    // create a stable branch that we can merge back into master later
+    BranchInput in = new BranchInput();
+    in.revision = initialHead.getName();
+    gApi.projects().name(project.get()).branch("refs/heads/stable").create(in);
+
+    // create one commit in the stable branch, which will become the second parent of the merge
+    // commit
+    PushOneCommit.Result parent2 =
+        pushFactory
+            .create(
+                admin.newIdent(),
+                testRepo,
+                "parent 1",
+                ImmutableMap.of("foo", "foo-1", "bar", "bar-1"))
+            .to("refs/heads/stable");
+
+    // create a merge change that merges the stable branch back into master
+    testRepo.reset(parent1.getCommit());
+    PushOneCommit m =
+        pushFactory.create(
+            admin.newIdent(), testRepo, "merge", ImmutableMap.of("foo", "foo-1", "bar", "bar-2"));
+    m.setParents(ImmutableList.of(parent1.getCommit(), parent2.getCommit()));
+    PushOneCommit.Result mergeChange = m.to("refs/for/master");
+    mergeChange.assertOkStatus();
+
+    // approve the merge change so that it becomes submittable
+    approve(mergeChange.getChangeId());
+
+    // create a successor change that depends on the merge change
+    PushOneCommit.Result successorChange = createChange("refs/for/master");
+
+    // simulate another developer submitting a change in the meantime (non-conflicting sibling
+    // commit of the merge commit), this means when the change series gets submitted Gerrit must
+    // perform a merge/rebase/cherry-pick now
+    testRepo.reset(parent1.getCommit());
+    submit(createChange("Other Change", "x.txt", "x content").getChangeId());
+
+    // submit the change series
+    if (getSubmitType() != SubmitType.FAST_FORWARD_ONLY) {
+      submit(successorChange.getChangeId());
+    } else {
+      submitWithConflict(
+          successorChange.getChangeId(),
+          "Failed to submit 2 changes due to the following problems:\n"
+              + "Change "
+              + mergeChange.getChange().getId()
+              + ": Project policy "
+              + "requires all submissions to be a fast-forward. Please "
+              + "rebase the change locally and upload again for review.\n"
+              + "Change "
+              + successorChange.getChange().getId()
+              + ": Project policy "
+              + "requires all submissions to be a fast-forward. Please "
+              + "rebase the change locally and upload again for review.");
+    }
+  }
+
   @Test
   public void submitNoPermission() throws Throwable {
     // create project where submit is blocked
diff --git a/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java b/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
index ecd4025..4fac821 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
@@ -18,7 +18,9 @@
 import static com.google.common.truth.Truth8.assertThat;
 import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME;
 import static com.google.gerrit.acceptance.PushOneCommit.SUBJECT;
+import static com.google.gerrit.entities.Patch.PATCHSET_LEVEL;
 import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static com.google.gerrit.truth.MapSubject.assertThatMap;
 import static java.util.stream.Collectors.groupingBy;
 import static java.util.stream.Collectors.toList;
 
@@ -176,6 +178,189 @@
   }
 
   @Test
+  public void patchsetLevelCommentCanBeAddedAndRetrieved() throws Exception {
+    PushOneCommit.Result result = createChange();
+    String changeId = result.getChangeId();
+    String ps1 = result.getCommit().name();
+
+    CommentInput comment = newCommentWithOnlyMandatoryFields(PATCHSET_LEVEL, "comment");
+    addComments(changeId, ps1, comment);
+
+    Map<String, List<CommentInfo>> results = getPublishedComments(changeId, ps1);
+    assertThatMap(results).keys().containsExactly(PATCHSET_LEVEL);
+  }
+
+  @Test
+  public void deletePatchsetLevelComment() throws Exception {
+    requestScopeOperations.setApiUser(admin.id());
+    PushOneCommit.Result r = createChange();
+    String changeId = r.getChangeId();
+    String revId = r.getCommit().getName();
+    String commentMessage = "to be deleted";
+    CommentInput comment = newCommentWithOnlyMandatoryFields(PATCHSET_LEVEL, commentMessage);
+    addComments(changeId, revId, comment);
+
+    Map<String, List<CommentInfo>> results = getPublishedComments(changeId, revId);
+    CommentInfo oldComment = Iterables.getOnlyElement(results.get(PATCHSET_LEVEL));
+
+    DeleteCommentInput input = new DeleteCommentInput("reason");
+    gApi.changes().id(changeId).revision(revId).comment(oldComment.id).delete(input);
+    CommentInfo updatedComment =
+        Iterables.getOnlyElement(getPublishedComments(changeId, revId).get(PATCHSET_LEVEL));
+
+    assertThat(updatedComment.message).doesNotContain(commentMessage);
+  }
+
+  @Test
+  public void patchsetLevelCommentCantHaveLine() throws Exception {
+    PushOneCommit.Result result = createChange();
+    String changeId = result.getChangeId();
+    String ps1 = result.getCommit().name();
+
+    CommentInput input = newCommentWithOnlyMandatoryFields(PATCHSET_LEVEL, "comment");
+    input.line = 1;
+    BadRequestException ex =
+        assertThrows(BadRequestException.class, () -> addComments(changeId, ps1, input));
+    assertThat(ex.getMessage()).contains("line");
+  }
+
+  @Test
+  public void patchsetLevelCommentCantHaveRange() throws Exception {
+    PushOneCommit.Result result = createChange();
+    String changeId = result.getChangeId();
+    String ps1 = result.getCommit().name();
+
+    CommentInput input = newCommentWithOnlyMandatoryFields(PATCHSET_LEVEL, "comment");
+    input.range = createLineRange(1, 3);
+    BadRequestException ex =
+        assertThrows(BadRequestException.class, () -> addComments(changeId, ps1, input));
+    assertThat(ex.getMessage()).contains("range");
+  }
+
+  @Test
+  public void patchsetLevelCommentCantHaveSide() throws Exception {
+    PushOneCommit.Result result = createChange();
+    String changeId = result.getChangeId();
+    String ps1 = result.getCommit().name();
+
+    CommentInput input = newCommentWithOnlyMandatoryFields(PATCHSET_LEVEL, "comment");
+    input.side = Side.REVISION;
+    BadRequestException ex =
+        assertThrows(BadRequestException.class, () -> addComments(changeId, ps1, input));
+    assertThat(ex.getMessage()).contains("side");
+  }
+
+  @Test
+  public void patchsetLevelDraftCommentCanBeAddedAndRetrieved() throws Exception {
+    PushOneCommit.Result r = createChange();
+    String changeId = r.getChangeId();
+    String revId = r.getCommit().getName();
+    DraftInput comment = newDraftWithOnlyMandatoryFields(PATCHSET_LEVEL, "comment");
+    addDraft(changeId, revId, comment);
+    Map<String, List<CommentInfo>> results = getDraftComments(changeId, revId);
+    assertThatMap(results).keys().containsExactly(PATCHSET_LEVEL);
+  }
+
+  @Test
+  public void deletePatchsetLevelDraft() throws Exception {
+    PushOneCommit.Result r = createChange();
+    String changeId = r.getChangeId();
+    String revId = r.getCommit().getName();
+    DraftInput draft = newDraftWithOnlyMandatoryFields(PATCHSET_LEVEL, "comment 1");
+    CommentInfo returned = addDraft(changeId, revId, draft);
+    deleteDraft(changeId, revId, returned.id);
+    Map<String, List<CommentInfo>> drafts = getDraftComments(changeId, revId);
+    assertThat(drafts).isEmpty();
+  }
+
+  @Test
+  public void patchsetLevelDraftCommentCantHaveLine() throws Exception {
+    PushOneCommit.Result r = createChange();
+    String changeId = r.getChangeId();
+    String revId = r.getCommit().getName();
+    DraftInput comment = newDraftWithOnlyMandatoryFields(PATCHSET_LEVEL, "comment");
+    comment.line = 1;
+    BadRequestException ex =
+        assertThrows(BadRequestException.class, () -> addDraft(changeId, revId, comment));
+    assertThat(ex.getMessage()).contains("line");
+  }
+
+  @Test
+  public void patchsetLevelDraftCommentCantHaveRange() throws Exception {
+    PushOneCommit.Result r = createChange();
+    String changeId = r.getChangeId();
+    String revId = r.getCommit().getName();
+    DraftInput comment = newDraftWithOnlyMandatoryFields(PATCHSET_LEVEL, "comment");
+    comment.range = createLineRange(1, 3);
+    BadRequestException ex =
+        assertThrows(BadRequestException.class, () -> addDraft(changeId, revId, comment));
+    assertThat(ex.getMessage()).contains("range");
+  }
+
+  @Test
+  public void patchsetLevelDraftCommentCantHaveSide() throws Exception {
+    PushOneCommit.Result r = createChange();
+    String changeId = r.getChangeId();
+    String revId = r.getCommit().getName();
+    DraftInput comment = newDraftWithOnlyMandatoryFields(PATCHSET_LEVEL, "comment");
+    comment.side = Side.REVISION;
+    BadRequestException ex =
+        assertThrows(BadRequestException.class, () -> addDraft(changeId, revId, comment));
+    assertThat(ex.getMessage()).contains("range");
+  }
+
+  @Test
+  public void patchsetLevelDraftCommentCantBeUpdatedToHaveLine() throws Exception {
+    PushOneCommit.Result r = createChange();
+    String changeId = r.getChangeId();
+    String revId = r.getCommit().getName();
+    DraftInput comment = newDraftWithOnlyMandatoryFields(PATCHSET_LEVEL, "comment");
+    addDraft(changeId, revId, comment);
+    Map<String, List<CommentInfo>> results = getDraftComments(changeId, revId);
+    DraftInput update = newDraftWithOnlyMandatoryFields(PATCHSET_LEVEL, "comment");
+    update.id = Iterables.getOnlyElement(results.get(PATCHSET_LEVEL)).id;
+    update.line = 1;
+    BadRequestException ex =
+        assertThrows(
+            BadRequestException.class, () -> updateDraft(changeId, revId, update, update.id));
+    assertThat(ex.getMessage()).contains("line");
+  }
+
+  @Test
+  public void patchsetLevelDraftCommentCantBeUpdatedToHaveRange() throws Exception {
+    PushOneCommit.Result r = createChange();
+    String changeId = r.getChangeId();
+    String revId = r.getCommit().getName();
+    DraftInput comment = newDraftWithOnlyMandatoryFields(PATCHSET_LEVEL, "comment");
+    addDraft(changeId, revId, comment);
+    Map<String, List<CommentInfo>> results = getDraftComments(changeId, revId);
+    DraftInput update = newDraftWithOnlyMandatoryFields(PATCHSET_LEVEL, "comment");
+    update.id = Iterables.getOnlyElement(results.get(PATCHSET_LEVEL)).id;
+    update.range = createLineRange(1, 3);
+    BadRequestException ex =
+        assertThrows(
+            BadRequestException.class, () -> updateDraft(changeId, revId, update, update.id));
+    assertThat(ex.getMessage()).contains("range");
+  }
+
+  @Test
+  public void patchsetLevelDraftCommentCantBeUpdatedToHaveSide() throws Exception {
+    PushOneCommit.Result r = createChange();
+    String changeId = r.getChangeId();
+    String revId = r.getCommit().getName();
+    DraftInput comment = newDraftWithOnlyMandatoryFields(PATCHSET_LEVEL, "comment");
+    addDraft(changeId, revId, comment);
+    Map<String, List<CommentInfo>> results = getDraftComments(changeId, revId);
+    DraftInput update = newDraftWithOnlyMandatoryFields(PATCHSET_LEVEL, "comment");
+    update.id = Iterables.getOnlyElement(results.get(PATCHSET_LEVEL)).id;
+    update.side = Side.REVISION;
+    BadRequestException ex =
+        assertThrows(
+            BadRequestException.class, () -> updateDraft(changeId, revId, update, update.id));
+    assertThat(ex.getMessage()).contains("side");
+  }
+
+  @Test
   public void postCommentWithReply() throws Exception {
     for (Integer line : lines) {
       String file = "file";
@@ -1083,7 +1268,7 @@
     addComments(changeId, ps4, c7, c8);
 
     // 11th commit: Add (c9) to PS2.
-    CommentInput c9 = newComment("b.txt", "comment 9");
+    CommentInput c9 = newCommentWithOnlyMandatoryFields("b.txt", "comment 9");
     addComments(changeId, ps2, c9);
 
     List<CommentInfo> commentsBeforeDelete = getChangeSortedComments(id.get());
@@ -1340,6 +1525,11 @@
     return newComment(file, Side.REVISION, 0, message, false);
   }
 
+  private static CommentInput newCommentWithOnlyMandatoryFields(String path, String message) {
+    CommentInput c = new CommentInput();
+    return populate(c, path, null, null, null, null, message, false);
+  }
+
   private static CommentInput newComment(
       String path, Side side, int line, String message, Boolean unresolved) {
     CommentInput c = new CommentInput();
@@ -1367,19 +1557,24 @@
     return populate(d, path, Side.PARENT, parent, line, message, false);
   }
 
+  private DraftInput newDraftWithOnlyMandatoryFields(String path, String message) {
+    DraftInput d = new DraftInput();
+    return populate(d, path, null, null, null, null, message, false);
+  }
+
   private static <C extends Comment> C populate(
       C c,
       String path,
       Side side,
       Integer parent,
-      int line,
+      Integer line,
       Comment.Range range,
       String message,
       Boolean unresolved) {
     c.path = path;
     c.side = side;
     c.parent = parent;
-    c.line = line != 0 ? line : null;
+    c.line = line != null && line != 0 ? line : null;
     c.message = message;
     c.unresolved = unresolved;
     if (range != null) {
diff --git a/javatests/com/google/gerrit/acceptance/server/permissions/PermissionBackendIT.java b/javatests/com/google/gerrit/acceptance/server/permissions/PermissionBackendIT.java
new file mode 100644
index 0000000..2aab159
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/server/permissions/PermissionBackendIT.java
@@ -0,0 +1,70 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.server.permissions;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.Iterables;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.permissions.ChangePermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.inject.Inject;
+import org.junit.Test;
+
+/** Asserts behavior on {@link PermissionBackend} using a fully-started Gerrit. */
+public class PermissionBackendIT extends AbstractDaemonTest {
+  @Inject PermissionBackend pb;
+  @Inject ChangeNotes.Factory changeNotesFactory;
+
+  @Test
+  public void changeDataFromIndex_canCheckReviewerState() throws Exception {
+    Change.Id changeId = createChange().getChange().getId();
+    gApi.changes().id(changeId.get()).setPrivate(true);
+    gApi.changes().id(changeId.get()).addReviewer(user.email());
+
+    ChangeData changeData =
+        Iterables.getOnlyElement(queryProvider.get().byLegacyChangeId(changeId));
+    boolean reviewerCanSee =
+        pb.absentUser(user.id()).change(changeData).test(ChangePermission.READ);
+    assertThat(reviewerCanSee).isTrue();
+  }
+
+  @Test
+  public void changeDataFromNoteDb_canCheckReviewerState() throws Exception {
+    Change.Id changeId = createChange().getChange().getId();
+    gApi.changes().id(changeId.get()).setPrivate(true);
+    gApi.changes().id(changeId.get()).addReviewer(user.email());
+
+    ChangeNotes notes = changeNotesFactory.create(project, changeId);
+    ChangeData changeData = changeDataFactory.create(notes);
+    boolean reviewerCanSee =
+        pb.absentUser(user.id()).change(changeData).test(ChangePermission.READ);
+    assertThat(reviewerCanSee).isTrue();
+  }
+
+  @Test
+  public void changeNotes_canCheckReviewerState() throws Exception {
+    Change.Id changeId = createChange().getChange().getId();
+    gApi.changes().id(changeId.get()).setPrivate(true);
+    gApi.changes().id(changeId.get()).addReviewer(user.email());
+
+    ChangeNotes notes = changeNotesFactory.create(project, changeId);
+    boolean reviewerCanSee = pb.absentUser(user.id()).change(notes).test(ChangePermission.READ);
+    assertThat(reviewerCanSee).isTrue();
+  }
+}
diff --git a/javatests/com/google/gerrit/acceptance/server/rules/RulesIT.java b/javatests/com/google/gerrit/acceptance/server/rules/RulesIT.java
index e5432d1..18ae6c4 100644
--- a/javatests/com/google/gerrit/acceptance/server/rules/RulesIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/rules/RulesIT.java
@@ -29,7 +29,6 @@
 import java.util.Collection;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Repository;
-import org.junit.Before;
 import org.junit.Test;
 
 /**
@@ -44,14 +43,6 @@
   @Inject private ProjectOperations projectOperations;
   @Inject private SubmitRuleEvaluator.Factory evaluatorFactory;
 
-  @Before
-  public void setUp() {
-    // We don't want caches to interfere with our tests. If we didn't, the cache would take
-    // precedence over the index, which would never be called.
-    baseConfig.setString("cache", "changes", "memoryLimit", "0");
-    baseConfig.setString("cache", "projects", "memoryLimit", "0");
-  }
-
   @Test
   public void testUnresolvedCommentsCountPredicate() throws Exception {
     modifySubmitRules("gerrit:unresolved_comments_count(0)");
@@ -119,5 +110,6 @@
           .message("Modify rules.pl")
           .create();
     }
+    projectCache.evict(project);
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/ssh/SetReviewersIT.java b/javatests/com/google/gerrit/acceptance/ssh/SetReviewersIT.java
index 5a31bfd..58c2517 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/SetReviewersIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/SetReviewersIT.java
@@ -65,7 +65,7 @@
     session.exec(
         String.format("gerrit set-reviewers -%s %s %s", add ? "a" : "r", user.email(), id));
     session.assertSuccess();
-    ImmutableSet<Account.Id> reviewers = change.getChange().getReviewers().all();
+    ImmutableSet<Account.Id> reviewers = change.getChange().reviewers().all();
     if (add) {
       assertThat(reviewers).contains(user.id());
     } else {
diff --git a/javatests/com/google/gerrit/entities/PatchTest.java b/javatests/com/google/gerrit/entities/PatchTest.java
index 9f906a9..dce1b3e 100644
--- a/javatests/com/google/gerrit/entities/PatchTest.java
+++ b/javatests/com/google/gerrit/entities/PatchTest.java
@@ -24,6 +24,7 @@
   public void isMagic() {
     assertThat(Patch.isMagic("/COMMIT_MSG")).isTrue();
     assertThat(Patch.isMagic("/MERGE_LIST")).isTrue();
+    assertThat(Patch.isMagic("/PATCHSET_LEVEL")).isTrue();
 
     assertThat(Patch.isMagic("/COMMIT_MSG/")).isFalse();
     assertThat(Patch.isMagic("COMMIT_MSG")).isFalse();
diff --git a/javatests/com/google/gerrit/server/patch/PatchListTest.java b/javatests/com/google/gerrit/server/patch/PatchListTest.java
index e224191a..56adefa 100644
--- a/javatests/com/google/gerrit/server/patch/PatchListTest.java
+++ b/javatests/com/google/gerrit/server/patch/PatchListTest.java
@@ -26,16 +26,30 @@
 import org.junit.Test;
 
 public class PatchListTest {
+
   @Test
   public void fileOrder() {
     String[] names = {
-      "zzz", "def/g", "/!xxx", "abc", Patch.MERGE_LIST, "qrx", Patch.COMMIT_MSG,
+      "zzz",
+      "def/g",
+      "/!xxx",
+      "abc",
+      Patch.MERGE_LIST,
+      "qrx",
+      Patch.COMMIT_MSG,
+      Patch.PATCHSET_LEVEL
     };
     String[] want = {
-      Patch.COMMIT_MSG, Patch.MERGE_LIST, "/!xxx", "abc", "def/g", "qrx", "zzz",
+      Patch.COMMIT_MSG,
+      Patch.MERGE_LIST,
+      Patch.PATCHSET_LEVEL,
+      "/!xxx",
+      "abc",
+      "def/g",
+      "qrx",
+      "zzz",
     };
-
-    Arrays.sort(names, 0, names.length, PatchList::comparePaths);
+    Arrays.sort(names, 0, names.length, PatchList.FILE_PATH_CMP);
     assertThat(names).isEqualTo(want);
   }
 
@@ -48,7 +62,7 @@
       Patch.COMMIT_MSG, "/!xxx", "abc", "def/g", "qrx", "zzz",
     };
 
-    Arrays.sort(names, 0, names.length, PatchList::comparePaths);
+    Arrays.sort(names, 0, names.length, PatchList.FILE_PATH_CMP);
     assertThat(names).isEqualTo(want);
   }
 
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index 1aa0f35..11e98c6 100644
--- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -3039,6 +3039,7 @@
     List<ChangeInfo> result = newQuery("attention:" + user2Id.toString()).get();
     assertThat(result).hasSize(1);
     ChangeInfo changeInfo = Iterables.getOnlyElement(result);
+    assertThat(changeInfo.attentionSet).isNotNull();
     assertThat(changeInfo.attentionSet.keySet()).containsExactly(userId.get(), user2Id.get());
     assertThat(changeInfo.attentionSet.get(userId.get()).reason).isEqualTo("reason 1");
     assertThat(changeInfo.attentionSet.get(user2Id.get()).reason).isEqualTo("reason 2");
diff --git a/javatests/com/google/gerrit/server/restapi/change/ListChangeCommentsTest.java b/javatests/com/google/gerrit/server/restapi/change/ListChangeCommentsTest.java
index 9f34377..5a28404 100644
--- a/javatests/com/google/gerrit/server/restapi/change/ListChangeCommentsTest.java
+++ b/javatests/com/google/gerrit/server/restapi/change/ListChangeCommentsTest.java
@@ -1,18 +1,16 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
 
 package com.google.gerrit.server.restapi.change;
 
@@ -22,6 +20,7 @@
 import com.google.gerrit.entities.Change;
 import com.google.gerrit.entities.ChangeMessage;
 import com.google.gerrit.extensions.common.CommentInfo;
+import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.CommentsUtil;
 import java.sql.Timestamp;
 import org.junit.Test;
@@ -33,27 +32,38 @@
 
   @Test
   public void commentsLinkedToChangeMessages() {
+    /**
+     * Human comments should not be linked to auto-generated messages. A comment is linked to the
+     * nearest next change message in timestamp
+     */
     CommentInfo c1 = getNewCommentInfo("c1", Timestamp.valueOf("2018-01-01 09:01:00"));
     CommentInfo c2 = getNewCommentInfo("c2", Timestamp.valueOf("2018-01-01 09:01:15"));
     CommentInfo c3 = getNewCommentInfo("c3", Timestamp.valueOf("2018-01-01 09:01:25"));
 
     ChangeMessage cm1 =
-        getNewChangeMessage("cm1key", "cm1", Timestamp.valueOf("2018-01-01 00:00:00"));
+        getNewChangeMessage("cm1key", "cm1", Timestamp.valueOf("2018-01-01 00:00:00"), null);
+    ChangeMessage cmIgnore =
+        getNewChangeMessage(
+            "cm2key",
+            "cm2",
+            Timestamp.valueOf("2018-01-01 09:01:15"),
+            ChangeMessagesUtil.AUTOGENERATED_TAG_PREFIX);
     ChangeMessage cm2 =
-        getNewChangeMessage("cm2key", "cm2", Timestamp.valueOf("2018-01-01 09:01:15"));
+        getNewChangeMessage("cm2key", "cm2", Timestamp.valueOf("2018-01-01 09:01:16"), null);
     ChangeMessage cm3 =
-        getNewChangeMessage("cm3key", "cm3", Timestamp.valueOf("2018-01-01 09:01:27"));
+        getNewChangeMessage("cm3key", "cm3", Timestamp.valueOf("2018-01-01 09:01:27"), null);
 
     assertThat(c1.changeMessageId).isNull();
     assertThat(c2.changeMessageId).isNull();
     assertThat(c3.changeMessageId).isNull();
 
     ImmutableList<CommentInfo> comments = ImmutableList.of(c1, c2, c3);
-    ImmutableList<ChangeMessage> changeMessages = ImmutableList.of(cm1, cm2, cm3);
+    ImmutableList<ChangeMessage> changeMessages = ImmutableList.of(cm1, cmIgnore, cm2, cm3);
 
-    CommentsUtil.linkCommentsToChangeMessages(comments, changeMessages);
+    CommentsUtil.linkCommentsToChangeMessages(comments, changeMessages, true);
 
     assertThat(c1.changeMessageId).isEqualTo(changeMessageKey(cm2));
+    /** comment 2 ignored the auto-generated change message */
     assertThat(c2.changeMessageId).isEqualTo(changeMessageKey(cm2));
     assertThat(c3.changeMessageId).isEqualTo(changeMessageKey(cm3));
   }
@@ -65,10 +75,12 @@
     return c;
   }
 
-  private static ChangeMessage getNewChangeMessage(String id, String message, Timestamp ts) {
+  private static ChangeMessage getNewChangeMessage(
+      String id, String message, Timestamp ts, String tag) {
     ChangeMessage.Key key = ChangeMessage.key(Change.id(1), id);
     ChangeMessage cm = new ChangeMessage(key, null, ts, null);
     cm.setMessage(message);
+    cm.setTag(tag);
     return cm;
   }
 
diff --git a/package.json b/package.json
index 526d201..926de4e 100644
--- a/package.json
+++ b/package.json
@@ -26,7 +26,10 @@
     "eslint": "npm run safe_bazelisk test polygerrit-ui/app:lint_test",
     "eslintfix": "npm run safe_bazelisk run polygerrit-ui/app:lint_bin -- -- --fix $(pwd)/polygerrit-ui/app",
     "test-template": "./polygerrit-ui/app/run_template_test.sh",
-    "polylint": "npm run safe_bazelisk test polygerrit-ui/app:polylint_test"
+    "polylint": "npm run safe_bazelisk test polygerrit-ui/app:polylint_test",
+    "test:karma:debug": "npm run safe_bazelisk run //polygerrit-ui:karma_bin -- -- start $(pwd)/polygerrit-ui/karma.conf.js --browsers ChromeDev --no-single-run --testFiles",
+    "test:karma": "npm run safe_bazelisk run //polygerrit-ui:karma_bin -- -- start $(pwd)/polygerrit-ui/karma.conf.js --testFiles",
+    "test:wct": "npm run test -- --test_tag_filters=wct"
   },
   "repository": {
     "type": "git",
diff --git a/plugins/delete-project b/plugins/delete-project
index d6ad8e7..e345e6e 160000
--- a/plugins/delete-project
+++ b/plugins/delete-project
@@ -1 +1 @@
-Subproject commit d6ad8e7de90331a317a8eaaf353f7cb7bd31d1a1
+Subproject commit e345e6e79900a72981e4ad19d37c7fbdcae4818b
diff --git a/plugins/plugin-manager b/plugins/plugin-manager
index 93d8e12..9e7b0a0 160000
--- a/plugins/plugin-manager
+++ b/plugins/plugin-manager
@@ -1 +1 @@
-Subproject commit 93d8e1248478c6b2db0a8c1080f2766406dd213d
+Subproject commit 9e7b0a086b3e4d56a3a9407d21b6648c650b8809
diff --git a/plugins/replication b/plugins/replication
index 88eeb9a..9ae2087 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 88eeb9aa1f15fcf0d891a901990efd5574fa8cf3
+Subproject commit 9ae20871646a0757979e3bef03aaf4e74af8ff73
diff --git a/polygerrit-ui/BUILD b/polygerrit-ui/BUILD
index a029df4..021ce08 100644
--- a/polygerrit-ui/BUILD
+++ b/polygerrit-ui/BUILD
@@ -1,5 +1,6 @@
 load("@io_bazel_rules_go//go:def.bzl", "go_binary")
 load("//tools/bzl:genrule2.bzl", "genrule2")
+load("@build_bazel_rules_nodejs//:index.bzl", "nodejs_binary")
 
 package(default_visibility = ["//visibility:public"])
 
@@ -32,3 +33,43 @@
         "@org_golang_x_tools//godoc/vfs/zipfs:go_default_library",
     ],
 )
+
+# Define a karma+plugins binary to run karma-mocha tests.
+# Can be reused multiple time, if there are multiple karma test rules
+sh_binary(
+    name = "karma_bin",
+    srcs = ["@ui_dev_npm//:node_modules/karma/bin/karma"],
+    data = [
+        "@ui_dev_npm//@open-wc/karma-esm",
+        "@ui_dev_npm//chai",
+        "@ui_dev_npm//karma-chrome-launcher",
+        "@ui_dev_npm//karma-mocha",
+        "@ui_dev_npm//karma-mocha-reporter",
+        "@ui_dev_npm//karma/bin:karma",
+        "@ui_dev_npm//mocha",
+    ],
+)
+
+# Run all tests in one.
+# TODO(dmfilippov): allow parallel tests for karma - either on the bazel level
+# or on the karma level. For now single sh_test is enough.
+sh_test(
+    name = "karma_test",
+    size = "enormous",
+    srcs = ["karma_test.sh"],
+    args = [
+        "$(location :karma_bin)",
+        "$(location karma.conf.js)",
+    ],
+    data = [
+        "karma.conf.js",
+        ":karma_bin",
+        "//polygerrit-ui/app:test-srcs-fg",
+    ],
+    # Should not run sandboxed.
+    tags = [
+        "karma",
+        "local",
+        "manual",
+    ],
+)
diff --git a/polygerrit-ui/FE_Style_Guide.md b/polygerrit-ui/FE_Style_Guide.md
new file mode 100644
index 0000000..c9a5d9b
--- /dev/null
+++ b/polygerrit-ui/FE_Style_Guide.md
@@ -0,0 +1,263 @@
+# Gerrit JavaScript style guide
+
+Gerrit frontend follows [recommended eslint rules](https://eslint.org/docs/rules/)
+and [Google JavaScript Style Guide](https://google.github.io/styleguide/jsguide.html).
+Eslint is used to automate rules checking where possible. You can find exact eslint rules
+[here](https://gerrit.googlesource.com/gerrit/+/refs/heads/master/polygerrit-ui/app/.eslintrc.js).
+
+Gerrit JavaScript code uses ES6 modules and doesn't use goog.module files.
+
+Additionally to the rules above, Gerrit frontend uses the following rules (some of them have automated checks,
+some don't):
+
+- [Use destructuring imports only](#destructuring-imports-only)
+- [Use classes and services for storing and manipulating global state](#services-for-global-state)
+- [Pass required services in the constructor for plain classes](#pass-dependencies-in-constructor)
+- [Assign required services in a HTML/Polymer element constructor](#assign-dependencies-in-html-element-constructor)
+
+## <a name="destructuring-imports-only"></a>Use destructuring imports only
+Always use destructuring import statement and specify all required names explicitly (e.g. `import {a,b,c} from '...'`)
+where possible.
+
+**Note:** Destructuring imports are not always possible with 3rd-party libraries, because a 3rd-party library
+can expose a class/function/const/etc... as a default export. In this situation you can use default import, but please
+keep consistent naming across the whole gerrit project. The best way to keep consistency is to search across our
+codebase for the same import. If you find an exact match - always use the same name for your import. If you can't
+find exact matches - find a similar import and assign appropriate/similar name for your default import. Usually the
+name should include a library name and part of the file path.
+
+You can read more about different type of imports
+[here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import).
+
+**Good:**
+```Javascript
+// Import from the module in the same project.
+import {getDisplayName, getAccount} from './user-utils.js'
+
+// The following default import is allowed only for 3rd-party libraries.
+// Please ensure, that all imports have the same name accross gerrit project (downloadImage in this example)
+import downloadImage from 'third-party-library/images/download.js'
+```
+
+**Bad:**
+```Javascript
+import * as userUtils from './user-utils.js'
+```
+
+## <a name="services-for-global-state"></a>Use classes and services for storing and manipulating global state
+
+You must use classes and services to share global state across the gerrit frontend code. Do not put a state at the
+top level of a module.
+
+It is not easy to define precise what can be a shared global state and what is not. Below are some
+examples of what can treated as a shared global state:
+
+* Information about enabled experiments
+* Information about current user
+* Information about current change
+
+**Note:**
+
+Service name must ends with a `Service` suffix.
+
+To share global state across modules in the project, do the following:
+- put the state in a class
+- add a new service to the
+[appContext](https://gerrit.googlesource.com/gerrit/+/refs/heads/master/polygerrit-ui/app/services/app-context.js)
+- add a service initialization code to the
+[services/app-context-init.js](https://gerrit.googlesource.com/gerrit/+/refs/heads/master/polygerrit-ui/app/services/app-context-init.js) file.
+- add a service or service-mock initialization code to the
+[embed/app-context-init.js](https://gerrit.googlesource.com/gerrit/+/refs/heads/master/polygerrit-ui/app/embed/app-context-init.js) file.
+- recommended: add a separate service-mock for testing. Do not use the same mock for testing and for
+the shared gr-diff (i.e. in the `services/app-context-init.js`). Even if the mocks are simple and looks
+identically, keep them separate. It allows to change them independently in the future.
+
+Also see the example below if a service depends on another services.
+
+**Note 1:** Be carefull with the shared gr-diff element. If a service is not required for the shared gr-diff,
+the safest option is to provide a mock for this service in the embed/app-context-init.js file. In exceptional
+cases you can keep the service uninitialized in
+[embed/app-context-init.js](https://gerrit.googlesource.com/gerrit/+/refs/heads/master/polygerrit-ui/app/embed/app-context-init.js) file
+, but it is recommended to write a comment why mocking is not possible. In the future we can
+review/update rules regarding the shared gr-diff element.
+
+**Good:**
+```Javascript
+export class CounterService {
+    constructor() {
+        this._count = 0;
+    }
+    get count() {
+        return this._count;
+    }
+    inc() {
+        this._count++;
+    }
+}
+
+// app-context.js
+export const appContext = {
+    //...
+    mouseClickCounterService: null,
+    keypressCounterService: null,
+};
+
+// services/app-context-init.js
+export function initAppContext() {
+    //...
+    // Add the following line before the Object.defineProperties(appContext, registeredServices);
+    addService('mouseClickCounterService', () => new CounterService());
+    addService('keypressCounterService', () => new CounterService());
+    // If a service depends on other services, pass dependencies as shown below
+    // If circular dependencies exist, app-init-context tests fail with timeout or stack overflow
+    // (we are  going to improve it in the future)
+    addService('analyticService', () =>
+        new CounterService(appContext.mouseClickCounterService, appContext.keypressCounterService));
+    //...
+    // This following line must remains the last one in the initAppContext
+    Object.defineProperties(appContext, registeredServices);
+}
+```
+
+**Bad:**
+```Javascript
+// module counter.js
+// Incorrect: shared state declared at the top level of the counter.js module
+let count = 0;
+export function getCount() {
+    return count;
+}
+export function incCount() {
+    count++;
+}
+```
+
+## <a name="pass-dependencies-in-constructor"></a>Pass required services in the constructor for plain classes
+
+If a class/service depends on some other service (or multiple services), the class must accept all dependencies
+as parameters in the constructor.
+
+Do not use appContext anywhere else in a class.
+
+**Note:** This rule doesn't apply for HTML/Polymer elements classes. A browser creates instances of such classes
+implicitly and calls the constructor without parameters. See
+[Assign required services in a HTML/Polymer element constructor](#assign-dependencies-in-html-element-constructor)
+
+**Good:**
+```Javascript
+export class UserService {
+    constructor(restApiService) {
+        this._restApiService = restApiService;
+    }
+    getLoggedIn() {
+        // Send request to server using this._restApiService
+    }
+}
+```
+
+**Bad:**
+```Javascript
+import {appContext} from "./app-context";
+
+export class UserService {
+    constructor() {
+        // Incorrect: you must pass all dependencies to a constructor
+        this._restApiService = appContext.restApiService;
+    }
+}
+
+export class AdminService {
+    isAdmin() {
+        // Incorrect: you must pass all dependencies to a constructor
+        return appContext.restApiService.sendRequest(...);
+    }
+}
+
+```
+
+## <a name="assign-dependencies-in-html-element-constructor"></a>Assign required services in a HTML/Polymer element constructor
+If a class is a custom HTML/Polymer element, the class must assign all required services in the constructor.
+A browser creates instances of such classes implicitly, so it is impossible to pass anything as a parameter to
+the element's class constructor.
+
+Do not use appContext anywhere except the constructor of the class.
+
+**Note for legacy elements:** If a polymer element extends a LegacyElementMixin and overrides the `created()` method,
+move all code from this method to a constructor right after the call to a `super()`
+([example](#assign-dependencies-legacy-element-example)). The `created()`
+method is [deprecated](https://polymer-library.polymer-project.org/2.0/docs/about_20#lifecycle-changes) and is called
+when a super (i.e. base) class constructor is called. If you are unsure about moving the code from the `created` method
+to the class constructor, consult with the source code:
+[`LegacyElementMixin._initializeProperties`](https://github.com/Polymer/polymer/blob/v3.4.0/lib/legacy/legacy-element-mixin.js#L318)
+and
+[`PropertiesChanged.constructor`](https://github.com/Polymer/polymer/blob/v3.4.0/lib/mixins/properties-changed.js#L177)
+
+
+
+**Good:**
+```Javascript
+import {appContext} from `.../services/app-context.js`;
+
+export class MyCustomElement extends ...{
+    constructor() {
+        super(); //This is mandatory to call parent constructor
+        this._userService = appContext.userService;
+    }
+    //...
+    _getUserName() {
+        return this._userService.activeUserName();
+    }
+}
+```
+
+**Bad:**
+```Javascript
+import {appContext} from `.../services/app-context.js`;
+
+export class MyCustomElement extends ...{
+    created() {
+        // Incorrect: assign all dependencies in the constructor
+        this._userService = appContext.userService;
+    }
+    //...
+    _getUserName() {
+        // Incorrect: use appContext outside of a constructor
+        return appContext.userService.activeUserName();
+    }
+}
+```
+
+<a name="assign-dependencies-legacy-element-example"></a>
+**Legacy element:**
+
+Before:
+```Javascript
+export class MyCustomElement extends ...LegacyElementMixin(...) {
+    constructor() {
+        super();
+        someAction();
+    }
+    created() {
+        super();
+        createdAction1();
+        createdAction2();
+    }
+}
+```
+
+After:
+```Javascript
+export class MyCustomElement extends ...LegacyElementMixin(...) {
+    constructor() {
+        super();
+        // Assign services here
+        this._userService = appContext.userService;
+        // Code from the created method - put it before existing actions in constructor
+        createdAction1();
+        createdAction2();
+        // Original constructor code
+        someAction();
+    }
+    // created method is removed
+}
+```
diff --git a/polygerrit-ui/README.md b/polygerrit-ui/README.md
index de25d79..29ba857 100644
--- a/polygerrit-ui/README.md
+++ b/polygerrit-ui/README.md
@@ -148,10 +148,13 @@
 ## Running Tests
 
 For daily development you typically only want to run and debug individual tests.
-Run the local [Go proxy server](#go-server) and navigate for example to
-<http://localhost:8081/elements/shared/gr-account-entry/gr-account-entry_test.html>.
-Check "Disable cache" in the "Network" tab of Chrome's dev tools, so code
-changes are picked up on "reload".
+There are 2 types of fronted tests in gerrit:
+- Karma tests - all tests matches `*_test.js` pattern
+- web-component-tester(WCT) tests - all tests matches the `*_test.html` pattern.
+
+**Note:** WCT tests are deprecated. We are migrating to Karma tests now. If you are going to change
+something in a WCT test file, we strongly recommend to convert it to Karma tests before making
+any change. See [Converting WCT tests to Karma](#wct-to-karma).
 
 Our CI integration ensures that all tests are run when you upload a change to
 Gerrit, but you can also run all tests locally in headless mode:
@@ -160,6 +163,44 @@
 npm test
 ```
 
+### Running Karma tests
+There are several ways to run Karma tests:
+
+* Run all Karma tests in headless mode:
+```sh
+npm run test:karma
+```
+
+* Run all Karma tests in debug mode (the command opens Chrome browser with
+the default Karma page; you should click the "Debug" button to start testing):
+```sh
+npm run test:karma:debug
+```
+
+* Run a single test file:
+```
+# Headless mode
+npm run test:karma async-foreach-behavior_test.js
+# Debug mode
+npm run test:karma:debug async-foreach-behavior_test.js
+```
+
+* You can run tests in IDE:
+  - [IntelliJ: running unit tests on Karma](https://www.jetbrains.com/help/idea/running-unit-tests-on-karma.html#ws_karma_running)
+
+### Running WCT tests
+
+Run the local [Go proxy server](#go-server) and navigate for example to
+<http://localhost:8081/elements/shared/gr-account-entry/gr-account-entry_test.html>.
+Check "Disable cache" in the "Network" tab of Chrome's dev tools, so code
+changes are picked up on "reload".
+
+You can also run all WCT tests locally in headless mode:
+
+```sh
+npm test:wct
+```
+
 To allow the tests to run in Safari:
 
 * In the Advanced preferences tab, check "Show Develop menu in menu bar".
@@ -171,6 +212,96 @@
 WCT_HEADLESS_MODE=1 WCT_ARGS='--verbose -l chrome' ./polygerrit-ui/app/run_test.sh
 ```
 
+### <a name="wct-to-karma"></a>Converting WCT tests to Karma
+
+If you are want to change a WCT test file (any `..._test.html` file), please convert the file to a
+Karma test file before making any changes. It is better to make a conversion in a separate change,
+so any conversion-related problems can be catch at this step.
+
+Usually, our WCT tests files have the following structure:
+```Html
+<!-- Test header: meta, title, wct scripts -->
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<meta charset="utf-8">
+<title>gr-account-link</title>
+
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
+
+<!-- Templates for test fixtures (optional) -->
+<test-fixture id="basic">
+   <template>
+     <gr-account-link></gr-account-link>
+   </template>
+ </test-fixture>
+
+<test-fixture id="other">
+   <template>
+     <gr-dialog>
+       <span>Hello!</span>
+     </gr-dialog>
+   </template>
+ </test-fixture>
+
+<!-- Tests -->
+<script type="module">
+// One or more imports:
+import '../../../test/common-test-setup.js';
+import ...;
+
+// Tests - one or more suites
+suite(..., () => {
+   ...
+   // instantiate 'basic' template:
+   element = fixture('basic');
+
+   ...
+   // instantiate 'other' template:
+   otherElements = fixture('other');
+
+);
+</script>
+```
+
+A conversion requires the following changes:
+* Rename the `..._test.html` file to the `..._test.js` file.
+* Remove test header (see a WCT test example above)
+* Remove all `<script...>` and `</script>` tags, but preserve javascript code
+* Change imports - use `test/common-test-setup-karma.js` instead of `test/common-test-setup.js`.
+Ensure, that the `common-test-setup-karma.js` import is placed above any other imports.
+* If there are test fixtures in the html file, move them inside a `<script>` tag and use
+the `fixtureFromTemplate` or `fixtureFromElement` (if there is only one element in a template)
+to define a test fixture template.
+* Use `instantiate` method instead of `fixture` method.
+
+After conversion, the Karma test file for the example above can look like:
+```Javascript
+import '../../../test/common-test-setup-karma.js';
+// Other imports:
+import ...
+
+// Define test fixtures templates:
+const fixture =
+  fixtureFromElement('gr-account-link');
+const otherFixture = fixtureFromTemplate(html`<gr-dialog>
+  <span>Hello!</span>
+</gr-dialog>
+`);
+
+// Tests - one or more suites
+suite(..., () => {
+   ...
+   // instantiate 'basic' template:
+   element = fixture.instantiate();
+
+   ...
+   // instantiate 'other' template:
+   otherElements = otherFixture.instantiate();
+
+);
+```
+
 ## Style guide
 
 We follow the [Google JavaScript Style Guide](https://google.github.io/styleguide/javascriptguide.xml)
@@ -299,4 +430,4 @@
 
 // install all dependencies and start the server
 npm start
-```
\ No newline at end of file
+```
diff --git a/polygerrit-ui/app/.eslintrc.js b/polygerrit-ui/app/.eslintrc.js
index 0302a76..e7506e0 100644
--- a/polygerrit-ui/app/.eslintrc.js
+++ b/polygerrit-ui/app/.eslintrc.js
@@ -109,7 +109,7 @@
     "prefer-promise-reject-errors": "error",
     "prefer-spread": "error",
     "quote-props": ["error", "consistent-as-needed"],
-    "semi": [2, "always"],
+    "semi": ["error", "always"],
     "template-curly-spacing": "error",
 
     "require-jsdoc": 0,
@@ -165,11 +165,9 @@
     // You must not add anything new in this list!
     // Instead export variables from modules
     // TODO(dmfilippov): Remove global variables from polygerrit
-    "GrReporting": "readonly",
     // Global variables from 3rd party libraries.
     // You should not add anything in this list, always try to import
     // If import is not possible - you can extend this list
-    "Polymer": "readonly",
     "ShadyCSS": "readonly",
     "linkify": "readonly",
     "security": "readonly",
@@ -182,7 +180,13 @@
       },
     },
     {
-      "files": ["*.html", "common-test-setup.js"],
+      "files": [
+        "*.html",
+        "common-test-setup.js",
+        "common-test-setup-karma.js",
+        "*_test.js",
+        "a11y-test-utils.js",
+      ],
       // Additional global variables allowed in tests
       "globals": {
         // Global variables from 3rd party test libraries/frameworks.
@@ -190,6 +194,7 @@
         // variables from these libraries and import is not possible
         "MockInteractions": "readonly",
         "_": "readonly",
+        "axs": "readonly",
         "a11ySuite": "readonly",
         "assert": "readonly",
         "expect": "readonly",
@@ -203,6 +208,8 @@
         "suiteSetup": "readonly",
         "teardown": "readonly",
         "test": "readonly",
+        "fixtureFromElement": "readonly",
+        "fixtureFromTemplate": "readonly",
       }
     },
     {
@@ -216,6 +223,7 @@
       "globals": {
         // Settings for samples. You can add globals here if you want to use it
         "Gerrit": "readonly",
+        "Polymer": "readonly",
       }
     },
     {
diff --git a/polygerrit-ui/app/BUILD b/polygerrit-ui/app/BUILD
index ca021db..096c665 100644
--- a/polygerrit-ui/app/BUILD
+++ b/polygerrit-ui/app/BUILD
@@ -58,6 +58,7 @@
     name = "test-srcs-fg",
     srcs = [
         "test/common-test-setup.js",
+        "test/common-test-setup-karma.js",
         "test/index.html",
         ":pg_code",
         "@ui_dev_npm//:node_modules",
diff --git a/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior_test.html b/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior_test.html
deleted file mode 100644
index 1d50cc4..0000000
--- a/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior_test.html
+++ /dev/null
@@ -1,56 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>async-foreach-behavior</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-<script type="module">
-import '../../test/common-test-setup.js';
-import {AsyncForeachBehavior} from './async-foreach-behavior.js';
-suite('async-foreach-behavior tests', () => {
-  test('loops over each item', () => {
-    const fn = sinon.stub().returns(Promise.resolve());
-    return AsyncForeachBehavior.asyncForeach([1, 2, 3], fn)
-        .then(() => {
-          assert.isTrue(fn.calledThrice);
-          assert.equal(fn.getCall(0).args[0], 1);
-          assert.equal(fn.getCall(1).args[0], 2);
-          assert.equal(fn.getCall(2).args[0], 3);
-        });
-  });
-
-  test('halts on stop condition', () => {
-    const stub = sinon.stub();
-    const fn = (e, stop) => {
-      stub(e);
-      stop();
-      return Promise.resolve();
-    };
-    return AsyncForeachBehavior.asyncForeach([1, 2, 3], fn)
-        .then(() => {
-          assert.isTrue(stub.calledOnce);
-          assert.equal(stub.lastCall.args[0], 1);
-        });
-  });
-});
-</script>
diff --git a/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior_test.js b/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior_test.js
new file mode 100644
index 0000000..0a44884
--- /dev/null
+++ b/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior_test.js
@@ -0,0 +1,45 @@
+/**
+ * @license
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '../../test/common-test-setup-karma.js';
+import {AsyncForeachBehavior} from './async-foreach-behavior.js';
+suite('async-foreach-behavior tests', () => {
+  test('loops over each item', () => {
+    const fn = sinon.stub().returns(Promise.resolve());
+    return AsyncForeachBehavior.asyncForeach([1, 2, 3], fn)
+        .then(() => {
+          assert.isTrue(fn.calledThrice);
+          assert.equal(fn.getCall(0).args[0], 1);
+          assert.equal(fn.getCall(1).args[0], 2);
+          assert.equal(fn.getCall(2).args[0], 3);
+        });
+  });
+
+  test('halts on stop condition', () => {
+    const stub = sinon.stub();
+    const fn = (e, stop) => {
+      stub(e);
+      stop();
+      return Promise.resolve();
+    };
+    return AsyncForeachBehavior.asyncForeach([1, 2, 3], fn)
+        .then(() => {
+          assert.isTrue(stub.calledOnce);
+          assert.equal(stub.lastCall.args[0], 1);
+        });
+  });
+});
diff --git a/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.js b/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.js
index 3c48fbe..51c52f1 100644
--- a/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.js
+++ b/polygerrit-ui/app/behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.js
@@ -87,7 +87,7 @@
     let links = ADMIN_LINKS.slice(0);
     let expandedSection;
 
-    const isExernalLink = link => link.url[0] !== '/';
+    const isExternalLink = link => link.url[0] !== '/';
 
     // Append top-level links that are defined by plugins.
     links.push(...getAdminMenuLinks().map(link => {
@@ -95,10 +95,10 @@
         url: link.url,
         name: link.text,
         capability: link.capability || null,
-        noBaseUrl: !isExernalLink(link),
+        noBaseUrl: !isExternalLink(link),
         view: null,
         viewableToAll: !link.capability,
-        target: isExernalLink(link) ? '_blank' : null,
+        target: isExternalLink(link) ? '_blank' : null,
       };
     }));
 
diff --git a/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior.js b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior.js
index 7745b42..be8c811 100644
--- a/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior.js
+++ b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior.js
@@ -15,12 +15,11 @@
  * limitations under the License.
  */
 
+import {SpecialFilePath} from '../../constants/constants.js';
+
 /** @polymerBehavior Gerrit.PathListBehavior */
 export const PathListBehavior = {
 
-  COMMIT_MESSAGE_PATH: '/COMMIT_MSG',
-  MERGE_LIST_PATH: '/MERGE_LIST',
-
   /**
    * @param {string} a
    * @param {string} b
@@ -28,18 +27,18 @@
    */
   specialFilePathCompare(a, b) {
     // The commit message always goes first.
-    if (a === PathListBehavior.COMMIT_MESSAGE_PATH) {
+    if (a === SpecialFilePath.COMMIT_MESSAGE) {
       return -1;
     }
-    if (b === PathListBehavior.COMMIT_MESSAGE_PATH) {
+    if (b === SpecialFilePath.COMMIT_MESSAGE) {
       return 1;
     }
 
     // The merge list always comes next.
-    if (a === PathListBehavior.MERGE_LIST_PATH) {
+    if (a === SpecialFilePath.MERGE_LIST) {
       return -1;
     }
-    if (b === PathListBehavior.MERGE_LIST_PATH) {
+    if (b === SpecialFilePath.MERGE_LIST) {
       return 1;
     }
 
@@ -67,10 +66,22 @@
     return aFile.localeCompare(bFile) || a.localeCompare(b);
   },
 
+  shouldHideFile(file) {
+    return file === SpecialFilePath.PATCHSET_LEVEL_COMMENTS;
+  },
+
+  addUnmodifiedFiles(files, commentedPaths) {
+    Object.keys(commentedPaths).forEach(commentedPath => {
+      if (files.hasOwnProperty(commentedPath) ||
+        this.shouldHideFile(commentedPath)) { return; }
+      files[commentedPath] = {status: 'U'};
+    });
+  },
+
   computeDisplayPath(path) {
-    if (path === PathListBehavior.COMMIT_MESSAGE_PATH) {
+    if (path === SpecialFilePath.COMMIT_MESSAGE) {
       return 'Commit message';
-    } else if (path === PathListBehavior.MERGE_LIST_PATH) {
+    } else if (path === SpecialFilePath.MERGE_LIST) {
       return 'Merge list';
     }
     return path;
@@ -78,8 +89,8 @@
 
   isMagicPath(path) {
     return !!path &&
-        (path === PathListBehavior.COMMIT_MESSAGE_PATH || path ===
-            PathListBehavior.MERGE_LIST_PATH);
+        (path === SpecialFilePath.COMMIT_MESSAGE || path ===
+            SpecialFilePath.MERGE_LIST);
   },
 
   computeTruncatedPath(path) {
diff --git a/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html
index 1b7a42a..cf5105e 100644
--- a/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html
@@ -24,6 +24,8 @@
 <script type="module">
 import '../../test/common-test-setup.js';
 import {PathListBehavior} from './gr-path-list-behavior.js';
+import {SpecialFilePath} from '../../constants/constants.js';
+
 suite('gr-path-list-behavior tests', () => {
   test('special sort', () => {
     const sort = PathListBehavior.specialFilePathCompare;
@@ -63,6 +65,20 @@
     assert.isTrue(isMagic('/MERGE_LIST'));
   });
 
+  test('patchset level comments are hidden', () => {
+    const commentedPaths = {
+      [SpecialFilePath.PATCHSET_LEVEL_COMMENTS]: true,
+      'file1.txt': true,
+    };
+
+    const files = {'file2.txt': {status: 'M'}};
+    PathListBehavior.addUnmodifiedFiles(files, commentedPaths);
+    assert.equal(files['file1.txt'].status, 'U');
+    assert.equal(files['file2.txt'].status, 'M');
+    assert.isFalse(files.hasOwnProperty(
+        SpecialFilePath.PATCHSET_LEVEL_COMMENTS));
+  });
+
   test('truncatePath with long path should add ellipsis', () => {
     const truncatePath = PathListBehavior.truncatePath;
     let path = 'level1/level2/level3/level4/file.js';
diff --git a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js
index 2a592f0..cc8b55a 100644
--- a/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js
+++ b/polygerrit-ui/app/behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../scripts/bundled-polymer.js';
-
 import '../../elements/shared/gr-tooltip/gr-tooltip.js';
 import {flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {getRootElement} from '../../scripts/rootElement.js';
diff --git a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js
index cb21a9f..09e3b72 100644
--- a/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js
+++ b/polygerrit-ui/app/behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js
@@ -95,12 +95,6 @@
 NOTE: doc-only shortcuts will not be customizable in the same way that other
 shortcuts are.
 */
-/*
-  FIXME(polymer-modulizer): the above comments were extracted
-  from HTML and may be out of place here. Review them and
-  then delete this comment!
-*/
-import '../../scripts/bundled-polymer.js';
 
 import {IronA11yKeysBehavior} from '@polymer/iron-a11y-keys-behavior/iron-a11y-keys-behavior.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
diff --git a/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.js b/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.js
index 919a763..05bf169 100644
--- a/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.js
+++ b/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.js
@@ -14,8 +14,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../scripts/bundled-polymer.js';
 import {BaseUrlBehavior} from '../base-url-behavior/base-url-behavior.js';
+import {ChangeStatus} from '../../constants/constants.js';
 
 /** @polymerBehavior Gerrit.RESTClientBehavior */
 export const RESTClientBehavior = [{
@@ -28,12 +28,6 @@
     REWRITE: 'REWRITE',
   },
 
-  ChangeStatus: {
-    ABANDONED: 'ABANDONED',
-    MERGED: 'MERGED',
-    NEW: 'NEW',
-  },
-
   // Must be kept in sync with the ListChangesOption enum and protobuf.
   ListChangesOption: {
     LABELS: 0,
@@ -125,7 +119,7 @@
   },
 
   changeIsOpen(change) {
-    return change && change.status === this.ChangeStatus.NEW;
+    return change && change.status === ChangeStatus.NEW;
   },
 
   /**
@@ -136,9 +130,9 @@
    */
   changeStatuses(change, opt_options) {
     const states = [];
-    if (change.status === this.ChangeStatus.MERGED) {
+    if (change.status === ChangeStatus.MERGED) {
       states.push('Merged');
-    } else if (change.status === this.ChangeStatus.ABANDONED) {
+    } else if (change.status === ChangeStatus.ABANDONED) {
       states.push('Abandoned');
     } else if (change.mergeable === false ||
         (opt_options && opt_options.mergeable === false)) {
diff --git a/polygerrit-ui/app/constants/constants.js b/polygerrit-ui/app/constants/constants.js
index cab50f6..5072594 100644
--- a/polygerrit-ui/app/constants/constants.js
+++ b/polygerrit-ui/app/constants/constants.js
@@ -19,8 +19,9 @@
  * @enum
  * @desc Tab names for primary tabs on change view page.
  */
-export const PrimaryTabs = {
+export const PrimaryTab = {
   FILES: '_files',
+  COMMENT_THREADS: '_commentThreads',
   FINDINGS: '_findings',
 };
 
@@ -28,8 +29,55 @@
  * @enum
  * @desc Tab names for secondary tabs on change view page.
  */
-export const SecondaryTabs = {
+export const SecondaryTab = {
   CHANGE_LOG: '_changeLog',
-  COMMENT_THREADS: '_commentThreads',
 };
 
+/**
+ * @enum
+ * @desc Tag names of change log messages.
+ */
+export const MessageTag = {
+  TAG_DELETE_REVIEWER: 'autogenerated:gerrit:deleteReviewer',
+  TAG_NEW_PATCHSET: 'autogenerated:gerrit:newPatchSet',
+  TAG_NEW_WIP_PATCHSET: 'autogenerated:gerrit:newWipPatchSet',
+  TAG_REVIEWER_UPDATE: 'autogenerated:gerrit:reviewerUpdate',
+  TAG_SET_PRIVATE: 'autogenerated:gerrit:setPrivate',
+  TAG_UNSET_PRIVATE: 'autogenerated:gerrit:unsetPrivate',
+  TAG_SET_READY: 'autogenerated:gerrit:setReadyForReview',
+  TAG_SET_WIP: 'autogenerated:gerrit:setWorkInProgress',
+  TAG_SET_ASSIGNEE: 'autogenerated:gerrit:setAssignee',
+  TAG_UNSET_ASSIGNEE: 'autogenerated:gerrit:deleteAssignee',
+};
+
+/**
+ * @enum
+ * @desc Modes for gr-diff-cursor
+ * The scroll behavior for the cursor. Values are 'never' and
+ * 'keep-visible'. 'keep-visible' will only scroll if the cursor is beyond
+ * the viewport.
+ */
+export const ScrollMode = {
+  KEEP_VISIBLE: 'keep-visible',
+  NEVER: 'never',
+};
+
+/**
+ * @enum
+ * @desc Specifies status for a change
+ */
+export const ChangeStatus = {
+  ABANDONED: 'ABANDONED',
+  MERGED: 'MERGED',
+  NEW: 'NEW',
+};
+
+/**
+ * @enum
+ * @desc Special file paths
+ */
+export const SpecialFilePath = {
+  PATCHSET_LEVEL_COMMENTS: '/PATCHSET_LEVEL',
+  COMMIT_MESSAGE: '/COMMIT_MSG',
+  MERGE_LIST: '/MERGE_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 e07a64e..632387e 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
@@ -14,7 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
 import '@polymer/iron-input/iron-input.js';
 import '../../../styles/gr-form-styles.js';
 import '../../../styles/shared-styles.js';
@@ -51,7 +50,7 @@
 const LABEL = 'Label';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrAccessSection extends mixinBehaviors( [
   AccessBehavior,
diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.html b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.js
similarity index 92%
rename from polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.html
rename to polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.js
index 95345fe..2128e29 100644
--- a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.js
@@ -1,47 +1,32 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2017 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
 
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-access-section</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/page/page.js"></script>
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-access-section></gr-access-section>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import './gr-access-section.js';
+
+const fixture = fixtureFromElement('gr-access-section');
+
 suite('gr-access-section tests', () => {
   let element;
   let sandbox;
 
   setup(() => {
     sandbox = sinon.sandbox.create();
-    element = fixture('basic');
+    element = fixture.instantiate();
   });
 
   teardown(() => {
@@ -245,7 +230,7 @@
     test('_computeSectionName', () => {
       let name;
       // When computing the section name for an undefined name, it means a
-      // new section is being added. In this case, it should defualt to
+      // new section is being added. In this case, it should default to
       // 'refs/heads/*'.
       element._editingRef = false;
       assert.equal(element._computeSectionName(name),
@@ -477,7 +462,7 @@
       });
 
       test('_handleValueChange', () => {
-        // For an exising section.
+        // For an existing section.
         const modifiedHandler = sandbox.stub();
         element.section = {id: 'refs/for/bar', value: {permissions: {}}};
         assert.notOk(element.section.value.updatedId);
@@ -553,4 +538,3 @@
     });
   });
 });
-</script>
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.js b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.js
index 36e2cc4..865fd33 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.js
+++ b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.js
@@ -14,7 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
 
 import '../../../styles/gr-table-styles.js';
 import '../../../styles/shared-styles.js';
@@ -33,7 +32,7 @@
 
 /**
  * @appliesMixin ListViewMixin
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrAdminGroupList extends mixinBehaviors( [
   ListViewBehavior,
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
index b318ee6..25ba83e 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
@@ -14,7 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
 import '../../../styles/gr-menu-page-styles.js';
 import '../../../styles/gr-page-nav-styles.js';
 import '../../../styles/shared-styles.js';
@@ -48,7 +47,7 @@
 const INTERNAL_GROUP_REGEX = /^[\da-f]{40}$/;
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrAdminView extends mixinBehaviors( [
   AdminNavBehavior,
diff --git a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.js b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.js
index b6b21bd..288af4c 100644
--- a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.js
+++ b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../shared/gr-dialog/gr-dialog.js';
 import '../../../styles/shared-styles.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
@@ -30,7 +28,7 @@
 };
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrConfirmDeleteItemDialog extends GestureEventListeners(
     LegacyElementMixin(PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.js b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.js
index 3347655..e16f5ce 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.js
+++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.js
@@ -14,10 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
-
 import '@polymer/iron-input/iron-input.js';
-import '../../../scripts/bundled-polymer.js';
 import '../../../styles/gr-form-styles.js';
 import '../../../styles/shared-styles.js';
 import '../../shared/gr-autocomplete/gr-autocomplete.js';
@@ -37,7 +34,7 @@
 const REF_PREFIX = 'refs/heads/';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrCreateChangeDialog extends mixinBehaviors( [
   BaseUrlBehavior,
@@ -152,11 +149,7 @@
     } else if (config && config.configured_value === 'FALSE') {
       return false;
     } else if (config && config.configured_value === 'INHERIT') {
-      if (config && config.inherited_value) {
-        return true;
-      } else {
-        return false;
-      }
+      return !!(config && config.inherited_value);
     } else {
       return false;
     }
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.html b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.html
index 87105a7..eaee4a4 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.html
@@ -57,7 +57,7 @@
       },
     });
     element = fixture('basic');
-    element.repoName = 'test-repo',
+    element.repoName = 'test-repo';
     element._repoConfig = {
       private_by_default: {
         configured_value: 'FALSE',
diff --git a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.js b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.js
index b21bdde..7f33663 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.js
+++ b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-input/iron-input.js';
 import '../../../styles/gr-form-styles.js';
 import '../../../styles/shared-styles.js';
@@ -30,7 +28,7 @@
 import page from 'page/page.mjs';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrCreateGroupDialog extends mixinBehaviors( [
   BaseUrlBehavior,
diff --git a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.js b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.js
index 72a6b99..eb131f5 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.js
+++ b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-input/iron-input.js';
 import '../../../styles/gr-form-styles.js';
 import '../../../styles/shared-styles.js';
@@ -37,7 +35,7 @@
 };
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrCreatePointerDialog extends mixinBehaviors( [
   BaseUrlBehavior,
diff --git a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.js b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.js
index 040f41b..0410f14 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.js
+++ b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-input/iron-input.js';
 import '../../../styles/gr-form-styles.js';
 import '../../../styles/shared-styles.js';
@@ -33,7 +31,7 @@
 import page from 'page/page.mjs';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrCreateRepoDialog extends mixinBehaviors( [
   BaseUrlBehavior,
diff --git a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.js b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.js
index a3c05cb..2c0c1302 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.js
+++ b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.js
@@ -15,7 +15,6 @@
  * limitations under the License.
  */
 
-import '../../../scripts/bundled-polymer.js';
 import '../../../styles/gr-table-styles.js';
 import '../../../styles/shared-styles.js';
 import '../../shared/gr-date-formatter/gr-date-formatter.js';
@@ -32,7 +31,7 @@
 const GROUP_EVENTS = ['ADD_GROUP', 'REMOVE_GROUP'];
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrGroupAuditLog extends mixinBehaviors( [
   ListViewBehavior,
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 45f7612..c54a709 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
@@ -14,7 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
 import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
 import '../../../styles/gr-form-styles.js';
 import '../../../styles/gr-subpage-styles.js';
@@ -41,7 +40,7 @@
 const URL_REGEX = '^(?:[a-z]+:)?//';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrGroupMembers extends mixinBehaviors( [
   BaseUrlBehavior,
@@ -122,12 +121,12 @@
           this._groupName = config.name;
 
           promises.push(this.$.restAPI.getIsAdmin().then(isAdmin => {
-            this._isAdmin = isAdmin ? true : false;
+            this._isAdmin = !!isAdmin;
           }));
 
           promises.push(this.$.restAPI.getIsGroupOwner(config.name)
               .then(isOwner => {
-                this._groupOwner = isOwner ? true : false;
+                this._groupOwner = !!isOwner;
               }));
 
           promises.push(this.$.restAPI.getGroupMembers(config.name).then(
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html
index 4dd9a7b..3a5bfd8 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html
@@ -302,32 +302,32 @@
   });
 
   test('delete member', () => {
-    const deletelBtns = dom(element.root)
+    const deleteBtns = dom(element.root)
         .querySelectorAll('.deleteMembersButton');
-    MockInteractions.tap(deletelBtns[0]);
+    MockInteractions.tap(deleteBtns[0]);
     assert.equal(element._itemId, '1000097');
     assert.equal(element._itemName, 'jane');
-    MockInteractions.tap(deletelBtns[1]);
+    MockInteractions.tap(deleteBtns[1]);
     assert.equal(element._itemId, '1000096');
     assert.equal(element._itemName, 'Test User');
-    MockInteractions.tap(deletelBtns[2]);
+    MockInteractions.tap(deleteBtns[2]);
     assert.equal(element._itemId, '1000095');
     assert.equal(element._itemName, 'Gerrit');
-    MockInteractions.tap(deletelBtns[3]);
+    MockInteractions.tap(deleteBtns[3]);
     assert.equal(element._itemId, '1000098');
     assert.equal(element._itemName, '1000098');
   });
 
   test('delete included groups', () => {
-    const deletelBtns = dom(element.root)
+    const deleteBtns = dom(element.root)
         .querySelectorAll('.deleteIncludedGroupButton');
-    MockInteractions.tap(deletelBtns[0]);
+    MockInteractions.tap(deleteBtns[0]);
     assert.equal(element._itemId, 'testId');
     assert.equal(element._itemName, 'testName');
-    MockInteractions.tap(deletelBtns[1]);
+    MockInteractions.tap(deleteBtns[1]);
     assert.equal(element._itemId, 'testId2');
     assert.equal(element._itemName, 'testName2');
-    MockInteractions.tap(deletelBtns[2]);
+    MockInteractions.tap(deleteBtns[2]);
     assert.equal(element._itemId, 'testId3');
     assert.equal(element._itemName, 'testName3');
   });
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group.js b/polygerrit-ui/app/elements/admin/gr-group/gr-group.js
index 1c7cc91..49cd8ea 100644
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group.js
+++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group.js
@@ -15,7 +15,6 @@
  * limitations under the License.
  */
 
-import '../../../scripts/bundled-polymer.js';
 import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
 import '../../../styles/gr-form-styles.js';
 import '../../../styles/gr-subpage-styles.js';
@@ -43,7 +42,7 @@
 };
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrGroup extends GestureEventListeners(
     LegacyElementMixin(PolymerElement)) {
@@ -142,12 +141,12 @@
           this._groupIsInternal = !!config.id.match(INTERNAL_GROUP_REGEX);
 
           promises.push(this.$.restAPI.getIsAdmin().then(isAdmin => {
-            this._isAdmin = isAdmin ? true : false;
+            this._isAdmin = !!isAdmin;
           }));
 
           promises.push(this.$.restAPI.getIsGroupOwner(config.name)
               .then(isOwner => {
-                this._groupOwner = isOwner ? true : false;
+                this._groupOwner = !!isOwner;
               }));
 
           // If visible to all is undefined, set to false. If it is defined
@@ -262,7 +261,7 @@
   }
 
   _computeGroupDisabled(owner, admin, groupIsInternal) {
-    return groupIsInternal && (admin || owner) ? false : true;
+    return !(groupIsInternal && (admin || owner));
   }
 
   _getGroupUUID(id) {
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group_html.js b/polygerrit-ui/app/elements/admin/gr-group/gr-group_html.js
index e11f989..0af87ad 100644
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group_html.js
+++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group_html.js
@@ -126,7 +126,7 @@
           <h3 id="options" class$="[[_computeHeaderClass(_options)]]">
             Group Options
           </h3>
-          <fieldset id="visableToAll">
+          <fieldset>
             <section>
               <span class="title">
                 Make group visible to all registered users
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 9fac11e..d25ee76 100644
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js
@@ -14,7 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
 
 import '@polymer/paper-toggle-button/paper-toggle-button.js';
 import '../../../styles/gr-form-styles.js';
@@ -48,7 +47,7 @@
  * Fired when a permission that was previously added was removed.
  *
  * @event added-permission-removed
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrPermission extends mixinBehaviors( [
   AccessBehavior,
@@ -211,11 +210,10 @@
     // It is possible to have a label name that is not included in the
     // 'labels' object. In this case, treat it like anything else.
     if (!labels[labelName]) { return; }
-    const label = {
+    return {
       name: labelName,
       values: this._computeLabelValues(labels[labelName].values),
     };
-    return label;
   }
 
   _computeLabelValues(values) {
@@ -299,7 +297,7 @@
     }
 
     // Wait for new rule to get value populated via gr-rule-editor, and then
-    // add to permission values as well, so that the change gets propogated
+    // add to permission values as well, so that the change gets propagated
     // back to the section. Since the rule is inside a dom-repeat, a flush
     // is needed.
     flush();
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.js b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.js
index 318c2c3..d9d37f6 100644
--- a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.js
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-input/iron-input.js';
 import '@polymer/paper-toggle-button/paper-toggle-button.js';
 import '../../../styles/gr-form-styles.js';
@@ -27,7 +25,7 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-plugin-config-array-editor_html.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrPluginConfigArrayEditor extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.js b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.js
index 154af6e..868a7cd 100644
--- a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.js
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/gr-table-styles.js';
 import '../../../styles/shared-styles.js';
 import '../../shared/gr-list-view/gr-list-view.js';
@@ -29,7 +27,7 @@
 
 /**
  * @appliesMixin ListViewMixin
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrPluginList extends mixinBehaviors( [
   ListViewBehavior,
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 9aa81f8..f6a1c10 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
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/gr-menu-page-styles.js';
 import '../../../styles/gr-subpage-styles.js';
 import '../../../styles/shared-styles.js';
@@ -83,7 +81,7 @@
 Defs.projectAccessInput;
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrRepoAccess extends mixinBehaviors( [
   AccessBehavior,
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 53b4989..d161423 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
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/shared-styles.js';
 import '../../shared/gr-button/gr-button.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
@@ -23,7 +21,7 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-repo-command_html.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrRepoCommand extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
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 1f1dc5b..03a2bd3 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
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
 import '../../../styles/gr-form-styles.js';
 import '../../../styles/gr-subpage-styles.js';
@@ -43,7 +41,7 @@
 const CREATE_CHANGE_SUCCEEDED_MESSAGE = 'Navigating to change';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrRepoCommands extends GestureEventListeners(
     LegacyElementMixin(PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.js b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.js
index 072fc721..f47ff76 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.js
@@ -14,7 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
 
 import '../../../styles/shared-styles.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
@@ -26,7 +25,7 @@
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrRepoDashboards extends GestureEventListeners(
     LegacyElementMixin(PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.js b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.js
index e8b3d9a..e3a9958 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.js
@@ -16,7 +16,6 @@
  */
 
 import '@polymer/iron-input/iron-input.js';
-import '../../../scripts/bundled-polymer.js';
 import '../../../styles/gr-form-styles.js';
 import '../../../styles/gr-table-styles.js';
 import '../../../styles/shared-styles.js';
@@ -47,7 +46,7 @@
 
 /**
  * @appliesMixin ListViewMixin
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrRepoDetailList extends mixinBehaviors( [
   ListViewBehavior,
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.js b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.js
index 59abb72..a8119e6 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/gr-table-styles.js';
 import '../../../styles/shared-styles.js';
 import '../../shared/gr-dialog/gr-dialog.js';
@@ -33,7 +31,7 @@
 
 /**
  * @appliesMixin ListViewMixin
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrRepoList extends mixinBehaviors( [
   ListViewBehavior,
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 d01f9ab..a70aa11 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
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-input/iron-input.js';
 import '@polymer/paper-toggle-button/paper-toggle-button.js';
 import '../../../styles/gr-form-styles.js';
@@ -34,7 +32,7 @@
 import {RepoPluginConfig} from '../../../behaviors/gr-repo-plugin-config-behavior/gr-repo-plugin-config-behavior.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrRepoPluginConfig extends mixinBehaviors( [
   RepoPluginConfig,
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js
index 05ae73d..f272708 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
 import '@polymer/iron-input/iron-input.js';
 import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
@@ -68,7 +66,7 @@
 };
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrRepo extends GestureEventListeners(
     LegacyElementMixin(PolymerElement)) {
@@ -355,8 +353,8 @@
       commands.push({
         title,
         command: commandObj[title]
-            .replace(/\$\{project\}/gi, encodeURI(repo))
-            .replace(/\$\{project-base-name\}/gi,
+            .replace(/\${project}/gi, encodeURI(repo))
+            .replace(/\${project-base-name}/gi,
                 encodeURI(repo.substring(repo.lastIndexOf('/') + 1))),
       });
     }
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 234015a..99957ff 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
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
 import '../../../styles/gr-form-styles.js';
 import '../../../styles/shared-styles.js';
@@ -79,7 +77,7 @@
 ];
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrRuleEditor extends mixinBehaviors( [
   AccessBehavior,
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 da13492..a3fe990 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
@@ -15,7 +15,6 @@
  * limitations under the License.
  */
 
-import '../../../scripts/bundled-polymer.js';
 import '../../../styles/gr-change-list-styles.js';
 import '../../shared/gr-account-link/gr-account-link.js';
 import '../../shared/gr-change-star/gr-change-star.js';
@@ -50,7 +49,7 @@
 
 /**
  * @appliesMixin RESTClientMixin
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrChangeListItem extends mixinBehaviors( [
   BaseUrlBehavior,
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 c416b11..361ad83 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
@@ -15,7 +15,6 @@
  * limitations under the License.
  */
 
-import '../../../scripts/bundled-polymer.js';
 import '../../shared/gr-icons/gr-icons.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
 import '../gr-change-list/gr-change-list.js';
@@ -45,7 +44,7 @@
 const LIMIT_OPERATOR_PATTERN = /\blimit:(\d+)/i;
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrChangeListView extends mixinBehaviors( [
   BaseUrlBehavior,
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 0a19ae1..bd78ae9 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
@@ -15,7 +15,6 @@
  * limitations under the License.
  */
 
-import '../../../scripts/bundled-polymer.js';
 import '../../../styles/gr-change-list-styles.js';
 import '../../shared/gr-cursor-manager/gr-cursor-manager.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
@@ -45,7 +44,7 @@
 const MAX_SHORTCUT_CHARS = 5;
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrChangeList extends mixinBehaviors( [
   BaseUrlBehavior,
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_html.js b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_html.js
index 17150df..e5193f8 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_html.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_html.js
@@ -138,7 +138,7 @@
   <gr-cursor-manager
     id="cursor"
     index="{{selectedIndex}}"
-    scroll-behavior="keep-visible"
+    scroll-mode="keep-visible"
     focus-on-move=""
   ></gr-cursor-manager>
   <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
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 3758a78..d9fd378 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
@@ -14,7 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
 
 import '../../../styles/shared-styles.js';
 import '../../shared/gr-button/gr-button.js';
@@ -24,7 +23,7 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-create-change-help_html.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrCreateChangeHelp extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.js b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.js
index 7e5e749..cc53e49 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.js
+++ b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.js
@@ -14,7 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
 
 import '../../shared/gr-dialog/gr-dialog.js';
 import '../../shared/gr-overlay/gr-overlay.js';
@@ -30,7 +29,7 @@
   PUSH_PREFIX: 'git push origin HEAD:refs/for/',
 };
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrCreateCommandsDialog extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.js b/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.js
index f8757ba..9062a3f 100644
--- a/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.js
+++ b/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.js
@@ -14,7 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
 
 import '../../shared/gr-dialog/gr-dialog.js';
 import '../../shared/gr-overlay/gr-overlay.js';
@@ -29,7 +28,7 @@
  * name and the branch name.
  *
  * @event confirm
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrCreateDestinationDialog extends GestureEventListeners(
     LegacyElementMixin(
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 8b8b981..1cc2316 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
@@ -14,11 +14,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/shared-styles.js';
 import '../gr-change-list/gr-change-list.js';
-import '../../core/gr-reporting/gr-reporting.js';
 import '../../shared/gr-button/gr-button.js';
 import '../../shared/gr-dialog/gr-dialog.js';
 import '../../shared/gr-overlay/gr-overlay.js';
@@ -34,11 +31,12 @@
 import {htmlTemplate} from './gr-dashboard-view_html.js';
 import {RESTClientBehavior} from '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
+import {appContext} from '../../../services/app-context.js';
 
 const PROJECT_PLACEHOLDER_PATTERN = /\$\{project\}/g;
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrDashboardView extends mixinBehaviors( [
   RESTClientBehavior,
@@ -98,6 +96,11 @@
     };
   }
 
+  constructor() {
+    super();
+    this.reporting = appContext.reportingService;
+  }
+
   static get observers() {
     return [
       '_paramsChanged(params.*)',
@@ -197,7 +200,7 @@
         })
         .then(() => {
           this._maybeShowDraftsBanner();
-          this.$.reporting.dashboardDisplayed();
+          this.reporting.dashboardDisplayed();
         })
         .catch(err => {
           this.dispatchEvent(new CustomEvent('title-change', {
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_html.js b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_html.js
index 3389bd0..cf1036f 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_html.js
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_html.js
@@ -119,5 +119,4 @@
   ></gr-create-destination-dialog>
   <gr-create-commands-dialog id="commandsDialog"></gr-create-commands-dialog>
   <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
-  <gr-reporting id="reporting"></gr-reporting>
 `;
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html
index 5965d06..2fcf233 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html
@@ -357,7 +357,7 @@
         });
     element.addEventListener('page-error', e => {
       assert.strictEqual(e.detail.response, response);
-      done();
+      paramsChangedPromise.then(done);
     });
     element.params = {
       view: GerritNav.View.DASHBOARD,
@@ -367,14 +367,14 @@
   });
 
   test('params change triggers dashboardDisplayed()', () => {
-    sandbox.stub(element.$.reporting, 'dashboardDisplayed');
+    sandbox.stub(element.reporting, 'dashboardDisplayed');
     element.params = {
       view: GerritNav.View.DASHBOARD,
       project: 'project',
       dashboard: 'dashboard',
     };
     return paramsChangedPromise.then(() => {
-      assert.isTrue(element.$.reporting.dashboardDisplayed.calledOnce);
+      assert.isTrue(element.reporting.dashboardDisplayed.calledOnce);
     });
   });
 });
diff --git a/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard.js b/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard.js
deleted file mode 100644
index 2523700..0000000
--- a/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard.js
+++ /dev/null
@@ -1,44 +0,0 @@
-/**
- * @license
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import '../../../scripts/bundled-polymer.js';
-
-import '../gr-change-list/gr-change-list.js';
-import '../gr-create-change-help/gr-create-change-help.js';
-import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
-import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
-import {PolymerElement} from '@polymer/polymer/polymer-element.js';
-import {htmlTemplate} from './gr-embed-dashboard_html.js';
-
-/** @extends Polymer.Element */
-class GrEmbedDashboard extends GestureEventListeners(
-    LegacyElementMixin(
-        PolymerElement)) {
-  static get template() { return htmlTemplate; }
-
-  static get is() { return 'gr-embed-dashboard'; }
-
-  static get properties() {
-    return {
-      account: Object,
-      sections: Array,
-      preferences: Object,
-      showNewUserHelp: Boolean,
-    };
-  }
-}
-
-customElements.define(GrEmbedDashboard.is, GrEmbedDashboard);
diff --git a/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard_html.js b/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard_html.js
deleted file mode 100644
index 802e365..0000000
--- a/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard_html.js
+++ /dev/null
@@ -1,35 +0,0 @@
-/**
- * @license
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-import {html} from '@polymer/polymer/lib/utils/html-tag.js';
-
-export const htmlTemplate = html`
-  <gr-change-list
-    show-star=""
-    account="[[account]]"
-    preferences="[[preferences]]"
-    sections="[[sections]]"
-  >
-    <div id="emptyOutgoing" slot="empty-outgoing">
-      <template is="dom-if" if="[[showNewUserHelp]]">
-        <gr-create-change-help></gr-create-change-help>
-      </template>
-      <template is="dom-if" if="[[!showNewUserHelp]]">
-        No changes
-      </template>
-    </div>
-  </gr-change-list>
-`;
diff --git a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.js b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.js
index 5f0021e..303c612 100644
--- a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.js
+++ b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.js
@@ -14,7 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
 
 import '../../../styles/dashboard-header-styles.js';
 import '../../../styles/shared-styles.js';
@@ -26,7 +25,7 @@
 import {htmlTemplate} from './gr-repo-header_html.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrRepoHeader extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js
index 6bb1bf8..7901c53 100644
--- a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js
+++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js
@@ -14,7 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
 
 import '../../../styles/shared-styles.js';
 import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
@@ -30,7 +29,7 @@
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrUserHeader extends GestureEventListeners(
     LegacyElementMixin(
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
index d65757a..03a64a5 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
@@ -14,10 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../admin/gr-create-change-dialog/gr-create-change-dialog.js';
-import '../../core/gr-reporting/gr-reporting.js';
 import '../../shared/gr-button/gr-button.js';
 import '../../shared/gr-dialog/gr-dialog.js';
 import '../../shared/gr-dropdown/gr-dropdown.js';
@@ -44,6 +41,7 @@
 import {RESTClientBehavior} from '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
+import {appContext} from '../../../services/app-context.js';
 
 const ERR_BRANCH_EMPTY = 'The destination branch can’t be empty.';
 const ERR_COMMIT_EMPTY = 'The commit message can’t be empty.';
@@ -232,7 +230,7 @@
 const SKIP_ACTION_KEYS = [ChangeActions.REVERT_SUBMISSION];
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrChangeActions extends mixinBehaviors( [
   PatchSetBehavior,
@@ -272,6 +270,7 @@
     this.ActionType = ActionType;
     this.ChangeActions = ChangeActions;
     this.RevisionActions = RevisionActions;
+    this.reporting = appContext.reportingService;
   }
 
   static get properties() {
@@ -998,7 +997,7 @@
     this._handleAction(type, key);
   }
 
-  _handleOveflowItemTap(e) {
+  _handleOverflowItemTap(e) {
     e.preventDefault();
     const el = dom(e).localTarget;
     const key = e.detail.action.__key;
@@ -1014,7 +1013,7 @@
   }
 
   _handleAction(type, key) {
-    this.$.reporting.reportInteraction(`${type}-${key}`);
+    this.reporting.reportInteraction(`${type}-${key}`);
     switch (type) {
       case ActionType.REVISION:
         this._handleRevisionAction(key);
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_html.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_html.js
index f12e600..916a56f 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_html.js
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_html.js
@@ -140,7 +140,7 @@
       tabindex="0"
       vertical-offset="32"
       horizontal-align="right"
-      on-tap-item="_handleOveflowItemTap"
+      on-tap-item="_handleOverflowItemTap"
       hidden$="[[_shouldHideActions(_menuActions.*, _loading)]]"
       disabled-ids="[[_disabledMenuActions]]"
       items="[[_menuActions]]"
@@ -271,5 +271,4 @@
   </gr-overlay>
   <gr-js-api-interface id="jsAPI"></gr-js-api-interface>
   <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
-  <gr-reporting id="reporting" category="change-actions"></gr-reporting>
 `;
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
index acf17ea..a763250 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
@@ -1966,7 +1966,7 @@
 
     test('_handleAction reports', () => {
       sandbox.stub(element, '_fireAction');
-      const reportStub = sandbox.stub(element.$.reporting, 'reportInteraction');
+      const reportStub = sandbox.stub(element.reporting, 'reportInteraction');
       element._handleAction('type', 'key');
       assert.isTrue(reportStub.called);
       assert.equal(reportStub.lastCall.args[0], 'type-key');
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 fda13cd..9d3b455 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
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/shared-styles.js';
 import '../../../styles/gr-change-metadata-shared-styles.js';
 import '../../../styles/gr-change-view-integration-shared-styles.js';
@@ -45,6 +43,7 @@
 import {RESTClientBehavior} from '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
 import {GrReviewerSuggestionsProvider, SUGGESTIONS_PROVIDERS_USERS_TYPES} from '../../../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
+import {ChangeStatus} from '../../../constants/constants.js';
 
 const HASHTAG_ADD_MESSAGE = 'Add Hashtag';
 
@@ -78,7 +77,7 @@
 };
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrChangeMetadata extends mixinBehaviors( [
   RESTClientBehavior,
@@ -323,7 +322,7 @@
   }
 
   _computeShowRequirements(change) {
-    if (change.status !== this.ChangeStatus.NEW) {
+    if (change.status !== ChangeStatus.NEW) {
       // TODO(maximeg) change this to display the stored
       // requirements, once it is implemented server-side.
       return false;
@@ -399,7 +398,7 @@
   _computeBranchUrl(project, branch) {
     if (!this.change || !this.change.status) return '';
     return GerritNav.getUrlForBranch(branch, project,
-        this.change.status == this.ChangeStatus.NEW ? 'open' :
+        this.change.status == ChangeStatus.NEW ? 'open' :
           this.change.status.toLowerCase());
   }
 
@@ -462,7 +461,7 @@
    *
    * @param {!Object} change
    * @param {string} role One of the values from _CHANGE_ROLE
-   * @return {Object|null} either an accound or null.
+   * @return {Object|null} either an account or null.
    */
   _getNonOwnerRole(change, role) {
     if (!change || !change.current_revision ||
diff --git a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.js b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.js
index d301813..78627a7 100644
--- a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.js
+++ b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/shared-styles.js';
 import '../../shared/gr-button/gr-button.js';
 import '../../shared/gr-icons/gr-icons.js';
@@ -30,7 +28,7 @@
 import {RESTClientBehavior} from '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrChangeRequirements extends mixinBehaviors( [
   RESTClientBehavior,
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 968f56f..5035818 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
@@ -14,11 +14,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/paper-tabs/paper-tabs.js';
 import '../../../styles/shared-styles.js';
-import '../../core/gr-reporting/gr-reporting.js';
 import '../../diff/gr-comment-api/gr-comment-api.js';
 import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
 import '../../plugins/gr-endpoint-param/gr-endpoint-param.js';
@@ -66,9 +63,11 @@
 import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
 import {RevisionInfo} from '../../shared/revision-info/revision-info.js';
 
-import {PrimaryTabs, SecondaryTabs} from '../../../constants/constants.js';
+import {PrimaryTab, SecondaryTab} from '../../../constants/constants.js';
 import {NO_ROBOT_COMMENTS_THREADS_MSG} from '../../../constants/messages.js';
 import {appContext} from '../../../services/app-context.js';
+import {ExperimentIds} from '../../../services/flags.js';
+import {ChangeStatus} from '../../../constants/constants.js';
 
 const CHANGE_ID_ERROR = {
   MISMATCH: 'mismatch',
@@ -131,7 +130,7 @@
 /**
  * @appliesMixin RESTClientMixin
  * @appliesMixin PatchSetMixin
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrChangeView extends mixinBehaviors( [
   KeyboardShortcutBehavior,
@@ -272,8 +271,8 @@
       _constants: {
         type: Object,
         value: {
-          SecondaryTabs,
-          PrimaryTabs,
+          SecondaryTab,
+          PrimaryTab,
         },
       },
       _messages: {
@@ -407,7 +406,7 @@
        */
       _activeTabs: {
         type: Array,
-        value: [PrimaryTabs.FILES, SecondaryTabs.CHANGE_LOG],
+        value: [PrimaryTab.FILES, SecondaryTab.CHANGE_LOG],
       },
       _showAllRobotComments: {
         type: Boolean,
@@ -449,6 +448,7 @@
   constructor() {
     super();
     this.flagsService = appContext.flagsService;
+    this.reporting = appContext.reportingService;
   }
 
   /** @override */
@@ -539,7 +539,7 @@
   }
 
   _isChangeLogExperimentEnabled() {
-    return this.flagsService.isEnabled('UiFeature__cleaner_changelog');
+    return this.flagsService.isEnabled(ExperimentIds.CLEANER_CHANGELOG);
   }
 
   get messagesList() {
@@ -626,7 +626,7 @@
     }
     if (paperTabs.selected !== activeIndex) {
       paperTabs.selected = activeIndex;
-      this.$.reporting.reportInteraction('show-tab', {tabName});
+      this.reporting.reportInteraction('show-tab', {tabName});
     }
     return tabName;
   }
@@ -741,7 +741,7 @@
   _computeHideEditCommitMessage(
       loggedIn, editing, change, editMode, collapsed, collapsible) {
     if (!loggedIn || editing ||
-        (change && change.status === this.ChangeStatus.MERGED) ||
+        (change && change.status === ChangeStatus.MERGED) ||
         editMode ||
         (collapsed && collapsible)) {
       return true;
@@ -820,8 +820,7 @@
     // Get any new drafts that have been saved in the diff view and show
     // in the comment thread view.
     this._reloadDrafts().then(() => {
-      this._commentThreads = this._changeComments.getAllThreadsForChange()
-          .map(c => Object.assign({}, c));
+      this._commentThreads = this._changeComments.getAllThreadsForChange();
       flush();
     });
   }
@@ -995,7 +994,7 @@
   _handleReplySent(e) {
     this.addEventListener('change-details-loaded',
         () => {
-          this.$.reporting.timeEnd(SEND_REPLY_TIMING_LABEL);
+          this.reporting.timeEnd(SEND_REPLY_TIMING_LABEL);
         }, {once: true});
     this.$.replyOverlay.close();
     this._reload();
@@ -1090,7 +1089,7 @@
   }
 
   _initActiveTabs(params = {}) {
-    let primaryTab = PrimaryTabs.FILES;
+    let primaryTab = PrimaryTab.FILES;
     if (params.queryMap && params.queryMap.has('tab')) {
       primaryTab = params.queryMap.get('tab');
     }
@@ -1102,7 +1101,7 @@
 
     // TODO: should drop this once we move CommentThreads tab
     // to primary as well
-    let secondaryTab = SecondaryTabs.CHANGE_LOG;
+    let secondaryTab = SecondaryTab.CHANGE_LOG;
     if (params.queryMap && params.queryMap.has('secondaryTab')) {
       secondaryTab = params.queryMap.get('secondaryTab');
     }
@@ -1199,7 +1198,7 @@
         .then(this._getLoggedIn.bind(this))
         .then(loggedIn => {
           if (!loggedIn || !this._change ||
-              this._change.status !== this.ChangeStatus.MERGED) {
+              this._change.status !== ChangeStatus.MERGED) {
           // Do not display dialog if not logged-in or the change is not
           // merged.
             return;
@@ -1739,11 +1738,18 @@
 
   _recomputeComments(comments) {
     this._changeComments = comments;
-    this._diffDrafts = Object.assign({}, this._changeComments.drafts);
-    this._commentThreads = this._changeComments.getAllThreadsForChange()
-        .map(c => Object.assign({}, c));
+    this._diffDrafts = {...this._changeComments.drafts};
+    this._commentThreads = this._changeComments.getAllThreadsForChange();
     this._draftCommentThreads = this._commentThreads
-        .filter(c => c.comments[c.comments.length - 1].__draft);
+        .filter(thread => thread.comments[thread.comments.length - 1].__draft)
+        .map(thread => {
+          const copiedThread = {...thread};
+          // Make a hardcopy of all comments and collapse all but last one
+          const commentsInThread = copiedThread.comments = thread.comments
+              .map(comment => { return {...comment, collapsed: true}; });
+          commentsInThread[commentsInThread.length - 1].collapsed = false;
+          return copiedThread;
+        });
   }
 
   /**
@@ -1758,8 +1764,8 @@
   _reload(opt_isLocationChange) {
     this._loading = true;
     this._relatedChangesCollapsed = true;
-    this.$.reporting.time(CHANGE_RELOAD_TIMING_LABEL);
-    this.$.reporting.time(CHANGE_DATA_TIMING_LABEL);
+    this.reporting.time(CHANGE_RELOAD_TIMING_LABEL);
+    this.reporting.time(CHANGE_DATA_TIMING_LABEL);
 
     // Array to house all promises related to data requests.
     const allDataPromises = [];
@@ -1778,9 +1784,9 @@
               {bubbles: true, composed: true}));
         })
         .then(() => {
-          this.$.reporting.timeEnd(CHANGE_RELOAD_TIMING_LABEL);
+          this.reporting.timeEnd(CHANGE_RELOAD_TIMING_LABEL);
           if (opt_isLocationChange) {
-            this.$.reporting.changeDisplayed();
+            this.reporting.changeDisplayed();
           }
         });
 
@@ -1850,9 +1856,9 @@
     }
 
     Promise.all(allDataPromises).then(() => {
-      this.$.reporting.timeEnd(CHANGE_DATA_TIMING_LABEL);
+      this.reporting.timeEnd(CHANGE_DATA_TIMING_LABEL);
       if (opt_isLocationChange) {
-        this.$.reporting.changeFullyLoaded();
+        this.reporting.changeFullyLoaded();
       }
     });
 
@@ -1878,8 +1884,8 @@
     // If the change is closed, it is not mergeable. Note: already merged
     // changes are obviously not mergeable, but the mergeability API will not
     // answer for abandoned changes.
-    if (this._change.status === this.ChangeStatus.MERGED ||
-        this._change.status === this.ChangeStatus.ABANDONED) {
+    if (this._change.status === ChangeStatus.MERGED ||
+        this._change.status === ChangeStatus.ABANDONED) {
       this._mergeable = false;
       return Promise.resolve();
     }
@@ -2059,11 +2065,11 @@
         let toastMessage = null;
         if (!result.isLatest) {
           toastMessage = ReloadToastMessage.NEWER_REVISION;
-        } else if (result.newStatus === this.ChangeStatus.MERGED) {
+        } else if (result.newStatus === ChangeStatus.MERGED) {
           toastMessage = ReloadToastMessage.MERGED;
-        } else if (result.newStatus === this.ChangeStatus.ABANDONED) {
+        } else if (result.newStatus === ChangeStatus.ABANDONED) {
           toastMessage = ReloadToastMessage.ABANDONED;
-        } else if (result.newStatus === this.ChangeStatus.NEW) {
+        } else if (result.newStatus === ChangeStatus.NEW) {
           toastMessage = ReloadToastMessage.RESTORED;
         } else if (result.newMessages) {
           toastMessage = ReloadToastMessage.NEW_MESSAGE;
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.js
index e8f8a68..7548017 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.js
@@ -551,7 +551,18 @@
     </section>
 
     <paper-tabs id="primaryTabs" on-selected-changed="_setActivePrimaryTab">
-      <paper-tab data-name$="[[_constants.PrimaryTabs.FILES]]">Files</paper-tab>
+      <paper-tab data-name$="[[_constants.PrimaryTab.FILES]]">Files</paper-tab>
+      <paper-tab
+        data-name$="[[_constants.PrimaryTab.COMMENT_THREADS]]"
+        class="commentThreads"
+      >
+        <gr-tooltip-content
+          has-tooltip=""
+          title$="[[_computeTotalCommentCounts(_change.unresolved_comment_count, _changeComments)]]"
+        >
+          <span>Comments</span></gr-tooltip-content
+        >
+      </paper-tab>
       <template
         is="dom-repeat"
         items="[[_dynamicTabHeaderEndpoints]]"
@@ -566,14 +577,14 @@
           </gr-endpoint-decorator>
         </paper-tab>
       </template>
-      <paper-tab data-name$="[[_constants.PrimaryTabs.FINDINGS]]">
+      <paper-tab data-name$="[[_constants.PrimaryTab.FINDINGS]]">
         Findings
       </paper-tab>
     </paper-tabs>
 
     <section class="patchInfo">
       <div
-        hidden$="[[!_isTabActive(_constants.PrimaryTabs.FILES, _activeTabs)]]"
+        hidden$="[[!_isTabActive(_constants.PrimaryTab.FILES, _activeTabs)]]"
       >
         <gr-file-list-header
           id="fileListHeader"
@@ -626,10 +637,22 @@
         >
         </gr-file-list>
       </div>
-
       <template
         is="dom-if"
-        if="[[_isTabActive(_constants.PrimaryTabs.FINDINGS, _activeTabs)]]"
+        if="[[_isTabActive(_constants.PrimaryTab.COMMENT_THREADS, _activeTabs)]]"
+      >
+        <gr-thread-list
+          threads="[[_commentThreads]]"
+          change="[[_change]]"
+          change-num="[[_changeNum]]"
+          logged-in="[[_loggedIn]]"
+          only-show-robot-comments-with-human-reply=""
+          on-thread-list-modified="_handleReloadDiffComments"
+        ></gr-thread-list>
+      </template>
+      <template
+        is="dom-if"
+        if="[[_isTabActive(_constants.PrimaryTab.FINDINGS, _activeTabs)]]"
       >
         <gr-dropdown-list
           class="patch-set-dropdown"
@@ -679,27 +702,16 @@
 
     <paper-tabs id="secondaryTabs" on-selected-changed="_setActiveSecondaryTab">
       <paper-tab
-        data-name$="[[_constants.SecondaryTabs.CHANGE_LOG]]"
+        data-name$="[[_constants.SecondaryTab.CHANGE_LOG]]"
         class="changeLog"
       >
         Change Log
       </paper-tab>
-      <paper-tab
-        data-name$="[[_constants.SecondaryTabs.COMMENT_THREADS]]"
-        class="commentThreads"
-      >
-        <gr-tooltip-content
-          has-tooltip=""
-          title$="[[_computeTotalCommentCounts(_change.unresolved_comment_count, _changeComments)]]"
-        >
-          <span>Comment Threads</span></gr-tooltip-content
-        >
-      </paper-tab>
     </paper-tabs>
     <section class="changeLog">
       <template
         is="dom-if"
-        if="[[_isTabActive(_constants.SecondaryTabs.CHANGE_LOG, _activeTabs)]]"
+        if="[[_isTabActive(_constants.SecondaryTab.CHANGE_LOG, _activeTabs)]]"
       >
         <template is="dom-if" if="[[!_isChangeLogExperimentEnabled()]]">
           <gr-messages-list
@@ -718,6 +730,7 @@
         <template is="dom-if" if="[[_isChangeLogExperimentEnabled()]]">
           <gr-messages-list-experimental
             class="hideOnMobileOverlay"
+            change="[[_change]]"
             change-num="[[_changeNum]]"
             labels="[[_change.labels]]"
             messages="[[_change.messages]]"
@@ -730,19 +743,6 @@
           ></gr-messages-list-experimental>
         </template>
       </template>
-      <template
-        is="dom-if"
-        if="[[_isTabActive(_constants.SecondaryTabs.COMMENT_THREADS, _activeTabs)]]"
-      >
-        <gr-thread-list
-          threads="[[_commentThreads]]"
-          change="[[_change]]"
-          change-num="[[_changeNum]]"
-          logged-in="[[_loggedIn]]"
-          only-show-robot-comments-with-human-reply=""
-          on-thread-list-modified="_handleReloadDiffComments"
-        ></gr-thread-list>
-      </template>
     </section>
   </div>
 
@@ -801,5 +801,4 @@
   <gr-js-api-interface id="jsAPI"></gr-js-api-interface>
   <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   <gr-comment-api id="commentAPI"></gr-comment-api>
-  <gr-reporting id="reporting"></gr-reporting>
 `;
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.js
similarity index 94%
rename from polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
rename to polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.js
index 62336e0..1895064 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.js
@@ -1,48 +1,24 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2015 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
 
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-change-view</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-<script src="/node_modules/page/page.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-change-view></gr-change-view>
-  </template>
-</test-fixture>
-
-<test-fixture id="blank">
-  <template>
-    <div></div>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import '../../edit/gr-edit-constants.js';
 import './gr-change-view.js';
-import {PrimaryTabs, SecondaryTabs} from '../../../constants/constants.js';
+import {PrimaryTab, SecondaryTab, ChangeStatus} from '../../../constants/constants.js';
 
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {KeyboardShortcutBinder} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
@@ -53,7 +29,10 @@
 import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
 import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
 
+import 'lodash/lodash.js';
+
 const pluginApi = _testOnly_initGerritPluginApi();
+const fixture = fixtureFromElement('gr-change-view');
 
 suite('gr-change-view tests', () => {
   const kb = KeyboardShortcutBinder;
@@ -76,6 +55,7 @@
 
   const ROBOT_COMMENTS_LIMIT = 10;
 
+  // TODO: should have a mock service to generate VALID fake data
   const THREADS = [
     {
       comments: [
@@ -88,7 +68,7 @@
           },
           patch_set: 2,
           robot_id: 'rb1',
-          id: 'ecf9fa_fe1a5f62',
+          id: 'ecf0b9fa_fe1a5f62',
           line: 5,
           updated: '2018-02-08 18:49:18.000000000',
           message: 'test',
@@ -102,7 +82,7 @@
             username: 'user',
           },
           patch_set: 4,
-          id: 'ecf0b9fa_fe1a5f62',
+          id: 'ecf0b9fa_fe1a5f62_1',
           line: 5,
           updated: '2018-02-08 18:49:18.000000000',
           message: 'test',
@@ -313,7 +293,7 @@
       getDiffDrafts() { return Promise.resolve({}); },
       _fetchSharedCacheURL() { return Promise.resolve({}); },
     });
-    element = fixture('basic');
+    element = fixture.instantiate();
     sandbox.stub(element.$.actions, 'reload').returns(Promise.resolve());
     pluginLoader.loadPlugins([]);
     pluginApi.install(
@@ -367,9 +347,9 @@
       assert(element._dynamicTabHeaderEndpoints.includes(
           'change-view-tab-header-url'));
       const paperTabs = element.shadowRoot.querySelector('#primaryTabs');
-      // 3 Tabs are : Files, Plugin, Findings
-      assert.equal(paperTabs.querySelectorAll('paper-tab').length, 3);
-      assert.equal(paperTabs.querySelectorAll('paper-tab')[1].dataset.name,
+      // 4 Tabs are : Files, Comment Threads, Plugin, Findings
+      assert.equal(paperTabs.querySelectorAll('paper-tab').length, 4);
+      assert.equal(paperTabs.querySelectorAll('paper-tab')[2].dataset.name,
           'change-view-tab-header-url');
     });
 
@@ -391,9 +371,9 @@
     });
 
     test('param change should switch primary tab correctly', done => {
-      assert.equal(element._activeTabs[0], PrimaryTabs.FILES);
+      assert.equal(element._activeTabs[0], PrimaryTab.FILES);
       const queryMap = new Map();
-      queryMap.set('tab', PrimaryTabs.FINDINGS);
+      queryMap.set('tab', PrimaryTab.FINDINGS);
       // view is required
       element.params = Object.assign(
           {
@@ -401,13 +381,13 @@
           },
           element.params, {queryMap});
       flush(() => {
-        assert.equal(element._activeTabs[0], PrimaryTabs.FINDINGS);
+        assert.equal(element._activeTabs[0], PrimaryTab.FINDINGS);
         done();
       });
     });
 
     test('invalid param change should not switch primary tab', done => {
-      assert.equal(element._activeTabs[0], PrimaryTabs.FILES);
+      assert.equal(element._activeTabs[0], PrimaryTab.FILES);
       const queryMap = new Map();
       queryMap.set('tab', 'random');
       // view is required
@@ -417,14 +397,14 @@
           },
           element.params, {queryMap});
       flush(() => {
-        assert.equal(element._activeTabs[0], PrimaryTabs.FILES);
+        assert.equal(element._activeTabs[0], PrimaryTab.FILES);
         done();
       });
     });
 
     test('switching tab sets _selectedTabPluginEndpoint', done => {
       const paperTabs = element.shadowRoot.querySelector('#primaryTabs');
-      MockInteractions.tap(paperTabs.querySelectorAll('paper-tab')[1]);
+      MockInteractions.tap(paperTabs.querySelectorAll('paper-tab')[2]);
       flush(() => {
         assert.equal(element._selectedTabPluginEndpoint,
             'change-view-tab-content-url');
@@ -714,6 +694,56 @@
     });
   });
 
+  suite('_recomputeComments', () => {
+    setup(() => {
+      // Fake computeDraftCount as its required for ChangeComments,
+      // see gr-comment-api#reloadDrafts.
+      sandbox.stub(element.$.commentAPI, 'reloadDrafts')
+          .returns(Promise.resolve({
+            drafts: {},
+            getAllThreadsForChange: () => THREADS,
+            computeDraftCount: () => 0,
+          }));
+    });
+
+    test('draft threads should be a new copy with correct states', done => {
+      element.$.fileList.dispatchEvent(
+          new CustomEvent('reload-drafts', {
+            detail: {
+              resolve: () => {
+                assert.equal(element._draftCommentThreads.length, 2);
+                assert.equal(
+                    element._draftCommentThreads[0].rootId,
+                    THREADS[0].rootId
+                );
+                assert.notEqual(
+                    element._draftCommentThreads[0].comments,
+                    THREADS[0].comments
+                );
+                assert.notEqual(
+                    element._draftCommentThreads[0].comments[0],
+                    THREADS[0].comments[0]
+                );
+                assert.isTrue(
+                    element._draftCommentThreads[0]
+                        .comments
+                        .slice(0, 2)
+                        .every(c => c.collapsed === true)
+                );
+
+                assert.isTrue(
+                    element._draftCommentThreads[0]
+                        .comments[2]
+                        .collapsed === false
+                );
+                done();
+              },
+            },
+            composed: true, bubbles: true,
+          }));
+    });
+  });
+
   test('diff comments modified', () => {
     sandbox.spy(element, '_handleReloadCommentThreads');
     return element._reloadComments().then(() => {
@@ -727,7 +757,7 @@
 
   test('thread list modified', () => {
     sandbox.spy(element, '_handleReloadDiffComments');
-    element._activeTabs = [PrimaryTabs.FILES, SecondaryTabs.COMMENT_THREADS];
+    element._activeTabs = [PrimaryTab.COMMENT_THREADS, SecondaryTab.CHANGE_LOG];
     flushAsynchronousOperations();
 
     return element._reloadComments().then(() => {
@@ -791,49 +821,8 @@
       element.params = {view: 'change', changeNum: '1'};
     });
 
-    test('tab switch works correctly', done => {
-      assert.isTrue(element._paramsChanged.called);
-      assert.equal(element._activeTabs[1], SecondaryTabs.CHANGE_LOG);
-
-      const commentTab = element.shadowRoot.querySelector(
-          'paper-tab.commentThreads'
-      );
-      // Switch to comment thread tab
-      MockInteractions.tap(commentTab);
-      assert.equal(element._activeTabs[1], SecondaryTabs.COMMENT_THREADS);
-
-      // Switch back to 'Change Log' tab
-      element._paramsChanged(element.params);
-      flush(() => {
-        assert.equal(element._activeTabs[1], SecondaryTabs.CHANGE_LOG);
-        done();
-      });
-    });
-
-    test('show-secondary-tab event works', () => {
-      assert.equal(element._activeTabs[1], SecondaryTabs.CHANGE_LOG);
-      // Switch to comment thread tab
-      element.fire('show-secondary-tab', {tab: SecondaryTabs.COMMENT_THREADS});
-      assert.equal(element._activeTabs[1], SecondaryTabs.COMMENT_THREADS);
-    });
-
-    test('param change should switched secondary tab correctly', done => {
-      assert.equal(element._activeTabs[1], SecondaryTabs.CHANGE_LOG);
-      const queryMap = new Map();
-      queryMap.set('secondaryTab', SecondaryTabs.COMMENT_THREADS);
-      // view is required
-      element.params = Object.assign(
-          {view: GerritNav.View.CHANGE},
-          element.params, {queryMap}
-      );
-      flush(() => {
-        assert.equal(element._activeTabs[1], SecondaryTabs.COMMENT_THREADS);
-        done();
-      });
-    });
-
     test('invalid secondaryTab should not switch tab', done => {
-      assert.equal(element._activeTabs[1], SecondaryTabs.CHANGE_LOG);
+      assert.equal(element._activeTabs[1], SecondaryTab.CHANGE_LOG);
       const queryMap = new Map();
       queryMap.set('secondaryTab', 'random');
       // view is required
@@ -841,7 +830,7 @@
         view: GerritNav.View.CHANGE,
       }, element.params, {queryMap});
       flush(() => {
-        assert.equal(element._activeTabs[1], SecondaryTabs.CHANGE_LOG);
+        assert.equal(element._activeTabs[1], SecondaryTab.CHANGE_LOG);
         done();
       });
     });
@@ -862,7 +851,7 @@
       };
       element._commentThreads = THREADS;
       const paperTabs = element.shadowRoot.querySelector('#primaryTabs');
-      MockInteractions.tap(paperTabs.querySelectorAll('paper-tab')[2]);
+      MockInteractions.tap(paperTabs.querySelectorAll('paper-tab')[3]);
       flush(() => {
         done();
       });
@@ -1009,7 +998,7 @@
     commitMessage = 'CC=test@google.com';
     result = element._prepareCommitMsgForLinkify(commitMessage);
     assert.equal(result, 'CC=\u200Btest@google.com');
-  }),
+  });
 
   test('_isSubmitEnabled', () => {
     assert.isFalse(element._isSubmitEnabled({}));
@@ -1319,7 +1308,7 @@
 
   test('show commit message edit button', () => {
     const _change = {
-      status: element.ChangeStatus.MERGED,
+      status: ChangeStatus.MERGED,
     };
     assert.isTrue(element._computeHideEditCommitMessage(false, false, {}));
     assert.isTrue(element._computeHideEditCommitMessage(true, true, {}));
@@ -1600,7 +1589,7 @@
         rev2: {_number: 2, commit: {parents: []}},
       },
       current_revision: 'rev1',
-      status: element.ChangeStatus.MERGED,
+      status: ChangeStatus.MERGED,
       labels: {},
       actions: {},
     };
@@ -1894,7 +1883,7 @@
         sandbox.stub(element, 'fetchChangeUpdates')
             .returns(Promise.resolve({
               isLatest: true,
-              newStatus: element.ChangeStatus.MERGED,
+              newStatus: ChangeStatus.MERGED,
             }));
         element.addEventListener('show-alert', e => {
           assert.equal(e.detail.message, 'This change has been merged');
@@ -2251,7 +2240,7 @@
 
     test('merged change', () => {
       element._mergeable = null;
-      element._change.status = element.ChangeStatus.MERGED;
+      element._change.status = ChangeStatus.MERGED;
       return element._getMergeability().then(() => {
         assert.isFalse(element._mergeable);
         assert.isFalse(getMergeableStub.called);
@@ -2260,7 +2249,7 @@
 
     test('abandoned change', () => {
       element._mergeable = null;
-      element._change.status = element.ChangeStatus.ABANDONED;
+      element._change.status = ChangeStatus.ABANDONED;
       return element._getMergeability().then(() => {
         assert.isFalse(element._mergeable);
         assert.isFalse(getMergeableStub.called);
@@ -2319,9 +2308,9 @@
 
     test('don\'t report changedDisplayed on reply', done => {
       const changeDisplayStub =
-        sandbox.stub(element.$.reporting, 'changeDisplayed');
+        sandbox.stub(element.reporting, 'changeDisplayed');
       const changeFullyLoadedStub =
-        sandbox.stub(element.$.reporting, 'changeFullyLoaded');
+        sandbox.stub(element.reporting, 'changeFullyLoaded');
       element._handleReplySent();
       flush(() => {
         assert.isFalse(changeDisplayStub.called);
@@ -2332,9 +2321,9 @@
 
     test('report changedDisplayed on _paramsChanged', done => {
       const changeDisplayStub =
-        sandbox.stub(element.$.reporting, 'changeDisplayed');
+        sandbox.stub(element.reporting, 'changeDisplayed');
       const changeFullyLoadedStub =
-        sandbox.stub(element.$.reporting, 'changeFullyLoaded');
+        sandbox.stub(element.reporting, 'changeFullyLoaded');
       element._paramsChanged({
         view: GerritNav.View.CHANGE,
         changeNum: 101,
@@ -2348,4 +2337,3 @@
     });
   });
 });
-</script>
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
index 7ca9d6b..576b8bd 100644
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
@@ -23,8 +23,6 @@
   from HTML and may be out of place here. Review them and
   then delete this comment!
 */
-
-import '../../../scripts/bundled-polymer.js';
 import '../../shared/gr-formatted-text/gr-formatted-text.js';
 import '../../../styles/shared-styles.js';
 import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
@@ -38,7 +36,7 @@
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrCommentList extends mixinBehaviors( [
   BaseUrlBehavior,
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_html.js b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_html.js
index 60b83ee..2811828 100644
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_html.js
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_html.js
@@ -34,6 +34,9 @@
       min-width: 135px;
       text-align: right;
     }
+    .patchset-level-comment-text {
+      margin-right: var(--spacing-m);
+    }
     .message {
       flex: 1;
       --gr-formatted-text-prose-max-width: 80ch;
@@ -55,9 +58,13 @@
     as="file"
   >
     <div class="file">
-      <a class="fileLink" href="[[_computeDiffURL(file, changeNum, comments)]]"
-        >[[computeDisplayPath(file)]]</a
-      >
+      <template is="dom-if" if="[[!shouldHideFile(file)]]">
+        <a
+          class="fileLink"
+          href="[[_computeDiffURL(file, changeNum, comments)]]"
+          >[[computeDisplayPath(file)]]
+        </a>
+      </template>
     </div>
     <template
       is="dom-repeat"
@@ -65,18 +72,25 @@
       as="comment"
     >
       <div class="container">
-        <a
-          class="lineNum"
-          href$="[[_computeDiffLineURL(file, changeNum, comment.patch_set, comment)]]"
-        >
-          <span hidden$="[[!comment.line]]">
-            <span>[[_computePatchDisplayName(comment)]]</span>
-            Line <span>[[comment.line]]</span>
+        <template is="dom-if" if="[[shouldHideFile(file)]]">
+          <span class="patchset-level-comment-text">
+            Patchset Comment:
           </span>
-          <span hidden$="[[comment.line]]">
-            File comment:
-          </span>
-        </a>
+        </template>
+        <template is="dom-if" if="[[!shouldHideFile(file)]]">
+          <a
+            class="lineNum"
+            href$="[[_computeDiffLineURL(file, changeNum, comment.patch_set, comment)]]"
+          >
+            <span hidden$="[[!comment.line]]">
+              <span>[[_computePatchDisplayName(comment)]]</span>
+              Line <span>[[comment.line]]</span>
+            </span>
+            <span hidden$="[[comment.line]]">
+              File comment:
+            </span>
+          </a>
+        </template>
         <gr-formatted-text
           class="message"
           no-trailing-margin=""
diff --git a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.js b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.js
index 4dba3af..052189e 100644
--- a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.js
+++ b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/shared-styles.js';
 import '../../shared/gr-copy-clipboard/gr-copy-clipboard.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
@@ -24,7 +22,7 @@
 import {htmlTemplate} from './gr-commit-info_html.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrCommitInfo extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.js
index d28e2b7..ada7dae 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.js
@@ -15,8 +15,6 @@
  * limitations under the License.
  */
 import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
-
-import '../../../scripts/bundled-polymer.js';
 import '../../shared/gr-dialog/gr-dialog.js';
 import '../../../styles/shared-styles.js';
 import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
@@ -27,7 +25,7 @@
 import {KeyboardShortcutBehavior} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrConfirmAbandonDialog extends mixinBehaviors( [
   KeyboardShortcutBehavior,
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.html
index 8010814..c1e1165 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.html
@@ -61,7 +61,6 @@
     assert.isTrue(confirmHandler.calledOnce);
     assert.isTrue(element._handleConfirmTap.called);
     assert.isTrue(element._confirm.called);
-    assert.isTrue(element._confirm.called);
     assert.isTrue(element._confirm.calledOnce);
   });
 
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.js
index 480e6cf..34a3dcc 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/shared-styles.js';
 import '../../shared/gr-dialog/gr-dialog.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
@@ -24,7 +22,7 @@
 import {htmlTemplate} from './gr-confirm-cherrypick-conflict-dialog_html.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrConfirmCherrypickConflictDialog extends GestureEventListeners(
     LegacyElementMixin(PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js
index 2802046..d2dcbca 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js
@@ -14,20 +14,18 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
 import '@polymer/iron-input/iron-input.js';
 import '../../../styles/shared-styles.js';
 import '../../shared/gr-autocomplete/gr-autocomplete.js';
 import '../../shared/gr-dialog/gr-dialog.js';
-import '../../core/gr-reporting/gr-reporting.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-confirm-cherrypick-dialog_html.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
+import {appContext} from '../../../services/app-context.js';
 
 const SUGGESTIONS_LIMIT = 15;
 const CHANGE_SUBJECT_LIMIT = 50;
@@ -37,7 +35,7 @@
 };
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrConfirmCherrypickDialog extends GestureEventListeners(
     LegacyElementMixin(PolymerElement)) {
@@ -97,6 +95,11 @@
     };
   }
 
+  constructor() {
+    super();
+    this.reporting = appContext.reportingService;
+  }
+
   static get observers() {
     return [
       '_computeMessage(changeStatus, commitNum, commitMessage)',
@@ -260,7 +263,7 @@
     e.preventDefault();
     e.stopPropagation();
     if (this._cherryPickType === CHERRY_PICK_TYPES.TOPIC) {
-      this.$.reporting.reportInteraction('cherry-pick-topic-clicked');
+      this.reporting.reportInteraction('cherry-pick-topic-clicked');
       this._handleCherryPickTopic();
       return;
     }
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_html.js b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_html.js
index aeb8061..eaf9cc8 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_html.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_html.js
@@ -212,9 +212,5 @@
       </template>
     </div>
   </gr-dialog>
-  <gr-reporting
-    id="reporting"
-    category="confirm-cherry-pick-dialog"
-  ></gr-reporting>
   <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
 `;
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.js
index 25beb2d..f2ea45d 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.js
@@ -14,9 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
-
-import '../../../scripts/bundled-polymer.js';
 import '../../../styles/shared-styles.js';
 import '../../shared/gr-autocomplete/gr-autocomplete.js';
 import '../../shared/gr-dialog/gr-dialog.js';
@@ -31,7 +28,7 @@
 const SUGGESTIONS_LIMIT = 15;
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrConfirmMoveDialog extends mixinBehaviors( [
   KeyboardShortcutBehavior,
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js
index e451034..0a45de2 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../shared/gr-autocomplete/gr-autocomplete.js';
 import '../../shared/gr-dialog/gr-dialog.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
@@ -25,7 +23,7 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-confirm-rebase-dialog_html.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrConfirmRebaseDialog extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js
index 6eb4c82..9489b94 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js
@@ -14,9 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
-
-import '../../../scripts/bundled-polymer.js';
 import '../../shared/gr-dialog/gr-dialog.js';
 import '../../../styles/shared-styles.js';
 import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
@@ -37,7 +34,7 @@
 };
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrConfirmRevertDialog extends GestureEventListeners(
     LegacyElementMixin(PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog.js
index 212f909..c8e14f7 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog.js
@@ -14,9 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
-
-import '../../../scripts/bundled-polymer.js';
 import '../../shared/gr-dialog/gr-dialog.js';
 import '../../../styles/shared-styles.js';
 import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
@@ -30,7 +27,7 @@
 const CHANGE_SUBJECT_LIMIT = 50;
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrConfirmRevertSubmissionDialog extends GestureEventListeners(
     LegacyElementMixin(PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.js
index 5d599b7..42afcc2 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-icon/iron-icon.js';
 import '../../shared/gr-icons/gr-icons.js';
 import '../../shared/gr-dialog/gr-dialog.js';
@@ -28,7 +26,7 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-confirm-submit-dialog_html.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrConfirmSubmitDialog extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js
index 4c457a1..6e22374 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/shared-styles.js';
 import '../../shared/gr-download-commands/gr-download-commands.js';
 import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
@@ -27,7 +25,7 @@
 import {RESTClientBehavior} from '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrDownloadDialog extends mixinBehaviors( [
   PatchSetBehavior,
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js
index 73c6721..1f9ca20 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/shared-styles.js';
 import '../../diff/gr-diff-mode-selector/gr-diff-mode-selector.js';
 import '../../diff/gr-patch-range-select/gr-patch-range-select.js';
@@ -43,7 +41,7 @@
 const MERGED_STATUS = 'MERGED';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrFileListHeader extends mixinBehaviors( [
   PatchSetBehavior,
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
index eb85cd7..12b5aad 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
@@ -14,10 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/shared-styles.js';
-import '../../core/gr-reporting/gr-reporting.js';
 import '../../diff/gr-diff-cursor/gr-diff-cursor.js';
 import '../../diff/gr-diff-host/gr-diff-host.js';
 import '../../diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.js';
@@ -46,6 +43,8 @@
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 import {pluginEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints.js';
 import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
+import {appContext} from '../../../services/app-context.js';
+import {SpecialFilePath} from '../../../constants/constants.js';
 
 // Maximum length for patch set descriptions.
 const PATCH_DESC_MAX_LENGTH = 500;
@@ -87,17 +86,7 @@
  */
 
 /**
- * Type for FileData
- *
- * This contains minimal info required about the file to get comments for
- *
- * @typedef {Object} FileData
- * @property {string} path
- * @property {?string} oldPath
- */
-
-/**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrFileList extends mixinBehaviors( [
   AsyncForeachBehavior,
@@ -206,7 +195,7 @@
        */
       _reportinShownFilesIncrement: Number,
 
-      /** @type {!Array<FileData>} */
+      /** @type {!Array<Gerrit.FileRange>} */
       _expandedFiles: {
         type: Array,
         value() { return []; },
@@ -290,6 +279,11 @@
     };
   }
 
+  constructor() {
+    super();
+    this.reporting = appContext.reportingService;
+  }
+
   /** @override */
   created() {
     super.created();
@@ -375,14 +369,14 @@
     return Promise.all(promises).then(() => {
       this._loading = false;
       this._detectChromiteButler();
-      this.$.reporting.fileListDisplayed();
+      this.reporting.fileListDisplayed();
     });
   }
 
   _detectChromiteButler() {
     const hasButler = !!document.getElementById('butler-suggested-owners');
     if (hasButler) {
-      this.$.reporting.reportExtension('butler');
+      this.reporting.reportExtension('butler');
     }
   }
 
@@ -444,13 +438,13 @@
   }
 
   _toggleFileExpandedByIndex(index) {
-    this._toggleFileExpanded(this._computeFileData(this._files[index]));
+    this._toggleFileExpanded(this._computeFileRange(this._files[index]));
   }
 
   _updateDiffPreferences() {
     if (!this.diffs.length) { return; }
     // Re-render all expanded diffs sequentially.
-    this.$.reporting.time(EXPAND_ALL_TIMING_LABEL);
+    this.reporting.time(EXPAND_ALL_TIMING_LABEL);
     this._renderInOrder(this._expandedFiles, this.diffs,
         this._expandedFiles.length);
   }
@@ -472,7 +466,7 @@
     for (let i = 0; i < this._shownFiles.length; i++) {
       path = this._shownFiles[i].__path;
       if (!this._expandedFiles.some(f => f.path === path)) {
-        newFiles.push(this._computeFileData(this._shownFiles[i]));
+        newFiles.push(this._computeFileRange(this._shownFiles[i]));
       }
     }
 
@@ -684,17 +678,17 @@
   }
 
   /**
-   * Generates file data from file info object.
+   * Generates file range from file info object.
    *
    * @param {FileInfo} file
-   * @returns {FileData}
+   * @returns {Gerrit.FileRange}
    */
-  _computeFileData(file) {
+  _computeFileRange(file) {
     const fileData = {
       path: file.__path,
     };
     if (file.old_path) {
-      fileData.oldPath = file.old_path;
+      fileData.basePath = file.old_path;
     }
     return fileData;
   }
@@ -910,7 +904,7 @@
         .some(arg => arg === undefined)) {
       return;
     }
-    if (editMode && path !== this.MERGE_LIST_PATH) {
+    if (editMode && path !== SpecialFilePath.MERGE_LIST) {
       return GerritNav.getEditUrlForDiff(change, path, patchRange.patchNum,
           patchRange.basePatchNum);
     }
@@ -953,12 +947,18 @@
     if (baseClass) {
       classes.push(baseClass);
     }
-    if (path === this.COMMIT_MESSAGE_PATH || path === this.MERGE_LIST_PATH) {
+    if (path === SpecialFilePath.COMMIT_MESSAGE ||
+      path === SpecialFilePath.MERGE_LIST) {
       classes.push('invisible');
     }
     return classes.join(' ');
   }
 
+  _computeStatusClass(file) {
+    const classStr = this._computeClass('status', file.__path);
+    return `${classStr} ${this._computeFileStatus(file.status)}`;
+  }
+
   _computePathClass(path, expandedFilesRecord) {
     return this._isFileExpanded(path, expandedFilesRecord) ? 'expanded' : '';
   }
@@ -985,10 +985,7 @@
 
     const commentedPaths = changeComments.getPaths(patchRange);
     const files = Object.assign({}, filesByPath);
-    Object.keys(commentedPaths).forEach(commentedPath => {
-      if (files.hasOwnProperty(commentedPath)) { return; }
-      files[commentedPath] = {status: 'U'};
-    });
+    this.addUnmodifiedFiles(files, commentedPaths);
     const reviewedSet = new Set(reviewed || []);
     for (const filePath in files) {
       if (!files.hasOwnProperty(filePath)) { continue; }
@@ -1016,7 +1013,7 @@
     // Start the timer for the rendering work hwere because this is where the
     // _shownFiles property is being set, and _shownFiles is used in the
     // dom-repeat binding.
-    this.$.reporting.time(RENDER_TIMING_LABEL);
+    this.reporting.time(RENDER_TIMING_LABEL);
 
     // How many more files are being shown (if it's an increase).
     this._reportinShownFilesIncrement =
@@ -1034,9 +1031,8 @@
   _filesChanged() {
     if (this._files && this._files.length > 0) {
       flush();
-      const files = Array.from(
+      this.$.fileCursor.stops = Array.from(
           dom(this.root).querySelectorAll('.file-row'));
-      this.$.fileCursor.stops = files;
       this.$.fileCursor.setCursorAtIndex(this.selectedIndex, true);
     }
   }
@@ -1141,7 +1137,7 @@
     // Required so that the newly created diff view is included in this.diffs.
     flush();
 
-    this.$.reporting.time(EXPAND_ALL_TIMING_LABEL);
+    this.reporting.time(EXPAND_ALL_TIMING_LABEL);
 
     if (newFiles.length) {
       this._renderInOrder(newFiles, this.diffs, newFiles.length);
@@ -1161,9 +1157,9 @@
   /**
    * Given an array of paths and a NodeList of diff elements, render the diff
    * for each path in order, awaiting the previous render to complete before
-   * continung.
+   * continuing.
    *
-   * @param  {!Array<FileData>} files
+   * @param  {!Array<Gerrit.FileRange>} files
    * @param  {!NodeList<!Object>} diffElements (GrDiffHostElement)
    * @param  {number} initialCount The total number of paths in the pass. This
    *   is used to generate log messages.
@@ -1200,9 +1196,17 @@
       this._cancelForEachDiff = null;
       this._nextRenderParams = null;
       console.log('Finished expanding', initialCount, 'diff(s)');
-      this.$.reporting.timeEndWithAverage(EXPAND_ALL_TIMING_LABEL,
+      this.reporting.timeEndWithAverage(EXPAND_ALL_TIMING_LABEL,
           EXPAND_ALL_AVG_TIMING_LABEL, initialCount);
-      this.$.diffCursor.handleDiffUpdate();
+      /* Block diff cursor from auto scrolling after files are done rendering.
+       * This prevents the bug where the screen jumps to the first diff chunk
+       * after files are done being rendered after the user has already begun
+       * scrolling.
+       * This also however results in the fact that the cursor does not auto
+       * focus on the first diff chunk on a small screen. This is however, a use
+       * case we are willing to not support for now.
+       */
+      this.$.diffCursor.reInitAndUpdateStops();
     }));
   }
 
@@ -1262,7 +1266,6 @@
         c, {__commentSide: threadEl.commentSide}
     ));
     flush();
-    return;
   }
 
   _handleEscKey(e) {
@@ -1307,7 +1310,8 @@
    * @return {boolean}
    */
   _showBarsForPath(path) {
-    return path !== this.COMMIT_MESSAGE_PATH && path !== this.MERGE_LIST_PATH;
+    return path !== SpecialFilePath.COMMIT_MESSAGE &&
+      path !== SpecialFilePath.MERGE_LIST;
   }
 
   /**
@@ -1421,7 +1425,7 @@
 
   /**
    * Shows registered dynamic columns iff the 'header', 'content' and
-   * 'summary' endpoints are regiestered the exact same number of times.
+   * 'summary' endpoints are registered the exact same number of times.
    * Ideally, there should be a better way to enforce the expectation of the
    * dependencies between dynamic endpoints.
    */
@@ -1452,7 +1456,7 @@
   _reportRenderedRow(index) {
     if (index === this._shownFiles.length - 1) {
       this.async(() => {
-        this.$.reporting.timeEndWithAverage(RENDER_TIMING_LABEL,
+        this.reporting.timeEndWithAverage(RENDER_TIMING_LABEL,
             RENDER_AVG_TIMING_LABEL, this._reportinShownFilesIncrement);
       }, 1);
     }
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.js
index a9a785e..df6f43d 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.js
@@ -26,8 +26,7 @@
       border-top: 1px solid var(--border-color);
       display: flex;
       min-height: calc(var(--line-height-normal) + 2 * var(--spacing-s));
-      padding: var(--spacing-xs) var(--spacing-l) var(--spacing-xs)
-        calc(var(--spacing-l) - 0.35rem);
+      padding: var(--spacing-xs) var(--spacing-l);
     }
     :host(.loading) .row {
       opacity: 0.5;
@@ -62,12 +61,31 @@
       align-items: center;
       display: inline-flex;
     }
-    .reviewed,
-    .status {
+    .reviewed {
       display: inline-block;
       text-align: left;
       width: 1.5em;
     }
+    .status {
+      display: inline-block;
+      border-radius: var(--border-radius);
+      margin-left: var(--spacing-s);
+      padding: 0 var(--spacing-m);
+      color: var(--primary-text-color);
+      font-size: var(--font-size-small);
+      background-color: var(--dark-add-highlight-color);
+    }
+    .status.M {
+      display: none;
+    }
+    .status.D,
+    .status.R,
+    .status.W {
+      background-color: var(--dark-remove-highlight-color);
+    }
+    .status.U {
+      background-color: var(--comment-background-color);
+    }
     .file-row {
       cursor: pointer;
     }
@@ -221,7 +239,8 @@
       padding: var(--spacing-s) 0;
       text-decoration: none;
     }
-    .pathLink:hover {
+    .pathLink:hover span.fullFileName,
+    .pathLink:hover span.truncatedFileName {
       text-decoration: underline;
     }
 
@@ -231,7 +250,6 @@
       display: inline-block;
       visibility: hidden;
       vertical-align: bottom;
-      text-decoration: none;
       --gr-button: {
         padding: 0px;
       }
@@ -277,7 +295,6 @@
   </style>
   <div id="container" on-click="_handleFileListClick">
     <div class="header-row row">
-      <div class="status"></div>
       <div class="path">File</div>
       <div class="comments">Comments</div>
       <div class="sizeBars">Size</div>
@@ -310,17 +327,9 @@
       <div class="stickyArea">
         <div
           class$="file-row row [[_computePathClass(file.__path, _expandedFiles.*)]]"
-          data-file$="[[_computeFileData(file)]]"
+          data-file$="[[_computeFileRange(file)]]"
           tabindex="-1"
         >
-          <div
-            class$="[[_computeClass('status', file.__path)]]"
-            tabindex="0"
-            title$="[[_computeFileStatusLabel(file.status)]]"
-            aria-label$="[[_computeFileStatusLabel(file.status)]]"
-          >
-            [[_computeFileStatus(file.status)]]
-          </div>
           <!-- TODO: Remove data-url as it appears its not used -->
           <span
             data-url="[[_computeDiffURL(change, patchRange, file.__path, editMode)]]"
@@ -342,6 +351,14 @@
               >
                 [[computeTruncatedPath(file.__path)]]
               </span>
+              <span
+                class$="[[_computeStatusClass(file)]]"
+                tabindex="0"
+                title$="[[_computeFileStatusLabel(file.status)]]"
+                aria-label$="[[_computeFileStatusLabel(file.status)]]"
+              >
+                [[_computeFileStatusLabel(file.status)]]
+              </span>
               <gr-copy-clipboard
                 hide-input=""
                 text="[[file.__path]]"
@@ -490,6 +507,7 @@
             hidden="[[!_isFileExpanded(file.__path, _expandedFiles.*)]]"
             change-num="[[changeNum]]"
             patch-range="[[patchRange]]"
+            file="[[_computeFileRange(file)]]"
             path="[[file.__path]]"
             prefs="[[diffPrefs]]"
             project-name="[[change.project]]"
@@ -583,9 +601,8 @@
   <gr-diff-cursor id="diffCursor"></gr-diff-cursor>
   <gr-cursor-manager
     id="fileCursor"
-    scroll-behavior="keep-visible"
+    scroll-mode="keep-visible"
     focus-on-move=""
     cursor-target-class="selected"
   ></gr-cursor-manager>
-  <gr-reporting id="reporting"></gr-reporting>
 `;
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
index 32945e4..f038f6c 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
@@ -45,9 +45,9 @@
 <script type="module">
 import '../../../test/common-test-setup.js';
 import '../../diff/gr-comment-api/gr-comment-api.js';
-import {getMockDiffResponse} from '../../../test/mock-diff-response.js';
+import {getMockDiffResponse} from '../../../test/mocks/diff-response.js';
 import './gr-file-list.js';
-import '../../diff/gr-comment-api/gr-comment-api-mock_test.js';
+import '../../../test/mocks/comment-api.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {KeyboardShortcutBinder} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
 import {GrFileListConstants} from '../gr-file-list-constants.js';
@@ -1641,7 +1641,7 @@
       assert.isTrue(diffStops[10].classList.contains('target-row'));
       assert.isFalse(diffStops[11].classList.contains('target-row'));
 
-      // The file cusor is now at 1.
+      // The file cursor is now at 1.
       assert.equal(element.$.fileCursor.index, 1);
       MockInteractions.keyUpOn(element, 73, null, 'i');
       flushAsynchronousOperations();
@@ -1652,7 +1652,7 @@
       const diffStopsFirst = diffs[0].getCursorStops();
       const diffStopsSecond = diffs[1].getCursorStops();
 
-      // The line on the first diff is stil selected
+      // The line on the first diff is still selected
       assert.isTrue(diffStopsFirst[10].classList.contains('target-row'));
       assert.isFalse(diffStopsSecond[10].classList.contains('target-row'));
     });
@@ -1683,7 +1683,7 @@
       assert.isFalse(diffStops[10].classList.contains('target-row'));
       assert.isTrue(diffStops[11].classList.contains('target-row'));
 
-      // The file cusor is still at 0.
+      // The file cursor is still at 0.
       assert.equal(element.$.fileCursor.index, 0);
     });
 
diff --git a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.js b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.js
index f79dba3..132cd16 100644
--- a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-input/iron-input.js';
 import '../../../styles/shared-styles.js';
 import '../../shared/gr-button/gr-button.js';
@@ -26,7 +24,7 @@
 import {htmlTemplate} from './gr-included-in-dialog_html.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrIncludedInDialog extends GestureEventListeners(
     LegacyElementMixin(PolymerElement)) {
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 8541840..5b9730b 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
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-selector/iron-selector.js';
 import '../../shared/gr-button/gr-button.js';
 import '../../../styles/gr-voting-styles.js';
@@ -25,7 +23,7 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-label-score-row_html.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrLabelScoreRow extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.html b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.html
index 6e0a90d..0fc6e4c 100644
--- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.html
+++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.html
@@ -268,7 +268,7 @@
     assert.isTrue(element.$.labelSelector.hidden);
   });
 
-  test('asymetrical labels', done => {
+  test('asymmetrical labels', done => {
     element.permittedLabels = {
       'Code-Review': [
         '-2',
diff --git a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.js b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.js
index 2d6825b..35ccc3c 100644
--- a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.js
+++ b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
 import '../gr-label-score-row/gr-label-score-row.js';
 import '../../../styles/shared-styles.js';
@@ -24,7 +22,7 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-label-scores_html.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrLabelScores extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
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 906ed34..3dda25b 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.js
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
@@ -14,7 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
 
 import '@polymer/iron-icon/iron-icon.js';
 import '../../shared/gr-account-label/gr-account-label.js';
@@ -26,16 +25,19 @@
 import '../../../styles/shared-styles.js';
 import '../../../styles/gr-voting-styles.js';
 import '../gr-comment-list/gr-comment-list.js';
+import {appContext} from '../../../services/app-context.js';
+import {ExperimentIds} from '../../../services/flags.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-message_html.js';
+import {SpecialFilePath} from '../../../constants/constants.js';
 
 const PATCH_SET_PREFIX_PATTERN = /^Patch Set \d+:\s*(.*)/;
 const LABEL_TITLE_SCORE_PATTERN = /^(-?)([A-Za-z0-9-]+?)([+-]\d+)?$/;
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrMessage extends GestureEventListeners(
     LegacyElementMixin(PolymerElement)) {
@@ -62,6 +64,8 @@
 
   static get properties() {
     return {
+      /** @type {?} */
+      change: Object,
       changeNum: Number,
       /** @type {?} */
       message: Object,
@@ -69,6 +73,19 @@
         type: Object,
         computed: '_computeAuthor(message)',
       },
+      /**
+       * Used in cleaner_changelog experiment
+       *
+       * @type {Array} - array of threads attached to the message
+       */
+      commentThreads: {
+        type: Array,
+      },
+      /**
+       * TODO(taoalpha): remove once the change log experiment is launched
+       *
+       * @type {Object} - a map on file and comments on it
+       */
       comments: {
         type: Object,
       },
@@ -122,11 +139,13 @@
       _messageContentCollapsed: {
         type: String,
         computed:
-            '_computeMessageContentCollapsed(message.message, message.tag)',
+            '_computeMessageContentCollapsed(message.message, message.tag,' +
+            ' commentThreads)',
       },
       _commentCountText: {
         type: Number,
-        computed: '_computeCommentCountText(comments)',
+        computed: '_computeCommentCountText(comments, commentThreads,'
+            + ' _isCleanerLogExperimentEnabled)',
       },
       _loggedIn: {
         type: Boolean,
@@ -140,6 +159,7 @@
         type: Boolean,
         value: false,
       },
+      _isCleanerLogExperimentEnabled: Boolean,
     };
   }
 
@@ -149,6 +169,11 @@
     ];
   }
 
+  constructor() {
+    super();
+    this.flagsService = appContext.flagsService;
+  }
+
   /** @override */
   created() {
     super.created();
@@ -159,6 +184,8 @@
   /** @override */
   ready() {
     super.ready();
+    this._isCleanerLogExperimentEnabled = this.flagsService
+        .isEnabled(ExperimentIds.CLEANER_CHANGELOG);
     this.$.restAPI.getConfig().then(config => {
       this.config = config;
     });
@@ -178,30 +205,76 @@
     }
   }
 
-  _computeCommentCountText(comments) {
-    if (!comments) return undefined;
-    let count = 0;
-    for (const file in comments) {
-      if (comments.hasOwnProperty(file)) {
-        const commentArray = comments[file] || [];
-        count += commentArray.length;
+  _computeCommentCountText(comments, threads, isCleanerLogExperimentEnabled) {
+    // TODO(taoalpha): clean up after cleaner-changelog experiment launched
+    if (isCleanerLogExperimentEnabled) {
+      if (!threads) return undefined;
+      const count = threads.length;
+      if (count === 0) {
+        return undefined;
+      } else if (count === 1) {
+        return '1 comment';
+      } else {
+        return `${count} comments`;
+      }
+    } else {
+      if (!comments) return undefined;
+      let count = 0;
+      for (const file in comments) {
+        if (comments.hasOwnProperty(file)) {
+          const commentArray = comments[file] || [];
+          count += commentArray.length;
+        }
+      }
+      if (count === 0) {
+        return undefined;
+      } else if (count === 1) {
+        return '1 comment';
+      } else {
+        return `${count} comments`;
       }
     }
-    if (count === 0) {
-      return undefined;
-    } else if (count === 1) {
-      return '1 comment';
-    } else {
-      return `${count} comments`;
-    }
+  }
+
+  _onThreadListModified() {
+    // TODO(taoalpha): this won't propagate the changes to the files
+    // should consider replacing this with either top level events
+    // or gerrit level events
+
+    // emit the event so change-view can also get updated with latest changes
+    this.fire('comment-refresh');
   }
 
   _computeMessageContentExpanded(content, tag) {
     return this._computeMessageContent(content, tag, true);
   }
 
-  _computeMessageContentCollapsed(content, tag) {
-    return this._computeMessageContent(content, tag, false);
+  _patchsetCommentSummary(commentThreads) {
+    const id = this.message.id;
+    if (!id) return '';
+    const patchsetThreads = commentThreads.filter(thread =>
+      thread.path === SpecialFilePath.PATCHSET_LEVEL_COMMENTS);
+    for (const thread of patchsetThreads) {
+      // Find if there was a patchset level comment created through the reply
+      // dialog and use it to determine the summary
+      if (thread.comments[0].change_message_id === id) {
+        return thread.comments[0].message;
+      }
+    }
+    // Find if there is a reply to some patchset comment left
+    for (const thread of patchsetThreads) {
+      for (const comment of thread.comments) {
+        if (comment.change_message_id === id) { return comment.message; }
+      }
+    }
+    return '';
+  }
+
+  _computeMessageContentCollapsed(content, tag, commentThreads) {
+    const summary =
+      this._computeMessageContent(content, tag, false);
+    if (summary || !commentThreads) return;
+    return this._patchsetCommentSummary(commentThreads);
   }
 
   _computeMessageContent(content, tag, isExpanded) {
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message_html.js b/polygerrit-ui/app/elements/change/gr-message/gr-message_html.js
index 753fd38..84713db 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message_html.js
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_html.js
@@ -66,6 +66,7 @@
       margin: 0 -4px;
     }
     .collapsed gr-comment-list,
+    .collapsed gr-thread-list,
     .collapsed .replyBtn,
     .collapsed .deleteBtn,
     .collapsed .hideOnCollapsed,
@@ -217,40 +218,56 @@
             content="[[_messageContentExpanded]]"
             config="[[_projectConfig.commentlinks]]"
           ></gr-formatted-text>
-          <template is="dom-if" if="[[_messageContentExpanded]]">
-            <div
-              class="replyActionContainer"
-              hidden$="[[!showReplyButton]]"
-              hidden=""
-            >
-              <gr-button
-                class="replyBtn"
-                link=""
-                small=""
-                on-click="_handleReplyTap"
-              >
-                Reply
-              </gr-button>
-              <gr-button
-                disabled$="[[_isDeletingChangeMsg]]"
-                class="deleteBtn"
-                hidden$="[[!_isAdmin]]"
+          <template is="dom-if" if="[[_expanded]]">
+            <template is="dom-if" if="[[_messageContentExpanded]]">
+              <div
+                class="replyActionContainer"
+                hidden$="[[!showReplyButton]]"
                 hidden=""
-                link=""
-                small=""
-                on-click="_handleDeleteMessage"
               >
-                Delete
-              </gr-button>
-            </div>
+                <gr-button
+                  class="replyBtn"
+                  link=""
+                  small=""
+                  on-click="_handleReplyTap"
+                >
+                  Reply
+                </gr-button>
+                <gr-button
+                  disabled$="[[_isDeletingChangeMsg]]"
+                  class="deleteBtn"
+                  hidden$="[[!_isAdmin]]"
+                  hidden=""
+                  link=""
+                  small=""
+                  on-click="_handleDeleteMessage"
+                >
+                  Delete
+                </gr-button>
+              </div>
+            </template>
+            <template is="dom-if" if="[[!_isCleanerLogExperimentEnabled]]">
+              <gr-comment-list
+                comments="[[comments]]"
+                change-num="[[changeNum]]"
+                patch-num="[[message._revision_number]]"
+                project-name="[[projectName]]"
+                project-config="[[_projectConfig]]"
+              ></gr-comment-list>
+            </template>
+            <template is="dom-if" if="[[_isCleanerLogExperimentEnabled]]">
+              <gr-thread-list
+                change="[[change]]"
+                hidden$="[[!commentThreads.length]]"
+                threads="[[commentThreads]]"
+                change-num="[[changeNum]]"
+                logged-in="[[_loggedIn]]"
+                hide-toggle-buttons
+                on-thread-list-modified="_onThreadListModified"
+              >
+              </gr-thread-list>
+            </template>
           </template>
-          <gr-comment-list
-            comments="[[comments]]"
-            change-num="[[changeNum]]"
-            patch-num="[[message._revision_number]]"
-            project-name="[[projectName]]"
-            project-config="[[_projectConfig]]"
-          ></gr-comment-list>
         </div>
       </template>
       <template is="dom-if" if="[[_computeIsReviewerUpdate(message)]]">
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
index 4651e66..edcb7f6 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
@@ -62,7 +62,7 @@
         date: '2016-01-12 20:24:49.448000000',
         message: 'Uploaded patch set 1.',
         _revision_number: 1,
-        expanded: false,
+        expanded: true,
       };
 
       element.addEventListener('reply', e => {
@@ -87,7 +87,7 @@
         date: '2016-01-12 20:24:49.448000000',
         message: 'Uploaded patch set 1.',
         _revision_number: 1,
-        expanded: false,
+        expanded: true,
       };
 
       flushAsynchronousOperations();
@@ -105,7 +105,7 @@
         date: '2016-01-12 20:24:49.448000000',
         message: 'Uploaded patch set 1.',
         _revision_number: 1,
-        expanded: false,
+        expanded: true,
       };
 
       element.addEventListener('change-message-deleted', e => {
@@ -378,7 +378,7 @@
         date: '2016-01-12 20:24:49.448000000',
         message: 'Uploaded patch set 1.',
         _revision_number: 1,
-        expanded: false,
+        expanded: true,
       };
 
       flushAsynchronousOperations();
@@ -391,6 +391,65 @@
     });
   });
 
+  suite('patchset comment summary', () => {
+    setup(() => {
+      element = fixture('basic');
+      element.message = {id: '6a07f64a82f96e7337ca5f7f84cfc73abf8ac2a3'};
+    });
+
+    test('single patchset comment posted', () => {
+      const threads = [{
+        comments: [{
+          __path: '/PATCHSET_LEVEL',
+          change_message_id: '6a07f64a82f96e7337ca5f7f84cfc73abf8ac2a3',
+          patch_set: 1,
+          id: 'e365b138_bed65caa',
+          updated: '2020-05-15 13:35:56.000000000',
+          message: 'testing the load',
+          unresolved: false,
+          path: '/PATCHSET_LEVEL',
+          collapsed: false,
+        }],
+        patchNum: 1,
+        path: '/PATCHSET_LEVEL',
+        rootId: 'e365b138_bed65caa',
+      }];
+      assert.equal(element._patchsetCommentSummary(threads),
+          'testing the load');
+    });
+
+    test('single patchset comment with reply', () => {
+      const threads = [{
+        comments: [{
+          __path: '/PATCHSET_LEVEL',
+          patch_set: 1,
+          id: 'e365b138_bed65caa',
+          updated: '2020-05-15 13:35:56.000000000',
+          message: 'testing the load',
+          unresolved: false,
+          path: '/PATCHSET_LEVEL',
+          collapsed: false,
+        }, {
+          __path: '/PATCHSET_LEVEL',
+          change_message_id: '6a07f64a82f96e7337ca5f7f84cfc73abf8ac2a3',
+          patch_set: 1,
+          id: 'd6efcc85_4cbbb6f4',
+          in_reply_to: 'e365b138_bed65caa',
+          updated: '2020-05-15 16:55:28.000000000',
+          message: 'n',
+          unresolved: false,
+          path: '/PATCHSET_LEVEL',
+          __draft: true,
+          collapsed: true,
+        }],
+        patchNum: 1,
+        path: '/PATCHSET_LEVEL',
+        rootId: 'e365b138_bed65caa',
+      }];
+      assert.equal(element._patchsetCommentSummary(threads), 'n');
+    });
+  });
+
   suite('when logged in but not admin', () => {
     setup(done => {
       stub('gr-rest-api-interface', {
@@ -414,7 +473,7 @@
         date: '2016-01-12 20:24:49.448000000',
         message: 'Uploaded patch set 1.',
         _revision_number: 1,
-        expanded: false,
+        expanded: true,
       };
 
       flushAsynchronousOperations();
@@ -444,7 +503,7 @@
         date: '2016-01-12 20:24:49.448000000',
         message: 'not empty',
         _revision_number: 1,
-        expanded: false,
+        expanded: true,
       };
       flushAsynchronousOperations();
       replyEl = element.shadowRoot.querySelector('.replyActionContainer');
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list-experimental.js b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list-experimental.js
index c888fcf..ccc4a76 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list-experimental.js
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list-experimental.js
@@ -14,11 +14,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/paper-toggle-button/paper-toggle-button.js';
-import '../../core/gr-reporting/gr-reporting.js';
 import '../../shared/gr-button/gr-button.js';
+import '../../shared/gr-icons/gr-icons.js';
 import '../gr-message/gr-message.js';
 import '../../../styles/shared-styles.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
@@ -28,7 +26,9 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-messages-list-experimental_html.js';
 import {KeyboardShortcutBehavior} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
-import {util} from '../../../scripts/util.js';
+import {parseDate} from '../../../utils/date-util.js';
+import {MessageTag} from '../../../constants/constants.js';
+import {appContext} from '../../../services/app-context.js';
 
 /**
  * The content of the enum is also used in the UI for the button text.
@@ -41,7 +41,115 @@
 };
 
 /**
- * @extends Polymer.Element
+ * Computes message author's comments for this change message. The backend
+ * sets comment.change_message_id for matching, so this computation is fairly
+ * straightforward.
+ */
+function computeThreads(message, allMessages, changeComments) {
+  if ([message, allMessages, changeComments].some(arg => arg === undefined)) {
+    return [];
+  }
+  if (message._index === undefined) {
+    return [];
+  }
+
+  return changeComments.getAllThreadsForChange().filter(
+      thread => thread.comments.map(comment => {
+        // collapse all by default
+        comment.collapsed = true;
+        return comment;
+      }).some(comment => {
+        const condition = comment.change_message_id === message.id;
+        // Since getAllThreadsForChange() always returns a new copy of
+        // all comments we can modify them here without worrying about
+        // polluting other threads.
+        comment.collapsed = !condition;
+        return condition;
+      })
+  );
+}
+
+/**
+ * If messages have the same tag, then that influences grouping and whether
+ * a message is initally hidden or not, see isImportant(). So we are applying
+ * some "magic" rules here in order to hide exactly the right messages.
+ *
+ * 1. If a message does not have a tag, but is associated with robot comments,
+ * then it gets a tag.
+ *
+ * 2. Use the same tag for some of Gerrit's standard events, if they should be
+ * considered one group, e.g. normal and wip patchset uploads.
+ *
+ * 3. Everything beyond the ~ character is cut off from the tag. That gives
+ * tools control over which messages will be hidden.
+ */
+function computeTag(message) {
+  if (!message.tag) {
+    const threads = message.commentThreads || [];
+    const comments = threads.map(
+        t => t.comments.find(c => c.change_message_id === message.id));
+    const isRobot = comments.some(c => c && !!c.robot_id);
+    return isRobot ? 'autogenerated:has-robot-comments' : undefined;
+  }
+
+  if (message.tag === MessageTag.TAG_NEW_WIP_PATCHSET) {
+    return MessageTag.TAG_NEW_PATCHSET;
+  }
+  if (message.tag === MessageTag.TAG_UNSET_ASSIGNEE) {
+    return MessageTag.TAG_SET_ASSIGNEE;
+  }
+  if (message.tag === MessageTag.TAG_UNSET_PRIVATE) {
+    return MessageTag.TAG_SET_PRIVATE;
+  }
+  if (message.tag === MessageTag.TAG_SET_WIP) {
+    return MessageTag.TAG_SET_READY;
+  }
+
+  return message.tag.replace(/~.*/, '');
+}
+
+/**
+ * Try to set a revision number that makes sense, if none is set. Just copy
+ * over the revision number of the next older message. This is mostly relevant
+ * for reviewer updates. Other messages should typically have the revision
+ * number already set.
+ */
+function computeRevision(message, allMessages) {
+  if (message._revision_number > 0) return message._revision_number;
+  let revision = 0;
+  for (const m of allMessages) {
+    if (m.date > message.date) break;
+    if (m._revision_number > revision) revision = m._revision_number;
+  }
+  return revision > 0 ? revision : undefined;
+}
+
+/**
+ * Unimportant messages are initially hidden.
+ *
+ * Human messages are always important. They have an undefined tag.
+ *
+ * Autogenerated messages are unimportant, if there is a message with the same
+ * tag and a higher revision number.
+ */
+function computeIsImportant(message, allMessages) {
+  if (!message.tag) return true;
+
+  const hasSameTag = m => m.tag === message.tag;
+  const revNumber = message._revision_number || 0;
+  const hasHigherRevisionNumber = m => m._revision_number > revNumber;
+  return !allMessages.filter(hasSameTag).some(hasHigherRevisionNumber);
+}
+
+export const TEST_ONLY = {
+  computeThreads,
+  computeTag,
+  computeRevision,
+  computeIsImportant,
+};
+
+/**
+ * @extends PolymerElement
  */
 class GrMessagesListExperimental extends mixinBehaviors( [
   KeyboardShortcutBehavior,
@@ -54,6 +162,8 @@
 
   static get properties() {
     return {
+      /** @type {?} */
+      change: Object,
       changeNum: Number,
       /**
        * These are just the change messages. They are combined with reviewer
@@ -95,17 +205,18 @@
         computed: '_computeExpandAllTitle(_expandAllState)',
       },
 
-      _hideAutomated: {
+      _showAllActivity: {
         type: Boolean,
         value: false,
-        observer: '_hideAutomatedChanged',
+        observer: '_observeShowAllActivity',
       },
       /**
        * The merged array of change messages and reviewer updates.
        */
       _combinedMessages: {
         type: Array,
-        computed: '_computeCombinedMessages(messages, reviewerUpdates)',
+        computed: '_computeCombinedMessages(messages, reviewerUpdates, '
+            + 'changeComments)',
         observer: '_combinedMessagesChanged',
       },
 
@@ -116,16 +227,21 @@
     };
   }
 
+  constructor() {
+    super();
+    this.reporting = appContext.reportingService;
+  }
+
   scrollToMessage(messageID) {
     const selector = `[data-message-id="${messageID}"]`;
     const el = this.shadowRoot.querySelector(selector);
 
-    if (!el && !this._hideAutomated) {
+    if (!el && this._showAllActivity) {
       console.warn(`Failed to scroll to message: ${messageID}`);
       return;
     }
     if (!el) {
-      this._hideAutomated = false;
+      this._showAllActivity = true;
       setTimeout(() => this.scrollToMessage(messageID));
       return;
     }
@@ -141,15 +257,7 @@
     this._highlightEl(el);
   }
 
-  _isAutomated(message) {
-    const isReviewerUpdate =
-        !!(message.reviewer || message.type === 'REVIEWER_UPDATE');
-    const isAutoGenerated =
-        !!(message.tag && message.tag.startsWith('autogenerated'));
-    return isReviewerUpdate || isAutoGenerated;
-  }
-
-  _hideAutomatedChanged(hideAutomated) {
+  _observeShowAllActivity(showAllActivity) {
     // We have to call render() such that the dom-repeat filter picks up the
     // change.
     this.$.messageRepeat.render();
@@ -159,13 +267,14 @@
    * Filter for the dom-repeat of combinedMessages.
    */
   _isMessageVisible(message) {
-    return !(this._hideAutomated && this._isAutomated(message));
+    return this._showAllActivity || message.isImportant;
   }
 
   /**
-   * Merges change messages and reviewer updates into one array.
+   * Merges change messages and reviewer updates into one array. Also processes
+   * all messages and updates, aligns or massages some of the properties.
    */
-  _computeCombinedMessages(messages, reviewerUpdates) {
+  _computeCombinedMessages(messages, reviewerUpdates, changeComments) {
     messages = messages || [];
     reviewerUpdates = reviewerUpdates || [];
     let mi = 0;
@@ -186,8 +295,8 @@
         combinedMessages = combinedMessages.concat(messages.slice(mi));
         break;
       }
-      mDate = mDate || util.parseDate(messages[mi].date);
-      rDate = rDate || util.parseDate(reviewerUpdates[ri].date);
+      mDate = mDate || parseDate(messages[mi].date);
+      rDate = rDate || parseDate(reviewerUpdates[ri].date);
       if (rDate < mDate) {
         combinedMessages.push(reviewerUpdates[ri++]);
         rDate = null;
@@ -200,6 +309,10 @@
       if (m.expanded === undefined) {
         m.expanded = false;
       }
+      m.commentThreads = computeThreads(m, combinedMessages, changeComments);
+      m._revision_number = computeRevision(m, combinedMessages);
+      m.tag = computeTag(m);
+      m.isImportant = computeIsImportant(m, combinedMessages);
     });
     return combinedMessages;
   }
@@ -228,8 +341,8 @@
   _highlightEl(el) {
     const highlightedEls =
         dom(this.root).querySelectorAll('.highlighted');
-    for (const highlighedEl of highlightedEls) {
-      highlighedEl.classList.remove('highlighted');
+    for (const highlightedEl of highlightedEls) {
+      highlightedEl.classList.remove('highlighted');
     }
     function handleAnimationEnd() {
       el.removeEventListener('animationend', handleAnimationEnd);
@@ -258,44 +371,12 @@
     this.scrollToMessage(e.detail.id);
   }
 
-  _hasAutomatedMessages(messages) {
-    if (!messages) { return false; }
-    for (const message of messages) {
-      if (this._isAutomated(message)) {
-        return true;
-      }
-    }
-    return false;
+  _isVisibleShowAllActivityToggle(messages = []) {
+    return messages.some(m => !m.isImportant);
   }
 
-  /**
-   * Computes message author's file comments for change's message. The backend
-   * sets comment.change_message_id for matching, so this computation is fairly
-   * straightforward.
-   *
-   * @param {!Object} changeComments changeComment object, which includes
-   *     a method to get all published comments (including robot comments),
-   *     which returns a Hash of arrays of comments, filename as key.
-   * @param {!Object} message
-   * @return {!Object} Hash of arrays of comments, filename as key.
-   */
-  _computeCommentsForMessage(changeComments, message) {
-    if ([changeComments, message].some(arg => arg === undefined)) {
-      return {};
-    }
-    const comments = changeComments.getAllPublishedComments();
-    if (message._index === undefined || !comments || !this.messages) {
-      return {};
-    }
-    const idFilter = comment => comment.change_message_id === message.id;
-
-    const msgComments = {};
-    for (const file in comments) {
-      if (!comments.hasOwnProperty(file)) { continue; }
-      const filtered = comments[file].filter(idFilter);
-      if (filtered.length) msgComments[file] = filtered;
-    }
-    return msgComments;
+  _computeHiddenEntriesCount(messages = []) {
+    return messages.filter(m => !m.isImportant).length;
   }
 
   /**
@@ -311,7 +392,7 @@
         acc[val] = (acc[val] || 0) + 1;
         return acc;
       }, {all: combinedMessages.length});
-      this.$.reporting.reportInteraction('messages-count', tagsCounted);
+      this.reporting.reportInteraction('messages-count', tagsCounted);
     }
   }
 
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list-experimental_html.js b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list-experimental_html.js
index 540418b..83edab3 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list-experimental_html.js
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list-experimental_html.js
@@ -45,29 +45,53 @@
       align-items: center;
       display: flex;
     }
+    .hiddenEntries {
+      color: var(--deemphasized-text-color);
+    }
     gr-message:not(:last-of-type) {
       border-bottom: 1px solid var(--border-color);
     }
-    gr-message:nth-child(2n) {
+    gr-message {
       background-color: var(--background-color-secondary);
     }
-    gr-message:nth-child(2n + 1) {
-      background-color: var(--background-color-tertiary);
+    .experimentMessage {
+      padding: var(--spacing-s) var(--spacing-m);
+      background-color: var(--emphasis-color);
+      border-radius: var(--border-radius);
+    }
+    .experimentMessage iron-icon {
+      vertical-align: top;
     }
   </style>
   <div class="header">
-    <span
-      id="automatedMessageToggleContainer"
-      class="container"
-      hidden$="[[!_hasAutomatedMessages(messages)]]"
-    >
-      <paper-toggle-button
-        id="automatedMessageToggle"
-        checked="{{_hideAutomated}}"
-      ></paper-toggle-button
-      >Only comments
-      <span class="transparent separator"></span>
-    </span>
+    <div id="showAllActivityToggleContainer" class="container">
+      <template
+        is="dom-if"
+        if="[[_isVisibleShowAllActivityToggle(_combinedMessages)]]"
+      >
+        <paper-toggle-button
+          class="showAllActivityToggle"
+          checked="{{_showAllActivity}}"
+        ></paper-toggle-button>
+        <div>
+          <span>Show all entries</span>
+          <span class="hiddenEntries" hidden$="[[_showAllActivity]]">
+            ([[_computeHiddenEntriesCount(_combinedMessages)]] hidden)
+          </span>
+        </div>
+        <span class="transparent separator"></span>
+      </template>
+    </div>
+    <div class="experimentMessage">
+      <iron-icon icon="gr-icons:pets"></iron-icon>
+      <span>You're currently viewing an experimental Change Log view.</span>
+      <a
+        target="_blank"
+        href="https://www.gerritcodereview.com/2020-05-06-change-log-experiment.html"
+      >
+        Learn more
+      </a>
+    </div>
     <gr-button
       id="collapse-messages"
       link=""
@@ -87,7 +111,7 @@
     <gr-message
       change-num="[[changeNum]]"
       message="[[message]]"
-      comments="[[_computeCommentsForMessage(changeComments, message)]]"
+      comment-threads="[[message.commentThreads]]"
       project-name="[[projectName]]"
       show-reply-button="[[showReplyButtons]]"
       on-message-anchor-tap="_handleAnchorClick"
@@ -95,5 +119,4 @@
       data-message-id$="[[message.id]]"
     ></gr-message>
   </template>
-  <gr-reporting id="reporting" category="message-list"></gr-reporting>
 `;
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list-experimental_test.html b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list-experimental_test.html
index 9c22ab5..71f25b7 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list-experimental_test.html
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list-experimental_test.html
@@ -46,8 +46,11 @@
 import '../../../test/common-test-setup.js';
 import '../../diff/gr-comment-api/gr-comment-api.js';
 import './gr-messages-list-experimental.js';
-import '../../diff/gr-comment-api/gr-comment-api-mock_test.js';
+import '../../../test/mocks/comment-api.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {TEST_ONLY} from './gr-messages-list-experimental.js';
+import {MessageTag} from '../../../constants/constants.js';
+
 const randomMessage = function(opt_params) {
   const params = opt_params || {};
   const author1 = {
@@ -61,14 +64,10 @@
     message: params.message || Math.random().toString(),
     _revision_number: params._revision_number || 1,
     author: params.author || author1,
+    tag: params.tag,
   };
 };
 
-const randomAutomated = function(opt_params) {
-  return Object.assign({tag: 'autogenerated:gerrit:replace'},
-      randomMessage(opt_params));
-};
-
 suite('gr-messages-list-experimental tests', () => {
   let element;
   let messages;
@@ -89,61 +88,53 @@
     email: 'marvin@sirius.org',
   };
 
+  const createComment = function() {
+    return {
+      id: '1a2b3c4d',
+      message: 'some random test text',
+      change_message_id: '8a7b6c5d',
+      updated: '2016-01-01 01:02:03.000000000',
+      line: 1,
+      patch_set: 1,
+      author,
+    };
+  };
+
   const comments = {
     file1: [
       {
-        message: 'message text',
+        ...createComment(),
         change_message_id: MESSAGE_ID_0,
-        updated: '2016-09-27 00:18:03.000000000',
         in_reply_to: '6505d749_f0bec0aa',
-        line: 62,
-        id: '6505d749_10ed44b2',
-        patch_set: 2,
         author: {
           email: 'some@email.com',
           _account_id: 123,
         },
       },
       {
-        message: 'message text',
+        ...createComment(),
+        id: '2b3c4d5e',
         change_message_id: MESSAGE_ID_1,
-        updated: '2016-09-27 00:18:03.000000000',
         in_reply_to: 'c5912363_6b820105',
-        line: 42,
-        id: '450a935e_0f1c05db',
-        patch_set: 2,
-        author,
       },
       {
-        message: 'message text',
+        ...createComment(),
+        id: '2b3c4d5e',
         change_message_id: MESSAGE_ID_1,
-        updated: '2016-09-27 00:18:03.000000000',
         in_reply_to: '6505d749_f0bec0aa',
-        line: 62,
-        id: '6505d749_10ed44b2',
-        patch_set: 2,
-        author,
       },
       {
-        message: 'message text',
-        change_message_id: MESSAGE_ID_2,
-        updated: '2016-09-27 00:18:03.000000000',
-        line: 64,
+        ...createComment(),
         id: '34ed05d749_10ed44b2',
-        patch_set: 2,
-        author,
+        change_message_id: MESSAGE_ID_2,
       },
     ],
     file2: [
       {
-        message: 'message text',
+        ...createComment(),
         change_message_id: MESSAGE_ID_1,
-        updated: '2016-09-27 00:18:03.000000000',
         in_reply_to: 'c5912363_4b7d450a',
-        line: 132,
         id: '450a935e_4f260d25',
-        patch_set: 2,
-        author,
       },
     ],
   };
@@ -215,9 +206,11 @@
       assert.isTrue([...getMessages()].filter(m => m._expanded).length == 0);
     });
 
-    test('hide messages does not appear when no automated messages', () => {
+    test('showAllActivity does not appear when all msgs are important', () => {
       assert.isOk(element.shadowRoot
-          .querySelector('#automatedMessageToggleContainer[hidden]'));
+          .querySelector('#showAllActivityToggleContainer'));
+      assert.isNotOk(element.shadowRoot
+          .querySelector('.showAllActivityToggle'));
     });
 
     test('scroll to message', () => {
@@ -265,7 +258,7 @@
               ._expanded);
     });
 
-    test('messages', () => {
+    test('associating messages with comments', () => {
       const messages = [].concat(
           randomMessage(),
           {
@@ -286,25 +279,145 @@
           }
       );
       element.messages = messages;
-      const isAuthor = function(author, comment) {
-        return comment.author._account_id === author._account_id;
-      };
-      const isMarvin = isAuthor.bind(null, author);
       flushAsynchronousOperations();
       const messageElements = getMessages();
       assert.equal(messageElements.length, messages.length);
       assert.deepEqual(messageElements[1].message, messages[1]);
       assert.deepEqual(messageElements[2].message, messages[2]);
-      assert.deepEqual(messageElements[1].comments.file1,
-          comments.file1.filter(isMarvin).filter(
-              c => c.change_message_id === messages[1].id));
-      assert.deepEqual(messageElements[1].comments.file2,
-          comments.file2.filter(isMarvin).filter(
-              c => c.change_message_id === messages[1].id));
-      assert.deepEqual(messageElements[2].comments.file1,
-          comments.file1.filter(isMarvin).filter(
-              c => c.change_message_id === messages[2].id));
-      assert.isUndefined(messageElements[2].comments.file2);
+    });
+
+    test('threads', () => {
+      const messages = [
+        {
+          _index: 5,
+          _revision_number: 4,
+          message: 'Uploaded patch set 4.',
+          date: '2016-09-28 13:36:33.000000000',
+          author,
+          id: '8c19ccc949c6d482b061be6a28e10782abf0e7af',
+        },
+      ];
+      element.messages = messages;
+      flushAsynchronousOperations();
+      const messageElements = getMessages();
+      // threads
+      assert.equal(
+          messageElements[0].commentThreads.length,
+          3);
+      // first thread contains 1 comment
+      assert.equal(
+          messageElements[0].commentThreads[0].comments.length,
+          1);
+    });
+
+    test('updateTag human message', () => {
+      const m = randomMessage();
+      assert.equal(TEST_ONLY.computeTag(m), undefined);
+    });
+
+    test('updateTag nothing to change', () => {
+      const m = randomMessage();
+      const tag = 'something-normal';
+      m.tag = tag;
+      assert.equal(TEST_ONLY.computeTag(m), tag);
+    });
+
+    test('updateTag TAG_NEW_WIP_PATCHSET', () => {
+      const m = randomMessage();
+      m.tag = MessageTag.TAG_NEW_WIP_PATCHSET;
+      assert.equal(TEST_ONLY.computeTag(m), MessageTag.TAG_NEW_PATCHSET);
+    });
+
+    test('updateTag remove postfix', () => {
+      const m = randomMessage();
+      m.tag = 'something~withpostfix';
+      assert.equal(TEST_ONLY.computeTag(m), 'something');
+    });
+
+    test('updateTag with robot comments', () => {
+      const m = randomMessage();
+      m.commentThreads = [{
+        comments: [{
+          robot_id: 'id314',
+          change_message_id: m.id,
+        }],
+      }];
+      assert.notEqual(TEST_ONLY.computeTag(m), undefined);
+    });
+
+    test('setRevisionNumber nothing to change', () => {
+      const m1 = randomMessage();
+      const m2 = randomMessage();
+      assert.equal(TEST_ONLY.computeRevision(m1, [m1, m2]), 1);
+      assert.equal(TEST_ONLY.computeRevision(m2, [m1, m2]), 1);
+    });
+
+    test('setRevisionNumber reviewer updates', () => {
+      const m1 = randomMessage(
+          {
+            tag: MessageTag.TAG_REVIEWER_UPDATE,
+            date: '2020-01-01 10:00:00.000000000',
+          });
+      m1._revision_number = undefined;
+      const m2 = randomMessage(
+          {
+            date: '2020-01-02 10:00:00.000000000',
+          });
+      m2._revision_number = 1;
+      const m3 = randomMessage(
+          {
+            tag: MessageTag.TAG_REVIEWER_UPDATE,
+            date: '2020-01-03 10:00:00.000000000',
+          });
+      m3._revision_number = undefined;
+      const m4 = randomMessage(
+          {
+            date: '2020-01-04 10:00:00.000000000',
+          });
+      m4._revision_number = 2;
+      const m5 = randomMessage(
+          {
+            tag: MessageTag.TAG_REVIEWER_UPDATE,
+            date: '2020-01-05 10:00:00.000000000',
+          });
+      m5._revision_number = undefined;
+      const allMessages = [m1, m2, m3, m4, m5];
+      assert.equal(TEST_ONLY.computeRevision(m1, allMessages), undefined);
+      assert.equal(TEST_ONLY.computeRevision(m2, allMessages), 1);
+      assert.equal(TEST_ONLY.computeRevision(m3, allMessages), 1);
+      assert.equal(TEST_ONLY.computeRevision(m4, allMessages), 2);
+      assert.equal(TEST_ONLY.computeRevision(m5, allMessages), 2);
+    });
+
+    test('isImportant human message', () => {
+      const m = randomMessage();
+      assert.isTrue(TEST_ONLY.computeIsImportant(m, []));
+      assert.isTrue(TEST_ONLY.computeIsImportant(m, [m]));
+    });
+
+    test('isImportant even with a tag', () => {
+      const m1 = randomMessage();
+      const m2 = randomMessage({tag: 'autogenerated:gerrit1'});
+      const m3 = randomMessage({tag: 'autogenerated:gerrit2'});
+      assert.isTrue(TEST_ONLY.computeIsImportant(m2, []));
+      assert.isTrue(TEST_ONLY.computeIsImportant(m1, [m1, m2, m3]));
+      assert.isTrue(TEST_ONLY.computeIsImportant(m2, [m1, m2, m3]));
+      assert.isTrue(TEST_ONLY.computeIsImportant(m3, [m1, m2, m3]));
+    });
+
+    test('isImportant filters same tag and older revision', () => {
+      const m1 = randomMessage({tag: 'auto', _revision_number: 2});
+      const m2 = randomMessage({tag: 'auto', _revision_number: 1});
+      const m3 = randomMessage({tag: 'auto'});
+      assert.isTrue(TEST_ONLY.computeIsImportant(m1, [m1]));
+      assert.isTrue(TEST_ONLY.computeIsImportant(m2, [m2]));
+      assert.isTrue(TEST_ONLY.computeIsImportant(m1, [m1, m2]));
+      assert.isFalse(TEST_ONLY.computeIsImportant(m2, [m1, m2]));
+      assert.isTrue(TEST_ONLY.computeIsImportant(m1, [m1, m3]));
+      assert.isFalse(TEST_ONLY.computeIsImportant(m3, [m1, m3]));
+      assert.isTrue(TEST_ONLY.computeIsImportant(m1, [m1, m2, m3]));
+      assert.isFalse(TEST_ONLY.computeIsImportant(m2, [m1, m2, m3]));
+      assert.isFalse(TEST_ONLY.computeIsImportant(m3, [m1, m2, m3]));
     });
 
     test('messages without author do not throw', () => {
@@ -329,18 +442,6 @@
     let sandbox;
     let commentApiWrapper;
 
-    const getMessages = function() {
-      return dom(element.root).querySelectorAll('gr-message');
-    };
-    const getHiddenMessages = function() {
-      return dom(element.root).querySelectorAll('gr-message[hidden]');
-    };
-
-    const randomMessageReviewer = {
-      reviewer: {},
-      date: '2016-01-13 20:30:33.038000',
-    };
-
     setup(() => {
       stub('gr-rest-api-interface', {
         getConfig() { return Promise.resolve({}); },
@@ -351,8 +452,11 @@
       });
 
       sandbox = sinon.sandbox.create();
-      messages = _.times(2, randomAutomated);
-      messages.push(randomMessageReviewer);
+      messages = [
+        randomMessage(),
+        randomMessage({tag: 'auto', _revision_number: 2}),
+        randomMessage({tag: 'auto', _revision_number: 3}),
+      ];
 
       // Element must be wrapped in an element with direct access to the
       // comment API.
@@ -371,41 +475,34 @@
     });
 
     test('hide autogenerated button is not hidden', () => {
-      assert.isNotOk(element.shadowRoot
-          .querySelector('#automatedMessageToggle[hidden]'));
+      const toggle = dom(element.root).querySelector('.showAllActivityToggle');
+      assert.isOk(toggle);
     });
 
-    test('autogenerated messages are not hidden initially', () => {
-      const allHiddenMessageEls = getHiddenMessages();
-
-      // There are no hidden messages.
-      assert.isFalse(!!allHiddenMessageEls.length);
+    test('one unimportant message is hidden initially', () => {
+      const displayedMsgs = dom(element.root).querySelectorAll('gr-message');
+      assert.equal(displayedMsgs.length, 2);
     });
 
-    test('autogenerated messages hidden after comments only toggle', () => {
-      let allHiddenMessageEls = getHiddenMessages();
-
-      element._hideAutomated = false;
-      MockInteractions.tap(element.$.automatedMessageToggle);
+    test('unimportant messages hidden after toggle', () => {
+      element._showAllActivity = true;
+      const toggle = dom(element.root).querySelector('.showAllActivityToggle');
+      assert.isOk(toggle);
+      MockInteractions.tap(toggle);
       flushAsynchronousOperations();
-      const allMessageEls = getMessages();
-      allHiddenMessageEls = getHiddenMessages();
-
-      // Autogenerated messages are now hidden.
-      assert.equal(allHiddenMessageEls.length, allMessageEls.length);
+      const displayedMsgs = dom(element.root).querySelectorAll('gr-message');
+      assert.equal(displayedMsgs.length, 2);
     });
 
-    test('autogenerated messages not hidden after comments only toggle',
-        () => {
-          let allHiddenMessageEls = getHiddenMessages();
-
-          element._hideAutomated = true;
-          MockInteractions.tap(element.$.automatedMessageToggle);
-          allHiddenMessageEls = getHiddenMessages();
-
-          // Autogenerated messages are now hidden.
-          assert.isFalse(!!allHiddenMessageEls.length);
-        });
+    test('unimportant messages shown after toggle', () => {
+      element._showAllActivity = false;
+      const toggle = dom(element.root).querySelector('.showAllActivityToggle');
+      assert.isOk(toggle);
+      MockInteractions.tap(toggle);
+      flushAsynchronousOperations();
+      const displayedMsgs = dom(element.root).querySelectorAll('gr-message');
+      assert.equal(displayedMsgs.length, 3);
+    });
 
     test('_computeLabelExtremes', () => {
       const computeSpy = sandbox.spy(element, '_computeLabelExtremes');
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
index 8df8566..adf9fd3 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
@@ -14,10 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/paper-toggle-button/paper-toggle-button.js';
-import '../../core/gr-reporting/gr-reporting.js';
 import '../../shared/gr-button/gr-button.js';
 import '../gr-message/gr-message.js';
 import '../../../styles/shared-styles.js';
@@ -28,7 +25,8 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-messages-list_html.js';
 import {KeyboardShortcutBehavior} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
-import {util} from '../../../scripts/util.js';
+import {parseDate} from '../../../utils/date-util.js';
+import {appContext} from '../../../services/app-context.js';
 
 const MAX_INITIAL_SHOWN_MESSAGES = 20;
 const MESSAGES_INCREMENT = 5;
@@ -49,7 +47,7 @@
 };
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrMessagesList extends mixinBehaviors( [
   KeyboardShortcutBehavior,
@@ -122,6 +120,11 @@
     };
   }
 
+  constructor() {
+    super();
+    this.reporting = appContext.reportingService;
+  }
+
   scrollToMessage(messageID) {
     let el = this.shadowRoot
         .querySelector('[data-message-id="' + messageID + '"]');
@@ -188,8 +191,8 @@
         result = result.concat(messages.slice(mi));
         break;
       }
-      mDate = mDate || util.parseDate(messages[mi].date);
-      rDate = rDate || util.parseDate(reviewerUpdates[ri].date);
+      mDate = mDate || parseDate(messages[mi].date);
+      rDate = rDate || parseDate(reviewerUpdates[ri].date);
       if (rDate < mDate) {
         result.push(reviewerUpdates[ri++]);
         rDate = null;
@@ -238,8 +241,8 @@
   _highlightEl(el) {
     const highlightedEls =
         dom(this.root).querySelectorAll('.highlighted');
-    for (const highlighedEl of highlightedEls) {
-      highlighedEl.classList.remove('highlighted');
+    for (const highlightedEl of highlightedEls) {
+      highlightedEl.classList.remove('highlighted');
     }
     function handleAnimationEnd() {
       el.removeEventListener('animationend', handleAnimationEnd);
@@ -300,14 +303,14 @@
     const messages = this.messages || [];
     const index = message._index;
     const authorId = message.author && message.author._account_id;
-    const mDate = util.parseDate(message.date).getTime();
+    const mDate = parseDate(message.date).getTime();
     // NB: Messages array has oldest messages first.
     let nextMDate;
     if (index > 0) {
       for (let i = index - 1; i >= 0; i--) {
         if (messages[i] && messages[i].author &&
             messages[i].author._account_id === authorId) {
-          nextMDate = util.parseDate(messages[i].date).getTime();
+          nextMDate = parseDate(messages[i].date).getTime();
           break;
         }
       }
@@ -321,7 +324,7 @@
             fileComments[i].author._account_id !== authorId) {
           continue;
         }
-        const cDate = util.parseDate(fileComments[i].updated).getTime();
+        const cDate = parseDate(fileComments[i].updated).getTime();
         if (cDate <= mDate) {
           if (nextMDate && cDate <= nextMDate) {
             continue;
@@ -401,7 +404,7 @@
 
   _handleShowAllTap() {
     this._visibleMessages = this._processedMessages;
-    this.$.reporting.reportInteraction(ReportingEvent.SHOW_ALL);
+    this.reporting.reportInteraction(ReportingEvent.SHOW_ALL);
   }
 
   _handleIncrementShownMessages() {
@@ -411,7 +414,7 @@
     const newMessages = this._processedMessages.slice(-(len + delta), -len);
     // Add newMessages to the beginning of _visibleMessages
     this.splice(...['_visibleMessages', 0, 0].concat(newMessages));
-    this.$.reporting.reportInteraction(ReportingEvent.SHOW_MORE);
+    this.reporting.reportInteraction(ReportingEvent.SHOW_MORE);
   }
 
   _processedMessagesChanged(messages) {
@@ -425,7 +428,7 @@
         acc[val] = (acc[val] || 0) + 1;
         return acc;
       }, {all: messages.length});
-      this.$.reporting.reportInteraction('messages-count', tagsCounted);
+      this.reporting.reportInteraction('messages-count', tagsCounted);
     }
   }
 
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_html.js b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_html.js
index 94ae1b0..ddae150 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_html.js
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_html.js
@@ -128,5 +128,4 @@
       data-message-id$="[[message.id]]"
     ></gr-message>
   </template>
-  <gr-reporting id="reporting" category="message-list"></gr-reporting>
 `;
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
index 80896aa..4a0f3f1 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
@@ -46,7 +46,7 @@
 import '../../../test/common-test-setup.js';
 import '../../diff/gr-comment-api/gr-comment-api.js';
 import './gr-messages-list.js';
-import '../../diff/gr-comment-api/gr-comment-api-mock_test.js';
+import '../../../test/mocks/comment-api.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 const randomMessage = function(opt_params) {
   const params = opt_params || {};
@@ -400,9 +400,15 @@
       assert.deepEqual(messageElements[1].message, messages[1]);
       assert.deepEqual(messageElements[2].message, messages[2]);
       assert.deepEqual(messageElements[1].comments.file1,
-          comments.file1.filter(isMarvin));
+          comments.file1.filter(isMarvin).map(c => {
+            return {...c,
+              path: 'file1'};
+          }));
       assert.deepEqual(messageElements[1].comments.file2,
-          comments.file2.filter(isMarvin));
+          comments.file2.filter(isMarvin).map(c => {
+            return {...c,
+              path: 'file2'};
+          }));
       assert.deepEqual(messageElements[2].comments, {});
     });
 
@@ -550,7 +556,7 @@
     });
 
     test('initially show only 20 messages', () => {
-      sandbox.stub(element.$.reporting, 'reportInteraction',
+      sandbox.stub(element.reporting, 'reportInteraction',
           (eventName, details) => {
             assert.equal(typeof(eventName), 'string');
             if (details) {
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js
index 631542a..8d72d21 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
 import '../../../styles/shared-styles.js';
 import {flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
@@ -27,9 +25,10 @@
 import {PatchSetBehavior} from '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
 import {RESTClientBehavior} from '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
+import {ChangeStatus} from '../../../constants/constants.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrRelatedChangesList extends mixinBehaviors( [
   PatchSetBehavior,
@@ -276,7 +275,7 @@
 
   _computeLinkClass(change) {
     const statuses = [];
-    if (change.status == this.ChangeStatus.ABANDONED) {
+    if (change.status == ChangeStatus.ABANDONED) {
       statuses.push('strikethrough');
     }
     if (change.submittable) {
@@ -293,7 +292,7 @@
       classes.push('indirectAncestor');
     } else if (change.submittable) {
       classes.push('submittable');
-    } else if (change.status == this.ChangeStatus.NEW) {
+    } else if (change.status == ChangeStatus.NEW) {
       classes.push('hidden');
     }
     return classes.join(' ');
@@ -301,9 +300,9 @@
 
   _computeChangeStatus(change) {
     switch (change.status) {
-      case this.ChangeStatus.MERGED:
+      case ChangeStatus.MERGED:
         return 'Merged';
-      case this.ChangeStatus.ABANDONED:
+      case ChangeStatus.ABANDONED:
         return 'Abandoned';
     }
     if (change._revision_number != change._current_revision_number) {
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 419e95b..fe3bc67 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
@@ -14,10 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
-import '../../core/gr-reporting/gr-reporting.js';
 import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
 import '../../shared/gr-account-chip/gr-account-chip.js';
 import '../../shared/gr-textarea/gr-textarea.js';
@@ -43,6 +40,9 @@
 import {RESTClientBehavior} from '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
 import {GrReviewerSuggestionsProvider, SUGGESTIONS_PROVIDERS_USERS_TYPES} from '../../../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
+import {appContext} from '../../../services/app-context.js';
+import {SpecialFilePath} from '../../../constants/constants.js';
+import {ExperimentIds} from '../../../services/flags.js';
 
 const STORAGE_DEBOUNCE_INTERVAL_MS = 400;
 
@@ -80,7 +80,7 @@
 const SEND_REPLY_TIMING_LABEL = 'SendReply';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrReplyDialog extends mixinBehaviors( [
   BaseUrlBehavior,
@@ -134,6 +134,8 @@
   constructor() {
     super();
     this.FocusTarget = FocusTarget;
+    this.reporting = appContext.reportingService;
+    this.flagsService = appContext.flagsService;
   }
 
   static get properties() {
@@ -259,6 +261,12 @@
         type: Array,
         observer: '_handleHeightChanged',
       },
+      // Track if the message typed in the reply dialog will be created as a
+      // resolved/unresolved patchset level comment
+      _isResolvedPatchsetLevelComment: {
+        type: Boolean,
+        value: true,
+      },
     };
   }
 
@@ -292,6 +300,8 @@
   /** @override */
   ready() {
     super.ready();
+    this._isPatchsetCommentsExperimentEnabled = this.flagsService
+        .isEnabled(ExperimentIds.PATCHSET_COMMENTS);
     this.$.jsAPI.addElement(this.$.jsAPI.Element.REPLY_DIALOG, this);
   }
 
@@ -477,7 +487,7 @@
   }
 
   send(includeComments, startReview) {
-    this.$.reporting.time(SEND_REPLY_TIMING_LABEL);
+    this.reporting.time(SEND_REPLY_TIMING_LABEL);
     const labels = this.$.labelScores.getLabelValues();
 
     const obj = {
@@ -490,7 +500,17 @@
     }
 
     if (this.draft != null) {
-      obj.message = this.draft;
+      if (this._isPatchsetCommentsExperimentEnabled) {
+        const comment = {
+          message: this.draft,
+          unresolved: !this._isResolvedPatchsetLevelComment,
+        };
+        obj.comments = {
+          [SpecialFilePath.PATCHSET_LEVEL_COMMENTS]: [comment],
+        };
+      } else {
+        obj.message = this.draft;
+      }
     }
 
     const accountAdditions = {};
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.js
index 54fd47a..7286678 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.js
@@ -137,6 +137,9 @@
     #pluginMessage:empty {
       display: none;
     }
+    .preview-formatting {
+      margin-left: var(--spacing-m);
+    }
   </style>
   <div class="container" tabindex="-1">
     <section class="peopleContainer">
@@ -210,7 +213,17 @@
       </gr-endpoint-decorator>
     </section>
     <section class="previewContainer">
-      <label>
+      <template is="dom-if" if="[[_isPatchsetCommentsExperimentEnabled]]">
+        <label>
+          <input
+            id="resolvedPatchsetLevelCommentCheckbox"
+            type="checkbox"
+            checked="{{_isResolvedPatchsetLevelComment::change}}"
+          />
+          Resolved
+        </label>
+      </template>
+      <label class="preview-formatting">
         <input type="checkbox" checked="{{_previewFormatting::change}}" />
         Preview formatting
       </label>
@@ -318,5 +331,4 @@
   <gr-js-api-interface id="jsAPI"></gr-js-api-interface>
   <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   <gr-storage id="storage"></gr-storage>
-  <gr-reporting id="reporting"></gr-reporting>
 `;
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 5a61864..576ee3e 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
@@ -36,6 +36,9 @@
 import '../../../test/common-test-setup.js';
 import './gr-reply-dialog.js';
 import {mockPromise} from '../../../test/test-utils.js';
+import {SpecialFilePath} from '../../../constants/constants.js';
+import {appContext} from '../../../services/app-context.js';
+
 function cloneableResponse(status, text) {
   return {
     ok: false,
@@ -82,6 +85,8 @@
       getChangeSuggestedReviewers() { return Promise.resolve([]); },
     });
 
+    sandbox.stub(appContext.flagsService, 'isEnabled').returns(true);
+
     element = fixture('basic');
     element.change = {
       _number: changeNum,
@@ -172,7 +177,12 @@
               'Code-Review': 0,
               'Verified': 0,
             },
-            message: 'I wholeheartedly disapprove',
+            comments: {
+              [SpecialFilePath.PATCHSET_LEVEL_COMMENTS]: [{
+                message: 'I wholeheartedly disapprove',
+                unresolved: false,
+              }],
+            },
             reviewers: [],
           });
           assert.isFalse(element.$.commentList.hidden);
@@ -189,6 +199,45 @@
     });
   });
 
+  test('toggle resolved checkbox', done => {
+    // Async tick is needed because iron-selector content is distributed and
+    // distributed content requires an observer to be set up.
+    // Note: Double flush seems to be needed in Safari. {@see Issue 4963}.
+    const checkboxEl = element.shadowRoot.querySelector(
+        '#resolvedPatchsetLevelCommentCheckbox');
+    MockInteractions.tap(checkboxEl);
+    flush(() => {
+      flush(() => {
+        element.draft = 'I wholeheartedly disapprove';
+
+        stubSaveReview(review => {
+          assert.deepEqual(review, {
+            drafts: 'PUBLISH_ALL_REVISIONS',
+            labels: {
+              'Code-Review': 0,
+              'Verified': 0,
+            },
+            comments: {
+              [SpecialFilePath.PATCHSET_LEVEL_COMMENTS]: [{
+                message: 'I wholeheartedly disapprove',
+                unresolved: true,
+              }],
+            },
+            reviewers: [],
+          });
+          done();
+        });
+
+        // This is needed on non-Blink engines most likely due to the ways in
+        // which the dom-repeat elements are stamped.
+        flush(() => {
+          MockInteractions.tap(element.shadowRoot
+              .querySelector('.send'));
+        });
+      });
+    });
+  });
+
   test('keep draft comments with reply', done => {
     MockInteractions.tap(element.shadowRoot.querySelector('#includeComments'));
     assert.equal(element._includeComments, false);
@@ -207,7 +256,12 @@
               'Code-Review': 0,
               'Verified': 0,
             },
-            message: 'I wholeheartedly disapprove',
+            comments: {
+              [SpecialFilePath.PATCHSET_LEVEL_COMMENTS]: [{
+                message: 'I wholeheartedly disapprove',
+                unresolved: false,
+              }],
+            },
             reviewers: [],
           });
           assert.isTrue(element.$.commentList.hidden);
@@ -233,7 +287,12 @@
           'Code-Review': -1,
           'Verified': -1,
         },
-        message: 'I wholeheartedly disapprove',
+        comments: {
+          [SpecialFilePath.PATCHSET_LEVEL_COMMENTS]: [{
+            message: 'I wholeheartedly disapprove',
+            unresolved: false,
+          }],
+        },
         reviewers: [],
       });
     });
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
index c933c7c..88e3160 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../shared/gr-account-chip/gr-account-chip.js';
 import '../../shared/gr-button/gr-button.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
@@ -27,7 +25,7 @@
 import {htmlTemplate} from './gr-reviewer-list_html.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrReviewerList extends GestureEventListeners(
     LegacyElementMixin(PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.js b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.js
index b8079eff..f2455a2 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.js
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/paper-toggle-button/paper-toggle-button.js';
 import '../../../styles/shared-styles.js';
 import '../../shared/gr-comment-thread/gr-comment-thread.js';
@@ -24,7 +22,7 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-thread-list_html.js';
-import {util} from '../../../scripts/util.js';
+import {parseDate} from '../../../utils/date-util.js';
 
 import {NO_THREADS_MSG} from '../../../constants/messages.js';
 
@@ -32,7 +30,7 @@
  * Fired when a comment is saved or deleted
  *
  * @event thread-list-modified
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrThreadList extends GestureEventListeners(
     LegacyElementMixin(
@@ -51,12 +49,6 @@
       _sortedThreads: {
         type: Array,
       },
-      _filteredThreads: {
-        type: Array,
-        computed: '_computeFilteredThreads(_sortedThreads, ' +
-          '_unresolvedOnly, _draftsOnly,' +
-          'onlyShowRobotCommentsWithHumanReply)',
-      },
       _unresolvedOnly: {
         type: Boolean,
         value: false,
@@ -98,108 +90,145 @@
    * @param {!Object} changeRecord
    */
   _computeSortedThreads(changeRecord) {
-    const threads = changeRecord.base;
-    if (!threads) { return []; }
-    this._updateSortedThreads(threads);
+    const baseThreads = changeRecord.base;
+    const threads = changeRecord.value;
+    if (!baseThreads) { return []; }
+    // TODO: should change how data flows to solve the root cause
+    // We only want to sort on thread additions / removals to avoid
+    // re-rendering on modifications (add new reply / edit draft etc)
+    //  https://polymer-library.polymer-project.org/3.0/docs/devguide/observers#array-observation
+    let shouldSort = true;
+    if (threads.indexSplices) {
+      // Array splice mutations
+      shouldSort = threads.indexSplices.addedCount !== 0
+        || threads.indexSplices.removed.length;
+    } else {
+      // A replace mutation
+      shouldSort = threads.length !== baseThreads.length;
+    }
+    this._updateSortedThreads(baseThreads, shouldSort);
   }
 
-  // TODO(taoalpha): should allow only sort once during initialization
-  // to avoid flickering
-  _updateSortedThreads(threads) {
-    this._sortedThreads =
-        threads.map(this._getThreadWithSortInfo).sort((c1, c2) => {
-          // threads will be sorted by:
-          // - unresolved first
-          // - with drafts
-          // - file path
-          // - line
-          // - updated time
-          if (c2.unresolved || c1.unresolved) {
-            if (!c1.unresolved) { return 1; }
-            if (!c2.unresolved) { return -1; }
-          }
-
-          if (c2.hasDraft || c1.hasDraft) {
-            if (!c1.hasDraft) { return 1; }
-            if (!c2.hasDraft) { return -1; }
-          }
-
-          // TODO: Update here once we introduce patchset level comments
-          // they may not have or have a special line or path attribute
-
-          if (c1.thread.path !== c2.thread.path) {
-            return c1.thread.path.localeCompare(c2.thread.path);
-          }
-
-          // File level comments (no `line` property)
-          // should always show before any lines
-          if ([c1, c2].some(c => c.thread.line === undefined)) {
-            if (!c1.thread.line) { return -1; }
-            if (!c2.thread.line) { return 1; }
-          } else if (c1.thread.line !== c2.thread.line) {
-            return c1.thread.line - c2.thread.line;
-          }
-
-          const c1Date = c1.__date || util.parseDate(c1.updated);
-          const c2Date = c2.__date || util.parseDate(c2.updated);
-          const dateCompare = c2Date - c1Date;
-          if (dateCompare === 0 && (!c1.id || !c1.id.localeCompare)) {
-            return 0;
-          }
-          return dateCompare ? dateCompare : c1.id.localeCompare(c2.id);
-        });
-  }
-
-  _computeFilteredThreads(sortedThreads, unresolvedOnly, draftsOnly,
-      onlyShowRobotCommentsWithHumanReply) {
-    // Polymer 2: check for undefined
-    if ([
-      sortedThreads,
-      unresolvedOnly,
-      draftsOnly,
-      onlyShowRobotCommentsWithHumanReply,
-    ].some(arg => arg === undefined)) {
-      return undefined;
+  _updateSortedThreads(threads, shouldSort) {
+    if (this._sortedThreads
+        && this._sortedThreads.length === threads.length
+        && !shouldSort) {
+      // Instead of replacing the _sortedThreads which will trigger a re-render,
+      // we override all threads inside of it
+      for (const thread of threads) {
+        const idxInSortedThreads = this._sortedThreads
+            .findIndex(t => t.rootId === thread.rootId);
+        this.set(`_sortedThreads.${idxInSortedThreads}`, {...thread});
+      }
+      return;
     }
 
-    return sortedThreads.filter(c => {
-      if (draftsOnly) {
-        return c.hasDraft;
-      } else if (unresolvedOnly) {
-        return c.unresolved;
-      } else {
-        const comments = c && c.thread && c.thread.comments;
-        let robotComment = false;
-        let humanReplyToRobotComment = false;
-        comments.forEach(comment => {
-          if (comment.robot_id) {
-            robotComment = true;
-          } else if (robotComment) {
-            // Robot comment exists and human comment exists after it
-            humanReplyToRobotComment = true;
-          }
-        });
-        if (robotComment && onlyShowRobotCommentsWithHumanReply) {
-          return humanReplyToRobotComment;
-        }
-        return c;
+    const threadsWithInfo = threads
+        .map(thread => this._getThreadWithStatusInfo(thread));
+    this._sortedThreads = threadsWithInfo.sort((c1, c2) => {
+      // threads will be sorted by:
+      // - unresolved first
+      // - with drafts
+      // - file path
+      // - line
+      // - updated time
+      if (c2.unresolved || c1.unresolved) {
+        if (!c1.unresolved) { return 1; }
+        if (!c2.unresolved) { return -1; }
       }
+
+      if (c2.hasDraft || c1.hasDraft) {
+        if (!c1.hasDraft) { return 1; }
+        if (!c2.hasDraft) { return -1; }
+      }
+
+      // TODO: Update here once we introduce patchset level comments
+      // they may not have or have a special line or path attribute
+
+      if (c1.thread.path !== c2.thread.path) {
+        return c1.thread.path.localeCompare(c2.thread.path);
+      }
+
+      // File level comments (no `line` property)
+      // should always show before any lines
+      if ([c1, c2].some(c => c.thread.line === undefined)) {
+        if (!c1.thread.line) { return -1; }
+        if (!c2.thread.line) { return 1; }
+      } else if (c1.thread.line !== c2.thread.line) {
+        return c1.thread.line - c2.thread.line;
+      }
+
+      const c1Date = c1.__date || parseDate(c1.updated);
+      const c2Date = c2.__date || parseDate(c2.updated);
+      const dateCompare = c2Date - c1Date;
+      if (dateCompare === 0 && (!c1.id || !c1.id.localeCompare)) {
+        return 0;
+      }
+      return dateCompare ? dateCompare : c1.id.localeCompare(c2.id);
     }).map(threadInfo => threadInfo.thread);
   }
 
-  _getThreadWithSortInfo(thread) {
-    const lastComment = thread.comments[thread.comments.length - 1] || {};
+  _shouldShowThread(
+      thread, unresolvedOnly, draftsOnly, onlyShowRobotCommentsWithHumanReply
+  ) {
+    if ([
+      thread,
+      unresolvedOnly,
+      draftsOnly,
+      onlyShowRobotCommentsWithHumanReply,
+    ].includes(undefined)) {
+      return false;
+    }
 
-    const lastNonDraftComment =
-        (lastComment.__draft && thread.comments.length > 1) ?
-          thread.comments[thread.comments.length - 2] :
-          lastComment;
+    if (!draftsOnly
+        && !unresolvedOnly
+        && !onlyShowRobotCommentsWithHumanReply) {
+      return true;
+    }
+
+    const threadInfo = this._getThreadWithStatusInfo(thread);
+
+    if (threadInfo.isEditing) {
+      return true;
+    }
+
+    if (threadInfo.hasRobotComment
+       && onlyShowRobotCommentsWithHumanReply
+       && !threadInfo.hasHumanReplyToRobotComment) {
+      return false;
+    }
+
+    let filtersCheck = true;
+    if (draftsOnly && unresolvedOnly) {
+      filtersCheck = threadInfo.hasDraft && threadInfo.unresolved;
+    } else if (draftsOnly) {
+      filtersCheck = threadInfo.hasDraft;
+    } else if (unresolvedOnly) {
+      filtersCheck = threadInfo.unresolved;
+    }
+
+    return filtersCheck;
+  }
+
+  _getThreadWithStatusInfo(thread) {
+    const comments = thread.comments;
+    const lastComment = comments[comments.length - 1] || {};
+    let hasRobotComment = false;
+    let hasHumanReplyToRobotComment = false;
+    comments.forEach(comment => {
+      if (comment.robot_id) {
+        hasRobotComment = true;
+      } else if (hasRobotComment) {
+        hasHumanReplyToRobotComment = true;
+      }
+    });
 
     return {
       thread,
-      // Use the unresolved bit for the last non draft comment. This is what
-      // anybody other than the current user would see.
-      unresolved: !!lastNonDraftComment.unresolved,
+      hasRobotComment,
+      hasHumanReplyToRobotComment,
+      unresolved: !!lastComment.unresolved,
+      isEditing: !!lastComment.__editing,
       hasDraft: !!lastComment.__draft,
       updated: lastComment.updated || lastComment.__date,
     };
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.js b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.js
index fd34b2d..721e7510 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.js
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.js
@@ -80,25 +80,30 @@
     </template>
     <template
       is="dom-repeat"
-      items="[[_filteredThreads]]"
+      items="[[_sortedThreads]]"
       as="thread"
-      initial-count="5"
+      initial-count="10"
       target-framerate="60"
     >
-      <gr-comment-thread
-        show-file-path=""
-        change-num="[[changeNum]]"
-        comments="[[thread.comments]]"
-        comment-side="[[thread.commentSide]]"
-        project-name="[[change.project]]"
-        is-on-parent="[[_isOnParent(thread.commentSide)]]"
-        line-num="[[thread.line]]"
-        patch-num="[[thread.patchNum]]"
-        path="[[thread.path]]"
-        root-id="{{thread.rootId}}"
-        on-thread-changed="_handleCommentsChanged"
-        on-thread-discard="_handleThreadDiscard"
-      ></gr-comment-thread>
+      <template
+        is="dom-if"
+        if="[[_shouldShowThread(thread, _unresolvedOnly, _draftsOnly, onlyShowRobotCommentsWithHumanReply)]]"
+      >
+        <gr-comment-thread
+          show-file-path=""
+          change-num="[[changeNum]]"
+          comments="[[thread.comments]]"
+          comment-side="[[thread.commentSide]]"
+          project-name="[[change.project]]"
+          is-on-parent="[[_isOnParent(thread.commentSide)]]"
+          line-num="[[thread.line]]"
+          patch-num="[[thread.patchNum]]"
+          path="[[thread.path]]"
+          root-id="{{thread.rootId}}"
+          on-thread-changed="_handleCommentsChanged"
+          on-thread-discard="_handleThreadDiscard"
+        ></gr-comment-thread>
+      </template>
     </template>
   </div>
 `;
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.html b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.html
index 4b00d5a..2195581 100644
--- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.html
@@ -41,10 +41,15 @@
   let sandbox;
   let threadElements;
 
-  setup(() => {
+  function getVisibleThreads() {
+    return [...dom(element.root)
+        .querySelectorAll('gr-comment-thread')]
+        .filter(e => e.style.display !== 'none');
+  }
+
+  setup(done => {
     sandbox = sinon.sandbox.create();
     element = fixture('basic');
-    element.onlyShowRobotCommentsWithHumanReply = true;
     element.threads = [
       {
         comments: [
@@ -69,7 +74,7 @@
             in_reply_to: 'ecf0b9fa_fe1a5f62',
             updated: '2018-02-13 22:48:48.018000000',
             message: 'draft',
-            unresolved: false,
+            unresolved: true,
             __draft: true,
             __draftID: '0.m683trwff68',
             __editing: false,
@@ -233,9 +238,13 @@
         start_datetime: '2019-03-08 18:49:18.000000000',
       },
     ];
-    flushAsynchronousOperations();
-    threadElements = dom(element.root)
-        .querySelectorAll('gr-comment-thread');
+
+    // use flush to render all (bypass initial-count set on dom-repeat)
+    flush(() => {
+      threadElements = dom(element.root)
+          .querySelectorAll('gr-comment-thread');
+      done();
+    });
   });
 
   teardown(() => {
@@ -252,48 +261,47 @@
     'none');
   });
 
-  test('there are five threads by default', () => {
+  test('show all threads by default', () => {
     assert.equal(dom(element.root)
-        .querySelectorAll('gr-comment-thread').length, 5);
+        .querySelectorAll('gr-comment-thread').length, element.threads.length);
+    assert.equal(getVisibleThreads().length, element.threads.length);
+  });
+
+  test('onlyShowRobotCommentsWithHumanReply ', () => {
+    element.onlyShowRobotCommentsWithHumanReply = true;
+    flushAsynchronousOperations();
+    assert.equal(
+        getVisibleThreads().length,
+        element.threads.length - 1);
+    assert.isNotOk(getVisibleThreads().find(th => th.rootId === 'rc1'));
   });
 
   test('_computeSortedThreads', () => {
     assert.equal(element._sortedThreads.length, 7);
     // Draft and unresolved for commit-msg at line 5
-    assert.equal(element._sortedThreads[0].thread.rootId,
+    assert.equal(element._sortedThreads[0].rootId,
         'ecf0b9fa_fe1a5f62');
-    // /COMMIT_MSG
     // unresolved no draft and file level
-    assert.equal(element._sortedThreads[1].thread.rootId,
+    assert.equal(element._sortedThreads[1].rootId,
         '8caddf38_44770ec1');
     // unresolved no draft at line 4
-    assert.equal(element._sortedThreads[2].thread.rootId,
+    assert.equal(element._sortedThreads[2].rootId,
         'scaddf38_44770ec1');
     // unresolved no draft at line 5
-    assert.equal(element._sortedThreads[3].thread.rootId,
+    assert.equal(element._sortedThreads[3].rootId,
         'rc1');
     // Unresolved no draft at line 7
-    assert.equal(element._sortedThreads[4].thread.rootId,
+    assert.equal(element._sortedThreads[4].rootId,
         'rc2');
     // resolved and draft on COMMIT_MSG
-    assert.equal(element._sortedThreads[5].thread.rootId,
+    assert.equal(element._sortedThreads[5].rootId,
         'zcf0b9fa_fe1a5f62');
     // resolved and on file test.txt
-    assert.equal(element._sortedThreads[6].thread.rootId,
+    assert.equal(element._sortedThreads[6].rootId,
         '09a9fb0a_1484e6cf');
   });
 
-  test('filtered threads do not contain robot comments without reply', () => {
-    const thread = element.threads.find(thread => thread.rootId === 'rc1');
-    assert.equal(element._filteredThreads.includes(thread), false);
-  });
-
-  test('filtered threads contains robot comments with reply', () => {
-    const thread = element.threads.find(thread => thread.rootId === 'rc2');
-    assert.equal(element._filteredThreads.includes(thread), true);
-  });
-
-  test('thread removal', () => {
+  test('thread removal and sort again', () => {
     threadElements[1].dispatchEvent(
         new CustomEvent('thread-discard', {
           detail: {rootId: 'rc2'},
@@ -301,23 +309,95 @@
         }));
     flushAsynchronousOperations();
     assert.equal(element._sortedThreads.length, 6);
-    assert.equal(element._sortedThreads[0].thread.rootId,
+    assert.equal(element._sortedThreads[0].rootId,
+        'ecf0b9fa_fe1a5f62');
+    // unresolved no draft and file level
+    assert.equal(element._sortedThreads[1].rootId,
+        '8caddf38_44770ec1');
+    // unresolved no draft at line 4
+    assert.equal(element._sortedThreads[2].rootId,
+        'scaddf38_44770ec1');
+    // unresolved no draft at line 5
+    assert.equal(element._sortedThreads[3].rootId,
+        'rc1');
+    // resolved and draft
+    assert.equal(element._sortedThreads[4].rootId,
+        'zcf0b9fa_fe1a5f62');
+    // resolved and on file test.txt
+    assert.equal(element._sortedThreads[5].rootId,
+        '09a9fb0a_1484e6cf');
+  });
+
+  test('modification on thread shold not trigger sort again', () => {
+    const currentSortedThreads = [...element._sortedThreads];
+    for (const thread of currentSortedThreads) {
+      thread.comments = [...thread.comments];
+    }
+    const modifiedThreads = [...element.threads];
+    modifiedThreads[5] = {...modifiedThreads[5]};
+    modifiedThreads[5].comments = [...modifiedThreads[5].comments, {
+      ...modifiedThreads[5].comments[0],
+      unresolved: false,
+    }];
+    element.threads = modifiedThreads;
+    assert.notDeepEqual(currentSortedThreads, element._sortedThreads);
+
+    // exact same order as in _computeSortedThreads
+    assert.equal(element._sortedThreads.length, 7);
+    // Draft and unresolved for commit-msg at line 5
+    assert.equal(element._sortedThreads[0].rootId,
+        'ecf0b9fa_fe1a5f62');
+    // unresolved no draft and file level
+    assert.equal(element._sortedThreads[1].rootId,
+        '8caddf38_44770ec1');
+    // unresolved no draft at line 4
+    assert.equal(element._sortedThreads[2].rootId,
+        'scaddf38_44770ec1');
+    // unresolved no draft at line 5
+    assert.equal(element._sortedThreads[3].rootId,
+        'rc1');
+    // Unresolved no draft at line 7
+    assert.equal(element._sortedThreads[4].rootId,
+        'rc2');
+    // resolved and draft on COMMIT_MSG
+    assert.equal(element._sortedThreads[5].rootId,
+        'zcf0b9fa_fe1a5f62');
+    // resolved and on file test.txt
+    assert.equal(element._sortedThreads[6].rootId,
+        '09a9fb0a_1484e6cf');
+  });
+
+  test('non-equal length of sortThreads and threads' +
+    ' shold trigger sort again', () => {
+    const modifiedThreads = [...element.threads];
+    const currentSortedThreads = [...element._sortedThreads];
+    element._sortedThreads = [];
+    element.threads = modifiedThreads;
+    assert.deepEqual(currentSortedThreads, element._sortedThreads);
+
+    // exact same order as in _computeSortedThreads
+    assert.equal(element._sortedThreads.length, 7);
+    // Draft and unresolved for commit-msg at line 5
+    assert.equal(element._sortedThreads[0].rootId,
         'ecf0b9fa_fe1a5f62');
     // /COMMIT_MSG
     // unresolved no draft and file level
-    assert.equal(element._sortedThreads[1].thread.rootId,
+    assert.equal(element._sortedThreads[1].rootId,
         '8caddf38_44770ec1');
     // unresolved no draft at line 4
-    assert.equal(element._sortedThreads[2].thread.rootId,
+    assert.equal(element._sortedThreads[2].rootId,
         'scaddf38_44770ec1');
     // unresolved no draft at line 5
-    assert.equal(element._sortedThreads[3].thread.rootId,
+    assert.equal(element._sortedThreads[3].rootId,
         'rc1');
-    // resolved and draft
-    assert.equal(element._sortedThreads[4].thread.rootId,
+    // Unresolved no draft at line 7
+    assert.equal(element._sortedThreads[4].rootId,
+        'rc2');
+    // resolved and draft on COMMIT_MSG
+    assert.equal(element._sortedThreads[5].rootId,
         'zcf0b9fa_fe1a5f62');
     // resolved and on file test.txt
-    assert.equal(element._sortedThreads[5].thread.rootId,
+    assert.equal(element._sortedThreads[6].rootId,
         '09a9fb0a_1484e6cf');
   });
 
@@ -325,15 +405,29 @@
     MockInteractions.tap(element.shadowRoot.querySelector(
         '#unresolvedToggle'));
     flushAsynchronousOperations();
-    assert.equal(dom(element.root)
-        .querySelectorAll('gr-comment-thread').length, 5);
+    assert.equal(getVisibleThreads().length, 5);
   });
 
   test('toggle drafts only shows threads with draft comments', () => {
     MockInteractions.tap(element.shadowRoot.querySelector('#draftToggle'));
     flushAsynchronousOperations();
-    assert.equal(dom(element.root)
-        .querySelectorAll('gr-comment-thread').length, 2);
+    assert.equal(getVisibleThreads().length, 2);
+  });
+
+  test('toggle drafts and unresolved should ignore comments in editing', () => {
+    const modifiedThreads = [...element.threads];
+    modifiedThreads[5] = {...modifiedThreads[5]};
+    modifiedThreads[5].comments = [...modifiedThreads[5].comments];
+    modifiedThreads[5].comments.push({
+      ...modifiedThreads[5].comments[0],
+      __editing: true,
+    });
+    element.threads = modifiedThreads;
+    MockInteractions.tap(element.shadowRoot.querySelector('#draftToggle'));
+    MockInteractions.tap(element.shadowRoot.querySelector(
+        '#unresolvedToggle'));
+    flushAsynchronousOperations();
+    assert.equal(getVisibleThreads().length, 2);
   });
 
   test('toggle drafts and unresolved only shows threads with drafts and ' +
@@ -342,8 +436,7 @@
     MockInteractions.tap(element.shadowRoot.querySelector(
         '#unresolvedToggle'));
     flushAsynchronousOperations();
-    assert.equal(dom(element.root)
-        .querySelectorAll('gr-comment-thread').length, 2);
+    assert.equal(getVisibleThreads().length, 1);
   });
 
   test('modification events are consumed and displatched', () => {
diff --git a/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.js b/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.js
index 9171908..0658996 100644
--- a/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../shared/gr-dialog/gr-dialog.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
 import '../../shared/gr-shell-command/gr-shell-command.js';
@@ -36,7 +34,7 @@
 ];
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrUploadHelpDialog extends GestureEventListeners(
     LegacyElementMixin(PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
index 2645c63..903552a 100644
--- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
@@ -14,7 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
 import '../../shared/gr-button/gr-button.js';
 import '../../shared/gr-dropdown/gr-dropdown.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
@@ -27,10 +26,10 @@
 import {htmlTemplate} from './gr-account-dropdown_html.js';
 import {DisplayNameBehavior} from '../../../behaviors/gr-display-name-behavior/gr-display-name-behavior.js';
 
-const INTERPOLATE_URL_PATTERN = /\$\{([\w]+)\}/g;
+const INTERPOLATE_URL_PATTERN = /\${([\w]+)}/g;
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrAccountDropdown extends mixinBehaviors( [
   DisplayNameBehavior,
diff --git a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.js b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.js
index 6814d89..99c4cb3 100644
--- a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.js
+++ b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../shared/gr-dialog/gr-dialog.js';
 import '../../../styles/shared-styles.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
@@ -23,7 +21,7 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-error-dialog_html.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrErrorDialog extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
index 4b5969a..9f6d050 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
@@ -16,15 +16,7 @@
  */
 /* Import to get Gerrit interface */
 /* TODO(taoalpha): decouple gr-gerrit from gr-js-api-interface */
-/*
-  FIXME(polymer-modulizer): the above comments were extracted
-  from HTML and may be out of place here. Review them and
-  then delete this comment!
-*/
-
-import '../../../scripts/bundled-polymer.js';
 import '../gr-error-dialog/gr-error-dialog.js';
-import '../gr-reporting/gr-reporting.js';
 import '../../shared/gr-alert/gr-alert.js';
 import '../../shared/gr-overlay/gr-overlay.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
@@ -37,6 +29,7 @@
 import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
 import {authService} from '../../shared/gr-rest-api-interface/gr-auth.js';
 import {gerritEventEmitter} from '../../shared/gr-event-emitter/gr-event-emitter.js';
+import {appContext} from '../../../services/app-context.js';
 
 const HIDE_ALERT_TIMEOUT_MS = 5000;
 const CHECK_SIGN_IN_INTERVAL_MS = 60 * 1000;
@@ -47,7 +40,7 @@
 const AUTHENTICATION_REQUIRED = 'Authentication required\n';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrErrorManager extends mixinBehaviors( [
   BaseUrlBehavior,
@@ -98,6 +91,8 @@
 
     /** @type {?Function} */
     this._authErrorHandlerDeregistrationHook;
+
+    this.reporting = appContext.reportingService;
   }
 
   /** @override */
@@ -205,7 +200,6 @@
         showSignInButton: !isLoggedIn,
       });
     });
-    return;
   }
 
   _constructServerErrorMsg({errorText, status, statusText, url, trace, tip}) {
@@ -406,7 +400,7 @@
   }
 
   _showErrorDialog(message, opt_options) {
-    this.$.reporting.reportErrorDialog(message);
+    this.reporting.reportErrorDialog(message);
     this.$.errorDialog.text = message;
     this.$.errorDialog.showSignInButton =
         opt_options && opt_options.showSignInButton;
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_html.js b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_html.js
index 4d32f24..2fa9b95 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_html.js
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_html.js
@@ -35,5 +35,4 @@
   >
   </gr-overlay>
   <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
-  <gr-reporting id="reporting"></gr-reporting>
 `;
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
index 8272c6e..b52bb7d 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
@@ -24,11 +24,6 @@
 
 <script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
 <script src="/components/wct-browser-legacy/browser.js"></script>
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-error-manager.js';
-void (0);
-</script>
 
 <test-fixture id="basic">
   <template>
@@ -338,7 +333,7 @@
           }));
       assert.equal(window.fetch.callCount, 1);
       flush(() => {
-        // here needs two flush as there are two chanined
+        // here needs two flush as there are two chained
         // promises on server-error handler and flush only flushes one
         assert.equal(window.fetch.callCount, 2);
         flush(() => {
@@ -396,7 +391,7 @@
           }));
       assert.equal(window.fetch.callCount, 1);
       flush(() => {
-        // here needs two flush as there are two chanined
+        // here needs two flush as there are two chained
         // promises on server-error handler and flush only flushes one
         assert.equal(window.fetch.callCount, 2);
         flush(() => {
@@ -490,7 +485,7 @@
       const openStub = sandbox.stub(element.$.errorOverlay, 'open');
       const closeStub = sandbox.stub(element.$.errorOverlay, 'close');
       const reportStub = sandbox.stub(
-          element.$.reporting,
+          element.reporting,
           'reportErrorDialog'
       );
 
diff --git a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.js b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.js
index 5d7ec27..5c54011 100644
--- a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.js
+++ b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.js
@@ -14,15 +14,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/shared-styles.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-key-binding-display_html.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrKeyBindingDisplay extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js
index beb0f7e..0c0daea 100644
--- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js
+++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../shared/gr-button/gr-button.js';
 import '../gr-key-binding-display/gr-key-binding-display.js';
 import '../../../styles/shared-styles.js';
@@ -29,7 +27,7 @@
 const {ShortcutSection} = KeyboardShortcutBinder;
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrKeyboardShortcutsDialog extends mixinBehaviors( [
   KeyboardShortcutBehavior,
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
index 3294f5f..684b472 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
 import '../../shared/gr-dropdown/gr-dropdown.js';
 import '../../shared/gr-icons/gr-icons.js';
@@ -86,7 +84,7 @@
 ]);
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrMainHeader extends mixinBehaviors( [
   AdminNavBehavior,
diff --git a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.js b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.js
index 2b87548..69e2989 100644
--- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.js
+++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.js
@@ -86,7 +86,7 @@
 const EDIT_PATCHNUM = 'edit';
 const PARENT_PATCHNUM = 'PARENT';
 
-const USER_PLACEHOLDER_PATTERN = /\$\{user\}/g;
+const USER_PLACEHOLDER_PATTERN = /\${user}/g;
 
 // NOTE: These queries are tested in Java. Any changes made to definitions
 // here require corresponding changes to:
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
deleted file mode 100644
index e140d6c..0000000
--- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
+++ /dev/null
@@ -1,437 +0,0 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-reporting</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-reporting></gr-reporting>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-reporting.js';
-suite('gr-reporting tests', () => {
-  let element;
-  let sandbox;
-  let clock;
-  let fakePerformance;
-
-  const NOW_TIME = 100;
-
-  setup(() => {
-    sandbox = sinon.sandbox.create();
-    clock = sinon.useFakeTimers(NOW_TIME);
-    element = fixture('basic');
-    element._baselines = Object.assign({}, GrReporting.STARTUP_TIMERS);
-    fakePerformance = {
-      navigationStart: 1,
-      loadEventEnd: 2,
-    };
-    fakePerformance.toJSON = () => fakePerformance;
-    sinon.stub(element, 'performanceTiming',
-        {get() { return fakePerformance; }});
-    sandbox.stub(element, 'reporter');
-  });
-
-  teardown(() => {
-    sandbox.restore();
-    clock.restore();
-  });
-
-  test('appStarted', () => {
-    sandbox.stub(element, 'now').returns(42);
-    element.appStarted();
-    assert.isTrue(
-        element.reporter.calledWithMatch(
-            'timing-report', 'UI Latency', 'App Started', 42
-        ));
-    assert.isTrue(
-        element.reporter.calledWithExactly(
-            'timing-report', 'UI Latency', 'NavResTime - loadEventEnd',
-            fakePerformance.loadEventEnd - fakePerformance.navigationStart,
-            undefined, true)
-    );
-  });
-
-  test('WebComponentsReady', () => {
-    sandbox.stub(element, 'now').returns(42);
-    element.timeEnd('WebComponentsReady');
-    assert.isTrue(element.reporter.calledWithMatch(
-        'timing-report', 'UI Latency', 'WebComponentsReady', 42
-    ));
-  });
-
-  test('beforeLocationChanged', () => {
-    element._baselines['garbage'] = 'monster';
-    sandbox.stub(element, 'time');
-    element.beforeLocationChanged();
-    assert.isTrue(element.time.calledWithExactly('DashboardDisplayed'));
-    assert.isTrue(element.time.calledWithExactly('ChangeDisplayed'));
-    assert.isTrue(element.time.calledWithExactly('ChangeFullyLoaded'));
-    assert.isTrue(element.time.calledWithExactly('DiffViewDisplayed'));
-    assert.isTrue(element.time.calledWithExactly('FileListDisplayed'));
-    assert.isFalse(element._baselines.hasOwnProperty('garbage'));
-  });
-
-  test('changeDisplayed', () => {
-    sandbox.spy(element, 'timeEnd');
-    element.changeDisplayed();
-    assert.isFalse(element.timeEnd.calledWith('ChangeDisplayed'));
-    assert.isTrue(element.timeEnd.calledWith('StartupChangeDisplayed'));
-    element.changeDisplayed();
-    assert.isTrue(element.timeEnd.calledWith('ChangeDisplayed'));
-  });
-
-  test('changeFullyLoaded', () => {
-    sandbox.spy(element, 'timeEnd');
-    element.changeFullyLoaded();
-    assert.isFalse(
-        element.timeEnd.calledWithExactly('ChangeFullyLoaded'));
-    assert.isTrue(
-        element.timeEnd.calledWithExactly('StartupChangeFullyLoaded'));
-    element.changeFullyLoaded();
-    assert.isTrue(element.timeEnd.calledWithExactly('ChangeFullyLoaded'));
-  });
-
-  test('diffViewDisplayed', () => {
-    sandbox.spy(element, 'timeEnd');
-    element.diffViewDisplayed();
-    assert.isFalse(element.timeEnd.calledWith('DiffViewDisplayed'));
-    assert.isTrue(element.timeEnd.calledWith('StartupDiffViewDisplayed'));
-    element.diffViewDisplayed();
-    assert.isTrue(element.timeEnd.calledWith('DiffViewDisplayed'));
-  });
-
-  test('fileListDisplayed', () => {
-    sandbox.spy(element, 'timeEnd');
-    element.fileListDisplayed();
-    assert.isFalse(
-        element.timeEnd.calledWithExactly('FileListDisplayed'));
-    assert.isTrue(
-        element.timeEnd.calledWithExactly('StartupFileListDisplayed'));
-    element.fileListDisplayed();
-    assert.isTrue(element.timeEnd.calledWithExactly('FileListDisplayed'));
-  });
-
-  test('dashboardDisplayed', () => {
-    sandbox.spy(element, 'timeEnd');
-    element.dashboardDisplayed();
-    assert.isFalse(element.timeEnd.calledWith('DashboardDisplayed'));
-    assert.isTrue(element.timeEnd.calledWith('StartupDashboardDisplayed'));
-    element.dashboardDisplayed();
-    assert.isTrue(element.timeEnd.calledWith('DashboardDisplayed'));
-  });
-
-  test('dashboardDisplayed details', () => {
-    sandbox.spy(element, 'timeEnd');
-    sandbox.stub(window, 'performance', {
-      memory: {
-        usedJSHeapSize: 1024 * 1024,
-      },
-      measure: () => {},
-    });
-    sandbox.stub(element, 'now').returns(42);
-    element.reportRpcTiming('/changes/*~*/comments', 500);
-    element.dashboardDisplayed();
-    assert.isTrue(
-        element.timeEnd.calledWithExactly('StartupDashboardDisplayed',
-            {rpcList: [
-              {
-                anonymizedUrl: '/changes/*~*/comments',
-                elapsed: 500,
-              },
-            ],
-            screenSize: {
-              width: window.screen.width,
-              height: window.screen.height,
-            },
-            viewport: {
-              width: document.documentElement.clientWidth,
-              height: document.documentElement.clientHeight,
-            },
-            usedJSHeapSizeMb: 1,
-            }
-        ));
-  });
-
-  test('time and timeEnd', () => {
-    const nowStub = sandbox.stub(element, 'now').returns(0);
-    element.time('foo');
-    nowStub.returns(1);
-    element.time('bar');
-    nowStub.returns(2);
-    element.timeEnd('bar');
-    nowStub.returns(3);
-    element.timeEnd('foo');
-    assert.isTrue(element.reporter.calledWithMatch(
-        'timing-report', 'UI Latency', 'foo', 3
-    ));
-    assert.isTrue(element.reporter.calledWithMatch(
-        'timing-report', 'UI Latency', 'bar', 1
-    ));
-  });
-
-  test('timer object', () => {
-    const nowStub = sandbox.stub(element, 'now').returns(100);
-    const timer = element.getTimer('foo-bar');
-    nowStub.returns(150);
-    timer.end();
-    assert.isTrue(element.reporter.calledWithMatch(
-        'timing-report', 'UI Latency', 'foo-bar', 50));
-  });
-
-  test('timer object double call', () => {
-    const timer = element.getTimer('foo-bar');
-    timer.end();
-    assert.isTrue(element.reporter.calledOnce);
-    assert.throws(() => {
-      timer.end();
-    }, 'Timer for "foo-bar" already ended.');
-  });
-
-  test('timer object maximum', () => {
-    const nowStub = sandbox.stub(element, 'now').returns(100);
-    const timer = element.getTimer('foo-bar').withMaximum(100);
-    nowStub.returns(150);
-    timer.end();
-    assert.isTrue(element.reporter.calledOnce);
-
-    timer.reset();
-    nowStub.returns(260);
-    timer.end();
-    assert.isTrue(element.reporter.calledOnce);
-  });
-
-  test('recordDraftInteraction', () => {
-    const key = 'TimeBetweenDraftActions';
-    const nowStub = sandbox.stub(element, 'now').returns(100);
-    const timingStub = sandbox.stub(element, '_reportTiming');
-    element.recordDraftInteraction();
-    assert.isFalse(timingStub.called);
-
-    nowStub.returns(200);
-    element.recordDraftInteraction();
-    assert.isTrue(timingStub.calledOnce);
-    assert.equal(timingStub.lastCall.args[0], key);
-    assert.equal(timingStub.lastCall.args[1], 100);
-
-    nowStub.returns(350);
-    element.recordDraftInteraction();
-    assert.isTrue(timingStub.calledTwice);
-    assert.equal(timingStub.lastCall.args[0], key);
-    assert.equal(timingStub.lastCall.args[1], 150);
-
-    nowStub.returns(370 + 2 * 60 * 1000);
-    element.recordDraftInteraction();
-    assert.isFalse(timingStub.calledThrice);
-  });
-
-  test('timeEndWithAverage', () => {
-    const nowStub = sandbox.stub(element, 'now').returns(0);
-    nowStub.returns(1000);
-    element.time('foo');
-    nowStub.returns(1100);
-    element.timeEndWithAverage('foo', 'bar', 10);
-    assert.isTrue(element.reporter.calledTwice);
-    assert.isTrue(element.reporter.calledWithMatch(
-        'timing-report', 'UI Latency', 'foo', 100));
-    assert.isTrue(element.reporter.calledWithMatch(
-        'timing-report', 'UI Latency', 'bar', 10));
-  });
-
-  test('reportExtension', () => {
-    element.reportExtension('foo');
-    assert.isTrue(element.reporter.calledWithExactly(
-        'lifecycle', 'Extension detected', 'foo'
-    ));
-  });
-
-  test('reportInteraction', () => {
-    element.reporter.restore();
-    sandbox.spy(element, '_reportEvent');
-    element.pluginsLoaded(); // so we don't cache
-    element.reportInteraction('button-click', {name: 'sendReply'});
-    assert.isTrue(element._reportEvent.getCall(2).calledWithMatch(
-        {
-          type: 'interaction',
-          name: 'button-click',
-          eventDetails: JSON.stringify({name: 'sendReply'}),
-        }
-    ));
-  });
-
-  test('report start time', () => {
-    element.reporter.restore();
-    sandbox.stub(element, 'now').returns(42);
-    sandbox.spy(element, '_reportEvent');
-    const dispatchStub = sandbox.spy(document, 'dispatchEvent');
-    element.pluginsLoaded();
-    element.time('timeAction');
-    element.timeEnd('timeAction');
-    assert.isTrue(element._reportEvent.getCall(2).calledWithMatch(
-        {
-          type: 'timing-report',
-          category: 'UI Latency',
-          name: 'timeAction',
-          value: 0,
-          eventStart: 42,
-        }
-    ));
-    assert.equal(dispatchStub.getCall(2).args[0].detail.eventStart, 42);
-  });
-
-  suite('plugins', () => {
-    setup(() => {
-      element.reporter.restore();
-      sandbox.stub(element, '_reportEvent');
-    });
-
-    test('pluginsLoaded reports time', () => {
-      sandbox.stub(element, 'now').returns(42);
-      element.pluginsLoaded();
-      assert.isTrue(element._reportEvent.calledWithMatch(
-          {
-            type: 'timing-report',
-            category: 'UI Latency',
-            name: 'PluginsLoaded',
-            value: 42,
-          }
-      ));
-    });
-
-    test('pluginsLoaded reports plugins', () => {
-      element.pluginsLoaded(['foo', 'bar']);
-      assert.isTrue(element._reportEvent.calledWithMatch(
-          {
-            type: 'lifecycle',
-            category: 'Plugins installed',
-            eventDetails: JSON.stringify({pluginsList: ['foo', 'bar']}),
-          }
-      ));
-    });
-
-    test('caches reports if plugins are not loaded', () => {
-      element.timeEnd('foo');
-      assert.isFalse(element._reportEvent.called);
-    });
-
-    test('reports if plugins are loaded', () => {
-      element.pluginsLoaded();
-      assert.isTrue(element._reportEvent.called);
-    });
-
-    test('reports if metrics plugin xyz is loaded', () => {
-      element.pluginLoaded('metrics-xyz');
-      assert.isTrue(element._reportEvent.called);
-    });
-
-    test('reports cached events preserving order', () => {
-      element.time('foo');
-      element.time('bar');
-      element.timeEnd('foo');
-      element.pluginsLoaded();
-      element.timeEnd('bar');
-      assert.isTrue(element._reportEvent.getCall(0).calledWithMatch(
-          {type: 'timing-report', category: 'UI Latency', name: 'foo'}
-      ));
-      assert.isTrue(element._reportEvent.getCall(1).calledWithMatch(
-          {type: 'timing-report', category: 'UI Latency',
-            name: 'PluginsLoaded'}
-      ));
-      assert.isTrue(element._reportEvent.getCall(2).calledWithMatch(
-          {type: 'lifecycle', category: 'Plugins installed'}
-      ));
-      assert.isTrue(element._reportEvent.getCall(3).calledWithMatch(
-          {type: 'timing-report', category: 'UI Latency', name: 'bar'}
-      ));
-    });
-  });
-
-  test('search', () => {
-    element.locationChanged('_handleSomeRoute');
-    assert.isTrue(element.reporter.calledWithExactly(
-        'nav-report', 'Location Changed', 'Page', '_handleSomeRoute'));
-  });
-
-  suite('exception logging', () => {
-    let fakeWindow;
-    let reporter;
-
-    const emulateThrow = function(msg, url, line, column, error) {
-      return fakeWindow.onerror(msg, url, line, column, error);
-    };
-
-    setup(() => {
-      reporter = sandbox.stub(GrReporting.prototype, 'reporter');
-      fakeWindow = {
-        handlers: {},
-        addEventListener(type, handler) {
-          this.handlers[type] = handler;
-        },
-      };
-      sandbox.stub(console, 'error');
-      window.GrReporting._catchErrors(fakeWindow);
-    });
-
-    test('is reported', () => {
-      const error = new Error('bar');
-      error.stack = undefined;
-      emulateThrow('bar', 'http://url', 4, 2, error);
-      assert.isTrue(reporter.calledWith('error', 'exception', 'bar'));
-      const payload = reporter.lastCall.args[3];
-      assert.deepEqual(payload, {
-        url: 'http://url',
-        line: 4,
-        column: 2,
-        error,
-      });
-    });
-
-    test('is reported with 3 lines of stack', () => {
-      const error = new Error('bar');
-      emulateThrow('bar', 'http://url', 4, 2, error);
-      const expectedStack = error.stack.split('\n').slice(0, 3)
-          .join('\n');
-      assert.isTrue(reporter.calledWith('error', 'exception',
-          expectedStack));
-    });
-
-    test('prevent default event handler', () => {
-      assert.isTrue(emulateThrow());
-    });
-
-    test('unhandled rejection', () => {
-      fakeWindow.handlers['unhandledrejection']({
-        reason: {
-          message: 'bar',
-        },
-      });
-      assert.isTrue(reporter.calledWith('error', 'exception', 'bar'));
-    });
-  });
-});
-</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 11465ba..c1cf169 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -14,10 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
-import '../gr-reporting/gr-reporting.js';
 import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
@@ -28,6 +25,7 @@
 import {PatchSetBehavior} from '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
 import {URLEncodingBehavior} from '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
 import {GerritNav} from '../gr-navigation/gr-navigation.js';
+import {appContext} from '../../../services/app-context.js';
 
 const RoutePattern = {
   ROOT: '/',
@@ -145,7 +143,7 @@
   DIFF: /^\/c\/(.+)\/\+\/(\d+)(\/((-?\d+|edit)(\.\.(\d+|edit))?(\/(.+))))\/?$/,
 
   // Matches /c/<project>/+/<changeNum>/[<patchNum|edit>]/<path>,edit[#lineNum]
-  DIFF_EDIT: /^\/c\/(.+)\/\+\/(\d+)\/(\d+|edit)\/(.+),edit(\#\d+)?$/,
+  DIFF_EDIT: /^\/c\/(.+)\/\+\/(\d+)\/(\d+|edit)\/(.+),edit(#\d+)?$/,
 
   // Matches non-project-relative
   // /c/<changeNum>/[<basePatchNum>..]<patchNum>/<path>.
@@ -161,7 +159,7 @@
 
   // Matches /c/<changeNum>/ /<URL tail>
   // Catches improperly encoded URLs (context: Issue 7100)
-  IMPROPERLY_ENCODED_PLUS: /^\/c\/(.+)\/\ \/(.+)$/,
+  IMPROPERLY_ENCODED_PLUS: /^\/c\/(.+)\/ \/(.+)$/,
 
   PLUGIN_SCREEN: /^\/x\/([\w-]+)\/([\w-]+)\/?/,
 
@@ -197,7 +195,7 @@
 
 const LEGACY_QUERY_SUFFIX_PATTERN = /,n,z$/;
 
-const REPO_TOKEN_PATTERN = /\$\{(project|repo)\}/g;
+const REPO_TOKEN_PATTERN = /\${(project|repo)}/g;
 
 // Polymer makes `app` intrinsically defined on the window by virtue of the
 // custom element having the id "app", but it is made explicit here.
@@ -210,15 +208,13 @@
 
 // Setup listeners outside of the router component initialization.
 (function() {
-  const reporting = document.createElement('gr-reporting');
-
   window.addEventListener('WebComponentsReady', () => {
-    reporting.timeEnd('WebComponentsReady');
+    appContext.reportingService.timeEnd('WebComponentsReady');
   });
 })();
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrRouter extends mixinBehaviors( [
   BaseUrlBehavior,
@@ -247,6 +243,11 @@
     };
   }
 
+  constructor() {
+    super();
+    this.reporting = appContext.reportingService;
+  }
+
   start() {
     if (!this._app) { return; }
     this._startRouter();
@@ -716,7 +717,7 @@
         (ctx, next) => this._loadUserMiddleware(ctx, next),
         (ctx, next) => this._queryStringMiddleware(ctx, next),
         data => {
-          this.$.reporting.locationChanged(handlerName);
+          this.reporting.locationChanged(handlerName);
           const promise = opt_authRedirect ?
             this._redirectIfNotLoggedIn(data) : Promise.resolve();
           promise.then(() => { this[handlerName](data); });
@@ -738,7 +739,7 @@
 
     page.exit('*', (ctx, next) => {
       if (!this._isRedirecting) {
-        this.$.reporting.beforeLocationChanged();
+        this.reporting.beforeLocationChanged();
       }
       this._isRedirecting = false;
       this._isInitialLoad = false;
@@ -1089,7 +1090,7 @@
       project,
       dashboard: decodeURIComponent(data.params[1]),
     });
-    this.$.reporting.setRepoName(project);
+    this.reporting.setRepoName(project);
   }
 
   _handleGroupInfoRoute(data) {
@@ -1170,7 +1171,7 @@
       detail: GerritNav.RepoDetailView.COMMANDS,
       repo,
     });
-    this.$.reporting.setRepoName(repo);
+    this.reporting.setRepoName(repo);
   }
 
   _handleRepoAccessRoute(data) {
@@ -1180,7 +1181,7 @@
       detail: GerritNav.RepoDetailView.ACCESS,
       repo,
     });
-    this.$.reporting.setRepoName(repo);
+    this.reporting.setRepoName(repo);
   }
 
   _handleRepoDashboardsRoute(data) {
@@ -1190,7 +1191,7 @@
       detail: GerritNav.RepoDetailView.DASHBOARDS,
       repo,
     });
-    this.$.reporting.setRepoName(repo);
+    this.reporting.setRepoName(repo);
   }
 
   _handleBranchListOffsetRoute(data) {
@@ -1296,7 +1297,7 @@
       view: GerritNav.View.REPO,
       repo,
     });
-    this.$.reporting.setRepoName(repo);
+    this.reporting.setRepoName(repo);
   }
 
   _handlePluginListOffsetRoute(data) {
@@ -1359,7 +1360,7 @@
       queryMap: ctx.queryMap,
     };
 
-    this.$.reporting.setRepoName(params.project);
+    this.reporting.setRepoName(params.project);
     this._redirectOrNavigate(params);
   }
 
@@ -1379,7 +1380,7 @@
       params.leftSide = address.leftSide;
       params.lineNum = address.lineNum;
     }
-    this.$.reporting.setRepoName(params.project);
+    this.reporting.setRepoName(params.project);
     this._redirectOrNavigate(params);
   }
 
@@ -1430,7 +1431,7 @@
       lineNum: ctx.hash,
       view: GerritNav.View.EDIT,
     });
-    this.$.reporting.setRepoName(project);
+    this.reporting.setRepoName(project);
   }
 
   _handleChangeEditRoute(ctx) {
@@ -1443,7 +1444,7 @@
       view: GerritNav.View.CHANGE,
       edit: true,
     });
-    this.$.reporting.setRepoName(project);
+    this.reporting.setRepoName(project);
   }
 
   /**
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_html.js b/polygerrit-ui/app/elements/core/gr-router/gr-router_html.js
index 07f067e..8aa0835 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router_html.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_html.js
@@ -18,5 +18,4 @@
 
 export const htmlTemplate = html`
   <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
-  <gr-reporting id="reporting"></gr-reporting>
 `;
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
index f638f14..02b4efd 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
@@ -14,7 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
 import '../../shared/gr-autocomplete/gr-autocomplete.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
 import '../../../styles/shared-styles.js';
@@ -114,7 +113,7 @@
 const TOKENIZE_REGEX = /(?:[^\s"]+|"[^"]*")+\s*/g;
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrSearchBar extends mixinBehaviors( [
   KeyboardShortcutBehavior,
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html
index 3b37e09..9e5fdfe 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html
@@ -26,12 +26,6 @@
 <script src="/components/wct-browser-legacy/browser.js"></script>
 <script src="/node_modules/page/page.js"></script>
 
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-search-bar.js';
-void (0);
-</script>
-
 <test-fixture id="basic">
   <template>
     <gr-search-bar></gr-search-bar>
diff --git a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.js b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.js
index dcece30..f6221c8 100644
--- a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.js
+++ b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
 import '../gr-search-bar/gr-search-bar.js';
 import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
@@ -31,7 +29,7 @@
 const ME_EXPRESSION = 'me';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrSmartSearch extends mixinBehaviors( [
   DisplayNameBehavior,
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.js b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.js
index e2dda2f..16dee09 100644
--- a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.js
+++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-icon/iron-icon.js';
 import '../../../styles/shared-styles.js';
 import '../../shared/gr-dialog/gr-dialog.js';
@@ -29,7 +27,7 @@
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrApplyFixDialog extends GestureEventListeners(
     LegacyElementMixin(PolymerElement)) {
@@ -120,10 +118,9 @@
         .getRobotCommentFixPreview(this.changeNum, this._patchNum, fixId)
         .then(res => {
           if (res != null) {
-            const previews = Object.keys(res).map(key => {
+            this._currentPreviews = Object.keys(res).map(key => {
               return {filepath: key, preview: res[key]};
             });
-            this._currentPreviews = previews;
           }
         })
         .catch(err => {
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.html b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.html
index 8874f71..65958c8 100644
--- a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.html
@@ -21,12 +21,6 @@
 
 <script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
 <script src="/components/wct-browser-legacy/browser.js"></script>
-<script type="module">
-import '../../../test/common-test-setup.js';
-import '../../../test/common-test-setup.js';
-import './gr-apply-fix-dialog.js';
-void (0);
-</script>
 
 <test-fixture id='basic'>
   <template>
@@ -36,7 +30,6 @@
 
 <script type="module">
 import '../../../test/common-test-setup.js';
-import '../../../test/common-test-setup.js';
 import './gr-apply-fix-dialog.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.js b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.js
index 3f7da5a..040b0bd 100644
--- a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.js
+++ b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
 import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
@@ -23,7 +21,7 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-comment-api_html.js';
 import {PatchSetBehavior} from '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
-import {util} from '../../../scripts/util.js';
+import {parseDate} from '../../../utils/date-util.js';
 
 const PARENT = 'PARENT';
 
@@ -47,12 +45,33 @@
     this._getParentIndex =
       PatchSetBehavior.getParentIndex;
 
-    this._comments = comments || {};
-    this._robotComments = robotComments || {};
-    this._drafts = drafts || {};
+    this._comments = this._addPath(comments);
+    this._robotComments = this._addPath(robotComments);
+    this._drafts = this._addPath(drafts);
     this._changeNum = changeNum;
   }
 
+  /**
+   * Add path info to every comment as CommentInfo returned
+   * from server does not have that.
+   *
+   * TODO(taoalpha): should consider changing BE to send path
+   * back within CommentInfo
+   *
+   * @param {Object} - map between file path and comments
+   */
+  _addPath(comments = {}) {
+    const updatedComments = {};
+    for (const filePath of Object.keys(comments)) {
+      const allCommentsForPath = comments[filePath] || [];
+      if (allCommentsForPath.length) {
+        updatedComments[filePath] = allCommentsForPath
+            .map(comment => { return {...comment, path: filePath}; });
+      }
+    }
+    return updatedComments;
+  }
+
   get comments() {
     return this._comments;
   }
@@ -160,19 +179,17 @@
     const paths = this.getPaths();
     const publishedComments = {};
     for (const path of Object.keys(paths)) {
-      let commentsToAdd = this.getAllCommentsForPath(path, opt_patchNum);
-      if (opt_includeDrafts) {
-        const drafts = this.getAllDraftsForPath(path, opt_patchNum)
-            .map(d => Object.assign({__draft: true}, d));
-        commentsToAdd = commentsToAdd.concat(drafts);
-      }
-      publishedComments[path] = commentsToAdd;
+      publishedComments[path] = this.getAllCommentsForPath(
+          path,
+          opt_patchNum,
+          opt_includeDrafts
+      );
     }
     return publishedComments;
   }
 
   /**
-   * Gets all the comments and robot comments for the given change.
+   * Gets all the drafts for the given change.
    *
    * @param {number=} opt_patchNum
    * @return {!Object}
@@ -189,6 +206,9 @@
   /**
    * Get the comments (robot comments) for a path and optional patch num.
    *
+   * This method will always return a new shallow copy of all comments,
+   * so manipulation on one copy won't affect other copies.
+   *
    * @param {!string} path
    * @param {number=} opt_patchNum
    * @param {boolean=} opt_includeDrafts
@@ -200,14 +220,15 @@
     const robotComments = this._robotComments[path] || [];
     let allComments = comments.concat(robotComments);
     if (opt_includeDrafts) {
-      const drafts = this.getAllDraftsForPath(path)
-          .map(d => Object.assign({__draft: true}, d));
+      const drafts = this.getAllDraftsForPath(path);
       allComments = allComments.concat(drafts);
     }
-    if (!opt_patchNum) { return allComments; }
-    return (allComments || []).filter(c =>
-      this._patchNumEquals(c.patch_set, opt_patchNum)
-    );
+    if (opt_patchNum) {
+      allComments = allComments.filter(c =>
+        this._patchNumEquals(c.patch_set, opt_patchNum)
+      );
+    }
+    return allComments.map(c => { return {...c}; });
   }
 
   /**
@@ -215,7 +236,7 @@
    *
    * // TODO(taoalpha): maybe merge in *ForPath
    *
-   * @param {!{path: string, oldPath?: string, patchNum?: number}} file
+   * @param {!{path: string, basePath?: string, patchNum?: number}} file
    * @param {boolean=} opt_includeDrafts
    * @return {!Array}
    */
@@ -224,10 +245,10 @@
         file.path, file.patchNum, opt_includeDrafts
     );
 
-    if (file.oldPath) {
+    if (file.basePath) {
       allComments = allComments.concat(
           this.getAllCommentsForPath(
-              file.oldPath, file.patchNum, opt_includeDrafts
+              file.basePath, file.patchNum, opt_includeDrafts
           )
       );
     }
@@ -238,17 +259,22 @@
   /**
    * Get the drafts for a path and optional patch num.
    *
+   * This will return a shallow copy of all drafts every time,
+   * so changes on any copy will not affect other copies.
+   *
    * @param {!string} path
    * @param {number=} opt_patchNum
    * @return {!Array}
    */
   getAllDraftsForPath(path,
       opt_patchNum) {
-    const comments = this._drafts[path] || [];
-    if (!opt_patchNum) { return comments; }
-    return (comments || []).filter(c =>
-      this._patchNumEquals(c.patch_set, opt_patchNum)
-    );
+    let comments = this._drafts[path] || [];
+    if (opt_patchNum) {
+      comments = comments.filter(c =>
+        this._patchNumEquals(c.patch_set, opt_patchNum)
+      );
+    }
+    return comments.map(c => { return {...c, __draft: true}; });
   }
 
   /**
@@ -256,14 +282,14 @@
    *
    * // TODO(taoalpha): maybe merge in *ForPath
    *
-   * @param {!{path: string, oldPath?: string, patchNum?: number}} file
+   * @param {!{path: string, basePath?: string, patchNum?: number}} file
    * @return {!Array}
    */
   getAllDraftsForFile(file) {
     let allDrafts = this.getAllDraftsForPath(file.path, file.patchNum);
-    if (file.oldPath) {
+    if (file.basePath) {
       allDrafts = allDrafts.concat(
-          this.getAllDraftsForPath(file.oldPath, file.patchNum)
+          this.getAllDraftsForPath(file.basePath, file.patchNum)
       );
     }
     return allDrafts;
@@ -298,7 +324,8 @@
 
     drafts.forEach(d => { d.__draft = true; });
 
-    const all = comments.concat(drafts).concat(robotComments);
+    const all = comments.concat(drafts).concat(robotComments)
+        .map(c => { return {...c}; });
 
     const baseComments = all.filter(c =>
       this._isInBaseOfPatchRange(c, patchRange));
@@ -324,7 +351,7 @@
    *
    * // TODO(taoalpha): maybe merge *ForPath so find all comments in one pass
    *
-   * @param {!{path: string, oldPath?: string, patchNum?: number}} file
+   * @param {!{path: string, basePath?: string, patchNum?: number}} file
    * @param {!Gerrit.PatchRange} patchRange The patch-range object containing patchNum
    *     and basePatchNum properties to represent the range.
    * @param {Object=} opt_projectConfig Optional project config object to
@@ -335,13 +362,13 @@
     const comments = this.getCommentsBySideForPath(
         file.path, patchRange, opt_projectConfig
     );
-    if (file.oldPath) {
-      const commentsForOldPath = this.getCommentsBySideForPath(
-          file.oldPath, patchRange, opt_projectConfig
+    if (file.basePath) {
+      const commentsForBasePath = this.getCommentsBySideForPath(
+          file.basePath, patchRange, opt_projectConfig
       );
       // merge in the left and right
-      comments.left = comments.left.concat(commentsForOldPath.left);
-      comments.right = comments.right.concat(commentsForOldPath.right);
+      comments.left = comments.left.concat(commentsForBasePath.left);
+      comments.right = comments.right.concat(commentsForBasePath.right);
     }
     return comments;
   }
@@ -376,7 +403,7 @@
   /**
    * Computes a string counting the number of commens in a given file.
    *
-   * @param {{path: string, oldPath?: string, patchNum?: number}} file
+   * @param {{path: string, basePath?: string, patchNum?: number}} file
    * @return {number}
    */
   computeCommentCount(file) {
@@ -391,7 +418,7 @@
    * Computes a string counting the number of draft comments in the entire
    * change, optionally filtered by path and/or patchNum.
    *
-   * @param {?{path: string, oldPath?: string, patchNum?: number}} file
+   * @param {?{path: string, basePath?: string, patchNum?: number}} file
    * @return {number}
    */
   computeDraftCount(file) {
@@ -405,7 +432,7 @@
   /**
    * Computes a number of unresolved comment threads in a given file and path.
    *
-   * @param {{path: string, oldPath?: string, patchNum?: number}} file
+   * @param {{path: string, basePath?: string, patchNum?: number}} file
    * @return {number}
    */
   computeUnresolvedNum(file) {
@@ -443,7 +470,7 @@
         .sort(
             (c1, c2) => {
               const dateDiff =
-                  util.parseDate(c1.updated) - util.parseDate(c2.updated);
+                  parseDate(c1.updated) - parseDate(c2.updated);
               if (dateDiff) {
                 return dateDiff;
               }
@@ -514,12 +541,9 @@
       return true;
     }
     // If the base of the range is not the parent of the patch:
-    if (range.basePatchNum !== PARENT &&
-      comment.side !== PARENT &&
-      this._patchNumEquals(comment.patch_set, range.basePatchNum)) {
-      return true;
-    }
-    return false;
+    return range.basePatchNum !== PARENT &&
+        comment.side !== PARENT &&
+        this._patchNumEquals(comment.patch_set, range.basePatchNum);
   }
 
   /**
@@ -550,7 +574,7 @@
 }
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrCommentApi extends mixinBehaviors( [
   PatchSetBehavior,
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.html b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.html
index 29262e3..172a342 100644
--- a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.html
@@ -370,16 +370,24 @@
       test('getAllCommentsForPath', () => {
         let path = 'file/one';
         let comments = element._changeComments.getAllCommentsForPath(path);
-        assert.deepEqual(comments.length, 4);
+        assert.equal(comments.length, 4);
         path = 'file/two';
         comments = element._changeComments.getAllCommentsForPath(path, 2);
-        assert.deepEqual(comments.length, 1);
+        assert.equal(comments.length, 1);
+        const aCopyOfComments = element._changeComments
+            .getAllCommentsForPath(path, 2);
+        assert.deepEqual(comments, aCopyOfComments);
+        assert.notEqual(comments[0], aCopyOfComments[0]);
       });
 
       test('getAllDraftsForPath', () => {
         const path = 'file/one';
         const drafts = element._changeComments.getAllDraftsForPath(path);
-        assert.deepEqual(drafts.length, 2);
+        assert.equal(drafts.length, 2);
+        const aCopyOfDrafts = element._changeComments
+            .getAllDraftsForPath(path);
+        assert.deepEqual(drafts, aCopyOfDrafts);
+        assert.notEqual(drafts[0], aCopyOfDrafts[0]);
       });
 
       test('computeUnresolvedNum', () => {
diff --git a/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer.js b/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer.js
index cdd6d8f..86537e8 100644
--- a/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer.js
+++ b/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
@@ -29,7 +27,7 @@
   [CoverageType.NOT_INSTRUMENTED, 'Not instrumented by any tests.'],
 ]);
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrCoverageLayer extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.js
index b38543c..1c4edec 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../gr-coverage-layer/gr-coverage-layer.js';
 import '../gr-diff-processor/gr-diff-processor.js';
 import '../../shared/gr-hovercard/gr-hovercard.js';
@@ -46,7 +44,7 @@
 const COMMIT_MSG_LINE_LENGTH = 72;
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrDiffBuilderElement extends GestureEventListeners(
     LegacyElementMixin(PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.html
index e5eec8d..67418e2 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.html
@@ -52,7 +52,7 @@
 import '../gr-diff/gr-diff-group.js';
 import './gr-diff-builder.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
-import {getMockDiffResponse} from '../../../test/mock-diff-response.js';
+import {getMockDiffResponse} from '../../../test/mocks/diff-response.js';
 import './gr-diff-builder-element.js';
 import {dom, flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {GrAnnotation} from '../gr-diff-highlight/gr-annotation.js';
@@ -96,44 +96,70 @@
     assert.isTrue(node.classList.contains('classes'));
   });
 
-  test('context control buttons', () => {
-    // Create 10 lines.
-    const lines = [];
-    for (let i = 0; i < 10; i++) {
-      const line = new GrDiffLine(GrDiffLine.Type.BOTH);
-      line.beforeNumber = i + 1;
-      line.afterNumber = i + 1;
-      line.text = 'lorem upsum';
-      lines.push(line);
+  suite('context control', () => {
+    function createContextLine(options) {
+      const offset = options.offset || 0;
+      const numLines = options.count || 10;
+      const lines = [];
+      for (let i = 0; i < numLines; i++) {
+        const line = new GrDiffLine(GrDiffLine.Type.BOTH);
+        line.beforeNumber = offset + i + 1;
+        line.afterNumber = offset + i + 1;
+        line.text = 'lorem upsum';
+        lines.push(line);
+      }
+
+      return {
+        contextGroups: [new GrDiffGroup(GrDiffGroup.Type.BOTH, lines)],
+      };
     }
 
-    const contextLine = {
-      contextGroups: [new GrDiffGroup(GrDiffGroup.Type.BOTH, lines)],
-    };
+    test('no +10 buttons for 10 or less lines', () => {
+      const contextLine = createContextLine({count: 10});
 
-    const section = {};
-    // Does not include +10 buttons when there are fewer than 11 lines.
-    let td = builder._createContextControl(section, contextLine);
-    let buttons = td.querySelectorAll('gr-button.showContext');
+      const td = builder._createContextControl({}, contextLine);
+      const buttons = td.querySelectorAll('gr-button.showContext');
 
-    assert.equal(buttons.length, 1);
-    assert.equal(dom(buttons[0]).textContent, 'Show 10 common lines');
+      assert.equal(buttons.length, 1);
+      assert.equal(dom(buttons[0]).textContent, 'Show 10 common lines');
+    });
 
-    // Add another line.
-    const line = new GrDiffLine(GrDiffLine.Type.BOTH);
-    line.text = 'lorem upsum';
-    line.beforeNumber = 11;
-    line.afterNumber = 11;
-    contextLine.contextGroups[0].addLine(line);
+    test('context control at the top', () => {
+      const contextLine = createContextLine({offset: 0, count: 20});
 
-    // Includes +10 buttons when there are at least 11 lines.
-    td = builder._createContextControl(section, contextLine);
-    buttons = td.querySelectorAll('gr-button.showContext');
+      builder._numLinesLeft = 50;
+      const td = builder._createContextControl({}, contextLine);
+      const buttons = td.querySelectorAll('gr-button.showContext');
 
-    assert.equal(buttons.length, 3);
-    assert.equal(dom(buttons[0]).textContent, '+10 above');
-    assert.equal(dom(buttons[1]).textContent, 'Show 11 common lines');
-    assert.equal(dom(buttons[2]).textContent, '+10 below');
+      assert.equal(buttons.length, 2);
+      assert.equal(dom(buttons[0]).textContent, 'Show 20 common lines');
+      assert.equal(dom(buttons[1]).textContent, '+10 below');
+    });
+
+    test('context control in the middle', () => {
+      const contextLine = createContextLine({offset: 10, count: 20});
+
+      builder._numLinesLeft = 50;
+      const td = builder._createContextControl({}, contextLine);
+      const buttons = td.querySelectorAll('gr-button.showContext');
+
+      assert.equal(buttons.length, 3);
+      assert.equal(dom(buttons[0]).textContent, '+10 above');
+      assert.equal(dom(buttons[1]).textContent, 'Show 20 common lines');
+      assert.equal(dom(buttons[2]).textContent, '+10 below');
+    });
+
+    test('context control at the top', () => {
+      const contextLine = createContextLine({offset: 30, count: 20});
+
+      builder._numLinesLeft = 50;
+      const td = builder._createContextControl({}, contextLine);
+      const buttons = td.querySelectorAll('gr-button.showContext');
+
+      assert.equal(buttons.length, 2);
+      assert.equal(dom(buttons[0]).textContent, '+10 above');
+      assert.equal(dom(buttons[1]).textContent, 'Show 20 common lines');
+    });
   });
 
   test('newlines 1', () => {
@@ -263,7 +289,6 @@
     expectTextLength('abc\t', 8, 8);
     expectTextLength('abc\t\t', 10, 20);
     expectTextLength('', 10, 0);
-    expectTextLength('', 10, 0);
     // 17 Thai combining chars.
     expectTextLength('ก้้้้้้้้้้้้้้้้', 4, 17);
     expectTextLength('abc\tde', 10, 12);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-image.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-image.js
index 1fc0d4f..6983af0 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-image.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-image.js
@@ -16,6 +16,7 @@
  */
 
 import {GrDiffBuilderSideBySide} from './gr-diff-builder-side-by-side.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 
 // MIME types for images we allow showing. Do not include SVG, it can contain
 // arbitrary JavaScript.
@@ -52,7 +53,7 @@
   // column limit.
   td.setAttribute('colspan', '4');
   const endpoint = this._createElement('gr-endpoint-decorator');
-  const endpointDomApi = Polymer.dom(endpoint);
+  const endpointDomApi = dom(endpoint);
   endpointDomApi.setAttribute('name', 'image-diff');
   endpointDomApi.appendChild(
       this._createEndpointParam('baseImage', this._baseImage));
@@ -106,7 +107,7 @@
 
 GrDiffBuilderImage.prototype._updateImageLabel = function(section, className,
     image) {
-  const label = Polymer.dom(section)
+  const label = dom(section)
       .querySelector('.' + className + ' span.label');
   this._setLabelText(label, image);
 };
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
index 4b91000..74a4591 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
@@ -16,6 +16,7 @@
  */
 import {GrDiffLine} from '../gr-diff/gr-diff-line.js';
 import {GrDiffGroup} from '../gr-diff/gr-diff-group.js';
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 
 /**
  * In JS, unicode code points above 0xFFFF occupy two elements of a string.
@@ -41,6 +42,11 @@
 
 export function GrDiffBuilder(diff, prefs, outputEl, layers) {
   this._diff = diff;
+  this._numLinesLeft = this._diff.content ? this._diff.content.reduce(
+      (sum, chunk) => {
+        const left = chunk.a || chunk.ab;
+        return sum + (left ? left.length : 0);
+      }, 0) : 0;
   this._prefs = prefs;
   this._outputEl = outputEl;
   this.groups = [];
@@ -142,7 +148,7 @@
 
 GrDiffBuilder.prototype.getContentByLine = function(lineNumber, opt_side,
     opt_root) {
-  const root = Polymer.dom(opt_root || this._outputEl);
+  const root = dom(opt_root || this._outputEl);
   const sideSelector = opt_side ? ('.' + opt_side) : '';
   return root.querySelector('td.lineNum[data-value="' + lineNumber +
       '"]' + sideSelector + ' ~ td.content .contentText');
@@ -221,16 +227,18 @@
 GrDiffBuilder.prototype._createContextControl = function(section, line) {
   if (!line.contextGroups) return null;
 
-  const numLines =
-      line.contextGroups[line.contextGroups.length - 1].lineRange.left.end -
-      line.contextGroups[0].lineRange.left.start + 1;
+  const leftStart = line.contextGroups[0].lineRange.left.start;
+  const leftEnd =
+      line.contextGroups[line.contextGroups.length - 1].lineRange.left.end;
+
+  const numLines = leftEnd - leftStart + 1;
 
   if (numLines === 0) return null;
 
   const td = this._createElement('td');
   const showPartialLinks = numLines > PARTIAL_CONTEXT_AMOUNT;
 
-  if (showPartialLinks) {
+  if (showPartialLinks && leftStart > 1) {
     td.appendChild(this._createContextButton(
         GrDiffBuilder.ContextButtonType.ABOVE, section, line, numLines));
   }
@@ -238,7 +246,7 @@
   td.appendChild(this._createContextButton(
       GrDiffBuilder.ContextButtonType.ALL, section, line, numLines));
 
-  if (showPartialLinks) {
+  if (showPartialLinks && leftEnd < this._numLinesLeft) {
     td.appendChild(this._createContextButton(
         GrDiffBuilder.ContextButtonType.BELOW, section, line, numLines));
   }
@@ -259,7 +267,7 @@
   if (type === GrDiffBuilder.ContextButtonType.ALL) {
     const icon = this._createElement('iron-icon', 'showContext');
     icon.setAttribute('icon', 'gr-icons:unfold-more');
-    Polymer.dom(button).appendChild(icon);
+    dom(button).appendChild(icon);
 
     text = 'Show ' + numLines + ' common line';
     if (numLines > 1) { text += 's'; }
@@ -274,8 +282,8 @@
         0, numLines - context);
   }
   const textSpan = this._createElement('span', 'showContext');
-  Polymer.dom(textSpan).textContent = text;
-  Polymer.dom(button).appendChild(textSpan);
+  dom(textSpan).textContent = text;
+  dom(button).appendChild(textSpan);
 
   button.addEventListener('tap', e => {
     e.detail = {
@@ -533,7 +541,7 @@
  * @return {HTMLTableDataCellElement}
  */
 GrDiffBuilder.prototype._getBlameByLineNum = function(lineNum) {
-  const root = Polymer.dom(this._outputEl);
+  const root = dom(this._outputEl);
   return root.querySelector(`td.blame[data-line-number="${lineNum}"]`);
 };
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js
index 0b9ae5b..acb8f0c 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js
@@ -14,7 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
 
 import '../../shared/gr-cursor-manager/gr-cursor-manager.js';
 import {afterNextRender} from '@polymer/polymer/lib/utils/render-status.js';
@@ -23,6 +22,7 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-diff-cursor_html.js';
+import {ScrollMode} from '../../../constants/constants.js';
 
 const DiffSides = {
   LEFT: 'left',
@@ -34,15 +34,10 @@
   UNIFIED: 'UNIFIED_DIFF',
 };
 
-const ScrollBehavior = {
-  KEEP_VISIBLE: 'keep-visible',
-  NEVER: 'never',
-};
-
 const LEFT_SIDE_CLASS = 'target-side-left';
 const RIGHT_SIDE_CLASS = 'target-side-right';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrDiffCursor extends GestureEventListeners(
     LegacyElementMixin(PolymerElement)) {
   static get template() { return htmlTemplate; }
@@ -93,9 +88,9 @@
        * 'keep-visible'. 'keep-visible' will only scroll if the cursor is beyond
        * the viewport.
        */
-      _scrollBehavior: {
+      _scrollMode: {
         type: String,
-        value: ScrollBehavior.KEEP_VISIBLE,
+        value: ScrollMode.KEEP_VISIBLE,
       },
 
       _focusOnMove: {
@@ -294,10 +289,9 @@
   reInitCursor() {
     if (!this.diffRow) {
       // does not scroll during init unless requested
-      const scrollingBehaviorForInit = this.initialLineNumber ?
-        ScrollBehavior.KEEP_VISIBLE :
-        ScrollBehavior.NEVER;
-      this._scrollBehavior = scrollingBehaviorForInit;
+      this._scrollMode = this.initialLineNumber ?
+        ScrollMode.KEEP_VISIBLE :
+        ScrollMode.NEVER;
       if (this.initialLineNumber) {
         this.moveToLineNumber(this.initialLineNumber, this.side);
         this.initialLineNumber = null;
@@ -309,17 +303,22 @@
   }
 
   reInit() {
-    this._scrollBehavior = ScrollBehavior.KEEP_VISIBLE;
+    this._scrollMode = ScrollMode.KEEP_VISIBLE;
   }
 
   _handleWindowScroll() {
     if (this._preventAutoScrollOnManualScroll) {
-      this._scrollBehavior = ScrollBehavior.NEVER;
+      this._scrollMode = ScrollMode.NEVER;
       this._focusOnMove = false;
       this._preventAutoScrollOnManualScroll = false;
     }
   }
 
+  reInitAndUpdateStops() {
+    this.reInit();
+    this._updateStops();
+  }
+
   handleDiffUpdate() {
     this._updateStops();
     this.reInitCursor();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_html.js b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_html.js
index 1ac47f6..e400792 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_html.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_html.js
@@ -19,7 +19,7 @@
 export const htmlTemplate = html`
   <gr-cursor-manager
     id="cursorManager"
-    scroll-behavior="[[_scrollBehavior]]"
+    scroll-mode="[[_scrollMode]]"
     cursor-target-class="target-row"
     focus-on-move="[[_focusOnMove]]"
     target="{{diffRow}}"
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html
index 77e5179..71fc341 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html
@@ -44,7 +44,7 @@
 import '../gr-diff/gr-diff.js';
 import './gr-diff-cursor.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
-import {getMockDiffResponse} from '../../../test/mock-diff-response.js';
+import {getMockDiffResponse} from '../../../test/mocks/diff-response.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 suite('gr-diff-cursor tests', () => {
   let sandbox;
@@ -117,20 +117,20 @@
   });
 
   test('cursor scroll behavior', () => {
-    assert.equal(cursorElement._scrollBehavior, 'keep-visible');
+    assert.equal(cursorElement._scrollMode, 'keep-visible');
 
     cursorElement._handleDiffRenderStart();
     assert.isTrue(cursorElement._focusOnMove);
 
     cursorElement._handleWindowScroll();
-    assert.equal(cursorElement._scrollBehavior, 'never');
+    assert.equal(cursorElement._scrollMode, 'never');
     assert.isFalse(cursorElement._focusOnMove);
 
     cursorElement._handleDiffRenderContent();
     assert.isTrue(cursorElement._focusOnMove);
 
     cursorElement.reInitCursor();
-    assert.equal(cursorElement._scrollBehavior, 'keep-visible');
+    assert.equal(cursorElement._scrollMode, 'keep-visible');
   });
 
   test('moves to selected line', () => {
@@ -237,7 +237,7 @@
     cursorElement.moveToNextChunk();
 
     // Since this chunk only has content on the left side. we should have been
-    // automatically mvoed over.
+    // automatically moved over.
     const previousIndex = currentIndex;
     currentIndex = indexOfChunk(cursorElement.diffRow.parentElement);
     assert.equal(currentIndex, previousIndex + 1);
@@ -248,7 +248,7 @@
     let scrollBehaviorDuringMove;
     const moveToNumStub = sandbox.stub(cursorElement, 'moveToLineNumber');
     const moveToChunkStub = sandbox.stub(cursorElement, 'moveToFirstChunk',
-        () => { scrollBehaviorDuringMove = cursorElement._scrollBehavior; });
+        () => { scrollBehaviorDuringMove = cursorElement._scrollMode; });
 
     function renderHandler() {
       diffElement.removeEventListener('render', renderHandler);
@@ -256,7 +256,7 @@
       assert.isFalse(moveToNumStub.called);
       assert.isTrue(moveToChunkStub.called);
       assert.equal(scrollBehaviorDuringMove, 'never');
-      assert.equal(cursorElement._scrollBehavior, 'keep-visible');
+      assert.equal(cursorElement._scrollMode, 'keep-visible');
       done();
     }
     diffElement.addEventListener('render', renderHandler);
@@ -266,7 +266,7 @@
   test('initialLineNumber provided', done => {
     let scrollBehaviorDuringMove;
     const moveToNumStub = sandbox.stub(cursorElement, 'moveToLineNumber',
-        () => { scrollBehaviorDuringMove = cursorElement._scrollBehavior; });
+        () => { scrollBehaviorDuringMove = cursorElement._scrollMode; });
     const moveToChunkStub = sandbox.stub(cursorElement, 'moveToFirstChunk');
     function renderHandler() {
       diffElement.removeEventListener('render', renderHandler);
@@ -276,7 +276,7 @@
       assert.equal(moveToNumStub.lastCall.args[0], 10);
       assert.equal(moveToNumStub.lastCall.args[1], 'right');
       assert.equal(scrollBehaviorDuringMove, 'keep-visible');
-      assert.equal(cursorElement._scrollBehavior, 'keep-visible');
+      assert.equal(cursorElement._scrollMode, 'keep-visible');
       done();
     }
     diffElement.addEventListener('render', renderHandler);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation.js b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation.js
index 4006d13..c86760a 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation.js
@@ -14,6 +14,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {sanitizeDOMValue} from '@polymer/polymer/lib/utils/settings.js';
 
 // TODO(wyatta): refactor this to be <MARK> rather than <HL>.
 const ANNOTATION_TAG = 'HL';
@@ -84,7 +86,7 @@
     }
 
     const wrapper = document.createElement(tagName);
-    const sanitizer = window.Polymer.sanitizeDOMValue;
+    const sanitizer = sanitizeDOMValue;
     for (const [name, value] of Object.entries(attributes)) {
       wrapper.setAttribute(
           name, sanitizer ?
@@ -149,8 +151,8 @@
     } else {
       hl = document.createElement(ANNOTATION_TAG);
       hl.className = cssClass;
-      Polymer.dom(node.parentElement).replaceChild(hl, node);
-      Polymer.dom(hl).appendChild(node);
+      dom(node.parentElement).replaceChild(hl, node);
+      dom(hl).appendChild(node);
     }
     return hl;
   },
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
index 53b416b..c5dab43 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/shared-styles.js';
 import '../gr-selection-action-box/gr-selection-action-box.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
@@ -27,7 +25,7 @@
 import {GrRangeNormalizer} from './gr-range-normalizer.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrDiffHighlight extends GestureEventListeners(
     LegacyElementMixin(PolymerElement)) {
@@ -340,12 +338,9 @@
     }
     const start = range.start;
     const end = range.end;
-    if (start.side !== end.side ||
+    return !(start.side !== end.side ||
         end.line < start.line ||
-        (start.line === end.line && start.column === end.column)) {
-      return false;
-    }
-    return true;
+        (start.line === end.line && start.column === end.column));
   }
 
   _handleSelection(selection, isMouseUp) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_html.js b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_html.js
index 08b21499..afd4ac5 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_html.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_html.js
@@ -23,7 +23,7 @@
     }
     gr-selection-action-box {
       /**
-         * Needs z-index to apear above wrapped content, since it's inseted
+         * Needs z-index to appear above wrapped content, since it's inserted
          * into DOM before it.
          */
       z-index: 10;
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 2ed69b6..b367265 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
@@ -14,9 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
-import '../../core/gr-reporting/gr-reporting.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
 import '../../shared/gr-comment-thread/gr-comment-thread.js';
 import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
@@ -30,9 +27,10 @@
 import {htmlTemplate} from './gr-diff-host_html.js';
 import {PatchSetBehavior} from '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js';
 import {GrDiffBuilder} from '../gr-diff-builder/gr-diff-builder.js';
-import {util} from '../../../scripts/util.js';
+import {parseDate} from '../../../utils/date-util.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 import {DiffSide, rangesEqual} from '../gr-diff/gr-diff-utils.js';
+import {appContext} from '../../../services/app-context.js';
 
 const MSG_EMPTY_BLAME = 'No blame information for this diff.';
 
@@ -85,7 +83,7 @@
  * Webcomponent fetching diffs and related data from restAPI and passing them
  * to the presentational gr-diff for rendering.
  *
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrDiffHost extends mixinBehaviors( [
   PatchSetBehavior,
@@ -122,6 +120,9 @@
       },
       /** @type {?} */
       patchRange: Object,
+      /** @type {!Gerrit.FileRange} */
+      file: Object,
+      // TODO: deprecate path since that info is included in file
       path: String,
       prefs: {
         type: Object,
@@ -259,6 +260,11 @@
     ];
   }
 
+  constructor() {
+    super();
+    this.reporting = appContext.reportingService;
+  }
+
   /** @override */
   created() {
     super.created();
@@ -360,21 +366,21 @@
               const needsSyntaxHighlighting = event.detail &&
                     event.detail.contentRendered;
               if (needsSyntaxHighlighting) {
-                this.$.reporting.time(TimingLabel.SYNTAX);
+                this.reporting.time(TimingLabel.SYNTAX);
                 this.$.syntaxLayer.process().then(() => {
-                  this.$.reporting.timeEnd(TimingLabel.SYNTAX);
-                  this.$.reporting.timeEnd(TimingLabel.TOTAL);
+                  this.reporting.timeEnd(TimingLabel.SYNTAX);
+                  this.reporting.timeEnd(TimingLabel.TOTAL);
                   resolve();
                 });
               } else {
-                this.$.reporting.timeEnd(TimingLabel.TOTAL);
+                this.reporting.timeEnd(TimingLabel.TOTAL);
                 resolve();
               }
               this.removeEventListener('render', callback);
               if (shouldReportMetric) {
                 // We report diffViewContentDisplayed only on reload caused
                 // by params changed - expected only on Diff Page.
-                this.$.reporting.diffViewContentDisplayed();
+                this.reporting.diffViewContentDisplayed();
               }
             };
             this.addEventListener('render', callback);
@@ -602,11 +608,11 @@
     // Report the due_to_rebase percentage in the "diff" category when
     // applicable.
     if (this.patchRange.basePatchNum === 'PARENT') {
-      this.$.reporting.reportInteraction(EVENT_AGAINST_PARENT);
+      this.reporting.reportInteraction(EVENT_AGAINST_PARENT);
     } else if (percentRebaseDelta === 0) {
-      this.$.reporting.reportInteraction(EVENT_ZERO_REBASE);
+      this.reporting.reportInteraction(EVENT_ZERO_REBASE);
     } else {
-      this.$.reporting.reportInteraction(EVENT_NONZERO_REBASE,
+      this.reporting.reportInteraction(EVENT_NONZERO_REBASE,
           {percentRebaseDelta});
     }
   }
@@ -661,7 +667,7 @@
     return comments.slice(0).sort((a, b) => {
       if (b.__draft && !a.__draft ) { return -1; }
       if (a.__draft && !b.__draft ) { return 1; }
-      return util.parseDate(a.updated) - util.parseDate(b.updated);
+      return parseDate(a.updated) - parseDate(b.updated);
     });
   }
 
@@ -726,7 +732,7 @@
         isOnParent);
     threadEl.addOrEditDraft(lineNum, range);
 
-    this.$.reporting.recordDraftInteraction();
+    this.reporting.recordDraftInteraction();
   }
 
   /**
@@ -775,6 +781,15 @@
     threadEl.commentSide = thread.commentSide;
     threadEl.isOnParent = !!thread.isOnParent;
     threadEl.parentIndex = this._parentIndex;
+    // Use path before renmaing when comment added on the left when comparing
+    // two patch sets (not against base)
+    if (this.file && this.file.basePath
+        && thread.commentSide === GrDiffBuilder.Side.LEFT
+        && !thread.isOnParent) {
+      threadEl.path = this.file.basePath;
+    } else {
+      threadEl.path = this.path;
+    }
     threadEl.changeNum = this.changeNum;
     threadEl.patchNum = thread.patchNum;
     threadEl.lineNum = thread.lineNum;
@@ -782,7 +797,6 @@
       thread.rootId = changeEvent.detail.value;
     };
     threadEl.addEventListener('root-id-changed', rootIdChangedListener);
-    threadEl.path = this.path;
     threadEl.projectName = this.projectName;
     threadEl.range = thread.range;
     const threadDiscardListener = e => {
@@ -941,7 +955,7 @@
   /**
    * Closure annotation for Polymer.prototype.push is off. Submitted PR:
    * https://github.com/Polymer/polymer/pull/4776
-   * but for not supressing annotations.
+   * but for not suppressing annotations.
    *
    * @suppress {checkTypes}
    */
@@ -1024,7 +1038,7 @@
   _listenToViewportRender() {
     const renderUpdateListener = start => {
       if (start > NUM_OF_LINES_THRESHOLD_FOR_VIEWPORT) {
-        this.$.reporting.diffViewDisplayed();
+        this.reporting.diffViewDisplayed();
         this.$.syntaxLayer.removeListener(renderUpdateListener);
       }
     };
@@ -1033,16 +1047,16 @@
   }
 
   _handleRenderStart() {
-    this.$.reporting.time(TimingLabel.TOTAL);
-    this.$.reporting.time(TimingLabel.CONTENT);
+    this.reporting.time(TimingLabel.TOTAL);
+    this.reporting.time(TimingLabel.CONTENT);
   }
 
   _handleRenderContent() {
-    this.$.reporting.timeEnd(TimingLabel.CONTENT);
+    this.reporting.timeEnd(TimingLabel.CONTENT);
   }
 
   _handleNormalizeRange(event) {
-    this.$.reporting.reportInteraction('normalize-range',
+    this.reporting.reportInteraction('normalize-range',
         {
           side: event.detail.side,
           lineNum: event.detail.lineNum,
@@ -1050,7 +1064,7 @@
   }
 
   _handleDiffContextExpanded(event) {
-    this.$.reporting.reportInteraction(
+    this.reporting.reportInteraction(
         'diff-context-expanded', {numLines: event.detail.numLines}
     );
   }
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_html.js b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_html.js
index 4e425dc..aa05bb5 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_html.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_html.js
@@ -53,5 +53,4 @@
   ></gr-syntax-layer>
   <gr-js-api-interface id="jsAPI"></gr-js-api-interface>
   <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
-  <gr-reporting id="reporting" category="diff"></gr-reporting>
 `;
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 0cb2b5e..b8f26b2 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
@@ -50,11 +50,9 @@
     stub('gr-rest-api-interface', {
       async getLoggedIn() { return getLoggedIn; },
     });
-    stub('gr-reporting', {
-      time: sandbox.stub(),
-      timeEnd: sandbox.stub(),
-    });
     element = fixture('basic');
+    sandbox.stub(element.reporting, 'time');
+    sandbox.stub(element.reporting, 'timeEnd');
   });
 
   teardown(() => {
@@ -307,9 +305,9 @@
     test('starts total and content timer on render-start', done => {
       element.dispatchEvent(
           new CustomEvent('render-start', {bubbles: true, composed: true}));
-      assert.isTrue(element.$.reporting.time.calledWithExactly(
+      assert.isTrue(element.reporting.time.calledWithExactly(
           'Diff Total Render'));
-      assert.isTrue(element.$.reporting.time.calledWithExactly(
+      assert.isTrue(element.reporting.time.calledWithExactly(
           'Diff Content Render'));
       done();
     });
@@ -317,11 +315,12 @@
     test('ends content timer on render-content', () => {
       element.dispatchEvent(
           new CustomEvent('render-content', {bubbles: true, composed: true}));
-      assert.isTrue(element.$.reporting.timeEnd.calledWithExactly(
+      assert.isTrue(element.reporting.timeEnd.calledWithExactly(
           'Diff Content Render'));
     });
 
     test('ends total and syntax timer after syntax layer processing', done => {
+      sandbox.stub(element.reporting, 'diffViewContentDisplayed');
       let notifySyntaxProcessed;
       sandbox.stub(element.$.syntaxLayer, 'process').returns(new Promise(
           resolve => {
@@ -339,12 +338,11 @@
         notifySyntaxProcessed();
         // Assert after the notification task is processed.
         Promise.resolve().then(() => {
-          assert.isTrue(element.$.reporting.timeEnd.calledWithExactly(
+          assert.isTrue(element.reporting.timeEnd.calledWithExactly(
               'Diff Total Render'));
-          assert.isTrue(element.$.reporting.timeEnd.calledWithExactly(
+          assert.isTrue(element.reporting.timeEnd.calledWithExactly(
               'Diff Syntax Render'));
-          assert.isTrue(element.$.reporting.timeEnd.calledWithExactly(
-              'StartupDiffViewOnlyContent'));
+          assert.isTrue(element.reporting.diffViewContentDisplayed.called);
           done();
         });
       });
@@ -357,8 +355,8 @@
       element.reload();
       // Multiple cascading microtasks are scheduled.
       setTimeout(() => {
-        assert.isTrue(element.$.reporting.timeEnd.calledOnce);
-        assert.isTrue(element.$.reporting.timeEnd.calledWithExactly(
+        assert.isTrue(element.reporting.timeEnd.calledOnce);
+        assert.isTrue(element.reporting.timeEnd.calledWithExactly(
             'Diff Total Render'));
         done();
       });
@@ -1026,8 +1024,9 @@
 
     setup(() => {
       element = fixture('basic');
+      element.path = 'file.txt';
       element.patchRange = {basePatchNum: 1};
-      reportStub = sandbox.stub(element.$.reporting, 'reportInteraction');
+      reportStub = sandbox.stub(element.reporting, 'reportInteraction');
     });
 
     test('null and content-less', () => {
@@ -1330,6 +1329,54 @@
     assert.equal(threads[1].patchNum, 3);
   });
 
+  test('thread should use old file path if first created' +
+   'on patch set (left) before renaming', () => {
+    const commentSide = 'left';
+    element.file = {basePath: 'file_renamed.txt', path: element.path};
+
+    assert.isOk(element._getOrCreateThread('2', 3,
+        commentSide, undefined, /* isOnParent= */ false));
+
+    const threads = dom(element.$.diff)
+        .queryDistributedElements('gr-comment-thread');
+
+    assert.equal(threads.length, 1);
+    assert.equal(threads[0].commentSide, commentSide);
+    assert.equal(threads[0].path, element.file.basePath);
+  });
+
+  test('thread should use new file path if first created' +
+   'on patch set (right) after renaming', () => {
+    const commentSide = 'right';
+    element.file = {basePath: 'file_renamed.txt', path: element.path};
+
+    assert.isOk(element._getOrCreateThread('2', 3,
+        commentSide, undefined, /* isOnParent= */ false));
+
+    const threads = dom(element.$.diff)
+        .queryDistributedElements('gr-comment-thread');
+
+    assert.equal(threads.length, 1);
+    assert.equal(threads[0].commentSide, commentSide);
+    assert.equal(threads[0].path, element.file.path);
+  });
+
+  test('thread should use new file path if first created' +
+   'on patch set (left) but is base', () => {
+    const commentSide = 'left';
+    element.file = {basePath: 'file_renamed.txt', path: element.path};
+
+    assert.isOk(element._getOrCreateThread('2', 3,
+        commentSide, undefined, /* isOnParent= */ true));
+
+    const threads = dom(element.$.diff)
+        .queryDistributedElements('gr-comment-thread');
+
+    assert.equal(threads.length, 1);
+    assert.equal(threads[0].commentSide, commentSide);
+    assert.equal(threads[0].path, element.file.path);
+  });
+
   test('_filterThreadElsForLocation with no threads', () => {
     const line = {beforeNumber: 3, afterNumber: 5};
 
@@ -1443,7 +1490,7 @@
     });
   });
 
-  suite('syntax layer with syntax_highlgihting off', () => {
+  suite('syntax layer with syntax_highlighting off', () => {
     setup(() => {
       const prefs = {
         line_length: 10,
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.js b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.js
index acd9457..7c9d1ec 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-icon/iron-icon.js';
 import '../../../styles/shared-styles.js';
 import '../../shared/gr-button/gr-button.js';
@@ -25,7 +23,7 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-diff-mode-selector_html.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrDiffModeSelector extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.js b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.js
index 8f48507..b68c889 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/shared-styles.js';
 import '../../shared/gr-button/gr-button.js';
 import '../../shared/gr-diff-preferences/gr-diff-preferences.js';
@@ -26,7 +24,7 @@
 import {htmlTemplate} from './gr-diff-preferences-dialog_html.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrDiffPreferencesDialog extends GestureEventListeners(
     LegacyElementMixin(PolymerElement)) {
@@ -36,7 +34,7 @@
 
   static get properties() {
     return {
-    /** @type {?} */
+      /** @type {?} */
       diffPrefs: Object,
 
       /**
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_test.js b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_test.js
new file mode 100644
index 0000000..07cca9a
--- /dev/null
+++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_test.js
@@ -0,0 +1,50 @@
+/**
+ * @license
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import '../../../test/common-test-setup-karma.js';
+
+const basicFixture = fixtureFromElement('gr-diff-preferences-dialog');
+
+suite('gr-diff-preferences-dialog', () => {
+  let element;
+  setup(() => {
+    element = basicFixture.instantiate();
+  });
+  test('changes applies only on save', async () => {
+    const originalDiffPrefs = {
+      line_wrapping: true,
+    };
+    element.diffPrefs = originalDiffPrefs;
+
+    element.open();
+    await flush();
+    assert.isTrue(element.$.diffPreferences.$.lineWrappingInput.checked);
+
+    MockInteractions.tap(element.$.diffPreferences.$.lineWrappingInput);
+    await flush();
+    assert.isFalse(element.$.diffPreferences.$.lineWrappingInput.checked);
+    assert.isTrue(element._diffPrefsChanged);
+    assert.isTrue(element.diffPrefs.line_wrapping);
+    assert.isTrue(originalDiffPrefs.line_wrapping);
+
+    MockInteractions.tap(element.$.saveButton);
+    await flush();
+    // Original prefs must remains unchanged, dialog must expose a new object
+    assert.isTrue(originalDiffPrefs.line_wrapping);
+    assert.isFalse(element.diffPrefs.line_wrapping);
+  });
+});
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js
index 62ddfee..b9c97a9 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
@@ -70,7 +68,7 @@
  *    that the part that is within the context or has comments is shown, while
  *    the rest is not.
  *
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrDiffProcessor extends GestureEventListeners(
     LegacyElementMixin(
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html
index 50bfe107..aafd374 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html
@@ -105,7 +105,6 @@
         group = groups[1];
         assert.equal(group.type, GrDiffGroup.Type.BOTH);
         assert.equal(group.lines.length, 2);
-        assert.equal(group.lines.length, 2);
 
         function beforeNumberFn(l) { return l.beforeNumber; }
         function afterNumberFn(l) { return l.afterNumber; }
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
index 08967e8..608e898 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/shared-styles.js';
 import {addListener} from '@polymer/polymer/lib/utils/gestures.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
@@ -42,7 +40,7 @@
 const getNewCache = () => { return {left: null, right: null}; };
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrDiffSelection extends mixinBehaviors( [
   DomUtilBehavior,
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
index e434e65..415ce1e 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
@@ -14,12 +14,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-dropdown/iron-dropdown.js';
 import '@polymer/iron-input/iron-input.js';
 import '../../../styles/shared-styles.js';
-import '../../core/gr-reporting/gr-reporting.js';
 import '../../shared/gr-button/gr-button.js';
 import '../../shared/gr-dropdown/gr-dropdown.js';
 import '../../shared/gr-dropdown-list/gr-dropdown-list.js';
@@ -48,6 +45,7 @@
 import {GrCountStringFormatter} from '../../shared/gr-count-string-formatter/gr-count-string-formatter.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 import {RevisionInfo} from '../../shared/revision-info/revision-info.js';
+import {appContext} from '../../../services/app-context.js';
 
 const ERR_REVIEW_STATUS = 'Couldn’t change file review status.';
 const MSG_LOADING_BLAME = 'Loading blame...';
@@ -67,7 +65,7 @@
 
 /**
  * @appliesMixin PatchSetMixin
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrDiffView extends mixinBehaviors( [
   KeyboardShortcutBehavior,
@@ -164,6 +162,12 @@
         value() { return {sortedFileList: [], changeFilesByPath: {}}; },
       },
 
+      /** @type {Gerrit.FileRange} */
+      _file: {
+        type: Object,
+        computed: '_getCurrentFile(_files, _path)',
+      },
+
       _path: {
         type: String,
         observer: '_pathChanged',
@@ -237,9 +241,9 @@
        * gr-diff-view has gr-fixed-panel on top. The panel can
        * intersect a main element and partially hides a content of
        * the main element. To correctly calculates visibility of an
-       * element, the cursor must know how much height occuped by a fixed
+       * element, the cursor must know how much height occupied by a fixed
        * panel.
-       * The scrollTopMargin defines margin occuped by fixed panel.
+       * The scrollTopMargin defines margin occupied by fixed panel.
        */
       _scrollTopMargin: {
         type: Number,
@@ -301,6 +305,11 @@
     };
   }
 
+  constructor() {
+    super();
+    this.reporting = appContext.reportingService;
+  }
+
   /** @override */
   attached() {
     super.attached();
@@ -345,6 +354,21 @@
     return files.sortedFileList;
   }
 
+  /**
+   * @param {!Object} files
+   * @param {string} path
+   * @returns {!Gerrit.FileRange}
+   */
+  _getCurrentFile(files, path) {
+    if ([files, path].includes(undefined)) return;
+    const fileInfo = files.changeFilesByPath[path];
+    const fileRange = {path};
+    if (fileInfo && fileInfo.old_path) {
+      fileRange.basePath = fileInfo.old_path;
+    }
+    return fileRange;
+  }
+
   _getFiles(changeNum, patchRangeRecord, changeComments) {
     // Polymer 2: check for undefined
     if ([changeNum, patchRangeRecord, patchRangeRecord.base, changeComments]
@@ -358,10 +382,7 @@
       if (!changeFiles) return;
       const commentedPaths = changeComments.getPaths(patchRange);
       const files = Object.assign({}, changeFiles);
-      Object.keys(commentedPaths).forEach(commentedPath => {
-        if (files.hasOwnProperty(commentedPath)) { return; }
-        files[commentedPath] = {status: 'U'};
-      });
+      this.addUnmodifiedFiles(files, commentedPaths);
       this._files = {
         sortedFileList: Object.keys(files).sort(this.specialFilePathCompare),
         changeFilesByPath: files,
@@ -647,7 +668,10 @@
   _goToEditFile() {
     // TODO(taoalpha): add a shortcut for editing
     const editUrl = GerritNav.getEditUrlForDiff(
-        this._change, this._path, this._patchRange.patchNum);
+        this._change,
+        this._path,
+        this._patchRange.patchNum
+    );
     return GerritNav.navigateToRelativeUrl(editUrl);
   }
 
@@ -790,9 +814,14 @@
           return this.$.diffHost.reload(true);
         })
         .then(() => {
-          this.$.reporting.diffViewFullyLoaded();
+          this.reporting.diffViewFullyLoaded();
           // If diff view displayed has not ended yet, it ends here.
-          this.$.reporting.diffViewDisplayed();
+          this.reporting.diffViewDisplayed();
+        })
+        .then(() => {
+          // If the blame was loaded for a previous file and user navigates to
+          // another file, then we load the blame for this file too
+          if (this._isBlameLoaded) this._loadBlame();
         });
   }
 
@@ -1129,7 +1158,7 @@
     const file = files[path];
     if (file && file.old_path) {
       this._commentsForDiff = this._changeComments.getCommentsBySideForFile(
-          {path, oldPath: file.old_path},
+          {path, basePath: file.old_path},
           patchRange,
           projectConfig);
 
@@ -1209,16 +1238,7 @@
     return 'Show blame';
   }
 
-  /**
-   * Load and display blame information if it has not already been loaded.
-   * Otherwise hide it.
-   */
-  _toggleBlame() {
-    if (this._isBlameLoaded) {
-      this.$.diffHost.clearBlame();
-      return;
-    }
-
+  _loadBlame() {
     this._isBlameLoading = true;
     this.dispatchEvent(new CustomEvent('show-alert', {
       detail: {message: MSG_LOADING_BLAME},
@@ -1237,6 +1257,18 @@
         });
   }
 
+  /**
+   * Load and display blame information if it has not already been loaded.
+   * Otherwise hide it.
+   */
+  _toggleBlame() {
+    if (this._isBlameLoaded) {
+      this.$.diffHost.clearBlame();
+      return;
+    }
+    this._loadBlame();
+  }
+
   _handleToggleBlame(e) {
     if (this.shouldSuppressKeyboardShortcut(e) ||
       this.modifierPressed(e)) { return; }
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.js
index c74a192..e735153 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.js
@@ -391,6 +391,7 @@
     change-num="[[_changeNum]]"
     commit-range="[[_commitRange]]"
     patch-range="[[_patchRange]]"
+    file="[[_file]]"
     path="[[_path]]"
     prefs="[[_prefs]]"
     project-name="[[_change.project]]"
@@ -420,5 +421,4 @@
     scroll-top-margin="[[_scrollTopMargin]]"
   ></gr-diff-cursor>
   <gr-comment-api id="commentAPI"></gr-comment-api>
-  <gr-reporting id="reporting"></gr-reporting>
 `;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
index f5275e2..98371dc 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
@@ -44,6 +44,7 @@
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {KeyboardShortcutBinder} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
+import {ChangeStatus} from '../../../constants/constants.js';
 
 suite('gr-diff-view tests', () => {
   suite('basic tests', () => {
@@ -134,7 +135,7 @@
     });
 
     test('params change triggers diffViewDisplayed()', () => {
-      sandbox.stub(element.$.reporting, 'diffViewDisplayed');
+      sandbox.stub(element.reporting, 'diffViewDisplayed');
       sandbox.stub(element.$.diffHost, 'reload').returns(Promise.resolve());
       sandbox.spy(element, '_paramsChanged');
       element.params = {
@@ -146,7 +147,28 @@
       };
 
       return element._paramsChanged.returnValues[0].then(() => {
-        assert.isTrue(element.$.reporting.diffViewDisplayed.calledOnce);
+        assert.isTrue(element.reporting.diffViewDisplayed.calledOnce);
+      });
+    });
+
+    test('params change cases blame to load if it was set to true', () => {
+      // Blame loads for subsequent files if it was loaded for one file
+      element._isBlameLoaded = true;
+      sandbox.stub(element.reporting, 'diffViewDisplayed');
+      sandbox.stub(element, '_loadBlame');
+      sandbox.stub(element.$.diffHost, 'reload').returns(Promise.resolve());
+      sandbox.spy(element, '_paramsChanged');
+      element.params = {
+        view: GerritNav.View.DIFF,
+        changeNum: '42',
+        patchNum: '2',
+        basePatchNum: '1',
+        path: '/COMMIT_MSG',
+      };
+
+      return element._paramsChanged.returnValues[0].then(() => {
+        assert.isTrue(element._isBlameLoaded);
+        assert.isTrue(element._loadBlame.calledOnce);
       });
     });
 
@@ -403,7 +425,8 @@
       };
       element._change = {
         _number: 42,
-        status: 'NEW',
+        project: 'gerrit',
+        status: ChangeStatus.NEW,
         revisions: {
           a: {_number: 1, commit: {parents: []}},
           b: {_number: 2, commit: {parents: []}},
@@ -416,6 +439,12 @@
         assert.isTrue(!!editBtn);
         MockInteractions.tap(editBtn);
         assert.isTrue(redirectStub.called);
+        assert.isTrue(redirectStub.lastCall.calledWithExactly(
+            GerritNav.getEditUrlForDiff(
+                element._change,
+                element._path,
+                element._patchRange.patchNum
+            )));
         done();
       });
     });
@@ -445,14 +474,14 @@
     }
 
     test('edit visible only when logged and status NEW', async () => {
-      for (const changeStatus in element.ChangeStatus) {
-        if (!element.ChangeStatus.hasOwnProperty(changeStatus)) {
+      for (const changeStatus in ChangeStatus) {
+        if (!ChangeStatus.hasOwnProperty(changeStatus)) {
           continue;
         }
         assert.isFalse(await isEditVisibile({loggedIn: false, changeStatus}),
             `loggedIn: false, changeStatus: ${changeStatus}`);
 
-        if (changeStatus !== element.ChangeStatus.NEW) {
+        if (changeStatus !== ChangeStatus.NEW) {
           assert.isFalse(await isEditVisibile({loggedIn: true, changeStatus}),
               `loggedIn: true, changeStatus: ${changeStatus}`);
         } else {
@@ -464,17 +493,17 @@
 
     test('edit visible when logged and status NEW', async () => {
       assert.isTrue(await isEditVisibile(
-          {loggedIn: true, changeStatus: element.ChangeStatus.NEW}));
+          {loggedIn: true, changeStatus: ChangeStatus.NEW}));
     });
 
     test('edit hidden when logged and status ABANDONED', async () => {
       assert.isFalse(await isEditVisibile(
-          {loggedIn: true, changeStatus: element.ChangeStatus.ABANDONED}));
+          {loggedIn: true, changeStatus: ChangeStatus.ABANDONED}));
     });
 
     test('edit hidden when logged and status MERGED', async () => {
       assert.isFalse(await isEditVisibile(
-          {loggedIn: true, changeStatus: element.ChangeStatus.MERGED}));
+          {loggedIn: true, changeStatus: ChangeStatus.MERGED}));
     });
 
     suite('diff prefs hidden', () => {
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 1bb3842..bfe7325 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/shared-styles.js';
 import '../../shared/gr-button/gr-button.js';
 import '../gr-diff-builder/gr-diff-builder-element.js';
@@ -58,7 +56,7 @@
 
 const COMMIT_MSG_PATH = '/COMMIT_MSG';
 /**
- * 72 is the inofficial length standard for git commit messages.
+ * 72 is the unofficial length standard for git commit messages.
  * Derived from the fact that git log/show appends 4 ws in the beginning of
  * each line when displaying commit messages. To center the commit message
  * in an 80 char terminal a 4 ws border is added to the rightmost side:
@@ -69,7 +67,7 @@
 const RENDER_DIFF_TABLE_DEBOUNCE_NAME = 'renderDiffTable';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrDiff extends mixinBehaviors( [
   PatchSetBehavior,
@@ -670,13 +668,10 @@
 
   /** @return {boolean} */
   _getIsParentCommentByLineAndContent(lineEl, contentEl) {
-    if ((lineEl.classList.contains(DiffSide.LEFT) ||
+    return (lineEl.classList.contains(DiffSide.LEFT) ||
         contentEl.classList.contains('remove')) &&
         (this.patchRange.basePatchNum === 'PARENT' ||
-        this.isMergeParent(this.patchRange.basePatchNum))) {
-      return true;
-    }
-    return false;
+            this.isMergeParent(this.patchRange.basePatchNum));
   }
 
   /** @return {string} */
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.js
similarity index 93%
rename from polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
rename to polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js
index bb0366b..d497b9a 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js
@@ -1,46 +1,38 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2015 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
 
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-diff</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-<script src="/components/web-component-tester/data/a11ySuite.js"></script>
-
-<test-fixture id="basic">
-  <template>
-    <gr-diff></gr-diff>
-  </template>
-</test-fixture>
-
-<script type="module">
-import '../../../test/common-test-setup.js';
+import '../../../test/common-test-setup-karma.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
-import {getMockDiffResponse} from '../../../test/mock-diff-response.js';
+import {getMockDiffResponse} from '../../../test/mocks/diff-response.js';
 import './gr-diff.js';
 import {dom, flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {GrDiffBuilderImage} from '../gr-diff-builder/gr-diff-builder-image.js';
 import {util} from '../../../scripts/util.js';
 import {_setHiddenScroll} from '../../../scripts/hiddenscroll.js';
+import {runA11yAudit} from '../../../test/a11y-test-utils.js';
+import '@polymer/paper-button/paper-button.js';
+
+const basicFixture = fixtureFromElement('gr-diff');
+
+suite('gr-diff a11y test', () => {
+  test('audit', async () => {
+    await runA11yAudit(basicFixture);
+  });
+});
 
 suite('gr-diff tests', () => {
   let element;
@@ -62,7 +54,7 @@
     };
 
     setup(() => {
-      element = fixture('basic');
+      element = basicFixture.instantiate();
       sandbox.stub(element.$.highlights, 'handleSelectionChange');
     });
 
@@ -80,21 +72,21 @@
   });
 
   test('cancel', () => {
-    element = fixture('basic');
+    element = basicFixture.instantiate();
     const cancelStub = sandbox.stub(element.$.diffBuilder, 'cancel');
     element.cancel();
     assert.isTrue(cancelStub.calledOnce);
   });
 
   test('line limit with line_wrapping', () => {
-    element = fixture('basic');
+    element = basicFixture.instantiate();
     element.prefs = Object.assign({}, MINIMAL_PREFS, {line_wrapping: true});
     flushAsynchronousOperations();
     assert.equal(util.getComputedStyleValue('--line-limit', element), '80ch');
   });
 
   test('line limit without line_wrapping', () => {
-    element = fixture('basic');
+    element = basicFixture.instantiate();
     element.prefs = Object.assign({}, MINIMAL_PREFS, {line_wrapping: false});
     flushAsynchronousOperations();
     assert.isNotOk(util.getComputedStyleValue('--line-limit', element));
@@ -105,7 +97,7 @@
     let contentEl;
 
     setup(() => {
-      element = fixture('basic');
+      element = basicFixture.instantiate();
       lineEl = document.createElement('td');
       contentEl = document.createElement('span');
     });
@@ -191,7 +183,7 @@
       stub('gr-rest-api-interface', {
         getLoggedIn() { return getLoggedInPromise; },
       });
-      element = fixture('basic');
+      element = basicFixture.instantiate();
       return getLoggedInPromise;
     });
 
@@ -645,7 +637,7 @@
   suite('logged in', () => {
     let fakeLineEl;
     setup(() => {
-      element = fixture('basic');
+      element = basicFixture.instantiate();
       element.loggedIn = true;
       element.patchRange = {};
 
@@ -740,7 +732,7 @@
 
   suite('diff header', () => {
     setup(() => {
-      element = fixture('basic');
+      element = basicFixture.instantiate();
       element.diff = {
         meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
         meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg',
@@ -785,7 +777,7 @@
     let renderStub;
 
     setup(() => {
-      element = fixture('basic');
+      element = basicFixture.instantiate();
       renderStub = sandbox.stub(element.$.diffBuilder, 'render',
           () => {
             element.$.diffBuilder.dispatchEvent(
@@ -837,7 +829,7 @@
 
   suite('blame', () => {
     setup(() => {
-      element = fixture('basic');
+      element = basicFixture.instantiate();
     });
 
     test('unsetting', () => {
@@ -850,8 +842,7 @@
     });
 
     test('setting', () => {
-      const mockBlame = [{id: 'commit id', ranges: [{start: 1, end: 2}]}];
-      element.blame = mockBlame;
+      element.blame = [{id: 'commit id', ranges: [{start: 1, end: 2}]}];
       assert.isTrue(element.classList.contains('showBlame'));
     });
   });
@@ -864,7 +855,7 @@
       element.shadowRoot.querySelector('.newlineWarning').textContent;
 
     setup(() => {
-      element = fixture('basic');
+      element = basicFixture.instantiate();
       element.showNewlineWarningLeft = false;
       element.showNewlineWarningRight = false;
     });
@@ -920,7 +911,7 @@
     });
 
     test('_prefsEqual', () => {
-      element = fixture('basic');
+      element = basicFixture.instantiate();
       assert.isTrue(element._prefsEqual(null, null));
       assert.isTrue(element._prefsEqual({}, {}));
       assert.isTrue(element._prefsEqual({x: 1}, {x: 1}));
@@ -942,7 +933,7 @@
     let renderStub;
 
     setup(() => {
-      element = fixture('basic');
+      element = basicFixture.instantiate();
       element.prefs = {};
       renderStub = sandbox.stub(element.$.diffBuilder, 'render')
           .returns(new Promise(() => {}));
@@ -991,7 +982,7 @@
   });
   const setupSampleDiff = function(params) {
     const {ignore_whitespace, content} = params;
-    element = fixture('basic');
+    element = basicFixture.instantiate();
     element.prefs = {
       ignore_whitespace: ignore_whitespace || 'IGNORE_ALL',
       auto_hide_diff_table_header: true,
@@ -1038,9 +1029,8 @@
     }
     setupSampleDiff({content});
     assertDiffTableWithContent();
-    const diffCopy = Object.assign({}, element.diff);
-    element.diff = diffCopy;
-    // immediatelly cleaned up
+    element.diff = Object.assign({}, element.diff);
+    // immediately cleaned up
     assert.equal(element.$.diffTable.innerHTML, '');
     element._renderDiffTable();
     flushAsynchronousOperations();
@@ -1151,7 +1141,7 @@
   });
 
   test('`render` event has contentRendered field in detail', done => {
-    element = fixture('basic');
+    element = basicFixture.instantiate();
     element.prefs = {};
     sandbox.stub(element.$.diffBuilder, 'render')
         .returns(Promise.resolve());
@@ -1161,7 +1151,21 @@
     });
     element._renderDiffTable();
   });
-});
 
-a11ySuite('basic');
-</script>
+  test('_prefsEqual', () => {
+    element = basicFixture.instantiate();
+    assert.isTrue(element._prefsEqual(null, null));
+    assert.isTrue(element._prefsEqual({}, {}));
+    assert.isTrue(element._prefsEqual({x: 1}, {x: 1}));
+    assert.isTrue(element._prefsEqual({x: 1, abc: 'def'}, {x: 1, abc: 'def'}));
+    const somePref = {abc: 'def', p: true};
+    assert.isTrue(element._prefsEqual(somePref, somePref));
+
+    assert.isFalse(element._prefsEqual({}, null));
+    assert.isFalse(element._prefsEqual(null, {}));
+    assert.isFalse(element._prefsEqual({x: 1}, {x: 2}));
+    assert.isFalse(element._prefsEqual({x: 1, y: 'abc'}, {x: 1, y: 'abcd'}));
+    assert.isFalse(element._prefsEqual({x: 1, y: 'abc'}, {x: 1}));
+    assert.isFalse(element._prefsEqual({x: 1}, {x: 1, y: 'abc'}));
+  });
+});
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js
index 8bdc1a8..3bf3697 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/shared-styles.js';
 import '../../shared/gr-dropdown-list/gr-dropdown-list.js';
 import '../../shared/gr-select/gr-select.js';
@@ -38,7 +36,7 @@
  *
  * @property {string} patchNum
  * @property {string} basePatchNum
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrPatchRangeSelect extends mixinBehaviors( [
   PatchSetBehavior,
@@ -215,7 +213,7 @@
    * In addition, if the current basePatchNum is 'PARENT', all patchNums are
    * valid.
    *
-   * If the curent basePatchNum is a parent index, then only patches that have
+   * If the current basePatchNum is a parent index, then only patches that have
    * at least that many parents are valid.
    *
    * @param {number|string} basePatchNum The current selected base patch num.
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
index 63e6fc6..218ca7e 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
@@ -45,7 +45,7 @@
 import '../gr-comment-api/gr-comment-api.js';
 import '../../shared/revision-info/revision-info.js';
 import './gr-patch-range-select.js';
-import '../gr-comment-api/gr-comment-api-mock_test.js';
+import '../../../test/mocks/comment-api.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {RevisionInfo} from '../../shared/revision-info/revision-info.js';
 suite('gr-patch-range-select tests', () => {
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 e6c49be..70aa1e7 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
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import {GrAnnotation} from '../gr-diff-highlight/gr-annotation.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
@@ -24,12 +22,12 @@
 import {GrDiffLine} from '../gr-diff/gr-diff-line.js';
 
 // Polymer 1 adds # before array's key, while Polymer 2 doesn't
-const HOVER_PATH_PATTERN = /^(commentRanges\.\#?\d+)\.hovering$/;
+const HOVER_PATH_PATTERN = /^(commentRanges\.#?\d+)\.hovering$/;
 
 const RANGE_HIGHLIGHT = 'style-scope gr-diff range';
 const HOVER_HIGHLIGHT = 'style-scope gr-diff rangeHighlight';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrRangedCommentLayer extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js
index f16db3b..79f937c 100644
--- a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js
+++ b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/shared-styles.js';
 import '../../shared/gr-tooltip/gr-tooltip.js';
 import {flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
@@ -25,7 +23,7 @@
 import {htmlTemplate} from './gr-selection-action-box_html.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrSelectionActionBox extends GestureEventListeners(
     LegacyElementMixin(PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
index f1e930f..ac5804f 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../shared/gr-lib-loader/gr-lib-loader.js';
 import {GrAnnotation} from '../gr-diff-highlight/gr-annotation.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
@@ -135,12 +133,12 @@
 };
 
 const CPP_DIRECTIVE_WITH_LT_PATTERN = /^\s*#(if|define).*</;
-const CPP_WCHAR_PATTERN = /L\'(\\)?.\'/g;
+const CPP_WCHAR_PATTERN = /L'(\\)?.'/g;
 const JAVA_PARAM_ANNOT_PATTERN = /(@[^\s]+)\(([^)]+)\)/g;
 const GO_BACKSLASH_LITERAL = '\'\\\\\'';
 const GLOBAL_LT_PATTERN = /</g;
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrSyntaxLayer extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html
index ccdbe8b..638b188 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html
@@ -33,7 +33,7 @@
 
 <script type="module">
 import '../../../test/common-test-setup.js';
-import {getMockDiffResponse} from '../../../test/mock-diff-response.js';
+import {getMockDiffResponse} from '../../../test/mocks/diff-response.js';
 import './gr-syntax-layer.js';
 import {GrAnnotation} from '../gr-diff-highlight/gr-annotation.js';
 import {GrDiffLine} from '../gr-diff/gr-diff-line.js';
diff --git a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.js b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.js
index 4d66699..01268b1 100644
--- a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.js
+++ b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/gr-table-styles.js';
 import '../../../styles/shared-styles.js';
 import '../../shared/gr-list-view/gr-list-view.js';
@@ -28,7 +26,7 @@
 import {ListViewBehavior} from '../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrDocumentationSearch extends mixinBehaviors( [
   ListViewBehavior,
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 09f4abf..9acfd72 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
@@ -14,15 +14,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/shared-styles.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-default-editor_html.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrDefaultEditor extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.js b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.js
index 7ca849f..d3a09fe 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.js
+++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-input/iron-input.js';
 import '../../shared/gr-autocomplete/gr-autocomplete.js';
 import '../../shared/gr-button/gr-button.js';
@@ -35,7 +33,7 @@
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrEditControls extends mixinBehaviors( [
   PatchSetBehavior,
@@ -229,7 +227,7 @@
 
   _handleDeleteConfirm(e) {
     // Get the dialog before the api call as the event will change during bubbling
-    // which will make Polymer.dom(e).path an emtpy array in polymer 2
+    // which will make Polymer.dom(e).path an empty array in polymer 2
     const dialog = this._getDialogFromEvent(e);
     this.$.restAPI.deleteFileInChangeEdit(this.change._number, this._path)
         .then(res => {
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 8a24e23..b144bad 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
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../shared/gr-button/gr-button.js';
 import '../../shared/gr-dropdown/gr-dropdown.js';
 import '../../../styles/shared-styles.js';
@@ -25,7 +23,7 @@
 import {htmlTemplate} from './gr-edit-file-controls_html.js';
 import {GrEditConstants} from '../gr-edit-constants.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrEditFileControls extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
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 d2ffa56..886908a 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
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
 import '../../plugins/gr-endpoint-param/gr-endpoint-param.js';
 import '../../shared/gr-button/gr-button.js';
@@ -43,7 +41,7 @@
 const STORAGE_DEBOUNCE_INTERVAL_MS = 100;
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrEditorView extends mixinBehaviors( [
   KeyboardShortcutBehavior,
diff --git a/polygerrit-ui/app/elements/gr-app-element.js b/polygerrit-ui/app/elements/gr-app-element.js
index 53c1e54..9c2c6ed 100644
--- a/polygerrit-ui/app/elements/gr-app-element.js
+++ b/polygerrit-ui/app/elements/gr-app-element.js
@@ -14,7 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../scripts/bundled-polymer.js';
 import '../styles/shared-styles.js';
 import '../styles/themes/app-theme.js';
 import './admin/gr-admin-view/gr-admin-view.js';
@@ -25,7 +24,6 @@
 import './core/gr-error-manager/gr-error-manager.js';
 import './core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js';
 import './core/gr-main-header/gr-main-header.js';
-import './core/gr-reporting/gr-reporting.js';
 import './core/gr-router/gr-router.js';
 import './core/gr-smart-search/gr-smart-search.js';
 import './diff/gr-diff-view/gr-diff-view.js';
@@ -48,9 +46,10 @@
 import {BaseUrlBehavior} from '../behaviors/base-url-behavior/base-url-behavior.js';
 import {KeyboardShortcutBehavior} from '../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
 import {GerritNav} from './core/gr-navigation/gr-navigation.js';
+import {appContext} from '../services/app-context.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrAppElement extends mixinBehaviors( [
   BaseUrlBehavior,
@@ -156,6 +155,11 @@
     };
   }
 
+  constructor() {
+    super();
+    this.reporting = appContext.reportingService;
+  }
+
   /** @override */
   created() {
     super.created();
@@ -176,11 +180,13 @@
   ready() {
     super.ready();
     this._updateLoginUrl();
-    this.$.reporting.appStarted();
+    this.reporting.appStarted();
     this.$.router.start();
 
     this.$.restAPI.getAccount().then(account => {
       this._account = account;
+      const role = account ? 'user' : 'guest';
+      this.reporting.reportLifeCycle(`Started as ${role}`);
     });
     this.$.restAPI.getConfig().then(config => {
       this._serverConfig = config;
@@ -411,7 +417,7 @@
     if (e.ctrlKey) key = 'ctrl+' + key;
     if (e.metaKey) key = 'meta+' + key;
     if (e.altKey) key = 'alt+' + key;
-    this.$.reporting.reportInteraction('shortcut-triggered', {
+    this.reporting.reportInteraction('shortcut-triggered', {
       key,
       from: event.path && event.path[0]
         && event.path[0].nodeName || 'unknown',
@@ -461,7 +467,7 @@
     const baseUrl = this.getBaseUrl();
     if (baseUrl) {
       // Strip the canonical path from the path since needing canonical in
-      // the path is uneeded and breaks the url.
+      // the path is unneeded and breaks the url.
       this._loginUrl = baseUrl + '/login/' + encodeURIComponent(
           '/' + window.location.pathname.substring(baseUrl.length) +
           window.location.search +
@@ -562,7 +568,7 @@
    * that would create a cyclic dependency.
    */
   _handleRpcLog(e) {
-    this.$.reporting.reportRpcTiming(e.detail.anonymizedUrl,
+    this.reporting.reportRpcTiming(e.detail.anonymizedUrl,
         e.detail.elapsed);
   }
 
diff --git a/polygerrit-ui/app/elements/gr-app-element_html.js b/polygerrit-ui/app/elements/gr-app-element_html.js
index 2ee48c1..ad47d52 100644
--- a/polygerrit-ui/app/elements/gr-app-element_html.js
+++ b/polygerrit-ui/app/elements/gr-app-element_html.js
@@ -219,7 +219,6 @@
     login-url="[[_loginUrl]]"
   ></gr-error-manager>
   <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
-  <gr-reporting id="reporting"></gr-reporting>
   <gr-router id="router"></gr-router>
   <gr-plugin-host id="plugins" config="[[_serverConfig]]"> </gr-plugin-host>
   <gr-lib-loader id="libLoader"></gr-lib-loader>
diff --git a/polygerrit-ui/app/elements/gr-app-global-var-init.js b/polygerrit-ui/app/elements/gr-app-global-var-init.js
index f0a9131..2c166f5 100644
--- a/polygerrit-ui/app/elements/gr-app-global-var-init.js
+++ b/polygerrit-ui/app/elements/gr-app-global-var-init.js
@@ -48,7 +48,6 @@
 import {GrCountStringFormatter} from './shared/gr-count-string-formatter/gr-count-string-formatter.js';
 import {GrReviewerSuggestionsProvider, SUGGESTIONS_PROVIDERS_USERS_TYPES} from '../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js';
 import {util} from '../scripts/util.js';
-import moment from 'moment/src/moment.js';
 import page from 'page/page.mjs';
 import {Auth} from './shared/gr-rest-api-interface/gr-auth.js';
 import {EventEmitter} from './shared/gr-event-interface/gr-event-interface.js';
@@ -103,7 +102,6 @@
   window.GrCountStringFormatter = GrCountStringFormatter;
   window.GrReviewerSuggestionsProvider = GrReviewerSuggestionsProvider;
   window.util = util;
-  window.moment = moment;
   window.page = page;
   window.Auth = Auth;
   window.EventEmitter = EventEmitter;
diff --git a/polygerrit-ui/app/elements/gr-app-init.js b/polygerrit-ui/app/elements/gr-app-init.js
index 7caec6d..780e64a 100644
--- a/polygerrit-ui/app/elements/gr-app-init.js
+++ b/polygerrit-ui/app/elements/gr-app-init.js
@@ -15,6 +15,8 @@
  * limitations under the License.
  */
 import {initAppContext} from '../services/app-context-init.js';
+import {initVisibilityReporter, initPerformanceReporter, initErrorReporter} from '../services/gr-reporting/gr-reporting.js';
+import {appContext} from '../services/app-context.js';
 
 if (!window.Polymer) {
   window.Polymer = {
@@ -24,4 +26,7 @@
 }
 window.Gerrit = window.Gerrit || {};
 
-initAppContext();
\ No newline at end of file
+initAppContext();
+initVisibilityReporter(appContext);
+initPerformanceReporter(appContext);
+initErrorReporter(appContext);
diff --git a/polygerrit-ui/app/elements/gr-app.js b/polygerrit-ui/app/elements/gr-app.js
index c9910d3..410539c 100644
--- a/polygerrit-ui/app/elements/gr-app.js
+++ b/polygerrit-ui/app/elements/gr-app.js
@@ -15,13 +15,9 @@
  * limitations under the License.
  */
 
-/*
-  FIXME(polymer-modulizer): the above comments were extracted
-  from HTML and may be out of place here. Review them and
-  then delete this comment!
-*/
 import './gr-app-init.js';
 import './font-roboto-local-loader.js';
+// Sets up global Polymer variable, because plugins requires it.
 import '../scripts/bundled-polymer.js';
 
 /**
@@ -36,7 +32,6 @@
 import 'polymer-resin/standalone/polymer-resin.js';
 import {initGlobalVariables} from './gr-app-global-var-init.js';
 import './gr-app-element.js';
-import './change-list/gr-embed-dashboard/gr-embed-dashboard.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
@@ -50,7 +45,7 @@
   safeTypesBridge: SafeTypes.safeTypesBridge,
 });
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrApp extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/gr-app_test.html b/polygerrit-ui/app/elements/gr-app_test.html
index 6a13789..6322518 100644
--- a/polygerrit-ui/app/elements/gr-app_test.html
+++ b/polygerrit-ui/app/elements/gr-app_test.html
@@ -34,6 +34,7 @@
 <script type="module">
 import '../test/common-test-setup.js';
 import './gr-app.js';
+import {appContext} from '../services/app-context.js';
 import {GerritNav} from './core/gr-navigation/gr-navigation.js';
 
 suite('gr-app tests', () => {
@@ -42,9 +43,7 @@
 
   setup(done => {
     sandbox = sinon.sandbox.create();
-    stub('gr-reporting', {
-      appStarted: sandbox.stub(),
-    });
+    sandbox.stub(appContext.reportingService, 'appStarted');
     stub('gr-account-dropdown', {
       _getTopContent: sinon.stub(),
     });
@@ -80,12 +79,12 @@
   const appElement = () => element.$['app-element'];
 
   test('reporting', () => {
-    assert.isTrue(appElement().$.reporting.appStarted.calledOnce);
+    assert.isTrue(appElement().reporting.appStarted.calledOnce);
   });
 
   test('reporting called before router start', () => {
     const element = appElement();
-    const appStartedStub = element.$.reporting.appStarted;
+    const appStartedStub = element.reporting.appStarted;
     const routerStartStub = element.$.router.start;
     sinon.assert.callOrder(appStartedStub, routerStartStub);
   });
diff --git a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.js b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.js
index d5ebb65..52c4f4e 100644
--- a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.js
+++ b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 const $_documentContainer = document.createElement('template');
 
 $_documentContainer.innerHTML = `<dom-module id="gr-attribute-helper">
diff --git a/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.js b/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.js
index 8be50b1..ea60dc6 100644
--- a/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.js
+++ b/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 const $_documentContainer = document.createElement('template');
 
 $_documentContainer.innerHTML = `<dom-module id="gr-change-metadata-api">
diff --git a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js
index 5ef9dbf..41d5fd5 100644
--- a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js
+++ b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
 const $_documentContainer = document.createElement('template');
 
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js
index 62057a1..9bfb7da 100644
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js
+++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
 import {importHref} from '../../../scripts/import-href.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
@@ -28,7 +26,7 @@
 
 const INIT_PROPERTIES_TIMEOUT_MS = 10000;
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrEndpointDecorator extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.js b/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.js
index 9574391..82402c0 100644
--- a/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.js
+++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.js
@@ -14,13 +14,11 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrEndpointParam extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.js b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.js
index 63d40fc..da85b9e 100644
--- a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.js
+++ b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 const $_documentContainer = document.createElement('template');
 
 $_documentContainer.innerHTML = `<dom-module id="gr-event-helper">
diff --git a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js
index 68b1494..f27053d 100644
--- a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js
+++ b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
 import {importHref} from '../../../scripts/import-href.js';
 import {updateStyles} from '@polymer/polymer/lib/mixins/element-mixin.js';
@@ -26,7 +24,7 @@
 import {pluginEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints.js';
 import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrExternalStyle extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
index d1b2106..1833efa 100644
--- a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
+++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
@@ -14,15 +14,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../shared/gr-js-api-interface/gr-js-api-interface.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrPluginHost extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.js b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.js
index db44cea..eaecd29 100644
--- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.js
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../shared/gr-overlay/gr-overlay.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
@@ -25,7 +23,7 @@
 (function(window) {
   'use strict';
 
-  /** @extends Polymer.Element */
+  /** @extends PolymerElement */
   class GrPluginPopup extends GestureEventListeners(
       LegacyElementMixin(
           PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.js b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.js
index 3363d72..738b276 100644
--- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.js
+++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import './gr-plugin-popup.js';
 import {dom, flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 const $_documentContainer = document.createElement('template');
diff --git a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command.js b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command.js
index f9a2bdf..752570f 100644
--- a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command.js
+++ b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../admin/gr-repo-command/gr-repo-command.js';
 import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
 import {html} from '@polymer/polymer/lib/utils/html-tag.js';
diff --git a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.js b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.js
index 36d822c..445356d 100644
--- a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.js
+++ b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import './gr-plugin-repo-command.js';
 const $_documentContainer = document.createElement('template');
 
diff --git a/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.js b/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.js
index 4fb971f..35396cf 100644
--- a/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.js
+++ b/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../settings/gr-settings-view/gr-settings-item.js';
 import '../../settings/gr-settings-view/gr-settings-menu-item.js';
 const $_documentContainer = document.createElement('template');
diff --git a/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api.js b/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api.js
index 3da60db..8a1b601 100644
--- a/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api.js
+++ b/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api.js
@@ -14,6 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+import {useShadow} from '@polymer/polymer/lib/utils/settings.js';
 
 let styleObjectCount = 0;
 
@@ -34,7 +35,7 @@
  * @return {string} Appropriate class name for the element is returned
  */
 GrStyleObject.prototype.getClassName = function(element) {
-  let rootNode = Polymer.Settings.useShadow
+  let rootNode = useShadow
     ? element.getRootNode() : document.body;
   if (rootNode === document) {
     rootNode = document.head;
diff --git a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.js b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.js
index 411a7c8..ae8b8ab 100644
--- a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.js
+++ b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
 import {html} from '@polymer/polymer/lib/utils/html-tag.js';
 Polymer({
diff --git a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.js b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.js
index c987af3..be427ec 100644
--- a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.js
+++ b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import './gr-custom-plugin-header.js';
 const $_documentContainer = document.createElement('template');
 
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js
index 7e312d3..14b6cfe 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js
@@ -14,7 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
 import '@polymer/iron-input/iron-input.js';
 import '../../shared/gr-avatar/gr-avatar.js';
 import '../../shared/gr-date-formatter/gr-date-formatter.js';
@@ -27,7 +26,7 @@
 import {htmlTemplate} from './gr-account-info_html.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrAccountInfo extends GestureEventListeners(
     LegacyElementMixin(PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.js b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.js
index 390baf6..0a84f56c 100644
--- a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.js
+++ b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.js
@@ -15,7 +15,6 @@
  * limitations under the License.
  */
 
-import '../../../scripts/bundled-polymer.js';
 import '../../../styles/gr-form-styles.js';
 import '../../../styles/shared-styles.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
@@ -27,7 +26,7 @@
 import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrAgreementsList extends mixinBehaviors( [
   BaseUrlBehavior,
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.js b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.js
index 61e8e93..2051947 100644
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.js
+++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.js
@@ -14,7 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
 import '../../shared/gr-button/gr-button.js';
 import '../../shared/gr-date-formatter/gr-date-formatter.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
@@ -29,7 +28,7 @@
 import {ChangeTableBehavior} from '../../../behaviors/gr-change-table-behavior/gr-change-table-behavior.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrChangeTableEditor extends mixinBehaviors( [
   ChangeTableBehavior,
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html
index 79d1390..27532b4 100644
--- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html
@@ -125,7 +125,7 @@
         columns.filter(c => c !== 'Assignee'));
   });
 
-  test('_handleCheckboxContainerClick relayes taps to checkboxes', () => {
+  test('_handleCheckboxContainerClick relays taps to checkboxes', () => {
     sandbox.stub(element, '_handleNumberCheckboxClick');
     sandbox.stub(element, '_handleTargetClick');
 
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 957eb48..8359f96 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
@@ -16,7 +16,6 @@
  */
 
 import '@polymer/iron-input/iron-input.js';
-import '../../../scripts/bundled-polymer.js';
 import '../../../styles/gr-form-styles.js';
 import '../../../styles/shared-styles.js';
 import '../../shared/gr-button/gr-button.js';
@@ -29,7 +28,7 @@
 import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrClaView extends mixinBehaviors( [
   BaseUrlBehavior,
@@ -112,7 +111,7 @@
     return this.$.restAPI.saveAccountAgreement({name}).then(res => {
       let message = 'Agreement failed to be submitted, please try again';
       if (res.status === 200) {
-        message = 'Agreement has been successfully submited.';
+        message = 'Agreement has been successfully submitted.';
       }
       this._createToast(message);
       this.loadData();
diff --git a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.js b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.js
index 2a7ac06..6973292 100644
--- a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.js
+++ b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-input/iron-input.js';
 import '../../../styles/gr-form-styles.js';
 import '../../../styles/shared-styles.js';
@@ -26,7 +24,7 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-edit-preferences_html.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrEditPreferences extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.js b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.js
index fc97079..0cc5c2c 100644
--- a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.js
+++ b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-input/iron-input.js';
 import '../../shared/gr-button/gr-button.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
@@ -26,7 +24,7 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-email-editor_html.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrEmailEditor extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.js b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.js
index 90631c7..6c6ad01 100644
--- a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.js
+++ b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
 import '../../../styles/gr-form-styles.js';
 import '../../shared/gr-button/gr-button.js';
@@ -29,7 +27,7 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-gpg-editor_html.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrGpgEditor extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.js b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.js
index 1cc1369..429a7c7 100644
--- a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.js
+++ b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/shared-styles.js';
 import '../../../styles/gr-form-styles.js';
 import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
@@ -25,7 +23,7 @@
 import {htmlTemplate} from './gr-group-list_html.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrGroupList extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.js b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.js
index 02657f8..164bdee 100644
--- a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.js
+++ b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/gr-form-styles.js';
 import '../../shared/gr-button/gr-button.js';
 import '../../shared/gr-copy-clipboard/gr-copy-clipboard.js';
@@ -27,7 +25,7 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-http-password_html.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrHttpPassword extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.js b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.js
index 74c5eed..d0d30ea 100644
--- a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.js
+++ b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/shared-styles.js';
 import '../../../styles/gr-form-styles.js';
 import '../../admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.js';
@@ -35,7 +33,7 @@
 ];
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrIdentities extends mixinBehaviors( [
   BaseUrlBehavior,
diff --git a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.js b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.js
index 42982fd..b68915a 100644
--- a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.js
+++ b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-input/iron-input.js';
 import '../../shared/gr-button/gr-button.js';
 import '../../shared/gr-date-formatter/gr-date-formatter.js';
@@ -28,7 +26,7 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-menu-editor_html.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrMenuEditor extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.js b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.js
index 6635de2..1da513b 100644
--- a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.js
+++ b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-input/iron-input.js';
 import '../../../styles/gr-form-styles.js';
 import '../../shared/gr-button/gr-button.js';
@@ -27,7 +25,7 @@
 import {htmlTemplate} from './gr-registration-dialog_html.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrRegistrationDialog extends GestureEventListeners(
     LegacyElementMixin(PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.js
index 3884a15..2455cec 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.js
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.js
@@ -14,13 +14,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-settings-item_html.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrSettingsItem extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.js
index 5b11516..4d839f8 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.js
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.js
@@ -14,15 +14,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/gr-page-nav-styles.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-settings-menu-item_html.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrSettingsMenuItem extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
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 c2f97ca..95f7a2c 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
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-input/iron-input.js';
 import '@polymer/paper-toggle-button/paper-toggle-button.js';
 import '../../../styles/gr-form-styles.js';
@@ -78,7 +76,7 @@
 ];
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrSettingsView extends mixinBehaviors( [
   DocsUrlBehavior,
diff --git a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.js b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.js
index 814eb7a..d869128 100644
--- a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.js
+++ b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
 import '../../../styles/gr-form-styles.js';
 import '../../shared/gr-button/gr-button.js';
@@ -29,7 +27,7 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-ssh-editor_html.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrSshEditor extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.js b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.js
index b8960e8..2af1bc7 100644
--- a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.js
+++ b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-input/iron-input.js';
 import '../../shared/gr-autocomplete/gr-autocomplete.js';
 import '../../shared/gr-button/gr-button.js';
@@ -36,7 +34,7 @@
   {name: 'Abandons', key: 'notify_abandoned_changes'},
 ];
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrWatchedProjectsEditor extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js
index 6ceee26..c74a396 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../gr-account-link/gr-account-link.js';
 import '../gr-button/gr-button.js';
 import '../gr-icons/gr-icons.js';
@@ -27,7 +25,7 @@
 import {htmlTemplate} from './gr-account-chip_html.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrAccountChip extends GestureEventListeners(
     LegacyElementMixin(PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.js b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.js
index c991a37..807aa81 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/shared-styles.js';
 import '../gr-autocomplete/gr-autocomplete.js';
 import '../gr-rest-api-interface/gr-rest-api-interface.js';
@@ -28,7 +26,7 @@
  * gr-account-entry is an element for entering account
  * and/or group with autocomplete support.
  *
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrAccountEntry extends GestureEventListeners(
     LegacyElementMixin(
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js
index 110d884..8c2b7da 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-icon/iron-icon.js';
 import '../../../styles/shared-styles.js';
 import '../gr-avatar/gr-avatar.js';
@@ -29,7 +27,7 @@
 import {DisplayNameBehavior} from '../../../behaviors/gr-display-name-behavior/gr-display-name-behavior.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrAccountLabel extends mixinBehaviors( [
   DisplayNameBehavior,
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js
index 27de4b3..d7dd88d 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js
@@ -15,7 +15,6 @@
  * limitations under the License.
  */
 
-import '../../../scripts/bundled-polymer.js';
 import '../gr-account-label/gr-account-label.js';
 import '../../../styles/shared-styles.js';
 import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
@@ -27,7 +26,7 @@
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrAccountLink extends mixinBehaviors( [
   BaseUrlBehavior,
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.js b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.js
index 73ccf7d..abe5206 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../gr-account-chip/gr-account-chip.js';
 import '../gr-account-entry/gr-account-entry.js';
 import '../../../styles/shared-styles.js';
@@ -24,11 +22,12 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-account-list_html.js';
+import {appContext} from '../../../services/app-context.js';
 
 const VALID_EMAIL_ALERT = 'Please input a valid email.';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrAccountList extends GestureEventListeners(
     LegacyElementMixin(PolymerElement)) {
@@ -119,6 +118,11 @@
     };
   }
 
+  constructor() {
+    super();
+    this.reporting = appContext.reportingService;
+  }
+
   /** @override */
   created() {
     super.created();
@@ -161,10 +165,12 @@
     // Append new account or group to the accounts property. We add our own
     // internal properties to the account/group here, so we clone the object
     // to avoid cluttering up the shared change object.
+    let itemTypeAdded = 'unknown';
     if (item.account) {
       const account =
           Object.assign({}, item.account, {_pendingAdd: true});
       this.push('accounts', account);
+      itemTypeAdded = 'account';
     } else if (item.group) {
       if (item.confirm) {
         this.pendingConfirmation = item;
@@ -173,6 +179,7 @@
       const group = Object.assign({}, item.group,
           {_pendingAdd: true, _group: true});
       this.push('accounts', group);
+      itemTypeAdded = 'group';
     } else if (this.allowAnyInput) {
       if (!item.includes('@')) {
         // Repopulate the input with what the user tried to enter and have
@@ -187,8 +194,11 @@
       } else {
         const account = {email: item, _pendingAdd: true};
         this.push('accounts', account);
+        itemTypeAdded = 'email';
       }
     }
+
+    this.reporting.reportInteraction(`Add to ${this.id}`, {itemTypeAdded});
     this.pendingConfirmation = null;
     return true;
   }
@@ -256,6 +266,7 @@
       }
       if (matches) {
         this.splice('accounts', i, 1);
+        this.reporting.reportInteraction(`Remove from ${this.id}`);
         return;
       }
     }
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.html b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.html
index b3b32606..2efc6e8 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.html
@@ -491,7 +491,7 @@
       sandbox.stub(input, '_updateSuggestions');
       sandbox.stub(element, '_computeRemovable').returns(true);
       flush(() => {
-        // Next line is a workaround for Firefix not moving cursor
+        // Next line is a workaround for Firefox not moving cursor
         // on input field update
         assert.equal(
             element._getNativeInput(input.$.input).selectionStart, 0);
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js
index 1ec453e..d3a6fad 100644
--- a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js
+++ b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../gr-button/gr-button.js';
 import '../../../styles/shared-styles.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
@@ -24,7 +22,7 @@
 import {htmlTemplate} from './gr-alert_html.js';
 import {getRootElement} from '../../../scripts/rootElement.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrAlert extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js
index 46e8829..3cf91fe 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js
@@ -14,7 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
 import '@polymer/iron-dropdown/iron-dropdown.js';
 import {IronFitBehavior} from '@polymer/iron-fit-behavior/iron-fit-behavior.js';
 import '../gr-cursor-manager/gr-cursor-manager.js';
@@ -28,7 +27,7 @@
 import {KeyboardShortcutBehavior} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrAutocompleteDropdown extends mixinBehaviors( [
   KeyboardShortcutBehavior,
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_html.js b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_html.js
index b31af73..fecaa73 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_html.js
@@ -95,7 +95,7 @@
     id="cursor"
     index="{{index}}"
     cursor-target-class="selected"
-    scroll-behavior="never"
+    scroll-mode="never"
     focus-on-move=""
     stops="[[_suggestionEls]]"
   ></gr-cursor-manager>
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
index 7f9ed72..e6a809e 100644
--- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
+++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/paper-input/paper-input.js';
 import '../gr-autocomplete-dropdown/gr-autocomplete-dropdown.js';
 import '../gr-cursor-manager/gr-cursor-manager.js';
@@ -33,7 +31,7 @@
 const DEBOUNCE_WAIT_MS = 200;
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrAutocomplete extends mixinBehaviors( [
   KeyboardShortcutBehavior,
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
index 75181b1..1385f6d 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/shared-styles.js';
 import '../gr-js-api-interface/gr-js-api-interface.js';
 import '../gr-rest-api-interface/gr-rest-api-interface.js';
@@ -28,7 +26,7 @@
 import {pluginLoader} from '../gr-js-api-interface/gr-plugin-loader.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrAvatar extends mixinBehaviors( [
   BaseUrlBehavior,
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.js b/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
index 3169c56..6ef4807 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
@@ -14,11 +14,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/paper-button/paper-button.js';
 import '../../../styles/shared-styles.js';
-import '../../core/gr-reporting/gr-reporting.js';
 import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
@@ -27,9 +24,10 @@
 import {TooltipBehavior} from '../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js';
 import {KeyboardShortcutBehavior} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
 import {util} from '../../../scripts/util.js';
+import {appContext} from '../../../services/app-context.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrButton extends mixinBehaviors( [
   KeyboardShortcutBehavior,
@@ -80,6 +78,11 @@
     };
   }
 
+  constructor() {
+    super();
+    this.reporting = appContext.reportingService;
+  }
+
   /** @override */
   created() {
     super.created();
@@ -104,7 +107,7 @@
       return;
     }
 
-    this.$.reporting.reportInteraction('button-click',
+    this.reporting.reportInteraction('button-click',
         {path: util.getEventPath(e)});
   }
 
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button_html.js b/polygerrit-ui/app/elements/shared/gr-button/gr-button_html.js
index db3b880..e992ffc 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button_html.js
@@ -39,7 +39,7 @@
           Without a copy, the @apply works incorrectly with Polymer 2.
           @apply is deprecated and is not recommended to use. It is expected
           that @apply will be replaced with the ::part CSS pseudo-element.
-          After replacecment copied lines can be removed.
+          After replacement copied lines can be removed.
         */
       @apply --layout-inline;
       @apply --layout-center-center;
@@ -141,7 +141,7 @@
     :host([link]) {
       --background-color: transparent;
       --margin: 0;
-      --padding: 5px 4px;
+      --padding: var(--spacing-s);
     }
     :host([disabled][link]),
     :host([loading][link]) {
@@ -173,5 +173,4 @@
     <slot></slot>
     <i class="downArrow"></i>
   </paper-button>
-  <gr-reporting id="reporting"></gr-reporting>
 `;
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html b/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html
index ae627d1..dfc3d75 100644
--- a/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html
@@ -49,6 +49,8 @@
 import '../../../test/common-test-setup.js';
 import './gr-button.js';
 import {addListener} from '@polymer/polymer/lib/utils/gestures.js';
+import {appContext} from '../../../services/app-context.js';
+
 suite('gr-button tests', () => {
   let element;
   let sandbox;
@@ -190,13 +192,10 @@
   });
 
   suite('reporting', () => {
-    const reportStub = sinon.stub();
+    let reportStub;
     setup(() => {
-      stub('gr-reporting', {
-        reportInteraction: (...args) => {
-          reportStub(...args);
-        },
-      });
+      reportStub = sandbox.stub(appContext.reportingService,
+          'reportInteraction');
       reportStub.reset();
     });
 
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 10e06dd..886f0c1 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
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../gr-icons/gr-icons.js';
 import '../../../styles/shared-styles.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
@@ -23,7 +21,7 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-change-star_html.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrChangeStar extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js
index b99612e..915d171 100644
--- a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js
+++ b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../gr-rest-api-interface/gr-rest-api-interface.js';
 import '../gr-tooltip-content/gr-tooltip-content.js';
 import '../../../styles/shared-styles.js';
@@ -43,7 +41,7 @@
 const PRIVATE_TOOLTIP = 'This change is only visible to its owner and ' +
     'current reviewers (or anyone with "View Private Changes" permission).';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrChangeStatus extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.js b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.js
index ccfc44c..0e2c8a3 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.js
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.js
@@ -14,10 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/shared-styles.js';
-import '../../core/gr-reporting/gr-reporting.js';
 import '../gr-rest-api-interface/gr-rest-api-interface.js';
 import '../gr-storage/gr-storage.js';
 import '../gr-comment/gr-comment.js';
@@ -29,14 +26,16 @@
 import {htmlTemplate} from './gr-comment-thread_html.js';
 import {PathListBehavior} from '../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.js';
 import {KeyboardShortcutBehavior} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
-import {util} from '../../../scripts/util.js';
+import {parseDate} from '../../../utils/date-util.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
+import {appContext} from '../../../services/app-context.js';
+import {SpecialFilePath} from '../../../constants/constants.js';
 
 const UNRESOLVED_EXPAND_COUNT = 5;
 const NEWLINE_PATTERN = /\n/g;
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrCommentThread extends mixinBehaviors( [
   /**
@@ -171,6 +170,11 @@
     };
   }
 
+  constructor() {
+    super();
+    this.reporting = appContext.reportingService;
+  }
+
   /** @override */
   created() {
     super.created();
@@ -223,6 +227,10 @@
         null, this.lineNum);
   }
 
+  _isPatchsetLevelComment(path) {
+    return path === SpecialFilePath.PATCHSET_LEVEL_COMMENTS;
+  }
+
   _computeDisplayPath(path) {
     const lineString = this.lineNum ? `#${this.lineNum}` : '';
     return this.computeDisplayPath(path) + lineString;
@@ -304,8 +312,8 @@
 
   _sortedComments(comments) {
     return comments.slice().sort((c1, c2) => {
-      const c1Date = c1.__date || util.parseDate(c1.updated);
-      const c2Date = c2.__date || util.parseDate(c2.updated);
+      const c1Date = c1.__date || parseDate(c1.updated);
+      const c2Date = c2.__date || parseDate(c2.updated);
       const dateCompare = c1Date - c2Date;
       // Ensure drafts are at the end. There should only be one but in edge
       // cases could be more. In the unlikely event two drafts are being
@@ -318,15 +326,13 @@
     });
   }
 
-  _createReplyComment(parent, content, opt_isEditing,
+  _createReplyComment(content, opt_isEditing,
       opt_unresolved) {
-    this.$.reporting.recordDraftInteraction();
+    this.reporting.recordDraftInteraction();
     const reply = this._newReply(
         this._orderedComments[this._orderedComments.length - 1].id,
-        parent.line,
         content,
-        opt_unresolved,
-        parent.range);
+        opt_unresolved);
 
     // If there is currently a comment in an editing state, add an attribute
     // so that the gr-comment knows not to populate the draft text.
@@ -366,25 +372,23 @@
       const msg = comment.message;
       quoteStr = '> ' + msg.replace(NEWLINE_PATTERN, '\n> ') + '\n\n';
     }
-    this._createReplyComment(comment, quoteStr, true, comment.unresolved);
+    this._createReplyComment(quoteStr, true, comment.unresolved);
   }
 
-  _handleCommentReply(e) {
+  _handleCommentReply() {
     this._processCommentReply();
   }
 
-  _handleCommentQuote(e) {
+  _handleCommentQuote() {
     this._processCommentReply(true);
   }
 
-  _handleCommentAck(e) {
-    const comment = this._lastComment;
-    this._createReplyComment(comment, 'Ack', false, false);
+  _handleCommentAck() {
+    this._createReplyComment('Ack', false, false);
   }
 
-  _handleCommentDone(e) {
-    const comment = this._lastComment;
-    this._createReplyComment(comment, 'Done', false, false);
+  _handleCommentDone() {
+    this._createReplyComment('Done', false, false);
   }
 
   _handleCommentFix(e) {
@@ -392,7 +396,7 @@
     const msg = comment.message;
     const quoteStr = '> ' + msg.replace(NEWLINE_PATTERN, '\n> ') + '\n\n';
     const response = quoteStr + 'Please fix.';
-    this._createReplyComment(comment, response, false, true);
+    this._createReplyComment(response, false, true);
   }
 
   _commentElWithDraftID(id) {
@@ -405,11 +409,9 @@
     return null;
   }
 
-  _newReply(inReplyTo, opt_lineNum, opt_message, opt_unresolved,
-      opt_range) {
-    const d = this._newDraft(opt_lineNum);
+  _newReply(inReplyTo, opt_message, opt_unresolved) {
+    const d = this._newDraft();
     d.in_reply_to = inReplyTo;
-    d.range = opt_range;
     if (opt_message != null) {
       d.message = opt_message;
     }
@@ -428,19 +430,40 @@
       __draft: true,
       __draftID: Math.random().toString(36),
       __date: new Date(),
-      path: this.path,
-      patchNum: this.patchNum,
-      side: this._getSide(this.isOnParent),
-      __commentSide: this.commentSide,
     };
-    if (opt_lineNum) {
-      d.line = opt_lineNum;
-    }
-    if (opt_range) {
-      d.range = opt_range;
-    }
-    if (this.parentIndex) {
-      d.parent = this.parentIndex;
+
+    // For replies, always use same meta info as root.
+    if (this.comments && this.comments.length >= 1) {
+      const rootComment = this.comments[0];
+      [
+        'path',
+        'patchNum',
+        'side',
+        '__commentSide',
+        'line',
+        'range',
+        'parent',
+      ].forEach(key => {
+        if (rootComment.hasOwnProperty(key)) {
+          d[key] = rootComment[key];
+        }
+      });
+    } else {
+      // Set meta info for root comment.
+      d.path = this.path;
+      d.patchNum = this.patchNum;
+      d.side = this._getSide(this.isOnParent);
+      d.__commentSide = this.commentSide;
+
+      if (opt_lineNum) {
+        d.line = opt_lineNum;
+      }
+      if (opt_range) {
+        d.range = opt_range;
+      }
+      if (this.parentIndex) {
+        d.parent = this.parentIndex;
+      }
     }
     return d;
   }
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.js b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.js
index fbc18b4..88f0ff6 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.js
@@ -27,12 +27,12 @@
     gr-button {
       margin-left: var(--spacing-m);
     }
-    gr-comment:not(:last-of-type) {
+    gr-comment {
       border-bottom: 1px solid var(--comment-separator-color);
     }
     #actions {
       margin-left: auto;
-      padding: var(--spacing-m);
+      padding: var(--spacing-s) var(--spacing-m);
     }
     #container {
       background-color: var(--comment-background-color);
@@ -55,7 +55,6 @@
       background-color: var(--robot-comment-background-color);
     }
     #commentInfoContainer {
-      border-top: 1px dotted var(--border-color);
       display: flex;
     }
     #unresolvedLabel {
@@ -76,11 +75,17 @@
   </style>
   <template is="dom-if" if="[[showFilePath]]">
     <div class="pathInfo">
-      <a
-        href$="[[_getDiffUrlForComment(projectName, changeNum, path, patchNum)]]"
-        >[[_computeDisplayPath(path)]]</a
-      >
-      <span class="descriptionText">Patchset [[patchNum]]</span>
+      <template is="dom-if" if="[[_isPatchsetLevelComment(path)]]">
+        <span>Patchset Comment</span>
+        <span class="descriptionText">Patchset [[patchNum]]</span>
+      </template>
+      <template is="dom-if" if="[[!_isPatchsetLevelComment(path)]]">
+        <a
+          href$="[[_getDiffUrlForComment(projectName, changeNum, path, patchNum)]]"
+          >[[_computeDisplayPath(path)]]</a
+        >
+        <span class="descriptionText">Patchset [[patchNum]]</span>
+      </template>
     </div>
   </template>
   <div
@@ -148,7 +153,6 @@
       </div>
     </div>
   </div>
-  <gr-reporting id="reporting"></gr-reporting>
   <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   <gr-storage id="storage"></gr-storage>
 `;
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.html b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.html
index 244a9ec..eaa6547 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.html
@@ -268,6 +268,8 @@
       updated: '2015-12-08 19:48:33.843000000',
       path: '/path/to/file.txt',
       unresolved: true,
+      patchNum: 3,
+      __commentSide: 'left',
     }];
     flushAsynchronousOperations();
   });
@@ -279,7 +281,7 @@
   test('reply', () => {
     const commentEl = element.shadowRoot
         .querySelector('gr-comment');
-    const reportStub = sandbox.stub(element.$.reporting,
+    const reportStub = sandbox.stub(element.reporting,
         'recordDraftInteraction');
     assert.ok(commentEl);
 
@@ -297,7 +299,7 @@
   test('quote reply', () => {
     const commentEl = element.shadowRoot
         .querySelector('gr-comment');
-    const reportStub = sandbox.stub(element.$.reporting,
+    const reportStub = sandbox.stub(element.reporting,
         'recordDraftInteraction');
     assert.ok(commentEl);
 
@@ -313,7 +315,7 @@
   });
 
   test('quote reply multiline', () => {
-    const reportStub = sandbox.stub(element.$.reporting,
+    const reportStub = sandbox.stub(element.reporting,
         'recordDraftInteraction');
     element.comments = [{
       author: {
@@ -344,7 +346,7 @@
   });
 
   test('ack', done => {
-    const reportStub = sandbox.stub(element.$.reporting,
+    const reportStub = sandbox.stub(element.reporting,
         'recordDraftInteraction');
     element.changeNum = '42';
     element.patchNum = '1';
@@ -367,7 +369,7 @@
   });
 
   test('done', done => {
-    const reportStub = sandbox.stub(element.$.reporting,
+    const reportStub = sandbox.stub(element.reporting,
         'recordDraftInteraction');
     element.changeNum = '42';
     element.patchNum = '1';
@@ -440,7 +442,6 @@
     element.path = '/path/to/file.txt';
     element.push('comments', element._newReply(
         element.comments[0].id,
-        element.comments[0].line,
         element.comments[0].path,
         'it’s pronouced jiff, not giff'));
     flushAsynchronousOperations();
@@ -634,7 +635,7 @@
     });
 
     test('comment in_reply_to is either null or most recent comment', () => {
-      element._createReplyComment(element.comments[3], 'dummy', true);
+      element._createReplyComment('dummy', true);
       flushAsynchronousOperations();
       assert.equal(element._orderedComments.length, 5);
       assert.equal(element._orderedComments[4].in_reply_to, 'jacks_reply');
@@ -642,7 +643,7 @@
 
     test('resolvable comments', () => {
       assert.isFalse(element.unresolved);
-      element._createReplyComment(element.comments[3], 'dummy', true, true);
+      element._createReplyComment('dummy', true, true);
       flushAsynchronousOperations();
       assert.isTrue(element.unresolved);
     });
@@ -705,14 +706,21 @@
     assert.equal(element.comments[0].unresolved, true);
   });
 
-  test('_newDraft', () => {
-    element.commentSide = 'left';
-    element.patchNum = 3;
+  test('_newDraft with root', () => {
     const draft = element._newDraft();
     assert.equal(draft.__commentSide, 'left');
     assert.equal(draft.patchNum, 3);
   });
 
+  test('_newDraft with no root', () => {
+    element.comments = [];
+    element.commentSide = 'right';
+    element.patchNum = 2;
+    const draft = element._newDraft();
+    assert.equal(draft.__commentSide, 'right');
+    assert.equal(draft.patchNum, 2);
+  });
+
   test('new comment gets created', () => {
     element.comments = [];
     element.addOrEditDraft(1);
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 ee9df11..153b98d 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js
@@ -14,11 +14,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
 import '../../../styles/shared-styles.js';
-import '../../core/gr-reporting/gr-reporting.js';
 import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
 import '../../plugins/gr-endpoint-param/gr-endpoint-param.js';
 import '../gr-button/gr-button.js';
@@ -41,6 +38,7 @@
 import {KeyboardShortcutBehavior} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
 import {getRootElement} from '../../../scripts/rootElement.js';
 import {GrDisplayNameUtils} from '../../../scripts/gr-display-name-utils/gr-display-name-utils.js';
+import {appContext} from '../../../services/app-context.js';
 
 const STORAGE_DEBOUNCE_INTERVAL = 400;
 const TOAST_DEBOUNCE_INTERVAL = 200;
@@ -69,7 +67,7 @@
 ];
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrComment extends mixinBehaviors( [
   KeyboardShortcutBehavior,
@@ -166,6 +164,7 @@
       collapsed: {
         type: Boolean,
         value: true,
+        reflectToAttribute: true,
         observer: '_toggleCollapseClass',
       },
       /** @type {?} */
@@ -241,6 +240,11 @@
     };
   }
 
+  constructor() {
+    super();
+    this.reporting = appContext.reportingService;
+  }
+
   /** @override */
   attached() {
     super.attached();
@@ -283,7 +287,7 @@
       this._showRespectfulTip = true;
       const randomIdx = this.getRandomNum(0, RESPECTFUL_REVIEW_TIPS.length);
       this._respectfulReviewTip = RESPECTFUL_REVIEW_TIPS[randomIdx];
-      this.$.reporting.reportInteraction(
+      this.reporting.reportInteraction(
           'respectful-tip-appeared',
           {tip: this._respectfulReviewTip}
       );
@@ -303,7 +307,7 @@
 
   _dismissRespectfulTip() {
     this._respectfulTipDismissed = true;
-    this.$.reporting.reportInteraction(
+    this.reporting.reportInteraction(
         'respectful-tip-dismissed',
         {tip: this._respectfulReviewTip}
     );
@@ -312,7 +316,7 @@
   }
 
   _onRespectfulReadMoreClick() {
-    this.$.reporting.reportInteraction('respectful-read-more-clicked');
+    this.reporting.reportInteraction('respectful-read-more-clicked');
   }
 
   get textarea() {
@@ -479,7 +483,10 @@
 
     this.$.container.classList.toggle('editing', editing);
     if (this.comment && this.comment.id) {
-      this.shadowRoot.querySelector('.cancel').hidden = !editing;
+      const cancelButton = this.shadowRoot.querySelector('.cancel');
+      if (cancelButton) {
+        cancelButton.hidden = !editing;
+      }
     }
     if (this.comment) {
       this.comment.__editing = this.editing;
@@ -583,7 +590,7 @@
     e.preventDefault();
     this._messageText = this.comment.message;
     this.editing = true;
-    this.$.reporting.recordDraftInteraction();
+    this.reporting.recordDraftInteraction();
   }
 
   _handleSave(e) {
@@ -595,7 +602,7 @@
     }
     const timingLabel = this.comment.id ?
       REPORT_UPDATE_DRAFT : REPORT_CREATE_DRAFT;
-    const timer = this.$.reporting.getTimer(timingLabel);
+    const timer = this.reporting.getTimer(timingLabel);
     this.set('comment.__editing', false);
     return this.save().then(() => { timer.end(); });
   }
@@ -643,7 +650,7 @@
 
   _handleDiscard(e) {
     e.preventDefault();
-    this.$.reporting.recordDraftInteraction();
+    this.reporting.recordDraftInteraction();
 
     if (!this._messageText) {
       this._discardDraft();
@@ -658,7 +665,7 @@
 
   _handleConfirmDiscard(e) {
     e.preventDefault();
-    const timer = this.$.reporting.getTimer(REPORT_DISCARD_DRAFT);
+    const timer = this.reporting.getTimer(REPORT_DISCARD_DRAFT);
     this._closeConfirmDiscardOverlay();
     return this._discardDraft().then(() => { timer.end(); });
   }
@@ -799,7 +806,7 @@
   }
 
   _handleToggleResolved() {
-    this.$.reporting.recordDraftInteraction();
+    this.reporting.recordDraftInteraction();
     this.resolved = !this.resolved;
     // Modify payload instead of this.comment, as this.comment is passed from
     // the parent by ref.
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.js b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.js
index fc79cba..3d27042 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.js
@@ -23,6 +23,9 @@
       font-family: var(--font-family);
       padding: var(--spacing-m);
     }
+    :host([collapsed]) {
+      padding: var(--spacing-s) var(--spacing-m);
+    }
     :host([disabled]) {
       pointer-events: none;
     }
@@ -34,20 +37,17 @@
     :host([discarding]) {
       display: none;
     }
+    .body {
+      padding-top: var(--spacing-m);
+    }
     .header {
       align-items: center;
       cursor: pointer;
       display: flex;
-      margin: calc(0px - var(--spacing-m)) calc(0px - var(--spacing-m)) 0
-        calc(0px - var(--spacing-m));
-      padding: var(--spacing-m);
     }
     .headerLeft > span {
       font-weight: var(--font-weight-bold);
     }
-    .container.collapsed .header {
-      margin-bottom: calc(0 - var(--spacing-m));
-    }
     .headerMiddle {
       color: var(--deemphasized-text-color);
       flex: 1;
@@ -60,8 +60,7 @@
     }
     .date {
       justify-content: flex-end;
-      margin-left: 5px;
-      min-width: 4.5em;
+      margin-left: var(--spacing-m);
       text-align: right;
       white-space: nowrap;
     }
@@ -77,6 +76,12 @@
       justify-content: flex-end;
       padding-top: 0;
     }
+    .robotActions {
+      /* Better than the negative margin would be to remove the gr-button
+       * padding, but then we would also need to fix the buttons that are
+       * inserted by plugins. :-/ */
+      margin: 4px 0 -4px;
+    }
     .action {
       margin-left: var(--spacing-l);
     }
@@ -131,16 +136,6 @@
     .robotId {
       color: var(--deemphasized-text-color);
       margin-bottom: var(--spacing-m);
-      margin-top: -0.4em;
-    }
-    .robotIcon {
-      margin-right: var(--spacing-xs);
-      /* because of the antenna of the robot, it looks off center even when it
-         is centered. artificially adjust margin to account for this. */
-      margin-top: -4px;
-    }
-    .runIdInformation {
-      margin: var(--spacing-m) 0;
     }
     .robotRun {
       margin-left: var(--spacing-m);
@@ -161,16 +156,18 @@
     #container .collapsedContent {
       display: none;
     }
-    #container.collapsed {
-      padding-bottom: 3px;
+    #container.collapsed .body {
+      padding-top: 0;
     }
     #container.collapsed .collapsedContent {
       display: block;
       overflow: hidden;
-      padding-left: 5px;
+      padding-left: var(--spacing-m);
       text-overflow: ellipsis;
       white-space: nowrap;
     }
+    #container.collapsed #deleteBtn,
+    #container.collapsed .date,
     #container.collapsed .actions,
     #container.collapsed gr-formatted-text,
     #container.collapsed gr-textarea,
@@ -192,12 +189,6 @@
       flex-direction: column;
       width: 100%;
     }
-    .comment-extra-note {
-      color: var(--deemphasized-text-color);
-      border: 1px solid var(--deemphasized-text-color);
-      border-radius: var(--border-radius);
-      padding: 0px var(--spacing-s);
-    }
     #deleteBtn {
       display: none;
       --gr-button: {
@@ -270,9 +261,6 @@
           </a>
         </div>
       </div>
-      <template is="dom-if" if="[[comment.extraNote]]">
-        <span class="comment-extra-note">[[comment.extraNote]]</span>
-      </template>
       <gr-button
         id="deleteBtn"
         link=""
@@ -373,33 +361,35 @@
             Resolved
           </label>
         </div>
-        <div class="rightActions">
-          <gr-button
-            link=""
-            class="action cancel hideOnPublished"
-            on-click="_handleCancel"
-            >Cancel</gr-button
-          >
-          <gr-button
-            link=""
-            class="action discard hideOnPublished"
-            on-click="_handleDiscard"
-            >Discard</gr-button
-          >
-          <gr-button
-            link=""
-            class="action edit hideOnPublished"
-            on-click="_handleEdit"
-            >Edit</gr-button
-          >
-          <gr-button
-            link=""
-            disabled$="[[_computeSaveDisabled(_messageText, comment, resolved)]]"
-            class="action save hideOnPublished"
-            on-click="_handleSave"
-            >Save</gr-button
-          >
-        </div>
+        <template is="dom-if" if="[[draft]]">
+          <div class="rightActions">
+            <gr-button
+              link=""
+              class="action cancel hideOnPublished"
+              on-click="_handleCancel"
+              >Cancel</gr-button
+            >
+            <gr-button
+              link=""
+              class="action discard hideOnPublished"
+              on-click="_handleDiscard"
+              >Discard</gr-button
+            >
+            <gr-button
+              link=""
+              class="action edit hideOnPublished"
+              on-click="_handleEdit"
+              >Edit</gr-button
+            >
+            <gr-button
+              link=""
+              disabled$="[[_computeSaveDisabled(_messageText, comment, resolved)]]"
+              class="action save hideOnPublished"
+              on-click="_handleSave"
+              >Save</gr-button
+            >
+          </div>
+        </template>
       </div>
       <div class="robotActions" hidden$="[[!_showRobotActions]]">
         <template is="dom-if" if="[[isRobotComment]]">
@@ -458,5 +448,4 @@
   </template>
   <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   <gr-storage id="storage"></gr-storage>
-  <gr-reporting id="reporting"></gr-reporting>
 `;
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.html b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.html
index 56581d4..17c01e9 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.html
@@ -259,14 +259,6 @@
       });
     });
 
-    test('extra note shown if exists', () => {
-      element.comment = {id: 'abc_123', extraNote: 'asd'};
-      flushAsynchronousOperations();
-      assert.equal(element.shadowRoot
-          .querySelector('.comment-extra-note')
-          .textContent, 'asd');
-    });
-
     test('delete comment button for non-admins is hidden', () => {
       element._isAdmin = false;
       assert.isFalse(element.shadowRoot
@@ -320,7 +312,7 @@
         sandbox.stub(element, '_discardDraft')
             .returns(Promise.resolve({}));
         endStub = sinon.stub();
-        getTimerStub = sandbox.stub(element.$.reporting, 'getTimer')
+        getTimerStub = sandbox.stub(element.reporting, 'getTimer')
             .returns({end: endStub});
       });
 
@@ -354,17 +346,20 @@
     });
 
     test('edit reports interaction', () => {
-      const reportStub = sandbox.stub(element.$.reporting,
+      const reportStub = sandbox.stub(element.reporting,
           'recordDraftInteraction');
+      element.draft = true;
+      flushAsynchronousOperations();
       MockInteractions.tap(element.shadowRoot
           .querySelector('.edit'));
       assert.isTrue(reportStub.calledOnce);
     });
 
     test('discard reports interaction', () => {
-      const reportStub = sandbox.stub(element.$.reporting,
+      const reportStub = sandbox.stub(element.reporting,
           'recordDraftInteraction');
       element.draft = true;
+      flushAsynchronousOperations();
       MockInteractions.tap(element.shadowRoot
           .querySelector('.discard'));
       assert.isTrue(reportStub.calledOnce);
@@ -435,6 +430,7 @@
           .querySelector('.robotActions').hasAttribute('hidden'));
 
       element.draft = true;
+      flushAsynchronousOperations();
       assert.isTrue(isVisible(element.shadowRoot
           .querySelector('.edit')), 'edit is visible');
       assert.isTrue(isVisible(element.shadowRoot
@@ -560,6 +556,8 @@
 
       // When the edit button is pressed, should still see the actions
       // and also textarea
+      element.draft = true;
+      flushAsynchronousOperations();
       MockInteractions.tap(element.shadowRoot
           .querySelector('.edit'));
       flushAsynchronousOperations();
@@ -663,6 +661,8 @@
 
     test('draft creation/cancellation', done => {
       assert.isFalse(element.editing);
+      element.draft = true;
+      flushAsynchronousOperations();
       MockInteractions.tap(element.shadowRoot
           .querySelector('.edit'));
       assert.isTrue(element.editing);
@@ -795,12 +795,13 @@
       const cancelDebounce = sandbox.stub(element, 'cancelDebouncer');
 
       element.draft = true;
+      flushAsynchronousOperations();
       MockInteractions.tap(element.shadowRoot
           .querySelector('.edit'));
       element._messageText = 'good news, everyone!';
       element.flushDebouncer('fire-update');
       element.flushDebouncer('store');
-      assert.equal(dispatchEventStub.lastCall.args[0].type, 'comment-update'),
+      assert.equal(dispatchEventStub.lastCall.args[0].type, 'comment-update');
       assert.isTrue(dispatchEventStub.calledTwice);
 
       element._messageText = 'good news, everyone!';
@@ -860,6 +861,7 @@
       const saveStub = sandbox.stub(element, 'save').returns(Promise.resolve());
       element.showActions = true;
       element.draft = true;
+      flushAsynchronousOperations();
       MockInteractions.tap(element.$.header);
       MockInteractions.tap(element.shadowRoot
           .querySelector('.edit'));
diff --git a/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.js b/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.js
index b0f387b..f9ce12e 100644
--- a/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.js
+++ b/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.js
@@ -14,9 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
-
-import '../../../scripts/bundled-polymer.js';
 import '../gr-dialog/gr-dialog.js';
 import '../../../styles/shared-styles.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
@@ -25,7 +22,7 @@
 import {htmlTemplate} from './gr-confirm-delete-comment-dialog_html.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrConfirmDeleteCommentDialog extends GestureEventListeners(
     LegacyElementMixin(PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.js b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.js
index 0f6168e..39c149f 100644
--- a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.js
+++ b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-input/iron-input.js';
 import '../../../styles/shared-styles.js';
 import '../gr-button/gr-button.js';
@@ -28,7 +26,7 @@
 
 const COPY_TIMEOUT_MS = 1000;
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrCopyClipboard extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js
index 222109e..647f41b 100644
--- a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js
+++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js
@@ -14,18 +14,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-cursor-manager_html.js';
+import {ScrollMode} from '../../../constants/constants.js';
 
-const ScrollBehavior = {
-  NEVER: 'never',
-  KEEP_VISIBLE: 'keep-visible',
-};
-
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrCursorManager extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
@@ -82,9 +77,9 @@
        *
        * @type {string|undefined}
        */
-      scrollBehavior: {
+      scrollMode: {
         type: String,
-        value: ScrollBehavior.NEVER,
+        value: ScrollMode.NEVER,
       },
 
       /**
@@ -214,8 +209,8 @@
   setCursor(element, opt_noScroll) {
     let behavior;
     if (opt_noScroll) {
-      behavior = this.scrollBehavior;
-      this.scrollBehavior = ScrollBehavior.NEVER;
+      behavior = this.scrollMode;
+      this.scrollMode = ScrollMode.NEVER;
     }
 
     this.unsetCursor();
@@ -223,7 +218,7 @@
     this._updateIndex();
     this._decorateTarget();
 
-    if (opt_noScroll) { this.scrollBehavior = behavior; }
+    if (opt_noScroll) { this.scrollMode = behavior; }
   }
 
   unsetCursor() {
@@ -391,7 +386,7 @@
    */
   _targetIsVisible(top) {
     const dims = this._getWindowDims();
-    return this.scrollBehavior === ScrollBehavior.KEEP_VISIBLE &&
+    return this.scrollMode === ScrollMode.KEEP_VISIBLE &&
         top > (dims.pageYOffset + this.scrollTopMargin) &&
         top < dims.pageYOffset + dims.innerHeight;
   }
@@ -403,7 +398,7 @@
   }
 
   _scrollToTarget() {
-    if (!this.target || this.scrollBehavior === ScrollBehavior.NEVER) {
+    if (!this.target || this.scrollMode === ScrollMode.NEVER) {
       return;
     }
 
@@ -415,8 +410,8 @@
 
     if (this._targetIsVisible(top)) {
       // Don't scroll if either the bottom is visible or if the position that
-      // would get scrolled to is higher up than the current position. this
-      // woulld cause less of the target content to be displayed than is
+      // would get scrolled to is higher up than the current position. This
+      // would cause less of the target content to be displayed than is
       // already.
       if (bottomIsVisible || scrollToValue < dims.scrollY) {
         return;
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.html b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.js
similarity index 86%
rename from polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.html
rename to polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.js
index 98a7d24..561746a 100644
--- a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.js
@@ -1,32 +1,25 @@
-<!DOCTYPE html>
-<!--
-@license
-Copyright (C) 2016 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
 
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
+import '../../../test/common-test-setup-karma.js';
+import './gr-cursor-manager.js';
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
 
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<meta charset="utf-8">
-<title>gr-cursor-manager</title>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
-
-<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/components/wct-browser-legacy/browser.js"></script>
-
-<test-fixture id="basic">
-  <template>
+const basicTestFixutre = fixtureFromTemplate(html`
     <gr-cursor-manager cursor-target-class="targeted"></gr-cursor-manager>
     <ul>
       <li>A</li>
@@ -34,12 +27,8 @@
       <li>C</li>
       <li>D</li>
     </ul>
-  </template>
-</test-fixture>
+`);
 
-<script type="module">
-import '../../../test/common-test-setup.js';
-import './gr-cursor-manager.js';
 suite('gr-cursor-manager tests', () => {
   let sandbox;
   let element;
@@ -47,7 +36,7 @@
 
   setup(() => {
     sandbox = sinon.sandbox.create();
-    const fixtureElements = fixture('basic');
+    const fixtureElements = basicTestFixutre.instantiate();
     element = fixtureElements[0];
     list = fixtureElements[1];
   });
@@ -178,7 +167,7 @@
     sandbox.stub(element, '_targetIsVisible', () => false);
     const scrollStub = sandbox.stub(window, 'scrollTo');
     element.stops = list.querySelectorAll('li');
-    element.scrollBehavior = 'keep-visible';
+    element.scrollMode = 'keep-visible';
 
     element.setCursorAtIndex(1, true);
     assert.isFalse(scrollStub.called);
@@ -236,7 +225,7 @@
     let scrollStub;
     setup(() => {
       element.stops = list.querySelectorAll('li');
-      element.scrollBehavior = 'keep-visible';
+      element.scrollMode = 'keep-visible';
 
       // There is a target which has a targetNext
       element.setCursor(list.children[0]);
@@ -300,4 +289,3 @@
     });
   });
 });
-</script>
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js
index 6f19587..5335ede 100644
--- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../gr-rest-api-interface/gr-rest-api-interface.js';
 import '../../../styles/shared-styles.js';
 import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
@@ -24,13 +22,7 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-date-formatter_html.js';
 import {TooltipBehavior} from '../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js';
-import {util} from '../../../scripts/util.js';
-import moment from 'moment/src/moment.js';
-
-const Duration = {
-  HOUR: 1000 * 60 * 60,
-  DAY: 1000 * 60 * 60 * 24,
-};
+import {parseDate, fromNow, isValidDate, isWithinDay, isWithinHalfYear, formatDate, utcOffsetString} from '../../../utils/date-util.js';
 
 const TimeFormats = {
   TIME_12: 'h:mm A', // 2:14 PM
@@ -63,7 +55,7 @@
 };
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrDateFormatter extends mixinBehaviors( [
   TooltipBehavior,
@@ -108,6 +100,10 @@
     };
   }
 
+  constructor() {
+    super();
+  }
+
   /** @override */
   attached() {
     super.attached();
@@ -115,7 +111,7 @@
   }
 
   _getUtcOffsetString() {
-    return ' UTC' + moment().format('Z');
+    return utcOffsetString();
   }
 
   _loadPreferences() {
@@ -192,50 +188,28 @@
     return this.$.restAPI.getPreferences();
   }
 
-  /**
-   * Return true if date is within 24 hours and on the same day.
-   */
-  _isWithinDay(now, date) {
-    const diff = -date.diff(now);
-    return diff < Duration.DAY && date.day() === now.getDay();
-  }
-
-  /**
-   * Returns true if date is from one to six months.
-   */
-  _isWithinHalfYear(now, date) {
-    const diff = -date.diff(now);
-    return (date.day() !== now.getDay() || diff >= Duration.DAY) &&
-      diff < 180 * Duration.DAY;
-  }
-
   _computeDateStr(
       dateStr, timeFormat, dateFormat, relative, showDateAndTime
   ) {
     if (!dateStr || !timeFormat || !dateFormat) { return ''; }
-    const date = moment(util.parseDate(dateStr));
-    if (!date.isValid()) { return ''; }
+    const date = parseDate(dateStr);
+    if (!isValidDate(date)) { return ''; }
     if (relative) {
-      const dateFromNow = date.fromNow();
-      if (dateFromNow === 'a few seconds ago') {
-        return 'just now';
-      } else {
-        return dateFromNow;
-      }
+      return fromNow(date);
     }
     const now = new Date();
     let format = dateFormat.full;
-    if (this._isWithinDay(now, date)) {
+    if (isWithinDay(now, date)) {
       format = timeFormat;
     } else {
-      if (this._isWithinHalfYear(now, date)) {
+      if (isWithinHalfYear(now, date)) {
         format = dateFormat.short;
       }
       if (this.showDateAndTime) {
         format = `${format} ${timeFormat}`;
       }
     }
-    return date.format(format);
+    return formatDate(date, format);
   }
 
   _timeToSecondsFormat(timeFormat) {
@@ -255,11 +229,11 @@
     }
 
     if (!dateStr) { return ''; }
-    const date = moment(util.parseDate(dateStr));
-    if (!date.isValid()) { return ''; }
+    const date = parseDate(dateStr);
+    if (!isValidDate(date)) { return ''; }
     let format = dateFormat.full + ', ';
     format += this._timeToSecondsFormat(timeFormat);
-    return date.format(format) + this._getUtcOffsetString();
+    return formatDate(date, format) + this._getUtcOffsetString();
   }
 }
 
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html
index 7169ef27..589a157 100644
--- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html
@@ -34,7 +34,7 @@
 <script type="module">
 import '../../../test/common-test-setup.js';
 import './gr-date-formatter.js';
-import {util} from '../../../scripts/util.js';
+import {parseDate} from '../../../utils/date-util.js';
 suite('gr-date-formatter tests', () => {
   let element;
   let sandbox;
@@ -51,7 +51,7 @@
    * Parse server-formatter date and normalize into current timezone.
    */
   function normalizedDate(dateStr) {
-    const d = util.parseDate(dateStr);
+    const d = parseDate(dateStr);
     d.setMinutes(d.getMinutes() + d.getTimezoneOffset());
     return d;
   }
diff --git a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.js b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.js
index db64661..2292ae7 100644
--- a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.js
+++ b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../gr-button/gr-button.js';
 import '../../../styles/shared-styles.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
@@ -24,7 +22,7 @@
 import {htmlTemplate} from './gr-dialog_html.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrDialog extends GestureEventListeners(
     LegacyElementMixin(PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.js b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.js
index 00f9078..1d00941 100644
--- a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.js
+++ b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-input/iron-input.js';
 import '../../../styles/shared-styles.js';
 import '../gr-button/gr-button.js';
@@ -26,7 +24,7 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-diff-preferences_html.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrDiffPreferences extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.js b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.js
index fcc09c4..7b92e3d 100644
--- a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.js
+++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/paper-tabs/paper-tabs.js';
 import '../gr-shell-command/gr-shell-command.js';
 import '../gr-rest-api-interface/gr-rest-api-interface.js';
@@ -28,7 +26,7 @@
 import {RESTClientBehavior} from '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrDownloadCommands extends mixinBehaviors( [
   RESTClientBehavior,
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js
index 6b250de..f84ef4a 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-dropdown/iron-dropdown.js';
 import '@polymer/paper-item/paper-item.js';
 import '@polymer/paper-listbox/paper-listbox.js';
@@ -56,7 +54,7 @@
  */
 Defs.item;
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrDropdownList extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
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 12a2025..8e7ea87 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
-import '../../../scripts/bundled-polymer.js';
 import '@polymer/iron-dropdown/iron-dropdown.js';
 import '../gr-button/gr-button.js';
 import '../gr-cursor-manager/gr-cursor-manager.js';
@@ -35,7 +33,7 @@
 const REL_EXTERNAL = 'external';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrDropdown extends mixinBehaviors( [
   BaseUrlBehavior,
@@ -184,7 +182,7 @@
   }
 
   /**
-   * Hanlde a click on the button to open the dropdown.
+   * Handle a click on the button to open the dropdown.
    *
    * @param {!Event} e
    */
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_html.js b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_html.js
index d0b0d09..6617a0e 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_html.js
@@ -161,7 +161,7 @@
   <gr-cursor-manager
     id="cursor"
     cursor-target-class="selected"
-    scroll-behavior="never"
+    scroll-mode="never"
     focus-on-move=""
     stops="[[_listElements]]"
   ></gr-cursor-manager>
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 804eb16..b2bcda9 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
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
 import '../../../styles/shared-styles.js';
 import '../gr-storage/gr-storage.js';
@@ -29,7 +27,7 @@
 const STORAGE_DEBOUNCE_INTERVAL_MS = 400;
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrEditableContent extends GestureEventListeners(
     LegacyElementMixin(PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js
index 8669f03..6c3c72f 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js
+++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import {IronOverlayBehaviorImpl} from '@polymer/iron-overlay-behavior/iron-overlay-behavior.js';
 import '@polymer/iron-dropdown/iron-dropdown.js';
 import '@polymer/paper-input/paper-input.js';
@@ -33,7 +31,7 @@
 const AWAIT_STEP = 5;
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrEditableLabel extends mixinBehaviors( [
   KeyboardShortcutBehavior,
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.html b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.html
index 5673194..1d1bce8 100644
--- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.html
@@ -188,7 +188,7 @@
 
     element.async(() => {
       assert.isFalse(editedStub.called);
-      // Text changes sould be discarded.
+      // Text changes should be discarded.
       assert.equal(input.value, 'value text');
       assert.isFalse(element.editing);
       done();
@@ -214,7 +214,7 @@
 
     element.async(() => {
       assert.isFalse(editedStub.called);
-      // Text changes sould be discarded.
+      // Text changes should be discarded.
       assert.equal(input.value, 'value text');
       assert.isFalse(element.editing);
       done();
diff --git a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.js b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.js
index 0d19f00..bc79737 100644
--- a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.js
+++ b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.js
@@ -14,15 +14,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/shared-styles.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-fixed-panel_html.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrFixedPanel extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.js b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.js
index 139e09c..039b95d 100644
--- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.js
+++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../gr-linked-text/gr-linked-text.js';
 import '../../../styles/shared-styles.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
@@ -28,7 +26,7 @@
 const QUOTE_MARKER_PATTERN = /\n\s?>\s/g;
 const CODE_MARKER_PATTERN = /^(`{1,3})([^`]+?)\1$/;
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrFormattedText extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
@@ -91,7 +89,7 @@
 
   /**
    * Given a source string, parse into an array of block objects. Each block
-   * has a `type` property which takes any of the follwoing values.
+   * has a `type` property which takes any of the following values.
    * * 'paragraph'
    * * 'quote' (Block quote.)
    * * 'pre' (Pre-formatted text.)
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.js b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.js
index 0bc9cb7..49aff7e 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.js
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account.js
@@ -14,7 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
 
 import '@polymer/iron-icon/iron-icon.js';
 import '../../../styles/shared-styles.js';
@@ -26,7 +25,7 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-hovercard-account_html.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrHovercardAccount extends GestureEventListeners(
     hovercardBehaviorMixin(LegacyElementMixin(
         PolymerElement))) {
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_html.js b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_html.js
index 8d14ff4..9c8788b 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_html.js
@@ -64,33 +64,35 @@
     }
   </style>
   <div id="container" role="tooltip" tabindex="-1">
-    <div class="top">
-      <div class="avatar">
-        <gr-avatar account="[[account]]" image-size="56"></gr-avatar>
+    <template is="dom-if" if="[[_isShowing]]">
+      <div class="top">
+        <div class="avatar">
+          <gr-avatar account="[[account]]" image-size="56"></gr-avatar>
+        </div>
+        <div class="account">
+          <h3 class="name">[[account.name]]</h3>
+          <div class="email">[[account.email]]</div>
+        </div>
       </div>
-      <div class="account">
-        <h3 class="name">[[account.name]]</h3>
-        <div class="email">[[account.email]]</div>
-      </div>
-    </div>
-    <template is="dom-if" if="[[account.status]]">
-      <div class="status">
-        <span class="title">
-          <iron-icon icon="gr-icons:calendar"></iron-icon>
-          Status:
-        </span>
-        <span class="value">[[account.status]]</span>
+      <template is="dom-if" if="[[account.status]]">
+        <div class="status">
+          <span class="title">
+            <iron-icon icon="gr-icons:calendar"></iron-icon>
+            Status:
+          </span>
+          <span class="value">[[account.status]]</span>
+        </div>
+      </template>
+      <template is="dom-if" if="[[voteableText]]">
+        <div class="voteable">
+          <span class="title">Voteable:</span>
+          <span class="value">[[voteableText]]</span>
+        </div>
+      </template>
+      <div class="attention">
+        <iron-icon icon="gr-icons:attention"></iron-icon>
+        <span>It is this user's turn to take action.</span>
       </div>
     </template>
-    <template is="dom-if" if="[[voteableText]]">
-      <div class="voteable">
-        <span class="title">Voteable:</span>
-        <span class="value">[[voteableText]]</span>
-      </div>
-    </template>
-    <div class="attention">
-      <iron-icon icon="gr-icons:attention"></iron-icon>
-      <span>It is this user's turn to take action.</span>
-    </div>
   </div>
 `;
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.html b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.html
index be0f2b2..884e1b6 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.html
@@ -49,6 +49,8 @@
     setup(() => {
       element = fixture('basic');
       element.account = Object.assign({}, ACCOUNT);
+      element.show({});
+      flushAsynchronousOperations();
     });
 
     test('account name is shown', () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-behavior.js b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-behavior.js
index 956c68c..1190908 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-behavior.js
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard-behavior.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/shared-styles.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {getRootElement} from '../../../scripts/rootElement.js';
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.js b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.js
index e77a4c5..e334064 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.js
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.js
@@ -14,7 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
 
 import '../../../styles/shared-styles.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
@@ -24,7 +23,7 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import './gr-hovercard-shared-style.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrHovercard extends GestureEventListeners(
     hovercardBehaviorMixin(LegacyElementMixin(PolymerElement))
 ) {
diff --git a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.js b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.js
index f7b8b63..bcb85e2 100644
--- a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.js
+++ b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.js
@@ -98,6 +98,8 @@
       <g id="zeroState"><path d="M22 9V7h-2V5c0-1.1-.9-2-2-2H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2v-2h2v-2h-2v-2h2v-2h-2V9h2zm-4 10H4V5h14v14zM6 13h5v4H6zm6-6h4v3h-4zM6 7h5v5H6zm6 4h4v6h-4z"></path></g>
       <!-- This SVG is an adaptation of material.io https://material.io/icons/#label_important-->
       <g id="attention"><path d="M5.5 19 l9 0 c.67 0 1.27 -.33 1.63 -.84 L20.5 12 l-4.37 -6.16 c-.36 -.51 -.96 -.84 -1.63 -.84 l-9 0 L9 12 z"></path></g>
+      <!-- This SVG is a copy from material.io https://material.io/icons/#pets-->
+      <g id="pets"><circle cx="4.5" cy="9.5" r="2.5"/><circle cx="9" cy="5.5" r="2.5"/><circle cx="15" cy="5.5" r="2.5"/><circle cx="19.5" cy="9.5" r="2.5"/><path d="M17.34 14.86c-.87-1.02-1.6-1.89-2.48-2.91-.46-.54-1.05-1.08-1.75-1.32-.11-.04-.22-.07-.33-.09-.25-.04-.52-.04-.78-.04s-.53 0-.79.05c-.11.02-.22.05-.33.09-.7.24-1.28.78-1.75 1.32-.87 1.02-1.6 1.89-2.48 2.91-1.31 1.31-2.92 2.76-2.62 4.79.29 1.02 1.02 2.03 2.33 2.32.73.15 3.06-.44 5.54-.44h.18c2.48 0 4.81.58 5.54.44 1.31-.29 2.04-1.31 2.33-2.32.31-2.04-1.3-3.49-2.61-4.8z"/><path d="M0 0h24v24H0z" fill="none"/></g>
     </defs>
   </svg>
 </iron-iconset-svg>`;
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.js
index 997e08c..a98936f 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
@@ -46,7 +44,7 @@
 };
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrJsApiInterface extends mixinBehaviors( [
   PatchSetBehavior,
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js
index 1cdb20f..6f0ade9 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-import '../../core/gr-reporting/gr-reporting.js';
 import '../gr-rest-api-interface/gr-rest-api-interface.js';
 import './gr-js-api-interface-element.js';
 import './gr-public-js-api.js';
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context.js
index e3256a1..63555da 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context.js
@@ -14,6 +14,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+
 export function GrPluginActionContext(plugin, action, change, revision) {
   this.action = action;
   this.plugin = plugin;
@@ -47,14 +49,14 @@
 
 GrPluginActionContext.prototype.msg = function(text) {
   const label = document.createElement('gr-label');
-  Polymer.dom(label).appendChild(document.createTextNode(text));
+  dom(label).appendChild(document.createTextNode(text));
   return label;
 };
 
 GrPluginActionContext.prototype.div = function(...els) {
   const div = document.createElement('div');
   for (const el of els) {
-    Polymer.dom(div).appendChild(el);
+    dom(div).appendChild(el);
   }
   return div;
 };
@@ -62,7 +64,7 @@
 GrPluginActionContext.prototype.button = function(label, callbacks) {
   const onClick = callbacks && callbacks.onclick;
   const button = document.createElement('gr-button');
-  Polymer.dom(button).appendChild(document.createTextNode(label));
+  dom(button).appendChild(document.createTextNode(label));
   if (onClick) {
     this.plugin.eventHelper(button).onTap(onClick);
   }
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.js
index 2f27304..6c5546e 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader.js
@@ -1,3 +1,5 @@
+import {appContext} from '../../../services/app-context.js';
+
 /**
  * @license
  * Copyright (C) 2019 The Android Open Source Project
@@ -15,6 +17,7 @@
  * limitations under the License.
  */
 import './gr-api-utils.js';
+import {importHref} from '../../../scripts/import-href.js';
 
 import {
   PLUGIN_LOADING_TIMEOUT_MS,
@@ -85,7 +88,7 @@
 
   _getReporting() {
     if (!this._reporting) {
-      this._reporting = document.createElement('gr-reporting');
+      this._reporting = appContext.reportingService;
     }
     return this._reporting;
   }
@@ -333,7 +336,7 @@
       };
     }
 
-    (Polymer.importHref || Polymer.Base.importHref)(
+    importHref(
         url, () => {},
         onerror,
         !sync);
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.html
index c972f53..ea1fcf2 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.html
@@ -99,12 +99,14 @@
   });
 
   test('report pluginsLoaded', done => {
-    stub('gr-reporting', {
-      pluginsLoaded() {
-        done();
-      },
+    const pluginsLoadedStub = sandbox.stub(pluginLoader._getReporting(),
+        'pluginsLoaded');
+    pluginsLoadedStub.reset();
+    window.Gerrit._loadPlugins([]);
+    flush(() => {
+      assert.isTrue(pluginsLoadedStub.calledOnce);
+      done();
     });
-    pluginLoader.loadPlugins([]);
   });
 
   test('arePluginsLoaded', done => {
@@ -129,10 +131,8 @@
     sandbox.stub(pluginLoader, '_loadJsPlugin', url => {
       pluginApi.install(() => void 0, undefined, url);
     });
-    const pluginsLoadedStub = sandbox.stub();
-    stub('gr-reporting', {
-      pluginsLoaded: (...args) => pluginsLoadedStub(...args),
-    });
+    const pluginsLoadedStub = sandbox.stub(pluginLoader._getReporting(),
+        'pluginsLoaded');
 
     const plugins = [
       'http://test.com/plugins/foo/static/test.js',
@@ -151,10 +151,6 @@
     sandbox.stub(pluginLoader, '_loadJsPlugin', url => {
       pluginApi.install(() => void 0, undefined, url);
     });
-    const pluginsLoadedStub = sandbox.stub();
-    stub('gr-reporting', {
-      pluginsLoaded: (...args) => pluginsLoadedStub(...args),
-    });
 
     const plugins = [
       'http://test.com/plugins/foo/static/test.js',
@@ -193,10 +189,8 @@
       }, undefined, url);
     });
 
-    const pluginsLoadedStub = sandbox.stub();
-    stub('gr-reporting', {
-      pluginsLoaded: (...args) => pluginsLoadedStub(...args),
-    });
+    const pluginsLoadedStub = sandbox.stub(pluginLoader._getReporting(),
+        'pluginsLoaded');
 
     pluginLoader.loadPlugins(plugins);
 
@@ -225,10 +219,8 @@
       }, undefined, url);
     });
 
-    const pluginsLoadedStub = sandbox.stub();
-    stub('gr-reporting', {
-      pluginsLoaded: (...args) => pluginsLoadedStub(...args),
-    });
+    const pluginsLoadedStub = sandbox.stub(pluginLoader._getReporting(),
+        'pluginsLoaded');
 
     pluginLoader.loadPlugins(plugins);
     assert.isTrue(
@@ -260,10 +252,8 @@
       }, undefined, url);
     });
 
-    const pluginsLoadedStub = sandbox.stub();
-    stub('gr-reporting', {
-      pluginsLoaded: (...args) => pluginsLoadedStub(...args),
-    });
+    const pluginsLoadedStub = sandbox.stub(pluginLoader._getReporting(),
+        'pluginsLoaded');
 
     pluginLoader.loadPlugins(plugins);
 
@@ -289,10 +279,8 @@
       }, url === plugins[0] ? '' : 'alpha', url);
     });
 
-    const pluginsLoadedStub = sandbox.stub();
-    stub('gr-reporting', {
-      pluginsLoaded: (...args) => pluginsLoadedStub(...args),
-    });
+    const pluginsLoadedStub = sandbox.stub(pluginLoader._getReporting(),
+        'pluginsLoaded');
 
     pluginLoader.loadPlugins(plugins);
 
@@ -308,10 +296,8 @@
     sandbox.stub(pluginLoader, '_loadJsPlugin', url => {
       pluginApi.install(() => void 0, undefined, url);
     });
-    const pluginsLoadedStub = sandbox.stub();
-    stub('gr-reporting', {
-      pluginsLoaded: (...args) => pluginsLoadedStub(...args),
-    });
+    const pluginsLoadedStub = sandbox.stub(pluginLoader._getReporting(),
+        'pluginsLoaded');
 
     const plugins = [
       'http://test.com/plugins/foo/static/test.js',
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.js b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.js
index 2c5589f..c003712 100644
--- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.js
+++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/gr-voting-styles.js';
 import '../../../styles/shared-styles.js';
 import '../gr-account-label/gr-account-label.js';
@@ -31,7 +29,7 @@
 import {htmlTemplate} from './gr-label-info_html.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrLabelInfo extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
@@ -131,7 +129,7 @@
 
   /**
    * Closure annotation for Polymer.prototype.splice is off.
-   * For now, supressing annotations.
+   * For now, suppressing annotations.
    *
    * @suppress {checkTypes} */
   _onDeleteVote(e) {
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.html b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.html
index a340550..94cfaea 100644
--- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.html
@@ -212,7 +212,7 @@
     let score = '0';
     assert.equal(element._computeValueTooltip(labelInfo, score), 'Baz');
 
-    // Non-exsistent score.
+    // Non-existent score.
     score = '2';
     assert.equal(element._computeValueTooltip(labelInfo, score), '');
 
diff --git a/polygerrit-ui/app/elements/shared/gr-label/gr-label.js b/polygerrit-ui/app/elements/shared/gr-label/gr-label.js
index 014e85e..fa1f758 100644
--- a/polygerrit-ui/app/elements/shared/gr-label/gr-label.js
+++ b/polygerrit-ui/app/elements/shared/gr-label/gr-label.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
@@ -24,7 +22,7 @@
 import {TooltipBehavior} from '../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrLabel extends mixinBehaviors( [
   TooltipBehavior,
diff --git a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.js b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.js
index f585347..d17f7d8 100644
--- a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.js
+++ b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../gr-autocomplete/gr-autocomplete.js';
 import '../../../styles/shared-styles.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
@@ -23,7 +21,7 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-labeled-autocomplete_html.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrLabeledAutocomplete extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js
index 9bd8a11..e9cd441 100644
--- a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js
+++ b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../gr-js-api-interface/gr-js-api-interface.js';
 import {importHref} from '../../../scripts/import-href.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
@@ -27,7 +25,7 @@
 const HLJS_PATH = 'bower_components/highlightjs/highlight.min.js';
 const DARK_THEME_PATH = 'styles/themes/dark-theme.html';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrLibLoader extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.js b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.js
index b7bfbf3..1b5224f 100644
--- a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.js
+++ b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
@@ -29,7 +27,7 @@
  * configured limit, then an ellipsis indicates that the text was truncated
  * and a tooltip containing the full text is enabled.
  *
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrLimitedText extends mixinBehaviors( [
   TooltipBehavior,
@@ -67,7 +65,7 @@
       },
 
       /**
-       * The maximum number of characters to display in the tooltop.
+       * The maximum number of characters to display in the tooltip.
        */
       tooltipLimit: {
         type: Number,
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.js b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.js
index 077ca74..46588ef 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.js
+++ b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.js
@@ -14,7 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
 
 import '../gr-button/gr-button.js';
 import '../gr-icons/gr-icons.js';
@@ -26,7 +25,7 @@
 import {htmlTemplate} from './gr-linked-chip_html.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrLinkedChip extends GestureEventListeners(
     LegacyElementMixin(PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js
index 6ea4b78..b969d1d 100644
--- a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js
+++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/shared-styles.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
@@ -26,7 +24,7 @@
 import {GrLinkTextParser} from './link-text-parser.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrLinkedText extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js
index 7a44c67..595694c 100644
--- a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js
+++ b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-input/iron-input.js';
 import '@polymer/iron-icon/iron-icon.js';
 import '../../../styles/shared-styles.js';
@@ -32,7 +30,7 @@
 const REQUEST_DEBOUNCE_INTERVAL_MS = 200;
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrListView extends mixinBehaviors( [
   BaseUrlBehavior,
diff --git a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js
index a5a3fb4..5437ca5 100644
--- a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js
+++ b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import {IronOverlayBehaviorImpl, IronOverlayBehavior} from '@polymer/iron-overlay-behavior/iron-overlay-behavior.js';
 import '../../../styles/shared-styles.js';
 import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
@@ -29,7 +27,7 @@
 const BREAKPOINT_FULLSCREEN_OVERLAY = '50em';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrOverlay extends mixinBehaviors( [
   IronOverlayBehavior,
diff --git a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.js b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.js
index 8366463..f191981 100644
--- a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.js
+++ b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.js
@@ -14,14 +14,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
 import '../../../styles/shared-styles.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-page-nav_html.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrPageNav extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.js b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.js
index c499f56..9439c08 100644
--- a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.js
+++ b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '@polymer/iron-icon/iron-icon.js';
 import '../../../styles/shared-styles.js';
 import '../gr-icons/gr-icons.js';
@@ -32,7 +30,7 @@
 const REF_PREFIX = 'refs/heads/';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrRepoBranchPicker extends mixinBehaviors( [
   URLEncodingBehavior,
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.html
index 5fa476f..f919cbb 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.html
@@ -87,7 +87,7 @@
     });
   });
 
-  suite('cache and events behaivor', () => {
+  suite('cache and events behavior', () => {
     let fakeFetch;
     let clock;
     setup(() => {
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.js
index 23b8de7..7402e22 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 const $_documentContainer = document.createElement('template');
 
 $_documentContainer.innerHTML = `<dom-module id="gr-etag-decorator">
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 b675df7..2d951d4 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
@@ -16,12 +16,6 @@
  */
 /* NB: es6-promise Needed for IE11 and fetch polyfill support, see Issue 4308 */
 /* NB: Order is important, because of namespaced classes. */
-/*
-  FIXME(polymer-modulizer): the above comments were extracted
-  from HTML and may be out of place here. Review them and
-  then delete this comment!
-*/
-import '../../../scripts/bundled-polymer.js';
 
 import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
@@ -35,7 +29,7 @@
 import {GrEtagDecorator} from './gr-etag-decorator.js';
 import {SiteBasedCache, FetchPromisesCache, GrRestApiHelper} from './gr-rest-apis/gr-rest-api-helper.js';
 import {GrReviewerUpdatesParser} from './gr-reviewer-updates-parser.js';
-import {util} from '../../../scripts/util.js';
+import {parseDate} from '../../../utils/date-util.js';
 import {authService} from './gr-auth.js';
 
 const DiffViewMode = {
@@ -61,7 +55,7 @@
     '/revisions/*';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrRestApiInterface extends mixinBehaviors( [
   PathListBehavior,
@@ -2081,7 +2075,7 @@
   _setRanges(comments) {
     comments = comments || [];
     comments.sort(
-        (a, b) => util.parseDate(a.updated) - util.parseDate(b.updated)
+        (a, b) => parseDate(a.updated) - parseDate(b.updated)
     );
     for (const comment of comments) {
       this._setRange(comments, comment);
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser.js
index 3d1ce05..ed474d8 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser.js
@@ -15,7 +15,8 @@
  * limitations under the License.
  */
 
-import {util} from '../../../scripts/util.js';
+import {parseDate} from '../../../utils/date-util.js';
+import {MessageTag} from '../../../constants/constants.js';
 
 /** @constructor */
 export function GrReviewerUpdatesParser(change) {
@@ -52,9 +53,7 @@
  */
 GrReviewerUpdatesParser.prototype._filterRemovedMessages = function() {
   this.result.messages = this.result.messages
-      .filter(
-          message => message.tag !== 'autogenerated:gerrit:deleteReviewer'
-      );
+      .filter(message => message.tag !== MessageTag.TAG_DELETE_REVIEWER);
 };
 
 /**
@@ -68,6 +67,7 @@
     author: update.updated_by,
     date: update.updated,
     type: 'REVIEWER_UPDATE',
+    tag: MessageTag.TAG_REVIEWER_UPDATE,
   };
 };
 
@@ -106,8 +106,8 @@
     if (!this._batch) {
       this._batch = this._startBatch(update);
     }
-    const updateDate = util.parseDate(update.updated).getTime();
-    const batchUpdateDate = util.parseDate(this._batch.date).getTime();
+    const updateDate = parseDate(update.updated).getTime();
+    const batchUpdateDate = parseDate(this._batch.date).getTime();
     const reviewerId = update.reviewer._account_id.toString();
     if (updateDate - batchUpdateDate >
         GrReviewerUpdatesParser.REVIEWER_UPDATE_THRESHOLD_MILLIS ||
@@ -206,14 +206,14 @@
   const updates = this.result.reviewer_updates;
   const messages = this.result.messages;
   messages.forEach((message, index) => {
-    const messageDate = util.parseDate(message.date).getTime();
+    const messageDate = parseDate(message.date).getTime();
     const nextMessageDate = index === messages.length - 1 ? null :
-      util.parseDate(messages[index + 1].date).getTime();
+      parseDate(messages[index + 1].date).getTime();
     for (const update of updates) {
-      const date = util.parseDate(update.date).getTime();
+      const date = parseDate(update.date).getTime();
       if (date >= messageDate &&
           (!nextMessageDate || date < nextMessageDate)) {
-        const timestamp = util.parseDate(update.date).getTime() -
+        const timestamp = parseDate(update.date).getTime() -
             GrReviewerUpdatesParser.MESSAGE_REVIEWERS_THRESHOLD_MILLIS;
         update.date = new Date(timestamp)
             .toISOString()
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html
index f2ccfb7..fa54d6b 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html
@@ -27,7 +27,7 @@
 <script type="module">
 import '../../../test/common-test-setup.js';
 import {GrReviewerUpdatesParser} from './gr-reviewer-updates-parser.js';
-import {util} from '../../../scripts/util.js';
+import {parseDate} from '../../../utils/date-util.js';
 
 suite('gr-reviewer-updates-parser tests', () => {
   let sandbox;
@@ -253,7 +253,7 @@
   });
 
   test('_advanceUpdates', () => {
-    const T0 = util.parseDate('2017-02-17 19:04:18.000000000').getTime();
+    const T0 = parseDate('2017-02-17 19:04:18.000000000').getTime();
     const tplus = delta => new Date(T0 + delta)
         .toISOString()
         .replace('T', ' ')
@@ -297,8 +297,8 @@
     instance = new GrReviewerUpdatesParser(change);
     instance._advanceUpdates();
     const updates = instance.result.reviewer_updates;
-    assert.isBelow(util.parseDate(updates[0].date).getTime(), T0);
-    assert.isBelow(util.parseDate(updates[1].date).getTime(), T0);
+    assert.isBelow(parseDate(updates[0].date).getTime(), T0);
+    assert.isBelow(parseDate(updates[1].date).getTime(), T0);
     assert.equal(updates[2].date, tplus(100));
     assert.equal(updates[3].date, tplus(500));
   });
diff --git a/polygerrit-ui/app/elements/shared/gr-select/gr-select.js b/polygerrit-ui/app/elements/shared/gr-select/gr-select.js
index e061e93..c89378c 100644
--- a/polygerrit-ui/app/elements/shared/gr-select/gr-select.js
+++ b/polygerrit-ui/app/elements/shared/gr-select/gr-select.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
@@ -29,7 +27,7 @@
 document.head.appendChild($_documentContainer.content);
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrSelect extends GestureEventListeners(
     LegacyElementMixin(PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.js b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.js
index 151498c..a5212fc 100644
--- a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.js
+++ b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/shared-styles.js';
 import '../gr-copy-clipboard/gr-copy-clipboard.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
@@ -23,7 +21,7 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-shell-command_html.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrShellCommand extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js
index 8f5c486..90fdcd3 100644
--- a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js
+++ b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
@@ -32,7 +30,7 @@
   'editablecontent:': DURATION_DAY,
 };
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrStorage extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js
index 15ab8e4..e98e11d 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js
@@ -14,15 +14,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../gr-autocomplete-dropdown/gr-autocomplete-dropdown.js';
 import '../gr-cursor-manager/gr-cursor-manager.js';
 import '../gr-overlay/gr-overlay.js';
 import '@polymer/iron-a11y-keys-behavior/iron-a11y-keys-behavior.js';
 import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
 import '../../../styles/shared-styles.js';
-import '../../core/gr-reporting/gr-reporting.js';
 import {flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
@@ -30,6 +27,7 @@
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-textarea_html.js';
 import {KeyboardShortcutBehavior} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import {appContext} from '../../../services/app-context.js';
 
 const MAX_ITEMS_DROPDOWN = 10;
 
@@ -67,7 +65,7 @@
 ];
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrTextarea extends mixinBehaviors( [
   KeyboardShortcutBehavior,
@@ -139,6 +137,11 @@
     };
   }
 
+  constructor() {
+    super();
+    this.reporting = appContext.reportingService;
+  }
+
   /** @override */
   ready() {
     super.ready();
@@ -214,7 +217,7 @@
     this.text = this._getText(text);
     this.$.textarea.selectionStart = colonIndex + 1;
     this.$.textarea.selectionEnd = colonIndex + 1;
-    this.$.reporting.reportInteraction('select-emoji', {type: text});
+    this.reporting.reportInteraction('select-emoji', {type: text});
     this._resetEmojiDropdown();
   }
 
@@ -302,7 +305,7 @@
 
   _openEmojiDropdown() {
     this.$.emojiSuggestions.open();
-    this.$.reporting.reportInteraction('open-emoji-dropdown');
+    this.reporting.reportInteraction('open-emoji-dropdown');
   }
 
   _formatSuggestions(matchedSuggestions) {
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_html.js b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_html.js
index 61e530a..8454f62 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_html.js
@@ -90,5 +90,4 @@
     value="{{text}}"
     on-bind-value-changed="_onValueChanged"
   ></iron-autogrow-textarea>
-  <gr-reporting id="reporting"></gr-reporting>
 `;
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html
index c33b2ae..bcf1207 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html
@@ -53,7 +53,7 @@
   setup(() => {
     sandbox = sinon.sandbox.create();
     element = fixture('basic');
-    sandbox.stub(element.$.reporting, 'reportInteraction');
+    sandbox.stub(element.reporting, 'reportInteraction');
   });
 
   teardown(() => {
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.js b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.js
index 160f50a..502c82e 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.js
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.js
@@ -14,8 +14,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../gr-icons/gr-icons.js';
 import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
@@ -25,7 +23,7 @@
 import {TooltipBehavior} from '../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js';
 
 /**
- * @extends Polymer.Element
+ * @extends PolymerElement
  */
 class GrTooltipContent extends mixinBehaviors( [
   TooltipBehavior,
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.js b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.js
index 0cd2d7c..2244cca 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.js
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.js
@@ -14,15 +14,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
 import '../../../styles/shared-styles.js';
 import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-tooltip_html.js';
 
-/** @extends Polymer.Element */
+/** @extends PolymerElement */
 class GrTooltip extends GestureEventListeners(
     LegacyElementMixin(
         PolymerElement)) {
diff --git a/polygerrit-ui/app/embed/README.md b/polygerrit-ui/app/embed/README.md
index bef098b..8860878 100644
--- a/polygerrit-ui/app/embed/README.md
+++ b/polygerrit-ui/app/embed/README.md
@@ -1,4 +1,4 @@
-This folder contains shared components that can be used independently from Gerrit.
+This folder contains shared components that can be used independently of Gerrit.
 
 ### gr-diff
 
@@ -10,4 +10,4 @@
 
 All supported attributes defined in `polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js`, you can pass them by just assigning them to the `gr-app` element.
 
-To customize the style of the diff, you can use `css variables`, all supported varibled defined in `polygerrit-ui/app/styles/themes/app-theme.html` and `polygerrit-ui/app/styles/themes/dark-theme.html`.
+To customize the style of the diff, you can use `css variables`, all supported variables defined in `polygerrit-ui/app/styles/themes/app-theme.html` and `polygerrit-ui/app/styles/themes/dark-theme.html`.
diff --git a/polygerrit-ui/app/embed/app-context-init.js b/polygerrit-ui/app/embed/gr-diff-app-context-init.js
similarity index 75%
rename from polygerrit-ui/app/embed/app-context-init.js
rename to polygerrit-ui/app/embed/gr-diff-app-context-init.js
index 55a5866..8a7fb66 100644
--- a/polygerrit-ui/app/embed/app-context-init.js
+++ b/polygerrit-ui/app/embed/gr-diff-app-context-init.js
@@ -16,6 +16,7 @@
  */
 
 import {appContext} from '../services/app-context.js';
+import {grReportingMock} from '../services/gr-reporting/gr-reporting_mock.js';
 
 class MockFlagsService {
   isEnabled(experimentId) {
@@ -34,5 +35,13 @@
 // This is a temporary solution
 // TODO(dmfilippov): find a better solution for gr-diff
 export function initDiffAppContext() {
-  appContext.flagsService = new MockFlagsService();
+  function setMock(serviceName, setupMock) {
+    Object.defineProperty(appContext, serviceName, {
+      get() {
+        return setupMock;
+      },
+    });
+  }
+  setMock('flagsService', new MockFlagsService);
+  setMock('reportingService', grReportingMock);
 }
diff --git a/polygerrit-ui/app/embed/gr-diff-app-context-init_test.html b/polygerrit-ui/app/embed/gr-diff-app-context-init_test.html
new file mode 100644
index 0000000..3aed13f
--- /dev/null
+++ b/polygerrit-ui/app/embed/gr-diff-app-context-init_test.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<!--
+@license
+Copyright (C) 2020 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
+
+<script type="module">
+  import '../test/common-test-setup.js';
+  import {appContext} from '../services/app-context.js';
+  import {initDiffAppContext} from './gr-diff-app-context-init.js';
+  suite('gr diff app context initializer tests', () => {
+    setup(() => {
+      initDiffAppContext();
+    });
+
+    test('all services initialized and are singletons', () => {
+      Object.keys(appContext).forEach(serviceName => {
+        const service = appContext[serviceName];
+        assert.isNotNull(service);
+        const service2 = appContext[serviceName];
+        assert.strictEqual(service, service2);
+      });
+    });
+  });
+</script>
\ No newline at end of file
diff --git a/polygerrit-ui/app/embed/gr-diff.js b/polygerrit-ui/app/embed/gr-diff.js
index a8b7e03..5907842 100644
--- a/polygerrit-ui/app/embed/gr-diff.js
+++ b/polygerrit-ui/app/embed/gr-diff.js
@@ -16,9 +16,16 @@
  */
 
 window.Gerrit = window.Gerrit || {};
+// TODO(dmfilippov): remove bundled-polymer.js imports when the following issue
+// https://github.com/Polymer/polymer-resin/issues/9 is resolved.
+// Because gr-diff.js is a shared component, it shouldn' pollute global
+// variables. If an application wants to use Polymer global variable -
+// the app must assign/import it and do not rely on the Polymer variable
+// exposed by shared gr-diff component.
+import '../scripts/bundled-polymer.js';
 import '../elements/diff/gr-diff/gr-diff.js';
 import '../elements/diff/gr-diff-cursor/gr-diff-cursor.js';
-import {initDiffAppContext} from './app-context-init.js';
+import {initDiffAppContext} from './gr-diff-app-context-init.js';
 import {GrDiffLine} from '../elements/diff/gr-diff/gr-diff-line.js';
 import {GrAnnotation} from '../elements/diff/gr-diff-highlight/gr-annotation.js';
 
diff --git a/polygerrit-ui/app/node_modules_licenses/licenses.ts b/polygerrit-ui/app/node_modules_licenses/licenses.ts
index fe07569..4c259cd 100644
--- a/polygerrit-ui/app/node_modules_licenses/licenses.ts
+++ b/polygerrit-ui/app/node_modules_licenses/licenses.ts
@@ -273,14 +273,6 @@
     license: SharedLicenses.IsArray
   },
   {
-    name: "moment",
-    license: {
-      name: "moment",
-      type: LicenseTypes.Mit,
-      packageLicenseFile: "LICENSE"
-    }
-  },
-  {
     name: "page",
     license: SharedLicenses.Page
   },
diff --git a/polygerrit-ui/app/package.json b/polygerrit-ui/app/package.json
index 5989493..a9668c3 100644
--- a/polygerrit-ui/app/package.json
+++ b/polygerrit-ui/app/package.json
@@ -26,7 +26,6 @@
     "@webcomponents/shadycss": "^1.9.2",
     "@webcomponents/webcomponentsjs": "^1.3.3",
     "es6-promise": "^3.3.1",
-    "moment": "^2.24.0",
     "page": "^1.11.5",
     "polymer-bridges": "file:../../polymer-bridges/",
     "ba-linkify": "file:../../lib/ba-linkify/src/",
diff --git a/polygerrit-ui/app/rules.bzl b/polygerrit-ui/app/rules.bzl
index 9303f2b..bc2830d 100644
--- a/polygerrit-ui/app/rules.bzl
+++ b/polygerrit-ui/app/rules.bzl
@@ -124,13 +124,14 @@
         tags = [
             "local",
             "manual",
+            "wct",
         ],
     )
 
 def wct_suite(name, srcs, split_count):
     """Define test suites for WCT tests.
 
-    All tests files are splited to split_count WCT suites
+    All tests files are split to split_count WCT suites
 
     Args:
         name: rule name. The macro create a test suite rule with the name name+"_test"
diff --git a/polygerrit-ui/app/run_test.sh b/polygerrit-ui/app/run_test.sh
index 5f61de7..a5231a1 100755
--- a/polygerrit-ui/app/run_test.sh
+++ b/polygerrit-ui/app/run_test.sh
@@ -14,4 +14,5 @@
       --test_env="DISPLAY=${DISPLAY}" \
       --test_env="WCT_HEADLESS_MODE=${WCT_HEADLESS_MODE}" \
       "$@" \
-      //polygerrit-ui/app:wct_test
+      //polygerrit-ui/app:wct_test \
+      //polygerrit-ui:karma_test
diff --git a/polygerrit-ui/app/scripts/bundled-polymer.js b/polygerrit-ui/app/scripts/bundled-polymer.js
index 711d587..780d82a 100644
--- a/polygerrit-ui/app/scripts/bundled-polymer.js
+++ b/polygerrit-ui/app/scripts/bundled-polymer.js
@@ -17,9 +17,9 @@
 
 // This file is a replacement for the
 // polymer-bridges/polymer/polymer.html file. The polymer.html file loads
-// other scripts to setup different global variables. Because polygerrit
-// code still uses global variables (like Polymer.importHref and other),
-// we must setup this global variables after conversion to es6 modules.
+// other scripts to setup different global variables. Because plugins
+// expects that Polymer is available we must setup all Polymer global
+// variables
 //
 // The bundled-polymer.js imports all scripts in the same order as the
 // polymer.html does and must be imported in all es6-modules instead
@@ -68,4 +68,4 @@
 import 'polymer-bridges/polymer/polymer-legacy_bridge.js';
 import {importHref} from './import-href.js';
 
-Polymer.importHref = importHref;
+window.Polymer.importHref = importHref;
diff --git a/polygerrit-ui/app/scripts/util.js b/polygerrit-ui/app/scripts/util.js
index 46fa1ad..0103bf2 100644
--- a/polygerrit-ui/app/scripts/util.js
+++ b/polygerrit-ui/app/scripts/util.js
@@ -29,14 +29,6 @@
 // TODO (dmfilippov): Each function must be exported separately. According to
 // the code style guide, a namespacing is not allowed.
 export const util = {
-  parseDate(dateStr) {
-    // Timestamps are given in UTC and have the format
-    // "'yyyy-mm-dd hh:mm:ss.fffffffff'" where "'ffffffffff'" represents
-    // nanoseconds.
-    // Munge the date into an ISO 8061 format and parse that.
-    return new Date(dateStr.replace(' ', 'T') + 'Z');
-  },
-
   getCookie(name) {
     const key = name + '=';
     const cookies = document.cookie.split(';');
@@ -89,7 +81,7 @@
    * Get computed style value.
    *
    * If ShadyCSS is provided, use ShadyCSS api.
-   * If `getComputedStyleValue` is provided on the elment, use it.
+   * If `getComputedStyleValue` is provided on the element, use it.
    * Otherwise fallback to native method (in polymer 2).
    *
    */
diff --git a/polygerrit-ui/app/services/app-context-init.js b/polygerrit-ui/app/services/app-context-init.js
index 1c32eee..983314e 100644
--- a/polygerrit-ui/app/services/app-context-init.js
+++ b/polygerrit-ui/app/services/app-context-init.js
@@ -16,14 +16,15 @@
  */
 import {appContext} from './app-context.js';
 import {FlagsService} from './flags.js';
+import {GrReporting} from './gr-reporting/gr-reporting.js';
 
 const initializedServices = new Map();
 
 function getService(serviceName, serviceInit) {
-  if (!initializedServices[serviceName]) {
-    initializedServices[serviceName] = serviceInit();
+  if (!initializedServices.has(serviceName)) {
+    initializedServices.set(serviceName, serviceInit());
   }
-  return initializedServices[serviceName];
+  return initializedServices.get(serviceName);
 }
 
 /**
@@ -43,6 +44,8 @@
   }
 
   addService('flagsService', () => new FlagsService());
+  addService('reportingService',
+      () => new GrReporting(appContext.flagsService));
 
   Object.defineProperties(appContext, registeredServices);
 }
diff --git a/polygerrit-ui/app/services/app-context.js b/polygerrit-ui/app/services/app-context.js
index e10ced5..62b396d 100644
--- a/polygerrit-ui/app/services/app-context.js
+++ b/polygerrit-ui/app/services/app-context.js
@@ -23,4 +23,5 @@
  */
 export const appContext = {
   flagsService: null,
+  reportingService: null,
 };
\ No newline at end of file
diff --git a/polygerrit-ui/app/services/flags.js b/polygerrit-ui/app/services/flags.js
index 8f04f4a..64f0115 100644
--- a/polygerrit-ui/app/services/flags.js
+++ b/polygerrit-ui/app/services/flags.js
@@ -16,6 +16,15 @@
  */
 
 /**
+ * @enum
+ * @desc Experiment ids used in Gerrit.
+ */
+export const ExperimentIds = {
+  CLEANER_CHANGELOG: 'UiFeature__cleaner_changelog',
+  PATCHSET_COMMENTS: 'UiFeature__patchset_comments',
+};
+
+/**
  * Flags service.
  *
  * Provides all related methods / properties regarding on feature flags.
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js b/polygerrit-ui/app/services/gr-reporting/gr-reporting.js
similarity index 65%
rename from polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
rename to polygerrit-ui/app/services/gr-reporting/gr-reporting.js
index c8b4ff5..42d112e 100644
--- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting.js
@@ -14,49 +14,53 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import '../../../scripts/bundled-polymer.js';
-
-import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
-import {appContext} from '../../../services/app-context.js';
 
 // Latency reporting constants.
+
 const TIMING = {
   TYPE: 'timing-report',
-  CATEGORY_UI_LATENCY: 'UI Latency',
-  CATEGORY_RPC: 'RPC Timing',
-  // Reported events - alphabetize below.
-  APP_STARTED: 'App Started',
+  CATEGORY: {
+    UI_LATENCY: 'UI Latency',
+    RPC: 'RPC Timing',
+  },
+  EVENT: {
+    APP_STARTED: 'App Started',
+  },
 };
 
-// Plugin-related reporting constants.
-const PLUGINS = {
+const LIFECYCLE = {
   TYPE: 'lifecycle',
-  // Reported events - alphabetize below.
-  INSTALLED: 'Plugins installed',
+  CATEGORY: {
+    DEFAULT: 'Default',
+    EXTENSION_DETECTED: 'Extension detected',
+    PLUGINS_INSTALLED: 'Plugins installed',
+  },
 };
 
-// Chrome extension-related reporting constants.
-const EXTENSION = {
-  TYPE: 'lifecycle',
-  // Reported events - alphabetize below.
-  DETECTED: 'Extension detected',
+const INTERACTION = {
+  TYPE: 'interaction',
+  CATEGORY: {
+    DEFAULT: 'Default',
+    VISIBILITY: 'Visibility',
+  },
 };
 
-// Navigation reporting constants.
 const NAVIGATION = {
   TYPE: 'nav-report',
-  CATEGORY: 'Location Changed',
-  PAGE: 'Page',
+  CATEGORY: {
+    LOCATION_CHANGED: 'Location Changed',
+  },
+  EVENT: {
+    PAGE: 'Page',
+  },
 };
 
 const ERROR = {
   TYPE: 'error',
-  CATEGORY: 'exception',
-};
-
-const ERROR_DIALOG = {
-  TYPE: 'error',
-  CATEGORY: 'Error Dialog',
+  CATEGORY: {
+    EXCEPTION: 'exception',
+    ERROR_DIALOG: 'Error Dialog',
+  },
 };
 
 const TIMER = {
@@ -89,125 +93,158 @@
 STARTUP_TIMERS[TIMER.STARTUP_DIFF_VIEW_DISPLAYED] = 0;
 STARTUP_TIMERS[TIMER.STARTUP_DIFF_VIEW_LOAD_FULL] = 0;
 STARTUP_TIMERS[TIMER.STARTUP_FILE_LIST_DISPLAYED] = 0;
-STARTUP_TIMERS[TIMING.APP_STARTED] = 0;
+STARTUP_TIMERS[TIMING.EVENT.APP_STARTED] = 0;
 // WebComponentsReady timer is triggered from gr-router.
 STARTUP_TIMERS[TIMER.WEB_COMPONENTS_READY] = 0;
 
-const INTERACTION_TYPE = 'interaction';
-
 const DRAFT_ACTION_TIMER = 'TimeBetweenDraftActions';
 const DRAFT_ACTION_TIMER_MAX = 2 * 60 * 1000; // 2 minutes.
-
-let pending = [];
-let slowRpcList = [];
 const SLOW_RPC_THRESHOLD = 500;
 
-// Variables that hold context info in global scope
-let reportRepoName = undefined;
-
-const onError = function(oldOnError, msg, url, line, column, error) {
-  if (oldOnError) {
-    oldOnError(msg, url, line, column, error);
-  }
-  if (error) {
-    line = line || error.lineNumber;
-    column = column || error.columnNumber;
-    let shortenedErrorStack = msg;
-    if (error.stack) {
-      const errorStackLines = error.stack.split('\n');
-      shortenedErrorStack = errorStackLines.slice(0,
-          Math.min(3, errorStackLines.length)).join('\n');
+export function initErrorReporter(appContext) {
+  const reportingService = appContext.reportingService;
+  const onError = function(oldOnError, msg, url, line, column, error) {
+    if (oldOnError) {
+      oldOnError(msg, url, line, column, error);
     }
-    msg = shortenedErrorStack || error.toString();
-  }
-  const payload = {
-    url,
-    line,
-    column,
-    error,
-  };
-  GrReporting.prototype.reporter(ERROR.TYPE, ERROR.CATEGORY, msg, payload);
-  return true;
-};
-
-const catchErrors = function(opt_context) {
-  const context = opt_context || window;
-  context.onerror = onError.bind(null, context.onerror);
-  context.addEventListener('unhandledrejection', e => {
-    const msg = e.reason.message;
-    const payload = {
-      error: e.reason,
-    };
-    GrReporting.prototype.reporter(ERROR.TYPE, ERROR.CATEGORY, msg, payload);
-  });
-};
-catchErrors();
-
-// PerformanceObserver interface is a browser API.
-if (window.PerformanceObserver) {
-  const supportedEntryTypes = PerformanceObserver.supportedEntryTypes || [];
-  // Safari doesn't support longtask yet
-  if (supportedEntryTypes.includes('longtask')) {
-    const catchLongJsTasks = new PerformanceObserver(list => {
-      for (const task of list.getEntries()) {
-        // We are interested in longtask longer than 200 ms (default is 50 ms)
-        if (task.duration > 200) {
-          GrReporting.prototype.reporter(TIMING.TYPE,
-              TIMING.CATEGORY_UI_LATENCY, `Task ${task.name}`,
-              Math.round(task.duration), {}, false);
-        }
+    if (error) {
+      line = line || error.lineNumber;
+      column = column || error.columnNumber;
+      let shortenedErrorStack = msg;
+      if (error.stack) {
+        const errorStackLines = error.stack.split('\n');
+        shortenedErrorStack = errorStackLines.slice(0,
+            Math.min(3, errorStackLines.length)).join('\n');
       }
+      msg = shortenedErrorStack || error.toString();
+    }
+    const payload = {
+      url,
+      line,
+      column,
+      error,
+    };
+    reportingService.reporter(ERROR.TYPE, ERROR.CATEGORY.EXCEPTION,
+        msg, payload);
+    return true;
+  };
+
+  const catchErrors = function(opt_context) {
+    const context = opt_context || window;
+    context.onerror = onError.bind(null, context.onerror);
+    context.addEventListener('unhandledrejection', e => {
+      const msg = e.reason.message;
+      const payload = {
+        error: e.reason,
+      };
+      reportingService.reporter(ERROR.TYPE,
+          ERROR.CATEGORY.EXCEPTION, msg, payload);
     });
-    catchLongJsTasks.observe({entryTypes: ['longtask']});
+  };
+
+  catchErrors();
+
+  // for testing
+  return {catchErrors};
+}
+
+export function initPerformanceReporter(appContext) {
+  const reportingService = appContext.reportingService;
+  // PerformanceObserver interface is a browser API.
+  if (window.PerformanceObserver) {
+    const supportedEntryTypes = PerformanceObserver.supportedEntryTypes || [];
+    // Safari doesn't support longtask yet
+    if (supportedEntryTypes.includes('longtask')) {
+      const catchLongJsTasks = new PerformanceObserver(list => {
+        for (const task of list.getEntries()) {
+          // We are interested in longtask longer than 200 ms (default is 50 ms)
+          if (task.duration > 200) {
+            reportingService.reporter(TIMING.TYPE,
+                TIMING.CATEGORY.UI_LATENCY, `Task ${task.name}`,
+                Math.round(task.duration), {}, false);
+          }
+        }
+      });
+      catchLongJsTasks.observe({entryTypes: ['longtask']});
+    }
   }
 }
 
-document.addEventListener('visibilitychange', () => {
-  const eventName = `Visibility changed to ${document.visibilityState}`;
-  GrReporting.prototype.reporter(INTERACTION_TYPE, undefined, eventName,
-      undefined, {}, true);
-});
+export function initVisibilityReporter(appContext) {
+  const reportingService = appContext.reportingService;
+  document.addEventListener('visibilitychange', () => {
+    reportingService.onVisibilityChange();
+  });
+}
 
-// The Polymer pass of JSCompiler requires this to be reassignable
-// eslint-disable-next-line prefer-const
-let GrReporting = Polymer({
-  is: 'gr-reporting',
+// Calculates the time of Gerrit being in a background tab. When Gerrit reports
+// a pageLoad metric it’s attached to its details for latency analysis.
+// It resets on locationChange.
+class HiddenDurationTimer {
+  constructor() {
+    this.reset();
+  }
 
-  properties: {
-    category: String,
+  reset() {
+    this.accHiddenDurationMs = 0;
+    this.lastVisibleTimestampMs = 0;
+  }
 
-    _baselines: {
-      type: Object,
-      value: STARTUP_TIMERS, // Shared across all instances.
-    },
+  onVisibilityChange() {
+    if (document.visibilityState === 'hidden') {
+      this.lastVisibleTimestampMs = now();
+    } else if (document.visibilityState === 'visible') {
+      if (this.lastVisibleTimestampMs !== null) {
+        this.accHiddenDurationMs += now() - this.lastVisibleTimestampMs;
+        // Set to null for guarding against two 'visible' events in a row.
+        this.lastVisibleTimestampMs = null;
+      }
+    }
+  }
 
-    _timers: {
-      type: Object,
-      value: {timeBetweenDraftActions: null}, // Shared across all instances.
-    },
-  },
+  get hiddenDurationMs() {
+    if (document.visibilityState === 'hidden'
+      && this.lastVisibleTimestampMs !== null) {
+      return this.accHiddenDurationMs + now() - this.lastVisibleTimestampMs;
+    }
+    return this.accHiddenDurationMs;
+  }
+}
+
+export function now() {
+  return Math.round(window.performance.now());
+}
+
+export class GrReporting {
+  constructor(flagsService) {
+    this._flagsService = flagsService;
+    this._baselines = STARTUP_TIMERS;
+    this._timers = {
+      timeBetweenDraftActions: null,
+    };
+    this._reportRepoName = undefined;
+    this._pending = [];
+    this._slowRpcList = [];
+    this.hiddenDurationTimer = new HiddenDurationTimer();
+  }
 
   get performanceTiming() {
     return window.performance.timing;
-  },
+  }
 
   get slowRpcSnapshot() {
-    return slowRpcList.slice();
-  },
-
-  now() {
-    return Math.round(window.performance.now());
-  },
+    return (this._slowRpcList || []).slice();
+  }
 
   _arePluginsLoaded() {
     return this._baselines &&
       !this._baselines.hasOwnProperty(TIMER.PLUGINS_LOADED);
-  },
+  }
 
   _isMetricsPluginLoaded() {
     return this._arePluginsLoaded() || this._baselines &&
       !this._baselines.hasOwnProperty(TIMER.METRICS_PLUGIN_LOADED);
-  },
+  }
 
   /**
    * Reporter reports events. Events will be queued if metrics plugin is not
@@ -224,24 +261,24 @@
   reporter(type, category, eventName, eventValue, eventDetails, opt_noLog) {
     const eventInfo = this._createEventInfo(type, category,
         eventName, eventValue, eventDetails);
-    if (type === ERROR.TYPE && category === ERROR.CATEGORY) {
+    if (type === ERROR.TYPE && category === ERROR.CATEGORY.EXCEPTION) {
       console.error(eventValue && eventValue.error || eventName);
     }
 
     // We report events immediately when metrics plugin is loaded
-    if (this._isMetricsPluginLoaded() && !pending.length) {
+    if (this._isMetricsPluginLoaded() && !this._pending.length) {
       this._reportEvent(eventInfo, opt_noLog);
     } else {
       // We cache until metrics plugin is loaded
-      pending.push([eventInfo, opt_noLog]);
+      this._pending.push([eventInfo, opt_noLog]);
       if (this._isMetricsPluginLoaded()) {
-        pending.forEach(([eventInfo, opt_noLog]) => {
+        this._pending.forEach(([eventInfo, opt_noLog]) => {
           this._reportEvent(eventInfo, opt_noLog);
         });
-        pending = [];
+        this._pending = [];
       }
     }
-  },
+  }
 
   _reportEvent(eventInfo, opt_noLog) {
     const {type, value, name} = eventInfo;
@@ -254,7 +291,7 @@
         console.log(`Reporting: ${name}`);
       }
     }
-  },
+  }
 
   _createEventInfo(type, category, name, value, eventDetails) {
     const eventInfo = {
@@ -262,7 +299,7 @@
       category,
       name,
       value,
-      eventStart: this.now(),
+      eventStart: now(),
     };
 
     if (typeof(eventDetails) === 'object' &&
@@ -270,8 +307,8 @@
       eventInfo.eventDetails = JSON.stringify(eventDetails);
     }
 
-    if (reportRepoName) {
-      eventInfo.repoName = reportRepoName;
+    if (this._reportRepoName) {
+      eventInfo.repoName = this._reportRepoName;
     }
 
     const isInBackgroundTab = document.visibilityState === 'hidden';
@@ -279,21 +316,30 @@
       eventInfo.inBackgroundTab = isInBackgroundTab;
     }
 
-    const enabledExperiments = appContext.flagsService.enabledExperiments;
-    if (enabledExperiments.length) {
-      eventInfo.enabledExperiments = JSON.stringify(enabledExperiments);
+    if (this._flagsService.enabledExperiments.length) {
+      eventInfo.enabledExperiments =
+        JSON.stringify(this._flagsService.enabledExperiments);
     }
 
     return eventInfo;
-  },
+  }
 
   /**
    * User-perceived app start time, should be reported when the app is ready.
    */
   appStarted() {
-    this.timeEnd(TIMING.APP_STARTED);
+    this.timeEnd(TIMING.EVENT.APP_STARTED);
     this._reportNavResTimes();
-  },
+  }
+
+  onVisibilityChange() {
+    this.hiddenDurationTimer.onVisibilityChange();
+    const eventName = `Visibility changed to ${document.visibilityState}`;
+    this.reporter(LIFECYCLE.TYPE, LIFECYCLE.CATEGORY.VISIBILITY,
+        eventName, undefined, {
+          hiddenDurationMs: this.hiddenDurationTimer.hiddenDurationMs,
+        }, true);
+  }
 
   /**
    * Browser's navigation and resource timings
@@ -303,7 +349,7 @@
     perfEvents.forEach(
         eventName => this._reportPerformanceTiming(eventName)
     );
-  },
+  }
 
   _reportPerformanceTiming(eventName, eventDetails) {
     const eventTiming = this.performanceTiming[eventName];
@@ -311,10 +357,10 @@
       const elapsedTime = eventTiming -
           this.performanceTiming.navigationStart;
       // NavResTime - Navigation and resource timings.
-      this.reporter(TIMING.TYPE, TIMING.CATEGORY_UI_LATENCY,
+      this.reporter(TIMING.TYPE, TIMING.CATEGORY.UI_LATENCY,
           `NavResTime - ${eventName}`, elapsedTime, eventDetails, true);
     }
-  },
+  }
 
   beforeLocationChanged() {
     for (const prop of Object.keys(this._baselines)) {
@@ -327,15 +373,16 @@
     this.time(TIMER.DIFF_VIEW_DISPLAYED);
     this.time(TIMER.DIFF_VIEW_LOAD_FULL);
     this.time(TIMER.FILE_LIST_DISPLAYED);
-    reportRepoName = undefined;
+    this._reportRepoName = undefined;
     // reset slow rpc list since here start page loads which report these rpcs
-    slowRpcList = [];
-  },
+    this._slowRpcList = [];
+    this.hiddenDurationTimer.reset();
+  }
 
   locationChanged(page) {
-    this.reporter(
-        NAVIGATION.TYPE, NAVIGATION.CATEGORY, NAVIGATION.PAGE, page);
-  },
+    this.reporter(NAVIGATION.TYPE, NAVIGATION.CATEGORY.LOCATION_CHANGED,
+        NAVIGATION.EVENT.PAGE, page);
+  }
 
   dashboardDisplayed() {
     if (this._baselines.hasOwnProperty(TIMER.STARTUP_DASHBOARD_DISPLAYED)) {
@@ -343,7 +390,7 @@
     } else {
       this.timeEnd(TIMER.DASHBOARD_DISPLAYED, this._pageLoadDetails());
     }
-  },
+  }
 
   changeDisplayed() {
     if (this._baselines.hasOwnProperty(TIMER.STARTUP_CHANGE_DISPLAYED)) {
@@ -351,7 +398,7 @@
     } else {
       this.timeEnd(TIMER.CHANGE_DISPLAYED, this._pageLoadDetails());
     }
-  },
+  }
 
   changeFullyLoaded() {
     if (this._baselines.hasOwnProperty(TIMER.STARTUP_CHANGE_LOAD_FULL)) {
@@ -359,7 +406,7 @@
     } else {
       this.timeEnd(TIMER.CHANGE_LOAD_FULL);
     }
-  },
+  }
 
   diffViewDisplayed() {
     if (this._baselines.hasOwnProperty(TIMER.STARTUP_DIFF_VIEW_DISPLAYED)) {
@@ -367,7 +414,7 @@
     } else {
       this.timeEnd(TIMER.DIFF_VIEW_DISPLAYED, this._pageLoadDetails());
     }
-  },
+  }
 
   diffViewFullyLoaded() {
     if (this._baselines.hasOwnProperty(TIMER.STARTUP_DIFF_VIEW_LOAD_FULL)) {
@@ -375,7 +422,7 @@
     } else {
       this.timeEnd(TIMER.DIFF_VIEW_LOAD_FULL);
     }
-  },
+  }
 
   diffViewContentDisplayed() {
     if (this._baselines.hasOwnProperty(
@@ -384,7 +431,7 @@
     } else {
       this.timeEnd(TIMER.DIFF_VIEW_CONTENT_DISPLAYED);
     }
-  },
+  }
 
   fileListDisplayed() {
     if (this._baselines.hasOwnProperty(TIMER.STARTUP_FILE_LIST_DISPLAYED)) {
@@ -392,7 +439,7 @@
     } else {
       this.timeEnd(TIMER.FILE_LIST_DISPLAYED);
     }
-  },
+  }
 
   _pageLoadDetails() {
     const details = {
@@ -419,33 +466,35 @@
         toMb(window.performance.memory.usedJSHeapSize);
     }
 
+    details.hiddenDurationMs = this.hiddenDurationTimer.hiddenDurationMs;
     return details;
-  },
+  }
 
   reportExtension(name) {
-    this.reporter(EXTENSION.TYPE, EXTENSION.DETECTED, name);
-  },
+    this.reporter(LIFECYCLE.TYPE, LIFECYCLE.CATEGORY.EXTENSION_DETECTED, name);
+  }
 
   pluginLoaded(name) {
     if (name.startsWith('metrics-')) {
       this.timeEnd(TIMER.METRICS_PLUGIN_LOADED);
     }
-  },
+  }
 
   pluginsLoaded(pluginsList) {
     this.timeEnd(TIMER.PLUGINS_LOADED);
     this.reporter(
-        PLUGINS.TYPE, PLUGINS.INSTALLED, PLUGINS.INSTALLED, undefined,
+        LIFECYCLE.TYPE, LIFECYCLE.CATEGORY.PLUGINS_INSTALLED,
+        LIFECYCLE.CATEGORY.PLUGINS_INSTALLED, undefined,
         {pluginsList: pluginsList || []}, true);
-  },
+  }
 
   /**
    * Reset named timer.
    */
   time(name) {
-    this._baselines[name] = this.now();
+    this._baselines[name] = now();
     window.performance.mark(`${name}-start`);
-  },
+  }
 
   /**
    * Finish named timer and report it to server.
@@ -454,7 +503,7 @@
     if (!this._baselines.hasOwnProperty(name)) { return; }
     const baseTime = this._baselines[name];
     delete this._baselines[name];
-    this._reportTiming(name, this.now() - baseTime, eventDetails);
+    this._reportTiming(name, now() - baseTime, eventDetails);
 
     // Finalize the interval. Either from a registered start mark or
     // the navigation start time (if baseTime is 0).
@@ -465,7 +514,7 @@
       // (if undefined).
       window.performance.measure(name);
     }
-  },
+  }
 
   /**
    * Reports just line timeEnd, but additionally reports an average given a
@@ -483,9 +532,9 @@
 
     // Guard against division by zero.
     if (!denominator) { return; }
-    const time = this.now() - baseTime;
+    const time = now() - baseTime;
     this._reportTiming(averageName, time / denominator);
-  },
+  }
 
   /**
    * Send a timing report with an arbitrary time value.
@@ -495,9 +544,9 @@
    * @param {Object} eventDetails non sensitive details
    */
   _reportTiming(name, time, eventDetails) {
-    this.reporter(TIMING.TYPE, TIMING.CATEGORY_UI_LATENCY, name, time,
+    this.reporter(TIMING.TYPE, TIMING.CATEGORY.UI_LATENCY, name, time,
         eventDetails);
-  },
+  }
 
   /**
    * Get a timer object to for reporing a user timing. The start time will be
@@ -517,7 +566,7 @@
       // Clear the timer and reset the start time.
       reset: () => {
         called = false;
-        start = this.now();
+        start = now();
         return timer;
       },
 
@@ -527,7 +576,7 @@
           throw new Error(`Timer for "${name}" already ended.`);
         }
         called = true;
-        const time = this.now() - start;
+        const time = now() - start;
 
         // If a maximum is specified and the time exceeds it, do not report.
         if (max && time > max) { return timer; }
@@ -546,7 +595,7 @@
 
     // The timer is initialized to its creation time.
     return timer.reset();
-  },
+  }
 
   /**
    * Log timing information for an RPC.
@@ -555,17 +604,22 @@
    * @param {number} elapsed The time elapsed of the RPC.
    */
   reportRpcTiming(anonymizedUrl, elapsed) {
-    this.reporter(TIMING.TYPE, TIMING.CATEGORY_RPC, 'RPC-' + anonymizedUrl,
+    this.reporter(TIMING.TYPE, TIMING.CATEGORY.RPC, 'RPC-' + anonymizedUrl,
         elapsed, {}, true);
     if (elapsed >= SLOW_RPC_THRESHOLD) {
-      slowRpcList.push({anonymizedUrl, elapsed});
+      this._slowRpcList.push({anonymizedUrl, elapsed});
     }
-  },
+  }
+
+  reportLifeCycle(eventName, details) {
+    this.reporter(LIFECYCLE.TYPE, LIFECYCLE.CATEGORY.DEFAULT, eventName,
+        undefined, details, true);
+  }
 
   reportInteraction(eventName, details) {
-    this.reporter(INTERACTION_TYPE, this.category, eventName, undefined,
-        details, true);
-  },
+    this.reporter(INTERACTION.TYPE, INTERACTION.CATEGORY.DEFAULT, eventName,
+        undefined, details, true);
+  }
 
   /**
    * A draft interaction was started. Update the time-betweeen-draft-actions
@@ -585,19 +639,16 @@
 
     // Mark the time and reinitialize the timer.
     timer.end().reset();
-  },
+  }
 
   reportErrorDialog(message) {
-    this.reporter(ERROR_DIALOG.TYPE, ERROR_DIALOG.CATEGORY,
+    this.reporter(ERROR.TYPE, ERROR.CATEGORY.ERROR_DIALOG,
         'ErrorDialog: ' + message, {error: new Error(message)});
-  },
+  }
 
   setRepoName(repoName) {
-    reportRepoName = repoName;
-  },
-});
+    this._reportRepoName = repoName;
+  }
+}
 
-window.GrReporting = GrReporting;
-// Expose onerror installation so it would be accessible from tests.
-window.GrReporting._catchErrors = catchErrors;
-window.GrReporting.STARTUP_TIMERS = Object.assign({}, STARTUP_TIMERS);
+export const DEFAULT_STARTUP_TIMERS = Object.assign({}, STARTUP_TIMERS);
diff --git a/polygerrit-ui/app/services/gr-reporting/gr-reporting_mock.js b/polygerrit-ui/app/services/gr-reporting/gr-reporting_mock.js
new file mode 100644
index 0000000..1ef2483
--- /dev/null
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting_mock.js
@@ -0,0 +1,45 @@
+/**
+ * @license
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export const grReportingMock = {
+  appStarted: () => {},
+  beforeLocationChanged: () => {},
+  changeDisplayed: () => {},
+  changeFullyLoaded: () => {},
+  dashboardDisplayed: () => {},
+  diffViewContentDisplayed: () => {},
+  diffViewDisplayed: () => {},
+  diffViewFullyLoaded: () => {},
+  fileListDisplayed: () => {},
+  getTimer: () => {
+    return {end: () => {}};
+  },
+  locationChanged: () => {},
+  onVisibilityChange: () => {},
+  pluginLoaded: () => {},
+  pluginsLoaded: () => {},
+  recordDraftInteraction: () => {},
+  reporter: () => {},
+  reportErrorDialog: () => {},
+  reportExtension: () => {},
+  reportInteraction: () => {},
+  reportLifeCycle: () => {},
+  reportRpcTiming: () => {},
+  setRepoName: () => {},
+  time: () => {},
+  timeEnd: () => {},
+  timeEndWithAverage: () => {},
+};
diff --git a/polygerrit-ui/app/services/gr-reporting/gr-reporting_mock_test.html b/polygerrit-ui/app/services/gr-reporting/gr-reporting_mock_test.html
new file mode 100644
index 0000000..e33a214
--- /dev/null
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting_mock_test.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<!--
+@license
+Copyright (C) 2020 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
+
+<script type="module">
+  import '../../test/common-test-setup.js';
+  import {GrReporting} from './gr-reporting.js';
+  import {grReportingMock} from './gr-reporting_mock.js';
+  suite('gr-reporting_mock tests', () => {
+    test('mocks all public methods', () => {
+      const methods = Object.getOwnPropertyNames(GrReporting.prototype)
+          .filter(name => typeof GrReporting.prototype[name] === 'function')
+          .filter(name => !name.startsWith('_') && name !== 'constructor')
+          .sort();
+      const mockMethods = Object.getOwnPropertyNames(grReportingMock)
+          .sort();
+      assert.deepEqual(methods, mockMethods);
+    });
+  });
+</script>
\ No newline at end of file
diff --git a/polygerrit-ui/app/services/gr-reporting/gr-reporting_test.html b/polygerrit-ui/app/services/gr-reporting/gr-reporting_test.html
new file mode 100644
index 0000000..e309c8c
--- /dev/null
+++ b/polygerrit-ui/app/services/gr-reporting/gr-reporting_test.html
@@ -0,0 +1,513 @@
+<!DOCTYPE html>
+<!--
+@license
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<meta charset="utf-8">
+<title>gr-reporting</title>
+
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-services-es5-adapter.js"></script>
+
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
+
+<script type="module">
+import '../../test/common-test-setup.js';
+import {GrReporting, DEFAULT_STARTUP_TIMERS, initErrorReporter} from './gr-reporting.js';
+import {appContext} from '../app-context.js';
+suite('gr-reporting tests', () => {
+  let service;
+  let sandbox;
+  let clock;
+  let fakePerformance;
+
+  const NOW_TIME = 100;
+
+  setup(() => {
+    sandbox = sinon.sandbox.create();
+    clock = sinon.useFakeTimers(NOW_TIME);
+    service = new GrReporting(appContext.flagsService);
+    service._baselines = Object.assign({}, DEFAULT_STARTUP_TIMERS);
+    sandbox.stub(service, 'reporter');
+  });
+
+  teardown(() => {
+    sandbox.restore();
+    clock.restore();
+  });
+
+  test('appStarted', () => {
+    fakePerformance = {
+      navigationStart: 1,
+      loadEventEnd: 2,
+    };
+    fakePerformance.toJSON = () => fakePerformance;
+    sinon.stub(service, 'performanceTiming',
+        {get() { return fakePerformance; }});
+    sandbox.stub(window.performance, 'now').returns(42);
+    service.appStarted();
+    assert.isTrue(
+        service.reporter.calledWithMatch(
+            'timing-report', 'UI Latency', 'App Started', 42
+        ));
+    assert.isTrue(
+        service.reporter.calledWithExactly(
+            'timing-report', 'UI Latency', 'NavResTime - loadEventEnd',
+            fakePerformance.loadEventEnd - fakePerformance.navigationStart,
+            undefined, true)
+    );
+  });
+
+  test('WebComponentsReady', () => {
+    sandbox.stub(window.performance, 'now').returns(42);
+    service.timeEnd('WebComponentsReady');
+    assert.isTrue(service.reporter.calledWithMatch(
+        'timing-report', 'UI Latency', 'WebComponentsReady', 42
+    ));
+  });
+
+  test('beforeLocationChanged', () => {
+    service._baselines['garbage'] = 'monster';
+    sandbox.stub(service, 'time');
+    service.beforeLocationChanged();
+    assert.isTrue(service.time.calledWithExactly('DashboardDisplayed'));
+    assert.isTrue(service.time.calledWithExactly('ChangeDisplayed'));
+    assert.isTrue(service.time.calledWithExactly('ChangeFullyLoaded'));
+    assert.isTrue(service.time.calledWithExactly('DiffViewDisplayed'));
+    assert.isTrue(service.time.calledWithExactly('FileListDisplayed'));
+    assert.isFalse(service._baselines.hasOwnProperty('garbage'));
+  });
+
+  test('changeDisplayed', () => {
+    sandbox.spy(service, 'timeEnd');
+    service.changeDisplayed();
+    assert.isFalse(service.timeEnd.calledWith('ChangeDisplayed'));
+    assert.isTrue(service.timeEnd.calledWith('StartupChangeDisplayed'));
+    service.changeDisplayed();
+    assert.isTrue(service.timeEnd.calledWith('ChangeDisplayed'));
+  });
+
+  test('changeFullyLoaded', () => {
+    sandbox.spy(service, 'timeEnd');
+    service.changeFullyLoaded();
+    assert.isFalse(
+        service.timeEnd.calledWithExactly('ChangeFullyLoaded'));
+    assert.isTrue(
+        service.timeEnd.calledWithExactly('StartupChangeFullyLoaded'));
+    service.changeFullyLoaded();
+    assert.isTrue(service.timeEnd.calledWithExactly('ChangeFullyLoaded'));
+  });
+
+  test('diffViewDisplayed', () => {
+    sandbox.spy(service, 'timeEnd');
+    service.diffViewDisplayed();
+    assert.isFalse(service.timeEnd.calledWith('DiffViewDisplayed'));
+    assert.isTrue(service.timeEnd.calledWith('StartupDiffViewDisplayed'));
+    service.diffViewDisplayed();
+    assert.isTrue(service.timeEnd.calledWith('DiffViewDisplayed'));
+  });
+
+  test('fileListDisplayed', () => {
+    sandbox.spy(service, 'timeEnd');
+    service.fileListDisplayed();
+    assert.isFalse(
+        service.timeEnd.calledWithExactly('FileListDisplayed'));
+    assert.isTrue(
+        service.timeEnd.calledWithExactly('StartupFileListDisplayed'));
+    service.fileListDisplayed();
+    assert.isTrue(service.timeEnd.calledWithExactly('FileListDisplayed'));
+  });
+
+  test('dashboardDisplayed', () => {
+    sandbox.spy(service, 'timeEnd');
+    service.dashboardDisplayed();
+    assert.isFalse(service.timeEnd.calledWith('DashboardDisplayed'));
+    assert.isTrue(service.timeEnd.calledWith('StartupDashboardDisplayed'));
+    service.dashboardDisplayed();
+    assert.isTrue(service.timeEnd.calledWith('DashboardDisplayed'));
+  });
+
+  test('dashboardDisplayed details', () => {
+    sandbox.spy(service, 'timeEnd');
+    sandbox.stub(window, 'performance', {
+      memory: {
+        usedJSHeapSize: 1024 * 1024,
+      },
+      measure: () => {},
+      now: () => { 42; },
+    });
+    service.reportRpcTiming('/changes/*~*/comments', 500);
+    service.dashboardDisplayed();
+    assert.isTrue(
+        service.timeEnd.calledWithExactly('StartupDashboardDisplayed',
+            {rpcList: [
+              {
+                anonymizedUrl: '/changes/*~*/comments',
+                elapsed: 500,
+              },
+            ],
+            screenSize: {
+              width: window.screen.width,
+              height: window.screen.height,
+            },
+            viewport: {
+              width: document.documentElement.clientWidth,
+              height: document.documentElement.clientHeight,
+            },
+            usedJSHeapSizeMb: 1,
+            hiddenDurationMs: 0,
+            }
+        ));
+  });
+
+  suite('hidden duration', () => {
+    let nowStub;
+    let visibilityStateStub;
+    const assertHiddenDurationsMs = hiddenDurationMs => {
+      service.dashboardDisplayed();
+      assert.isTrue(
+          service.timeEnd.calledWithMatch('StartupDashboardDisplayed',
+              {hiddenDurationMs}
+          ));
+    };
+
+    setup(() => {
+      sandbox.spy(service, 'timeEnd');
+      nowStub = sandbox.stub(window.performance, 'now');
+      visibilityStateStub = {
+        value: value => {
+          Object.defineProperty(document, 'visibilityState',
+              {value, configurable: true});
+        },
+      };
+    });
+
+    test('starts in hidden', () => {
+      nowStub.returns(10);
+      visibilityStateStub.value('hidden');
+      service.onVisibilityChange();
+      nowStub.returns(15);
+      visibilityStateStub.value('visible');
+      service.onVisibilityChange();
+      assertHiddenDurationsMs(5);
+    });
+
+    test('full in hidden', () => {
+      nowStub.returns(10);
+      visibilityStateStub.value('hidden');
+      assertHiddenDurationsMs(10);
+    });
+
+    test('full in visible', () => {
+      nowStub.returns(10);
+      visibilityStateStub.value('visible');
+      assertHiddenDurationsMs(0);
+    });
+
+    test('accumulated', () => {
+      nowStub.returns(10);
+      visibilityStateStub.value('hidden');
+      service.onVisibilityChange();
+      nowStub.returns(15);
+      visibilityStateStub.value('visible');
+      service.onVisibilityChange();
+      nowStub.returns(20);
+      visibilityStateStub.value('hidden');
+      service.onVisibilityChange();
+      nowStub.returns(25);
+      assertHiddenDurationsMs(10);
+    });
+
+    test('reset after location change', () => {
+      nowStub.returns(10);
+      visibilityStateStub.value('hidden');
+      assertHiddenDurationsMs(10);
+      visibilityStateStub.value('visible');
+      nowStub.returns(15);
+      service.beforeLocationChanged();
+      service.timeEnd.reset();
+      service.dashboardDisplayed();
+      assert.isTrue(
+          service.timeEnd.calledWithMatch('DashboardDisplayed',
+              {hiddenDurationMs: 0}
+          ));
+    });
+  });
+
+  test('time and timeEnd', () => {
+    const nowStub = sandbox.stub(window.performance, 'now').returns(0);
+    service.time('foo');
+    nowStub.returns(1);
+    service.time('bar');
+    nowStub.returns(2);
+    service.timeEnd('bar');
+    nowStub.returns(3);
+    service.timeEnd('foo');
+    assert.isTrue(service.reporter.calledWithMatch(
+        'timing-report', 'UI Latency', 'foo', 3
+    ));
+    assert.isTrue(service.reporter.calledWithMatch(
+        'timing-report', 'UI Latency', 'bar', 1
+    ));
+  });
+
+  test('timer object', () => {
+    const nowStub = sandbox.stub(window.performance, 'now').returns(100);
+    const timer = service.getTimer('foo-bar');
+    nowStub.returns(150);
+    timer.end();
+    assert.isTrue(service.reporter.calledWithMatch(
+        'timing-report', 'UI Latency', 'foo-bar', 50));
+  });
+
+  test('timer object double call', () => {
+    const timer = service.getTimer('foo-bar');
+    timer.end();
+    assert.isTrue(service.reporter.calledOnce);
+    assert.throws(() => {
+      timer.end();
+    }, 'Timer for "foo-bar" already ended.');
+  });
+
+  test('timer object maximum', () => {
+    const nowStub = sandbox.stub(window.performance, 'now').returns(100);
+    const timer = service.getTimer('foo-bar').withMaximum(100);
+    nowStub.returns(150);
+    timer.end();
+    assert.isTrue(service.reporter.calledOnce);
+
+    timer.reset();
+    nowStub.returns(260);
+    timer.end();
+    assert.isTrue(service.reporter.calledOnce);
+  });
+
+  test('recordDraftInteraction', () => {
+    const key = 'TimeBetweenDraftActions';
+    const nowStub = sandbox.stub(window.performance, 'now').returns(100);
+    const timingStub = sandbox.stub(service, '_reportTiming');
+    service.recordDraftInteraction();
+    assert.isFalse(timingStub.called);
+
+    nowStub.returns(200);
+    service.recordDraftInteraction();
+    assert.isTrue(timingStub.calledOnce);
+    assert.equal(timingStub.lastCall.args[0], key);
+    assert.equal(timingStub.lastCall.args[1], 100);
+
+    nowStub.returns(350);
+    service.recordDraftInteraction();
+    assert.isTrue(timingStub.calledTwice);
+    assert.equal(timingStub.lastCall.args[0], key);
+    assert.equal(timingStub.lastCall.args[1], 150);
+
+    nowStub.returns(370 + 2 * 60 * 1000);
+    service.recordDraftInteraction();
+    assert.isFalse(timingStub.calledThrice);
+  });
+
+  test('timeEndWithAverage', () => {
+    const nowStub = sandbox.stub(window.performance, 'now').returns(0);
+    nowStub.returns(1000);
+    service.time('foo');
+    nowStub.returns(1100);
+    service.timeEndWithAverage('foo', 'bar', 10);
+    assert.isTrue(service.reporter.calledTwice);
+    assert.isTrue(service.reporter.calledWithMatch(
+        'timing-report', 'UI Latency', 'foo', 100));
+    assert.isTrue(service.reporter.calledWithMatch(
+        'timing-report', 'UI Latency', 'bar', 10));
+  });
+
+  test('reportExtension', () => {
+    service.reportExtension('foo');
+    assert.isTrue(service.reporter.calledWithExactly(
+        'lifecycle', 'Extension detected', 'foo'
+    ));
+  });
+
+  test('reportInteraction', () => {
+    service.reporter.restore();
+    sandbox.spy(service, '_reportEvent');
+    service.pluginsLoaded(); // so we don't cache
+    service.reportInteraction('button-click', {name: 'sendReply'});
+    assert.isTrue(service._reportEvent.getCall(2).calledWithMatch(
+        {
+          type: 'interaction',
+          name: 'button-click',
+          eventDetails: JSON.stringify({name: 'sendReply'}),
+        }
+    ));
+  });
+
+  test('report start time', () => {
+    service.reporter.restore();
+    sandbox.stub(window.performance, 'now').returns(42);
+    sandbox.spy(service, '_reportEvent');
+    const dispatchStub = sandbox.spy(document, 'dispatchEvent');
+    service.pluginsLoaded();
+    service.time('timeAction');
+    service.timeEnd('timeAction');
+    assert.isTrue(service._reportEvent.getCall(2).calledWithMatch(
+        {
+          type: 'timing-report',
+          category: 'UI Latency',
+          name: 'timeAction',
+          value: 0,
+          eventStart: 42,
+        }
+    ));
+    assert.equal(dispatchStub.getCall(2).args[0].detail.eventStart, 42);
+  });
+
+  suite('plugins', () => {
+    setup(() => {
+      service.reporter.restore();
+      sandbox.stub(service, '_reportEvent');
+    });
+
+    test('pluginsLoaded reports time', () => {
+      sandbox.stub(window.performance, 'now').returns(42);
+      service.pluginsLoaded();
+      assert.isTrue(service._reportEvent.calledWithMatch(
+          {
+            type: 'timing-report',
+            category: 'UI Latency',
+            name: 'PluginsLoaded',
+            value: 42,
+          }
+      ));
+    });
+
+    test('pluginsLoaded reports plugins', () => {
+      service.pluginsLoaded(['foo', 'bar']);
+      assert.isTrue(service._reportEvent.calledWithMatch(
+          {
+            type: 'lifecycle',
+            category: 'Plugins installed',
+            eventDetails: JSON.stringify({pluginsList: ['foo', 'bar']}),
+          }
+      ));
+    });
+
+    test('caches reports if plugins are not loaded', () => {
+      service.timeEnd('foo');
+      assert.isFalse(service._reportEvent.called);
+    });
+
+    test('reports if plugins are loaded', () => {
+      service.pluginsLoaded();
+      assert.isTrue(service._reportEvent.called);
+    });
+
+    test('reports if metrics plugin xyz is loaded', () => {
+      service.pluginLoaded('metrics-xyz');
+      assert.isTrue(service._reportEvent.called);
+    });
+
+    test('reports cached events preserving order', () => {
+      service.time('foo');
+      service.time('bar');
+      service.timeEnd('foo');
+      service.pluginsLoaded();
+      service.timeEnd('bar');
+      assert.isTrue(service._reportEvent.getCall(0).calledWithMatch(
+          {type: 'timing-report', category: 'UI Latency', name: 'foo'}
+      ));
+      assert.isTrue(service._reportEvent.getCall(1).calledWithMatch(
+          {type: 'timing-report', category: 'UI Latency',
+            name: 'PluginsLoaded'}
+      ));
+      assert.isTrue(service._reportEvent.getCall(2).calledWithMatch(
+          {type: 'lifecycle', category: 'Plugins installed'}
+      ));
+      assert.isTrue(service._reportEvent.getCall(3).calledWithMatch(
+          {type: 'timing-report', category: 'UI Latency', name: 'bar'}
+      ));
+    });
+  });
+
+  test('search', () => {
+    service.locationChanged('_handleSomeRoute');
+    assert.isTrue(service.reporter.calledWithExactly(
+        'nav-report', 'Location Changed', 'Page', '_handleSomeRoute'));
+  });
+
+  suite('exception logging', () => {
+    let fakeWindow;
+    let reporter;
+
+    const emulateThrow = function(msg, url, line, column, error) {
+      return fakeWindow.onerror(msg, url, line, column, error);
+    };
+
+    setup(() => {
+      reporter = service.reporter;
+      fakeWindow = {
+        handlers: {},
+        addEventListener(type, handler) {
+          this.handlers[type] = handler;
+        },
+      };
+      sandbox.stub(console, 'error');
+      Object.defineProperty(appContext, 'reportingService', {
+        get() {
+          return service;
+        },
+      });
+      const errorReporter = initErrorReporter(appContext);
+      errorReporter.catchErrors(fakeWindow);
+    });
+
+    test('is reported', () => {
+      const error = new Error('bar');
+      error.stack = undefined;
+      emulateThrow('bar', 'http://url', 4, 2, error);
+      assert.isTrue(reporter.calledWith('error', 'exception', 'bar'));
+      const payload = reporter.lastCall.args[3];
+      assert.deepEqual(payload, {
+        url: 'http://url',
+        line: 4,
+        column: 2,
+        error,
+      });
+    });
+
+    test('is reported with 3 lines of stack', () => {
+      const error = new Error('bar');
+      emulateThrow('bar', 'http://url', 4, 2, error);
+      const expectedStack = error.stack.split('\n').slice(0, 3)
+          .join('\n');
+      assert.isTrue(reporter.calledWith('error', 'exception',
+          expectedStack));
+    });
+
+    test('prevent default event handler', () => {
+      assert.isTrue(emulateThrow());
+    });
+
+    test('unhandled rejection', () => {
+      fakeWindow.handlers['unhandledrejection']({
+        reason: {
+          message: 'bar',
+        },
+      });
+      assert.isTrue(reporter.calledWith('error', 'exception', 'bar'));
+    });
+  });
+});
+</script>
diff --git a/polygerrit-ui/app/test/a11y-test-utils.js b/polygerrit-ui/app/test/a11y-test-utils.js
new file mode 100644
index 0000000..a687e07
--- /dev/null
+++ b/polygerrit-ui/app/test/a11y-test-utils.js
@@ -0,0 +1,44 @@
+/**
+ * @license
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import './common-test-setup-karma.js';
+
+// Run a11y audit on test fixture
+// The code is inspired by the
+// https://github.com/Polymer/web-component-tester/blob/master/data/a11ySuite.js
+export async function runA11yAudit(fixture, ignoredRules) {
+  fixture.instantiate();
+  await flush();
+  const axsConfig = new axs.AuditConfiguration();
+  axsConfig.scope = document.body;
+  axsConfig.showUnsupportedRulesWarning = false;
+  axsConfig.auditRulesToIgnore = ignoredRules;
+
+  const auditResults = axs.Audit.run(axsConfig);
+  const errors = [];
+  auditResults.forEach((result, index) => {
+    // only show applicable tests
+    if (result.result === 'FAIL') {
+      const title = result.rule.heading;
+      // fail test if audit result is FAIL
+      const error = axs.Audit.accessibilityErrorMessage(result);
+      errors.push(`${title}: ${error}`);
+    }
+  });
+  if (errors.length > 0) {
+    assert.fail(errors.join('\n') + '\n');
+  }
+}
diff --git a/polygerrit-ui/app/test/common-test-setup-karma.js b/polygerrit-ui/app/test/common-test-setup-karma.js
new file mode 100644
index 0000000..4fd3b03
--- /dev/null
+++ b/polygerrit-ui/app/test/common-test-setup-karma.js
@@ -0,0 +1,152 @@
+/**
+ * @license
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import './common-test-setup.js';
+import '@polymer/test-fixture/test-fixture.js';
+import 'chai/chai.js';
+self.assert = window.chai.assert;
+
+/**
+ * Triggers a flush of any pending events, observations, etc and calls you back
+ * after they have been processed if callback is passed; otherwise returns
+ * promise.
+ *
+ * @param {function()} callback
+ */
+function flush(callback) {
+  // Ideally, this function would be a call to Polymer.dom.flush, but that
+  // doesn't support a callback yet
+  // (https://github.com/Polymer/polymer-dev/issues/851)
+  window.Polymer.dom.flush();
+  if (callback) {
+    window.setTimeout(callback, 0);
+  } else {
+    return new Promise(resolve => {
+      window.setTimeout(resolve, 0);
+    });
+  }
+}
+
+self.flush = flush;
+
+class TestFixtureIdProvider {
+  static get instance() {
+    if (!TestFixtureIdProvider._instance) {
+      TestFixtureIdProvider._instance = new TestFixtureIdProvider();
+    }
+    return TestFixtureIdProvider._instance;
+  }
+
+  constructor() {
+    this.fixturesCount = 1;
+  }
+
+  generateNewFixtureId() {
+    this.fixturesCount++;
+    return `fixture-${this.fixturesCount}`;
+  }
+}
+
+class TestFixture {
+  constructor(fixtureId) {
+    this.fixtureId = fixtureId;
+  }
+
+  /**
+   * Create an instance of a fixture's template.
+   *
+   * @param {Object} model - see Data-bound sections at
+   *   https://www.webcomponents.org/element/@polymer/test-fixture
+   * @return {HTMLElement | HTMLElement[]} - if the fixture's template contains
+   *   a single element, returns the appropriated instantiated element.
+   *   Otherwise, it return an array of all instantiated elements from the
+   *   template.
+   */
+  instantiate(model) {
+    // The window.fixture method is defined in common-test-setup.js
+    return window.fixture(this.fixtureId, model);
+  }
+}
+
+/**
+ * Wraps provided template to a test-fixture tag and adds test-fixture to
+ * the document. You can use the html function to create a template.
+ *
+ * Example:
+ * import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+ *
+ * // Create fixture at the root level of a test file
+ * const basicTestFixture = fixtureFromTemplate(html`
+ *   <gr-cursor-manager cursor-target-class="targeted"></gr-cursor-manager>
+ *   <ul>
+ *    <li>A</li>
+ *    <li>B</li>
+ *    <li>C</li>
+ *    <li>D</li>
+ *   </ul>
+ * `);
+ * ...
+ * // Instantiate fixture when needed:
+ *
+ * suite('example') {
+ *   let elements;
+ *   setup(() => {
+ *     elements = basicTestFixture.instantiate();
+ *   });
+ * }
+ *
+ * @param {HTMLTemplateElement} template - a template for a fixture
+ * @return {TestFixture} - the instance of TestFixture class
+ */
+function fixtureFromTemplate(template) {
+  const fixtureId = TestFixtureIdProvider.instance.generateNewFixtureId();
+  const testFixture = document.createElement('test-fixture');
+  testFixture.setAttribute('id', fixtureId);
+  testFixture.appendChild(template);
+  document.body.appendChild(testFixture);
+  return new TestFixture(fixtureId);
+}
+
+/**
+ * Wraps provided tag to a test-fixture/template tags and adds test-fixture
+ * to the document.
+ *
+ * Example:
+ *
+ * // Create fixture at the root level of a test file
+ * const basicTestFixture = fixtureFromElement('gr-diff-view');
+ * ...
+ * // Instantiate fixture when needed:
+ *
+ * suite('example') {
+ *   let element;
+ *   setup(() => {
+ *     element = basicTestFixture.instantiate();
+ *   });
+ * }
+ *
+ * @param {HTMLTemplateElement} template - a template for a fixture
+ * @return {TestFixture} - the instance of TestFixture class
+ */
+function fixtureFromElement(tagName) {
+  const template = document.createElement('template');
+  template.innerHTML = `<${tagName}></${tagName}>`;
+  return fixtureFromTemplate(template);
+}
+
+window.fixtureFromTemplate = fixtureFromTemplate;
+window.fixtureFromElement = fixtureFromElement;
diff --git a/polygerrit-ui/app/test/common-test-setup.js b/polygerrit-ui/app/test/common-test-setup.js
index aea24da..db0d279 100644
--- a/polygerrit-ui/app/test/common-test-setup.js
+++ b/polygerrit-ui/app/test/common-test-setup.js
@@ -14,14 +14,24 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+// TODO(dmfilippov): remove bundled-polymer.js imports when the following issue
+// https://github.com/Polymer/polymer-resin/issues/9 is resolved.
 import '../scripts/bundled-polymer.js';
 
 import 'polymer-resin/standalone/polymer-resin.js';
 import '@polymer/iron-test-helpers/iron-test-helpers.js';
 import './test-router.js';
 import {SafeTypes} from '../behaviors/safe-types-behavior/safe-types-behavior.js';
+import {appContext} from '../services/app-context.js';
 import {initAppContext} from '../services/app-context-init.js';
 import {_testOnly_resetPluginLoader} from '../elements/shared/gr-js-api-interface/gr-plugin-loader.js';
+import {grReportingMock} from '../services/gr-reporting/gr-reporting_mock.js';
+
+// Returns true if tests run under the Karma
+function isKarmaTest() {
+  return window.__karma__ !== undefined;
+}
 
 security.polymer_resin.install({
   allowedIdentifierPrefixes: [''],
@@ -57,28 +67,45 @@
 // Note, that fixture(...) and stub(..) methods are registered different by
 // WCT. This is why these methods implemented slightly different here.
 const cleanups = [];
-if (!window.fixture) {
+if (isKarmaTest() || !window.fixture) {
+  // For karma always set our implementation
+  // (karma doesn't provide the fixture method)
   window.fixture = function(fixtureId, model) {
     // This method is inspired by WCT method
     cleanups.push(() => document.getElementById(fixtureId).restore());
     return document.getElementById(fixtureId).create(model);
   };
 } else {
+  // The following error is important for WCT tests.
+  // If window.fixture already installed by WCT at this point, WCT tests
+  // performance decreases rapidly.
+  // It allows to catch performance problems earlier.
   throw new Error('window.fixture must be set before wct sets it');
 }
 
 // On the first call to the setup, WCT installs window.fixture
-// and widnow.stub methods
+// and window.stub methods
 setup(() => {
   // If the following asserts fails - then window.stub is
   // overwritten by some other code.
   assert.equal(cleanups.length, 0);
 
   _testOnly_resetPluginLoader();
+
   initAppContext();
+  function setMock(serviceName, setupMock) {
+    Object.defineProperty(appContext, serviceName, {
+      get() {
+        return setupMock;
+      },
+    });
+  }
+  setMock('reportingService', grReportingMock);
 });
 
-if (window.stub) {
+if (isKarmaTest() || window.stub) {
+  // For karma always set our implementation
+  // (karma doesn't provide the stub method)
   window.stub = function(tagName, implementation) {
     // This method is inspired by WCT method
     const proto = document.createElement(tagName).constructor.prototype;
@@ -91,6 +118,10 @@
     });
   };
 } else {
+  // The following error is important for WCT tests.
+  // If window.fixture already installed by WCT at this point, WCT tests
+  // performance decreases rapidly.
+  // It allows to catch performance problems earlier.
   throw new Error('window.stub must be set after wct sets it');
 }
 
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api-mock_test.js b/polygerrit-ui/app/test/mocks/comment-api.js
similarity index 100%
rename from polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api-mock_test.js
rename to polygerrit-ui/app/test/mocks/comment-api.js
diff --git a/polygerrit-ui/app/test/mock-diff-response.js b/polygerrit-ui/app/test/mocks/diff-response.js
similarity index 100%
rename from polygerrit-ui/app/test/mock-diff-response.js
rename to polygerrit-ui/app/test/mocks/diff-response.js
diff --git a/polygerrit-ui/app/test/tests.js b/polygerrit-ui/app/test/tests.js
index 934d9e8..6892287 100644
--- a/polygerrit-ui/app/test/tests.js
+++ b/polygerrit-ui/app/test/tests.js
@@ -24,10 +24,9 @@
 // Elements tests.
 /* eslint-disable max-len */
 const elements = [
-  // This seemed to be flakey when it was farther down the list. Keep at the
+  // This seemed to be flaky when it was farther down the list. Keep at the
   // beginning.
   'gr-app_test.html',
-  'admin/gr-access-section/gr-access-section_test.html',
   'admin/gr-admin-group-list/gr-admin-group-list_test.html',
   'admin/gr-admin-view/gr-admin-view_test.html',
   'admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_test.html',
@@ -62,7 +61,6 @@
   'change/gr-change-metadata/gr-change-metadata-it_test.html',
   'change/gr-change-metadata/gr-change-metadata_test.html',
   'change/gr-change-requirements/gr-change-requirements_test.html',
-  'change/gr-change-view/gr-change-view_test.html',
   'change/gr-comment-list/gr-comment-list_test.html',
   'change/gr-commit-info/gr-commit-info_test.html',
   'change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.html',
@@ -94,7 +92,6 @@
   'core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.html',
   'core/gr-main-header/gr-main-header_test.html',
   'core/gr-navigation/gr-navigation_test.html',
-  'core/gr-reporting/gr-reporting_test.html',
   'core/gr-router/gr-router_test.html',
   'core/gr-search-bar/gr-search-bar_test.html',
   'core/gr-smart-search/gr-smart-search_test.html',
@@ -112,7 +109,6 @@
   'diff/gr-diff-selection/gr-diff-selection_test.html',
   'diff/gr-diff-view/gr-diff-view_test.html',
   'diff/gr-diff/gr-diff-group_test.html',
-  'diff/gr-diff/gr-diff_test.html',
   'diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.html',
   'diff/gr-patch-range-select/gr-patch-range-select_test.html',
   'diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html',
@@ -167,7 +163,6 @@
   'shared/gr-comment/gr-comment_test.html',
   'shared/gr-copy-clipboard/gr-copy-clipboard_test.html',
   'shared/gr-count-string-formatter/gr-count-string-formatter_test.html',
-  'shared/gr-cursor-manager/gr-cursor-manager_test.html',
   'shared/gr-date-formatter/gr-date-formatter_test.html',
   'shared/gr-dialog/gr-dialog_test.html',
   'shared/gr-diff-preferences/gr-diff-preferences_test.html',
@@ -223,7 +218,6 @@
 // Behaviors tests.
 /* eslint-disable max-len */
 const behaviors = [
-  'async-foreach-behavior/async-foreach-behavior_test.html',
   'base-url-behavior/base-url-behavior_test.html',
   'docs-url-behavior/docs-url-behavior_test.html',
   'dom-util-behavior/dom-util-behavior_test.html',
@@ -261,13 +255,19 @@
 }
 
 const services = [
+  'app-context-init_test.html',
   'flags_test.html',
+  'gr-reporting/gr-reporting_test.html',
+  'gr-reporting/gr-reporting_mock_test.html',
 ];
 for (let file of services) {
   file = servicesPath + file;
   testFiles.push(file);
 }
 
+// embed test
+testFiles.push('../embed/gr-diff-app-context-init_test.html');
+
 /**
  * Converts multiline string to a map<file_name, test_count>.
  *
diff --git a/polygerrit-ui/app/types/custom-externs.js b/polygerrit-ui/app/types/custom-externs.js
index afa094c..bc95b3f 100644
--- a/polygerrit-ui/app/types/custom-externs.js
+++ b/polygerrit-ui/app/types/custom-externs.js
@@ -58,6 +58,5 @@
 var GrRestApiHelper;
 var GrDisplayNameUtils;
 var GrReviewerSuggestionsProvider;
-var moment;
 var page;
 var util;
\ No newline at end of file
diff --git a/polygerrit-ui/app/types/types.js b/polygerrit-ui/app/types/types.js
index 5408eea..91909a6 100644
--- a/polygerrit-ui/app/types/types.js
+++ b/polygerrit-ui/app/types/types.js
@@ -309,3 +309,16 @@
  *  }}
  */
 Gerrit.Comment;
+
+/**
+ * This contains path info used in diff, basePath
+ * is used on the left while path is used on the right.
+ *
+ * TODO(taoalpha): unify all *Range into one.
+ *
+ * @typedef {{
+ *  basePath: ?string,
+ *  path: string,
+ *  }}
+ */
+Gerrit.FileRange;
\ No newline at end of file
diff --git a/polygerrit-ui/app/utils/date-util.js b/polygerrit-ui/app/utils/date-util.js
new file mode 100644
index 0000000..475fdc6
--- /dev/null
+++ b/polygerrit-ui/app/utils/date-util.js
@@ -0,0 +1,178 @@
+/**
+ * @license
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const Duration = {
+  HOUR: 1000 * 60 * 60,
+  DAY: 1000 * 60 * 60 * 24,
+};
+
+export function parseDate(dateStr) {
+  // Timestamps are given in UTC and have the format
+  // "'yyyy-mm-dd hh:mm:ss.fffffffff'" where "'ffffffffff'" represents
+  // nanoseconds.
+  // Munge the date into an ISO 8061 format and parse that.
+  return new Date(dateStr.replace(' ', 'T') + 'Z');
+}
+
+export function isValidDate(date) {
+  return date instanceof Date && !isNaN(date);
+}
+
+// similar to fromNow from moment.js
+export function fromNow(date) {
+  const now = new Date();
+  const secondsAgo = Math.round((now - date) / 1000);
+  if (secondsAgo <= 44) return 'just now';
+  if (secondsAgo <= 89) return 'a minute ago';
+  const minutesAgo = Math.round(secondsAgo / 60);
+  if (minutesAgo <= 44) return `${minutesAgo} minutes ago`;
+  if (minutesAgo <= 89) return 'an hour ago';
+  const hoursAgo = Math.round(minutesAgo / 60);
+  if (hoursAgo <= 21) return `${hoursAgo} hours ago`;
+  if (hoursAgo <= 35) return 'a day ago';
+  const daysAgo = Math.round(hoursAgo / 24);
+  if (daysAgo <= 25) return `${daysAgo} days ago`;
+  if (daysAgo <= 45) return `a month ago`;
+  const monthsAgo = Math.round(daysAgo / 30);
+  if (daysAgo <= 319) return `${monthsAgo} months ago`;
+  if (daysAgo <= 547) return `a year ago`;
+  const yearsAgo = Math.round(daysAgo / 365);
+  return `${yearsAgo} years ago`;
+}
+
+/**
+ * Return true if date is within 24 hours and on the same day.
+ */
+export function isWithinDay(now, date) {
+  const diff = now - date;
+  return diff < Duration.DAY && date.getDay() == now.getDay();
+}
+
+/**
+ * Returns true if date is from one to six months.
+ */
+export function isWithinHalfYear(now, date) {
+  const diff = now - date;
+  return diff < 180 * Duration.DAY;
+}
+
+export function formatDate(date, format) {
+  const options = {};
+  if (format.includes('MM')) {
+    if (format.includes('MMM')) {
+      options.month = 'short';
+    } else {
+      options.month = '2-digit';
+    }
+  }
+  if (format.includes('YY')) {
+    if (format.includes('YYYY')) {
+      options.year = 'numeric';
+    } else {
+      options.year = '2-digit';
+    }
+  }
+
+  if (format.includes('DD')) {
+    options.day = '2-digit';
+  }
+
+  if (format.includes('HH')) {
+    options.hour = '2-digit';
+    options.hour12 = false;
+  }
+
+  if (format.includes('h')) {
+    options.hour = 'numeric';
+    options.hour12 = true;
+  }
+
+  if (format.includes('mm')) {
+    options.minute = '2-digit';
+  }
+
+  if (format.includes('ss')) {
+    options.second = '2-digit';
+  }
+  let locale = 'en-US';
+  // Workaround for Chrome 80, en-US is using h24 (midnight is 24:00),
+  // en-GB is using h23 (midnight is 00:00)
+  if (format.includes('HH')) {
+    locale = 'en-GB';
+  }
+
+  const dtf = new Intl.DateTimeFormat(locale, options);
+  const parts = dtf.formatToParts(date).filter(o => o.type != 'literal')
+      .reduce((acc, o) => {
+        acc[o.type] = o.value;
+        return acc;
+      }, {});
+  if (format.includes('YY')) {
+    if (format.includes('YYYY')) {
+      format = format.replace('YYYY', parts.year);
+    } else {
+      format = format.replace('YY', parts.year);
+    }
+  }
+
+  if (format.includes('DD')) {
+    format = format.replace('DD', parts.day);
+  }
+
+  if (format.includes('HH')) {
+    format = format.replace('HH', parts.hour);
+  }
+
+  if (format.includes('h')) {
+    format = format.replace('h', parts.hour);
+  }
+
+  if (format.includes('mm')) {
+    format = format.replace('mm', parts.minute);
+  }
+
+  if (format.includes('ss')) {
+    format = format.replace('ss', parts.second);
+  }
+
+  if (format.includes('A')) {
+    if (parts.dayperiod) {
+      // Workaround for chrome 70 and below
+      format = format.replace('A', parts.dayperiod.toUpperCase());
+    } else {
+      format = format.replace('A', parts.dayPeriod.toUpperCase());
+    }
+  }
+  if (format.includes('MM')) {
+    if (format.includes('MMM')) {
+      format = format.replace('MMM', parts.month);
+    } else {
+      format = format.replace('MM', parts.month);
+    }
+  }
+  return format;
+}
+
+export function utcOffsetString() {
+  const now = new Date();
+  const tzo = -now.getTimezoneOffset();
+  const pad = num => {
+    const norm = Math.floor(Math.abs(num));
+    return (norm < 10 ? '0' : '') + norm;
+  };
+  return ` UTC${tzo >= 0 ? '+' : '-'}${pad(tzo / 60)}:${pad(tzo%60)}`;
+}
\ No newline at end of file
diff --git a/polygerrit-ui/app/utils/date-util_test.js b/polygerrit-ui/app/utils/date-util_test.js
new file mode 100644
index 0000000..7b22cc6
--- /dev/null
+++ b/polygerrit-ui/app/utils/date-util_test.js
@@ -0,0 +1,121 @@
+/**
+ * @license
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import '../test/common-test-setup-karma.js';
+import {isValidDate, parseDate, fromNow, isWithinDay, isWithinHalfYear, formatDate} from './date-util.js';
+
+suite('date-util tests', () => {
+  suite('parseDate', () => {
+    test('parseDate server date', () => {
+      const parsed = parseDate('2015-09-15 20:34:00.000000000');
+      assert.equal('2015-09-15T20:34:00.000Z', parsed.toISOString());
+    });
+  });
+
+  suite('isValidDate', () => {
+    test('date is valid', () => {
+      assert.isTrue(isValidDate(new Date()));
+    });
+    test('broken date is invalid', () => {
+      assert.isFalse(isValidDate(new Date('xxx')));
+    });
+  });
+
+  suite('fromNow', () => {
+    test('test all variants', () => {
+      const fakeNow = new Date('May 08 2020 12:00:00');
+      sinon.useFakeTimers(fakeNow.getTime());
+      assert.equal('just now', fromNow(new Date('May 08 2020 11:59:30')));
+      assert.equal('a minute ago', fromNow(new Date('May 08 2020 11:59:00')));
+      assert.equal('5 minutes ago', fromNow(new Date('May 08 2020 11:55:00')));
+      assert.equal('an hour ago', fromNow(new Date('May 08 2020 11:00:00')));
+      assert.equal('3 hours ago', fromNow(new Date('May 08 2020 9:00:00')));
+      assert.equal('a day ago', fromNow(new Date('May 07 2020 12:00:00')));
+      assert.equal('3 days ago', fromNow(new Date('May 05 2020 12:00:00')));
+      assert.equal('a month ago', fromNow(new Date('Apr 05 2020 12:00:00')));
+      assert.equal('2 months ago', fromNow(new Date('Mar 05 2020 12:00:00')));
+      assert.equal('a year ago', fromNow(new Date('May 05 2019 12:00:00')));
+      assert.equal('10 years ago', fromNow(new Date('May 05 2010 12:00:00')));
+    });
+  });
+
+  suite('isWithinDay', () => {
+    test('basics works', () => {
+      assert.isTrue(isWithinDay(new Date('May 08 2020 12:00:00'),
+          new Date('May 08 2020 02:00:00')));
+      assert.isFalse(isWithinDay(new Date('May 08 2020 12:00:00'),
+          new Date('May 07 2020 12:00:00')));
+    });
+  });
+
+  suite('isWithinHalfYear', () => {
+    test('basics works', () => {
+      assert.isTrue(isWithinHalfYear(new Date('May 08 2020 12:00:00'),
+          new Date('Feb 08 2020 12:00:00')));
+      assert.isFalse(isWithinHalfYear(new Date('May 08 2020 12:00:00'),
+          new Date('Nov 07 2019 12:00:00')));
+    });
+  });
+
+  suite('formatDate', () => {
+    test('works for standard format', () => {
+      const stdFormat = 'MMM DD, YYYY';
+      assert.equal('May 08, 2020',
+          formatDate(new Date('May 08 2020 12:00:00'), stdFormat));
+      assert.equal('Feb 28, 2020',
+          formatDate(new Date('Feb 28 2020 12:00:00'), stdFormat));
+
+      const time24Format = 'HH:mm:ss';
+      assert.equal('Feb 28, 2020 12:01:12',
+          formatDate(new Date('Feb 28 2020 12:01:12'), stdFormat + ' '
+          + time24Format));
+    });
+    test('works for euro format', () => {
+      const euroFormat = 'DD.MM.YYYY';
+      assert.equal('01.12.2019',
+          formatDate(new Date('Dec 01 2019 12:00:00'), euroFormat));
+      assert.equal('20.01.2002',
+          formatDate(new Date('Jan 20 2002 12:00:00'), euroFormat));
+
+      const time24Format = 'HH:mm:ss';
+      assert.equal('28.02.2020 00:01:12',
+          formatDate(new Date('Feb 28 2020 00:01:12'), euroFormat + ' '
+          + time24Format));
+    });
+    test('works for iso format', () => {
+      const isoFormat = 'YYYY-MM-DD';
+      assert.equal('2015-01-01',
+          formatDate(new Date('Jan 01 2015 12:00:00'), isoFormat));
+      assert.equal('2013-07-03',
+          formatDate(new Date('Jul 03 2013 12:00:00'), isoFormat));
+
+      const timeFormat = 'h:mm:ss A';
+      assert.equal('2013-07-03 5:00:00 AM',
+          formatDate(new Date('Jul 03 2013 05:00:00'), isoFormat + ' '
+          + timeFormat));
+      assert.equal('2013-07-03 5:00:00 PM',
+          formatDate(new Date('Jul 03 2013 17:00:00'), isoFormat + ' '
+          + timeFormat));
+    });
+    test('h:mm:ss A shows correctly midnight and midday', () => {
+      const timeFormat = 'h:mm A';
+      assert.equal('12:14 PM',
+          formatDate(new Date('Jul 03 2013 12:14:00'), timeFormat));
+      assert.equal('12:15 AM',
+          formatDate(new Date('Jul 03 2013 00:15:00'), timeFormat));
+    });
+  });
+});
\ No newline at end of file
diff --git a/polygerrit-ui/app/wct_test.sh b/polygerrit-ui/app/wct_test.sh
index 42b98ab..829a507 100755
--- a/polygerrit-ui/app/wct_test.sh
+++ b/polygerrit-ui/app/wct_test.sh
@@ -14,7 +14,7 @@
 
 # Copy ui_npm, so it will override ui_dev_npm modules (in case of conflicts)
 # Because browser always requests specific exact files (i.e. not a directory),
-# it always receives file from ui_npm. It can broke WCT itself but luckely it works.
+# it always receives file from ui_npm. It can broke WCT itself but luckily it works.
 cp -R -L ./external/ui_npm/node_modules/* $t/node_modules
 
 cp -R -L ./polygerrit-ui/app/* $t/
@@ -25,13 +25,13 @@
 echo "export const config=$JSON_CONFIG;" > ./test/suite_conf.js
 echo "export const testsPerFileString=\`" >> ./test/suite_conf.js
 # Count number of tests in each file.
-# We don't need accurate data, use simpliest method
+# We don't need accurate data, use simplest method
 # TODO(dmfilippov): collect data only once
 # In the current implementation, the same data is collected for each split,
 # It takes less than a second which many times less than the overall wct test time
 grep -rnw '.' --include=\*_test.html -e "test(" -c >> ./test/suite_conf.js
 echo "\`;" >>./test/suite_conf.js
 
-# If wct doesn't receive any paramenters, it fails (can't find files)
+# If wct doesn't receive any parameters, it fails (can't find files)
 # Pass --config-file as a parameter to have some arguments in command line
 $root_dir/$1 --config-file wct.conf.js ${WCT_ARGS}
diff --git a/polygerrit-ui/app/yarn.lock b/polygerrit-ui/app/yarn.lock
index 5724ffa..821b724 100644
--- a/polygerrit-ui/app/yarn.lock
+++ b/polygerrit-ui/app/yarn.lock
@@ -338,11 +338,6 @@
   resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
   integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=
 
-moment@^2.24.0:
-  version "2.24.0"
-  resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
-  integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==
-
 page@^1.11.5:
   version "1.11.5"
   resolved "https://registry.yarnpkg.com/page/-/page-1.11.5.tgz#0cfc8608be337f26f4377f31df0787aef0ca1af7"
diff --git a/polygerrit-ui/karma.conf.js b/polygerrit-ui/karma.conf.js
new file mode 100644
index 0000000..744c234
--- /dev/null
+++ b/polygerrit-ui/karma.conf.js
@@ -0,0 +1,152 @@
+/**
+ * @license
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const runUnderBazel = !!process.env["RUNFILES_DIR"];
+const path = require('path');
+
+function getModulesDir() {
+  if(runUnderBazel) {
+    // Run under bazel
+    return [
+      `external/ui_npm/node_modules`,
+      `external/ui_dev_npm/node_modules`
+    ];
+  }
+
+  // Run from intellij or npm run test:kdebug
+  return [
+    path.join(__dirname, 'app/node_modules'),
+    path.join(__dirname, 'node_modules'),
+  ];
+}
+
+function getUiDevNpmFilePath(importPath) {
+  if(runUnderBazel) {
+    return `external/ui_dev_npm/node_modules/${importPath}`;
+  }
+  else {
+    return `polygerrit-ui/node_modules/${importPath}`
+  }
+}
+
+module.exports = function(config) {
+  const testFilesLocationPattern =
+      'polygerrit-ui/app/**/!(template_test_srcs)/';
+  // Use --test-files to specify pattern for a test files.
+  // It can be just a file name, without a path:
+  // --test-files async-foreach-behavior_test.js
+  // If you specify --test-files without pattern, it gets true value
+  // In this case we ill run all tests (usefull for package.json "debugtest"
+  // script)
+  const testFilesPattern = (typeof config.testFiles == 'string') ?
+      testFilesLocationPattern + config.testFiles :
+      testFilesLocationPattern + '*_test.js';
+  config.set({
+    // base path that will be used to resolve all patterns (eg. files, exclude)
+    basePath: '../',
+    plugins: [
+      // Do not use karma-* to load all installed plugin
+      // This can lead to unexpected behavior under bazel
+      // if you forget to add a plugin in a bazel rule.
+      require.resolve('@open-wc/karma-esm'),
+      'karma-mocha',
+      'karma-chrome-launcher',
+      'karma-mocha-reporter',
+    ],
+    // frameworks to use
+    // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
+    frameworks: ['mocha', 'esm'],
+
+    // list of files / patterns to load in the browser
+    files: [
+      getUiDevNpmFilePath('accessibility-developer-tools/dist/js/axs_testing.js'),
+      getUiDevNpmFilePath('sinon/pkg/sinon.js'),
+      { pattern: testFilesPattern, type: 'module' },
+    ],
+    esm: {
+      nodeResolve: true,
+      moduleDirs: getModulesDir(),
+      // Bazel and yarn uses symlinks for files.
+      // preserveSymlinks is necessary for correct modules paths resolving
+      preserveSymlinks: true,
+      // By default, esm-dev-server uses 'auto' compatibility mode.
+      // In the 'auto' mode it incorrectly applies polyfills and
+      // breaks tests in some browser versions
+      // (for example, Chrome 69 on gerrit-ci).
+      compatibility: 'none',
+    },
+    // test results reporter to use
+    // possible values: 'dots', 'progress'
+    // available reporters: https://npmjs.org/browse/keyword/karma-reporter
+    reporters: ['mocha'],
+
+
+    // web server port
+    port: 9876,
+
+
+    // enable / disable colors in the output (reporters and logs)
+    colors: true,
+
+
+    // level of logging
+    // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
+    logLevel: config.LOG_INFO,
+
+
+    // enable / disable watching file and executing tests whenever any file changes
+    autoWatch: false,
+
+
+    // start these browsers
+    // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
+    browsers: ["CustomChromeHeadless"],
+    browserForDebugging: "CustomChromeHeadlessWithDebugPort",
+
+
+    // Continuous Integration mode
+    // if true, Karma captures browsers, runs the tests and exits
+    singleRun: true,
+
+    // Concurrency level
+    // how many browser should be started simultaneous
+    concurrency: Infinity,
+
+    client: {
+      mocha: {
+        ui: 'tdd',
+        timeout: 5000,
+      }
+    },
+
+    customLaunchers: {
+      // Based on https://developers.google.com/web/updates/2017/06/headless-karma-mocha-chai
+      "CustomChromeHeadless": {
+        base: 'ChromeHeadless',
+        flags: ['--disable-translate', '--disable-extensions'],
+      },
+      "ChromeDev": {
+        base: 'Chrome',
+        flags: ['--disable-extensions', ' --auto-open-devtools-for-tabs'],
+      },
+      "CustomChromeHeadlessWithDebugPort": {
+        base: 'CustomChromeHeadless',
+        flags: ['--remote-debugging-port=9222'],
+      }
+    }
+  });
+};
diff --git a/polygerrit-ui/karma_test.sh b/polygerrit-ui/karma_test.sh
new file mode 100755
index 0000000..5fab442
--- /dev/null
+++ b/polygerrit-ui/karma_test.sh
@@ -0,0 +1,4 @@
+#!/bin/bash
+
+set -euo pipefail
+./$1 start $2 --single-run
diff --git a/polygerrit-ui/package.json b/polygerrit-ui/package.json
index 3d35e3e..527763b 100644
--- a/polygerrit-ui/package.json
+++ b/polygerrit-ui/package.json
@@ -4,12 +4,23 @@
   "browser": true,
   "dependencies": {},
   "devDependencies": {
+    "@open-wc/karma-esm": "^2.13.21",
     "@polymer/iron-test-helpers": "^3.0.1",
+    "@polymer/test-fixture": "^4.0.2",
+    "accessibility-developer-tools": "^2.12.0",
     "chai": "^4.2.0",
-    "mocha": "^6.2.2",
+    "karma": "^4.4.1",
+    "karma-chrome-launcher": "^3.1.0",
+    "karma-mocha": "^2.0.1",
+    "karma-mocha-reporter": "^2.2.5",
+    "lodash": "^4.17.15",
+    "mocha": "^7.1.1",
     "wct-browser-legacy": "^1.0.2",
     "web-component-tester": "^6.9.2"
   },
+  "scripts": {
+    "postinstall": "selenium-standalone install"
+  },
   "license": "Apache-2.0",
   "private": true
 }
diff --git a/polygerrit-ui/yarn.lock b/polygerrit-ui/yarn.lock
index 12d39aa..5ad28b6 100644
--- a/polygerrit-ui/yarn.lock
+++ b/polygerrit-ui/yarn.lock
@@ -9,6 +9,15 @@
   dependencies:
     "@babel/highlight" "^7.8.3"
 
+"@babel/compat-data@^7.8.6", "@babel/compat-data@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.9.0.tgz#04815556fc90b0c174abd2c0c1bb966faa036a6c"
+  integrity sha512-zeFQrr+284Ekvd9e7KAX954LkapWiOmQtsfHirhxqfdlX6MEC32iRE+pqUGlYIBchdevaCwvzxWGSy/YBNI85g==
+  dependencies:
+    browserslist "^4.9.1"
+    invariant "^2.2.4"
+    semver "^5.5.0"
+
 "@babel/core@^7.0.0":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.8.3.tgz#30b0ebb4dd1585de6923a0b4d179e0b9f5d82941"
@@ -30,6 +39,28 @@
     semver "^5.4.1"
     source-map "^0.5.0"
 
+"@babel/core@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.9.0.tgz#ac977b538b77e132ff706f3b8a4dbad09c03c56e"
+  integrity sha512-kWc7L0fw1xwvI0zi8OKVBuxRVefwGOrKSQMvrQ3dW+bIIavBY3/NpXmpjMy7bQnLgwgzWQZ8TlM57YHpHNHz4w==
+  dependencies:
+    "@babel/code-frame" "^7.8.3"
+    "@babel/generator" "^7.9.0"
+    "@babel/helper-module-transforms" "^7.9.0"
+    "@babel/helpers" "^7.9.0"
+    "@babel/parser" "^7.9.0"
+    "@babel/template" "^7.8.6"
+    "@babel/traverse" "^7.9.0"
+    "@babel/types" "^7.9.0"
+    convert-source-map "^1.7.0"
+    debug "^4.1.0"
+    gensync "^1.0.0-beta.1"
+    json5 "^2.1.2"
+    lodash "^4.17.13"
+    resolve "^1.3.2"
+    semver "^5.4.1"
+    source-map "^0.5.0"
+
 "@babel/generator@^7.0.0-beta.42", "@babel/generator@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.8.3.tgz#0e22c005b0a94c1c74eafe19ef78ce53a4d45c03"
@@ -40,6 +71,16 @@
     lodash "^4.17.13"
     source-map "^0.5.0"
 
+"@babel/generator@^7.4.0", "@babel/generator@^7.9.0":
+  version "7.9.4"
+  resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.9.4.tgz#12441e90c3b3c4159cdecf312075bf1a8ce2dbce"
+  integrity sha512-rjP8ahaDy/ouhrvCoU1E5mqaitWrxwuNGU+dy1EpaoK48jZay4MdkskKGIMHLZNewg8sAsqpGSREJwP0zH3YQA==
+  dependencies:
+    "@babel/types" "^7.9.0"
+    jsesc "^2.5.1"
+    lodash "^4.17.13"
+    source-map "^0.5.0"
+
 "@babel/helper-annotate-as-pure@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.8.3.tgz#60bc0bc657f63a0924ff9a4b4a0b24a13cf4deee"
@@ -64,6 +105,17 @@
     "@babel/traverse" "^7.8.3"
     "@babel/types" "^7.8.3"
 
+"@babel/helper-compilation-targets@^7.8.7":
+  version "7.8.7"
+  resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.8.7.tgz#dac1eea159c0e4bd46e309b5a1b04a66b53c1dde"
+  integrity sha512-4mWm8DCK2LugIS+p1yArqvG1Pf162upsIsjE7cNBjez+NjliQpVhj20obE520nao0o14DaTnFJv+Fw5a0JpoUw==
+  dependencies:
+    "@babel/compat-data" "^7.8.6"
+    browserslist "^4.9.1"
+    invariant "^2.2.4"
+    levenary "^1.1.1"
+    semver "^5.5.0"
+
 "@babel/helper-create-regexp-features-plugin@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.8.3.tgz#c774268c95ec07ee92476a3862b75cc2839beb79"
@@ -72,6 +124,15 @@
     "@babel/helper-regex" "^7.8.3"
     regexpu-core "^4.6.0"
 
+"@babel/helper-create-regexp-features-plugin@^7.8.8":
+  version "7.8.8"
+  resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.8.8.tgz#5d84180b588f560b7864efaeea89243e58312087"
+  integrity sha512-LYVPdwkrQEiX9+1R29Ld/wTrmQu1SSKYnuOk3g0CkcZMA1p0gsNxJFj/3gBdaJ7Cg0Fnek5z0DsMULePP7Lrqg==
+  dependencies:
+    "@babel/helper-annotate-as-pure" "^7.8.3"
+    "@babel/helper-regex" "^7.8.3"
+    regexpu-core "^4.7.0"
+
 "@babel/helper-define-map@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.8.3.tgz#a0655cad5451c3760b726eba875f1cd8faa02c15"
@@ -138,6 +199,19 @@
     "@babel/types" "^7.8.3"
     lodash "^4.17.13"
 
+"@babel/helper-module-transforms@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.9.0.tgz#43b34dfe15961918707d247327431388e9fe96e5"
+  integrity sha512-0FvKyu0gpPfIQ8EkxlrAydOWROdHpBmiCiRwLkUiBGhCUPRRbVD2/tm3sFr/c/GWFrQ/ffutGUAnx7V0FzT2wA==
+  dependencies:
+    "@babel/helper-module-imports" "^7.8.3"
+    "@babel/helper-replace-supers" "^7.8.6"
+    "@babel/helper-simple-access" "^7.8.3"
+    "@babel/helper-split-export-declaration" "^7.8.3"
+    "@babel/template" "^7.8.6"
+    "@babel/types" "^7.9.0"
+    lodash "^4.17.13"
+
 "@babel/helper-optimise-call-expression@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.8.3.tgz#7ed071813d09c75298ef4f208956006b6111ecb9"
@@ -145,7 +219,7 @@
   dependencies:
     "@babel/types" "^7.8.3"
 
-"@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3":
+"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz#9ea293be19babc0f52ff8ca88b34c3611b208670"
   integrity sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==
@@ -178,6 +252,16 @@
     "@babel/traverse" "^7.8.3"
     "@babel/types" "^7.8.3"
 
+"@babel/helper-replace-supers@^7.8.6":
+  version "7.8.6"
+  resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.8.6.tgz#5ada744fd5ad73203bf1d67459a27dcba67effc8"
+  integrity sha512-PeMArdA4Sv/Wf4zXwBKPqVj7n9UF/xg6slNRtZW84FM7JpE1CbG8B612FyM4cxrf4fMAMGO0kR7voy1ForHHFA==
+  dependencies:
+    "@babel/helper-member-expression-to-functions" "^7.8.3"
+    "@babel/helper-optimise-call-expression" "^7.8.3"
+    "@babel/traverse" "^7.8.6"
+    "@babel/types" "^7.8.6"
+
 "@babel/helper-simple-access@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.8.3.tgz#7f8109928b4dab4654076986af575231deb639ae"
@@ -193,6 +277,11 @@
   dependencies:
     "@babel/types" "^7.8.3"
 
+"@babel/helper-validator-identifier@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.0.tgz#ad53562a7fc29b3b9a91bbf7d10397fd146346ed"
+  integrity sha512-6G8bQKjOh+of4PV/ThDm/rRqlU7+IGoJuofpagU5GlEl29Vv0RGqqt86ZGRV8ZuSOY3o+8yXl5y782SMcG7SHw==
+
 "@babel/helper-wrap-function@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.8.3.tgz#9dbdb2bb55ef14aaa01fe8c99b629bd5352d8610"
@@ -212,6 +301,15 @@
     "@babel/traverse" "^7.8.3"
     "@babel/types" "^7.8.3"
 
+"@babel/helpers@^7.9.0":
+  version "7.9.2"
+  resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.9.2.tgz#b42a81a811f1e7313b88cba8adc66b3d9ae6c09f"
+  integrity sha512-JwLvzlXVPjO8eU9c/wF9/zOIN7X6h8DYf7mG4CiFRZRvZNKEF5dQ3H3V+ASkHoIB3mWhatgl5ONhyqHRI6MppA==
+  dependencies:
+    "@babel/template" "^7.8.3"
+    "@babel/traverse" "^7.9.0"
+    "@babel/types" "^7.9.0"
+
 "@babel/highlight@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.8.3.tgz#28f173d04223eaaa59bc1d439a3836e6d1265797"
@@ -221,6 +319,11 @@
     esutils "^2.0.2"
     js-tokens "^4.0.0"
 
+"@babel/parser@^7.4.3", "@babel/parser@^7.8.6", "@babel/parser@^7.9.0":
+  version "7.9.4"
+  resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.9.4.tgz#68a35e6b0319bbc014465be43828300113f2f2e8"
+  integrity sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA==
+
 "@babel/parser@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.8.3.tgz#790874091d2001c9be6ec426c2eed47bc7679081"
@@ -233,7 +336,7 @@
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.3"
 
-"@babel/plugin-proposal-async-generator-functions@^7.0.0":
+"@babel/plugin-proposal-async-generator-functions@^7.0.0", "@babel/plugin-proposal-async-generator-functions@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.8.3.tgz#bad329c670b382589721b27540c7d288601c6e6f"
   integrity sha512-NZ9zLv848JsV3hs8ryEh7Uaz/0KsmPLqv0+PdkDJL1cJy0K4kOCFa8zc1E3mp+RHPQcpdfb/6GovEsW4VDrOMw==
@@ -242,6 +345,38 @@
     "@babel/helper-remap-async-to-generator" "^7.8.3"
     "@babel/plugin-syntax-async-generators" "^7.8.0"
 
+"@babel/plugin-proposal-dynamic-import@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.8.3.tgz#38c4fe555744826e97e2ae930b0fb4cc07e66054"
+  integrity sha512-NyaBbyLFXFLT9FP+zk0kYlUlA8XtCUbehs67F0nnEg7KICgMc2mNkIeu9TYhKzyXMkrapZFwAhXLdnt4IYHy1w==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/plugin-syntax-dynamic-import" "^7.8.0"
+
+"@babel/plugin-proposal-json-strings@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.8.3.tgz#da5216b238a98b58a1e05d6852104b10f9a70d6b"
+  integrity sha512-KGhQNZ3TVCQG/MjRbAUwuH+14y9q0tpxs1nWWs3pbSleRdDro9SAMMDyye8HhY1gqZ7/NqIc8SKhya0wRDgP1Q==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/plugin-syntax-json-strings" "^7.8.0"
+
+"@babel/plugin-proposal-nullish-coalescing-operator@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.8.3.tgz#e4572253fdeed65cddeecfdab3f928afeb2fd5d2"
+  integrity sha512-TS9MlfzXpXKt6YYomudb/KU7nQI6/xnapG6in1uZxoxDghuSMZsPb6D2fyUwNYSAp4l1iR7QtFOjkqcRYcUsfw==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0"
+
+"@babel/plugin-proposal-numeric-separator@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.8.3.tgz#5d6769409699ec9b3b68684cd8116cedff93bad8"
+  integrity sha512-jWioO1s6R/R+wEHizfaScNsAx+xKgwTLNXSh7tTC4Usj3ItsPEhYkEpU4h+lpnBwq7NBVOJXfO6cRFYcX69JUQ==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/plugin-syntax-numeric-separator" "^7.8.3"
+
 "@babel/plugin-proposal-object-rest-spread@^7.0.0":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.8.3.tgz#eb5ae366118ddca67bed583b53d7554cad9951bb"
@@ -250,6 +385,38 @@
     "@babel/helper-plugin-utils" "^7.8.3"
     "@babel/plugin-syntax-object-rest-spread" "^7.8.0"
 
+"@babel/plugin-proposal-object-rest-spread@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.9.0.tgz#a28993699fc13df165995362693962ba6b061d6f"
+  integrity sha512-UgqBv6bjq4fDb8uku9f+wcm1J7YxJ5nT7WO/jBr0cl0PLKb7t1O6RNR1kZbjgx2LQtsDI9hwoQVmn0yhXeQyow==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/plugin-syntax-object-rest-spread" "^7.8.0"
+
+"@babel/plugin-proposal-optional-catch-binding@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.8.3.tgz#9dee96ab1650eed88646ae9734ca167ac4a9c5c9"
+  integrity sha512-0gkX7J7E+AtAw9fcwlVQj8peP61qhdg/89D5swOkjYbkboA2CVckn3kiyum1DE0wskGb7KJJxBdyEBApDLLVdw==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/plugin-syntax-optional-catch-binding" "^7.8.0"
+
+"@babel/plugin-proposal-optional-chaining@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.9.0.tgz#31db16b154c39d6b8a645292472b98394c292a58"
+  integrity sha512-NDn5tu3tcv4W30jNhmc2hyD5c56G6cXx4TesJubhxrJeCvuuMpttxr0OnNCqbZGhFjLrg+NIhxxC+BK5F6yS3w==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/plugin-syntax-optional-chaining" "^7.8.0"
+
+"@babel/plugin-proposal-unicode-property-regex@^7.4.4", "@babel/plugin-proposal-unicode-property-regex@^7.8.3":
+  version "7.8.8"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.8.8.tgz#ee3a95e90cdc04fe8cd92ec3279fa017d68a0d1d"
+  integrity sha512-EVhjVsMpbhLw9ZfHWSx2iy13Q8Z/eg8e8ccVWt23sWQK5l1UdkoLJPN5w69UA4uITGBnEZD2JOe4QOHycYKv8A==
+  dependencies:
+    "@babel/helper-create-regexp-features-plugin" "^7.8.8"
+    "@babel/helper-plugin-utils" "^7.8.3"
+
 "@babel/plugin-syntax-async-generators@^7.0.0", "@babel/plugin-syntax-async-generators@^7.8.0":
   version "7.8.4"
   resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d"
@@ -257,20 +424,48 @@
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.0"
 
-"@babel/plugin-syntax-dynamic-import@^7.0.0":
+"@babel/plugin-syntax-class-properties@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.8.3.tgz#6cb933a8872c8d359bfde69bbeaae5162fd1e8f7"
+  integrity sha512-UcAyQWg2bAN647Q+O811tG9MrJ38Z10jjhQdKNAL8fsyPzE3cCN/uT+f55cFVY4aGO4jqJAvmqsuY3GQDwAoXg==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-syntax-dynamic-import@^7.0.0", "@babel/plugin-syntax-dynamic-import@^7.8.0", "@babel/plugin-syntax-dynamic-import@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3"
   integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.0"
 
-"@babel/plugin-syntax-import-meta@^7.0.0":
+"@babel/plugin-syntax-import-meta@^7.0.0", "@babel/plugin-syntax-import-meta@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.8.3.tgz#230afff79d3ccc215b5944b438e4e266daf3d84d"
   integrity sha512-vYiGd4wQ9gx0Lngb7+bPCwQXGK/PR6FeTIJ+TIOlq+OfOKG/kCAOO2+IBac3oMM9qV7/fU76hfcqxUaLKZf1hQ==
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.3"
 
+"@babel/plugin-syntax-json-strings@^7.8.0":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a"
+  integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.0", "@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9"
+  integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-numeric-separator@^7.8.0", "@babel/plugin-syntax-numeric-separator@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.8.3.tgz#0e3fb63e09bea1b11e96467271c8308007e7c41f"
+  integrity sha512-H7dCMAdN83PcCmqmkHB5dtp+Xa9a6LKSvA2hiFBC/5alSHxM5VgWZXFqDi0YFe8XNGT6iCa+z4V4zSt/PdZ7Dw==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
 "@babel/plugin-syntax-object-rest-spread@^7.0.0", "@babel/plugin-syntax-object-rest-spread@^7.8.0":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871"
@@ -278,14 +473,35 @@
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.0"
 
-"@babel/plugin-transform-arrow-functions@^7.0.0":
+"@babel/plugin-syntax-optional-catch-binding@^7.8.0":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1"
+  integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-optional-chaining@^7.8.0", "@babel/plugin-syntax-optional-chaining@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a"
+  integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.0"
+
+"@babel/plugin-syntax-top-level-await@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.8.3.tgz#3acdece695e6b13aaf57fc291d1a800950c71391"
+  integrity sha512-kwj1j9lL/6Wd0hROD3b/OZZ7MSrZLqqn9RAZ5+cYYsflQ9HZBIKCUkr3+uL1MEJ1NePiUbf98jjiMQSv0NMR9g==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-arrow-functions@^7.0.0", "@babel/plugin-transform-arrow-functions@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.8.3.tgz#82776c2ed0cd9e1a49956daeb896024c9473b8b6"
   integrity sha512-0MRF+KC8EqH4dbuITCWwPSzsyO3HIWWlm30v8BbbpOrS1B++isGxPnnuq/IZvOX5J2D/p7DQalQm+/2PnlKGxg==
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.3"
 
-"@babel/plugin-transform-async-to-generator@^7.0.0":
+"@babel/plugin-transform-async-to-generator@^7.0.0", "@babel/plugin-transform-async-to-generator@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.8.3.tgz#4308fad0d9409d71eafb9b1a6ee35f9d64b64086"
   integrity sha512-imt9tFLD9ogt56Dd5CI/6XgpukMwd/fLGSrix2httihVe7LOGVPhyhMh1BU5kDM7iHD08i8uUtmV2sWaBFlHVQ==
@@ -294,14 +510,14 @@
     "@babel/helper-plugin-utils" "^7.8.3"
     "@babel/helper-remap-async-to-generator" "^7.8.3"
 
-"@babel/plugin-transform-block-scoped-functions@^7.0.0":
+"@babel/plugin-transform-block-scoped-functions@^7.0.0", "@babel/plugin-transform-block-scoped-functions@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.8.3.tgz#437eec5b799b5852072084b3ae5ef66e8349e8a3"
   integrity sha512-vo4F2OewqjbB1+yaJ7k2EJFHlTP3jR634Z9Cj9itpqNjuLXvhlVxgnjsHsdRgASR8xYDrx6onw4vW5H6We0Jmg==
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.3"
 
-"@babel/plugin-transform-block-scoping@^7.0.0":
+"@babel/plugin-transform-block-scoping@^7.0.0", "@babel/plugin-transform-block-scoping@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.8.3.tgz#97d35dab66857a437c166358b91d09050c868f3a"
   integrity sha512-pGnYfm7RNRgYRi7bids5bHluENHqJhrV4bCZRwc5GamaWIIs07N4rZECcmJL6ZClwjDz1GbdMZFtPs27hTB06w==
@@ -323,7 +539,21 @@
     "@babel/helper-split-export-declaration" "^7.8.3"
     globals "^11.1.0"
 
-"@babel/plugin-transform-computed-properties@^7.0.0":
+"@babel/plugin-transform-classes@^7.9.0":
+  version "7.9.2"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.9.2.tgz#8603fc3cc449e31fdbdbc257f67717536a11af8d"
+  integrity sha512-TC2p3bPzsfvSsqBZo0kJnuelnoK9O3welkUpqSqBQuBF6R5MN2rysopri8kNvtlGIb2jmUO7i15IooAZJjZuMQ==
+  dependencies:
+    "@babel/helper-annotate-as-pure" "^7.8.3"
+    "@babel/helper-define-map" "^7.8.3"
+    "@babel/helper-function-name" "^7.8.3"
+    "@babel/helper-optimise-call-expression" "^7.8.3"
+    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/helper-replace-supers" "^7.8.6"
+    "@babel/helper-split-export-declaration" "^7.8.3"
+    globals "^11.1.0"
+
+"@babel/plugin-transform-computed-properties@^7.0.0", "@babel/plugin-transform-computed-properties@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.8.3.tgz#96d0d28b7f7ce4eb5b120bb2e0e943343c86f81b"
   integrity sha512-O5hiIpSyOGdrQZRQ2ccwtTVkgUDBBiCuK//4RJ6UfePllUTCENOzKxfh6ulckXKc0DixTFLCfb2HVkNA7aDpzA==
@@ -337,14 +567,29 @@
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.3"
 
-"@babel/plugin-transform-duplicate-keys@^7.0.0":
+"@babel/plugin-transform-destructuring@^7.8.3":
+  version "7.8.8"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.8.8.tgz#fadb2bc8e90ccaf5658de6f8d4d22ff6272a2f4b"
+  integrity sha512-eRJu4Vs2rmttFCdhPUM3bV0Yo/xPSdPw6ML9KHs/bjB4bLA5HXlbvYXPOD5yASodGod+krjYx21xm1QmL8dCJQ==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-dotall-regex@^7.4.4", "@babel/plugin-transform-dotall-regex@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.8.3.tgz#c3c6ec5ee6125c6993c5cbca20dc8621a9ea7a6e"
+  integrity sha512-kLs1j9Nn4MQoBYdRXH6AeaXMbEJFaFu/v1nQkvib6QzTj8MZI5OQzqmD83/2jEM1z0DLilra5aWO5YpyC0ALIw==
+  dependencies:
+    "@babel/helper-create-regexp-features-plugin" "^7.8.3"
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-duplicate-keys@^7.0.0", "@babel/plugin-transform-duplicate-keys@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.8.3.tgz#8d12df309aa537f272899c565ea1768e286e21f1"
   integrity sha512-s8dHiBUbcbSgipS4SMFuWGqCvyge5V2ZeAWzR6INTVC3Ltjig/Vw1G2Gztv0vU/hRG9X8IvKvYdoksnUfgXOEQ==
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.3"
 
-"@babel/plugin-transform-exponentiation-operator@^7.0.0":
+"@babel/plugin-transform-exponentiation-operator@^7.0.0", "@babel/plugin-transform-exponentiation-operator@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.8.3.tgz#581a6d7f56970e06bf51560cd64f5e947b70d7b7"
   integrity sha512-zwIpuIymb3ACcInbksHaNcR12S++0MDLKkiqXHl3AzpgdKlFNhog+z/K0+TGW+b0w5pgTq4H6IwV/WhxbGYSjQ==
@@ -359,7 +604,14 @@
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.3"
 
-"@babel/plugin-transform-function-name@^7.0.0":
+"@babel/plugin-transform-for-of@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.9.0.tgz#0f260e27d3e29cd1bb3128da5e76c761aa6c108e"
+  integrity sha512-lTAnWOpMwOXpyDx06N+ywmF3jNbafZEqZ96CGYabxHrxNX8l5ny7dt4bK/rGwAh9utyP2b2Hv7PlZh1AAS54FQ==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-function-name@^7.0.0", "@babel/plugin-transform-function-name@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.8.3.tgz#279373cb27322aaad67c2683e776dfc47196ed8b"
   integrity sha512-rO/OnDS78Eifbjn5Py9v8y0aR+aSYhDhqAwVfsTl0ERuMZyr05L1aFSCJnbv2mmsLkit/4ReeQ9N2BgLnOcPCQ==
@@ -374,13 +626,20 @@
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.3"
 
-"@babel/plugin-transform-literals@^7.0.0":
+"@babel/plugin-transform-literals@^7.0.0", "@babel/plugin-transform-literals@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.8.3.tgz#aef239823d91994ec7b68e55193525d76dbd5dc1"
   integrity sha512-3Tqf8JJ/qB7TeldGl+TT55+uQei9JfYaregDcEAyBZ7akutriFrt6C/wLYIer6OYhleVQvH/ntEhjE/xMmy10A==
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.3"
 
+"@babel/plugin-transform-member-expression-literals@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.8.3.tgz#963fed4b620ac7cbf6029c755424029fa3a40410"
+  integrity sha512-3Wk2EXhnw+rP+IDkK6BdtPKsUE5IeZ6QOGrPYvw52NwBStw9V1ZVzxgK6fSKSxqUvH9eQPR3tm3cOq79HlsKYA==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
 "@babel/plugin-transform-modules-amd@^7.0.0":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.8.3.tgz#65606d44616b50225e76f5578f33c568a0b876a5"
@@ -390,7 +649,58 @@
     "@babel/helper-plugin-utils" "^7.8.3"
     babel-plugin-dynamic-import-node "^2.3.0"
 
-"@babel/plugin-transform-object-super@^7.0.0":
+"@babel/plugin-transform-modules-amd@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.9.0.tgz#19755ee721912cf5bb04c07d50280af3484efef4"
+  integrity sha512-vZgDDF003B14O8zJy0XXLnPH4sg+9X5hFBBGN1V+B2rgrB+J2xIypSN6Rk9imB2hSTHQi5OHLrFWsZab1GMk+Q==
+  dependencies:
+    "@babel/helper-module-transforms" "^7.9.0"
+    "@babel/helper-plugin-utils" "^7.8.3"
+    babel-plugin-dynamic-import-node "^2.3.0"
+
+"@babel/plugin-transform-modules-commonjs@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.9.0.tgz#e3e72f4cbc9b4a260e30be0ea59bdf5a39748940"
+  integrity sha512-qzlCrLnKqio4SlgJ6FMMLBe4bySNis8DFn1VkGmOcxG9gqEyPIOzeQrA//u0HAKrWpJlpZbZMPB1n/OPa4+n8g==
+  dependencies:
+    "@babel/helper-module-transforms" "^7.9.0"
+    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/helper-simple-access" "^7.8.3"
+    babel-plugin-dynamic-import-node "^2.3.0"
+
+"@babel/plugin-transform-modules-systemjs@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.9.0.tgz#e9fd46a296fc91e009b64e07ddaa86d6f0edeb90"
+  integrity sha512-FsiAv/nao/ud2ZWy4wFacoLOm5uxl0ExSQ7ErvP7jpoihLR6Cq90ilOFyX9UXct3rbtKsAiZ9kFt5XGfPe/5SQ==
+  dependencies:
+    "@babel/helper-hoist-variables" "^7.8.3"
+    "@babel/helper-module-transforms" "^7.9.0"
+    "@babel/helper-plugin-utils" "^7.8.3"
+    babel-plugin-dynamic-import-node "^2.3.0"
+
+"@babel/plugin-transform-modules-umd@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.9.0.tgz#e909acae276fec280f9b821a5f38e1f08b480697"
+  integrity sha512-uTWkXkIVtg/JGRSIABdBoMsoIeoHQHPTL0Y2E7xf5Oj7sLqwVsNXOkNk0VJc7vF0IMBsPeikHxFjGe+qmwPtTQ==
+  dependencies:
+    "@babel/helper-module-transforms" "^7.9.0"
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-named-capturing-groups-regex@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.8.3.tgz#a2a72bffa202ac0e2d0506afd0939c5ecbc48c6c"
+  integrity sha512-f+tF/8UVPU86TrCb06JoPWIdDpTNSGGcAtaD9mLP0aYGA0OS0j7j7DHJR0GTFrUZPUU6loZhbsVZgTh0N+Qdnw==
+  dependencies:
+    "@babel/helper-create-regexp-features-plugin" "^7.8.3"
+
+"@babel/plugin-transform-new-target@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.8.3.tgz#60cc2ae66d85c95ab540eb34babb6434d4c70c43"
+  integrity sha512-QuSGysibQpyxexRyui2vca+Cmbljo8bcRckgzYV4kRIsHpVeyeC3JDO63pY+xFZ6bWOBn7pfKZTqV4o/ix9sFw==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-object-super@^7.0.0", "@babel/plugin-transform-object-super@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.8.3.tgz#ebb6a1e7a86ffa96858bd6ac0102d65944261725"
   integrity sha512-57FXk+gItG/GejofIyLIgBKTas4+pEU47IXKDBWFTxdPd7F80H8zybyAY7UoblVfBhBGs2EKM+bJUu2+iUYPDQ==
@@ -407,6 +717,21 @@
     "@babel/helper-get-function-arity" "^7.8.3"
     "@babel/helper-plugin-utils" "^7.8.3"
 
+"@babel/plugin-transform-parameters@^7.8.7":
+  version "7.9.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.9.3.tgz#3028d0cc20ddc733166c6e9c8534559cee09f54a"
+  integrity sha512-fzrQFQhp7mIhOzmOtPiKffvCYQSK10NR8t6BBz2yPbeUHb9OLW8RZGtgDRBn8z2hGcwvKDL3vC7ojPTLNxmqEg==
+  dependencies:
+    "@babel/helper-get-function-arity" "^7.8.3"
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-property-literals@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.8.3.tgz#33194300d8539c1ed28c62ad5087ba3807b98263"
+  integrity sha512-uGiiXAZMqEoQhRWMK17VospMZh5sXWg+dlh2soffpkAl96KAm+WZuJfa6lcELotSRmooLqg0MWdH6UUq85nmmg==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
 "@babel/plugin-transform-regenerator@^7.0.0":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.8.3.tgz#b31031e8059c07495bf23614c97f3d9698bc6ec8"
@@ -414,21 +739,35 @@
   dependencies:
     regenerator-transform "^0.14.0"
 
-"@babel/plugin-transform-shorthand-properties@^7.0.0":
+"@babel/plugin-transform-regenerator@^7.8.7":
+  version "7.8.7"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.8.7.tgz#5e46a0dca2bee1ad8285eb0527e6abc9c37672f8"
+  integrity sha512-TIg+gAl4Z0a3WmD3mbYSk+J9ZUH6n/Yc57rtKRnlA/7rcCvpekHXe0CMZHP1gYp7/KLe9GHTuIba0vXmls6drA==
+  dependencies:
+    regenerator-transform "^0.14.2"
+
+"@babel/plugin-transform-reserved-words@^7.8.3":
+  version "7.8.3"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.8.3.tgz#9a0635ac4e665d29b162837dd3cc50745dfdf1f5"
+  integrity sha512-mwMxcycN3omKFDjDQUl+8zyMsBfjRFr0Zn/64I41pmjv4NJuqcYlEtezwYtw9TFd9WR1vN5kiM+O0gMZzO6L0A==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-shorthand-properties@^7.0.0", "@babel/plugin-transform-shorthand-properties@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.8.3.tgz#28545216e023a832d4d3a1185ed492bcfeac08c8"
   integrity sha512-I9DI6Odg0JJwxCHzbzW08ggMdCezoWcuQRz3ptdudgwaHxTjxw5HgdFJmZIkIMlRymL6YiZcped4TTCB0JcC8w==
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.3"
 
-"@babel/plugin-transform-spread@^7.0.0":
+"@babel/plugin-transform-spread@^7.0.0", "@babel/plugin-transform-spread@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.8.3.tgz#9c8ffe8170fdfb88b114ecb920b82fb6e95fe5e8"
   integrity sha512-CkuTU9mbmAoFOI1tklFWYYbzX5qCIZVXPVy0jpXgGwkplCndQAa58s2jr66fTeQnA64bDox0HL4U56CFYoyC7g==
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.3"
 
-"@babel/plugin-transform-sticky-regex@^7.0.0":
+"@babel/plugin-transform-sticky-regex@^7.0.0", "@babel/plugin-transform-sticky-regex@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.8.3.tgz#be7a1290f81dae767475452199e1f76d6175b100"
   integrity sha512-9Spq0vGCD5Bb4Z/ZXXSK5wbbLFMG085qd2vhL1JYu1WcQ5bXqZBAYRzU1d+p79GcHs2szYv5pVQCX13QgldaWw==
@@ -436,7 +775,7 @@
     "@babel/helper-plugin-utils" "^7.8.3"
     "@babel/helper-regex" "^7.8.3"
 
-"@babel/plugin-transform-template-literals@^7.0.0":
+"@babel/plugin-transform-template-literals@^7.0.0", "@babel/plugin-transform-template-literals@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.8.3.tgz#7bfa4732b455ea6a43130adc0ba767ec0e402a80"
   integrity sha512-820QBtykIQOLFT8NZOcTRJ1UNuztIELe4p9DCgvj4NK+PwluSJ49we7s9FB1HIGNIYT7wFUJ0ar2QpCDj0escQ==
@@ -451,7 +790,14 @@
   dependencies:
     "@babel/helper-plugin-utils" "^7.8.3"
 
-"@babel/plugin-transform-unicode-regex@^7.0.0":
+"@babel/plugin-transform-typeof-symbol@^7.8.4":
+  version "7.8.4"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.8.4.tgz#ede4062315ce0aaf8a657a920858f1a2f35fc412"
+  integrity sha512-2QKyfjGdvuNfHsb7qnBBlKclbD4CfshH2KvDabiijLMGXPHJXGxtDzwIF7bQP+T0ysw8fYTtxPafgfs/c1Lrqg==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.8.3"
+
+"@babel/plugin-transform-unicode-regex@^7.0.0", "@babel/plugin-transform-unicode-regex@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.8.3.tgz#0cef36e3ba73e5c57273effb182f46b91a1ecaad"
   integrity sha512-+ufgJjYdmWfSQ+6NS9VGUR2ns8cjJjYbrbi11mZBTaWm+Fui/ncTLFF28Ei1okavY+xkojGr1eJxNsWYeA5aZw==
@@ -459,6 +805,99 @@
     "@babel/helper-create-regexp-features-plugin" "^7.8.3"
     "@babel/helper-plugin-utils" "^7.8.3"
 
+"@babel/preset-env@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.9.0.tgz#a5fc42480e950ae8f5d9f8f2bbc03f52722df3a8"
+  integrity sha512-712DeRXT6dyKAM/FMbQTV/FvRCms2hPCx+3weRjZ8iQVQWZejWWk1wwG6ViWMyqb/ouBbGOl5b6aCk0+j1NmsQ==
+  dependencies:
+    "@babel/compat-data" "^7.9.0"
+    "@babel/helper-compilation-targets" "^7.8.7"
+    "@babel/helper-module-imports" "^7.8.3"
+    "@babel/helper-plugin-utils" "^7.8.3"
+    "@babel/plugin-proposal-async-generator-functions" "^7.8.3"
+    "@babel/plugin-proposal-dynamic-import" "^7.8.3"
+    "@babel/plugin-proposal-json-strings" "^7.8.3"
+    "@babel/plugin-proposal-nullish-coalescing-operator" "^7.8.3"
+    "@babel/plugin-proposal-numeric-separator" "^7.8.3"
+    "@babel/plugin-proposal-object-rest-spread" "^7.9.0"
+    "@babel/plugin-proposal-optional-catch-binding" "^7.8.3"
+    "@babel/plugin-proposal-optional-chaining" "^7.9.0"
+    "@babel/plugin-proposal-unicode-property-regex" "^7.8.3"
+    "@babel/plugin-syntax-async-generators" "^7.8.0"
+    "@babel/plugin-syntax-dynamic-import" "^7.8.0"
+    "@babel/plugin-syntax-json-strings" "^7.8.0"
+    "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0"
+    "@babel/plugin-syntax-numeric-separator" "^7.8.0"
+    "@babel/plugin-syntax-object-rest-spread" "^7.8.0"
+    "@babel/plugin-syntax-optional-catch-binding" "^7.8.0"
+    "@babel/plugin-syntax-optional-chaining" "^7.8.0"
+    "@babel/plugin-syntax-top-level-await" "^7.8.3"
+    "@babel/plugin-transform-arrow-functions" "^7.8.3"
+    "@babel/plugin-transform-async-to-generator" "^7.8.3"
+    "@babel/plugin-transform-block-scoped-functions" "^7.8.3"
+    "@babel/plugin-transform-block-scoping" "^7.8.3"
+    "@babel/plugin-transform-classes" "^7.9.0"
+    "@babel/plugin-transform-computed-properties" "^7.8.3"
+    "@babel/plugin-transform-destructuring" "^7.8.3"
+    "@babel/plugin-transform-dotall-regex" "^7.8.3"
+    "@babel/plugin-transform-duplicate-keys" "^7.8.3"
+    "@babel/plugin-transform-exponentiation-operator" "^7.8.3"
+    "@babel/plugin-transform-for-of" "^7.9.0"
+    "@babel/plugin-transform-function-name" "^7.8.3"
+    "@babel/plugin-transform-literals" "^7.8.3"
+    "@babel/plugin-transform-member-expression-literals" "^7.8.3"
+    "@babel/plugin-transform-modules-amd" "^7.9.0"
+    "@babel/plugin-transform-modules-commonjs" "^7.9.0"
+    "@babel/plugin-transform-modules-systemjs" "^7.9.0"
+    "@babel/plugin-transform-modules-umd" "^7.9.0"
+    "@babel/plugin-transform-named-capturing-groups-regex" "^7.8.3"
+    "@babel/plugin-transform-new-target" "^7.8.3"
+    "@babel/plugin-transform-object-super" "^7.8.3"
+    "@babel/plugin-transform-parameters" "^7.8.7"
+    "@babel/plugin-transform-property-literals" "^7.8.3"
+    "@babel/plugin-transform-regenerator" "^7.8.7"
+    "@babel/plugin-transform-reserved-words" "^7.8.3"
+    "@babel/plugin-transform-shorthand-properties" "^7.8.3"
+    "@babel/plugin-transform-spread" "^7.8.3"
+    "@babel/plugin-transform-sticky-regex" "^7.8.3"
+    "@babel/plugin-transform-template-literals" "^7.8.3"
+    "@babel/plugin-transform-typeof-symbol" "^7.8.4"
+    "@babel/plugin-transform-unicode-regex" "^7.8.3"
+    "@babel/preset-modules" "^0.1.3"
+    "@babel/types" "^7.9.0"
+    browserslist "^4.9.1"
+    core-js-compat "^3.6.2"
+    invariant "^2.2.2"
+    levenary "^1.1.1"
+    semver "^5.5.0"
+
+"@babel/preset-modules@^0.1.3":
+  version "0.1.3"
+  resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.3.tgz#13242b53b5ef8c883c3cf7dddd55b36ce80fbc72"
+  integrity sha512-Ra3JXOHBq2xd56xSF7lMKXdjBn3T772Y1Wet3yWnkDly9zHvJki029tAFzvAAK5cf4YV3yoxuP61crYRol6SVg==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.0.0"
+    "@babel/plugin-proposal-unicode-property-regex" "^7.4.4"
+    "@babel/plugin-transform-dotall-regex" "^7.4.4"
+    "@babel/types" "^7.4.4"
+    esutils "^2.0.2"
+
+"@babel/runtime@^7.8.4":
+  version "7.9.2"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.9.2.tgz#d90df0583a3a252f09aaa619665367bae518db06"
+  integrity sha512-NE2DtOdufG7R5vnfQUTehdTfNycfUANEtCa9PssN9O/xmTzP4E08UI797ixaei6hBEVL9BI/PsdJS5x7mWoB9Q==
+  dependencies:
+    regenerator-runtime "^0.13.4"
+
+"@babel/template@^7.4.0", "@babel/template@^7.8.6":
+  version "7.8.6"
+  resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.6.tgz#86b22af15f828dfb086474f964dcc3e39c43ce2b"
+  integrity sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==
+  dependencies:
+    "@babel/code-frame" "^7.8.3"
+    "@babel/parser" "^7.8.6"
+    "@babel/types" "^7.8.6"
+
 "@babel/template@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.3.tgz#e02ad04fe262a657809327f578056ca15fd4d1b8"
@@ -483,6 +922,21 @@
     globals "^11.1.0"
     lodash "^4.17.13"
 
+"@babel/traverse@^7.4.3", "@babel/traverse@^7.8.6", "@babel/traverse@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.9.0.tgz#d3882c2830e513f4fe4cec9fe76ea1cc78747892"
+  integrity sha512-jAZQj0+kn4WTHO5dUZkZKhbFrqZE7K5LAQ5JysMnmvGij+wOdr+8lWqPeW0BcF4wFwrEXXtdGO7wcV6YPJcf3w==
+  dependencies:
+    "@babel/code-frame" "^7.8.3"
+    "@babel/generator" "^7.9.0"
+    "@babel/helper-function-name" "^7.8.3"
+    "@babel/helper-split-export-declaration" "^7.8.3"
+    "@babel/parser" "^7.9.0"
+    "@babel/types" "^7.9.0"
+    debug "^4.1.0"
+    globals "^11.1.0"
+    lodash "^4.17.13"
+
 "@babel/types@^7.0.0-beta.42", "@babel/types@^7.8.3":
   version "7.8.3"
   resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.8.3.tgz#5a383dffa5416db1b73dedffd311ffd0788fb31c"
@@ -492,6 +946,62 @@
     lodash "^4.17.13"
     to-fast-properties "^2.0.0"
 
+"@babel/types@^7.4.0", "@babel/types@^7.4.4", "@babel/types@^7.8.6", "@babel/types@^7.9.0":
+  version "7.9.0"
+  resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.9.0.tgz#00b064c3df83ad32b2dbf5ff07312b15c7f1efb5"
+  integrity sha512-BS9JKfXkzzJl8RluW4JGknzpiUV7ZrvTayM6yfqLTVBEnFtyowVIOu6rqxRd5cVO6yGoWf4T8u8dgK9oB+GCng==
+  dependencies:
+    "@babel/helper-validator-identifier" "^7.9.0"
+    lodash "^4.17.13"
+    to-fast-properties "^2.0.0"
+
+"@open-wc/building-utils@^2.16.1":
+  version "2.16.1"
+  resolved "https://registry.yarnpkg.com/@open-wc/building-utils/-/building-utils-2.16.1.tgz#093d74881b996fe9497d628cdf55b6757422d894"
+  integrity sha512-0nUktFyelvSbCc8+T4w4PCyIy3i8blOFS0/EiG5xbVJ0HejDPQLTSmRBpZkn6X57tHhwUfjIdv0EAQuo2sbHEw==
+  dependencies:
+    "@babel/core" "^7.9.0"
+    "@babel/plugin-syntax-dynamic-import" "^7.8.3"
+    "@webcomponents/shadycss" "^1.9.4"
+    "@webcomponents/webcomponentsjs" "^2.4.0"
+    arrify "^2.0.1"
+    browserslist "^4.9.1"
+    chokidar "^3.0.0"
+    clean-css "^4.2.1"
+    clone "^2.1.2"
+    core-js-bundle "^3.6.0"
+    deepmerge "^3.2.0"
+    es-module-shims "^0.4.6"
+    html-minifier "^4.0.0"
+    lru-cache "^5.1.1"
+    minimatch "^3.0.4"
+    parse5 "^5.1.1"
+    path-is-inside "^1.0.2"
+    regenerator-runtime "^0.13.3"
+    resolve "^1.11.1"
+    rimraf "^3.0.0"
+    shady-css-scoped-element "^0.0.2"
+    systemjs "^4.0.0"
+    terser "^4.6.4"
+    valid-url "^1.0.9"
+    whatwg-fetch "^3.0.0"
+    whatwg-url "^7.0.0"
+
+"@open-wc/karma-esm@^2.13.21":
+  version "2.13.21"
+  resolved "https://registry.yarnpkg.com/@open-wc/karma-esm/-/karma-esm-2.13.21.tgz#bef38b4e153b5728a6934de8a926d8bd9b9bb4db"
+  integrity sha512-qJREvj5HbYpUb6IeQXXiylPtqSnknUhBeK3PmhlnVdsXCeuPucmKJHbInd8ThYjX5/UJSp/cWe/Dt4H8GqHPHw==
+  dependencies:
+    "@open-wc/building-utils" "^2.16.1"
+    babel-plugin-istanbul "^5.1.4"
+    chokidar "^3.0.0"
+    deepmerge "^3.2.0"
+    es-dev-server "^1.46.0"
+    minimatch "^3.0.4"
+    node-fetch "^2.6.0"
+    portfinder "^1.0.21"
+    request "^2.88.0"
+
 "@polymer/esm-amd-loader@^1.0.0":
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/@polymer/esm-amd-loader/-/esm-amd-loader-1.0.4.tgz#4e77f2f59b29b01e0ad02aa83d33716cddc5f9f9"
@@ -526,6 +1036,29 @@
   resolved "https://registry.yarnpkg.com/@polymer/test-fixture/-/test-fixture-3.0.0-pre.21.tgz#85152207cb0bf57caebc191c80bb0fdb6952614e"
   integrity sha512-IxzUe6YzaORzUksafHAXHprV29YncOJgr0+1zNAifl0/f+cb5iAd4IWUrnsnVFHG5UGTLjvis5RgV6vvIZPDrA==
 
+"@polymer/test-fixture@^4.0.2":
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/@polymer/test-fixture/-/test-fixture-4.0.2.tgz#2f4777ecdcfb22ee000db35a05e0edf27c722c19"
+  integrity sha512-tLX8tFE4mkc4p84YG5239G0hbgTVv2irZYrSyO0OblUqIRbRoCPmbydm3HRFQkJeAB3rPCtyeZ2roJULsmTG3A==
+
+"@rollup/plugin-node-resolve@^6.1.0":
+  version "6.1.0"
+  resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-6.1.0.tgz#0d2909f4bf606ae34d43a9bc8be06a9b0c850cf0"
+  integrity sha512-Cv7PDIvxdE40SWilY5WgZpqfIUEaDxFxs89zCAHjqyRwlTSuql4M5hjIuc5QYJkOH0/vyiyNXKD72O+LhRipGA==
+  dependencies:
+    "@rollup/pluginutils" "^3.0.0"
+    "@types/resolve" "0.0.8"
+    builtin-modules "^3.1.0"
+    is-module "^1.0.0"
+    resolve "^1.11.1"
+
+"@rollup/pluginutils@^3.0.0":
+  version "3.0.8"
+  resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-3.0.8.tgz#4e94d128d94b90699e517ef045422960d18c8fde"
+  integrity sha512-rYGeAc4sxcZ+kPG/Tw4/fwJODC3IXHYDH4qusdN/b6aLw5LPUbzpecYbEJh4sVQGPFJxd2dBU4kc1H3oy9/bnw==
+  dependencies:
+    estree-walker "^1.0.1"
+
 "@types/babel-generator@^6.25.1":
   version "6.25.3"
   resolved "https://registry.yarnpkg.com/@types/babel-generator/-/babel-generator-6.25.3.tgz#8f06caa12d0595a0538560abe771966d77d29286"
@@ -719,7 +1252,7 @@
   resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.1.tgz#dc488842312a7f075149312905b5e3c0b054c79d"
   integrity sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw==
 
-"@types/minimatch@*", "@types/minimatch@^3.0.1":
+"@types/minimatch@*", "@types/minimatch@^3.0.1", "@types/minimatch@^3.0.3":
   version "3.0.3"
   resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
   integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==
@@ -799,6 +1332,13 @@
   dependencies:
     "@types/node" "*"
 
+"@types/resolve@0.0.8":
+  version "0.0.8"
+  resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-0.0.8.tgz#f26074d238e02659e323ce1a13d041eee280e194"
+  integrity sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ==
+  dependencies:
+    "@types/node" "*"
+
 "@types/serve-static@*", "@types/serve-static@^1.7.31":
   version "1.13.3"
   resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.3.tgz#eb7e1c41c4468272557e897e9171ded5e2ded9d1"
@@ -867,6 +1407,11 @@
   resolved "https://registry.yarnpkg.com/@webcomponents/shadycss/-/shadycss-1.9.4.tgz#4f9d8ea1526bab084c60b53d4854dc39fdb2bb48"
   integrity sha512-tgNcVEaKssyeZPbUBjVQf4aryO5Fi7fxRvOxV982ZJuRVDcefmIblBh0SXAbcvAAlQ2zpNEP4SuQUnr8uApIpw==
 
+"@webcomponents/shadycss@^1.9.4":
+  version "1.9.6"
+  resolved "https://registry.yarnpkg.com/@webcomponents/shadycss/-/shadycss-1.9.6.tgz#a8c5db867e49200a05cf8d5008029c09b7861979"
+  integrity sha512-5fFjvP0jQJZoXK6YzYeYcIDGJ5oEsdjr1L9VaYLw5yxNd4aRz4srMpwCwldeNG0A6Hvr9igbG7fCsBeiiCXd7A==
+
 "@webcomponents/webcomponentsjs@^1.0.7":
   version "1.3.3"
   resolved "https://registry.yarnpkg.com/@webcomponents/webcomponentsjs/-/webcomponentsjs-1.3.3.tgz#5bb82a0d3210c836bd4623e13a4a93145cb9dc27"
@@ -877,7 +1422,17 @@
   resolved "https://registry.yarnpkg.com/@webcomponents/webcomponentsjs/-/webcomponentsjs-2.4.1.tgz#7baadec56ed2fd79b94ddfd509132d8c0c295c5c"
   integrity sha512-7jxBb+KoWncKb/JGFyTY40PjV4yRx2zd35ZLuvRP+6WndJDL7X32ZIZ7bN3sSQIl+NzJkCo7chfXJyzn+6WZaQ==
 
-accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7:
+"@webcomponents/webcomponentsjs@^2.4.0":
+  version "2.4.3"
+  resolved "https://registry.yarnpkg.com/@webcomponents/webcomponentsjs/-/webcomponentsjs-2.4.3.tgz#384f4f6d54563ba465fb4df21fe89e78a76fc530"
+  integrity sha512-cV4+sAmshf8ysU2USutrSRYQkJzEYKHsRCGa0CkMElGpG5747VHtkfsW3NdVIBV/m2MDKXTDydT4lkrysH7IFA==
+
+abortcontroller-polyfill@^1.4.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/abortcontroller-polyfill/-/abortcontroller-polyfill-1.4.0.tgz#0d5eb58e522a461774af8086414f68e1dda7a6c4"
+  integrity sha512-3ZFfCRfDzx3GFjO6RAkYx81lPGpUS20ISxux9gLxuKnqafNcFQo59+IoZqpO2WvQlyc287B62HDnDdNYRmlvWA==
+
+accepts@^1.3.5, accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7:
   version "1.3.7"
   resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd"
   integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==
@@ -983,11 +1538,19 @@
   resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.0.0.tgz#cb102df1c56f5123eab8b67cd7b98027a0279178"
   integrity sha1-yxAt8cVvUSPquLZ817mAJ6AnkXg=
 
-any-promise@^1.0.0:
+any-promise@^1.0.0, any-promise@^1.1.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f"
   integrity sha1-q8av7tzqUugJzcA3au0845Y10X8=
 
+anymatch@~3.1.1:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142"
+  integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==
+  dependencies:
+    normalize-path "^3.0.0"
+    picomatch "^2.0.4"
+
 append-field@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/append-field/-/append-field-1.0.0.tgz#1e3440e915f0b1203d23748e78edd7b9b5b43e56"
@@ -1063,6 +1626,11 @@
   resolved "https://registry.yarnpkg.com/array-back/-/array-back-3.1.0.tgz#b8859d7a508871c9a7b2cf42f99428f65e96bfb0"
   integrity sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==
 
+array-back@^4.0.0, array-back@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/array-back/-/array-back-4.0.1.tgz#9b80312935a52062e1a233a9c7abeb5481b30e90"
+  integrity sha512-Z/JnaVEXv+A9xabHzN43FiiiWEE7gPCRXMrVmRm00tWbjZRul1iHm7ECzlyNq1p4a4ATXz+G9FJ3GqGOkOV3fg==
+
 array-find-index@^1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1"
@@ -1088,6 +1656,11 @@
   resolved "https://registry.yarnpkg.com/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz#3bbc4275dd584cc1b10809b89d4e8b63a69e7675"
   integrity sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==
 
+arrify@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa"
+  integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==
+
 asn1@~0.2.3:
   version "0.2.4"
   resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136"
@@ -1224,6 +1797,16 @@
   dependencies:
     object.assign "^4.1.0"
 
+babel-plugin-istanbul@^5.1.4:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-5.2.0.tgz#df4ade83d897a92df069c4d9a25cf2671293c854"
+  integrity sha512-5LphC0USA8t4i1zCtjbbNb6jJj/9+X6P37Qfirc/70EQ34xKlMW+a1RHGwxGI+SwWpNwZ27HqvzAobeqaXwiZw==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.0.0"
+    find-up "^3.0.0"
+    istanbul-lib-instrument "^3.3.0"
+    test-exclude "^5.2.3"
+
 babel-plugin-minify-builtins@^0.5.0:
   version "0.5.0"
   resolved "https://registry.yarnpkg.com/babel-plugin-minify-builtins/-/babel-plugin-minify-builtins-0.5.0.tgz#31eb82ed1a0d0efdc31312f93b6e4741ce82c36b"
@@ -1456,6 +2039,11 @@
   resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1"
   integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==
 
+base64id@1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/base64id/-/base64id-1.0.0.tgz#47688cb99bb6804f0e06d3e763b1c32e57d8e6b6"
+  integrity sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY=
+
 base64id@2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6"
@@ -1488,6 +2076,11 @@
   dependencies:
     callsite "1.0.0"
 
+binary-extensions@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c"
+  integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==
+
 bl@^2.2.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/bl/-/bl-2.2.0.tgz#e1a574cdf528e4053019bb800b041c0ac88da493"
@@ -1508,7 +2101,12 @@
   resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.5.tgz#d680eeef25f8cd91ad533f5b01eed48e64caf683"
   integrity sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==
 
-body-parser@1.19.0, body-parser@^1.17.2:
+bluebird@^3.3.0:
+  version "3.7.2"
+  resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
+  integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
+
+body-parser@1.19.0, body-parser@^1.16.1, body-parser@^1.17.2:
   version "1.19.0"
   resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
   integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==
@@ -1581,6 +2179,13 @@
     split-string "^3.0.2"
     to-regex "^3.0.1"
 
+braces@^3.0.2, braces@~3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
+  integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
+  dependencies:
+    fill-range "^7.0.1"
+
 browser-capabilities@^1.0.0:
   version "1.1.4"
   resolved "https://registry.yarnpkg.com/browser-capabilities/-/browser-capabilities-1.1.4.tgz#a6bd657a07a134532ad66c722b8949904478b973"
@@ -1599,6 +2204,25 @@
   resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60"
   integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==
 
+browserslist-useragent@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/browserslist-useragent/-/browserslist-useragent-3.0.2.tgz#f0e209b2742baa5de0e451b52e678e8b4402617c"
+  integrity sha512-/UPzK9xZnk5mwwWx4wcuBKAKx/mD3MNY8sUuZ2NPqnr4RVFWZogX+8mOP0cQEYo8j78sHk0hiDNaVXZ1U3hM9A==
+  dependencies:
+    browserslist "^4.6.6"
+    semver "^6.3.0"
+    useragent "^2.3.0"
+
+browserslist@^4.0.0, browserslist@^4.6.6, browserslist@^4.8.3, browserslist@^4.9.1:
+  version "4.11.1"
+  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.11.1.tgz#92f855ee88d6e050e7e7311d987992014f1a1f1b"
+  integrity sha512-DCTr3kDrKEYNw6Jb9HFxVLQNaue8z+0ZfRBRjmCunKDEXEBajKDj2Y+Uelg+Pi29OnvaSGwjOsnRyNEkXzHg5g==
+  dependencies:
+    caniuse-lite "^1.0.30001038"
+    electron-to-chromium "^1.3.390"
+    node-releases "^1.1.53"
+    pkg-up "^2.0.0"
+
 browserstack@^1.2.0:
   version "1.5.3"
   resolved "https://registry.yarnpkg.com/browserstack/-/browserstack-1.5.3.tgz#93ab48799a12ef99dbd074dd595410ddb196a7ac"
@@ -1606,11 +2230,29 @@
   dependencies:
     https-proxy-agent "^2.2.1"
 
+buffer-alloc-unsafe@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0"
+  integrity sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==
+
+buffer-alloc@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec"
+  integrity sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==
+  dependencies:
+    buffer-alloc-unsafe "^1.1.0"
+    buffer-fill "^1.0.0"
+
 buffer-crc32@^0.2.1, buffer-crc32@^0.2.13, buffer-crc32@~0.2.3:
   version "0.2.13"
   resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
   integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=
 
+buffer-fill@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c"
+  integrity sha1-+PeLdniYiO858gXNY39o5wISKyw=
+
 buffer-from@^1.0.0:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
@@ -1624,6 +2266,11 @@
     base64-js "^1.0.2"
     ieee754 "^1.1.4"
 
+builtin-modules@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.1.0.tgz#aad97c15131eb76b65b50ef208e7584cd76a7484"
+  integrity sha512-k0KL0aWZuBt2lrxrcASWDfwOLMnodeQjodT/1SxEQAXsHANgo6ZC/VEaSEHCXt7aSTZ4/4H5LKa+tBXmW7Vtvw==
+
 busboy@^0.2.11:
   version "0.2.14"
   resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.2.14.tgz#6c2a622efcf47c57bbbe1e2a9c37ad36c7925453"
@@ -1637,7 +2284,7 @@
   resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
   integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=
 
-bytes@3.1.0:
+bytes@3.1.0, bytes@^3.0.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
   integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
@@ -1657,12 +2304,20 @@
     union-value "^1.0.0"
     unset-value "^1.0.0"
 
+cache-content-type@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/cache-content-type/-/cache-content-type-1.0.1.tgz#035cde2b08ee2129f4a8315ea8f00a00dba1453c"
+  integrity sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==
+  dependencies:
+    mime-types "^2.1.18"
+    ylru "^1.2.0"
+
 callsite@1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/callsite/-/callsite-1.0.0.tgz#280398e5d664bd74038b6f0905153e6e8af1bc20"
   integrity sha1-KAOY5dZkvXQDi28JBRU+borxvCA=
 
-camel-case@3.0.x:
+camel-case@3.0.x, camel-case@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-3.0.0.tgz#ca3c3688a4e9cf3a4cda777dc4dcbc713249cf73"
   integrity sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M=
@@ -1688,7 +2343,7 @@
   resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd"
   integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=
 
-camelcase@^5.0.0:
+camelcase@^5.0.0, camelcase@^5.3.1:
   version "5.3.1"
   resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
   integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
@@ -1700,6 +2355,21 @@
   dependencies:
     "@types/node" "^4.0.30"
 
+caniuse-api@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0"
+  integrity sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==
+  dependencies:
+    browserslist "^4.0.0"
+    caniuse-lite "^1.0.0"
+    lodash.memoize "^4.1.2"
+    lodash.uniq "^4.5.0"
+
+caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001033, caniuse-lite@^1.0.30001038:
+  version "1.0.30001038"
+  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001038.tgz#44da3cbca2ab6cb6aa83d1be5d324e17f141caff"
+  integrity sha512-zii9quPo96XfOiRD4TrfYGs+QsGZpb2cGiMAzPjtf/hpFgB6zCPZgJb7I1+EATeMw/o+lG8FyRAnI+CWStHcaQ==
+
 capture-stack-trace@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/capture-stack-trace/-/capture-stack-trace-1.0.1.tgz#a6c0bbe1f38f3aa0b92238ecb6ff42c344d4135d"
@@ -1742,7 +2412,7 @@
     strip-ansi "^3.0.0"
     supports-color "^2.0.0"
 
-chalk@^2.0.0, chalk@^2.0.1, chalk@^2.3.0, chalk@^2.4.1:
+chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2:
   version "2.4.2"
   resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
   integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
@@ -1770,6 +2440,36 @@
   resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82"
   integrity sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=
 
+chokidar@3.3.0:
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.3.0.tgz#12c0714668c55800f659e262d4962a97faf554a6"
+  integrity sha512-dGmKLDdT3Gdl7fBUe8XK+gAtGmzy5Fn0XkkWQuYxGIgWVPPse2CxFA5mtrlD0TOHaHjEUqkWNyP1XdHoJES/4A==
+  dependencies:
+    anymatch "~3.1.1"
+    braces "~3.0.2"
+    glob-parent "~5.1.0"
+    is-binary-path "~2.1.0"
+    is-glob "~4.0.1"
+    normalize-path "~3.0.0"
+    readdirp "~3.2.0"
+  optionalDependencies:
+    fsevents "~2.1.1"
+
+chokidar@^3.0.0:
+  version "3.3.1"
+  resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.3.1.tgz#c84e5b3d18d9a4d77558fef466b1bf16bbeb3450"
+  integrity sha512-4QYCEWOcK3OJrxwvyyAOxFuhpvOVCYkr33LPfFNBjAD/w3sEzWsp2BUOkI4l9bHvWioAd0rc6NlHUOEaWkTeqg==
+  dependencies:
+    anymatch "~3.1.1"
+    braces "~3.0.2"
+    glob-parent "~5.1.0"
+    is-binary-path "~2.1.0"
+    is-glob "~4.0.1"
+    normalize-path "~3.0.0"
+    readdirp "~3.3.0"
+  optionalDependencies:
+    fsevents "~2.1.2"
+
 ci-info@^1.5.0:
   version "1.6.0"
   resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.6.0.tgz#2ca20dbb9ceb32d4524a683303313f0304b1e497"
@@ -1792,6 +2492,13 @@
   dependencies:
     source-map "~0.6.0"
 
+clean-css@^4.2.1:
+  version "4.2.3"
+  resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.3.tgz#507b5de7d97b48ee53d84adb0160ff6216380f78"
+  integrity sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA==
+  dependencies:
+    source-map "~0.6.0"
+
 cleankill@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/cleankill/-/cleankill-2.0.0.tgz#59830dfc8b411d53dc72ad09d45a78ea33161a91"
@@ -1821,11 +2528,16 @@
   resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
   integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4=
 
-clone@^2.0.0, clone@^2.1.0:
+clone@^2.0.0, clone@^2.1.0, clone@^2.1.2:
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f"
   integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=
 
+co@^4.6.0:
+  version "4.6.0"
+  resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
+  integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=
+
 collection-visit@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0"
@@ -1872,7 +2584,7 @@
   resolved "https://registry.yarnpkg.com/colornames/-/colornames-1.1.1.tgz#f8889030685c7c4ff9e2a559f5077eb76a816f96"
   integrity sha1-+IiQMGhcfE/54qVZ9Qd+t2qBb5Y=
 
-colors@^1.2.1:
+colors@^1.1.0, colors@^1.2.1:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78"
   integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==
@@ -1912,6 +2624,16 @@
     table-layout "^0.4.3"
     typical "^2.6.1"
 
+command-line-usage@^6.1.0:
+  version "6.1.0"
+  resolved "https://registry.yarnpkg.com/command-line-usage/-/command-line-usage-6.1.0.tgz#f28376a3da3361ff3d36cfd31c3c22c9a64c7cb6"
+  integrity sha512-Ew1clU4pkUeo6AFVDFxCbnN7GIZfXl48HIOQeFQnkO3oOqvpI7wdqtLRwv9iOCZ/7A+z4csVZeiDdEcj8g6Wiw==
+  dependencies:
+    array-back "^4.0.0"
+    chalk "^2.4.2"
+    table-layout "^1.0.0"
+    typical "^5.2.0"
+
 commander@2.17.x:
   version "2.17.1"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf"
@@ -1924,7 +2646,7 @@
   dependencies:
     graceful-readlink ">= 1.0.0"
 
-commander@^2.19.0:
+commander@^2.19.0, commander@^2.20.0, commander@~2.20.3:
   version "2.20.3"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
   integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
@@ -1964,7 +2686,7 @@
     normalize-path "^3.0.0"
     readable-stream "^2.3.6"
 
-compressible@~2.0.16:
+compressible@^2.0.0, compressible@~2.0.16:
   version "2.0.18"
   resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba"
   integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==
@@ -2011,14 +2733,24 @@
     write-file-atomic "^2.0.0"
     xdg-basedir "^3.0.0"
 
-content-disposition@0.5.3:
+connect@^3.6.0:
+  version "3.7.0"
+  resolved "https://registry.yarnpkg.com/connect/-/connect-3.7.0.tgz#5d49348910caa5e07a01800b030d0c35f20484f8"
+  integrity sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==
+  dependencies:
+    debug "2.6.9"
+    finalhandler "1.1.2"
+    parseurl "~1.3.3"
+    utils-merge "1.0.1"
+
+content-disposition@0.5.3, content-disposition@~0.5.2:
   version "0.5.3"
   resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd"
   integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==
   dependencies:
     safe-buffer "5.1.2"
 
-content-type@^1.0.2, content-type@~1.0.4:
+content-type@^1.0.2, content-type@^1.0.4, content-type@~1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
   integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
@@ -2045,11 +2777,32 @@
   resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba"
   integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==
 
+cookies@~0.8.0:
+  version "0.8.0"
+  resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.8.0.tgz#1293ce4b391740a8406e3c9870e828c4b54f3f90"
+  integrity sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow==
+  dependencies:
+    depd "~2.0.0"
+    keygrip "~1.1.0"
+
 copy-descriptor@^0.1.0:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
   integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
 
+core-js-bundle@^3.6.0:
+  version "3.6.4"
+  resolved "https://registry.yarnpkg.com/core-js-bundle/-/core-js-bundle-3.6.4.tgz#d4e098323c035f4a1b61f00db0b8def04c243920"
+  integrity sha512-qDHS3GbIEs5dZaBiCVhhtCoF79KU/ek0w+H7zfJf9RuGN0GiKfxHZfAtDy4zFtQ6X00t7Wvvr3wHzMj+/IgbPg==
+
+core-js-compat@^3.6.2:
+  version "3.6.4"
+  resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.6.4.tgz#938476569ebb6cda80d339bcf199fae4f16fff17"
+  integrity sha512-zAa3IZPvsJ0slViBQ2z+vgyyTuhd3MFn1rBQjZSKVEgB0UMYhUkCj9jJUVPgGTGqWvsBVmfnruXgTcNyTlEiSA==
+  dependencies:
+    browserslist "^4.8.3"
+    semver "7.0.0"
+
 core-js@^2.4.0:
   version "2.6.11"
   resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c"
@@ -2143,6 +2896,11 @@
   dependencies:
     array-find-index "^1.0.1"
 
+custom-event@~1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425"
+  integrity sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU=
+
 dashdash@^1.12.0:
   version "1.14.1"
   resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
@@ -2150,6 +2908,16 @@
   dependencies:
     assert-plus "^1.0.0"
 
+date-format@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/date-format/-/date-format-2.1.0.tgz#31d5b5ea211cf5fd764cd38baf9d033df7e125cf"
+  integrity sha512-bYQuGLeFxhkxNOF3rcMtiZxvCBAquGzZm6oWA1oZ0g2THUzivaRhv8uOhdr19LmoobSOLoIAxeUK2RdbM8IFTA==
+
+debounce@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.0.tgz#44a540abc0ea9943018dc0eaa95cce87f65cd131"
+  integrity sha512-mYtLl1xfZLi1m4RtQYlZgJUNQjl4ZxVnHzIR8nLLgi4q1YT8o/WM+MK/f8yfcc9s5Ir5zRaPZyZU6xs1Syoocg==
+
 debug@2.6.8:
   version "2.6.8"
   resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc"
@@ -2164,7 +2932,7 @@
   dependencies:
     ms "2.0.0"
 
-debug@3.2.6, debug@^3.0.0, debug@^3.1.0:
+debug@3.2.6, debug@^3.0.0, debug@^3.1.0, debug@^3.1.1, debug@^3.2.6:
   version "3.2.6"
   resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
   integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==
@@ -2209,11 +2977,21 @@
   dependencies:
     type-detect "^4.0.0"
 
+deep-equal@~1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
+  integrity sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=
+
 deep-extend@^0.6.0, deep-extend@~0.6.0:
   version "0.6.0"
   resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
   integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
 
+deepmerge@^3.2.0:
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-3.3.0.tgz#d3c47fd6f3a93d517b14426b0628a17b0125f5f7"
+  integrity sha512-GRQOafGHwMHpjPx9iCvTgpu9NojZ49q794EEL94JVEw6VaeA8XTUyBKvAkOOjBX9oJNiV6G3P+T+tihFjo2TqA==
+
 define-properties@^1.1.2, define-properties@^1.1.3:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
@@ -2248,12 +3026,22 @@
   resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
   integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
 
-depd@~1.1.2:
+delegates@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
+  integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=
+
+depd@^1.1.2, depd@~1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
   integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
 
-destroy@~1.0.4:
+depd@~2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
+  integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
+
+destroy@^1.0.4, destroy@~1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
   integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
@@ -2275,6 +3063,11 @@
   resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c"
   integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==
 
+di@^0.0.1:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c"
+  integrity sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw=
+
 diagnostics@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/diagnostics/-/diagnostics-1.1.1.tgz#cab6ac33df70c9d9a727490ae43ac995a769b22a"
@@ -2309,6 +3102,16 @@
   dependencies:
     esutils "^2.0.2"
 
+dom-serialize@^2.2.0:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/dom-serialize/-/dom-serialize-2.2.1.tgz#562ae8999f44be5ea3076f5419dcd59eb43ac95b"
+  integrity sha1-ViromZ9Evl6jB29UGdzVnrQ6yVs=
+  dependencies:
+    custom-event "~1.0.0"
+    ent "~2.2.0"
+    extend "^3.0.0"
+    void-elements "^2.0.0"
+
 dom-urls@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/dom-urls/-/dom-urls-1.1.0.tgz#001ddf81628cd1e706125c7176f53ccec55d918e"
@@ -2354,6 +3157,11 @@
     readable-stream "^2.0.0"
     stream-shift "^1.0.0"
 
+dynamic-import-polyfill@^0.1.1:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/dynamic-import-polyfill/-/dynamic-import-polyfill-0.1.1.tgz#e1f9eb1876ee242bd56572f8ed4df768e143083f"
+  integrity sha512-m953zv0w5oDagTItWm6Auhmk/pY7EiejaqiVbnzSS3HIjh1FCUeK7WzuaVtWPNs58A+/xpIE+/dVk6pKsrua8g==
+
 ecc-jsbn@~0.1.1:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
@@ -2367,6 +3175,11 @@
   resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
   integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
 
+electron-to-chromium@^1.3.390:
+  version "1.3.392"
+  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.392.tgz#280ab4f7a3ae47419cfabb15dbfc1567be7f1111"
+  integrity sha512-/hsgeVdReDsyTBE0aU9FRdh1wnNPrX3xlz3t61F+CJPOT+Umfi9DXHsCX85TEgWZQqlow0Rw44/4/jbU2Sqgkg==
+
 emitter-component@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/emitter-component/-/emitter-component-1.1.1.tgz#065e2dbed6959bf470679edabeaf7981d1003ab6"
@@ -2384,18 +3197,35 @@
   dependencies:
     env-variable "0.0.x"
 
-encodeurl@~1.0.2:
+encodeurl@^1.0.2, encodeurl@~1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
   integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
 
-end-of-stream@^1.0.0, end-of-stream@^1.4.1:
+end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1:
   version "1.4.4"
   resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
   integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
   dependencies:
     once "^1.4.0"
 
+engine.io-client@~3.2.0:
+  version "3.2.1"
+  resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-3.2.1.tgz#6f54c0475de487158a1a7c77d10178708b6add36"
+  integrity sha512-y5AbkytWeM4jQr7m/koQLc5AxpRKC1hEVUb/s1FUAWEJq5AzJJ4NLvzuKPuxtDi5Mq755WuDvZ6Iv2rXj4PTzw==
+  dependencies:
+    component-emitter "1.2.1"
+    component-inherit "0.0.3"
+    debug "~3.1.0"
+    engine.io-parser "~2.1.1"
+    has-cors "1.1.0"
+    indexof "0.0.1"
+    parseqs "0.0.5"
+    parseuri "0.0.5"
+    ws "~3.3.1"
+    xmlhttprequest-ssl "~1.5.4"
+    yeast "0.1.2"
+
 engine.io-client@~3.4.0:
   version "3.4.0"
   resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-3.4.0.tgz#82a642b42862a9b3f7a188f41776b2deab643700"
@@ -2413,6 +3243,17 @@
     xmlhttprequest-ssl "~1.5.4"
     yeast "0.1.2"
 
+engine.io-parser@~2.1.0, engine.io-parser@~2.1.1:
+  version "2.1.3"
+  resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-2.1.3.tgz#757ab970fbf2dfb32c7b74b033216d5739ef79a6"
+  integrity sha512-6HXPre2O4Houl7c4g7Ic/XzPnHBvaEmN90vtRO9uLmwtRqQmTOw0QMevL1TOfL2Cpu1VzsaTmMotQgMdkzGkVA==
+  dependencies:
+    after "0.8.2"
+    arraybuffer.slice "~0.0.7"
+    base64-arraybuffer "0.1.5"
+    blob "0.0.5"
+    has-binary2 "~1.0.2"
+
 engine.io-parser@~2.2.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-2.2.0.tgz#312c4894f57d52a02b420868da7b5c1c84af80ed"
@@ -2424,6 +3265,18 @@
     blob "0.0.5"
     has-binary2 "~1.0.2"
 
+engine.io@~3.2.0:
+  version "3.2.1"
+  resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-3.2.1.tgz#b60281c35484a70ee0351ea0ebff83ec8c9522a2"
+  integrity sha512-+VlKzHzMhaU+GsCIg4AoXF1UdDFjHHwMmMKqMJNDNLlUlejz58FCy4LBqB2YVJskHGYl06BatYWKP2TVdVXE5w==
+  dependencies:
+    accepts "~1.3.4"
+    base64id "1.0.0"
+    cookie "0.3.1"
+    debug "~3.1.0"
+    engine.io-parser "~2.1.0"
+    ws "~3.3.1"
+
 engine.io@~3.4.0:
   version "3.4.0"
   resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-3.4.0.tgz#3a962cc4535928c252759a00f98519cb46c53ff3"
@@ -2436,18 +3289,28 @@
     engine.io-parser "~2.2.0"
     ws "^7.1.2"
 
+ent@~2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d"
+  integrity sha1-6WQhkyWiHQX0RGai9obtbOX13R0=
+
 env-variable@0.0.x:
   version "0.0.5"
   resolved "https://registry.yarnpkg.com/env-variable/-/env-variable-0.0.5.tgz#913dd830bef11e96a039c038d4130604eba37f88"
   integrity sha512-zoB603vQReOFvTg5xMl9I1P2PnHsHQQKTEowsKKD7nseUfJq6UWzK+4YtlWUO1nhiQUxe6XMkk+JleSZD1NZFA==
 
-error-ex@^1.2.0:
+error-ex@^1.2.0, error-ex@^1.3.1:
   version "1.3.2"
   resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
   integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==
   dependencies:
     is-arrayish "^0.2.1"
 
+error-inject@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/error-inject/-/error-inject-1.0.0.tgz#e2b3d91b54aed672f309d950d154850fa11d4f37"
+  integrity sha1-4rPZG1Su1nLzCdlQ0VSFD6EdTzc=
+
 es-abstract@^1.17.0-next.1:
   version "1.17.4"
   resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.4.tgz#e3aedf19706b20e7c2594c35fc0d57605a79e184"
@@ -2465,6 +3328,67 @@
     string.prototype.trimleft "^2.1.1"
     string.prototype.trimright "^2.1.1"
 
+es-dev-server@^1.46.0:
+  version "1.46.0"
+  resolved "https://registry.yarnpkg.com/es-dev-server/-/es-dev-server-1.46.0.tgz#6fa8615604d8bfaa6a181f3bfb8b62d9f4b3dd81"
+  integrity sha512-+6RDz/YeBEkEHcf84I2pS+JYY1ov3d24JAda8hVMFQtpI21+G4/t0YA3lZSIYlPZ6AIoTgmb/+pKQh6JzmOUVA==
+  dependencies:
+    "@babel/core" "^7.9.0"
+    "@babel/plugin-proposal-dynamic-import" "^7.8.3"
+    "@babel/plugin-proposal-nullish-coalescing-operator" "^7.8.3"
+    "@babel/plugin-proposal-optional-chaining" "^7.9.0"
+    "@babel/plugin-syntax-class-properties" "^7.8.3"
+    "@babel/plugin-syntax-import-meta" "^7.8.3"
+    "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3"
+    "@babel/plugin-syntax-numeric-separator" "^7.8.3"
+    "@babel/plugin-syntax-optional-chaining" "^7.8.3"
+    "@babel/plugin-transform-template-literals" "^7.8.3"
+    "@babel/preset-env" "^7.9.0"
+    "@open-wc/building-utils" "^2.16.1"
+    "@rollup/plugin-node-resolve" "^6.1.0"
+    "@rollup/pluginutils" "^3.0.0"
+    "@types/minimatch" "^3.0.3"
+    browserslist "^4.9.1"
+    browserslist-useragent "^3.0.2"
+    builtin-modules "^3.1.0"
+    camelcase "^5.3.1"
+    caniuse-api "^3.0.0"
+    caniuse-lite "^1.0.30001033"
+    chokidar "^3.0.0"
+    command-line-args "^5.0.2"
+    command-line-usage "^6.1.0"
+    debounce "^1.2.0"
+    deepmerge "^3.2.0"
+    es-module-lexer "^0.3.13"
+    get-stream "^5.1.0"
+    is-stream "^2.0.0"
+    isbinaryfile "^4.0.2"
+    koa "^2.7.0"
+    koa-compress "^3.0.0"
+    koa-etag "^3.0.0"
+    koa-static "^5.0.0"
+    lru-cache "^5.1.1"
+    minimatch "^3.0.4"
+    opn "^5.4.0"
+    parse5 "^5.1.1"
+    path-is-inside "^1.0.2"
+    polyfills-loader "^1.5.2"
+    portfinder "^1.0.21"
+    strip-ansi "^5.2.0"
+    systemjs "^4.0.0"
+    useragent "^2.3.0"
+    whatwg-url "^7.0.0"
+
+es-module-lexer@^0.3.13:
+  version "0.3.17"
+  resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.3.17.tgz#a248dec2870934d9054420fead19db095ea21537"
+  integrity sha512-nwvMtzyEB6FhlyXBlV+BW2By3Vn2sUvlQBYP4LvdK8YpdbFQUOiBoeuB7/ip1+EbjmgNydkJ8+dIlyO09VP9BA==
+
+es-module-shims@^0.4.6:
+  version "0.4.6"
+  resolved "https://registry.yarnpkg.com/es-module-shims/-/es-module-shims-0.4.6.tgz#5decb313d52e5c62f6c19ed7e664ee9d66317d8a"
+  integrity sha512-EzVhnLyA/zvmGrAy2RU8m9xpxX7u2yb2by1GZH80SHF6lakG21YAm3Vo56KsLIXaIjT9QabqjYpQU1S5FkM8+Q==
+
 es-to-primitive@^1.2.1:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a"
@@ -2514,12 +3438,17 @@
   resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
   integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
 
+estree-walker@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-1.0.1.tgz#31bc5d612c96b704106b477e6dd5d8aa138cb700"
+  integrity sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==
+
 esutils@^2.0.2:
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
   integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
 
-etag@~1.8.1:
+etag@^1.3.0, etag@~1.8.1:
   version "1.8.1"
   resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
   integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
@@ -2716,7 +3645,14 @@
     repeat-string "^1.6.1"
     to-regex-range "^2.1.0"
 
-finalhandler@~1.1.2:
+fill-range@^7.0.1:
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
+  integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
+  dependencies:
+    to-regex-range "^5.0.1"
+
+finalhandler@1.1.2, finalhandler@~1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d"
   integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==
@@ -2758,6 +3694,13 @@
     path-exists "^2.0.0"
     pinkie-promise "^2.0.0"
 
+find-up@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7"
+  integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c=
+  dependencies:
+    locate-path "^2.0.0"
+
 findup-sync@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-2.0.0.tgz#9326b1488c22d1a6088650a86901b2d9a90a2cbc"
@@ -2780,6 +3723,11 @@
   dependencies:
     is-buffer "~2.0.3"
 
+flatted@^2.0.0:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138"
+  integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==
+
 follow-redirects@^1.0.0:
   version "1.9.0"
   resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.9.0.tgz#8d5bcdc65b7108fe1508649c79c12d732dcedb4f"
@@ -2849,7 +3797,7 @@
   resolved "https://registry.yarnpkg.com/freeport/-/freeport-1.0.5.tgz#255e8ab84170c33ba85d990e821ae5f4a1a9bc5d"
   integrity sha1-JV6KuEFwwzuoXZkOghrl9KGpvF0=
 
-fresh@0.5.2:
+fresh@0.5.2, fresh@~0.5.2:
   version "0.5.2"
   resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
   integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=
@@ -2859,11 +3807,25 @@
   resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
   integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==
 
+fs-extra@^7.0.1:
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9"
+  integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==
+  dependencies:
+    graceful-fs "^4.1.2"
+    jsonfile "^4.0.0"
+    universalify "^0.1.0"
+
 fs.realpath@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
   integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
 
+fsevents@~2.1.1, fsevents@~2.1.2:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.2.tgz#4c0a1fb34bc68e543b4b82a9ec392bfbda840805"
+  integrity sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA==
+
 function-bind@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
@@ -2894,6 +3856,13 @@
   resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
   integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=
 
+get-stream@^5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.1.0.tgz#01203cdc92597f9b909067c3e656cc1f4d3c4dc9"
+  integrity sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==
+  dependencies:
+    pump "^3.0.0"
+
 get-value@^2.0.3, get-value@^2.0.6:
   version "2.0.6"
   resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28"
@@ -2929,6 +3898,13 @@
     is-glob "^3.1.0"
     path-dirname "^1.0.0"
 
+glob-parent@~5.1.0:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229"
+  integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==
+  dependencies:
+    is-glob "^4.0.1"
+
 glob-stream@^5.3.2:
   version "5.3.5"
   resolved "https://registry.yarnpkg.com/glob-stream/-/glob-stream-5.3.5.tgz#a55665a9a8ccdc41915a87c701e32d4e016fad22"
@@ -3044,7 +4020,7 @@
     unzip-response "^2.0.1"
     url-parse-lax "^1.0.0"
 
-graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.3, graceful-fs@^4.2.0:
+graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.3, graceful-fs@^4.1.6, graceful-fs@^4.2.0:
   version "4.2.3"
   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423"
   integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==
@@ -3101,7 +4077,7 @@
   resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
   integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=
 
-har-validator@~5.1.0:
+har-validator@~5.1.0, har-validator@~5.1.3:
   version "5.1.3"
   resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080"
   integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==
@@ -3191,7 +4167,7 @@
   resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd"
   integrity sha1-k0EP0hsAlzUVH4howvJx80J+I/0=
 
-he@1.2.0, he@1.2.x:
+he@1.2.0, he@1.2.x, he@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
   integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
@@ -3231,6 +4207,27 @@
     relateurl "0.2.x"
     uglify-js "3.4.x"
 
+html-minifier@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/html-minifier/-/html-minifier-4.0.0.tgz#cca9aad8bce1175e02e17a8c33e46d8988889f56"
+  integrity sha512-aoGxanpFPLg7MkIl/DDFYtb0iWz7jMFGqFhvEDZga6/4QTjneiD8I/NXL1x5aaoCp7FSIT6h/OhykDdPsbtMig==
+  dependencies:
+    camel-case "^3.0.0"
+    clean-css "^4.2.1"
+    commander "^2.19.0"
+    he "^1.2.0"
+    param-case "^2.1.1"
+    relateurl "^0.2.7"
+    uglify-js "^3.5.1"
+
+http-assert@^1.3.0:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/http-assert/-/http-assert-1.4.1.tgz#c5f725d677aa7e873ef736199b89686cceb37878"
+  integrity sha512-rdw7q6GTlibqVVbXr0CKelfV5iY8G2HqEUkhSk297BMbSpSL8crXC+9rjKoMcZZEsksX30le6f/4ul4E28gegw==
+  dependencies:
+    deep-equal "~1.0.1"
+    http-errors "~1.7.2"
+
 http-deceiver@^1.2.7:
   version "1.2.7"
   resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87"
@@ -3247,17 +4244,7 @@
     statuses ">= 1.5.0 < 2"
     toidentifier "1.0.0"
 
-http-errors@~1.6.2:
-  version "1.6.3"
-  resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d"
-  integrity sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=
-  dependencies:
-    depd "~1.1.2"
-    inherits "2.0.3"
-    setprototypeof "1.1.0"
-    statuses ">= 1.4.0 < 2"
-
-http-errors@~1.7.2:
+http-errors@^1.6.3, http-errors@~1.7.2:
   version "1.7.3"
   resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06"
   integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==
@@ -3268,6 +4255,16 @@
     statuses ">= 1.5.0 < 2"
     toidentifier "1.0.0"
 
+http-errors@~1.6.2:
+  version "1.6.3"
+  resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d"
+  integrity sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=
+  dependencies:
+    depd "~1.1.2"
+    inherits "2.0.3"
+    setprototypeof "1.1.0"
+    statuses ">= 1.4.0 < 2"
+
 http-proxy-middleware@^0.17.2:
   version "0.17.4"
   resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.17.4.tgz#642e8848851d66f09d4f124912846dbaeb41b833"
@@ -3278,7 +4275,7 @@
     lodash "^4.17.2"
     micromatch "^2.3.11"
 
-http-proxy@^1.16.2:
+http-proxy@^1.13.0, http-proxy@^1.16.2:
   version "1.18.0"
   resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.0.tgz#dbe55f63e75a347db7f3d99974f2692a314a6a3a"
   integrity sha512-84I2iJM/n1d4Hdgc6y2+qY5mDaz2PUVjlg9znE9byl+q0uC3DeByqBGReQu5tpLK0TAqTIXScRUV+dg7+bUPpQ==
@@ -3374,7 +4371,12 @@
   resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
   integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
 
-invariant@^2.2.2:
+intersection-observer@^0.7.0:
+  version "0.7.0"
+  resolved "https://registry.yarnpkg.com/intersection-observer/-/intersection-observer-0.7.0.tgz#ee16bee978db53516ead2f0a8154b09b400bbdc9"
+  integrity sha512-Id0Fij0HsB/vKWGeBe9PxeY45ttRiBmhFyyt/geBdDHBYNctMRTE3dC1U3ujzz3lap+hVXlEcVaB56kZP/eEUg==
+
+invariant@^2.2.2, invariant@^2.2.4:
   version "2.2.4"
   resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
   integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==
@@ -3415,6 +4417,13 @@
   resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03"
   integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==
 
+is-binary-path@~2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
+  integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
+  dependencies:
+    binary-extensions "^2.0.0"
+
 is-buffer@^1.1.5, is-buffer@~1.1.1:
   version "1.1.6"
   resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
@@ -3503,7 +4512,7 @@
   resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0"
   integrity sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=
 
-is-extglob@^2.1.0:
+is-extglob@^2.1.0, is-extglob@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
   integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=
@@ -3539,6 +4548,13 @@
   dependencies:
     is-extglob "^2.1.0"
 
+is-glob@^4.0.1, is-glob@~4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc"
+  integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==
+  dependencies:
+    is-extglob "^2.1.1"
+
 is-installed-globally@^0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.1.0.tgz#0dfd98f5a9111716dd535dda6492f67bf3d25a80"
@@ -3547,6 +4563,11 @@
     global-dirs "^0.1.0"
     is-path-inside "^1.0.0"
 
+is-module@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591"
+  integrity sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=
+
 is-npm@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-1.0.0.tgz#f2fb63a65e4905b406c86072765a1a4dc793b9f4"
@@ -3571,6 +4592,11 @@
   resolved "https://registry.yarnpkg.com/is-number/-/is-number-4.0.0.tgz#0026e37f5454d73e356dfe6564699867c6a7f0ff"
   integrity sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==
 
+is-number@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
+  integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
+
 is-obj@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f"
@@ -3622,6 +4648,11 @@
   resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
   integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ=
 
+is-stream@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3"
+  integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==
+
 is-symbol@^1.0.2:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937"
@@ -3649,6 +4680,11 @@
   resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
   integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==
 
+is-wsl@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d"
+  integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=
+
 isarray@0.0.1:
   version "0.0.1"
   resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
@@ -3664,6 +4700,18 @@
   resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.1.tgz#a37d94ed9cda2d59865c9f76fe596ee1f338741e"
   integrity sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=
 
+isbinaryfile@^3.0.0:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-3.0.3.tgz#5d6def3edebf6e8ca8cae9c30183a804b5f8be80"
+  integrity sha512-8cJBL5tTd2OS0dM4jz07wQd5g0dCCqIhUxPIGtZfa5L6hWlvV5MHTITy/DBAsF+Oe2LS1X3krBUhNwaGUWpWxw==
+  dependencies:
+    buffer-alloc "^1.2.0"
+
+isbinaryfile@^4.0.2:
+  version "4.0.5"
+  resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.5.tgz#7193454fdd7fc0b12855c36c48d4ac7368fa3ec9"
+  integrity sha512-Jvz0gpTh1AILHMCBUyqq7xv1ZOQrxTDwyp1/QUq1xFpOBvp4AH5uEobPePJht8KnBGqQIH7We6OR73mXsjG0cA==
+
 isexe@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
@@ -3686,6 +4734,24 @@
   resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
   integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=
 
+istanbul-lib-coverage@^2.0.5:
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz#675f0ab69503fad4b1d849f736baaca803344f49"
+  integrity sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==
+
+istanbul-lib-instrument@^3.3.0:
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz#a5f63d91f0bbc0c3e479ef4c5de027335ec6d630"
+  integrity sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA==
+  dependencies:
+    "@babel/generator" "^7.4.0"
+    "@babel/parser" "^7.4.3"
+    "@babel/template" "^7.4.0"
+    "@babel/traverse" "^7.4.3"
+    "@babel/types" "^7.4.0"
+    istanbul-lib-coverage "^2.0.5"
+    semver "^6.0.0"
+
 "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
@@ -3724,6 +4790,11 @@
   resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
   integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=
 
+json-parse-better-errors@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
+  integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==
+
 json-schema-traverse@^0.4.1:
   version "0.4.1"
   resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
@@ -3756,6 +4827,20 @@
   dependencies:
     minimist "^1.2.0"
 
+json5@^2.1.2:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.2.tgz#43ef1f0af9835dd624751a6b7fa48874fb2d608e"
+  integrity sha512-MoUOQ4WdiN3yxhm7NEVJSJrieAo5hNSLQ5sj05OTRHPL9HOBy8u4Bu88jsC1jvqAdN+E1bJmsUcZH+1HQxliqQ==
+  dependencies:
+    minimist "^1.2.5"
+
+jsonfile@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
+  integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=
+  optionalDependencies:
+    graceful-fs "^4.1.6"
+
 jsonschema@^1.1.0, jsonschema@^1.1.1:
   version "1.2.5"
   resolved "https://registry.yarnpkg.com/jsonschema/-/jsonschema-1.2.5.tgz#bab69d97fa28946aec0a56a9cc266d23fe80ae61"
@@ -3771,6 +4856,68 @@
     json-schema "0.2.3"
     verror "1.10.0"
 
+karma-chrome-launcher@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/karma-chrome-launcher/-/karma-chrome-launcher-3.1.0.tgz#805a586799a4d05f4e54f72a204979f3f3066738"
+  integrity sha512-3dPs/n7vgz1rxxtynpzZTvb9y/GIaW8xjAwcIGttLbycqoFtI7yo1NGnQi6oFTherRE+GIhCAHZC4vEqWGhNvg==
+  dependencies:
+    which "^1.2.1"
+
+karma-mocha-reporter@^2.2.5:
+  version "2.2.5"
+  resolved "https://registry.yarnpkg.com/karma-mocha-reporter/-/karma-mocha-reporter-2.2.5.tgz#15120095e8ed819186e47a0b012f3cd741895560"
+  integrity sha1-FRIAlejtgZGG5HoLAS8810GJVWA=
+  dependencies:
+    chalk "^2.1.0"
+    log-symbols "^2.1.0"
+    strip-ansi "^4.0.0"
+
+karma-mocha@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/karma-mocha/-/karma-mocha-2.0.1.tgz#4b0254a18dfee71bdbe6188d9a6861bf86b0cd7d"
+  integrity sha512-Tzd5HBjm8his2OA4bouAsATYEpZrp9vC7z5E5j4C5Of5Rrs1jY67RAwXNcVmd/Bnk1wgvQRou0zGVLey44G4tQ==
+  dependencies:
+    minimist "^1.2.3"
+
+karma@^4.4.1:
+  version "4.4.1"
+  resolved "https://registry.yarnpkg.com/karma/-/karma-4.4.1.tgz#6d9aaab037a31136dc074002620ee11e8c2e32ab"
+  integrity sha512-L5SIaXEYqzrh6b1wqYC42tNsFMx2PWuxky84pK9coK09MvmL7mxii3G3bZBh/0rvD27lqDd0le9jyhzvwif73A==
+  dependencies:
+    bluebird "^3.3.0"
+    body-parser "^1.16.1"
+    braces "^3.0.2"
+    chokidar "^3.0.0"
+    colors "^1.1.0"
+    connect "^3.6.0"
+    di "^0.0.1"
+    dom-serialize "^2.2.0"
+    flatted "^2.0.0"
+    glob "^7.1.1"
+    graceful-fs "^4.1.2"
+    http-proxy "^1.13.0"
+    isbinaryfile "^3.0.0"
+    lodash "^4.17.14"
+    log4js "^4.0.0"
+    mime "^2.3.1"
+    minimatch "^3.0.2"
+    optimist "^0.6.1"
+    qjobs "^1.1.4"
+    range-parser "^1.2.0"
+    rimraf "^2.6.0"
+    safe-buffer "^5.0.1"
+    socket.io "2.1.1"
+    source-map "^0.6.1"
+    tmp "0.0.33"
+    useragent "2.3.0"
+
+keygrip@~1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.1.0.tgz#871b1681d5e159c62a445b0c74b615e0917e7226"
+  integrity sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==
+  dependencies:
+    tsscmp "1.0.6"
+
 kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0:
   version "3.2.2"
   resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
@@ -3795,6 +4942,97 @@
   resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
   integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
 
+koa-compose@^3.0.0:
+  version "3.2.1"
+  resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-3.2.1.tgz#a85ccb40b7d986d8e5a345b3a1ace8eabcf54de7"
+  integrity sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec=
+  dependencies:
+    any-promise "^1.1.0"
+
+koa-compose@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-4.1.0.tgz#507306b9371901db41121c812e923d0d67d3e877"
+  integrity sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==
+
+koa-compress@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/koa-compress/-/koa-compress-3.0.0.tgz#3194059c215cbc24e59bbc84c2c7453a4c88564f"
+  integrity sha512-xol+LkNB1mozKJkB5Kj6nYXbJXhkLkZlXl9BsGBPjujVfZ8MsIXwU4GHRTT7TlSfUcl2DU3JtC+j6wOWcovfuQ==
+  dependencies:
+    bytes "^3.0.0"
+    compressible "^2.0.0"
+    koa-is-json "^1.0.0"
+    statuses "^1.0.0"
+
+koa-convert@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/koa-convert/-/koa-convert-1.2.0.tgz#da40875df49de0539098d1700b50820cebcd21d0"
+  integrity sha1-2kCHXfSd4FOQmNFwC1CCDOvNIdA=
+  dependencies:
+    co "^4.6.0"
+    koa-compose "^3.0.0"
+
+koa-etag@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/koa-etag/-/koa-etag-3.0.0.tgz#9ef7382ddd5a82ab0deb153415c915836f771d3f"
+  integrity sha1-nvc4Ld1agqsN6xU0FckVg293HT8=
+  dependencies:
+    etag "^1.3.0"
+    mz "^2.1.0"
+
+koa-is-json@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/koa-is-json/-/koa-is-json-1.0.0.tgz#273c07edcdcb8df6a2c1ab7d59ee76491451ec14"
+  integrity sha1-JzwH7c3Ljfaiwat9We52SRRR7BQ=
+
+koa-send@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/koa-send/-/koa-send-5.0.0.tgz#5e8441e07ef55737734d7ced25b842e50646e7eb"
+  integrity sha512-90ZotV7t0p3uN9sRwW2D484rAaKIsD8tAVtypw/aBU+ryfV+fR2xrcAwhI8Wl6WRkojLUs/cB9SBSCuIb+IanQ==
+  dependencies:
+    debug "^3.1.0"
+    http-errors "^1.6.3"
+    mz "^2.7.0"
+    resolve-path "^1.4.0"
+
+koa-static@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/koa-static/-/koa-static-5.0.0.tgz#5e92fc96b537ad5219f425319c95b64772776943"
+  integrity sha512-UqyYyH5YEXaJrf9S8E23GoJFQZXkBVJ9zYYMPGz919MSX1KuvAcycIuS0ci150HCoPf4XQVhQ84Qf8xRPWxFaQ==
+  dependencies:
+    debug "^3.1.0"
+    koa-send "^5.0.0"
+
+koa@^2.7.0:
+  version "2.11.0"
+  resolved "https://registry.yarnpkg.com/koa/-/koa-2.11.0.tgz#fe5a51c46f566d27632dd5dc8fd5d7dd44f935a4"
+  integrity sha512-EpR9dElBTDlaDgyhDMiLkXrPwp6ZqgAIBvhhmxQ9XN4TFgW+gEz6tkcsNI6BnUbUftrKDjVFj4lW2/J2aNBMMA==
+  dependencies:
+    accepts "^1.3.5"
+    cache-content-type "^1.0.0"
+    content-disposition "~0.5.2"
+    content-type "^1.0.4"
+    cookies "~0.8.0"
+    debug "~3.1.0"
+    delegates "^1.0.0"
+    depd "^1.1.2"
+    destroy "^1.0.4"
+    encodeurl "^1.0.2"
+    error-inject "^1.0.0"
+    escape-html "^1.0.3"
+    fresh "~0.5.2"
+    http-assert "^1.3.0"
+    http-errors "^1.6.3"
+    is-generator-function "^1.0.7"
+    koa-compose "^4.1.0"
+    koa-convert "^1.2.0"
+    on-finished "^2.3.0"
+    only "~0.0.2"
+    parseurl "^1.3.2"
+    statuses "^1.5.0"
+    type-is "^1.6.16"
+    vary "^1.1.2"
+
 kuler@1.0.x:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/kuler/-/kuler-1.0.1.tgz#ef7c784f36c9fb6e16dd3150d152677b2b0228a6"
@@ -3830,6 +5068,18 @@
   dependencies:
     readable-stream "^2.0.5"
 
+leven@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2"
+  integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==
+
+levenary@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/levenary/-/levenary-1.1.1.tgz#842a9ee98d2075aa7faeedbe32679e9205f46f77"
+  integrity sha512-mkAdOIt79FD6irqjYSs4rdbnlT5vRonMEvBVPVb3XmevfS8kgRXwfes0dhPdEtzTWD/1eNE/Bm/G1iRt6DcnQQ==
+  dependencies:
+    leven "^3.1.0"
+
 load-json-file@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0"
@@ -3841,6 +5091,24 @@
     pinkie-promise "^2.0.0"
     strip-bom "^2.0.0"
 
+load-json-file@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b"
+  integrity sha1-L19Fq5HjMhYjT9U62rZo607AmTs=
+  dependencies:
+    graceful-fs "^4.1.2"
+    parse-json "^4.0.0"
+    pify "^3.0.0"
+    strip-bom "^3.0.0"
+
+locate-path@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e"
+  integrity sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=
+  dependencies:
+    p-locate "^2.0.0"
+    path-exists "^3.0.0"
+
 locate-path@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e"
@@ -3940,6 +5208,11 @@
     lodash.isarguments "^3.0.0"
     lodash.isarray "^3.0.0"
 
+lodash.memoize@^4.1.2:
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
+  integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
+
 lodash.padend@^4.6.1:
   version "4.6.1"
   resolved "https://registry.yarnpkg.com/lodash.padend/-/lodash.padend-4.6.1.tgz#53ccba047d06e158d311f45da625f4e49e6f166e"
@@ -3970,6 +5243,11 @@
   resolved "https://registry.yarnpkg.com/lodash.union/-/lodash.union-4.6.0.tgz#48bb5088409f16f1821666641c44dd1aaae3cd88"
   integrity sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=
 
+lodash.uniq@^4.5.0:
+  version "4.5.0"
+  resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
+  integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
+
 lodash@^3.0.0, lodash@^3.10.1:
   version "3.10.1"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
@@ -3980,13 +5258,31 @@
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
   integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
 
-log-symbols@2.2.0:
+log-symbols@3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-3.0.0.tgz#f3a08516a5dea893336a7dee14d18a1cfdab77c4"
+  integrity sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==
+  dependencies:
+    chalk "^2.4.2"
+
+log-symbols@^2.1.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a"
   integrity sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==
   dependencies:
     chalk "^2.0.1"
 
+log4js@^4.0.0:
+  version "4.5.1"
+  resolved "https://registry.yarnpkg.com/log4js/-/log4js-4.5.1.tgz#e543625e97d9e6f3e6e7c9fc196dd6ab2cae30b5"
+  integrity sha512-EEEgFcE9bLgaYUKuozyFfytQM2wDHtXn4tAN41pkaxpNjAykv11GVdeI4tHtmPWW4Xrgh9R/2d7XYghDVjbKKw==
+  dependencies:
+    date-format "^2.0.0"
+    debug "^4.1.1"
+    flatted "^2.0.0"
+    rfdc "^1.1.4"
+    streamroller "^1.0.6"
+
 logform@^1.9.1:
   version "1.10.0"
   resolved "https://registry.yarnpkg.com/logform/-/logform-1.10.0.tgz#c9d5598714c92b546e23f4e78147c40f1e02012e"
@@ -4044,7 +5340,7 @@
   resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f"
   integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==
 
-lru-cache@^4.0.1, lru-cache@^4.0.2:
+lru-cache@4.1.x, lru-cache@^4.0.1, lru-cache@^4.0.2:
   version "4.1.5"
   resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd"
   integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==
@@ -4052,6 +5348,13 @@
     pseudomap "^1.0.2"
     yallist "^2.1.2"
 
+lru-cache@^5.1.1:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
+  integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==
+  dependencies:
+    yallist "^3.0.2"
+
 magic-string@^0.22.4:
   version "0.22.5"
   resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.22.5.tgz#8e9cf5afddf44385c1da5bc2a6a0dbd10b03657e"
@@ -4185,7 +5488,7 @@
   resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.43.0.tgz#0a12e0502650e473d735535050e7c8f4eb4fae58"
   integrity sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==
 
-mime-types@^2.1.12, mime-types@~2.1.19, mime-types@~2.1.24:
+mime-types@^2.1.12, mime-types@^2.1.18, mime-types@~2.1.19, mime-types@~2.1.24:
   version "2.1.26"
   resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.26.tgz#9c921fc09b7e149a65dfdc0da4d20997200b0a06"
   integrity sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==
@@ -4236,6 +5539,11 @@
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
   integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=
 
+minimist@^1.2.3, minimist@^1.2.5:
+  version "1.2.5"
+  resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
+  integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
+
 minimist@~0.0.1:
   version "0.0.10"
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf"
@@ -4256,6 +5564,13 @@
   dependencies:
     minimist "0.0.8"
 
+mkdirp@0.5.3:
+  version "0.5.3"
+  resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.3.tgz#5a514b7179259287952881e94410ec5465659f8c"
+  integrity sha512-P+2gwrFqx8lhew375MQHHeTlY8AuOJSrGf0R5ddkEndUkmwpgUob/vQuBD1V22/Cw1/lJr4x+EjllSezBThzBg==
+  dependencies:
+    minimist "^1.2.5"
+
 mocha@^3.4.2:
   version "3.5.3"
   resolved "https://registry.yarnpkg.com/mocha/-/mocha-3.5.3.tgz#1e0480fe36d2da5858d1eb6acc38418b26eaa20d"
@@ -4274,13 +5589,14 @@
     mkdirp "0.5.1"
     supports-color "3.1.2"
 
-mocha@^6.2.2:
-  version "6.2.2"
-  resolved "https://registry.yarnpkg.com/mocha/-/mocha-6.2.2.tgz#5d8987e28940caf8957a7d7664b910dc5b2fea20"
-  integrity sha512-FgDS9Re79yU1xz5d+C4rv1G7QagNGHZ+iXF81hO8zY35YZZcLEsJVfFolfsqKFWunATEvNzMK0r/CwWd/szO9A==
+mocha@^7.1.1:
+  version "7.1.1"
+  resolved "https://registry.yarnpkg.com/mocha/-/mocha-7.1.1.tgz#89fbb30d09429845b1bb893a830bf5771049a441"
+  integrity sha512-3qQsu3ijNS3GkWcccT5Zw0hf/rWvu1fTN9sPvEd81hlwsr30GX2GcDSSoBxo24IR8FelmrAydGC6/1J5QQP4WA==
   dependencies:
     ansi-colors "3.2.3"
     browser-stdout "1.3.1"
+    chokidar "3.3.0"
     debug "3.2.6"
     diff "3.5.0"
     escape-string-regexp "1.0.5"
@@ -4289,18 +5605,18 @@
     growl "1.10.5"
     he "1.2.0"
     js-yaml "3.13.1"
-    log-symbols "2.2.0"
+    log-symbols "3.0.0"
     minimatch "3.0.4"
-    mkdirp "0.5.1"
+    mkdirp "0.5.3"
     ms "2.1.1"
-    node-environment-flags "1.0.5"
+    node-environment-flags "1.0.6"
     object.assign "4.1.0"
     strip-json-comments "2.0.1"
     supports-color "6.0.0"
     which "1.3.1"
     wide-align "1.1.3"
-    yargs "13.3.0"
-    yargs-parser "13.1.1"
+    yargs "13.3.2"
+    yargs-parser "13.1.2"
     yargs-unparser "1.6.0"
 
 mout@^1.0.0:
@@ -4345,7 +5661,7 @@
     duplexer2 "^0.1.2"
     object-assign "^4.1.0"
 
-mz@^2.4.0, mz@^2.6.0:
+mz@^2.1.0, mz@^2.4.0, mz@^2.6.0, mz@^2.7.0:
   version "2.7.0"
   resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32"
   integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==
@@ -4393,14 +5709,24 @@
   dependencies:
     lower-case "^1.1.1"
 
-node-environment-flags@1.0.5:
-  version "1.0.5"
-  resolved "https://registry.yarnpkg.com/node-environment-flags/-/node-environment-flags-1.0.5.tgz#fa930275f5bf5dae188d6192b24b4c8bbac3d76a"
-  integrity sha512-VNYPRfGfmZLx0Ye20jWzHUjyTW/c+6Wq+iLhDzUI4XmhrDd9l/FozXV3F2xOaXjvp0co0+v1YSR3CMP6g+VvLQ==
+node-environment-flags@1.0.6:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/node-environment-flags/-/node-environment-flags-1.0.6.tgz#a30ac13621f6f7d674260a54dede048c3982c088"
+  integrity sha512-5Evy2epuL+6TM0lCQGpFIj6KwiEsGh1SrHUhTbNX+sLbBtjidPZFAnVK9y5yU1+h//RitLbRHTIMyxQPtxMdHw==
   dependencies:
     object.getownpropertydescriptors "^2.0.3"
     semver "^5.7.0"
 
+node-fetch@^2.6.0:
+  version "2.6.0"
+  resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd"
+  integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==
+
+node-releases@^1.1.53:
+  version "1.1.53"
+  resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.53.tgz#2d821bfa499ed7c5dffc5e2f28c88e78a08ee3f4"
+  integrity sha512-wp8zyQVwef2hpZ/dJH7SfSrIPD6YoJz6BDQDpGEkcA0s3LpAQoxBIYmfIq6QAhC1DhwsyCgTaTTcONwX8qzCuQ==
+
 nomnom@^1.8.1:
   version "1.8.1"
   resolved "https://registry.yarnpkg.com/nomnom/-/nomnom-1.8.1.tgz#2151f722472ba79e50a76fc125bb8c8f2e4dc2a7"
@@ -4426,7 +5752,7 @@
   dependencies:
     remove-trailing-separator "^1.0.1"
 
-normalize-path@^3.0.0:
+normalize-path@^3.0.0, normalize-path@~3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
   integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
@@ -4544,7 +5870,7 @@
   resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f"
   integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==
 
-once@^1.3.0, once@^1.4.0:
+once@^1.3.0, once@^1.3.1, once@^1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
   integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
@@ -4556,6 +5882,11 @@
   resolved "https://registry.yarnpkg.com/one-time/-/one-time-0.0.4.tgz#f8cdf77884826fe4dff93e3a9cc37b1e4480742e"
   integrity sha1-+M33eISCb+Tf+T46nMN7HkSAdC4=
 
+only@~0.0.2:
+  version "0.0.2"
+  resolved "https://registry.yarnpkg.com/only/-/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4"
+  integrity sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q=
+
 opn@^3.0.2:
   version "3.0.3"
   resolved "https://registry.yarnpkg.com/opn/-/opn-3.0.3.tgz#b6d99e7399f78d65c3baaffef1fb288e9b85243a"
@@ -4563,6 +5894,13 @@
   dependencies:
     object-assign "^4.0.1"
 
+opn@^5.4.0:
+  version "5.5.0"
+  resolved "https://registry.yarnpkg.com/opn/-/opn-5.5.0.tgz#fc7164fab56d235904c51c3b27da6758ca3b9bfc"
+  integrity sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA==
+  dependencies:
+    is-wsl "^1.1.0"
+
 optimist@^0.6.1:
   version "0.6.1"
   resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686"
@@ -4584,7 +5922,7 @@
   resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
   integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M=
 
-os-tmpdir@^1.0.0, os-tmpdir@^1.0.1:
+os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
   integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=
@@ -4602,6 +5940,13 @@
   resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
   integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=
 
+p-limit@^1.1.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8"
+  integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==
+  dependencies:
+    p-try "^1.0.0"
+
 p-limit@^2.0.0:
   version "2.2.2"
   resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.2.tgz#61279b67721f5287aa1c13a9a7fbbc48c9291b1e"
@@ -4609,6 +5954,13 @@
   dependencies:
     p-try "^2.0.0"
 
+p-locate@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43"
+  integrity sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=
+  dependencies:
+    p-limit "^1.1.0"
+
 p-locate@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4"
@@ -4616,6 +5968,11 @@
   dependencies:
     p-limit "^2.0.0"
 
+p-try@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3"
+  integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=
+
 p-try@^2.0.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
@@ -4631,7 +5988,7 @@
     registry-url "^3.0.3"
     semver "^5.1.0"
 
-param-case@2.1.x:
+param-case@2.1.x, param-case@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/param-case/-/param-case-2.1.1.tgz#df94fd8cf6531ecf75e6bef9a0858fbc72be2247"
   integrity sha1-35T9jPZTHs915r75oIWPvHK+Ikc=
@@ -4655,6 +6012,14 @@
   dependencies:
     error-ex "^1.2.0"
 
+parse-json@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0"
+  integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=
+  dependencies:
+    error-ex "^1.3.1"
+    json-parse-better-errors "^1.0.1"
+
 parse-passwd@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6"
@@ -4665,6 +6030,11 @@
   resolved "https://registry.yarnpkg.com/parse5/-/parse5-4.0.0.tgz#6d78656e3da8d78b4ec0b906f7c08ef1dfe3f608"
   integrity sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==
 
+parse5@^5.1.1:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178"
+  integrity sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==
+
 parseqs@0.0.5:
   version "0.0.5"
   resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.5.tgz#d5208a3738e46766e291ba2ea173684921a8b89d"
@@ -4679,7 +6049,7 @@
   dependencies:
     better-assert "~1.0.0"
 
-parseurl@~1.3.3:
+parseurl@^1.3.2, parseurl@~1.3.3:
   version "1.3.3"
   resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
   integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
@@ -4706,7 +6076,7 @@
   resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
   integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=
 
-path-is-absolute@^1.0.0:
+path-is-absolute@1.0.1, path-is-absolute@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
   integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
@@ -4747,6 +6117,13 @@
     pify "^2.0.0"
     pinkie-promise "^2.0.0"
 
+path-type@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f"
+  integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==
+  dependencies:
+    pify "^3.0.0"
+
 pathval@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.0.tgz#b942e6d4bde653005ef6b71361def8727d0645e0"
@@ -4772,6 +6149,11 @@
   resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
   integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
 
+picomatch@^2.0.4, picomatch@^2.0.7:
+  version "2.2.2"
+  resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad"
+  integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==
+
 pify@^2.0.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
@@ -4794,6 +6176,13 @@
   resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870"
   integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA=
 
+pkg-up@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-2.0.0.tgz#c819ac728059a461cab1c3889a2be3c49a004d7f"
+  integrity sha1-yBmscoBZpGHKscOImivjxJoATX8=
+  dependencies:
+    find-up "^2.1.0"
+
 plist@^2.0.1:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/plist/-/plist-2.1.0.tgz#57ccdb7a0821df21831217a3cad54e3e146a1025"
@@ -4812,6 +6201,28 @@
     winston "^3.0.0"
     winston-transport "^4.2.0"
 
+polyfills-loader@^1.5.2:
+  version "1.5.2"
+  resolved "https://registry.yarnpkg.com/polyfills-loader/-/polyfills-loader-1.5.2.tgz#2fe63063da0d74aa69b611bd189d64ee443358c7"
+  integrity sha512-bcv8Id4Ylae0eSmnlMpKfba36TNr1Mh7uMGN4OEIspHLGR5/IBGSBteQp/NpbZ45T7UWQuTBvVJNQCS16E1N4A==
+  dependencies:
+    "@babel/core" "^7.9.0"
+    "@open-wc/building-utils" "^2.16.1"
+    "@webcomponents/webcomponentsjs" "^2.4.0"
+    abortcontroller-polyfill "^1.4.0"
+    core-js-bundle "^3.6.0"
+    deepmerge "^3.2.0"
+    dynamic-import-polyfill "^0.1.1"
+    es-module-shims "^0.4.6"
+    html-minifier "^4.0.0"
+    intersection-observer "^0.7.0"
+    parse5 "^5.1.1"
+    regenerator-runtime "^0.13.3"
+    resize-observer-polyfill "^1.5.1"
+    systemjs "^4.0.0"
+    terser "^4.6.4"
+    whatwg-fetch "^3.0.0"
+
 polymer-analyzer@^3.1.3, polymer-analyzer@^3.2.2:
   version "3.2.4"
   resolved "https://registry.yarnpkg.com/polymer-analyzer/-/polymer-analyzer-3.2.4.tgz#7d76356620a2328e8bc9e30e47069f9729260ca1"
@@ -5001,6 +6412,15 @@
     send "^0.16.2"
     spdy "^3.3.3"
 
+portfinder@^1.0.21:
+  version "1.0.25"
+  resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.25.tgz#254fd337ffba869f4b9d37edc298059cb4d35eca"
+  integrity sha512-6ElJnHBbxVA1XSLgBp7G1FiCkQdlqGzuF7DswL5tcea+E8UpuvPU7beVAjjRwCioTS9ZluNbu+ZyRvgTsmqEBg==
+  dependencies:
+    async "^2.6.2"
+    debug "^3.1.1"
+    mkdirp "^0.5.1"
+
 posix-character-classes@^0.1.0:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
@@ -5021,7 +6441,7 @@
   resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-4.0.2.tgz#b2bf82e7350d65c6c33aa95aaa5a4f6327f61cd9"
   integrity sha1-sr+C5zUNZcbDOqlaqlpPYyf2HNk=
 
-private@^0.1.6:
+private@^0.1.6, private@^0.1.8:
   version "0.1.8"
   resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
   integrity sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==
@@ -5054,12 +6474,25 @@
   resolved "https://registry.yarnpkg.com/psl/-/psl-1.7.0.tgz#f1c4c47a8ef97167dea5d6bbf4816d736e884a3c"
   integrity sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ==
 
+psl@^1.1.28:
+  version "1.8.0"
+  resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24"
+  integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==
+
+pump@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
+  integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==
+  dependencies:
+    end-of-stream "^1.1.0"
+    once "^1.3.1"
+
 punycode@^1.4.1:
   version "1.4.1"
   resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
   integrity sha1-wNWmOycYgArY4esPpSachN1BhF4=
 
-punycode@^2.1.0:
+punycode@^2.1.0, punycode@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
   integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
@@ -5069,6 +6502,11 @@
   resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
   integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=
 
+qjobs@^1.1.4:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.2.0.tgz#c45e9c61800bd087ef88d7e256423bdd49e5d071"
+  integrity sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==
+
 qs@6.7.0:
   version "6.7.0"
   resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
@@ -5088,7 +6526,7 @@
     kind-of "^6.0.0"
     math-random "^1.0.1"
 
-range-parser@~1.2.0, range-parser@~1.2.1:
+range-parser@^1.2.0, range-parser@~1.2.0, range-parser@~1.2.1:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
   integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
@@ -5121,6 +6559,14 @@
     find-up "^1.0.0"
     read-pkg "^1.0.0"
 
+read-pkg-up@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-4.0.0.tgz#1b221c6088ba7799601c808f91161c66e58f8978"
+  integrity sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA==
+  dependencies:
+    find-up "^3.0.0"
+    read-pkg "^3.0.0"
+
 read-pkg@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28"
@@ -5130,6 +6576,15 @@
     normalize-package-data "^2.3.2"
     path-type "^1.0.0"
 
+read-pkg@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389"
+  integrity sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=
+  dependencies:
+    load-json-file "^4.0.0"
+    normalize-package-data "^2.3.2"
+    path-type "^3.0.0"
+
 readable-stream@1.1.x:
   version "1.1.14"
   resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9"
@@ -5172,6 +6627,20 @@
     string_decoder "^1.1.1"
     util-deprecate "^1.0.1"
 
+readdirp@~3.2.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.2.0.tgz#c30c33352b12c96dfb4b895421a49fd5a9593839"
+  integrity sha512-crk4Qu3pmXwgxdSgGhgA/eXiJAPQiX4GMOZZMXnqKxHX7TaoL+3gQVo/WeuAiogr07DpnfjIMpXXa+PAIvwPGQ==
+  dependencies:
+    picomatch "^2.0.4"
+
+readdirp@~3.3.0:
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.3.0.tgz#984458d13a1e42e2e9f5841b129e162f369aff17"
+  integrity sha512-zz0pAkSPOXXm1viEwygWIPSPkcBYjW1xU5j/JBh5t9bGCJwa6f9+BJa6VaB2g+b55yVrmXzqkyLf4xaWYM0IkQ==
+  dependencies:
+    picomatch "^2.0.7"
+
 redent@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde"
@@ -5185,6 +6654,11 @@
   resolved "https://registry.yarnpkg.com/reduce-flatten/-/reduce-flatten-1.0.1.tgz#258c78efd153ddf93cb561237f61184f3696e327"
   integrity sha1-JYx479FT3fk8tWEjf2EYTzaW4yc=
 
+reduce-flatten@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/reduce-flatten/-/reduce-flatten-2.0.0.tgz#734fd84e65f375d7ca4465c69798c25c9d10ae27"
+  integrity sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w==
+
 regenerate-unicode-properties@^8.1.0:
   version "8.1.0"
   resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.1.0.tgz#ef51e0f0ea4ad424b77bf7cb41f3e015c70a3f0e"
@@ -5192,6 +6666,13 @@
   dependencies:
     regenerate "^1.4.0"
 
+regenerate-unicode-properties@^8.2.0:
+  version "8.2.0"
+  resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec"
+  integrity sha512-F9DjY1vKLo/tPePDycuH3dn9H1OTPIkVD9Kz4LODu+F2C75mgjAJ7x/gwy6ZcSNRAAkhNlJSOHRe8k3p+K9WhA==
+  dependencies:
+    regenerate "^1.4.0"
+
 regenerate@^1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11"
@@ -5202,6 +6683,11 @@
   resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
   integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
 
+regenerator-runtime@^0.13.3, regenerator-runtime@^0.13.4:
+  version "0.13.5"
+  resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz#d878a1d094b4306d10b9096484b33ebd55e26697"
+  integrity sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==
+
 regenerator-transform@^0.14.0:
   version "0.14.1"
   resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.1.tgz#3b2fce4e1ab7732c08f665dfdb314749c7ddd2fb"
@@ -5209,6 +6695,14 @@
   dependencies:
     private "^0.1.6"
 
+regenerator-transform@^0.14.2:
+  version "0.14.4"
+  resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.4.tgz#5266857896518d1616a78a0479337a30ea974cc7"
+  integrity sha512-EaJaKPBI9GvKpvUz2mz4fhx7WPgvwRLY9v3hlNHWmAuJHI13T4nwKnNvm5RWJzEdnI5g5UwtOww+S8IdoUC2bw==
+  dependencies:
+    "@babel/runtime" "^7.8.4"
+    private "^0.1.8"
+
 regex-cache@^0.4.2:
   version "0.4.4"
   resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.4.tgz#75bdc58a2a1496cec48a12835bc54c8d562336dd"
@@ -5236,6 +6730,18 @@
     unicode-match-property-ecmascript "^1.0.4"
     unicode-match-property-value-ecmascript "^1.1.0"
 
+regexpu-core@^4.7.0:
+  version "4.7.0"
+  resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.7.0.tgz#fcbf458c50431b0bb7b45d6967b8192d91f3d938"
+  integrity sha512-TQ4KXRnIn6tz6tjnrXEkD/sshygKH/j5KzK86X8MkeHyZ8qst/LZ89j3X4/8HEIfHANTFIP/AbXakeRhWIl5YQ==
+  dependencies:
+    regenerate "^1.4.0"
+    regenerate-unicode-properties "^8.2.0"
+    regjsgen "^0.5.1"
+    regjsparser "^0.6.4"
+    unicode-match-property-ecmascript "^1.0.4"
+    unicode-match-property-value-ecmascript "^1.2.0"
+
 registry-auth-token@^3.0.1:
   version "3.4.0"
   resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-3.4.0.tgz#d7446815433f5d5ed6431cd5dca21048f66b397e"
@@ -5251,7 +6757,7 @@
   dependencies:
     rc "^1.0.1"
 
-regjsgen@^0.5.0:
+regjsgen@^0.5.0, regjsgen@^0.5.1:
   version "0.5.1"
   resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.1.tgz#48f0bf1a5ea205196929c0d9798b42d1ed98443c"
   integrity sha512-5qxzGZjDs9w4tzT3TPhCJqWdCc3RLYwy9J2NB0nm5Lz+S273lvWcpjaTGHsT1dc6Hhfq41uSEOw8wBmxrKOuyg==
@@ -5263,7 +6769,14 @@
   dependencies:
     jsesc "~0.5.0"
 
-relateurl@0.2.x:
+regjsparser@^0.6.4:
+  version "0.6.4"
+  resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.6.4.tgz#a769f8684308401a66e9b529d2436ff4d0666272"
+  integrity sha512-64O87/dPDgfk8/RQqC4gkZoGyyWFIEUTTh80CU6CWuK5vkCGyekIx+oKcEIYtP/RAxSQltCZHCNu/mdd7fqlJw==
+  dependencies:
+    jsesc "~0.5.0"
+
+relateurl@0.2.x, relateurl@^0.2.7:
   version "0.2.7"
   resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9"
   integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=
@@ -5321,6 +6834,32 @@
     tunnel-agent "^0.6.0"
     uuid "^3.3.2"
 
+request@^2.88.0:
+  version "2.88.2"
+  resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3"
+  integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==
+  dependencies:
+    aws-sign2 "~0.7.0"
+    aws4 "^1.8.0"
+    caseless "~0.12.0"
+    combined-stream "~1.0.6"
+    extend "~3.0.2"
+    forever-agent "~0.6.1"
+    form-data "~2.3.2"
+    har-validator "~5.1.3"
+    http-signature "~1.2.0"
+    is-typedarray "~1.0.0"
+    isstream "~0.1.2"
+    json-stringify-safe "~5.0.1"
+    mime-types "~2.1.19"
+    oauth-sign "~0.9.0"
+    performance-now "^2.1.0"
+    qs "~6.5.2"
+    safe-buffer "^5.1.2"
+    tough-cookie "~2.5.0"
+    tunnel-agent "^0.6.0"
+    uuid "^3.3.2"
+
 require-directory@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
@@ -5341,6 +6880,11 @@
   resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
   integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
 
+resize-observer-polyfill@^1.5.1:
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
+  integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
+
 resolve-dir@^1.0.0, resolve-dir@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-1.0.1.tgz#79a40644c362be82f26effe739c9bb5382046f43"
@@ -5349,6 +6893,14 @@
     expand-tilde "^2.0.0"
     global-modules "^1.0.0"
 
+resolve-path@^1.4.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/resolve-path/-/resolve-path-1.4.0.tgz#c4bda9f5efb2fce65247873ab36bb4d834fe16f7"
+  integrity sha1-xL2p9e+y/OZSR4c6s2u02DT+Fvc=
+  dependencies:
+    http-errors "~1.6.2"
+    path-is-absolute "1.0.1"
+
 resolve-url@^0.2.1:
   version "0.2.1"
   resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
@@ -5361,12 +6913,24 @@
   dependencies:
     path-parse "^1.0.6"
 
+resolve@^1.11.1:
+  version "1.15.1"
+  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.15.1.tgz#27bdcdeffeaf2d6244b95bb0f9f4b4653451f3e8"
+  integrity sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==
+  dependencies:
+    path-parse "^1.0.6"
+
 ret@~0.1.10:
   version "0.1.15"
   resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
   integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
 
-rimraf@^2.5.4:
+rfdc@^1.1.4:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.1.4.tgz#ba72cc1367a0ccd9cf81a870b3b58bd3ad07f8c2"
+  integrity sha512-5C9HXdzK8EAqN7JDif30jqsBzavB7wLpaubisuQIGHWf2gUXSpzy6ArX/+Da8RjFpagWsCn+pIgxTMAmKw9Zug==
+
+rimraf@^2.5.4, rimraf@^2.6.0:
   version "2.7.1"
   resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
   integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
@@ -5480,6 +7044,16 @@
   resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
   integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
 
+semver@7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e"
+  integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==
+
+semver@^6.0.0, semver@^6.3.0:
+  version "6.3.0"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
+  integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
+
 send@0.17.1:
   version "0.17.1"
   resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8"
@@ -5568,6 +7142,11 @@
   resolved "https://registry.yarnpkg.com/shady-css-parser/-/shady-css-parser-0.1.0.tgz#534dc79c8ca5884c5ed92a4e5a13d6d863bca428"
   integrity sha512-irfJUUkEuDlNHKZNAp2r7zOyMlmbfVJ+kWSfjlCYYUx/7dJnANLCyTzQZsuxy5NJkvtNwSxY5Gj8MOlqXUQPyA==
 
+shady-css-scoped-element@^0.0.2:
+  version "0.0.2"
+  resolved "https://registry.yarnpkg.com/shady-css-scoped-element/-/shady-css-scoped-element-0.0.2.tgz#c538fcfe2317e979cd02dfec533898b95b4ea8fe"
+  integrity sha512-Dqfl70x6JiwYDujd33ZTbtCK0t52E7+H2swdWQNSTzfsolSa6LJHnTpN4T9OpJJEq4bxuzHRLFO9RBcy/UfrMQ==
+
 shebang-command@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
@@ -5656,6 +7235,26 @@
   resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-1.1.2.tgz#ab3f0d6f66b8fc7fca3959ab5991f82221789be9"
   integrity sha512-WzZRUj1kUjrTIrUKpZLEzFZ1OLj5FwLlAFQs9kuZJzJi5DKdU7FsWc36SNmA8iDOtwBQyT8FkrriRM8vXLYz8g==
 
+socket.io-client@2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-2.1.1.tgz#dcb38103436ab4578ddb026638ae2f21b623671f"
+  integrity sha512-jxnFyhAuFxYfjqIgduQlhzqTcOEQSn+OHKVfAxWaNWa7ecP7xSNk2Dx/3UEsDcY7NcFafxvNvKPmmO7HTwTxGQ==
+  dependencies:
+    backo2 "1.0.2"
+    base64-arraybuffer "0.1.5"
+    component-bind "1.0.0"
+    component-emitter "1.2.1"
+    debug "~3.1.0"
+    engine.io-client "~3.2.0"
+    has-binary2 "~1.0.2"
+    has-cors "1.1.0"
+    indexof "0.0.1"
+    object-component "0.0.3"
+    parseqs "0.0.5"
+    parseuri "0.0.5"
+    socket.io-parser "~3.2.0"
+    to-array "0.1.4"
+
 socket.io-client@2.3.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-2.3.0.tgz#14d5ba2e00b9bcd145ae443ab96b3f86cbcc1bb4"
@@ -5676,6 +7275,15 @@
     socket.io-parser "~3.3.0"
     to-array "0.1.4"
 
+socket.io-parser@~3.2.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.2.0.tgz#e7c6228b6aa1f814e6148aea325b51aa9499e077"
+  integrity sha512-FYiBx7rc/KORMJlgsXysflWx/RIvtqZbyGLlHZvjfmPTPeuD/I8MaW7cfFrj5tRltICJdgwflhfZ3NVVbVLFQA==
+  dependencies:
+    component-emitter "1.2.1"
+    debug "~3.1.0"
+    isarray "2.0.1"
+
 socket.io-parser@~3.3.0:
   version "3.3.0"
   resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.3.0.tgz#2b52a96a509fdf31440ba40fed6094c7d4f1262f"
@@ -5694,6 +7302,18 @@
     debug "~4.1.0"
     isarray "2.0.1"
 
+socket.io@2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-2.1.1.tgz#a069c5feabee3e6b214a75b40ce0652e1cfb9980"
+  integrity sha512-rORqq9c+7W0DAK3cleWNSyfv/qKXV99hV4tZe+gGLfBECw3XEhBy7x85F3wypA9688LKjtwO9pX9L33/xQI8yA==
+  dependencies:
+    debug "~3.1.0"
+    engine.io "~3.2.0"
+    has-binary2 "~1.0.2"
+    socket.io-adapter "~1.1.0"
+    socket.io-client "2.1.1"
+    socket.io-parser "~3.2.0"
+
 socket.io@^2.0.3:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-2.3.0.tgz#cd762ed6a4faeca59bc1f3e243c0969311eb73fb"
@@ -5717,6 +7337,14 @@
     source-map-url "^0.4.0"
     urix "^0.1.0"
 
+source-map-support@~0.5.12:
+  version "0.5.16"
+  resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.16.tgz#0ae069e7fe3ba7538c64c98515e35339eac5a042"
+  integrity sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==
+  dependencies:
+    buffer-from "^1.0.0"
+    source-map "^0.6.0"
+
 source-map-url@^0.4.0:
   version "0.4.0"
   resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3"
@@ -5727,7 +7355,7 @@
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
   integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
 
-source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1:
+source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1:
   version "0.6.1"
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
   integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
@@ -5836,7 +7464,7 @@
     define-property "^0.2.5"
     object-copy "^0.1.0"
 
-"statuses@>= 1.4.0 < 2", "statuses@>= 1.5.0 < 2", statuses@~1.5.0:
+"statuses@>= 1.4.0 < 2", "statuses@>= 1.5.0 < 2", statuses@^1.0.0, statuses@^1.5.0, statuses@~1.5.0:
   version "1.5.0"
   resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
   integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
@@ -5858,6 +7486,17 @@
   dependencies:
     emitter-component "^1.1.1"
 
+streamroller@^1.0.6:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-1.0.6.tgz#8167d8496ed9f19f05ee4b158d9611321b8cacd9"
+  integrity sha512-3QC47Mhv3/aZNFpDDVO44qQb9gwB9QggMEE0sQmkTAwBVYdBRWISdsywlkfm5II1Q5y/pmrHflti/IgmIzdDBg==
+  dependencies:
+    async "^2.6.2"
+    date-format "^2.0.0"
+    debug "^3.2.6"
+    fs-extra "^7.0.1"
+    lodash "^4.17.14"
+
 streamsearch@0.1.2:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a"
@@ -5956,6 +7595,11 @@
   dependencies:
     is-utf8 "^0.2.0"
 
+strip-bom@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
+  integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=
+
 strip-eof@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
@@ -6028,6 +7672,11 @@
     path-to-regexp "^1.0.1"
     serviceworker-cache-polyfill "^4.0.0"
 
+systemjs@^4.0.0:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/systemjs/-/systemjs-4.1.1.tgz#c90061456f9707478d487b47f3b92b9896032889"
+  integrity sha512-/0x3bcMrl1pxDCLw6sJWEKPVy0ZGEu7I0nItFSHxfPoDU2Lll6TUyB1wqltvbm7n5y5jVOoK4lei4oMpmW7XJQ==
+
 table-layout@^0.4.3:
   version "0.4.5"
   resolved "https://registry.yarnpkg.com/table-layout/-/table-layout-0.4.5.tgz#d906de6a25fa09c0c90d1d08ecd833ecedcb7378"
@@ -6039,6 +7688,16 @@
     typical "^2.6.1"
     wordwrapjs "^3.0.0"
 
+table-layout@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/table-layout/-/table-layout-1.0.1.tgz#8411181ee951278ad0638aea2f779a9ce42894f9"
+  integrity sha512-dEquqYNJiGwY7iPfZ3wbXDI944iqanTSchrACLL2nOB+1r+h1Nzu2eH+DuPPvWvm5Ry7iAPeFlgEtP5bIp5U7Q==
+  dependencies:
+    array-back "^4.0.1"
+    deep-extend "~0.6.0"
+    typical "^5.2.0"
+    wordwrapjs "^4.0.0"
+
 tar-stream@2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.0.0.tgz#8829bbf83067bc0288a9089db49c56be395b6aea"
@@ -6085,6 +7744,25 @@
     merge-stream "^1.0.0"
     through2 "^2.0.1"
 
+terser@^4.6.4:
+  version "4.6.10"
+  resolved "https://registry.yarnpkg.com/terser/-/terser-4.6.10.tgz#90f5bd069ff456ddbc9503b18e52f9c493d3b7c2"
+  integrity sha512-qbF/3UOo11Hggsbsqm2hPa6+L4w7bkr+09FNseEe8xrcVD3APGLFqE+Oz1ZKAxjYnFsj80rLOfgAtJ0LNJjtTA==
+  dependencies:
+    commander "^2.20.0"
+    source-map "~0.6.1"
+    source-map-support "~0.5.12"
+
+test-exclude@^5.2.3:
+  version "5.2.3"
+  resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-5.2.3.tgz#c3d3e1e311eb7ee405e092dac10aefd09091eac0"
+  integrity sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g==
+  dependencies:
+    glob "^7.1.3"
+    minimatch "^3.0.4"
+    read-pkg-up "^4.0.0"
+    require-main-filename "^2.0.0"
+
 text-encoding@0.6.4:
   version "0.6.4"
   resolved "https://registry.yarnpkg.com/text-encoding/-/text-encoding-0.6.4.tgz#e399a982257a276dae428bb92845cb71bdc26d19"
@@ -6146,6 +7824,13 @@
   resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f"
   integrity sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=
 
+tmp@0.0.33, tmp@0.0.x:
+  version "0.0.33"
+  resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
+  integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==
+  dependencies:
+    os-tmpdir "~1.0.2"
+
 to-absolute-glob@^0.1.1:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/to-absolute-glob/-/to-absolute-glob-0.1.1.tgz#1cdfa472a9ef50c239ee66999b662ca0eb39937f"
@@ -6183,6 +7868,13 @@
     is-number "^3.0.0"
     repeat-string "^1.6.1"
 
+to-regex-range@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
+  integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
+  dependencies:
+    is-number "^7.0.0"
+
 to-regex@^3.0.1, to-regex@^3.0.2:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce"
@@ -6206,6 +7898,14 @@
     psl "^1.1.24"
     punycode "^1.4.1"
 
+tough-cookie@~2.5.0:
+  version "2.5.0"
+  resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"
+  integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==
+  dependencies:
+    psl "^1.1.28"
+    punycode "^2.1.1"
+
 tr46@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09"
@@ -6228,6 +7928,11 @@
   resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9"
   integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==
 
+tsscmp@1.0.6:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/tsscmp/-/tsscmp-1.0.6.tgz#85b99583ac3589ec4bfef825b5000aa911d605eb"
+  integrity sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==
+
 tunnel-agent@^0.6.0:
   version "0.6.0"
   resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
@@ -6255,7 +7960,7 @@
   resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
   integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==
 
-type-is@^1.6.4, type-is@~1.6.17, type-is@~1.6.18:
+type-is@^1.6.16, type-is@^1.6.4, type-is@~1.6.17, type-is@~1.6.18:
   version "1.6.18"
   resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
   integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
@@ -6278,6 +7983,11 @@
   resolved "https://registry.yarnpkg.com/typical/-/typical-4.0.0.tgz#cbeaff3b9d7ae1e2bbfaf5a4e6f11eccfde94fc4"
   integrity sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==
 
+typical@^5.0.0, typical@^5.2.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/typical/-/typical-5.2.0.tgz#4daaac4f2b5315460804f0acf6cb69c52bb93066"
+  integrity sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==
+
 ua-parser-js@^0.7.15:
   version "0.7.21"
   resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.21.tgz#853cf9ce93f642f67174273cc34565ae6f308777"
@@ -6291,6 +8001,19 @@
     commander "~2.19.0"
     source-map "~0.6.1"
 
+uglify-js@^3.5.1:
+  version "3.8.1"
+  resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.8.1.tgz#43bb15ce6f545eaa0a64c49fd29375ea09fa0f93"
+  integrity sha512-W7KxyzeaQmZvUFbGj4+YFshhVrMBGSg2IbcYAjGWGvx8DHvJMclbTDMpffdxFUGPBHjIytk7KJUR/KUXstUGDw==
+  dependencies:
+    commander "~2.20.3"
+    source-map "~0.6.1"
+
+ultron@~1.1.0:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz#9fe1536a10a664a65266a1e3ccf85fd36302bc9c"
+  integrity sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==
+
 underscore@^1.8.3:
   version "1.9.2"
   resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.9.2.tgz#0c8d6f536d6f378a5af264a72f7bec50feb7cf2f"
@@ -6319,6 +8042,11 @@
   resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.1.0.tgz#5b4b426e08d13a80365e0d657ac7a6c1ec46a277"
   integrity sha512-hDTHvaBk3RmFzvSl0UVrUmC3PuW9wKVnpoUDYH0JDkSIovzw+J5viQmeYHxVSBptubnr7PbH2e0fnpDRQnQl5g==
 
+unicode-match-property-value-ecmascript@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.2.0.tgz#0d91f600eeeb3096aa962b1d6fc88876e64ea531"
+  integrity sha512-wjuQHGQVofmSJv1uVISKLE5zO2rNGzM/KCYZch/QQvez7C1hUhBIuZ701fYXExuufJFMPhv2SyL8CyoIfMLbIQ==
+
 unicode-property-aliases-ecmascript@^1.0.4:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.5.tgz#a9cc6cc7ce63a0a3023fc99e341b94431d405a57"
@@ -6349,6 +8077,11 @@
   dependencies:
     crypto-random-string "^1.0.0"
 
+universalify@^0.1.0:
+  version "0.1.2"
+  resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
+  integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
+
 unpipe@1.0.0, unpipe@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
@@ -6424,6 +8157,14 @@
   resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
   integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==
 
+useragent@2.3.0, useragent@^2.3.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/useragent/-/useragent-2.3.0.tgz#217f943ad540cb2128658ab23fc960f6a88c9972"
+  integrity sha512-4AoH4pxuSvHCjqLO04sU6U/uE65BYza8l/KKBS0b0hnUPWi+cQ2BpeTEwejCSx9SPV5/U03nniDTrWx5NrmKdw==
+  dependencies:
+    lru-cache "4.1.x"
+    tmp "0.0.x"
+
 util-deprecate@^1.0.1, util-deprecate@~1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
@@ -6455,6 +8196,11 @@
   resolved "https://registry.yarnpkg.com/vali-date/-/vali-date-1.0.0.tgz#1b904a59609fb328ef078138420934f6b86709a6"
   integrity sha1-G5BKWWCfsyjvB4E4Qgk09rhnCaY=
 
+valid-url@^1.0.9:
+  version "1.0.9"
+  resolved "https://registry.yarnpkg.com/valid-url/-/valid-url-1.0.9.tgz#1c14479b40f1397a75782f115e4086447433a200"
+  integrity sha1-HBRHm0DxOXp1eC8RXkCGRHQzogA=
+
 validate-npm-package-license@^3.0.1:
   version "3.0.4"
   resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
@@ -6468,7 +8214,7 @@
   resolved "https://registry.yarnpkg.com/vargs/-/vargs-0.1.0.tgz#6b6184da6520cc3204ce1b407cac26d92609ebff"
   integrity sha1-a2GE2mUgzDIEzhtAfKwm2SYJ6/8=
 
-vary@^1, vary@~1.1.2:
+vary@^1, vary@^1.1.2, vary@~1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
   integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=
@@ -6519,6 +8265,11 @@
   resolved "https://registry.yarnpkg.com/vlq/-/vlq-0.2.3.tgz#8f3e4328cf63b1540c0d67e1b2778386f8975b26"
   integrity sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow==
 
+void-elements@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec"
+  integrity sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=
+
 vscode-uri@=1.0.6:
   version "1.0.6"
   resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-1.0.6.tgz#6b8f141b0bbc44ad7b07e94f82f168ac7608ad4d"
@@ -6631,6 +8382,11 @@
   resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
   integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==
 
+whatwg-fetch@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz#fc804e458cc460009b1a2b966bc8817d2578aefb"
+  integrity sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q==
+
 whatwg-url@^6.4.0:
   version "6.5.0"
   resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.5.0.tgz#f2df02bff176fd65070df74ad5ccbb5a199965a8"
@@ -6640,12 +8396,21 @@
     tr46 "^1.0.1"
     webidl-conversions "^4.0.2"
 
+whatwg-url@^7.0.0:
+  version "7.1.0"
+  resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06"
+  integrity sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==
+  dependencies:
+    lodash.sortby "^4.7.0"
+    tr46 "^1.0.1"
+    webidl-conversions "^4.0.2"
+
 which-module@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
   integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
 
-which@1.3.1, which@^1.0.8, which@^1.2.14, which@^1.2.9, which@^1.3.1:
+which@1.3.1, which@^1.0.8, which@^1.2.1, which@^1.2.14, which@^1.2.9, which@^1.3.1:
   version "1.3.1"
   resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
   integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
@@ -6702,6 +8467,14 @@
     reduce-flatten "^1.0.1"
     typical "^2.6.1"
 
+wordwrapjs@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/wordwrapjs/-/wordwrapjs-4.0.0.tgz#9aa9394155993476e831ba8e59fb5795ebde6800"
+  integrity sha512-Svqw723a3R34KvsMgpjFBYCgNOSdcW3mQFK4wIfhGQhtaFVOJmdYoXgi63ne3dTlWgatVcUc7t4HtQ/+bUVIzQ==
+  dependencies:
+    reduce-flatten "^2.0.0"
+    typical "^5.0.0"
+
 wrap-ansi@^5.1.0:
   version "5.1.0"
   resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09"
@@ -6730,6 +8503,15 @@
   resolved "https://registry.yarnpkg.com/ws/-/ws-7.2.1.tgz#03ed52423cd744084b2cf42ed197c8b65a936b8e"
   integrity sha512-sucePNSafamSKoOqoNfBd8V0StlkzJKL2ZAhGQinCfNQ+oacw+Pk7lcdAElecBF2VkLNZRiIb5Oi1Q5lVUVt2A==
 
+ws@~3.3.1:
+  version "3.3.3"
+  resolved "https://registry.yarnpkg.com/ws/-/ws-3.3.3.tgz#f1cf84fe2d5e901ebce94efaece785f187a228f2"
+  integrity sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==
+  dependencies:
+    async-limiter "~1.0.0"
+    safe-buffer "~5.1.0"
+    ultron "~1.1.0"
+
 ws@~6.1.0:
   version "6.1.4"
   resolved "https://registry.yarnpkg.com/ws/-/ws-6.1.4.tgz#5b5c8800afab925e94ccb29d153c8d02c1776ef9"
@@ -6772,7 +8554,20 @@
   resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
   integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=
 
-yargs-parser@13.1.1, yargs-parser@^13.1.1:
+yallist@^3.0.2:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
+  integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
+
+yargs-parser@13.1.2, yargs-parser@^13.1.2:
+  version "13.1.2"
+  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38"
+  integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==
+  dependencies:
+    camelcase "^5.0.0"
+    decamelize "^1.2.0"
+
+yargs-parser@^13.1.1:
   version "13.1.1"
   resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.1.tgz#d26058532aa06d365fe091f6a1fc06b2f7e5eca0"
   integrity sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==
@@ -6789,7 +8584,23 @@
     lodash "^4.17.15"
     yargs "^13.3.0"
 
-yargs@13.3.0, yargs@^13.3.0:
+yargs@13.3.2:
+  version "13.3.2"
+  resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd"
+  integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==
+  dependencies:
+    cliui "^5.0.0"
+    find-up "^3.0.0"
+    get-caller-file "^2.0.1"
+    require-directory "^2.1.1"
+    require-main-filename "^2.0.0"
+    set-blocking "^2.0.0"
+    string-width "^3.0.0"
+    which-module "^2.0.0"
+    y18n "^4.0.0"
+    yargs-parser "^13.1.2"
+
+yargs@^13.3.0:
   version "13.3.0"
   resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.0.tgz#4c657a55e07e5f2cf947f8a366567c04a0dedc83"
   integrity sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==
@@ -6818,6 +8629,11 @@
   resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419"
   integrity sha1-AI4G2AlDIMNy28L47XagymyKxBk=
 
+ylru@^1.2.0:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/ylru/-/ylru-1.2.1.tgz#f576b63341547989c1de7ba288760923b27fe84f"
+  integrity sha512-faQrqNMzcPCHGVC2aaOINk13K+aaBDUPjGWl0teOXywElLjyVAB6Oe2jj62jHYtwsU49jXhScYbvPENK+6zAvQ==
+
 zip-stream@^2.1.2:
   version "2.1.3"
   resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-2.1.3.tgz#26cc4bdb93641a8590dd07112e1f77af1758865b"
diff --git a/tools/maven/gerrit-acceptance-framework_pom.xml b/tools/maven/gerrit-acceptance-framework_pom.xml
index 4240a9b..0f6f6d4 100644
--- a/tools/maven/gerrit-acceptance-framework_pom.xml
+++ b/tools/maven/gerrit-acceptance-framework_pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-acceptance-framework</artifactId>
-  <version>3.2.0-SNAPSHOT</version>
+  <version>3.3.0-SNAPSHOT</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Acceptance Test Framework</name>
   <description>Framework for Gerrit's acceptance tests</description>
diff --git a/tools/maven/gerrit-extension-api_pom.xml b/tools/maven/gerrit-extension-api_pom.xml
index cf2b080..cb0f882 100644
--- a/tools/maven/gerrit-extension-api_pom.xml
+++ b/tools/maven/gerrit-extension-api_pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-extension-api</artifactId>
-  <version>3.2.0-SNAPSHOT</version>
+  <version>3.3.0-SNAPSHOT</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Extension API</name>
   <description>API for Gerrit Extensions</description>
diff --git a/tools/maven/gerrit-plugin-api_pom.xml b/tools/maven/gerrit-plugin-api_pom.xml
index 7d3c4f0..e542b47 100644
--- a/tools/maven/gerrit-plugin-api_pom.xml
+++ b/tools/maven/gerrit-plugin-api_pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-plugin-api</artifactId>
-  <version>3.2.0-SNAPSHOT</version>
+  <version>3.3.0-SNAPSHOT</version>
   <packaging>jar</packaging>
   <name>Gerrit Code Review - Plugin API</name>
   <description>API for Gerrit Plugins</description>
diff --git a/tools/maven/gerrit-war_pom.xml b/tools/maven/gerrit-war_pom.xml
index 9478283..4a69f2e 100644
--- a/tools/maven/gerrit-war_pom.xml
+++ b/tools/maven/gerrit-war_pom.xml
@@ -2,7 +2,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-war</artifactId>
-  <version>3.2.0-SNAPSHOT</version>
+  <version>3.3.0-SNAPSHOT</version>
   <packaging>war</packaging>
   <name>Gerrit Code Review - WAR</name>
   <description>Gerrit WAR</description>
diff --git a/version.bzl b/version.bzl
index fb1e5ca..78b286b 100644
--- a/version.bzl
+++ b/version.bzl
@@ -2,4 +2,4 @@
 # Used by :api_install and :api_deploy targets
 # when talking to the destination repository.
 #
-GERRIT_VERSION = "3.2.0-SNAPSHOT"
+GERRIT_VERSION = "3.3.0-SNAPSHOT"