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();
});
});