diff --git a/superset-frontend/src/features/home/DashboardTable.test.tsx b/superset-frontend/src/features/home/DashboardTable.test.tsx index f33673539ad..9962027f304 100644 --- a/superset-frontend/src/features/home/DashboardTable.test.tsx +++ b/superset-frontend/src/features/home/DashboardTable.test.tsx @@ -105,301 +105,402 @@ const defaultProps = { otherTabTitle: 'Examples', }; -// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks -describe('DashboardTable', () => { - const history = createMemoryHistory(); - const store = configureStore({ - reducer: { - dashboards: (state = { dashboards: [] }) => state, +const history = createMemoryHistory(); +const store = configureStore({ + reducer: { + dashboards: (state = { dashboards: [] }) => state, + }, + preloadedState: { + dashboards: { + dashboards: mockDashboards, }, - preloadedState: { - dashboards: { - dashboards: mockDashboards, - }, - }, - }); + }, +}); - beforeEach(() => { - jest.spyOn(SupersetClient, 'get').mockImplementation(() => - Promise.resolve({ - json: { - result: mockDashboards[0], - }, - response: new Response(), - }), - ); - - fetchMock.get( - 'glob:*/api/v1/dashboard/*', - { +beforeEach(() => { + jest.spyOn(SupersetClient, 'get').mockImplementation(() => + Promise.resolve({ + json: { result: mockDashboards[0], }, - { overwriteRoutes: true }, - ); // Add overwriteRoutes option + response: new Response(), + }), + ); - // Mock loading state for first render - jest.spyOn(hooks, 'useListViewResource').mockImplementationOnce(() => ({ - state: { - loading: true, - resourceCollection: [], - resourceCount: 0, - bulkSelectEnabled: false, - lastFetched: undefined, - }, - setResourceCollection: jest.fn(), - hasPerm: jest.fn().mockReturnValue(true), - refreshData: jest.fn(), - fetchData: jest.fn(), - toggleBulkSelect: jest.fn(), - })); - }); + fetchMock.get( + 'glob:*/api/v1/dashboard/*', + { + result: mockDashboards[0], + }, + { overwriteRoutes: true }, + ); // Add overwriteRoutes option - test('renders loading state initially', () => { - render( - - - , - { store }, - ); - expect(screen.getByRole('img', { name: 'empty' })).toBeInTheDocument(); - }); + // Mock loading state for first render + jest.spyOn(hooks, 'useListViewResource').mockImplementationOnce(() => ({ + state: { + loading: true, + resourceCollection: [], + resourceCount: 0, + bulkSelectEnabled: false, + lastFetched: undefined, + }, + setResourceCollection: jest.fn(), + hasPerm: jest.fn().mockReturnValue(true), + refreshData: jest.fn(), + fetchData: jest.fn(), + toggleBulkSelect: jest.fn(), + })); +}); - test('renders empty state when no dashboards', async () => { - render( - - - , - { store }, - ); +test('renders loading state initially', () => { + render( + + + , + { store }, + ); + expect(screen.getByRole('img', { name: 'empty' })).toBeInTheDocument(); +}); - await waitFor(() => { - expect(screen.getByText('No results')).toBeInTheDocument(); - }); - }); +test('renders empty state when no dashboards', async () => { + render( + + + , + { store }, + ); - test('renders dashboard cards when data is loaded', async () => { - jest.spyOn(hooks, 'useListViewResource').mockImplementation(() => ({ - state: { - loading: false, - resourceCollection: mockDashboards, - resourceCount: mockDashboards.length, - bulkSelectEnabled: false, - lastFetched: new Date().toISOString(), - }, - setResourceCollection: jest.fn(), - hasPerm: jest.fn().mockReturnValue(true), - refreshData: jest.fn(), - fetchData: jest.fn(), - toggleBulkSelect: jest.fn(), - })); - - render( - - - , - { store }, - ); - - await waitFor(() => { - mockDashboards.forEach(dashboard => { - expect(screen.getByText(dashboard.dashboard_title)).toBeInTheDocument(); - }); - }); - }); - - test('switches to Mine tab correctly', async () => { - const props = { - ...defaultProps, - mine: mockDashboards, - }; - - render( - - - , - { store }, - ); - - const mineTab = screen.getByRole('menuitem', { name: /mine/i }); - await userEvent.click(mineTab); - await waitFor(() => { - expect(mineTab).toHaveClass('ant-menu-item-selected'); - }); - }); - - test('handles create dashboard button click', async () => { - const assignMock = jest.fn(); - Object.defineProperty(window, 'location', { - value: { assign: assignMock }, - writable: true, - }); - - render( - - - , - { store }, - ); - - const createButton = screen.getByRole('button', { name: /dashboard$/i }); - await userEvent.click(createButton); - expect(assignMock).toHaveBeenCalledWith('/dashboard/new'); - }); - - test('switches to Other tab when available', async () => { - const props = { - ...defaultProps, - otherTabData: mockDashboards, - otherTabTitle: 'Examples', - }; - - render( - - - , - { store }, - ); - - const otherTab = screen.getByRole('tab', { name: 'Examples' }); - await userEvent.click(otherTab); - expect(otherTab).toHaveClass('active'); - }); - - test('handles bulk dashboard export with correct ID and shows spinner', async () => { - // Mock export to take some time before calling the done callback - mockExport.mockImplementation( - (resource: string, ids: number[], done: () => void) => - new Promise(resolve => { - setTimeout(() => { - done(); - resolve(); - }, 100); - }), - ); - - const props = { - ...defaultProps, - mine: mockDashboards, - }; - - jest.spyOn(hooks, 'useListViewResource').mockImplementation(() => ({ - state: { - loading: false, - resourceCollection: mockDashboards, - resourceCount: mockDashboards.length, - bulkSelectEnabled: false, - lastFetched: new Date().toISOString(), - }, - setResourceCollection: jest.fn(), - hasPerm: jest.fn().mockReturnValue(true), - refreshData: jest.fn(), - fetchData: jest.fn(), - toggleBulkSelect: jest.fn(), - })); - - render( - - - , - { store }, - ); - - const moreOptionsButton = screen.getAllByRole('img', { - name: 'more', - })[0]; - await userEvent.click(moreOptionsButton); - - // Wait for dropdown menu to appear - await waitFor(() => { - expect(screen.getByText('Export')).toBeInTheDocument(); - }); - - const exportOption = screen.getByText('Export'); - await userEvent.click(exportOption); - - // Verify spinner shows up during export - await waitFor(() => { - expect(screen.getByRole('status')).toBeInTheDocument(); - }); - - // Verify the export was called with correct parameters - expect(mockExport).toHaveBeenCalledWith( - 'dashboard', - [1], - expect.any(Function), - ); - - // Wait for export to complete and spinner to disappear - await waitFor( - () => { - expect(screen.queryByRole('status')).not.toBeInTheDocument(); - }, - { timeout: 3000 }, - ); - }); - - test('handles dashboard deletion confirmation', async () => { - const props = { - ...defaultProps, - mine: mockDashboards, - }; - - const refreshDataMock = jest.fn(); - jest.spyOn(hooks, 'useListViewResource').mockImplementation(() => ({ - state: { - loading: false, - resourceCollection: mockDashboards, - resourceCount: mockDashboards.length, - bulkSelectEnabled: false, - lastFetched: new Date().toISOString(), - }, - setResourceCollection: jest.fn(), - hasPerm: jest.fn().mockReturnValue(true), - refreshData: refreshDataMock, - fetchData: jest.fn(), - toggleBulkSelect: jest.fn(), - })); - - render( - - - , - { store }, - ); - - const moreOptionsButton = screen.getAllByLabelText('more')[0]; - await userEvent.click(moreOptionsButton); - - await waitFor(() => { - expect(screen.getByText('Delete')).toBeInTheDocument(); - }); - - const deleteOption = screen.getByText('Delete'); - await userEvent.click(deleteOption); - - // Verify Delete button is initially disabled - const confirmDeleteButton = screen.getByTestId('modal-confirm-button'); - expect(confirmDeleteButton).toBeDisabled(); - - // Type DELETE in the confirmation input - const deleteInput = screen.getByTestId('delete-modal-input'); - await userEvent.type(deleteInput, 'DELETE'); - - // Verify Delete button becomes enabled - await waitFor(() => { - expect(confirmDeleteButton).toBeEnabled(); - }); - - // Click the now-enabled Delete button - await userEvent.click(confirmDeleteButton); - - await waitFor( - () => { - expect(refreshDataMock).toHaveBeenCalled(); - }, - { timeout: 3000 }, - ); - - expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText('No results')).toBeInTheDocument(); + }); +}); + +test('renders dashboard cards when data is loaded', async () => { + jest.spyOn(hooks, 'useListViewResource').mockImplementation(() => ({ + state: { + loading: false, + resourceCollection: mockDashboards, + resourceCount: mockDashboards.length, + bulkSelectEnabled: false, + lastFetched: new Date().toISOString(), + }, + setResourceCollection: jest.fn(), + hasPerm: jest.fn().mockReturnValue(true), + refreshData: jest.fn(), + fetchData: jest.fn(), + toggleBulkSelect: jest.fn(), + })); + + render( + + + , + { store }, + ); + + await waitFor(() => { + mockDashboards.forEach(dashboard => { + expect(screen.getByText(dashboard.dashboard_title)).toBeInTheDocument(); + }); + }); +}); + +test('switches to Mine tab correctly', async () => { + const props = { + ...defaultProps, + mine: mockDashboards, + }; + + render( + + + , + { store }, + ); + + const mineTab = screen.getByRole('menuitem', { name: /mine/i }); + await userEvent.click(mineTab); + await waitFor(() => { + expect(mineTab).toHaveClass('ant-menu-item-selected'); + }); +}); + +test('handles create dashboard button click', async () => { + const assignMock = jest.fn(); + Object.defineProperty(window, 'location', { + value: { assign: assignMock }, + writable: true, + }); + + render( + + + , + { store }, + ); + + const createButton = screen.getByRole('button', { name: /dashboard$/i }); + await userEvent.click(createButton); + expect(assignMock).toHaveBeenCalledWith('/dashboard/new'); +}); + +test('switches to Other tab when available', async () => { + const props = { + ...defaultProps, + otherTabData: mockDashboards, + otherTabTitle: 'Examples', + }; + + render( + + + , + { store }, + ); + + const otherTab = screen.getByRole('tab', { name: 'Examples' }); + await userEvent.click(otherTab); + expect(otherTab).toHaveClass('active'); +}); + +test('handles bulk dashboard export with correct ID and shows spinner', async () => { + // Mock export to take some time before calling the done callback + mockExport.mockImplementation( + (resource: string, ids: number[], done: () => void) => + new Promise(resolve => { + setTimeout(() => { + done(); + resolve(); + }, 100); + }), + ); + + const props = { + ...defaultProps, + mine: mockDashboards, + }; + + jest.spyOn(hooks, 'useListViewResource').mockImplementation(() => ({ + state: { + loading: false, + resourceCollection: mockDashboards, + resourceCount: mockDashboards.length, + bulkSelectEnabled: false, + lastFetched: new Date().toISOString(), + }, + setResourceCollection: jest.fn(), + hasPerm: jest.fn().mockReturnValue(true), + refreshData: jest.fn(), + fetchData: jest.fn(), + toggleBulkSelect: jest.fn(), + })); + + render( + + + , + { store }, + ); + + const moreOptionsButton = screen.getAllByRole('img', { + name: 'more', + })[0]; + await userEvent.click(moreOptionsButton); + + // Wait for dropdown menu to appear + await waitFor(() => { + expect(screen.getByText('Export')).toBeInTheDocument(); + }); + + const exportOption = screen.getByText('Export'); + await userEvent.click(exportOption); + + // Verify spinner shows up during export + await waitFor(() => { + expect(screen.getByRole('status')).toBeInTheDocument(); + }); + + // Verify the export was called with correct parameters + expect(mockExport).toHaveBeenCalledWith( + 'dashboard', + [1], + expect.any(Function), + ); + + // Wait for export to complete and spinner to disappear + await waitFor( + () => { + expect(screen.queryByRole('status')).not.toBeInTheDocument(); + }, + { timeout: 3000 }, + ); +}); + +test('handles dashboard deletion confirmation', async () => { + const props = { + ...defaultProps, + mine: mockDashboards, + }; + + const refreshDataMock = jest.fn(); + jest.spyOn(hooks, 'useListViewResource').mockImplementation(() => ({ + state: { + loading: false, + resourceCollection: mockDashboards, + resourceCount: mockDashboards.length, + bulkSelectEnabled: false, + lastFetched: new Date().toISOString(), + }, + setResourceCollection: jest.fn(), + hasPerm: jest.fn().mockReturnValue(true), + refreshData: refreshDataMock, + fetchData: jest.fn(), + toggleBulkSelect: jest.fn(), + })); + + render( + + + , + { store }, + ); + + const moreOptionsButton = screen.getAllByLabelText('more')[0]; + await userEvent.click(moreOptionsButton); + + await waitFor(() => { + expect(screen.getByText('Delete')).toBeInTheDocument(); + }); + + const deleteOption = screen.getByText('Delete'); + await userEvent.click(deleteOption); + + // Verify Delete button is initially disabled + const confirmDeleteButton = screen.getByTestId('modal-confirm-button'); + expect(confirmDeleteButton).toBeDisabled(); + + // Type DELETE in the confirmation input + const deleteInput = screen.getByTestId('delete-modal-input'); + await userEvent.type(deleteInput, 'DELETE'); + + // Verify Delete button becomes enabled + await waitFor(() => { + expect(confirmDeleteButton).toBeEnabled(); + }); + + // Click the now-enabled Delete button + await userEvent.click(confirmDeleteButton); + + await waitFor( + () => { + expect(refreshDataMock).toHaveBeenCalled(); + }, + { timeout: 3000 }, + ); + + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); +}); + +test('passes correct parameters to handleDashboardDelete for Other tab', async () => { + const mockHandleDashboardDelete = + require('src/views/CRUD/utils').handleDashboardDelete; + mockHandleDashboardDelete.mockClear(); + + const refreshDataMock = jest.fn(); + const fetchDataMock = jest.fn().mockName('getData'); + + jest.spyOn(hooks, 'useListViewResource').mockImplementation(() => ({ + state: { + loading: false, + resourceCollection: mockDashboards, + resourceCount: mockDashboards.length, + bulkSelectEnabled: false, + lastFetched: new Date().toISOString(), + }, + setResourceCollection: jest.fn(), + hasPerm: jest.fn().mockReturnValue(true), + refreshData: refreshDataMock, + fetchData: fetchDataMock, + toggleBulkSelect: jest.fn(), + })); + + const props = { + ...defaultProps, + otherTabData: mockDashboards, + otherTabTitle: 'All', + }; + + render( + + + , + { store }, + ); + + await waitFor(() => { + expect(screen.getByText('Test Dashboard 1')).toBeInTheDocument(); + }); + + const otherTab = screen.getByRole('tab', { name: 'All' }); + await userEvent.click(otherTab); + + await waitFor(() => { + expect(screen.getByText('Test Dashboard 1')).toBeInTheDocument(); + }); + + const moreOptionsButtons = screen.getAllByLabelText(/more|options/i); + expect(moreOptionsButtons.length).toBeGreaterThan(0); + + await userEvent.click(moreOptionsButtons[0]); + + await waitFor(() => { + expect(screen.getByText('Delete')).toBeInTheDocument(); + }); + + const deleteOption = screen.getByText('Delete'); + await userEvent.click(deleteOption); + + await waitFor(() => { + expect(screen.getByText('Please confirm')).toBeInTheDocument(); + expect(screen.getByTestId('delete-modal-input')).toBeInTheDocument(); + }); + + const deleteInput = screen.getByTestId('delete-modal-input'); + await userEvent.type(deleteInput, 'DELETE'); + + const confirmDeleteButton = screen.getByTestId('modal-confirm-button'); + + await waitFor(() => { + expect(confirmDeleteButton).toBeEnabled(); + }); + + await userEvent.click(confirmDeleteButton); + + await waitFor(() => { + expect(mockHandleDashboardDelete).toHaveBeenCalled(); + }); + + expect(mockHandleDashboardDelete).toHaveBeenCalledWith( + expect.objectContaining({ + id: 1, + dashboard_title: 'Test Dashboard 1', + }), + expect.any(Function), + expect.any(Function), + expect.any(Function), + 'Other', + mockUser.userId, + expect.any(Function), + ); + + const lastCall = mockHandleDashboardDelete.mock.calls[0]; + const getDataParam = lastCall[6]; + + getDataParam('Other'); + expect(fetchDataMock).toHaveBeenCalledWith({ + filters: [], + pageIndex: 0, + pageSize: 5, + sortBy: [{ desc: true, id: 'changed_on_delta_humanized' }], }); }); diff --git a/superset-frontend/src/features/home/DashboardTable.tsx b/superset-frontend/src/features/home/DashboardTable.tsx index efa2256465e..9857df4ca47 100644 --- a/superset-frontend/src/features/home/DashboardTable.tsx +++ b/superset-frontend/src/features/home/DashboardTable.tsx @@ -242,6 +242,7 @@ function DashboardTable({ addDangerToast, activeTab, user?.userId, + getData, ); setDashboardToDelete(null); }} diff --git a/superset-frontend/src/views/CRUD/utils.tsx b/superset-frontend/src/views/CRUD/utils.tsx index 45c2824347a..c88aa382598 100644 --- a/superset-frontend/src/views/CRUD/utils.tsx +++ b/superset-frontend/src/views/CRUD/utils.tsx @@ -310,6 +310,7 @@ export function handleDashboardDelete( addDangerToast: (arg0: string) => void, dashboardFilter?: string, userId?: string | number, + getData?: (tab: TableTab) => void, ) { return SupersetClient.delete({ endpoint: `/api/v1/dashboard/${id}`, @@ -333,6 +334,8 @@ export function handleDashboardDelete( ], }; if (dashboardFilter === 'Mine') refreshData(filters); + else if (dashboardFilter === 'Other' && getData) + getData(dashboardFilter as TableTab); else refreshData(); addSuccessToast(t('Deleted: %s', dashboardTitle)); },