mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
added functional tests for dashboard controls integration (#119755)
This commit is contained in:
parent
9110908e35
commit
bf2779c708
21 changed files with 629 additions and 21 deletions
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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()}
|
||||
|
|
|
@ -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(),
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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';
|
|
@ -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}
|
||||
|
|
|
@ -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={() => {
|
||||
|
|
|
@ -7,4 +7,5 @@
|
|||
*/
|
||||
|
||||
export * from './control_group';
|
||||
export * from './control_types';
|
||||
export * from './types';
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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,
|
||||
})}
|
||||
|
|
|
@ -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}
|
||||
|
|
216
test/functional/apps/dashboard/dashboard_controls_integration.ts
Normal file
216
test/functional/apps/dashboard/dashboard_controls_integration.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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'));
|
||||
|
|
254
test/functional/page_objects/dashboard_page_controls.ts
Normal file
254
test/functional/page_objects/dashboard_page_controls.ts
Normal 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}`);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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'));
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue