import React from 'react'; import { act } from 'react-dom/test-utils'; import { mount } from 'enzyme'; import Upload from '..'; import UploadList from '../UploadList'; import Form from '../../form'; import { errorRequest, successRequest } from './requests'; import { setup, teardown } from './mock'; import { sleep } from '../../../tests/utils'; const fileList = [ { uid: '-1', name: 'xxx.png', status: 'done', url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png', thumbUrl: 'https://zos.alipayobjects.com/rmsportal/IQKRngzUuFzJzGzRJXUs.png', }, { uid: '-2', name: 'yyy.png', status: 'done', url: 'https://zos.alipayobjects.com/rmsportal/IQKRngzUuFzJzGzRJXUs.png', thumbUrl: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png', }, ]; describe('Upload List', () => { // Mock for rc-util raf window.requestAnimationFrame = callback => { window.setTimeout(callback, 16); }; window.cancelAnimationFrame = id => { window.clearTimeout(id); }; // jsdom not support `createObjectURL` yet. Let's handle this. const originCreateObjectURL = window.URL.createObjectURL; window.URL.createObjectURL = jest.fn(() => ''); // Mock dom let size = { width: 0, height: 0 }; function setSize(width, height) { size = { width, height }; } const mockWidthGet = jest.spyOn(Image.prototype, 'width', 'get'); const mockHeightGet = jest.spyOn(Image.prototype, 'height', 'get'); const mockSrcSet = jest.spyOn(Image.prototype, 'src', 'set'); let drawImageCallback = null; function hookDrawImageCall(callback) { drawImageCallback = callback; } const mockGetCanvasContext = jest.spyOn(HTMLCanvasElement.prototype, 'getContext'); const mockToDataURL = jest.spyOn(HTMLCanvasElement.prototype, 'toDataURL'); // HTMLCanvasElement.prototype beforeEach(() => setup()); afterEach(() => { teardown(); drawImageCallback = null; }); let open; beforeAll(() => { open = jest.spyOn(window, 'open').mockImplementation(() => {}); mockWidthGet.mockImplementation(() => size.width); mockHeightGet.mockImplementation(() => size.height); mockSrcSet.mockImplementation(function fn() { if (this.onload) { this.onload(); } }); mockGetCanvasContext.mockReturnValue({ drawImage: (...args) => { if (drawImageCallback) drawImageCallback(...args); }, }); mockToDataURL.mockReturnValue('data:image/png;base64,'); }); afterAll(() => { window.URL.createObjectURL = originCreateObjectURL; mockWidthGet.mockRestore(); mockHeightGet.mockRestore(); mockSrcSet.mockRestore(); mockGetCanvasContext.mockRestore(); mockToDataURL.mockRestore(); open.mockRestore(); }); // https://github.com/ant-design/ant-design/issues/4653 it('should use file.thumbUrl for in priority', () => { const wrapper = mount( , ); fileList.forEach((file, i) => { const linkNode = wrapper.find('.ant-upload-list-item-thumbnail').at(i); const imgNode = wrapper.find('.ant-upload-list-item-thumbnail img').at(i); expect(linkNode.prop('href')).toBe(file.url); expect(imgNode.prop('src')).toBe(file.thumbUrl); }); }); // https://github.com/ant-design/ant-design/issues/7269 it('should remove correct item when uid is 0', async () => { const list = [ { uid: '0', name: 'xxx.png', status: 'done', url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png', thumbUrl: 'https://zos.alipayobjects.com/rmsportal/IQKRngzUuFzJzGzRJXUs.png', }, { uid: '1', name: 'xxx.png', status: 'done', url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png', thumbUrl: 'https://zos.alipayobjects.com/rmsportal/IQKRngzUuFzJzGzRJXUs.png', }, ]; const wrapper = mount( , ); expect(wrapper.find('.ant-upload-list-item').length).toBe(2); wrapper.find('.ant-upload-list-item').at(0).find('.anticon-delete').simulate('click'); await sleep(400); wrapper.update(); expect(wrapper.find('.ant-upload-list-item').hostNodes().length).toBe(1); }); it('should be uploading when upload a file', done => { let wrapper; let latestFileList = null; const onChange = ({ file, fileList: eventFileList }) => { expect(eventFileList === latestFileList).toBeFalsy(); if (file.status === 'uploading') { expect(wrapper.render()).toMatchSnapshot(); } if (file.status === 'done') { expect(wrapper.render()).toMatchSnapshot(); done(); } latestFileList = eventFileList; }; wrapper = mount( , ); wrapper.find('input').simulate('change', { target: { files: [{ name: 'foo.png' }], }, }); }); it('handle error', done => { let wrapper; const onChange = ({ file }) => { if (file.status !== 'uploading') { expect(wrapper.render()).toMatchSnapshot(); done(); } }; wrapper = mount( , ); wrapper.find('input').simulate('change', { target: { files: [{ name: 'foo.png' }], }, }); }); it('does concat filelist when beforeUpload returns false', () => { const handleChange = jest.fn(); const ref = React.createRef(); const wrapper = mount( false} > , ); wrapper.find('input').simulate('change', { target: { files: [{ name: 'foo.png' }], }, }); expect(ref.current.fileList.length).toBe(fileList.length + 1); expect(handleChange.mock.calls[0][0].fileList).toHaveLength(3); }); it('In the case of listType=picture, the error status does not show the download.', () => { const file = { status: 'error', uid: 'file' }; const wrapper = mount( , ); expect(wrapper.find('div.ant-upload-list-item i.anticon-download').length).toBe(0); }); it('In the case of listType=picture-card, the error status does not show the download.', () => { const file = { status: 'error', uid: 'file' }; const wrapper = mount( , ); expect(wrapper.find('div.ant-upload-list-item i.anticon-download').length).toBe(0); }); it('In the case of listType=text, the error status does not show the download.', () => { const file = { status: 'error', uid: 'file' }; const wrapper = mount( , ); expect(wrapper.find('div.ant-upload-list-item i.anticon-download').length).toBe(0); }); it('should support onPreview', () => { const handlePreview = jest.fn(); const wrapper = mount( , ); wrapper.find('.anticon-eye').at(0).simulate('click'); expect(handlePreview).toHaveBeenCalledWith(fileList[0]); wrapper.find('.anticon-eye').at(1).simulate('click'); expect(handlePreview).toHaveBeenCalledWith(fileList[1]); }); it('should support onRemove', async () => { const handleRemove = jest.fn(); const handleChange = jest.fn(); const wrapper = mount( , ); wrapper.find('.anticon-delete').at(0).simulate('click'); expect(handleRemove).toHaveBeenCalledWith(fileList[0]); wrapper.find('.anticon-delete').at(1).simulate('click'); expect(handleRemove).toHaveBeenCalledWith(fileList[1]); await sleep(); expect(handleChange.mock.calls.length).toBe(2); }); it('should support onDownload', async () => { const handleDownload = jest.fn(); const wrapper = mount( , ); wrapper.find('.anticon-download').at(0).simulate('click'); }); it('should support no onDownload', async () => { const wrapper = mount( , ); wrapper.find('.anticon-download').at(0).simulate('click'); }); describe('should generate thumbUrl from file', () => { [ { width: 100, height: 200, name: 'height large than width' }, { width: 200, height: 100, name: 'width large than height' }, ].forEach(({ width, height, name }) => { it(name, async () => { setSize(width, height); const onDrawImage = jest.fn(); hookDrawImageCall(onDrawImage); const handlePreview = jest.fn(); const newFileList = [...fileList]; const newFile = { ...fileList[0], uid: '-3', originFileObj: new File([], 'xxx.png', { type: 'image/png' }), }; delete newFile.thumbUrl; newFileList.push(newFile); const ref = React.createRef(); mount( , ); ref.current.forceUpdate(); await sleep(); expect(ref.current.fileList[2].thumbUrl).not.toBe(undefined); expect(onDrawImage).toHaveBeenCalled(); // Offset check const [, offsetX, offsetY] = onDrawImage.mock.calls[0]; if (width > height) { expect(offsetX < 0).toBeTruthy(); } else { expect(offsetY < 0).toBeTruthy(); } }); }); }); it('should non-image format file preview', () => { const list = [ { name: 'not-image', status: 'done', uid: '-3', url: 'https://cdn.xxx.com/aaa.zip', thumbUrl: 'data:application/zip;base64,UEsDBAoAAAAAADYZYkwAAAAAAAAAAAAAAAAdAAk', originFileObj: new File([], 'aaa.zip'), }, { name: 'image', status: 'done', uid: '-4', url: 'https://cdn.xxx.com/aaa', }, { name: 'not-image', status: 'done', uid: '-5', url: 'https://cdn.xxx.com/aaa.xx', }, { name: 'not-image', status: 'done', uid: '-6', url: 'https://cdn.xxx.com/aaa.png/xx.xx', }, { name: 'image', status: 'done', uid: '-7', url: 'https://cdn.xxx.com/xx.xx/aaa.png', }, { name: 'image', status: 'done', uid: '-8', url: 'https://cdn.xxx.com/xx.xx/aaa.png', thumbUrl: 'data:image/png;base64,UEsDBAoAAAAAADYZYkwAAAAAAAAAAAAAAAAdAAk', }, { name: 'image', status: 'done', uid: '-9', url: 'https://cdn.xxx.com/xx.xx/aaa.png?query=123', }, { name: 'image', status: 'done', uid: '-10', url: 'https://cdn.xxx.com/xx.xx/aaa.png#anchor', }, { name: 'image', status: 'done', uid: '-11', url: 'https://cdn.xxx.com/xx.xx/aaa.png?query=some.query.with.dot', }, { name: 'image', status: 'done', uid: '-12', url: 'https://publish-pic-cpu.baidu.com/1296beb3-50d9-4276-885f-52645cbb378e.jpeg@w_228%2ch_152', type: 'image/png', }, ]; const wrapper = mount( , ); expect(wrapper.render()).toMatchSnapshot(); }); it('should support showRemoveIcon and showPreviewIcon', () => { const list = [ { name: 'image', status: 'uploading', uid: '-4', url: 'https://cdn.xxx.com/aaa', }, { name: 'image', status: 'done', uid: '-5', url: 'https://cdn.xxx.com/aaa', }, ]; const wrapper = mount( , ); expect(wrapper.render()).toMatchSnapshot(); }); it('should support custom onClick in custom icon', async () => { const handleRemove = jest.fn(); const handleChange = jest.fn(); const myClick = jest.fn(); const wrapper = mount( RM ), }} > , ); wrapper.find('.custom-delete').at(0).simulate('click'); expect(handleRemove).toHaveBeenCalledWith(fileList[0]); expect(myClick).toHaveBeenCalled(); wrapper.find('.custom-delete').at(1).simulate('click'); expect(handleRemove).toHaveBeenCalledWith(fileList[1]); expect(myClick).toHaveBeenCalled(); await sleep(); expect(handleChange.mock.calls.length).toBe(2); }); it('should support removeIcon and downloadIcon', () => { const list = [ { name: 'image', status: 'uploading', uid: '-4', url: 'https://cdn.xxx.com/aaa', }, { name: 'image', status: 'done', uid: '-5', url: 'https://cdn.xxx.com/aaa', }, ]; const wrapper = mount( RM, downloadIcon: DL, }} > , ); expect(wrapper.render()).toMatchSnapshot(); }); // https://github.com/ant-design/ant-design/issues/7762 it('work with form validation', async () => { let formRef; const TestForm = () => { const [form] = Form.useForm(); formRef = form; return (
e.fileList} rules={[ { required: true, validator: (rule, value, callback) => { if (!value || value.length === 0) { callback('file required'); } else { callback(); } }, }, ]} > false}>
); }; const wrapper = mount(); wrapper.find(Form).simulate('submit'); await sleep(); expect(formRef.getFieldError(['file'])).toEqual(['file required']); wrapper.find('input').simulate('change', { target: { files: [{ name: 'foo.png' }], }, }); wrapper.find(Form).simulate('submit'); await sleep(); expect(formRef.getFieldError(['file'])).toEqual([]); }); it('return when prop onPreview not exists', () => { const ref = React.createRef(); mount(); expect(ref.current.handlePreview()).toBe(undefined); }); it('return when prop onDownload not exists', () => { const file = new File([''], 'test.txt', { type: 'text/plain' }); const items = [{ uid: 'upload-list-item', url: '' }]; const ref = React.createRef(); mount( , ); expect(ref.current.handleDownload(file)).toBe(undefined); }); it('previewFile should work correctly', async () => { const file = new File([''], 'test.txt', { type: 'text/plain' }); const items = [{ uid: 'upload-list-item', url: '' }]; const wrapper = mount( , ); return wrapper.props().previewFile(file); }); it('downloadFile should work correctly', async () => { const file = new File([''], 'test.txt', { type: 'text/plain' }); const items = [{ uid: 'upload-list-item', url: '' }]; const wrapper = mount( {}} locale={{ downloadFile: '' }} showUploadList={{ showDownloadIcon: true }} />, ); return wrapper.props().onDownload(file); }); it('extname should work correctly when url not exists', () => { const items = [{ uid: 'upload-list-item', url: '' }]; const wrapper = mount( , ); expect(wrapper.find('.ant-upload-list-item-thumbnail').length).toBe(1); }); it('extname should work correctly when url exists', done => { const items = [{ status: 'done', uid: 'upload-list-item', url: '/example' }]; const wrapper = mount( { expect(file.url).toBe('/example'); done(); }} items={items} locale={{ downloadFile: '' }} showDownloadIcon />, ); wrapper.find('div.ant-upload-list-item .anticon-download').simulate('click'); }); it('when picture-card is loading, icon should render correctly', () => { const items = [{ status: 'uploading', uid: 'upload-list-item' }]; const wrapper = mount( , ); expect(wrapper.find('.ant-upload-list-item-thumbnail').length).toBe(1); expect(wrapper.find('.ant-upload-list-item-thumbnail').text()).toBe('uploading'); }); it('onPreview should be called, when url exists', () => { const onPreview = jest.fn(); const items = [{ thumbUrl: 'thumbUrl', url: 'url', uid: 'upload-list-item' }]; const wrapper = mount( , ); wrapper.find('.ant-upload-list-item-thumbnail').simulate('click'); expect(onPreview).toHaveBeenCalled(); wrapper.find('.ant-upload-list-item-name').simulate('click'); expect(onPreview).toHaveBeenCalled(); wrapper.setProps({ items: [{ thumbUrl: 'thumbUrl', uid: 'upload-list-item' }] }); wrapper.find('.ant-upload-list-item-name').simulate('click'); expect(onPreview).toHaveBeenCalled(); }); it('upload image file should be converted to the base64', async () => { const mockFile = new File([''], 'foo.png', { type: 'image/png', }); const wrapper = mount( , ); return wrapper .props() .previewFile(mockFile) .then(dataUrl => { expect(dataUrl).toEqual('data:image/png;base64,'); }); }); it("upload non image file shouldn't be converted to the base64", async () => { const mockFile = new File([''], 'foo.7z', { type: 'application/x-7z-compressed', }); const wrapper = mount( , ); return wrapper .props() .previewFile(mockFile) .then(dataUrl => { expect(dataUrl).toBe(''); }); }); describe('customize previewFile support', () => { function test(name, renderInstance) { it(name, async () => { const mockThumbnail = 'mock-image'; const previewFile = jest.fn(() => { return Promise.resolve(mockThumbnail); }); const file = { ...fileList[0], originFileObj: renderInstance(), }; delete file.thumbUrl; const ref = React.createRef(); const wrapper = mount( , ); ref.current.forceUpdate(); expect(previewFile).toHaveBeenCalledWith(file.originFileObj); await sleep(100); wrapper.update(); expect(wrapper.find('.ant-upload-list-item-thumbnail img').prop('src')).toBe(mockThumbnail); }); } test('File', () => new File([], 'xxx.png')); test('Blob', () => new Blob()); }); // https://github.com/ant-design/ant-design/issues/22958 describe('customize isImageUrl support', () => { const list = [ ...fileList, { uid: '0', name: 'xxx.png', status: 'done', url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png', thumbUrl: 'http://image-demo.oss-cn-hangzhou.aliyuncs.com/example.jpg@!panda_style?spm=a2c4g.11186623.2.17.4dc56b29BHokyg&file=example.jpg@!panda_style', }, ]; it('should not render when file.thumbUrl use "!" as separator', () => { const wrapper = mount( , ); const imgNode = wrapper.find('.ant-upload-list-item-thumbnail img'); expect(imgNode.length).toBe(2); }); it('should render when custom imageUrl return true', () => { const isImageUrl = jest.fn(() => { return true; }); const wrapper = mount( , ); const imgNode = wrapper.find('.ant-upload-list-item-thumbnail img'); expect(isImageUrl).toHaveBeenCalled(); expect(imgNode.length).toBe(3); }); it('should not render when custom imageUrl return false', () => { const isImageUrl = jest.fn(() => { return false; }); const wrapper = mount( , ); const imgNode = wrapper.find('.ant-upload-list-item-thumbnail img'); expect(isImageUrl).toHaveBeenCalled(); expect(imgNode.length).toBe(0); }); }); describe('thumbUrl support for non-image', () => { const nonImageFile = new File([''], 'foo.7z', { type: 'application/x-7z-compressed' }); it('should render when upload non-image file and configure thumbUrl in onChange', done => { let wrapper; const onChange = async ({ file, fileList: files }) => { const newFileList = files.map(item => ({ ...item, thumbUrl: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png', })); wrapper.setProps({ fileList: newFileList }); if (file.status === 'uploading') { return; } act(async () => { await sleep(); wrapper.update(); }); const imgNode = wrapper.find('.ant-upload-list-item-thumbnail img'); expect(imgNode.length).toBe(1); done(); }; wrapper = mount( , ); const imgNode = wrapper.find('.ant-upload-list-item-thumbnail img'); expect(imgNode.length).toBe(0); wrapper.find('input').simulate('change', { target: { files: [nonImageFile] } }); }); it('should not render when upload non-image file without thumbUrl in onChange', done => { let wrapper; const onChange = async ({ fileList: files }) => { wrapper.setProps({ fileList: files }); await sleep(); const imgNode = wrapper.find('.ant-upload-list-item-thumbnail img'); expect(imgNode.length).toBe(0); done(); }; wrapper = mount( , ); const imgNode = wrapper.find('.ant-upload-list-item-thumbnail img'); expect(imgNode.length).toBe(0); wrapper.find('input').simulate('change', { target: { files: [nonImageFile] } }); }); }); it('should support transformFile', done => { const handleTransformFile = jest.fn(); const onChange = ({ file }) => { if (file.status === 'done') { expect(handleTransformFile).toHaveBeenCalled(); done(); } }; const wrapper = mount( , ); wrapper.find('input').simulate('change', { target: { files: [{ name: 'foo.png' }], }, }); }); it('should render button inside UploadList when listStyle is picture-card', () => { const wrapper = mount( , ); expect(wrapper.exists('.ant-upload-list button.trigger')).toBe(true); wrapper.setProps({ showUploadList: false }); expect(wrapper.exists('.ant-upload-list button.trigger')).toBe(false); }); // https://github.com/ant-design/ant-design/issues/26536 it('multiple file upload should keep the internal fileList async', async () => { jest.useFakeTimers(); const uploadRef = React.createRef(); const MyUpload = () => { const [testFileList, setTestFileList] = React.useState([]); return ( { setTestFileList([...info.fileList]); }} > ); }; mount(); // Mock async update in a frame const files = ['light', 'bamboo', 'little']; /* eslint-disable no-await-in-loop */ for (let i = 0; i < files.length; i += 1) { await Promise.resolve(); uploadRef.current.onStart({ uid: files[i], name: files[i], }); } /* eslint-enable */ expect(uploadRef.current.fileList).toHaveLength(files.length); jest.runAllTimers(); expect(uploadRef.current.fileList).toHaveLength(files.length); jest.useRealTimers(); }); });