[controls] Refactor control group settings functional tests (#190999)

Refactor control group settings tests
1. move non-functional test cases to unit tests
2. use pre-built dashboard to avoid time of building dashboard in test

### Before
Tests take 4 minutes to run locally
<img width="400" alt="Screenshot 2024-08-21 at 3 12 31 PM"
src="https://github.com/user-attachments/assets/96cf584c-2b32-4281-86ee-9791544bd5fa">


### After
Tests take 1 minute to run locally
<img width="400" alt="Screenshot 2024-08-21 at 2 53 37 PM"
src="https://github.com/user-attachments/assets/853a6f3a-74c5-4ca8-a488-99dd24477b1e">

---------

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Nathan Reese 2024-08-22 14:13:51 -06:00 committed by GitHub
parent b6ce155bb5
commit 79051d46f7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 221 additions and 140 deletions

View file

@ -0,0 +1,50 @@
/*
* 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 React from 'react';
import { BehaviorSubject } from 'rxjs';
import { render } from '@testing-library/react';
import { ControlGroupEditor } from './control_group_editor';
import { ControlGroupApi, ControlStyle, ParentIgnoreSettings } from '../../..';
import { ControlGroupChainingSystem, DEFAULT_CONTROL_STYLE } from '../../../../common';
import { DefaultControlApi } from '../../controls/types';
describe('render', () => {
const children$ = new BehaviorSubject<{ [key: string]: DefaultControlApi }>({});
const props = {
api: {
children$,
} as unknown as ControlGroupApi,
onCancel: () => {},
onSave: () => {},
onDeleteAll: () => {},
stateManager: {
chainingSystem: new BehaviorSubject<ControlGroupChainingSystem>('HIERARCHICAL'),
labelPosition: new BehaviorSubject<ControlStyle>(DEFAULT_CONTROL_STYLE),
autoApplySelections: new BehaviorSubject<boolean>(true),
ignoreParentSettings: new BehaviorSubject<ParentIgnoreSettings | undefined>(undefined),
},
};
beforeEach(() => {
children$.next({});
});
test('should not display delete all controls button when there are no controls', () => {
const editor = render(<ControlGroupEditor {...props} />);
expect(editor.queryByTestId('delete-all-controls-button')).not.toBeInTheDocument();
});
test('should display delete all controls button when there are controls', () => {
children$.next({
alpha: {} as unknown as DefaultControlApi,
});
const editor = render(<ControlGroupEditor {...props} />);
expect(editor.queryByTestId('delete-all-controls-button')).toBeInTheDocument();
});
});

View file

@ -27,11 +27,11 @@ import {
} from '@elastic/eui';
import { css } from '@emotion/react';
import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing';
import { ControlStyle, ParentIgnoreSettings } from '../..';
import { ControlStyle, ParentIgnoreSettings } from '../../..';
import { ControlStateManager } from '../controls/types';
import { ControlGroupStrings } from './control_group_strings';
import { ControlGroupApi, ControlGroupEditorState } from './types';
import { ControlStateManager } from '../../controls/types';
import { ControlGroupStrings } from '../control_group_strings';
import { ControlGroupApi, ControlGroupEditorState } from '../types';
const CONTROL_LAYOUT_OPTIONS = [
{
@ -46,7 +46,7 @@ const CONTROL_LAYOUT_OPTIONS = [
},
];
interface EditControlGroupProps {
interface Props {
onCancel: () => void;
onSave: () => void;
onDeleteAll: () => void;
@ -54,13 +54,7 @@ interface EditControlGroupProps {
api: ControlGroupApi; // controls must always have a parent API
}
export const ControlGroupEditor = ({
onCancel,
onSave,
onDeleteAll,
stateManager,
api,
}: EditControlGroupProps) => {
export const ControlGroupEditor = ({ onCancel, onSave, onDeleteAll, stateManager, api }: Props) => {
const [
children,
selectedLabelPosition,

View file

@ -0,0 +1,106 @@
/*
* 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 React, { useImperativeHandle } from 'react';
import { BehaviorSubject } from 'rxjs';
import { render, waitFor } from '@testing-library/react';
import { ControlPanel } from './control_panel';
import { registry as presentationUtilServicesRegistry } from '@kbn/presentation-util-plugin/public/services/plugin_services.story';
import { pluginServices as presentationUtilPluginServices } from '@kbn/presentation-util-plugin/public/services';
import { ControlStyle, ControlWidth } from '../../..';
describe('render', () => {
let mockApi = {};
const Component = React.forwardRef((_, ref) => {
// expose the api into the imperative handle
useImperativeHandle(ref, () => mockApi, []);
return <div />;
}) as any;
beforeAll(() => {
presentationUtilServicesRegistry.start({});
presentationUtilPluginServices.setRegistry(presentationUtilServicesRegistry);
presentationUtilPluginServices.getServices().uiActions.getTriggerCompatibleActions = jest
.fn()
.mockImplementation(() => {
return [
{
isCompatible: jest.fn().mockResolvedValue(true),
id: 'testAction',
MenuItem: () => <div>test1</div>,
},
];
});
});
beforeEach(() => {
mockApi = {};
});
describe('control width', () => {
test('defaults to medium and grow enabled', async () => {
const controlPanel = render(<ControlPanel uuid="control1" Component={Component} />);
await waitFor(() => {
const controlFrame = controlPanel.getByTestId('control-frame');
expect(controlFrame.getAttribute('class')).toContain('controlFrameWrapper--medium');
expect(controlFrame.getAttribute('class')).toContain('controlFrameWrapper--grow');
});
});
test('should use small class when using small width', async () => {
mockApi = {
uuid: 'control1',
width: new BehaviorSubject<ControlWidth>('small'),
};
const controlPanel = render(<ControlPanel uuid="control1" Component={Component} />);
await waitFor(() => {
const controlFrame = controlPanel.getByTestId('control-frame');
expect(controlFrame.getAttribute('class')).toContain('controlFrameWrapper--small');
});
});
});
describe('label position', () => {
test('should use one line layout class when using one line layout', async () => {
mockApi = {
uuid: 'control1',
parentApi: {
labelPosition: new BehaviorSubject<ControlStyle>('oneLine'),
},
};
const controlPanel = render(<ControlPanel uuid="control1" Component={Component} />);
await waitFor(() => {
const floatingActions = controlPanel.getByTestId(
'presentationUtil__floatingActions__control1'
);
expect(floatingActions.getAttribute('class')).toContain(
'controlFrameFloatingActions--oneLine'
);
});
});
test('should use two line layout class when using two line layout', async () => {
mockApi = {
uuid: 'control1',
parentApi: {
labelPosition: new BehaviorSubject<ControlStyle>('twoLine'),
},
};
const controlPanel = render(<ControlPanel uuid="control1" Component={Component} />);
await waitFor(() => {
const floatingActions = controlPanel.getByTestId(
'presentationUtil__floatingActions__control1'
);
expect(floatingActions.getAttribute('class')).toContain(
'controlFrameFloatingActions--twoLine'
);
});
});
});
});

View file

@ -27,7 +27,7 @@ import {
useBatchedOptionalPublishingSubjects,
} from '@kbn/presentation-publishing';
import { FloatingActions } from '@kbn/presentation-util-plugin/public';
import { DEFAULT_CONTROL_WIDTH } from '../../../../common';
import { DEFAULT_CONTROL_GROW, DEFAULT_CONTROL_WIDTH } from '../../../../common';
import { ControlPanelProps, DefaultControlApi } from '../../controls/types';
import { ControlError } from './control_error';
@ -121,17 +121,18 @@ export const ControlPanel = <ApiType extends DefaultControlApi = DefaultControlA
const viewMode = (rawViewMode ?? ViewMode.VIEW) as ViewMode;
const isEditable = viewMode === ViewMode.EDIT;
const controlWidth = width ?? DEFAULT_CONTROL_WIDTH;
const controlGrow = grow ?? DEFAULT_CONTROL_GROW;
return (
<EuiFlexItem
ref={setNodeRef}
style={style}
grow={grow}
grow={controlGrow}
data-control-id={uuid}
data-test-subj={`control-frame`}
data-test-subj="control-frame"
data-render-complete="true"
className={classNames('controlFrameWrapper', {
'controlFrameWrapper--grow': grow,
'controlFrameWrapper--grow': controlGrow,
'controlFrameWrapper--small': controlWidth === 'small',
'controlFrameWrapper--medium': controlWidth === 'medium',
'controlFrameWrapper--large': controlWidth === 'large',

View file

@ -16,7 +16,7 @@ import React from 'react';
import { BehaviorSubject } from 'rxjs';
import { ControlStateManager } from '../controls/types';
import { ControlGroupEditor } from './control_group_editor';
import { ControlGroupEditor } from './components/control_group_editor';
import { ControlGroupApi, ControlGroupEditorState } from './types';
export const openEditControlGroupFlyout = (

View file

@ -6,13 +6,10 @@
* Side Public License, v 1.
*/
import { OPTIONS_LIST_CONTROL, RANGE_SLIDER_CONTROL } from '@kbn/controls-plugin/common';
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../../ftr_provider_context';
export default function ({ getService, getPageObjects }: FtrProviderContext) {
const find = getService('find');
const queryBar = getService('queryBar');
const filterBar = getService('filterBar');
const testSubjects = getService('testSubjects');
@ -24,70 +21,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
describe('Dashboard control group settings', () => {
before(async () => {
await dashboard.navigateToApp();
await dashboard.gotoDashboardLandingPage();
await dashboard.clickNewDashboard();
await timePicker.setDefaultDataRange();
await dashboard.saveDashboard('Test Control Group Settings');
});
it('adjust layout of controls', async () => {
await dashboard.loadSavedDashboard('control group settings test dashboard');
await dashboard.switchToEditMode();
await dashboardControls.createControl({
controlType: OPTIONS_LIST_CONTROL,
dataViewTitle: 'animals-*',
fieldName: 'sound.keyword',
});
await dashboardControls.adjustControlsLayout('twoLine');
const controlGroupWrapper = await testSubjects.find('controls-group-wrapper');
expect(await controlGroupWrapper.elementHasClass('controlsWrapper--twoLine')).to.be(true);
});
describe('apply new default width and grow', async () => {
it('defaults to medium width and grow enabled', async () => {
await dashboardControls.openCreateControlFlyout();
const mediumWidthButton = await testSubjects.find('control-editor-width-medium');
expect(await mediumWidthButton.elementHasClass('euiButtonGroupButton-isSelected')).to.be(
true
);
const growSwitch = await testSubjects.find('control-editor-grow-switch');
expect(await growSwitch.getAttribute('aria-checked')).to.be('true');
await testSubjects.click('control-editor-cancel');
});
it('sets default to width and grow of last created control', async () => {
await dashboardControls.createControl({
controlType: OPTIONS_LIST_CONTROL,
dataViewTitle: 'animals-*',
fieldName: 'name.keyword',
width: 'small',
grow: false,
});
const controlIds = await dashboardControls.getAllControlIds();
const firstControl = await find.byXPath(`//div[@data-control-id="${controlIds[0]}"]`);
expect(await firstControl.elementHasClass('controlFrameWrapper--medium')).to.be(true);
expect(await firstControl.getAttribute('class')).not.to.contain('euiFlexItem-growZero');
const secondControl = await find.byXPath(`//div[@data-control-id="${controlIds[1]}"]`);
expect(await secondControl.elementHasClass('controlFrameWrapper--small')).to.be(true);
expect(await secondControl.getAttribute('class')).to.contain('euiFlexItem-growZero');
await dashboardControls.openCreateControlFlyout();
const smallWidthButton = await testSubjects.find('control-editor-width-small');
expect(await smallWidthButton.elementHasClass('euiButtonGroupButton-isSelected')).to.be(
true
);
const growSwitch = await testSubjects.find('control-editor-grow-switch');
expect(await growSwitch.getAttribute('aria-checked')).to.be('false');
await testSubjects.click('control-editor-cancel');
});
});
describe('filtering settings', async () => {
let firstOptionsListId: string;
const firstOptionsListId = 'bcb81550-0843-44ea-9020-6c1ebf3228ac';
let beforeCount: number;
let rangeSliderId: string;
const rangeSliderId = '15925456-9e12-4b08-b2e6-4ae6ac27114d';
let beforeRange: number;
const getRange = async () => {
@ -106,19 +48,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
};
before(async () => {
await dashboardControls.createControl({
controlType: RANGE_SLIDER_CONTROL,
dataViewTitle: 'animals-*',
fieldName: 'weightLbs',
});
await dashboard.clickQuickSave();
firstOptionsListId = (await dashboardControls.getAllControlIds())[0];
await dashboardControls.optionsListWaitForLoading(firstOptionsListId);
await dashboardControls.optionsListOpenPopover(firstOptionsListId);
beforeCount = await dashboardControls.optionsListPopoverGetAvailableOptionsCount();
rangeSliderId = (await dashboardControls.getAllControlIds())[2];
beforeRange = await getRange();
});
@ -172,65 +105,13 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
});
describe('flyout only show settings that are relevant', async () => {
before(async () => {
await dashboard.switchToEditMode();
});
it('when no controls', async () => {
await dashboardControls.deleteAllControls();
await dashboardControls.openControlGroupSettingsFlyout();
await testSubjects.missingOrFail('delete-all-controls-button');
});
it('when at least one control', async () => {
await dashboardControls.createControl({
controlType: OPTIONS_LIST_CONTROL,
dataViewTitle: 'animals-*',
fieldName: 'sound.keyword',
});
await dashboardControls.openControlGroupSettingsFlyout();
await testSubjects.existOrFail('delete-all-controls-button');
});
afterEach(async () => {
await testSubjects.click('euiFlyoutCloseButton');
if (await testSubjects.exists('confirmModalConfirmButton')) {
await testSubjects.click('confirmModalConfirmButton');
}
});
after(async () => {
await dashboardControls.deleteAllControls();
});
});
describe('control group settings flyout closes', async () => {
it('on save', async () => {
await dashboardControls.openControlGroupSettingsFlyout();
await dashboard.saveDashboard('Test Control Group Settings', {
saveAsNew: false,
exitFromEditMode: false,
});
await testSubjects.missingOrFail('control-group-settings-flyout');
});
it('on view mode change', async () => {
await dashboardControls.openControlGroupSettingsFlyout();
await dashboard.clickCancelOutOfEditMode();
await testSubjects.missingOrFail('control-group-settings-flyout');
});
it('when navigating away from dashboard', async () => {
await dashboard.switchToEditMode();
await dashboardControls.openControlGroupSettingsFlyout();
await dashboard.gotoDashboardLandingPage();
await testSubjects.missingOrFail('control-group-settings-flyout');
});
after(async () => {
await dashboard.loadSavedDashboard('Test Control Group Settings');
});
});
});
}

View file

@ -26,7 +26,6 @@ export default function ({ loadTestFile, getService, getPageObjects }: FtrProvid
defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c',
});
// enable the controls lab and navigate to the dashboard listing page to start
await dashboard.navigateToApp();
await dashboard.preserveCrossAppState();
}

View file

@ -3175,3 +3175,53 @@
"coreMigrationVersion": "8.8.0",
"typeMigrationVersion": "8.9.0"
}
{
"id": "153e3302-1b37-4c45-9b11-91deec40ab47",
"type": "dashboard",
"namespaces": [
"default"
],
"updated_at": "2024-08-21T20:48:22.388Z",
"created_at": "2024-08-21T20:46:21.250Z",
"version": "WzQ4MCwxXQ==",
"attributes": {
"version": 2,
"controlGroupInput": {
"controlStyle": "oneLine",
"chainingSystem": "HIERARCHICAL",
"showApplySelections": false,
"panelsJSON": "{\"bcb81550-0843-44ea-9020-6c1ebf3228ac\":{\"type\":\"optionsListControl\",\"order\":0,\"grow\":true,\"width\":\"medium\",\"explicitInput\":{\"id\":\"bcb81550-0843-44ea-9020-6c1ebf3228ac\",\"fieldName\":\"sound.keyword\",\"title\":\"sound.keyword\",\"grow\":true,\"width\":\"medium\",\"searchTechnique\":\"prefix\",\"enhancements\":{}}},\"15925456-9e12-4b08-b2e6-4ae6ac27114d\":{\"type\":\"rangeSliderControl\",\"order\":1,\"grow\":true,\"width\":\"medium\",\"explicitInput\":{\"fieldName\":\"weightLbs\",\"title\":\"weightLbs\",\"searchTechnique\":\"exact\",\"id\":\"15925456-9e12-4b08-b2e6-4ae6ac27114d\",\"enhancements\":{}}}}",
"ignoreParentSettingsJSON": "{\"ignoreFilters\":false,\"ignoreQuery\":false,\"ignoreTimerange\":false,\"ignoreValidations\":false}"
},
"kibanaSavedObjectMeta": {
"searchSourceJSON": "{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}"
},
"description": "",
"refreshInterval": {
"pause": true,
"value": 60000
},
"timeRestore": true,
"optionsJSON": "{\"useMargins\":true,\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"hidePanelTitles\":false}",
"panelsJSON": "[]",
"timeFrom": "2018-01-01T00:00:00.000Z",
"title": "control group settings test dashboard",
"timeTo": "2018-04-13T00:00:00.000Z"
},
"references": [
{
"name": "controlGroup_bcb81550-0843-44ea-9020-6c1ebf3228ac:optionsListDataView",
"type": "index-pattern",
"id": "a0f483a0-3dc9-11e8-8660-4d65aa086b3c"
},
{
"name": "controlGroup_15925456-9e12-4b08-b2e6-4ae6ac27114d:rangeSliderDataView",
"type": "index-pattern",
"id": "a0f483a0-3dc9-11e8-8660-4d65aa086b3c"
}
],
"managed": false,
"coreMigrationVersion": "8.8.0",
"typeMigrationVersion": "10.2.0"
}