This is an automated email from the ASF dual-hosted git repository. beto pushed a commit to branch file-handler2 in repository https://gitbox.apache.org/repos/asf/superset.git
commit 80f6f93ea2309709f2f44bf957b8c530714d107c Author: Beto Dealmeida <[email protected]> AuthorDate: Wed Nov 19 15:10:38 2025 -0500 Add tests --- .../src/pages/FileHandler/index.test.tsx | 335 +++++++++++++++++++++ 1 file changed, 335 insertions(+) diff --git a/superset-frontend/src/pages/FileHandler/index.test.tsx b/superset-frontend/src/pages/FileHandler/index.test.tsx new file mode 100644 index 0000000000..2e9cf6b473 --- /dev/null +++ b/superset-frontend/src/pages/FileHandler/index.test.tsx @@ -0,0 +1,335 @@ +/** + * 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 { 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(); + +// Mock the withToasts HOC +jest.mock('src/components/MessageToasts/withToasts', () => ({ + __esModule: true, + default: (Component: any) => (props: any) => ( + <Component + {...props} + addDangerToast={mockAddDangerToast} + addSuccessToast={mockAddSuccessToast} + /> + ), +})); + +// Mock the UploadDataModal +jest.mock('src/features/databases/UploadDataModel', () => ({ + __esModule: true, + default: ({ show, onHide, type, allowedExtensions }: any) => ( + <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> + <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 +class MockFile { + name: string; + + constructor(name: string) { + this.name = name; + } +} + +interface MockFileHandle { + getFile: () => Promise<MockFile>; +} + +const createMockFileHandle = (fileName: string): MockFileHandle => ({ + getFile: async () => new MockFile(fileName), +}); + +const setupLaunchQueue = (fileHandle: MockFileHandle | null = null) => { + let savedConsumer: ((params: any) => void) | null = null; + (window as any).launchQueue = { + setConsumer: (consumer: (params: any) => void) => { + savedConsumer = consumer; + // Automatically trigger the consumer if a fileHandle is provided + if (fileHandle) { + setTimeout(() => { + consumer({ + files: [fileHandle], + }); + }, 0); + } + }, + }; + return { + triggerConsumer: (params: any) => { + if (savedConsumer) { + 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'); +}); + +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(); +});
