Add gr-smart-search to handle rest API calls and navigation

This allows gr-search-bar to be more flexible, and can be used with a
separate router and rest API.

Change-Id: Ife602e0caab8cc52e4977b80931bea0b242820a1
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
index 4823ef93..7af5fd5 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
@@ -22,7 +22,7 @@
 <link rel="import" href="../../shared/gr-dropdown/gr-dropdown.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../gr-account-dropdown/gr-account-dropdown.html">
-<link rel="import" href="../gr-search-bar/gr-search-bar.html">
+<link rel="import" href="../gr-smart-search/gr-smart-search.html">
 
 <dom-module id="gr-main-header">
   <template>
@@ -86,7 +86,7 @@
       .rightItems gr-endpoint-decorator:not(:empty) {
         margin-left: 1em;
       }
-      gr-search-bar {
+      gr-smart-search {
         flex-grow: 1;
         margin-left: .5em;
         max-width: 500px;
@@ -129,7 +129,7 @@
           font-size: var(--font-size-large);
           font-family: var(--font-family-bold);
         }
-        gr-search-bar,
+        gr-smart-search,
         .browse,
         .rightItems .hideOnMobile,
         .links > li.hideOnMobile {
@@ -171,7 +171,7 @@
         </li>
       </ul>
       <div class="rightItems">
-        <gr-search-bar value="{{searchQuery}}" role="search"></gr-search-bar>
+        <gr-smart-search id="search" value="{{searchQuery}}"></gr-smart-search>
         <gr-endpoint-decorator
             class="hideOnMobile"
             name="header-browse-source"></gr-endpoint-decorator>
diff --git a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
index b97c056..8332706 100644
--- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
+++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
@@ -238,6 +238,15 @@
       },
 
       /**
+       * Navigate to a search query
+       * @param {string} query
+       * @param {number=} opt_offset
+       */
+      navigateToSearchQuery(query, opt_offset) {
+        return this._navigate(this.getUrlForSearchQuery(query, opt_offset));
+      },
+
+      /**
        * @param {!Object} change The change object.
        * @param {number=} opt_patchNum
        * @param {number|string=} opt_basePatchNum The string 'PARENT' can be
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html
index fdd730d..b7b2147 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.html
@@ -15,20 +15,15 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../behaviors/gr-anonymous-name-behavior/gr-anonymous-name-behavior.html">
 <link rel="import" href="../../../behaviors/gr-url-encoding-behavior.html">
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../../../styles/shared-styles.html">
 
 <dom-module id="gr-search-bar">
   <template>
     <style include="shared-styles">
-      :host {
-        display: inline-block;
-      }
       form {
         display: flex;
       }
@@ -55,7 +50,6 @@
           threshold="[[_threshold]]"
           tab-complete
           vertical-offset="30"></gr-autocomplete>
-      <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
     </form>
   </template>
   <script src="gr-search-bar.js"></script>
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 c410486..2d0a759 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
@@ -92,9 +92,6 @@
   const SEARCH_OPERATORS_WITH_NEGATIONS =
       SEARCH_OPERATORS.concat(SEARCH_OPERATORS.map(op => `-${op}`));
 
-  const SELF_EXPRESSION = 'self';
-  const ME_EXPRESSION = 'me';
-
   const MAX_AUTOCOMPLETE_RESULTS = 10;
 
   const TOKENIZE_REGEX = /(?:[^\s"]+|"[^"]*")+\s*/g;
@@ -102,8 +99,13 @@
   Polymer({
     is: 'gr-search-bar',
 
+    /**
+     * Fired when a search is committed
+     *
+     * @event handle-search
+     */
+
     behaviors: [
-      Gerrit.AnonymousNameBehavior,
       Gerrit.KeyboardShortcutBehavior,
       Gerrit.URLEncodingBehavior,
     ],
@@ -129,18 +131,29 @@
           return this._getSearchSuggestions.bind(this);
         },
       },
+      projectSuggestions: {
+        type: Function,
+        value() {
+          return () => Promise.resolve([]);
+        },
+      },
+      groupSuggestions: {
+        type: Function,
+        value() {
+          return () => Promise.resolve([]);
+        },
+      },
+      accountSuggestions: {
+        type: Function,
+        value() {
+          return () => Promise.resolve([]);
+        },
+      },
       _inputVal: String,
       _threshold: {
         type: Number,
         value: 1,
       },
-      _config: Object,
-    },
-
-    attached() {
-      this.$.restAPI.getConfig().then(cfg => {
-        this._config = cfg;
-      });
     },
 
     _valueChanged(value) {
@@ -170,87 +183,12 @@
         target.blur();
       }
       if (this._inputVal) {
-        page.show('/q/' + this.encodeURL(this._inputVal, false));
+        this.dispatchEvent(new CustomEvent('handle-search', {
+          detail: {inputVal: this._inputVal},
+        }));
       }
     },
 
-    _accountOrAnon(name) {
-      return this.getUserName(this._config, name, false);
-    },
-
-    /**
-     * Fetch from the API the predicted accounts.
-     * @param {string} predicate - The first part of the search term, e.g.
-     *     'owner'
-     * @param {string} expression - The second part of the search term, e.g.
-     *     'kasp'
-     * @return {!Promise} This returns a promise that resolves to an array of
-     *     strings.
-     */
-    _fetchAccounts(predicate, expression) {
-      if (expression.length === 0) { return Promise.resolve([]); }
-      return this.$.restAPI.getSuggestedAccounts(
-          expression,
-          MAX_AUTOCOMPLETE_RESULTS)
-          .then(accounts => {
-            if (!accounts) { return []; }
-            return accounts.map(acct => acct.email ?
-              `${predicate}:${acct.email}` :
-              `${predicate}:"${this._accountOrAnon(acct)}"`);
-          }).then(accounts => {
-            // When the expression supplied is a beginning substring of 'self',
-            // add it as an autocomplete option.
-            if (SELF_EXPRESSION.startsWith(expression)) {
-              return accounts.concat([predicate + ':' + SELF_EXPRESSION]);
-            } else if (ME_EXPRESSION.startsWith(expression)) {
-              return accounts.concat([predicate + ':' + ME_EXPRESSION]);
-            } else {
-              return accounts;
-            }
-          });
-    },
-
-    /**
-     * Fetch from the API the predicted groups.
-     * @param {string} predicate - The first part of the search term, e.g.
-     *     'ownerin'
-     * @param {string} expression - The second part of the search term, e.g.
-     *     'polyger'
-     * @return {!Promise} This returns a promise that resolves to an array of
-     *     strings.
-     */
-    _fetchGroups(predicate, expression) {
-      if (expression.length === 0) { return Promise.resolve([]); }
-      return this.$.restAPI.getSuggestedGroups(
-          expression,
-          MAX_AUTOCOMPLETE_RESULTS)
-          .then(groups => {
-            if (!groups) { return []; }
-            const keys = Object.keys(groups);
-            return keys.map(key => predicate + ':' + key);
-          });
-    },
-
-    /**
-     * Fetch from the API the predicted projects.
-     * @param {string} predicate - The first part of the search term, e.g.
-     *     'project'
-     * @param {string} expression - The second part of the search term, e.g.
-     *     'gerr'
-     * @return {!Promise} This returns a promise that resolves to an array of
-     *     strings.
-     */
-    _fetchProjects(predicate, expression) {
-      return this.$.restAPI.getSuggestedProjects(
-          expression,
-          MAX_AUTOCOMPLETE_RESULTS)
-          .then(projects => {
-            if (!projects) { return []; }
-            const keys = Object.keys(projects);
-            return keys.map(key => predicate + ':' + key);
-          });
-    },
-
     /**
      * Determine what array of possible suggestions should be provided
      *     to _getSearchSuggestions.
@@ -268,12 +206,12 @@
         case 'ownerin':
         case 'reviewerin':
           // Fetch groups.
-          return this._fetchGroups(predicate, expression);
+          return this.groupSuggestions(predicate, expression);
 
         case 'parentproject':
         case 'project':
           // Fetch projects.
-          return this._fetchProjects(predicate, expression);
+          return this.projectSuggestions(predicate, expression);
 
         case 'author':
         case 'cc':
@@ -284,7 +222,7 @@
         case 'reviewedby':
         case 'reviewer':
           // Fetch accounts.
-          return this._fetchAccounts(predicate, expression);
+          return this.accountSuggestions(predicate, expression);
 
         default:
           return Promise.resolve(SEARCH_OPERATORS_WITH_NEGATIONS
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 60e8a7e..60d6237 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
@@ -60,9 +60,8 @@
           document.activeElement;
     };
 
-    test('enter in search input triggers nav', done => {
-      sandbox.stub(page, 'show', () => {
-        page.show.restore();
+    test('enter in search input fires event', done => {
+      element.addEventListener('handle-search', () => {
         assert.notEqual(getActiveElement(), element.$.searchInput);
         assert.notEqual(getActiveElement(), element.$.searchButton);
         done();
@@ -72,16 +71,7 @@
           null, 'enter');
     });
 
-    test('search query should be double-escaped', () => {
-      const showStub = sandbox.stub(page, 'show');
-      element.$.searchInput.text = 'fate/stay';
-      MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13,
-          null, 'enter');
-      assert.equal(showStub.lastCall.args[0], '/q/fate%252Fstay');
-    });
-
     test('input blurred after commit', () => {
-      sandbox.stub(page, 'show');
       const blurSpy = sandbox.spy(element.$.searchInput.$.input, 'blur');
       element.$.searchInput.text = 'fate/stay';
       MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13,
@@ -90,11 +80,12 @@
     });
 
     test('empty search query does not trigger nav', () => {
-      const showSpy = sandbox.spy(page, 'show');
+      const searchSpy = sandbox.spy();
+      element.addEventListener('handle-search', searchSpy);
       element.value = '';
       MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13,
           null, 'enter');
-      assert.isFalse(showSpy.called);
+      assert.isFalse(searchSpy.called);
     });
 
     test('keyboard shortcuts', () => {
@@ -107,64 +98,20 @@
 
     suite('_getSearchSuggestions', () => {
       test('Autocompletes accounts', () => {
-        sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () =>
-          Promise.resolve([
-            {
-              name: 'fred',
-              email: 'fred@goog.co',
-            },
-          ])
+        sandbox.stub(element, 'accountSuggestions', () =>
+          Promise.resolve(['owner:fred@goog.co'])
         );
         return element._getSearchSuggestions('owner:fr').then(s => {
           assert.equal(s[0].value, 'owner:fred@goog.co');
         });
       });
 
-      test('Inserts self as option when valid', done => {
-        sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () =>
-          Promise.resolve([
-            {
-              name: 'fred',
-              email: 'fred@goog.co',
-            },
-          ])
-        );
-        element._getSearchSuggestions('owner:s').then(s => {
-          assert.equal(s[0].value, 'owner:self');
-        }).then(() => {
-          element._getSearchSuggestions('owner:selfs').then(s => {
-            assert.notEqual(s[0].value, 'owner:self');
-            done();
-          });
-        });
-      });
-
-      test('Inserts me as option when valid', done => {
-        sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () =>
-          Promise.resolve([
-            {
-              name: 'fred',
-              email: 'fred@goog.co',
-            },
-          ])
-        );
-        element._getSearchSuggestions('owner:m').then(s => {
-          assert.equal(s[0].value, 'owner:me');
-        }).then(() => {
-          element._getSearchSuggestions('owner:meme').then(s => {
-            assert.notEqual(s[0].value, 'owner:me');
-            done();
-          });
-        });
-      });
-
       test('Autocompletes groups', done => {
-        sandbox.stub(element.$.restAPI, 'getSuggestedGroups', () =>
-          Promise.resolve({
-            Polygerrit: 0,
-            gerrit: 0,
-            gerrittest: 0,
-          })
+        sandbox.stub(element, 'groupSuggestions', () =>
+          Promise.resolve([
+            'ownerin:Polygerrit',
+            'ownerin:gerrit',
+          ])
         );
         element._getSearchSuggestions('ownerin:pol').then(s => {
           assert.equal(s[0].value, 'ownerin:Polygerrit');
@@ -173,10 +120,12 @@
       });
 
       test('Autocompletes projects', done => {
-        sandbox.stub(element.$.restAPI, 'getSuggestedProjects', () =>
-          Promise.resolve({
-            Polygerrit: 0,
-          })
+        sandbox.stub(element, 'projectSuggestions', () =>
+          Promise.resolve([
+            'project:Polygerrit',
+            'project:gerrit',
+            'project:gerrittest',
+          ])
         );
         element._getSearchSuggestions('project:pol').then(s => {
           assert.equal(s[0].value, 'project:Polygerrit');
@@ -200,67 +149,6 @@
           done();
         });
       });
-
-      test('Autocomplete doesnt override exact matches to input', done => {
-        sandbox.stub(element.$.restAPI, 'getSuggestedGroups', () =>
-          Promise.resolve({
-            Polygerrit: 0,
-            gerrit: 0,
-            gerrittest: 0,
-          })
-        );
-        element._getSearchSuggestions('ownerin:gerrit').then(s => {
-          assert.equal(s[0].value, 'ownerin:gerrit');
-          done();
-        });
-      });
-
-      test('Autocomplete respects spaces', done => {
-        sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () =>
-          Promise.resolve([
-            {
-              name: 'fred',
-              email: 'fred@goog.co',
-            },
-          ])
-        );
-        element._getSearchSuggestions('is:ope').then(s => {
-          assert.equal(s[0].name, 'is:open');
-          assert.equal(s[0].value, 'is:open');
-          element._getSearchSuggestions('is:ope ').then(s => {
-            assert.equal(s.length, 0);
-            done();
-          });
-        });
-      });
-
-      test('Autocompletes accounts with no email', done => {
-        sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () =>
-          Promise.resolve([
-            {
-              name: 'fred',
-            },
-          ])
-        );
-        element._getSearchSuggestions('owner:fr').then(s => {
-          assert.equal(s[0].value, 'owner:"fred"');
-          done();
-        });
-      });
-
-      test('Autocompletes accounts with email', done => {
-        sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () =>
-          Promise.resolve([
-            {
-              email: 'fred@goog.co',
-            },
-          ])
-        );
-        element._getSearchSuggestions('owner:fr').then(s => {
-          assert.equal(s[0].value, 'owner:fred@goog.co');
-          done();
-        });
-      });
     });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.html b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.html
new file mode 100644
index 0000000..4c98068
--- /dev/null
+++ b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.html
@@ -0,0 +1,38 @@
+<!--
+@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.
+-->
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+
+<link rel="import" href="../../../behaviors/gr-anonymous-name-behavior/gr-anonymous-name-behavior.html">
+<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
+<link rel="import" href="../gr-search-bar/gr-search-bar.html">
+
+<dom-module id="gr-smart-search">
+  <template>
+    <style include="shared-styles">
+
+    </style>
+    <gr-search-bar id="search"
+        value="{{searchQuery}}"
+        on-handle-search="_handleSearch"
+        project-suggestions="[[_projectSuggestions]]"
+        group-suggestions="[[_groupSuggestions]]"
+        account-suggestions="[[_accountSuggestions]]"></gr-search-bar>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+  </template>
+  <script src="gr-smart-search.js"></script>
+</dom-module>
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
new file mode 100644
index 0000000..5dcd2bb
--- /dev/null
+++ b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.js
@@ -0,0 +1,144 @@
+/**
+ * @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.
+ */
+(function() {
+  'use strict';
+
+  const MAX_AUTOCOMPLETE_RESULTS = 10;
+  const SELF_EXPRESSION = 'self';
+  const ME_EXPRESSION = 'me';
+
+  Polymer({
+    is: 'gr-smart-search',
+
+    properties: {
+      searchQuery: String,
+      _config: Object,
+      _projectSuggestions: {
+        type: Function,
+        value() {
+          return this._fetchProjects.bind(this);
+        },
+      },
+      _groupSuggestions: {
+        type: Function,
+        value() {
+          return this._fetchGroups.bind(this);
+        },
+      },
+      _accountSuggestions: {
+        type: Function,
+        value() {
+          return this._fetchAccounts.bind(this);
+        },
+      },
+    },
+
+    behaviors: [
+      Gerrit.AnonymousNameBehavior,
+    ],
+
+    attached() {
+      this.$.restAPI.getConfig().then(cfg => {
+        this._config = cfg;
+      });
+    },
+
+    _handleSearch(e) {
+      const input = e.detail.inputVal;
+      if (input) {
+        Gerrit.Nav.navigateToSearchQuery(input);
+      }
+    },
+
+    _accountOrAnon(name) {
+      return this.getUserName(this._serverConfig, name, false);
+    },
+
+    /**
+     * Fetch from the API the predicted projects.
+     * @param {string} predicate - The first part of the search term, e.g.
+     *     'project'
+     * @param {string} expression - The second part of the search term, e.g.
+     *     'gerr'
+     * @return {!Promise} This returns a promise that resolves to an array of
+     *     strings.
+     */
+    _fetchProjects(predicate, expression) {
+      return this.$.restAPI.getSuggestedProjects(
+          expression,
+          MAX_AUTOCOMPLETE_RESULTS)
+          .then(projects => {
+            if (!projects) { return []; }
+            const keys = Object.keys(projects);
+            return keys.map(key => predicate + ':' + key);
+          });
+    },
+
+    /**
+     * Fetch from the API the predicted groups.
+     * @param {string} predicate - The first part of the search term, e.g.
+     *     'ownerin'
+     * @param {string} expression - The second part of the search term, e.g.
+     *     'polyger'
+     * @return {!Promise} This returns a promise that resolves to an array of
+     *     strings.
+     */
+    _fetchGroups(predicate, expression) {
+      if (expression.length === 0) { return Promise.resolve([]); }
+      return this.$.restAPI.getSuggestedGroups(
+          expression,
+          MAX_AUTOCOMPLETE_RESULTS)
+          .then(groups => {
+            if (!groups) { return []; }
+            const keys = Object.keys(groups);
+            return keys.map(key => predicate + ':' + key);
+          });
+    },
+
+    /**
+     * Fetch from the API the predicted accounts.
+     * @param {string} predicate - The first part of the search term, e.g.
+     *     'owner'
+     * @param {string} expression - The second part of the search term, e.g.
+     *     'kasp'
+     * @return {!Promise} This returns a promise that resolves to an array of
+     *     strings.
+     */
+    _fetchAccounts(predicate, expression) {
+      if (expression.length === 0) { return Promise.resolve([]); }
+      return this.$.restAPI.getSuggestedAccounts(
+          expression,
+          MAX_AUTOCOMPLETE_RESULTS)
+          .then(accounts => {
+            if (!accounts) { return []; }
+            return accounts.map(acct => acct.email ?
+              `${predicate}:${acct.email}` :
+              `${predicate}:"${this._accountOrAnon(acct)}"`);
+          }).then(accounts => {
+            // When the expression supplied is a beginning substring of 'self',
+            // add it as an autocomplete option.
+            if (SELF_EXPRESSION.startsWith(expression)) {
+              return accounts.concat([predicate + ':' + SELF_EXPRESSION]);
+            } else if (ME_EXPRESSION.startsWith(expression)) {
+              return accounts.concat([predicate + ':' + ME_EXPRESSION]);
+            } else {
+              return accounts;
+            }
+          });
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.html b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.html
new file mode 100644
index 0000000..66dc0f0
--- /dev/null
+++ b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.html
@@ -0,0 +1,174 @@
+<!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">
+<title>gr-smart-search</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+<link rel="import" href="gr-smart-search.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+  <template>
+    <gr-smart-search></gr-smart-search>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-smart-search tests', () => {
+    let element;
+    let sandbox;
+
+    setup(() => {
+      sandbox = sinon.sandbox.create();
+      element = fixture('basic');
+    });
+
+    teardown(() => {
+      sandbox.restore();
+    });
+
+
+    test('Autocompletes accounts', () => {
+      sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () =>
+        Promise.resolve([
+          {
+            name: 'fred',
+            email: 'fred@goog.co',
+          },
+        ])
+      );
+      return element._fetchAccounts('owner', 'fr').then(s => {
+        assert.equal(s[0], 'owner:fred@goog.co');
+      });
+    });
+
+    test('Inserts self as option when valid', done => {
+      sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () =>
+        Promise.resolve([
+          {
+            name: 'fred',
+            email: 'fred@goog.co',
+          },
+        ])
+      );
+      element._fetchAccounts('owner', 's').then(s => {
+        assert.equal(s[0], 'owner:fred@goog.co');
+        assert.equal(s[1], 'owner:self');
+      }).then(() => {
+        element._fetchAccounts('owner', 'selfs').then(s => {
+          assert.notEqual(s[0], 'owner:self');
+          done();
+        });
+      });
+    });
+
+    test('Inserts me as option when valid', done => {
+      sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () =>
+        Promise.resolve([
+          {
+            name: 'fred',
+            email: 'fred@goog.co',
+          },
+        ])
+      );
+      element._fetchAccounts('owner', 'm').then(s => {
+        assert.equal(s[0], 'owner:fred@goog.co');
+        assert.equal(s[1], 'owner:me');
+      }).then(() => {
+        element._fetchAccounts('owner', 'meme').then(s => {
+          assert.notEqual(s[0], 'owner:me');
+          done();
+        });
+      });
+    });
+
+    test('Autocompletes groups', done => {
+      sandbox.stub(element.$.restAPI, 'getSuggestedGroups', () =>
+        Promise.resolve({
+          Polygerrit: 0,
+          gerrit: 0,
+          gerrittest: 0,
+        })
+      );
+      element._fetchGroups('ownerin', 'pol').then(s => {
+        assert.equal(s[0], 'ownerin:Polygerrit');
+        done();
+      });
+    });
+
+    test('Autocompletes projects', done => {
+      sandbox.stub(element.$.restAPI, 'getSuggestedProjects', () =>
+        Promise.resolve({
+          Polygerrit: 0,
+        })
+      );
+      element._fetchProjects('project', 'pol').then(s => {
+        assert.equal(s[0], 'project:Polygerrit');
+        done();
+      });
+    });
+
+    test('Autocomplete doesnt override exact matches to input', done => {
+      sandbox.stub(element.$.restAPI, 'getSuggestedGroups', () =>
+        Promise.resolve({
+          Polygerrit: 0,
+          gerrit: 0,
+          gerrittest: 0,
+        })
+      );
+      element._fetchGroups('ownerin', 'gerrit').then(s => {
+        assert.equal(s[0], 'ownerin:Polygerrit');
+        assert.equal(s[1], 'ownerin:gerrit');
+        assert.equal(s[2], 'ownerin:gerrittest');
+        done();
+      });
+    });
+
+    test('Autocompletes accounts with no email', done => {
+      sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () =>
+        Promise.resolve([
+          {
+            name: 'fred',
+          },
+        ])
+      );
+      element._fetchAccounts('owner', 'fr').then(s => {
+        assert.equal(s[0], 'owner:"fred"');
+        done();
+      });
+    });
+
+    test('Autocompletes accounts with email', done => {
+      sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () =>
+        Promise.resolve([
+          {
+            email: 'fred@goog.co',
+          },
+        ])
+      );
+      element._fetchAccounts('owner', 'fr').then(s => {
+        assert.equal(s[0], 'owner:fred@goog.co');
+        done();
+      });
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index dbc7dc9..c29e3b5 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -91,6 +91,7 @@
     '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',
     'diff/gr-comment-api/gr-comment-api_test.html',
     'diff/gr-diff-builder/gr-diff-builder_test.html',
     'diff/gr-diff-comment-thread-group/gr-diff-comment-thread-group_test.html',