UNPKG

@mozaic-ds/vue

Version:

Mozaic-Vue is the Vue.js implementation of ADEO Design system

305 lines (260 loc) 9.12 kB
import { describe, it, expect, vi } from 'vitest'; import { mount } from '@vue/test-utils'; import MFileUploader, { type FilesValidationState, type FileUploaderProps, } from './MFileUploader.vue'; import { nextTick } from 'vue'; type WrapperVm = { dragCounter: number; }; const globalStubs = { MFileUploaderItem: { template: '<div class="file-item"><slot name="action" /></div>', name: 'MFileUploaderItem', props: ['file', 'valid'], emits: ['delete'], }, Upload24: true, }; const createFile = (name: string, size: number, type: string) => { const file = new File(['a'.repeat(size)], name, { type }); Object.defineProperty(file, 'size', { value: size }); return file; }; const mountUploader = (props = {} as FileUploaderProps) => mount(MFileUploader, { props, global: { stubs: globalStubs } }); const triggerFileInputChange = async ( wrapper: ReturnType<typeof mount>, files: File[], ) => { const input = wrapper.find('input[type="file"]'); Object.defineProperty(input.element, 'files', { value: files, configurable: true, }); await input.trigger('change'); }; const triggerDrop = async ( wrapper: ReturnType<typeof mount>, files: File[], ) => { const dropZone = wrapper.find('.mc-file-uploader__input'); await dropZone.trigger('drop', { dataTransfer: { files, items: files.map((f) => ({ kind: 'file', type: f.type, getAsFile: () => f, })), }, preventDefault: vi.fn(), stopPropagation: vi.fn(), }); }; describe('MFileUploader.vue', () => { it('allows file upload via input', async () => { const wrapper = mountUploader({ modelValue: [], }); const file = createFile('test.png', 1024, 'image/png'); await triggerFileInputChange(wrapper, [file]); expect(wrapper.emitted('update:modelValue')).toBeTruthy(); }); it('clears input and returns if no new valid files', async () => { const existingFile = createFile('exist.png', 100, 'image/png'); const wrapper = mountUploader({ modelValue: [existingFile], }); const input = wrapper.find('input[type="file"]'); Object.defineProperty(input.element, 'files', { value: [], configurable: true, }); await input.trigger('change'); expect((input.element as HTMLInputElement).value).toBe(''); expect(wrapper.emitted('update:modelValue')).toBeFalsy(); }); it.each([ ['Enter', 'Enter'], ['Space', ' '], ])('clicks file input when %s is pressed', async (_, key) => { const wrapper = mountUploader({ modelValue: [], }); const inputEl = wrapper.find('input[type="file"]') .element as HTMLInputElement; const clickSpy = vi.spyOn(inputEl, 'click'); const label = wrapper.find('.mc-file-uploader__input'); await label.trigger('keydown', { key }); expect(clickSpy).toHaveBeenCalled(); }); it('allows drag and drop', async () => { const wrapper = mountUploader({ modelValue: [], hasDragDrop: true, }); const file = createFile('dragged.pdf', 500, 'application/pdf'); await triggerDrop(wrapper, [file]); expect(wrapper.emitted('update:modelValue')).toBeTruthy(); }); it('handles dragenter/dragleave class correctly', async () => { const wrapper = mountUploader({ modelValue: [], hasDragDrop: true, }); const labelEl = wrapper.get('.mc-file-uploader__input'); await labelEl.trigger('dragenter'); expect(labelEl.classes()).toContain('mc-file-uploader__input--dragged'); await labelEl.trigger('dragleave'); expect(labelEl.classes()).not.toContain('mc-file-uploader__input--dragged'); }); it('does not update on drop if disabled or hasDragDrop is false', async () => { const wrapperFalse = mountUploader({ modelValue: [], hasDragDrop: false, }); await triggerDrop(wrapperFalse, [ createFile('a.pdf', 100, 'application/pdf'), ]); expect(wrapperFalse.emitted('update:modelValue')).toBeFalsy(); const wrapperDisabled = mountUploader({ modelValue: [], hasDragDrop: true, disabled: true, }); await triggerDrop(wrapperDisabled, [ createFile('b.pdf', 100, 'application/pdf'), ]); expect(wrapperDisabled.emitted('update:modelValue')).toBeFalsy(); }); it('does not update dragCounter on dragEnter if hasDragDrop is false', async () => { const buttonWrapper = mountUploader({ modelValue: [], hasDragDrop: false, }); const vm = buttonWrapper.vm as unknown as WrapperVm; vm.dragCounter = 1; const input = buttonWrapper.find('.mc-file-uploader__input'); await input.trigger('dragenter'); expect(vm.dragCounter).toBe(1); }); it('does not update dragCounter on dragLeave if hasDragDrop is false', async () => { const buttonWrapper = mountUploader({ modelValue: [], hasDragDrop: false, }); const vm = buttonWrapper.vm as unknown as WrapperVm; vm.dragCounter = 1; const input = buttonWrapper.find('.mc-file-uploader__input'); await input.trigger('dragleave'); expect(vm.dragCounter).toBe(1); }); it('adds new files on change when multiple = true', async () => { const file1 = createFile('a.txt', 1, 'text/plain'); const file2 = createFile('b.txt', 1, 'text/plain'); const wrapper = mountUploader({ modelValue: [file1], multiple: true, }); const input = wrapper.find('input[type="file"]'); Object.defineProperty(input.element, 'files', { value: [file2], configurable: true, }); await input.trigger('change'); const updates = wrapper.emitted('update:modelValue')![0][0] as File[]; expect(updates.map((f) => f.name)).toEqual(['a.txt', 'b.txt']); expect((input.element as HTMLInputElement).value).toBe(''); }); it('replaces existing file on change when multiple = false', async () => { const file1 = createFile('a.txt', 1, 'text/plain'); const file2 = createFile('b.txt', 1, 'text/plain'); const wrapper = mountUploader({ modelValue: [file1], multiple: false, }); const input = wrapper.find('input[type="file"]'); Object.defineProperty(input.element, 'files', { value: [file2], configurable: true, }); await input.trigger('change'); const updates = wrapper.emitted('update:modelValue')![0][0] as File[]; expect(updates.map((f) => f.name)).toEqual(['b.txt']); expect((input.element as HTMLInputElement).value).toBe(''); }); it('merges or replaces files on drop based on multiple prop', async () => { const file1 = createFile('a.txt', 1, 'text/plain'); const file2 = createFile('b.txt', 1, 'text/plain'); // multiple = true const wrapperMulti = mountUploader({ modelValue: [file1], multiple: true, hasDragDrop: true, }); await triggerDrop(wrapperMulti, [file2]); const payloadMulti = wrapperMulti.emitted( 'update:modelValue', )![0][0] as File[]; expect(payloadMulti.map((f) => f.name)).toEqual(['a.txt', 'b.txt']); // multiple = false const wrapperSingle = mountUploader({ modelValue: [file1], multiple: false, }); await triggerFileInputChange(wrapperSingle, [file2]); const payloadSingle = wrapperSingle.emitted( 'update:modelValue', )![0][0] as File[]; expect(payloadSingle.map((f) => f.name)).toEqual(['b.txt']); }); describe('Validation', () => { it('validates size, extension and custom rules', async () => { const valid = createFile('good.png', 1000, 'image/png'); const heavy = createFile('heavy.png', 5000, 'image/png'); const wrong = createFile('wrong.txt', 1000, 'text/plain'); const customRule = vi.fn().mockReturnValue(false); const wrapper = mountUploader({ modelValue: [ valid, heavy, wrong, createFile('test.png', 100, 'image/png'), ], maxSize: 2000, allowedExtensions: ['png', 'jpg'], rules: [customRule], }); const emitted = wrapper.emitted( 'validation', )![0][0] as FilesValidationState; expect(emitted['good.png'].size).toBe(true); expect(emitted['heavy.png'].size).toBe(false); expect(emitted['test.png'].customValidation).toBe(false); }); }); it('handles file deletion', async () => { const f1 = createFile('1.png', 100, 'image/png'); const f2 = createFile('2.png', 100, 'image/png'); const wrapper = mountUploader({ modelValue: [f1, f2], showFilesList: true, }); const items = wrapper.findAllComponents({ name: 'MFileUploaderItem' }); await items[0].vm.$emit('delete'); await nextTick(); const payload = wrapper.emitted('update:modelValue')![0][0] as File[]; expect(payload.map((f) => f.name)).toEqual(['2.png']); }); it('disables input when disabled=true', () => { const wrapper = mountUploader({ disabled: true, modelValue: [], }); expect( wrapper.find('input[type="file"]').attributes('disabled'), ).toBeDefined(); }); });