[ML] Anomaly Detection: Functional tests for anomaly detection forecasts. (#116140)

Functional tests for anomaly detection forecasts.
This commit is contained in:
Walter Rafelsberger 2021-10-28 14:18:53 +02:00 committed by GitHub
parent 753da1ac1d
commit 0f73b57cfd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 282 additions and 56 deletions

View file

@ -78,6 +78,12 @@ function getColumns(viewForecast) {
// TODO - add in ml-info-icon to the h3 element,
// then remove tooltip and inline style.
export function ForecastsList({ forecasts, viewForecast }) {
const getRowProps = (item) => {
return {
'data-test-subj': `mlForecastsListRow row-${item.rowId}`,
};
};
return (
<EuiText>
<h3
@ -105,6 +111,7 @@ export function ForecastsList({ forecasts, viewForecast }) {
columns={getColumns(viewForecast)}
pagination={false}
data-test-subj="mlModalForecastTable"
rowProps={getRowProps}
/>
</EuiText>
);

View file

@ -547,9 +547,18 @@ class TimeseriesChartIntl extends Component {
// Create the path elements for the forecast value line and bounds area.
if (contextForecastData) {
fcsGroup.append('path').attr('class', 'area forecast');
fcsGroup.append('path').attr('class', 'values-line forecast');
fcsGroup.append('g').attr('class', 'focus-chart-markers forecast');
fcsGroup
.append('path')
.attr('class', 'area forecast')
.attr('data-test-subj', 'mlForecastArea');
fcsGroup
.append('path')
.attr('class', 'values-line forecast')
.attr('data-test-subj', 'mlForecastValuesline');
fcsGroup
.append('g')
.attr('class', 'focus-chart-markers forecast')
.attr('data-test-subj', 'mlForecastMarkers');
}
fcsGroup

View file

@ -1170,9 +1170,13 @@ export class TimeSeriesExplorer extends React.Component {
<EuiFlexItem grow={false}>
<EuiCheckbox
id="toggleShowForecastCheckbox"
label={i18n.translate('xpack.ml.timeSeriesExplorer.showForecastLabel', {
defaultMessage: 'show forecast',
})}
label={
<span data-test-subj={'mlForecastCheckbox'}>
{i18n.translate('xpack.ml.timeSeriesExplorer.showForecastLabel', {
defaultMessage: 'show forecast',
})}
</span>
}
checked={showForecast}
onChange={this.toggleShowForecastHandler}
/>

View file

@ -0,0 +1,116 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { FtrProviderContext } from '../../../ftr_provider_context';
import { Job, Datafeed } from '../../../../../plugins/ml/common/types/anomaly_detection_jobs';
// @ts-expect-error not full interface
const JOB_CONFIG: Job = {
job_id: `fq_single_1_smv`,
description: 'count() on farequote dataset with 15m bucket span',
groups: ['farequote', 'automated', 'single-metric'],
analysis_config: {
bucket_span: '15m',
influencers: [],
detectors: [
{
function: 'count',
},
],
},
data_description: { time_field: '@timestamp' },
analysis_limits: { model_memory_limit: '10mb' },
model_plot_config: { enabled: true },
};
// @ts-expect-error not full interface
const DATAFEED_CONFIG: Datafeed = {
datafeed_id: 'datafeed-fq_single_1_smv',
indices: ['ft_farequote'],
job_id: 'fq_single_1_smv',
query: { bool: { must: [{ match_all: {} }] } },
};
export default function ({ getService }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const ml = getService('ml');
describe('forecasts', function () {
this.tags(['mlqa']);
describe('with single metric job', function () {
before(async () => {
await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote');
await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp');
await ml.testResources.setKibanaTimeZoneToUTC();
await ml.api.createAndRunAnomalyDetectionLookbackJob(JOB_CONFIG, DATAFEED_CONFIG);
await ml.securityUI.loginAsMlPowerUser();
});
after(async () => {
await ml.api.cleanMlIndices();
});
it('opens a job from job list link', async () => {
await ml.testExecution.logTestStep('navigate to job list');
await ml.navigation.navigateToMl();
await ml.navigation.navigateToJobManagement();
await ml.testExecution.logTestStep('open job in single metric viewer');
await ml.jobTable.waitForJobsToLoad();
await ml.jobTable.filterWithSearchString(JOB_CONFIG.job_id, 1);
await ml.jobTable.clickOpenJobInSingleMetricViewerButton(JOB_CONFIG.job_id);
await ml.commonUI.waitForMlLoadingIndicatorToDisappear();
});
it('displays job results', async () => {
await ml.testExecution.logTestStep('pre-fills the job selection');
await ml.jobSelection.assertJobSelection([JOB_CONFIG.job_id]);
await ml.testExecution.logTestStep('pre-fills the detector input');
await ml.singleMetricViewer.assertDetectorInputExist();
await ml.singleMetricViewer.assertDetectorInputValue('0');
await ml.testExecution.logTestStep('displays the chart');
await ml.singleMetricViewer.assertChartExist();
await ml.testExecution.logTestStep('should not display the forecasts toggle checkbox');
await ml.forecast.assertForecastCheckboxMissing();
await ml.testExecution.logTestStep('should open the forecasts modal');
await ml.forecast.assertForecastButtonExists();
await ml.forecast.assertForecastButtonEnabled(true);
await ml.forecast.openForecastModal();
await ml.forecast.assertForecastModalRunButtonEnabled(true);
await ml.testExecution.logTestStep('should run the forecast and close the modal');
await ml.forecast.clickForecastModalRunButton();
await ml.testExecution.logTestStep('should display the forecasts toggle checkbox');
await ml.forecast.assertForecastCheckboxExists();
await ml.testExecution.logTestStep(
'should display the forecast in the single metric chart'
);
await ml.forecast.assertForecastChartElementsExists();
await ml.testExecution.logTestStep('should hide the forecast in the single metric chart');
await ml.forecast.clickForecastCheckbox();
await ml.forecast.assertForecastChartElementsHidden();
await ml.testExecution.logTestStep('should open the forecasts modal and list the forecast');
await ml.forecast.assertForecastButtonExists();
await ml.forecast.assertForecastButtonEnabled(true);
await ml.forecast.openForecastModal();
await ml.forecast.assertForecastTableExists();
await ml.forecast.assertForecastTableNotEmpty();
});
});
});
}

View file

@ -24,5 +24,6 @@ export default function ({ loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./annotations'));
loadTestFile(require.resolve('./aggregated_scripted_job'));
loadTestFile(require.resolve('./custom_urls'));
loadTestFile(require.resolve('./forecasts'));
});
}

View file

@ -237,11 +237,11 @@ export default function ({ getService }: FtrProviderContext) {
await ml.testExecution.logTestStep(
'should display the forecast modal with enabled run button'
);
await ml.singleMetricViewer.assertForecastButtonExists();
await ml.singleMetricViewer.assertForecastButtonEnabled(true);
await ml.singleMetricViewer.openForecastModal();
await ml.singleMetricViewer.assertForecastModalRunButtonEnabled(true);
await ml.singleMetricViewer.closeForecastModal();
await ml.forecast.assertForecastButtonExists();
await ml.forecast.assertForecastButtonEnabled(true);
await ml.forecast.openForecastModal();
await ml.forecast.assertForecastModalRunButtonEnabled(true);
await ml.forecast.closeForecastModal();
});
it('should display elements on Anomaly Explorer page correctly', async () => {

View file

@ -230,11 +230,11 @@ export default function ({ getService }: FtrProviderContext) {
await ml.testExecution.logTestStep(
'should display the forecast modal with disabled run button'
);
await ml.singleMetricViewer.assertForecastButtonExists();
await ml.singleMetricViewer.assertForecastButtonEnabled(true);
await ml.singleMetricViewer.openForecastModal();
await ml.singleMetricViewer.assertForecastModalRunButtonEnabled(false);
await ml.singleMetricViewer.closeForecastModal();
await ml.forecast.assertForecastButtonExists();
await ml.forecast.assertForecastButtonEnabled(true);
await ml.forecast.openForecastModal();
await ml.forecast.assertForecastModalRunButtonEnabled(false);
await ml.forecast.closeForecastModal();
});
it('should display elements on Anomaly Explorer page correctly', async () => {

View file

@ -0,0 +1,126 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
export function MachineLearningForecastProvider({ getService }: FtrProviderContext) {
const testSubjects = getService('testSubjects');
return {
async assertForecastButtonExists() {
await testSubjects.existOrFail(
'mlSingleMetricViewerSeriesControls > mlSingleMetricViewerButtonForecast'
);
},
async assertForecastButtonEnabled(expectedValue: boolean) {
const isEnabled = await testSubjects.isEnabled(
'mlSingleMetricViewerSeriesControls > mlSingleMetricViewerButtonForecast'
);
expect(isEnabled).to.eql(
expectedValue,
`Expected "forecast" button to be '${expectedValue ? 'enabled' : 'disabled'}' (got '${
isEnabled ? 'enabled' : 'disabled'
}')`
);
},
async assertForecastChartElementsExists() {
await testSubjects.existOrFail(`mlForecastArea`, {
timeout: 30 * 1000,
});
await testSubjects.existOrFail(`mlForecastValuesline`, {
timeout: 30 * 1000,
});
await testSubjects.existOrFail(`mlForecastMarkers`, {
timeout: 30 * 1000,
});
},
async assertForecastChartElementsHidden() {
await testSubjects.missingOrFail(`mlForecastArea`, {
allowHidden: true,
timeout: 30 * 1000,
});
await testSubjects.missingOrFail(`mlForecastValuesline`, {
allowHidden: true,
timeout: 30 * 1000,
});
await testSubjects.missingOrFail(`mlForecastMarkers`, {
allowHidden: true,
timeout: 30 * 1000,
});
},
async assertForecastCheckboxExists() {
await testSubjects.existOrFail(`mlForecastCheckbox`, {
timeout: 30 * 1000,
});
},
async assertForecastCheckboxMissing() {
await testSubjects.missingOrFail(`mlForecastCheckbox`, {
timeout: 30 * 1000,
});
},
async clickForecastCheckbox() {
await testSubjects.click('mlForecastCheckbox');
},
async openForecastModal() {
await testSubjects.click(
'mlSingleMetricViewerSeriesControls > mlSingleMetricViewerButtonForecast'
);
await testSubjects.existOrFail('mlModalForecast');
},
async closeForecastModal() {
await testSubjects.click('mlModalForecast > mlModalForecastButtonClose');
await this.assertForecastModalMissing();
},
async assertForecastModalMissing() {
await testSubjects.missingOrFail(`mlModalForecast`, {
timeout: 30 * 1000,
});
},
async assertForecastModalRunButtonEnabled(expectedValue: boolean) {
const isEnabled = await testSubjects.isEnabled('mlModalForecast > mlModalForecastButtonRun');
expect(isEnabled).to.eql(
expectedValue,
`Expected forecast "run" button to be '${expectedValue ? 'enabled' : 'disabled'}' (got '${
isEnabled ? 'enabled' : 'disabled'
}')`
);
},
async assertForecastTableExists() {
await testSubjects.existOrFail('mlModalForecast > mlModalForecastTable');
},
async clickForecastModalRunButton() {
await testSubjects.click('mlModalForecast > mlModalForecastButtonRun');
await this.assertForecastModalMissing();
},
async getForecastTableRows() {
return await testSubjects.findAll('mlModalForecastTable > ~mlForecastsListRow');
},
async assertForecastTableNotEmpty() {
const tableRows = await this.getForecastTableRows();
expect(tableRows.length).to.be.greaterThan(
0,
`Forecast table should have at least one row (got '${tableRows.length}')`
);
},
};
}

View file

@ -24,6 +24,7 @@ import { MachineLearningDataVisualizerProvider } from './data_visualizer';
import { MachineLearningDataVisualizerFileBasedProvider } from './data_visualizer_file_based';
import { MachineLearningDataVisualizerIndexBasedProvider } from './data_visualizer_index_based';
import { MachineLearningDataVisualizerIndexPatternManagementProvider } from './data_visualizer_index_pattern_management';
import { MachineLearningForecastProvider } from './forecast';
import { MachineLearningJobManagementProvider } from './job_management';
import { MachineLearningJobSelectionProvider } from './job_selection';
import { MachineLearningJobSourceSelectionProvider } from './job_source_selection';
@ -92,6 +93,7 @@ export function MachineLearningProvider(context: FtrProviderContext) {
const dataVisualizerIndexPatternManagement =
MachineLearningDataVisualizerIndexPatternManagementProvider(context, dataVisualizerTable);
const forecast = MachineLearningForecastProvider(context);
const jobAnnotations = MachineLearningJobAnnotationsProvider(context);
const jobManagement = MachineLearningJobManagementProvider(context, api);
const jobSelection = MachineLearningJobSelectionProvider(context);
@ -145,6 +147,7 @@ export function MachineLearningProvider(context: FtrProviderContext) {
dataVisualizerIndexBased,
dataVisualizerIndexPatternManagement,
dataVisualizerTable,
forecast,
jobAnnotations,
jobManagement,
jobSelection,

View file

@ -22,24 +22,6 @@ export function MachineLearningSingleMetricViewerProvider(
await testSubjects.existOrFail('mlNoSingleMetricJobsFound');
},
async assertForecastButtonExists() {
await testSubjects.existOrFail(
'mlSingleMetricViewerSeriesControls > mlSingleMetricViewerButtonForecast'
);
},
async assertForecastButtonEnabled(expectedValue: boolean) {
const isEnabled = await testSubjects.isEnabled(
'mlSingleMetricViewerSeriesControls > mlSingleMetricViewerButtonForecast'
);
expect(isEnabled).to.eql(
expectedValue,
`Expected "forecast" button to be '${expectedValue ? 'enabled' : 'disabled'}' (got '${
isEnabled ? 'enabled' : 'disabled'
}')`
);
},
async assertDetectorInputExist() {
await testSubjects.existOrFail(
'mlSingleMetricViewerSeriesControls > mlSingleMetricViewerDetectorSelect'
@ -97,28 +79,6 @@ export function MachineLearningSingleMetricViewerProvider(
});
},
async openForecastModal() {
await testSubjects.click(
'mlSingleMetricViewerSeriesControls > mlSingleMetricViewerButtonForecast'
);
await testSubjects.existOrFail('mlModalForecast');
},
async closeForecastModal() {
await testSubjects.click('mlModalForecast > mlModalForecastButtonClose');
await testSubjects.missingOrFail('mlModalForecast');
},
async assertForecastModalRunButtonEnabled(expectedValue: boolean) {
const isEnabled = await testSubjects.isEnabled('mlModalForecast > mlModalForecastButtonRun');
expect(isEnabled).to.eql(
expectedValue,
`Expected forecast "run" button to be '${expectedValue ? 'enabled' : 'disabled'}' (got '${
isEnabled ? 'enabled' : 'disabled'
}')`
);
},
async openAnomalyExplorer() {
await testSubjects.click('mlAnomalyResultsViewSelectorExplorer');
await testSubjects.existOrFail('mlPageAnomalyExplorer');