/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*
* Modifications Copyright OpenSearch Contributors. See
* GitHub history for details.
*/
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. 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, { ReactNode } from 'react';
import { shallow, render, mount } from 'enzyme';
import {
requiredProps,
findTestSubject,
takeMountedSnapshot,
} from '../../test';
import { comboBoxKeys } from '../../services';
import { OuiComboBox, OuiComboBoxProps } from './combo_box';
jest.mock('../portal', () => ({
OuiPortal: ({ children }: { children: ReactNode }) => children,
}));
interface TitanOption {
'data-test-subj'?: 'titanOption';
label: string;
}
const options: TitanOption[] = [
{
'data-test-subj': 'titanOption',
label: 'Titan',
},
{
label: 'Enceladus',
},
{
label: 'Mimas',
},
{
label: 'Dione',
},
{
label: 'Iapetus',
},
{
label: 'Phoebe',
},
{
label: 'Rhea',
},
{
label:
"Pandora is one of Saturn's moons, named for a Titaness of Greek mythology",
},
{
label: 'Tethys',
},
{
label: 'Hyperion',
},
];
describe('OuiComboBox', () => {
test('is rendered', () => {
const component = render();
expect(component).toMatchSnapshot();
});
});
describe('props', () => {
test('options list is rendered', () => {
const component = mount(
);
component.setState({ isListOpen: true });
expect(takeMountedSnapshot(component)).toMatchSnapshot();
});
test('selectedOptions are rendered', () => {
const component = shallow(
);
expect(component).toMatchSnapshot();
});
describe('isClearable=false disallows user from clearing input', () => {
test('when no options are selected', () => {
const component = shallow(
);
expect(component).toMatchSnapshot();
});
test('when options are selected', () => {
const component = shallow(
);
expect(component).toMatchSnapshot();
});
});
describe('singleSelection', () => {
test('is rendered', () => {
const component = shallow(
);
expect(component).toMatchSnapshot();
});
test('selects existing option when opened', () => {
const component = shallow(
);
component.setState({ isListOpen: true });
expect(component).toMatchSnapshot();
});
test('prepend and append is rendered', () => {
const component = shallow(
);
component.setState({ isListOpen: true });
expect(component).toMatchSnapshot();
});
});
test('isDisabled is rendered', () => {
const component = shallow(
);
expect(component).toMatchSnapshot();
});
test('full width is rendered', () => {
const component = shallow(
);
expect(component).toMatchSnapshot();
});
test('delimiter is rendered', () => {
const component = shallow(
);
expect(component).toMatchSnapshot();
});
test('autoFocus is rendered', () => {
const component = shallow(
);
expect(component).toMatchSnapshot();
});
test('icon is rendered', () => {
const component = mount(
);
expect(component).toMatchSnapshot();
const icon = findTestSubject(component, 'comboBoxIcon');
expect(icon).toBeDefined();
expect(icon.render().attr('data-ouiicon-type')).toBe('search');
});
test('custom icon is rendered', () => {
const component = mount(
);
expect(component).toMatchSnapshot();
const icon = findTestSubject(component, 'comboBoxIcon');
expect(icon).toBeDefined();
expect(icon.render().attr('data-ouiicon-type')).toBe('menu');
});
});
test('does not show multiple checkmarks with duplicate labels', () => {
const options = [
{
label: 'Titan',
key: 'titan1',
},
{
label: 'Titan',
key: 'titan2',
},
{
label: 'Tethys',
},
];
const component = mount(
);
const searchInput = findTestSubject(component, 'comboBoxSearchInput');
searchInput.simulate('focus');
expect(component.find('OuiFilterSelectItem[checked="on"]').length).toBe(1);
});
describe('behavior', () => {
describe('hitting "Enter"', () => {
test('calls the onCreateOption callback when there is input', () => {
const onCreateOptionHandler = jest.fn();
const component = mount(
);
component.setState({ searchValue: 'foo' });
const searchInput = findTestSubject(component, 'comboBoxSearchInput');
searchInput.simulate('focus');
searchInput.simulate('keyDown', { key: comboBoxKeys.ENTER });
expect(onCreateOptionHandler).toHaveBeenCalledTimes(1);
expect(onCreateOptionHandler).toHaveBeenNthCalledWith(1, 'foo', options);
});
test("doesn't the onCreateOption callback when there is no input", () => {
const onCreateOptionHandler = jest.fn();
const component = mount(
);
const searchInput = findTestSubject(component, 'comboBoxSearchInput');
searchInput.simulate('focus');
searchInput.simulate('keyDown', { key: comboBoxKeys.ENTER });
expect(onCreateOptionHandler).not.toHaveBeenCalled();
});
});
describe('tabbing', () => {
test("off the search input closes the options list if the user isn't navigating the options", () => {
const onKeyDownWrapper = jest.fn();
const component = mount(
);
const searchInput = findTestSubject(component, 'comboBoxSearchInput');
searchInput.simulate('focus');
// Focusing the input should open the options list.
expect(findTestSubject(component, 'comboBoxOptionsList')).toBeDefined();
// Tab backwards to take focus off the combo box.
searchInput.simulate('keyDown', {
key: comboBoxKeys.TAB,
shiftKey: true,
});
// If the TAB keydown propagated to the wrapper, then a browser DOM would shift the focus
expect(onKeyDownWrapper).toHaveBeenCalledTimes(1);
});
test('off the search input calls onCreateOption', () => {
const onCreateOptionHandler = jest.fn();
const component = mount(
);
component.setState({ searchValue: 'foo' });
const searchInput = findTestSubject(component, 'comboBoxSearchInput');
searchInput.simulate('focus');
const searchInputNode = searchInput.getDOMNode();
// React doesn't support `focusout` so we have to manually trigger it
searchInputNode.dispatchEvent(
new FocusEvent('focusout', { bubbles: true })
);
expect(onCreateOptionHandler).toHaveBeenCalledTimes(1);
expect(onCreateOptionHandler).toHaveBeenNthCalledWith(1, 'foo', options);
});
test('off the search input does nothing if the user is navigating the options', () => {
const onKeyDownWrapper = jest.fn();
const component = mount(
);
const searchInput = findTestSubject(component, 'comboBoxSearchInput');
searchInput.simulate('focus');
// Focusing the input should open the options list.
expect(findTestSubject(component, 'comboBoxOptionsList')).toBeDefined();
// Navigate to an option.
searchInput.simulate('keyDown', { key: comboBoxKeys.ARROW_DOWN });
// Tab backwards to take focus off the combo box.
searchInput.simulate('keyDown', {
key: comboBoxKeys.TAB,
shiftKey: true,
});
// If the TAB keydown did not bubble to the wrapper, then the tab event was prevented
expect(onKeyDownWrapper.mock.calls.length).toBe(0);
});
});
describe('clear button', () => {
test('calls onChange callback with empty array', () => {
const onChangeHandler = jest.fn();
const component = mount(
);
findTestSubject(component, 'comboBoxClearButton').simulate('click');
expect(onChangeHandler).toHaveBeenCalledTimes(1);
expect(onChangeHandler).toHaveBeenNthCalledWith(1, []);
});
test('focuses the input', () => {
const component = mount(
{}}
/>
);
findTestSubject(component, 'comboBoxClearButton').simulate('click');
expect(
findTestSubject(component, 'comboBoxSearchInput').getDOMNode()
).toBe(document.activeElement);
});
});
describe('sortMatchesBy', () => {
const sortMatchesByOptions = [
{
label: 'Something is Disabled',
},
...options,
];
test('options "none"', () => {
const component = mount<
OuiComboBox,
OuiComboBoxProps,
{ matchingOptions: TitanOption[] }
>();
findTestSubject(component, 'comboBoxSearchInput').simulate('change', {
target: { value: 'di' },
});
expect(component.state('matchingOptions')[0].label).toBe(
'Something is Disabled'
);
});
test('options "startsWith"', () => {
const component = mount<
OuiComboBox,
OuiComboBoxProps,
{ matchingOptions: TitanOption[] }
>(
);
findTestSubject(component, 'comboBoxSearchInput').simulate('change', {
target: { value: 'di' },
});
expect(component.state('matchingOptions')[0].label).toBe('Dione');
});
});
it('calls the inputRef prop with the input element', () => {
const inputRefCallback = jest.fn();
const component = mount<
OuiComboBox,
OuiComboBoxProps,
{ matchingOptions: TitanOption[] }
>();
expect(inputRefCallback).toHaveBeenCalledTimes(1);
expect(component.find('input[role="textbox"]').getDOMNode()).toBe(
inputRefCallback.mock.calls[0][0]
);
});
});