mirror of
https://github.com/apache/superset.git
synced 2026-04-19 16:14:52 +00:00
feat(listviews): SIP-34 Bulk Select (#10298)
This commit is contained in:
34
superset-frontend/spec/helpers/waitForComponentToPaint.ts
Normal file
34
superset-frontend/spec/helpers/waitForComponentToPaint.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ReactWrapper } from 'enzyme';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
// taken from: https://github.com/enzymejs/enzyme/issues/2073
|
||||
// There is currently and issue with enzyme and react-16's hooks
|
||||
// that results in a race condition between tests and react hook updates.
|
||||
// This function ensures tests run after all react updates are done.
|
||||
export default async function waitForComponentToPaint<P = {}>(
|
||||
wrapper: ReactWrapper<P>,
|
||||
amount = 0,
|
||||
) {
|
||||
await act(async () => {
|
||||
await new Promise(resolve => setTimeout(resolve, amount));
|
||||
wrapper.update();
|
||||
});
|
||||
}
|
||||
@@ -22,13 +22,17 @@ import { act } from 'react-dom/test-utils';
|
||||
import { MenuItem } from 'react-bootstrap';
|
||||
import Select from 'src/components/Select';
|
||||
import { QueryParamProvider } from 'use-query-params';
|
||||
import { supersetTheme, ThemeProvider } from '@superset-ui/style';
|
||||
|
||||
import ListView from 'src/components/ListView/ListView';
|
||||
import ListViewFilters from 'src/components/ListView/Filters';
|
||||
import ListViewPagination from 'src/components/ListView/Pagination';
|
||||
import Pagination from 'src/components/Pagination';
|
||||
import Button from 'src/components/Button';
|
||||
import { areArraysShallowEqual } from 'src/reduxUtils';
|
||||
import { supersetTheme, ThemeProvider } from '@superset-ui/style';
|
||||
|
||||
import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
|
||||
import IndeterminateCheckbox from 'src/components/IndeterminateCheckbox';
|
||||
|
||||
function makeMockLocation(query) {
|
||||
const queryStr = encodeURIComponent(query);
|
||||
@@ -72,8 +76,15 @@ const mockedProps = {
|
||||
pageSize: 1,
|
||||
fetchData: jest.fn(() => []),
|
||||
loading: false,
|
||||
bulkSelectEnabled: true,
|
||||
disableBulkSelect: jest.fn(),
|
||||
bulkActions: [
|
||||
{ key: 'something', name: 'do something', onSelect: jest.fn() },
|
||||
{
|
||||
key: 'something',
|
||||
name: 'do something',
|
||||
style: 'danger',
|
||||
onSelect: jest.fn(),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -89,7 +100,10 @@ const factory = (props = mockedProps) =>
|
||||
);
|
||||
|
||||
describe('ListView', () => {
|
||||
const wrapper = factory();
|
||||
let wrapper = beforeAll(async () => {
|
||||
wrapper = factory();
|
||||
await waitForComponentToPaint(wrapper);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockedProps.fetchData.mockClear();
|
||||
@@ -227,18 +241,17 @@ Array [
|
||||
wrapper.find('input[id="0"]').at(0).prop('onChange')({
|
||||
target: { value: 'on' },
|
||||
});
|
||||
});
|
||||
wrapper.update();
|
||||
|
||||
act(() => {
|
||||
wrapper
|
||||
.find('.dropdown-toggle')
|
||||
.children('button')
|
||||
.at(1)
|
||||
.find('[data-test="bulk-select-controls"]')
|
||||
.find(Button)
|
||||
.props()
|
||||
.onClick();
|
||||
});
|
||||
wrapper.update();
|
||||
const bulkActionsProps = wrapper.find(MenuItem).last().props();
|
||||
|
||||
bulkActionsProps.onSelect(bulkActionsProps.eventKey);
|
||||
expect(mockedProps.bulkActions[0].onSelect.mock.calls[0])
|
||||
.toMatchInlineSnapshot(`
|
||||
Array [
|
||||
@@ -257,18 +270,17 @@ Array [
|
||||
wrapper.find('input[id="header-toggle-all"]').at(0).prop('onChange')({
|
||||
target: { value: 'on' },
|
||||
});
|
||||
});
|
||||
wrapper.update();
|
||||
|
||||
act(() => {
|
||||
wrapper
|
||||
.find('.dropdown-toggle')
|
||||
.children('button')
|
||||
.at(1)
|
||||
.find('[data-test="bulk-select-controls"]')
|
||||
.find(Button)
|
||||
.props()
|
||||
.onClick();
|
||||
});
|
||||
wrapper.update();
|
||||
const bulkActionsProps = wrapper.find(MenuItem).last().props();
|
||||
|
||||
bulkActionsProps.onSelect(bulkActionsProps.eventKey);
|
||||
expect(mockedProps.bulkActions[0].onSelect.mock.calls[0])
|
||||
.toMatchInlineSnapshot(`
|
||||
Array [
|
||||
@@ -286,6 +298,34 @@ Array [
|
||||
`);
|
||||
});
|
||||
|
||||
it('allows deselecting all', async () => {
|
||||
act(() => {
|
||||
wrapper.find('[data-test="bulk-select-deselect-all"]').props().onClick();
|
||||
});
|
||||
await waitForComponentToPaint(wrapper);
|
||||
wrapper.update();
|
||||
wrapper.find(IndeterminateCheckbox).forEach(input => {
|
||||
expect(input.props().checked).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('allows disabling bulkSelect', () => {
|
||||
wrapper
|
||||
.find('[data-test="bulk-select-controls"]')
|
||||
.at(0)
|
||||
.props()
|
||||
.onDismiss();
|
||||
expect(mockedProps.disableBulkSelect).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('disables bulk select based on prop', async () => {
|
||||
const wrapper2 = factory({ ...mockedProps, bulkSelectEnabled: false });
|
||||
await waitForComponentToPaint(wrapper2);
|
||||
expect(wrapper2.find('[data-test="bulk-select-controls"]').exists()).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('Throws an exception if filter missing in columns', () => {
|
||||
expect.assertions(1);
|
||||
const props = {
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
*/
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { supersetTheme, ThemeProvider } from '@superset-ui/style';
|
||||
|
||||
import CodeModal from 'src/dashboard/components/CodeModal';
|
||||
|
||||
@@ -29,7 +30,10 @@ describe('CodeModal', () => {
|
||||
expect(React.isValidElement(<CodeModal {...mockedProps} />)).toBe(true);
|
||||
});
|
||||
it('renders the trigger node', () => {
|
||||
const wrapper = mount(<CodeModal {...mockedProps} />);
|
||||
const wrapper = mount(<CodeModal {...mockedProps} />, {
|
||||
wrappingComponent: ThemeProvider,
|
||||
wrappingComponentProps: { theme: supersetTheme },
|
||||
});
|
||||
expect(wrapper.find('.fa-edit')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,10 +21,14 @@ import { mount } from 'enzyme';
|
||||
import thunk from 'redux-thunk';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import { supersetTheme, ThemeProvider } from '@superset-ui/style';
|
||||
|
||||
import DatasetList from 'src/views/datasetList/DatasetList';
|
||||
import ListView from 'src/components/ListView/ListView';
|
||||
import { supersetTheme, ThemeProvider } from '@superset-ui/style';
|
||||
import Button from 'src/components/Button';
|
||||
import IndeterminateCheckbox from 'src/components/IndeterminateCheckbox';
|
||||
import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
// store needed for withToasts(datasetTable)
|
||||
const mockStore = configureStore([thunk]);
|
||||
@@ -37,7 +41,7 @@ const datasetsEndpoint = 'glob:*/api/v1/dataset/?*';
|
||||
|
||||
const mockdatasets = [...new Array(3)].map((_, i) => ({
|
||||
changed_by_name: 'user',
|
||||
kind: ['physical', 'virtual'][Math.floor(Math.random() * 2)],
|
||||
kind: i === 0 ? 'virtual' : 'physical', // ensure there is 1 virtual
|
||||
changed_by_url: 'changed_by_url',
|
||||
changed_by: 'user',
|
||||
changed_on: new Date().toISOString(),
|
||||
@@ -49,7 +53,7 @@ const mockdatasets = [...new Array(3)].map((_, i) => ({
|
||||
}));
|
||||
|
||||
fetchMock.get(datasetsInfoEndpoint, {
|
||||
permissions: ['can_list', 'can_edit'],
|
||||
permissions: ['can_list', 'can_edit', 'can_add', 'can_delete'],
|
||||
filters: {
|
||||
database: [],
|
||||
schema: [],
|
||||
@@ -69,13 +73,24 @@ fetchMock.get(databaseEndpoint, {
|
||||
result: [],
|
||||
});
|
||||
|
||||
describe('DatasetList', () => {
|
||||
const mockedProps = {};
|
||||
const wrapper = mount(<DatasetList {...mockedProps} />, {
|
||||
async function mountAndWait(props) {
|
||||
const mounted = mount(<DatasetList {...props} />, {
|
||||
context: { store },
|
||||
wrappingComponent: ThemeProvider,
|
||||
wrappingComponentProps: { theme: supersetTheme },
|
||||
});
|
||||
await waitForComponentToPaint(mounted);
|
||||
|
||||
return mounted;
|
||||
}
|
||||
|
||||
describe('DatasetList', () => {
|
||||
const mockedProps = {};
|
||||
let wrapper;
|
||||
|
||||
beforeAll(async () => {
|
||||
wrapper = await mountAndWait(mockedProps);
|
||||
});
|
||||
|
||||
it('renders', () => {
|
||||
expect(wrapper.find(DatasetList)).toHaveLength(1);
|
||||
@@ -96,11 +111,63 @@ describe('DatasetList', () => {
|
||||
});
|
||||
|
||||
it('fetches data', () => {
|
||||
// wrapper.update();
|
||||
const callsD = fetchMock.calls(/dataset\/\?q/);
|
||||
expect(callsD).toHaveLength(1);
|
||||
expect(callsD[0][0]).toMatchInlineSnapshot(
|
||||
`"http://localhost/api/v1/dataset/?q=(order_column:changed_on,order_direction:desc,page:0,page_size:25)"`,
|
||||
);
|
||||
});
|
||||
|
||||
it('shows/hides bulk actions when bulk actions is clicked', async () => {
|
||||
await waitForComponentToPaint(wrapper);
|
||||
const button = wrapper.find(Button).at(0);
|
||||
act(() => {
|
||||
button.props().onClick();
|
||||
});
|
||||
await waitForComponentToPaint(wrapper);
|
||||
expect(wrapper.find(IndeterminateCheckbox)).toHaveLength(
|
||||
mockdatasets.length + 1, // 1 for each row and 1 for select all
|
||||
);
|
||||
});
|
||||
|
||||
it('renders different bulk selected copy depending on type of row selected', async () => {
|
||||
// None selected
|
||||
const checkedEvent = { target: { checked: true } };
|
||||
const uncheckedEvent = { target: { checked: false } };
|
||||
expect(
|
||||
wrapper.find('[data-test="bulk-select-copy"]').text(),
|
||||
).toMatchInlineSnapshot(`"0 Selected"`);
|
||||
|
||||
// Vitual Selected
|
||||
act(() => {
|
||||
wrapper.find(IndeterminateCheckbox).at(1).props().onChange(checkedEvent);
|
||||
});
|
||||
await waitForComponentToPaint(wrapper);
|
||||
expect(
|
||||
wrapper.find('[data-test="bulk-select-copy"]').text(),
|
||||
).toMatchInlineSnapshot(`"1 Selected (Virtual)"`);
|
||||
|
||||
// Physical Selected
|
||||
act(() => {
|
||||
wrapper
|
||||
.find(IndeterminateCheckbox)
|
||||
.at(1)
|
||||
.props()
|
||||
.onChange(uncheckedEvent);
|
||||
wrapper.find(IndeterminateCheckbox).at(2).props().onChange(checkedEvent);
|
||||
});
|
||||
await waitForComponentToPaint(wrapper);
|
||||
expect(
|
||||
wrapper.find('[data-test="bulk-select-copy"]').text(),
|
||||
).toMatchInlineSnapshot(`"1 Selected (Physical)"`);
|
||||
|
||||
// All Selected
|
||||
act(() => {
|
||||
wrapper.find(IndeterminateCheckbox).at(0).props().onChange(checkedEvent);
|
||||
});
|
||||
await waitForComponentToPaint(wrapper);
|
||||
expect(
|
||||
wrapper.find('[data-test="bulk-select-copy"]').text(),
|
||||
).toMatchInlineSnapshot(`"3 Selected (2 Physical, 1 Virtual)"`);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user