Select: Portal select menu to document.body (#36398)

* ValueMappings: Force overflowing modal content to scroll

* ValueMappings: Update unit tests

* Select: Portal Select to document.body, close menu on scroll

* Select: Fix tests + apply updates from https://github.com/grafana/grafana/pull/32833

* ValueMappingsEditorModal: Revert to using selectEvent in the tests

* Select: Fix remaining unit tests

* Portal: Rewrite Portal as a functional component so we can use useTheme2

* Modal: Remove modal styles from this PR

* Update E2E tests

* More unit test fixes

* Select: Fix remaining E2E tests

* Select: Create util method to select an option in tests
This commit is contained in:
Ashley Harrison 2021-07-14 14:04:23 +01:00 committed by GitHub
parent f41f00dec4
commit 54f8996acf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 178 additions and 237 deletions

View File

@ -15,10 +15,10 @@ export const smokeTestScenario = {
.should('be.visible')
.within(() => {
e2e.components.Select.input().should('be.visible').click();
cy.contains('CSV Metric Values').scrollIntoView().should('be.visible').click();
});
cy.contains('CSV Metric Values').scrollIntoView().should('be.visible').click();
// Make sure the graph renders via checking legend
e2e.components.VizLegend.seriesName('A-series').should('be.visible');

View File

@ -47,10 +47,10 @@ e2e.scenario({
e2e.components.Select.singleValue().should('be.visible').should('have.text', fromTimeZone);
e2e.components.Select.input().should('be.visible').click();
e2e.components.Select.option().should('be.visible').contains(toTimeZone).click();
});
e2e.components.Select.option().should('be.visible').contains(toTimeZone).click();
// click to go back to the dashboard.
e2e.components.BackButton.backArrow().click({ force: true }).wait(2000);

View File

@ -4,7 +4,7 @@ const dataSourceName = 'PromExemplar';
const addDataSource = () => {
e2e.flows.addDataSource({
type: 'Prometheus',
expectedAlertMessage: 'HTTP error Bad Gateway',
expectedAlertMessage: 'Bad Gateway',
name: dataSourceName,
form: () => {
e2e.components.DataSource.Prometheus.configPage.exemplarsAddButton().click();
@ -13,9 +13,9 @@ const addDataSource = () => {
.should('be.visible')
.within(() => {
e2e.components.Select.input().should('be.visible').click({ force: true });
e2e().contains('gdev-tempo').scrollIntoView().should('be.visible').click();
});
e2e().contains('gdev-tempo').scrollIntoView().should('be.visible').click();
},
});
};
@ -54,9 +54,8 @@ describe('Exemplars', () => {
.should('be.visible')
.within(() => {
e2e.components.Select.input().should('be.visible').click();
e2e().contains(dataSourceName).scrollIntoView().should('be.visible').click();
});
e2e().contains(dataSourceName).scrollIntoView().should('be.visible').click();
e2e.components.TimePicker.openButton().click();
e2e.components.TimePicker.fromField().clear().type('2021-05-11 19:30:00');
e2e.components.TimePicker.toField().clear().type('2021-05-11 21:40:00');

View File

@ -15,10 +15,10 @@ e2e.scenario({
.should('be.visible')
.within(() => {
e2e.components.Select.input().should('be.visible').click();
cy.contains('CSV Metric Values').scrollIntoView().should('be.visible').click();
});
cy.contains('CSV Metric Values').scrollIntoView().should('be.visible').click();
const canvases = e2e().get('canvas');
canvases.should('have.length', 1);
},

View File

@ -51,10 +51,10 @@ e2e.scenario({
.should('be.visible')
.within(() => {
e2e.components.Select.input().eq(0).should('be.visible').click();
cy.contains('CSV Metric Values').scrollIntoView().should('be.visible').eq(0).click();
});
cy.contains('CSV Metric Values').scrollIntoView().should('be.visible').eq(0).click();
// Disable / enable row
expectInspectorResultAndClose((keys) => {
const length = keys.length;

View File

@ -12,9 +12,9 @@ e2e.scenario({
.should('be.visible')
.within(() => {
e2e.components.Select.input().should('be.visible').click();
cy.contains('gdev-prometheus').scrollIntoView().should('be.visible').click();
});
cy.contains('gdev-prometheus').scrollIntoView().should('be.visible').click();
const queryText = 'http_requests_total';
e2e.components.QueryField.container().should('be.visible').type(queryText).type('{backspace}');

View File

@ -14,9 +14,13 @@ e2e.scenario({
.should('be.visible')
.within(() => {
e2e.components.Select.input().should('be.visible').click();
});
e2e.components.Select.option().should('be.visible').first().click();
e2e.components.Select.option().should('be.visible').first().click();
e2e.components.FolderPicker.container()
.should('be.visible')
.within(() => {
e2e.components.Select.input().should('exist').should('have.focus');
});

View File

@ -15,10 +15,10 @@ describe('Trace view', () => {
.should('be.visible')
.within(() => {
e2e.components.Select.input().should('be.visible').click();
e2e().contains('gdev-jaeger').scrollIntoView().should('be.visible').click();
});
e2e().contains('gdev-jaeger').scrollIntoView().should('be.visible').click();
e2e.components.DataSource.Jaeger.traceIDInput().should('be.visible').type('long-trace');
e2e.components.RefreshPicker.runButton().should('be.visible').click();

View File

@ -98,15 +98,7 @@ export function createV1Theme(theme: Omit<GrafanaTheme2, 'v1'>): GrafanaTheme {
},
panelPadding: theme.components.panel.padding * theme.spacing.gridSize,
panelHeaderHeight: theme.spacing.gridSize * theme.components.panel.headerHeight,
zIndex: {
navbarFixed: theme.zIndex.navbarFixed,
sidemenu: theme.zIndex.sidemenu,
dropdown: theme.zIndex.dropdown,
typeahead: theme.zIndex.typeahead,
tooltip: theme.zIndex.tooltip,
modalBackdrop: theme.zIndex.modalBackdrop,
modal: theme.zIndex.modal,
},
zIndex: theme.zIndex,
};
const basicColors = {

View File

@ -8,6 +8,7 @@ export const zIndex = {
tooltip: 1040,
modalBackdrop: 1050,
modal: 1060,
portal: 1061,
};
/** @beta */

View File

@ -108,6 +108,7 @@ export interface GrafanaThemeCommons {
tooltip: number;
modalBackdrop: number;
modal: number;
portal: number;
typeahead: number;
};
}

View File

@ -17,23 +17,22 @@ export const selectOption = (config: SelectOptionConfig): any => {
const { clickToOpen, container, forceClickOption, optionText } = fullConfig;
return container.within(() => {
container.within(() => {
if (clickToOpen) {
e2e().get('[class$="-input-suffix"]').click();
}
e2e.components.Select.option()
.filter((_, { textContent }) => {
if (textContent === null) {
return false;
} else if (typeof optionText === 'string') {
return textContent.includes(optionText);
} else {
return optionText.test(textContent);
}
})
.scrollIntoView()
.click({ force: forceClickOption });
e2e().root().scrollIntoView();
});
return e2e.components.Select.option()
.filter((_, { textContent }) => {
if (textContent === null) {
return false;
} else if (typeof optionText === 'string') {
return textContent.includes(optionText);
} else {
return optionText.test(textContent);
}
})
.scrollIntoView()
.click({ force: forceClickOption });
};

View File

@ -1,7 +1,5 @@
import { e2e } from '../index';
import { setTimeRange, TimeRangeConfig } from './setTimeRange';
export { TimeRangeConfig };
export const setDashboardTimeRange = (config: TimeRangeConfig) =>
e2e.components.PageToolbar.container().within(() => setTimeRange(config));
export const setDashboardTimeRange = (config: TimeRangeConfig) => setTimeRange(config);

View File

@ -71,6 +71,10 @@ export function Modal(props: PropsWithChildren<Props>) {
return (
<Portal>
<div
className={styles.modalBackdrop}
onClick={onClickBackdrop || (closeOnBackdropClick ? onDismiss : undefined)}
/>
<div className={cx(styles.modal, className)}>
<div className={headerClass}>
{typeof title === 'string' && <DefaultModalHeader {...props} title={title} />}
@ -81,10 +85,6 @@ export function Modal(props: PropsWithChildren<Props>) {
</div>
<div className={cx(styles.modalContent, contentClassName)}>{children}</div>
</div>
<div
className={styles.modalBackdrop}
onClick={onClickBackdrop || (closeOnBackdropClick ? onDismiss : undefined)}
/>
</Portal>
);
}

View File

@ -29,7 +29,6 @@ export const getModalStyles = stylesFactory((theme: GrafanaTheme2) => {
right: 0;
bottom: 0;
left: 0;
z-index: ${theme.zIndex.modalBackdrop};
background-color: ${theme.components.overlay.background};
backdrop-filter: blur(1px);
`,

View File

@ -1,5 +1,6 @@
import React, { PureComponent } from 'react';
import React, { PropsWithChildren, useEffect, useState } from 'react';
import ReactDOM from 'react-dom';
import { useTheme2 } from '../../themes';
interface Props {
className?: string;
@ -7,37 +8,29 @@ interface Props {
forwardedRef?: any;
}
export class Portal extends PureComponent<Props> {
node: HTMLElement = document.createElement('div');
portalRoot: HTMLElement;
export function Portal(props: PropsWithChildren<Props>) {
const { children, className, root = document.body, forwardedRef } = props;
const theme = useTheme2();
const [node] = useState(document.createElement('div'));
const portalRoot = root;
constructor(props: Props) {
super(props);
const { className, root = document.body } = this.props;
if (className) {
this.node.classList.add(className);
}
this.portalRoot = root;
this.portalRoot.appendChild(this.node);
if (className) {
node.classList.add(className);
}
node.style.position = 'relative';
node.style.zIndex = `${theme.zIndex.portal}`;
componentWillUnmount() {
this.portalRoot.removeChild(this.node);
}
useEffect(() => {
portalRoot.appendChild(node);
return () => {
portalRoot.removeChild(node);
};
}, [node, portalRoot]);
render() {
// Default z-index is high to make sure
return ReactDOM.createPortal(
<div style={{ zIndex: 1051, position: 'relative' }} ref={this.props.forwardedRef}>
{this.props.children}
</div>,
this.node
);
}
return ReactDOM.createPortal(<div ref={forwardedRef}>{children}</div>, node);
}
export const RefForwardingPortal = React.forwardRef<HTMLDivElement, Props>((props, ref) => {
return <Portal {...props} forwardedRef={ref} />;
});
RefForwardingPortal.displayName = 'RefForwardingPortal';

View File

@ -1,44 +1,38 @@
import React, { useState } from 'react';
import { mount, ReactWrapper } from 'enzyme';
import { render, screen } from '@testing-library/react';
import { render, screen, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import selectEvent from 'react-select-event';
import { SelectBase } from './SelectBase';
import { SelectBaseProps } from './types';
import { selectOptionInTest } from './test-utils';
import { SelectableValue } from '@grafana/data';
import { MultiValueContainer } from './MultiValue';
const onChangeHandler = () => jest.fn();
const findMenuElement = (container: ReactWrapper) => container.find({ 'aria-label': 'Select options menu' });
const options: Array<SelectableValue<number>> = [
{
label: 'Option 1',
value: 1,
},
{
label: 'Option 2',
value: 2,
},
];
import { SelectBase } from './SelectBase';
describe('SelectBase', () => {
const onChangeHandler = () => jest.fn();
const options: Array<SelectableValue<number>> = [
{
label: 'Option 1',
value: 1,
},
{
label: 'Option 2',
value: 2,
},
];
it('renders without error', () => {
mount(<SelectBase onChange={onChangeHandler} />);
render(<SelectBase onChange={onChangeHandler} />);
});
it('renders empty options information', () => {
const container = mount(<SelectBase onChange={onChangeHandler} isOpen />);
const noopt = container.find({ 'aria-label': 'No options provided' });
expect(noopt).toHaveLength(1);
render(<SelectBase onChange={onChangeHandler} />);
userEvent.click(screen.getByText(/choose/i));
expect(screen.queryByText(/no options found/i)).toBeVisible();
});
it('is selectable via its label text', async () => {
const onChange = jest.fn();
render(
<>
<label htmlFor="my-select">My select</label>
<SelectBase onChange={onChange} options={options} inputId="my-select" />
<SelectBase onChange={onChangeHandler} options={options} inputId="my-select" />
</>
);
@ -67,11 +61,9 @@ describe('SelectBase', () => {
describe('when openMenuOnFocus prop', () => {
describe('is provided', () => {
it('opens on focus', () => {
const container = mount(<SelectBase onChange={onChangeHandler} openMenuOnFocus />);
container.find('input').simulate('focus');
const menu = findMenuElement(container);
expect(menu).toHaveLength(1);
render(<SelectBase onChange={onChangeHandler} openMenuOnFocus />);
fireEvent.focus(screen.getByRole('textbox'));
expect(screen.queryByText(/no options found/i)).toBeVisible();
});
});
describe('is not provided', () => {
@ -81,12 +73,10 @@ describe('SelectBase', () => {
${'ArrowUp'}
${' '}
`('opens on arrow down/up or space', ({ key }) => {
const container = mount(<SelectBase onChange={onChangeHandler} />);
const input = container.find('input');
input.simulate('focus');
input.simulate('keydown', { key });
const menu = findMenuElement(container);
expect(menu).toHaveLength(1);
render(<SelectBase onChange={onChangeHandler} />);
fireEvent.focus(screen.getByRole('textbox'));
fireEvent.keyDown(screen.getByRole('textbox'), { key });
expect(screen.queryByText(/no options found/i)).toBeVisible();
});
});
});
@ -120,7 +110,7 @@ describe('SelectBase', () => {
describe('is provided', () => {
it('should only display maxVisibleValues options, and additional number of values should be displayed as indicator', () => {
const container = mount(
render(
<SelectBase
onChange={onChangeHandler}
isMulti={true}
@ -130,14 +120,13 @@ describe('SelectBase', () => {
isOpen={false}
/>
);
expect(container.find(MultiValueContainer)).toHaveLength(3);
expect(container.find('#excess-values').text()).toBe('(+2)');
expect(screen.queryAllByText(/option/i).length).toBe(3);
expect(screen.queryByText(/\(\+2\)/i)).toBeVisible();
});
describe('and showAllSelectedWhenOpen prop is true', () => {
it('should show all selected options when menu is open', () => {
const container = mount(
render(
<SelectBase
onChange={onChangeHandler}
isMulti={true}
@ -149,14 +138,14 @@ describe('SelectBase', () => {
/>
);
expect(container.find(MultiValueContainer)).toHaveLength(5);
expect(container.find('#excess-values')).toHaveLength(0);
expect(screen.queryAllByText(/option/i).length).toBe(5);
expect(screen.queryByText(/\(\+2\)/i)).not.toBeInTheDocument();
});
});
describe('and showAllSelectedWhenOpen prop is false', () => {
it('should not show all selected options when menu is open', () => {
const container = mount(
render(
<SelectBase
onChange={onChangeHandler}
isMulti={true}
@ -168,15 +157,15 @@ describe('SelectBase', () => {
/>
);
expect(container.find('#excess-values').text()).toBe('(+2)');
expect(container.find(MultiValueContainer)).toHaveLength(3);
expect(screen.queryAllByText(/option/i).length).toBe(3);
expect(screen.queryByText(/\(\+2\)/i)).toBeVisible();
});
});
});
describe('is not provided', () => {
it('should always show all selected options', () => {
const container = mount(
render(
<SelectBase
onChange={onChangeHandler}
isMulti={true}
@ -186,15 +175,16 @@ describe('SelectBase', () => {
/>
);
expect(container.find(MultiValueContainer)).toHaveLength(5);
expect(container.find('#excess-values')).toHaveLength(0);
expect(screen.queryAllByText(/option/i).length).toBe(5);
expect(screen.queryByText(/\(\+2\)/i)).not.toBeInTheDocument();
});
});
});
describe('options', () => {
it('renders menu with provided options', () => {
render(<SelectBase options={options} onChange={onChangeHandler} isOpen />);
render(<SelectBase options={options} onChange={onChangeHandler} />);
userEvent.click(screen.getByText(/choose/i));
const menuOptions = screen.getAllByLabelText('Select option');
expect(menuOptions).toHaveLength(2);
});
@ -207,60 +197,11 @@ describe('SelectBase', () => {
const selectEl = screen.getByLabelText('My select');
expect(selectEl).toBeInTheDocument();
await selectEvent.select(selectEl, 'Option 2');
await selectOptionInTest(selectEl, 'Option 2');
expect(spy).toHaveBeenCalledWith({
label: 'Option 2',
value: 2,
});
});
});
describe('When allowCustomValue is set to true', () => {
it('Should allow creating a new option', async () => {
const valueIsStrictlyEqual: SelectBaseProps<string>['filterOption'] = (option, value) => option.value === value;
const valueIsStrictlyNotEqual: SelectBaseProps<string>['isValidNewOption'] = (newOption, _, options) =>
options.every(({ value }) => value !== newOption);
const spy = jest.fn();
render(
<SelectBase
onChange={spy}
isOpen
allowCustomValue
filterOption={valueIsStrictlyEqual}
isValidNewOption={valueIsStrictlyNotEqual}
/>
);
const textBox = screen.getByRole('textbox');
userEvent.type(textBox, 'NOT AN OPTION');
let creatableOption = screen.getByLabelText('Select option');
expect(creatableOption).toBeInTheDocument();
expect(creatableOption).toHaveTextContent('Create: NOT AN OPTION');
userEvent.click(creatableOption);
expect(spy).toHaveBeenCalledWith(
expect.objectContaining({
label: 'NOT AN OPTION',
value: 'NOT AN OPTION',
})
);
// Should also create options in a case-insensitive way.
userEvent.type(textBox, 'not an option');
creatableOption = screen.getByLabelText('Select option');
expect(creatableOption).toBeInTheDocument();
expect(creatableOption).toHaveTextContent('Create: not an option');
userEvent.click(creatableOption);
expect(spy).toHaveBeenCalledWith(
expect.objectContaining({
label: 'not an option',
value: 'not an option',
})
);
});
});
});

View File

@ -115,7 +115,7 @@ export function SelectBase<T>({
maxMenuHeight = 300,
minMenuHeight,
maxVisibleValues,
menuPlacement = 'bottom',
menuPlacement = 'auto',
menuPosition,
noOptionsMessage = 'No options found',
onBlur,
@ -175,6 +175,10 @@ export function SelectBase<T>({
backspaceRemovesValue,
captureMenuScroll: false,
closeMenuOnSelect,
// We don't want to close if we're actually scrolling the menu
// So only close if none of the parents are the select menu itself
closeMenuOnScroll: (scrollEvent: Event) =>
!scrollEvent.composedPath().some((pathItem) => (pathItem as Element).classList?.value.includes(styles.menu)),
defaultValue,
// Also passing disabled, as this is the new Select API, and I want to use this prop instead of react-select's one
disabled,
@ -197,6 +201,7 @@ export function SelectBase<T>({
maxVisibleValues,
menuIsOpen: isOpen,
menuPlacement,
menuPortalTarget: document.body,
menuPosition,
menuShouldScrollIntoView: false,
onBlur,
@ -329,19 +334,16 @@ export function SelectBase<T>({
}}
styles={{
...resetSelectStyles(),
menuPortal: ({ position, width }: any) => ({
position,
width,
zIndex: theme.zIndex.dropdown,
menuPortal: (base: any) => ({
...base,
zIndex: theme.zIndex.portal,
}),
//These are required for the menu positioning to function
menu: ({ top, bottom, position }: any) => ({
top,
bottom,
position,
marginBottom: !!bottom ? '10px' : '0',
minWidth: '100%',
zIndex: theme.zIndex.dropdown,
}),
container: () => ({
position: 'relative',

View File

@ -0,0 +1,7 @@
import { select } from 'react-select-event';
// Used to select an option or options from a Select in unit tests
export const selectOptionInTest = async (
input: HTMLElement,
optionOrOptions: string | RegExp | Array<string | RegExp>
) => await select(input, optionOrOptions, { container: document.body });

View File

@ -3,7 +3,7 @@ import { fireEvent, render, screen } from '@testing-library/react';
import { ValueMappingsEditorModal, Props } from './ValueMappingsEditorModal';
import { MappingType } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import selectEvent from 'react-select-event';
import { selectOptionInTest } from '../Select/test-utils';
const setup = (spy?: any, propOverrides?: object) => {
const props: Props = {
@ -79,7 +79,8 @@ describe('When adding and updating value mapp', () => {
fireEvent.click(screen.getByLabelText(selectors.components.ValuePicker.button('Add a new mapping')));
const selectComponent = await screen.findByLabelText(selectors.components.ValuePicker.select('Add a new mapping'));
await selectEvent.select(selectComponent, 'Value');
await selectOptionInTest(selectComponent, 'Value');
const input = (await screen.findAllByPlaceholderText('Exact value to match'))[1];
@ -123,7 +124,7 @@ describe('When adding and updating range map', () => {
fireEvent.click(screen.getByLabelText(selectors.components.ValuePicker.button('Add a new mapping')));
const selectComponent = await screen.findByLabelText(selectors.components.ValuePicker.select('Add a new mapping'));
await selectEvent.select(selectComponent, 'Range');
await selectOptionInTest(selectComponent, 'Range');
fireEvent.change(screen.getByPlaceholderText('Range start'), { target: { value: '10' } });
fireEvent.change(screen.getByPlaceholderText('Range end'), { target: { value: '20' } });

View File

@ -186,6 +186,7 @@ export { InlineFieldRow } from './Forms/InlineFieldRow';
export { FieldArray } from './Forms/FieldArray';
export { default as resetSelectStyles } from './Select/resetSelectStyles';
export { selectOptionInTest } from './Select/test-utils';
export * from './Select/Select';
export { HorizontalGroup, VerticalGroup, Container } from './Layout/Layout';

View File

@ -131,6 +131,7 @@ const theme: GrafanaThemeCommons = {
tooltip: 1040,
modalBackdrop: 1050,
modal: 1060,
portal: 1061,
},
};

View File

@ -1,7 +1,7 @@
import React from 'react';
import { toDataFrame, FieldType } from '@grafana/data';
import { fireEvent, render, screen, getByText } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { fireEvent, render, screen } from '@testing-library/react';
import { selectOptionInTest } from '@grafana/ui';
import { Props, ConfigFromQueryTransformerEditor } from './ConfigFromQueryTransformerEditor';
beforeEach(() => {
@ -40,7 +40,7 @@ describe('ConfigFromQueryTransformerEditor', () => {
let select = (await screen.findByText('Config query')).nextSibling!;
await fireEvent.keyDown(select, { keyCode: 40 });
await userEvent.click(getByText(select as HTMLElement, 'A'));
await selectOptionInTest(select as HTMLElement, 'A');
expect(mockOnChange).toHaveBeenCalledWith(
expect.objectContaining({

View File

@ -2,6 +2,7 @@ import React from 'react';
import { toDataFrame, FieldType } from '@grafana/data';
import { fireEvent, render, screen, getByText, getByLabelText } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { selectOptionInTest } from '@grafana/ui';
import { Props, FieldToConfigMappingEditor } from './FieldToConfigMappingEditor';
beforeEach(() => {
@ -45,7 +46,7 @@ describe('FieldToConfigMappingEditor', () => {
const select = (await screen.findByTestId('Miiin-config-key')).childNodes[0];
await fireEvent.keyDown(select, { keyCode: 40 });
await userEvent.click(getByText(select as HTMLElement, 'Min'));
await selectOptionInTest(select as HTMLElement, 'Min');
expect(mockOnChange).toHaveBeenCalledWith(expect.arrayContaining([{ fieldName: 'Miiin', handlerKey: 'min' }]));
});
@ -80,7 +81,7 @@ describe('FieldToConfigMappingEditor', () => {
const reducer = await (await screen.findByTestId('max-reducer')).childNodes[0];
await fireEvent.keyDown(reducer, { keyCode: 40 });
await userEvent.click(getByText(reducer as HTMLElement, 'Last'));
await selectOptionInTest(reducer as HTMLElement, 'Last');
expect(mockOnChange).toHaveBeenCalledWith(
expect.arrayContaining([{ fieldName: 'max', handlerKey: 'max', reducerId: 'last' }])

View File

@ -1,7 +1,7 @@
import React from 'react';
import { toDataFrame, FieldType } from '@grafana/data';
import { fireEvent, render, screen, getByText } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { fireEvent, render, screen } from '@testing-library/react';
import { selectOptionInTest } from '@grafana/ui';
import { Props, RowsToFieldsTransformerEditor } from './RowsToFieldsTransformerEditor';
beforeEach(() => {
@ -37,7 +37,7 @@ describe('RowsToFieldsTransformerEditor', () => {
const select = (await screen.findByTestId('Name-config-key')).childNodes[0];
await fireEvent.keyDown(select, { keyCode: 40 });
await userEvent.click(getByText(select as HTMLElement, 'Field name'));
await selectOptionInTest(select as HTMLElement, 'Field name');
expect(mockOnChange).toHaveBeenCalledWith(
expect.objectContaining({
@ -51,7 +51,7 @@ describe('RowsToFieldsTransformerEditor', () => {
const select = (await screen.findByTestId('Value-config-key')).childNodes[0];
await fireEvent.keyDown(select, { keyCode: 40 });
await userEvent.click(getByText(select as HTMLElement, 'Field value'));
await selectOptionInTest(select as HTMLElement, 'Field value');
expect(mockOnChange).toHaveBeenCalledWith(
expect.objectContaining({

View File

@ -13,6 +13,7 @@ import { mockDataSource, MockDataSourceSrv } from './mocks';
import { getAllDataSources } from './utils/config';
import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
import userEvent from '@testing-library/user-event';
import { selectOptionInTest } from '@grafana/ui';
Object.defineProperty(window, 'matchMedia', {
writable: true,
@ -239,7 +240,7 @@ describe('AmRoutes', () => {
// configure receiver & group by
const receiverSelect = await ui.receiverSelect.find();
clickSelectOption(receiverSelect, 'critical');
await clickSelectOption(receiverSelect, 'critical');
const groupSelect = ui.groupSelect.get();
await userEvent.type(byRole('textbox').get(groupSelect), 'namespace{enter}');
@ -298,7 +299,7 @@ describe('AmRoutes', () => {
// configure receiver & group by
const receiverSelect = await ui.receiverSelect.find();
clickSelectOption(receiverSelect, 'default');
await clickSelectOption(receiverSelect, 'default');
const groupSelect = ui.groupSelect.get();
await userEvent.type(byRole('textbox').get(groupSelect), 'severity{enter}');
@ -343,7 +344,7 @@ describe('AmRoutes', () => {
const clickSelectOption = async (selectElement: HTMLElement, optionText: string): Promise<void> => {
userEvent.click(byRole('textbox').get(selectElement));
userEvent.click(byText(optionText).get(selectElement));
await selectOptionInTest(selectElement, optionText);
};
const updateTiming = async (selectElement: HTMLElement, value: string, timeUnit: string): Promise<void> => {
@ -351,5 +352,5 @@ const updateTiming = async (selectElement: HTMLElement, value: string, timeUnit:
expect(inputs).toHaveLength(2);
await userEvent.type(inputs[0], value);
userEvent.click(inputs[1]);
userEvent.click(byText(timeUnit).get(selectElement));
await selectOptionInTest(selectElement, timeUnit);
};

View File

@ -23,6 +23,7 @@ import userEvent from '@testing-library/user-event';
import { ALERTMANAGER_NAME_LOCAL_STORAGE_KEY, ALERTMANAGER_NAME_QUERY_KEY } from './utils/constants';
import store from 'app/core/store';
import { contextSrv } from 'app/core/services/context_srv';
import { selectOptionInTest } from '@grafana/ui';
jest.mock('./api/alertmanager');
jest.mock('./api/grafana');
@ -94,7 +95,7 @@ const ui = {
const clickSelectOption = async (selectElement: HTMLElement, optionText: string): Promise<void> => {
userEvent.click(byRole('textbox').get(selectElement));
userEvent.click(byText(optionText).get(selectElement));
await selectOptionInTest(selectElement, optionText);
};
describe('Receivers', () => {
@ -166,7 +167,7 @@ describe('Receivers', () => {
await ui.inputs.name.find();
// select hipchat
clickSelectOption(byTestId('items.0.type').get(), 'HipChat');
await clickSelectOption(byTestId('items.0.type').get(), 'HipChat');
// check that email options are gone and hipchat options appear
expect(ui.inputs.email.addresses.query()).not.toBeInTheDocument();

View File

@ -6,6 +6,7 @@ import RuleEditor from './RuleEditor';
import { Router, Route } from 'react-router-dom';
import React from 'react';
import { byLabelText, byRole, byTestId, byText } from 'testing-library-selector';
import { selectOptionInTest } from '@grafana/ui';
import { contextSrv } from 'app/core/services/context_srv';
import { mockDataSource, MockDataSourceSrv } from './mocks';
import userEvent from '@testing-library/user-event';
@ -124,13 +125,13 @@ describe('RuleEditor', () => {
await renderRuleEditor();
await userEvent.type(await ui.inputs.name.find(), 'my great new rule');
clickSelectOption(ui.inputs.alertType.get(), /Cortex\/Loki managed alert/);
await clickSelectOption(ui.inputs.alertType.get(), /Cortex\/Loki managed alert/);
const dataSourceSelect = ui.inputs.dataSource.get();
userEvent.click(byRole('textbox').get(dataSourceSelect));
userEvent.click(await byText('Prom (default)').find(dataSourceSelect));
await clickSelectOption(dataSourceSelect, 'Prom (default)');
await waitFor(() => expect(mocks.api.fetchRulerRules).toHaveBeenCalled());
clickSelectOption(ui.inputs.namespace.get(), 'namespace2');
clickSelectOption(ui.inputs.group.get(), 'group2');
await clickSelectOption(ui.inputs.namespace.get(), 'namespace2');
await clickSelectOption(ui.inputs.group.get(), 'group2');
await userEvent.type(ui.inputs.expr.get(), 'up == 1');
@ -192,10 +193,10 @@ describe('RuleEditor', () => {
// fill out the form
await renderRuleEditor();
userEvent.type(await ui.inputs.name.find(), 'my great new rule');
clickSelectOption(ui.inputs.alertType.get(), /Classic Grafana alerts based on thresholds/);
await clickSelectOption(ui.inputs.alertType.get(), /Classic Grafana alerts based on thresholds/);
const folderInput = await ui.inputs.folder.find();
await waitFor(() => expect(searchFolderMock).toHaveBeenCalled());
clickSelectOption(folderInput, 'Folder A');
await clickSelectOption(folderInput, 'Folder A');
await userEvent.type(ui.inputs.annotationValue(0).get(), 'some summary');
await userEvent.type(ui.inputs.annotationValue(1).get(), 'some description');
@ -308,7 +309,7 @@ describe('RuleEditor', () => {
// render rule editor, select cortex/loki managed alerts
await renderRuleEditor();
await ui.inputs.name.find();
clickSelectOption(ui.inputs.alertType.get(), /Cortex\/Loki managed alert/);
await clickSelectOption(ui.inputs.alertType.get(), /Cortex\/Loki managed alert/);
// wait for ui theck each datasource if it supports rule editing
await waitFor(() => expect(mocks.api.fetchRulerRulesGroup).toHaveBeenCalledTimes(4));
@ -316,16 +317,16 @@ describe('RuleEditor', () => {
// check that only rules sources that have ruler available are there
const dataSourceSelect = ui.inputs.dataSource.get();
userEvent.click(byRole('textbox').get(dataSourceSelect));
expect(await byText('loki with ruler').find(dataSourceSelect)).toBeInTheDocument();
expect(byText('cortex with ruler').query(dataSourceSelect)).toBeInTheDocument();
expect(byText('loki with local rule store').query(dataSourceSelect)).not.toBeInTheDocument();
expect(byText('prom without ruler api').query(dataSourceSelect)).not.toBeInTheDocument();
expect(byText('splunk').query(dataSourceSelect)).not.toBeInTheDocument();
expect(byText('loki disabled for alerting').query(dataSourceSelect)).not.toBeInTheDocument();
expect(await byText('loki with ruler').query()).toBeInTheDocument();
expect(byText('cortex with ruler').query()).toBeInTheDocument();
expect(byText('loki with local rule store').query()).not.toBeInTheDocument();
expect(byText('prom without ruler api').query()).not.toBeInTheDocument();
expect(byText('splunk').query()).not.toBeInTheDocument();
expect(byText('loki disabled for alerting').query()).not.toBeInTheDocument();
});
});
const clickSelectOption = (selectElement: HTMLElement, optionText: Matcher): void => {
const clickSelectOption = async (selectElement: HTMLElement, optionText: Matcher): Promise<void> => {
userEvent.click(byRole('textbox').get(selectElement));
userEvent.click(byText(optionText).get(selectElement));
await selectOptionInTest(selectElement, optionText as string);
};

View File

@ -1,8 +1,9 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { selectOptionInTest } from '@grafana/ui';
import { byRole, byText } from 'testing-library-selector';
import { byRole } from 'testing-library-selector';
import { Props, GeneralSettingsUnconnected as GeneralSettings } from './GeneralSettings';
import { DashboardModel } from '../../state';
@ -45,11 +46,6 @@ const setupTestContext = (options: Partial<Props>) => {
return { rerender, props };
};
const clickSelectOption = async (selectElement: HTMLElement, optionText: string): Promise<void> => {
userEvent.click(byRole('textbox').get(selectElement));
userEvent.click(byText(optionText).get(selectElement));
};
describe('General Settings', () => {
describe('when component is mounted with timezone', () => {
it('should render correctly', () => {
@ -66,7 +62,9 @@ describe('General Settings', () => {
it('should call update function', async () => {
const { props } = setupTestContext({});
userEvent.click(screen.getByLabelText('Time zone picker select container'));
await clickSelectOption(screen.getByLabelText('Time zone picker select container'), 'Browser Time');
const timeZonePicker = screen.getByLabelText('Time zone picker select container');
userEvent.click(byRole('textbox').get(timeZonePicker));
await selectOptionInTest(timeZonePicker, 'Browser Time');
expect(props.updateTimeZone).toHaveBeenCalledWith('browser');
expect(props.dashboard.timezone).toBe('browser');
});

View File

@ -1,6 +1,6 @@
import React from 'react';
import { render, fireEvent, screen, waitFor, act } from '@testing-library/react';
import selectEvent from 'react-select-event';
import { selectOptionInTest } from '@grafana/ui';
import * as SearchSrv from 'app/core/services/search_srv';
import * as MockSearchSrv from 'app/core/services/__mocks__/search_srv';
import { DashboardSearch, Props } from './DashboardSearch';
@ -105,7 +105,7 @@ describe('DashboardSearch', () => {
await waitFor(() => screen.getByLabelText('Tag filter'));
const tagComponent = screen.getByLabelText('Tag filter');
await selectEvent.select(tagComponent, 'tag1');
await selectOptionInTest(tagComponent, 'tag1');
expect(tagComponent).toBeInTheDocument();

View File

@ -1,6 +1,6 @@
import React from 'react';
import { fireEvent, render, screen } from '@testing-library/react';
import selectEvent from 'react-select-event';
import { selectOptionInTest } from '@grafana/ui';
import { Props, SearchResultsFilter } from './SearchResultsFilter';
import { SearchLayout } from '../types';
@ -82,7 +82,7 @@ describe('SearchResultsFilter', () => {
query: { ...searchQuery, tag: [] },
});
const tagComponent = await screen.findByLabelText('Tag filter');
await selectEvent.select(tagComponent, 'tag1');
await selectOptionInTest(tagComponent, 'tag1');
expect(mockFilterByTags).toHaveBeenCalledTimes(1);
expect(mockFilterByTags).toHaveBeenCalledWith(['tag1']);

View File

@ -1,6 +1,6 @@
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import selectEvent from 'react-select-event';
import { selectOptionInTest } from '@grafana/ui';
import MetricsQueryEditor from './MetricsQueryEditor';
@ -56,7 +56,7 @@ describe('Azure Monitor QueryEditor', () => {
);
const subscriptions = await screen.findByLabelText('Subscription');
await selectEvent.select(subscriptions, 'Another Subscription');
await selectOptionInTest(subscriptions, 'Another Subscription');
expect(onChange).toHaveBeenCalledWith({
...mockQuery,
@ -102,7 +102,7 @@ describe('Azure Monitor QueryEditor', () => {
await waitFor(() => expect(screen.getByTestId('azure-monitor-metrics-query-editor')).toBeInTheDocument());
const metrics = await screen.findByLabelText('Metric');
await selectEvent.select(metrics, 'Metric B');
await selectOptionInTest(metrics, 'Metric B');
expect(onChange).toHaveBeenLastCalledWith({
...mockQuery,
@ -141,7 +141,7 @@ describe('Azure Monitor QueryEditor', () => {
await waitFor(() => expect(screen.getByTestId('azure-monitor-metrics-query-editor')).toBeInTheDocument());
const metrics = await screen.findByLabelText('Metric');
await selectEvent.select(metrics, 'Metric B');
await selectOptionInTest(metrics, 'Metric B');
expect(onChange).toHaveBeenLastCalledWith({
...mockQuery,
@ -170,7 +170,7 @@ describe('Azure Monitor QueryEditor', () => {
await waitFor(() => expect(screen.getByTestId('azure-monitor-metrics-query-editor')).toBeInTheDocument());
const aggregation = await screen.findByLabelText('Aggregation');
await selectEvent.select(aggregation, 'Maximum');
await selectOptionInTest(aggregation, 'Maximum');
expect(onChange).toHaveBeenLastCalledWith({
...mockQuery,

View File

@ -77,7 +77,7 @@ describe('Azure Monitor QueryEditor', () => {
await waitFor(() => expect(screen.getByTestId('azure-monitor-query-editor')).toBeInTheDocument());
const metrics = await screen.findByLabelText('Service');
await selectEvent.select(metrics, 'Logs');
await ui.selectOptionInTest(metrics, 'Logs');
expect(onChange).toHaveBeenCalledWith({
...mockQuery,
@ -127,7 +127,7 @@ describe('Azure Monitor QueryEditor', () => {
expect(screen.queryByText('Application Insights')).toBeInTheDocument();
const metrics = await screen.findByLabelText('Service');
await selectEvent.select(metrics, 'Logs');
await ui.selectOptionInTest(metrics, 'Logs');
expect(screen.queryByText('Application Insights')).toBeInTheDocument();
});

View File

@ -1,7 +1,7 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { select } from 'react-select-event';
import { selectOptionInTest } from '@grafana/ui';
import { RawInfluxQLEditor } from './RawInfluxQLEditor';
import { InfluxQuery } from '../types';
@ -57,7 +57,7 @@ describe('RawInfluxQLEditor', () => {
const formatSelect = screen.getByLabelText('Format as');
expect(formatSelect).toBeInTheDocument();
await select(formatSelect, 'Time series');
await selectOptionInTest(formatSelect, 'Time series');
expect(onChange).toHaveBeenCalledWith({ ...query, resultFormat: 'time_series' });
});