UNPKG

17.3 kBJavaScriptView Raw
1const ThumbnailGeneratorPlugin = require('./index')
2const { Plugin } = require('@uppy/core')
3const emitter = require('namespace-emitter')
4
5const delay = duration => new Promise(resolve => setTimeout(resolve, duration))
6
7function MockCore () {
8 const core = emitter()
9 const files = {}
10 core.state = {
11 files,
12 plugins: {}
13 }
14 core.mockFile = (id, f) => { files[id] = f }
15 core.getFile = (id) => files[id]
16 core.log = (message, level = 'log') => {
17 if (level === 'warn' || level === 'error') {
18 console[level](message)
19 }
20 }
21 core.getState = () => core.state
22 core.setState = () => null
23 return core
24}
25
26describe('uploader/ThumbnailGeneratorPlugin', () => {
27 it('should initialise successfully', () => {
28 const plugin = new ThumbnailGeneratorPlugin(new MockCore(), {})
29 expect(plugin instanceof Plugin).toEqual(true)
30 })
31
32 it('should accept the thumbnailWidth and thumbnailHeight option and override the default', () => {
33 const plugin1 = new ThumbnailGeneratorPlugin(new MockCore()) // eslint-disable-line no-new
34 expect(plugin1.opts.thumbnailWidth).toEqual(null)
35 expect(plugin1.opts.thumbnailHeight).toEqual(null)
36
37 const plugin2 = new ThumbnailGeneratorPlugin(new MockCore(), { thumbnailWidth: 100 }) // eslint-disable-line no-new
38 expect(plugin2.opts.thumbnailWidth).toEqual(100)
39
40 const plugin3 = new ThumbnailGeneratorPlugin(new MockCore(), { thumbnailHeight: 100 }) // eslint-disable-line no-new
41 expect(plugin3.opts.thumbnailHeight).toEqual(100)
42 })
43
44 describe('install', () => {
45 it('should subscribe to uppy file-added event', () => {
46 const core = Object.assign(new MockCore(), {
47 on: jest.fn()
48 })
49
50 const plugin = new ThumbnailGeneratorPlugin(core)
51 plugin.addToQueue = jest.fn()
52 plugin.install()
53
54 expect(core.on).toHaveBeenCalledTimes(3)
55 expect(core.on).toHaveBeenCalledWith('file-added', plugin.onFileAdded)
56 })
57 })
58
59 describe('uninstall', () => {
60 it('should unsubscribe from uppy file-added event', () => {
61 const core = Object.assign(new MockCore(), {
62 on: jest.fn(),
63 off: jest.fn()
64 })
65
66 const plugin = new ThumbnailGeneratorPlugin(core)
67 plugin.addToQueue = jest.fn()
68 plugin.install()
69
70 expect(core.on).toHaveBeenCalledTimes(3)
71
72 plugin.uninstall()
73
74 expect(core.off).toHaveBeenCalledTimes(3)
75 expect(core.off).toHaveBeenCalledWith('file-added', plugin.onFileAdded)
76 })
77 })
78
79 describe('queue', () => {
80 it('should add a new file to the queue and start processing the queue when queueProcessing is false', () => {
81 const core = new MockCore()
82 const plugin = new ThumbnailGeneratorPlugin(core)
83 plugin.processQueue = jest.fn()
84
85 const file = { id: 'bar', type: 'image/jpeg' }
86 plugin.queueProcessing = false
87 plugin.addToQueue(file.id)
88 expect(plugin.queue).toEqual(['bar'])
89 expect(plugin.processQueue).toHaveBeenCalledTimes(1)
90
91 const file2 = { id: 'bar2', type: 'image/jpeg' }
92 plugin.queueProcessing = true
93 plugin.addToQueue(file2.id)
94 expect(plugin.queue).toEqual(['bar', 'bar2'])
95 expect(plugin.processQueue).toHaveBeenCalledTimes(1)
96 })
97
98 it('should process items in the queue one by one', () => {
99 const core = new MockCore()
100 const plugin = new ThumbnailGeneratorPlugin(core)
101 plugin.requestThumbnail = jest.fn(() => delay(100))
102 plugin.install()
103
104 const file1 = { id: 'bar', type: 'image/jpeg' }
105 const file2 = { id: 'bar2', type: 'image/jpeg' }
106 const file3 = { id: 'bar3', type: 'image/jpeg' }
107 core.mockFile(file1.id, file1)
108 core.emit('file-added', file1)
109 core.mockFile(file2.id, file2)
110 core.emit('file-added', file2)
111 core.mockFile(file3.id, file3)
112 core.emit('file-added', file3)
113
114 expect(plugin.requestThumbnail).toHaveBeenCalledTimes(1)
115 expect(plugin.requestThumbnail).toHaveBeenCalledWith(file1)
116
117 return delay(110)
118 .then(() => {
119 expect(plugin.requestThumbnail).toHaveBeenCalledTimes(2)
120 expect(plugin.requestThumbnail).toHaveBeenCalledWith(file2)
121 return delay(110)
122 })
123 .then(() => {
124 expect(plugin.requestThumbnail).toHaveBeenCalledTimes(3)
125 expect(plugin.requestThumbnail).toHaveBeenCalledWith(file3)
126 return delay(110)
127 })
128 .then(() => {
129 expect(plugin.queue).toEqual([])
130 expect(plugin.queueProcessing).toEqual(false)
131 })
132 })
133
134 it('should revoke object URLs when files are removed', async () => {
135 const core = new MockCore()
136 const plugin = new ThumbnailGeneratorPlugin(core)
137 plugin.install()
138
139 URL.revokeObjectURL = jest.fn(() => null)
140
141 try {
142 plugin.createThumbnail = jest.fn(async () => {
143 await delay(50)
144 return 'blob:http://uppy.io/fake-thumbnail'
145 })
146 plugin.setPreviewURL = jest.fn((id, preview) => {
147 if (id === 1) file1.preview = preview
148 if (id === 2) file2.preview = preview
149 })
150
151 const file1 = { id: 1, name: 'bar.jpg', type: 'image/jpeg' }
152 const file2 = { id: 2, name: 'bar2.jpg', type: 'image/jpeg' }
153 core.mockFile(file1.id, file1)
154 core.emit('file-added', file1)
155 core.mockFile(file2.id, file2)
156 core.emit('file-added', file2)
157 expect(plugin.queue).toHaveLength(1)
158 // should drop it from the queue
159 core.emit('file-removed', file2)
160 expect(plugin.queue).toHaveLength(0)
161
162 expect(plugin.createThumbnail).toHaveBeenCalledTimes(1)
163 expect(URL.revokeObjectURL).not.toHaveBeenCalled()
164
165 await delay(110)
166
167 core.emit('file-removed', file1)
168 expect(URL.revokeObjectURL).toHaveBeenCalledTimes(1)
169 } finally {
170 delete URL.revokeObjectURL
171 }
172 })
173 })
174
175 describe('events', () => {
176 const core = new MockCore()
177 const plugin = new ThumbnailGeneratorPlugin(core)
178 plugin.createThumbnail = jest.fn((file) => delay(100).then(() => `blob:${file.id}.png`))
179 plugin.setPreviewURL = jest.fn()
180 plugin.install()
181
182 function add (file) {
183 core.mockFile(file.id, file)
184 core.emit('file-added', file)
185 }
186
187 it('should emit thumbnail:generated when a thumbnail was generated', () => new Promise((resolve, reject) => {
188 const expected = ['bar', 'bar2', 'bar3']
189 core.on('thumbnail:generated', (file, preview) => {
190 try {
191 expect(file.id).toBe(expected.shift())
192 expect(preview).toBe(`blob:${file.id}.png`)
193 } catch (err) {
194 return reject(err)
195 }
196 if (expected.length === 0) resolve()
197 })
198 add({ id: 'bar', type: 'image/png' })
199 add({ id: 'bar2', type: 'image/png' })
200 add({ id: 'bar3', type: 'image/png' })
201 }))
202
203 it('should emit thumbnail:all-generated when all thumbnails were generated', () => {
204 return new Promise((resolve) => {
205 core.on('thumbnail:all-generated', resolve)
206 add({ id: 'bar4', type: 'image/png' })
207 add({ id: 'bar5', type: 'image/png' })
208 }).then(() => {
209 expect(plugin.queue).toHaveLength(0)
210 })
211 })
212 })
213
214 describe('requestThumbnail', () => {
215 it('should call createThumbnail if it is a supported filetype', () => {
216 const core = new MockCore()
217 const plugin = new ThumbnailGeneratorPlugin(core)
218
219 plugin.createThumbnail = jest
220 .fn()
221 .mockReturnValue(Promise.resolve('preview'))
222 plugin.setPreviewURL = jest.fn()
223
224 const file = { id: 'file1', type: 'image/png', isRemote: false }
225 return plugin.requestThumbnail(file).then(() => {
226 expect(plugin.createThumbnail).toHaveBeenCalledTimes(1)
227 expect(plugin.createThumbnail).toHaveBeenCalledWith(
228 file,
229 plugin.opts.thumbnailWidth,
230 plugin.opts.thumbnailHeight
231 )
232 })
233 })
234
235 it('should not call createThumbnail if it is not a supported filetype', () => {
236 const core = new MockCore()
237 const plugin = new ThumbnailGeneratorPlugin(core)
238
239 plugin.createThumbnail = jest
240 .fn()
241 .mockReturnValue(Promise.resolve('preview'))
242 plugin.setPreviewURL = jest.fn()
243
244 const file = { id: 'file1', type: 'text/html', isRemote: false }
245 return plugin.requestThumbnail(file).then(() => {
246 expect(plugin.createThumbnail).toHaveBeenCalledTimes(0)
247 })
248 })
249
250 it('should not call createThumbnail if the file is remote', () => {
251 const core = new MockCore()
252 const plugin = new ThumbnailGeneratorPlugin(core)
253
254 plugin.createThumbnail = jest
255 .fn()
256 .mockReturnValue(Promise.resolve('preview'))
257 plugin.setPreviewURL = jest.fn()
258
259 const file = { id: 'file1', type: 'image/png', isRemote: true }
260 return plugin.requestThumbnail(file).then(() => {
261 expect(plugin.createThumbnail).toHaveBeenCalledTimes(0)
262 })
263 })
264
265 it('should call setPreviewURL with the thumbnail image', () => {
266 const core = new MockCore()
267 const plugin = new ThumbnailGeneratorPlugin(core)
268
269 plugin.createThumbnail = jest
270 .fn()
271 .mockReturnValue(Promise.resolve('preview'))
272 plugin.setPreviewURL = jest.fn()
273
274 const file = { id: 'file1', type: 'image/png', isRemote: false }
275 return plugin.requestThumbnail(file).then(() => {
276 expect(plugin.setPreviewURL).toHaveBeenCalledTimes(1)
277 expect(plugin.setPreviewURL).toHaveBeenCalledWith('file1', 'preview')
278 })
279 })
280 })
281
282 describe('setPreviewURL', () => {
283 it('should update the preview url for the specified image', () => {
284 const core = {
285 state: {
286 files: {
287 file1: {
288 preview: 'foo'
289 },
290 file2: {
291 preview: 'boo'
292 }
293 }
294 },
295 setFileState: jest.fn(),
296 plugins: {}
297 }
298 core.state = {
299 plugins: {}
300 }
301 core.setState = () => null
302 core.getState = () => core.state
303
304 const plugin = new ThumbnailGeneratorPlugin(core)
305 plugin.setPreviewURL('file1', 'moo')
306 expect(core.setFileState).toHaveBeenCalledTimes(1)
307 expect(core.setFileState).toHaveBeenCalledWith('file1', {
308 preview: 'moo'
309 })
310 })
311 })
312
313 describe('getProportionalDimensions', () => {
314 function resize (thumbnailPlugin, image, width, height) {
315 return thumbnailPlugin.getProportionalDimensions(image, width, height)
316 }
317
318 it('should calculate the thumbnail dimensions based on the width whilst keeping aspect ratio', () => {
319 const core = new MockCore()
320 const plugin = new ThumbnailGeneratorPlugin(core)
321 expect(resize(plugin, { width: 200, height: 100 }, 50)).toEqual({ width: 50, height: 25 })
322 expect(resize(plugin, { width: 66, height: 66 }, 33)).toEqual({ width: 33, height: 33 })
323 expect(resize(plugin, { width: 201.2, height: 198.2 }, 47)).toEqual({ width: 47, height: 46 })
324 })
325
326 it('should calculate the thumbnail dimensions based on the height whilst keeping aspect ratio', () => {
327 const core = new MockCore()
328 const plugin = new ThumbnailGeneratorPlugin(core)
329 expect(resize(plugin, { width: 200, height: 100 }, null, 50)).toEqual({ width: 100, height: 50 })
330 expect(resize(plugin, { width: 66, height: 66 }, null, 33)).toEqual({ width: 33, height: 33 })
331 expect(resize(plugin, { width: 201.2, height: 198.2 }, null, 47)).toEqual({ width: 48, height: 47 })
332 })
333
334 it('should calculate the thumbnail dimensions based on the default width if no custom width is given', () => {
335 const core = new MockCore()
336 const plugin = new ThumbnailGeneratorPlugin(core)
337 plugin.defaultThumbnailDimension = 50
338 expect(resize(plugin, { width: 200, height: 100 })).toEqual({ width: 50, height: 25 })
339 })
340
341 it('should calculate the thumbnail dimensions based on the width if both width and height are given', () => {
342 const core = new MockCore()
343 const plugin = new ThumbnailGeneratorPlugin(core)
344 expect(resize(plugin, { width: 200, height: 100 }, 50, 42)).toEqual({ width: 50, height: 25 })
345 })
346 })
347
348 describe('canvasToBlob', () => {
349 it('should use canvas.toBlob if available', () => {
350 const core = new MockCore()
351 const plugin = new ThumbnailGeneratorPlugin(core)
352 const canvas = {
353 toBlob: jest.fn()
354 }
355 plugin.canvasToBlob(canvas, 'type', 90)
356 expect(canvas.toBlob).toHaveBeenCalledTimes(1)
357 expect(canvas.toBlob.mock.calls[0][1]).toEqual('type')
358 expect(canvas.toBlob.mock.calls[0][2]).toEqual(90)
359 })
360 })
361
362 describe('downScaleInSteps', () => {
363 let originalDocumentCreateElement
364 let originalURLCreateObjectURL
365
366 beforeEach(() => {
367 originalDocumentCreateElement = document.createElement
368 originalURLCreateObjectURL = URL.createObjectURL
369 })
370
371 afterEach(() => {
372 document.createElement = originalDocumentCreateElement
373 URL.createObjectURL = originalURLCreateObjectURL
374 })
375
376 xit('should scale down the image by the specified number of steps', () => {
377 const core = new MockCore()
378 const plugin = new ThumbnailGeneratorPlugin(core)
379 const image = {
380 width: 1000,
381 height: 800
382 }
383 const context = {
384 drawImage: jest.fn()
385 }
386 const canvas = {
387 width: 0,
388 height: 0,
389 getContext: jest.fn().mockReturnValue(context)
390 }
391 document.createElement = jest.fn().mockReturnValue(canvas)
392 const result = plugin.downScaleInSteps(image, 3)
393 const newImage = {
394 getContext: canvas.getContext,
395 height: 100,
396 width: 125
397 }
398 expect(result).toEqual({
399 image: newImage,
400 sourceWidth: 125,
401 sourceHeight: 100
402 })
403 expect(context.drawImage).toHaveBeenCalledTimes(3)
404 expect(context.drawImage.mock.calls).toEqual([
405 [{ width: 1000, height: 800 }, 0, 0, 1000, 800, 0, 0, 500, 400],
406 [
407 { width: 125, height: 100, getContext: canvas.getContext },
408 0,
409 0,
410 500,
411 400,
412 0,
413 0,
414 250,
415 200
416 ],
417 [
418 { width: 125, height: 100, getContext: canvas.getContext },
419 0,
420 0,
421 250,
422 200,
423 0,
424 0,
425 125,
426 100
427 ]
428 ])
429 })
430 })
431
432 describe('resizeImage', () => {
433 it('should return a canvas with the resized image on it', () => {
434 const core = new MockCore()
435 const plugin = new ThumbnailGeneratorPlugin(core)
436 const image = {
437 width: 1000,
438 height: 800
439 }
440 const context = {
441 drawImage: jest.fn()
442 }
443 const canvas = {
444 width: 0,
445 height: 0,
446 getContext: jest.fn().mockReturnValue(context)
447 }
448 document.createElement = jest.fn().mockReturnValue(canvas)
449
450 const result = plugin.resizeImage(image, 200, 160)
451 expect(result).toEqual({
452 width: 200,
453 height: 160,
454 getContext: canvas.getContext
455 })
456 })
457
458 it('should upsize if original image is smaller than target size', () => {
459 const core = new MockCore()
460 const plugin = new ThumbnailGeneratorPlugin(core)
461 const image = {
462 width: 100,
463 height: 80
464 }
465 const context = {
466 drawImage: jest.fn()
467 }
468 const canvas = {
469 width: 0,
470 height: 0,
471 getContext: jest.fn().mockReturnValue(context)
472 }
473 document.createElement = jest.fn().mockReturnValue(canvas)
474
475 const result = plugin.resizeImage(image, 200, 160)
476 expect(result).toEqual({
477 width: 200,
478 height: 160,
479 getContext: canvas.getContext
480 })
481 })
482 })
483
484 describe('onRestored', () => {
485 it('should enqueue restored files', () => {
486 const files = {
487 a: { id: 'a', type: 'image/jpeg', preview: 'blob:abc', isRestored: true },
488 b: { id: 'b', type: 'image/jpeg', preview: 'blob:def' },
489 c: { id: 'c', type: 'image/jpeg', preview: 'blob:xyz', isRestored: true }
490 }
491 const core = Object.assign(new MockCore(), {
492 getState () {
493 return { files, plugins: {} }
494 },
495 getFile (id) {
496 return files[id]
497 }
498 })
499
500 const plugin = new ThumbnailGeneratorPlugin(core)
501 plugin.addToQueue = jest.fn()
502 plugin.install()
503
504 core.emit('restored')
505
506 expect(plugin.addToQueue).toHaveBeenCalledTimes(2)
507 expect(plugin.addToQueue).toHaveBeenCalledWith(files.a.id)
508 expect(plugin.addToQueue).toHaveBeenCalledWith(files.c.id)
509 })
510
511 it('should not regenerate thumbnail for remote files', () => {
512 const files = {
513 a: { preview: 'http://abc', isRestored: true }
514 }
515 const core = Object.assign(new MockCore(), {
516 getState () {
517 return { files, plugins: {} }
518 },
519 getFile (id) {
520 return files[id]
521 }
522 })
523
524 const plugin = new ThumbnailGeneratorPlugin(core)
525 plugin.addToQueue = jest.fn()
526 plugin.install()
527
528 core.emit('restored')
529
530 expect(plugin.addToQueue).not.toHaveBeenCalled()
531 })
532 })
533})