[ML] Functional tests for Analytics list row expansion content (#98678)

This commit is contained in:
Quynh Nguyen 2021-04-29 12:06:39 -05:00 committed by GitHub
parent 18213b673f
commit 762c57b995
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 331 additions and 22 deletions

View file

@ -93,6 +93,7 @@ export const JobMessages: FC<JobMessagesProps> = ({ messages, loading, error, re
compressed={true}
loading={loading}
error={error}
data-test-subj={'mlAnalyticsDetailsJobMessagesTable'}
/>
</>
);

View file

@ -192,6 +192,7 @@ export const ExpandedRow: FC<Props> = ({ item }) => {
}),
items: stateItems as SectionItem[],
position: 'left',
dataTestSubj: 'mlAnalyticsTableRowDetailsSection state',
};
const { currentPhase, totalPhases } = getDataFrameAnalyticsProgressPhase(item.stats);
@ -217,6 +218,7 @@ export const ExpandedRow: FC<Props> = ({ item }) => {
}),
],
position: 'right',
dataTestSubj: 'mlAnalyticsTableRowDetailsSection progress',
};
const stats: SectionConfig = {
@ -234,6 +236,7 @@ export const ExpandedRow: FC<Props> = ({ 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<Props> = ({ item }) => {
}),
],
position: 'right',
dataTestSubj: 'mlAnalyticsTableRowDetailsSection analysisStats',
}
: undefined;
@ -364,7 +368,13 @@ export const ExpandedRow: FC<Props> = ({ item }) => {
name: i18n.translate('xpack.ml.dataframe.analyticsList.expandedRow.tabs.jobSettingsLabel', {
defaultMessage: 'Job details',
}),
content: <ExpandedRowDetailsPane sections={detailsSections} />,
content: (
<ExpandedRowDetailsPane
sections={detailsSections}
dataTestSubj={`mlAnalyticsTableRowDetailsTabContent job-details ${item.config.id}`}
/>
),
'data-test-subj': `mlAnalyticsTableRowDetailsTab job-details ${item.config.id}`,
},
{
id: 'ml-analytics-job-stats',
@ -374,12 +384,24 @@ export const ExpandedRow: FC<Props> = ({ item }) => {
defaultMessage: 'Job stats',
}
),
content: <ExpandedRowDetailsPane sections={statsSections} />,
content: (
<ExpandedRowDetailsPane
sections={statsSections}
dataTestSubj={`mlAnalyticsTableRowDetailsTabContent job-stats ${item.config.id}`}
/>
),
'data-test-subj': `mlAnalyticsTableRowDetailsTab job-stats ${item.config.id}`,
},
{
id: 'ml-analytics-job-json',
name: 'JSON',
content: <ExpandedRowJsonPane json={item.config} />,
content: (
<ExpandedRowJsonPane
json={item.config}
dataTestSubj={`mlAnalyticsTableRowDetailsTabContent json ${item.config.id}`}
/>
),
'data-test-subj': `mlAnalyticsTableRowDetailsTab json ${item.config.id}`,
},
{
id: 'ml-analytics-job-messages',
@ -389,7 +411,13 @@ export const ExpandedRow: FC<Props> = ({ item }) => {
defaultMessage: 'Job messages',
}
),
content: <ExpandedRowMessagesPane analyticsId={item.id} />,
content: (
<ExpandedRowMessagesPane
analyticsId={item.id}
dataTestSubj={`mlAnalyticsTableRowDetailsTabContent job-messages ${item.config.id}`}
/>
),
'data-test-subj': `mlAnalyticsTableRowDetailsTab job-messages ${item.config.id}`,
},
];
@ -406,6 +434,7 @@ export const ExpandedRow: FC<Props> = ({ item }) => {
onTabClick={() => {}}
expand={false}
style={{ width: '100%' }}
data-test-subj={`mlAnalyticsTableRowDetails-${item.config.id}`}
/>
);
};

View file

@ -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<SectionProps> = ({ section }) => {
];
return (
<>
<div data-test-subj={section.dataTestSubj}>
<EuiTitle size="xs">
<span>{section.title}</span>
</EuiTitle>
@ -55,18 +56,23 @@ export const Section: FC<SectionProps> = ({ section }) => {
tableCaption={section.title}
tableLayout="auto"
className="mlExpandedRowDetailsSection"
data-test-subj={`${section.dataTestSubj}-table`}
/>
</>
</div>
);
};
interface ExpandedRowDetailsPaneProps {
sections: SectionConfig[];
dataTestSubj: string;
}
export const ExpandedRowDetailsPane: FC<ExpandedRowDetailsPaneProps> = ({ sections }) => {
export const ExpandedRowDetailsPane: FC<ExpandedRowDetailsPaneProps> = ({
sections,
dataTestSubj,
}) => {
return (
<EuiFlexGroup className="mlExpandedRowDetails">
<EuiFlexGroup className="mlExpandedRowDetails" data-test-subj={dataTestSubj}>
<EuiFlexItem style={{ width: '50%' }}>
{sections
.filter((s) => s.position === 'left')

View file

@ -11,11 +11,12 @@ import { EuiCodeEditor, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eu
interface Props {
json: object;
dataTestSubj: string;
}
export const ExpandedRowJsonPane: FC<Props> = ({ json }) => {
export const ExpandedRowJsonPane: FC<Props> = ({ json, dataTestSubj }) => {
return (
<EuiFlexGroup>
<EuiFlexGroup data-test-subj={dataTestSubj}>
<EuiFlexItem>
<EuiSpacer size="s" />
<EuiCodeEditor
@ -24,6 +25,7 @@ export const ExpandedRowJsonPane: FC<Props> = ({ json }) => {
mode="json"
style={{ width: '100%' }}
theme="textmate"
data-test-subj={`mlAnalyticsDetailsJsonPreview`}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>&nbsp;</EuiFlexItem>

View file

@ -17,9 +17,10 @@ import { useToastNotificationService } from '../../../../../services/toast_notif
interface Props {
analyticsId: string;
dataTestSubj: string;
}
export const ExpandedRowMessagesPane: FC<Props> = ({ analyticsId }) => {
export const ExpandedRowMessagesPane: FC<Props> = ({ analyticsId, dataTestSubj }) => {
const [messages, setMessages] = useState<JobMessage[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [errorMessage, setErrorMessage] = useState('');
@ -58,7 +59,7 @@ export const ExpandedRowMessagesPane: FC<Props> = ({ analyticsId }) => {
useRefreshAnalyticsList({ onRefresh: getMessages });
return (
<div className="mlExpandedRowJobMessages">
<div className="mlExpandedRowJobMessages" data-test-subj={dataTestSubj}>
<JobMessages
messages={messages}
loading={isLoading}

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { AnalyticsTableRowDetails } from '../../../services/ml/data_frame_analytics_table';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getService }: FtrProviderContext) {
@ -25,11 +26,12 @@ export default function ({ getService }: FtrProviderContext) {
await ml.api.cleanMlIndices();
});
const jobId = `bm_1_${Date.now()}`;
const testDataList = [
{
suiteTitle: 'bank marketing',
jobType: 'classification',
jobId: `bm_1_${Date.now()}`,
jobId,
jobDescription:
"Classification job based on 'ft_bank_marketing' dataset with dependentVariable 'y' and trainingPercent '20'",
source: 'ft_bank_marketing',
@ -68,6 +70,30 @@ export default function ({ getService }: FtrProviderContext) {
status: 'stopped',
progress: '100',
},
rowDetails: {
jobDetails: [
{
section: 'state',
expectedEntries: {
id: jobId,
state: 'stopped',
data_counts:
'{"training_docs_count":1862,"test_docs_count":7452,"skipped_docs_count":0}',
description:
"Classification job based on 'ft_bank_marketing' dataset with dependentVariable 'y' and trainingPercent '20'",
},
},
{ section: 'progress', expectedEntries: { Phase: '8/8' } },
],
jobStats: [
{
section: 'stats',
expectedEntries: {
version: '8.0.0',
},
},
],
} as AnalyticsTableRowDetails,
},
},
];
@ -230,6 +256,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 () => {

View file

@ -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 () => {

View file

@ -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 () => {

View file

@ -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');

View file

@ -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(

View file

@ -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<string, string>;
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<T>(jobId: string, block: () => Promise<T>): Promise<T> {
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<string, string> = {};
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);
});
}
})();
}