From f77ff2d396da04d9ef1bfdbed71c63051d2edd89 Mon Sep 17 00:00:00 2001 From: Pete Harverson Date: Wed, 26 May 2021 17:48:03 +0100 Subject: [PATCH] [ML] Adds functional tests for anomaly detection job custom URLs (#100455) * [ML] Adds functional tests for anomaly detection job custom URLs * [ML] Remove debug test tag from custom URL tests * [ML] Update custom URL editor Jest snapshots * [ML] Clean up in embeddables tests to fix dashboard test * [ML] Delete test dashboard after test suites complete * [ML] Edits to custom URL tests following review Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../__snapshots__/editor.test.tsx.snap | 28 +++ .../__snapshots__/list.test.tsx.snap | 9 + .../components/custom_url_editor/editor.tsx | 9 +- .../components/custom_url_editor/list.tsx | 3 + .../edit_job_flyout/edit_job_flyout.js | 7 + .../apps/ml/anomaly_detection/advanced_job.ts | 2 +- .../ml/anomaly_detection/anomaly_explorer.ts | 4 + .../anomaly_detection/categorization_job.ts | 2 +- .../apps/ml/anomaly_detection/custom_urls.ts | 188 ++++++++++++++++ .../apps/ml/anomaly_detection/index.ts | 1 + .../ml/anomaly_detection/multi_metric_job.ts | 2 +- .../ml/anomaly_detection/population_job.ts | 2 +- .../ml/anomaly_detection/single_metric_job.ts | 2 +- .../test/functional/services/ml/common_ui.ts | 19 ++ .../functional/services/ml/custom_urls.ts | 146 ++++++++++++- x-pack/test/functional/services/ml/index.ts | 2 +- .../test/functional/services/ml/job_table.ts | 204 +++++++++++++++++- .../services/ml/job_wizard_common.ts | 2 +- .../functional/services/ml/test_resources.ts | 4 + 19 files changed, 621 insertions(+), 15 deletions(-) create mode 100644 x-pack/test/functional/apps/ml/anomaly_detection/custom_urls.ts diff --git a/x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/__snapshots__/editor.test.tsx.snap b/x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/__snapshots__/editor.test.tsx.snap index 7d5c73b42f15..d14fc6df0669 100644 --- a/x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/__snapshots__/editor.test.tsx.snap +++ b/x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/__snapshots__/editor.test.tsx.snap @@ -18,6 +18,7 @@ exports[`CustomUrlEditor renders the editor for a dashboard type URL with a labe /> = ({ - + @@ -239,6 +239,7 @@ export const CustomUrlEditor: FC = ({ idSelected={type} onChange={onTypeChange} className="url-link-to-radio" + data-test-subj="mlJobCustomUrlLinkToTypeInput" /> @@ -256,6 +257,7 @@ export const CustomUrlEditor: FC = ({ options={dashboardOptions} value={kibanaSettings.dashboardId} onChange={onDashboardChange} + data-test-subj="mlJobCustomUrlDashboardNameInput" compressed /> @@ -275,6 +277,7 @@ export const CustomUrlEditor: FC = ({ options={indexPatternOptions} value={kibanaSettings.discoverIndexPatternId} onChange={onDiscoverIndexPatternChange} + data-test-subj="mlJobCustomUrlDiscoverIndexPatternInput" compressed /> @@ -298,6 +301,7 @@ export const CustomUrlEditor: FC = ({ selectedOptions={selectedEntityOptions} onChange={onQueryEntitiesChange} isClearable={true} + data-test-subj="mlJobCustomUrlQueryEntitiesInput" /> )} @@ -321,6 +325,7 @@ export const CustomUrlEditor: FC = ({ options={timeRangeOptions} value={timeRange.type} onChange={onTimeRangeTypeChange} + data-test-subj="mlJobCustomUrlTimeRangeInput" compressed /> @@ -343,6 +348,7 @@ export const CustomUrlEditor: FC = ({ value={timeRange.interval} onChange={onTimeRangeIntervalChange} isInvalid={isInvalidTimeRange} + data-test-subj="mlJobCustomUrlTimeRangeIntervalInput" compressed /> @@ -365,6 +371,7 @@ export const CustomUrlEditor: FC = ({ rows={2} value={otherUrlSettings.urlValue} onChange={onOtherUrlValueChange} + data-test-subj="mlJobCustomUrlOtherTypeUrlInput" compressed /> diff --git a/x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/list.tsx b/x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/list.tsx index 5c61de38df37..96b09aff64f0 100644 --- a/x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/list.tsx +++ b/x-pack/plugins/ml/public/application/jobs/components/custom_url_editor/list.tsx @@ -160,6 +160,7 @@ export const CustomUrlList: FC = ({ job, customUrls, setCust } isInvalid={isInvalidLabel} error={invalidLabelError} + data-test-subj="mlJobEditCustomUrlItemLabel" > = ({ job, customUrls, setCust aria-label={i18n.translate('xpack.ml.customUrlEditorList.testCustomUrlAriaLabel', { defaultMessage: 'Test custom URL', })} + data-test-subj="mlJobEditTestCustomUrlButton" /> @@ -264,6 +266,7 @@ export const CustomUrlList: FC = ({ job, customUrls, setCust defaultMessage: 'Delete custom URL', } )} + data-test-subj={`mlJobEditDeleteCustomUrlButton_${index}`} /> diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js index 758e3fa472a0..d7f42daf5f3c 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js @@ -326,6 +326,7 @@ export class EditJobFlyoutUI extends Component { const tabs = [ { id: 'job-details', + 'data-test-subj': 'mlEditJobFlyout-jobDetails', name: i18n.translate('xpack.ml.jobsList.editJobFlyout.jobDetailsTitle', { defaultMessage: 'Job details', }), @@ -346,6 +347,7 @@ export class EditJobFlyoutUI extends Component { }, { id: 'detectors', + 'data-test-subj': 'mlEditJobFlyout-detectors', name: i18n.translate('xpack.ml.jobsList.editJobFlyout.detectorsTitle', { defaultMessage: 'Detectors', }), @@ -359,6 +361,7 @@ export class EditJobFlyoutUI extends Component { }, { id: 'datafeed', + 'data-test-subj': 'mlEditJobFlyout-datafeed', name: i18n.translate('xpack.ml.jobsList.editJobFlyout.datafeedTitle', { defaultMessage: 'Datafeed', }), @@ -376,6 +379,7 @@ export class EditJobFlyoutUI extends Component { }, { id: 'custom-urls', + 'data-test-subj': 'mlEditJobFlyout-customUrls', name: i18n.translate('xpack.ml.jobsList.editJobFlyout.customUrlsTitle', { defaultMessage: 'Custom URLs', }), @@ -395,6 +399,7 @@ export class EditJobFlyoutUI extends Component { this.closeFlyout(); }} size="m" + data-test-subj="mlJobEditFlyout" > @@ -419,6 +424,7 @@ export class EditJobFlyoutUI extends Component { this.closeFlyout(); }} flush="left" + data-test-subj="mlEditJobFlyoutCloseButton" > { + await ml.testResources.deleteMLTestDashboard(); + }); + for (const testData of testDataList) { describe(testData.suiteSuffix, function () { before(async () => { diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/categorization_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/categorization_job.ts index 611c7be8b067..85eeacc58514 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/categorization_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/categorization_job.ts @@ -278,7 +278,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.jobWizardCommon.ensureAdditionalSettingsSectionOpen(); await ml.testExecution.logTestStep('job cloning persists custom urls'); - await ml.customUrls.assertCustomUrlItem(0, 'check-kibana-dashboard'); + await ml.customUrls.assertCustomUrlLabel(0, 'check-kibana-dashboard'); await ml.testExecution.logTestStep('job cloning persists assigned calendars'); await ml.jobWizardCommon.assertCalendarsSelection([calendarId]); diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/custom_urls.ts b/x-pack/test/functional/apps/ml/anomaly_detection/custom_urls.ts new file mode 100644 index 000000000000..a743e00b64ad --- /dev/null +++ b/x-pack/test/functional/apps/ml/anomaly_detection/custom_urls.ts @@ -0,0 +1,188 @@ +/* + * 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. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { Job, Datafeed } from '../../../../../plugins/ml/common/types/anomaly_detection_jobs'; +import { + TimeRangeType, + TIME_RANGE_TYPE, +} from '../../../../../plugins/ml/public/application/jobs/components/custom_url_editor/constants'; + +interface DiscoverUrlConfig { + label: string; + indexPattern: string; + queryEntityFieldNames: string[]; + timeRange: TimeRangeType; + timeRangeInterval?: string; +} + +interface DashboardUrlConfig { + label: string; + dashboardName: string; + queryEntityFieldNames: string[]; + timeRange: TimeRangeType; + timeRangeInterval?: string; +} + +interface OtherUrlConfig { + label: string; + url: string; +} + +// @ts-expect-error doesn't implement the full interface +const JOB_CONFIG: Job = { + job_id: `fq_multi_1_custom_urls`, + description: 'mean(responsetime) partition=airline on farequote dataset with 30m bucket span', + groups: ['farequote', 'automated', 'multi-metric'], + analysis_config: { + bucket_span: '30m', + 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: true }, +}; + +// @ts-expect-error doesn't implement the full interface +const DATAFEED_CONFIG: Datafeed = { + datafeed_id: 'datafeed-fq_multi_1_custom_urls', + indices: ['ft_farequote'], + job_id: 'fq_multi_1_custom_urls', + query: { bool: { must: [{ match_all: {} }] } }, +}; + +const testDiscoverCustomUrl: DiscoverUrlConfig = { + label: 'Show data', + indexPattern: 'ft_farequote', + queryEntityFieldNames: ['airline'], + timeRange: TIME_RANGE_TYPE.AUTO, +}; + +const testDashboardCustomUrl: DashboardUrlConfig = { + label: 'Show dashboard', + dashboardName: 'ML Test', + queryEntityFieldNames: [], + timeRange: TIME_RANGE_TYPE.INTERVAL, + timeRangeInterval: '1h', +}; + +const testOtherCustomUrl: OtherUrlConfig = { + label: 'elastic.co', + url: 'https://www.elastic.co/', +}; + +export default function ({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const ml = getService('ml'); + const browser = getService('browser'); + + describe('custom urls', function () { + this.tags(['mlqa']); + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); + await ml.testResources.createMLTestDashboardIfNeeded(); + await ml.testResources.setKibanaTimeZoneToUTC(); + + await ml.api.createAndRunAnomalyDetectionLookbackJob(JOB_CONFIG, DATAFEED_CONFIG); + await ml.securityUI.loginAsMlPowerUser(); + }); + + after(async () => { + await ml.testResources.deleteMLTestDashboard(); + await ml.api.cleanMlIndices(); + }); + + it('opens the custom URLs tab in the edit job flyout', async () => { + await ml.testExecution.logTestStep('load the job management page'); + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToJobManagement(); + + await ml.testExecution.logTestStep('open the custom URLs tab in the edit job flyout'); + await ml.jobTable.openEditCustomUrlsForJobTab(JOB_CONFIG.job_id); + await ml.jobTable.closeEditJobFlyout(); + }); + + it('adds a custom URL with query entities to Discover in the edit job flyout', async () => { + await ml.jobTable.addDiscoverCustomUrl(JOB_CONFIG.job_id, testDiscoverCustomUrl); + }); + + it('adds a custom URL to Dashboard in the edit job flyout', async () => { + await ml.jobTable.addDashboardCustomUrl(JOB_CONFIG.job_id, testDashboardCustomUrl); + }); + + it('adds a custom URL to an external page in the edit job flyout', async () => { + await ml.jobTable.addOtherTypeCustomUrl(JOB_CONFIG.job_id, testOtherCustomUrl); + }); + + it('tests other type custom URL', async () => { + await ml.jobTable.testOtherTypeCustomUrlAction(JOB_CONFIG.job_id, 2, testOtherCustomUrl.url); + }); + + it('edits other type custom URL', async () => { + const edit = { + label: `${testOtherCustomUrl.url} edited`, + url: `${testOtherCustomUrl.url}guide/index.html`, + }; + await ml.testExecution.logTestStep('edit the custom URL in the edit job flyout'); + await ml.jobTable.editCustomUrl(JOB_CONFIG.job_id, 2, edit); + + await ml.testExecution.logTestStep('tests custom URL edit has been applied'); + await ml.jobTable.testOtherTypeCustomUrlAction(JOB_CONFIG.job_id, 2, edit.url); + await ml.jobTable.closeEditJobFlyout(); + }); + + it('deletes a custom URL', async () => { + await ml.jobTable.deleteCustomUrl(JOB_CONFIG.job_id, 2); + }); + + // wrapping into own describe to make sure new tab is cleaned up even if test failed + // see: https://github.com/elastic/kibana/pull/67280#discussion_r430528122 + describe('tests Discover type custom URL', () => { + let tabsCount = 1; + const docCountFormatted = '268'; + + it('opens Discover page from test link in the edit job flyout', async () => { + await ml.jobTable.openTestCustomUrl(JOB_CONFIG.job_id, 0); + await browser.switchTab(1); + tabsCount++; + await ml.jobTable.testDiscoverCustomUrlAction(docCountFormatted); + }); + + after(async () => { + if (tabsCount > 1) { + await browser.closeCurrentWindow(); + await browser.switchTab(0); + await ml.jobTable.closeEditJobFlyout(); + } + }); + }); + + // wrapping into own describe to make sure new tab is cleaned up even if test failed + // see: https://github.com/elastic/kibana/pull/67280#discussion_r430528122 + describe('tests Dashboard type custom URL', () => { + let tabsCount = 1; + const testDashboardPanelCount = 0; // ML Test dashboard has no content. + + it('opens Dashboard page from test link in the edit job flyout', async () => { + await ml.jobTable.openTestCustomUrl(JOB_CONFIG.job_id, 1); + await browser.switchTab(1); + tabsCount++; + await ml.jobTable.testDashboardCustomUrlAction(testDashboardPanelCount); + }); + + after(async () => { + if (tabsCount > 1) { + await browser.closeCurrentWindow(); + await browser.switchTab(0); + await ml.jobTable.closeEditJobFlyout(); + } + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/index.ts b/x-pack/test/functional/apps/ml/anomaly_detection/index.ts index 6b7afacbb721..d87da8469db1 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/index.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/index.ts @@ -23,5 +23,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./date_nanos_job')); loadTestFile(require.resolve('./annotations')); loadTestFile(require.resolve('./aggregated_scripted_job')); + loadTestFile(require.resolve('./custom_urls')); }); } diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/multi_metric_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/multi_metric_job.ts index b12eff71d825..256f9da313e4 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/multi_metric_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/multi_metric_job.ts @@ -299,7 +299,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.jobWizardCommon.ensureAdditionalSettingsSectionOpen(); await ml.testExecution.logTestStep('job cloning persists custom urls'); - await ml.customUrls.assertCustomUrlItem(0, 'check-kibana-dashboard'); + await ml.customUrls.assertCustomUrlLabel(0, 'check-kibana-dashboard'); await ml.testExecution.logTestStep('job cloning persists assigned calendars'); await ml.jobWizardCommon.assertCalendarsSelection([calendarId]); diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/population_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/population_job.ts index 8fea197e0566..2bdda2c81c71 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/population_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/population_job.ts @@ -336,7 +336,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.jobWizardCommon.ensureAdditionalSettingsSectionOpen(); await ml.testExecution.logTestStep('job cloning persists custom urls'); - await ml.customUrls.assertCustomUrlItem(0, 'check-kibana-dashboard'); + await ml.customUrls.assertCustomUrlLabel(0, 'check-kibana-dashboard'); await ml.testExecution.logTestStep('job cloning persists assigned calendars'); await ml.jobWizardCommon.assertCalendarsSelection([calendarId]); diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job.ts index 4f9b265c3b1e..eedb130215f7 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job.ts @@ -262,7 +262,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.jobWizardCommon.ensureAdditionalSettingsSectionOpen(); await ml.testExecution.logTestStep('job cloning persists custom urls'); - await ml.customUrls.assertCustomUrlItem(0, 'check-kibana-dashboard'); + await ml.customUrls.assertCustomUrlLabel(0, 'check-kibana-dashboard'); await ml.testExecution.logTestStep('job cloning persists assigned calendars'); await ml.jobWizardCommon.assertCalendarsSelection([calendarId]); diff --git a/x-pack/test/functional/services/ml/common_ui.ts b/x-pack/test/functional/services/ml/common_ui.ts index b7288d5927b4..b61bc8871482 100644 --- a/x-pack/test/functional/services/ml/common_ui.ts +++ b/x-pack/test/functional/services/ml/common_ui.ts @@ -123,6 +123,25 @@ export function MachineLearningCommonUIProvider({ getService }: FtrProviderConte await this.assertRadioGroupValue(testSubject, value); }, + async assertSelectSelectedOptionVisibleText(testSubject: string, visibleText: string) { + // Need to validate the selected option text, as the option value may be different to the visible text. + const selectControl = await testSubjects.find(testSubject); + const selectedValue = await selectControl.getAttribute('value'); + const selectedOption = await selectControl.findByCssSelector(`[value="${selectedValue}"]`); + const selectedOptionText = await selectedOption.getVisibleText(); + expect(selectedOptionText).to.eql( + visibleText, + `Expected selected option visible text to be '${visibleText}' (got '${selectedOptionText}')` + ); + }, + + async selectSelectValueByVisibleText(testSubject: string, visibleText: string) { + // Cannot use await testSubjects.selectValue as the option value may be different to the text. + const selectControl = await testSubjects.find(testSubject); + await selectControl.type(visibleText); + await this.assertSelectSelectedOptionVisibleText(testSubject, visibleText); + }, + async setMultiSelectFilter(testDataSubj: string, fieldTypes: string[]) { await testSubjects.clickWhenNotDisabled(`${testDataSubj}-button`); await testSubjects.existOrFail(`${testDataSubj}-popover`); diff --git a/x-pack/test/functional/services/ml/custom_urls.ts b/x-pack/test/functional/services/ml/custom_urls.ts index 0b24c565b2fa..67640eff7129 100644 --- a/x-pack/test/functional/services/ml/custom_urls.ts +++ b/x-pack/test/functional/services/ml/custom_urls.ts @@ -12,10 +12,25 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export type MlCustomUrls = ProvidedType; -export function MachineLearningCustomUrlsProvider({ getService }: FtrProviderContext) { +export function MachineLearningCustomUrlsProvider({ + getService, + getPageObjects, +}: FtrProviderContext) { const testSubjects = getService('testSubjects'); + const retry = getService('retry'); + const comboBox = getService('comboBox'); + const PageObjects = getPageObjects(['dashboard', 'discover', 'header']); return { + async assertCustomUrlsLength(expectedLength: number) { + const customUrls = await testSubjects.findAll('mlJobEditCustomUrlItemLabel'); + const actualLength = customUrls.length; + expect(expectedLength).to.eql( + actualLength, + `Expected number of custom urls to be '${expectedLength}' (got '${actualLength}')` + ); + }, + async assertCustomUrlLabelValue(expectedValue: string) { const actualCustomUrlLabel = await testSubjects.getAttribute( 'mlJobCustomUrlLabelInput', @@ -27,15 +42,68 @@ export function MachineLearningCustomUrlsProvider({ getService }: FtrProviderCon ); }, - async setCustomUrlLabel(customUrlsLabel: string) { - await testSubjects.setValue('mlJobCustomUrlLabelInput', customUrlsLabel, { + async setCustomUrlLabel(customUrlLabel: string) { + await testSubjects.setValue('mlJobCustomUrlLabelInput', customUrlLabel, { clearWithKeyboard: true, }); - await this.assertCustomUrlLabelValue(customUrlsLabel); + await this.assertCustomUrlLabelValue(customUrlLabel); }, - async assertCustomUrlItem(index: number, expectedLabel: string) { - await testSubjects.existOrFail(`mlJobEditCustomUrlItem_${index}`); + async assertCustomUrlQueryEntitySelection(expectedFieldNames: string[]) { + const actualFieldNames = await comboBox.getComboBoxSelectedOptions( + 'mlJobCustomUrlQueryEntitiesInput > comboBoxInput' + ); + expect(actualFieldNames).to.eql( + expectedFieldNames, + `Expected query entity selection to be '${expectedFieldNames}' (got '${actualFieldNames}')` + ); + }, + + async setCustomUrlQueryEntityFieldNames(fieldNames: string[]) { + for (const fieldName of fieldNames) { + await comboBox.set('mlJobCustomUrlQueryEntitiesInput > comboBoxInput', fieldName); + } + await this.assertCustomUrlQueryEntitySelection(fieldNames); + }, + + async assertCustomUrlTimeRangeIntervalValue(expectedInterval: string) { + const actualCustomUrlTimeRangeInterval = await testSubjects.getAttribute( + 'mlJobCustomUrlTimeRangeIntervalInput', + 'value' + ); + expect(actualCustomUrlTimeRangeInterval).to.eql( + expectedInterval, + `Expected custom url time range interval to be '${expectedInterval}' (got '${actualCustomUrlTimeRangeInterval}')` + ); + }, + + async setCustomUrlTimeRangeInterval(interval: string) { + await testSubjects.setValue('mlJobCustomUrlTimeRangeIntervalInput', interval, { + clearWithKeyboard: true, + }); + await this.assertCustomUrlTimeRangeIntervalValue(interval); + }, + + async assertCustomUrlOtherTypeUrlValue(expectedUrl: string) { + const actualCustomUrlValue = await testSubjects.getAttribute( + 'mlJobCustomUrlOtherTypeUrlInput', + 'value' + ); + expect(actualCustomUrlValue).to.eql( + expectedUrl, + `Expected other type custom url value to be '${expectedUrl}' (got '${actualCustomUrlValue}')` + ); + }, + + async setCustomUrlOtherTypeUrl(url: string) { + await testSubjects.setValue('mlJobCustomUrlOtherTypeUrlInput', url, { + clearWithKeyboard: true, + }); + await this.assertCustomUrlOtherTypeUrlValue(url); + }, + + async assertCustomUrlLabel(index: number, expectedLabel: string) { + await testSubjects.existOrFail(`mlJobEditCustomUrlLabelInput_${index}`); const actualLabel = await testSubjects.getAttribute( `mlJobEditCustomUrlLabelInput_${index}`, 'value' @@ -46,6 +114,44 @@ export function MachineLearningCustomUrlsProvider({ getService }: FtrProviderCon ); }, + async assertCustomUrlUrlValue(index: number, expectedUrl: string) { + await testSubjects.existOrFail(`mlJobEditCustomUrlInput_${index}`); + const actualUrl = await testSubjects.getAttribute( + `mlJobEditCustomUrlInput_${index}`, + 'value' + ); + expect(actualUrl).to.eql( + expectedUrl, + `Expected custom url item to be '${expectedUrl}' (got '${actualUrl}')` + ); + }, + + async editCustomUrlLabel(index: number, label: string) { + await testSubjects.existOrFail(`mlJobEditCustomUrlLabelInput_${index}`); + await testSubjects.setValue(`mlJobEditCustomUrlLabelInput_${index}`, label, { + clearWithKeyboard: true, + }); + await this.assertCustomUrlLabel(index, label); + }, + + async editCustomUrlUrlValue(index: number, urlValue: string) { + await testSubjects.existOrFail(`mlJobEditCustomUrlInput_${index}`); + await testSubjects.setValue(`mlJobEditCustomUrlInput_${index}`, urlValue, { + clearWithKeyboard: true, + }); + + // Click away, so the textarea reverts back to the standard input. + await testSubjects.click(`mlJobEditCustomUrlLabelInput_${index}`); + await this.assertCustomUrlUrlValue(index, urlValue); + }, + + async deleteCustomUrl(index: number) { + await testSubjects.existOrFail(`mlJobEditDeleteCustomUrlButton_${index}`); + const beforeCustomUrls = await testSubjects.findAll('mlJobEditCustomUrlItemLabel'); + await testSubjects.click(`mlJobEditDeleteCustomUrlButton_${index}`); + await this.assertCustomUrlsLength(beforeCustomUrls.length - 1); + }, + /** * Submits the custom url form and adds it to the list. * @param formContainerSelector - selector for the element that wraps the custom url creation form. @@ -54,5 +160,33 @@ export function MachineLearningCustomUrlsProvider({ getService }: FtrProviderCon await testSubjects.click('mlJobAddCustomUrl'); await testSubjects.missingOrFail(formContainerSelector, { timeout: 10 * 1000 }); }, + + async clickTestCustomUrl(index: number) { + await testSubjects.existOrFail(`mlJobEditCustomUrlItem_${index}`); + await testSubjects.click(`mlJobEditCustomUrlItem_${index} > mlJobEditTestCustomUrlButton`); + await PageObjects.header.waitUntilLoadingHasFinished(); + }, + + async assertDiscoverCustomUrlAction(expectedHitCountFormatted: string) { + await PageObjects.discover.waitForDiscoverAppOnScreen(); + await retry.tryForTime(5000, async () => { + const hitCount = await PageObjects.discover.getHitCount(); + expect(hitCount).to.eql( + expectedHitCountFormatted, + `Expected Discover hit count to be '${expectedHitCountFormatted}' (got '${hitCount}')` + ); + }); + }, + + async assertDashboardCustomUrlAction(expectedPanelCount: number) { + await PageObjects.dashboard.waitForRenderComplete(); + await retry.tryForTime(5000, async () => { + const panelCount = await PageObjects.dashboard.getPanelCount(); + expect(panelCount).to.eql( + expectedPanelCount, + `Expected Dashboard panel count to be '${expectedPanelCount}' (got '${panelCount}')` + ); + }); + }, }; } diff --git a/x-pack/test/functional/services/ml/index.ts b/x-pack/test/functional/services/ml/index.ts index 6a2e1158e70a..64298bbdedd6 100644 --- a/x-pack/test/functional/services/ml/index.ts +++ b/x-pack/test/functional/services/ml/index.ts @@ -90,7 +90,7 @@ export function MachineLearningProvider(context: FtrProviderContext) { const jobManagement = MachineLearningJobManagementProvider(context, api); const jobSelection = MachineLearningJobSelectionProvider(context); const jobSourceSelection = MachineLearningJobSourceSelectionProvider(context); - const jobTable = MachineLearningJobTableProvider(context); + const jobTable = MachineLearningJobTableProvider(context, commonUI, customUrls); const jobTypeSelection = MachineLearningJobTypeSelectionProvider(context); const jobWizardAdvanced = MachineLearningJobWizardAdvancedProvider(context, commonUI); const jobWizardCategorization = MachineLearningJobWizardCategorizationProvider(context); diff --git a/x-pack/test/functional/services/ml/job_table.ts b/x-pack/test/functional/services/ml/job_table.ts index a179983a4627..a39e62d6281f 100644 --- a/x-pack/test/functional/services/ml/job_table.ts +++ b/x-pack/test/functional/services/ml/job_table.ts @@ -8,8 +8,20 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; +import { MlCommonUI } from './common_ui'; +import { MlCustomUrls } from './custom_urls'; -export function MachineLearningJobTableProvider({ getService }: FtrProviderContext) { +import { + TimeRangeType, + TIME_RANGE_TYPE, + URL_TYPE, +} from '../../../../plugins/ml/public/application/jobs/components/custom_url_editor/constants'; + +export function MachineLearningJobTableProvider( + { getService }: FtrProviderContext, + mlCommonUI: MlCommonUI, + customUrls: MlCustomUrls +) { const testSubjects = getService('testSubjects'); const retry = getService('retry'); @@ -311,6 +323,12 @@ export function MachineLearningJobTableProvider({ getService }: FtrProviderConte await testSubjects.existOrFail('~mlPageJobWizard'); } + public async clickEditJobAction(jobId: string) { + await this.ensureJobActionsMenuOpen(jobId); + await testSubjects.click('mlActionButtonEditJob'); + await testSubjects.existOrFail('mlJobEditFlyout'); + } + public async clickDeleteJobAction(jobId: string) { await this.ensureJobActionsMenuOpen(jobId); await testSubjects.click('mlActionButtonDeleteJob'); @@ -456,5 +474,189 @@ export function MachineLearningJobTableProvider({ getService }: FtrProviderConte } }); } + + public async openEditCustomUrlsForJobTab(jobId: string) { + await this.clickEditJobAction(jobId); + // click Custom URLs tab + await testSubjects.click('mlEditJobFlyout-customUrls'); + await this.ensureEditCustomUrlTabOpen(); + } + + public async ensureEditCustomUrlTabOpen() { + await testSubjects.existOrFail('mlJobOpenCustomUrlFormButton', { timeout: 5000 }); + } + + public async closeEditJobFlyout() { + if (await testSubjects.exists('mlEditJobFlyoutCloseButton')) { + await testSubjects.click('mlEditJobFlyoutCloseButton'); + await testSubjects.missingOrFail('mlJobEditFlyout'); + } + } + + public async saveEditJobFlyoutChanges() { + await testSubjects.click('mlEditJobFlyoutSaveButton'); + await testSubjects.missingOrFail('mlJobEditFlyout', { timeout: 5000 }); + } + + public async clickOpenCustomUrlEditor() { + await this.ensureEditCustomUrlTabOpen(); + await testSubjects.click('mlJobOpenCustomUrlFormButton'); + await testSubjects.existOrFail('mlJobCustomUrlForm'); + } + + public async addDiscoverCustomUrl( + jobId: string, + customUrl: { + label: string; + indexPattern: string; + queryEntityFieldNames: string[]; + timeRange: TimeRangeType; + timeRangeInterval?: string; + } + ) { + await this.openEditCustomUrlsForJobTab(jobId); + + const existingCustomUrls = await testSubjects.findAll('mlJobEditCustomUrlItemLabel'); + + // Fill-in the form + await this.clickOpenCustomUrlEditor(); + await customUrls.setCustomUrlLabel(customUrl.label); + await mlCommonUI.selectRadioGroupValue( + `mlJobCustomUrlLinkToTypeInput`, + URL_TYPE.KIBANA_DISCOVER + ); + await mlCommonUI.selectSelectValueByVisibleText( + 'mlJobCustomUrlDiscoverIndexPatternInput', + customUrl.indexPattern + ); + await customUrls.setCustomUrlQueryEntityFieldNames(customUrl.queryEntityFieldNames); + await mlCommonUI.selectSelectValueByVisibleText( + 'mlJobCustomUrlTimeRangeInput', + customUrl.timeRange + ); + if (customUrl.timeRange === TIME_RANGE_TYPE.INTERVAL) { + await customUrls.setCustomUrlTimeRangeInterval(customUrl.timeRangeInterval!); + } + + // Save custom URL + await testSubjects.click('mlJobAddCustomUrl'); + const expectedIndex = existingCustomUrls.length; + await customUrls.assertCustomUrlLabel(expectedIndex, customUrl.label); + + // Save the job + await this.saveEditJobFlyoutChanges(); + } + + public async addDashboardCustomUrl( + jobId: string, + customUrl: { + label: string; + dashboardName: string; + queryEntityFieldNames: string[]; + timeRange: TimeRangeType; + timeRangeInterval?: string; + } + ) { + await this.openEditCustomUrlsForJobTab(jobId); + + const existingCustomUrls = await testSubjects.findAll('mlJobEditCustomUrlItemLabel'); + + // Fill-in the form + await this.clickOpenCustomUrlEditor(); + await customUrls.setCustomUrlLabel(customUrl.label); + await mlCommonUI.selectRadioGroupValue( + `mlJobCustomUrlLinkToTypeInput`, + URL_TYPE.KIBANA_DASHBOARD + ); + await mlCommonUI.selectSelectValueByVisibleText( + 'mlJobCustomUrlDashboardNameInput', + customUrl.dashboardName + ); + await customUrls.setCustomUrlQueryEntityFieldNames(customUrl.queryEntityFieldNames); + await mlCommonUI.selectSelectValueByVisibleText( + 'mlJobCustomUrlTimeRangeInput', + customUrl.timeRange + ); + if (customUrl.timeRange === TIME_RANGE_TYPE.INTERVAL) { + await customUrls.setCustomUrlTimeRangeInterval(customUrl.timeRangeInterval!); + } + + // Save custom URL + await testSubjects.click('mlJobAddCustomUrl'); + const expectedIndex = existingCustomUrls.length; + await customUrls.assertCustomUrlLabel(expectedIndex, customUrl.label); + + // Save the job + await this.saveEditJobFlyoutChanges(); + } + + public async addOtherTypeCustomUrl(jobId: string, customUrl: { label: string; url: string }) { + await this.openEditCustomUrlsForJobTab(jobId); + + const existingCustomUrls = await testSubjects.findAll('mlJobEditCustomUrlItemLabel'); + + // Fill-in the form + await this.clickOpenCustomUrlEditor(); + await customUrls.setCustomUrlLabel(customUrl.label); + await mlCommonUI.selectRadioGroupValue(`mlJobCustomUrlLinkToTypeInput`, URL_TYPE.OTHER); + await customUrls.setCustomUrlOtherTypeUrl(customUrl.url); + + // Save custom URL + await testSubjects.click('mlJobAddCustomUrl'); + const expectedIndex = existingCustomUrls.length; + await customUrls.assertCustomUrlLabel(expectedIndex, customUrl.label); + + // Save the job + await this.saveEditJobFlyoutChanges(); + } + + public async editCustomUrl( + jobId: string, + indexInList: number, + customUrl: { label: string; url: string } + ) { + await this.openEditCustomUrlsForJobTab(jobId); + await customUrls.editCustomUrlLabel(indexInList, customUrl.label); + await customUrls.editCustomUrlUrlValue(indexInList, customUrl.url); + + // Save the edit + await this.saveEditJobFlyoutChanges(); + } + + public async deleteCustomUrl(jobId: string, indexInList: number) { + await this.openEditCustomUrlsForJobTab(jobId); + const beforeCustomUrls = await testSubjects.findAll('mlJobEditCustomUrlItemLabel'); + await customUrls.deleteCustomUrl(indexInList); + + // Save the edit and check the custom URL has been deleted. + await testSubjects.click('mlEditJobFlyoutSaveButton'); + await this.openEditCustomUrlsForJobTab(jobId); + await customUrls.assertCustomUrlsLength(beforeCustomUrls.length - 1); + await this.closeEditJobFlyout(); + } + + public async openTestCustomUrl(jobId: string, indexInList: number) { + await this.openEditCustomUrlsForJobTab(jobId); + await customUrls.clickTestCustomUrl(indexInList); + } + + public async testDiscoverCustomUrlAction(expectedHitCountFormatted: string) { + await customUrls.assertDiscoverCustomUrlAction(expectedHitCountFormatted); + } + + public async testDashboardCustomUrlAction(expectedPanelCount: number) { + await customUrls.assertDashboardCustomUrlAction(expectedPanelCount); + } + + public async testOtherTypeCustomUrlAction( + jobId: string, + indexInList: number, + expectedUrl: string + ) { + // Can't test the contents of the external page, so just check the expected URL. + await this.openEditCustomUrlsForJobTab(jobId); + await customUrls.assertCustomUrlUrlValue(indexInList, expectedUrl); + await this.closeEditJobFlyout(); + } })(); } diff --git a/x-pack/test/functional/services/ml/job_wizard_common.ts b/x-pack/test/functional/services/ml/job_wizard_common.ts index 7754432c99ab..2990f7005976 100644 --- a/x-pack/test/functional/services/ml/job_wizard_common.ts +++ b/x-pack/test/functional/services/ml/job_wizard_common.ts @@ -527,7 +527,7 @@ export function MachineLearningJobWizardCommonProvider( const expectedIndex = existingCustomUrls.length; - await customUrls.assertCustomUrlItem(expectedIndex, customUrl.label); + await customUrls.assertCustomUrlLabel(expectedIndex, customUrl.label); }, async ensureAdvancedSectionOpen() { diff --git a/x-pack/test/functional/services/ml/test_resources.ts b/x-pack/test/functional/services/ml/test_resources.ts index f967099e10fa..a8db7ccb7a76 100644 --- a/x-pack/test/functional/services/ml/test_resources.ts +++ b/x-pack/test/functional/services/ml/test_resources.ts @@ -305,6 +305,10 @@ export function MachineLearningTestResourcesProvider({ getService }: FtrProvider await this.createDashboardIfNeeded(dashboards.mlTestDashboard); }, + async deleteMLTestDashboard() { + await this.deleteDashboardByTitle(dashboards.mlTestDashboard.requestBody.attributes.title); + }, + async createDashboardIfNeeded(dashboard: any) { const title = dashboard.requestBody.attributes.title; const dashboardId = await this.getDashboardId(title);