import { GlIcon, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
describe('Upload dropzone component', () => {
let wrapper;
const mockDragEvent = ({ types = ['Files'], files = [] }) => {
return { dataTransfer: { types, files } };
};
const findDropzoneCard = () => wrapper.find('.upload-dropzone-card');
const findDropzoneArea = () => wrapper.find('[data-testid="dropzone-area"]');
const findIcon = () => wrapper.findComponent(GlIcon);
const findUploadText = () => wrapper.find('[data-testid="upload-text"]').text();
const findFileInput = () => wrapper.find('input[type="file"]');
function createComponent({ slots = {}, data = {}, props = {} } = {}) {
wrapper = shallowMount(UploadDropzone, {
slots,
propsData: {
displayAsCard: true,
...props,
},
stubs: {
GlSprintf,
},
data() {
return data;
},
});
}
describe('when slot provided', () => {
it('renders dropzone with slot content', () => {
createComponent({
slots: {
default: ['
dropzone slot
'],
},
});
expect(wrapper.element).toMatchSnapshot();
});
});
describe('when no slot provided', () => {
it('renders default dropzone card', () => {
createComponent();
expect(wrapper.element).toMatchSnapshot();
});
it('triggers click event on file input element when clicked', () => {
createComponent();
const clickSpy = jest.spyOn(wrapper.find('input').element, 'click');
findDropzoneCard().trigger('click');
expect(clickSpy).toHaveBeenCalled();
});
});
describe('upload text', () => {
it.each`
collection | description | props | expected
${'multiple'} | ${'by default'} | ${null} | ${'files to attach'}
${'singular'} | ${'when singleFileSelection'} | ${{ singleFileSelection: true }} | ${'file to attach'}
`('displays $collection version $description', ({ props, expected }) => {
createComponent({ props });
expect(findUploadText()).toContain(expected);
});
});
describe('when dragging', () => {
it.each`
description | eventPayload
${'is empty'} | ${{}}
${'contains text'} | ${mockDragEvent({ types: ['text'] })}
${'contains files and text'} | ${mockDragEvent({ types: ['Files', 'text'] })}
${'contains files'} | ${mockDragEvent({ types: ['Files'] })}
`('renders correct template when drag event $description', async ({ eventPayload }) => {
createComponent();
wrapper.trigger('dragenter', eventPayload);
await nextTick();
expect(wrapper.element).toMatchSnapshot();
});
it('renders correct template when dragging stops', async () => {
createComponent();
wrapper.trigger('dragenter');
await nextTick();
wrapper.trigger('dragleave');
await nextTick();
expect(wrapper.element).toMatchSnapshot();
});
});
describe('when dropping', () => {
it('emits upload event', async () => {
createComponent();
const mockFile = { name: 'test', type: 'image/jpg' };
const mockEvent = mockDragEvent({ files: [mockFile] });
wrapper.trigger('dragenter', mockEvent);
await nextTick();
wrapper.trigger('drop', mockEvent);
await nextTick();
expect(wrapper.emitted().change[0]).toEqual([[mockFile]]);
});
});
describe('ondrop', () => {
const mockData = { dragCounter: 1, isDragDataValid: true };
describe('when drag data is valid', () => {
it('emits upload event for valid files', () => {
createComponent({ data: mockData });
const mockFile = { type: 'image/jpg' };
const mockEvent = mockDragEvent({ files: [mockFile] });
wrapper.vm.ondrop(mockEvent);
expect(wrapper.emitted().change[0]).toEqual([[mockFile]]);
});
it('emits error event when files are invalid', () => {
createComponent({ data: mockData });
const mockEvent = mockDragEvent({ files: [{ type: 'audio/midi' }] });
wrapper.vm.ondrop(mockEvent);
expect(wrapper.emitted()).toHaveProperty('error');
});
it('allows validation function to be overwritten', () => {
createComponent({ data: mockData, props: { isFileValid: () => true } });
const mockEvent = mockDragEvent({ files: [{ type: 'audio/midi' }] });
wrapper.vm.ondrop(mockEvent);
expect(wrapper.emitted()).not.toHaveProperty('error');
});
describe('singleFileSelection = true', () => {
it('emits a single file on drop', () => {
createComponent({
data: mockData,
props: { singleFileSelection: true },
});
const mockFile = { type: 'image/jpg' };
const mockEvent = mockDragEvent({ files: [mockFile] });
wrapper.vm.ondrop(mockEvent);
expect(wrapper.emitted().change[0]).toEqual([mockFile]);
});
});
});
});
it('applies correct classes when displaying as a standalone item', () => {
createComponent({ props: { displayAsCard: false } });
expect(findDropzoneArea().classes()).not.toContain('gl-flex-direction-column');
expect(findIcon().classes()).toEqual(['gl-mr-3', 'gl-text-gray-500']);
expect(findIcon().props('size')).toBe(16);
});
it('applies correct classes when displaying in card mode', () => {
createComponent({ props: { displayAsCard: true } });
expect(findDropzoneArea().classes()).toContain('gl-flex-direction-column');
expect(findIcon().classes()).toEqual(['gl-mb-2']);
expect(findIcon().props('size')).toBe(24);
});
it('correctly overrides description and drop messages', () => {
createComponent({
props: {
dropToStartMessage: 'Test drop-to-start message.',
validFileMimetypes: ['image/jpg', 'image/jpeg'],
},
slots: {
'upload-text': 'Test %{linkStart}description%{linkEnd} message.',
},
});
expect(wrapper.element).toMatchSnapshot();
});
describe('file input form name', () => {
it('applies inputFieldName as file input name', () => {
createComponent({ props: { inputFieldName: 'test_field_name' } });
expect(findFileInput().attributes('name')).toBe('test_field_name');
});
it('uses default file input name if no inputFieldName provided', () => {
createComponent();
expect(findFileInput().attributes('name')).toBe('upload_file');
});
});
describe('updates file input files value', () => {
// NOTE: the component assigns dropped files from the drop event to the
// input.files property. There's a restriction that nothing but a FileList
// can be assigned to this property. While FileList can't be created
// manually: it has no constructor. And currently there's no good workaround
// for jsdom. So we have to stub the file input in vm.$refs to ensure that
// the files property is updated. This enforces following tests to know a
// bit too much about the SUT internals See this thread for more details on
// FileList in jsdom: https://github.com/jsdom/jsdom/issues/1272
function stubFileInputOnWrapper() {
const fakeFileInput = { files: [] };
wrapper.vm.$refs.fileUpload = fakeFileInput;
}
it('assigns dragged files to the input files property', async () => {
const mockFile = { name: 'test', type: 'image/jpg' };
const mockEvent = mockDragEvent({ files: [mockFile] });
createComponent({ props: { shouldUpdateInputOnFileDrop: true } });
stubFileInputOnWrapper();
wrapper.trigger('dragenter', mockEvent);
await nextTick();
wrapper.trigger('drop', mockEvent);
await nextTick();
expect(wrapper.vm.$refs.fileUpload.files).toEqual([mockFile]);
});
it('throws an error when multiple files are dropped on a single file input dropzone', async () => {
const mockFile = { name: 'test', type: 'image/jpg' };
const mockEvent = mockDragEvent({ files: [mockFile, mockFile] });
createComponent({ props: { shouldUpdateInputOnFileDrop: true, singleFileSelection: true } });
stubFileInputOnWrapper();
wrapper.trigger('dragenter', mockEvent);
await nextTick();
wrapper.trigger('drop', mockEvent);
await nextTick();
expect(wrapper.vm.$refs.fileUpload.files).toEqual([]);
expect(wrapper.emitted('error')).toHaveLength(1);
});
});
});