import { render, fireEvent, waitFor, screen } from '../test-utils'; import { act } from 'react'; import Upload from '.'; import { asyncFileRead } from './utils/asyncFileRead'; import { postData } from './utils/postData'; jest.mock('./utils/asyncFileRead'); jest.mock('./utils/postData'); jest.mock('commonmark', () => ({ Parser: jest.fn().mockImplementation(() => ({ parse: jest.fn(() => ({})), })), HtmlRenderer: jest.fn().mockImplementation(() => ({ render: jest.fn(() => ''), })), })); const TEST_FILE = new File(['test content'], 'test.png', { type: 'image/png' }); const INVALID_FILE = new File(['invalid content'], 'invalid.txt', { type: 'text/plain' }); beforeAll(() => { class MockDataTransfer { items: { add: (file: File) => void }; files: File[]; constructor() { this.items = { add: (file: File) => { this.files.push(file); }, }; this.files = []; } } // @ts-expect-error: Mocking global DataTransfer for testing purposes global.DataTransfer = MockDataTransfer; }); describe('Upload Component', () => { const props = { animationDelay: 100, csButtonText: 'csButtonText', csFailureText: 'csFailureText', csSuccessText: 'csSuccessText', csTooLargeMessage: 'csTooLargeMessage', maxSize: 5000000, onCancel: jest.fn(), onChange: jest.fn(), onFailure: jest.fn(), onStart: jest.fn(), onSuccess: jest.fn(), psButtonText: 'psButtonText', psProcessingText: 'psProcessingText', usAccept: 'image/*', usButtonText: 'Or Select File', usButtonRetryText: 'Try again', usDropMessage: 'Drop file to start upload', usPlaceholder: 'Drag and drop a file less than 5MB', }; const waitForUpload = async () => { for (let i = 0; i < 4; i += 1) { await act(async () => { await jest.runOnlyPendingTimersAsync(); }); } }; beforeEach(() => { jest.useFakeTimers(); (asyncFileRead as jest.Mock).mockImplementation(async () => 'a value'); (postData as jest.Mock).mockResolvedValue('ServerResponse'); Object.defineProperty(window, 'matchMedia', { writable: true, value: jest.fn().mockImplementation((query: string) => ({ matches: false, media: query, onchange: null, addListener: jest.fn(), removeListener: jest.fn(), addEventListener: jest.fn(), removeEventListener: jest.fn(), dispatchEvent: jest.fn(), })), }); props.onStart.mockReset(); props.onSuccess.mockReset(); props.onFailure.mockReset(); props.onCancel.mockReset(); props.onChange.mockReset(); }); afterEach(async () => { await jest.runOnlyPendingTimersAsync(); jest.runOnlyPendingTimers(); jest.useRealTimers(); }); test('adds droppable-dropping class on dragenter', async () => { const { container } = render(); const droppableElement = await waitFor(() => container.querySelector('.droppable-area')); expect(droppableElement).toBeTruthy(); fireEvent.dragEnter(droppableElement!, { dataTransfer: new DataTransfer() }); await waitFor(() => { expect(droppableElement).toHaveClass('droppable-dropping'); }); }); test('removes droppable-dropping class on dragleave', async () => { const { container } = render(); const droppableElement = await waitFor(() => container.querySelector('.droppable-area')); expect(droppableElement).toBeTruthy(); fireEvent.dragEnter(droppableElement!, { dataTransfer: new DataTransfer() }); await waitFor(() => { expect(droppableElement).toHaveClass('droppable-dropping'); }); fireEvent.dragLeave(droppableElement!); await waitFor(() => { expect(droppableElement).not.toHaveClass('droppable-dropping'); }); }); test('adds droppable-processing when file is dropped', async () => { const { container } = render(); const droppableElement = await waitFor(() => container.querySelector('.droppable-area')); const dataTransfer = new DataTransfer(); dataTransfer.items.add(TEST_FILE); fireEvent.drop(droppableElement!, { dataTransfer }); await waitFor(() => expect(droppableElement).toHaveClass('droppable-processing')); }); test('should handle successful file upload', async () => { (asyncFileRead as jest.Mock).mockResolvedValue('mockBase64Image'); (postData as jest.Mock).mockResolvedValue('mockSuccessResponse'); const { container } = render(); await act(async () => { const droppableElement = await waitFor(() => container.querySelector('.droppable-area')); const dataTransfer = new DataTransfer(); dataTransfer.items.add(TEST_FILE); fireEvent.drop(droppableElement!, { dataTransfer }); }); await act(async () => { jest.advanceTimersByTime(10); jest.advanceTimersByTime(props.animationDelay); }); expect(await screen.findByText(/csSuccessText/i)).toBeInTheDocument(); }); test('should handle file size exceeding the limit', async () => { const largeFile = new File(['large '.repeat(1000000)], 'large.jpg', { type: 'image/jpeg' }); const { container } = render(); await act(async () => { const droppableElement = await waitFor(() => container.querySelector('.droppable-area')); const dataTransfer = new DataTransfer(); dataTransfer.items.add(largeFile); fireEvent.drop(droppableElement!, { dataTransfer }); }); await act(async () => { jest.advanceTimersByTime(1000); jest.advanceTimersByTime(props.animationDelay); }); await waitForUpload(); expect(await screen.findByText(/csTooLargeMessage/i)).toBeInTheDocument(); }); test('should handle invalid file type', async () => { (postData as jest.Mock).mockRejectedValue(new Error('mockErrorResponse')); const { container } = render(); await act(async () => { const droppableElement = await waitFor(() => container.querySelector('.droppable-area')); const dataTransfer = new DataTransfer(); dataTransfer.items.add(INVALID_FILE); fireEvent.drop(droppableElement!, { dataTransfer }); }); await waitForUpload(); expect( await screen.findByText(/File type not supported. Please try again with a different file/i), ).toBeInTheDocument(); }); test('should handle canceling the upload', async () => { const { container } = render(); await act(async () => { const droppableElement = await waitFor(() => container.querySelector('.droppable-area')); const dataTransfer = new DataTransfer(); dataTransfer.items.add(TEST_FILE); fireEvent.drop(droppableElement!, { dataTransfer }); }); fireEvent.click(await screen.findByText(/csButtonText/i)); expect(props.onCancel).toHaveBeenCalled(); }); test('should handle retrying the upload', async () => { (asyncFileRead as jest.Mock).mockRejectedValueOnce('error'); const { container } = render(); await act(async () => { const droppableElement = await waitFor(() => container.querySelector('.droppable-area')); const dataTransfer = new DataTransfer(); dataTransfer.items.add(TEST_FILE); fireEvent.drop(droppableElement!, { dataTransfer }); }); await waitForUpload(); fireEvent.click(await screen.findByText(/Try again/i)); expect(props.onStart).toHaveBeenCalled(); }); test('should call onSuccess with the server response when httpOptions are provided', async () => { (asyncFileRead as jest.Mock).mockResolvedValue('mockBase64Image'); (postData as jest.Mock).mockResolvedValue('mockServerResponse'); const { container } = render(); const droppableElement = await waitFor(() => container.querySelector('.droppable-area')); const dataTransfer = new DataTransfer(); dataTransfer.items.add(TEST_FILE); await act(async () => { fireEvent.drop(droppableElement!, { dataTransfer }); }); await waitForUpload(); expect(props.onSuccess).toHaveBeenCalledWith('mockServerResponse', TEST_FILE.name); }); test('should not allow new file to process while current one is in progress', async () => { (asyncFileRead as jest.Mock).mockResolvedValue('someResult'); const { container } = render(); const droppableElement = await waitFor(() => container.querySelector('.droppable-area')); const dataTransfer = new DataTransfer(); dataTransfer.items.add(TEST_FILE); await act(async () => { fireEvent.drop(droppableElement!, { dataTransfer }); }); const secondFileDataTransfer = new DataTransfer(); secondFileDataTransfer.items.add(TEST_FILE); let dropResult = true; await act(async () => { try { fireEvent.drop(droppableElement!, { dataTransfer: secondFileDataTransfer }); } catch { dropResult = false; } }); expect(dropResult).toBe(true); }); test('should override the error icon label when errorIconLabel prop is provided', async () => { (asyncFileRead as jest.Mock).mockRejectedValueOnce('mockReadError'); (postData as jest.Mock).mockRejectedValueOnce(new Error('mockPostError')); const customLabel = 'Custom error label'; const { container } = render(); const droppableElement = await waitFor(() => container.querySelector('.droppable-area')); const dataTransfer = new DataTransfer(); dataTransfer.items.add(TEST_FILE); await act(async () => { fireEvent.drop(droppableElement!, { dataTransfer }); }); await waitForUpload(); const errorIcon = await screen.findByLabelText(/Custom error label/i); expect(errorIcon).toBeInTheDocument(); }); function runAnimationDelayTest(label: string, delay: number) { test(`should respect a ${label} animationDelay`, async () => { (asyncFileRead as jest.Mock).mockResolvedValue('mockBase64Image'); (postData as jest.Mock).mockResolvedValue('mockSuccessResponse'); const { container, unmount } = render(); await act(async () => { const droppableElement = await waitFor(() => container.querySelector('.droppable-area')); const dataTransfer = new DataTransfer(); dataTransfer.items.add(TEST_FILE); fireEvent.drop(droppableElement!, { dataTransfer }); }); // Advance half the delay, should still be processing await act(async () => { jest.advanceTimersByTime(delay / 2); }); expect(container.querySelector('.droppable-processing')).toBeInTheDocument(); expect(container.querySelector('.droppable-complete')).not.toBeInTheDocument(); // Advance the rest of the delay await act(async () => { jest.advanceTimersByTime(delay / 2 + 10); }); expect(container.querySelector('.droppable-complete')).toBeInTheDocument(); expect(container.querySelector('.droppable-processing')).not.toBeInTheDocument(); unmount(); }); } runAnimationDelayTest('short', 50); runAnimationDelayTest('long', 500); test('should handle disabled state correctly', async () => { const { container } = render(); const droppableElement = await waitFor(() => container.querySelector('.droppable-area')); const dataTransfer = new DataTransfer(); dataTransfer.items.add(TEST_FILE); await act(async () => { fireEvent.drop(droppableElement!, { dataTransfer }); }); expect(container.querySelector('.droppable-processing')).not.toBeInTheDocument(); expect(props.onStart).not.toHaveBeenCalled(); }); test('should remove max size limit, when maxSize is the `null`', async () => { render(); expect(await screen.findByText(/Drag and drop a file/i)).toBeInTheDocument(); expect(screen.queryByText(/less than/i)).not.toBeInTheDocument(); }); });