UNPKG

@uppy/thumbnail-generator

Version:

Uppy plugin that generates small previews of images to show on your upload UI.

572 lines (503 loc) 17.4 kB
import { UIPlugin } from '@uppy/core' import emitter from 'namespace-emitter' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import ThumbnailGeneratorPlugin from './index.ts' const delay = (duration) => new Promise((resolve) => setTimeout(resolve, duration)) function MockCore() { const core = emitter() const files = {} core.state = { files, plugins: {}, } core.mockFile = (id, f) => { files[id] = f } core.getFile = (id) => files[id] core.log = (message, level = 'log') => { if (level === 'warn' || level === 'error') { console[level](message) } } core.getState = () => core.state core.setState = () => null return core } describe('uploader/ThumbnailGeneratorPlugin', () => { it('should initialise successfully', () => { const plugin = new ThumbnailGeneratorPlugin(new MockCore(), {}) expect(plugin instanceof UIPlugin).toEqual(true) }) it('should accept the thumbnailWidth and thumbnailHeight option and override the default', () => { const plugin1 = new ThumbnailGeneratorPlugin(new MockCore()) expect(plugin1.opts.thumbnailWidth).toEqual(null) expect(plugin1.opts.thumbnailHeight).toEqual(null) const plugin2 = new ThumbnailGeneratorPlugin(new MockCore(), { thumbnailWidth: 100, }) expect(plugin2.opts.thumbnailWidth).toEqual(100) const plugin3 = new ThumbnailGeneratorPlugin(new MockCore(), { thumbnailHeight: 100, }) expect(plugin3.opts.thumbnailHeight).toEqual(100) }) describe('install', () => { it('should subscribe to uppy file-added event', () => { const core = Object.assign(new MockCore(), { on: vi.fn(), }) const plugin = new ThumbnailGeneratorPlugin(core) plugin.addToQueue = vi.fn() plugin.install() expect(core.on).toHaveBeenCalledTimes(5) expect(core.on).toHaveBeenCalledWith('file-added', plugin.onFileAdded) }) }) describe('uninstall', () => { it('should unsubscribe from uppy file-added event', () => { const core = Object.assign(new MockCore(), { on: vi.fn(), off: vi.fn(), }) const plugin = new ThumbnailGeneratorPlugin(core) plugin.addToQueue = vi.fn() plugin.install() expect(core.on).toHaveBeenCalledTimes(5) plugin.uninstall() expect(core.off).toHaveBeenCalledTimes(5) expect(core.off).toHaveBeenCalledWith('file-added', plugin.onFileAdded) }) }) describe('queue', () => { it('should add a new file to the queue and start processing the queue when queueProcessing is false', () => { const core = new MockCore() const plugin = new ThumbnailGeneratorPlugin(core) plugin.processQueue = vi.fn() const file = { id: 'bar', type: 'image/jpeg' } plugin.queueProcessing = false plugin.addToQueue(file.id) expect(plugin.queue).toEqual(['bar']) expect(plugin.processQueue).toHaveBeenCalledTimes(1) const file2 = { id: 'bar2', type: 'image/jpeg' } plugin.queueProcessing = true plugin.addToQueue(file2.id) expect(plugin.queue).toEqual(['bar', 'bar2']) expect(plugin.processQueue).toHaveBeenCalledTimes(1) }) it('should process items in the queue one by one', () => { const core = new MockCore() const plugin = new ThumbnailGeneratorPlugin(core) plugin.requestThumbnail = vi.fn(() => delay(100)) plugin.install() const file1 = { id: 'bar', type: 'image/jpeg', data: new Blob() } const file2 = { id: 'bar2', type: 'image/jpeg', data: new Blob() } const file3 = { id: 'bar3', type: 'image/jpeg', data: new Blob() } core.mockFile(file1.id, file1) core.emit('file-added', file1) core.mockFile(file2.id, file2) core.emit('file-added', file2) core.mockFile(file3.id, file3) core.emit('file-added', file3) expect(plugin.requestThumbnail).toHaveBeenCalledTimes(1) expect(plugin.requestThumbnail).toHaveBeenCalledWith(file1) return delay(110) .then(() => { expect(plugin.requestThumbnail).toHaveBeenCalledTimes(2) expect(plugin.requestThumbnail).toHaveBeenCalledWith(file2) return delay(110) }) .then(() => { expect(plugin.requestThumbnail).toHaveBeenCalledTimes(3) expect(plugin.requestThumbnail).toHaveBeenCalledWith(file3) return delay(110) }) .then(() => { expect(plugin.queue).toEqual([]) expect(plugin.queueProcessing).toEqual(false) }) }) it('should revoke object URLs when files are removed', async () => { const core = new MockCore() const plugin = new ThumbnailGeneratorPlugin(core) plugin.install() URL.revokeObjectURL = vi.fn(() => null) try { const file1 = { id: 1, name: 'bar.jpg', type: 'image/jpeg', data: new Blob(), } const file2 = { id: 2, name: 'bar2.jpg', type: 'image/jpeg', data: new Blob(), } plugin.createThumbnail = vi.fn(async () => { await delay(50) return 'blob:http://uppy.io/fake-thumbnail' }) plugin.setPreviewURL = vi.fn((id, preview) => { if (id === 1) file1.preview = preview if (id === 2) file2.preview = preview }) core.mockFile(file1.id, file1) core.emit('file-added', file1) core.mockFile(file2.id, file2) core.emit('file-added', file2) expect(plugin.queue).toHaveLength(1) // should drop it from the queue core.emit('file-removed', file2) expect(plugin.queue).toHaveLength(0) expect(plugin.createThumbnail).toHaveBeenCalledTimes(1) expect(URL.revokeObjectURL).not.toHaveBeenCalled() await delay(110) core.emit('file-removed', file1) expect(URL.revokeObjectURL).toHaveBeenCalledTimes(1) } finally { delete URL.revokeObjectURL } }) }) describe('events', () => { const core = new MockCore() const plugin = new ThumbnailGeneratorPlugin(core) plugin.createThumbnail = vi.fn((file) => delay(100).then(() => `blob:${file.id}.png`), ) plugin.setPreviewURL = vi.fn() plugin.install() function add(file) { core.mockFile(file.id, file) core.emit('file-added', 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) { reject(err) return } if (expected.length === 0) resolve() }) add({ id: 'bar', type: 'image/png', data: new Blob() }) add({ id: 'bar2', type: 'image/png', data: new Blob() }) add({ id: 'bar3', type: 'image/png', data: new Blob() }) })) 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', data: new Blob() }) add({ id: 'bar5', type: 'image/png', data: new Blob() }) }).then(() => { expect(plugin.queue).toHaveLength(0) }) }) }) describe('requestThumbnail', () => { it('should call createThumbnail if it is a supported filetype', () => { const core = new MockCore() const plugin = new ThumbnailGeneratorPlugin(core) plugin.createThumbnail = vi .fn() .mockReturnValue(Promise.resolve('preview')) plugin.setPreviewURL = vi.fn() const file = { id: 'file1', type: 'image/png', isRemote: false } return plugin.requestThumbnail(file).then(() => { expect(plugin.createThumbnail).toHaveBeenCalledTimes(1) expect(plugin.createThumbnail).toHaveBeenCalledWith( file, plugin.opts.thumbnailWidth, plugin.opts.thumbnailHeight, ) }) }) it('should not call createThumbnail if it is not a supported filetype', () => { const core = new MockCore() const plugin = new ThumbnailGeneratorPlugin(core) plugin.createThumbnail = vi .fn() .mockReturnValue(Promise.resolve('preview')) plugin.setPreviewURL = vi.fn() const file = { id: 'file1', type: 'text/html', isRemote: false } return plugin.requestThumbnail(file).then(() => { expect(plugin.createThumbnail).toHaveBeenCalledTimes(0) }) }) it('should not call createThumbnail if the file is remote', () => { const core = new MockCore() const plugin = new ThumbnailGeneratorPlugin(core) plugin.createThumbnail = vi .fn() .mockReturnValue(Promise.resolve('preview')) plugin.setPreviewURL = vi.fn() const file = { id: 'file1', type: 'image/png', isRemote: true } return plugin.requestThumbnail(file).then(() => { expect(plugin.createThumbnail).toHaveBeenCalledTimes(0) }) }) it('should call setPreviewURL with the thumbnail image', () => { const core = new MockCore() const plugin = new ThumbnailGeneratorPlugin(core) plugin.createThumbnail = vi .fn() .mockReturnValue(Promise.resolve('preview')) plugin.setPreviewURL = vi.fn() const file = { id: 'file1', type: 'image/png', isRemote: false } return plugin.requestThumbnail(file).then(() => { expect(plugin.setPreviewURL).toHaveBeenCalledTimes(1) expect(plugin.setPreviewURL).toHaveBeenCalledWith('file1', 'preview') }) }) }) describe('setPreviewURL', () => { it('should update the preview url for the specified image', () => { const core = { state: { files: { file1: { preview: 'foo', }, file2: { preview: 'boo', }, }, }, setFileState: vi.fn(), plugins: {}, } core.state = { plugins: {}, } core.setState = () => null core.getState = () => core.state const plugin = new ThumbnailGeneratorPlugin(core) plugin.setPreviewURL('file1', 'moo') expect(core.setFileState).toHaveBeenCalledTimes(1) expect(core.setFileState).toHaveBeenCalledWith('file1', { preview: 'moo', }) }) }) describe('getProportionalDimensions', () => { function resize(thumbnailPlugin, image, width, height) { return thumbnailPlugin.getProportionalDimensions(image, width, height) } it('should calculate the thumbnail dimensions based on the width whilst keeping aspect ratio', () => { const core = new MockCore() const plugin = new ThumbnailGeneratorPlugin(core) expect(resize(plugin, { width: 200, height: 100 }, 50)).toEqual({ width: 50, height: 25, }) expect(resize(plugin, { width: 66, height: 66 }, 33)).toEqual({ width: 33, height: 33, }) expect(resize(plugin, { width: 201.2, height: 198.2 }, 47)).toEqual({ width: 47, height: 46, }) }) it('should calculate the thumbnail dimensions based on the height whilst keeping aspect ratio', () => { const core = new MockCore() const plugin = new ThumbnailGeneratorPlugin(core) expect(resize(plugin, { width: 200, height: 100 }, null, 50)).toEqual({ width: 100, height: 50, }) expect(resize(plugin, { width: 66, height: 66 }, null, 33)).toEqual({ width: 33, height: 33, }) expect(resize(plugin, { width: 201.2, height: 198.2 }, null, 47)).toEqual( { width: 48, height: 47 }, ) }) it('should calculate the thumbnail dimensions based on the default width if no custom width is given', () => { const core = new MockCore() const plugin = new ThumbnailGeneratorPlugin(core) plugin.defaultThumbnailDimension = 50 expect(resize(plugin, { width: 200, height: 100 })).toEqual({ width: 50, height: 25, }) }) it('should calculate the thumbnail dimensions based on the width if both width and height are given', () => { const core = new MockCore() const plugin = new ThumbnailGeneratorPlugin(core) expect(resize(plugin, { width: 200, height: 100 }, 50, 42)).toEqual({ width: 50, height: 25, }) }) }) describe('downScaleInSteps', () => { let originalDocumentCreateElement let originalURLCreateObjectURL beforeEach(() => { originalDocumentCreateElement = document.createElement originalURLCreateObjectURL = URL.createObjectURL }) afterEach(() => { document.createElement = originalDocumentCreateElement URL.createObjectURL = originalURLCreateObjectURL }) it.skip('should scale down the image by the specified number of steps', () => { const core = new MockCore() const plugin = new ThumbnailGeneratorPlugin(core) const image = { width: 1000, height: 800, } const context = { drawImage: vi.fn(), } const canvas = { width: 0, height: 0, getContext: vi.fn().mockReturnValue(context), } document.createElement = vi.fn().mockReturnValue(canvas) const result = plugin.downScaleInSteps(image, 3) const newImage = { getContext: canvas.getContext, height: 100, width: 125, } expect(result).toEqual({ image: newImage, sourceWidth: 125, sourceHeight: 100, }) expect(context.drawImage).toHaveBeenCalledTimes(3) expect(context.drawImage.mock.calls).toEqual([ [{ width: 1000, height: 800 }, 0, 0, 1000, 800, 0, 0, 500, 400], [ { width: 125, height: 100, getContext: canvas.getContext }, 0, 0, 500, 400, 0, 0, 250, 200, ], [ { width: 125, height: 100, getContext: canvas.getContext }, 0, 0, 250, 200, 0, 0, 125, 100, ], ]) }) }) describe('resizeImage', () => { it('should return a canvas with the resized image on it', () => { const core = new MockCore() const plugin = new ThumbnailGeneratorPlugin(core) const image = { width: 1000, height: 800, } const context = { drawImage: vi.fn(), } const canvas = { width: 0, height: 0, getContext: vi.fn().mockReturnValue(context), } document.createElement = vi.fn().mockReturnValue(canvas) const result = plugin.resizeImage(image, 200, 160) expect(result).toEqual({ width: 200, height: 160, getContext: canvas.getContext, }) }) it('should upsize if original image is smaller than target size', () => { const core = new MockCore() const plugin = new ThumbnailGeneratorPlugin(core) const image = { width: 100, height: 80, } const context = { drawImage: vi.fn(), } const canvas = { width: 0, height: 0, getContext: vi.fn().mockReturnValue(context), } document.createElement = vi.fn().mockReturnValue(canvas) const result = plugin.resizeImage(image, 200, 160) expect(result).toEqual({ width: 200, height: 160, getContext: canvas.getContext, }) }) }) describe('onRestored', () => { it('should enqueue restored files', () => { const files = { a: { id: 'a', type: 'image/jpeg', isRestored: true, data: new Blob() }, b: { id: 'b', type: 'image/jpeg', data: new Blob() }, c: { id: 'c', type: 'image/jpeg', isRestored: true, data: new Blob() }, } const core = Object.assign(new MockCore(), { getState() { return { files, plugins: {} } }, getFile(id) { return files[id] }, getFiles() { return Object.values(files) }, }) const plugin = new ThumbnailGeneratorPlugin(core) plugin.addToQueue = vi.fn() plugin.install() core.emit('restored') expect(plugin.addToQueue).toHaveBeenCalledTimes(2) expect(plugin.addToQueue).toHaveBeenCalledWith(files.a.id) expect(plugin.addToQueue).toHaveBeenCalledWith(files.c.id) }) it('should not regenerate thumbnail for remote files', () => { const files = { a: { preview: 'http://abc', isRestored: true }, } const core = Object.assign(new MockCore(), { getState() { return { files, plugins: {} } }, getFile(id) { return files[id] }, getFiles() { return Object.values(files) }, }) const plugin = new ThumbnailGeneratorPlugin(core) plugin.addToQueue = vi.fn() plugin.install() core.emit('restored') expect(plugin.addToQueue).not.toHaveBeenCalled() }) }) })