Skip to content

Commit

Permalink
[ML] Single metric viewer in dashboards: adds functional test (#178768)
Browse files Browse the repository at this point in the history
## Summary

Related meta issue: #176651

This PR adds functional tests for SMV in dashboards.
Flaky test runner:
https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/5485


### Checklist

Delete any items that are not applicable to this PR.

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] [Flaky Test
Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was
used on any tests changed

Co-authored-by: Kibana Machine <[email protected]>
  • Loading branch information
alvarezmelissa87 and kibanamachine authored Mar 18, 2024
1 parent da8e891 commit a6174d4
Show file tree
Hide file tree
Showing 6 changed files with 161 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ export const SingleMetricViewerInitializer: FC<SingleMetricViewerInitializerProp
</EuiButtonEmpty>

<EuiButton
data-test-subj="mlsingleMetricViewerInitializerConfirmButton"
data-test-subj="mlSingleMetricViewerInitializerConfirmButton"
isDisabled={!isPanelTitleValid}
onClick={onCreate.bind(null, {
functionDescription,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,5 @@ export const DATAFEED_CONFIG: Datafeed = {
export const ML_EMBEDDABLE_TYPES = {
ANOMALY_SWIMLANE: 'ml_anomaly_swimlane',
ANOMALY_CHARTS: 'ml_anomaly_charts',
ANOMALY_SINGLE_METRIC_VIEWER_EMBEDDABLE_TYPE: 'ml_single_metric_viewer',
} as const;
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
});

loadTestFile(require.resolve('./anomaly_charts_dashboard_embeddables'));
loadTestFile(require.resolve('./single_metric_viewer_dashboard_embeddables'));
loadTestFile(require.resolve('./anomaly_embeddables_migration'));
loadTestFile(require.resolve('./lens_to_ml'));
loadTestFile(require.resolve('./map_to_ml'));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* 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_CONFIG, DATAFEED_CONFIG, ML_EMBEDDABLE_TYPES } from './constants';

const testDataList = [
{
type: 'testData',
suiteSuffix: 'with multi metric job',
panelTitle: `ML single metric viewer for ${JOB_CONFIG.job_id}`,
jobConfig: JOB_CONFIG,
datafeedConfig: DATAFEED_CONFIG,
dashboardTitle: `ML single metric viewer for fq_multi_1_ae ${Date.now()}`,
expected: { detectorInputValue: '0', entityInputLabel: 'airline', entityInputValue: 'AAL' },
},
];

export default function ({ getService, getPageObjects }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const ml = getService('ml');
const PageObjects = getPageObjects(['common', 'timePicker', 'dashboard']);
const from = 'Feb 7, 2016 @ 00:00:00.000';
const to = 'Feb 11, 2016 @ 00:00:00.000';

describe('single metric viewer in dashboard', function () {
this.tags(['ml']);

before(async () => {
await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote');
await ml.testResources.createDataViewIfNeeded('ft_farequote', '@timestamp');
await ml.testResources.setKibanaTimeZoneToUTC();
await ml.securityUI.loginAsMlPowerUser();
await PageObjects.common.setTime({ from, to });
});

after(async () => {
await ml.api.cleanMlIndices();
await ml.testResources.deleteDataViewByTitle('ft_farequote');
await PageObjects.common.unsetTime();
});

for (const testData of testDataList) {
describe(testData.suiteSuffix, function () {
before(async () => {
await ml.api.createAndRunAnomalyDetectionLookbackJob(
testData.jobConfig,
testData.datafeedConfig
);
await PageObjects.dashboard.navigateToApp();
});

after(async () => {
await ml.testResources.deleteDashboardByTitle(testData.dashboardTitle);
});

it('can open job selection flyout', async () => {
await PageObjects.dashboard.clickNewDashboard();
await ml.dashboardEmbeddables.assertDashboardIsEmpty();
await ml.dashboardEmbeddables.openAnomalyJobSelectionFlyout(
ML_EMBEDDABLE_TYPES.ANOMALY_SINGLE_METRIC_VIEWER_EMBEDDABLE_TYPE
);
});

it('can select jobs', async () => {
await ml.dashboardJobSelectionTable.setRowRadioButtonState(
testData.jobConfig.job_id,
true
);
await ml.dashboardJobSelectionTable.applyJobSelection();
});

it('can configure single metric viewer panel', async () => {
await ml.dashboardEmbeddables.assertSingleMetricViewerEmbeddableInitializerExists();
await ml.singleMetricViewer.assertDetectorInputExist();
await ml.singleMetricViewer.assertDetectorInputValue(
testData.expected.detectorInputValue
);
await ml.singleMetricViewer.assertEntityInputExist(testData.expected.entityInputLabel);
await ml.singleMetricViewer.selectEntityValue(
testData.expected.entityInputLabel,
testData.expected.entityInputValue
);
});

it('create new single metric viewer panel', async () => {
await ml.dashboardEmbeddables.clickSingleMetricViewerInitializerConfirmButtonEnabled();
await PageObjects.timePicker.pauseAutoRefresh();
await ml.dashboardEmbeddables.assertDashboardPanelExists(testData.panelTitle);
await ml.singleMetricViewer.assertChartExist();
await ml.singleMetricViewer.assertAnomalyMarkerExist();
await PageObjects.dashboard.saveDashboard(testData.dashboardTitle);
});
});
}
});
}
34 changes: 29 additions & 5 deletions x-pack/test/functional/services/ml/dashboard_embeddables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export function MachineLearningDashboardEmbeddablesProvider(
const retry = getService('retry');
const testSubjects = getService('testSubjects');
const find = getService('find');
// const ml = getService('ml');
const dashboardAddPanel = getService('dashboardAddPanel');
const PageObjects = getPageObjects(['discover']);

Expand All @@ -26,6 +27,22 @@ export function MachineLearningDashboardEmbeddablesProvider(
});
},

async assertSingleMetricViewerEmbeddableInitializerExists() {
await retry.tryForTime(10 * 1000, async () => {
await testSubjects.existOrFail('mlSingleMetricViewerEmbeddableInitializer', {
timeout: 1000,
});
});
},

async assertSingleMetricViewerEmbeddableInitializerNotExists() {
await retry.tryForTime(10 * 1000, async () => {
await testSubjects.missingOrFail('mlSingleMetricViewerEmbeddableInitializer', {
timeout: 1000,
});
});
},

async assertAnomalyChartsEmbeddableInitializerNotExists() {
await retry.tryForTime(10 * 1000, async () => {
await testSubjects.missingOrFail('mlAnomalyChartsEmbeddableInitializer', { timeout: 1000 });
Expand All @@ -46,9 +63,7 @@ export function MachineLearningDashboardEmbeddablesProvider(
});
},

async assertInitializerConfirmButtonEnabled() {
const subj = 'mlAnomalyChartsInitializerConfirmButton';

async assertInitializerConfirmButtonEnabled(subj: string) {
await retry.tryForTime(60 * 1000, async () => {
await testSubjects.existOrFail(subj);
await testSubjects.isEnabled(subj);
Expand All @@ -58,12 +73,21 @@ export function MachineLearningDashboardEmbeddablesProvider(
async clickInitializerConfirmButtonEnabled() {
const subj = 'mlAnomalyChartsInitializerConfirmButton';
await retry.tryForTime(60 * 1000, async () => {
await this.assertInitializerConfirmButtonEnabled();
await this.assertInitializerConfirmButtonEnabled(subj);
await testSubjects.clickWhenNotDisabledWithoutRetry(subj);
await this.assertAnomalyChartsEmbeddableInitializerNotExists();
});
},

async clickSingleMetricViewerInitializerConfirmButtonEnabled() {
const subj = 'mlSingleMetricViewerInitializerConfirmButton';
await retry.tryForTime(60 * 1000, async () => {
await this.assertInitializerConfirmButtonEnabled(subj);
await testSubjects.clickWhenNotDisabledWithoutRetry(subj);
await this.assertSingleMetricViewerEmbeddableInitializerNotExists();
});
},

async assertDashboardIsEmpty() {
await retry.tryForTime(60 * 1000, async () => {
await testSubjects.existOrFail('emptyDashboardWidget');
Expand Down Expand Up @@ -101,7 +125,7 @@ export function MachineLearningDashboardEmbeddablesProvider(
},

async openAnomalyJobSelectionFlyout(
mlEmbeddableType: 'ml_anomaly_swimlane' | 'ml_anomaly_charts'
mlEmbeddableType: 'ml_anomaly_swimlane' | 'ml_anomaly_charts' | 'ml_single_metric_viewer'
) {
await retry.tryForTime(60 * 1000, async () => {
await dashboardAddPanel.clickEditorMenuButton();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export function MachineLearningDashboardJobSelectionTableProvider({
}: FtrProviderContext) {
const retry = getService('retry');
const testSubjects = getService('testSubjects');
const find = getService('find');

return {
async assertJobSelectionTableExists(): Promise<void> {
Expand Down Expand Up @@ -71,6 +72,33 @@ export function MachineLearningDashboardJobSelectionTableProvider({
}
},

async assertRowRadioButtonCheckedState(jobId: string, expectedCheckState: boolean) {
const actualCheckState = await this.getRowRadioButtonCheckedState(jobId);
expect(actualCheckState).to.eql(
expectedCheckState,
`Table row for job '${jobId}' check state should be '${expectedCheckState}' (got '${actualCheckState}')`
);
},

async getRowRadioButtonCheckedState(jobId: string): Promise<boolean> {
const radioButtonInput = await find.byCssSelector(
`[data-test-subj="${jobId}-radio-button"] > input#${jobId}`
);
const radioButtonChecked = await radioButtonInput.getAttribute('checked');
return radioButtonChecked === 'true';
},

async setRowRadioButtonState(jobId: string, expectCheckedState: boolean) {
const subj = this.rowSelector(jobId, `${jobId}-radio-button`);
const checkedState = await this.getRowRadioButtonCheckedState(jobId);
if (checkedState !== expectCheckedState) {
await retry.tryForTime(5 * 1000, async () => {
await testSubjects.clickWhenNotDisabledWithoutRetry(subj);
await this.assertRowRadioButtonCheckedState(jobId, expectCheckedState);
});
}
},

async assertApplyJobSelectionEnabled() {
const subj = 'mlFlyoutJobSelectorButtonApply';
await testSubjects.existOrFail(subj);
Expand Down

0 comments on commit a6174d4

Please sign in to comment.