From 75384e1bcd96edbb338c1f604154ee03cd9e4831 Mon Sep 17 00:00:00 2001 From: Robert Oskamp Date: Tue, 29 Oct 2019 13:36:45 +0100 Subject: [PATCH] [ML] Advanced wizard functional UI tests (#49480) This PR adds tests for the ML advanced wizard. --- .../start_datafeed_modal.js | 3 + .../components/datafeed_step/datafeed.tsx | 2 +- .../advanced_detector_modal.tsx | 13 +- .../advanced_detector_modal/modal_wrapper.tsx | 15 +- .../advanced_view/detector_list.tsx | 18 +- .../advanced_view/metric_selector.tsx | 2 +- .../anomaly_detection/advanced_job.ts | 805 ++++++++++++++++++ .../anomaly_detection/index.ts | 1 + .../anomaly_detection/multi_metric_job.ts | 14 +- .../anomaly_detection/population_job.ts | 18 +- .../anomaly_detection/saved_search_job.ts | 6 + .../anomaly_detection/single_metric_job.ts | 8 + .../es_archives/ml/farequote/mappings.json | 20 +- .../services/machine_learning/api.ts | 107 +++ .../services/machine_learning/index.ts | 1 + .../machine_learning/job_management.ts | 27 +- .../services/machine_learning/job_table.ts | 7 +- .../machine_learning/job_type_selection.ts | 9 + .../machine_learning/job_wizard_advanced.ts | 315 +++++++ .../machine_learning/job_wizard_common.ts | 165 ++-- .../job_wizard_multi_metric.ts | 7 +- .../machine_learning/job_wizard_population.ts | 17 +- x-pack/test/functional/services/ml.ts | 5 +- 23 files changed, 1483 insertions(+), 102 deletions(-) create mode 100644 x-pack/test/functional/apps/machine_learning/anomaly_detection/advanced_job.ts create mode 100644 x-pack/test/functional/services/machine_learning/job_wizard_advanced.ts diff --git a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/start_datafeed_modal.js b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/start_datafeed_modal.js index 133adea7cff6e..0c356959cd2af 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/start_datafeed_modal.js +++ b/x-pack/legacy/plugins/ml/public/jobs/jobs_list/components/start_datafeed_modal/start_datafeed_modal.js @@ -136,6 +136,7 @@ export class StartDatafeedModal extends Component { onClose={this.closeModal} style={{ width: '850px' }} maxWidth={false} + data-test-subj="mlStartDatafeedModal" > @@ -178,6 +179,7 @@ export class StartDatafeedModal extends Component { = ({ setCurrentStep, isCurrentStep }) = {isCurrentStep && ( - + diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx index b9e9df77d35e3..5f93361982ea0 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx @@ -213,7 +213,7 @@ export const AdvancedDetectorModal: FC = ({ - + = ({ /> - + = ({ - + = ({ /> - + = ({ /> - + = ({ /> - + = ({ placeholder={descriptionPlaceholder} value={descriptionOption} onChange={e => setDescriptionOption(e.target.value)} + data-test-subj="mlAdvancedDetectorDescriptionInput" /> diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/advanced_detector_modal/modal_wrapper.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/advanced_detector_modal/modal_wrapper.tsx index 8b85c658fcadf..d1ee8a6be557a 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/advanced_detector_modal/modal_wrapper.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/advanced_detector_modal/modal_wrapper.tsx @@ -28,7 +28,11 @@ interface Props { export const ModalWrapper: FC = ({ onCreateClick, closeModal, saveEnabled, children }) => { return ( - + = ({ onCreateClick, closeModal, saveEnabled {children} - + - + = ({ isActive, onEditJob, onDeleteJob }) => defaultMessage: 'Edit', } )} + data-test-subj="mlAdvancedDetectorEditButton" /> @@ -75,6 +77,7 @@ export const DetectorList: FC = ({ isActive, onEditJob, onDeleteJob }) => defaultMessage: 'Delete', } )} + data-test-subj="mlAdvancedDetectorDeleteButton" /> @@ -98,14 +101,16 @@ export const DetectorList: FC = ({ isActive, onEditJob, onDeleteJob }) => {detectors.map((d, i) => ( - + {d.detector_description !== undefined ? ( -
{d.detector_description}
+
+ {d.detector_description} +
) : ( - detectorToString(d) + )}
{isActive && ( @@ -117,7 +122,7 @@ export const DetectorList: FC = ({ isActive, onEditJob, onDeleteJob }) => {d.detector_description !== undefined && ( - {detectorToString(d)} + )}
@@ -142,6 +147,7 @@ const NoDetectorsWarning: FC<{ show: boolean }> = ({ show }) => { defaultMessage: 'No detectors', })} iconType="alert" + data-test-subj="mlAdvancedNoDetectorsMessage" > = ({ validation
); }; + +const StringifiedDetector: FC<{ detector: Detector }> = ({ detector }) => { + return
{detectorToString(detector)}
; +}; diff --git a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/advanced_view/metric_selector.tsx b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/advanced_view/metric_selector.tsx index fd597875f2716..5bc38ca934165 100644 --- a/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/advanced_view/metric_selector.tsx +++ b/x-pack/legacy/plugins/ml/public/jobs/new_job_new/pages/components/pick_fields_step/components/advanced_view/metric_selector.tsx @@ -35,7 +35,7 @@ export const MetricSelector: FC = ({ - + > => { + return arg.hasOwnProperty('field'); +}; +const isDetectorWithByField = (arg: any): arg is Required> => { + return arg.hasOwnProperty('byField'); +}; +const isDetectorWithOverField = (arg: any): arg is Required> => { + return arg.hasOwnProperty('overField'); +}; +const isDetectorWithPartitionField = ( + arg: any +): arg is Required> => { + return arg.hasOwnProperty('partitionField'); +}; +const isDetectorWithExcludeFrequent = ( + arg: any +): arg is Required> => { + return arg.hasOwnProperty('excludeFrequent'); +}; +const isDetectorWithDescription = (arg: any): arg is Required> => { + return arg.hasOwnProperty('description'); +}; + +// DatafeedConfig +const isDatafeedConfigWithQueryDelay = ( + arg: any +): arg is Required> => { + return arg.hasOwnProperty('queryDelay'); +}; +const isDatafeedConfigWithFrequency = ( + arg: any +): arg is Required> => { + return arg.hasOwnProperty('frequency'); +}; +const isDatafeedConfigWithScrollSize = ( + arg: any +): arg is Required> => { + return arg.hasOwnProperty('scrollSize'); +}; + +// PickFieldsConfig +const isPickFieldsConfigWithCategorizationField = ( + arg: any +): arg is Required> => { + return arg.hasOwnProperty('categorizationField'); +}; +const isPickFieldsConfigWithSummaryCountField = ( + arg: any +): arg is Required> => { + return arg.hasOwnProperty('summaryCountField'); +}; + +// eslint-disable-next-line import/no-default-export +export default function({ getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const ml = getService('ml'); + + const defaultValues = { + datafeedQuery: `{ + "bool": { + "must": [ + { + "match_all": {} + } + ] + } +}`, + queryDelay: '60s', + frequency: '450s', + scrollSize: '1000', + }; + + const testDataList = [ + { + suiteTitle: 'with multiple metric detectors and custom datafeed settings', + jobSource: 'ecommerce', + jobId: `ec_advanced_1_${Date.now()}`, + get jobIdClone(): string { + return `${this.jobId}_clone`; + }, + jobDescription: + 'Create advanced job from ecommerce dataset with multiple metric detectors and custom datafeed settings', + jobGroups: ['automated', 'ecommerce', 'advanced'], + get jobGroupsClone(): string[] { + return [...this.jobGroups, 'clone']; + }, + pickFieldsConfig: { + detectors: [ + { + identifier: 'high_count', + function: 'high_count', + description: 'high_count detector without split', + } as Detector, + { + identifier: 'mean("products.base_price") by "category.keyword"', + function: 'mean', + field: 'products.base_price', + byField: 'category.keyword', + } as Detector, + { + identifier: 'sum("products.discount_amount") over customer_id', + function: 'sum', + field: 'products.discount_amount', + overField: 'customer_id', + } as Detector, + { + identifier: 'median(total_quantity) partition_field_name=customer_gender', + function: 'median', + field: 'total_quantity', + partitionField: 'customer_gender', + } as Detector, + { + identifier: + 'max(total_quantity) by "geoip.continent_name" over customer_id partition_field_name=customer_gender', + function: 'max', + field: 'total_quantity', + byField: 'geoip.continent_name', + overField: 'customer_id', + partitionField: 'customer_gender', + } as Detector, + ], + influencers: ['customer_id', 'category.keyword', 'geoip.continent_name', 'customer_gender'], + bucketSpan: '1h', + memoryLimit: '10mb', + } as PickFieldsConfig, + datafeedConfig: { + queryDelay: '55s', + frequency: '350s', + scrollSize: '999', + } as DatafeedConfig, + expected: { + wizard: { + timeField: 'order_date', + }, + row: { + recordCount: '4,675', + memoryStatus: 'ok', + jobState: 'closed', + datafeedState: 'stopped', + latestTimestamp: '2019-07-12 23:45:36', + }, + counts: { + processed_record_count: '4,675', + processed_field_count: '32,725', + input_bytes: '1.1 MB', + input_field_count: '32,725', + invalid_date_count: '0', + missing_field_count: '0', + out_of_order_timestamp_count: '0', + empty_bucket_count: '0', + sparse_bucket_count: '0', + bucket_count: '743', + earliest_record_timestamp: '2019-06-12 00:04:19', + latest_record_timestamp: '2019-07-12 23:45:36', + input_record_count: '4,675', + latest_bucket_timestamp: '2019-07-12 23:00:00', + }, + modelSizeStats: { + result_type: 'model_size_stats', + model_bytes_exceeded: '0', + model_bytes_memory_limit: '10485760', + total_by_field_count: '37', + total_over_field_count: '92', + total_partition_field_count: '8', + bucket_allocation_failures_count: '0', + memory_status: 'ok', + timestamp: '2019-07-12 22:00:00', + }, + }, + }, + { + suiteTitle: 'with categorization detector and default datafeed settings', + jobSource: 'ecommerce', + jobId: `ec_advanced_2_${Date.now()}`, + get jobIdClone(): string { + return `${this.jobId}_clone`; + }, + jobDescription: + 'Create advanced job from ecommerce dataset with a categorization detector and default datafeed settings', + jobGroups: ['automated', 'ecommerce', 'advanced'], + get jobGroupsClone(): string[] { + return [...this.jobGroups, 'clone']; + }, + pickFieldsConfig: { + categorizationField: 'products.product_name', + detectors: [ + { + identifier: 'count by mlcategory', + function: 'count', + byField: 'mlcategory', + } as Detector, + ], + influencers: ['mlcategory'], + bucketSpan: '12h', + memoryLimit: '100mb', + } as PickFieldsConfig, + datafeedConfig: {} as DatafeedConfig, + expected: { + wizard: { + timeField: 'order_date', + }, + row: { + recordCount: '4,675', + memoryStatus: 'ok', + jobState: 'closed', + datafeedState: 'stopped', + latestTimestamp: '2019-07-12 23:45:36', + }, + counts: { + processed_record_count: '4,675', + processed_field_count: '4,588', + input_bytes: '4.4 MB', + input_field_count: '6,154', + invalid_date_count: '0', + missing_field_count: '87', + out_of_order_timestamp_count: '0', + empty_bucket_count: '0', + sparse_bucket_count: '0', + bucket_count: '61', + earliest_record_timestamp: '2019-06-12 00:04:19', + latest_record_timestamp: '2019-07-12 23:45:36', + input_record_count: '4,675', + latest_bucket_timestamp: '2019-07-12 12:00:00', + }, + modelSizeStats: { + result_type: 'model_size_stats', + model_bytes_exceeded: '0', + model_bytes_memory_limit: '104857600', + total_by_field_count: '3,787', + total_over_field_count: '0', + total_partition_field_count: '2', + bucket_allocation_failures_count: '0', + memory_status: 'ok', + timestamp: '2019-07-12 00:00:00', + }, + }, + }, + ]; + + describe('advanced job', function() { + this.tags(['smoke', 'mlqa']); + before(async () => { + await esArchiver.load('ml/ecommerce'); + }); + + after(async () => { + await esArchiver.unload('ml/ecommerce'); + await ml.api.cleanMlIndices(); + }); + + for (const testData of testDataList) { + describe(`${testData.suiteTitle}`, function() { + it('job creation loads the job management page', async () => { + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToJobManagement(); + }); + + it('job creation loads the new job source selection page', async () => { + await ml.jobManagement.navigateToNewJobSourceSelection(); + }); + + it('job creation loads the job type selection page', async () => { + await ml.jobSourceSelection.selectSource(testData.jobSource); + }); + + it('job creation loads the advanced job wizard page', async () => { + await ml.jobTypeSelection.selectAdvancedJob(); + }); + + it('job creation displays the configure datafeed step', async () => { + await ml.jobWizardCommon.assertConfigureDatafeedSectionExists(); + }); + + it('job creation pre-fills the datafeed query editor', async () => { + await ml.jobWizardAdvanced.assertDatafeedQueryEditorExists(); + await ml.jobWizardAdvanced.assertDatafeedQueryEditorValue(defaultValues.datafeedQuery); + }); + + it('job creation inputs the query delay', async () => { + await ml.jobWizardAdvanced.assertQueryDelayInputExists(); + await ml.jobWizardAdvanced.assertQueryDelayValue(defaultValues.queryDelay); + if (isDatafeedConfigWithQueryDelay(testData.datafeedConfig)) { + await ml.jobWizardAdvanced.setQueryDelay(testData.datafeedConfig.queryDelay); + } + }); + + it('job creation inputs the frequency', async () => { + await ml.jobWizardAdvanced.assertFrequencyInputExists(); + await ml.jobWizardAdvanced.assertFrequencyValue(defaultValues.frequency); + if (isDatafeedConfigWithFrequency(testData.datafeedConfig)) { + await ml.jobWizardAdvanced.setFrequency(testData.datafeedConfig.frequency); + } + }); + + it('job creation inputs the scroll size', async () => { + await ml.jobWizardAdvanced.assertScrollSizeInputExists(); + await ml.jobWizardAdvanced.assertScrollSizeValue(defaultValues.scrollSize); + if (isDatafeedConfigWithScrollSize(testData.datafeedConfig)) { + await ml.jobWizardAdvanced.setScrollSize(testData.datafeedConfig.scrollSize); + } + }); + + it('job creation pre-fills the time field', async () => { + await ml.jobWizardAdvanced.assertTimeFieldInputExists(); + await ml.jobWizardAdvanced.assertTimeFieldSelection([testData.expected.wizard.timeField]); + }); + + it('job creation displays the pick fields step', async () => { + await ml.jobWizardCommon.advanceToPickFieldsSection(); + }); + + it('job creation selects the categorization field', async () => { + await ml.jobWizardAdvanced.assertCategorizationFieldInputExists(); + if (isPickFieldsConfigWithCategorizationField(testData.pickFieldsConfig)) { + await ml.jobWizardAdvanced.selectCategorizationField( + testData.pickFieldsConfig.categorizationField + ); + } else { + await ml.jobWizardAdvanced.assertCategorizationFieldSelection([]); + } + }); + + it('job creation selects the summary count field', async () => { + await ml.jobWizardAdvanced.assertSummaryCountFieldInputExists(); + if (isPickFieldsConfigWithSummaryCountField(testData.pickFieldsConfig)) { + await ml.jobWizardAdvanced.selectSummaryCountField( + testData.pickFieldsConfig.summaryCountField + ); + } else { + await ml.jobWizardAdvanced.assertSummaryCountFieldSelection([]); + } + }); + + it('job creation adds detectors', async () => { + for (const detector of testData.pickFieldsConfig.detectors) { + await ml.jobWizardAdvanced.openCreateDetectorModal(); + await ml.jobWizardAdvanced.assertDetectorFunctionInputExists(); + await ml.jobWizardAdvanced.assertDetectorFunctionSelection([]); + await ml.jobWizardAdvanced.assertDetectorFieldInputExists(); + await ml.jobWizardAdvanced.assertDetectorFieldSelection([]); + await ml.jobWizardAdvanced.assertDetectorByFieldInputExists(); + await ml.jobWizardAdvanced.assertDetectorByFieldSelection([]); + await ml.jobWizardAdvanced.assertDetectorOverFieldInputExists(); + await ml.jobWizardAdvanced.assertDetectorOverFieldSelection([]); + await ml.jobWizardAdvanced.assertDetectorPartitionFieldInputExists(); + await ml.jobWizardAdvanced.assertDetectorPartitionFieldSelection([]); + await ml.jobWizardAdvanced.assertDetectorExcludeFrequentInputExists(); + await ml.jobWizardAdvanced.assertDetectorExcludeFrequentSelection([]); + await ml.jobWizardAdvanced.assertDetectorDescriptionInputExists(); + await ml.jobWizardAdvanced.assertDetectorDescriptionValue(''); + + await ml.jobWizardAdvanced.selectDetectorFunction(detector.function); + if (isDetectorWithField(detector)) { + await ml.jobWizardAdvanced.selectDetectorField(detector.field); + } + if (isDetectorWithByField(detector)) { + await ml.jobWizardAdvanced.selectDetectorByField(detector.byField); + } + if (isDetectorWithOverField(detector)) { + await ml.jobWizardAdvanced.selectDetectorOverField(detector.overField); + } + if (isDetectorWithPartitionField(detector)) { + await ml.jobWizardAdvanced.selectDetectorPartitionField(detector.partitionField); + } + if (isDetectorWithExcludeFrequent(detector)) { + await ml.jobWizardAdvanced.selectDetectorExcludeFrequent(detector.excludeFrequent); + } + if (isDetectorWithDescription(detector)) { + await ml.jobWizardAdvanced.setDetectorDescription(detector.description); + } + + await ml.jobWizardAdvanced.confirmAddDetectorModal(); + } + }); + + it('job creation displays detector entries', async () => { + for (const [index, detector] of testData.pickFieldsConfig.detectors.entries()) { + await ml.jobWizardAdvanced.assertDetectorEntryExists( + index, + detector.identifier, + isDetectorWithDescription(detector) ? detector.description : undefined + ); + } + }); + + it('job creation inputs the bucket span', async () => { + await ml.jobWizardCommon.assertBucketSpanInputExists(); + await ml.jobWizardCommon.setBucketSpan(testData.pickFieldsConfig.bucketSpan); + }); + + it('job creation inputs influencers', async () => { + await ml.jobWizardCommon.assertInfluencerInputExists(); + await ml.jobWizardCommon.assertInfluencerSelection([]); + for (const influencer of testData.pickFieldsConfig.influencers) { + await ml.jobWizardCommon.addInfluencer(influencer); + } + }); + + it('job creation inputs the model memory limit', async () => { + await ml.jobWizardCommon.assertModelMemoryLimitInputExists({ + withAdvancedSection: false, + }); + await ml.jobWizardCommon.setModelMemoryLimit(testData.pickFieldsConfig.memoryLimit, { + withAdvancedSection: false, + }); + }); + + it('job creation displays the job details step', async () => { + await ml.jobWizardCommon.advanceToJobDetailsSection(); + }); + + it('job creation inputs the job id', async () => { + await ml.jobWizardCommon.assertJobIdInputExists(); + await ml.jobWizardCommon.setJobId(testData.jobId); + }); + + it('job creation inputs the job description', async () => { + await ml.jobWizardCommon.assertJobDescriptionInputExists(); + await ml.jobWizardCommon.setJobDescription(testData.jobDescription); + }); + + it('job creation inputs job groups', async () => { + await ml.jobWizardCommon.assertJobGroupInputExists(); + for (const jobGroup of testData.jobGroups) { + await ml.jobWizardCommon.addJobGroup(jobGroup); + } + await ml.jobWizardCommon.assertJobGroupSelection(testData.jobGroups); + }); + + it('job creation displays the model plot switch', async () => { + await ml.jobWizardCommon.assertModelPlotSwitchExists({ withAdvancedSection: false }); + }); + + it('job creation enables the dedicated index switch', async () => { + await ml.jobWizardCommon.assertDedicatedIndexSwitchExists({ withAdvancedSection: false }); + await ml.jobWizardCommon.activateDedicatedIndexSwitch({ withAdvancedSection: false }); + }); + + it('job creation displays the validation step', async () => { + await ml.jobWizardCommon.advanceToValidationSection(); + }); + + it('job creation displays the summary step', async () => { + await ml.jobWizardCommon.advanceToSummarySection(); + }); + + it('job creation creates the job and finishes processing', async () => { + await ml.jobWizardCommon.assertCreateJobButtonExists(); + await ml.jobWizardAdvanced.createJob(); + await ml.jobManagement.assertStartDatafeedModalExists(); + await ml.jobManagement.confirmStartDatafeedModal(); + await ml.jobManagement.waitForJobCompletion(testData.jobId); + }); + + it('job creation displays the created job in the job list', async () => { + await ml.jobTable.refreshJobList(); + await ml.jobTable.filterWithSearchString(testData.jobId); + const rows = await ml.jobTable.parseJobTable(); + expect(rows.filter(row => row.id === testData.jobId)).to.have.length(1); + }); + + it('job creation displays details for the created job in the job list', async () => { + await ml.jobTable.assertJobRowFields(testData.jobId, { + id: testData.jobId, + description: testData.jobDescription, + jobGroups: [...new Set(testData.jobGroups)].sort(), + recordCount: testData.expected.row.recordCount, + memoryStatus: testData.expected.row.memoryStatus, + jobState: testData.expected.row.jobState, + datafeedState: testData.expected.row.datafeedState, + latestTimestamp: testData.expected.row.latestTimestamp, + }); + + await ml.jobTable.assertJobRowDetailsCounts( + testData.jobId, + { + job_id: testData.jobId, + processed_record_count: testData.expected.counts.processed_record_count, + processed_field_count: testData.expected.counts.processed_field_count, + input_bytes: testData.expected.counts.input_bytes, + input_field_count: testData.expected.counts.input_field_count, + invalid_date_count: testData.expected.counts.invalid_date_count, + missing_field_count: testData.expected.counts.missing_field_count, + out_of_order_timestamp_count: testData.expected.counts.out_of_order_timestamp_count, + empty_bucket_count: testData.expected.counts.empty_bucket_count, + sparse_bucket_count: testData.expected.counts.sparse_bucket_count, + bucket_count: testData.expected.counts.bucket_count, + earliest_record_timestamp: testData.expected.counts.earliest_record_timestamp, + latest_record_timestamp: testData.expected.counts.latest_record_timestamp, + input_record_count: testData.expected.counts.input_record_count, + latest_bucket_timestamp: testData.expected.counts.latest_bucket_timestamp, + }, + { + job_id: testData.jobId, + result_type: testData.expected.modelSizeStats.result_type, + model_bytes_exceeded: testData.expected.modelSizeStats.model_bytes_exceeded, + model_bytes_memory_limit: testData.expected.modelSizeStats.model_bytes_memory_limit, + total_by_field_count: testData.expected.modelSizeStats.total_by_field_count, + total_over_field_count: testData.expected.modelSizeStats.total_over_field_count, + total_partition_field_count: + testData.expected.modelSizeStats.total_partition_field_count, + bucket_allocation_failures_count: + testData.expected.modelSizeStats.bucket_allocation_failures_count, + memory_status: testData.expected.modelSizeStats.memory_status, + timestamp: testData.expected.modelSizeStats.timestamp, + } + ); + }); + + it('job creation has detector results', async () => { + for (let i = 0; i < testData.pickFieldsConfig.detectors.length; i++) { + await ml.api.assertDetectorResultsExist(testData.jobId, i); + } + }); + + it('job cloning clicks the clone action and loads the advanced wizard', async () => { + await ml.jobTable.clickCloneJobAction(testData.jobId); + await ml.jobTypeSelection.assertAdvancedJobWizardOpen(); + }); + + it('job cloning displays the configure datafeed step', async () => { + await ml.jobWizardCommon.assertConfigureDatafeedSectionExists(); + }); + + it('job cloning pre-fills the datafeed query editor', async () => { + await ml.jobWizardAdvanced.assertDatafeedQueryEditorExists(); + await ml.jobWizardAdvanced.assertDatafeedQueryEditorValue(defaultValues.datafeedQuery); + }); + + it('job cloning pre-fills the query delay', async () => { + await ml.jobWizardAdvanced.assertQueryDelayInputExists(); + if (isDatafeedConfigWithQueryDelay(testData.datafeedConfig)) { + await ml.jobWizardAdvanced.assertQueryDelayValue(testData.datafeedConfig.queryDelay); + } + }); + + it('job cloning pre-fills the frequency', async () => { + await ml.jobWizardAdvanced.assertFrequencyInputExists(); + if (isDatafeedConfigWithFrequency(testData.datafeedConfig)) { + await ml.jobWizardAdvanced.assertFrequencyValue(testData.datafeedConfig.frequency); + } + }); + + it('job cloning pre-fills the scroll size', async () => { + await ml.jobWizardAdvanced.assertScrollSizeInputExists(); + await ml.jobWizardAdvanced.assertScrollSizeValue( + isDatafeedConfigWithScrollSize(testData.datafeedConfig) + ? testData.datafeedConfig.scrollSize + : defaultValues.scrollSize + ); + }); + + it('job creation pre-fills the time field', async () => { + await ml.jobWizardAdvanced.assertTimeFieldInputExists(); + await ml.jobWizardAdvanced.assertTimeFieldSelection([testData.expected.wizard.timeField]); + }); + + it('job cloning displays the pick fields step', async () => { + await ml.jobWizardCommon.advanceToPickFieldsSection(); + }); + + it('job cloning pre-fills the categorization field', async () => { + await ml.jobWizardAdvanced.assertCategorizationFieldInputExists(); + await ml.jobWizardAdvanced.assertCategorizationFieldSelection( + isPickFieldsConfigWithCategorizationField(testData.pickFieldsConfig) + ? [testData.pickFieldsConfig.categorizationField] + : [] + ); + }); + + it('job cloning pre-fills the summary count field', async () => { + await ml.jobWizardAdvanced.assertSummaryCountFieldInputExists(); + await ml.jobWizardAdvanced.assertSummaryCountFieldSelection( + isPickFieldsConfigWithSummaryCountField(testData.pickFieldsConfig) + ? [testData.pickFieldsConfig.summaryCountField] + : [] + ); + }); + + it('job cloning pre-fills detectors', async () => { + for (const [index, detector] of testData.pickFieldsConfig.detectors.entries()) { + await ml.jobWizardAdvanced.assertDetectorEntryExists( + index, + detector.identifier, + isDetectorWithDescription(detector) ? detector.description : undefined + ); + await ml.jobWizardAdvanced.clickEditDetector(index); + + await ml.jobWizardAdvanced.assertDetectorFunctionInputExists(); + await ml.jobWizardAdvanced.assertDetectorFieldInputExists(); + await ml.jobWizardAdvanced.assertDetectorByFieldInputExists(); + await ml.jobWizardAdvanced.assertDetectorOverFieldInputExists(); + await ml.jobWizardAdvanced.assertDetectorPartitionFieldInputExists(); + await ml.jobWizardAdvanced.assertDetectorExcludeFrequentInputExists(); + await ml.jobWizardAdvanced.assertDetectorDescriptionInputExists(); + + await ml.jobWizardAdvanced.assertDetectorFunctionSelection([detector.function]); + await ml.jobWizardAdvanced.assertDetectorFieldSelection( + isDetectorWithField(detector) ? [detector.field] : [] + ); + await ml.jobWizardAdvanced.assertDetectorByFieldSelection( + isDetectorWithByField(detector) ? [detector.byField] : [] + ); + await ml.jobWizardAdvanced.assertDetectorOverFieldSelection( + isDetectorWithOverField(detector) ? [detector.overField] : [] + ); + await ml.jobWizardAdvanced.assertDetectorPartitionFieldSelection( + isDetectorWithPartitionField(detector) ? [detector.partitionField] : [] + ); + await ml.jobWizardAdvanced.assertDetectorExcludeFrequentSelection( + isDetectorWithExcludeFrequent(detector) ? [detector.excludeFrequent] : [] + ); + // Currently, a description different form the identifier is generated for detectors with partition field + await ml.jobWizardAdvanced.assertDetectorDescriptionValue( + isDetectorWithDescription(detector) + ? detector.description + : detector.identifier.replace('partition_field_name', 'partitionfield') + ); + + await ml.jobWizardAdvanced.cancelAddDetectorModal(); + } + }); + + it('job cloning pre-fills the bucket span', async () => { + await ml.jobWizardCommon.assertBucketSpanInputExists(); + await ml.jobWizardCommon.assertBucketSpanValue(testData.pickFieldsConfig.bucketSpan); + }); + + it('job cloning pre-fills influencers', async () => { + await ml.jobWizardCommon.assertInfluencerInputExists(); + await ml.jobWizardCommon.assertInfluencerSelection(testData.pickFieldsConfig.influencers); + }); + + it('job cloning pre-fills the model memory limit', async () => { + await ml.jobWizardCommon.assertModelMemoryLimitInputExists({ + withAdvancedSection: false, + }); + await ml.jobWizardCommon.assertModelMemoryLimitValue( + testData.pickFieldsConfig.memoryLimit, + { + withAdvancedSection: false, + } + ); + }); + + it('job cloning displays the job details step', async () => { + await ml.jobWizardCommon.advanceToJobDetailsSection(); + }); + + it('job cloning does not pre-fill the job id', async () => { + await ml.jobWizardCommon.assertJobIdInputExists(); + await ml.jobWizardCommon.assertJobIdValue(''); + }); + + it('job cloning inputs the clone job id', async () => { + await ml.jobWizardCommon.setJobId(testData.jobIdClone); + }); + + it('job cloning pre-fills the job description', async () => { + await ml.jobWizardCommon.assertJobDescriptionInputExists(); + await ml.jobWizardCommon.assertJobDescriptionValue(testData.jobDescription); + }); + + it('job cloning pre-fills job groups', async () => { + await ml.jobWizardCommon.assertJobGroupInputExists(); + await ml.jobWizardCommon.assertJobGroupSelection(testData.jobGroups); + }); + + it('job cloning inputs the clone job group', async () => { + await ml.jobWizardCommon.assertJobGroupInputExists(); + await ml.jobWizardCommon.addJobGroup('clone'); + await ml.jobWizardCommon.assertJobGroupSelection(testData.jobGroupsClone); + }); + + it('job cloning pre-fills the model plot switch', async () => { + await ml.jobWizardCommon.assertModelPlotSwitchExists({ withAdvancedSection: false }); + await ml.jobWizardCommon.assertModelPlotSwitchCheckedState(false, { + withAdvancedSection: false, + }); + }); + + it('job cloning pre-fills the dedicated index switch', async () => { + await ml.jobWizardCommon.assertDedicatedIndexSwitchExists({ withAdvancedSection: false }); + await ml.jobWizardCommon.assertDedicatedIndexSwitchCheckedState(true, { + withAdvancedSection: false, + }); + }); + + it('job cloning displays the validation step', async () => { + await ml.jobWizardCommon.advanceToValidationSection(); + }); + + it('job cloning displays the summary step', async () => { + await ml.jobWizardCommon.advanceToSummarySection(); + }); + + it('job cloning creates the job and finishes processing', async () => { + await ml.jobWizardCommon.assertCreateJobButtonExists(); + await ml.jobWizardAdvanced.createJob(); + await ml.jobManagement.assertStartDatafeedModalExists(); + await ml.jobManagement.confirmStartDatafeedModal(); + await ml.jobManagement.waitForJobCompletion(testData.jobIdClone); + }); + + it('job cloning displays the created job in the job list', async () => { + await ml.jobTable.refreshJobList(); + await ml.jobTable.filterWithSearchString(testData.jobIdClone); + const rows = await ml.jobTable.parseJobTable(); + expect(rows.filter(row => row.id === testData.jobIdClone)).to.have.length(1); + }); + + it('job creation displays details for the created job in the job list', async () => { + await ml.jobTable.assertJobRowFields(testData.jobIdClone, { + id: testData.jobIdClone, + description: testData.jobDescription, + jobGroups: [...new Set(testData.jobGroupsClone)].sort(), + recordCount: testData.expected.row.recordCount, + memoryStatus: testData.expected.row.memoryStatus, + jobState: testData.expected.row.jobState, + datafeedState: testData.expected.row.datafeedState, + latestTimestamp: testData.expected.row.latestTimestamp, + }); + + await ml.jobTable.assertJobRowDetailsCounts( + testData.jobIdClone, + { + job_id: testData.jobIdClone, + processed_record_count: testData.expected.counts.processed_record_count, + processed_field_count: testData.expected.counts.processed_field_count, + input_bytes: testData.expected.counts.input_bytes, + input_field_count: testData.expected.counts.input_field_count, + invalid_date_count: testData.expected.counts.invalid_date_count, + missing_field_count: testData.expected.counts.missing_field_count, + out_of_order_timestamp_count: testData.expected.counts.out_of_order_timestamp_count, + empty_bucket_count: testData.expected.counts.empty_bucket_count, + sparse_bucket_count: testData.expected.counts.sparse_bucket_count, + bucket_count: testData.expected.counts.bucket_count, + earliest_record_timestamp: testData.expected.counts.earliest_record_timestamp, + latest_record_timestamp: testData.expected.counts.latest_record_timestamp, + input_record_count: testData.expected.counts.input_record_count, + latest_bucket_timestamp: testData.expected.counts.latest_bucket_timestamp, + }, + { + job_id: testData.jobIdClone, + result_type: testData.expected.modelSizeStats.result_type, + model_bytes_exceeded: testData.expected.modelSizeStats.model_bytes_exceeded, + model_bytes_memory_limit: testData.expected.modelSizeStats.model_bytes_memory_limit, + total_by_field_count: testData.expected.modelSizeStats.total_by_field_count, + total_over_field_count: testData.expected.modelSizeStats.total_over_field_count, + total_partition_field_count: + testData.expected.modelSizeStats.total_partition_field_count, + bucket_allocation_failures_count: + testData.expected.modelSizeStats.bucket_allocation_failures_count, + memory_status: testData.expected.modelSizeStats.memory_status, + timestamp: testData.expected.modelSizeStats.timestamp, + } + ); + }); + + it('job creation has detector results', async () => { + for (let i = 0; i < testData.pickFieldsConfig.detectors.length; i++) { + await ml.api.assertDetectorResultsExist(testData.jobIdClone, i); + } + }); + }); + } + }); +} diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/index.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/index.ts index 8d2e58bb0614d..ba307a24cd739 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/index.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/index.ts @@ -11,5 +11,6 @@ export default function({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./multi_metric_job')); loadTestFile(require.resolve('./population_job')); loadTestFile(require.resolve('./saved_search_job')); + loadTestFile(require.resolve('./advanced_job')); }); } diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/multi_metric_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/multi_metric_job.ts index 6163e99b5eaa4..11cb48de260f1 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/multi_metric_job.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/multi_metric_job.ts @@ -221,6 +221,12 @@ export default function({ getService }: FtrProviderContext) { ); }); + it('job creation has detector results', async () => { + for (let i = 0; i < aggAndFieldIdentifiers.length; i++) { + await ml.api.assertDetectorResultsExist(jobId, i); + } + }); + it('job cloning clicks the clone action and loads the multi metric wizard', async () => { await ml.jobTable.clickCloneJobAction(jobId); await ml.jobTypeSelection.assertMultiMetricJobWizardOpen(); @@ -258,7 +264,7 @@ export default function({ getService }: FtrProviderContext) { it('job cloning pre-fills the split field', async () => { await ml.jobWizardMultiMetric.assertSplitFieldInputExists(); - await ml.jobWizardMultiMetric.assertSplitFieldSelection(splitField); + await ml.jobWizardMultiMetric.assertSplitFieldSelection([splitField]); }); it('job cloning pre-fills influencers', async () => { @@ -351,5 +357,11 @@ export default function({ getService }: FtrProviderContext) { getExpectedModelSizeStats(jobIdClone) ); }); + + it('job cloning has detector results', async () => { + for (let i = 0; i < aggAndFieldIdentifiers.length; i++) { + await ml.api.assertDetectorResultsExist(jobId, i); + } + }); }); } diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/population_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/population_job.ts index 7ccd9214591f2..71e66cc569f4e 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/population_job.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/population_job.ts @@ -248,6 +248,12 @@ export default function({ getService }: FtrProviderContext) { ); }); + it('job creation has detector results', async () => { + for (let i = 0; i < detectors.length; i++) { + await ml.api.assertDetectorResultsExist(jobId, i); + } + }); + it('job cloning clicks the clone action and loads the population wizard', async () => { await ml.jobTable.clickCloneJobAction(jobId); await ml.jobTypeSelection.assertPopulationJobWizardOpen(); @@ -275,14 +281,16 @@ export default function({ getService }: FtrProviderContext) { it('job cloning pre-fills the population field', async () => { await ml.jobWizardPopulation.assertPopulationFieldInputExists(); - await ml.jobWizardPopulation.assertPopulationFieldSelection(populationField); + await ml.jobWizardPopulation.assertPopulationFieldSelection([populationField]); }); it('job cloning pre-fills detectors and shows preview with split cards', async () => { for (const [index, detector] of detectors.entries()) { await ml.jobWizardCommon.assertDetectorPreviewExists(detector.identifier, index, 'SCATTER'); - await ml.jobWizardPopulation.assertDetectorSplitFieldSelection(index, detector.splitField); + await ml.jobWizardPopulation.assertDetectorSplitFieldSelection(index, [ + detector.splitField, + ]); await ml.jobWizardPopulation.assertDetectorSplitExists(index); await ml.jobWizardPopulation.assertDetectorSplitFrontCardTitle( index, @@ -387,5 +395,11 @@ export default function({ getService }: FtrProviderContext) { getExpectedModelSizeStats(jobIdClone) ); }); + + it('job cloning has detector results', async () => { + for (let i = 0; i < detectors.length; i++) { + await ml.api.assertDetectorResultsExist(jobId, i); + } + }); }); } diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/saved_search_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/saved_search_job.ts index 5645bc7277d19..0330e141b0890 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/saved_search_job.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/saved_search_job.ts @@ -468,6 +468,12 @@ export default function({ getService }: FtrProviderContext) { } ); }); + + it('has detector results', async () => { + for (let i = 0; i < testData.aggAndFieldIdentifiers.length; i++) { + await ml.api.assertDetectorResultsExist(testData.jobId, i); + } + }); }); } }); diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/single_metric_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/single_metric_job.ts index 06ec840b36aae..b5a544b7af9f6 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/single_metric_job.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/single_metric_job.ts @@ -202,6 +202,10 @@ export default function({ getService }: FtrProviderContext) { ); }); + it('job creation has detector results', async () => { + await ml.api.assertDetectorResultsExist(jobId, 0); + }); + it('job cloning clicks the clone action and loads the single metric wizard', async () => { await ml.jobTable.clickCloneJobAction(jobId); await ml.jobTypeSelection.assertSingleMetricJobWizardOpen(); @@ -319,6 +323,10 @@ export default function({ getService }: FtrProviderContext) { ); }); + it('job cloning has detector results', async () => { + await ml.api.assertDetectorResultsExist(jobId, 0); + }); + it('job deletion has results for the job before deletion', async () => { await ml.api.assertJobResultsExist(jobIdClone); }); diff --git a/x-pack/test/functional/es_archives/ml/farequote/mappings.json b/x-pack/test/functional/es_archives/ml/farequote/mappings.json index b4c6be7655805..4fe559cc85fe1 100644 --- a/x-pack/test/functional/es_archives/ml/farequote/mappings.json +++ b/x-pack/test/functional/es_archives/ml/farequote/mappings.json @@ -21,24 +21,6 @@ "airline": { "type": "keyword" }, - "host": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, - "path": { - "fields": { - "keyword": { - "ignore_above": 256, - "type": "keyword" - } - }, - "type": "text" - }, "responsetime": { "type": "float" }, @@ -1102,4 +1084,4 @@ } } } -} \ No newline at end of file +} diff --git a/x-pack/test/functional/services/machine_learning/api.ts b/x-pack/test/functional/services/machine_learning/api.ts index 148e276dc4a14..270722a97d6b6 100644 --- a/x-pack/test/functional/services/machine_learning/api.ts +++ b/x-pack/test/functional/services/machine_learning/api.ts @@ -8,10 +8,13 @@ import expect from '@kbn/expect'; import { isEmpty } from 'lodash'; import { FtrProviderContext } from '../../ftr_provider_context'; +import { JOB_STATE, DATAFEED_STATE } from '../../../../legacy/plugins/ml/common/constants/states'; + export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { const es = getService('es'); const log = getService('log'); const retry = getService('retry'); + const esSupertest = getService('esSupertest'); return { async hasJobResults(jobId: string): Promise { @@ -54,6 +57,54 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { ); }, + async hasDetectorResults(jobId: string, detectorIndex: number): Promise { + const response = await es.search({ + index: '.ml-anomalies-*', + body: { + size: 1, + query: { + bool: { + must: [ + { + match: { + job_id: jobId, + }, + }, + { + match: { + result_type: 'record', + }, + }, + { + match: { + detector_index: detectorIndex, + }, + }, + ], + }, + }, + }, + }); + + return response.hits.hits.length > 0; + }, + + async assertDetectorResultsExist(jobId: string, detectorIndex: number) { + await retry.waitForWithTimeout( + `results for detector ${detectorIndex} on job ${jobId} to exist`, + 30 * 1000, + async () => { + if ((await this.hasDetectorResults(jobId, detectorIndex)) === true) { + return true; + } else { + throw new Error( + `expected results for detector ${detectorIndex} on job '${jobId}' to exist` + ); + } + } + ); + }, + async deleteIndices(indices: string) { log.debug(`Deleting indices: '${indices}'...`); const deleteResponse = await es.indices.delete({ @@ -79,5 +130,61 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { async cleanMlIndices() { await this.deleteIndices('.ml-*'); }, + + async getJobState(jobId: string): Promise { + log.debug(`Fetching job state for job ${jobId}`); + const jobStats = await esSupertest + .get(`/_ml/anomaly_detectors/${jobId}/_stats`) + .expect(200) + .then((res: any) => res.body); + + expect(jobStats.jobs).to.have.length(1); + const state: JOB_STATE = jobStats.jobs[0].state; + + return state; + }, + + async waitForJobState(jobId: string, expectedJobState: JOB_STATE) { + await retry.waitForWithTimeout( + `job state to be ${expectedJobState}`, + 2 * 60 * 1000, + async () => { + const state = await this.getJobState(jobId); + if (state === expectedJobState) { + return true; + } else { + throw new Error(`expected job state to be ${expectedJobState} but got ${state}`); + } + } + ); + }, + + async getDatafeedState(datafeedId: string): Promise { + log.debug(`Fetching datafeed state for datafeed ${datafeedId}`); + const datafeedStats = await esSupertest + .get(`/_ml/datafeeds/${datafeedId}/_stats`) + .expect(200) + .then((res: any) => res.body); + + expect(datafeedStats.datafeeds).to.have.length(1); + const state: DATAFEED_STATE = datafeedStats.datafeeds[0].state; + + return state; + }, + + async waitForDatafeedState(datafeedId: string, expectedDatafeedState: DATAFEED_STATE) { + await retry.waitForWithTimeout( + `datafeed state to be ${expectedDatafeedState}`, + 2 * 60 * 1000, + async () => { + const state = await this.getDatafeedState(datafeedId); + if (state === expectedDatafeedState) { + return true; + } else { + throw new Error(`expected job state to be ${expectedDatafeedState} but got ${state}`); + } + } + ); + }, }; } diff --git a/x-pack/test/functional/services/machine_learning/index.ts b/x-pack/test/functional/services/machine_learning/index.ts index 0dc588ffcc9e5..c5ebe3e9cc156 100644 --- a/x-pack/test/functional/services/machine_learning/index.ts +++ b/x-pack/test/functional/services/machine_learning/index.ts @@ -12,6 +12,7 @@ export { MachineLearningJobManagementProvider } from './job_management'; export { MachineLearningJobSourceSelectionProvider } from './job_source_selection'; export { MachineLearningJobTableProvider } from './job_table'; export { MachineLearningJobTypeSelectionProvider } from './job_type_selection'; +export { MachineLearningJobWizardAdvancedProvider } from './job_wizard_advanced'; export { MachineLearningJobWizardCommonProvider } from './job_wizard_common'; export { MachineLearningJobWizardMultiMetricProvider } from './job_wizard_multi_metric'; export { MachineLearningJobWizardPopulationProvider } from './job_wizard_population'; diff --git a/x-pack/test/functional/services/machine_learning/job_management.ts b/x-pack/test/functional/services/machine_learning/job_management.ts index 44cf06ab44735..ddab5fd68f13c 100644 --- a/x-pack/test/functional/services/machine_learning/job_management.ts +++ b/x-pack/test/functional/services/machine_learning/job_management.ts @@ -3,11 +3,19 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import { ProvidedType } from '@kbn/test/types/ftr'; import { FtrProviderContext } from '../../ftr_provider_context'; +import { MachineLearningAPIProvider } from './api'; -export function MachineLearningJobManagementProvider({ getService }: FtrProviderContext) { +import { JOB_STATE, DATAFEED_STATE } from '../../../../legacy/plugins/ml/common/constants/states'; + +export function MachineLearningJobManagementProvider( + { getService }: FtrProviderContext, + mlApi: ProvidedType +) { const testSubjects = getService('testSubjects'); + const retry = getService('retry'); return { async navigateToNewJobSourceSelection() { @@ -26,5 +34,22 @@ export function MachineLearningJobManagementProvider({ getService }: FtrProvider async assertJobStatsBarExists() { await testSubjects.existOrFail('~mlJobStatsBar'); }, + + async assertStartDatafeedModalExists() { + // this retry can be removed as soon as #48734 is merged + await retry.tryForTime(5000, async () => { + await testSubjects.existOrFail('mlStartDatafeedModal'); + }); + }, + + async confirmStartDatafeedModal() { + await testSubjects.click('mlStartDatafeedModalStartButton'); + await testSubjects.missingOrFail('mlStartDatafeedModal'); + }, + + async waitForJobCompletion(jobId: string) { + await mlApi.waitForDatafeedState(`datafeed-${jobId}`, DATAFEED_STATE.STOPPED); + await mlApi.waitForJobState(jobId, JOB_STATE.CLOSED); + }, }; } diff --git a/x-pack/test/functional/services/machine_learning/job_table.ts b/x-pack/test/functional/services/machine_learning/job_table.ts index 17c5d0f54eaa3..7eded43d1f058 100644 --- a/x-pack/test/functional/services/machine_learning/job_table.ts +++ b/x-pack/test/functional/services/machine_learning/job_table.ts @@ -157,6 +157,11 @@ export function MachineLearningJobTableProvider({ getService }: FtrProviderConte }); } + public async refreshJobList() { + await testSubjects.click('mlRefreshJobListButton'); + await this.waitForJobsToLoad(); + } + public async waitForJobsToLoad() { await testSubjects.existOrFail('~mlJobListTable', { timeout: 60 * 1000 }); await testSubjects.existOrFail('mlJobListTable loaded', { timeout: 30 * 1000 }); @@ -206,7 +211,7 @@ export function MachineLearningJobTableProvider({ getService }: FtrProviderConte } public async clickActionsMenu(jobId: string) { - retry.tryForTime(30 * 1000, async () => { + await retry.tryForTime(30 * 1000, async () => { if (!(await testSubjects.exists('mlActionButtonDeleteJob'))) { await testSubjects.click(this.rowSelector(jobId, 'euiCollapsedItemActionsButton')); await testSubjects.existOrFail('mlActionButtonDeleteJob', { timeout: 5000 }); diff --git a/x-pack/test/functional/services/machine_learning/job_type_selection.ts b/x-pack/test/functional/services/machine_learning/job_type_selection.ts index 0957558f62165..6686b5b28f200 100644 --- a/x-pack/test/functional/services/machine_learning/job_type_selection.ts +++ b/x-pack/test/functional/services/machine_learning/job_type_selection.ts @@ -36,5 +36,14 @@ export function MachineLearningJobTypeSelectionProvider({ getService }: FtrProvi async assertPopulationJobWizardOpen() { await testSubjects.existOrFail('mlPageJobWizard population'); }, + + async selectAdvancedJob() { + await testSubjects.clickWhenNotDisabled('mlJobTypeLinkAdvancedJob'); + await this.assertAdvancedJobWizardOpen(); + }, + + async assertAdvancedJobWizardOpen() { + await testSubjects.existOrFail('mlPageJobWizard advanced'); + }, }; } diff --git a/x-pack/test/functional/services/machine_learning/job_wizard_advanced.ts b/x-pack/test/functional/services/machine_learning/job_wizard_advanced.ts new file mode 100644 index 0000000000000..71b76a6885592 --- /dev/null +++ b/x-pack/test/functional/services/machine_learning/job_wizard_advanced.ts @@ -0,0 +1,315 @@ +/* + * 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 expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export function MachineLearningJobWizardAdvancedProvider({ + getService, + getPageObjects, +}: FtrProviderContext) { + const comboBox = getService('comboBox'); + const testSubjects = getService('testSubjects'); + const retry = getService('retry'); + const aceEditor = getService('aceEditor'); + + return { + async getValueOrPlaceholder(inputLocator: string): Promise { + const value = await testSubjects.getAttribute(inputLocator, 'value'); + if (value !== '') { + return value; + } else { + return await testSubjects.getAttribute(inputLocator, 'placeholder'); + } + }, + + async assertDatafeedQueryEditorExists() { + await testSubjects.existOrFail('mlAdvancedDatafeedQueryEditor > codeEditorContainer'); + }, + + async assertDatafeedQueryEditorValue(expectedValue: string) { + const actualValue = await aceEditor.getValue( + 'mlAdvancedDatafeedQueryEditor > codeEditorContainer' + ); + expect(actualValue).to.eql(expectedValue); + }, + + async assertQueryDelayInputExists() { + await testSubjects.existOrFail('mlJobWizardInputQueryDelay'); + }, + + async assertQueryDelayValue(expectedValue: string) { + const actualQueryDelay = await this.getValueOrPlaceholder('mlJobWizardInputQueryDelay'); + expect(actualQueryDelay).to.eql(expectedValue); + }, + + async setQueryDelay(queryDelay: string) { + await testSubjects.setValue('mlJobWizardInputQueryDelay', queryDelay, { + clearWithKeyboard: true, + typeCharByChar: true, + }); + await this.assertQueryDelayValue(queryDelay); + }, + + async assertFrequencyInputExists() { + await testSubjects.existOrFail('mlJobWizardInputFrequency'); + }, + + async assertFrequencyValue(expectedValue: string) { + const actualFrequency = await this.getValueOrPlaceholder('mlJobWizardInputFrequency'); + expect(actualFrequency).to.eql(expectedValue); + }, + + async setFrequency(frequency: string) { + await testSubjects.setValue('mlJobWizardInputFrequency', frequency, { + clearWithKeyboard: true, + typeCharByChar: true, + }); + await this.assertFrequencyValue(frequency); + }, + + async assertScrollSizeInputExists() { + await testSubjects.existOrFail('mlJobWizardInputScrollSize'); + }, + + async assertScrollSizeValue(expectedValue: string) { + const actualScrollSize = await this.getValueOrPlaceholder('mlJobWizardInputScrollSize'); + expect(actualScrollSize).to.eql(expectedValue); + }, + + async setScrollSize(scrollSize: string) { + await testSubjects.setValue('mlJobWizardInputScrollSize', scrollSize, { + clearWithKeyboard: true, + typeCharByChar: true, + }); + await this.assertScrollSizeValue(scrollSize); + }, + + async assertTimeFieldInputExists() { + await testSubjects.existOrFail('mlTimeFieldNameSelect > comboBoxInput'); + }, + + async assertTimeFieldSelection(expectedIdentifier: string[]) { + const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( + 'mlTimeFieldNameSelect > comboBoxInput' + ); + expect(comboBoxSelectedOptions).to.eql(expectedIdentifier); + }, + + async selectTimeField(identifier: string) { + await comboBox.set('mlTimeFieldNameSelect > comboBoxInput', identifier); + await this.assertTimeFieldSelection([identifier]); + }, + + async assertCategorizationFieldInputExists() { + await testSubjects.existOrFail('mlCategorizationFieldNameSelect > comboBoxInput'); + }, + + async assertCategorizationFieldSelection(expectedIdentifier: string[]) { + const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( + 'mlCategorizationFieldNameSelect > comboBoxInput' + ); + expect(comboBoxSelectedOptions).to.eql(expectedIdentifier); + }, + + async selectCategorizationField(identifier: string) { + await comboBox.set('mlCategorizationFieldNameSelect > comboBoxInput', identifier); + await this.assertCategorizationFieldSelection([identifier]); + }, + + async assertSummaryCountFieldInputExists() { + await testSubjects.existOrFail('mlSummaryCountFieldNameSelect > comboBoxInput'); + }, + + async assertSummaryCountFieldSelection(expectedIdentifier: string[]) { + const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( + 'mlSummaryCountFieldNameSelect > comboBoxInput' + ); + expect(comboBoxSelectedOptions).to.eql(expectedIdentifier); + }, + + async selectSummaryCountField(identifier: string) { + await comboBox.set('mlSummaryCountFieldNameSelect > comboBoxInput', identifier); + await this.assertSummaryCountFieldSelection([identifier]); + }, + + async assertAddDetectorButtonExists() { + await testSubjects.existOrFail('mlAddDetectorButton'); + }, + + async openCreateDetectorModal() { + await testSubjects.click('mlAddDetectorButton'); + await this.assertCreateDetectorModalExists(); + }, + + async assertCreateDetectorModalExists() { + // this retry can be removed as soon as #48734 is merged + await retry.tryForTime(5000, async () => { + await testSubjects.existOrFail('mlCreateDetectorModal'); + }); + }, + + async assertDetectorFunctionInputExists() { + await testSubjects.existOrFail('mlAdvancedFunctionSelect > comboBoxInput'); + }, + + async assertDetectorFunctionSelection(expectedIdentifier: string[]) { + const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( + 'mlAdvancedFunctionSelect > comboBoxInput' + ); + expect(comboBoxSelectedOptions).to.eql(expectedIdentifier); + }, + + async selectDetectorFunction(identifier: string) { + await comboBox.set('mlAdvancedFunctionSelect > comboBoxInput', identifier); + await this.assertDetectorFunctionSelection([identifier]); + }, + + async assertDetectorFieldInputExists() { + await testSubjects.existOrFail('mlAdvancedFieldSelect > comboBoxInput'); + }, + + async assertDetectorFieldSelection(expectedIdentifier: string[]) { + const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( + 'mlAdvancedFieldSelect > comboBoxInput' + ); + expect(comboBoxSelectedOptions).to.eql(expectedIdentifier); + }, + + async selectDetectorField(identifier: string) { + await comboBox.set('mlAdvancedFieldSelect > comboBoxInput', identifier); + await this.assertDetectorFieldSelection([identifier]); + }, + + async assertDetectorByFieldInputExists() { + await testSubjects.existOrFail('mlAdvancedByFieldSelect > comboBoxInput'); + }, + + async assertDetectorByFieldSelection(expectedIdentifier: string[]) { + const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( + 'mlAdvancedByFieldSelect > comboBoxInput' + ); + expect(comboBoxSelectedOptions).to.eql(expectedIdentifier); + }, + + async selectDetectorByField(identifier: string) { + await comboBox.set('mlAdvancedByFieldSelect > comboBoxInput', identifier); + await this.assertDetectorByFieldSelection([identifier]); + }, + + async assertDetectorOverFieldInputExists() { + await testSubjects.existOrFail('mlAdvancedOverFieldSelect > comboBoxInput'); + }, + + async assertDetectorOverFieldSelection(expectedIdentifier: string[]) { + const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( + 'mlAdvancedOverFieldSelect > comboBoxInput' + ); + expect(comboBoxSelectedOptions).to.eql(expectedIdentifier); + }, + + async selectDetectorOverField(identifier: string) { + await comboBox.set('mlAdvancedOverFieldSelect > comboBoxInput', identifier); + await this.assertDetectorOverFieldSelection([identifier]); + }, + + async assertDetectorPartitionFieldInputExists() { + await testSubjects.existOrFail('mlAdvancedPartitionFieldSelect > comboBoxInput'); + }, + + async assertDetectorPartitionFieldSelection(expectedIdentifier: string[]) { + const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( + 'mlAdvancedPartitionFieldSelect > comboBoxInput' + ); + expect(comboBoxSelectedOptions).to.eql(expectedIdentifier); + }, + + async selectDetectorPartitionField(identifier: string) { + await comboBox.set('mlAdvancedPartitionFieldSelect > comboBoxInput', identifier); + await this.assertDetectorPartitionFieldSelection([identifier]); + }, + + async assertDetectorExcludeFrequentInputExists() { + await testSubjects.existOrFail('mlAdvancedExcludeFrequentSelect > comboBoxInput'); + }, + + async assertDetectorExcludeFrequentSelection(expectedIdentifier: string[]) { + const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( + 'mlAdvancedExcludeFrequentSelect > comboBoxInput' + ); + expect(comboBoxSelectedOptions).to.eql(expectedIdentifier); + }, + + async selectDetectorExcludeFrequent(identifier: string) { + await comboBox.set('mlAdvancedExcludeFrequentSelect > comboBoxInput', identifier); + await this.assertDetectorExcludeFrequentSelection([identifier]); + }, + + async assertDetectorDescriptionInputExists() { + await testSubjects.existOrFail('mlAdvancedDetectorDescriptionInput'); + }, + + async assertDetectorDescriptionValue(expectedValue: string) { + const actualDetectorDescription = await testSubjects.getAttribute( + 'mlAdvancedDetectorDescriptionInput', + 'value' + ); + expect(actualDetectorDescription).to.eql(expectedValue); + }, + + async setDetectorDescription(description: string) { + await testSubjects.setValue('mlAdvancedDetectorDescriptionInput', description, { + clearWithKeyboard: true, + }); + await this.assertDetectorDescriptionValue(description); + }, + + async confirmAddDetectorModal() { + await testSubjects.clickWhenNotDisabled('mlCreateDetectorModalSaveButton'); + await testSubjects.missingOrFail('mlCreateDetectorModal'); + }, + + async cancelAddDetectorModal() { + await testSubjects.clickWhenNotDisabled('mlCreateDetectorModalCancelButton'); + await testSubjects.missingOrFail('mlCreateDetectorModal'); + }, + + async assertDetectorEntryExists( + detectorIndex: number, + expectedDetectorName: string, + expectedDetectorDescription?: string + ) { + await testSubjects.existOrFail(`mlAdvancedDetector ${detectorIndex}`); + + const actualDetectorIdentifier = await testSubjects.getVisibleText( + `mlAdvancedDetector ${detectorIndex} > mlDetectorIdentifier` + ); + expect(actualDetectorIdentifier).to.eql(expectedDetectorName); + + if (expectedDetectorDescription !== undefined) { + const actualDetectorDescription = await testSubjects.getVisibleText( + `mlAdvancedDetector ${detectorIndex} > mlDetectorDescription` + ); + expect(actualDetectorDescription).to.eql(expectedDetectorDescription); + } + }, + + async clickEditDetector(detectorIndex: number) { + await testSubjects.click( + `mlAdvancedDetector ${detectorIndex} > mlAdvancedDetectorEditButton` + ); + await this.assertCreateDetectorModalExists(); + }, + + async createJob() { + await testSubjects.clickWhenNotDisabled('mlJobWizardButtonCreateJob'); + // this retry can be removed as soon as #48734 is merged + await retry.tryForTime(5000, async () => { + await testSubjects.existOrFail('mlStartDatafeedModal'); + }); + }, + }; +} diff --git a/x-pack/test/functional/services/machine_learning/job_wizard_common.ts b/x-pack/test/functional/services/machine_learning/job_wizard_common.ts index 73764e8f36518..3a71f96fa3fbd 100644 --- a/x-pack/test/functional/services/machine_learning/job_wizard_common.ts +++ b/x-pack/test/functional/services/machine_learning/job_wizard_common.ts @@ -12,6 +12,15 @@ export function MachineLearningJobWizardCommonProvider({ getService }: FtrProvid const retry = getService('retry'); const testSubjects = getService('testSubjects'); + interface SectionOptions { + withAdvancedSection: boolean; + } + + function advancedSectionSelector(subSelector?: string) { + const subj = 'mlJobWizardAdvancedSection'; + return !subSelector ? subj : `${subj} > ${subSelector}`; + } + return { async clickNextButton() { await testSubjects.existOrFail('mlJobWizardNavButtonNext'); @@ -38,6 +47,10 @@ export function MachineLearningJobWizardCommonProvider({ getService }: FtrProvid await testSubjects.existOrFail('mlJobWizardStepTitleSummary'); }, + async assertConfigureDatafeedSectionExists() { + await testSubjects.existOrFail('mlJobWizardStepTitleConfigureDatafeed'); + }, + async advanceToPickFieldsSection() { await this.clickNextButton(); await this.assertPickFieldsSectionExists(); @@ -153,75 +166,127 @@ export function MachineLearningJobWizardCommonProvider({ getService }: FtrProvid expect(await this.getSelectedJobGroups()).to.contain(jobGroup); }, - async assertModelPlotSwitchExists() { - await this.ensureAdvancedSectionOpen(); - await testSubjects.existOrFail('mlJobWizardAdvancedSection > mlJobWizardSwitchModelPlot', { - allowHidden: true, - }); + async assertModelPlotSwitchExists( + sectionOptions: SectionOptions = { withAdvancedSection: true } + ) { + let subj = 'mlJobWizardSwitchModelPlot'; + if (sectionOptions.withAdvancedSection === true) { + await this.ensureAdvancedSectionOpen(); + subj = advancedSectionSelector(subj); + } + await testSubjects.existOrFail(subj, { allowHidden: true }); }, - async getModelPlotSwitchCheckedState(): Promise { - await this.ensureAdvancedSectionOpen(); - return await testSubjects.isSelected( - 'mlJobWizardAdvancedSection > mlJobWizardSwitchModelPlot' - ); + async getModelPlotSwitchCheckedState( + sectionOptions: SectionOptions = { withAdvancedSection: true } + ): Promise { + let subj = 'mlJobWizardSwitchModelPlot'; + if (sectionOptions.withAdvancedSection === true) { + await this.ensureAdvancedSectionOpen(); + subj = advancedSectionSelector(subj); + } + return await testSubjects.isSelected(subj); }, - async assertModelPlotSwitchCheckedState(expectedValue: boolean) { - await this.ensureAdvancedSectionOpen(); - const actualCheckedState = await this.getModelPlotSwitchCheckedState(); + async assertModelPlotSwitchCheckedState( + expectedValue: boolean, + sectionOptions: SectionOptions = { withAdvancedSection: true } + ) { + const actualCheckedState = await this.getModelPlotSwitchCheckedState({ + withAdvancedSection: sectionOptions.withAdvancedSection, + }); expect(actualCheckedState).to.eql(expectedValue); }, - async assertDedicatedIndexSwitchExists() { - await this.ensureAdvancedSectionOpen(); - await testSubjects.existOrFail( - 'mlJobWizardAdvancedSection > mlJobWizardSwitchUseDedicatedIndex', - { allowHidden: true } - ); + async assertDedicatedIndexSwitchExists( + sectionOptions: SectionOptions = { withAdvancedSection: true } + ) { + let subj = 'mlJobWizardSwitchUseDedicatedIndex'; + if (sectionOptions.withAdvancedSection === true) { + await this.ensureAdvancedSectionOpen(); + subj = advancedSectionSelector(subj); + } + await testSubjects.existOrFail(subj, { allowHidden: true }); }, - async getDedicatedIndexSwitchCheckedState(): Promise { - await this.ensureAdvancedSectionOpen(); - return await testSubjects.isSelected( - 'mlJobWizardAdvancedSection > mlJobWizardSwitchUseDedicatedIndex' - ); + async getDedicatedIndexSwitchCheckedState( + sectionOptions: SectionOptions = { withAdvancedSection: true } + ): Promise { + let subj = 'mlJobWizardSwitchUseDedicatedIndex'; + if (sectionOptions.withAdvancedSection === true) { + await this.ensureAdvancedSectionOpen(); + subj = advancedSectionSelector(subj); + } + return await testSubjects.isSelected(subj); }, - async assertDedicatedIndexSwitchCheckedState(expectedValue: boolean) { - await this.ensureAdvancedSectionOpen(); - const actualCheckedState = await this.getDedicatedIndexSwitchCheckedState(); + async assertDedicatedIndexSwitchCheckedState( + expectedValue: boolean, + sectionOptions: SectionOptions = { withAdvancedSection: true } + ) { + const actualCheckedState = await this.getDedicatedIndexSwitchCheckedState({ + withAdvancedSection: sectionOptions.withAdvancedSection, + }); expect(actualCheckedState).to.eql(expectedValue); }, - async activateDedicatedIndexSwitch() { - if ((await this.getDedicatedIndexSwitchCheckedState()) === false) { - await testSubjects.clickWhenNotDisabled('mlJobWizardSwitchUseDedicatedIndex'); + async activateDedicatedIndexSwitch( + sectionOptions: SectionOptions = { withAdvancedSection: true } + ) { + let subj = 'mlJobWizardSwitchUseDedicatedIndex'; + if (sectionOptions.withAdvancedSection === true) { + await this.ensureAdvancedSectionOpen(); + subj = advancedSectionSelector(subj); + } + if ( + (await this.getDedicatedIndexSwitchCheckedState({ + withAdvancedSection: sectionOptions.withAdvancedSection, + })) === false + ) { + await testSubjects.clickWhenNotDisabled(subj); } - await this.assertDedicatedIndexSwitchCheckedState(true); + await this.assertDedicatedIndexSwitchCheckedState(true, { + withAdvancedSection: sectionOptions.withAdvancedSection, + }); }, - async assertModelMemoryLimitInputExists() { - await this.ensureAdvancedSectionOpen(); - await testSubjects.existOrFail( - 'mlJobWizardAdvancedSection > mlJobWizardInputModelMemoryLimit' - ); + async assertModelMemoryLimitInputExists( + sectionOptions: SectionOptions = { withAdvancedSection: true } + ) { + let subj = 'mlJobWizardInputModelMemoryLimit'; + if (sectionOptions.withAdvancedSection === true) { + await this.ensureAdvancedSectionOpen(); + subj = advancedSectionSelector(subj); + } + await testSubjects.existOrFail(subj); }, - async assertModelMemoryLimitValue(expectedValue: string) { - await this.ensureAdvancedSectionOpen(); - const actualModelMemoryLimit = await testSubjects.getAttribute( - 'mlJobWizardAdvancedSection > mlJobWizardInputModelMemoryLimit', - 'value' - ); + async assertModelMemoryLimitValue( + expectedValue: string, + sectionOptions: SectionOptions = { withAdvancedSection: true } + ) { + let subj = 'mlJobWizardInputModelMemoryLimit'; + if (sectionOptions.withAdvancedSection === true) { + await this.ensureAdvancedSectionOpen(); + subj = advancedSectionSelector(subj); + } + const actualModelMemoryLimit = await testSubjects.getAttribute(subj, 'value'); expect(actualModelMemoryLimit).to.eql(expectedValue); }, - async setModelMemoryLimit(modelMemoryLimit: string) { - await testSubjects.setValue('mlJobWizardInputModelMemoryLimit', modelMemoryLimit, { - clearWithKeyboard: true, + async setModelMemoryLimit( + modelMemoryLimit: string, + sectionOptions: SectionOptions = { withAdvancedSection: true } + ) { + let subj = 'mlJobWizardInputModelMemoryLimit'; + if (sectionOptions.withAdvancedSection === true) { + await this.ensureAdvancedSectionOpen(); + subj = advancedSectionSelector(subj); + } + await testSubjects.setValue(subj, modelMemoryLimit, { clearWithKeyboard: true }); + await this.assertModelMemoryLimitValue(modelMemoryLimit, { + withAdvancedSection: sectionOptions.withAdvancedSection, }); - await this.assertModelMemoryLimitValue(modelMemoryLimit); }, async assertInfluencerInputExists() { @@ -237,7 +302,7 @@ export function MachineLearningJobWizardCommonProvider({ getService }: FtrProvid }, async addInfluencer(influencer: string) { - await comboBox.setCustom('mlInfluencerSelect > comboBoxInput', influencer); + await comboBox.set('mlInfluencerSelect > comboBoxInput', influencer); expect(await this.getSelectedInfluencers()).to.contain(influencer); }, @@ -293,16 +358,16 @@ export function MachineLearningJobWizardCommonProvider({ getService }: FtrProvid async ensureAdvancedSectionOpen() { await retry.tryForTime(5000, async () => { - if ((await testSubjects.exists('mlJobWizardAdvancedSection')) === false) { + if ((await testSubjects.exists(advancedSectionSelector())) === false) { await testSubjects.click('mlJobWizardToggleAdvancedSection'); - await testSubjects.existOrFail('mlJobWizardAdvancedSection', { timeout: 1000 }); + await testSubjects.existOrFail(advancedSectionSelector(), { timeout: 1000 }); } }); }, async createJobAndWaitForCompletion() { await testSubjects.clickWhenNotDisabled('mlJobWizardButtonCreateJob'); - await testSubjects.existOrFail('mlJobWizardButtonRunInRealTime', { timeout: 5 * 60 * 1000 }); + await testSubjects.existOrFail('mlJobWizardButtonRunInRealTime', { timeout: 2 * 60 * 1000 }); }, }; } diff --git a/x-pack/test/functional/services/machine_learning/job_wizard_multi_metric.ts b/x-pack/test/functional/services/machine_learning/job_wizard_multi_metric.ts index d9df6a9d682a7..2fb768d924cff 100644 --- a/x-pack/test/functional/services/machine_learning/job_wizard_multi_metric.ts +++ b/x-pack/test/functional/services/machine_learning/job_wizard_multi_metric.ts @@ -16,17 +16,16 @@ export function MachineLearningJobWizardMultiMetricProvider({ getService }: FtrP await testSubjects.existOrFail('mlMultiMetricSplitFieldSelect > comboBoxInput'); }, - async assertSplitFieldSelection(identifier: string) { + async assertSplitFieldSelection(expectedIdentifier: string[]) { const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( 'mlMultiMetricSplitFieldSelect > comboBoxInput' ); - expect(comboBoxSelectedOptions.length).to.eql(1); - expect(comboBoxSelectedOptions[0]).to.eql(identifier); + expect(comboBoxSelectedOptions).to.eql(expectedIdentifier); }, async selectSplitField(identifier: string) { await comboBox.set('mlMultiMetricSplitFieldSelect > comboBoxInput', identifier); - await this.assertSplitFieldSelection(identifier); + await this.assertSplitFieldSelection([identifier]); }, async assertDetectorSplitExists(splitField: string) { diff --git a/x-pack/test/functional/services/machine_learning/job_wizard_population.ts b/x-pack/test/functional/services/machine_learning/job_wizard_population.ts index 892bdaf394936..8ff9d5c12a642 100644 --- a/x-pack/test/functional/services/machine_learning/job_wizard_population.ts +++ b/x-pack/test/functional/services/machine_learning/job_wizard_population.ts @@ -16,17 +16,16 @@ export function MachineLearningJobWizardPopulationProvider({ getService }: FtrPr await testSubjects.existOrFail('mlPopulationSplitFieldSelect > comboBoxInput'); }, - async assertPopulationFieldSelection(identifier: string) { + async assertPopulationFieldSelection(expectedIdentifier: string[]) { const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( 'mlPopulationSplitFieldSelect > comboBoxInput' ); - expect(comboBoxSelectedOptions.length).to.eql(1); - expect(comboBoxSelectedOptions[0]).to.eql(identifier); + expect(comboBoxSelectedOptions).to.eql(expectedIdentifier); }, async selectPopulationField(identifier: string) { await comboBox.set('mlPopulationSplitFieldSelect > comboBoxInput', identifier); - await this.assertPopulationFieldSelection(identifier); + await this.assertPopulationFieldSelection([identifier]); }, async assertDetectorSplitFieldInputExists(detectorPosition: number) { @@ -35,12 +34,14 @@ export function MachineLearningJobWizardPopulationProvider({ getService }: FtrPr ); }, - async assertDetectorSplitFieldSelection(detectorPosition: number, identifier: string) { + async assertDetectorSplitFieldSelection( + detectorPosition: number, + expectedIdentifier: string[] + ) { const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( `mlDetector ${detectorPosition} > mlByFieldSelect > comboBoxInput` ); - expect(comboBoxSelectedOptions.length).to.eql(1); - expect(comboBoxSelectedOptions[0]).to.eql(identifier); + expect(comboBoxSelectedOptions).to.eql(expectedIdentifier); }, async selectDetectorSplitField(detectorPosition: number, identifier: string) { @@ -48,7 +49,7 @@ export function MachineLearningJobWizardPopulationProvider({ getService }: FtrPr `mlDetector ${detectorPosition} > mlByFieldSelect > comboBoxInput`, identifier ); - await this.assertDetectorSplitFieldSelection(detectorPosition, identifier); + await this.assertDetectorSplitFieldSelection(detectorPosition, [identifier]); }, async assertDetectorSplitExists(detectorPosition: number) { diff --git a/x-pack/test/functional/services/ml.ts b/x-pack/test/functional/services/ml.ts index 3feb45ae23bbc..8609552b5dc55 100644 --- a/x-pack/test/functional/services/ml.ts +++ b/x-pack/test/functional/services/ml.ts @@ -15,6 +15,7 @@ import { MachineLearningJobSourceSelectionProvider, MachineLearningJobTableProvider, MachineLearningJobTypeSelectionProvider, + MachineLearningJobWizardAdvancedProvider, MachineLearningJobWizardCommonProvider, MachineLearningJobWizardMultiMetricProvider, MachineLearningJobWizardPopulationProvider, @@ -28,10 +29,11 @@ export function MachineLearningProvider(context: FtrProviderContext) { const api = MachineLearningAPIProvider(context); const dataFrameAnalytics = MachineLearningDataFrameAnalyticsProvider(context); const dataVisualizer = MachineLearningDataVisualizerProvider(context); - const jobManagement = MachineLearningJobManagementProvider(context); + const jobManagement = MachineLearningJobManagementProvider(context, api); const jobSourceSelection = MachineLearningJobSourceSelectionProvider(context); const jobTable = MachineLearningJobTableProvider(context); const jobTypeSelection = MachineLearningJobTypeSelectionProvider(context); + const jobWizardAdvanced = MachineLearningJobWizardAdvancedProvider(context); const jobWizardCommon = MachineLearningJobWizardCommonProvider(context); const jobWizardMultiMetric = MachineLearningJobWizardMultiMetricProvider(context); const jobWizardPopulation = MachineLearningJobWizardPopulationProvider(context); @@ -48,6 +50,7 @@ export function MachineLearningProvider(context: FtrProviderContext) { jobSourceSelection, jobTable, jobTypeSelection, + jobWizardAdvanced, jobWizardCommon, jobWizardMultiMetric, jobWizardPopulation,