Merge "Standalone PolyGerrit plugins"
This commit is contained in:
		
							
								
								
									
										24
									
								
								Documentation/dev-plugins-pg.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								Documentation/dev-plugins-pg.txt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
			
		||||
= Gerrit Code Review - PolyGerrit Plugin Development
 | 
			
		||||
 | 
			
		||||
CAUTION: Work in progress. Hard hat area. +
 | 
			
		||||
This document will be populated with details along with implementation. +
 | 
			
		||||
link:https://groups.google.com/d/topic/repo-discuss/vb8WJ4m0hK0/discussion[Join the discussion.]
 | 
			
		||||
 | 
			
		||||
== Plugin loading and initialization
 | 
			
		||||
 | 
			
		||||
link:https://gerrit-review.googlesource.com/Documentation/js-api.html#_entry_point[Entry point] for the plugin and the loading method is based on link:http://w3c.github.io/webcomponents/spec/imports/[HTML Imports] spec.
 | 
			
		||||
 | 
			
		||||
* Plugin provides index.html, similar to link:https://gerrit-review.googlesource.com/Documentation/dev-plugins.html#deployment[.js Web UI plugins]
 | 
			
		||||
* index.html contains a `dom-module` tag with a script that uses `Gerrit.install()`.
 | 
			
		||||
* PolyGerrit imports index.html along with all required resources defined in it (fonts, styles, etc)
 | 
			
		||||
* For standalone plugins, the entry point file is a `pluginname.html` file located in `gerrit-site/plugins` folder, where pluginname is an alphanumeric plugin name.
 | 
			
		||||
 | 
			
		||||
Here's a sample `myplugin.html`:
 | 
			
		||||
 | 
			
		||||
``` html
 | 
			
		||||
<dom-module id="my-plugin">
 | 
			
		||||
  <script>
 | 
			
		||||
    Gerrit.install(function() { console.log('Ready.'); });
 | 
			
		||||
  </script>
 | 
			
		||||
</dom-module>
 | 
			
		||||
```
 | 
			
		||||
@@ -19,4 +19,5 @@ import java.util.List;
 | 
			
		||||
public class PluginConfigInfo {
 | 
			
		||||
  public Boolean hasAvatars;
 | 
			
		||||
  public List<String> jsResourcePaths;
 | 
			
		||||
  public List<String> htmlResourcePaths;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -28,6 +28,7 @@ import com.google.gwt.user.client.rpc.AsyncCallback;
 | 
			
		||||
import com.google.gwt.user.client.ui.DialogBox;
 | 
			
		||||
import com.google.gwtexpui.progress.client.ProgressBar;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.stream.Collectors;
 | 
			
		||||
 | 
			
		||||
/** Loads JavaScript plugins with a progress meter visible. */
 | 
			
		||||
public class PluginLoader extends DialogBox {
 | 
			
		||||
@@ -37,6 +38,13 @@ public class PluginLoader extends DialogBox {
 | 
			
		||||
      List<String> plugins, int loadTimeout, AsyncCallback<VoidResult> callback) {
 | 
			
		||||
    if (plugins == null || plugins.isEmpty()) {
 | 
			
		||||
      callback.onSuccess(VoidResult.create());
 | 
			
		||||
    }
 | 
			
		||||
    plugins = plugins
 | 
			
		||||
        .stream()
 | 
			
		||||
        .filter(p -> p.endsWith(".js"))
 | 
			
		||||
        .collect(Collectors.toList());
 | 
			
		||||
    if (plugins.isEmpty()) {
 | 
			
		||||
      callback.onSuccess(VoidResult.create());
 | 
			
		||||
    } else {
 | 
			
		||||
      self = new PluginLoader(loadTimeout, callback);
 | 
			
		||||
      self.load(plugins);
 | 
			
		||||
 
 | 
			
		||||
@@ -64,6 +64,7 @@ import java.util.ArrayList;
 | 
			
		||||
import java.util.Collections;
 | 
			
		||||
import java.util.HashMap;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Locale;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.Optional;
 | 
			
		||||
import java.util.concurrent.ConcurrentMap;
 | 
			
		||||
@@ -637,7 +638,11 @@ class HttpPluginServlet extends HttpServlet implements StartPluginListener, Relo
 | 
			
		||||
    Path path = plugin.getSrcFile();
 | 
			
		||||
    if (req.getRequestURI().endsWith(getJsPluginPath(plugin)) && Files.exists(path)) {
 | 
			
		||||
      res.setHeader("Content-Length", Long.toString(Files.size(path)));
 | 
			
		||||
      if (path.toString().toLowerCase(Locale.US).endsWith(".html")) {
 | 
			
		||||
        res.setContentType("text/html");
 | 
			
		||||
      } else {
 | 
			
		||||
        res.setContentType("application/javascript");
 | 
			
		||||
      }
 | 
			
		||||
      writeToResponse(res, Files.newInputStream(path));
 | 
			
		||||
    } else {
 | 
			
		||||
      resourceCache.put(key, Resource.NOT_FOUND);
 | 
			
		||||
 
 | 
			
		||||
@@ -309,9 +309,15 @@ public class GetServerInfo implements RestReadView<ConfigResource> {
 | 
			
		||||
    PluginConfigInfo info = new PluginConfigInfo();
 | 
			
		||||
    info.hasAvatars = toBoolean(avatar.get() != null);
 | 
			
		||||
    info.jsResourcePaths = new ArrayList<>();
 | 
			
		||||
    info.htmlResourcePaths = new ArrayList<>();
 | 
			
		||||
    for (WebUiPlugin u : plugins) {
 | 
			
		||||
      info.jsResourcePaths.add(
 | 
			
		||||
          String.format("plugins/%s/%s", u.getPluginName(), u.getJavaScriptResourcePath()));
 | 
			
		||||
      String path = String.format(
 | 
			
		||||
          "plugins/%s/%s", u.getPluginName(), u.getJavaScriptResourcePath());
 | 
			
		||||
      if (path.endsWith(".html")) {
 | 
			
		||||
        info.htmlResourcePaths.add(path);
 | 
			
		||||
      } else {
 | 
			
		||||
        info.jsResourcePaths.add(path);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return info;
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -404,7 +404,7 @@ public class PluginLoader implements LifecycleListener {
 | 
			
		||||
      String name = entry.getKey();
 | 
			
		||||
      Path path = entry.getValue();
 | 
			
		||||
      String fileName = path.getFileName().toString();
 | 
			
		||||
      if (!isJsPlugin(fileName) && !serverPluginFactory.handles(path)) {
 | 
			
		||||
      if (!isUiPlugin(fileName) && !serverPluginFactory.handles(path)) {
 | 
			
		||||
        log.warn("No Plugin provider was found that handles this file format: {}", fileName);
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
@@ -586,7 +586,7 @@ public class PluginLoader implements LifecycleListener {
 | 
			
		||||
  private Plugin loadPlugin(String name, Path srcPlugin, FileSnapshot snapshot)
 | 
			
		||||
      throws InvalidPluginException {
 | 
			
		||||
    String pluginName = srcPlugin.getFileName().toString();
 | 
			
		||||
    if (isJsPlugin(pluginName)) {
 | 
			
		||||
    if (isUiPlugin(pluginName)) {
 | 
			
		||||
      return loadJsPlugin(name, srcPlugin, snapshot);
 | 
			
		||||
    } else if (serverPluginFactory.handles(srcPlugin)) {
 | 
			
		||||
      return loadServerPlugin(srcPlugin, snapshot);
 | 
			
		||||
@@ -718,8 +718,8 @@ public class PluginLoader implements LifecycleListener {
 | 
			
		||||
 | 
			
		||||
  public String getGerritPluginName(Path srcPath) {
 | 
			
		||||
    String fileName = srcPath.getFileName().toString();
 | 
			
		||||
    if (isJsPlugin(fileName)) {
 | 
			
		||||
      return fileName.substring(0, fileName.length() - 3);
 | 
			
		||||
    if (isUiPlugin(fileName)) {
 | 
			
		||||
      return fileName.substring(0, fileName.lastIndexOf('.'));
 | 
			
		||||
    }
 | 
			
		||||
    if (serverPluginFactory.handles(srcPath)) {
 | 
			
		||||
      return serverPluginFactory.getPluginName(srcPath);
 | 
			
		||||
@@ -735,8 +735,8 @@ public class PluginLoader implements LifecycleListener {
 | 
			
		||||
    return map;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private static boolean isJsPlugin(String name) {
 | 
			
		||||
    return isPlugin(name, "js");
 | 
			
		||||
  private static boolean isUiPlugin(String name) {
 | 
			
		||||
    return isPlugin(name, "js") || isPlugin(name, "html");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private static boolean isPlugin(String fileName, String ext) {
 | 
			
		||||
 
 | 
			
		||||
@@ -74,6 +74,18 @@ limitations under the License.
 | 
			
		||||
      .webLink {
 | 
			
		||||
        display: block;
 | 
			
		||||
      }
 | 
			
		||||
      section.assignee {
 | 
			
		||||
        @apply(--change-metadata-assignee);
 | 
			
		||||
      }
 | 
			
		||||
      section.labelStatus {
 | 
			
		||||
        @apply(--change-metadata-label-status);
 | 
			
		||||
      }
 | 
			
		||||
      section.strategy {
 | 
			
		||||
        @apply(--change-metadata-strategy);
 | 
			
		||||
      }
 | 
			
		||||
      section.topic {
 | 
			
		||||
        @apply(--change-metadata-topic);
 | 
			
		||||
      }
 | 
			
		||||
      @media screen and (max-width: 50em), screen and (min-width: 75em) {
 | 
			
		||||
        :host {
 | 
			
		||||
          display: table;
 | 
			
		||||
 
 | 
			
		||||
@@ -17,6 +17,7 @@ limitations under the License.
 | 
			
		||||
<link rel="import" href="../bower_components/polymer/polymer.html">
 | 
			
		||||
<link rel="import" href="../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
 | 
			
		||||
<link rel="import" href="../styles/app-theme.html">
 | 
			
		||||
<link rel="import" href="./plugins/gr-plugin-host/gr-plugin-host.html">
 | 
			
		||||
 | 
			
		||||
<link rel="import" href="./admin/gr-admin-view/gr-admin-view.html">
 | 
			
		||||
 | 
			
		||||
@@ -170,6 +171,9 @@ limitations under the License.
 | 
			
		||||
    <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.plugin]]">
 | 
			
		||||
    </gr-plugin-host>
 | 
			
		||||
  </template>
 | 
			
		||||
  <script src="gr-app.js" crossorigin="anonymous"></script>
 | 
			
		||||
</dom-module>
 | 
			
		||||
 
 | 
			
		||||
@@ -62,7 +62,6 @@
 | 
			
		||||
 | 
			
		||||
    observers: [
 | 
			
		||||
      '_viewChanged(params.view)',
 | 
			
		||||
      '_loadPlugins(_serverConfig.plugin.js_resource_paths)',
 | 
			
		||||
    ],
 | 
			
		||||
 | 
			
		||||
    behaviors: [
 | 
			
		||||
@@ -128,17 +127,6 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    _loadPlugins: function(plugins) {
 | 
			
		||||
      Gerrit._setPluginsCount(plugins.length);
 | 
			
		||||
      for (var i = 0; i < plugins.length; i++) {
 | 
			
		||||
        var scriptEl = document.createElement('script');
 | 
			
		||||
        scriptEl.defer = true;
 | 
			
		||||
        scriptEl.src = '/' + plugins[i];
 | 
			
		||||
        scriptEl.onerror = Gerrit._pluginInstalled;
 | 
			
		||||
        document.body.appendChild(scriptEl);
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    _loginTapHandler: function(e) {
 | 
			
		||||
      e.preventDefault();
 | 
			
		||||
      page.show('/login/' + encodeURIComponent(
 | 
			
		||||
 
 | 
			
		||||
@@ -89,10 +89,16 @@ limitations under the License.
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('sets plugins count', function() {
 | 
			
		||||
      sandbox.stub(Gerrit, '_setPluginsCount');
 | 
			
		||||
      element._loadPlugins([]);
 | 
			
		||||
      assert.isTrue(Gerrit._setPluginsCount.calledWithExactly(0));
 | 
			
		||||
    test('passes config to gr-plugin-host', function(done) {
 | 
			
		||||
      var config = {plugin: {}};
 | 
			
		||||
      element.$.restAPI.getConfig.restore();
 | 
			
		||||
      sandbox.stub(
 | 
			
		||||
          element.$.restAPI, 'getConfig').returns(Promise.resolve(config));
 | 
			
		||||
      element.attached();
 | 
			
		||||
      flush(function() {
 | 
			
		||||
        assert.strictEqual(element.$.plugins.config, config.plugin);
 | 
			
		||||
        done();
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,22 @@
 | 
			
		||||
<!--
 | 
			
		||||
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.
 | 
			
		||||
-->
 | 
			
		||||
 | 
			
		||||
<link rel="import" href="../../../bower_components/polymer/polymer.html">
 | 
			
		||||
<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
 | 
			
		||||
 | 
			
		||||
<dom-module id="gr-plugin-host">
 | 
			
		||||
  <script src="gr-plugin-host.js"></script>
 | 
			
		||||
</dom-module>
 | 
			
		||||
@@ -0,0 +1,52 @@
 | 
			
		||||
// 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.
 | 
			
		||||
(function() {
 | 
			
		||||
  'use strict';
 | 
			
		||||
 | 
			
		||||
  Polymer({
 | 
			
		||||
    is: 'gr-plugin-host',
 | 
			
		||||
 | 
			
		||||
    properties: {
 | 
			
		||||
      config: {
 | 
			
		||||
        type: Object,
 | 
			
		||||
        observer: "_configChanged",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    _configChanged: function(config) {
 | 
			
		||||
      var jsPlugins = config.js_resource_paths || [];
 | 
			
		||||
      var htmlPlugins = config.html_resource_paths || [];
 | 
			
		||||
      Gerrit._setPluginsCount(jsPlugins.length + htmlPlugins.length);
 | 
			
		||||
      this._loadJsPlugins(jsPlugins);
 | 
			
		||||
      this._importHtmlPlugins(htmlPlugins);
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    _importHtmlPlugins: function(plugins) {
 | 
			
		||||
      plugins.forEach(function(url) {
 | 
			
		||||
        this.importHref('/' + url, null, Gerrit._pluginInstalled, true);
 | 
			
		||||
      }.bind(this));
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    _loadJsPlugins: function(plugins) {
 | 
			
		||||
      for (var i = 0; i < plugins.length; i++) {
 | 
			
		||||
        var url = plugins[i];
 | 
			
		||||
        var scriptEl = document.createElement('script');
 | 
			
		||||
        scriptEl.defer = true;
 | 
			
		||||
        scriptEl.src = '/' + plugins[i];
 | 
			
		||||
        scriptEl.onerror = Gerrit._pluginInstalled;
 | 
			
		||||
        document.body.appendChild(scriptEl);
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
})();
 | 
			
		||||
@@ -0,0 +1,67 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<!--
 | 
			
		||||
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">
 | 
			
		||||
<title>gr-plugin-host</title>
 | 
			
		||||
 | 
			
		||||
<script src="../../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
 | 
			
		||||
<script src="../../../bower_components/web-component-tester/browser.js"></script>
 | 
			
		||||
 | 
			
		||||
<link rel="import" href="gr-plugin-host.html">
 | 
			
		||||
 | 
			
		||||
<test-fixture id="basic">
 | 
			
		||||
  <template>
 | 
			
		||||
    <gr-plugin-host></gr-plugin-host>
 | 
			
		||||
  </template>
 | 
			
		||||
</test-fixture>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
  suite('gr-diff tests', function() {
 | 
			
		||||
    var element;
 | 
			
		||||
    var sandbox;
 | 
			
		||||
 | 
			
		||||
    setup(function() {
 | 
			
		||||
      element = fixture('basic');
 | 
			
		||||
      sandbox = sinon.sandbox.create();
 | 
			
		||||
      sandbox.stub(document.body, 'appendChild');
 | 
			
		||||
      sandbox.stub(element, 'importHref');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    teardown(function() {
 | 
			
		||||
      sandbox.restore();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('counts plugins', function() {
 | 
			
		||||
      sandbox.stub(Gerrit, '_setPluginsCount');
 | 
			
		||||
      element.config = {
 | 
			
		||||
        html_resource_paths: ['foo/bar', 'baz'],
 | 
			
		||||
        js_resource_paths: ['42'],
 | 
			
		||||
      };
 | 
			
		||||
      assert.isTrue(Gerrit._setPluginsCount.calledWith(3));
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('imports html plugins from config', function() {
 | 
			
		||||
      element.config = {
 | 
			
		||||
        html_resource_paths: ['foo/bar', 'baz'],
 | 
			
		||||
      };
 | 
			
		||||
      assert.isTrue(element.importHref.calledWith(
 | 
			
		||||
          '/foo/bar', null, Gerrit._pluginInstalled, true));
 | 
			
		||||
      assert.isTrue(element.importHref.calledWith(
 | 
			
		||||
          '/baz', null, Gerrit._pluginInstalled, true));
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
</script>
 | 
			
		||||
@@ -107,7 +107,8 @@
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // TODO(andybons): Polyfill currentScript for IE10/11 (edge supports it).
 | 
			
		||||
    var src = opt_src || (document.currentScript && document.currentScript.src);
 | 
			
		||||
    var src = opt_src || (document.currentScript &&
 | 
			
		||||
         document.currentScript.src || document.currentScript.baseURI);
 | 
			
		||||
    var plugin = new Plugin(src);
 | 
			
		||||
    try {
 | 
			
		||||
      callback(plugin);
 | 
			
		||||
 
 | 
			
		||||
@@ -37,9 +37,9 @@ limitations under the License.
 | 
			
		||||
    '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-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.html',
 | 
			
		||||
    'change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html',
 | 
			
		||||
    'change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html',
 | 
			
		||||
    'change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.html',
 | 
			
		||||
    'change/gr-download-dialog/gr-download-dialog_test.html',
 | 
			
		||||
    'change/gr-file-list/gr-file-list_test.html',
 | 
			
		||||
    'change/gr-message/gr-message_test.html',
 | 
			
		||||
@@ -53,8 +53,8 @@ limitations under the License.
 | 
			
		||||
    'core/gr-reporting/gr-reporting_test.html',
 | 
			
		||||
    'core/gr-search-bar/gr-search-bar_test.html',
 | 
			
		||||
    'diff/gr-diff-builder/gr-diff-builder_test.html',
 | 
			
		||||
    'diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html',
 | 
			
		||||
    'diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group_test.html',
 | 
			
		||||
    'diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html',
 | 
			
		||||
    'diff/gr-diff-comment/gr-diff-comment_test.html',
 | 
			
		||||
    'diff/gr-diff-cursor/gr-diff-cursor_test.html',
 | 
			
		||||
    'diff/gr-diff-highlight/gr-annotation_test.html',
 | 
			
		||||
@@ -71,6 +71,7 @@ limitations under the License.
 | 
			
		||||
    'diff/gr-syntax-layer/gr-syntax-layer_test.html',
 | 
			
		||||
    'diff/gr-syntax-lib-loader/gr-syntax-lib-loader_test.html',
 | 
			
		||||
    'gr-app_test.html',
 | 
			
		||||
    'plugins/gr-plugin-host/gr-plugin-host_test.html',
 | 
			
		||||
    'settings/gr-account-info/gr-account-info_test.html',
 | 
			
		||||
    'settings/gr-change-table-editor/gr-change-table-editor_test.html',
 | 
			
		||||
    'settings/gr-email-editor/gr-email-editor_test.html',
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user