diff --git a/.buildkite/ftr_platform_stateful_configs.yml b/.buildkite/ftr_platform_stateful_configs.yml index 244f04257ef924..8fb7807711d7d7 100644 --- a/.buildkite/ftr_platform_stateful_configs.yml +++ b/.buildkite/ftr_platform_stateful_configs.yml @@ -249,6 +249,7 @@ enabled: - x-pack/test/functional/apps/ml/permissions/config.ts - x-pack/test/functional/apps/ml/short_tests/config.ts - x-pack/test/functional/apps/ml/stack_management_jobs/config.ts + - x-pack/test/functional/apps/ml/memory_usage/config.ts - x-pack/test/functional/apps/monitoring/config.ts - x-pack/test/functional/apps/painless_lab/config.ts - x-pack/test/functional/apps/remote_clusters/config.ts diff --git a/x-pack/plugins/ml/public/application/memory_usage/memory_tree_map/tree_map.tsx b/x-pack/plugins/ml/public/application/memory_usage/memory_tree_map/tree_map.tsx index c3c92fecb28115..d9e4c0e25f78d3 100644 --- a/x-pack/plugins/ml/public/application/memory_usage/memory_tree_map/tree_map.tsx +++ b/x-pack/plugins/ml/public/application/memory_usage/memory_tree_map/tree_map.tsx @@ -161,7 +161,7 @@ export const JobMemoryTreeMap: FC = ({ node, type, height }) => { options={typeOptions} selectedOptions={selectedOptions ?? []} onChange={setSelectedOptions} - isClearable={false} + data-test-subj="mlJobTreeMapComboBox" /> diff --git a/x-pack/plugins/ml/public/application/memory_usage/memory_usage_page.tsx b/x-pack/plugins/ml/public/application/memory_usage/memory_usage_page.tsx index b0def27254f339..3e445fafbf67c3 100644 --- a/x-pack/plugins/ml/public/application/memory_usage/memory_usage_page.tsx +++ b/x-pack/plugins/ml/public/application/memory_usage/memory_usage_page.tsx @@ -50,16 +50,18 @@ export const MemoryUsagePage: FC = () => { {showNodeInfo ? ( <> - + setSelectedTab(TAB.NODES)} + data-test-subj="mlMemoryUsageTab-nodes" > setSelectedTab(TAB.MEMORY_USAGE)} + data-test-subj="mlMemoryUsageTab-memory-usage" > diff --git a/x-pack/plugins/ml/public/application/memory_usage/nodes_overview/expanded_row.tsx b/x-pack/plugins/ml/public/application/memory_usage/nodes_overview/expanded_row.tsx index f197ebc08825de..6df9f426524552 100644 --- a/x-pack/plugins/ml/public/application/memory_usage/nodes_overview/expanded_row.tsx +++ b/x-pack/plugins/ml/public/application/memory_usage/nodes_overview/expanded_row.tsx @@ -64,6 +64,7 @@ export const ExpandedRow: FC = ({ item }) => { setSelectedTab(TAB.DETAILS)} + data-test-subj="mlNodesOverviewPanelDetailsTab" > = ({ item }) => { setSelectedTab(TAB.MEMORY_USAGE)} + data-test-subj="mlNodesOverviewPanelMemoryTab" > = ({ item }) => { <> - +
@@ -104,7 +106,7 @@ export const ExpandedRow: FC = ({ item }) => { - +
diff --git a/x-pack/plugins/ml/public/application/memory_usage/nodes_overview/nodes_list.tsx b/x-pack/plugins/ml/public/application/memory_usage/nodes_overview/nodes_list.tsx index d31981decb7e9a..b9893f92c83b4e 100644 --- a/x-pack/plugins/ml/public/application/memory_usage/nodes_overview/nodes_list.tsx +++ b/x-pack/plugins/ml/public/application/memory_usage/nodes_overview/nodes_list.tsx @@ -185,6 +185,7 @@ export const NodesList: FC = ({ compactView = false }) => { }, box: { incremental: true, + 'data-test-subj': 'mlNodesTableSearchInput', }, }; diff --git a/x-pack/plugins/ml/public/application/routing/routes/memory_usage.tsx b/x-pack/plugins/ml/public/application/routing/routes/memory_usage.tsx index 527e870557c687..e9d5c092311f80 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/memory_usage.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/memory_usage.tsx @@ -39,6 +39,7 @@ export const nodesListRouteFactory = ( }, ], enableDatePicker: true, + 'data-test-subj': 'mlPageMemoryUsage', }); const PageWrapper: FC = () => { diff --git a/x-pack/test/functional/apps/ml/memory_usage/config.ts b/x-pack/test/functional/apps/ml/memory_usage/config.ts new file mode 100644 index 00000000000000..b6467013287233 --- /dev/null +++ b/x-pack/test/functional/apps/ml/memory_usage/config.ts @@ -0,0 +1,20 @@ +/* + * 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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + junit: { + reportName: 'Chrome X-Pack UI Functional Tests - ML memory_usage', + }, + }; +} diff --git a/x-pack/test/functional/apps/ml/memory_usage/index.ts b/x-pack/test/functional/apps/ml/memory_usage/index.ts new file mode 100644 index 00000000000000..e0d8096952702a --- /dev/null +++ b/x-pack/test/functional/apps/ml/memory_usage/index.ts @@ -0,0 +1,33 @@ +/* + * 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'; + +export default function ({ getService, loadTestFile }: FtrProviderContext) { + const ml = getService('ml'); + + describe('machine learning - overview page', function () { + this.tags(['skipFirefox']); + + before(async () => { + await ml.securityCommon.createMlRoles(); + await ml.securityCommon.createMlUsers(); + await ml.securityUI.loginAsMlPowerUser(); + }); + + after(async () => { + await ml.securityUI.logout(); + + await ml.securityCommon.cleanMlUsers(); + await ml.securityCommon.cleanMlRoles(); + + await ml.testResources.resetKibanaTimeZone(); + }); + + loadTestFile(require.resolve('./memory_usage_page')); + }); +} diff --git a/x-pack/test/functional/apps/ml/memory_usage/memory_usage_page.ts b/x-pack/test/functional/apps/ml/memory_usage/memory_usage_page.ts new file mode 100644 index 00000000000000..319d54c78d5412 --- /dev/null +++ b/x-pack/test/functional/apps/ml/memory_usage/memory_usage_page.ts @@ -0,0 +1,72 @@ +/* + * 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'; + +export default function ({ getService }: FtrProviderContext) { + const ml = getService('ml'); + const esArchiver = getService('esArchiver'); + + const jobId = 'sample_job'; + + describe('ML memory usage page', function () { + this.tags(['ml']); + + before(async () => { + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); + + const jobConfig = ml.commonConfig.getADFqSingleMetricJobConfig(jobId); + + // Create and open AD job + await ml.api.createAnomalyDetectionJob(jobConfig); + await ml.api.openAnomalyDetectionJob(jobId); + + await ml.navigation.navigateToMl(); + await ml.navigation.navigateToMemoryUsage(); + }); + + after(async () => { + await ml.api.closeAnomalyDetectionJob(jobId); + await ml.api.cleanMlIndices(); + }); + + it('opens page with nodes tab selected', async () => { + await ml.memoryUsage.assertMemoryUsageTabIsSelected('nodes'); + }); + + it('allows sorting', async () => { + await ml.memoryUsage.sortColumn('tableHeaderCell_name_1'); + await ml.memoryUsage.assertColumnIsSorted('tableHeaderCell_name_1', 'descending'); + }); + + it('allows searching for a node', async () => { + await ml.memoryUsage.searchForNode('ftr'); + await ml.memoryUsage.assertRowCount(1); + }); + + it('expands node details and displays memory usage details', async () => { + await ml.memoryUsage.expandRow(); + await ml.memoryUsage.assertNodeExpandedDetailsPanelsExist(); + await ml.memoryUsage.selectNodeExpandedRowTab('mlNodesOverviewPanelMemoryTab'); + await ml.memoryUsage.assertChartItemsSelectedByDefault(); + await ml.memoryUsage.assertTreeChartExists(); + }); + + it('clears selected chart items', async () => { + await ml.memoryUsage.clearSelectedChartItems(); + await ml.memoryUsage.assertEmptyTreeChartExists(); + }); + + it('selects memory usage tab and displays chart', async () => { + await ml.memoryUsage.selectTab('memory-usage'); + await ml.memoryUsage.assertTreeChartExists(); + + await ml.memoryUsage.clearSelectedChartItems(); + await ml.memoryUsage.assertEmptyTreeChartExists(); + }); + }); +} diff --git a/x-pack/test/functional/services/ml/index.ts b/x-pack/test/functional/services/ml/index.ts index d62dfe921f69cb..74834eaa0dba67 100644 --- a/x-pack/test/functional/services/ml/index.ts +++ b/x-pack/test/functional/services/ml/index.ts @@ -69,6 +69,8 @@ import { MlTableServiceProvider } from './common_table_service'; import { MachineLearningFieldStatsFlyoutProvider } from './field_stats_flyout'; import { MachineLearningDataDriftProvider } from './data_drift'; import { TrainedModelsFlyoutProvider } from './add_trained_models_flyout'; +import { MachineLearningMemoryUsageProvider } from './memory_usage'; + export function MachineLearningProvider(context: FtrProviderContext) { const commonAPI = MachineLearningCommonAPIProvider(context); const commonUI = MachineLearningCommonUIProvider(context); @@ -178,7 +180,7 @@ export function MachineLearningProvider(context: FtrProviderContext) { const deployDFAModelFlyout = DeployDFAModelFlyoutProvider(context, commonUI); const mlNodesPanel = MlNodesPanelProvider(context); const notifications = NotificationsProvider(context, commonUI, tableService); - + const memoryUsage = MachineLearningMemoryUsageProvider(context); const cases = MachineLearningCasesProvider(context, swimLane, anomalyCharts); return { @@ -244,5 +246,6 @@ export function MachineLearningProvider(context: FtrProviderContext) { trainedModelsFlyout, deployDFAModelFlyout, trainedModelsTable, + memoryUsage, }; } diff --git a/x-pack/test/functional/services/ml/memory_usage.ts b/x-pack/test/functional/services/ml/memory_usage.ts new file mode 100644 index 00000000000000..76d6259bc57d50 --- /dev/null +++ b/x-pack/test/functional/services/ml/memory_usage.ts @@ -0,0 +1,107 @@ +/* + * 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 MachineLearningMemoryUsageProvider({ getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const comboBox = getService('comboBox'); + + return { + async assertNodeExpandedDetailsPanelsExist() { + await testSubjects.existOrFail('mlNodesTableRowDetailsPanel'); + await testSubjects.existOrFail('mlNodesTableRowDetailsAttributesPanel'); + }, + + async assertTabIsSelected(tabName: string) { + await testSubjects.existOrFail(`mlNodesOverviewPanel ${tabName}Tab`); + }, + + async selectTab(tabName: string) { + await testSubjects.click(`mlMemoryUsageTab-${tabName}`); + }, + + async assertMemoryUsageTabsExist() { + await testSubjects.existOrFail('mlMemoryUsageTabs'); + }, + + async assertMemoryUsageTabIsSelected(tabName: string) { + const isSelected = await testSubjects.getAttribute( + `mlMemoryUsageTab-${tabName}`, + 'aria-selected' + ); + expect(isSelected).to.eql('true'); + }, + + async assertRowCount(expectedCount: number) { + const rowCount = await this.getRowCount(); + expect(rowCount).to.eql(expectedCount); + }, + + async getAllRows() { + return await testSubjects.findAll('~mlNodesTableRow'); + }, + + async expandRow() { + await testSubjects.click('mlNodesTableRowDetailsToggle'); + }, + + async getRowCount() { + const rows = await this.getAllRows(); + return rows.length; + }, + + async assertColumnHeaderExists(columnName: string) { + await testSubjects.existOrFail(columnName); + }, + + async assertColumnIsSorted(columnName: string, sortDirection: 'ascending' | 'descending') { + const sorted = await testSubjects.getAttribute(columnName, 'aria-sort'); + expect(sorted).to.eql(sortDirection); + }, + + async sortColumn(columnName: string) { + await this.assertColumnHeaderExists(columnName); + await testSubjects.click(columnName); + }, + + async assertSearchBarExists() { + await testSubjects.existOrFail('mlNodesTableSearchInput'); + }, + + async searchForNode(nodeId: string) { + await this.assertSearchBarExists(); + await testSubjects.setValue('mlNodesTableSearchInput', nodeId); + }, + + async selectNodeExpandedRowTab(tabName: string) { + await testSubjects.click(tabName); + }, + + async clearSelectedChartItems() { + await comboBox.clear('~mlJobTreeMap > mlJobTreeMapComboBox'); + }, + + async getSelectedChartItems() { + return await comboBox.getComboBoxSelectedOptions('~mlJobTreeMap > comboBoxInput'); + }, + + async assertChartItemsSelectedByDefault() { + const selectedOptions = await this.getSelectedChartItems(); + expect(selectedOptions.length).to.be.greaterThan(0); + }, + + async assertTreeChartExists() { + await testSubjects.existOrFail('mlJobTreeMap withData'); + }, + + async assertEmptyTreeChartExists() { + await testSubjects.existOrFail('mlJobTreeMap empty'); + }, + }; +} diff --git a/x-pack/test/functional/services/ml/navigation.ts b/x-pack/test/functional/services/ml/navigation.ts index b92ade51063d5f..649042bffa6d98 100644 --- a/x-pack/test/functional/services/ml/navigation.ts +++ b/x-pack/test/functional/services/ml/navigation.ts @@ -154,6 +154,10 @@ export function MachineLearningNavigationProvider({ await this.navigateToArea('~mlMainTab & ~notifications', 'mlPageNotifications'); }, + async navigateToMemoryUsage() { + await this.navigateToArea('~mlMainTab & ~nodesOverview', 'mlPageMemoryUsage'); + }, + async navigateToAnomalyDetection() { await this.navigateToArea('~mlMainTab & ~anomalyDetection', 'mlPageJobManagement'); },