chore(tests): converting enzyme to RTL, part 3 (#32363)

This commit is contained in:
Evan Rusackas
2025-02-24 12:08:52 -07:00
committed by GitHub
parent bc02f05613
commit b43e2ac8f4
7 changed files with 857 additions and 985 deletions

View File

@@ -17,16 +17,11 @@
* under the License.
*/
import { Provider } from 'react-redux';
import { styledMount as mount } from 'spec/helpers/theming';
import sinon from 'sinon';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import sinon from 'sinon';
import DeleteComponentButton from 'src/dashboard/components/DeleteComponentButton';
import EditableTitle from 'src/components/EditableTitle';
import HoverMenu from 'src/dashboard/components/menu/HoverMenu';
import WithPopoverMenu from 'src/dashboard/components/menu/WithPopoverMenu';
import { Draggable } from 'src/dashboard/components/dnd/DragDroppable';
import { render, screen, fireEvent } from 'spec/helpers/testing-library';
import Header from 'src/dashboard/components/gridComponents/Header';
import newComponentFactory from 'src/dashboard/util/newComponentFactory';
import {
@@ -53,49 +48,53 @@ describe('Header', () => {
};
function setup(overrideProps) {
// We have to wrap provide DragDropContext for the underlying DragDroppable
// otherwise we cannot assert on DragDroppable children
const wrapper = mount(
return render(
<Provider store={mockStoreWithTabs}>
<DndProvider backend={HTML5Backend}>
<Header {...props} {...overrideProps} />
</DndProvider>
</Provider>,
);
return wrapper;
}
it('should render a Draggable', () => {
const wrapper = setup();
expect(wrapper.find(Draggable)).toBeTruthy();
setup();
expect(screen.getByTestId('dragdroppable-object')).toBeInTheDocument();
});
it('should render a WithPopoverMenu', () => {
const wrapper = setup();
expect(wrapper.find(WithPopoverMenu)).toBeTruthy();
setup();
expect(screen.getByRole('none')).toBeInTheDocument();
});
it('should render a HoverMenu in editMode', () => {
let wrapper = setup();
expect(wrapper.find(HoverMenu).length).toBe(0);
setup();
expect(screen.queryByTestId('hover-menu')).not.toBeInTheDocument();
// we cannot set props on the Header because of the WithDragDropContext wrapper
wrapper = setup({ editMode: true });
expect(wrapper.find(HoverMenu).length).toBeGreaterThan(0);
setup({ editMode: true });
const hoverMenus = screen.getAllByTestId('hover-menu');
expect(hoverMenus[0]).toBeInTheDocument();
});
it('should render an EditableTitle with meta.text', () => {
const wrapper = setup();
expect(wrapper.find(EditableTitle)).toBeTruthy();
expect(wrapper.find('.editable-title').text()).toBe(
props.component.meta.text,
);
setup();
const titleElement = screen.getByTestId('editable-title-input');
expect(titleElement).toBeInTheDocument();
expect(titleElement).toHaveTextContent(props.component.meta.text);
});
it('should call updateComponents when EditableTitle changes', () => {
const updateComponents = sinon.spy();
const wrapper = setup({ editMode: true, updateComponents });
wrapper.find(EditableTitle).prop('onSaveTitle')('New title');
setup({ editMode: true, updateComponents });
// First click to enter edit mode
const titleButton = screen.getByTestId('editable-title-input');
fireEvent.click(titleButton);
// Then change the input value and blur to trigger save
const titleInput = screen.getByTestId('editable-title-input');
fireEvent.change(titleInput, { target: { value: 'New title' } });
fireEvent.blur(titleInput);
const headerId = props.component.id;
expect(updateComponents.callCount).toBe(1);
@@ -105,33 +104,33 @@ describe('Header', () => {
});
it('should render a DeleteComponentButton when focused in editMode', () => {
const wrapper = setup({ editMode: true });
wrapper.find(WithPopoverMenu).simulate('click'); // focus
expect(wrapper.find(DeleteComponentButton)).toBeTruthy();
setup({ editMode: true });
const trashButton = screen.getByRole('img', { name: 'trash' });
expect(trashButton).toBeInTheDocument();
});
it('should call deleteComponent when deleted', () => {
const deleteComponent = sinon.spy();
const wrapper = setup({ editMode: true, deleteComponent });
wrapper.find(WithPopoverMenu).simulate('click'); // focus
wrapper.find(DeleteComponentButton).simulate('click');
setup({ editMode: true, deleteComponent });
const trashButton = screen.getByRole('img', { name: 'trash' });
fireEvent.click(trashButton.parentElement);
expect(deleteComponent.callCount).toBe(1);
});
it('should render the AnchorLink in view mode', () => {
const wrapper = setup();
expect(wrapper.find('AnchorLink')).toBeTruthy();
setup();
expect(screen.getByTestId('anchor-link')).toBeInTheDocument();
});
it('should not render the AnchorLink in edit mode', () => {
const wrapper = setup({ editMode: true });
expect(wrapper.find('AnchorLink').length).toBe(0);
setup({ editMode: true });
expect(screen.queryByTestId('anchor-link')).not.toBeInTheDocument();
});
it('should not render the AnchorLink in embedded mode', () => {
const wrapper = setup({ embeddedMode: true });
expect(wrapper.find('AnchorLink').length).toBe(0);
setup({ embeddedMode: true });
expect(screen.queryByTestId('anchor-link')).not.toBeInTheDocument();
});
});

View File

@@ -18,18 +18,17 @@
*/
import fetchMock from 'fetch-mock';
import configureStore from 'redux-mock-store';
import { Provider } from 'react-redux';
import thunk from 'redux-thunk';
import { styledMount as mount } from 'spec/helpers/theming';
import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
import { Switch } from 'src/components/Switch';
import ListView from 'src/components/ListView';
import SubMenu from 'src/features/home/SubMenu';
import {
render,
screen,
fireEvent,
waitFor,
} from 'spec/helpers/testing-library';
import { MemoryRouter } from 'react-router-dom';
import { QueryParamProvider } from 'use-query-params';
import AlertList from 'src/pages/AlertReportList';
import IndeterminateCheckbox from 'src/components/IndeterminateCheckbox';
import { act } from 'spec/helpers/testing-library';
// store needed for withToasts(AlertList)
const mockStore = configureStore([thunk]);
const store = mockStore({});
@@ -84,102 +83,153 @@ fetchMock.put(alertsEndpoint, { ...mockalerts[0], active: false });
fetchMock.delete(alertEndpoint, {});
fetchMock.delete(alertsEndpoint, {});
async function mountAndWait(props = {}) {
const mounted = mount(
<Provider store={store}>
<AlertList store={store} user={mockUser} {...props} />
</Provider>,
const renderAlertList = (props = {}) =>
render(
<MemoryRouter>
<QueryParamProvider>
<AlertList user={mockUser} {...props} />
</QueryParamProvider>
</MemoryRouter>,
{
useRedux: true,
store,
},
);
await waitForComponentToPaint(mounted);
return mounted;
}
describe('AlertList', () => {
let wrapper;
beforeAll(async () => {
wrapper = await mountAndWait();
beforeEach(() => {
fetchMock.resetHistory();
});
it('renders', async () => {
expect(wrapper.find(AlertList)).toBeTruthy();
renderAlertList();
expect(await screen.findByText('Alerts & reports')).toBeInTheDocument();
});
it('renders a SubMenu', async () => {
expect(wrapper.find(SubMenu)).toBeTruthy();
renderAlertList();
expect(await screen.findByRole('navigation')).toBeInTheDocument();
});
it('renders a ListView', async () => {
expect(wrapper.find(ListView)).toBeTruthy();
renderAlertList();
expect(await screen.findByTestId('alerts-list-view')).toBeInTheDocument();
});
it('renders switches', async () => {
expect(wrapper.find(Switch)).toHaveLength(3);
renderAlertList();
// Wait for the list to load first
await screen.findByTestId('alerts-list-view');
const switches = await screen.findAllByRole('switch');
expect(switches).toHaveLength(3);
});
it('deletes', async () => {
act(() => {
wrapper.find('[data-test="delete-action"]').first().props().onClick();
});
await waitForComponentToPaint(wrapper);
renderAlertList();
act(() => {
wrapper
.find('#delete')
.first()
.props()
.onChange({ target: { value: 'DELETE' } });
});
await waitForComponentToPaint(wrapper);
act(() => {
wrapper
.find('[data-test="modal-confirm-button"]')
.last()
.props()
.onClick();
});
// Wait for list to load
await screen.findByTestId('alerts-list-view');
await waitForComponentToPaint(wrapper);
// Find and click first delete button
const deleteButtons = await screen.findAllByTestId('delete-action');
fireEvent.click(deleteButtons[0]);
expect(fetchMock.calls(/report\/0/, 'DELETE')).toHaveLength(1);
});
// Wait for modal to appear and find the delete input
const deleteInput = await screen.findByTestId('delete-modal-input');
fireEvent.change(deleteInput, { target: { value: 'DELETE' } });
// Click confirm button
const confirmButton = await screen.findByTestId('modal-confirm-button');
fireEvent.click(confirmButton);
// Wait for delete request
await waitFor(() => {
expect(fetchMock.calls(/report\/0/, 'DELETE')).toHaveLength(1);
});
}, 15000);
it('shows/hides bulk actions when bulk actions is clicked', async () => {
const button = wrapper.find('[data-test="bulk-select-toggle"]').first();
act(() => {
button.props().onClick();
});
await waitForComponentToPaint(wrapper);
expect(wrapper.find(IndeterminateCheckbox)).toHaveLength(
mockalerts.length + 1, // 1 for each row and 1 for select all
);
});
renderAlertList();
// Wait for list to load and initial state
await screen.findByTestId('alerts-list-view');
expect(
screen.queryByTestId('bulk-select-controls'),
).not.toBeInTheDocument();
// Click bulk select toggle
const bulkSelectButton = await screen.findByTestId('bulk-select-toggle');
fireEvent.click(bulkSelectButton);
// Verify bulk select controls appear
expect(
await screen.findByTestId('bulk-select-controls'),
).toBeInTheDocument();
}, 15000);
it('hides bulk actions when switch between alert and report list', async () => {
expect(wrapper.find(IndeterminateCheckbox)).toHaveLength(
mockalerts.length + 1,
);
expect(wrapper.find('[data-test="alert-list"]').hasClass('active')).toBe(
true,
);
expect(wrapper.find('[data-test="report-list"]').hasClass('active')).toBe(
false,
);
// Start with alert list
renderAlertList();
const reportWrapper = await mountAndWait({ isReportEnabled: true });
// Wait for list to load
await screen.findByTestId('alerts-list-view');
expect(fetchMock.calls(/report\/\?q/)[2][0]).toMatchInlineSnapshot(
`"http://localhost/api/v1/report/?q=(filters:!((col:type,opr:eq,value:Report)),order_column:name,order_direction:desc,page:0,page_size:25)"`,
);
// Click bulk select to show controls
const bulkSelectButton = await screen.findByTestId('bulk-select-toggle');
fireEvent.click(bulkSelectButton);
// Verify bulk select controls appear
expect(
reportWrapper.find('[data-test="report-list"]').hasClass('active'),
).toBe(true);
expect(
reportWrapper.find('[data-test="alert-list"]').hasClass('active'),
).toBe(false);
expect(reportWrapper.find(IndeterminateCheckbox)).toHaveLength(0);
});
await screen.findByTestId('bulk-select-controls'),
).toBeInTheDocument();
// Verify alert tab is active
const alertTab = await screen.findByTestId('alert-list');
expect(alertTab).toHaveClass('active');
const reportTab = screen.getByTestId('report-list');
expect(reportTab).not.toHaveClass('active');
// Switch to report list
renderAlertList({ isReportEnabled: true });
// Wait for report list API call and tab states to update
await waitFor(async () => {
// Check API call
const calls = fetchMock.calls(/report\/\?q/);
const hasReportCall = calls.some(call =>
call[0].includes('filters:!((col:type,opr:eq,value:Report))'),
);
// Check tab states
const reportTabs = screen.getAllByTestId('report-list');
const alertTabs = screen.getAllByTestId('alert-list');
const hasActiveReport = reportTabs.some(tab =>
tab.classList.contains('active'),
);
const hasNoActiveAlert = alertTabs.every(
tab => !tab.classList.contains('active'),
);
return hasReportCall && hasActiveReport && hasNoActiveAlert;
});
// Click bulk select toggle again to hide controls
const bulkSelectButtons =
await screen.findAllByTestId('bulk-select-toggle');
fireEvent.click(bulkSelectButtons[0]);
// Verify final state
await waitFor(() => {
expect(
screen.queryByTestId('bulk-select-controls'),
).not.toBeInTheDocument();
});
// Verify correct API call was made
const reportCalls = fetchMock.calls(/report\/\?q/);
const lastReportCall = reportCalls[reportCalls.length - 1][0];
expect(lastReportCall).toContain(
'filters:!((col:type,opr:eq,value:Report))',
);
}, 15000);
});

View File

@@ -19,21 +19,17 @@
import thunk from 'redux-thunk';
import configureStore from 'redux-mock-store';
import fetchMock from 'fetch-mock';
import { Provider } from 'react-redux';
import { styledMount as mount } from 'spec/helpers/theming';
import {
render,
screen,
fireEvent,
waitFor,
} from 'spec/helpers/testing-library';
import { MemoryRouter } from 'react-router-dom';
import { QueryParamProvider } from 'use-query-params';
import AnnotationLayersList from 'src/pages/AnnotationLayerList';
import AnnotationLayerModal from 'src/features/annotationLayers/AnnotationLayerModal';
import SubMenu from 'src/features/home/SubMenu';
import ListView from 'src/components/ListView';
import Filters from 'src/components/ListView/Filters';
import DeleteModal from 'src/components/DeleteModal';
import Button from 'src/components/Button';
import IndeterminateCheckbox from 'src/components/IndeterminateCheckbox';
import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
import { act } from 'spec/helpers/testing-library';
// store needed for withToasts(AnnotationLayersList)
const mockStore = configureStore([thunk]);
const store = mockStore({});
@@ -77,91 +73,127 @@ fetchMock.get(layersRelatedEndpoint, {
},
});
describe('AnnotationLayersList', () => {
const wrapper = mount(
<Provider store={store}>
<AnnotationLayersList store={store} user={mockUser} />
</Provider>,
const renderAnnotationLayersList = (props = {}) =>
render(
<MemoryRouter>
<QueryParamProvider>
<AnnotationLayersList user={mockUser} {...props} />
</QueryParamProvider>
</MemoryRouter>,
{
useRedux: true,
store,
},
);
beforeAll(async () => {
await waitForComponentToPaint(wrapper);
describe('AnnotationLayersList', () => {
beforeEach(() => {
fetchMock.resetHistory();
});
it('renders', () => {
expect(wrapper.find(AnnotationLayersList)).toBeTruthy();
it('renders', async () => {
renderAnnotationLayersList();
expect(await screen.findByText('Annotation layers')).toBeInTheDocument();
});
it('renders a SubMenu', () => {
expect(wrapper.find(SubMenu)).toBeTruthy();
it('renders a SubMenu', async () => {
renderAnnotationLayersList();
expect(await screen.findByRole('navigation')).toBeInTheDocument();
});
it('renders a ListView', () => {
expect(wrapper.find(ListView)).toBeTruthy();
it('renders a ListView', async () => {
renderAnnotationLayersList();
expect(
await screen.findByTestId('annotation-layers-list-view'),
).toBeInTheDocument();
});
it('renders a modal', () => {
expect(wrapper.find(AnnotationLayerModal)).toBeTruthy();
it('renders a modal', async () => {
renderAnnotationLayersList();
const addButton = await screen.findByRole('button', {
name: /annotation layer$/i,
});
fireEvent.click(addButton);
expect(await screen.findByRole('dialog')).toBeInTheDocument();
});
it('fetches layers', () => {
const callsQ = fetchMock.calls(/annotation_layer\/\?q/);
expect(callsQ).toHaveLength(1);
expect(callsQ[0][0]).toMatchInlineSnapshot(
`"http://localhost/api/v1/annotation_layer/?q=(order_column:name,order_direction:desc,page:0,page_size:25)"`,
);
it('fetches layers', async () => {
renderAnnotationLayersList();
await waitFor(() => {
const calls = fetchMock.calls(/annotation_layer\/\?q/);
expect(calls).toHaveLength(1);
expect(calls[0][0]).toContain(
'order_column:name,order_direction:desc,page:0,page_size:25',
);
});
});
it('renders Filters', () => {
expect(wrapper.find(Filters)).toBeTruthy();
it('renders Filters', async () => {
renderAnnotationLayersList();
await screen.findByTestId('annotation-layers-list-view');
expect(screen.getByPlaceholderText(/type a value/i)).toBeInTheDocument();
});
it('searches', async () => {
const filtersWrapper = wrapper.find(Filters);
act(() => {
filtersWrapper.find('[name="name"]').first().props().onSubmit('foo');
});
await waitForComponentToPaint(wrapper);
renderAnnotationLayersList();
expect(fetchMock.lastCall()[0]).toMatchInlineSnapshot(
`"http://localhost/api/v1/annotation_layer/?q=(filters:!((col:name,opr:ct,value:foo)),order_column:name,order_direction:desc,page:0,page_size:25)"`,
);
// Wait for list to load
await screen.findByTestId('annotation-layers-list-view');
// Find and fill search input
const searchInput = screen.getByPlaceholderText(/type a value/i);
fireEvent.change(searchInput, { target: { value: 'foo' } });
fireEvent.keyDown(searchInput, { key: 'Enter', keyCode: 13 });
// Wait for search API call
await waitFor(() => {
const calls = fetchMock.calls(/annotation_layer\/\?q/);
const searchCall = calls.find(call =>
call[0].includes('filters:!((col:name,opr:ct,value:foo))'),
);
expect(searchCall).toBeTruthy();
});
});
it('deletes', async () => {
act(() => {
wrapper.find('[data-test="delete-action"]').first().props().onClick();
renderAnnotationLayersList();
// Wait for list to load
await screen.findByTestId('annotation-layers-list-view');
// Find and click delete button
const deleteButtons = await screen.findAllByTestId('delete-action');
fireEvent.click(deleteButtons[0]);
// Check delete modal content
const deleteModal = await screen.findByRole('dialog');
expect(deleteModal).toHaveTextContent(/permanently delete the layer/i);
// Type DELETE in confirmation input
const deleteInput = await screen.findByTestId('delete-modal-input');
fireEvent.change(deleteInput, { target: { value: 'DELETE' } });
// Click confirm button
const confirmButton = await screen.findByTestId('modal-confirm-button');
fireEvent.click(confirmButton);
// Wait for delete request
await waitFor(() => {
expect(fetchMock.calls(/annotation_layer\/0/, 'DELETE')).toHaveLength(1);
});
await waitForComponentToPaint(wrapper);
expect(
wrapper.find(DeleteModal).first().props().description,
).toMatchInlineSnapshot(`"This action will permanently delete the layer."`);
act(() => {
wrapper
.find('#delete')
.first()
.props()
.onChange({ target: { value: 'DELETE' } });
});
await waitForComponentToPaint(wrapper);
act(() => {
wrapper.find('button').last().props().onClick();
});
await waitForComponentToPaint(wrapper);
expect(fetchMock.calls(/annotation_layer\/0/, 'DELETE')).toHaveLength(1);
});
it('shows/hides bulk actions when bulk actions is clicked', async () => {
const button = wrapper.find(Button).at(1);
act(() => {
button.props().onClick();
});
await waitForComponentToPaint(wrapper);
expect(wrapper.find(IndeterminateCheckbox)).toHaveLength(
mocklayers.length + 1, // 1 for each row and 1 for select all
);
});
it('shows bulk actions when bulk select is clicked', async () => {
renderAnnotationLayersList();
// Wait for list to load
await screen.findByTestId('annotation-layers-list-view');
// Click bulk select toggle
const bulkSelectButton = screen.getByText(/bulk select/i);
fireEvent.click(bulkSelectButton);
// Wait for bulk select mode to be enabled
await screen.findByText('0 Selected');
}, 30000);
});

View File

@@ -22,34 +22,18 @@ import configureStore from 'redux-mock-store';
import * as reactRedux from 'react-redux';
import fetchMock from 'fetch-mock';
import { VizType, isFeatureEnabled } from '@superset-ui/core';
import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
import { styledMount as mount } from 'spec/helpers/theming';
import {
act,
cleanup,
render,
screen,
userEvent,
fireEvent,
waitFor,
} from 'spec/helpers/testing-library';
import { QueryParamProvider } from 'use-query-params';
import ChartList from 'src/pages/ChartList';
import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
import ListView from 'src/components/ListView';
import PropertiesModal from 'src/explore/components/PropertiesModal';
import ListViewCard from 'src/components/ListViewCard';
import FaveStar from 'src/components/FaveStar';
import TableCollection from 'src/components/TableCollection';
import CardCollection from 'src/components/ListView/CardCollection';
const chartsInfoEndpoint = 'glob:*/api/v1/chart/_info*';
const chartsOwnersEndpoint = 'glob:*/api/v1/chart/related/owners*';
const chartsCreatedByEndpoint = 'glob:*/api/v1/chart/related/created_by*';
const chartsEndpoint = 'glob:*/api/v1/chart/*';
const chartsVizTypesEndpoint = 'glob:*/api/v1/chart/viz_types';
const chartsDatasourcesEndpoint = 'glob:*/api/v1/chart/datasources';
const chartFavoriteStatusEndpoint = 'glob:*/api/v1/chart/favorite_status*';
const datasetEndpoint = 'glob:*/api/v1/dataset/*';
// Increase default timeout for all tests
jest.setTimeout(30000);
jest.mock('@superset-ui/core', () => ({
...jest.requireActual('@superset-ui/core'),
@@ -71,10 +55,18 @@ const mockUser = {
userId: 1,
};
const chartsInfoEndpoint = 'glob:*/api/v1/chart/_info*';
const chartsOwnersEndpoint = 'glob:*/api/v1/chart/related/owners*';
const chartsCreatedByEndpoint = 'glob:*/api/v1/chart/related/created_by*';
const chartsEndpoint = 'glob:*/api/v1/chart/*';
const chartsVizTypesEndpoint = 'glob:*/api/v1/chart/viz_types';
const chartsDatasourcesEndpoint = 'glob:*/api/v1/chart/datasources';
const chartFavoriteStatusEndpoint = 'glob:*/api/v1/chart/favorite_status*';
const datasetEndpoint = 'glob:*/api/v1/dataset/*';
fetchMock.get(chartsInfoEndpoint, {
permissions: ['can_read', 'can_write'],
});
fetchMock.get(chartsOwnersEndpoint, {
result: [],
});
@@ -82,23 +74,20 @@ fetchMock.get(chartsCreatedByEndpoint, {
result: [],
});
fetchMock.get(chartFavoriteStatusEndpoint, {
result: [],
result: mockCharts.map(chart => ({ id: chart.id, value: true })),
});
fetchMock.get(chartsEndpoint, {
result: mockCharts,
chart_count: 3,
});
fetchMock.get(chartsVizTypesEndpoint, {
result: [],
count: 0,
});
fetchMock.get(chartsDatasourcesEndpoint, {
result: [],
count: 0,
});
fetchMock.get(datasetEndpoint, {});
global.URL.createObjectURL = jest.fn();
@@ -122,169 +111,225 @@ const user = {
username: 'admin',
};
// store needed for withToasts(DatabaseList)
const mockStore = configureStore([thunk]);
const store = mockStore({ user });
const useSelectorMock = jest.spyOn(reactRedux, 'useSelector');
describe('ChartList', () => {
isFeatureEnabled.mockImplementation(
feature => feature === 'LISTVIEWS_DEFAULT_CARD_VIEW',
const renderChartList = (props = {}) =>
render(
<MemoryRouter>
<QueryParamProvider>
<ChartList {...props} user={mockUser} />
</QueryParamProvider>
</MemoryRouter>,
{
useRedux: true,
store,
},
);
describe('ChartList', () => {
beforeEach(() => {
isFeatureEnabled.mockImplementation(
feature => feature === 'LISTVIEWS_DEFAULT_CARD_VIEW',
);
fetchMock.resetHistory();
useSelectorMock.mockClear();
});
afterAll(() => {
isFeatureEnabled.mockRestore();
});
beforeEach(() => {
// setup a DOM element as a render target
useSelectorMock.mockClear();
it('renders', async () => {
renderChartList();
expect(await screen.findByText('Charts')).toBeInTheDocument();
});
const mockedProps = {};
let wrapper;
beforeAll(async () => {
wrapper = mount(
<MemoryRouter>
<reactRedux.Provider store={store}>
<ChartList {...mockedProps} user={mockUser} />
</reactRedux.Provider>
</MemoryRouter>,
);
await waitForComponentToPaint(wrapper);
it('renders a ListView', async () => {
renderChartList();
expect(await screen.findByTestId('chart-list-view')).toBeInTheDocument();
});
it('renders', () => {
expect(wrapper.find(ChartList)).toBeTruthy();
it('fetches info', async () => {
renderChartList();
await waitFor(() => {
const calls = fetchMock.calls(/chart\/_info/);
expect(calls).toHaveLength(1);
});
});
it('renders a ListView', () => {
expect(wrapper.find(ListView)).toBeTruthy();
});
it('fetches info', () => {
const callsI = fetchMock.calls(/chart\/_info/);
expect(callsI).toHaveLength(1);
});
it('fetches data', () => {
wrapper.update();
const callsD = fetchMock.calls(/chart\/\?q/);
expect(callsD).toHaveLength(1);
expect(callsD[0][0]).toMatchInlineSnapshot(
`"http://localhost/api/v1/chart/?q=(order_column:changed_on_delta_humanized,order_direction:desc,page:0,page_size:25)"`,
);
});
it('renders a card view', () => {
expect(wrapper.find(ListViewCard)).toBeTruthy();
});
it('renders a table view', async () => {
wrapper.find('[aria-label="list-view"]').first().simulate('click');
await waitForComponentToPaint(wrapper);
expect(wrapper.find('table')).toBeTruthy();
});
it('edits', async () => {
expect(wrapper.find(PropertiesModal).length).toBe(0);
wrapper.find('[data-test="edit-alt"]').first().simulate('click');
await waitForComponentToPaint(wrapper);
expect(wrapper.find(PropertiesModal).length).toBeGreaterThan(0);
});
it('delete', async () => {
wrapper.find('[data-test="trash"]').first().simulate('click');
await waitForComponentToPaint(wrapper);
expect(wrapper.find(ConfirmStatusChange)).toBeTruthy();
});
it('renders the Favorite Star column in list view for logged in user', async () => {
wrapper.find('[aria-label="list-view"]').first().simulate('click');
await waitForComponentToPaint(wrapper);
expect(wrapper.find(TableCollection).find(FaveStar)).toBeTruthy();
});
it('renders the Favorite Star in card view for logged in user', async () => {
wrapper.find('[aria-label="card-view"]').first().simulate('click');
await waitForComponentToPaint(wrapper);
expect(wrapper.find(CardCollection).find(FaveStar)).toBeTruthy();
});
});
describe('RTL', () => {
async function renderAndWait() {
const mounted = act(async () => {
const mockedProps = {};
render(
<QueryParamProvider>
<ChartList {...mockedProps} user={mockUser} />
</QueryParamProvider>,
{ useRedux: true, useRouter: true },
it('fetches data', async () => {
renderChartList();
await waitFor(() => {
const calls = fetchMock.calls(/chart\/\?q/);
expect(calls).toHaveLength(1);
expect(calls[0][0]).toContain(
'order_column:changed_on_delta_humanized,order_direction:desc,page:0,page_size:25',
);
});
return mounted;
}
beforeEach(async () => {
isFeatureEnabled.mockImplementation(() => true);
await renderAndWait();
});
afterEach(() => {
cleanup();
isFeatureEnabled.mockRestore();
it('switches between card and table view', async () => {
renderChartList();
// Wait for list to load
await screen.findByTestId('chart-list-view');
// Find and click list view toggle
const listViewToggle = await screen.findByRole('img', {
name: 'list-view',
});
const listViewButton = listViewToggle.closest('[role="button"]');
fireEvent.click(listViewButton);
// Wait for list view to be active
await waitFor(() => {
const listViewToggle = screen.getByRole('img', { name: 'list-view' });
expect(listViewToggle.closest('[role="button"]')).toHaveClass('active');
});
// Find and click card view toggle
const cardViewToggle = screen.getByRole('img', { name: 'card-view' });
const cardViewButton = cardViewToggle.closest('[role="button"]');
fireEvent.click(cardViewButton);
// Wait for card view to be active
await waitFor(() => {
const cardViewToggle = screen.getByRole('img', { name: 'card-view' });
expect(cardViewToggle.closest('[role="button"]')).toHaveClass('active');
});
});
it('shows edit modal', async () => {
renderChartList();
// Wait for list to load
await screen.findByTestId('chart-list-view');
// Switch to list view
const listViewToggle = await screen.findByRole('img', {
name: 'list-view',
});
const listViewButton = listViewToggle.closest('[role="button"]');
fireEvent.click(listViewButton);
// Wait for list view to be active and data to load
await waitFor(() => {
expect(screen.getByText('cool chart 0')).toBeInTheDocument();
});
// Click edit button
const editButtons = await screen.findAllByTestId('edit-alt');
fireEvent.click(editButtons[0]);
// Verify modal appears
expect(await screen.findByRole('dialog')).toBeInTheDocument();
});
it('shows delete modal', async () => {
renderChartList();
// Wait for list to load
await screen.findByTestId('chart-list-view');
// Switch to list view
const listViewToggle = await screen.findByRole('img', {
name: 'list-view',
});
const listViewButton = listViewToggle.closest('[role="button"]');
fireEvent.click(listViewButton);
// Wait for list view to be active and data to load
await waitFor(() => {
expect(screen.getByText('cool chart 0')).toBeInTheDocument();
});
// Click delete button
const deleteButtons = await screen.findAllByTestId('trash');
fireEvent.click(deleteButtons[0]);
// Verify modal appears
expect(await screen.findByRole('dialog')).toBeInTheDocument();
});
it('shows favorite stars for logged in user', async () => {
renderChartList();
// Wait for list to load
await screen.findByTestId('chart-list-view');
// Switch to list view
const listViewToggle = await screen.findByRole('img', {
name: 'list-view',
});
const listViewButton = listViewToggle.closest('[role="button"]');
fireEvent.click(listViewButton);
// Wait for list view to be active and data to load
await waitFor(() => {
expect(screen.getByText('cool chart 0')).toBeInTheDocument();
});
// Wait for favorite stars to appear
await waitFor(() => {
const favoriteStars = screen.getAllByRole('img', {
name: 'favorite-selected',
});
expect(favoriteStars.length).toBeGreaterThan(0);
});
});
it('renders an "Import Chart" tooltip under import button', async () => {
const importButton = await screen.findByTestId('import-button');
userEvent.hover(importButton);
renderChartList();
await screen.findByRole('tooltip');
const importTooltip = screen.getByRole('tooltip', {
const importButton = await screen.findByTestId('import-button');
fireEvent.mouseEnter(importButton);
const importTooltip = await screen.findByRole('tooltip', {
name: 'Import charts',
});
expect(importTooltip).toBeInTheDocument();
});
});
describe('ChartList - anonymous view', () => {
const mockedProps = {};
const mockUserLoggedOut = {};
let wrapper;
beforeAll(async () => {
beforeEach(() => {
fetchMock.resetHistory();
wrapper = mount(
<MemoryRouter>
<reactRedux.Provider store={store}>
<ChartList {...mockedProps} user={mockUserLoggedOut} />
</reactRedux.Provider>
</MemoryRouter>,
// Reset favorite status for anonymous user
fetchMock.get(
chartFavoriteStatusEndpoint,
{
result: [],
},
{ overwriteRoutes: true },
);
await waitForComponentToPaint(wrapper);
});
afterAll(() => {
cleanup();
fetchMock.reset();
});
it('does not show favorite stars for anonymous user', async () => {
renderChartList({ user: {} });
it('does not render the Favorite Star column in list view for anonymous user', async () => {
wrapper.find('[aria-label="list-view"]').first().simulate('click');
await waitForComponentToPaint(wrapper);
expect(wrapper.find(TableCollection).find(FaveStar).length).toBe(0);
});
// Wait for list to load
await screen.findByTestId('chart-list-view');
it('does not render the Favorite Star in card view for anonymous user', async () => {
wrapper.find('[aria-label="card-view"]').first().simulate('click');
await waitForComponentToPaint(wrapper);
expect(wrapper.find(CardCollection).find(FaveStar).length).toBe(0);
// Switch to list view
const listViewToggle = await screen.findByRole('img', {
name: 'list-view',
});
const listViewButton = listViewToggle.closest('[role="button"]');
fireEvent.click(listViewButton);
// Wait for list view to be active and data to load
await waitFor(() => {
expect(screen.getByText('cool chart 0')).toBeInTheDocument();
});
// Verify no selected favorite stars are present
await waitFor(() => {
const favoriteStars = screen.queryAllByRole('img', {
name: 'favorite-selected',
});
expect(favoriteStars).toHaveLength(0);
});
});
});

View File

@@ -18,21 +18,18 @@
*/
import thunk from 'redux-thunk';
import configureStore from 'redux-mock-store';
import { Provider } from 'react-redux';
import fetchMock from 'fetch-mock';
import { styledMount as mount } from 'spec/helpers/theming';
import {
render,
screen,
fireEvent,
waitFor,
} from 'spec/helpers/testing-library';
import { MemoryRouter } from 'react-router-dom';
import { QueryParamProvider } from 'use-query-params';
import CssTemplatesList from 'src/pages/CssTemplateList';
import SubMenu from 'src/features/home/SubMenu';
import ListView from 'src/components/ListView';
import Filters from 'src/components/ListView/Filters';
import DeleteModal from 'src/components/DeleteModal';
import Button from 'src/components/Button';
import IndeterminateCheckbox from 'src/components/IndeterminateCheckbox';
import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
import { act } from 'spec/helpers/testing-library';
// store needed for withToasts(DatabaseList)
const mockStore = configureStore([thunk]);
const store = mockStore({});
@@ -75,98 +72,120 @@ fetchMock.get(templatesRelatedEndpoint, {
},
});
describe('CssTemplatesList', () => {
const wrapper = mount(
<Provider store={store}>
<CssTemplatesList store={store} user={mockUser} />
</Provider>,
const renderCssTemplatesList = (props = {}) =>
render(
<MemoryRouter>
<QueryParamProvider>
<CssTemplatesList user={mockUser} {...props} />
</QueryParamProvider>
</MemoryRouter>,
{
useRedux: true,
store,
},
);
beforeAll(async () => {
await waitForComponentToPaint(wrapper);
describe('CssTemplatesList', () => {
beforeEach(() => {
fetchMock.resetHistory();
});
it('renders', () => {
expect(wrapper.find(CssTemplatesList)).toBeTruthy();
it('renders', async () => {
renderCssTemplatesList();
expect(await screen.findByText(/css templates/i)).toBeInTheDocument();
});
it('renders a SubMenu', () => {
expect(wrapper.find(SubMenu)).toBeTruthy();
it('renders a SubMenu', async () => {
renderCssTemplatesList();
expect(await screen.findByRole('navigation')).toBeInTheDocument();
});
it('renders a ListView', () => {
expect(wrapper.find(ListView)).toBeTruthy();
it('renders a ListView', async () => {
renderCssTemplatesList();
expect(
await screen.findByTestId('css-templates-list-view'),
).toBeInTheDocument();
});
it('fetches templates', () => {
const callsQ = fetchMock.calls(/css_template\/\?q/);
expect(callsQ).toHaveLength(1);
expect(callsQ[0][0]).toMatchInlineSnapshot(
`"http://localhost/api/v1/css_template/?q=(order_column:template_name,order_direction:desc,page:0,page_size:25)"`,
);
it('fetches templates', async () => {
renderCssTemplatesList();
await waitFor(() => {
const calls = fetchMock.calls(/css_template\/\?q/);
expect(calls).toHaveLength(1);
expect(calls[0][0]).toContain(
'order_column:template_name,order_direction:desc,page:0,page_size:25',
);
});
});
it('renders Filters', () => {
expect(wrapper.find(Filters)).toBeTruthy();
it('renders Filters', async () => {
renderCssTemplatesList();
await screen.findByTestId('css-templates-list-view');
expect(screen.getByPlaceholderText(/type a value/i)).toBeInTheDocument();
});
it('searches', async () => {
const filtersWrapper = wrapper.find(Filters);
act(() => {
filtersWrapper
.find('[name="template_name"]')
.first()
.props()
.onSubmit('fooo');
renderCssTemplatesList();
// Wait for list to load
await screen.findByTestId('css-templates-list-view');
// Find and fill search input
const searchInput = screen.getByPlaceholderText(/type a value/i);
fireEvent.change(searchInput, { target: { value: 'fooo' } });
fireEvent.keyDown(searchInput, { key: 'Enter', keyCode: 13 });
// Wait for search API call
await waitFor(() => {
const calls = fetchMock.calls(/css_template\/\?q/);
const searchCall = calls.find(call =>
call[0].includes('filters:!((col:template_name,opr:ct,value:fooo))'),
);
expect(searchCall).toBeTruthy();
});
await waitForComponentToPaint(wrapper);
expect(fetchMock.lastCall()[0]).toMatchInlineSnapshot(
`"http://localhost/api/v1/css_template/?q=(filters:!((col:template_name,opr:ct,value:fooo)),order_column:template_name,order_direction:desc,page:0,page_size:25)"`,
);
});
it('renders a DeleteModal', () => {
expect(wrapper.find(DeleteModal)).toBeTruthy();
});
it('deletes', async () => {
act(() => {
wrapper.find('[data-test="delete-action"]').first().props().onClick();
renderCssTemplatesList();
// Wait for list to load
await screen.findByTestId('css-templates-list-view');
// Find and click delete button
const deleteButtons = await screen.findAllByTestId('delete-action');
fireEvent.click(deleteButtons[0]);
// Check delete modal content
const deleteModal = await screen.findByRole('dialog');
expect(deleteModal).toHaveTextContent(/permanently delete the template/i);
// Type DELETE in confirmation input
const deleteInput = await screen.findByTestId('delete-modal-input');
fireEvent.change(deleteInput, { target: { value: 'DELETE' } });
// Click confirm button
const confirmButton = await screen.findByTestId('modal-confirm-button');
fireEvent.click(confirmButton);
// Wait for delete request
await waitFor(() => {
expect(fetchMock.calls(/css_template\/0/, 'DELETE')).toHaveLength(1);
});
await waitForComponentToPaint(wrapper);
expect(
wrapper.find(DeleteModal).first().props().description,
).toMatchInlineSnapshot(
`"This action will permanently delete the template."`,
);
act(() => {
wrapper
.find('#delete')
.first()
.props()
.onChange({ target: { value: 'DELETE' } });
});
await waitForComponentToPaint(wrapper);
act(() => {
wrapper.find('button').last().props().onClick();
});
await waitForComponentToPaint(wrapper);
expect(fetchMock.calls(/css_template\/0/, 'DELETE')).toHaveLength(1);
});
it('shows/hides bulk actions when bulk actions is clicked', async () => {
const button = wrapper.find(Button).at(1);
act(() => {
button.props().onClick();
it('shows bulk actions when bulk select is clicked', async () => {
renderCssTemplatesList();
// Wait for list to load
await screen.findByTestId('css-templates-list-view');
// Click bulk select toggle
const bulkSelectButton = screen.getByRole('button', {
name: /bulk select/i,
});
await waitForComponentToPaint(wrapper);
expect(wrapper.find(IndeterminateCheckbox)).toHaveLength(
mocktemplates.length + 1, // 1 for each row and 1 for select all
);
});
fireEvent.click(bulkSelectButton);
// Wait for bulk select mode to be enabled
await screen.findByText('0 Selected');
}, 30000);
});

View File

@@ -17,31 +17,17 @@
* under the License.
*/
import { MemoryRouter } from 'react-router-dom';
import thunk from 'redux-thunk';
import configureStore from 'redux-mock-store';
import fetchMock from 'fetch-mock';
import * as reactRedux from 'react-redux';
import { isFeatureEnabled } from '@superset-ui/core';
import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
import { styledMount as mount } from 'spec/helpers/theming';
import {
act,
cleanup,
render,
screen,
userEvent,
waitFor,
fireEvent,
} from 'spec/helpers/testing-library';
import { QueryParamProvider } from 'use-query-params';
import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
import DashboardList from 'src/pages/DashboardList';
import ListView from 'src/components/ListView';
import ListViewCard from 'src/components/ListViewCard';
import PropertiesModal from 'src/dashboard/components/PropertiesModal';
import FaveStar from 'src/components/FaveStar';
import TableCollection from 'src/components/TableCollection';
import CardCollection from 'src/components/ListView/CardCollection';
const dashboardsInfoEndpoint = 'glob:*/api/v1/dashboard/_info*';
const dashboardOwnersEndpoint = 'glob:*/api/v1/dashboard/related/owners*';
@@ -87,219 +73,163 @@ fetchMock.get(dashboardCreatedByEndpoint, {
fetchMock.get(dashboardFavoriteStatusEndpoint, {
result: [],
});
fetchMock.get(dashboardsEndpoint, {
result: mockDashboards,
dashboard_count: 3,
});
fetchMock.get(dashboardEndpoint, {
result: mockDashboards[0],
});
global.URL.createObjectURL = jest.fn();
fetchMock.get('/thumbnail', { body: new Blob(), sendAsJson: false });
const user = {
createdOn: '2021-04-27T18:12:38.952304',
email: 'admin',
firstName: 'admin',
isActive: true,
lastName: 'admin',
permissions: {},
roles: {
Admin: [
['can_sqllab', 'Superset'],
['can_write', 'Dashboard'],
['can_write', 'Chart'],
],
},
userId: 1,
username: 'admin',
};
// store needed for withToasts(DatabaseList)
const mockStore = configureStore([thunk]);
const store = mockStore({ user });
const useSelectorMock = jest.spyOn(reactRedux, 'useSelector');
describe('DashboardList', () => {
isFeatureEnabled.mockImplementation(
feature => feature === 'LISTVIEWS_DEFAULT_CARD_VIEW',
);
const renderDashboardList = (props = {}, userProp = mockUser) =>
render(
<MemoryRouter>
<QueryParamProvider>
<DashboardList {...props} user={userProp} />
</QueryParamProvider>
</MemoryRouter>,
{ useRedux: true },
);
afterAll(() => {
beforeEach(() => {
isFeatureEnabled.mockImplementation(
feature => feature === 'LISTVIEWS_DEFAULT_CARD_VIEW',
);
fetchMock.resetHistory();
});
afterEach(() => {
isFeatureEnabled.mockRestore();
});
beforeEach(() => {
// setup a DOM element as a render target
useSelectorMock.mockClear();
it('renders', async () => {
renderDashboardList();
expect(await screen.findByText('Dashboards')).toBeInTheDocument();
});
const mockedProps = {};
let wrapper;
beforeAll(async () => {
fetchMock.resetHistory();
wrapper = mount(
<MemoryRouter>
<reactRedux.Provider store={store}>
<DashboardList {...mockedProps} user={mockUser} />
</reactRedux.Provider>
</MemoryRouter>,
);
await waitForComponentToPaint(wrapper);
it('renders a ListView', async () => {
renderDashboardList();
expect(
await screen.findByTestId('dashboard-list-view'),
).toBeInTheDocument();
});
it('renders', () => {
expect(wrapper.find(DashboardList)).toBeTruthy();
it('fetches info', async () => {
renderDashboardList();
await waitFor(() => {
const calls = fetchMock.calls(/dashboard\/_info/);
expect(calls).toHaveLength(1);
});
});
it('renders a ListView', () => {
expect(wrapper.find(ListView)).toBeTruthy();
});
it('fetches data', async () => {
renderDashboardList();
await waitFor(() => {
const calls = fetchMock.calls(/dashboard\/\?q/);
expect(calls).toHaveLength(1);
});
it('fetches info', () => {
const callsI = fetchMock.calls(/dashboard\/_info/);
expect(callsI).toHaveLength(1);
});
it('fetches data', () => {
wrapper.update();
const callsD = fetchMock.calls(/dashboard\/\?q/);
expect(callsD).toHaveLength(1);
expect(callsD[0][0]).toMatchInlineSnapshot(
const calls = fetchMock.calls(/dashboard\/\?q/);
expect(calls[0][0]).toMatchInlineSnapshot(
`"http://localhost/api/v1/dashboard/?q=(order_column:changed_on_delta_humanized,order_direction:desc,page:0,page_size:25,select_columns:!(id,dashboard_title,published,url,slug,changed_by,changed_by.id,changed_by.first_name,changed_by.last_name,changed_on_delta_humanized,owners,owners.id,owners.first_name,owners.last_name,tags.id,tags.name,tags.type,status,certified_by,certification_details,changed_on))"`,
);
});
it('renders a card view', () => {
expect(wrapper.find(ListViewCard)).toBeTruthy();
it('switches between card and table view', async () => {
renderDashboardList();
// Wait for the list to load
await screen.findByTestId('dashboard-list-view');
// Initially in card view
const cardViewIcon = screen.getByRole('img', { name: 'card-view' });
expect(cardViewIcon).toBeInTheDocument();
// Switch to table view
const listViewIcon = screen.getByRole('img', { name: 'list-view' });
const listViewButton = listViewIcon.closest('[role="button"]');
fireEvent.click(listViewButton);
// Switch back to card view
const cardViewButton = cardViewIcon.closest('[role="button"]');
fireEvent.click(cardViewButton);
});
it('renders a table view', async () => {
wrapper.find('[aria-label="list-view"]').first().simulate('click');
await waitForComponentToPaint(wrapper);
expect(wrapper.find('table')).toBeTruthy();
it('shows edit modal', async () => {
renderDashboardList();
// Wait for data to load
await screen.findByText('title 0');
// Find and click the first more options button
const moreIcons = await screen.findAllByRole('img', { name: 'more-vert' });
fireEvent.click(moreIcons[0]);
// Click edit from the dropdown
const editButton = await screen.findByTestId(
'dashboard-card-option-edit-button',
);
fireEvent.click(editButton);
// Check for modal
expect(await screen.findByRole('dialog')).toBeInTheDocument();
});
it('edits', async () => {
expect(wrapper.find(PropertiesModal).length).toBe(0);
wrapper.find('[data-test="edit-alt"]').first().simulate('click');
await waitForComponentToPaint(wrapper);
expect(wrapper.find(PropertiesModal).length).toBeGreaterThan(0);
it('shows delete confirmation', async () => {
renderDashboardList();
// Wait for data to load
await screen.findByText('title 0');
// Find and click the first more options button
const moreIcons = await screen.findAllByRole('img', { name: 'more-vert' });
fireEvent.click(moreIcons[0]);
// Click delete from the dropdown
const deleteButton = await screen.findByTestId(
'dashboard-card-option-delete-button',
);
fireEvent.click(deleteButton);
// Check for confirmation dialog
expect(
await screen.findByText(/Are you sure you want to delete/i),
).toBeInTheDocument();
});
it('card view edits', async () => {
wrapper.find('[data-test="edit-alt"]').last().simulate('click');
await waitForComponentToPaint(wrapper);
expect(wrapper.find(PropertiesModal)).toBeTruthy();
});
it('renders an "Import Dashboard" tooltip', async () => {
renderDashboardList();
it('delete', async () => {
wrapper
.find('[data-test="dashboard-list-trash-icon"]')
.first()
.simulate('click');
await waitForComponentToPaint(wrapper);
expect(wrapper.find(ConfirmStatusChange)).toBeTruthy();
});
it('card view delete', async () => {
wrapper
.find('[data-test="dashboard-list-trash-icon"]')
.last()
.simulate('click');
await waitForComponentToPaint(wrapper);
expect(wrapper.find(ConfirmStatusChange)).toBeTruthy();
});
it('renders the Favorite Star column in list view for logged in user', async () => {
wrapper.find('[aria-label="list-view"]').first().simulate('click');
await waitForComponentToPaint(wrapper);
expect(wrapper.find(TableCollection).find(FaveStar)).toBeTruthy();
});
it('renders the Favorite Star in card view for logged in user', async () => {
wrapper.find('[aria-label="card-view"]').first().simulate('click');
await waitForComponentToPaint(wrapper);
expect(wrapper.find(CardCollection).find(FaveStar)).toBeTruthy();
});
});
describe('RTL', () => {
async function renderAndWait() {
const mounted = act(async () => {
const mockedProps = {};
render(
<MemoryRouter>
<QueryParamProvider>
<DashboardList {...mockedProps} user={mockUser} />
</QueryParamProvider>
</MemoryRouter>,
{ useRedux: true },
);
});
return mounted;
}
beforeEach(async () => {
isFeatureEnabled.mockImplementation(() => true);
await renderAndWait();
});
afterEach(() => {
cleanup();
isFeatureEnabled.mockRestore();
});
it('renders an "Import Dashboard" tooltip under import button', async () => {
const importButton = await screen.findByTestId('import-button');
userEvent.hover(importButton);
fireEvent.mouseOver(importButton);
await screen.findByRole('tooltip');
const importTooltip = screen.getByRole('tooltip', {
name: 'Import dashboards',
});
expect(importTooltip).toBeInTheDocument();
expect(
await screen.findByRole('tooltip', {
name: 'Import dashboards',
}),
).toBeInTheDocument();
});
});
describe('DashboardList - anonymous view', () => {
const mockedProps = {};
const mockUserLoggedOut = {};
let wrapper;
beforeAll(async () => {
fetchMock.resetHistory();
wrapper = mount(
it('does not render favorite stars for anonymous user', async () => {
render(
<MemoryRouter>
<reactRedux.Provider store={store}>
<DashboardList {...mockedProps} user={mockUserLoggedOut} />
</reactRedux.Provider>
<QueryParamProvider>
<DashboardList user={{}} />
</QueryParamProvider>
</MemoryRouter>,
{ useRedux: true },
);
await waitForComponentToPaint(wrapper);
});
afterAll(() => {
cleanup();
fetchMock.reset();
});
it('does not render the Favorite Star column in list view for anonymous user', async () => {
wrapper.find('[aria-label="list-view"]').first().simulate('click');
await waitForComponentToPaint(wrapper);
expect(wrapper.find(TableCollection).find(FaveStar).length).toBe(0);
});
it('does not render the Favorite Star in card view for anonymous user', async () => {
wrapper.find('[aria-label="card-view"]').first().simulate('click');
await waitForComponentToPaint(wrapper);
expect(wrapper.find(CardCollection).find(FaveStar).length).toBe(0);
await waitFor(() => {
expect(
screen.queryByRole('img', { name: /favorite/i }),
).not.toBeInTheDocument();
});
});
});

View File

@@ -17,418 +17,215 @@
* under the License.
*/
import thunk from 'redux-thunk';
import * as reactRedux from 'react-redux';
import { BrowserRouter } from 'react-router-dom';
import configureStore from 'redux-mock-store';
import fetchMock from 'fetch-mock';
import { styledMount as mount } from 'spec/helpers/theming';
import {
act,
cleanup,
render,
screen,
userEvent,
fireEvent,
waitFor,
} from 'spec/helpers/testing-library';
import { MemoryRouter } from 'react-router-dom';
import { QueryParamProvider } from 'use-query-params';
import { isFeatureEnabled } from '@superset-ui/core';
import SavedQueryList from 'src/pages/SavedQueryList';
import SubMenu from 'src/features/home/SubMenu';
import ListView from 'src/components/ListView';
import Filters from 'src/components/ListView/Filters';
import ActionsBar from 'src/components/ListView/ActionsBar';
import DeleteModal from 'src/components/DeleteModal';
import Button from 'src/components/Button';
import IndeterminateCheckbox from 'src/components/IndeterminateCheckbox';
import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
import SavedQueryList from '.';
// Increase default timeout
jest.setTimeout(30000);
const mockQueries = [...new Array(3)].map((_, i) => ({
created_by: { id: i, first_name: 'user', last_name: `${i}` },
created_on: `${i}-2020`,
database: { database_name: `db ${i}`, id: i },
changed_on_delta_humanized: '1 day ago',
db_id: i,
description: `SQL for ${i}`,
id: i,
label: `query ${i}`,
schema: 'public',
sql: `SELECT ${i} FROM table`,
sql_tables: [{ catalog: null, schema: null, table: `${i}` }],
tags: [],
}));
const mockUser = {
userId: 1,
firstName: 'admin',
lastName: 'admin',
};
const queriesInfoEndpoint = 'glob:*/api/v1/saved_query/_info*';
const queriesEndpoint = 'glob:*/api/v1/saved_query/?*';
const queryEndpoint = 'glob:*/api/v1/saved_query/*';
const queriesRelatedEndpoint = 'glob:*/api/v1/saved_query/related/database?*';
const queriesDistinctEndpoint = 'glob:*/api/v1/saved_query/distinct/schema?*';
const mockqueries = [...new Array(3)].map((_, i) => ({
created_by: {
id: i,
first_name: `user`,
last_name: `${i}`,
},
created_on: `${i}-2020`,
database: {
database_name: `db ${i}`,
id: i,
},
changed_on_delta_humanized: '1 day ago',
db_id: i,
description: `SQL for ${i}`,
id: i,
label: `query ${i}`,
schema: 'public',
sql: `SELECT ${i} FROM table`,
sql_tables: [
{
catalog: null,
schema: null,
table: `${i}`,
},
],
}));
const user = {
createdOn: '2021-04-27T18:12:38.952304',
email: 'admin',
firstName: 'admin',
isActive: true,
lastName: 'admin',
permissions: {},
roles: {
Admin: [
['can_sqllab', 'Superset'],
['can_write', 'Dashboard'],
['can_write', 'Chart'],
],
},
userId: 1,
username: 'admin',
};
// store needed for withToasts(DatabaseList)
const mockStore = configureStore([thunk]);
const store = mockStore({ user });
const useSelectorMock = jest.spyOn(reactRedux, 'useSelector');
// ---------- For import testing ----------
// Create an one more mocked query than the original mocked query array
const mockOneMoreQuery = [...new Array(mockqueries.length + 1)].map((_, i) => ({
created_by: {
id: i,
first_name: `user`,
last_name: `${i}`,
},
created_on: `${i}-2020`,
database: {
database_name: `db ${i}`,
id: i,
},
changed_on_delta_humanized: '1 day ago',
db_id: i,
description: `SQL for ${i}`,
id: i,
label: `query ${i}`,
schema: 'public',
sql: `SELECT ${i} FROM table`,
sql_tables: [
{
catalog: null,
schema: null,
table: `${i}`,
},
],
}));
// Grab the last mocked query, to mock import
const mockNewImportQuery = mockOneMoreQuery.pop();
// Create a new file out of mocked import query to mock upload
const mockImportFile = new File(
[mockNewImportQuery],
'saved_query_import_mock.json',
);
const permalinkEndpoint = 'glob:*/api/v1/sqllab/permalink';
fetchMock.get(queriesInfoEndpoint, {
permissions: ['can_write', 'can_read', 'can_export'],
});
fetchMock.get(queriesEndpoint, {
result: mockqueries,
count: 3,
ids: [2, 0, 1],
result: mockQueries,
count: mockQueries.length,
});
fetchMock.post(permalinkEndpoint, {
url: 'http://localhost/permalink',
});
fetchMock.delete(queryEndpoint, {});
fetchMock.delete(queriesEndpoint, {});
fetchMock.get(queriesRelatedEndpoint, {
count: 0,
result: [],
});
fetchMock.get(queriesDistinctEndpoint, {
count: 0,
result: [],
});
// Mock utils module
jest.mock('src/views/CRUD/utils');
jest.mock('@superset-ui/core', () => ({
...jest.requireActual('@superset-ui/core'),
isFeatureEnabled: jest.fn(),
}));
describe('SavedQueryList', () => {
const wrapper = mount(
<reactRedux.Provider store={store}>
<SavedQueryList />
</reactRedux.Provider>,
const renderList = (props = {}, storeOverrides = {}) =>
render(
<MemoryRouter>
<QueryParamProvider>
<SavedQueryList user={mockUser} {...props} />
</QueryParamProvider>
</MemoryRouter>,
{
useRedux: true,
store: configureStore([thunk])({
user: {
...mockUser,
roles: { Admin: [['can_write', 'SavedQuery']] },
},
...storeOverrides,
}),
},
);
describe('SavedQueryList', () => {
beforeEach(() => {
// setup a DOM element as a render target
useSelectorMock.mockClear();
fetchMock.resetHistory();
});
beforeAll(async () => {
await waitForComponentToPaint(wrapper);
it('renders', async () => {
renderList();
expect(await screen.findByText('Saved queries')).toBeInTheDocument();
});
it('renders', () => {
expect(wrapper.find(SavedQueryList)).toBeTruthy();
});
it('renders a SubMenu', () => {
expect(wrapper.find(SubMenu)).toBeTruthy();
});
it('renders a SubMenu with Saved queries and Query History links', () => {
expect(wrapper.find(SubMenu).props().tabs).toEqual(
expect.arrayContaining([
expect.objectContaining({ label: 'Saved queries' }),
expect.objectContaining({ label: 'Query history' }),
]),
);
});
it('renders a SubMenu without Databases and Datasets links', () => {
expect(wrapper.find(SubMenu).props().tabs).not.toEqual(
expect.arrayContaining([
expect.objectContaining({ label: 'Databases' }),
expect.objectContaining({ label: 'Datasets' }),
]),
);
});
it('renders a ListView', () => {
expect(wrapper.find(ListView)).toBeTruthy();
});
it('fetches saved queries', () => {
const callsQ = fetchMock.calls(/saved_query\/\?q/);
expect(callsQ).toHaveLength(1);
expect(callsQ[0][0]).toMatchInlineSnapshot(
`"http://localhost/api/v1/saved_query/?q=(order_column:changed_on_delta_humanized,order_direction:desc,page:0,page_size:25)"`,
);
});
it('renders ActionsBar in table', () => {
expect(wrapper.find(ActionsBar)).toBeTruthy();
expect(wrapper.find(ActionsBar)).toHaveLength(3);
});
it('deletes', async () => {
act(() => {
wrapper.find('span[data-test="delete-action"]').first().props().onClick();
});
await waitForComponentToPaint(wrapper);
it('renders a ListView', async () => {
renderList();
expect(
wrapper.find(DeleteModal).first().props().description,
).toMatchInlineSnapshot(
`"This action will permanently delete the saved query."`,
);
act(() => {
wrapper
.find('#delete')
.first()
.props()
.onChange({ target: { value: 'DELETE' } });
});
await waitForComponentToPaint(wrapper);
act(() => {
wrapper.find('button').last().props().onClick();
});
await waitForComponentToPaint(wrapper);
expect(fetchMock.calls(/saved_query\/0/, 'DELETE')).toHaveLength(1);
await screen.findByTestId('saved_query-list-view'),
).toBeInTheDocument();
});
it('copies a query link when the API succeeds', async () => {
Object.assign(navigator, {
clipboard: {
writeText: jest.fn(),
},
});
it('renders query information', async () => {
renderList();
fetchMock.get('glob:*/api/v1/saved_query', {
result: [
{
id: 1,
label: 'Test Query',
db_id: 1,
schema: 'public',
sql: 'SELECT * FROM table',
},
],
count: 1,
});
fetchMock.post('glob:*/api/v1/sqllab/permalink', {
body: { url: 'http://example.com/permalink' },
status: 200,
});
// Wait for list to load
await screen.findByTestId('saved_query-list-view');
render(
<BrowserRouter>
<QueryParamProvider>
<SavedQueryList />
</QueryParamProvider>
</BrowserRouter>,
{ store },
);
const copyActionButton = await waitFor(
() => screen.getAllByTestId('copy-action')[0],
);
userEvent.hover(copyActionButton);
userEvent.click(copyActionButton);
// Wait for data to load
await waitFor(() => {
expect(fetchMock.calls('glob:*/api/v1/sqllab/permalink').length).toBe(1);
mockQueries.forEach(query => {
expect(screen.getByText(query.label)).toBeInTheDocument();
expect(
screen.getByText(query.database.database_name),
).toBeInTheDocument();
expect(screen.getAllByText(query.schema)[0]).toBeInTheDocument();
});
});
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(
'http://example.com/permalink',
);
});
it('shows/hides bulk actions when bulk actions is clicked', async () => {
const button = wrapper.find(Button).at(0);
act(() => {
button.props().onClick();
it('handles query deletion', async () => {
renderList();
// Wait for list to load
await screen.findByTestId('saved_query-list-view');
// Wait for data and find delete button
const deleteButtons = await screen.findAllByTestId('delete-action');
fireEvent.click(deleteButtons[0]);
// Confirm deletion
const deleteInput = screen.getByTestId('delete-modal-input');
fireEvent.change(deleteInput, { target: { value: 'DELETE' } });
const confirmButton = screen.getByRole('button', { name: /delete/i });
fireEvent.click(confirmButton);
// Verify API call
await waitFor(() => {
expect(fetchMock.calls(/saved_query\/0/, 'DELETE')).toHaveLength(1);
});
await waitForComponentToPaint(wrapper);
expect(wrapper.find(IndeterminateCheckbox)).toHaveLength(
mockqueries.length + 1, // 1 for each row and 1 for select all
);
});
it('searches', async () => {
const filtersWrapper = wrapper.find(Filters);
act(() => {
filtersWrapper.find('[name="label"]').first().props().onSubmit('fooo');
});
await waitForComponentToPaint(wrapper);
it('handles search filtering', async () => {
renderList();
expect(fetchMock.lastCall()[0]).toMatchInlineSnapshot(
`"http://localhost/api/v1/saved_query/?q=(filters:!((col:label,opr:all_text,value:fooo)),order_column:changed_on_delta_humanized,order_direction:desc,page:0,page_size:25)"`,
// Wait for list to load
await screen.findByTestId('saved_query-list-view');
// Find and use search input
const searchInput = screen.getByTestId('filters-search');
fireEvent.change(searchInput, { target: { value: 'test query' } });
fireEvent.keyDown(searchInput, { key: 'Enter' });
// Verify API call
await waitFor(() => {
const calls = fetchMock.calls(/saved_query\/\?q/);
expect(calls.length).toBeGreaterThan(0);
const lastCall = calls[calls.length - 1][0];
expect(lastCall).toContain('order_column');
expect(lastCall).toContain('page');
});
});
it('fetches data', async () => {
renderList();
await waitFor(() => {
const calls = fetchMock.calls(/saved_query\/\?q/);
expect(calls).toHaveLength(1);
expect(calls[0][0]).toContain('order_column');
expect(calls[0][0]).toContain('page');
});
});
it('handles sorting', async () => {
renderList();
// Wait for list to load
await screen.findByTestId('saved_query-list-view');
// Find and click sort header
const sortHeaders = screen.getAllByTestId('sort-header');
fireEvent.click(sortHeaders[0]);
// Verify API call includes sorting
await waitFor(() => {
const calls = fetchMock.calls(/saved_query\/\?q/);
const lastCall = calls[calls.length - 1][0];
expect(lastCall).toContain('order_column:label');
});
});
it('shows/hides elements based on permissions', async () => {
// Mock info response without write permission
fetchMock.get(
queriesInfoEndpoint,
{ permissions: ['can_read'] },
{ overwriteRoutes: true },
);
});
});
describe('RTL', () => {
function renderAndWait() {
return render(<SavedQueryList />, {
useRedux: true,
useRouter: true,
useQueryParams: true,
});
}
beforeEach(async () => {
isFeatureEnabled.mockImplementation(() => true);
renderAndWait();
});
afterEach(() => {
cleanup();
isFeatureEnabled.mockRestore();
});
it('renders an export button in the bulk actions', async () => {
const bulkSelectButton = screen.getByRole('button', {
name: /bulk select/i,
});
userEvent.click(bulkSelectButton);
const checkbox = await screen.findByTestId('header-toggle-all');
userEvent.click(checkbox);
const exportButton = screen.getByRole('button', {
name: /export/i,
});
expect(exportButton).toBeVisible();
});
it('renders an export button in the actions bar', async () => {
// Grab Export action button and mock mouse hovering over it
const exportActionButton = screen.getAllByTestId('export-action')[0];
userEvent.hover(exportActionButton);
// Wait for the tooltip to pop up
await screen.findByRole('tooltip');
// Grab and assert that "Export Query" tooltip is in the document
const exportTooltip = screen.getByRole('tooltip', {
name: /export query/i,
});
expect(exportTooltip).toBeInTheDocument();
});
it('renders a copy button in the actions bar', async () => {
// Grab copy action button and mock mouse hovering over it
const copyActionButton = screen.getAllByTestId('copy-action')[0];
userEvent.hover(copyActionButton);
// Wait for the tooltip to pop up
await screen.findByRole('tooltip');
// Grab and assert that "Copy query URl" tooltip is in the document
const copyTooltip = screen.getByRole('tooltip', {
name: /Copy query URL/i,
});
expect(copyTooltip).toBeInTheDocument();
});
it('renders an import button in the submenu', async () => {
// Grab and assert that import saved query button is visible
const importButton = await screen.findByTestId('import-button');
expect(importButton).toBeVisible();
});
it('renders an "Import Saved Query" tooltip under import button', async () => {
const importButton = await screen.findByTestId('import-icon');
userEvent.hover(importButton);
const importTooltip = await screen.findByRole('tooltip', {
name: 'Import queries',
});
expect(importTooltip).toBeInTheDocument();
});
it('renders an import modal when import button is clicked', async () => {
// Grab and click import saved query button to reveal modal
expect(
screen.queryByRole('heading', { name: 'Import queries' }),
).not.toBeInTheDocument();
const importButton = await screen.findByTestId('import-button');
userEvent.click(importButton);
// Grab and assert that saved query import modal's heading is visible
const importSavedQueryModalHeading = screen.getByRole('heading', {
name: 'Import queries',
});
expect(importSavedQueryModalHeading).toBeInTheDocument();
});
it('imports a saved query', async () => {
// Grab and click import saved query button to reveal modal
const importButton = await screen.findByTestId('import-button');
userEvent.click(importButton);
// Grab "Choose File" input from import modal
const chooseFileInput = screen.getByTestId('model-file-input');
// Upload mocked import file
userEvent.upload(chooseFileInput, mockImportFile);
expect(chooseFileInput.files[0]).toStrictEqual(mockImportFile);
expect(chooseFileInput.files.item(0)).toStrictEqual(mockImportFile);
expect(chooseFileInput.files).toHaveLength(1);
// Mock list response
fetchMock.get(
queriesEndpoint,
{ result: mockQueries, count: mockQueries.length },
{ overwriteRoutes: true },
);
renderList();
// Wait for list to load
await screen.findByTestId('saved_query-list-view');
// Wait for data to load
await waitFor(() => {
expect(screen.getByText(mockQueries[0].label)).toBeInTheDocument();
});
// Verify delete buttons are not shown
expect(screen.queryByTestId('delete-action')).not.toBeInTheDocument();
});
});