From 762c57b9957d6c298f8addfa2887bc0d20f30d5c Mon Sep 17 00:00:00 2001 From: Quynh Nguyen <43350163+qn895@users.noreply.github.com> Date: Thu, 29 Apr 2021 12:06:39 -0500 Subject: [PATCH] [ML] Functional tests for Analytics list row expansion content (#98678) --- .../components/job_messages/job_messages.tsx | 1 + .../analytics_list/expanded_row.tsx | 37 +++- .../expanded_row_details_pane.tsx | 14 +- .../analytics_list/expanded_row_json_pane.tsx | 6 +- .../expanded_row_messages_pane.tsx | 5 +- .../classification_creation.ts | 33 +++- .../outlier_detection_creation.ts | 38 +++- .../regression_creation.ts | 36 +++- .../apps/ml/permissions/full_ml_access.ts | 2 +- .../apps/ml/permissions/read_ml_access.ts | 2 +- .../services/ml/data_frame_analytics_table.ts | 179 +++++++++++++++++- 11 files changed, 331 insertions(+), 22 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/job_messages/job_messages.tsx b/x-pack/plugins/ml/public/application/components/job_messages/job_messages.tsx index 76a7bc3d38f1..6ff348860253 100644 --- a/x-pack/plugins/ml/public/application/components/job_messages/job_messages.tsx +++ b/x-pack/plugins/ml/public/application/components/job_messages/job_messages.tsx @@ -93,6 +93,7 @@ export const JobMessages: FC = ({ messages, loading, error, re compressed={true} loading={loading} error={error} + data-test-subj={'mlAnalyticsDetailsJobMessagesTable'} /> ); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx index f7810c9be27a..3f7072fba404 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx @@ -192,6 +192,7 @@ export const ExpandedRow: FC = ({ item }) => { }), items: stateItems as SectionItem[], position: 'left', + dataTestSubj: 'mlAnalyticsTableRowDetailsSection state', }; const { currentPhase, totalPhases } = getDataFrameAnalyticsProgressPhase(item.stats); @@ -217,6 +218,7 @@ export const ExpandedRow: FC = ({ item }) => { }), ], position: 'right', + dataTestSubj: 'mlAnalyticsTableRowDetailsSection progress', }; const stats: SectionConfig = { @@ -234,6 +236,7 @@ export const ExpandedRow: FC = ({ item }) => { { title: 'version', description: item.config.version }, ], position: 'left', + dataTestSubj: 'mlAnalyticsTableRowDetailsSection stats', }; const analysisStats: SectionConfig | undefined = analysisStatsValues @@ -263,6 +266,7 @@ export const ExpandedRow: FC = ({ item }) => { }), ], position: 'right', + dataTestSubj: 'mlAnalyticsTableRowDetailsSection analysisStats', } : undefined; @@ -364,7 +368,13 @@ export const ExpandedRow: FC = ({ item }) => { name: i18n.translate('xpack.ml.dataframe.analyticsList.expandedRow.tabs.jobSettingsLabel', { defaultMessage: 'Job details', }), - content: , + content: ( + + ), + 'data-test-subj': `mlAnalyticsTableRowDetailsTab job-details ${item.config.id}`, }, { id: 'ml-analytics-job-stats', @@ -374,12 +384,24 @@ export const ExpandedRow: FC = ({ item }) => { defaultMessage: 'Job stats', } ), - content: , + content: ( + + ), + 'data-test-subj': `mlAnalyticsTableRowDetailsTab job-stats ${item.config.id}`, }, { id: 'ml-analytics-job-json', name: 'JSON', - content: , + content: ( + + ), + 'data-test-subj': `mlAnalyticsTableRowDetailsTab json ${item.config.id}`, }, { id: 'ml-analytics-job-messages', @@ -389,7 +411,13 @@ export const ExpandedRow: FC = ({ item }) => { defaultMessage: 'Job messages', } ), - content: , + content: ( + + ), + 'data-test-subj': `mlAnalyticsTableRowDetailsTab job-messages ${item.config.id}`, }, ]; @@ -406,6 +434,7 @@ export const ExpandedRow: FC = ({ item }) => { onTabClick={() => {}} expand={false} style={{ width: '100%' }} + data-test-subj={`mlAnalyticsTableRowDetails-${item.config.id}`} /> ); }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_details_pane.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_details_pane.tsx index 52234f8f2e4b..426bd89f07cc 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_details_pane.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_details_pane.tsx @@ -19,6 +19,7 @@ export interface SectionConfig { title: string; position: 'left' | 'right'; items: SectionItem[]; + dataTestSubj: string; } interface SectionProps { @@ -44,7 +45,7 @@ export const Section: FC = ({ section }) => { ]; return ( - <> +
{section.title} @@ -55,18 +56,23 @@ export const Section: FC = ({ section }) => { tableCaption={section.title} tableLayout="auto" className="mlExpandedRowDetailsSection" + data-test-subj={`${section.dataTestSubj}-table`} /> - +
); }; interface ExpandedRowDetailsPaneProps { sections: SectionConfig[]; + dataTestSubj: string; } -export const ExpandedRowDetailsPane: FC = ({ sections }) => { +export const ExpandedRowDetailsPane: FC = ({ + sections, + dataTestSubj, +}) => { return ( - + {sections .filter((s) => s.position === 'left') diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_json_pane.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_json_pane.tsx index 0fb5dd949190..eba0ac40e937 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_json_pane.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_json_pane.tsx @@ -11,11 +11,12 @@ import { EuiCodeEditor, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eu interface Props { json: object; + dataTestSubj: string; } -export const ExpandedRowJsonPane: FC = ({ json }) => { +export const ExpandedRowJsonPane: FC = ({ json, dataTestSubj }) => { return ( - + = ({ json }) => { mode="json" style={{ width: '100%' }} theme="textmate" + data-test-subj={`mlAnalyticsDetailsJsonPreview`} />   diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_messages_pane.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_messages_pane.tsx index 4f2d9c302184..7b90648967f3 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_messages_pane.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row_messages_pane.tsx @@ -17,9 +17,10 @@ import { useToastNotificationService } from '../../../../../services/toast_notif interface Props { analyticsId: string; + dataTestSubj: string; } -export const ExpandedRowMessagesPane: FC = ({ analyticsId }) => { +export const ExpandedRowMessagesPane: FC = ({ analyticsId, dataTestSubj }) => { const [messages, setMessages] = useState([]); const [isLoading, setIsLoading] = useState(false); const [errorMessage, setErrorMessage] = useState(''); @@ -58,7 +59,7 @@ export const ExpandedRowMessagesPane: FC = ({ analyticsId }) => { useRefreshAnalyticsList({ onRefresh: getMessages }); return ( -
+
{ diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts index 3866642383b2..332819f8a5f7 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts @@ -6,6 +6,7 @@ */ import { FtrProviderContext } from '../../../ftr_provider_context'; +import { AnalyticsTableRowDetails } from '../../../services/ml/data_frame_analytics_table'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); @@ -25,15 +26,17 @@ export default function ({ getService }: FtrProviderContext) { await ml.api.cleanMlIndices(); }); + const jobId = `ihp_1_${Date.now()}`; + const testDataList = [ { suiteTitle: 'iowa house prices', jobType: 'outlier_detection', - jobId: `ihp_1_${Date.now()}`, - jobDescription: 'This is the job description', + jobId, + jobDescription: 'Outlier detection job based on ft_ihp_outlier dataset with runtime fields', source: 'ft_ihp_outlier', get destinationIndex(): string { - return `user-${this.jobId}`; + return `user-${jobId}`; }, runtimeFields: { lowercase_central_air: { @@ -82,6 +85,30 @@ export default function ({ getService }: FtrProviderContext) { status: 'stopped', progress: '100', }, + rowDetails: { + jobDetails: [ + { + section: 'state', + expectedEntries: { + id: jobId, + state: 'stopped', + data_counts: + '{"training_docs_count":1460,"test_docs_count":0,"skipped_docs_count":0}', + description: + 'Outlier detection job based on ft_ihp_outlier dataset with runtime fields', + }, + }, + { section: 'progress', expectedEntries: { Phase: '4/4' } }, + ], + jobStats: [ + { + section: 'stats', + expectedEntries: { + version: '8.0.0', + }, + }, + ], + } as AnalyticsTableRowDetails, }, }, ]; @@ -246,6 +273,11 @@ export default function ({ getService }: FtrProviderContext) { status: testData.expected.row.status, progress: testData.expected.row.progress, }); + + await ml.dataFrameAnalyticsTable.assertAnalyticsRowDetails( + testData.jobId, + testData.expected.rowDetails + ); }); it('edits the analytics job and displays it correctly in the job list', async () => { diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts index a65d8986595c..b3603988e5b8 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts @@ -6,6 +6,7 @@ */ import { FtrProviderContext } from '../../../ftr_provider_context'; +import { AnalyticsTableRowDetails } from '../../../services/ml/data_frame_analytics_table'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); @@ -25,15 +26,16 @@ export default function ({ getService }: FtrProviderContext) { await ml.api.cleanMlIndices(); }); + const jobId = `egs_1_${Date.now()}`; const testDataList = [ { suiteTitle: 'electrical grid stability', jobType: 'regression', - jobId: `egs_1_${Date.now()}`, - jobDescription: 'This is the job description', + jobId, + jobDescription: 'Regression job based on ft_egs_regression dataset with runtime fields', source: 'ft_egs_regression', get destinationIndex(): string { - return `user-${this.jobId}`; + return `user-${jobId}`; }, runtimeFields: { uppercase_stab: { @@ -61,6 +63,30 @@ export default function ({ getService }: FtrProviderContext) { status: 'stopped', progress: '100', }, + rowDetails: { + jobDetails: [ + { + section: 'state', + expectedEntries: { + id: jobId, + state: 'stopped', + data_counts: + '{"training_docs_count":400,"test_docs_count":1600,"skipped_docs_count":0}', + description: + 'Regression job based on ft_egs_regression dataset with runtime fields', + }, + }, + { section: 'progress', expectedEntries: { Phase: '8/8' } }, + ], + jobStats: [ + { + section: 'stats', + expectedEntries: { + version: '8.0.0', + }, + }, + ], + } as AnalyticsTableRowDetails, }, }, ]; @@ -219,6 +245,10 @@ export default function ({ getService }: FtrProviderContext) { status: testData.expected.row.status, progress: testData.expected.row.progress, }); + await ml.dataFrameAnalyticsTable.assertAnalyticsRowDetails( + testData.jobId, + testData.expected.rowDetails + ); }); it('edits the analytics job and displays it correctly in the job list', async () => { diff --git a/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts b/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts index ac8ff055209c..a203b078774c 100644 --- a/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts +++ b/x-pack/test/functional/apps/ml/permissions/full_ml_access.ts @@ -309,7 +309,7 @@ export default function ({ getService }: FtrProviderContext) { 'should display enabled DFA job view and action menu' ); await ml.dataFrameAnalyticsTable.assertJobRowViewButtonEnabled(dfaJobId, true); - await ml.dataFrameAnalyticsTable.assertJowRowActionsMenuButtonEnabled(dfaJobId, true); + await ml.dataFrameAnalyticsTable.assertJobRowActionsMenuButtonEnabled(dfaJobId, true); await ml.dataFrameAnalyticsTable.assertJobActionViewButtonEnabled(dfaJobId, true); await ml.testExecution.logTestStep('should display enabled DFA job row action buttons'); diff --git a/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts b/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts index 95d0d2091642..55cfb035d0cf 100644 --- a/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts +++ b/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts @@ -300,7 +300,7 @@ export default function ({ getService }: FtrProviderContext) { 'should display enabled DFA job view and action menu' ); await ml.dataFrameAnalyticsTable.assertJobRowViewButtonEnabled(dfaJobId, true); - await ml.dataFrameAnalyticsTable.assertJowRowActionsMenuButtonEnabled(dfaJobId, true); + await ml.dataFrameAnalyticsTable.assertJobRowActionsMenuButtonEnabled(dfaJobId, true); await ml.dataFrameAnalyticsTable.assertJobActionViewButtonEnabled(dfaJobId, true); await ml.testExecution.logTestStep( diff --git a/x-pack/test/functional/services/ml/data_frame_analytics_table.ts b/x-pack/test/functional/services/ml/data_frame_analytics_table.ts index b069e6e35905..b499ce31ea2d 100644 --- a/x-pack/test/functional/services/ml/data_frame_analytics_table.ts +++ b/x-pack/test/functional/services/ml/data_frame_analytics_table.ts @@ -10,6 +10,13 @@ import expect from '@kbn/expect'; import { WebElementWrapper } from 'test/functional/services/lib/web_element_wrapper'; import { FtrProviderContext } from '../../ftr_provider_context'; +type ExpectedSectionTableEntries = Record; +export interface ExpectedSectionTable { + section: string; + expectedEntries: ExpectedSectionTableEntries; +} + +export type AnalyticsTableRowDetails = Record<'jobDetails' | 'jobStats', ExpectedSectionTable[]>; export function MachineLearningDataFrameAnalyticsTableProvider({ getService }: FtrProviderContext) { const find = getService('find'); const retry = getService('retry'); @@ -160,7 +167,7 @@ export function MachineLearningDataFrameAnalyticsTableProvider({ getService }: F ); } - public async assertJowRowActionsMenuButtonEnabled(analyticsId: string, expectedValue: boolean) { + public async assertJobRowActionsMenuButtonEnabled(analyticsId: string, expectedValue: boolean) { const isEnabled = await testSubjects.isEnabled( this.rowSelector(analyticsId, 'euiCollapsedItemActionsButton') ); @@ -259,5 +266,175 @@ export function MachineLearningDataFrameAnalyticsTableProvider({ getService }: F await testSubjects.click(`mlAnalyticsJobCloneButton`); await testSubjects.existOrFail('mlAnalyticsCreationContainer'); } + + public detailsSelector(jobId: string, subSelector?: string) { + const row = `mlAnalyticsTableRowDetails-${jobId}`; + return !subSelector ? row : `${row} > ${subSelector}`; + } + + public async assertRowDetailsTabExist(jobId: string, tabId: string) { + const selector = `~mlAnalyticsTableRowDetailsTab > ~${tabId} > ${jobId}`; + await testSubjects.existOrFail(selector); + } + + public async withDetailsOpen(jobId: string, block: () => Promise): Promise { + await this.ensureDetailsOpen(jobId); + try { + return await block(); + } finally { + await this.ensureDetailsClosed(jobId); + } + } + + public async ensureDetailsOpen(jobId: string) { + await retry.tryForTime(10000, async () => { + if (!(await testSubjects.exists(this.detailsSelector(jobId)))) { + await testSubjects.click(this.rowSelector(jobId, 'mlAnalyticsTableRowDetailsToggle')); + await testSubjects.existOrFail(this.detailsSelector(jobId), { timeout: 1000 }); + } + }); + } + + public async ensureDetailsClosed(jobId: string) { + await retry.tryForTime(10000, async () => { + if (await testSubjects.exists(this.detailsSelector(jobId))) { + await testSubjects.click(this.rowSelector(jobId, 'mlAnalyticsTableRowDetailsToggle')); + await testSubjects.missingOrFail(this.detailsSelector(jobId), { timeout: 1000 }); + } + }); + } + + public async assertRowDetailsTabsExist(tabTypeSubject: string, areaSubjects: string[]) { + await retry.tryForTime(10000, async () => { + const allTabs = await testSubjects.findAll(`~${tabTypeSubject}`, 3); + expect(allTabs).to.have.length( + areaSubjects.length, + `Expected number of '${tabTypeSubject}' to be '${areaSubjects.length}' (got '${allTabs.length}')` + ); + for (const areaSubj of areaSubjects) { + await testSubjects.existOrFail(`~${tabTypeSubject}&~${areaSubj}`, { timeout: 1000 }); + } + }); + } + + public async assertRowDetailsTabEnabled(tabSubject: string, expectedValue: boolean) { + const isEnabled = await testSubjects.isEnabled(tabSubject); + expect(isEnabled).to.eql( + expectedValue, + `Expected Analytics details tab '${tabSubject}' to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + } + + public async ensureDetailsTabOpen(jobId: string, tabSubject: string) { + const tabSelector = `~mlAnalyticsTableRowDetailsTab&~${tabSubject}&~${jobId}`; + const tabContentSelector = `~mlAnalyticsTableRowDetailsTabContent&~${tabSubject}&~${jobId}`; + + await retry.tryForTime(10000, async () => { + if (!(await testSubjects.exists(tabContentSelector))) { + await this.assertRowDetailsTabEnabled(tabSelector, true); + await testSubjects.click(tabSelector); + await testSubjects.existOrFail(tabContentSelector, { timeout: 1000 }); + } + }); + } + + public detailsSectionSelector(jobId: string, sectionSubject: string) { + const subSelector = `~mlAnalyticsTableRowDetailsSection&~${sectionSubject}`; + return this.detailsSelector(jobId, subSelector); + } + + public async assertDetailsSectionExists(jobId: string, sectionSubject: string) { + const selector = this.detailsSectionSelector(jobId, sectionSubject); + await retry.tryForTime(10000, async () => { + await testSubjects.existOrFail(selector, { timeout: 1000 }); + }); + } + + public async parseDetailsSectionTable(el: WebElementWrapper) { + const $ = await el.parseDomContent(); + const vars: Record = {}; + + for (const row of $('tr').toArray()) { + const [name, value] = $(row).find('td').toArray(); + + vars[$(name).text().trim()] = $(value).text().trim(); + } + + return vars; + } + + public async assertRowDetailsSectionContent( + jobId: string, + sectionSubject: string, + expectedEntries: ExpectedSectionTable['expectedEntries'] + ) { + const sectionSelector = this.detailsSectionSelector(jobId, sectionSubject); + await this.assertDetailsSectionExists(jobId, sectionSubject); + + const sectionTable = await testSubjects.find(`${sectionSelector}-table`); + const parsedSectionTableEntries = await this.parseDetailsSectionTable(sectionTable); + + for (const [key, value] of Object.entries(expectedEntries)) { + expect(parsedSectionTableEntries) + .to.have.property(key) + .eql( + value, + `Expected ${sectionSubject} property '${key}' to exist with value '${value}'` + ); + } + } + + public async assertJobDetailsTabContent(jobId: string, sections: ExpectedSectionTable[]) { + const tabSubject = 'job-details'; + await this.ensureDetailsTabOpen(jobId, tabSubject); + + for (const { section, expectedEntries } of sections) { + await this.assertRowDetailsSectionContent(jobId, section, expectedEntries); + } + } + + public async assertJobStatsTabContent(jobId: string, sections: ExpectedSectionTable[]) { + const tabSubject = 'job-stats'; + await this.ensureDetailsTabOpen(jobId, tabSubject); + await this.assertDetailsSectionExists(jobId, 'stats'); + + for (const { section, expectedEntries } of sections) { + await this.assertRowDetailsSectionContent(jobId, section, expectedEntries); + } + } + + public async assertJsonTabContent(jobId: string) { + const tabSubject = 'json'; + await this.ensureDetailsTabOpen(jobId, tabSubject); + await testSubjects.existOrFail(this.detailsSelector(jobId, 'mlAnalyticsDetailsJsonPreview')); + } + + public async assertJobMessagesTabContent(jobId: string) { + const tabSubject = 'job-messages'; + await this.ensureDetailsTabOpen(jobId, tabSubject); + await testSubjects.existOrFail( + this.detailsSelector(jobId, 'mlAnalyticsDetailsJobMessagesTable') + ); + } + + public async assertAnalyticsRowDetails( + jobId: string, + expectedRowDetails: AnalyticsTableRowDetails + ) { + return await this.withDetailsOpen(jobId, async () => { + await this.assertRowDetailsTabsExist('mlAnalyticsTableRowDetailsTab', [ + 'job-details', + 'job-stats', + 'json', + 'job-messages', + ]); + await this.assertJobDetailsTabContent(jobId, expectedRowDetails.jobDetails); + await this.assertJobStatsTabContent(jobId, expectedRowDetails.jobStats); + await this.assertJsonTabContent(jobId); + await this.assertJobMessagesTabContent(jobId); + }); + } })(); }