瀏覽代碼

Merge pull request #970 from transloadit/feature/thumbnail-test

Add thumbnail generation integration test
Renée Kooi 6 年之前
父節點
當前提交
fc3dfcbcca

+ 24 - 13
.travis.yml

@@ -1,6 +1,28 @@
 language: node_js
 node_js:
 - 8.11.4
+
+before_install:
+  - nvm install-latest-npm
+
+install:
+  - npm install
+  - npm run bootstrap -- --no-ci
+
+script:
+  - npm run build
+  - npm run test
+  - 'if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then npm run test:acceptance; fi'
+  - npm run uploadcdn
+
+cache:
+  apt: true
+  directories:
+  - ~/.npm
+
+services:
+  - docker
+
 addons:
   apt:
     sources:
@@ -12,19 +34,7 @@ addons:
       secure: nAMJ/d1fm9urTYsQ+1uqj6Jjf71J8rzwYBSZbTDAeUEZzAdvGc0a9H3PYWM4pnUDPo5s1c9MMetXi2XNdUbXgMKHbEnePZ2mJamqFtXMmpG8pgFmMqj+btMd7Yybt070tRsn4Vy0uBSi2H/en7F3j+grABJV+SAXqWkSB7CU1fZaN/u0DpoGBNj1ZNwkYCIhpLueYJTPRWBOodMAarXuFv5+7KFOKuZM3tF/JjsMNSSaDgTkz13BZnbX6vNPxGJJNJcyJGSaXrVW8hh1Zmvnk/XdiLy+vt7Wz1wz3A9ebiFDuydo5AAkxrLFsCJ5nGEqLg3bkr6NaTRpbM84ZT3i1FQMTdKP6OHHqwAeBscB6BkyhZhzvsFtl2YRBNK9mA3OtOYvBmTkFkNqvrPQlfu7cFtyG5+AUfSCiTTgS/vWIwoqSVAXaOEqN8Fp54ecUdkzCTttl3gXteZzNLRYvyQcFpoJb6E+dS8qAW0OFOteiwKVuPCh3nGUzBP13bRo1i9UAX7ZCTlpjinkxE8ryzbToo6ZcVQMBAkKhaw/x8GzOtfm5rgYMeQzGEoBJNfr7qqfs7JMxAIEMYjrTL9PXVOp/R8F3FdsqbV70jSyfsxMSMkwSWFRmVslG8+Djy8P3LnckGy1FEbMHnH8GZHZg+hbBzN8Be1/1fV0oRRAr939WRc=
     access_key:
       secure: OY3oWwiJghfty9wSPVvlhirvFGxPHDdIRuVkzAv6j7C/hj2BWYAP/UHrwdQ9XiYisHi/B5mGeyRVlrAf0MNGrG84rTDUbTWZbmktfuxl7A+Y6c0czk+s4SdhOiANG5b3tFl5wKq8h7uhrWH5/jWoKQ2Fz1VDCqxTvvZQbo41jSBhi7TBia626hxEePzdaiuw6HhGFZtfaoVs/FX30ylz8WDNrBjwCynjxsT52BaQrVvgEhuyzlOpI69YkZBPOq4fc3KiZ2YR43gLTx8K+sYCE9yJxdg1xT/UAawEhmedU83nyBZVo4rr7+03AixIxtI28MUCfBMlcsGwBxcKEKY/IWcp9UkPCq6+zALQoncV478tP21eYvlmxSFhYCrv+WEQlN+BcNjr4OJlmmFDbCVaF7r9qLeQPImU0+9iJU3OjrW7lpfLxORpGDEr2Nx6awKkIJCxNyK9weefeNo6Fz3V1kkyZ/7yWFeniJnRUCbahrB2XgzxIE+W307s1Qs4fm6JK7hVLTtG4fBzjChmAyGIzu744ws9WqmjvkC9D7OfnuXqanv/VcBFqPiudInerv7NL8FketUC+fxe/7XJfcxdaDGBjk8Kq7zXDohGRGymUXEoMDNJsKkMMlaKzdf7tgqdhsRJoH9NCVqrDXuG5al0UtrDP5RS7qfoxUunJmNFhlg=
-cache:
-  apt: true
-  directories:
-  - ~/.npm
-before_install:
-- nvm install-latest-npm
-services:
-  - docker
-script:
-- npm run build
-- npm run test
-- '{ if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then npm run test:acceptance; fi } || true'
-- npm run uploadcdn
+
 env:
   global:
   - CXX=g++-4.8
@@ -50,6 +60,7 @@ env:
   - secure: V6qVB6vHAuSt1YPgzYpknLglnxbYJM5bsjAjQi78zZQSqrrWCMBgp5Q+Sq7Ad8nycgnPl8W8VpMnUIBqZcK4ciCW+vJBtZ7fBJLIEGMEk8+c8az8JePFcBRiaEicZPvZFuIHPteaAy6jK1GRVpjSjJi3NYEMpcDUs2aTM2bIpX0ks2WjUqTXiK8EvIaK2LTOgSBvyGUOiHwwTkom+PyD50t7jP98g+9Pn7GPwtWaUo1K8UpLArwR2fZ/nm0F2DU6ohz7o2t4LATFXuZhbOCj7O9vh+CmBf+/0C7giT3K23LbckDSK8CNTCDC2bsv8zDo5ZZtgdOvdatOD+VlsbAMkmUzzDgQ3410arZ8g1LVQw8eJwu1H+etcAinYf4r+Tr7s3dTBliMX70/IpjZrkHR9uXEi4LelBNYn6TKdLTW3lfVWkiFs/HPu4SoAmu1+sxSvwxVDDLeexQmb2fwp0r2tDKZ3eRfpQXLPACBaORnGW6ki9qXo8PBdaElxPbpzEWzAfT3F6zRjebpow+cdFBQrAga7/LrOrXerZxH2ekRWKgXzuhw7ERwVFrVN4/NlmDJXo+rm4na7n7CxY/QpZcdpthksDO9KYCqmn5dqMqFGislRvRe7PqIti8gC4pSdeUhDKsIh9OEgVNjCczvnA8rMRQ5aNmFx6wspmrBvO7qRLM=
   - secure: rVsiFPA9TvH/d2wkP8+1i5QGUuYw0q2BUAUdxyxO9hQcG/nRiHXtQfLbTRZHKwvqf0vyV6J1pJqLlVN4JO/bPhAvk55KAQJWl8UqyaeZiEN9KMcTr3fJuNFlBj4ciYiZ3BWwakblsiaGCjKMRdjki58a9f8XL2rcM8R6ccndjkTMYnBKaopSsAgouI8D5n74wQz6lODUayGOlbwlGLfGtPYplUfSLK4wghC+jgWsNjJySqJhfgYS0JCZc10Qw+FI2BoU4SZ4+P0L0YPIC9zV/cUW4qDT11N/oUgwfjZbPWfM9A/xn/d7sgDH+SpeoGdYler/lvxojj6L2mD/wAh8/lg1E6nL2aKgExE3z+fd2XV8L0osB/sulB7/Exrezg/mVejAx2IkWVHi4VEJmcTV+3WeEvTFOM3fID8dOVf+GUv+hcHdZMxS/hfj3keKCYG1P5ameMJO8FehRqhetNYnr6FTyrK+S+xitaZ/nXrTbHItPS0pZ4XA6CFs5uzMBPeDnk5/D7paPyrE/k2HAc1WmA6g37OyzYIMEV1laBz8IG0qMqg6JJmr09P/Iwrim5Ex2fAssT9Yr1WuOE2gWoF0A3XuVXQHVf4tJT6x/WDKChmbX588a47AvBgkFyoXLRilUYlET2tWnEpVxUovsbJXqvHwTXWMLO9riRjjeInbpvA=
   - secure: eq5hOqRBN2R7YO2dYdn5OjZc/zLLYYDZcCpCu/K/8fU4HYWTqxrBntjv8T0sZ5qdlAs3IniEfXxemz9V3zwvxR+vh2bGuYr2Xo7RRa2TIDuw+KUPZogrVxhXHPKfyJqstxy+dee2+pWhGkAP7caiu49eyqlboBMkzgpO/xcdehEWYRY5jPgvnlH+QRZ3GADKs1JEeltHDiZ6rYA7nj5Tyx9UoLgv4Av9UXdC29we7dLFTkVfCHE//7wfZW9+/IbxthA4qMjQOFaBrmagN5yweDg87iPTqNMth7FjzOavdUgQ2TW6d10VDEhLIZh36gLGreViKMDCEWKMQ8f/mv05Ao8+DXyXgxIn56II8lhUp5ukQ27ZWixfEKFx2lynJWRZE0pWwf8ec1+bXLQiBOE181Cl4nUT/TbFWzvV6yA+cMiQKe4y59bC4nhkK3IYgpR5kfCFOT+1tFknQ4hNJNacWwUmaDFMxYJaXEtRUn5jJa7eGRYSCrmnymbnzZ6w3Q3nQGNvNxpbBIXX/pzs0VDVTxSlgN4gA+n2jeCyjgVVrMQ/HoAS4uwm1cx89AttW+TANppg1PqWhhrJYuVEZSnvV8PM6R7rbvlS5tluezQj41YklgjsSopH7//+dbGGDNbrTTLic4J9PJR3yEtlAMdOCi53iT0R0Dt5X2WBv2QT5Eg=
+
 notifications:
   slack:
     secure: L3iQQE8sZ0ik1Z26gPoNMiIam9EOEwYhraHCY60Jk/wmfH6SW/727yKXpgcb/yayx37rUZplvoO7H8e05ISxTJKSepEeqbBUIBQs48S8hr+FHk0VPtpP4HGxqaITRLm+mI1coPRvfISxzrB8d240oup6muhC9Ws4/LXi6v8miyIOs2zoYmGxd56TrUeON3UYlKt6dMava0V4bugARzrafN/tfyI9ccqbHzQLBspQvBI61DzZ5I2vnWpkjfWgIHz9Fl4VzXHqMXwjuTUEu8ibA12b3dHZiJEAoqeb9Oj9QcLPbstPLhlNTZZaOrfiFtwLctI2rFh37slDpAfk5idv3ycxcoG5rbCxgyg5i6dpQqrqHxnyglgHg2/nZ+YA5okeS7nJJNtU/4S6AFRWOUUWMVVY0VBEV+8w+uurl0PDy80RUY3uyK64qAgQ8U0M81/Ys1oyWyn78TqHcbby7V2Ws5I9Yakrq8D+mdfsWYCio8F6LXHSwJ0mt2FanJtdDvpPk9sAwsXZN0n8xhELt5TiRp3bzVIQ0IPUgF54dTG9/zWRvC1P4TFaFU/2fg73ZEUC5aWJoFMnLSZjbZvp5gwpCVd0MjSBk80nF9dHYcavIgJ0wMGI3BMb8Nn6+T11Gw/ycr7OGU4NMkj7i8vSFgKF74piWZyiNW8orkMN6XZgM+o=

+ 12 - 5
bin/endtoend-build-ci

@@ -39,15 +39,22 @@ npm run build
 # https://github.com/facebook/create-react-app/pull/4626
 (cd && npm-auth-to-token -u user -p password -e user@example.com -r "$VERDACCIO_REGISTRY")
 
+git checkout -b endtoend-test-build
+# HACK this thing changes all the time for some reason on CI
+# so I'll just ignore it…
+git checkout -- package-lock.json
+
 # Simulate a publish of everything, to the local registry,
 # without changing things in git
-# Use --cd-version to skip version prompts
-lerna publish --yes \
+lerna version prerelease --yes \
+  --exact \
   --force-publish \
-  --registry="$VERDACCIO_REGISTRY" \
   --npm-client=npm \
-  --no-git-tag-version --no-push \
-  --canary
+  --no-push
+lerna publish from-git --yes \
+  --registry="$VERDACCIO_REGISTRY" \
+  --no-verify-access \
+  --npm-client=npm
 
 # revert version changes
 git checkout -- packages/*/package.json packages/@uppy/*/package.json

+ 7 - 2
bin/endtoend-build-tests

@@ -11,7 +11,8 @@ __file="${__dir}/$(basename "${BASH_SOURCE[0]}")"
 __base="$(basename ${__file} .sh)"
 __root="$(cd "$(dirname "${__dir}")" && pwd)"
 
-tests="tus-drag-drop tus-dashboard i18n-drag-drop xhr-limit providers"
+# Tests using a simple build setup.
+tests="tus-drag-drop tus-dashboard i18n-drag-drop xhr-limit providers thumbnails"
 
 for t in $tests; do
   mkdir -p "${__root}/test/endtoend/$t/dist"
@@ -22,4 +23,8 @@ for t in $tests; do
     -t babelify
 done
 
-(cd "${__root}/test/endtoend/create-react-app" && npm install && npm run build > /dev/null)
+# Speeecial tests that need custom builds.
+pushd "${__root}/test/endtoend/create-react-app"
+  npm install
+  npm run build
+popd

文件差異過大導致無法顯示
+ 373 - 242
package-lock.json


+ 2 - 2
package.json

@@ -49,7 +49,7 @@
     "isomorphic-fetch": "2.2.1",
     "jest": "^23.5.0",
     "json3": "^3.3.2",
-    "lerna": "^3.0.6",
+    "lerna": "^3.4.0",
     "lint-staged": "^6.1.1",
     "minify-stream": "^1.2.0",
     "mkdirp": "0.5.1",
@@ -126,7 +126,7 @@
     "web:update:frontpage:code:sample": "cd website && ./node_modules/.bin/hexo generate && cp -f public/frontpage-code-sample.html ./themes/uppy/layout/partials/frontpage-code-sample.html",
     "web": "npm-run-all web:clean web:build",
     "uploadcdn": "bin/upload-to-cdn.sh",
-    "prepare": "lerna bootstrap --hoist",
+    "bootstrap": "lerna bootstrap --hoist",
     "contributors": "githubcontrib --owner transloadit --repo uppy --cols 6 $([ \"${GITHUB_TOKEN:-}\" == \"\" ] && echo \"\" || echo \"--authToken ${GITHUB_TOKEN}\") --showlogin true --sortOrder desc",
     "contributors:save": "replace-x -m '<!--contributors-->[\\s\\S]+<!--/contributors-->' \"<!--contributors-->\n## Contributors\n\n$(npm run --silent contributors)\n<!--/contributors-->\" README.md"
   },

+ 18 - 11
packages/@uppy/thumbnail-generator/src/index.js

@@ -20,7 +20,10 @@ module.exports = class ThumbnailGenerator extends Plugin {
       thumbnailWidth: 200
     }
 
-    this.opts = Object.assign({}, defaultOptions, opts)
+    this.opts = {
+      ...defaultOptions,
+      ...opts
+    }
 
     this.addToQueue = this.addToQueue.bind(this)
     this.onRestored = this.onRestored.bind(this)
@@ -35,18 +38,18 @@ module.exports = class ThumbnailGenerator extends Plugin {
    */
   createThumbnail (file, targetWidth) {
     const originalUrl = URL.createObjectURL(file.data)
+
     const onload = new Promise((resolve, reject) => {
       const image = new Image()
       image.src = originalUrl
-      image.onload = () => {
+      image.addEventListener('load', () => {
         URL.revokeObjectURL(originalUrl)
         resolve(image)
-      }
-      image.onerror = () => {
-        // The onerror event is totally useless unfortunately, as far as I know
+      })
+      image.addEventListener('error', (event) => {
         URL.revokeObjectURL(originalUrl)
-        reject(new Error('Could not create thumbnail'))
-      }
+        reject(event.error || new Error('Could not create thumbnail'))
+      })
     })
 
     return onload
@@ -154,9 +157,7 @@ module.exports = class ThumbnailGenerator extends Plugin {
    * Set the preview URL for a file.
    */
   setPreviewURL (fileID, preview) {
-    this.uppy.setFileState(fileID, {
-      preview: preview
-    })
+    this.uppy.setFileState(fileID, { preview })
   }
 
   addToQueue (item) {
@@ -175,6 +176,8 @@ module.exports = class ThumbnailGenerator extends Plugin {
         .then(() => this.processQueue())
     } else {
       this.queueProcessing = false
+      this.uppy.log('[ThumbnailGenerator] Emptied thumbnail queue')
+      this.uppy.emit('thumbnail:all-generated')
     }
   }
 
@@ -183,9 +186,13 @@ module.exports = class ThumbnailGenerator extends Plugin {
       return this.createThumbnail(file, this.opts.thumbnailWidth)
         .then(preview => {
           this.setPreviewURL(file.id, preview)
+          this.uppy.log(`[ThumbnailGenerator] Generated thumbnail for ${file.id}`)
+          this.uppy.emit('thumbnail:generated', this.uppy.getFile(file.id), preview)
         })
         .catch(err => {
-          console.warn(err.stack || err.message)
+          this.uppy.log(`[ThumbnailGenerator] Failed thumbnail for ${file.id}`)
+          this.uppy.log(err, 'warning')
+          this.uppy.emit('thumbnail:error', this.uppy.getFile(file.id), err)
         })
     }
     return Promise.resolve()

+ 67 - 20
packages/@uppy/thumbnail-generator/src/index.test.js

@@ -4,25 +4,34 @@ const emitter = require('namespace-emitter')
 
 const delay = duration => new Promise(resolve => setTimeout(resolve, duration))
 
+function MockCore () {
+  const core = emitter()
+  const files = {}
+  core.mockFile = (id, f) => { files[id] = f }
+  core.getFile = (id) => files[id]
+  core.log = () => null
+  return core
+}
+
 describe('uploader/ThumbnailGeneratorPlugin', () => {
   it('should initialise successfully', () => {
-    const plugin = new ThumbnailGeneratorPlugin(null, {})
+    const plugin = new ThumbnailGeneratorPlugin(new MockCore(), {})
     expect(plugin instanceof Plugin).toEqual(true)
   })
 
   it('should accept the thumbnailWidth option and override the default', () => {
-    const plugin1 = new ThumbnailGeneratorPlugin(null) // eslint-disable-line no-new
+    const plugin1 = new ThumbnailGeneratorPlugin(new MockCore()) // eslint-disable-line no-new
     expect(plugin1.opts.thumbnailWidth).toEqual(200)
 
-    const plugin2 = new ThumbnailGeneratorPlugin(null, { thumbnailWidth: 100 }) // eslint-disable-line no-new
+    const plugin2 = new ThumbnailGeneratorPlugin(new MockCore(), { thumbnailWidth: 100 }) // eslint-disable-line no-new
     expect(plugin2.opts.thumbnailWidth).toEqual(100)
   })
 
   describe('install', () => {
     it('should subscribe to uppy file-added event', () => {
-      const core = {
+      const core = Object.assign(new MockCore(), {
         on: jest.fn()
-      }
+      })
 
       const plugin = new ThumbnailGeneratorPlugin(core)
       plugin.addToQueue = jest.fn()
@@ -35,10 +44,10 @@ describe('uploader/ThumbnailGeneratorPlugin', () => {
 
   describe('uninstall', () => {
     it('should unsubscribe from uppy file-added event', () => {
-      const core = {
+      const core = Object.assign(new MockCore(), {
         on: jest.fn(),
         off: jest.fn()
-      }
+      })
 
       const plugin = new ThumbnailGeneratorPlugin(core)
       plugin.addToQueue = jest.fn()
@@ -55,7 +64,7 @@ describe('uploader/ThumbnailGeneratorPlugin', () => {
 
   describe('queue', () => {
     it('should add a new file to the queue and start processing the queue when queueProcessing is false', () => {
-      const core = {}
+      const core = new MockCore()
       const plugin = new ThumbnailGeneratorPlugin(core)
       plugin.processQueue = jest.fn()
 
@@ -73,7 +82,7 @@ describe('uploader/ThumbnailGeneratorPlugin', () => {
     })
 
     it('should process items in the queue one by one', () => {
-      const core = {}
+      const core = new MockCore()
       const plugin = new ThumbnailGeneratorPlugin(core)
 
       plugin.requestThumbnail = jest.fn(() => delay(100))
@@ -106,9 +115,47 @@ describe('uploader/ThumbnailGeneratorPlugin', () => {
     })
   })
 
+  describe('events', () => {
+    const core = new MockCore()
+    const plugin = new ThumbnailGeneratorPlugin(core)
+    plugin.createThumbnail = jest.fn((file) => delay(100).then(() => `blob:${file.id}.png`))
+    plugin.setPreviewURL = jest.fn()
+
+    function add (file) {
+      core.mockFile(file.id, file)
+      plugin.addToQueue(file)
+    }
+
+    it('should emit thumbnail:generated when a thumbnail was generated', () => new Promise((resolve, reject) => {
+      const expected = ['bar', 'bar2', 'bar3']
+      core.on('thumbnail:generated', (file, preview) => {
+        try {
+          expect(file.id).toBe(expected.shift())
+          expect(preview).toBe(`blob:${file.id}.png`)
+        } catch (err) {
+          return reject(err)
+        }
+        if (expected.length === 0) resolve()
+      })
+      add({ id: 'bar', type: 'image/png' })
+      add({ id: 'bar2', type: 'image/png' })
+      add({ id: 'bar3', type: 'image/png' })
+    }))
+
+    it('should emit thumbnail:all-generated when all thumbnails were generated', () => {
+      return new Promise((resolve) => {
+        core.on('thumbnail:all-generated', resolve)
+        add({ id: 'bar4', type: 'image/png' })
+        add({ id: 'bar5', type: 'image/png' })
+      }).then(() => {
+        expect(plugin.queue).toHaveLength(0)
+      })
+    })
+  })
+
   describe('requestThumbnail', () => {
     it('should call createThumbnail if it is a supported filetype', () => {
-      const core = {}
+      const core = new MockCore()
       const plugin = new ThumbnailGeneratorPlugin(core)
 
       plugin.createThumbnail = jest
@@ -127,7 +174,7 @@ describe('uploader/ThumbnailGeneratorPlugin', () => {
     })
 
     it('should not call createThumbnail if it is not a supported filetype', () => {
-      const core = {}
+      const core = new MockCore()
       const plugin = new ThumbnailGeneratorPlugin(core)
 
       plugin.createThumbnail = jest
@@ -142,7 +189,7 @@ describe('uploader/ThumbnailGeneratorPlugin', () => {
     })
 
     it('should not call createThumbnail if the file is remote', () => {
-      const core = {}
+      const core = new MockCore()
       const plugin = new ThumbnailGeneratorPlugin(core)
 
       plugin.createThumbnail = jest
@@ -157,7 +204,7 @@ describe('uploader/ThumbnailGeneratorPlugin', () => {
     })
 
     it('should call setPreviewURL with the thumbnail image', () => {
-      const core = {}
+      const core = new MockCore()
       const plugin = new ThumbnailGeneratorPlugin(core)
 
       plugin.createThumbnail = jest
@@ -199,7 +246,7 @@ describe('uploader/ThumbnailGeneratorPlugin', () => {
 
   describe('getProportionalHeight', () => {
     it('should calculate the resized height based on the specified width of the image whilst keeping aspect ratio', () => {
-      const core = {}
+      const core = new MockCore()
       const plugin = new ThumbnailGeneratorPlugin(core)
       expect(
         plugin.getProportionalHeight({ width: 200, height: 100 }, 50)
@@ -215,7 +262,7 @@ describe('uploader/ThumbnailGeneratorPlugin', () => {
 
   describe('canvasToBlob', () => {
     it('should use canvas.toBlob if available', () => {
-      const core = {}
+      const core = new MockCore()
       const plugin = new ThumbnailGeneratorPlugin(core)
       const canvas = {
         toBlob: jest.fn()
@@ -242,7 +289,7 @@ describe('uploader/ThumbnailGeneratorPlugin', () => {
     })
 
     xit('should scale down the image by the specified number of steps', () => {
-      const core = {}
+      const core = new MockCore()
       const plugin = new ThumbnailGeneratorPlugin(core)
       const image = {
         width: 1000,
@@ -299,7 +346,7 @@ describe('uploader/ThumbnailGeneratorPlugin', () => {
 
   describe('resizeImage', () => {
     it('should return a canvas with the resized image on it', () => {
-      const core = {}
+      const core = new MockCore()
       const plugin = new ThumbnailGeneratorPlugin(core)
       const image = {
         width: 1000,
@@ -324,7 +371,7 @@ describe('uploader/ThumbnailGeneratorPlugin', () => {
     })
 
     it('should upsize if original image is smaller than target size', () => {
-      const core = {}
+      const core = new MockCore()
       const plugin = new ThumbnailGeneratorPlugin(core)
       const image = {
         width: 100,
@@ -356,7 +403,7 @@ describe('uploader/ThumbnailGeneratorPlugin', () => {
         b: { preview: 'blob:def' },
         c: { preview: 'blob:xyz', isRestored: true }
       }
-      const core = Object.assign(emitter(), {
+      const core = Object.assign(new MockCore(), {
         getState () {
           return { files }
         },
@@ -380,7 +427,7 @@ describe('uploader/ThumbnailGeneratorPlugin', () => {
       const files = {
         a: { preview: 'http://abc', isRestored: true }
       }
-      const core = Object.assign(emitter(), {
+      const core = Object.assign(new MockCore(), {
         getState () {
           return { files }
         },

+ 25 - 0
test/endtoend/thumbnails/index.html

@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <title>Uppy test page</title>
+  </head>
+  <body>
+    <style>
+      main {
+        max-width: 700px;
+        margin: auto;
+      }
+    </style>
+    <main>
+      <h2>Thumbnails</h2>
+      <div>
+        <div id="uppyThumbnails"></div>
+      </div>
+    </main>
+
+    <link href="uppy.min.css" rel="stylesheet">
+    <script src="bundle.js"></script>
+  </body>
+</html>

+ 39 - 0
test/endtoend/thumbnails/main.js

@@ -0,0 +1,39 @@
+/* eslint-disable */
+require('es6-promise/auto')
+require('whatwg-fetch')
+const Uppy = require('@uppy/core')
+const ThumbnailGenerator = require('@uppy/thumbnail-generator')
+const FileInput = require('@uppy/file-input')
+
+const uppyThumbnails = Uppy({
+  id: 'uppyThumbnails',
+  autoProceed: false,
+  debug: true
+})
+
+uppyThumbnails.use(ThumbnailGenerator, {})
+uppyThumbnails.use(FileInput, { target: '#uppyThumbnails', pretty: false })
+
+uppyThumbnails.on('file-added', (file) => {
+  const el = document.createElement('p')
+  el.className = 'file-name'
+  el.textContent = file.name
+  document.body.appendChild(el)
+})
+
+// Dump errors to the screen so saucelabs shows them in screenshots.
+uppyThumbnails.on('thumbnail:error', (file, err) => {
+  const el = document.createElement('pre')
+  el.style = 'font: 14pt monospace; background: red; color: white'
+  el.textContent = `Error: ${err.stack}`
+  document.body.appendChild(el)
+})
+
+uppyThumbnails.on('thumbnail:generated', (file, preview) => {
+  const img = new Image()
+  img.src = file.preview
+  img.className = 'file-preview'
+  img.style.display = 'block'
+
+  document.body.appendChild(img)
+})

+ 90 - 0
test/endtoend/thumbnails/test.js

@@ -0,0 +1,90 @@
+/* global browser, expect, $, $$ */
+const path = require('path')
+const fs = require('fs')
+const { selectFakeFile, supportsChooseFile } = require('../utils')
+
+const testURL = 'http://localhost:4567/thumbnails'
+
+const images = [
+  path.join(__dirname, '../../resources/image.jpg'),
+  path.join(__dirname, '../../resources/baboon.png'),
+  path.join(__dirname, '../../resources/kodim23.png'),
+  path.join(__dirname, '../../resources/invalid.png')
+]
+const notImages = [
+  { type: 'text/javascript', file: __filename }
+]
+
+describe('ThumbnailGenerator', () => {
+  beforeEach(() => {
+    browser.url(testURL)
+  })
+
+  it('should generate thumbnails for images', function () {
+    // FIXME why isn't the selectFakeFile alternative below working?
+    if (!supportsChooseFile()) {
+      return this.skip()
+    }
+
+    $('#uppyThumbnails .uppy-FileInput-input').waitForExist()
+
+    browser.execute(/* must be valid ES5 for IE */ function () {
+      window.thumbnailsReady = new Promise(function (resolve) {
+        window.uppyThumbnails.on('thumbnail:all-generated', resolve)
+      })
+    })
+
+    if (supportsChooseFile()) {
+      for (const img of images) {
+        browser.chooseFile('#uppyThumbnails .uppy-FileInput-input', img)
+      }
+      for (const { file } of notImages) {
+        browser.chooseFile('#uppyThumbnails .uppy-FileInput-input', file)
+      }
+    } else {
+      for (const img of images) {
+        browser.execute(
+          selectFakeFile,
+          'uppyThumbnails',
+          path.basename(img), // name
+          `image/${path.extname(img).slice(1)}`, // type
+          fs.readFileSync(img, 'base64') // b64
+        )
+      }
+      for (const { type, file } of notImages) {
+        browser.execute(
+          selectFakeFile,
+          'uppyThumbnails',
+          path.basename(file), // name
+          type, // type
+          fs.readFileSync(file, 'base64') // b64
+        )
+      }
+    }
+
+    browser.executeAsync(/* must be valid ES5 for IE */ function (done) {
+      window.thumbnailsReady.then(done)
+    })
+
+    // const names = $$('p.file-name')
+    const previews = $$('img.file-preview')
+
+    // Names should all be listed before previews--indicates that previews were generated asynchronously.
+    /* Nevermind this, chooseFile() doesn't accept multiple files so they are added one by one and the thumbnails
+     * have finished generating by the time we add the next.
+    const nys = names.map((el) => el.getLocation('y'))
+    const pys = previews.map((el) => el.getLocation('y'))
+    for (const ny of nys) {
+      for (const py of pys) {
+        expect(ny).to.be.below(py, 'names should be listed before previews')
+      }
+    }
+    */
+
+    expect(previews).to.have.lengthOf(3) // ex. the invalid image
+    for (const p of previews) {
+      expect(p.getAttribute('src')).to.match(/^blob:/)
+      expect(p.getElementSize('width')).to.equal(200)
+    }
+  })
+})

+ 8 - 4
test/endtoend/utils.js

@@ -2,14 +2,18 @@
 const path = require('path')
 const { spawn } = require('child_process')
 
-function selectFakeFile (uppyID) {
+// This function must be valid ES5, because it is run in the browser
+// and IE10/IE11 do not support new syntax features
+function selectFakeFile (uppyID, name, type, b64) {
+  if (!b64) b64 = 'PHN2ZyB2aWV3Qm94PSIwIDAgMTIwIDEyMCI+CiAgPGNpcmNsZSBjeD0iNjAiIGN5PSI2MCIgcj0iNTAiLz4KPC9zdmc+Cg=='
+
   var blob = new Blob(
-    ['data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMTIwIDEyMCI+CiAgPGNpcmNsZSBjeD0iNjAiIGN5PSI2MCIgcj0iNTAiLz4KPC9zdmc+Cg=='],
-    { type: 'image/svg+xml' }
+    ['data:image/svg+xml;base64,' + b64],
+    { type: type || 'image/svg+xml' }
   )
   window[uppyID].addFile({
     source: 'test',
-    name: 'test-file',
+    name: name || 'test-file',
     type: blob.type,
     data: blob
   })

+ 1 - 0
test/endtoend/wdio.base.conf.js

@@ -92,6 +92,7 @@ exports.config = {
     { mount: '/tus-drag-drop', path: './test/endtoend/tus-drag-drop/dist' },
     { mount: '/xhr-limit', path: './test/endtoend/xhr-limit/dist' },
     { mount: '/providers', path: './test/endtoend/providers/dist' },
+    { mount: '/thumbnails', path: './test/endtoend/thumbnails/dist' },
     { mount: '/create-react-app', path: './test/endtoend/create-react-app/build' }
   ],
 

+ 17 - 5
test/endtoend/wdio.local.conf.js

@@ -1,12 +1,24 @@
 const base = require('./wdio.base.conf')
 const { CompanionService } = require('./utils')
 
+// Use "npm run test:acceptance:local -- -b chrome" to test in chrome
+// "npm run test:acceptance:local -- -b firefox -b chrome" to test in FF and chrome
+let prevIsDashB = false
+const capabilities = []
+process.argv.forEach((arg) => {
+  if (prevIsDashB) {
+    capabilities.push({ browserName: arg })
+  }
+  prevIsDashB = arg === '-b'
+})
+
+// default to testing in firefox
+if (capabilities.length === 0) {
+  capabilities.push({ browserName: 'firefox' })
+}
+
 exports.config = Object.assign(base.config, {
-  capabilities: [
-    { browserName: 'firefox' }
-    // { browserName: 'MicrosoftEdge', version: '14.14393', platform: 'Windows 10' },
-    // { browserName: 'safari', version: '11.0', platform: 'macOS 10.12' }
-  ],
+  capabilities,
 
   // If you only want to run your tests until a specific amount of tests have failed use
   // bail (default is 0 - don't bail, run all tests).

+ 10 - 4
test/endtoend/wdio.remote.conf.js

@@ -2,18 +2,24 @@ const base = require('./wdio.base.conf')
 const { CompanionService } = require('./utils')
 
 function createCapability (capability) {
-  return Object.assign({
+  return {
     'tunnel-identifier': process.env.TRAVIS_JOB_NUMBER,
-    build: process.env.TRAVIS_BUILD_NUMBER
-  }, capability)
+    build: process.env.TRAVIS_BUILD_NUMBER,
+    extendedDebugging: true,
+    ...capability
+  }
 }
 
 exports.config = Object.assign(base.config, {
   capabilities: [
     { browserName: 'firefox', version: '38.0', platform: 'Linux' },
+    { browserName: 'firefox', version: '61.0', platform: 'Windows 10' },
     { browserName: 'internet explorer', version: '10.0', platform: 'Windows 7' },
+    { browserName: 'internet explorer', version: '11.0', platform: 'Windows 7' },
     { browserName: 'chrome', version: '50.0', platform: 'Windows 7' },
-    { browserName: 'MicrosoftEdge', version: '14.14393', platform: 'Windows 10' },
+    { browserName: 'chrome', version: '69.0', platform: 'Windows 10' },
+    { browserName: 'MicrosoftEdge', version: '14', platform: 'Windows 10' },
+    { browserName: 'MicrosoftEdge', version: '17', platform: 'Windows 10' },
     // { browserName: 'safari', version: '11.0', platform: 'macOS 10.12' },
     { browserName: 'safari', version: '10.0', platformName: 'iOS', platformVersion: '10.0', deviceOrientation: 'portrait', deviceName: 'iPhone 6 Simulator', appiumVersion: '1.7.1' },
     { browserName: 'chrome', platformName: 'Android', platformVersion: '6.0', deviceOrientation: 'portrait', deviceName: 'Android Emulator', appiumVersion: '1.7.1' }

二進制
test/resources/baboon.png


二進制
test/resources/invalid.png


二進制
test/resources/kodim23.png


+ 3 - 0
website/src/docs/contributing.md

@@ -14,8 +14,11 @@ After you have successfully forked the repo, clone and install the project:
 git clone git@github.com:YOUR_USERNAME/uppy.git
 cd uppy
 npm install
+npm run bootstrap
 ```
 
+We use lerna to manage the many plugin packages Uppy has. You should always do `npm run bootstrap` after an `npm install` to make sure lerna has installed the dependencies of each package and that the `package-lock.json` in the repository root is up to date.
+
 Our website’s examples section is also our playground, please read the [Local Previews](#Local-Previews) section to get up and running.
 
 ## Tests

部分文件因文件數量過多而無法顯示