diff --git a/superset-frontend/src/features/home/DashboardTable.test.tsx b/superset-frontend/src/features/home/DashboardTable.test.tsx
index d7400254ff6..b258794ff76 100644
--- a/superset-frontend/src/features/home/DashboardTable.test.tsx
+++ b/superset-frontend/src/features/home/DashboardTable.test.tsx
@@ -28,18 +28,35 @@ import { Router } from 'react-router-dom';
import { configureStore } from '@reduxjs/toolkit';
import fetchMock from 'fetch-mock';
import * as hooks from 'src/views/CRUD/hooks';
+import handleResourceExport from 'src/utils/export';
+import { handleDashboardDelete } from 'src/views/CRUD/utils';
import DashboardTable from './DashboardTable';
-jest.mock('src/views/CRUD/utils', () => ({
- ...jest.requireActual('src/views/CRUD/utils'),
- handleDashboardDelete: jest
- .fn()
- .mockImplementation((dashboard, refreshData) => {
- refreshData();
- return Promise.resolve();
- }),
+const mockHandleDashboardDelete = handleDashboardDelete as jest.Mock;
+
+// Mock the export module
+jest.mock('src/utils/export', () => ({
+ __esModule: true,
+ default: jest.fn(),
}));
+const mockExport = handleResourceExport as jest.MockedFunction<
+ typeof handleResourceExport
+>;
+
+jest.mock('src/views/CRUD/utils', () => {
+ const actual = jest.requireActual('src/views/CRUD/utils');
+ return {
+ ...actual,
+ handleDashboardDelete: jest
+ .fn()
+ .mockImplementation((dashboard, refreshData) => {
+ refreshData();
+ return Promise.resolve();
+ }),
+ };
+});
+
// Mock the CRUD hooks
jest.mock('src/views/CRUD/hooks', () => ({
...jest.requireActual('src/views/CRUD/hooks'),
@@ -94,256 +111,400 @@ const defaultProps = {
otherTabTitle: 'Examples',
};
-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
+
+ // 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 loading state initially', () => {
+ render(
+
+
+ ,
+ { store },
+ );
+ expect(screen.getByRole('img', { name: 'empty' })).toBeInTheDocument();
+});
+
+test('renders empty state when no dashboards', async () => {
+ render(
+
+
+ ,
+ { store },
+ );
+
+ await waitFor(() => {
+ expect(screen.getByText('No results')).toBeInTheDocument();
});
+});
- it('renders loading state initially', () => {
- render(
-
-
- ,
- { store },
- );
- expect(screen.getByRole('img', { name: 'empty' })).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(),
+ }));
- it('renders empty state when no dashboards', async () => {
- render(
-
-
- ,
- { store },
- );
+ render(
+
+
+ ,
+ { store },
+ );
- await waitFor(() => {
- expect(screen.getByText('No results')).toBeInTheDocument();
+ await waitFor(() => {
+ mockDashboards.forEach(dashboard => {
+ expect(screen.getByText(dashboard.dashboard_title)).toBeInTheDocument();
});
});
+});
- it('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(),
- }));
+test('switches to Mine tab correctly', async () => {
+ const props = {
+ ...defaultProps,
+ mine: mockDashboards,
+ };
- render(
-
-
- ,
- { store },
- );
+ render(
+
+
+ ,
+ { store },
+ );
- await waitFor(() => {
- mockDashboards.forEach(dashboard => {
- expect(screen.getByText(dashboard.dashboard_title)).toBeInTheDocument();
- });
- });
+ 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,
});
- it('switches to Mine tab correctly', async () => {
- const props = {
- ...defaultProps,
- mine: mockDashboards,
- };
+ render(
+
+
+ ,
+ { store },
+ );
- render(
-
-
- ,
- { store },
- );
+ const createButton = screen.getByRole('button', { name: /dashboard$/i });
+ await userEvent.click(createButton);
+ expect(assignMock).toHaveBeenCalledWith('/dashboard/new');
+});
- const mineTab = screen.getByRole('menuitem', { name: /mine/i });
- await userEvent.click(mineTab);
- await waitFor(() => {
- expect(mineTab).toHaveClass('ant-menu-item-selected');
- });
+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();
});
- it('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');
- });
-
- it('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');
- });
-
- it('handles bulk dashboard export', async () => {
- const props = {
- ...defaultProps,
- mine: mockDashboards,
- };
-
- 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);
+ const exportOption = screen.getByText('Export');
+ await userEvent.click(exportOption);
+ // Verify spinner shows up during export
+ await waitFor(() => {
expect(screen.getByRole('status')).toBeInTheDocument();
});
- it('handles dashboard deletion confirmation', async () => {
- const props = {
- ...defaultProps,
- mine: mockDashboards,
- };
+ // Verify the export was called with correct parameters
+ expect(mockExport).toHaveBeenCalledWith(
+ 'dashboard',
+ [1],
+ expect.any(Function),
+ );
- 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(),
- }));
+ // Wait for export to complete and spinner to disappear
+ await waitFor(
+ () => {
+ expect(screen.queryByRole('status')).not.toBeInTheDocument();
+ },
+ { timeout: 3000 },
+ );
+});
- render(
-
-
- ,
- { store },
- );
+test('handles dashboard deletion confirmation', async () => {
+ const props = {
+ ...defaultProps,
+ mine: mockDashboards,
+ };
- const moreOptionsButton = screen.getAllByLabelText('more')[0];
- await userEvent.click(moreOptionsButton);
+ 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(),
+ }));
- await waitFor(() => {
- expect(screen.getByText('Delete')).toBeInTheDocument();
- });
+ render(
+
+
+ ,
+ { store },
+ );
- const deleteOption = screen.getByText('Delete');
- await userEvent.click(deleteOption);
+ const moreOptionsButton = screen.getAllByLabelText('more')[0];
+ await userEvent.click(moreOptionsButton);
- // Verify Delete button is initially disabled
- const confirmDeleteButton = screen.getByTestId('modal-confirm-button');
- expect(confirmDeleteButton).toBeDisabled();
+ await waitFor(() => {
+ expect(screen.getByText('Delete')).toBeInTheDocument();
+ });
- // Type DELETE in the confirmation input
- const deleteInput = screen.getByTestId('delete-modal-input');
- await userEvent.type(deleteInput, 'DELETE');
+ const deleteOption = screen.getByText('Delete');
+ await userEvent.click(deleteOption);
- // Verify Delete button becomes enabled
- await waitFor(() => {
- expect(confirmDeleteButton).toBeEnabled();
- });
+ // Verify Delete button is initially disabled
+ const confirmDeleteButton = screen.getByTestId('modal-confirm-button');
+ expect(confirmDeleteButton).toBeDisabled();
- // Click the now-enabled Delete button
- await userEvent.click(confirmDeleteButton);
+ // Type DELETE in the confirmation input
+ const deleteInput = screen.getByTestId('delete-modal-input');
+ await userEvent.type(deleteInput, 'DELETE');
- await waitFor(
- () => {
- expect(refreshDataMock).toHaveBeenCalled();
- },
- { timeout: 3000 },
- );
+ // Verify Delete button becomes enabled
+ await waitFor(() => {
+ expect(confirmDeleteButton).toBeEnabled();
+ });
- expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
+ // 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 () => {
+ 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 bd23d455480..9e685f242cd 100644
--- a/superset-frontend/src/features/home/DashboardTable.tsx
+++ b/superset-frontend/src/features/home/DashboardTable.tsx
@@ -237,6 +237,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 f7e4082cfb7..5e3df011ebd 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));
},