[ML] Add functional tests for file data visualizer (#60413)

This PR adds basic functional tests for the file data visualizer, covering a file import and error messages for non-log files. It also moves the file input path handling to a common location in order to avoid code duplication.
This commit is contained in:
Robert Oskamp 2020-03-19 09:08:43 +01:00 committed by GitHub
parent 9cd0a36740
commit 836b3d00ef
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 275 additions and 15 deletions

View file

@ -514,6 +514,12 @@ export function CommonPageProvider({ getService, getPageObjects }: FtrProviderCo
}
});
}
async setFileInputPath(path: string) {
log.debug(`Setting the path '${path}' on the file input`);
const input = await find.byCssSelector('.euiFilePicker__input');
await input.type(path);
}
}
return new CommonPage();

View file

@ -612,9 +612,7 @@ export function SettingsPageProvider({ getService, getPageObjects }: FtrProvider
log.debug(`Clicking importObjects`);
await testSubjects.click('importObjects');
log.debug(`Setting the path on the file input`);
const input = await find.byCssSelector('.euiFilePicker__input');
await input.type(path);
await PageObjects.common.setFileInputPath(path);
if (!overwriteAll) {
log.debug(`Toggling overwriteAll`);

View file

@ -25,7 +25,7 @@ import { WelcomeContent } from './welcome_content';
export function AboutPanel({ onFilePickerChange }) {
return (
<EuiPage restrictWidth={1000}>
<EuiPage restrictWidth={1000} data-test-subj="mlPageFileDataVisualizerUpload">
<EuiPageBody>
<EuiPageContent className="file-datavisualizer-about-panel__content">
<EuiFlexGroup gutterSize="xl">
@ -58,7 +58,7 @@ export function AboutPanel({ onFilePickerChange }) {
export function LoadingPanel() {
return (
<EuiPage restrictWidth={400}>
<EuiPage restrictWidth={400} data-test-subj="mlPageFileDataVisLoading">
<EuiPageBody>
<EuiPageContent className="file-datavisualizer-about-panel__content" paddingSize="l">
<div style={{ textAlign: 'center' }}>

View file

@ -48,6 +48,7 @@ export const BottomBar: FC<BottomBarProps> = ({ mode, onChangeMode, onCancel, di
fill
isDisabled={disableImport}
onClick={() => onChangeMode(DATAVISUALIZER_MODE.IMPORT)}
data-test-subj="mlFileDataVisOpenImportPageButton"
>
<FormattedMessage
id="xpack.ml.fileDatavisualizer.bottomBar.readMode.importButtonLabel"

View file

@ -62,6 +62,7 @@ export function FileTooLarge({ fileSize, maxFileSize }) {
}
color="danger"
iconType="cross"
data-test-subj="mlFileUploadErrorCallout fileTooLarge"
>
{errorText}
</EuiCallOut>
@ -79,6 +80,7 @@ export function FileCouldNotBeRead({ error, loaded }) {
}
color="danger"
iconType="cross"
data-test-subj="mlFileUploadErrorCallout fileCouldNotBeRead"
>
{error !== undefined && <p>{error}</p>}
{loaded && (

View file

@ -47,6 +47,7 @@ export const SimpleSettings = ({
defaultMessage: 'Index name, required field',
}
)}
data-test-subj="mlFileDataVisIndexNameInput"
/>
</EuiFormRow>
@ -63,6 +64,7 @@ export const SimpleSettings = ({
checked={createIndexPattern === true}
disabled={initialized === true}
onChange={onCreateIndexPatternChange}
data-test-subj="mlFileDataVisCreateIndexPatternCheckbox"
/>
</React.Fragment>
);

View file

@ -39,6 +39,7 @@ export function ImportSummary({
}
color="success"
iconType="check"
data-test-subj="mlFileImportSuccessCallout"
>
<EuiDescriptionList type="column" listItems={items} className="import-summary-list" />
</EuiCallOut>

View file

@ -462,7 +462,7 @@ export class ImportView extends Component {
initialized === true;
return (
<EuiPage>
<EuiPage data-test-subj="mlPageFileDataVisImport">
<EuiPageBody>
<EuiPageContentHeader>
<EuiTitle>
@ -470,7 +470,7 @@ export class ImportView extends Component {
</EuiTitle>
</EuiPageContentHeader>
<EuiSpacer size="m" />
<EuiPanel>
<EuiPanel data-test-subj="mlFileDataVisImportSettingsPanel">
<EuiTitle size="s">
<h2>
<FormattedMessage
@ -516,6 +516,7 @@ export class ImportView extends Component {
isLoading={importing}
iconSide="right"
fill
data-test-subj="mlFileDataVisImportButton"
>
<FormattedMessage
id="xpack.ml.fileDatavisualizer.importView.importButtonLabel"

View file

@ -36,16 +36,16 @@ export const ResultsView = ({ data, fileName, results, showEditFlyout }) => {
];
return (
<EuiPage>
<EuiPage data-test-subj="mlPageFileDataVisResults">
<EuiPageBody>
<EuiPageContentHeader>
<EuiTitle>
<h1>{fileName}</h1>
<h1 data-test-subj="mlFileDataVisResultsTitle">{fileName}</h1>
</EuiTitle>
</EuiPageContentHeader>
<EuiSpacer size="m" />
<div className="results">
<EuiPanel>
<EuiPanel data-test-subj="mlFileDataVisFileContentPanel">
<FileContents
data={data}
format={results.format}
@ -55,7 +55,7 @@ export const ResultsView = ({ data, fileName, results, showEditFlyout }) => {
<EuiSpacer size="m" />
<EuiPanel>
<EuiPanel data-test-subj="mlFileDataVisSummaryPanel">
<AnalysisSummary results={results} />
<EuiSpacer size="m" />
@ -70,7 +70,7 @@ export const ResultsView = ({ data, fileName, results, showEditFlyout }) => {
<EuiSpacer size="m" />
<EuiPanel>
<EuiPanel data-test-subj="mlFileDataVisFileStatsPanel">
<EuiTabbedContent tabs={tabs} initialSelectedTab={tabs[0]} onTabClick={() => {}} />
</EuiPanel>
</div>

View file

@ -0,0 +1,104 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import path from 'path';
import { FtrProviderContext } from '../../../ftr_provider_context';
// eslint-disable-next-line import/no-default-export
export default function({ getService }: FtrProviderContext) {
const ml = getService('ml');
const testDataListPositive = [
{
suiteSuffix: 'with an artificial server log',
filePath: path.join(__dirname, 'files_to_import', 'artificial_server_log'),
indexName: 'user-import_1',
createIndexPattern: false,
expected: {
results: {
title: 'artificial_server_log',
},
},
},
];
const testDataListNegative = [
{
suiteSuffix: 'with a non-log file',
filePath: path.join(__dirname, 'files_to_import', 'not_a_log_file'),
},
];
describe('file based', function() {
this.tags(['smoke', 'mlqa']);
before(async () => {
await ml.securityUI.loginAsMlPowerUser();
await ml.navigation.navigateToMl();
});
for (const testData of testDataListPositive) {
describe(testData.suiteSuffix, function() {
after(async () => {
await ml.api.deleteIndices(testData.indexName);
});
it('loads the data visualizer selector page', async () => {
await ml.navigation.navigateToDataVisualizer();
});
it('loads the file upload page', async () => {
await ml.dataVisualizer.navigateToFileUpload();
});
it('selects a file and loads visualizer results', async () => {
await ml.dataVisualizerFileBased.selectFile(testData.filePath);
});
it('displays the components of the file details page', async () => {
await ml.dataVisualizerFileBased.assertFileTitle(testData.expected.results.title);
await ml.dataVisualizerFileBased.assertFileContentPanelExists();
await ml.dataVisualizerFileBased.assertSummaryPanelExists();
await ml.dataVisualizerFileBased.assertFileStatsPanelExists();
});
it('loads the import settings page', async () => {
await ml.dataVisualizerFileBased.navigateToFileImport();
});
it('sets the index name', async () => {
await ml.dataVisualizerFileBased.setIndexName(testData.indexName);
});
it('sets the create index pattern checkbox', async () => {
await ml.dataVisualizerFileBased.setCreateIndexPatternCheckboxState(
testData.createIndexPattern
);
});
it('imports the file', async () => {
await ml.dataVisualizerFileBased.startImportAndWaitForProcessing();
});
});
}
for (const testData of testDataListNegative) {
describe(testData.suiteSuffix, function() {
it('loads the data visualizer selector page', async () => {
await ml.navigation.navigateToDataVisualizer();
});
it('loads the file upload page', async () => {
await ml.dataVisualizer.navigateToFileUpload();
});
it('selects a file and displays an error', async () => {
await ml.dataVisualizerFileBased.selectFile(testData.filePath, true);
});
});
}
});
}

View file

@ -0,0 +1,19 @@
2018-01-06 16:56:14.295748 INFO host:'Server A' Incoming connection from ip 123.456.789.0
2018-01-06 16:56:15.295748 INFO host:'Server A' Incoming connection from ip 123.456.789.1
2018-01-06 16:56:16.295748 INFO host:'Server A' Incoming connection from ip 123.456.789.2
2018-01-06 16:56:17.295748 INFO host:'Server A' Incoming connection from ip 123.456.789.3
2018-01-06 16:56:18.295748 INFO host:'Server B' Incoming connection from ip 123.456.789.0
2018-01-06 16:56:19.295748 INFO host:'Server B' Incoming connection from ip 123.456.789.2
2018-01-06 16:56:20.295748 INFO host:'Server B' Incoming connection from ip 123.456.789.3
2018-01-06 16:56:21.295748 INFO host:'Server B' Incoming connection from ip 123.456.789.4
2018-01-06 16:56:22.295748 WARN host:'Server A' Disk watermark 80%
2018-01-06 17:16:23.295748 WARN host:'Server A' Disk watermark 90%
2018-01-06 17:36:10.295748 ERROR host:'Server A' Main process crashed
2018-01-06 17:36:14.295748 INFO host:'Server A' Connection from ip 123.456.789.0 closed
2018-01-06 17:36:15.295748 INFO host:'Server A' Connection from ip 123.456.789.1 closed
2018-01-06 17:36:16.295748 INFO host:'Server A' Connection from ip 123.456.789.2 closed
2018-01-06 17:36:17.295748 INFO host:'Server A' Connection from ip 123.456.789.3 closed
2018-01-06 17:46:11.295748 INFO host:'Server B' Some special characters °!"§$%&/()=?`'^²³{[]}\+*~#'-_.:,;µ|<>äöüß
2018-01-06 17:46:12.295748 INFO host:'Server B' Shutting down

View file

@ -0,0 +1,8 @@
This
is
not
a
log
file

View file

@ -10,5 +10,6 @@ export default function({ loadTestFile }: FtrProviderContext) {
this.tags(['skipFirefox']);
loadTestFile(require.resolve('./index_data_visualizer'));
loadTestFile(require.resolve('./file_data_visualizer'));
});
}

View file

@ -515,9 +515,7 @@ export function GisPageProvider({ getService, getPageObjects }) {
}
async uploadJsonFileForIndexing(path) {
log.debug(`Setting the path on the file input`);
const input = await find.byCssSelector('.euiFilePicker__input');
await input.type(path);
await PageObjects.common.setFileInputPath(path);
log.debug(`File selected`);
await PageObjects.header.waitUntilLoadingHasFinished();

View file

@ -22,5 +22,10 @@ export function MachineLearningDataVisualizerProvider({ getService }: FtrProvide
await testSubjects.click('mlDataVisualizerSelectIndexButton');
await testSubjects.existOrFail('mlPageSourceSelection');
},
async navigateToFileUpload() {
await testSubjects.click('mlDataVisualizerUploadFileButton');
await testSubjects.existOrFail('mlPageFileDataVisualizerUpload');
},
};
}

View file

@ -0,0 +1,110 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* 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 { MlCommon } from './common';
export function MachineLearningDataVisualizerFileBasedProvider(
{ getService, getPageObjects }: FtrProviderContext,
mlCommon: MlCommon
) {
const log = getService('log');
const retry = getService('retry');
const testSubjects = getService('testSubjects');
const PageObjects = getPageObjects(['common']);
return {
async selectFile(path: string, expectError: boolean = false) {
log.debug(`Importing file '${path}' ...`);
await PageObjects.common.setFileInputPath(path);
await testSubjects.waitForDeleted('mlPageFileDataVisLoading');
if (expectError) {
await testSubjects.existOrFail('~mlFileUploadErrorCallout');
} else {
await testSubjects.missingOrFail('~mlFileUploadErrorCallout');
await testSubjects.existOrFail('mlPageFileDataVisResults');
}
},
async assertFileTitle(expectedTitle: string) {
const actualTitle = await testSubjects.getVisibleText('mlFileDataVisResultsTitle');
expect(actualTitle).to.eql(
expectedTitle,
`Expected file title to be '${expectedTitle}' (got '${actualTitle}')`
);
},
async assertFileContentPanelExists() {
await testSubjects.existOrFail('mlFileDataVisFileContentPanel');
},
async assertSummaryPanelExists() {
await testSubjects.existOrFail('mlFileDataVisSummaryPanel');
},
async assertFileStatsPanelExists() {
await testSubjects.existOrFail('mlFileDataVisFileStatsPanel');
},
async navigateToFileImport() {
await testSubjects.click('mlFileDataVisOpenImportPageButton');
await testSubjects.existOrFail('mlPageFileDataVisImport');
},
async assertImportSettingsPanelExists() {
await testSubjects.existOrFail('mlFileDataVisImportSettingsPanel');
},
async assertIndexNameValue(expectedValue: string) {
const actualIndexName = await testSubjects.getAttribute(
'mlFileDataVisIndexNameInput',
'value'
);
expect(actualIndexName).to.eql(
expectedValue,
`Expected index name to be '${expectedValue}' (got '${actualIndexName}')`
);
},
async setIndexName(indexName: string) {
await mlCommon.setValueWithChecks('mlFileDataVisIndexNameInput', indexName, {
clearWithKeyboard: true,
});
await this.assertIndexNameValue(indexName);
},
async assertCreateIndexPatternCheckboxValue(expectedValue: boolean) {
const isChecked = await testSubjects.isChecked('mlFileDataVisCreateIndexPatternCheckbox');
expect(isChecked).to.eql(
expectedValue,
`Expected create index pattern checkbox to be ${expectedValue ? 'checked' : 'unchecked'}`
);
},
async setCreateIndexPatternCheckboxState(newState: boolean) {
const isChecked = await testSubjects.isChecked('mlFileDataVisCreateIndexPatternCheckbox');
if (isChecked !== newState) {
// this checkbox can't be clicked directly, instead click the corresponding label
const panel = await testSubjects.find('mlFileDataVisImportSettingsPanel');
const label = await panel.findByCssSelector('[for="createIndexPattern"]');
await label.click();
}
await this.assertCreateIndexPatternCheckboxValue(newState);
},
async startImportAndWaitForProcessing() {
await testSubjects.click('mlFileDataVisImportButton');
await retry.tryForTime(60 * 1000, async () => {
await testSubjects.existOrFail('mlFileImportSuccessCallout');
});
},
};
}

View file

@ -13,6 +13,7 @@ export { MachineLearningDataFrameAnalyticsProvider } from './data_frame_analytic
export { MachineLearningDataFrameAnalyticsCreationProvider } from './data_frame_analytics_creation';
export { MachineLearningDataFrameAnalyticsTableProvider } from './data_frame_analytics_table';
export { MachineLearningDataVisualizerProvider } from './data_visualizer';
export { MachineLearningDataVisualizerFileBasedProvider } from './data_visualizer_file_based';
export { MachineLearningDataVisualizerIndexBasedProvider } from './data_visualizer_index_based';
export { MachineLearningJobManagementProvider } from './job_management';
export { MachineLearningJobSelectionProvider } from './job_selection';

View file

@ -16,6 +16,7 @@ import {
MachineLearningDataFrameAnalyticsCreationProvider,
MachineLearningDataFrameAnalyticsTableProvider,
MachineLearningDataVisualizerProvider,
MachineLearningDataVisualizerFileBasedProvider,
MachineLearningDataVisualizerIndexBasedProvider,
MachineLearningJobManagementProvider,
MachineLearningJobSelectionProvider,
@ -48,6 +49,7 @@ export function MachineLearningProvider(context: FtrProviderContext) {
);
const dataFrameAnalyticsTable = MachineLearningDataFrameAnalyticsTableProvider(context);
const dataVisualizer = MachineLearningDataVisualizerProvider(context);
const dataVisualizerFileBased = MachineLearningDataVisualizerFileBasedProvider(context, common);
const dataVisualizerIndexBased = MachineLearningDataVisualizerIndexBasedProvider(context);
const jobManagement = MachineLearningJobManagementProvider(context, api);
const jobSelection = MachineLearningJobSelectionProvider(context);
@ -75,6 +77,7 @@ export function MachineLearningProvider(context: FtrProviderContext) {
dataFrameAnalyticsCreation,
dataFrameAnalyticsTable,
dataVisualizer,
dataVisualizerFileBased,
dataVisualizerIndexBased,
jobManagement,
jobSelection,