// Licensed to Cloudera, Inc. under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. Cloudera, Inc. licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from 'react';
import { render, fireEvent, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom';
import FileUploadQueue from './FileUploadQueue';
import { FileStatus, RegularFile } from '../../utils/hooks/useFileUpload/types';
import { act } from 'react-dom/test-utils';
import huePubSub from '../../utils/huePubSub';
import { FILE_UPLOAD_START_EVENT } from './event';
import { CancellablePromise } from '../../api/cancellablePromise';
import useFileUpload from '../../utils/hooks/useFileUpload/useFileUpload';
import * as apiUtils from '../../api/utils';
const mockGet = jest.spyOn(apiUtils, 'get');
const mockFilesQueue: RegularFile[] = [
{
uuid: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx1',
filePath: '/path/to/file1.txt',
status: FileStatus.Pending,
file: new File([], 'file1.txt')
},
{
uuid: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx2',
filePath: '/path/to/file2.txt',
status: FileStatus.Pending,
file: new File([], 'file2.txt')
}
];
const mockCancelFile = jest.fn();
const mockAddFiles = jest.fn();
const mockRemoveAllFiles = jest.fn();
jest.mock('../../utils/hooks/useFileUpload/useFileUpload', () => ({
__esModule: true,
default: jest.fn(() => ({
uploadQueue: mockFilesQueue,
cancelFile: mockCancelFile,
addFiles: mockAddFiles,
removeAllFiles: mockRemoveAllFiles,
isLoading: false
}))
}));
const mockUseFileUpload = jest.mocked(useFileUpload);
describe('FileUploadQueue', () => {
beforeEach(() => {
jest.clearAllMocks();
mockGet.mockRejectedValue({ response: { status: 404 } });
huePubSub.removeAll(FILE_UPLOAD_START_EVENT);
huePubSub.removeAll('set.current.app.name');
huePubSub.removeAll('get.current.app.name');
// Set up the get.current.app.name subscription to return storagebrowser by default
huePubSub.subscribe('get.current.app.name', (callback: (appName: string) => void) => {
callback('storagebrowser');
});
mockUseFileUpload.mockReturnValue({
uploadQueue: mockFilesQueue,
cancelFile: mockCancelFile,
addFiles: mockAddFiles,
removeAllFiles: mockRemoveAllFiles,
isLoading: false
});
});
afterEach(() => {
huePubSub.removeAll('get.current.app.name');
});
it('should not render when current app is not storagebrowser', async () => {
huePubSub.removeAll('get.current.app.name');
huePubSub.subscribe('get.current.app.name', (callback: (appName: string) => void) => {
callback('editor');
});
const { container } = render();
act(() => {
huePubSub.publish(FILE_UPLOAD_START_EVENT, { files: mockFilesQueue });
});
await waitFor(() => {
expect(container.firstChild).toBeNull();
});
});
it('should render when current app is storagebrowser', async () => {
const { container } = render();
await waitFor(() => {
expect(container.firstChild).not.toBeNull();
});
});
it('should render the component with initial files in the queue', async () => {
const { getByText } = render();
act(() => {
huePubSub.publish(FILE_UPLOAD_START_EVENT, { files: mockFilesQueue });
});
await waitFor(() => {
expect(getByText('file1.txt')).toBeVisible();
expect(getByText('file2.txt')).toBeVisible();
});
});
it('should toggle the visibility of the queue when the header is clicked', async () => {
const { getByText, queryByText, getByRole } = render();
act(() => {
huePubSub.publish(FILE_UPLOAD_START_EVENT, { files: mockFilesQueue });
});
// Wait for items to appear
await waitFor(() => expect(getByText('file1.txt')).toBeVisible());
await waitFor(() => expect(getByText('file2.txt')).toBeVisible());
const header = getByRole('button', { name: /toggle upload queue/i });
fireEvent.click(header);
expect(queryByText('file1.txt')).toBeNull();
expect(queryByText('file2.txt')).toBeNull();
fireEvent.click(header);
await waitFor(() => expect(getByText('file1.txt')).toBeVisible());
await waitFor(() => expect(getByText('file2.txt')).toBeVisible());
});
it('should show conflict modal when files already exist', async () => {
const files: RegularFile[] = [
{
uuid: 'c1',
filePath: '/dir',
status: FileStatus.Pending,
file: new File([], 'conflict.txt')
},
{
uuid: 'n1',
filePath: '/dir',
status: FileStatus.Pending,
file: new File([], 'new.txt')
}
];
mockGet.mockImplementation((_url: string, data?: unknown) => {
const params = data as { path: string };
return CancellablePromise.resolve({ path: params.path });
});
const { getByText } = render();
act(() => {
huePubSub.publish(FILE_UPLOAD_START_EVENT, { files });
});
await waitFor(() => {
expect(mockGet).toHaveBeenCalled();
expect(getByText('Detected Filename Conflicts')).toBeVisible();
expect(
getByText('2 files you are trying to upload already exist in the uploaded files.')
).toBeVisible();
expect(getByText('conflict.txt')).toBeVisible();
});
});
it('should allow canceling conflict resolution', async () => {
const files: RegularFile[] = [
{
uuid: 'c1',
filePath: '/dir',
status: FileStatus.Pending,
file: new File([], 'conflict.txt')
}
];
mockGet.mockImplementation((_url: string, data?: unknown) => {
const params = data as { path: string };
return CancellablePromise.resolve({ path: params.path });
});
const { getByText, queryByText } = render();
act(() => {
huePubSub.publish(FILE_UPLOAD_START_EVENT, { files });
});
await waitFor(() => {
expect(mockGet).toHaveBeenCalled();
expect(getByText('Detected Filename Conflicts')).toBeVisible();
expect(getByText('conflict.txt')).toBeVisible();
});
fireEvent.click(getByText('Cancel'));
await waitFor(() => {
expect(queryByText('Detected Filename Conflicts')).toBeNull();
expect(mockAddFiles).toHaveBeenCalledWith([]);
});
});
it('should allow skipping upload for conflicted files', async () => {
const files: RegularFile[] = [
{
uuid: 'c1',
filePath: '/dir',
status: FileStatus.Pending,
file: new File([], 'conflict.txt')
}
];
mockGet.mockImplementation(() => {
return CancellablePromise.resolve({ path: '/dir/conflict.txt' });
});
const { getByText, queryByText } = render();
act(() => {
huePubSub.publish(FILE_UPLOAD_START_EVENT, { files });
});
await waitFor(() => {
expect(mockGet).toHaveBeenCalled();
expect(getByText('Detected Filename Conflicts')).toBeVisible();
expect(getByText('conflict.txt')).toBeVisible();
});
fireEvent.click(getByText('Skip Upload'));
await waitFor(() => {
expect(queryByText('Detected Filename Conflicts')).toBeNull();
expect(mockAddFiles).toHaveBeenCalledWith([]);
});
});
it('should allow individual file cancellation', async () => {
const { getAllByTestId } = render();
act(() => {
huePubSub.publish(FILE_UPLOAD_START_EVENT, { files: mockFilesQueue });
});
await waitFor(() => {
const closeButtons = getAllByTestId('hue-upload-queue-row__close-icon');
expect(closeButtons).toHaveLength(2);
fireEvent.click(closeButtons[0]);
expect(mockCancelFile).toHaveBeenCalledWith(mockFilesQueue[0]);
});
});
it('should show cancel all button when files are in pending or uploading state', async () => {
mockUseFileUpload.mockReturnValue({
uploadQueue: mockFilesQueue,
cancelFile: mockCancelFile,
addFiles: mockAddFiles,
removeAllFiles: mockRemoveAllFiles,
isLoading: true
});
const { getByText } = render();
await waitFor(() => {
expect(getByText('Cancel All')).toBeVisible();
});
fireEvent.click(getByText('Cancel All'));
expect(mockRemoveAllFiles).toHaveBeenCalled();
});
it('should sort files by status priority', async () => {
const mixedStatusFiles: RegularFile[] = [
{
uuid: '1',
filePath: '/path/to/uploaded.txt',
status: FileStatus.Uploaded,
file: new File([], 'uploaded.txt')
},
{
uuid: '2',
filePath: '/path/to/uploading.txt',
status: FileStatus.Uploading,
file: new File([], 'uploading.txt')
},
{
uuid: '3',
filePath: '/path/to/failed.txt',
status: FileStatus.Failed,
file: new File([], 'failed.txt')
},
{
uuid: '4',
filePath: '/path/to/pending.txt',
status: FileStatus.Pending,
file: new File([], 'pending.txt')
}
];
mockUseFileUpload.mockReturnValue({
uploadQueue: mixedStatusFiles,
cancelFile: mockCancelFile,
addFiles: mockAddFiles,
removeAllFiles: mockRemoveAllFiles,
isLoading: true
});
const { container } = render();
await waitFor(() => {
const fileRows = container.querySelectorAll('.hue-upload-queue-row__name');
const fileNames = Array.from(fileRows).map(row => row.textContent);
expect(fileNames).toEqual(['uploading.txt', 'failed.txt', 'pending.txt', 'uploaded.txt']);
});
});
it('should display uploading progress for files in progress', async () => {
const uploadingFile: RegularFile = {
uuid: '1',
filePath: '/path/to/uploading.txt',
status: FileStatus.Uploading,
progress: 65,
file: new File([], 'uploading.txt')
};
mockUseFileUpload.mockReturnValue({
uploadQueue: [uploadingFile],
cancelFile: mockCancelFile,
addFiles: mockAddFiles,
removeAllFiles: mockRemoveAllFiles,
isLoading: false
});
const { getByTestId } = render();
await waitFor(() => {
const progressBar = getByTestId('hue-upload-queue-row__progressbar');
expect(progressBar).toHaveStyle('width: 65%');
});
});
it('should show preparing to upload state during conflict checking', async () => {
const files: RegularFile[] = [
{
uuid: 'p1',
filePath: '/dir',
status: FileStatus.Pending,
file: new File([], 'pending.txt')
}
];
let resolveFn: (value: unknown) => void;
const pending = new CancellablePromise((resolve, _reject, onCancel) => {
resolveFn = resolve;
onCancel(() => {});
});
mockGet.mockImplementation(() => pending);
const { getByText, queryByText } = render();
act(() => {
huePubSub.publish(FILE_UPLOAD_START_EVENT, { files });
});
await waitFor(() => {
expect(getByText('Preparing to upload...')).toBeVisible();
});
act(() => {
resolveFn({ path: undefined });
});
await waitFor(() => {
expect(queryByText('Preparing to upload...')).toBeNull();
});
});
it('should display correct header text for different upload states', async () => {
// Test pending files
const { getByText, rerender } = render();
act(() => {
huePubSub.publish(FILE_UPLOAD_START_EVENT, { files: mockFilesQueue });
});
await waitFor(() => {
expect(getByText('2 files remaining')).toBeVisible();
});
// Test completed files
const completedFiles: RegularFile[] = mockFilesQueue.map(file => ({
...file,
status: FileStatus.Uploaded
}));
mockUseFileUpload.mockReturnValue({
uploadQueue: completedFiles,
cancelFile: mockCancelFile,
addFiles: mockAddFiles,
removeAllFiles: mockRemoveAllFiles,
isLoading: false
});
rerender();
await waitFor(() => {
expect(getByText('2 files uploaded')).toBeVisible();
});
});
});