[ML] Anomaly Detection: Functional tests for anomaly detection forecasts. (#116140)
Functional tests for anomaly detection forecasts.
This commit is contained in:
parent
753da1ac1d
commit
0f73b57cfd
10 changed files with 282 additions and 56 deletions
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1170,9 +1170,13 @@ export class TimeSeriesExplorer extends React.Component {
|
|||
<EuiFlexItem grow={false}>
|
||||
<EuiCheckbox
|
||||
id="toggleShowForecastCheckbox"
|
||||
label={i18n.translate('xpack.ml.timeSeriesExplorer.showForecastLabel', {
|
||||
label={
|
||||
<span data-test-subj={'mlForecastCheckbox'}>
|
||||
{i18n.translate('xpack.ml.timeSeriesExplorer.showForecastLabel', {
|
||||
defaultMessage: 'show forecast',
|
||||
})}
|
||||
</span>
|
||||
}
|
||||
checked={showForecast}
|
||||
onChange={this.toggleShowForecastHandler}
|
||||
/>
|
||||
|
|
116
x-pack/test/functional/apps/ml/anomaly_detection/forecasts.ts
Normal file
116
x-pack/test/functional/apps/ml/anomaly_detection/forecasts.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
126
x-pack/test/functional/services/ml/forecast.ts
Normal file
126
x-pack/test/functional/services/ml/forecast.ts
Normal 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}')`
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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');
|
||||
|
|
Loading…
Reference in a new issue