diff --git a/x-pack/plugins/ml/public/application/settings/calendars/edit/calendar_form/__snapshots__/calendar_form.test.js.snap b/x-pack/plugins/ml/public/application/settings/calendars/edit/calendar_form/__snapshots__/calendar_form.test.js.snap index fed435d47dfc6b..ad76bb91156172 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/edit/calendar_form/__snapshots__/calendar_form.test.js.snap +++ b/x-pack/plugins/ml/public/application/settings/calendars/edit/calendar_form/__snapshots__/calendar_form.test.js.snap @@ -56,6 +56,7 @@ exports[`CalendarForm Renders calendar form 1`] = ` labelType="label" > - +

{description}

@@ -116,6 +116,7 @@ export const CalendarForm = ({ value={calendarId} onChange={onCalendarIdChange} disabled={isEdit === true || saving === true} + data-test-subj="mlCalendarIdInput" /> @@ -132,6 +133,7 @@ export const CalendarForm = ({ value={description} onChange={onDescriptionChange} disabled={isEdit === true || saving === true} + data-test-subj="mlCalendarDescriptionInput" /> diff --git a/x-pack/plugins/ml/public/application/settings/calendars/edit/new_event_modal/new_event_modal.js b/x-pack/plugins/ml/public/application/settings/calendars/edit/new_event_modal/new_event_modal.js index d80e248674a8f6..0b5d2b7b5a3ea6 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/edit/new_event_modal/new_event_modal.js +++ b/x-pack/plugins/ml/public/application/settings/calendars/edit/new_event_modal/new_event_modal.js @@ -257,7 +257,12 @@ export class NewEventModal extends Component { return ( - + @@ -293,13 +299,18 @@ export class NewEventModal extends Component { - + - + c.calendar_id).join(', '), + }} /> } onCancel={this.closeDestroyModal} @@ -130,18 +135,7 @@ export class CalendarsListUI extends Component { } buttonColor="danger" defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} - > -

- c.calendar_id).join(', '), - }} - /> -

-
+ /> ); } diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/components/add_item_popover/__snapshots__/add_item_popover.test.js.snap b/x-pack/plugins/ml/public/application/settings/filter_lists/components/add_item_popover/__snapshots__/add_item_popover.test.js.snap index 6e9cd17deabee8..969406724537d2 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/components/add_item_popover/__snapshots__/add_item_popover.test.js.snap +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/components/add_item_popover/__snapshots__/add_item_popover.test.js.snap @@ -7,7 +7,7 @@ exports[`AddItemPopover calls addItems with multiple items on clicking Add butto button={ @@ -71,6 +72,7 @@ exports[`AddItemPopover calls addItems with multiple items on clicking Add butto grow={false} > @@ -93,7 +95,7 @@ exports[`AddItemPopover opens the popover onButtonClick 1`] = ` button={ @@ -157,6 +160,7 @@ exports[`AddItemPopover opens the popover onButtonClick 1`] = ` grow={false} > @@ -179,7 +183,7 @@ exports[`AddItemPopover renders the popover 1`] = ` button={ @@ -243,6 +248,7 @@ exports[`AddItemPopover renders the popover 1`] = ` grow={false} > diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/components/add_item_popover/add_item_popover.js b/x-pack/plugins/ml/public/application/settings/filter_lists/components/add_item_popover/add_item_popover.js index 07e060d87b36a4..53a3877e2f1bdb 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/components/add_item_popover/add_item_popover.js +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/components/add_item_popover/add_item_popover.js @@ -84,7 +84,7 @@ export class AddItemPopover extends Component { iconSide="right" onClick={this.onButtonClick} isDisabled={this.props.canCreateFilter === false} - data-test-subj="mlFilterListAddItemButton" + data-test-subj="mlFilterListOpenNewItemsPopoverButton" > } > - + @@ -127,6 +131,7 @@ export class AddItemPopover extends Component { } + data-test-subj="mlFilterListDeleteConfirmation" defaultFocusedButton="confirm" onCancel={[Function]} onConfirm={[Function]} diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.js b/x-pack/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.js index 75fdce8e2bac80..5aafe79645f6a2 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.js +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/components/delete_filter_list_modal/delete_filter_list_modal.js @@ -86,6 +86,7 @@ export class DeleteFilterListModal extends Component { } buttonColor="danger" defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} + data-test-subj={'mlFilterListDeleteConfirmation'} /> ); diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/components/edit_description_popover/__snapshots__/edit_description_popover.test.js.snap b/x-pack/plugins/ml/public/application/settings/filter_lists/components/edit_description_popover/__snapshots__/edit_description_popover.test.js.snap index 9904e90a5afae9..268b93923a4325 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/components/edit_description_popover/__snapshots__/edit_description_popover.test.js.snap +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/components/edit_description_popover/__snapshots__/edit_description_popover.test.js.snap @@ -47,6 +47,7 @@ exports[`FilterListUsagePopover opens the popover onButtonClick 1`] = ` labelType="label" > diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/components/edit_description_popover/edit_description_popover.js b/x-pack/plugins/ml/public/application/settings/filter_lists/components/edit_description_popover/edit_description_popover.js index 06ace034ca8193..b7bcb201f2438b 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/components/edit_description_popover/edit_description_popover.js +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/components/edit_description_popover/edit_description_popover.js @@ -102,6 +102,7 @@ export class EditDescriptionPopover extends Component { name="filter_list_description" value={value} onChange={this.onChange} + data-test-subj={'mlFilterListDescriptionInput'} /> diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/edit/__snapshots__/edit_filter_list.test.js.snap b/x-pack/plugins/ml/public/application/settings/filter_lists/edit/__snapshots__/edit_filter_list.test.js.snap index c2fab64473228c..f6a4f769755534 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/edit/__snapshots__/edit_filter_list.test.js.snap +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/edit/__snapshots__/edit_filter_list.test.js.snap @@ -80,6 +80,7 @@ exports[`EditFilterList adds new items to filter list 1`] = ` grow={false} > - +

A test filter list

@@ -180,6 +183,7 @@ exports[`EditFilterListHeader renders the header when creating a new filter list labelType="label" > - +

A test filter list

diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.js b/x-pack/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.js index 681c54ca9eee07..9ea470a388f024 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.js +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/edit/edit_filter_list.js @@ -362,7 +362,10 @@ export class EditFilterListUI extends Component { /> - this.returnToFiltersList()}> + this.returnToFiltersList()} + > updateNewFilterId(e.target.value)} + data-test-subj={'mlNewFilterListIdInput'} /> ); @@ -96,7 +97,7 @@ export const EditFilterListHeader = ({ if (description !== undefined && description.length > 0) { descriptionField = ( - +

{description}

); diff --git a/x-pack/plugins/ml/public/application/settings/filter_lists/list/table.js b/x-pack/plugins/ml/public/application/settings/filter_lists/list/table.js index ed992b4e866ffa..9e1457483cb2c4 100644 --- a/x-pack/plugins/ml/public/application/settings/filter_lists/list/table.js +++ b/x-pack/plugins/ml/public/application/settings/filter_lists/list/table.js @@ -214,7 +214,7 @@ export function FilterListsTable({ isSelectable={true} data-test-subj="mlFilterListsTable" rowProps={(item) => ({ - 'data-test-subj': `mlFilterListsRow row-${item.filter_id}`, + 'data-test-subj': `mlFilterListRow row-${item.filter_id}`, })} /> diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index f626835da8e11e..7ca4e02068d418 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -10527,8 +10527,6 @@ "xpack.ml.calendarsList.deleteCalendars.deletingCalendarSuccessNotificationMessage": "{messageId} が選択されました", "xpack.ml.calendarsList.deleteCalendarsModal.cancelButtonLabel": "キャンセル", "xpack.ml.calendarsList.deleteCalendarsModal.deleteButtonLabel": "削除", - "xpack.ml.calendarsList.deleteCalendarsModal.deleteCalendarsDescription": "{calendarsCount, plural, one {このカレンダー} other {これらのカレンダー}}を削除しますか?{calendarsList}", - "xpack.ml.calendarsList.deleteCalendarsModal.deleteCalendarTitle": "カレンダーの削除", "xpack.ml.calendarsList.errorWithLoadingListOfCalendarsErrorMessage": "カレンダーのリストの読み込み中にエラーが発生しました。", "xpack.ml.calendarsList.table.allJobsLabel": "すべてのジョブに適用", "xpack.ml.calendarsList.table.deleteButtonLabel": "削除", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index d6baa87ca9e2f0..2e1fb55777cdf3 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -10533,8 +10533,6 @@ "xpack.ml.calendarsList.deleteCalendars.deletingCalendarSuccessNotificationMessage": "已删除 {messageId}", "xpack.ml.calendarsList.deleteCalendarsModal.cancelButtonLabel": "取消", "xpack.ml.calendarsList.deleteCalendarsModal.deleteButtonLabel": "删除", - "xpack.ml.calendarsList.deleteCalendarsModal.deleteCalendarsDescription": "是否删除{calendarsCount, plural, one {此日历} other {这些日历}}?{calendarsList}", - "xpack.ml.calendarsList.deleteCalendarsModal.deleteCalendarTitle": "删除日历", "xpack.ml.calendarsList.errorWithLoadingListOfCalendarsErrorMessage": "加载日历列表时出错。", "xpack.ml.calendarsList.table.allJobsLabel": "应用到所有作业", "xpack.ml.calendarsList.table.deleteButtonLabel": "删除", diff --git a/x-pack/test/functional/apps/ml/index.ts b/x-pack/test/functional/apps/ml/index.ts index e224f5c8bb1285..74dc0fc3ca9f0f 100644 --- a/x-pack/test/functional/apps/ml/index.ts +++ b/x-pack/test/functional/apps/ml/index.ts @@ -46,5 +46,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./anomaly_detection')); loadTestFile(require.resolve('./data_visualizer')); loadTestFile(require.resolve('./data_frame_analytics')); + loadTestFile(require.resolve('./settings')); }); } diff --git a/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts b/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts index eed7489b09fe63..c3dde872fa4a67 100644 --- a/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts +++ b/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts @@ -438,7 +438,7 @@ export default function ({ getService }: FtrProviderContext) { 'should display enabled elements of the edit calendar page' ); await ml.settingsFilterList.assertEditDescriptionButtonEnabled(true); - await ml.settingsFilterList.assertAddItemButtonEnabled(true); + await ml.settingsFilterList.assertAddItemsButtonEnabled(true); await ml.testExecution.logTestStep('should display the filter item in the list'); await ml.settingsFilterList.assertFilterItemExists(filterItems[0]); diff --git a/x-pack/test/functional/apps/ml/settings/calendar_creation.ts b/x-pack/test/functional/apps/ml/settings/calendar_creation.ts new file mode 100644 index 00000000000000..5b1e3b0a12b134 --- /dev/null +++ b/x-pack/test/functional/apps/ml/settings/calendar_creation.ts @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { asyncForEach, createJobConfig } from './common'; + +export default function ({ getService }: FtrProviderContext) { + const ml = getService('ml'); + const esArchiver = getService('esArchiver'); + + const calendarId = 'test_calendar_id'; + const jobConfigs = [createJobConfig('test_calendar_ad_1'), createJobConfig('test_calendar_ad_2')]; + + describe('calendar creation', function () { + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); + + await asyncForEach(jobConfigs, async (jobConfig) => { + await ml.api.createAnomalyDetectionJob(jobConfig); + }); + await ml.testResources.setKibanaTimeZoneToUTC(); + await ml.securityUI.loginAsMlPowerUser(); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + afterEach(async () => { + await ml.api.deleteCalendar(calendarId); + }); + + it('creates new calendar that applies to all jobs', async () => { + await ml.testExecution.logTestStep('calendar creation loads the calendar management page'); + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToSettings(); + await ml.settings.navigateToCalendarManagement(); + + await ml.testExecution.logTestStep('calendar creation loads the new calendar edit page'); + await ml.settingsCalendar.assertCreateCalendarButtonEnabled(true); + await ml.settingsCalendar.navigateToCalendarCreationPage(); + + await ml.testExecution.logTestStep('calendar creation sets calendar to apply to all jobs'); + await ml.settingsCalendar.toggleApplyToAllJobsSwitch(true); + await ml.settingsCalendar.assertJobSelectionNotExists(); + await ml.settingsCalendar.assertJobGroupSelectionNotExists(); + + await ml.testExecution.logTestStep('calendar creation sets the calendar id and description'); + await ml.settingsCalendar.setCalendarId(calendarId); + await ml.settingsCalendar.setCalendarDescription('test calendar description'); + + await ml.testExecution.logTestStep('calendar creation creates new calendar event'); + await ml.settingsCalendar.openNewCalendarEventForm(); + await ml.settingsCalendar.setCalendarEventDescription('holiday'); + await ml.settingsCalendar.addNewCalendarEvent(); + await ml.settingsCalendar.assertEventRowExists('holiday'); + + await ml.testExecution.logTestStep( + 'calendar creation saves the new calendar and displays it in the list of calendars ' + ); + await ml.settingsCalendar.saveCalendar(); + + await ml.settingsCalendar.assertCalendarRowExists(calendarId); + }); + + it('creates new calendar that applies to specific jobs', async () => { + await ml.testExecution.logTestStep('calendar creation loads the calendar management page'); + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToSettings(); + await ml.settings.navigateToCalendarManagement(); + + await ml.testExecution.logTestStep('calendar creation loads the new calendar edit page'); + await ml.settingsCalendar.assertCreateCalendarButtonEnabled(true); + await ml.settingsCalendar.navigateToCalendarCreationPage(); + + await ml.testExecution.logTestStep( + 'calendar creation verifies the job selection and job group section are displayed' + ); + await ml.settingsCalendar.assertJobSelectionExists(); + await ml.settingsCalendar.assertJobSelectionEnabled(true); + await ml.settingsCalendar.assertJobGroupSelectionExists(); + await ml.settingsCalendar.assertJobGroupSelectionEnabled(true); + + await ml.testExecution.logTestStep('calendar creation sets the calendar id'); + await ml.settingsCalendar.setCalendarId(calendarId); + + await ml.testExecution.logTestStep('calendar creation sets the job selection'); + await asyncForEach(jobConfigs, async (jobConfig) => { + await ml.settingsCalendar.selectJob(jobConfig.job_id); + }); + + await ml.settingsCalendar.saveCalendar(); + await ml.settingsCalendar.assertCalendarRowExists(calendarId); + }); + }); +} diff --git a/x-pack/test/functional/apps/ml/settings/calendar_delete.ts b/x-pack/test/functional/apps/ml/settings/calendar_delete.ts new file mode 100644 index 00000000000000..2cc4f91d5528f6 --- /dev/null +++ b/x-pack/test/functional/apps/ml/settings/calendar_delete.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { asyncForEach } from './common'; + +export default function ({ getService }: FtrProviderContext) { + const ml = getService('ml'); + + const testDataList = [1, 2].map((n) => ({ + calendarId: `test_delete_calendar_${n}`, + description: `test description ${n}`, + })); + + describe('calendar delete', function () { + before(async () => { + await ml.testResources.setKibanaTimeZoneToUTC(); + await ml.securityUI.loginAsMlPowerUser(); + + await asyncForEach(testDataList, async ({ calendarId, description }) => { + await ml.api.createCalendar(calendarId, { + description, + }); + }); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + + // clean up created calendars + await asyncForEach(testDataList, async ({ calendarId }) => { + await ml.api.deleteCalendar(calendarId); + }); + }); + + it('deletes multiple calendars', async () => { + await ml.testExecution.logTestStep('calendar delete loads the calendar list management page'); + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToSettings(); + await ml.settings.navigateToCalendarManagement(); + + await ml.testExecution.logTestStep('calendar delete selects multiple calendars for deletion'); + await asyncForEach(testDataList, async ({ calendarId }) => { + await ml.settingsCalendar.assertCalendarRowExists(calendarId); + await ml.settingsCalendar.selectCalendarRow(calendarId); + }); + + await ml.testExecution.logTestStep('calendar delete clicks the delete button'); + await ml.settingsCalendar.deleteCalendar(); + + await ml.testExecution.logTestStep( + 'calendar delete validates the calendars are deleted from the table' + ); + await asyncForEach(testDataList, async ({ calendarId }) => { + await ml.settingsCalendar.assertCalendarRowNotExists(calendarId); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/ml/settings/calendar_edit.ts b/x-pack/test/functional/apps/ml/settings/calendar_edit.ts new file mode 100644 index 00000000000000..f7c8c1f6f85f5b --- /dev/null +++ b/x-pack/test/functional/apps/ml/settings/calendar_edit.ts @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { asyncForEach, createJobConfig } from './common'; + +export default function ({ getService }: FtrProviderContext) { + const ml = getService('ml'); + const esArchiver = getService('esArchiver'); + const comboBox = getService('comboBox'); + + const calendarId = 'test_edit_calendar_id'; + const testEvents = [ + { description: 'event_1', start_time: 1513641600000, end_time: 1513728000000 }, + { description: 'event_2', start_time: 1513814400000, end_time: 1513900800000 }, + ]; + const jobConfigs = [createJobConfig('test_calendar_ad_1'), createJobConfig('test_calendar_ad_2')]; + const newJobGroups = ['farequote']; + + describe('calendar edit', function () { + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); + + await asyncForEach(jobConfigs, async (jobConfig) => { + await ml.api.createAnomalyDetectionJob(jobConfig); + }); + + await ml.api.createCalendar(calendarId, { + job_ids: jobConfigs.map((c) => c.job_id), + description: 'Test calendar', + }); + await ml.api.createCalendarEvents(calendarId, testEvents); + + await ml.testResources.setKibanaTimeZoneToUTC(); + await ml.securityUI.loginAsMlPowerUser(); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + afterEach(async () => { + await ml.api.deleteCalendar(calendarId); + }); + + it('updates jobs, groups and events', async () => { + await ml.testExecution.logTestStep('calendar edit loads the calendar management page'); + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToSettings(); + await ml.settings.navigateToCalendarManagement(); + + await ml.testExecution.logTestStep('calendar edit opens existing calendar'); + await ml.settingsCalendar.openCalendarEditForm(calendarId); + + await ml.testExecution.logTestStep( + 'calendar edit deselects previous job selection and assigns new job groups' + ); + await comboBox.clear('mlCalendarJobSelection'); + await asyncForEach(newJobGroups, async (newJobGroup) => { + await ml.settingsCalendar.selectJobGroup(newJobGroup); + }); + + await ml.testExecution.logTestStep('calendar edit deletes old events'); + + await asyncForEach(testEvents, async ({ description }) => { + await ml.settingsCalendar.deleteCalendarEventRow(description); + }); + + await ml.testExecution.logTestStep('calendar edit creates new calendar event'); + await ml.settingsCalendar.openNewCalendarEventForm(); + await ml.settingsCalendar.setCalendarEventDescription('holiday'); + await ml.settingsCalendar.addNewCalendarEvent(); + await ml.settingsCalendar.assertEventRowExists('holiday'); + + await ml.testExecution.logTestStep( + 'calendar edit saves the new calendar and displays it in the list of calendars ' + ); + await ml.settingsCalendar.saveCalendar(); + await ml.settingsCalendar.assertCalendarRowExists(calendarId); + + await ml.testExecution.logTestStep('calendar edit re-opens the updated calendar'); + await ml.settingsCalendar.openCalendarEditForm(calendarId); + await ml.testExecution.logTestStep('calendar edit verifies the job selection is empty'); + await ml.settingsCalendar.assertJobSelection([]); + await ml.testExecution.logTestStep( + 'calendar edit verifies the job group selection was updated' + ); + await ml.settingsCalendar.assertJobGroupSelection(newJobGroups); + + await ml.testExecution.logTestStep('calendar edit verifies calendar updated correctly'); + await asyncForEach(testEvents, async ({ description }) => { + await ml.settingsCalendar.assertEventRowMissing(description); + }); + await ml.settingsCalendar.assertEventRowExists('holiday'); + }); + }); +} diff --git a/x-pack/test/functional/apps/ml/settings/common.ts b/x-pack/test/functional/apps/ml/settings/common.ts new file mode 100644 index 00000000000000..9fada028ff3da8 --- /dev/null +++ b/x-pack/test/functional/apps/ml/settings/common.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export async function asyncForEach(array: T[], callback: (item: T, index: number) => void) { + for (let index = 0; index < array.length; index++) { + await callback(array[index], index); + } +} + +export const createJobConfig = (jobId: string) => ({ + job_id: jobId, + description: + 'mean/min/max(responsetime) partition=airline on farequote dataset with 1h bucket span', + groups: ['farequote', 'automated', 'multi-metric'], + analysis_config: { + bucket_span: '1h', + influencers: ['airline'], + detectors: [{ function: 'mean', field_name: 'responsetime', partition_field_name: 'airline' }], + }, + data_description: { time_field: '@timestamp' }, + analysis_limits: { model_memory_limit: '20mb' }, + model_plot_config: { enabled: false }, +}); diff --git a/x-pack/test/functional/apps/ml/settings/filter_list_creation.ts b/x-pack/test/functional/apps/ml/settings/filter_list_creation.ts new file mode 100644 index 00000000000000..22affa1cada38b --- /dev/null +++ b/x-pack/test/functional/apps/ml/settings/filter_list_creation.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const ml = getService('ml'); + const filterId = 'test_create_filter'; + const description = 'test description'; + const keywords = ['filter word 1', 'filter word 2', 'filter word 3']; + + describe('filter list creation', function () { + before(async () => { + await ml.testResources.setKibanaTimeZoneToUTC(); + await ml.securityUI.loginAsMlPowerUser(); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + // clean up created filters + await ml.api.deleteFilter(filterId); + }); + + it('creates new filter list', async () => { + await ml.testExecution.logTestStep( + 'filter list creation loads the filter list management page' + ); + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToSettings(); + await ml.settings.navigateToFilterListsManagement(); + + await ml.testExecution.logTestStep('filter list creation loads the filter creation page'); + await ml.settingsFilterList.navigateToFilterListCreationPage(); + + await ml.testExecution.logTestStep('filter list creation sets the list name and description'); + await ml.settingsFilterList.setFilterListId(filterId); + await ml.settingsFilterList.setFilterListDescription(description); + + await ml.testExecution.logTestStep('filter list creation adds items to the filter list'); + await ml.settingsFilterList.addFilterListKeywords(keywords); + await ml.testExecution.logTestStep('filter list creation saves the settings'); + await ml.settingsFilterList.saveFilterList(); + await ml.settingsFilterList.assertFilterListRowExists(filterId); + }); + }); +} diff --git a/x-pack/test/functional/apps/ml/settings/filter_list_delete.ts b/x-pack/test/functional/apps/ml/settings/filter_list_delete.ts new file mode 100644 index 00000000000000..9e30d2c8915d26 --- /dev/null +++ b/x-pack/test/functional/apps/ml/settings/filter_list_delete.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { asyncForEach } from './common'; + +export default function ({ getService }: FtrProviderContext) { + const ml = getService('ml'); + + const testDataList = [1, 2].map((n) => ({ + filterId: `test_delete_filter_${n}`, + description: `test description ${n}`, + items: ['filter word 1', 'filter word 2', 'filter word 3'], + })); + + describe('filter list delete', function () { + before(async () => { + await ml.testResources.setKibanaTimeZoneToUTC(); + await ml.securityUI.loginAsMlPowerUser(); + + for (let index = 0; index < testDataList.length; index++) { + const { filterId, description, items } = testDataList[index]; + + await ml.api.createFilter(filterId, { + description, + items, + }); + } + }); + + after(async () => { + await ml.api.cleanMlIndices(); + + // clean up created filters + await asyncForEach(testDataList, async ({ filterId }) => { + await ml.api.deleteFilter(filterId); + }); + }); + + it('deletes filter list with items', async () => { + await ml.testExecution.logTestStep( + 'filter list delete loads the filter list management page' + ); + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToSettings(); + await ml.settings.navigateToFilterListsManagement(); + + await ml.testExecution.logTestStep( + 'filter list delete selects list entries and deletes them' + ); + for (const testData of testDataList) { + const { filterId } = testData; + await ml.settingsFilterList.selectFilterListRow(filterId); + } + await ml.settingsFilterList.deleteFilterList(); + + await ml.testExecution.logTestStep( + 'filter list delete validates selected filter lists are deleted' + ); + await asyncForEach(testDataList, async ({ filterId }) => { + await ml.settingsFilterList.assertFilterListRowNotExists(filterId); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/ml/settings/filter_list_edit.ts b/x-pack/test/functional/apps/ml/settings/filter_list_edit.ts new file mode 100644 index 00000000000000..8c39c679ac6f2a --- /dev/null +++ b/x-pack/test/functional/apps/ml/settings/filter_list_edit.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { asyncForEach } from './common'; + +export default function ({ getService }: FtrProviderContext) { + const ml = getService('ml'); + + const filterId = 'test_filter_list_edit'; + const keywordToDelete = 'keyword_to_delete'; + const oldKeyword = 'old_keyword'; + const oldDescription = 'Old filter list description'; + + const newKeywords = ['new_keyword1', 'new_keyword2']; + const newDescription = 'New filter list description'; + + describe('filter list edit', function () { + before(async () => { + await ml.testResources.setKibanaTimeZoneToUTC(); + await ml.securityUI.loginAsMlPowerUser(); + + await ml.api.createFilter(filterId, { + description: oldDescription, + items: [keywordToDelete, oldKeyword], + }); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + await ml.api.deleteFilter(filterId); + }); + + it('updates description and filter items', async () => { + await ml.testExecution.logTestStep('filter list edit loads the filter list management page'); + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToSettings(); + await ml.settings.navigateToFilterListsManagement(); + + await ml.testExecution.logTestStep('filter list edit opens existing filter list'); + await ml.settingsFilterList.selectFilterListRowEditLink(filterId); + await ml.settingsFilterList.assertFilterItemExists(keywordToDelete); + await ml.settingsFilterList.assertFilterListDescriptionValue(oldDescription); + + await ml.testExecution.logTestStep('filter list edit deletes existing filter item'); + await ml.settingsFilterList.deleteFilterItem(keywordToDelete); + + await ml.testExecution.logTestStep('filter list edit sets new keywords and description'); + await ml.settingsFilterList.setFilterListDescription(newDescription); + await ml.settingsFilterList.addFilterListKeywords(newKeywords); + + await ml.testExecution.logTestStep( + 'filter list edit saves the new filter list and displays it in the list of entries' + ); + await ml.settingsFilterList.saveFilterList(); + await ml.settingsFilterList.assertFilterListRowExists(filterId); + + await ml.testExecution.logTestStep('filter list edit reopens the edited filter list'); + await ml.settingsFilterList.selectFilterListRowEditLink(filterId); + + await ml.testExecution.logTestStep( + 'filter list edit verifies the filter list description updated correctly' + ); + await ml.settingsFilterList.assertFilterListDescriptionValue(newDescription); + + await ml.testExecution.logTestStep( + 'filter list edit verifies the filter items updated correctly' + ); + await ml.settingsFilterList.assertFilterItemNotExists(keywordToDelete); + await asyncForEach([...newKeywords, oldKeyword], async (filterItem) => { + await ml.settingsFilterList.assertFilterItemExists(filterItem); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/ml/settings/index.ts b/x-pack/test/functional/apps/ml/settings/index.ts new file mode 100644 index 00000000000000..5b2c7d15e19595 --- /dev/null +++ b/x-pack/test/functional/apps/ml/settings/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('settings', function () { + this.tags(['quynh', 'skipFirefox']); + + loadTestFile(require.resolve('./calendar_creation')); + loadTestFile(require.resolve('./calendar_edit')); + loadTestFile(require.resolve('./calendar_delete')); + + loadTestFile(require.resolve('./filter_list_creation')); + loadTestFile(require.resolve('./filter_list_edit')); + loadTestFile(require.resolve('./filter_list_delete')); + }); +} diff --git a/x-pack/test/functional/services/ml/index.ts b/x-pack/test/functional/services/ml/index.ts index 325ea41ae3977b..50da8425e493d6 100644 --- a/x-pack/test/functional/services/ml/index.ts +++ b/x-pack/test/functional/services/ml/index.ts @@ -78,8 +78,8 @@ export function MachineLearningProvider(context: FtrProviderContext) { const securityCommon = MachineLearningSecurityCommonProvider(context); const securityUI = MachineLearningSecurityUIProvider(context, securityCommon); const settings = MachineLearningSettingsProvider(context); - const settingsCalendar = MachineLearningSettingsCalendarProvider(context); - const settingsFilterList = MachineLearningSettingsFilterListProvider(context); + const settingsCalendar = MachineLearningSettingsCalendarProvider(context, commonUI); + const settingsFilterList = MachineLearningSettingsFilterListProvider(context, commonUI); const singleMetricViewer = MachineLearningSingleMetricViewerProvider(context); const testExecution = MachineLearningTestExecutionProvider(context); const testResources = MachineLearningTestResourcesProvider(context); diff --git a/x-pack/test/functional/services/ml/security_ui.ts b/x-pack/test/functional/services/ml/security_ui.ts index e09467ff36a34c..da4324901d38e9 100644 --- a/x-pack/test/functional/services/ml/security_ui.ts +++ b/x-pack/test/functional/services/ml/security_ui.ts @@ -16,7 +16,6 @@ export function MachineLearningSecurityUIProvider( return { async loginAs(user: USER) { const password = mlSecurityCommon.getPasswordForUser(user); - await PageObjects.security.forceLogout(); await PageObjects.security.login(user, password, { diff --git a/x-pack/test/functional/services/ml/settings_calendar.ts b/x-pack/test/functional/services/ml/settings_calendar.ts index 34d18c6e12c474..c269636522923f 100644 --- a/x-pack/test/functional/services/ml/settings_calendar.ts +++ b/x-pack/test/functional/services/ml/settings_calendar.ts @@ -7,9 +7,15 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; +import { MlCommonUI } from './common_ui'; -export function MachineLearningSettingsCalendarProvider({ getService }: FtrProviderContext) { +export function MachineLearningSettingsCalendarProvider( + { getService }: FtrProviderContext, + mlCommonUI: MlCommonUI +) { const testSubjects = getService('testSubjects'); + const retry = getService('retry'); + const comboBox = getService('comboBox'); return { async parseCalendarTable() { @@ -172,6 +178,11 @@ export function MachineLearningSettingsCalendarProvider({ getService }: FtrProvi ); }, + calendarRowSelector(calendarId: string, subSelector?: string) { + const row = `~mlCalendarTable > ~row-${calendarId}`; + return !subSelector ? row : `${row} > ${subSelector}`; + }, + eventRowSelector(eventDescription: string, subSelector?: string) { const row = `~mlCalendarEventsTable > ~row-${eventDescription}`; return !subSelector ? row : `${row} > ${subSelector}`; @@ -181,6 +192,10 @@ export function MachineLearningSettingsCalendarProvider({ getService }: FtrProvi await testSubjects.existOrFail(this.eventRowSelector(eventDescription)); }, + async assertEventRowMissing(eventDescription: string) { + await testSubjects.missingOrFail(this.eventRowSelector(eventDescription)); + }, + async assertDeleteEventButtonEnabled(eventDescription: string, expectedValue: boolean) { const isEnabled = await testSubjects.isEnabled( this.eventRowSelector(eventDescription, 'mlCalendarEventDeleteButton') @@ -192,5 +207,191 @@ export function MachineLearningSettingsCalendarProvider({ getService }: FtrProvi }' (got '${isEnabled ? 'enabled' : 'disabled'}')` ); }, + + async assertCalendarRowExists(calendarId: string) { + await testSubjects.existOrFail(this.calendarRowSelector(calendarId)); + }, + + async assertCalendarRowNotExists(calendarId: string) { + await testSubjects.missingOrFail(this.calendarRowSelector(calendarId)); + }, + + async assertCalendarIdValue(expectedValue: string) { + const actualValue = await testSubjects.getAttribute('mlCalendarIdInput', 'value'); + expect(actualValue).to.eql( + expectedValue, + `Calendar id should be '${expectedValue}' (got '${actualValue}')` + ); + }, + + async setCalendarId(calendarId: string) { + await mlCommonUI.setValueWithChecks('mlCalendarIdInput', calendarId, { + clearWithKeyboard: true, + }); + await this.assertCalendarIdValue(calendarId); + }, + + async assertCalendarDescriptionValue(expectedValue: string) { + const actualValue = await testSubjects.getAttribute('mlCalendarDescriptionInput', 'value'); + expect(actualValue).to.eql( + expectedValue, + `Calendar description should be '${expectedValue}' (got '${actualValue}')` + ); + }, + + async setCalendarDescription(description: string) { + await mlCommonUI.setValueWithChecks('mlCalendarDescriptionInput', description, { + clearWithKeyboard: true, + }); + await this.assertCalendarDescriptionValue(description); + }, + + async getApplyToAllJobsSwitchCheckedState(): Promise { + const subj = 'mlCalendarApplyToAllJobsSwitch'; + const isSelected = await testSubjects.getAttribute(subj, 'aria-checked'); + return isSelected === 'true'; + }, + + async toggleApplyToAllJobsSwitch(toggle: boolean) { + const subj = 'mlCalendarApplyToAllJobsSwitch'; + if ((await this.getApplyToAllJobsSwitchCheckedState()) !== toggle) { + await retry.tryForTime(5 * 1000, async () => { + await testSubjects.clickWhenNotDisabled(subj); + await this.assertApplyToAllJobsSwitchEnabled(toggle); + }); + } + }, + + async saveCalendar() { + await testSubjects.existOrFail('mlSaveCalendarButton'); + await testSubjects.click('mlSaveCalendarButton'); + await testSubjects.existOrFail('mlPageCalendarManagement'); + }, + + async navigateToCalendarCreationPage() { + await testSubjects.existOrFail('mlCalendarButtonCreate'); + await testSubjects.click('mlCalendarButtonCreate'); + await testSubjects.existOrFail('mlPageCalendarEdit'); + }, + + async openNewCalendarEventForm() { + await testSubjects.existOrFail('mlCalendarNewEventButton'); + await testSubjects.click('mlCalendarNewEventButton'); + await testSubjects.existOrFail('mlPageCalendarEdit'); + }, + + async assertCalendarEventDescriptionValue(expectedValue: string) { + const actualValue = await testSubjects.getAttribute( + 'mlCalendarEventDescriptionInput', + 'value' + ); + expect(actualValue).to.eql( + expectedValue, + `Calendar event description should be '${expectedValue}' (got '${actualValue}')` + ); + }, + + async setCalendarEventDescription(eventDescription: string) { + await testSubjects.existOrFail('mlCalendarEventDescriptionInput'); + await mlCommonUI.setValueWithChecks('mlCalendarEventDescriptionInput', eventDescription, { + clearWithKeyboard: true, + }); + await this.assertCalendarEventDescriptionValue(eventDescription); + }, + + async cancelNewCalendarEvent() { + await testSubjects.existOrFail('mlCalendarCancelEventButton'); + await testSubjects.click('mlCalendarCancelEventButton'); + await testSubjects.missingOrFail('mlCalendarEventForm'); + }, + + async addNewCalendarEvent() { + await testSubjects.existOrFail('mlCalendarAddEventButton'); + await testSubjects.click('mlCalendarAddEventButton'); + await testSubjects.missingOrFail('mlCalendarEventForm'); + }, + + async assertJobSelectionExists() { + await testSubjects.existOrFail('mlCalendarJobSelection'); + }, + + async assertJobSelectionNotExists() { + await testSubjects.missingOrFail('mlCalendarJobSelection'); + }, + + async assertJobSelection(expectedIdentifier: string[]) { + const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( + 'mlCalendarJobSelection > comboBoxInput' + ); + expect(comboBoxSelectedOptions).to.eql( + expectedIdentifier, + `Expected job selection to be '${expectedIdentifier}' (got '${comboBoxSelectedOptions}')` + ); + }, + + async assertJobSelectionContain(expectedIdentifier: string) { + const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( + 'mlCalendarJobSelection > comboBoxInput' + ); + expect(comboBoxSelectedOptions).to.contain( + expectedIdentifier, + `Expected job selection to contain '${expectedIdentifier}' (got '${comboBoxSelectedOptions}')` + ); + }, + + async selectJob(identifier: string) { + await comboBox.set('mlCalendarJobSelection > comboBoxInput', identifier); + await this.assertJobSelectionContain(identifier); + }, + + async assertJobGroupSelectionExists() { + await testSubjects.existOrFail('mlCalendarJobGroupSelection'); + }, + + async assertJobGroupSelectionNotExists() { + await testSubjects.missingOrFail('mlCalendarJobGroupSelection'); + }, + + async assertJobGroupSelection(expectedIdentifier: string[]) { + const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( + 'mlCalendarJobGroupSelection > comboBoxInput' + ); + expect(comboBoxSelectedOptions).to.eql( + expectedIdentifier, + `Expected job group selection to be'${expectedIdentifier}' (got '${comboBoxSelectedOptions}')` + ); + }, + + async assertJobGroupSelectionContain(expectedIdentifier: string) { + const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( + 'mlCalendarJobGroupSelection > comboBoxInput' + ); + expect(comboBoxSelectedOptions).to.contain( + expectedIdentifier, + `Expected job group selection to contain'${expectedIdentifier}' (got '${comboBoxSelectedOptions}')` + ); + }, + + async selectJobGroup(identifier: string) { + await comboBox.set('mlCalendarJobGroupSelection > comboBoxInput', identifier); + await this.assertJobGroupSelectionContain(identifier); + }, + + async deleteCalendarEventRow(eventDescription: string) { + await this.assertEventRowExists(eventDescription); + await testSubjects.click( + this.eventRowSelector(eventDescription, 'mlCalendarEventDeleteButton') + ); + await this.assertEventRowMissing(eventDescription); + }, + + async deleteCalendar() { + await this.assertDeleteCalendarButtonEnabled(true); + await testSubjects.click('mlCalendarButtonDelete'); + await testSubjects.existOrFail('mlCalendarDeleteConfirmation'); + await testSubjects.existOrFail('confirmModalConfirmButton'); + await testSubjects.click('confirmModalConfirmButton'); + await testSubjects.missingOrFail('mlCalendarDeleteConfirmation'); + }, }; } diff --git a/x-pack/test/functional/services/ml/settings_filter_list.ts b/x-pack/test/functional/services/ml/settings_filter_list.ts index 0afe9f21b03a69..bcac575b65c08e 100644 --- a/x-pack/test/functional/services/ml/settings_filter_list.ts +++ b/x-pack/test/functional/services/ml/settings_filter_list.ts @@ -7,9 +7,14 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; +import { MlCommonUI } from './common_ui'; -export function MachineLearningSettingsFilterListProvider({ getService }: FtrProviderContext) { +export function MachineLearningSettingsFilterListProvider( + { getService }: FtrProviderContext, + mlCommonUI: MlCommonUI +) { const testSubjects = getService('testSubjects'); + const browser = getService('browser'); return { async parseFilterListTable() { @@ -17,7 +22,7 @@ export function MachineLearningSettingsFilterListProvider({ getService }: FtrPro const $ = await table.parseDomContent(); const rows = []; - for (const tr of $.findTestSubjects('~mlFilterListsRow').toArray()) { + for (const tr of $.findTestSubjects('~mlFilterListRow').toArray()) { const $tr = $(tr); const inUseSubject = $tr @@ -55,6 +60,14 @@ export function MachineLearningSettingsFilterListProvider({ getService }: FtrPro return !subSelector ? row : `${row} > ${subSelector}`; }, + async assertFilterListRowExists(filterId: string) { + return await testSubjects.existOrFail(this.rowSelector(filterId)); + }, + + async assertFilterListRowNotExists(filterId: string) { + return await testSubjects.missingOrFail(this.rowSelector(filterId)); + }, + async filterWithSearchString(filter: string, expectedRowCount: number = 1) { const tableListContainer = await testSubjects.find('mlFilterListTableContainer'); const searchBarInput = await tableListContainer.findByClassName('euiFieldSearch'); @@ -101,6 +114,12 @@ export function MachineLearningSettingsFilterListProvider({ getService }: FtrPro await this.assertFilterListRowSelected(filterId, false); }, + async selectFilterListRowEditLink(filterId: string) { + await this.assertFilterListRowExists(filterId); + await testSubjects.click(this.rowSelector(filterId, `mlEditFilterListLink`)); + await testSubjects.existOrFail('mlPageFilterListEdit'); + }, + async assertCreateFilterListButtonEnabled(expectedValue: boolean) { const isEnabled = await testSubjects.isEnabled('mlFilterListsButtonCreate'); expect(isEnabled).to.eql( @@ -111,6 +130,10 @@ export function MachineLearningSettingsFilterListProvider({ getService }: FtrPro ); }, + async assertDeleteFilterListButtonExists() { + await testSubjects.existOrFail('mlFilterListsDeleteButton'); + }, + async assertDeleteFilterListButtonEnabled(expectedValue: boolean) { const isEnabled = await testSubjects.isEnabled('mlFilterListsDeleteButton'); expect(isEnabled).to.eql( @@ -121,6 +144,16 @@ export function MachineLearningSettingsFilterListProvider({ getService }: FtrPro ); }, + async deleteFilterList() { + await this.assertDeleteFilterListButtonExists(); + await this.assertDeleteFilterListButtonEnabled(true); + await testSubjects.click('mlFilterListsDeleteButton'); + await testSubjects.existOrFail('mlFilterListsDeleteButton'); + await testSubjects.existOrFail('mlFilterListDeleteConfirmation'); + await testSubjects.click('confirmModalConfirmButton'); + await testSubjects.missingOrFail('mlFilterListDeleteConfirmation'); + }, + async openFilterListEditForm(filterId: string) { await testSubjects.click(this.rowSelector(filterId, 'mlEditFilterListLink')); await testSubjects.existOrFail('mlPageFilterListEdit'); @@ -136,8 +169,8 @@ export function MachineLearningSettingsFilterListProvider({ getService }: FtrPro ); }, - async assertAddItemButtonEnabled(expectedValue: boolean) { - const isEnabled = await testSubjects.isEnabled('mlFilterListAddItemButton'); + async assertOpenNewItemsPopoverButtonEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled('mlFilterListOpenNewItemsPopoverButton'); expect(isEnabled).to.eql( expectedValue, `Expected "add item" button to be '${expectedValue ? 'enabled' : 'disabled'}' (got '${ @@ -146,6 +179,16 @@ export function MachineLearningSettingsFilterListProvider({ getService }: FtrPro ); }, + async assertAddItemsButtonEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled('mlFilterListOpenNewItemsPopoverButton'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "add" button to be '${expectedValue ? 'enabled' : 'disabled'}' (got '${ + isEnabled ? 'enabled' : 'disabled' + }')` + ); + }, + async assertDeleteItemButtonEnabled(expectedValue: boolean) { const isEnabled = await testSubjects.isEnabled('mlFilterListDeleteItemButton'); expect(isEnabled).to.eql( @@ -156,11 +199,25 @@ export function MachineLearningSettingsFilterListProvider({ getService }: FtrPro ); }, + async assertSaveFilterListButtonEnabled(expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled('mlFilterListSaveButton'); + expect(isEnabled).to.eql( + expectedValue, + `Expected "save filter list" button to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + }, + filterItemSelector(filterItem: string, subSelector?: string) { const row = `mlGridItem ${filterItem}`; return !subSelector ? row : `${row} > ${subSelector}`; }, + async assertFilterItemNotExists(filterItem: string) { + await testSubjects.missingOrFail(this.filterItemSelector(filterItem)); + }, + async assertFilterItemExists(filterItem: string) { await testSubjects.existOrFail(this.filterItemSelector(filterItem)); }, @@ -189,6 +246,13 @@ export function MachineLearningSettingsFilterListProvider({ getService }: FtrPro await this.assertFilterItemSelected(filterItem, true); }, + async deleteFilterItem(filterItem: string) { + await testSubjects.existOrFail('mlFilterListDeleteItemButton'); + await this.selectFilterItem(filterItem); + await testSubjects.click('mlFilterListDeleteItemButton'); + await this.assertFilterItemNotExists(filterItem); + }, + async deselectFilterItem(filterItem: string) { if ((await this.isFilterItemSelected(filterItem)) === true) { await testSubjects.click(this.filterItemSelector(filterItem)); @@ -196,5 +260,70 @@ export function MachineLearningSettingsFilterListProvider({ getService }: FtrPro await this.assertFilterItemSelected(filterItem, false); }, + + async navigateToFilterListCreationPage() { + await this.assertCreateFilterListButtonEnabled(true); + await testSubjects.click('mlFilterListsButtonCreate'); + await testSubjects.existOrFail('mlPageFilterListEdit'); + }, + + async assertFilterListIdValue(expectedValue: string) { + const subj = 'mlNewFilterListIdInput'; + const actualFilterListId = await testSubjects.getAttribute(subj, 'value'); + expect(actualFilterListId).to.eql( + expectedValue, + `Filter list id should be '${expectedValue}' (got '${actualFilterListId}')` + ); + }, + + async setFilterListId(filterId: string) { + const subj = 'mlNewFilterListIdInput'; + await mlCommonUI.setValueWithChecks(subj, filterId, { + clearWithKeyboard: true, + }); + await this.assertFilterListIdValue(filterId); + }, + + async setFilterListDescription(description: string) { + await this.assertEditDescriptionButtonEnabled(true); + await testSubjects.click('mlFilterListEditDescriptionButton'); + await testSubjects.existOrFail('mlFilterListDescriptionInput'); + await mlCommonUI.setValueWithChecks('mlFilterListDescriptionInput', description, { + clearWithKeyboard: true, + }); + await browser.pressKeys(browser.keys.ESCAPE); + await this.assertFilterListDescriptionValue(description); + }, + + async addFilterListKeywords(keywords: string[]) { + await this.assertOpenNewItemsPopoverButtonEnabled(true); + await testSubjects.click('mlFilterListOpenNewItemsPopoverButton'); + await mlCommonUI.setValueWithChecks('mlFilterListAddItemTextArea', keywords.join('\n'), { + clearWithKeyboard: true, + }); + await testSubjects.existOrFail('mlFilterListAddItemsButton'); + await this.assertAddItemsButtonEnabled(true); + await testSubjects.click('mlFilterListAddItemsButton'); + + for (let index = 0; index < keywords.length; index++) { + await this.assertFilterItemExists(keywords[index]); + } + }, + + async assertFilterListDescriptionValue(expectedDescription: string) { + const actualFilterListDescription = await testSubjects.getVisibleText( + 'mlNewFilterListDescriptionText' + ); + expect(actualFilterListDescription).to.eql( + expectedDescription, + `Filter list description should be '${expectedDescription}' (got '${actualFilterListDescription}')` + ); + }, + + async saveFilterList() { + await this.assertSaveFilterListButtonEnabled(true); + await testSubjects.click('mlFilterListSaveButton'); + await testSubjects.existOrFail('mlPageFilterListManagement'); + }, }; }