@mozaic-ds/vue
Version:
Mozaic-Vue is the Vue.js implementation of ADEO Design system
305 lines (260 loc) • 9.12 kB
text/typescript
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();
});
});