blob: 4d99471a69020a075aa4e87f305982c6aa20efb0 [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF 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 { ComponentType } from 'react';
import { render, screen, waitFor } from 'spec/helpers/testing-library';
import { MemoryRouter, Route } from 'react-router-dom';
import FileHandler from './index';
const mockAddDangerToast = jest.fn();
const mockAddSuccessToast = jest.fn();
const mockHistoryPush = jest.fn();
type ToastInjectedProps = {
addDangerToast: (msg: string) => void;
addSuccessToast: (msg: string) => void;
};
// Mock the withToasts HOC
jest.mock('src/components/MessageToasts/withToasts', () => ({
__esModule: true,
default:
<P extends object>(Component: ComponentType<P & ToastInjectedProps>) =>
(props: P) => (
<Component
{...props}
addDangerToast={mockAddDangerToast}
addSuccessToast={mockAddSuccessToast}
/>
),
}));
interface UploadDataModalProps {
show: boolean;
onHide: () => void;
type: string;
allowedExtensions: string[];
fileListOverride?: File[];
}
// Mock the UploadDataModal
jest.mock('src/features/databases/UploadDataModel', () => ({
__esModule: true,
default: ({
show,
onHide,
type,
allowedExtensions,
fileListOverride,
}: UploadDataModalProps) => (
<div data-test="upload-modal">
<div data-test="modal-show">{show.toString()}</div>
<div data-test="modal-type">{type}</div>
<div data-test="modal-extensions">{allowedExtensions.join(',')}</div>
<div data-test="modal-file">{fileListOverride?.[0]?.name ?? ''}</div>
<button onClick={onHide}>Close</button>
</div>
),
}));
// Mock react-router-dom's useHistory
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useHistory: () => ({
push: mockHistoryPush,
}),
}));
// Mock the File API
type MockFileHandle = {
kind: 'file';
name: string;
getFile: () => Promise<File>;
isSameEntry: () => Promise<boolean>;
queryPermission: () => Promise<PermissionState>;
requestPermission: () => Promise<PermissionState>;
};
const createMockFileHandle = (fileName: string): MockFileHandle => ({
kind: 'file',
name: fileName,
getFile: async () => new File(['test'], fileName),
isSameEntry: async () => false,
queryPermission: async () => 'granted',
requestPermission: async () => 'granted',
});
type LaunchQueue = {
setConsumer: (
consumer: (params: { files?: MockFileHandle[] }) => void,
) => void;
};
const setupLaunchQueue = (fileHandle: MockFileHandle | null = null) => {
let savedConsumer: ((params: { files?: MockFileHandle[] }) => void) | null =
null;
(window as Window & { launchQueue: LaunchQueue }).launchQueue = {
setConsumer: (consumer: (params: { files?: MockFileHandle[] }) => void) => {
savedConsumer = consumer;
if (fileHandle) {
setTimeout(() => {
consumer({
files: [fileHandle],
});
}, 0);
}
},
};
return {
triggerConsumer: (params: { files?: MockFileHandle[] }) => {
savedConsumer?.(params);
},
};
};
beforeEach(() => {
jest.clearAllMocks();
delete (window as any).launchQueue;
});
test('shows error when launchQueue is not supported', async () => {
render(
<MemoryRouter initialEntries={['/file-handler']}>
<Route path="/file-handler">
<FileHandler />
</Route>
</MemoryRouter>,
{ useRedux: true },
);
await waitFor(() => {
expect(mockAddDangerToast).toHaveBeenCalledWith(
'File handling is not supported in this browser. Please use a modern browser like Chrome or Edge.',
);
expect(mockHistoryPush).toHaveBeenCalledWith('/superset/welcome/');
});
});
test('redirects when no files are provided', async () => {
const { triggerConsumer } = setupLaunchQueue();
render(
<MemoryRouter initialEntries={['/file-handler']}>
<Route path="/file-handler">
<FileHandler />
</Route>
</MemoryRouter>,
{ useRedux: true },
);
// Trigger the consumer with no files
triggerConsumer({ files: [] });
await waitFor(() => {
expect(mockHistoryPush).toHaveBeenCalledWith('/superset/welcome/');
});
});
test('handles CSV file correctly', async () => {
const fileHandle = createMockFileHandle('test.csv');
setupLaunchQueue(fileHandle);
render(
<MemoryRouter initialEntries={['/file-handler']}>
<Route path="/file-handler">
<FileHandler />
</Route>
</MemoryRouter>,
{ useRedux: true },
);
const modal = await screen.findByTestId('upload-modal');
expect(modal).toBeInTheDocument();
expect(screen.getByTestId('modal-show')).toHaveTextContent('true');
expect(screen.getByTestId('modal-type')).toHaveTextContent('csv');
expect(screen.getByTestId('modal-extensions')).toHaveTextContent('csv');
expect(screen.getByTestId('modal-file')).toHaveTextContent('test.csv');
});
test('handles Excel (.xls) file correctly', async () => {
const fileHandle = createMockFileHandle('test.xls');
setupLaunchQueue(fileHandle);
render(
<MemoryRouter initialEntries={['/file-handler']}>
<Route path="/file-handler">
<FileHandler />
</Route>
</MemoryRouter>,
{ useRedux: true },
);
const modal = await screen.findByTestId('upload-modal');
expect(modal).toBeInTheDocument();
expect(screen.getByTestId('modal-type')).toHaveTextContent('excel');
expect(screen.getByTestId('modal-extensions')).toHaveTextContent('xls,xlsx');
});
test('handles Excel (.xlsx) file correctly', async () => {
const fileHandle = createMockFileHandle('test.xlsx');
setupLaunchQueue(fileHandle);
render(
<MemoryRouter initialEntries={['/file-handler']}>
<Route path="/file-handler">
<FileHandler />
</Route>
</MemoryRouter>,
{ useRedux: true },
);
const modal = await screen.findByTestId('upload-modal');
expect(modal).toBeInTheDocument();
expect(screen.getByTestId('modal-type')).toHaveTextContent('excel');
expect(screen.getByTestId('modal-extensions')).toHaveTextContent('xls,xlsx');
});
test('handles Parquet file correctly', async () => {
const fileHandle = createMockFileHandle('test.parquet');
setupLaunchQueue(fileHandle);
render(
<MemoryRouter initialEntries={['/file-handler']}>
<Route path="/file-handler">
<FileHandler />
</Route>
</MemoryRouter>,
{ useRedux: true },
);
const modal = await screen.findByTestId('upload-modal');
expect(modal).toBeInTheDocument();
expect(screen.getByTestId('modal-type')).toHaveTextContent('columnar');
expect(screen.getByTestId('modal-extensions')).toHaveTextContent('parquet');
});
test('shows error for unsupported file type', async () => {
const { triggerConsumer } = setupLaunchQueue();
render(
<MemoryRouter initialEntries={['/file-handler']}>
<Route path="/file-handler">
<FileHandler />
</Route>
</MemoryRouter>,
{ useRedux: true },
);
// Trigger with unsupported file
const fileHandle = createMockFileHandle('test.pdf');
triggerConsumer({ files: [fileHandle] });
await waitFor(() => {
expect(mockAddDangerToast).toHaveBeenCalledWith(
'Unsupported file type. Please use CSV, Excel, or Columnar files.',
);
expect(mockHistoryPush).toHaveBeenCalledWith('/superset/welcome/');
});
});
test('handles file with uppercase extension', async () => {
const fileHandle = createMockFileHandle('test.CSV');
setupLaunchQueue(fileHandle);
render(
<MemoryRouter initialEntries={['/file-handler']}>
<Route path="/file-handler">
<FileHandler />
</Route>
</MemoryRouter>,
{ useRedux: true },
);
const modal = await screen.findByTestId('upload-modal');
expect(modal).toBeInTheDocument();
expect(screen.getByTestId('modal-type')).toHaveTextContent('csv');
});
test('handles errors during file processing', async () => {
const { triggerConsumer } = setupLaunchQueue();
render(
<MemoryRouter initialEntries={['/file-handler']}>
<Route path="/file-handler">
<FileHandler />
</Route>
</MemoryRouter>,
{ useRedux: true },
);
// Trigger with a file handle that throws an error
const errorFileHandle = {
getFile: async () => {
throw new Error('File access denied');
},
};
triggerConsumer({ files: [errorFileHandle] });
await waitFor(() => {
expect(mockAddDangerToast).toHaveBeenCalledWith(
'Failed to open file. Please try again.',
);
expect(mockHistoryPush).toHaveBeenCalledWith('/superset/welcome/');
});
});
test('modal close redirects to welcome page', async () => {
const fileHandle = createMockFileHandle('test.csv');
setupLaunchQueue(fileHandle);
render(
<MemoryRouter initialEntries={['/file-handler']}>
<Route path="/file-handler">
<FileHandler />
</Route>
</MemoryRouter>,
{ useRedux: true },
);
const modal = await screen.findByTestId('upload-modal');
expect(modal).toBeInTheDocument();
// Click the close button in the mocked modal
const closeButton = screen.getByRole('button', { name: 'Close' });
closeButton.click();
await waitFor(() => {
expect(mockHistoryPush).toHaveBeenCalledWith('/superset/welcome/');
});
});
test('shows loading state while waiting for file', () => {
setupLaunchQueue();
render(
<MemoryRouter initialEntries={['/file-handler']}>
<Route path="/file-handler">
<FileHandler />
</Route>
</MemoryRouter>,
{ useRedux: true },
);
// Should show loading initially before file is processed
expect(screen.getByRole('status')).toBeInTheDocument();
});