* [ML] update @elastic/charts * [ML] swim lane service, axes tests * [ML] check single cell selection and current URL * [ML] clear selection * [ML] assert anomaly explorer charts * [ML] fix unit test * [ML] assert anomalies table and top influencers list * [ML] update apiDoc version * [ML] exclude host from the URL assertion * [ML] clicks view by swim lane * [ML] fix method for cell selection * [ML] brush action tests * [ML] use debug state flag * [ML] declare window interface * [ML] pagination tests * [ML] enable test * [ML] scroll into view for swim lane actions * [ML] rename URL assertion method * [ML] fix assertion for charts count * [ML] extend assertion * [ML] refactor test subjects selection * [ML] fix assertSelection * [ML] reduce timeout for charts assertion Co-authored-by: Dima Arnautov <dmitrii.arnautov@elastic.co>
This commit is contained in:
parent
72ef242a38
commit
95e07fe5c7
|
@ -29,7 +29,11 @@ export function ElasticChartProvider({ getService }: FtrProviderContext) {
|
|||
const browser = getService('browser');
|
||||
|
||||
class ElasticChart {
|
||||
public async getCanvas() {
|
||||
public async getCanvas(dataTestSubj?: string) {
|
||||
if (dataTestSubj) {
|
||||
const chart = await this.getChart(dataTestSubj);
|
||||
return await chart.findByClassName('echCanvasRenderer');
|
||||
}
|
||||
return await find.byCssSelector('.echChart canvas:last-of-type');
|
||||
}
|
||||
|
||||
|
|
|
@ -160,7 +160,11 @@ export const AnomalyTimeline: FC<AnomalyTimelineProps> = React.memo(
|
|||
</EuiFlexItem>
|
||||
{selectedCells ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty size="xs" onClick={setSelectedCells.bind(null, undefined)}>
|
||||
<EuiButtonEmpty
|
||||
size="xs"
|
||||
onClick={setSelectedCells.bind(null, undefined)}
|
||||
data-test-subj="mlAnomalyTimelineClearSelection"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.explorer.clearSelectionLabel"
|
||||
defaultMessage="Clear selection"
|
||||
|
|
|
@ -255,7 +255,7 @@ export const ExplorerChartsContainerUI = ({
|
|||
return (
|
||||
<>
|
||||
<ExplorerChartsErrorCallOuts errorMessagesByType={errorMessages} />
|
||||
<EuiFlexGrid columns={chartsColumns}>
|
||||
<EuiFlexGrid columns={chartsColumns} data-test-subj="mlExplorerChartsContainer">
|
||||
{seriesToUse.length > 0 &&
|
||||
seriesToUse.map((series) => (
|
||||
<EuiFlexItem
|
||||
|
|
|
@ -79,7 +79,7 @@ describe('ExplorerChartsContainer', () => {
|
|||
);
|
||||
|
||||
expect(wrapper.html()).toBe(
|
||||
'<div class="euiFlexGrid euiFlexGrid--gutterLarge euiFlexGrid--wrap euiFlexGrid--responsive"></div>'
|
||||
'<div class="euiFlexGrid euiFlexGrid--gutterLarge euiFlexGrid--wrap euiFlexGrid--responsive" data-test-subj="mlExplorerChartsContainer"></div>'
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -26,6 +26,8 @@ import {
|
|||
HeatmapSpec,
|
||||
TooltipSettings,
|
||||
HeatmapBrushEvent,
|
||||
Position,
|
||||
ScaleType,
|
||||
} from '@elastic/charts';
|
||||
import moment from 'moment';
|
||||
|
||||
|
@ -44,6 +46,15 @@ import './_explorer.scss';
|
|||
import { EMPTY_FIELD_VALUE_LABEL } from '../timeseriesexplorer/components/entity_control/entity_control';
|
||||
import { useUiSettings } from '../contexts/kibana';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
/**
|
||||
* Flag used to enable debugState on elastic charts
|
||||
*/
|
||||
_echDebugStateFlag?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ignore insignificant resize, e.g. browser scrollbar appearance.
|
||||
*/
|
||||
|
@ -352,7 +363,7 @@ export const SwimlaneContainer: FC<SwimlaneProps> = ({
|
|||
direction={'column'}
|
||||
style={{ width: '100%', height: '100%', overflow: 'hidden' }}
|
||||
ref={resizeRef}
|
||||
data-test-subj="mlSwimLaneContainer"
|
||||
data-test-subj={dataTestSubj}
|
||||
>
|
||||
<EuiFlexItem
|
||||
style={{
|
||||
|
@ -361,26 +372,24 @@ export const SwimlaneContainer: FC<SwimlaneProps> = ({
|
|||
}}
|
||||
grow={false}
|
||||
>
|
||||
<div
|
||||
style={{ height: `${containerHeight}px`, position: 'relative' }}
|
||||
data-test-subj={dataTestSubj}
|
||||
>
|
||||
<div style={{ height: `${containerHeight}px`, position: 'relative' }}>
|
||||
{showSwimlane && !isLoading && (
|
||||
<Chart className={'mlSwimLaneContainer'}>
|
||||
<Settings
|
||||
onElementClick={onElementClick}
|
||||
showLegend
|
||||
legendPosition="top"
|
||||
legendPosition={Position.Top}
|
||||
xDomain={{
|
||||
min: swimlaneData.earliest * 1000,
|
||||
max: swimlaneData.latest * 1000,
|
||||
minInterval: swimlaneData.interval * 1000,
|
||||
}}
|
||||
tooltip={tooltipOptions}
|
||||
debugState={window._echDebugStateFlag ?? false}
|
||||
/>
|
||||
<Heatmap
|
||||
id={id}
|
||||
colorScale="threshold"
|
||||
colorScale={ScaleType.Threshold}
|
||||
ranges={[
|
||||
ANOMALY_THRESHOLD.LOW,
|
||||
ANOMALY_THRESHOLD.WARNING,
|
||||
|
@ -402,7 +411,7 @@ export const SwimlaneContainer: FC<SwimlaneProps> = ({
|
|||
valueAccessor="value"
|
||||
highlightedData={highlightedData}
|
||||
valueFormatter={getFormattedSeverityScore}
|
||||
xScaleType="time"
|
||||
xScaleType={ScaleType.Time}
|
||||
ySortPredicate="dataIndex"
|
||||
config={swimLaneConfig}
|
||||
/>
|
||||
|
|
|
@ -57,6 +57,7 @@ export const SwimLanePagination: FC<SwimLanePaginationProps> = ({
|
|||
closePopover();
|
||||
setPerPage(v);
|
||||
}}
|
||||
data-test-subj={`${v} rows`}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.explorer.swimLaneSelectRowsPerPage"
|
||||
|
@ -78,19 +79,22 @@ export const SwimLanePagination: FC<SwimLanePaginationProps> = ({
|
|||
iconType="arrowDown"
|
||||
iconSide="right"
|
||||
onClick={onButtonClick}
|
||||
data-test-subj="mlSwimLanePageSizeControl"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.explorer.swimLaneRowsPerPage"
|
||||
defaultMessage="Rows per page: {rowsCount}"
|
||||
values={{ rowsCount: perPage }}
|
||||
/>
|
||||
<span data-test-subj={perPage}>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.explorer.swimLaneRowsPerPage"
|
||||
defaultMessage="Rows per page: {rowsCount}"
|
||||
values={{ rowsCount: perPage }}
|
||||
/>
|
||||
</span>
|
||||
</EuiButtonEmpty>
|
||||
}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={closePopover}
|
||||
panelPaddingSize="none"
|
||||
>
|
||||
<EuiContextMenuPanel items={items} />
|
||||
<EuiContextMenuPanel items={items} data-test-subj="mlSwimLanePageSizePanel" />
|
||||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
|
||||
|
@ -102,6 +106,7 @@ export const SwimLanePagination: FC<SwimLanePaginationProps> = ({
|
|||
pageCount={pageCount}
|
||||
activePage={componentFromPage}
|
||||
onPageClick={goToPage}
|
||||
data-test-subj="mlSwimLanePagination"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "ml_kibana_api",
|
||||
"version": "7.11.0",
|
||||
"version": "7.13.0",
|
||||
"description": "This is the documentation of the REST API provided by the Machine Learning Kibana plugin. Each API is experimental and can include breaking changes in any version.",
|
||||
"title": "ML Kibana API",
|
||||
"order": [
|
||||
|
@ -159,6 +159,9 @@
|
|||
"GetTrainedModel",
|
||||
"GetTrainedModelStats",
|
||||
"GetTrainedModelPipelines",
|
||||
"DeleteTrainedModel"
|
||||
"DeleteTrainedModel",
|
||||
|
||||
"Alerting",
|
||||
"PreviewAlert"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { Block } from './types';
|
||||
|
||||
const API_VERSION = '7.8.0';
|
||||
const API_VERSION = '7.13.0';
|
||||
|
||||
/**
|
||||
* Post Filter parsed results.
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
import { Job, Datafeed } from '../../../../../plugins/ml/common/types/anomaly_detection_jobs';
|
||||
|
||||
|
@ -51,9 +52,15 @@ const testDataList = [
|
|||
},
|
||||
];
|
||||
|
||||
const cellSize = 15;
|
||||
|
||||
const overallSwimLaneTestSubj = 'mlAnomalyExplorerSwimlaneOverall';
|
||||
const viewBySwimLaneTestSubj = 'mlAnomalyExplorerSwimlaneViewBy';
|
||||
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
const esArchiver = getService('esArchiver');
|
||||
const ml = getService('ml');
|
||||
const elasticChart = getService('elasticChart');
|
||||
|
||||
describe('anomaly explorer', function () {
|
||||
this.tags(['mlqa']);
|
||||
|
@ -76,12 +83,16 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
after(async () => {
|
||||
await elasticChart.setNewChartUiDebugFlag(false);
|
||||
await ml.api.cleanMlIndices();
|
||||
});
|
||||
|
||||
it('opens a job from job list link', async () => {
|
||||
await ml.testExecution.logTestStep('navigate to job list');
|
||||
await ml.navigation.navigateToMl();
|
||||
// Set debug state has to happen at this point
|
||||
// because page refresh happens after navigation to the ML app.
|
||||
await elasticChart.setNewChartUiDebugFlag(true);
|
||||
await ml.navigation.navigateToJobManagement();
|
||||
|
||||
await ml.testExecution.logTestStep('open job in anomaly explorer');
|
||||
|
@ -126,6 +137,183 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
await ml.anomaliesTable.assertTableNotEmpty();
|
||||
});
|
||||
|
||||
it('renders Overall swim lane', async () => {
|
||||
await ml.testExecution.logTestStep('has correct axes labels');
|
||||
await ml.swimLane.assertAxisLabels(overallSwimLaneTestSubj, 'x', [
|
||||
'2016-02-07 00:00',
|
||||
'2016-02-08 00:00',
|
||||
'2016-02-09 00:00',
|
||||
'2016-02-10 00:00',
|
||||
'2016-02-11 00:00',
|
||||
'2016-02-12 00:00',
|
||||
]);
|
||||
await ml.swimLane.assertAxisLabels(overallSwimLaneTestSubj, 'y', ['Overall']);
|
||||
});
|
||||
|
||||
it('renders View By swim lane', async () => {
|
||||
await ml.testExecution.logTestStep('has correct axes labels');
|
||||
await ml.swimLane.assertAxisLabels(viewBySwimLaneTestSubj, 'x', [
|
||||
'2016-02-07 00:00',
|
||||
'2016-02-08 00:00',
|
||||
'2016-02-09 00:00',
|
||||
'2016-02-10 00:00',
|
||||
'2016-02-11 00:00',
|
||||
'2016-02-12 00:00',
|
||||
]);
|
||||
await ml.swimLane.assertAxisLabels(viewBySwimLaneTestSubj, 'y', [
|
||||
'AAL',
|
||||
'VRD',
|
||||
'EGF',
|
||||
'SWR',
|
||||
'AMX',
|
||||
'JZA',
|
||||
'TRS',
|
||||
'ACA',
|
||||
'BAW',
|
||||
'ASA',
|
||||
]);
|
||||
});
|
||||
|
||||
it('supports cell selection by click on Overall swim lane', async () => {
|
||||
await ml.testExecution.logTestStep('checking page state before the cell selection');
|
||||
await ml.anomalyExplorer.assertClearSelectionButtonVisible(false);
|
||||
await ml.anomaliesTable.assertTableRowsCount(25);
|
||||
await ml.anomalyExplorer.assertInfluencerFieldListLength('airline', 10);
|
||||
await ml.anomalyExplorer.assertAnomalyExplorerChartsCount(0);
|
||||
|
||||
await ml.testExecution.logTestStep('clicks on the Overall swim lane cell');
|
||||
const sampleCell = (await ml.swimLane.getCells(overallSwimLaneTestSubj))[0];
|
||||
await ml.swimLane.selectSingleCell(overallSwimLaneTestSubj, {
|
||||
x: sampleCell.x + cellSize,
|
||||
y: sampleCell.y + cellSize,
|
||||
});
|
||||
// TODO extend cell data with X and Y values, and cell width
|
||||
await ml.swimLane.assertSelection(overallSwimLaneTestSubj, {
|
||||
x: [1454846400000, 1454860800000],
|
||||
y: ['Overall'],
|
||||
});
|
||||
await ml.anomalyExplorer.assertClearSelectionButtonVisible(true);
|
||||
|
||||
await ml.testExecution.logTestStep('updates the View By swim lane');
|
||||
await ml.swimLane.assertAxisLabels(viewBySwimLaneTestSubj, 'y', ['EGF', 'DAL']);
|
||||
|
||||
await ml.testExecution.logTestStep('renders anomaly explorer charts');
|
||||
await ml.anomalyExplorer.assertAnomalyExplorerChartsCount(4);
|
||||
|
||||
await ml.testExecution.logTestStep('updates top influencers list');
|
||||
await ml.anomalyExplorer.assertInfluencerFieldListLength('airline', 2);
|
||||
|
||||
await ml.testExecution.logTestStep('updates anomalies table');
|
||||
await ml.anomaliesTable.assertTableRowsCount(4);
|
||||
|
||||
await ml.testExecution.logTestStep('updates the URL state');
|
||||
await ml.navigation.assertCurrentURLContains(
|
||||
'selectedLanes%3A!(Overall)%2CselectedTimes%3A!(1454846400%2C1454860800)%2CselectedType%3Aoverall%2CshowTopFieldValues%3A!t%2CviewByFieldName%3Aairline%2CviewByFromPage%3A1%2CviewByPerPage%3A10'
|
||||
);
|
||||
|
||||
await ml.testExecution.logTestStep('clears the selection');
|
||||
await ml.anomalyExplorer.clearSwimLaneSelection();
|
||||
await ml.navigation.assertCurrentURLNotContain(
|
||||
'selectedLanes%3A!(Overall)%2CselectedTimes%3A!(1454846400%2C1454860800)%2CselectedType%3Aoverall%2CshowTopFieldValues%3A!t%2CviewByFieldName%3Aairline%2CviewByFromPage%3A1%2CviewByPerPage%3A10'
|
||||
);
|
||||
await ml.anomaliesTable.assertTableRowsCount(25);
|
||||
await ml.anomalyExplorer.assertInfluencerFieldListLength('airline', 10);
|
||||
await ml.anomalyExplorer.assertAnomalyExplorerChartsCount(0);
|
||||
});
|
||||
|
||||
it('allows to change the swim lane pagination', async () => {
|
||||
await ml.testExecution.logTestStep('checks default pagination');
|
||||
await ml.swimLane.assertPageSize(viewBySwimLaneTestSubj, 10);
|
||||
await ml.swimLane.assertActivePage(viewBySwimLaneTestSubj, 1);
|
||||
|
||||
await ml.testExecution.logTestStep('updates pagination');
|
||||
await ml.swimLane.setPageSize(viewBySwimLaneTestSubj, 5);
|
||||
|
||||
const axisLabels = await ml.swimLane.getAxisLabels(viewBySwimLaneTestSubj, 'y');
|
||||
expect(axisLabels.length).to.eql(5);
|
||||
|
||||
await ml.swimLane.selectPage(viewBySwimLaneTestSubj, 3);
|
||||
|
||||
await ml.testExecution.logTestStep('resets pagination');
|
||||
await ml.swimLane.setPageSize(viewBySwimLaneTestSubj, 10);
|
||||
await ml.swimLane.assertActivePage(viewBySwimLaneTestSubj, 1);
|
||||
});
|
||||
|
||||
it('supports cell selection by click on View By swim lane', async () => {
|
||||
await ml.testExecution.logTestStep('checking page state before the cell selection');
|
||||
await ml.anomalyExplorer.assertClearSelectionButtonVisible(false);
|
||||
await ml.anomaliesTable.assertTableRowsCount(25);
|
||||
await ml.anomalyExplorer.assertInfluencerFieldListLength('airline', 10);
|
||||
await ml.anomalyExplorer.assertAnomalyExplorerChartsCount(0);
|
||||
|
||||
await ml.testExecution.logTestStep('clicks on the View By swim lane cell');
|
||||
await ml.anomalyExplorer.assertSwimlaneViewByExists();
|
||||
const sampleCell = (await ml.swimLane.getCells(viewBySwimLaneTestSubj))[0];
|
||||
await ml.swimLane.selectSingleCell(viewBySwimLaneTestSubj, {
|
||||
x: sampleCell.x + cellSize,
|
||||
y: sampleCell.y + cellSize,
|
||||
});
|
||||
|
||||
await ml.testExecution.logTestStep('check page content');
|
||||
await ml.swimLane.assertSelection(viewBySwimLaneTestSubj, {
|
||||
x: [1454817600000, 1454832000000],
|
||||
y: ['AAL'],
|
||||
});
|
||||
|
||||
await ml.anomaliesTable.assertTableRowsCount(1);
|
||||
await ml.anomalyExplorer.assertInfluencerFieldListLength('airline', 1);
|
||||
await ml.anomalyExplorer.assertAnomalyExplorerChartsCount(1);
|
||||
|
||||
await ml.testExecution.logTestStep('highlights the Overall swim lane');
|
||||
await ml.swimLane.assertSelection(overallSwimLaneTestSubj, {
|
||||
x: [1454817600000, 1454832000000],
|
||||
y: ['Overall'],
|
||||
});
|
||||
|
||||
await ml.testExecution.logTestStep('clears the selection');
|
||||
await ml.anomalyExplorer.clearSwimLaneSelection();
|
||||
await ml.anomaliesTable.assertTableRowsCount(25);
|
||||
await ml.anomalyExplorer.assertInfluencerFieldListLength('airline', 10);
|
||||
await ml.anomalyExplorer.assertAnomalyExplorerChartsCount(0);
|
||||
});
|
||||
|
||||
it('supports cell selection by brush action', async () => {
|
||||
await ml.testExecution.logTestStep('checking page state before the cell selection');
|
||||
await ml.anomalyExplorer.assertClearSelectionButtonVisible(false);
|
||||
await ml.anomaliesTable.assertTableRowsCount(25);
|
||||
await ml.anomalyExplorer.assertInfluencerFieldListLength('airline', 10);
|
||||
await ml.anomalyExplorer.assertAnomalyExplorerChartsCount(0);
|
||||
|
||||
await ml.anomalyExplorer.assertSwimlaneViewByExists();
|
||||
const cells = await ml.swimLane.getCells(viewBySwimLaneTestSubj);
|
||||
|
||||
const sampleCell1 = cells[0];
|
||||
// Get cell from another row
|
||||
const sampleCell2 = cells.find((c) => c.y !== sampleCell1.y);
|
||||
|
||||
await ml.swimLane.selectCells(viewBySwimLaneTestSubj, {
|
||||
x1: sampleCell1.x + cellSize,
|
||||
y1: sampleCell1.y + cellSize,
|
||||
x2: sampleCell2!.x + cellSize,
|
||||
y2: sampleCell2!.y + cellSize,
|
||||
});
|
||||
|
||||
await ml.swimLane.assertSelection(viewBySwimLaneTestSubj, {
|
||||
x: [1454817600000, 1454846400000],
|
||||
y: ['AAL', 'VRD'],
|
||||
});
|
||||
|
||||
await ml.anomaliesTable.assertTableRowsCount(2);
|
||||
await ml.anomalyExplorer.assertInfluencerFieldListLength('airline', 2);
|
||||
await ml.anomalyExplorer.assertAnomalyExplorerChartsCount(2);
|
||||
|
||||
await ml.testExecution.logTestStep('clears the selection');
|
||||
await ml.anomalyExplorer.clearSwimLaneSelection();
|
||||
await ml.anomaliesTable.assertTableRowsCount(25);
|
||||
await ml.anomalyExplorer.assertInfluencerFieldListLength('airline', 10);
|
||||
await ml.anomalyExplorer.assertAnomalyExplorerChartsCount(0);
|
||||
});
|
||||
|
||||
it('adds swim lane embeddable to a dashboard', async () => {
|
||||
// should be the last step because it navigates away from the Anomaly Explorer page
|
||||
await ml.testExecution.logTestStep(
|
||||
|
|
|
@ -22,6 +22,14 @@ export function MachineLearningAnomaliesTableProvider({ getService }: FtrProvide
|
|||
return await testSubjects.findAll('mlAnomaliesTable > ~mlAnomaliesListRow');
|
||||
},
|
||||
|
||||
async assertTableRowsCount(expectedCount: number) {
|
||||
const actualCount = (await this.getTableRows()).length;
|
||||
expect(actualCount).to.eql(
|
||||
expectedCount,
|
||||
`Expect anomaly table rows count to be ${expectedCount}, got ${actualCount}`
|
||||
);
|
||||
},
|
||||
|
||||
async getRowSubjByRowIndex(rowIndex: number) {
|
||||
const tableRows = await this.getTableRows();
|
||||
expect(tableRows.length).to.be.greaterThan(
|
||||
|
|
|
@ -111,5 +111,30 @@ export function MachineLearningAnomalyExplorerProvider({ getService }: FtrProvid
|
|||
await searchBarInput.clearValueWithKeyboard();
|
||||
await searchBarInput.type(filter);
|
||||
},
|
||||
|
||||
async assertClearSelectionButtonVisible(expectVisible: boolean) {
|
||||
if (expectVisible) {
|
||||
await testSubjects.existOrFail('mlAnomalyTimelineClearSelection');
|
||||
} else {
|
||||
await testSubjects.missingOrFail('mlAnomalyTimelineClearSelection');
|
||||
}
|
||||
},
|
||||
|
||||
async clearSwimLaneSelection() {
|
||||
await this.assertClearSelectionButtonVisible(true);
|
||||
await testSubjects.click('mlAnomalyTimelineClearSelection');
|
||||
await this.assertClearSelectionButtonVisible(false);
|
||||
},
|
||||
|
||||
async assertAnomalyExplorerChartsCount(expectedChartsCount: number) {
|
||||
const chartsContainer = await testSubjects.find('mlExplorerChartsContainer');
|
||||
const actualChartsCount = (
|
||||
await chartsContainer.findAllByClassName('ml-explorer-chart-container', 3000)
|
||||
).length;
|
||||
expect(actualChartsCount).to.eql(
|
||||
expectedChartsCount,
|
||||
`Expect ${expectedChartsCount} charts to appear, got ${actualChartsCount}`
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -45,6 +45,7 @@ import { MachineLearningTestExecutionProvider } from './test_execution';
|
|||
import { MachineLearningTestResourcesProvider } from './test_resources';
|
||||
import { MachineLearningDataVisualizerTableProvider } from './data_visualizer_table';
|
||||
import { MachineLearningAlertingProvider } from './alerting';
|
||||
import { SwimLaneProvider } from './swim_lane';
|
||||
|
||||
export function MachineLearningProvider(context: FtrProviderContext) {
|
||||
const commonAPI = MachineLearningCommonAPIProvider(context);
|
||||
|
@ -96,6 +97,7 @@ export function MachineLearningProvider(context: FtrProviderContext) {
|
|||
const testExecution = MachineLearningTestExecutionProvider(context);
|
||||
const testResources = MachineLearningTestResourcesProvider(context);
|
||||
const alerting = MachineLearningAlertingProvider(context, commonUI);
|
||||
const swimLane = SwimLaneProvider(context);
|
||||
|
||||
return {
|
||||
anomaliesTable,
|
||||
|
@ -134,6 +136,7 @@ export function MachineLearningProvider(context: FtrProviderContext) {
|
|||
settingsCalendar,
|
||||
settingsFilterList,
|
||||
singleMetricViewer,
|
||||
swimLane,
|
||||
testExecution,
|
||||
testResources,
|
||||
};
|
||||
|
|
|
@ -14,6 +14,7 @@ export function MachineLearningNavigationProvider({
|
|||
getPageObjects,
|
||||
}: FtrProviderContext) {
|
||||
const retry = getService('retry');
|
||||
const browser = getService('browser');
|
||||
const testSubjects = getService('testSubjects');
|
||||
const PageObjects = getPageObjects(['common']);
|
||||
|
||||
|
@ -156,7 +157,7 @@ export function MachineLearningNavigationProvider({
|
|||
},
|
||||
|
||||
async navigateToSingleMetricViewerViaAnomalyExplorer() {
|
||||
// clicks the `Single Metric Viewere` icon on the button group to switch result views
|
||||
// clicks the `Single Metric Viewer` icon on the button group to switch result views
|
||||
await testSubjects.click('mlAnomalyResultsViewSelectorSingleMetricViewer');
|
||||
await retry.tryForTime(60 * 1000, async () => {
|
||||
// verify that the single metric viewer page is visible
|
||||
|
@ -193,5 +194,25 @@ export function MachineLearningNavigationProvider({
|
|||
await testSubjects.existOrFail('homeApp', { timeout: 2000 });
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Assert the active URL.
|
||||
* @param expectedUrlPart - URL component excluding host
|
||||
*/
|
||||
async assertCurrentURLContains(expectedUrlPart: string) {
|
||||
const currentUrl = await browser.getCurrentUrl();
|
||||
expect(currentUrl).to.include.string(
|
||||
expectedUrlPart,
|
||||
`Expected the current URL "${currentUrl}" to include ${expectedUrlPart}`
|
||||
);
|
||||
},
|
||||
|
||||
async assertCurrentURLNotContain(expectedUrlPart: string) {
|
||||
const currentUrl = await browser.getCurrentUrl();
|
||||
expect(currentUrl).to.not.include.string(
|
||||
expectedUrlPart,
|
||||
`Expected the current URL "${currentUrl}" to not include ${expectedUrlPart}`
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
212
x-pack/test/functional/services/ml/swim_lane.ts
Normal file
212
x-pack/test/functional/services/ml/swim_lane.ts
Normal file
|
@ -0,0 +1,212 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { DebugState } from '@elastic/charts';
|
||||
import { DebugStateAxis } from '@elastic/charts/dist/state/types';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
import { WebElementWrapper } from '../../../../../test/functional/services/lib/web_element_wrapper';
|
||||
|
||||
type HeatmapDebugState = Required<Pick<DebugState, 'heatmap' | 'axes' | 'legend'>>;
|
||||
|
||||
export function SwimLaneProvider({ getService }: FtrProviderContext) {
|
||||
const elasticChart = getService('elasticChart');
|
||||
const browser = getService('browser');
|
||||
const testSubjects = getService('testSubjects');
|
||||
|
||||
/**
|
||||
* Y axis labels width + padding
|
||||
*/
|
||||
const xOffset = 185;
|
||||
|
||||
/**
|
||||
* Get coordinates relative to the left top corner of the canvas
|
||||
* and transpose them from the center point.
|
||||
*/
|
||||
async function getCoordinatesFromCenter(
|
||||
el: WebElementWrapper,
|
||||
coordinates: { x: number; y: number }
|
||||
) {
|
||||
const { width, height } = await el.getSize();
|
||||
|
||||
const elCenter = {
|
||||
x: Math.round(width / 2),
|
||||
y: Math.round(height / 2),
|
||||
};
|
||||
|
||||
/**
|
||||
* Origin of the element uses the center point, hence we need ot adjust
|
||||
* the click coordinated accordingly.
|
||||
*/
|
||||
const resultX = xOffset + Math.round(coordinates.x) - elCenter.x;
|
||||
const resultY = Math.round(coordinates.y) - elCenter.y;
|
||||
|
||||
return {
|
||||
x: resultX,
|
||||
y: resultY,
|
||||
};
|
||||
}
|
||||
|
||||
const getRenderTracker = async (testSubj: string) => {
|
||||
const renderCount = await elasticChart.getVisualizationRenderingCount(testSubj);
|
||||
|
||||
return {
|
||||
async verify() {
|
||||
if (testSubj === 'mlAnomalyExplorerSwimlaneViewBy') {
|
||||
// We have a glitchy behaviour when clicking on the View By swim lane.
|
||||
// The entire charts is re-rendered, hence it requires a different check
|
||||
await testSubjects.existOrFail(testSubj);
|
||||
await elasticChart.waitForRenderComplete(testSubj);
|
||||
} else {
|
||||
await elasticChart.waitForRenderingCount(renderCount + 1, testSubj);
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
async getDebugState(testSubj: string): Promise<HeatmapDebugState> {
|
||||
const state = await elasticChart.getChartDebugData(testSubj);
|
||||
if (!state) {
|
||||
throw new Error('Swim lane debug state is not available');
|
||||
}
|
||||
return state as HeatmapDebugState;
|
||||
},
|
||||
|
||||
async getAxisLabels(testSubj: string, axis: 'x' | 'y'): Promise<DebugStateAxis['labels']> {
|
||||
const state = await this.getDebugState(testSubj);
|
||||
return state.axes[axis][0].labels;
|
||||
},
|
||||
|
||||
async assertAxisLabels(testSubj: string, axis: 'x' | 'y', expectedValues: string[]) {
|
||||
const actualValues = await this.getAxisLabels(testSubj, axis);
|
||||
expect(actualValues).to.eql(
|
||||
expectedValues,
|
||||
`Expected swim lane ${axis} labels to be ${expectedValues}, got ${actualValues}`
|
||||
);
|
||||
},
|
||||
|
||||
async getCells(testSubj: string): Promise<HeatmapDebugState['heatmap']['cells']> {
|
||||
const state = await this.getDebugState(testSubj);
|
||||
return state.heatmap.cells;
|
||||
},
|
||||
|
||||
async getHighlighted(testSubj: string): Promise<HeatmapDebugState['heatmap']['selection']> {
|
||||
const state = await this.getDebugState(testSubj);
|
||||
return state.heatmap.selection;
|
||||
},
|
||||
|
||||
async assertSelection(
|
||||
testSubj: string,
|
||||
expectedData: HeatmapDebugState['heatmap']['selection']['data'],
|
||||
expectedArea?: HeatmapDebugState['heatmap']['selection']['area']
|
||||
) {
|
||||
const actualSelection = await this.getHighlighted(testSubj);
|
||||
expect(actualSelection.data).to.eql(
|
||||
expectedData,
|
||||
`Expected swim lane to have ${
|
||||
expectedData
|
||||
? `selected X-axis values ${expectedData.x.join(
|
||||
','
|
||||
)} and Y-axis values ${expectedData.y.join(',')}`
|
||||
: 'no data selected'
|
||||
}, got ${
|
||||
actualSelection.data
|
||||
? `${actualSelection.data.x.join(',')} and ${actualSelection.data.y.join(',')}`
|
||||
: 'null'
|
||||
}`
|
||||
);
|
||||
if (expectedArea) {
|
||||
expect(actualSelection.area).to.eql(expectedArea);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Selects a single cell
|
||||
* @param testSubj
|
||||
* @param x - number of pixels from the Y-axis
|
||||
* @param y - number of pixels from the top of the canvas element
|
||||
*/
|
||||
async selectSingleCell(testSubj: string, { x, y }: { x: number; y: number }) {
|
||||
await testSubjects.existOrFail(testSubj);
|
||||
await testSubjects.scrollIntoView(testSubj);
|
||||
const renderTracker = await getRenderTracker(testSubj);
|
||||
const el = await elasticChart.getCanvas(testSubj);
|
||||
|
||||
const { x: resultX, y: resultY } = await getCoordinatesFromCenter(el, { x, y });
|
||||
|
||||
await browser
|
||||
.getActions()
|
||||
.move({ x: resultX, y: resultY, origin: el._webElement })
|
||||
.click()
|
||||
.perform();
|
||||
|
||||
await renderTracker.verify();
|
||||
},
|
||||
|
||||
async selectCells(
|
||||
testSubj: string,
|
||||
coordinates: { x1: number; x2: number; y1: number; y2: number }
|
||||
) {
|
||||
await testSubjects.existOrFail(testSubj);
|
||||
await testSubjects.scrollIntoView(testSubj);
|
||||
const renderTracker = await getRenderTracker(testSubj);
|
||||
|
||||
const el = await elasticChart.getCanvas(testSubj);
|
||||
|
||||
const { x: resultX1, y: resultY1 } = await getCoordinatesFromCenter(el, {
|
||||
x: coordinates.x1,
|
||||
y: coordinates.y1,
|
||||
});
|
||||
const { x: resultX2, y: resultY2 } = await getCoordinatesFromCenter(el, {
|
||||
x: coordinates.x2,
|
||||
y: coordinates.y2,
|
||||
});
|
||||
|
||||
await browser.dragAndDrop(
|
||||
{
|
||||
location: el,
|
||||
offset: { x: resultX1, y: resultY1 },
|
||||
},
|
||||
{
|
||||
location: el,
|
||||
offset: { x: resultX2, y: resultY2 },
|
||||
}
|
||||
);
|
||||
|
||||
await renderTracker.verify();
|
||||
},
|
||||
|
||||
async assertActivePage(testSubj: string, expectedPage: number) {
|
||||
const pagination = await testSubjects.find(`${testSubj} > mlSwimLanePagination`);
|
||||
const activePage = await pagination.findByCssSelector(
|
||||
'.euiPaginationButton-isActive .euiButtonEmpty__text'
|
||||
);
|
||||
const text = await activePage.getVisibleText();
|
||||
expect(text).to.eql(expectedPage);
|
||||
},
|
||||
|
||||
async assertPageSize(testSubj: string, expectedPageSize: number) {
|
||||
const actualPageSize = await testSubjects.find(
|
||||
`${testSubj} > ${expectedPageSize.toString()}`
|
||||
);
|
||||
expect(await actualPageSize.isDisplayed()).to.be(true);
|
||||
},
|
||||
|
||||
async selectPage(testSubj: string, page: number) {
|
||||
await testSubjects.click(`${testSubj} > pagination-button-${page - 1}`);
|
||||
await this.assertActivePage(testSubj, page);
|
||||
},
|
||||
|
||||
async setPageSize(testSubj: string, rowsCount: 5 | 10 | 20 | 50 | 100) {
|
||||
await testSubjects.click(`${testSubj} > mlSwimLanePageSizeControl`);
|
||||
await testSubjects.existOrFail('mlSwimLanePageSizePanel');
|
||||
await testSubjects.click(`mlSwimLanePageSizePanel > ${rowsCount} rows`);
|
||||
await this.assertPageSize(testSubj, rowsCount);
|
||||
},
|
||||
};
|
||||
}
|
Loading…
Reference in a new issue