mirror of
https://github.com/apache/superset.git
synced 2026-05-12 11:25:56 +00:00
test: Adds tests to the OptionControls component (#13729)
This commit is contained in:
committed by
GitHub
parent
ec5d2f5f32
commit
6fd62e3f7e
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* 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 { DndProvider } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import {
|
||||
OptionControlLabel,
|
||||
DragContainer,
|
||||
OptionControlContainer,
|
||||
Label,
|
||||
CaretContainer,
|
||||
CloseContainer,
|
||||
HeaderContainer,
|
||||
LabelsContainer,
|
||||
DndLabelsContainer,
|
||||
AddControlLabel,
|
||||
AddIconButton,
|
||||
} from 'src/explore/components/controls/OptionControls';
|
||||
|
||||
const defaultProps = {
|
||||
label: <span>Test label</span>,
|
||||
onRemove: jest.fn(),
|
||||
onMoveLabel: jest.fn(),
|
||||
onDropLabel: jest.fn(),
|
||||
type: 'test',
|
||||
index: 0,
|
||||
};
|
||||
|
||||
const setup = (overrides?: Record<string, any>) =>
|
||||
render(
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<OptionControlLabel {...defaultProps} {...overrides} />
|
||||
</DndProvider>,
|
||||
);
|
||||
|
||||
test('should render', () => {
|
||||
const { container } = setup();
|
||||
expect(container).toBeVisible();
|
||||
});
|
||||
|
||||
test('should display a label', () => {
|
||||
setup();
|
||||
expect(screen.getByText('Test label')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should display a certification icon if saved metric is certified', () => {
|
||||
const { container } = setup({
|
||||
savedMetric: {
|
||||
metric_name: 'test_metric',
|
||||
is_certified: true,
|
||||
},
|
||||
});
|
||||
screen.getByText('test_metric');
|
||||
expect(screen.queryByText('Test label')).toBeFalsy();
|
||||
expect(container.querySelector('.metric-option > svg')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('triggers onMoveLabel on drop', () => {
|
||||
render(
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<OptionControlLabel
|
||||
{...defaultProps}
|
||||
index={1}
|
||||
label={<span>Label 1</span>}
|
||||
/>
|
||||
<OptionControlLabel
|
||||
{...defaultProps}
|
||||
index={2}
|
||||
label={<span>Label 2</span>}
|
||||
/>
|
||||
</DndProvider>,
|
||||
);
|
||||
fireEvent.dragStart(screen.getByText('Label 1'));
|
||||
fireEvent.drop(screen.getByText('Label 2'));
|
||||
expect(defaultProps.onMoveLabel).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('renders DragContainer', () => {
|
||||
const { container } = render(<DragContainer />);
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders OptionControlContainer', () => {
|
||||
const { container } = render(<OptionControlContainer />);
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders Label', () => {
|
||||
const { container } = render(<Label />);
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders CaretContainer', () => {
|
||||
const { container } = render(<CaretContainer />);
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders CloseContainer', () => {
|
||||
const { container } = render(<CloseContainer />);
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders HeaderContainer', () => {
|
||||
const { container } = render(<HeaderContainer />);
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders LabelsContainer', () => {
|
||||
const { container } = render(<LabelsContainer />);
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders DndLabelsContainer', () => {
|
||||
const { container } = render(<DndLabelsContainer />);
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders AddControlLabel', () => {
|
||||
const { container } = render(<AddControlLabel />);
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders AddIconButton', () => {
|
||||
const { container } = render(<AddIconButton />);
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
@@ -0,0 +1,292 @@
|
||||
/**
|
||||
* 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, { useRef } from 'react';
|
||||
import { useDrag, useDrop, DropTargetMonitor } from 'react-dnd';
|
||||
import { styled, t, useTheme } from '@superset-ui/core';
|
||||
import {
|
||||
MetricOption,
|
||||
InfoTooltipWithTrigger,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import { Tooltip } from 'src/common/components/Tooltip';
|
||||
import Icon from 'src/components/Icon';
|
||||
import { savedMetricType } from 'src/explore/components/controls/MetricControl/types';
|
||||
import AdhocMetric from 'src/explore/components/controls/MetricControl/AdhocMetric';
|
||||
|
||||
export const DragContainer = styled.div`
|
||||
margin-bottom: ${({ theme }) => theme.gridUnit}px;
|
||||
:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export const OptionControlContainer = styled.div<{
|
||||
withCaret?: boolean;
|
||||
}>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
font-size: ${({ theme }) => theme.typography.sizes.s}px;
|
||||
height: ${({ theme }) => theme.gridUnit * 6}px;
|
||||
background-color: ${({ theme }) => theme.colors.grayscale.light3};
|
||||
border-radius: 3px;
|
||||
cursor: ${({ withCaret }) => (withCaret ? 'pointer' : 'default')};
|
||||
`;
|
||||
|
||||
export const Label = styled.div`
|
||||
display: flex;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
padding-left: ${({ theme }) => theme.gridUnit}px;
|
||||
svg {
|
||||
margin-right: ${({ theme }) => theme.gridUnit}px;
|
||||
}
|
||||
.option-label {
|
||||
display: inline;
|
||||
}
|
||||
`;
|
||||
|
||||
export const CaretContainer = styled.div`
|
||||
height: 100%;
|
||||
border-left: solid 1px ${({ theme }) => theme.colors.grayscale.dark2}0C;
|
||||
margin-left: auto;
|
||||
`;
|
||||
|
||||
export const CloseContainer = styled.div`
|
||||
height: 100%;
|
||||
width: ${({ theme }) => theme.gridUnit * 6}px;
|
||||
border-right: solid 1px ${({ theme }) => theme.colors.grayscale.dark2}0C;
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
const StyledInfoTooltipWithTrigger = styled(InfoTooltipWithTrigger)`
|
||||
margin: 0 ${({ theme }) => theme.gridUnit}px;
|
||||
`;
|
||||
|
||||
export const HeaderContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
export const LabelsContainer = styled.div`
|
||||
padding: ${({ theme }) => theme.gridUnit}px;
|
||||
border: solid 1px ${({ theme }) => theme.colors.grayscale.light2};
|
||||
border-radius: ${({ theme }) => theme.gridUnit}px;
|
||||
`;
|
||||
|
||||
export const DndLabelsContainer = styled.div<{
|
||||
canDrop?: boolean;
|
||||
isOver?: boolean;
|
||||
}>`
|
||||
padding: ${({ theme }) => theme.gridUnit}px;
|
||||
border: ${({ canDrop, isOver, theme }) => {
|
||||
if (canDrop) {
|
||||
return `dashed 1px ${theme.colors.info.dark1}`;
|
||||
}
|
||||
if (isOver && !canDrop) {
|
||||
return `dashed 1px ${theme.colors.error.dark1}`;
|
||||
}
|
||||
return `solid 1px ${theme.colors.grayscale.light2}`;
|
||||
}};
|
||||
border-radius: ${({ theme }) => theme.gridUnit}px;
|
||||
`;
|
||||
|
||||
export const AddControlLabel = styled.div<{
|
||||
cancelHover?: boolean;
|
||||
}>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: ${({ theme }) => theme.gridUnit * 6}px;
|
||||
padding-left: ${({ theme }) => theme.gridUnit}px;
|
||||
font-size: ${({ theme }) => theme.typography.sizes.s}px;
|
||||
color: ${({ theme }) => theme.colors.grayscale.light1};
|
||||
border: dashed 1px ${({ theme }) => theme.colors.grayscale.light2};
|
||||
border-radius: ${({ theme }) => theme.gridUnit}px;
|
||||
cursor: ${({ cancelHover }) => (cancelHover ? 'inherit' : 'pointer')};
|
||||
|
||||
:hover {
|
||||
background-color: ${({ cancelHover, theme }) =>
|
||||
cancelHover ? 'inherit' : theme.colors.grayscale.light4};
|
||||
}
|
||||
|
||||
:active {
|
||||
background-color: ${({ cancelHover, theme }) =>
|
||||
cancelHover ? 'inherit' : theme.colors.grayscale.light3};
|
||||
}
|
||||
`;
|
||||
|
||||
export const AddIconButton = styled.button`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: ${({ theme }) => theme.gridUnit * 4}px;
|
||||
width: ${({ theme }) => theme.gridUnit * 4}px;
|
||||
padding: 0;
|
||||
background-color: ${({ theme }) => theme.colors.primary.dark1};
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
|
||||
:disabled {
|
||||
cursor: not-allowed;
|
||||
background-color: ${({ theme }) => theme.colors.grayscale.light1};
|
||||
}
|
||||
`;
|
||||
|
||||
interface DragItem {
|
||||
index: number;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export const OptionControlLabel = ({
|
||||
label,
|
||||
savedMetric,
|
||||
adhocMetric,
|
||||
onRemove,
|
||||
onMoveLabel,
|
||||
onDropLabel,
|
||||
withCaret,
|
||||
isFunction,
|
||||
type,
|
||||
index,
|
||||
isExtra,
|
||||
...props
|
||||
}: {
|
||||
label: string | React.ReactNode;
|
||||
savedMetric?: savedMetricType;
|
||||
adhocMetric?: AdhocMetric;
|
||||
onRemove: () => void;
|
||||
onMoveLabel: (dragIndex: number, hoverIndex: number) => void;
|
||||
onDropLabel: () => void;
|
||||
withCaret?: boolean;
|
||||
isFunction?: boolean;
|
||||
isDraggable?: boolean;
|
||||
type: string;
|
||||
index: number;
|
||||
isExtra?: boolean;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [, drop] = useDrop({
|
||||
accept: type,
|
||||
drop() {
|
||||
onDropLabel?.();
|
||||
},
|
||||
hover(item: DragItem, monitor: DropTargetMonitor) {
|
||||
if (!ref.current) {
|
||||
return;
|
||||
}
|
||||
const dragIndex = item.index;
|
||||
const hoverIndex = index;
|
||||
// Don't replace items with themselves
|
||||
if (dragIndex === hoverIndex) {
|
||||
return;
|
||||
}
|
||||
// Determine rectangle on screen
|
||||
const hoverBoundingRect = ref.current?.getBoundingClientRect();
|
||||
// Get vertical middle
|
||||
const hoverMiddleY =
|
||||
(hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
|
||||
// Determine mouse position
|
||||
const clientOffset = monitor.getClientOffset();
|
||||
// Get pixels to the top
|
||||
const hoverClientY = clientOffset?.y
|
||||
? clientOffset?.y - hoverBoundingRect.top
|
||||
: 0;
|
||||
// Only perform the move when the mouse has crossed half of the items height
|
||||
// When dragging downwards, only move when the cursor is below 50%
|
||||
// When dragging upwards, only move when the cursor is above 50%
|
||||
// Dragging downwards
|
||||
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
|
||||
return;
|
||||
}
|
||||
// Dragging upwards
|
||||
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
|
||||
return;
|
||||
}
|
||||
// Time to actually perform the action
|
||||
onMoveLabel?.(dragIndex, hoverIndex);
|
||||
// Note: we're mutating the monitor item here!
|
||||
// Generally it's better to avoid mutations,
|
||||
// but it's good here for the sake of performance
|
||||
// to avoid expensive index searches.
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
item.index = hoverIndex;
|
||||
},
|
||||
});
|
||||
const [, drag] = useDrag({
|
||||
item: {
|
||||
type,
|
||||
index,
|
||||
value: savedMetric?.metric_name ? savedMetric : adhocMetric,
|
||||
},
|
||||
collect: monitor => ({
|
||||
isDragging: monitor.isDragging(),
|
||||
}),
|
||||
});
|
||||
|
||||
const getLabelContent = () => {
|
||||
if (savedMetric?.metric_name) {
|
||||
return <MetricOption metric={savedMetric} />;
|
||||
}
|
||||
return <Tooltip title={label}>{label}</Tooltip>;
|
||||
};
|
||||
|
||||
const getOptionControlContent = () => (
|
||||
<OptionControlContainer
|
||||
withCaret={withCaret}
|
||||
data-test="option-label"
|
||||
{...props}
|
||||
>
|
||||
<CloseContainer
|
||||
role="button"
|
||||
data-test="remove-control-button"
|
||||
onClick={onRemove}
|
||||
>
|
||||
<Icon name="x-small" color={theme.colors.grayscale.light1} />
|
||||
</CloseContainer>
|
||||
<Label data-test="control-label">
|
||||
{isFunction && <Icon name="function" viewBox="0 0 16 11" />}
|
||||
{getLabelContent()}
|
||||
</Label>
|
||||
{isExtra && (
|
||||
<StyledInfoTooltipWithTrigger
|
||||
icon="exclamation-triangle"
|
||||
placement="top"
|
||||
bsStyle="warning"
|
||||
tooltip={t(`
|
||||
This filter was inherited from the dashboard's context.
|
||||
It won't be saved when saving the chart.
|
||||
`)}
|
||||
/>
|
||||
)}
|
||||
{withCaret && (
|
||||
<CaretContainer>
|
||||
<Icon name="caret-right" color={theme.colors.grayscale.light1} />
|
||||
</CaretContainer>
|
||||
)}
|
||||
</OptionControlContainer>
|
||||
);
|
||||
|
||||
drag(drop(ref));
|
||||
return <DragContainer ref={ref}>{getOptionControlContent()}</DragContainer>;
|
||||
};
|
||||
Reference in New Issue
Block a user