Skip to content

Commit

Permalink
feat(listviews): SIP-34 Bulk Select (apache#10298)
Browse files Browse the repository at this point in the history
  • Loading branch information
nytai authored and auxten committed Nov 20, 2020
1 parent 527bc7e commit b0b0ec0
Show file tree
Hide file tree
Showing 18 changed files with 590 additions and 286 deletions.
1 change: 1 addition & 0 deletions superset-frontend/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ module.exports = {
'\\.(gif|ttf|eot)$': '<rootDir>/spec/__mocks__/fileMock.js',
'\\.svg$': '<rootDir>/spec/__mocks__/svgrMock.js',
'^src/(.*)$': '<rootDir>/src/$1',
'^spec/(.*)$': '<rootDir>/spec/$1',
},
setupFilesAfterEnv: ['<rootDir>/spec/helpers/shim.js'],
testURL: 'http://localhost',
Expand Down
34 changes: 34 additions & 0 deletions superset-frontend/spec/helpers/waitForComponentToPaint.ts
Original file line number Diff line number Diff line change
@@ -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();
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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(),
},
],
};

Expand All @@ -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();
Expand Down Expand Up @@ -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 [
Expand All @@ -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 [
Expand All @@ -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 = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand All @@ -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(),
Expand All @@ -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: [],
Expand All @@ -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);
Expand All @@ -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)"`);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import React from 'react';
import { t } from '@superset-ui/translation';

import Button from '../../components/Button';
import Button, { ButtonProps } from '../../components/Button';

const NO_OP = () => undefined;

Expand Down Expand Up @@ -47,7 +47,7 @@ const RunQueryActionButton = ({
const shouldShowStopBtn =
!!queryState && ['running', 'pending'].indexOf(queryState) > -1;

const commonBtnProps = {
const commonBtnProps: ButtonProps = {
bsSize: 'small',
bsStyle: btnStyle,
disabled: !dbId,
Expand Down
Loading

0 comments on commit b0b0ec0

Please sign in to comment.