gr-request/gr-ajax cleanup (gr-reviewer-list)

Bug: Issue 3988
Change-Id: I432557954fa8d745ce1ac766e15082650fd13100
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html
index 6e081ea..3fee424 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.html
@@ -18,9 +18,8 @@
 <link rel="import" href="../../../bower_components/iron-input/iron-input.html">
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior.html">
 <link rel="import" href="../../shared/gr-account-chip/gr-account-chip.html">
-<link rel="import" href="../../shared/gr-ajax/gr-ajax.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-request/gr-request.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 
 <dom-module id="gr-reviewer-list">
   <template>
@@ -80,10 +79,6 @@
         }
       }
     </style>
-    <gr-ajax id="autocompleteXHR"
-        url="[[_computeAutocompleteURL(change)]]"
-        params="[[_computeAutocompleteParams(_inputVal)]]"
-        on-response="_handleResponse"></gr-ajax>
     <template is="dom-repeat" items="[[_reviewers]]" as="reviewer">
       <gr-account-chip class="reviewer" account="[[reviewer]]"
           on-remove="_handleRemove"
@@ -119,6 +114,7 @@
       <gr-button link id="addReviewer" class="addReviewer" on-tap="_handleAddTap"
           hidden$="[[_showInput]]">Add reviewer</gr-button>
     </div>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
   <script src="gr-reviewer-list.js"></script>
 </dom-module>
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 275ab6f..09ce7d7 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
@@ -121,35 +121,10 @@
       return false;
     },
 
-    _computeAutocompleteURL: function(change) {
-      return '/changes/' + change._number + '/suggest_reviewers';
-    },
-
-    _computeAutocompleteParams: function(inputVal) {
-      return {
-        n: 10,  // Return max 10 results
-        q: inputVal,
-      };
-    },
-
     _computeSelected: function(index, selectedIndex) {
       return index == selectedIndex;
     },
 
-    _handleResponse: function(e) {
-      this._autocompleteData = e.detail.response.filter(function(reviewer) {
-        var account = reviewer.account;
-        if (!account) { return true; }
-        for (var i = 0; i < this._reviewers.length; i++) {
-          if (account._account_id == this.change.owner._account_id ||
-              account._account_id == this._reviewers[i]._account_id) {
-            return false;
-          }
-        }
-        return true;
-      }, this);
-    },
-
     _handleBodyClick: function(e) {
       var eventPath = Polymer.dom(e).path;
       for (var i = 0; i < eventPath.length; i++) {
@@ -165,7 +140,15 @@
       e.preventDefault();
       var target = Polymer.dom(e).rootTarget;
       var accountID = parseInt(target.getAttribute('data-account-id'), 10);
-      this._send('DELETE', this._restEndpoint(accountID)).then(function(req) {
+      this.disabled = true;
+      this._xhrPromise =
+          this._removeReviewer(accountID).then(function(response) {
+        this.disabled = false;
+        if (!response.ok) {
+          return response.text().then(function(text) {
+            alert(text);
+          });
+        }
         var reviewers = this.change.reviewers;
         ['REVIEWER', 'CC'].forEach(function(type) {
           reviewers[type] = reviewers[type] || [];
@@ -176,10 +159,6 @@
             }
           }
         }, this);
-      }.bind(this)).catch(function(err) {
-        alert('Oops. Something went wrong. Check the console and bug the ' +
-            'PolyGerrit team for assistance.');
-        throw err;
       }.bind(this));
     },
 
@@ -241,7 +220,8 @@
           return;
         }
         this._lastAutocompleteRequest =
-            this.$.autocompleteXHR.generateRequest();
+            this._getSuggestedReviewers(this.change._number, val).then(
+                this._handleReviewersResponse.bind(this));
       }.bind(this);
 
       this._clearInputRequestHandle();
@@ -253,6 +233,24 @@
       }
     },
 
+    _handleReviewersResponse: function(response) {
+      this._autocompleteData = response.filter(function(reviewer) {
+        var account = reviewer.account;
+        if (!account) { return true; }
+        for (var i = 0; i < this._reviewers.length; i++) {
+          if (account._account_id == this.change.owner._account_id ||
+              account._account_id == this._reviewers[i]._account_id) {
+            return false;
+          }
+        }
+        return true;
+      }, this);
+    },
+
+    _getSuggestedReviewers: function(changeNum, inputVal) {
+      return this.$.restAPI.getChangeSuggestedReviewers(changeNum, inputVal);
+    },
+
     _handleKey: function(e) {
       if (this._hideAutocomplete) {
         if (e.keyCode == 27) {  // 'esc'
@@ -302,43 +300,32 @@
         reviewerID = reviewer.group.id;
       }
       this._autocompleteData = [];
-      this._send('POST', this._restEndpoint(), reviewerID).then(function(req) {
-        this.change.reviewers.CC = this.change.reviewers.CC || [];
-        req.response.reviewers.forEach(function(r) {
-          this.push('change.removable_reviewers', r);
-          this.push('change.reviewers.CC', r);
-        }, this);
-        this._inputVal = '';
-        this.$.input.focus();
-      }.bind(this)).catch(function(err) {
-        // TODO(andybons): Use the message returned by the server.
-        alert('Unable to add ' + reviewerID + ' as a reviewer.');
-        throw err;
+      this.disabled = true;
+      this._xhrPromise = this._addReviewer(reviewerID).then(function(response) {
+        this.change.reviewers['CC'] = this.change.reviewers['CC'] || [];
+        this.disabled = false;
+        if (!response.ok) {
+          return response.text().then(function(text) {
+            alert(text);
+          });
+        }
+        return this.$.restAPI.getResponseObject(response).then(function(obj) {
+          obj.reviewers.forEach(function(r) {
+            this.push('change.removable_reviewers', r);
+            this.push('change.reviewers.CC', r);
+          }, this);
+          this._inputVal = '';
+          this.$.input.focus();
+        }.bind(this));
       }.bind(this));
     },
 
-    _send: function(method, url, reviewerID) {
-      this.disabled = true;
-      var request = document.createElement('gr-request');
-      var opts = {
-        method: method,
-        url: url,
-      };
-      if (reviewerID) {
-        opts.body = {reviewer: reviewerID};
-      }
-      this._xhrPromise = request.send(opts);
-      var enableEl = function() { this.disabled = false; }.bind(this);
-      this._xhrPromise.then(enableEl).catch(enableEl);
-      return this._xhrPromise;
+    _addReviewer: function(id) {
+      return this.$.restAPI.addChangeReviewer(this.change._number, id);
     },
 
-    _restEndpoint: function(id) {
-      var path = '/changes/' + this.change._number + '/reviewers';
-      if (id) {
-        path += '/' + id;
-      }
-      return path;
+    _removeReviewer: function(id) {
+      return this.$.restAPI.removeChangeReviewer(this.change._number, id);
     },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
index eccc72e..9311e91 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
@@ -34,20 +34,12 @@
 <script>
   suite('gr-reviewer-list tests', function() {
     var element;
-    var server;
 
     setup(function() {
       element = fixture('basic');
-
-      server = sinon.fakeServer.create();
-      server.respondWith(
-        'GET',
-        /\/changes\/42\/suggest_reviewers\?n=10&q=andy(.*)/,
-        [
-          200,
-          {'Content-Type': 'application/json'},
-          ')]}\'\n' +
-          JSON.stringify([
+      stub('gr-rest-api-interface', {
+        getChangeSuggestedReviewers: function() {
+          return Promise.resolve([
             {
               account: {
                 _account_id: 1021482,
@@ -68,41 +60,32 @@
                 name: 'andy',
               }
             }
-          ]),
-        ]
-      );
-      server.respondWith(
-        'POST',
-        '/changes/42/reviewers',
-        [
-          200,
-          {'Content-Type': 'application/json'},
-          ')]}\'\n' +
-          JSON.stringify({
-            reviewers: [{
-              _account_id: 1021482,
-              approvals: {
-                'Code-Review': ' 0'
-              },
-              email: 'andybons@chromium.org',
-              name: 'Andrew Bonventre',
-            }]
-          }),
-        ]
-      );
-      server.respondWith(
-        'DELETE',
-        '/changes/42/reviewers/1021482',
-        [
-          204,
-          {'Content-Type': 'application/json'},
-          ')]}\'\n{}',
-        ]
-      );
-    });
-
-    teardown(function() {
-      server.restore();
+          ]);
+        },
+        addChangeReviewer: function() {
+          return Promise.resolve({
+            ok: true,
+            text: function() {
+              return Promise.resolve(
+                ')]}\'\n' +
+                JSON.stringify({
+                  reviewers: [{
+                    _account_id: 1021482,
+                    approvals: {
+                      'Code-Review': ' 0'
+                    },
+                    email: 'andybons@chromium.org',
+                    name: 'Andrew Bonventre',
+                  }]
+                })
+              );
+            },
+          });
+        },
+        removeChangeReviewer: function() {
+          return Promise.resolve({ok: true});
+        },
+      });
     });
 
     test('controls hidden on immutable element', function() {
@@ -176,7 +159,7 @@
       };
       flushAsynchronousOperations();
       var chips =
-        Polymer.dom(element.root).querySelectorAll('gr-account-chip');
+          Polymer.dom(element.root).querySelectorAll('gr-account-chip');
       assert.equal(chips.length, 3);
       Array.from(chips).forEach(function(el) {
         var accountID = parseInt(el.getAttribute('data-account-id'), 10);
@@ -195,17 +178,15 @@
     test('autocomplete starts at >= 3 chars', function() {
       element._inputRequestTimeout = 0;
       element._mutable = true;
-      var genRequestStub = sinon.stub(
-        element.$.autocompleteXHR,
-        'generateRequest',
+      var requestStub = sinon.stub(element, '_getSuggestedReviewers',
         function() {
-          assert(false, 'generateRequest should not be called for input ' +
-              'lengths of less than 3 chars');
+          assert(false, '_getSuggestedReviewers should not be called for ' +
+              'input lengths of less than 3 chars');
         }
       );
       element._inputVal = 'fo';
       flushAsynchronousOperations();
-      genRequestStub.restore();
+      requestStub.restore();
     });
 
     test('add/remove reviewer flow', function(done) {
@@ -220,9 +201,8 @@
       MockInteractions.tap(element.$$('.addReviewer'));
       flushAsynchronousOperations();
       element._inputVal = 'andy';
-      server.respond();
 
-      element._lastAutocompleteRequest.completes.then(function() {
+      element._lastAutocompleteRequest.then(function() {
         flushAsynchronousOperations();
         assert.isFalse(element.$$('.dropdown').hasAttribute('hidden'));
         var itemEls = Polymer.dom(element.root).querySelectorAll('.reviewer');
@@ -242,8 +222,7 @@
         assert.isTrue(element.$$('.dropdown').hasAttribute('hidden'));
 
         element._inputVal = 'andyb';
-        server.respond();
-        element._lastAutocompleteRequest.completes.then(function() {
+        element._lastAutocompleteRequest.then(function() {
           assert.isFalse(element.$$('.dropdown').hasAttribute('hidden'));
           var itemEls = Polymer.dom(element.root).querySelectorAll('.reviewer');
           assert.equal(itemEls.length, 3);
@@ -251,7 +230,6 @@
           assert.isFalse(itemEls[1].hasAttribute('selected'));
           MockInteractions.pressAndReleaseKeyOn(element, 13); // 'enter'
           assert.isTrue(element.disabled);
-          server.respond();
 
           element._xhrPromise.then(function() {
             assert.isFalse(element.disabled);
@@ -262,13 +240,12 @@
             MockInteractions.tap(element.$$('.reviewer').$$('gr-button'));
             flushAsynchronousOperations();
             assert.isTrue(element.disabled);
-            server.respond();
 
             element._xhrPromise.then(function() {
               flushAsynchronousOperations();
               assert.isFalse(element.disabled);
               var reviewerEls =
-                Polymer.dom(element.root).querySelectorAll('.reviewer');
+                  Polymer.dom(element.root).querySelectorAll('.reviewer');
               assert.equal(reviewerEls.length, 0);
               done();
             });
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 54ce15c..0765ec8 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
@@ -311,6 +311,40 @@
               });
     },
 
+    getChangeSuggestedReviewers: function(changeNum, inputVal, opt_errFn,
+        opt_ctx) {
+      var url = this.getChangeActionURL(changeNum, null, '/suggest_reviewers');
+      return this.fetchJSON(url, opt_errFn, opt_ctx, {
+        n: 10,  // Return max 10 results
+        q: inputVal,
+      });
+    },
+
+    addChangeReviewer: function(changeNum, reviewerID) {
+      return this._sendChangeReviewerRequest('POST', changeNum, reviewerID);
+    },
+
+    removeChangeReviewer: function(changeNum, reviewerID) {
+      return this._sendChangeReviewerRequest('DELETE', changeNum, reviewerID);
+    },
+
+    _sendChangeReviewerRequest: function(method, changeNum, reviewerID) {
+      var url = this.getChangeActionURL(changeNum, null, '/reviewers');
+      var body;
+      switch(method) {
+        case 'POST':
+          body = {reviewer: reviewerID};
+          break;
+        case 'DELETE':
+          url += '/' + reviewerID;
+          break;
+        default:
+          throw Error('Unsupported HTTP method: ' + method);
+      }
+
+      return this.send(method, url, body);
+    },
+
     getReviewedFiles: function(changeNum, patchNum) {
       return this.fetchJSON(
           this.getChangeActionURL(changeNum, patchNum, '/files?reviewed'));