1 | const ThumbnailGeneratorPlugin = require('./index')
|
2 | const { Plugin } = require('@uppy/core')
|
3 | const emitter = require('namespace-emitter')
|
4 |
|
5 | const delay = duration => new Promise(resolve => setTimeout(resolve, duration))
|
6 |
|
7 | function 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 |
|
26 | describe('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())
|
34 | expect(plugin1.opts.thumbnailWidth).toEqual(null)
|
35 | expect(plugin1.opts.thumbnailHeight).toEqual(null)
|
36 |
|
37 | const plugin2 = new ThumbnailGeneratorPlugin(new MockCore(), { thumbnailWidth: 100 })
|
38 | expect(plugin2.opts.thumbnailWidth).toEqual(100)
|
39 |
|
40 | const plugin3 = new ThumbnailGeneratorPlugin(new MockCore(), { thumbnailHeight: 100 })
|
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 |
|
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 | })
|