Add async-foreach-behavior

Adds a utility behavior to enable looping over a list with promises.
This generalizes the asynchronous loop in the file-list's multi-diff
render process so that it can be used by other components.

Change-Id: I4c4cc729aed383ffb52b29b9f1f6c9ba0c83c77d
diff --git a/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior.html b/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior.html
new file mode 100644
index 0000000..0148377
--- /dev/null
+++ b/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior.html
@@ -0,0 +1,37 @@
+<!--
+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.
+-->
+
+<script>
+(function(window) {
+  'use strict';
+
+  window.Gerrit = window.Gerrit || {};
+
+  /** @polymerBehavior Gerrit.AsyncForeachBehavior */
+  Gerrit.AsyncForeachBehavior = {
+    /**
+     * @template T
+     * @param {!Array<T>} array
+     * @param {!Function} fn
+     * @return {!Promise<undefined>}
+     */
+    asyncForeach(array, fn) {
+      if (!array.length) { return Promise.resolve(); }
+      return fn(array[0]).then(() => this.asyncForeach(array.slice(1), fn));
+    },
+  };
+})(window);
+</script>
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
new file mode 100644
index 0000000..ba15ad7
--- /dev/null
+++ b/polygerrit-ui/app/behaviors/async-foreach-behavior/async-foreach-behavior_test.html
@@ -0,0 +1,39 @@
+<!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>async-foreach-behavior</title>
+
+<script src="../../bower_components/webcomponentsjs/webcomponents.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="async-foreach-behavior.html">
+
+<script>
+  suite('async-foreach-behavior tests', () => {
+    test('loops over each item', () => {
+      const fn = sinon.stub().returns(Promise.resolve());
+      return Gerrit.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);
+          });
+    });
+  });
+</script>
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
index a558dbe..8a6eb16 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
@@ -14,10 +14,10 @@
 limitations under the License.
 -->
 
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/async-foreach-behavior/async-foreach-behavior.html">
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
 <link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
-<link rel="import" href="../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.html">
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
 <link rel="import" href="../../core/gr-navigation/gr-navigation.html">
 <link rel="import" href="../../core/gr-reporting/gr-reporting.html">
 <link rel="import" href="../../diff/gr-comment-api/gr-comment-api.html">
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 aed4f02..68761fa 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
@@ -118,6 +118,7 @@
     },
 
     behaviors: [
+      Gerrit.AsyncForeachBehavior,
       Gerrit.KeyboardShortcutBehavior,
       Gerrit.PatchSetBehavior,
       Gerrit.PathListBehavior,
@@ -844,22 +845,20 @@
      * @return {!Promise}
      */
     _renderInOrder(paths, diffElements, initialCount) {
-      if (!paths.length) {
+      let iter = 0;
+      return this.asyncForeach(paths, path => {
+        iter++;
+        console.log('Expanding diff', iter, 'of', initialCount, ':', path);
+        const diffElem = this._findDiffByPath(path, diffElements);
+        diffElem.comments = this.$.commentAPI.getCommentsForPath(path,
+            this.patchRange, this.projectConfig);
+        const promises = [diffElem.reload()];
+        if (this._isLoggedIn) {
+          promises.push(this._reviewFile(path));
+        }
+        return Promise.all(promises);
+      }).then(() => {
         console.log('Finished expanding', initialCount, 'diff(s)');
-        return Promise.resolve();
-      }
-      console.log('Expanding diff', 1 + initialCount - paths.length, 'of',
-          initialCount, ':', paths[0]);
-      const diffElem = this._findDiffByPath(paths[0], diffElements);
-      diffElem.comments = this.$.commentAPI.getCommentsForPath(paths[0],
-          this.patchRange, this.projectConfig);
-
-      const promises = [diffElem.reload()];
-      if (this._isLoggedIn) {
-        promises.push(this._reviewFile(paths[0]));
-      }
-      return Promise.all(promises).then(() => {
-        return this._renderInOrder(paths.slice(1), diffElements, initialCount);
       });
     },
 
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index c748a9b..fa1ecee 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -160,6 +160,7 @@
 
   // Behaviors tests.
   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',
     'keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html',