test: Adds tests to the OptionControls component (#13729)

This commit is contained in:
Michael S. Molina
2021-03-31 22:07:22 -03:00
committed by GitHub
parent ec5d2f5f32
commit 6fd62e3f7e
12 changed files with 156 additions and 82 deletions

View File

@@ -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();
});

View File

@@ -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>;
};