[ML] Add population job wizard test (#45765) (#45907)

This PR adds functional UI tests to create a machine learning job using the population wizard.
This commit is contained in:
Robert Oskamp 2019-09-17 21:54:37 +02:00 committed by GitHub
parent d2321ac427
commit 6a091380b1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 1630 additions and 47 deletions

View file

@ -35,6 +35,7 @@ export const InfluencersSelect: FC<Props> = ({ fields, changeHandler, selectedIn
selectedOptions={selection}
onChange={onChange}
isClearable={false}
data-test-subj="influencerSelect"
/>
);
};

View file

@ -34,10 +34,10 @@ export const MultiMetricSettings: FC<Props> = ({ setIsValid }) => {
return (
<Fragment>
<EuiFlexGroup gutterSize="xl">
<EuiFlexItem data-test-subj="mlJobWizardSplitFieldSelection">
<EuiFlexItem>
<SplitFieldSelector />
</EuiFlexItem>
<EuiFlexItem data-test-subj="mlJobWizardInfluencerSelection">
<EuiFlexItem>
<Influencers />
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -49,7 +49,7 @@ export const ChartGrid: FC<ChartGridProps> = ({
return (
<EuiFlexGrid columns={chartSettings.cols}>
{aggFieldPairList.map((af, i) => (
<EuiFlexItem key={i}>
<EuiFlexItem key={i} data-test-subj={`detector ${i}`}>
<Fragment>
<EuiFlexGroup>
<EuiFlexItem>

View file

@ -58,6 +58,7 @@ export const ByFieldSelector: FC<Props> = ({ detectorIndex }) => {
changeHandler={setByField}
selectedField={byField}
isClearable={true}
testSubject="byFieldSelect"
placeholder={i18n.translate(
'xpack.ml.newJob.wizard.pickFieldsStep.populationField.placeholder',
{

View file

@ -48,6 +48,13 @@ export const SplitFieldSelector: FC = () => {
changeHandler={setSplitField}
selectedField={splitField}
isClearable={canClearSelection}
testSubject={
isMultiMetricJobCreator(jc)
? 'multiMetricSplitFieldSelect'
: isPopulationJobCreator(jc)
? 'populationSplitFieldSelect'
: undefined
}
/>
</Description>
);

View file

@ -19,6 +19,7 @@ interface Props {
changeHandler(f: SplitField): void;
selectedField: SplitField;
isClearable: boolean;
testSubject?: string;
placeholder?: string;
}
@ -27,6 +28,7 @@ export const SplitFieldSelect: FC<Props> = ({
changeHandler,
selectedField,
isClearable,
testSubject,
placeholder,
}) => {
const options: EuiComboBoxOptionProps[] = fields.map(
@ -59,6 +61,7 @@ export const SplitFieldSelect: FC<Props> = ({
onChange={onChange}
isClearable={isClearable}
placeholder={placeholder}
data-test-subj={testSubject}
/>
);
};

View file

@ -46,7 +46,7 @@ export default function({ getService }: FtrProviderContext) {
await ml.jobSourceSelection.selectSourceIndexPattern('farequote');
});
it('loads the single metric job wizard page', async () => {
it('loads the multi metric job wizard page', async () => {
await ml.jobTypeSelection.selectMultiMetricJob();
});
@ -73,13 +73,13 @@ export default function({ getService }: FtrProviderContext) {
});
it('inputs the split field and displays split cards', async () => {
await ml.jobWizardCommon.assertMultiMetricSplitFieldInputExists();
await ml.jobWizardCommon.selectMultiMetricSplitField(splitField);
await ml.jobWizardCommon.assertMultiMetricSplitFieldSelection(splitField);
await ml.jobWizardMultiMetric.assertSplitFieldInputExists();
await ml.jobWizardMultiMetric.selectSplitField(splitField);
await ml.jobWizardMultiMetric.assertSplitFieldSelection(splitField);
await ml.jobWizardCommon.assertDetectorSplitExists(splitField);
await ml.jobWizardCommon.assertDetectorSplitFrontCardTitle('AAL');
await ml.jobWizardCommon.assertDetectorSplitNumberOfBackCards(9);
await ml.jobWizardMultiMetric.assertDetectorSplitExists(splitField);
await ml.jobWizardMultiMetric.assertDetectorSplitFrontCardTitle('AAL');
await ml.jobWizardMultiMetric.assertDetectorSplitNumberOfBackCards(9);
await ml.jobWizardCommon.assertInfluencerSelection([splitField]);
});

View file

@ -0,0 +1,243 @@
/*
* 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';
// eslint-disable-next-line import/no-default-export
export default function({ getService }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const ml = getService('ml');
const log = getService('log');
const jobId = `ec_population_1_${Date.now()}`;
const jobDescription =
'Create population job based on the ecommerce sample dataset with 2h bucketspan over customer_id' +
' - detectors: (Mean(products.base_price) by customer_gender), (Mean(products.quantity) by category.leyword)';
const jobGroups = ['automated', 'ecommerce', 'population'];
const populationField = 'customer_id';
const detectors = [
{
identifier: 'Mean(products.base_price)',
splitField: 'customer_gender',
frontCardTitle: 'FEMALE',
numberOfBackCards: 1,
},
{
identifier: 'Mean(products.quantity)',
splitField: 'category.keyword',
frontCardTitle: "Men's Clothing",
numberOfBackCards: 5,
},
];
const bucketSpan = '2h';
const memoryLimit = '8MB';
describe('population job creation', function() {
this.tags(['smoke', 'mlqa']);
before(async () => {
await esArchiver.loadIfNeeded('ml/ecommerce');
});
after(async () => {
await esArchiver.unload('ml/farequote');
await ml.api.cleanMlIndices();
await ml.api.cleanDataframeIndices();
});
it('loads the job management page', async () => {
await ml.navigation.navigateToMl();
await ml.navigation.navigateToJobManagement();
});
it('loads the new job source selection page', async () => {
await ml.jobManagement.navigateToNewJobSourceSelection();
});
it('loads the job type selection page', async () => {
await ml.jobSourceSelection.selectSourceIndexPattern('ecommerce');
});
it('loads the population job wizard page', async () => {
await ml.jobTypeSelection.selectPopulationJob();
});
it('displays the time range step', async () => {
await ml.jobWizardCommon.assertTimeRangeSectionExists();
});
it('displays the event rate chart', async () => {
await ml.jobWizardCommon.clickUseFullDataButton();
await ml.jobWizardCommon.assertEventRateChartExists();
});
it('displays the pick fields step', async () => {
await ml.jobWizardCommon.clickNextButton();
await ml.jobWizardCommon.assertPickFieldsSectionExists();
});
it('selects the population field', async () => {
await ml.jobWizardPopulation.assertPopulationFieldInputExists();
await ml.jobWizardPopulation.selectPopulationField(populationField);
await ml.jobWizardPopulation.assertPopulationFieldSelection(populationField);
});
it('selects detectors and displays detector previews', async () => {
for (const [index, detector] of detectors.entries()) {
await ml.jobWizardCommon.assertAggAndFieldInputExists();
await ml.jobWizardCommon.selectAggAndField(detector.identifier);
await ml.jobWizardCommon.assertDetectorPreviewExists(detector.identifier, index, 'SCATTER');
}
});
it('inputs detector split fields and displays split cards', async () => {
for (const [index, detector] of detectors.entries()) {
log.debug(detector);
await ml.jobWizardPopulation.assertDetectorSplitFieldInputExists(index);
await ml.jobWizardPopulation.selectDetectorSplitField(index, detector.splitField);
await ml.jobWizardPopulation.assertDetectorSplitFieldSelection(index, detector.splitField);
await ml.jobWizardPopulation.assertDetectorSplitExists(index);
await ml.jobWizardPopulation.assertDetectorSplitFrontCardTitle(
index,
detector.frontCardTitle
);
await ml.jobWizardPopulation.assertDetectorSplitNumberOfBackCards(
index,
detector.numberOfBackCards
);
}
});
it('displays the influencer field', async () => {
await ml.jobWizardCommon.assertInfluencerInputExists();
await ml.jobWizardCommon.assertInfluencerSelection(
[populationField].concat(detectors.map(detector => detector.splitField))
);
});
it('inputs the bucket span', async () => {
await ml.jobWizardCommon.assertBucketSpanInputExists();
await ml.jobWizardCommon.setBucketSpan(bucketSpan);
await ml.jobWizardCommon.assertBucketSpanValue(bucketSpan);
});
it('displays the job details step', async () => {
await ml.jobWizardCommon.clickNextButton();
await ml.jobWizardCommon.assertJobDetailsSectionExists();
});
it('inputs the job id', async () => {
await ml.jobWizardCommon.assertJobIdInputExists();
await ml.jobWizardCommon.setJobId(jobId);
await ml.jobWizardCommon.assertJobIdValue(jobId);
});
it('inputs the job description', async () => {
await ml.jobWizardCommon.assertJobDescriptionInputExists();
await ml.jobWizardCommon.setJobDescription(jobDescription);
await ml.jobWizardCommon.assertJobDescriptionValue(jobDescription);
});
it('inputs job groups', async () => {
await ml.jobWizardCommon.assertJobGroupInputExists();
for (const jobGroup of jobGroups) {
await ml.jobWizardCommon.addJobGroup(jobGroup);
}
await ml.jobWizardCommon.assertJobGroupSelection(jobGroups);
});
it('opens the advanced section', async () => {
await ml.jobWizardCommon.ensureAdvancedSectionOpen();
});
it('displays the model plot switch', async () => {
await ml.jobWizardCommon.assertModelPlotSwitchExists();
});
it('enables the dedicated index switch', async () => {
await ml.jobWizardCommon.assertDedicatedIndexSwitchExists();
await ml.jobWizardCommon.activateDedicatedIndexSwitch();
await ml.jobWizardCommon.assertDedicatedIndexSwitchCheckedState(true);
});
it('inputs the model memory limit', async () => {
await ml.jobWizardCommon.assertModelMemoryLimitInputExists();
await ml.jobWizardCommon.setModelMemoryLimit(memoryLimit);
await ml.jobWizardCommon.assertModelMemoryLimitValue(memoryLimit);
});
it('displays the validation step', async () => {
await ml.jobWizardCommon.clickNextButton();
await ml.jobWizardCommon.assertValidationSectionExists();
});
it('displays the summary step', async () => {
await ml.jobWizardCommon.clickNextButton();
await ml.jobWizardCommon.assertSummarySectionExists();
});
it('creates the job and finishes processing', async () => {
await ml.jobWizardCommon.assertCreateJobButtonExists();
await ml.jobWizardCommon.createJobAndWaitForCompletion();
});
it('displays the created job in the job list', async () => {
await ml.navigation.navigateToMl();
await ml.navigation.navigateToJobManagement();
await ml.jobTable.waitForJobsToLoad();
await ml.jobTable.filterWithSearchString(jobId);
const rows = await ml.jobTable.parseJobTable();
expect(rows.filter(row => row.id === jobId)).to.have.length(1);
});
it('displays details for the created job in the job list', async () => {
const expectedRow = {
id: jobId,
description: jobDescription,
jobGroups,
recordCount: '4,675',
memoryStatus: 'ok',
jobState: 'closed',
datafeedState: 'stopped',
latestTimestamp: '2019-07-12 23:45:36',
};
await ml.jobTable.assertJobRowFields(jobId, expectedRow);
const expectedCounts = {
job_id: jobId,
processed_record_count: '4,675',
processed_field_count: '23,375',
input_bytes: '867.7 KB',
input_field_count: '23,375',
invalid_date_count: '0',
missing_field_count: '0',
out_of_order_timestamp_count: '0',
empty_bucket_count: '0',
sparse_bucket_count: '0',
bucket_count: '371',
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 22:00:00',
};
const expectedModelSizeStats = {
job_id: jobId,
result_type: 'model_size_stats',
model_bytes_exceeded: '0',
model_bytes_memory_limit: '8388608',
total_by_field_count: '25',
total_over_field_count: '92',
total_partition_field_count: '3',
bucket_allocation_failures_count: '0',
memory_status: 'ok',
timestamp: '2019-07-12 20:00:00',
};
await ml.jobTable.assertJobRowDetailsCounts(jobId, expectedCounts, expectedModelSizeStats);
});
});
}

View file

@ -13,5 +13,6 @@ export default function({ loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./pages'));
loadTestFile(require.resolve('./create_single_metric_job'));
loadTestFile(require.resolve('./create_multi_metric_job'));
loadTestFile(require.resolve('./create_population_job'));
});
}

File diff suppressed because it is too large Load diff

View file

@ -13,6 +13,8 @@ export { MachineLearningJobSourceSelectionProvider } from './job_source_selectio
export { MachineLearningJobTableProvider } from './job_table';
export { MachineLearningJobTypeSelectionProvider } from './job_type_selection';
export { MachineLearningJobWizardCommonProvider } from './job_wizard_common';
export { MachineLearningJobWizardMultiMetricProvider } from './job_wizard_multi_metric';
export { MachineLearningJobWizardPopulationProvider } from './job_wizard_population';
export { MachineLearningNavigationProvider } from './navigation';
export { MachineLearningSettingsProvider } from './settings';
export { MachineLearningSingleMetricViewerProvider } from './single_metric_viewer';

View file

@ -19,5 +19,10 @@ export function MachineLearningJobTypeSelectionProvider({ getService }: FtrProvi
await testSubjects.clickWhenNotDisabled('mlJobTypeLinkMultiMetricJob');
await testSubjects.existOrFail('mlPageJobWizard');
},
async selectPopulationJob() {
await testSubjects.clickWhenNotDisabled('mlJobTypeLinkPopulationJob');
await testSubjects.existOrFail('mlPageJobWizard');
},
};
}

View file

@ -140,25 +140,13 @@ export function MachineLearningJobWizardCommonProvider({ getService }: FtrProvid
expect(actualModelMemoryLimit).to.eql(expectedValue);
},
async assertMultiMetricSplitFieldInputExists() {
await testSubjects.existOrFail('mlJobWizardSplitFieldSelection > comboBoxInput');
},
async assertMultiMetricSplitFieldSelection(identifier: string) {
const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions(
'mlJobWizardSplitFieldSelection > comboBoxInput'
);
expect(comboBoxSelectedOptions.length).to.eql(1);
expect(comboBoxSelectedOptions[0]).to.eql(identifier);
},
async assertInfluencerInputExists() {
await testSubjects.existOrFail('mlJobWizardInfluencerSelection > comboBoxInput');
await testSubjects.existOrFail('influencerSelect > comboBoxInput');
},
async assertInfluencerSelection(influencers: string[]) {
const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions(
'mlJobWizardInfluencerSelection > comboBoxInput'
'influencerSelect > comboBoxInput'
);
expect(comboBoxSelectedOptions).to.eql(influencers);
},
@ -176,24 +164,6 @@ export function MachineLearningJobWizardCommonProvider({ getService }: FtrProvid
await testSubjects.existOrFail(`detector ${detectorPosition} > mlAnomalyChart ${chartType}`);
},
async assertDetectorSplitExists(splitField: string) {
await testSubjects.existOrFail(`dataSplit > dataSplitTitle ${splitField}`);
await testSubjects.existOrFail(`dataSplit > splitCard front`);
await testSubjects.existOrFail(`dataSplit > splitCard back`);
},
async assertDetectorSplitFrontCardTitle(frontCardTitle: string) {
expect(
await testSubjects.getVisibleText(`dataSplit > splitCard front > splitCardTitle`)
).to.eql(frontCardTitle);
},
async assertDetectorSplitNumberOfBackCards(numberOfBackCards: number) {
expect(await testSubjects.findAll(`dataSplit > splitCard back`)).to.have.length(
numberOfBackCards
);
},
async clickNextButton() {
await testSubjects.clickWhenNotDisabled('mlJobWizardNavButtonNext');
},
@ -226,12 +196,8 @@ export function MachineLearningJobWizardCommonProvider({ getService }: FtrProvid
await comboBox.setCustom('mlJobWizardComboBoxJobGroups > comboBoxInput', jobGroup);
},
async selectMultiMetricSplitField(identifier: string) {
await comboBox.set('mlJobWizardSplitFieldSelection > comboBoxInput', identifier);
},
async addInfluencer(influencer: string) {
await comboBox.setCustom('mlJobWizardInfluencerSelection > comboBoxInput', influencer);
await comboBox.setCustom('influencerSelect > comboBoxInput', influencer);
},
async ensureAdvancedSectionOpen() {

View file

@ -0,0 +1,48 @@
/*
* 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 MachineLearningJobWizardMultiMetricProvider({ getService }: FtrProviderContext) {
const comboBox = getService('comboBox');
const testSubjects = getService('testSubjects');
return {
async assertSplitFieldInputExists() {
await testSubjects.existOrFail('multiMetricSplitFieldSelect > comboBoxInput');
},
async assertSplitFieldSelection(identifier: string) {
const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions(
'multiMetricSplitFieldSelect > comboBoxInput'
);
expect(comboBoxSelectedOptions.length).to.eql(1);
expect(comboBoxSelectedOptions[0]).to.eql(identifier);
},
async selectSplitField(identifier: string) {
await comboBox.set('multiMetricSplitFieldSelect > comboBoxInput', identifier);
},
async assertDetectorSplitExists(splitField: string) {
await testSubjects.existOrFail(`dataSplit > dataSplitTitle ${splitField}`);
await testSubjects.existOrFail(`dataSplit > splitCard front`);
},
async assertDetectorSplitFrontCardTitle(frontCardTitle: string) {
expect(
await testSubjects.getVisibleText(`dataSplit > splitCard front > splitCardTitle`)
).to.eql(frontCardTitle);
},
async assertDetectorSplitNumberOfBackCards(numberOfBackCards: number) {
expect(await testSubjects.findAll(`dataSplit > splitCard back`)).to.have.length(
numberOfBackCards
);
},
};
}

View file

@ -0,0 +1,74 @@
/*
* 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 MachineLearningJobWizardPopulationProvider({ getService }: FtrProviderContext) {
const comboBox = getService('comboBox');
const testSubjects = getService('testSubjects');
return {
async assertPopulationFieldInputExists() {
await testSubjects.existOrFail('populationSplitFieldSelect > comboBoxInput');
},
async assertPopulationFieldSelection(identifier: string) {
const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions(
'populationSplitFieldSelect > comboBoxInput'
);
expect(comboBoxSelectedOptions.length).to.eql(1);
expect(comboBoxSelectedOptions[0]).to.eql(identifier);
},
async selectPopulationField(identifier: string) {
await comboBox.set('populationSplitFieldSelect > comboBoxInput', identifier);
},
async assertDetectorSplitFieldInputExists(detectorPosition: number) {
await testSubjects.existOrFail(
`detector ${detectorPosition} > byFieldSelect > comboBoxInput`
);
},
async assertDetectorSplitFieldSelection(detectorPosition: number, identifier: string) {
const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions(
`detector ${detectorPosition} > byFieldSelect > comboBoxInput`
);
expect(comboBoxSelectedOptions.length).to.eql(1);
expect(comboBoxSelectedOptions[0]).to.eql(identifier);
},
async selectDetectorSplitField(detectorPosition: number, identifier: string) {
await comboBox.set(
`detector ${detectorPosition} > byFieldSelect > comboBoxInput`,
identifier
);
},
async assertDetectorSplitExists(detectorPosition: number) {
await testSubjects.existOrFail(`detector ${detectorPosition} > dataSplit`);
await testSubjects.existOrFail(`detector ${detectorPosition} > dataSplit > splitCard front`);
},
async assertDetectorSplitFrontCardTitle(detectorPosition: number, frontCardTitle: string) {
expect(
await testSubjects.getVisibleText(
`detector ${detectorPosition} > dataSplit > splitCard front > splitCardTitle`
)
).to.eql(frontCardTitle);
},
async assertDetectorSplitNumberOfBackCards(
detectorPosition: number,
numberOfBackCards: number
) {
expect(
await testSubjects.findAll(`detector ${detectorPosition} > dataSplit > splitCard back`)
).to.have.length(numberOfBackCards);
},
};
}

View file

@ -16,6 +16,8 @@ import {
MachineLearningJobTableProvider,
MachineLearningJobTypeSelectionProvider,
MachineLearningJobWizardCommonProvider,
MachineLearningJobWizardMultiMetricProvider,
MachineLearningJobWizardPopulationProvider,
MachineLearningNavigationProvider,
MachineLearningSettingsProvider,
MachineLearningSingleMetricViewerProvider,
@ -31,6 +33,8 @@ export function MachineLearningProvider(context: FtrProviderContext) {
const jobTable = MachineLearningJobTableProvider(context);
const jobTypeSelection = MachineLearningJobTypeSelectionProvider(context);
const jobWizardCommon = MachineLearningJobWizardCommonProvider(context);
const jobWizardMultiMetric = MachineLearningJobWizardMultiMetricProvider(context);
const jobWizardPopulation = MachineLearningJobWizardPopulationProvider(context);
const navigation = MachineLearningNavigationProvider(context);
const settings = MachineLearningSettingsProvider(context);
const singleMetricViewer = MachineLearningSingleMetricViewerProvider(context);
@ -45,6 +49,8 @@ export function MachineLearningProvider(context: FtrProviderContext) {
jobTable,
jobTypeSelection,
jobWizardCommon,
jobWizardMultiMetric,
jobWizardPopulation,
navigation,
settings,
singleMetricViewer,