personalization: Stub in photos grid for Google Photos collection.

Demo: http://shortn/_jtLy7ONoQ6

Note 1: This CL assumes that the photos grid does not need to be
implemented in an untrusted iframe in order render images in the future.

Note 2: This CL does *not* use a 'grid' iron-list as the spec for the
Google Photos collection [1] requires grouping photos under a header by
date/location. That is not possible to do via a 'grid' iron-list, so
this CL uses a 'list' iron-list and constructs the grid manually.
Headers will be added in a future CL.

[1] http://shortn/_2eijfld3iS

Bug: b:204610310
Change-Id: I4945b0f65c8b8055839b3b0cb3da6c6e713f54c3
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3253769
Commit-Queue: David Black <dmblack@google.com>
Reviewed-by: Jeffrey Young <cowmoo@chromium.org>
Cr-Commit-Position: refs/heads/main@{#938102}
diff --git a/ash/webui/personalization_app/resources/common/styles.js b/ash/webui/personalization_app/resources/common/styles.js
index d53ed05..8c465b6e 100644
--- a/ash/webui/personalization_app/resources/common/styles.js
+++ b/ash/webui/personalization_app/resources/common/styles.js
@@ -14,6 +14,10 @@
 <template>
   <style>
     :host {
+      --personalization-app-grid-item-border-radius: 12px;
+      --personalization-app-grid-item-height: 120px;
+      --personalization-app-grid-item-spacing: 16px;
+
       --personalization-app-text-shadow-elevation-1: 0 1px 3px
           rgba(0, 0, 0, 15%), 0 1px 2px rgba(0, 0, 0, 30%);
 
@@ -37,10 +41,11 @@
     }
     .photo-container {
       box-sizing: border-box;
-      /* 8 + 120 + 8 */
-      height: 136px;
+      height: calc(
+        var(--personalization-app-grid-item-height) +
+        var(--personalization-app-grid-item-spacing));
       overflow: hidden;
-      padding: 8px;
+      padding: calc(var(--personalization-app-grid-item-spacing) / 2);
       /* Media queries in trusted and untrusted code will resize to 25% at
        * correct widths.  Subtract 0.34px to fix subpixel rounding issues with
        * iron-list. This makes sure all photo containers on a row add up to at
@@ -54,7 +59,7 @@
        elements ignoring parent interior padding. */
     .photo-inner-container {
       align-items: center;
-      border-radius: 12px;
+      border-radius: var(--personalization-app-grid-item-border-radius);
       display: flex;
       cursor: pointer;
       height: 100%;
diff --git a/ash/webui/personalization_app/resources/common/utils.js b/ash/webui/personalization_app/resources/common/utils.js
index 3b73b96..537967f8 100644
--- a/ash/webui/personalization_app/resources/common/utils.js
+++ b/ash/webui/personalization_app/resources/common/utils.js
@@ -86,3 +86,30 @@
 export function getLoadingPlaceholderAnimationDelay(index) {
   return `--animation-delay: ${index * 83}ms;`;
 }
+
+/**
+ * Returns the number of grid items to render per row given the current inner
+ * width of the |window|.
+ * @return {number}
+ */
+export function getNumberOfGridItemsPerRow() {
+  return window.innerWidth > 688 ? 4 : 3;
+}
+
+/**
+ * Normalizes the given |key| for RTL.
+ * @param {string} key
+ * @param {boolean} isRTL
+ * @return {string}
+ */
+export function normalizeKeyForRTL(key, isRTL) {
+  if (isRTL) {
+    if (key === 'ArrowLeft') {
+      return 'ArrowRight';
+    }
+    if (key === 'ArrowRight') {
+      return 'ArrowLeft';
+    }
+  }
+  return key;
+}
diff --git a/ash/webui/personalization_app/resources/trusted/BUILD.gn b/ash/webui/personalization_app/resources/trusted/BUILD.gn
index 573426c4..4cb5659e 100644
--- a/ash/webui/personalization_app/resources/trusted/BUILD.gn
+++ b/ash/webui/personalization_app/resources/trusted/BUILD.gn
@@ -36,6 +36,7 @@
     ":personalization_store",
     ":styles",
     "../common:styles",
+    "../common:utils",
     "//third_party/polymer/v3_0/components-chromium/polymer:polymer_bundled",
   ]
 }
diff --git a/ash/webui/personalization_app/resources/trusted/google_photos_element.html b/ash/webui/personalization_app/resources/trusted/google_photos_element.html
index d67e8309..f1c3196a 100644
--- a/ash/webui/personalization_app/resources/trusted/google_photos_element.html
+++ b/ash/webui/personalization_app/resources/trusted/google_photos_element.html
@@ -4,22 +4,32 @@
   }
 
   #main {
+    display: flex;
+    flex-direction: column;
     height: 100%;
-    overflow-y: auto;
+    overflow: hidden;
     width: 100%;
   }
 
-  cr-button {
+  .tab-strip {
+    flex: 0 0 auto;
+    width: 100%;
+  }
+
+  .tab-strip > cr-button {
     border: 0;
   }
 
-  cr-button[aria-pressed=false] {
+  .tab-strip > cr-button[aria-pressed='false'] {
     color: var(--cros-text-color-secondary);
   }
 
   #albumsContent,
   #photosContent {
+    flex: 1 1 auto;
     height: 100%;
+    margin-top: var(--personalization-app-grid-item-spacing);
+    overflow: hidden;
     width: 100%;
   }
 
@@ -28,13 +38,64 @@
     background-color: red;
   }
 
-  #photosContent {
-    /** TODO(dmblack): Remove when implementing UI. */
-    background-color: blue;
+  #photosContent > iron-list {
+    height: 100%;
+    width: 100%;
+  }
+
+  #photosContent > iron-list > .row {
+    display: flex;
+    flex-direction: row;
+    width: 100%;
+  }
+
+  #photosContent > iron-list > .row:focus-visible {
+    outline: 0;
+  }
+
+  #photosContent > iron-list > .row:not([rowindex='0']) {
+    padding-top: var(--personalization-app-grid-item-spacing);
+  }
+
+  #photosContent > iron-list > .row > .photo {
+    align-items: center;
+    background: rgba(0, 0, 0, 0.12);
+    border-radius: var(--personalization-app-grid-item-border-radius);
+    display: flex;
+    flex: 1 1 auto;
+    height: var(--personalization-app-grid-item-height);
+    justify-content: center;
+    position: relative;
+    width: 100%;
+  }
+
+  #photosContent > iron-list > .row > .photo:focus-visible {
+    outline: 0;
+  }
+
+  #photosContent > iron-list > .row > .photo:focus-visible::before {
+    border: 2px solid var(--cros-focus-ring-color);
+    border-radius: var(--personalization-app-grid-item-border-radius);
+    box-sizing: border-box;
+    content: '';
+    display: block;
+    height: 100%;
+    left: 0;
+    position: absolute;
+    top: 0;
+    width: 100%;
+  }
+
+  #photosContent > iron-list > .row > .photo:not(:first-of-type) {
+    margin-inline-start: calc(var(--personalization-app-grid-item-spacing) / 2);
+  }
+
+  #photosContent > iron-list > .row > .photo:not(:last-of-type) {
+    margin-inline-end: calc(var(--personalization-app-grid-item-spacing) / 2);
   }
 </style>
 <main id="main" aria-label$="[[i18n('googlePhotosLabel')]]" tabindex="-1">
-  <div>
+  <div class="tab-strip">
     <cr-button id="photosTab" aria-pressed="[[isPhotosTabSelected_(tab_)]]"
       on-click="onTabSelected_">
       <div class="text">[[i18n('googlePhotosPhotosTabLabel')]]</div>
@@ -44,6 +105,20 @@
       <div class="text">[[i18n('googlePhotosAlbumsTabLabel')]]</div>
     </cr-button>
   </div>
-  <div id="photosContent" hidden$="[[!isPhotosTabSelected_(tab_)]]"></div>
+  <div id="photosContent" hidden$="[[!isPhotosTabSelected_(tab_)]]">
+    <iron-list id="photosGrid" items="[[photosByRow_]]" as="row">
+      <template>
+        <div class="row" rowindex$="[[index]]" tabindex$="[[tabIndex]]"
+          on-focus="onPhotosGridRowFocused_"
+          on-keydown="onPhotosGridRowKeyDown_">
+          <template is="dom-repeat" items="[[row]]" as="photo">
+            <div class="photo" colindex$="[[index]]" tabindex="-1">
+              [[photo]]
+            </div>
+          </template>
+        </div>
+      </template>
+    </iron-list>
+  </div>
   <div id="albumsContent" hidden$="[[!isAlbumsTabSelected_(tab_)]]"></div>
 </main>
diff --git a/ash/webui/personalization_app/resources/trusted/google_photos_element.js b/ash/webui/personalization_app/resources/trusted/google_photos_element.js
index 8c753ff..52f2e5d 100644
--- a/ash/webui/personalization_app/resources/trusted/google_photos_element.js
+++ b/ash/webui/personalization_app/resources/trusted/google_photos_element.js
@@ -7,11 +7,13 @@
  * collection.
  */
 
+import 'chrome://resources/polymer/v3_0/iron-list/iron-list.js';
 import 'chrome://resources/cr_elements/cr_button/cr_button.m.js';
 import './styles.js';
 import '../common/styles.js';
 import {assertNotReached} from '/assert.m.js';
-import {html} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
+import {afterNextRender, html} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
+import {getNumberOfGridItemsPerRow, isNonEmptyArray, normalizeKeyForRTL} from '../common/utils.js';
 import {getWallpaperProvider} from './mojo_interface_provider.js';
 import {initializeGooglePhotosData} from './personalization_controller.js';
 import {WithPersonalizationStore} from './personalization_store.js';
@@ -38,6 +40,16 @@
   static get properties() {
     return {
       /**
+       * Whether or not this element is currently hidden.
+       * @type {boolean}
+       */
+      hidden: {
+        type: Boolean,
+        value: true,
+        reflectToAttribute: true,
+      },
+
+      /**
        * The list of albums.
        * @type {?Array<undefined>}
        * @private
@@ -54,6 +66,7 @@
       albumsLoading_: {
         type: Boolean,
       },
+
       /**
        * The list of photos.
        * @type {?Array<undefined>}
@@ -64,6 +77,26 @@
       },
 
       /**
+       * The list of |photos_| split into the appropriate number of
+       * |photosPerRow_| so as to be rendered in a grid.
+       * @type {?Array<Array<undefined>>}
+       * @private
+       */
+      photosByRow_: {
+        type: Array,
+      },
+
+      /**
+       * The index of the currently focused column in the photos grid.
+       * @type {number}
+       * @private
+       */
+      photosGridFocusedColIndex_: {
+        type: Number,
+        value: 0,
+      },
+
+      /**
        * Whether the list of photos is currently loading.
        * @type {boolean}
        * @private
@@ -73,6 +106,18 @@
       },
 
       /**
+       * The number of photos to render per row in a grid.
+       * @type {number}
+       * @private
+       */
+      photosPerRow_: {
+        type: Number,
+        value: function() {
+          return getNumberOfGridItemsPerRow();
+        },
+      },
+
+      /**
        * The currently selected tab.
        * @type {!Tab}
        * @private
@@ -86,8 +131,9 @@
 
   static get observers() {
     return [
-      'onAlbumsLoaded_(albums_, albumsLoading_)',
-      'onPhotosLoaded_(photos_, photosLoading_)',
+      'onHiddenChanged_(hidden)',
+      'onAlbumsChanged_(albums_, albumsLoading_)',
+      'onPhotosChanged_(photos_, photosLoading_, photosPerRow_)',
     ];
   }
 
@@ -102,6 +148,8 @@
   connectedCallback() {
     super.connectedCallback();
 
+    this.addEventListener('iron-resize', this.onResized_.bind(this));
+
     this.watch('albums_', state => state.googlePhotos.albums);
     this.watch('albumsLoading_', state => state.loading.googlePhotos.albums);
     this.watch('photos_', state => state.googlePhotos.photos);
@@ -112,23 +160,128 @@
   }
 
   /**
-   * Invoked on changes to the list of albums and its loading state.
-   * @param {?Array<undefined>} albums
-   * @param {boolean} albumsLoading
+   * Invoked on changes to this element's hidden state.
    * @private
    */
-  onAlbumsLoaded_(albums, albumsLoading) {
+  onHiddenChanged_() {
+    if (this.hidden) {
+      return;
+    }
+
+    document.title = this.i18n('googlePhotosLabel');
+    this.shadowRoot.getElementById('main').focus();
+
+    // When iron-list items change while their parent element is hidden, the
+    // iron-list will render incorrectly. Force another layout to happen by
+    // firing an iron-resize event when this element becomes visible.
+    afterNextRender(this, () => {
+      [...this.shadowRoot.querySelectorAll('iron-list')].forEach(ironList => {
+        ironList.fire('iron-resize');
+      });
+    });
+  }
+
+  /**
+   * Invoked on changes to the list of albums and its loading state.
+   * @param {?Array<undefined>} albums
+   * @param {?boolean} albumsLoading
+   * @private
+   */
+  onAlbumsChanged_(albums, albumsLoading) {
     // TODO(dmblack): Send event to untrusted via iframe API.
   }
 
   /**
-   * Invoked on changes to the list of photos and its loading state.
+   * Invoked on changes to the list of photos, its loading state, and the
+   * number of photos to render per row in a grid.
    * @param {?Array<undefined>} photos
-   * @param {boolean} photosLoading
+   * @param {?boolean} photosLoading
+   * @param {?number} photosPerRow
    * @private
    */
-  onPhotosLoaded_(photos, photosLoading) {
-    // TODO(dmblack): Send event to untrusted via iframe API.
+  onPhotosChanged_(photos, photosLoading, photosPerRow) {
+    if (photosLoading || !photosPerRow) {
+      return;
+    }
+    if (!isNonEmptyArray(photos)) {
+      this.photosByRow_ = null;
+      return;
+    }
+    let index = 0;
+    this.photosByRow_ = Array.from(
+        {length: Math.ceil(photos.length / photosPerRow)}, (_, i) => {
+          i *= photosPerRow;
+          const row = photos.slice(i, i + photosPerRow).map(photo => index++);
+          while (row.length < photosPerRow) {
+            row.push(undefined);
+          }
+          return row;
+        });
+  }
+
+  /**
+   * Invoked on focus of a photos grid row.
+   * @param {!Event } e
+   * @private
+   */
+  onPhotosGridRowFocused_(e) {
+    // When a grid row is focused, forward the focus event on to the grid item
+    // at the focused column index.
+    const selector = `.photo[colindex="${this.photosGridFocusedColIndex_}"]`;
+    e.currentTarget.querySelector(selector)?.focus();
+  }
+
+  /**
+   * Invoked on key down of a photos grid row.
+   * @param {!Event} e
+   * @private
+   */
+  onPhotosGridRowKeyDown_(e) {
+    const row = (/** @type {{row: Array<undefined>}} */ (e.model)).row;
+
+    switch (normalizeKeyForRTL(e.key, this.i18n('textdirection') === 'rtl')) {
+      case 'ArrowLeft':
+        if (this.photosGridFocusedColIndex_ > 0) {
+          // Left arrow moves focus to the preceding grid item.
+          this.photosGridFocusedColIndex_ -= 1;
+          this.$.photosGrid.focusItem(e.model.index);
+        } else if (e.model.index > 0) {
+          // Left arrow moves focus to the preceding grid item, wrapping to the
+          // preceding grid row.
+          this.photosGridFocusedColIndex_ = row.length - 1;
+          this.$.photosGrid.focusItem(e.model.index - 1);
+        }
+        return;
+      case 'ArrowRight':
+        if (this.photosGridFocusedColIndex_ < row.length - 1) {
+          // Right arrow moves focus to the succeeding grid item.
+          this.photosGridFocusedColIndex_ += 1;
+          this.$.photosGrid.focusItem(e.model.index);
+        } else if (e.model.index < this.photosByRow_.length - 1) {
+          // Right arrow moves focus to the succeeding grid item, wrapping to
+          // the succeeding grid row.
+          this.photosGridFocusedColIndex_ = 0;
+          this.$.photosGrid.focusItem(e.model.index + 1);
+        }
+        return;
+      case 'Tab':
+        // The grid contains a single |focusable| row which becomes a focus trap
+        // due to the synthetic redirect of focus events to grid items. To
+        // escape the trap, make the |focusable| row unfocusable until has
+        // advanced to the next candidate.
+        const focusable = this.$.photosGrid.querySelector('[tabindex="0"]');
+        focusable.setAttribute('tabindex', -1);
+        afterNextRender(focusable, () => focusable.setAttribute('tabindex', 0));
+        return;
+    }
+  }
+
+  /**
+   * Invoked on resize of this element.
+   * @private
+   */
+  onResized_() {
+    this.photosPerRow_ = getNumberOfGridItemsPerRow();
   }
 
   /**
diff --git a/ash/webui/personalization_app/resources/trusted/personalization_controller.js b/ash/webui/personalization_app/resources/trusted/personalization_controller.js
index 52415e6..e1f1dbab 100644
--- a/ash/webui/personalization_app/resources/trusted/personalization_controller.js
+++ b/ash/webui/personalization_app/resources/trusted/personalization_controller.js
@@ -85,9 +85,9 @@
   store.dispatch(action.beginLoadGooglePhotosCountAction());
 
   // TODO(dmblack): Create and wire up mojo API. For now, simulate an async
-  // request that returns a zero count of Google Photos photos.
+  // request that returns a count of 1,000 Google Photos photos.
   return new Promise(resolve => setTimeout(() => {
-                       store.dispatch(action.setGooglePhotosCountAction(0));
+                       store.dispatch(action.setGooglePhotosCountAction(1000));
                        resolve();
                      }, 1000));
 }
@@ -101,9 +101,10 @@
   store.dispatch(action.beginLoadGooglePhotosPhotosAction());
 
   // TODO(dmblack): Create and wire up mojo API. For now, simulate an async
-  // request that returns an empty response list of Google Photos photos.
+  // request that returns a list of 1,000 Google Photos photos.
   return new Promise(resolve => setTimeout(() => {
-                       store.dispatch(action.setGooglePhotosPhotosAction([]));
+                       store.dispatch(action.setGooglePhotosPhotosAction(
+                           Array.from({length: 1000})));
                        resolve();
                      }, 1000));
 }
diff --git a/ash/webui/personalization_app/resources/trusted/personalization_router_element.html b/ash/webui/personalization_app/resources/trusted/personalization_router_element.html
index b522a35..6272d829 100644
--- a/ash/webui/personalization_app/resources/trusted/personalization_router_element.html
+++ b/ash/webui/personalization_app/resources/trusted/personalization_router_element.html
@@ -64,7 +64,7 @@
   <wallpaper-images collection-id="[[queryParams_.id]]"
     hidden="[[!shouldShowCollectionImages_(path_)]]"></wallpaper-images>
   <template is="dom-if" if="[[isGooglePhotosIntegrationEnabled_()]]">
-    <google-photos hidden$="[[!shouldShowGooglePhotosCollection_(path_)]]">
+    <google-photos hidden="[[!shouldShowGooglePhotosCollection_(path_)]]">
     </google-photos>
   </template>
   <local-images hidden="[[!shouldShowLocalCollection_(path_)]]"></local-images>
diff --git a/ash/webui/personalization_app/resources/trusted/personalization_store.js b/ash/webui/personalization_app/resources/trusted/personalization_store.js
index 865f9a4..356296e 100644
--- a/ash/webui/personalization_app/resources/trusted/personalization_store.js
+++ b/ash/webui/personalization_app/resources/trusted/personalization_store.js
@@ -5,6 +5,7 @@
 import {Store, StoreObserver} from 'chrome://resources/js/cr/ui/store.js';
 import {StoreClient, StoreClientInterface} from 'chrome://resources/js/cr/ui/store_client.js';
 import {I18nBehavior, I18nBehaviorInterface} from 'chrome://resources/js/i18n_behavior.m.js';
+import {IronResizableBehavior} from 'chrome://resources/polymer/v3_0/iron-resizable-behavior/iron-resizable-behavior.js';
 import {mixinBehaviors, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
 import {emptyState, PersonalizationState, reduce} from './personalization_reducers.js';
 
@@ -96,9 +97,14 @@
  * @implements {StoreClientInterface}
  * @implements {StoreObserver<PersonalizationState>}
  * @implements {I18nBehavior}
+ * @implements {IronResizableBehavior}
  */
-export const PersonalizationStoreClient =
-    [StoreClient, PersonalizationStoreClientImpl, I18nBehavior];
+export const PersonalizationStoreClient = [
+  StoreClient,
+  PersonalizationStoreClientImpl,
+  I18nBehavior,
+  IronResizableBehavior,
+];
 
 /**
  * @constructor
diff --git a/ash/webui/personalization_app/resources/untrusted/BUILD.gn b/ash/webui/personalization_app/resources/untrusted/BUILD.gn
index 3e09515..2fcb51e 100644
--- a/ash/webui/personalization_app/resources/untrusted/BUILD.gn
+++ b/ash/webui/personalization_app/resources/untrusted/BUILD.gn
@@ -13,6 +13,7 @@
     "../../mojom:mojom_js_library_for_compile",
     "../common:constants",
     "../common:iframe_api",
+    "../common:utils",
     "//third_party/polymer/v3_0/components-chromium/polymer:polymer_bundled",
   ]
 }
diff --git a/ash/webui/personalization_app/resources/untrusted/collections_grid.js b/ash/webui/personalization_app/resources/untrusted/collections_grid.js
index 25677020..03a41a9 100644
--- a/ash/webui/personalization_app/resources/untrusted/collections_grid.js
+++ b/ash/webui/personalization_app/resources/untrusted/collections_grid.js
@@ -8,7 +8,7 @@
 import {afterNextRender, html, PolymerElement} from 'chrome-untrusted://personalization/polymer/v3_0/polymer/polymer_bundled.min.js';
 import {EventType, kMaximumLocalImagePreviews} from '../common/constants.js';
 import {selectCollection, selectGooglePhotosCollection, selectLocalCollection, validateReceivedData} from '../common/iframe_api.js';
-import {getLoadingPlaceholderAnimationDelay, isNullOrArray, isNullOrNumber, isSelectionEvent} from '../common/utils.js';
+import {getLoadingPlaceholderAnimationDelay, getNumberOfGridItemsPerRow, isNullOrArray, isNullOrNumber, isSelectionEvent} from '../common/utils.js';
 
 /**
  * @fileoverview Responds to |SendCollectionsEvent| from trusted. Handles user
@@ -18,9 +18,6 @@
 const kGooglePhotosCollectionId = 'google_photos_';
 const kLocalCollectionId = 'local_';
 
-/** Width in pixels of when the app switches from 3 to 4 tiles wide. */
-const k3to4WidthCutoffPx = 688;
-
 /** Height in pixels of a tile. */
 const kTileHeightPx = 136;
 
@@ -236,7 +233,7 @@
         value() {
           // Fill the view with loading tiles. Will be adjusted to the correct
           // number of tiles when collections are received.
-          const x = window.innerWidth > k3to4WidthCutoffPx ? 4 : 3;
+          const x = getNumberOfGridItemsPerRow();
           const y = Math.floor(window.innerHeight / kTileHeightPx);
           return Array.from({length: x * y}, () => ({type: TileType.loading}));
         }
diff --git a/chrome/test/data/webui/chromeos/personalization_app/personalization_app_controller_test.js b/chrome/test/data/webui/chromeos/personalization_app/personalization_app_controller_test.js
index 91e279c..0f31ae5 100644
--- a/chrome/test/data/webui/chromeos/personalization_app/personalization_app_controller_test.js
+++ b/chrome/test/data/webui/chromeos/personalization_app/personalization_app_controller_test.js
@@ -59,21 +59,21 @@
           },
           {
             name: 'set_google_photos_count',
-            count: 0,
+            count: 1000,
           },
           {
             name: 'begin_load_google_photos_albums',
           },
           {
+            name: 'begin_load_google_photos_photos',
+          },
+          {
             name: 'set_google_photos_albums',
             albums: [],
           },
           {
-            name: 'begin_load_google_photos_photos',
-          },
-          {
             name: 'set_google_photos_photos',
-            photos: [],
+            photos: Array.from({length: 1000}),
           },
         ],
         personalizationStore.actions);
@@ -101,7 +101,7 @@
               photos: false,
             },
             googlePhotos: {
-              count: 0,
+              count: 1000,
               albums: undefined,
               photos: undefined,
             },
@@ -114,7 +114,20 @@
               photos: false,
             },
             googlePhotos: {
-              count: 0,
+              count: 1000,
+              albums: undefined,
+              photos: undefined,
+            },
+          },
+          // BEGIN_LOAD_GOOGLE_PHOTOS_PHOTOS.
+          {
+            'loading.googlePhotos': {
+              count: false,
+              albums: true,
+              photos: true,
+            },
+            googlePhotos: {
+              count: 1000,
               albums: undefined,
               photos: undefined,
             },
@@ -124,23 +137,10 @@
             'loading.googlePhotos': {
               count: false,
               albums: false,
-              photos: false,
-            },
-            googlePhotos: {
-              count: 0,
-              albums: [],
-              photos: undefined,
-            },
-          },
-          // BEGIN_LOAD_GOOGLE_PHOTOS_PHOTOS.
-          {
-            'loading.googlePhotos': {
-              count: false,
-              albums: false,
               photos: true,
             },
             googlePhotos: {
-              count: 0,
+              count: 1000,
               albums: [],
               photos: undefined,
             },
@@ -153,9 +153,9 @@
               photos: false,
             },
             googlePhotos: {
-              count: 0,
+              count: 1000,
               albums: [],
-              photos: [],
+              photos: Array.from({length: 1000}),
             },
           },
         ],