diff --git a/superset-frontend/spec/helpers/testing-library.tsx b/superset-frontend/spec/helpers/testing-library.tsx index 71672444d..c9e6c46cb 100644 --- a/superset-frontend/spec/helpers/testing-library.tsx +++ b/superset-frontend/spec/helpers/testing-library.tsx @@ -23,34 +23,41 @@ import { ThemeProvider, supersetTheme } from '@superset-ui/core'; import { Provider } from 'react-redux'; import { combineReducers, createStore, applyMiddleware, compose } from 'redux'; import thunk from 'redux-thunk'; +import { DndProvider } from 'react-dnd'; +import { HTML5Backend } from 'react-dnd-html5-backend'; import reducerIndex from 'spec/helpers/reducerIndex'; type Options = Omit & { useRedux?: boolean; + useDnd?: boolean; initialState?: {}; reducers?: {}; }; function createWrapper(options?: Options) { - const { useRedux, initialState, reducers } = options || {}; + const { useDnd, useRedux, initialState, reducers } = options || {}; - if (useRedux) { - const store = createStore( - combineReducers(reducers || reducerIndex), - initialState || {}, - compose(applyMiddleware(thunk)), + return ({ children }: { children?: ReactNode }) => { + let result = ( + {children} ); - return ({ children }: { children?: ReactNode }) => ( - - {children} - - ); - } + if (useDnd) { + result = {result}; + } - return ({ children }: { children?: ReactNode }) => ( - {children} - ); + if (useRedux) { + const store = createStore( + combineReducers(reducers || reducerIndex), + initialState || {}, + compose(applyMiddleware(thunk)), + ); + + result = {result}; + } + + return result; + }; } const customRender = (ui: ReactElement, options?: Options) => diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndColumnSelect.test.tsx b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndColumnSelect.test.tsx new file mode 100644 index 000000000..3a1325875 --- /dev/null +++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndColumnSelect.test.tsx @@ -0,0 +1,40 @@ +/** + * 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 React from 'react'; +import { render, screen } from 'spec/helpers/testing-library'; +import { LabelProps } from 'src/explore/components/controls/DndColumnSelectControl/types'; +import { DndColumnSelect } from 'src/explore/components/controls/DndColumnSelectControl/DndColumnSelect'; + +const defaultProps: LabelProps = { + name: 'Filter', + onChange: jest.fn(), + options: { string: { column_name: 'Column A' } }, +}; + +test('renders with default props', () => { + render(, { useDnd: true }); + expect(screen.getByText('Drop columns')).toBeInTheDocument(); +}); + +test('renders with value', () => { + render(, { + useDnd: true, + }); + expect(screen.getByText('Column A')).toBeInTheDocument(); +}); diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndColumnSelect.tsx b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndColumnSelect.tsx index 67c8d615a..8a680bd39 100644 --- a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndColumnSelect.tsx +++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndColumnSelect.tsx @@ -19,12 +19,12 @@ import React, { useState } from 'react'; import { ColumnMeta, ColumnOption } from '@superset-ui/chart-controls'; import { isEmpty } from 'lodash'; -import { LabelProps } from './types'; -import DndSelectLabel from './DndSelectLabel'; -import OptionWrapper from './components/OptionWrapper'; -import { OptionSelector } from './utils'; -import { DatasourcePanelDndItem } from '../../DatasourcePanel/types'; -import { DndItemType } from '../../DndItemType'; +import { LabelProps } from 'src/explore/components/controls/DndColumnSelectControl/types'; +import DndSelectLabel from 'src/explore/components/controls/DndColumnSelectControl/DndSelectLabel'; +import OptionWrapper from 'src/explore/components/controls/DndColumnSelectControl/OptionWrapper'; +import { OptionSelector } from 'src/explore/components/controls/DndColumnSelectControl/utils'; +import { DatasourcePanelDndItem } from 'src/explore/components/DatasourcePanel/types'; +import { DndItemType } from 'src/explore/components/DndItemType'; export const DndColumnSelect = (props: LabelProps) => { const { value, options } = props; diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndFilterSelect.test.tsx b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndFilterSelect.test.tsx new file mode 100644 index 000000000..2f1b0e9b4 --- /dev/null +++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndFilterSelect.test.tsx @@ -0,0 +1,83 @@ +/** + * 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 React from 'react'; +import { render, screen } from 'spec/helpers/testing-library'; +import AdhocMetric from 'src/explore/components/controls/MetricControl/AdhocMetric'; +import AdhocFilter, { + EXPRESSION_TYPES, +} from 'src/explore/components/controls/FilterControl/AdhocFilter'; +import { DndFilterSelect } from 'src/explore/components/controls/DndColumnSelectControl/DndFilterSelect'; + +const defaultProps = { + name: 'Filter', + value: [], + columns: [], + datasource: {}, + formData: {}, + savedMetrics: [], + onChange: jest.fn(), + options: { string: { column_name: 'Column' } }, +}; + +test('renders with default props', () => { + render(, { useDnd: true }); + expect(screen.getByText('Drop columns or metrics')).toBeInTheDocument(); +}); + +test('renders with value', () => { + const value = new AdhocFilter({ + sqlExpression: 'COUNT(*)', + expressionType: EXPRESSION_TYPES.SQL, + }); + render(, { + useDnd: true, + }); + expect(screen.getByText('COUNT(*)')).toBeInTheDocument(); +}); + +test('renders options with saved metric', () => { + render(, { + useDnd: true, + }); + expect(screen.getByText('Drop columns or metrics')).toBeInTheDocument(); +}); + +test('renders options with column', () => { + render( + , + { + useDnd: true, + }, + ); + expect(screen.getByText('Drop columns or metrics')).toBeInTheDocument(); +}); + +test('renders options with adhoc metric', () => { + const adhocMetric = new AdhocMetric({ + expression: 'AVG(birth_names.num)', + metric_name: 'avg__num', + }); + render(, { + useDnd: true, + }); + expect(screen.getByText('Drop columns or metrics')).toBeInTheDocument(); +}); diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndFilterSelect.tsx b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndFilterSelect.tsx index 9e94d0ac6..38fb2d367 100644 --- a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndFilterSelect.tsx +++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndFilterSelect.tsx @@ -22,20 +22,23 @@ import { ColumnMeta } from '@superset-ui/chart-controls'; import { Tooltip } from 'src/common/components/Tooltip'; import { OPERATORS } from 'src/explore/constants'; import { OptionSortType } from 'src/explore/types'; -import { DndFilterSelectProps, OptionValueType } from './types'; -import AdhocFilterPopoverTrigger from '../FilterControl/AdhocFilterPopoverTrigger'; -import OptionWrapper from './components/OptionWrapper'; -import DndSelectLabel from './DndSelectLabel'; +import { + DndFilterSelectProps, + OptionValueType, +} from 'src/explore/components/controls/DndColumnSelectControl/types'; +import AdhocFilterPopoverTrigger from 'src/explore/components/controls/FilterControl/AdhocFilterPopoverTrigger'; +import OptionWrapper from 'src/explore/components/controls/DndColumnSelectControl/OptionWrapper'; +import DndSelectLabel from 'src/explore/components/controls/DndColumnSelectControl/DndSelectLabel'; import AdhocFilter, { CLAUSES, EXPRESSION_TYPES, -} from '../FilterControl/AdhocFilter'; -import AdhocMetric from '../MetricControl/AdhocMetric'; +} from 'src/explore/components/controls/FilterControl/AdhocFilter'; +import AdhocMetric from 'src/explore/components/controls/MetricControl/AdhocMetric'; import { DatasourcePanelDndItem, DndItemValue, -} from '../../DatasourcePanel/types'; -import { DndItemType } from '../../DndItemType'; +} from 'src/explore/components/DatasourcePanel/types'; +import { DndItemType } from 'src/explore/components/DndItemType'; const isDictionaryForAdhocFilter = (value: OptionValueType) => !(value instanceof AdhocFilter) && value?.expressionType; diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndMetricSelect.test.tsx b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndMetricSelect.test.tsx new file mode 100644 index 000000000..eaa18babe --- /dev/null +++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndMetricSelect.test.tsx @@ -0,0 +1,35 @@ +/** + * 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 React from 'react'; +import { render, screen } from 'spec/helpers/testing-library'; +import { DndMetricSelect } from 'src/explore/components/controls/DndColumnSelectControl/DndMetricSelect'; + +const defaultProps = { + savedMetrics: [ + { + metric_name: 'Metric A', + expression: 'Expression A', + }, + ], +}; + +test('renders with default props', () => { + render(, { useDnd: true }); + expect(screen.getByText('Drop columns or metrics')).toBeInTheDocument(); +}); diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndMetricSelect.tsx b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndMetricSelect.tsx index eb55faaf3..f28dcee42 100644 --- a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndMetricSelect.tsx +++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndMetricSelect.tsx @@ -22,14 +22,14 @@ import { ensureIsArray, Metric, t } from '@superset-ui/core'; import { ColumnMeta } from '@superset-ui/chart-controls'; import { isEqual } from 'lodash'; import { usePrevious } from 'src/common/hooks/usePrevious'; -import AdhocMetric from '../MetricControl/AdhocMetric'; -import AdhocMetricPopoverTrigger from '../MetricControl/AdhocMetricPopoverTrigger'; -import MetricDefinitionValue from '../MetricControl/MetricDefinitionValue'; -import { OptionValueType } from './types'; -import { DatasourcePanelDndItem } from '../../DatasourcePanel/types'; -import { DndItemType } from '../../DndItemType'; -import DndSelectLabel from './DndSelectLabel'; -import { savedMetricType } from '../MetricControl/types'; +import AdhocMetric from 'src/explore/components/controls/MetricControl/AdhocMetric'; +import AdhocMetricPopoverTrigger from 'src/explore/components/controls/MetricControl/AdhocMetricPopoverTrigger'; +import MetricDefinitionValue from 'src/explore/components/controls/MetricControl/MetricDefinitionValue'; +import { OptionValueType } from 'src/explore/components/controls/DndColumnSelectControl/types'; +import { DatasourcePanelDndItem } from 'src/explore/components/DatasourcePanel/types'; +import { DndItemType } from 'src/explore/components/DndItemType'; +import DndSelectLabel from 'src/explore/components/controls/DndColumnSelectControl/DndSelectLabel'; +import { savedMetricType } from 'src/explore/components/controls/MetricControl/types'; const isDictionaryForAdhocMetric = (value: any) => value && !(value instanceof AdhocMetric) && value.expressionType; diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndSelectLabel.test.tsx b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndSelectLabel.test.tsx new file mode 100644 index 000000000..82d99266e --- /dev/null +++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndSelectLabel.test.tsx @@ -0,0 +1,55 @@ +/** + * 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 React from 'react'; +import { render, screen } from 'spec/helpers/testing-library'; +import { DndItemType } from 'src/explore/components/DndItemType'; +import DndSelectLabel from 'src/explore/components/controls/DndColumnSelectControl/DndSelectLabel'; + +const defaultProps = { + name: 'Column', + accept: 'Column' as DndItemType, + onDrop: jest.fn(), + canDrop: () => false, + valuesRenderer: () => , + onChange: jest.fn(), + options: { string: { column_name: 'Column' } }, +}; + +test('renders with default props', () => { + render(, { useDnd: true }); + expect(screen.getByText('Drop columns')).toBeInTheDocument(); +}); + +test('renders ghost button when empty', () => { + const ghostButtonText = 'Ghost button text'; + render( + , + { useDnd: true }, + ); + expect(screen.getByText(ghostButtonText)).toBeInTheDocument(); +}); + +test('renders values', () => { + const values = 'Values'; + const valuesRenderer = () => {values}; + render(, { + useDnd: true, + }); + expect(screen.getByText(values)).toBeInTheDocument(); +}); diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/Option.test.tsx b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/Option.test.tsx new file mode 100644 index 000000000..7a85997db --- /dev/null +++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/Option.test.tsx @@ -0,0 +1,56 @@ +/** + * 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 React from 'react'; +import { render, screen } from 'spec/helpers/testing-library'; +import userEvent from '@testing-library/user-event'; +import Option from 'src/explore/components/controls/DndColumnSelectControl/Option'; + +test('renders with default props', () => { + const { container } = render( + , + ); + expect(container).toBeInTheDocument(); + expect(screen.getByRole('img', { name: 'x-small' })).toBeInTheDocument(); + expect( + screen.queryByRole('img', { name: 'caret-right' }), + ).not.toBeInTheDocument(); +}); + +test('renders with caret', () => { + render( + , + ); + expect(screen.getByRole('img', { name: 'x-small' })).toBeInTheDocument(); + expect(screen.getByRole('img', { name: 'caret-right' })).toBeInTheDocument(); +}); + +test('triggers onClose', () => { + const clickClose = jest.fn(); + render( + , + ); + userEvent.click(screen.getByRole('img', { name: 'x-small' })); + expect(clickClose).toHaveBeenCalled(); +}); diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/components/Option.tsx b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/Option.tsx similarity index 95% rename from superset-frontend/src/explore/components/controls/DndColumnSelectControl/components/Option.tsx rename to superset-frontend/src/explore/components/controls/DndColumnSelectControl/Option.tsx index 7afd96cc0..654eadf4f 100644 --- a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/components/Option.tsx +++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/Option.tsx @@ -25,7 +25,7 @@ import { OptionControlContainer, Label, } from 'src/explore/components/controls/OptionControls'; -import { OptionProps } from '../types'; +import { OptionProps } from 'src/explore/components/controls/DndColumnSelectControl/types'; export default function Option(props: OptionProps) { const theme = useTheme(); diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/OptionWrapper.test.tsx b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/OptionWrapper.test.tsx new file mode 100644 index 000000000..c46f49be0 --- /dev/null +++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/OptionWrapper.test.tsx @@ -0,0 +1,67 @@ +/** + * 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 React from 'react'; +import { render, screen, fireEvent } from 'spec/helpers/testing-library'; +import { DndItemType } from 'src/explore/components/DndItemType'; +import OptionWrapper from 'src/explore/components/controls/DndColumnSelectControl/OptionWrapper'; + +test('renders with default props', () => { + const { container } = render( + + Option + , + { useDnd: true }, + ); + expect(container).toBeInTheDocument(); + expect(screen.getByRole('img', { name: 'x-small' })).toBeInTheDocument(); +}); + +test('triggers onShiftOptions on drop', () => { + const onShiftOptions = jest.fn(); + render( + <> + + Option 1 + + + Option 2 + + , + { useDnd: true }, + ); + + fireEvent.dragStart(screen.getByText('Option 1')); + fireEvent.drop(screen.getByText('Option 2')); + expect(onShiftOptions).toHaveBeenCalled(); +}); diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/components/OptionWrapper.tsx b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/OptionWrapper.tsx similarity index 89% rename from superset-frontend/src/explore/components/controls/DndColumnSelectControl/components/OptionWrapper.tsx rename to superset-frontend/src/explore/components/controls/DndColumnSelectControl/OptionWrapper.tsx index f1520aa3a..8912c23df 100644 --- a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/components/OptionWrapper.tsx +++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/OptionWrapper.tsx @@ -25,11 +25,17 @@ import { } from 'react-dnd'; import { DragContainer } from 'src/explore/components/controls/OptionControls'; import { DndItemType } from 'src/explore/components/DndItemType'; +import { + OptionProps, + OptionItemInterface, +} from 'src/explore/components/controls/DndColumnSelectControl/types'; import Option from './Option'; -import { OptionProps, OptionItemInterface } from '../types'; export default function OptionWrapper( - props: OptionProps & { type: DndItemType }, + props: OptionProps & { + type: DndItemType; + onShiftOptions: (dragIndex: number, hoverIndex: number) => void; + }, ) { const { index, @@ -99,13 +105,8 @@ export default function OptionWrapper( drag(drop(ref)); return ( - -