Repository: mirumee/saleor-dashboard (ts)

Issue:
Allow uploading multiple images at a time
### What I'm trying to achieve
Upload multiple product images at a time, either through drag and drop or the file explorer. The new dashboard currently does not allow that. Even if multiple images are dropped into the widget, only 1 is uploaded.

### Describe a proposed solution
The old dashboard allowed uploading multiple product images in one go. That would be useful to have in this dashboard as well.


Pull request:
Allow multiple images to be uploaded
I want to merge this change because it resolves #241 

### Screenshots
<img width="870" alt="Screenshot 2019-11-28 at 16 22 08" src="https://user-images.githubusercontent.com/6833443/69817774-a1f6f000-11fb-11ea-96a4-c9d0fa305d76.png">


### Pull Request Checklist

<!-- Please keep this section. It will make maintainer's life easier. -->

1. [x] All visible strings are translated with proper context.
1. [x] All data-formatting is locale-aware (dates, numbers, and so on).
1. [x] Translated strings are extracted to `.pot` file.
1. [x] Number of API calls is optimized.
1. [x] The changes are tested.
1. [x] Type definitions are up to date.
1. [x] Changes are mentioned in the changelog.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5bb57de0f..075d61cb5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,7 @@ All notable, unreleased changes to this project will be documented in this file.
 - Use searches as hooks instead of components - #262 by @dominik-zeglen
 - Add navigator - #267 by @dominik-zeglen
 - Fix voucher limit - #271 by @dominik-zeglen
+- Allow multiple images to be uploaded - #277 by @dominik-zeglen
 
 ## 2.0.0
 
diff --git a/src/categories/components/CategoryBackground/CategoryBackground.tsx b/src/categories/components/CategoryBackground/CategoryBackground.tsx
index ca042996e..8e340e304 100644
--- a/src/categories/components/CategoryBackground/CategoryBackground.tsx
+++ b/src/categories/components/CategoryBackground/CategoryBackground.tsx
@@ -77,6 +77,7 @@ const CategoryBackground: React.FC<CategoryBackgroundProps> = props => {
               onChange={event => onImageUpload(event.target.files[0])}
               type="file"
               ref={anchor}
+              accept="image/*"
             />
           </>
         }
@@ -90,7 +91,7 @@ const CategoryBackground: React.FC<CategoryBackgroundProps> = props => {
           </div>
         </CardContent>
       ) : image === null ? (
-        <ImageUpload onImageUpload={onImageUpload} />
+        <ImageUpload onImageUpload={files => onImageUpload(files[0])} />
       ) : (
         <CardContent>
           <ImageTile image={image} onImageDelete={onImageDelete} />
diff --git a/src/collections/components/CollectionImage/CollectionImage.tsx b/src/collections/components/CollectionImage/CollectionImage.tsx
index a4cb01d32..6e96904c6 100644
--- a/src/collections/components/CollectionImage/CollectionImage.tsx
+++ b/src/collections/components/CollectionImage/CollectionImage.tsx
@@ -92,6 +92,7 @@ export const CollectionImage: React.FC<CollectionImageProps> = props => {
               onChange={event => onImageUpload(event.target.files[0])}
               type="file"
               ref={anchor}
+              accept="image/*"
             />
           </>
         }
@@ -105,7 +106,7 @@ export const CollectionImage: React.FC<CollectionImageProps> = props => {
           </div>
         </CardContent>
       ) : image === null ? (
-        <ImageUpload onImageUpload={onImageUpload} />
+        <ImageUpload onImageUpload={files => onImageUpload(files[0])} />
       ) : (
         <CardContent>
           <ImageTile image={image} onImageDelete={onImageDelete} />
diff --git a/src/components/ImageTile/ImageTile.tsx b/src/components/ImageTile/ImageTile.tsx
index 9e71294a5..5d9312f22 100644
--- a/src/components/ImageTile/ImageTile.tsx
+++ b/src/components/ImageTile/ImageTile.tsx
@@ -1,9 +1,10 @@
-import { makeStyles } from "@material-ui/core/styles";
-import React from "react";
-
+import CircularProgress from "@material-ui/core/CircularProgress";
 import IconButton from "@material-ui/core/IconButton";
+import { makeStyles } from "@material-ui/core/styles";
 import DeleteIcon from "@material-ui/icons/Delete";
 import EditIcon from "@material-ui/icons/Edit";
+import classNames from "classnames";
+import React from "react";
 
 const useStyles = makeStyles(theme => ({
   image: {
@@ -37,6 +38,13 @@ const useStyles = makeStyles(theme => ({
     top: 0,
     width: 148
   },
+  imageOverlayShow: {
+    "&$imageOverlay": {
+      alignItems: "center",
+      display: "flex",
+      justifyContent: "center"
+    }
+  },
   imageOverlayToolbar: {
     display: "flex",
     justifyContent: "flex-end"
@@ -48,30 +56,39 @@ interface ImageTileProps {
     alt?: string;
     url: string;
   };
+  loading?: boolean;
   onImageDelete?: () => void;
   onImageEdit?: (event: React.ChangeEvent<any>) => void;
 }
 
 const ImageTile: React.FC<ImageTileProps> = props => {
-  const { onImageDelete, onImageEdit, image } = props;
+  const { loading, onImageDelete, onImageEdit, image } = props;
 
   const classes = useStyles(props);
 
   return (
     <div className={classes.imageContainer} data-tc="product-image">
-      <div className={classes.imageOverlay}>
-        <div className={classes.imageOverlayToolbar}>
-          {onImageEdit && (
-            <IconButton color="primary" onClick={onImageEdit}>
-              <EditIcon />
-            </IconButton>
-          )}
-          {onImageDelete && (
-            <IconButton color="primary" onClick={onImageDelete}>
-              <DeleteIcon />
-            </IconButton>
-          )}
-        </div>
+      <div
+        className={classNames(classes.imageOverlay, {
+          [classes.imageOverlayShow]: loading
+        })}
+      >
+        {loading ? (
+          <CircularProgress size={32} />
+        ) : (
+          <div className={classes.imageOverlayToolbar}>
+            {onImageEdit && (
+              <IconButton color="primary" onClick={onImageEdit}>
+                <EditIcon />
+              </IconButton>
+            )}
+            {onImageDelete && (
+              <IconButton color="primary" onClick={onImageDelete}>
+                <DeleteIcon />
+              </IconButton>
+            )}
+          </div>
+        )}
       </div>
       <img className={classes.image} src={image.url} alt={image.alt} />
     </div>
diff --git a/src/components/ImageUpload/ImageUpload.tsx b/src/components/ImageUpload/ImageUpload.tsx
index 5216a0013..070ecc974 100644
--- a/src/components/ImageUpload/ImageUpload.tsx
+++ b/src/components/ImageUpload/ImageUpload.tsx
@@ -14,11 +14,11 @@ interface ImageUploadProps {
   isActiveClassName?: string;
   iconContainerClassName?: string;
   iconContainerActiveClassName?: string;
-  onImageUpload: (file: File) => void;
+  onImageUpload: (file: FileList) => void;
 }
 
 const useStyles = makeStyles(theme => ({
-  containerDragActive: {
+  backdrop: {
     background: fade(theme.palette.primary.main, 0.1),
     color: theme.palette.primary.main
   },
@@ -57,41 +57,37 @@ const useStyles = makeStyles(theme => ({
 export const ImageUpload: React.FC<ImageUploadProps> = props => {
   const {
     children,
-
     className,
     disableClick,
-    isActiveClassName,
     iconContainerActiveClassName,
     iconContainerClassName,
+    isActiveClassName,
     onImageUpload
   } = props;
 
   const classes = useStyles(props);
 
   return (
-    <Dropzone
-      disableClick={disableClick}
-      onDrop={files => onImageUpload(files[0])}
-    >
+    <Dropzone disableClick={disableClick} onDrop={onImageUpload}>
       {({ isDragActive, getInputProps, getRootProps }) => (
         <>
           <div
             {...getRootProps()}
-            className={classNames({
-              [classes.photosIconContainer]: true,
-              [classes.containerDragActive]: isDragActive,
-              [className]: !!className,
-              [isActiveClassName]: !!isActiveClassName && isDragActive
+            className={classNames(className, classes.photosIconContainer, {
+              [classes.backdrop]: isDragActive,
+              [isActiveClassName]: isDragActive
             })}
           >
             <div
-              className={classNames({
-                [iconContainerClassName]: !!iconContainerClassName,
-                [iconContainerActiveClassName]:
-                  !!iconContainerActiveClassName && isDragActive
+              className={classNames(iconContainerClassName, {
+                [iconContainerActiveClassName]: isDragActive
               })}
             >
-              <input {...getInputProps()} className={classes.fileField} />
+              <input
+                {...getInputProps()}
+                className={classes.fileField}
+                accept="image/*"
+              />
               <ImageIcon className={classes.photosIcon} />
               <Typography className={classes.uploadText}>
                 <FormattedMessage
diff --git a/src/products/components/ProductImages/ProductImages.tsx b/src/products/components/ProductImages/ProductImages.tsx
index b132d134e..9a34d63e2 100644
--- a/src/products/components/ProductImages/ProductImages.tsx
+++ b/src/products/components/ProductImages/ProductImages.tsx
@@ -7,6 +7,7 @@ import ImageTile from "@saleor/components/ImageTile";
 import ImageUpload from "@saleor/components/ImageUpload";
 import { commonMessages } from "@saleor/intl";
 import { ReorderAction } from "@saleor/types";
+import createMultiFileUploadHandler from "@saleor/utils/handlers/multiFileUploadHandler";
 import classNames from "classnames";
 import React from "react";
 import { useIntl } from "react-intl";
@@ -72,6 +73,7 @@ const useStyles = makeStyles(theme => ({
   imageUpload: {
     height: "100%",
     left: 0,
+    outline: 0,
     position: "absolute",
     top: 0,
     width: "100%"
@@ -134,13 +136,14 @@ const SortableImage = SortableElement<SortableImageProps>(
 
 interface ImageListContainerProps {
   className: string;
-  items: any;
+  items: ProductDetails_product_images[];
+  preview: ProductDetails_product_images[];
   onImageDelete: (id: string) => () => void;
   onImageEdit: (id: string) => () => void;
 }
 
 const ImageListContainer = SortableContainer<ImageListContainerProps>(
-  ({ items, onImageDelete, onImageEdit, ...props }) => (
+  ({ items, preview, onImageDelete, onImageEdit, ...props }) => (
     <div {...props}>
       {items.map((image, index) => (
         <SortableImage
@@ -151,6 +154,11 @@ const ImageListContainer = SortableContainer<ImageListContainerProps>(
           onImageDelete={onImageDelete(image.id)}
         />
       ))}
+      {preview
+        .sort((a, b) => (a.sortOrder > b.sortOrder ? 1 : -1))
+        .map(image => (
+          <ImageTile loading={true} image={image} />
+        ))}
     </div>
   )
 );
@@ -169,6 +177,32 @@ const ProductImages: React.FC<ProductImagesProps> = props => {
   const classes = useStyles(props);
   const intl = useIntl();
   const upload = React.useRef(null);
+  const [imagesToUpload, setImagesToUpload] = React.useState<
+    ProductDetails_product_images[]
+  >([]);
+
+  const handleImageUpload = createMultiFileUploadHandler(onImageUpload, {
+    onAfterUpload: () =>
+      setImagesToUpload(prevImagesToUpload => prevImagesToUpload.slice(1)),
+    onStart: files => {
+      Array.from(files).forEach((file, fileIndex) => {
+        const reader = new FileReader();
+        reader.onload = event => {
+          setImagesToUpload(prevImagesToUpload => [
+            ...prevImagesToUpload,
+            {
+              __typename: "ProductImage",
+              alt: "",
+              id: "",
+              sortOrder: fileIndex,
+              url: event.target.result as string
+            }
+          ]);
+        };
+        reader.readAsDataURL(file);
+      });
+    }
+  });
 
   return (
     <Card className={classes.card}>
@@ -191,9 +225,11 @@ const ProductImages: React.FC<ProductImagesProps> = props => {
             <input
               className={classes.fileField}
               id="fileUpload"
-              onChange={event => onImageUpload(event.target.files[0])}
+              onChange={event => handleImageUpload(event.target.files)}
+              multiple
               type="file"
               ref={upload}
+              accept="image/*"
             />
           </>
         }
@@ -215,7 +251,7 @@ const ProductImages: React.FC<ProductImagesProps> = props => {
               disableClick={true}
               iconContainerClassName={classes.imageUploadIcon}
               iconContainerActiveClassName={classes.imageUploadIconActive}
-              onImageUpload={onImageUpload}
+              onImageUpload={handleImageUpload}
             >
               {({ isDragActive }) => (
                 <CardContent>
@@ -224,6 +260,7 @@ const ProductImages: React.FC<ProductImagesProps> = props => {
                     helperClass="dragged"
                     axis="xy"
                     items={images}
+                    preview={imagesToUpload}
                     onSortEnd={onImageReorder}
                     className={classNames({
                       [classes.root]: true,
@@ -237,7 +274,7 @@ const ProductImages: React.FC<ProductImagesProps> = props => {
             </ImageUpload>
           </>
         ) : (
-          <ImageUpload onImageUpload={onImageUpload} />
+          <ImageUpload onImageUpload={handleImageUpload} />
         )}
       </div>
     </Card>
diff --git a/src/utils/handlers/multiFileUploadHandler.ts b/src/utils/handlers/multiFileUploadHandler.ts
new file mode 100644
index 000000000..c97b17418
--- /dev/null
+++ b/src/utils/handlers/multiFileUploadHandler.ts
@@ -0,0 +1,58 @@
+export type CreateMultiFileUploadHandlerCallbacks = Partial<{
+  onAfterUpload: (index: number, files: File[]) => void;
+  onBeforeUpload: (index: number, files: File[]) => void;
+  onCompleted: (files: File[]) => void;
+  onError: (index: number, files: File[]) => void;
+  onStart: (files: File[]) => void;
+}>;
+
+function createMultiFileUploadHandler<T>(
+  upload: (file: File, fileIndex: number) => Promise<T>,
+  {
+    onAfterUpload,
+    onBeforeUpload,
+    onCompleted,
+    onError,
+    onStart
+  }: CreateMultiFileUploadHandlerCallbacks
+) {
+  async function uploadImage(files: File[], fileIndex: number): Promise<void> {
+    if (files.length > fileIndex) {
+      try {
+        if (onBeforeUpload) {
+          onBeforeUpload(fileIndex, files);
+        }
+
+        await upload(files[fileIndex], fileIndex);
+
+        if (onAfterUpload) {
+          onAfterUpload(fileIndex, files);
+        }
+      } catch (exception) {
+        console.error(
+          `Could not upload file #${fileIndex + 1}. Reason: ${exception}`
+        );
+        if (onError) {
+          onError(fileIndex, files);
+        }
+      } finally {
+        await uploadImage(files, fileIndex + 1);
+      }
+    }
+  }
+  return async (files: FileList): Promise<void> => {
+    const fileArray = Array.from(files);
+
+    if (onStart) {
+      onStart(fileArray);
+    }
+
+    await uploadImage(fileArray, 0);
+
+    if (onCompleted) {
+      onCompleted(fileArray);
+    }
+  };
+}
+
+export default createMultiFileUploadHandler;


Test patch:
diff --git a/src/storybook/__snapshots__/Stories.test.ts.snap b/src/storybook/__snapshots__/Stories.test.ts.snap
index ec9718967..6487a2acc 100644
--- a/src/storybook/__snapshots__/Stories.test.ts.snap
+++ b/src/storybook/__snapshots__/Stories.test.ts.snap
@@ -16634,6 +16634,7 @@ Ctrl + K"
               </span>
             </button>
             <input
+              accept="image/*"
               class="makeStyles-fileField-id"
               id="fileUpload"
               type="file"
@@ -17411,6 +17412,7 @@ Ctrl + K"
               </span>
             </button>
             <input
+              accept="image/*"
               class="makeStyles-fileField-id"
               id="fileUpload"
               type="file"
@@ -18363,6 +18365,7 @@ Ctrl + K"
               </span>
             </button>
             <input
+              accept="image/*"
               class="makeStyles-fileField-id"
               id="fileUpload"
               type="file"
@@ -18383,6 +18386,7 @@ Ctrl + K"
             class=""
           >
             <input
+              accept="image/*"
               autocomplete="off"
               class="makeStyles-fileField-id"
               multiple=""
@@ -19292,6 +19296,7 @@ Ctrl + K"
               </span>
             </button>
             <input
+              accept="image/*"
               class="makeStyles-fileField-id"
               id="fileUpload"
               type="file"
@@ -20296,6 +20301,7 @@ Ctrl + K"
               </span>
             </button>
             <input
+              accept="image/*"
               class="makeStyles-fileField-id"
               id="fileUpload"
               type="file"
@@ -21271,6 +21277,7 @@ Ctrl + K"
               </span>
             </button>
             <input
+              accept="image/*"
               class="makeStyles-fileField-id"
               id="fileUpload"
               type="file"
@@ -22873,6 +22880,7 @@ Ctrl + K"
                   </span>
                 </button>
                 <input
+                  accept="image/*"
                   class="CollectionImage-fileField-id"
                   id="fileUpload"
                   type="file"
@@ -24156,6 +24164,7 @@ Ctrl + K"
                   </span>
                 </button>
                 <input
+                  accept="image/*"
                   class="CollectionImage-fileField-id"
                   id="fileUpload"
                   type="file"
@@ -25352,6 +25361,7 @@ Ctrl + K"
                   </span>
                 </button>
                 <input
+                  accept="image/*"
                   class="CollectionImage-fileField-id"
                   id="fileUpload"
                   type="file"
@@ -27476,6 +27486,7 @@ Ctrl + K"
                   </span>
                 </button>
                 <input
+                  accept="image/*"
                   class="CollectionImage-fileField-id"
                   id="fileUpload"
                   type="file"
@@ -27496,6 +27507,7 @@ Ctrl + K"
                 class=""
               >
                 <input
+                  accept="image/*"
                   autocomplete="off"
                   class="makeStyles-fileField-id"
                   multiple=""
@@ -28217,6 +28229,7 @@ Ctrl + K"
                   </span>
                 </button>
                 <input
+                  accept="image/*"
                   class="CollectionImage-fileField-id"
                   id="fileUpload"
                   type="file"
@@ -28237,6 +28250,7 @@ Ctrl + K"
                 class=""
               >
                 <input
+                  accept="image/*"
                   autocomplete="off"
                   class="makeStyles-fileField-id"
                   multiple=""
@@ -93010,8 +93024,10 @@ Ctrl + K"
                   </span>
                 </button>
                 <input
+                  accept="image/*"
                   class="makeStyles-fileField-id"
                   id="fileUpload"
+                  multiple=""
                   type="file"
                 />
               </div>
@@ -93026,13 +93042,14 @@ Ctrl + K"
               class="makeStyles-imageGridContainer-id"
             >
               <div
-                class="makeStyles-photosIconContainer-id makeStyles-imageUpload-id"
+                class="makeStyles-imageUpload-id makeStyles-photosIconContainer-id"
                 tabindex="0"
               >
                 <div
                   class="makeStyles-imageUploadIcon-id"
                 >
                   <input
+                    accept="image/*"
                     autocomplete="off"
                     class="makeStyles-fileField-id"
                     multiple=""
@@ -94532,8 +94549,10 @@ Ctrl + K"
                   </span>
                 </button>
                 <input
+                  accept="image/*"
                   class="makeStyles-fileField-id"
                   id="fileUpload"
+                  multiple=""
                   type="file"
                 />
               </div>
@@ -94548,13 +94567,14 @@ Ctrl + K"
               class="makeStyles-imageGridContainer-id"
             >
               <div
-                class="makeStyles-photosIconContainer-id makeStyles-imageUpload-id"
+                class="makeStyles-imageUpload-id makeStyles-photosIconContainer-id"
                 tabindex="0"
               >
                 <div
                   class="makeStyles-imageUploadIcon-id"
                 >
                   <input
+                    accept="image/*"
                     autocomplete="off"
                     class="makeStyles-fileField-id"
                     multiple=""
@@ -96313,8 +96333,10 @@ Ctrl + K"
                   </span>
                 </button>
                 <input
+                  accept="image/*"
                   class="makeStyles-fileField-id"
                   id="fileUpload"
+                  multiple=""
                   type="file"
                 />
               </div>
@@ -96329,13 +96351,14 @@ Ctrl + K"
               class="makeStyles-imageGridContainer-id"
             >
               <div
-                class="makeStyles-photosIconContainer-id makeStyles-imageUpload-id"
+                class="makeStyles-imageUpload-id makeStyles-photosIconContainer-id"
                 tabindex="0"
               >
                 <div
                   class="makeStyles-imageUploadIcon-id"
                 >
                   <input
+                    accept="image/*"
                     autocomplete="off"
                     class="makeStyles-fileField-id"
                     multiple=""
@@ -97896,8 +97919,10 @@ Ctrl + K"
                   </span>
                 </button>
                 <input
+                  accept="image/*"
                   class="makeStyles-fileField-id"
                   id="fileUpload"
+                  multiple=""
                   type="file"
                 />
               </div>
@@ -99186,8 +99211,10 @@ Ctrl + K"
                   </span>
                 </button>
                 <input
+                  accept="image/*"
                   class="makeStyles-fileField-id"
                   id="fileUpload"
+                  multiple=""
                   type="file"
                 />
               </div>
@@ -99209,6 +99236,7 @@ Ctrl + K"
                   class=""
                 >
                   <input
+                    accept="image/*"
                     autocomplete="off"
                     class="makeStyles-fileField-id"
                     multiple=""
@@ -100874,8 +100902,10 @@ Ctrl + K"
                   </span>
                 </button>
                 <input
+                  accept="image/*"
                   class="makeStyles-fileField-id"
                   id="fileUpload"
+                  multiple=""
                   type="file"
                 />
               </div>
@@ -100890,13 +100920,14 @@ Ctrl + K"
               class="makeStyles-imageGridContainer-id"
             >
               <div
-                class="makeStyles-photosIconContainer-id makeStyles-imageUpload-id"
+                class="makeStyles-imageUpload-id makeStyles-photosIconContainer-id"
                 tabindex="0"
               >
                 <div
                   class="makeStyles-imageUploadIcon-id"
                 >
                   <input
+                    accept="image/*"
                     autocomplete="off"
                     class="makeStyles-fileField-id"
                     multiple=""
diff --git a/src/utils/handlers/multiFileUploadHandler.test.ts b/src/utils/handlers/multiFileUploadHandler.test.ts
new file mode 100644
index 000000000..cc1294c40
--- /dev/null
+++ b/src/utils/handlers/multiFileUploadHandler.test.ts
@@ -0,0 +1,67 @@
+import createMultiFileUploadHandler from "./multiFileUploadHandler";
+
+const testFiles = Array(5)
+  .fill(0)
+  .map(() => new File([""], "mockFile"));
+
+describe("Multiple file upload handler", () => {
+  it("properly handles success", done => {
+    const cbs = {
+      onAfterUpload: jest.fn(),
+      onBeforeUpload: jest.fn(),
+      onCompleted: jest.fn(files =>
+        expect(files.length).toBe(testFiles.length)
+      ),
+      onError: jest.fn(),
+      onStart: jest.fn()
+    };
+    const handle = createMultiFileUploadHandler(() => {
+      const promise = new Promise(resolve => {
+        expect(cbs.onBeforeUpload).toBeCalledTimes(
+          cbs.onAfterUpload.mock.calls.length + 1
+        );
+        resolve();
+      });
+      return promise;
+    }, cbs);
+
+    handle((testFiles as unknown) as FileList).then(() => {
+      expect(cbs.onAfterUpload).toBeCalledTimes(testFiles.length);
+      expect(cbs.onBeforeUpload).toBeCalledTimes(testFiles.length);
+      expect(cbs.onCompleted).toBeCalledTimes(1);
+      expect(cbs.onError).toBeCalledTimes(0);
+      expect(cbs.onStart).toBeCalledTimes(1);
+      done();
+    });
+  });
+
+  it("properly handles error", done => {
+    const cbs = {
+      onAfterUpload: jest.fn(),
+      onBeforeUpload: jest.fn(),
+      onCompleted: jest.fn(files =>
+        expect(files.length).toBe(testFiles.length)
+      ),
+      onError: jest.fn(),
+      onStart: jest.fn()
+    };
+    const handle = createMultiFileUploadHandler((_, fileIndex) => {
+      const promise = new Promise((resolve, reject) => {
+        if (fileIndex === 2) {
+          reject();
+        } else {
+          resolve();
+        }
+      });
+      return promise;
+    }, cbs);
+
+    handle((testFiles as unknown) as FileList).then(() => {
+      expect(cbs.onAfterUpload).toBeCalledTimes(testFiles.length - 1);
+      expect(cbs.onBeforeUpload).toBeCalledTimes(testFiles.length);
+      expect(cbs.onCompleted).toBeCalledTimes(1);
+      expect(cbs.onError).toBeCalledTimes(1);
+      done();
+    });
+  });
+});