blob: d7400254ff617d1002312b22481fdc3746f663b7 [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 {
render,
screen,
userEvent,
waitFor,
} from 'spec/helpers/testing-library';
import { SupersetClient } from '@superset-ui/core';
import { createMemoryHistory } from 'history';
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 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();
}),
}));
// Mock the CRUD hooks
jest.mock('src/views/CRUD/hooks', () => ({
...jest.requireActual('src/views/CRUD/hooks'),
useListViewResource: jest.fn().mockReturnValue({
state: {
loading: false,
resourceCollection: [],
resourceCount: 0,
bulkSelectEnabled: false,
lastFetched: undefined,
},
setResourceCollection: jest.fn(),
hasPerm: jest.fn().mockReturnValue(true),
refreshData: jest.fn(),
fetchData: jest.fn(),
toggleBulkSelect: jest.fn(),
}),
useFavoriteStatus: jest.fn().mockReturnValue([jest.fn(), {}]),
}));
const mockDashboards = [
{
id: 1,
dashboard_title: 'Test Dashboard 1',
changed_on_delta_humanized: '1 day ago',
url: '/dashboard/1',
thumbnail_url: '/thumbnail/1',
},
{
id: 2,
dashboard_title: 'Test Dashboard 2',
changed_on_delta_humanized: '2 days ago',
url: '/dashboard/2',
thumbnail_url: '/thumbnail/2',
},
];
const mockUser = {
userId: 1,
firstName: 'Test',
lastName: 'User',
};
const defaultProps = {
user: mockUser,
addDangerToast: jest.fn(),
addSuccessToast: jest.fn(),
mine: [],
showThumbnails: true,
otherTabData: [],
otherTabFilters: [],
otherTabTitle: 'Examples',
};
describe('DashboardTable', () => {
const history = createMemoryHistory();
const store = configureStore({
reducer: {
dashboards: (state = { dashboards: [] }) => state,
},
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/*',
{
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(),
}));
});
it('renders loading state initially', () => {
render(
<Router history={history}>
<DashboardTable {...defaultProps} />
</Router>,
{ store },
);
expect(screen.getByRole('img', { name: 'empty' })).toBeInTheDocument();
});
it('renders empty state when no dashboards', async () => {
render(
<Router history={history}>
<DashboardTable {...defaultProps} />
</Router>,
{ store },
);
await waitFor(() => {
expect(screen.getByText('No results')).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(),
}));
render(
<Router history={history}>
<DashboardTable {...defaultProps} mine={mockDashboards} />
</Router>,
{ store },
);
await waitFor(() => {
mockDashboards.forEach(dashboard => {
expect(screen.getByText(dashboard.dashboard_title)).toBeInTheDocument();
});
});
});
it('switches to Mine tab correctly', async () => {
const props = {
...defaultProps,
mine: mockDashboards,
};
render(
<Router history={history}>
<DashboardTable {...props} />
</Router>,
{ store },
);
const mineTab = screen.getByRole('menuitem', { name: /mine/i });
await userEvent.click(mineTab);
await waitFor(() => {
expect(mineTab).toHaveClass('ant-menu-item-selected');
});
});
it('handles create dashboard button click', async () => {
const assignMock = jest.fn();
Object.defineProperty(window, 'location', {
value: { assign: assignMock },
writable: true,
});
render(
<Router history={history}>
<DashboardTable {...defaultProps} />
</Router>,
{ 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(
<Router history={history}>
<DashboardTable {...props} />
</Router>,
{ 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(
<Router history={history}>
<DashboardTable {...props} />
</Router>,
{ 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);
expect(screen.getByRole('status')).toBeInTheDocument();
});
it('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(
<Router history={history}>
<DashboardTable {...props} />
</Router>,
{ 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();
});
});