[ML] Functional tests for Anomaly swim lane (#94723) (#95313)

* [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:
Kibana Machine 2021-03-24 12:53:14 -04:00 committed by GitHub
parent 72ef242a38
commit 95e07fe5c7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 504 additions and 22 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -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}
/>

View file

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

View file

@ -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"
]
}

View file

@ -7,7 +7,7 @@
import { Block } from './types';
const API_VERSION = '7.8.0';
const API_VERSION = '7.13.0';
/**
* Post Filter parsed results.

View file

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

View file

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

View file

@ -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}`
);
},
};
}

View file

@ -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,
};

View file

@ -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}`
);
},
};
}

View 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);
},
};
}