mirror of
https://github.com/apache/superset.git
synced 2026-04-18 07:35:09 +00:00
chore(tests): converting enzyme to RTL, part 3 (#32363)
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user