added functional tests for dashboard controls integration (#119755)

This commit is contained in:
Devon Thomson 2021-12-06 13:58:33 -05:00 committed by GitHub
parent 9110908e35
commit bf2779c708
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 629 additions and 21 deletions

View file

@ -70,6 +70,7 @@ export const ControlFrame = ({ customPrepend, enableActions, embeddableId }: Con
</EuiToolTip>
<EuiToolTip content={ControlGroupStrings.floatingActions.getRemoveButtonTitle()}>
<EuiButtonIcon
data-test-subj={`control-action-${embeddableId}-delete`}
aria-label={ControlGroupStrings.floatingActions.getRemoveButtonTitle()}
onClick={() =>
openConfirm(ControlGroupStrings.management.deleteControls.getSubtitle(), {
@ -131,6 +132,7 @@ export const ControlFrame = ({ customPrepend, enableActions, embeddableId }: Con
<>
{embeddable && enableActions && floatingActions}
<EuiFormRow
data-test-subj="control-frame-title"
fullWidth
label={
usingTwoLineLayout

View file

@ -129,6 +129,8 @@ export const ControlGroup = () => {
direction="row"
responsive={false}
alignItems="center"
data-test-subj="controls-group"
data-shared-items-count={idsInOrder.length}
>
<EuiFlexItem>
<DndContext
@ -175,7 +177,7 @@ export const ControlGroup = () => {
aria-label={ControlGroupStrings.management.getManageButtonTitle()}
iconType="gear"
color="text"
data-test-subj="inputControlsSortingButton"
data-test-subj="controls-sorting-button"
onClick={() => {
const flyoutInstance = openFlyout(
forwardAllContext(
@ -198,7 +200,7 @@ export const ControlGroup = () => {
</EuiFlexGroup>
) : (
<>
<EuiFlexGroup alignItems="center" gutterSize="xs">
<EuiFlexGroup alignItems="center" gutterSize="xs" data-test-subj="controls-empty">
<EuiFlexItem grow={1}>
<EuiText className="emptyStateText eui-textCenter" size="s">
<p>{ControlGroupStrings.emptyState.getCallToAction()}</p>

View file

@ -76,6 +76,9 @@ const SortableControlInner = forwardRef<
return (
<EuiFlexItem
grow={width === 'auto'}
data-control-id={embeddableId}
data-test-subj={`control-frame`}
data-render-complete="true"
className={classNames('controlFrameWrapper', {
'controlFrameWrapper-isDragging': isDragging,
'controlFrameWrapper--small': width === 'small',

View file

@ -86,7 +86,7 @@ export const ControlEditor = ({
</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<EuiFlyoutBody data-test-subj="control-editor-flyout">
<EuiForm>
<EuiSpacer size="l" />
{ControlTypeEditor && (
@ -105,6 +105,7 @@ export const ControlEditor = ({
)}
<EuiFormRow label={ControlGroupStrings.manageControl.getTitleInputTitle()}>
<EuiFieldText
data-test-subj="control-editor-title-input"
placeholder={defaultTitle}
value={currentTitle}
onChange={(e) => {
@ -147,6 +148,7 @@ export const ControlEditor = ({
<EuiFlexItem grow={false}>
<EuiButtonEmpty
aria-label={`cancel-${title}`}
data-test-subj="control-editor-cancel"
iconType="cross"
onClick={() => {
onCancel();
@ -158,6 +160,7 @@ export const ControlEditor = ({
<EuiFlexItem grow={false}>
<EuiButton
aria-label={`save-${title}`}
data-test-subj="control-editor-save"
iconType="check"
color="primary"
disabled={!controlEditorValid}

View file

@ -117,13 +117,6 @@ export const CreateControlButton = ({ isIconButton }: { isIconButton: boolean })
if (getControlTypes().length === 0) return null;
const commonButtonProps = {
iconType: 'plusInCircle',
color: 'primary' as EuiButtonIconColor,
'data-test-subj': 'controlsCreateButton',
'aria-label': ControlGroupStrings.management.getManageButtonTitle(),
};
const onCreateButtonClick = () => {
if (getControlTypes().length > 1) {
setIsControlTypePopoverOpen(!isControlTypePopoverOpen);
@ -132,15 +125,17 @@ export const CreateControlButton = ({ isIconButton }: { isIconButton: boolean })
createNewControl(getControlTypes()[0]);
};
const commonButtonProps = {
onClick: onCreateButtonClick,
color: 'primary' as EuiButtonIconColor,
'data-test-subj': 'controls-create-button',
'aria-label': ControlGroupStrings.management.getManageButtonTitle(),
};
const createControlButton = isIconButton ? (
<EuiButtonIcon {...commonButtonProps} onClick={onCreateButtonClick} />
<EuiButtonIcon {...commonButtonProps} iconType={'plusInCircle'} />
) : (
<EuiButton
data-test-subj={'controlsCreateButton'}
onClick={onCreateButtonClick}
color="primary"
size="s"
>
<EuiButton {...commonButtonProps} size="s">
{ControlGroupStrings.emptyState.getAddControlButtonTitle()}
</EuiButton>
);
@ -153,6 +148,7 @@ export const CreateControlButton = ({ isIconButton }: { isIconButton: boolean })
<EuiContextMenuItem
key={type}
icon={factory.getIconType?.()}
data-test-subj={`create-${type}-control`}
onClick={() => {
setIsControlTypePopoverOpen(false);
createNewControl(type);
@ -169,6 +165,7 @@ export const CreateControlButton = ({ isIconButton }: { isIconButton: boolean })
isOpen={isControlTypePopoverOpen}
panelPaddingSize="none"
anchorPosition="downLeft"
data-test-subj="control-type-picker"
closePopover={() => setIsControlTypePopoverOpen(false)}
>
<EuiContextMenuPanel size="s" items={items} />

View file

@ -132,6 +132,7 @@ export const EditControlButton = ({ embeddableId }: { embeddableId: string }) =>
return (
<EuiButtonIcon
data-test-subj={`control-action-${embeddableId}-edit`}
aria-label={ControlGroupStrings.floatingActions.getEditButtonTitle()}
iconType="pencil"
onClick={() => editControl()}

View file

@ -14,18 +14,22 @@ export const DEFAULT_CONTROL_WIDTH: ControlWidth = 'auto';
export const CONTROL_WIDTH_OPTIONS = [
{
id: `auto`,
'data-test-subj': 'control-editor-width-auto',
label: ControlGroupStrings.management.controlWidth.getAutoWidthTitle(),
},
{
id: `small`,
'data-test-subj': 'control-editor-width-small',
label: ControlGroupStrings.management.controlWidth.getSmallWidthTitle(),
},
{
id: `medium`,
'data-test-subj': 'control-editor-width-medium',
label: ControlGroupStrings.management.controlWidth.getMediumWidthTitle(),
},
{
id: `large`,
'data-test-subj': 'control-editor-width-large',
label: ControlGroupStrings.management.controlWidth.getLargeWidthTitle(),
},
];

View file

@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export * from './options_list';

View file

@ -44,7 +44,9 @@ export const OptionsListComponent = ({
actions: { replaceSelection },
} = useReduxEmbeddableContext<OptionsListEmbeddableInput, typeof optionsListReducers>();
const dispatch = useEmbeddableDispatch();
const { controlStyle, selectedOptions, singleSelect } = useEmbeddableSelector((state) => state);
const { controlStyle, selectedOptions, singleSelect, id } = useEmbeddableSelector(
(state) => state
);
// useStateObservable to get component state from Embeddable
const { availableOptions, loading } = useStateObservable<OptionsListComponentState>(
@ -90,6 +92,7 @@ export const OptionsListComponent = ({
'optionsList--filterBtnSingle': controlStyle !== 'twoLine',
'optionsList--filterBtnPlaceholder': !selectedOptionsCount,
})}
data-test-subj={`optionsList-control-${id}`}
onClick={() => setIsPopoverOpen((openState) => !openState)}
isSelected={isPopoverOpen}
numActiveFilters={selectedOptionsCount}

View file

@ -63,6 +63,7 @@ export const OptionsListPopover = ({
disabled={showOnlySelected}
onChange={(event) => updateSearchString(event.target.value)}
value={searchString}
data-test-subj="optionsList-control-search-input"
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
@ -74,6 +75,7 @@ export const OptionsListPopover = ({
size="s"
color="danger"
iconType="eraser"
data-test-subj="optionsList-control-clear-all-selections"
aria-label={OptionsListStrings.popover.getClearAllSelectionsButtonTitle()}
onClick={() => dispatch(clearSelections({}))}
/>
@ -102,11 +104,16 @@ export const OptionsListPopover = ({
</EuiFlexGroup>
</EuiFormRow>
</div>
<div className="optionsList__items">
<div
className="optionsList__items"
data-option-count={availableOptions?.length ?? 0}
data-test-subj={`optionsList-control-available-options`}
>
{!showOnlySelected && (
<>
{availableOptions?.map((availableOption, index) => (
<EuiFilterSelectItem
data-test-subj={`optionsList-control-selection-${availableOption}`}
checked={selectedOptionsSet?.has(availableOption) ? 'on' : undefined}
key={index}
onClick={() => {

View file

@ -7,4 +7,5 @@
*/
export * from './control_group';
export * from './control_types';
export * from './types';

View file

@ -47,6 +47,7 @@ export function DataViewPicker({
return (
<ToolbarButton
title={title}
data-test-subj="open-data-view-picker"
onClick={() => setPopoverIsOpen(!isPopoverOpen)}
fullWidth
{...colorProp}
@ -68,7 +69,7 @@ export function DataViewPicker({
ownFocus
>
<div style={{ width: 368 }}>
<EuiPopoverTitle>
<EuiPopoverTitle data-test-subj="data-view-picker-title">
{i18n.translate('presentationUtil.dataViewPicker.changeDataViewTitle', {
defaultMessage: 'Data view',
})}
@ -86,6 +87,7 @@ export function DataViewPicker({
key: id,
label: title,
value: id,
'data-test-subj': `data-view-picker-${title}`,
checked: id === selectedDataViewId ? 'on' : undefined,
}))}
onChange={(choices) => {

View file

@ -88,6 +88,7 @@ export const FieldPicker = ({
return (
<EuiFlexItem key={f.name}>
<FieldButton
data-test-subj={`field-picker-select-${f.name}`}
className={classNames('presFieldPicker__fieldButton', {
presFieldPickerFieldButtonActive: f.name === selectedFieldName,
})}

View file

@ -74,7 +74,7 @@ export function FieldSearch({
<EuiFlexItem>
<EuiFieldSearch
aria-label={searchPlaceholder}
data-test-subj="fieldFilterSearchInput"
data-test-subj="field-search-input"
fullWidth
onChange={(event) => onSearchChange(event.currentTarget.value)}
placeholder={searchPlaceholder}

View file

@ -0,0 +1,216 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const retry = getService('retry');
const security = getService('security');
const queryBar = getService('queryBar');
const pieChart = getService('pieChart');
const filterBar = getService('filterBar');
const esArchiver = getService('esArchiver');
const testSubjects = getService('testSubjects');
const kibanaServer = getService('kibanaServer');
const dashboardAddPanel = getService('dashboardAddPanel');
const { dashboardControls, timePicker, common, dashboard, header } = getPageObjects([
'dashboardControls',
'timePicker',
'dashboard',
'common',
'header',
]);
describe('Dashboard controls integration', () => {
before(async () => {
await esArchiver.load('test/functional/fixtures/es_archiver/dashboard/current/kibana');
await security.testUser.setRoles(['kibana_admin', 'test_logstash_reader', 'animals']);
await kibanaServer.uiSettings.replace({
defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c',
});
await common.navigateToApp('dashboard');
await dashboardControls.enableControlsLab();
await common.navigateToApp('dashboard');
await dashboard.preserveCrossAppState();
await dashboard.gotoDashboardLandingPage();
await dashboard.clickNewDashboard();
await timePicker.setDefaultDataRange();
});
it('shows the empty control callout on a new dashboard', async () => {
await testSubjects.existOrFail('controls-empty');
});
describe('Options List Control creation and editing experience', async () => {
it('can add a new options list control from a blank state', async () => {
await dashboardControls.createOptionsListControl({ fieldName: 'machine.os.raw' });
expect(await dashboardControls.getControlsCount()).to.be(1);
});
it('can add a second options list control with a non-default data view', async () => {
await dashboardControls.createOptionsListControl({
dataViewTitle: 'animals-*',
fieldName: 'sound.keyword',
});
expect(await dashboardControls.getControlsCount()).to.be(2);
// data views should be properly propagated from the control group to the dashboard
expect(await filterBar.getIndexPatterns()).to.be('logstash-*,animals-*');
});
it('renames an existing control', async () => {
const secondId = (await dashboardControls.getAllControlIds())[1];
const newTitle = 'wow! Animal sounds?';
await dashboardControls.editExistingControl(secondId);
await dashboardControls.controlEditorSetTitle(newTitle);
await dashboardControls.controlEditorSave();
expect(await dashboardControls.doesControlTitleExist(newTitle)).to.be(true);
});
it('can change the data view and field of an existing options list', async () => {
const firstId = (await dashboardControls.getAllControlIds())[0];
await dashboardControls.editExistingControl(firstId);
await dashboardControls.optionsListEditorSetDataView('animals-*');
await dashboardControls.optionsListEditorSetfield('animal.keyword');
await dashboardControls.controlEditorSave();
// when creating a new filter, the ability to select a data view should be removed, because the dashboard now only has one data view
await testSubjects.click('addFilter');
await testSubjects.missingOrFail('filterIndexPatternsSelect');
await filterBar.ensureFieldEditorModalIsClosed();
});
it('deletes an existing control', async () => {
const firstId = (await dashboardControls.getAllControlIds())[0];
await dashboardControls.removeExistingControl(firstId);
expect(await dashboardControls.getControlsCount()).to.be(1);
});
after(async () => {
const controlIds = await dashboardControls.getAllControlIds();
for (const controlId of controlIds) {
await dashboardControls.removeExistingControl(controlId);
}
});
});
describe('Interact with options list on dashboard', async () => {
before(async () => {
await dashboardAddPanel.addVisualization('Rendering-Test:-animal-sounds-pie');
await dashboardControls.createOptionsListControl({
dataViewTitle: 'animals-*',
fieldName: 'sound.keyword',
title: 'Animal Sounds',
});
});
it('Shows available options in options list', async () => {
const controlIds = await dashboardControls.getAllControlIds();
await dashboardControls.optionsListOpenPopover(controlIds[0]);
await retry.try(async () => {
expect(await dashboardControls.optionsListPopoverGetAvailableOptionsCount()).to.be(8);
});
await dashboardControls.optionsListEnsurePopoverIsClosed(controlIds[0]);
});
it('Can search options list for available options', async () => {
const controlIds = await dashboardControls.getAllControlIds();
await dashboardControls.optionsListOpenPopover(controlIds[0]);
await dashboardControls.optionsListPopoverSearchForOption('meo');
await retry.try(async () => {
expect(await dashboardControls.optionsListPopoverGetAvailableOptionsCount()).to.be(1);
expect(await dashboardControls.optionsListPopoverGetAvailableOptions()).to.eql(['meow']);
});
await dashboardControls.optionsListPopoverClearSearch();
await dashboardControls.optionsListEnsurePopoverIsClosed(controlIds[0]);
});
it('Applies dashboard query to options list control', async () => {
await queryBar.setQuery('isDog : true ');
await queryBar.submitQuery();
await dashboard.waitForRenderComplete();
await header.waitUntilLoadingHasFinished();
const controlIds = await dashboardControls.getAllControlIds();
await dashboardControls.optionsListOpenPopover(controlIds[0]);
await retry.try(async () => {
expect(await dashboardControls.optionsListPopoverGetAvailableOptionsCount()).to.be(5);
expect(await dashboardControls.optionsListPopoverGetAvailableOptions()).to.eql([
'ruff',
'bark',
'grrr',
'bow ow ow',
'grr',
]);
});
await queryBar.setQuery('');
await queryBar.submitQuery();
});
it('Applies dashboard filters to options list control', async () => {
await filterBar.addFilter('sound.keyword', 'is one of', ['bark', 'bow ow ow', 'ruff']);
await dashboard.waitForRenderComplete();
await header.waitUntilLoadingHasFinished();
const controlIds = await dashboardControls.getAllControlIds();
await dashboardControls.optionsListOpenPopover(controlIds[0]);
await retry.try(async () => {
expect(await dashboardControls.optionsListPopoverGetAvailableOptionsCount()).to.be(3);
expect(await dashboardControls.optionsListPopoverGetAvailableOptions()).to.eql([
'ruff',
'bark',
'bow ow ow',
]);
});
await filterBar.removeAllFilters();
});
it('Can select multiple available options', async () => {
const controlIds = await dashboardControls.getAllControlIds();
await dashboardControls.optionsListOpenPopover(controlIds[0]);
await dashboardControls.optionsListPopoverSelectOption('hiss');
await dashboardControls.optionsListPopoverSelectOption('grr');
await dashboardControls.optionsListEnsurePopoverIsClosed(controlIds[0]);
});
it('Selected options appear in control', async () => {
const controlIds = await dashboardControls.getAllControlIds();
const selectionString = await dashboardControls.optionsListGetSelectionsString(
controlIds[0]
);
expect(selectionString).to.be('hiss, grr');
});
it('Applies options list control options to dashboard', async () => {
expect(await pieChart.getPieSliceCount()).to.be(2);
});
it('Applies options list control options to dashboard by default on open', async () => {
await dashboard.gotoDashboardLandingPage();
await header.waitUntilLoadingHasFinished();
await dashboard.clickUnsavedChangesContinueEditing('New Dashboard');
await header.waitUntilLoadingHasFinished();
expect(await pieChart.getPieSliceCount()).to.be(2);
const controlIds = await dashboardControls.getAllControlIds();
const selectionString = await dashboardControls.optionsListGetSelectionsString(
controlIds[0]
);
expect(selectionString).to.be('hiss, grr');
});
});
});
}

View file

@ -72,6 +72,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./full_screen_mode'));
loadTestFile(require.resolve('./dashboard_filter_bar'));
loadTestFile(require.resolve('./dashboard_filtering'));
loadTestFile(require.resolve('./dashboard_controls_integration'));
loadTestFile(require.resolve('./panel_expand_toggle'));
loadTestFile(require.resolve('./dashboard_grid'));
loadTestFile(require.resolve('./view_edit'));

View file

@ -0,0 +1,254 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { WebElementWrapper } from 'test/functional/services/lib/web_element_wrapper';
import { OPTIONS_LIST_CONTROL } from '../../../src/plugins/presentation_util/common/controls/';
import { ControlWidth } from '../../../src/plugins/presentation_util/public/components/controls';
import { FtrService } from '../ftr_provider_context';
export class DashboardPageControls extends FtrService {
private readonly log = this.ctx.getService('log');
private readonly find = this.ctx.getService('find');
private readonly retry = this.ctx.getService('retry');
private readonly common = this.ctx.getPageObject('common');
private readonly header = this.ctx.getPageObject('header');
private readonly settings = this.ctx.getPageObject('settings');
private readonly testSubjects = this.ctx.getService('testSubjects');
/* -----------------------------------------------------------
General controls functions
----------------------------------------------------------- */
public async enableControlsLab() {
await this.header.clickStackManagement();
await this.settings.clickKibanaSettings();
await this.settings.toggleAdvancedSettingCheckbox('labs:dashboard:dashboardControls');
}
public async expectControlsEmpty() {
await this.testSubjects.existOrFail('controls-empty');
}
public async getAllControlIds() {
const controlFrames = await this.testSubjects.findAll('control-frame');
const ids = await Promise.all(
controlFrames.map(async (controlFrame) => await controlFrame.getAttribute('data-control-id'))
);
this.log.debug('Got all control ids:', ids);
return ids;
}
public async getAllControlTitles() {
const titleObjects = await this.testSubjects.findAll('control-frame-title');
const titles = await Promise.all(
titleObjects.map(async (title) => (await title.getVisibleText()).split('\n')[0])
);
this.log.debug('Got all control titles:', titles);
return titles;
}
public async doesControlTitleExist(title: string) {
const titles = await this.getAllControlTitles();
return Boolean(titles.find((currentTitle) => currentTitle.indexOf(title)));
}
public async getControlsCount() {
const allTitles = await this.getAllControlTitles();
return allTitles.length;
}
public async openCreateControlFlyout(type: string) {
this.log.debug(`Opening flyout for ${type} control`);
await this.testSubjects.click('controls-create-button');
if (await this.testSubjects.exists('control-type-picker')) {
await this.testSubjects.click(`create-${type}-control`);
}
await this.retry.try(async () => {
await this.testSubjects.existOrFail('control-editor-flyout');
});
}
/* -----------------------------------------------------------
Individual controls functions
----------------------------------------------------------- */
// Control Frame functions
public async getControlElementById(controlId: string): Promise<WebElementWrapper> {
const errorText = `Control frame ${controlId} could not be found`;
let controlElement: WebElementWrapper | undefined;
await this.retry.try(async () => {
const controlFrames = await this.testSubjects.findAll('control-frame');
const framesWithIds = await Promise.all(
controlFrames.map(async (frame) => {
const id = await frame.getAttribute('data-control-id');
return { id, element: frame };
})
);
const foundControlFrame = framesWithIds.find(({ id }) => id === controlId);
if (!foundControlFrame) throw new Error(errorText);
controlElement = foundControlFrame.element;
});
if (!controlElement) throw new Error(errorText);
return controlElement;
}
public async hoverOverExistingControl(controlId: string) {
const elementToHover = await this.getControlElementById(controlId);
await this.retry.try(async () => {
await elementToHover.moveMouseTo();
await this.testSubjects.existOrFail(`control-action-${controlId}-edit`);
});
}
public async editExistingControl(controlId: string) {
this.log.debug(`Opening control editor for control: ${controlId}`);
await this.hoverOverExistingControl(controlId);
await this.testSubjects.click(`control-action-${controlId}-edit`);
}
public async removeExistingControl(controlId: string) {
this.log.debug(`Removing control: ${controlId}`);
await this.hoverOverExistingControl(controlId);
await this.testSubjects.click(`control-action-${controlId}-delete`);
await this.common.clickConfirmOnModal();
}
// Options list functions
public async optionsListGetSelectionsString(controlId: string) {
this.log.debug(`Getting selections string for Options List: ${controlId}`);
const controlElement = await this.getControlElementById(controlId);
return (await controlElement.getVisibleText()).split('\n')[1];
}
public async optionsListOpenPopover(controlId: string) {
this.log.debug(`Opening popover for Options List: ${controlId}`);
await this.testSubjects.click(`optionsList-control-${controlId}`);
await this.retry.try(async () => {
await this.testSubjects.existOrFail(`optionsList-control-available-options`);
});
}
public async optionsListEnsurePopoverIsClosed(controlId: string) {
this.log.debug(`Opening popover for Options List: ${controlId}`);
await this.testSubjects.click(`optionsList-control-${controlId}`);
await this.testSubjects.waitForDeleted(`optionsList-control-available-options`);
}
public async optionsListPopoverAssertOpen() {
await this.retry.try(async () => {
if (!(await this.testSubjects.exists(`optionsList-control-available-options`))) {
throw new Error('options list popover must be open before calling selectOption');
}
});
}
public async optionsListPopoverGetAvailableOptionsCount() {
this.log.debug(`getting available options count from options list`);
const availableOptions = await this.testSubjects.find(`optionsList-control-available-options`);
return +(await availableOptions.getAttribute('data-option-count'));
}
public async optionsListPopoverGetAvailableOptions() {
this.log.debug(`getting available options count from options list`);
const availableOptions = await this.testSubjects.find(`optionsList-control-available-options`);
return (await availableOptions.getVisibleText()).split('\n');
}
public async optionsListPopoverSearchForOption(search: string) {
this.log.debug(`searching for ${search} in options list`);
await this.optionsListPopoverAssertOpen();
await this.testSubjects.setValue(`optionsList-control-search-input`, search);
}
public async optionsListPopoverClearSearch() {
this.log.debug(`clearing search from options list`);
await this.optionsListPopoverAssertOpen();
await this.find.clickByCssSelector('.euiFormControlLayoutClearButton');
}
public async optionsListPopoverSelectOption(availableOption: string) {
this.log.debug(`selecting ${availableOption} from options list`);
await this.optionsListPopoverAssertOpen();
await this.testSubjects.click(`optionsList-control-selection-${availableOption}`);
}
public async optionsListPopoverClearSelections() {
this.log.debug(`clearing all selections from options list`);
await this.optionsListPopoverAssertOpen();
await this.testSubjects.click(`optionsList-control-clear-all-selections`);
}
/* -----------------------------------------------------------
Control editor flyout
----------------------------------------------------------- */
// Generic control editor functions
public async controlEditorSetTitle(title: string) {
this.log.debug(`Setting control title to ${title}`);
await this.testSubjects.setValue('control-editor-title-input', title);
}
public async controlEditorSetWidth(width: ControlWidth) {
this.log.debug(`Setting control width to ${width}`);
await this.testSubjects.click(`control-editor-width-${width}`);
}
public async controlEditorSave() {
this.log.debug(`Saving changes in control editor`);
await this.testSubjects.click(`control-editor-save`);
}
public async controlEditorCancel() {
this.log.debug(`Canceling changes in control editor`);
await this.testSubjects.click(`control-editor-cancel`);
}
// Options List editor functions
public async createOptionsListControl({
dataViewTitle,
fieldName,
width,
title,
}: {
title?: string;
fieldName: string;
width?: ControlWidth;
dataViewTitle?: string;
}) {
this.log.debug(`Creating options list control ${title ?? fieldName}`);
await this.openCreateControlFlyout(OPTIONS_LIST_CONTROL);
if (dataViewTitle) await this.optionsListEditorSetDataView(dataViewTitle);
if (fieldName) await this.optionsListEditorSetfield(fieldName);
if (title) await this.controlEditorSetTitle(title);
if (width) await this.controlEditorSetWidth(width);
await this.controlEditorSave();
}
public async optionsListEditorSetDataView(dataViewTitle: string) {
this.log.debug(`Setting options list data view to ${dataViewTitle}`);
await this.testSubjects.click('open-data-view-picker');
await this.retry.try(async () => {
await this.testSubjects.existOrFail('data-view-picker-title');
});
await this.testSubjects.click(`data-view-picker-${dataViewTitle}`);
}
public async optionsListEditorSetfield(fieldName: string, shouldSearch: boolean = false) {
this.log.debug(`Setting options list field to ${fieldName}`);
if (shouldSearch) {
await this.testSubjects.setValue('field-search-input', fieldName);
}
await this.retry.try(async () => {
await this.testSubjects.existOrFail(`field-picker-select-${fieldName}`);
});
await this.testSubjects.click(`field-picker-select-${fieldName}`);
}
}

View file

@ -30,12 +30,14 @@ import { VegaChartPageObject } from './vega_chart_page';
import { SavedObjectsPageObject } from './management/saved_objects_page';
import { LegacyDataTableVisPageObject } from './legacy/data_table_vis';
import { IndexPatternFieldEditorPageObject } from './management/indexpattern_field_editor_page';
import { DashboardPageControls } from './dashboard_page_controls';
export const pageObjects = {
common: CommonPageObject,
console: ConsolePageObject,
context: ContextPageObject,
dashboard: DashboardPageObject,
dashboardControls: DashboardPageControls,
discover: DiscoverPageObject,
error: ErrorPageObject,
header: HeaderPageObject,

View file

@ -22,6 +22,7 @@ export default function ({ loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./dashboard_maps_by_value'));
loadTestFile(require.resolve('./migration_smoke_tests/lens_migration_smoke_test'));
loadTestFile(require.resolve('./migration_smoke_tests/controls_migration_smoke_test'));
loadTestFile(require.resolve('./migration_smoke_tests/visualize_migration_smoke_test'));
loadTestFile(require.resolve('./migration_smoke_tests/tsvb_migration_smoke_test'));
});

View file

@ -0,0 +1,96 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
/*
* This test imports a dashboard saved with controls from 8.0.0, because that is the earliest version
* with the dashboard controls integration in place.
*/
import expect from '@kbn/expect';
import path from 'path';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const retry = getService('retry');
const pieChart = getService('pieChart');
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
const testSubjects = getService('testSubjects');
const { common, settings, savedObjects, dashboard, dashboardControls } = getPageObjects([
'common',
'settings',
'dashboard',
'savedObjects',
'dashboardControls',
]);
describe('Export import saved objects between versions', () => {
before(async () => {
await esArchiver.loadIfNeeded(
'x-pack/test/functional/es_archives/getting_started/shakespeare'
);
await kibanaServer.uiSettings.replace({});
await settings.navigateTo();
await settings.clickKibanaSavedObjects();
await savedObjects.importFile(
path.join(__dirname, 'exports', 'controls_dashboard_migration_test_8_0_0.ndjson')
);
});
after(async () => {
await esArchiver.unload('x-pack/test/functional/es_archives/getting_started/shakespeare');
await esArchiver.load('x-pack/test/functional/es_archives/empty_kibana');
});
it('should be able to import dashboard with controls from 8.0.0', async () => {
// this will catch cases where there is an error in the migrations.
await savedObjects.checkImportSucceeded();
await savedObjects.clickImportDone();
});
it('should render all panels on the dashboard', async () => {
await dashboardControls.enableControlsLab();
await common.navigateToApp('dashboard');
await dashboard.loadSavedDashboard('[8.0.0] Controls Dashboard');
// dashboard should load properly
await dashboard.expectOnDashboard('[8.0.0] Controls Dashboard');
await dashboard.waitForRenderComplete();
// There should be 0 error embeddables on the dashboard
const errorEmbeddables = await testSubjects.findAll('embeddableStackError');
expect(errorEmbeddables.length).to.be(0);
});
it('loads all controls from the saved dashboard', async () => {
expect(await dashboardControls.getControlsCount()).to.be(2);
expect(await dashboardControls.getAllControlTitles()).to.eql(['Speaker Name', 'Play Name']);
const ids = await dashboardControls.getAllControlIds();
for (const id of ids) {
await dashboardControls.optionsListOpenPopover(id);
await retry.try(async () => {
// Value counts should be 10, because there are more than 10 speakers and plays in the data set
expect(await dashboardControls.optionsListPopoverGetAvailableOptionsCount()).to.be(10);
});
await dashboardControls.optionsListEnsurePopoverIsClosed(id);
}
});
it('applies default selected options list options to control', async () => {
const controlIds = await dashboardControls.getAllControlIds();
const selectionString = await dashboardControls.optionsListGetSelectionsString(controlIds[0]);
expect(selectionString).to.be('HAMLET, ROMEO, JULIET, BRUTUS');
});
it('applies default selected options list options to dashboard', async () => {
// because 4 selections are made on the control, the pie chart should only show 4 slices.
expect(await pieChart.getPieSliceCount()).to.be(4);
});
});
}