From 7e64bedb18089e41dfb0b5ae20f1de048c12208e Mon Sep 17 00:00:00 2001 From: Quynh Nguyen <43350163+qn895@users.noreply.github.com> Date: Thu, 17 Dec 2020 15:31:47 -0600 Subject: [PATCH] [ML] Fix functional tests for index based Data Visualizer (#86071) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../multi_select_picker.tsx | 42 ++- .../document_count_chart.tsx | 2 +- .../examples_list/examples_list.tsx | 2 +- .../metric_distribution_chart.tsx | 2 +- .../field_data_card/top_values/top_values.tsx | 4 +- .../field_data_row/number_content_preview.tsx | 2 +- .../stats_datagrid/expanded_row.tsx | 2 +- .../stats_datagrid/stats_datagrid.tsx | 2 +- .../data_visualizer/index_data_visualizer.ts | 173 +++++----- .../test/functional/services/ml/common_ui.ts | 45 +++ .../ml/data_visualizer_index_based.ts | 298 +----------------- .../services/ml/data_visualizer_table.ts | 266 ++++++++++++---- x-pack/test/functional/services/ml/index.ts | 9 +- 13 files changed, 388 insertions(+), 461 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/multi_select_picker/multi_select_picker.tsx b/x-pack/plugins/ml/public/application/components/multi_select_picker/multi_select_picker.tsx index 30af6b80266c6..659c4a1005453 100644 --- a/x-pack/plugins/ml/public/application/components/multi_select_picker/multi_select_picker.tsx +++ b/x-pack/plugins/ml/public/application/components/multi_select_picker/multi_select_picker.tsx @@ -84,6 +84,7 @@ export const MultiSelectPicker: FC<{ const button = ( - setSearchTerm(e.target.value)} /> + setSearchTerm(e.target.value)} + data-test-subj={`${dataTestSubj}-searchInput`} + />
{Array.isArray(items) && items.length > 0 ? ( - items.map((item, index) => ( - fieldValue === item.value) > -1 - ? 'on' - : undefined - } - key={index} - onClick={() => handleOnChange(index)} - style={{ flexDirection: 'row' }} - > - {item.name ?? item.value} - - )) + items.map((item, index) => { + const checked = + checkedOptions && + checkedOptions.findIndex((fieldValue) => fieldValue === item.value) > -1; + + return ( + handleOnChange(index)} + style={{ flexDirection: 'row' }} + data-test-subj={`${dataTestSubj}-option-${item.value}${ + checked ? '-checked' : '' + }`} + > + {item.name ?? item.value} + + ); + }) ) : ( )} diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/document_count_chart/document_count_chart.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/document_count_chart/document_count_chart.tsx index 6023d32767039..6a02cb6acebd4 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/document_count_chart/document_count_chart.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/components/field_data_card/document_count_chart/document_count_chart.tsx @@ -52,7 +52,7 @@ export const DocumentCountChart: FC = ({ const dateFormatter = niceTimeFormatter([timeRangeEarliest, timeRangeLatest]); return ( -
+
= ({ examples }) => { }); return ( -
+
= ({ }; return ( -
+
= ({ stats, fieldFormat, barColor, compressed } = stats; const progressBarMax = isTopValuesSampled === true ? topValuesSampleSize : count; return ( -
+
{Array.isArray(topValues) && topValues.map((value: any) => ( @@ -64,7 +64,7 @@ export const TopValues: FC = ({ stats, fieldFormat, barColor, compressed - + = ({ config }) => { const defaultChartData: MetricDistributionChartData[] = []; const [distributionChartData, setDistributionChartData] = useState(defaultChartData); const [legendText, setLegendText] = useState<{ min: number; max: number } | undefined>(); - const dataTestSubj = fieldName; + const dataTestSubj = `mlDataGridChart-${fieldName}`; useEffect(() => { const chartData = buildChartDataFromStats(stats, METRIC_DISTRIBUTION_CHART_WIDTH); if ( diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/expanded_row.tsx b/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/expanded_row.tsx index fe6e0b6ec9a8a..cb83d6db83ed3 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/expanded_row.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/expanded_row.tsx @@ -60,7 +60,7 @@ export const DataVisualizerFieldExpandedRow = ({ item }: { item: FieldVisConfig return (
{loading === true ? : getCardContent()}
diff --git a/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/stats_datagrid.tsx b/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/stats_datagrid.tsx index be85d7ad01596..3cd7851175a85 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/stats_datagrid.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/stats_datagrid/stats_datagrid.tsx @@ -112,7 +112,7 @@ export const DataVisualizerDataGrid = ({ const direction = expandedRowItemIds.includes(item.fieldName) ? 'arrowUp' : 'arrowDown'; return ( toggleDetails(item)} aria-label={ expandedRowItemIds.includes(item.fieldName) diff --git a/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer.ts b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer.ts index 4b793c4c3adba..5a8b9bfc114ee 100644 --- a/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer.ts +++ b/x-pack/test/functional/apps/ml/data_visualizer/index_data_visualizer.ts @@ -16,28 +16,25 @@ interface MetricFieldVisConfig extends FieldVisConfig { interface NonMetricFieldVisConfig extends FieldVisConfig { docCountFormatted: string; - exampleCount?: number; + exampleCount: number; } interface TestData { suiteTitle: string; sourceIndexOrSavedSearch: string; - metricFieldsFilter: string; - nonMetricFieldsFilter: string; - nonMetricFieldsTypeFilter: string; + fieldNameFilters: string[]; + fieldTypeFilters: string[]; expected: { totalDocCountFormatted: string; - fieldsPanelCount: number; - documentCountCard: FieldVisConfig; - metricCards?: MetricFieldVisConfig[]; - nonMetricCards?: NonMetricFieldVisConfig[]; - nonMetricFieldsTypeFilterCardCount: number; - metricFieldsFilterCardCount: number; - nonMetricFieldsFilterCardCount: number; + metricFields?: MetricFieldVisConfig[]; + nonMetricFields?: NonMetricFieldVisConfig[]; + emptyFields: string[]; visibleMetricFieldsCount: number; totalMetricFieldsCount: number; populatedFieldsCount: number; totalFieldsCount: number; + fieldNameFiltersResultCount: number; + fieldTypeFiltersResultCount: number; }; } @@ -48,19 +45,11 @@ export default function ({ getService }: FtrProviderContext) { const farequoteIndexPatternTestData: TestData = { suiteTitle: 'index pattern', sourceIndexOrSavedSearch: 'ft_farequote', - metricFieldsFilter: 'document', - nonMetricFieldsFilter: 'airline', - nonMetricFieldsTypeFilter: 'keyword', + fieldNameFilters: ['airline', '@timestamp'], + fieldTypeFilters: [ML_JOB_FIELD_TYPES.KEYWORD], expected: { totalDocCountFormatted: '86,274', - fieldsPanelCount: 2, // Metrics panel and Fields panel - documentCountCard: { - type: ML_JOB_FIELD_TYPES.NUMBER, - existsInDocs: true, - aggregatable: true, - loading: false, - }, - metricCards: [ + metricFields: [ { fieldName: 'responsetime', type: ML_JOB_FIELD_TYPES.NUMBER, @@ -72,7 +61,7 @@ export default function ({ getService }: FtrProviderContext) { topValuesCount: 10, }, ], - nonMetricCards: [ + nonMetricFields: [ { fieldName: '@timestamp', type: ML_JOB_FIELD_TYPES.DATE, @@ -80,6 +69,7 @@ export default function ({ getService }: FtrProviderContext) { aggregatable: true, loading: false, docCountFormatted: '5000 (100%)', + exampleCount: 2, }, { fieldName: '@version', @@ -127,32 +117,24 @@ export default function ({ getService }: FtrProviderContext) { docCountFormatted: '5000 (100%)', }, ], - nonMetricFieldsTypeFilterCardCount: 3, - metricFieldsFilterCardCount: 1, - nonMetricFieldsFilterCardCount: 1, + emptyFields: ['sourcetype'], visibleMetricFieldsCount: 1, totalMetricFieldsCount: 1, populatedFieldsCount: 7, totalFieldsCount: 8, + fieldNameFiltersResultCount: 2, + fieldTypeFiltersResultCount: 3, }, }; const farequoteKQLSearchTestData: TestData = { suiteTitle: 'KQL saved search', sourceIndexOrSavedSearch: 'ft_farequote_kuery', - metricFieldsFilter: 'responsetime', - nonMetricFieldsFilter: 'airline', - nonMetricFieldsTypeFilter: 'keyword', + fieldNameFilters: ['@version'], + fieldTypeFilters: [ML_JOB_FIELD_TYPES.DATE, ML_JOB_FIELD_TYPES.TEXT], expected: { totalDocCountFormatted: '34,415', - fieldsPanelCount: 2, // Metrics panel and Fields panel - documentCountCard: { - type: ML_JOB_FIELD_TYPES.NUMBER, - existsInDocs: true, - aggregatable: true, - loading: false, - }, - metricCards: [ + metricFields: [ { fieldName: 'responsetime', type: ML_JOB_FIELD_TYPES.NUMBER, @@ -164,7 +146,7 @@ export default function ({ getService }: FtrProviderContext) { topValuesCount: 10, }, ], - nonMetricCards: [ + nonMetricFields: [ { fieldName: '@timestamp', type: ML_JOB_FIELD_TYPES.DATE, @@ -172,6 +154,7 @@ export default function ({ getService }: FtrProviderContext) { aggregatable: true, loading: false, docCountFormatted: '5000 (100%)', + exampleCount: 2, }, { fieldName: '@version', @@ -219,32 +202,24 @@ export default function ({ getService }: FtrProviderContext) { docCountFormatted: '5000 (100%)', }, ], - nonMetricFieldsTypeFilterCardCount: 3, - metricFieldsFilterCardCount: 2, - nonMetricFieldsFilterCardCount: 1, + emptyFields: ['sourcetype'], visibleMetricFieldsCount: 1, totalMetricFieldsCount: 1, populatedFieldsCount: 7, totalFieldsCount: 8, + fieldNameFiltersResultCount: 1, + fieldTypeFiltersResultCount: 3, }, }; const farequoteLuceneSearchTestData: TestData = { suiteTitle: 'lucene saved search', sourceIndexOrSavedSearch: 'ft_farequote_lucene', - metricFieldsFilter: 'responsetime', - nonMetricFieldsFilter: 'version', - nonMetricFieldsTypeFilter: 'keyword', + fieldNameFilters: ['@version.keyword', 'type'], + fieldTypeFilters: [ML_JOB_FIELD_TYPES.NUMBER], expected: { totalDocCountFormatted: '34,416', - fieldsPanelCount: 2, // Metrics panel and Fields panel - documentCountCard: { - type: ML_JOB_FIELD_TYPES.NUMBER, // document count card - existsInDocs: true, - aggregatable: true, - loading: false, - }, - metricCards: [ + metricFields: [ { fieldName: 'responsetime', type: ML_JOB_FIELD_TYPES.NUMBER, @@ -256,7 +231,7 @@ export default function ({ getService }: FtrProviderContext) { topValuesCount: 10, }, ], - nonMetricCards: [ + nonMetricFields: [ { fieldName: '@timestamp', type: ML_JOB_FIELD_TYPES.DATE, @@ -264,6 +239,7 @@ export default function ({ getService }: FtrProviderContext) { aggregatable: true, loading: false, docCountFormatted: '5000 (100%)', + exampleCount: 2, }, { fieldName: '@version', @@ -311,13 +287,13 @@ export default function ({ getService }: FtrProviderContext) { docCountFormatted: '5000 (100%)', }, ], - nonMetricFieldsTypeFilterCardCount: 3, - metricFieldsFilterCardCount: 2, - nonMetricFieldsFilterCardCount: 1, + emptyFields: ['sourcetype'], visibleMetricFieldsCount: 1, totalMetricFieldsCount: 1, populatedFieldsCount: 7, totalFieldsCount: 8, + fieldNameFiltersResultCount: 2, + fieldTypeFiltersResultCount: 1, }, }; @@ -345,21 +321,25 @@ export default function ({ getService }: FtrProviderContext) { testData.expected.totalDocCountFormatted ); - await ml.testExecution.logTestStep(`${testData.suiteTitle} displays the doc count`); + await ml.testExecution.logTestStep( + `${testData.suiteTitle} displays elements in the doc count panel correctly` + ); await ml.dataVisualizerIndexBased.assertTotalDocCountHeaderExist(); await ml.dataVisualizerIndexBased.assertTotalDocCountChartExist(); - await ml.testExecution.logTestStep(`${testData.suiteTitle} displays the search panel count`); - await ml.dataVisualizerIndexBased.assertSearchPanelExist(); - await ml.dataVisualizerIndexBased.assertSampleSizeInputExists(); - await ml.dataVisualizerIndexBased.assertFieldTypeInputExists(); - await ml.dataVisualizerIndexBased.assertFieldNameInputExists(); + await ml.testExecution.logTestStep( + `${testData.suiteTitle} displays elements in the data visualizer table correctly` + ); + await ml.dataVisualizerIndexBased.assertDataVisualizerTableExist(); + + await ml.dataVisualizerTable.assertSearchPanelExist(); + await ml.dataVisualizerTable.assertSampleSizeInputExists(); + await ml.dataVisualizerTable.assertFieldTypeInputExists(); + await ml.dataVisualizerTable.assertFieldNameInputExists(); - await ml.testExecution.logTestStep(`${testData.suiteTitle} displays the field count`); await ml.dataVisualizerIndexBased.assertFieldCountPanelExist(); await ml.dataVisualizerIndexBased.assertMetricFieldsSummaryExist(); await ml.dataVisualizerIndexBased.assertFieldsSummaryExist(); - await ml.dataVisualizerIndexBased.assertShowEmptyFieldsSwitchExists(); await ml.dataVisualizerIndexBased.assertVisibleMetricFieldsCount( testData.expected.visibleMetricFieldsCount ); @@ -372,35 +352,58 @@ export default function ({ getService }: FtrProviderContext) { await ml.dataVisualizerIndexBased.assertTotalFieldsCount(testData.expected.totalFieldsCount); await ml.testExecution.logTestStep( - `${testData.suiteTitle} displays the data visualizer table` - ); - await ml.dataVisualizerIndexBased.assertDataVisualizerTableExist(); - - await ml.testExecution.logTestStep( - 'displays details for the created job in the analytics table' + 'displays details for metric fields and non-metric fields correctly' ); - for (const fieldCard of testData.expected.metricCards as Array< + for (const fieldRow of testData.expected.metricFields as Array< Required >) { - await ml.dataVisualizerTable.assertNumberRowContents( - fieldCard.fieldName, - fieldCard.docCountFormatted + await ml.dataVisualizerTable.assertNumberFieldContents( + fieldRow.fieldName, + fieldRow.docCountFormatted, + fieldRow.topValuesCount ); } - for (const fieldCard of testData.expected.nonMetricCards as Array< - Required - >) { - await ml.dataVisualizerTable.assertRowExists(fieldCard.fieldName); - } - - for (const fieldCard of testData.expected.nonMetricCards!) { - await ml.dataVisualizerTable.assertNonMetricCardContents( - fieldCard.type, - fieldCard.fieldName!, - fieldCard.docCountFormatted + for (const fieldRow of testData.expected.nonMetricFields!) { + await ml.dataVisualizerTable.assertNonMetricFieldContents( + fieldRow.type, + fieldRow.fieldName!, + fieldRow.docCountFormatted, + fieldRow.exampleCount ); } + + await ml.testExecution.logTestStep( + `${testData.suiteTitle} sample size control changes non-metric fields` + ); + await ml.dataVisualizerTable.setSampleSizeInputValue(1000, 'airline', '1000 (100%)'); + await ml.dataVisualizerTable.setSampleSizeInputValue(5000, '@timestamp', '5000 (100%)'); + + await ml.testExecution.logTestStep('sets and resets field type filter correctly'); + await ml.dataVisualizerTable.setFieldTypeFilter( + testData.fieldTypeFilters, + testData.expected.fieldTypeFiltersResultCount + ); + await ml.dataVisualizerTable.removeFieldTypeFilter( + testData.fieldTypeFilters, + testData.expected.populatedFieldsCount + ); + + await ml.testExecution.logTestStep('sets and resets field name filter correctly'); + await ml.dataVisualizerTable.setFieldNameFilter( + testData.fieldNameFilters, + testData.expected.fieldNameFiltersResultCount + ); + await ml.dataVisualizerTable.removeFieldNameFilter( + testData.fieldNameFilters, + testData.expected.populatedFieldsCount + ); + + await ml.testExecution.logTestStep('displays unpopulated fields correctly'); + await ml.dataVisualizerTable.setShowEmptyFieldsSwitchState( + true, + testData.expected.emptyFields + ); }); } diff --git a/x-pack/test/functional/services/ml/common_ui.ts b/x-pack/test/functional/services/ml/common_ui.ts index ef2fccab8a2cc..8113a1ab9ac5f 100644 --- a/x-pack/test/functional/services/ml/common_ui.ts +++ b/x-pack/test/functional/services/ml/common_ui.ts @@ -20,6 +20,7 @@ export function MachineLearningCommonUIProvider({ getService }: FtrProviderConte const retry = getService('retry'); const testSubjects = getService('testSubjects'); const find = getService('find'); + const browser = getService('browser'); return { async setValueWithChecks( @@ -116,5 +117,49 @@ export function MachineLearningCommonUIProvider({ getService }: FtrProviderConte await label.click(); await this.assertRadioGroupValue(testSubject, value); }, + + async setMultiSelectFilter(testDataSubj: string, fieldTypes: string[]) { + await testSubjects.clickWhenNotDisabled(`${testDataSubj}-button`); + await testSubjects.existOrFail(`${testDataSubj}-popover`); + await testSubjects.existOrFail(`${testDataSubj}-searchInput`); + const searchBarInput = await testSubjects.find(`${testDataSubj}-searchInput`); + + for (const fieldType of fieldTypes) { + await retry.tryForTime(5000, async () => { + await searchBarInput.clearValueWithKeyboard(); + await searchBarInput.type(fieldType); + if (!(await testSubjects.exists(`${testDataSubj}-option-${fieldType}-checked`))) { + await testSubjects.existOrFail(`${testDataSubj}-option-${fieldType}`); + await testSubjects.click(`${testDataSubj}-option-${fieldType}`); + await testSubjects.existOrFail(`${testDataSubj}-option-${fieldType}-checked`); + } + }); + } + + // escape popover + await browser.pressKeys(browser.keys.ESCAPE); + }, + + async removeMultiSelectFilter(testDataSubj: string, fieldTypes: string[]) { + await testSubjects.clickWhenNotDisabled(`${testDataSubj}-button`); + await testSubjects.existOrFail(`${testDataSubj}-popover`); + await testSubjects.existOrFail(`${testDataSubj}-searchInput`); + const searchBarInput = await testSubjects.find(`${testDataSubj}-searchInput`); + + for (const fieldType of fieldTypes) { + await retry.tryForTime(5000, async () => { + await searchBarInput.clearValueWithKeyboard(); + await searchBarInput.type(fieldType); + if (!(await testSubjects.exists(`${testDataSubj}-option-${fieldType}`))) { + await testSubjects.existOrFail(`${testDataSubj}-option-${fieldType}-checked`); + await testSubjects.click(`${testDataSubj}-option-${fieldType}-checked`); + await testSubjects.existOrFail(`${testDataSubj}-option-${fieldType}`); + } + }); + } + + // escape popover + await browser.pressKeys(browser.keys.ESCAPE); + }, }; } diff --git a/x-pack/test/functional/services/ml/data_visualizer_index_based.ts b/x-pack/test/functional/services/ml/data_visualizer_index_based.ts index 413f4565ef30d..5fc5caf81c23b 100644 --- a/x-pack/test/functional/services/ml/data_visualizer_index_based.ts +++ b/x-pack/test/functional/services/ml/data_visualizer_index_based.ts @@ -4,19 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ import expect from '@kbn/expect'; - import { FtrProviderContext } from '../../ftr_provider_context'; -import { ML_JOB_FIELD_TYPES } from '../../../../plugins/ml/common/constants/field_types'; -import { MlCommonUI } from './common_ui'; -import { MlJobFieldType } from '../../../../plugins/ml/common/types/field_types'; -export function MachineLearningDataVisualizerIndexBasedProvider( - { getService }: FtrProviderContext, - mlCommonUI: MlCommonUI -) { +export function MachineLearningDataVisualizerIndexBasedProvider({ + getService, +}: FtrProviderContext) { const testSubjects = getService('testSubjects'); const retry = getService('retry'); - const browser = getService('browser'); return { async assertTimeRangeSelectorSectionExists() { @@ -43,15 +37,7 @@ export function MachineLearningDataVisualizerIndexBasedProvider( }, async assertTotalDocCountChartExist() { - await testSubjects.existOrFail(`mlFieldDataCardDocumentCountChart`); - }, - - async assertSearchPanelExist() { - await testSubjects.existOrFail(`mlDataVisualizerSearchPanel`); - }, - - async assertSearchQueryInputExist() { - await testSubjects.existOrFail(`mlDataVisualizerQueryInput`); + await testSubjects.existOrFail(`mlFieldDataDocumentCountChart`); }, async assertFieldCountPanelExist() { @@ -122,282 +108,6 @@ export function MachineLearningDataVisualizerIndexBasedProvider( await testSubjects.existOrFail(`mlDataVisualizerTable`); }, - async assertFieldsPanelForTypesExist(fieldTypes: MlJobFieldType[]) { - await testSubjects.existOrFail(`mlDataVisualizerFieldsPanel ${fieldTypes}`); - }, - - async assertCardExists(cardType: string, fieldName?: string) { - await testSubjects.existOrFail(`mlFieldDataCard ${fieldName} ${cardType}`); - }, - - async assertCardContentsExists(cardType: string, fieldName?: string) { - await testSubjects.existOrFail( - `mlFieldDataCard ${fieldName} ${cardType} > mlFieldDataCardContent` - ); - }, - - async assertNonMetricCardContents(cardType: string, fieldName: string, exampleCount?: number) { - await this.assertCardContentsExists(cardType, fieldName); - - // Currently the data used in the data visualizer tests only contains these field types. - if (cardType === ML_JOB_FIELD_TYPES.DATE) { - await this.assertDateCardContents(fieldName); - } else if (cardType === ML_JOB_FIELD_TYPES.KEYWORD) { - await this.assertKeywordCardContents(fieldName, exampleCount!); - } else if (cardType === ML_JOB_FIELD_TYPES.TEXT) { - await this.assertTextCardContents(fieldName, exampleCount!); - } - }, - - async assertDocumentCountCardContents() { - await this.assertCardContentsExists('number', undefined); - await testSubjects.existOrFail( - 'mlFieldDataCard undefined number > mlFieldDataCardDocumentCountChart' - ); - }, - - async assertNumberCardContents( - fieldName: string, - docCountFormatted: string, - statsMaxDecimalPlaces: number, - selectedDetailsMode: 'distribution' | 'top_values', - topValuesCount: number - ) { - await this.assertCardContentsExists('number', fieldName); - await this.assertFieldDocCountExists('number', fieldName); - await this.assertFieldDocCountContents('number', fieldName, docCountFormatted); - await this.assertFieldCardinalityExists('number', fieldName); - - await this.assertNumberStatsContents(fieldName, 'Min', statsMaxDecimalPlaces); - await this.assertNumberStatsContents(fieldName, 'Median', statsMaxDecimalPlaces); - await this.assertNumberStatsContents(fieldName, 'Max', statsMaxDecimalPlaces); - - await testSubjects.existOrFail( - `mlFieldDataCard ${fieldName} number > mlFieldDataCardDetailsSelect` - ); - - if (selectedDetailsMode === 'distribution') { - await mlCommonUI.assertRadioGroupValue( - `mlFieldDataCard ${fieldName} number > mlFieldDataCardDetailsSelect`, - 'distribution' - ); - await testSubjects.existOrFail( - `mlFieldDataCard ${fieldName} number > mlFieldDataCardMetricDistributionChart` - ); - - await mlCommonUI.selectRadioGroupValue( - `mlFieldDataCard ${fieldName} number > mlFieldDataCardDetailsSelect`, - 'top_values' - ); - await this.assertTopValuesContents('number', fieldName, topValuesCount); - } else { - await mlCommonUI.assertRadioGroupValue( - `mlFieldDataCard ${fieldName} number > mlFieldDataCardDetailsSelect`, - 'top_values' - ); - await this.assertTopValuesContents('number', fieldName, topValuesCount); - - await mlCommonUI.selectRadioGroupValue( - `mlFieldDataCard ${fieldName} number > mlFieldDataCardDetailsSelect`, - 'distribution' - ); - await testSubjects.existOrFail( - `mlFieldDataCard ${fieldName} number > mlFieldDataCardMetricDistributionChart` - ); - } - }, - - async assertDateCardContents(fieldName: string) { - await this.assertFieldDocCountExists('date', fieldName); - await testSubjects.existOrFail(`mlFieldDataCard ${fieldName} date > mlFieldDataCardEarliest`); - await testSubjects.existOrFail(`mlFieldDataCard ${fieldName} date > mlFieldDataCardLatest`); - }, - - async assertKeywordCardContents(fieldName: string, expectedTopValuesCount: number) { - await this.assertFieldDocCountExists('keyword', fieldName); - await this.assertFieldCardinalityExists('keyword', fieldName); - await this.assertTopValuesContents('keyword', fieldName, expectedTopValuesCount); - }, - - async assertTextCardContents(fieldName: string, expectedExamplesCount: number) { - const examplesList = await testSubjects.find( - `mlFieldDataCard ${fieldName} text > mlFieldDataCardExamplesList` - ); - const examplesListItems = await examplesList.findAllByTagName('li'); - expect(examplesListItems).to.have.length( - expectedExamplesCount, - `Expected example list item count for field '${fieldName}' to be '${expectedExamplesCount}' (got '${examplesListItems.length}')` - ); - }, - - async assertFieldDocCountExists(cardType: string, fieldName: string) { - await testSubjects.existOrFail( - `mlFieldDataCard ${fieldName} ${cardType} > mlFieldDataCardDocCount` - ); - }, - - async assertFieldDocCountContents( - cardType: string, - fieldName: string, - docCountFormatted: string - ) { - const docCountText = await testSubjects.getVisibleText( - `mlFieldDataCard ${fieldName} ${cardType} > mlFieldDataCardDocCount` - ); - expect(docCountText).to.contain( - docCountFormatted, - `Expected doc count for '${fieldName}' to be '${docCountFormatted}' (got contents '${docCountText}')` - ); - }, - - async assertFieldCardinalityExists(cardType: string, fieldName: string) { - await testSubjects.existOrFail( - `mlFieldDataCard ${fieldName} ${cardType} > mlFieldDataCardCardinality` - ); - }, - - async assertNumberStatsContents( - fieldName: string, - stat: 'Min' | 'Median' | 'Max', - maxDecimalPlaces: number - ) { - const statElement = await testSubjects.find( - `mlFieldDataCard ${fieldName} number > mlFieldDataCard${stat}` - ); - const statValue = await statElement.getVisibleText(); - const dotIdx = statValue.indexOf('.'); - const numDecimalPlaces = dotIdx === -1 ? 0 : statValue.length - dotIdx - 1; - expect(numDecimalPlaces).to.be.lessThan( - maxDecimalPlaces + 1, - `Expected number of decimal places for '${fieldName}' '${stat}' to be less than or equal to '${maxDecimalPlaces}' (got '${numDecimalPlaces}')` - ); - }, - - async assertTopValuesContents( - cardType: string, - fieldName: string, - expectedTopValuesCount: number - ) { - const topValuesElement = await testSubjects.find( - `mlFieldDataCard ${fieldName} ${cardType} > mlFieldDataCardTopValues` - ); - const topValuesBars = await topValuesElement.findAllByTestSubject( - 'mlFieldDataCardTopValueBar' - ); - expect(topValuesBars).to.have.length( - expectedTopValuesCount, - `Expected top values count for field '${fieldName}' to be '${expectedTopValuesCount}' (got '${topValuesBars.length}')` - ); - }, - - async assertFieldsPanelCardCount(panelFieldTypes: string[], expectedCardCount: number) { - await retry.tryForTime(5000, async () => { - const filteredCards = await testSubjects.findAll( - `mlDataVisualizerFieldsPanel ${panelFieldTypes} > ~mlFieldDataCard` - ); - expect(filteredCards).to.have.length( - expectedCardCount, - `Expected field card count for panels '${panelFieldTypes}' to be '${expectedCardCount}' (got '${filteredCards.length}')` - ); - }); - }, - - async assertFieldsPanelSearchInputValue(fieldTypes: string[], expectedSearchValue: string) { - const searchBar = await testSubjects.find( - `mlDataVisualizerFieldsPanel ${fieldTypes} > mlDataVisualizerFieldsSearchBarDiv` - ); - const searchBarInput = await searchBar.findByTagName('input'); - const actualSearchValue = await searchBarInput.getAttribute('value'); - expect(actualSearchValue).to.eql( - expectedSearchValue, - `Expected search value for field types '${fieldTypes}' to be '${expectedSearchValue}' (got '${actualSearchValue}')` - ); - }, - - async clearFieldsPanelSearchInput(fieldTypes: string[]) { - const searchBar = await testSubjects.find( - `mlDataVisualizerFieldsPanel ${fieldTypes} > mlDataVisualizerFieldsSearchBarDiv` - ); - const searchBarInput = await searchBar.findByTagName('input'); - await searchBarInput.clearValueWithKeyboard(); - await searchBarInput.pressKeys(browser.keys.ENTER); - }, - - async filterFieldsPanelWithSearchString( - fieldTypes: string[], - filter: string, - expectedCardCount: number - ) { - const searchBar = await testSubjects.find( - `mlDataVisualizerFieldsPanel ${fieldTypes} > mlDataVisualizerFieldsSearchBarDiv` - ); - const searchBarInput = await searchBar.findByTagName('input'); - await searchBarInput.clearValueWithKeyboard(); - await searchBarInput.type(filter); - await searchBarInput.pressKeys(browser.keys.ENTER); - await this.assertFieldsPanelSearchInputValue(fieldTypes, filter); - - await this.assertFieldsPanelCardCount(fieldTypes, expectedCardCount); - }, - - async assertFieldsPanelTypeInputExists(panelFieldTypes: string[]) { - await testSubjects.existOrFail( - `mlDataVisualizerFieldsPanel ${panelFieldTypes} > mlDataVisualizerFieldTypesSelect` - ); - }, - - async assertFieldsPanelTypeInputValue(expectedTypeValue: string) { - const actualTypeValue = await testSubjects.getAttribute( - 'mlDataVisualizerFieldTypesSelect', - 'value' - ); - expect(actualTypeValue).to.eql( - expectedTypeValue, - `Expected fields panel type value to be '${expectedTypeValue}' (got '${actualTypeValue}')` - ); - }, - - async setFieldsPanelTypeInputValue( - panelFieldTypes: string[], - filterFieldType: string, - expectedCardCount: number - ) { - await testSubjects.selectValue('mlDataVisualizerFieldTypesSelect', filterFieldType); - await this.assertFieldsPanelTypeInputValue(filterFieldType); - await this.assertFieldsPanelCardCount(panelFieldTypes, expectedCardCount); - }, - - async assertShowEmptyFieldsSwitchExists() { - await testSubjects.existOrFail('mlDataVisualizerShowEmptyFieldsSwitch'); - }, - - async assertFieldNameInputExists() { - await testSubjects.existOrFail('mlDataVisualizerFieldNameSelect'); - }, - - async assertFieldTypeInputExists() { - await testSubjects.existOrFail('mlDataVisualizerFieldTypeSelect'); - }, - - async assertSampleSizeInputExists() { - await testSubjects.existOrFail('mlDataVisualizerShardSizeSelect'); - }, - - async setSampleSizeInputValue( - sampleSize: number, - cardType: string, - fieldName: string, - docCountFormatted: string - ) { - await testSubjects.clickWhenNotDisabled('mlDataVisualizerShardSizeSelect'); - await testSubjects.existOrFail(`mlDataVisualizerShardSizeOption ${sampleSize}`); - await testSubjects.click(`mlDataVisualizerShardSizeOption ${sampleSize}`); - - await retry.tryForTime(5000, async () => { - await this.assertFieldDocCountContents(cardType, fieldName, docCountFormatted); - }); - }, - async assertActionsPanelExists() { await testSubjects.existOrFail('mlDataVisualizerActionsPanel'); }, diff --git a/x-pack/test/functional/services/ml/data_visualizer_table.ts b/x-pack/test/functional/services/ml/data_visualizer_table.ts index 94f7b618af998..f8623842a596d 100644 --- a/x-pack/test/functional/services/ml/data_visualizer_table.ts +++ b/x-pack/test/functional/services/ml/data_visualizer_table.ts @@ -4,10 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ import expect from '@kbn/expect'; +import { ProvidedType } from '@kbn/test/types/ftr'; import { FtrProviderContext } from '../../ftr_provider_context'; import { ML_JOB_FIELD_TYPES } from '../../../../plugins/ml/common/constants/field_types'; +import { MlCommonUI } from './common_ui'; +export type MlDataVisualizerTable = ProvidedType; -export function MachineLearningDataVisualizerTableProvider({ getService }: FtrProviderContext) { +export function MachineLearningDataVisualizerTableProvider( + { getService }: FtrProviderContext, + mlCommonUI: MlCommonUI +) { const retry = getService('retry'); const testSubjects = getService('testSubjects'); @@ -52,6 +58,16 @@ export function MachineLearningDataVisualizerTableProvider({ getService }: FtrPr return rows; } + public async assertTableRowCount(expectedRowCount: number) { + await retry.tryForTime(5000, async () => { + const tableRows = await this.parseDataVisualizerTable(); + expect(tableRows).to.have.length( + expectedRowCount, + `Data Visualizer table should have ${expectedRowCount} row(s) (got '${tableRows.length}')` + ); + }); + } + public rowSelector(fieldName: string, subSelector?: string) { const row = `~mlDataVisualizerTable > ~row-${fieldName}`; return !subSelector ? row : `${row} > ${subSelector}`; @@ -61,20 +77,44 @@ export function MachineLearningDataVisualizerTableProvider({ getService }: FtrPr await testSubjects.existOrFail(this.rowSelector(fieldName)); } - public async expandRowDetails(fieldName: string, fieldType: string) { - const selector = this.rowSelector( - fieldName, - `mlDataVisualizerToggleDetails ${fieldName} arrowDown` - ); - await testSubjects.existOrFail(selector); - await testSubjects.click(selector); - await retry.tryForTime(5000, async () => { - await testSubjects.existOrFail( - this.rowSelector(fieldName, `mlDataVisualizerToggleDetails ${fieldName} arrowUp`) - ); - await testSubjects.existOrFail( - `mlDataVisualizerFieldExpandedRow ${fieldName} ${fieldType}` - ); + public detailsSelector(fieldName: string, subSelector?: string) { + const row = `~mlDataVisualizerTable > ~mlDataVisualizerFieldExpandedRow-${fieldName}`; + return !subSelector ? row : `${row} > ${subSelector}`; + } + + public async ensureDetailsOpen(fieldName: string) { + await retry.tryForTime(10000, async () => { + if (!(await testSubjects.exists(this.detailsSelector(fieldName)))) { + const selector = this.rowSelector( + fieldName, + `mlDataVisualizerDetailsToggle-${fieldName}-arrowDown` + ); + await testSubjects.click(selector); + await testSubjects.existOrFail( + this.rowSelector(fieldName, `mlDataVisualizerDetailsToggle-${fieldName}-arrowUp`), + { + timeout: 1000, + } + ); + await testSubjects.existOrFail(this.detailsSelector(fieldName), { timeout: 1000 }); + } + }); + } + + public async ensureDetailsClosed(fieldName: string) { + await retry.tryForTime(10000, async () => { + if (await testSubjects.exists(this.detailsSelector(fieldName))) { + await testSubjects.click( + this.rowSelector(fieldName, `mlDataVisualizerDetailsToggle-${fieldName}-arrowUp`) + ); + await testSubjects.existOrFail( + this.rowSelector(fieldName, `mlDataVisualizerDetailsToggle-${fieldName}-arrowDown`), + { + timeout: 1000, + } + ); + await testSubjects.missingOrFail(this.detailsSelector(fieldName), { timeout: 1000 }); + } }); } @@ -87,77 +127,197 @@ export function MachineLearningDataVisualizerTableProvider({ getService }: FtrPr const docCount = await testSubjects.getVisibleText(docCountFormattedSelector); expect(docCount).to.eql( docCountFormatted, - `Expected total document count to be '${docCountFormatted}' (got '${docCount}')` + `Expected field document count to be '${docCountFormatted}' (got '${docCount}')` ); } - public async assertNumberRowContents(fieldName: string, docCountFormatted: string) { - const fieldType = ML_JOB_FIELD_TYPES.NUMBER; - await this.assertRowExists(fieldName); - await this.assertFieldDocCount(fieldName, docCountFormatted); + public async assertFieldDistinctValuesExist(fieldName: string) { + const selector = this.rowSelector(fieldName, 'mlDataVisualizerTableColumnDistinctValues'); + await testSubjects.existOrFail(selector); + } - await this.expandRowDetails(fieldName, fieldType); + public async assertFieldDistributionExist(fieldName: string) { + const selector = this.rowSelector(fieldName, 'mlDataVisualizerTableColumnDistribution'); + await testSubjects.existOrFail(selector); + } - await testSubjects.existOrFail( - `mlDataVisualizerFieldExpandedRow ${fieldName} ${fieldType} > mlNumberSummaryTable` + public async assertSearchPanelExist() { + await testSubjects.existOrFail(`mlDataVisualizerSearchPanel`); + } + + public async assertFieldNameInputExists() { + await testSubjects.existOrFail('mlDataVisualizerFieldNameSelect'); + } + + public async assertFieldTypeInputExists() { + await testSubjects.existOrFail('mlDataVisualizerFieldTypeSelect'); + } + + public async assertSampleSizeInputExists() { + await testSubjects.existOrFail('mlDataVisualizerShardSizeSelect'); + } + + public async setSampleSizeInputValue( + sampleSize: number, + fieldName: string, + docCountFormatted: string + ) { + await this.assertSampleSizeInputExists(); + await testSubjects.clickWhenNotDisabled('mlDataVisualizerShardSizeSelect'); + await testSubjects.existOrFail(`mlDataVisualizerShardSizeOption ${sampleSize}`); + await testSubjects.click(`mlDataVisualizerShardSizeOption ${sampleSize}`); + + await retry.tryForTime(5000, async () => { + await this.assertFieldDocCount(fieldName, docCountFormatted); + }); + } + + public async setFieldTypeFilter(fieldTypes: string[], expectedRowCount = 1) { + await this.assertFieldTypeInputExists(); + await mlCommonUI.setMultiSelectFilter('mlDataVisualizerFieldTypeSelect', fieldTypes); + await this.assertTableRowCount(expectedRowCount); + } + + async removeFieldTypeFilter(fieldTypes: string[], expectedRowCount = 1) { + await this.assertFieldTypeInputExists(); + await mlCommonUI.removeMultiSelectFilter('mlDataVisualizerFieldTypeSelect', fieldTypes); + await this.assertTableRowCount(expectedRowCount); + } + + public async setFieldNameFilter(fieldNames: string[], expectedRowCount = 1) { + await this.assertFieldNameInputExists(); + await mlCommonUI.setMultiSelectFilter('mlDataVisualizerFieldNameSelect', fieldNames); + await this.assertTableRowCount(expectedRowCount); + } + + public async removeFieldNameFilter(fieldNames: string[], expectedRowCount: number) { + await this.assertFieldNameInputExists(); + await mlCommonUI.removeMultiSelectFilter('mlDataVisualizerFieldNameSelect', fieldNames); + await this.assertTableRowCount(expectedRowCount); + } + + public async assertShowEmptyFieldsSwitchExists() { + await testSubjects.existOrFail('mlDataVisualizerShowEmptyFieldsSwitch'); + } + + public async assertShowEmptyFieldsCheckState(expectedCheckState: boolean) { + const actualCheckState = + (await testSubjects.getAttribute( + 'mlDataVisualizerShowEmptyFieldsSwitch', + 'aria-checked' + )) === 'true'; + expect(actualCheckState).to.eql( + expectedCheckState, + `Show empty fields check state should be '${expectedCheckState}' (got '${actualCheckState}')` ); + return actualCheckState === expectedCheckState; + } - await testSubjects.existOrFail( - `mlDataVisualizerFieldExpandedRow ${fieldName} ${fieldType} > mlTopValues` + public async setShowEmptyFieldsSwitchState(checkState: boolean, expectedEmptyFields: string[]) { + await this.assertShowEmptyFieldsSwitchExists(); + await retry.tryForTime(5000, async () => { + if (await this.assertShowEmptyFieldsCheckState(!checkState)) { + await testSubjects.click('mlDataVisualizerShowEmptyFieldsSwitch'); + } + await this.assertShowEmptyFieldsCheckState(checkState); + for (const field of expectedEmptyFields) { + await this.assertRowExists(field); + } + }); + } + + public async assertTopValuesContents(fieldName: string, expectedTopValuesCount: number) { + const selector = this.detailsSelector(fieldName, 'mlFieldDataTopValues'); + const topValuesElement = await testSubjects.find(selector); + const topValuesBars = await topValuesElement.findAllByTestSubject('mlFieldDataTopValueBar'); + expect(topValuesBars).to.have.length( + expectedTopValuesCount, + `Expected top values count for field '${fieldName}' to be '${expectedTopValuesCount}' (got '${topValuesBars.length}')` ); + } + + public async assertDistributionPreviewExist(fieldName: string) { + await testSubjects.existOrFail(this.rowSelector(fieldName, `mlDataGridChart-${fieldName}`)); await testSubjects.existOrFail( - `mlDataVisualizerFieldExpandedRow ${fieldName} ${fieldType} > mlMetricDistribution` + this.rowSelector(fieldName, `mlDataGridChart-${fieldName}-histogram`) ); } - public async assertDateRowContents(fieldName: string, docCountFormatted: string) { - const fieldType = ML_JOB_FIELD_TYPES.DATE; + public async assertNumberFieldContents( + fieldName: string, + docCountFormatted: string, + topValuesCount: number + ) { await this.assertRowExists(fieldName); await this.assertFieldDocCount(fieldName, docCountFormatted); + await this.ensureDetailsOpen(fieldName); - await this.expandRowDetails(fieldName, fieldType); + await testSubjects.existOrFail(this.detailsSelector(fieldName, 'mlNumberSummaryTable')); - await testSubjects.existOrFail( - `mlDataVisualizerFieldExpandedRow ${fieldName} ${fieldType} > mlDateSummaryTable` - ); + await testSubjects.existOrFail(this.detailsSelector(fieldName, 'mlTopValues')); + await this.assertTopValuesContents(fieldName, topValuesCount); + + await this.assertDistributionPreviewExist(fieldName); + + await this.ensureDetailsClosed(fieldName); } - public async assertKeywordRowContents(fieldName: string, docCountFormatted: string) { - const fieldType = ML_JOB_FIELD_TYPES.KEYWORD; + public async assertDateFieldContents(fieldName: string, docCountFormatted: string) { await this.assertRowExists(fieldName); await this.assertFieldDocCount(fieldName, docCountFormatted); + await this.ensureDetailsOpen(fieldName); - await this.expandRowDetails(fieldName, fieldType); - - await testSubjects.existOrFail( - `mlDataVisualizerFieldExpandedRow ${fieldName} ${fieldType} > mlFieldDataCardTopValues` - ); + await testSubjects.existOrFail(this.detailsSelector(fieldName, 'mlDateSummaryTable')); + await this.ensureDetailsClosed(fieldName); } - public async assertTextRowContents(fieldName: string, docCountFormatted: string) { - const fieldType = ML_JOB_FIELD_TYPES.TEXT; + public async assertKeywordFieldContents( + fieldName: string, + docCountFormatted: string, + topValuesCount: number + ) { await this.assertRowExists(fieldName); await this.assertFieldDocCount(fieldName, docCountFormatted); + await this.ensureDetailsOpen(fieldName); - await this.expandRowDetails(fieldName, fieldType); + await testSubjects.existOrFail(this.detailsSelector(fieldName, 'mlFieldDataTopValues')); + await this.assertTopValuesContents(fieldName, topValuesCount); + await this.ensureDetailsClosed(fieldName); + } - await testSubjects.existOrFail( - `mlDataVisualizerFieldExpandedRow ${fieldName} ${fieldType} > mlFieldDataCardExamplesList` + public async assertTextFieldContents( + fieldName: string, + docCountFormatted: string, + expectedExamplesCount: number + ) { + await this.assertRowExists(fieldName); + await this.assertFieldDocCount(fieldName, docCountFormatted); + await this.ensureDetailsOpen(fieldName); + + const examplesList = await testSubjects.find( + this.detailsSelector(fieldName, 'mlFieldDataExamplesList') ); + const examplesListItems = await examplesList.findAllByTagName('li'); + expect(examplesListItems).to.have.length( + expectedExamplesCount, + `Expected example list item count for field '${fieldName}' to be '${expectedExamplesCount}' (got '${examplesListItems.length}')` + ); + await this.ensureDetailsClosed(fieldName); } - async assertNonMetricCardContents( - cardType: string, + public async assertNonMetricFieldContents( + fieldType: string, fieldName: string, - docCountFormatted: string + docCountFormatted: string, + exampleCount: number ) { // Currently the data used in the data visualizer tests only contains these field types. - if (cardType === ML_JOB_FIELD_TYPES.DATE) { - await this.assertDateRowContents(fieldName, docCountFormatted); - } else if (cardType === ML_JOB_FIELD_TYPES.KEYWORD) { - await this.assertKeywordRowContents(fieldName, docCountFormatted!); - } else if (cardType === ML_JOB_FIELD_TYPES.TEXT) { - await this.assertTextRowContents(fieldName, docCountFormatted!); + if (fieldType === ML_JOB_FIELD_TYPES.DATE) { + await this.assertDateFieldContents(fieldName, docCountFormatted); + } else if (fieldType === ML_JOB_FIELD_TYPES.KEYWORD) { + await this.assertKeywordFieldContents(fieldName, docCountFormatted, exampleCount); + } else if (fieldType === ML_JOB_FIELD_TYPES.TEXT) { + await this.assertTextFieldContents(fieldName, docCountFormatted, exampleCount); } } })(); diff --git a/x-pack/test/functional/services/ml/index.ts b/x-pack/test/functional/services/ml/index.ts index 559a12f0f5573..c1a9ac304dd69 100644 --- a/x-pack/test/functional/services/ml/index.ts +++ b/x-pack/test/functional/services/ml/index.ts @@ -63,13 +63,12 @@ export function MachineLearningProvider(context: FtrProviderContext) { const dataFrameAnalyticsResults = MachineLearningDataFrameAnalyticsResultsProvider(context); const dataFrameAnalyticsMap = MachineLearningDataFrameAnalyticsMapProvider(context); const dataFrameAnalyticsTable = MachineLearningDataFrameAnalyticsTableProvider(context); + const dataVisualizer = MachineLearningDataVisualizerProvider(context); + const dataVisualizerTable = MachineLearningDataVisualizerTableProvider(context, commonUI); + const dataVisualizerFileBased = MachineLearningDataVisualizerFileBasedProvider(context, commonUI); - const dataVisualizerIndexBased = MachineLearningDataVisualizerIndexBasedProvider( - context, - commonUI - ); - const dataVisualizerTable = MachineLearningDataVisualizerTableProvider(context); + const dataVisualizerIndexBased = MachineLearningDataVisualizerIndexBasedProvider(context); const jobManagement = MachineLearningJobManagementProvider(context, api); const jobSelection = MachineLearningJobSelectionProvider(context);