From 8a805bf06a6a73fb1d4023dd6ce47e9e95853f5b Mon Sep 17 00:00:00 2001 From: Diana Derevyankina <54894989+DziyanaDzeraviankina@users.noreply.github.com> Date: Wed, 4 Aug 2021 21:56:46 +0300 Subject: [PATCH] [Timelion Viz] Add functional tests (#107287) * First draft migrate timelion to elastic-charts * Some refactoring. Added brush event. * Added title. Some refactoring * Fixed some type problems. Added logic for yaxes function * Fixed some types, added missing functionality for yaxes * Fixed some types, added missing functionality for stack property * Fixed unit test * Removed unneeded code * Some refactoring * Some refactoring * Fixed some remarks. * Fixed some styles * Added themes. Removed unneeded styles in BarSeries * removed unneeded code. * Fixed some comments * Fixed vertical cursor across Timelion visualizations of a dashboad * Fix some problems with styles * Use RxJS instead of jQuery * Remove unneeded code * Fixed some problems * Fixed unit test * Fix CI * Fix eslint * Fix some gaps * Fix legend columns * Some fixes * add 2 versions of Timeline app * fix CI * cleanup code * fix CI * fix legend position * fix some cases * fix some cases * remove extra casting * cleanup code * fix issue with static * fix header formatter * fix points * fix ts error * Fix yaxis behavior * Fix some case with yaxis * Add deprecation message and update asciidoc * Fix title * some text improvements * [Timelion Viz] Add functional tests * Add more complex cases for _timelion * Update test expected data Co-authored-by: Uladzislau Lasitsa Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Alexey Antonov --- .../public/components/timelion_interval.tsx | 1 + .../components/timelion_vis_component.tsx | 12 +- .../apps/visualize/_point_series_options.ts | 2 +- test/functional/apps/visualize/_timelion.ts | 206 ++++++++++++++++++ test/functional/apps/visualize/index.ts | 1 + .../page_objects/visualize_chart_page.ts | 31 ++- .../page_objects/visualize_editor_page.ts | 6 + .../functional/page_objects/visualize_page.ts | 4 + test/functional/services/monaco_editor.ts | 4 +- .../functional/page_objects/security_page.ts | 5 +- 10 files changed, 256 insertions(+), 16 deletions(-) create mode 100644 test/functional/apps/visualize/_timelion.ts diff --git a/src/plugins/vis_type_timelion/public/components/timelion_interval.tsx b/src/plugins/vis_type_timelion/public/components/timelion_interval.tsx index 40702c39deaf4..047de1bdb0708 100644 --- a/src/plugins/vis_type_timelion/public/components/timelion_interval.tsx +++ b/src/plugins/vis_type_timelion/public/components/timelion_interval.tsx @@ -126,6 +126,7 @@ function TimelionInterval({ value, setValue, setValidity }: TimelionIntervalProp placeholder={i18n.translate('timelion.vis.selectIntervalPlaceholder', { defaultMessage: 'Select an interval', })} + data-test-subj="timelionIntervalComboBox" /> ); diff --git a/src/plugins/vis_type_timelion/public/components/timelion_vis_component.tsx b/src/plugins/vis_type_timelion/public/components/timelion_vis_component.tsx index 4690f4fe11e45..7f422b2a00710 100644 --- a/src/plugins/vis_type_timelion/public/components/timelion_vis_component.tsx +++ b/src/plugins/vis_type_timelion/public/components/timelion_vis_component.tsx @@ -43,6 +43,15 @@ import type { Series } from '../helpers/timelion_request_handler'; import './timelion_vis.scss'; +declare global { + interface Window { + /** + * Flag used to enable debugState on elastic charts + */ + _echDebugStateFlag?: boolean; + } +} + interface TimelionVisComponentProps { interval: string; seriesList: Sheet; @@ -174,7 +183,7 @@ const TimelionVisComponent = ({ }, [chart]); return ( -
+
{title && (

{title}

@@ -182,6 +191,7 @@ const TimelionVisComponent = ({ )} { + before(async () => { + await kibanaServer.uiSettings.update({ + 'timelion:legacyChartsLibrary': false, + }); + await visualize.initTests(true); + await visualize.navigateToNewAggBasedVisualization(); + await visualize.clickTimelion(); + await timePicker.setDefaultAbsoluteRange(); + }); + + const initVisualization = async (expression: string, interval: string = '12h') => { + await visEditor.setTimelionInterval(interval); + await monacoEditor.setCodeEditorValue(expression); + await visEditor.clickGo(); + }; + + it('should display correct data for specified index pattern and timefield', async () => { + await initVisualization('.es(index=long-window-logstash-*,timefield=@timestamp)'); + + const chartData = await visChart.getAreaChartData('q:* > count'); + expect(chartData).to.eql([3, 5, 2, 6, 1, 6, 1, 7, 0, 0]); + }); + + it('should display correct chart colors for multiple expressions', async () => { + const expectedColors = ['#01A4A4', '#FFCFDF', '#BFA7DA', '#AD7DE6']; + await initVisualization( + '.es(*), .es(*).color("#FFCFDF"), .es(*).color("#BFA7DA"), .es(*).color("#AD7DE6")' + ); + + const areas = (await elasticChart.getChartDebugData())?.areas; + expect(areas?.map(({ color }) => color)).to.eql(expectedColors); + }); + + it('should display correct chart data for average, min, max and cardinality aggregations', async () => { + await initVisualization( + '.es(index=logstash-*,metric=avg:bytes), .es(index=logstash-*,metric=min:bytes),' + + '.es(index=logstash-*,metric=max:bytes,), .es(index=logstash-*,metric=cardinality:bytes)', + '36h' + ); + + const firstAreaChartData = await visChart.getAreaChartData('q:* > avg(bytes)'); + const secondAreaChartData = await visChart.getAreaChartData('q:* > min(bytes)'); + const thirdAreaChartData = await visChart.getAreaChartData('q:* > max(bytes)'); + const forthAreaChartData = await visChart.getAreaChartData('q:* > cardinality(bytes)'); + + expect(firstAreaChartData).to.eql([5732.783676366217, 5721.775973559419]); + expect(secondAreaChartData).to.eql([0, 0]); + expect(thirdAreaChartData).to.eql([19985, 19986]); + expect(forthAreaChartData).to.eql([5019, 4958, 0, 0]); + }); + + it('should display correct chart data for expressions using functions', async () => { + const firstAreaExpectedChartData = [3, 2421, 2343, 2294, 2327, 2328, 2312, 7, 0, 0]; + const thirdAreaExpectedChartData = [200, 167, 199, 200, 200, 198, 108, 200, 200]; + const forthAreaExpectedChartData = [150, 50, 50, 50, 50, 50, 50, 150, 150, 150]; + await initVisualization( + '.es(*).label("initial"),' + + '.es(*).add(term=.es(*).multiply(-1).abs()).divide(2).label("add multiply abs divide"),' + + '.es(q="bytes<100").derivative().sum(200).min(200).label("query derivative min sum"),' + + '.es(*).if(operator=gt,if=200,then=50,else=150).label("condition")' + ); + + const firstAreaChartData = await visChart.getAreaChartData('initial'); + const secondAreaChartData = await visChart.getAreaChartData('add multiply abs divide'); + const thirdAreaChartData = await visChart.getAreaChartData('query derivative min sum'); + const forthAreaChartData = await visChart.getAreaChartData('condition'); + + expect(firstAreaChartData).to.eql(firstAreaExpectedChartData); + expect(secondAreaChartData).to.eql(firstAreaExpectedChartData); + expect(thirdAreaChartData).to.eql(thirdAreaExpectedChartData); + expect(forthAreaChartData).to.eql(forthAreaExpectedChartData); + }); + + it('should display correct chart title, data and labels for expressions with custom labels, yaxis and offset', async () => { + const firstAreaExpectedChartData = [13112352443.375292, 13095637741.055172]; + const secondAreaExpectedChartData = [ + [1442642400000, 5732.783676366217], + [1442772000000, 5721.775973559419], + ]; + const thirdAreaExpectedChartData = [ + [1442772000000, 5732.783676366217], + [1442901600000, 5721.775973559419], + ]; + await initVisualization( + '.es(index=logstash*,timefield="@timestamp",metric=avg:machine.ram).label("Average Machine RAM amount").yaxis(2,units=bytes,position=right),' + + '.es(index=logstash*,timefield="@timestamp",metric=avg:bytes).label("Average Bytes for request").yaxis(1,units=bytes,position=left),' + + '.es(index=logstash*,timefield="@timestamp",metric=avg:bytes, offset=-12h).label("Average Bytes for request with offset").yaxis(3,units=custom:BYTES_,position=right)', + '36h' + ); + + const leftAxesCount = await visChart.getAxesCountByPosition('left'); + const rightAxesCount = await visChart.getAxesCountByPosition('right'); + const firstAxesLabels = await visChart.getYAxisLabels(); + const secondAxesLabels = await visChart.getYAxisLabels(1); + const thirdAxesLabels = await visChart.getYAxisLabels(2); + const firstAreaChartData = await visChart.getAreaChartData('Average Machine RAM amount'); + const secondAreaChartData = await visChart.getAreaChartData( + 'Average Bytes for request', + undefined, + true + ); + const thirdAreaChartData = await visChart.getAreaChartData( + 'Average Bytes for request with offset', + undefined, + true + ); + + expect(leftAxesCount).to.be(1); + expect(rightAxesCount).to.be(2); + expect(firstAreaChartData).to.eql(firstAreaExpectedChartData); + expect(secondAreaChartData).to.eql(secondAreaExpectedChartData); + expect(thirdAreaChartData).to.eql(thirdAreaExpectedChartData); + expect(firstAxesLabels).to.eql(['12.19GB', '12.2GB', '12.21GB']); + expect(secondAxesLabels).to.eql(['5.59KB', '5.6KB']); + expect(thirdAxesLabels.toString()).to.be( + 'BYTES_5721,BYTES_5722,BYTES_5723,BYTES_5724,BYTES_5725,BYTES_5726,BYTES_5727,BYTES_5728,BYTES_5729,BYTES_5730,BYTES_5731,BYTES_5732,BYTES_5733' + ); + }); + + it('should display correct chart data for split expression', async () => { + await initVisualization('.es(index=logstash-*, split=geo.dest:3)', '1 day'); + + const firstAreaChartData = await visChart.getAreaChartData('q:* > geo.dest:CN > count'); + const secondAreaChartData = await visChart.getAreaChartData('q:* > geo.dest:IN > count'); + const thirdAreaChartData = await visChart.getAreaChartData('q:* > geo.dest:US > count'); + + expect(firstAreaChartData).to.eql([0, 905, 910, 850, 0]); + expect(secondAreaChartData).to.eql([0, 763, 699, 825, 0]); + expect(thirdAreaChartData).to.eql([0, 423, 386, 389, 0]); + }); + + it('should display two areas and one bar chart items', async () => { + await initVisualization('.es(*), .es(*), .es(*).bars(stack=true)'); + + const areasChartsCount = await visChart.getAreaSeriesCount(); + const barsChartsCount = await visChart.getHistogramSeriesCount(); + + expect(areasChartsCount).to.be(2); + expect(barsChartsCount).to.be(1); + }); + + describe('Legend', () => { + it('should correctly display the legend items names and position', async () => { + await initVisualization('.es(*).label("first series"), .es(*).label("second series")'); + + const legendNames = await visChart.getLegendEntries(); + const legendElement = await find.byClassName('echLegend'); + const isLegendTopPositioned = await legendElement.elementHasClass('echLegend--top'); + const isLegendLeftPositioned = await legendElement.elementHasClass('echLegend--left'); + + expect(legendNames).to.eql(['first series', 'second series']); + expect(isLegendTopPositioned).to.be(true); + expect(isLegendLeftPositioned).to.be(true); + }); + + it('should correctly display the legend position', async () => { + await initVisualization('.es(*).legend(position=se)'); + + const legendElement = await find.byClassName('echLegend'); + const isLegendBottomPositioned = await legendElement.elementHasClass('echLegend--bottom'); + const isLegendRightPositioned = await legendElement.elementHasClass('echLegend--right'); + + expect(isLegendBottomPositioned).to.be(true); + expect(isLegendRightPositioned).to.be(true); + }); + + it('should not display the legend', async () => { + await initVisualization('.es(*), .es(*).label("second series").legend(position=false)'); + + const isLegendElementExists = await find.existsByCssSelector('.echLegend'); + expect(isLegendElementExists).to.be(false); + }); + }); + + after( + async () => + await kibanaServer.uiSettings.update({ + 'timelion:legacyChartsLibrary': true, + }) + ); + }); +} diff --git a/test/functional/apps/visualize/index.ts b/test/functional/apps/visualize/index.ts index bc6160eba3846..4af871bd9347d 100644 --- a/test/functional/apps/visualize/index.ts +++ b/test/functional/apps/visualize/index.ts @@ -52,6 +52,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./_vertical_bar_chart')); loadTestFile(require.resolve('./_vertical_bar_chart_nontimeindex')); loadTestFile(require.resolve('./_pie_chart')); + loadTestFile(require.resolve('./_timelion')); }); describe('visualize ciGroup9', function () { diff --git a/test/functional/page_objects/visualize_chart_page.ts b/test/functional/page_objects/visualize_chart_page.ts index 64b8c363fa6c2..bcee77a21c0b0 100644 --- a/test/functional/page_objects/visualize_chart_page.ts +++ b/test/functional/page_objects/visualize_chart_page.ts @@ -115,10 +115,10 @@ export class VisualizeChartPageObject extends FtrService { .map((tick) => $(tick).text().trim()); } - public async getYAxisLabels() { + public async getYAxisLabels(nth = 0) { if (await this.isNewLibraryChart(xyChartSelector)) { - const [yAxis] = (await this.getEsChartDebugState(xyChartSelector))?.axes?.y ?? []; - return yAxis?.labels; + const yAxis = (await this.getEsChartDebugState(xyChartSelector))?.axes?.y ?? []; + return yAxis[nth]?.labels; } const yAxis = await this.find.byCssSelector('.visAxis__column--y.visAxis__column--left'); @@ -141,14 +141,19 @@ export class VisualizeChartPageObject extends FtrService { * Gets the chart data and scales it based on chart height and label. * @param dataLabel data-label value * @param axis axis value, 'ValueAxis-1' by default + * @param shouldContainXAxisData boolean value for mapping points, false by default * * Returns an array of height values */ - public async getAreaChartData(dataLabel: string, axis = 'ValueAxis-1') { + public async getAreaChartData( + dataLabel: string, + axis = 'ValueAxis-1', + shouldContainXAxisData = false + ) { if (await this.isNewLibraryChart(xyChartSelector)) { const areas = (await this.getEsChartDebugState(xyChartSelector))?.areas ?? []; const points = areas.find(({ name }) => name === dataLabel)?.lines.y1.points ?? []; - return points.map(({ y }) => y); + return shouldContainXAxisData ? points.map(({ x, y }) => [x, y]) : points.map(({ y }) => y); } const yAxisRatio = await this.getChartYAxisRatio(axis); @@ -556,12 +561,12 @@ export class VisualizeChartPageObject extends FtrService { return values.filter((item) => item.length > 0); } - public async getRightValueAxesCount() { + public async getAxesCountByPosition(axesPosition: typeof Position[keyof typeof Position]) { if (await this.isNewLibraryChart(xyChartSelector)) { const yAxes = (await this.getEsChartDebugState(xyChartSelector))?.axes?.y ?? []; - return yAxes.filter(({ position }) => position === Position.Right).length; + return yAxes.filter(({ position }) => position === axesPosition).length; } - const axes = await this.find.allByCssSelector('.visAxis__column--right g.axis'); + const axes = await this.find.allByCssSelector(`.visAxis__column--${axesPosition} g.axis`); return axes.length; } @@ -576,6 +581,16 @@ export class VisualizeChartPageObject extends FtrService { await gauge.clickMouseButton({ xOffset: 0, yOffset }); } + public async getAreaSeriesCount() { + if (await this.isNewLibraryChart(xyChartSelector)) { + const areas = (await this.getEsChartDebugState(xyChartSelector))?.areas ?? []; + return areas.filter((area) => area.lines.y1.visible).length; + } + + const series = await this.find.allByCssSelector('.points.area'); + return series.length; + } + public async getHistogramSeriesCount() { if (await this.isNewLibraryChart(xyChartSelector)) { const bars = (await this.getEsChartDebugState(xyChartSelector))?.bars ?? []; diff --git a/test/functional/page_objects/visualize_editor_page.ts b/test/functional/page_objects/visualize_editor_page.ts index ab458c2c0fdc1..90fc320da3cda 100644 --- a/test/functional/page_objects/visualize_editor_page.ts +++ b/test/functional/page_objects/visualize_editor_page.ts @@ -532,4 +532,10 @@ export class VisualizeEditorPageObject extends FtrService { public async setSeriesType(seriesNth: number, type: string) { await this.find.selectValue(`select#seriesType${seriesNth}`, type); } + + public async setTimelionInterval(interval: string) { + const timelionIntervalComboBoxSelector = 'timelionIntervalComboBox'; + await this.comboBox.clearInputField(timelionIntervalComboBoxSelector); + await this.comboBox.setCustom(timelionIntervalComboBoxSelector, interval); + } } diff --git a/test/functional/page_objects/visualize_page.ts b/test/functional/page_objects/visualize_page.ts index 7e87312a70910..617cd42c06546 100644 --- a/test/functional/page_objects/visualize_page.ts +++ b/test/functional/page_objects/visualize_page.ts @@ -197,6 +197,10 @@ export class VisualizePageObject extends FtrService { return await this.hasVisType('tile_map'); } + public async clickTimelion() { + await this.clickVisType('timelion'); + } + public async clickTagCloud() { await this.clickVisType('tagcloud'); } diff --git a/test/functional/services/monaco_editor.ts b/test/functional/services/monaco_editor.ts index d2afa1e3b6e92..90674e101fc4e 100644 --- a/test/functional/services/monaco_editor.ts +++ b/test/functional/services/monaco_editor.ts @@ -27,13 +27,13 @@ export class MonacoEditorService extends FtrService { return values[nthIndex] as string; } - public async setCodeEditorValue(nthIndex: number, value: string) { + public async setCodeEditorValue(value: string, nthIndex = 0) { await this.retry.try(async () => { await this.browser.execute( (editorIndex, codeEditorValue) => { const editor = (window as any).MonacoEnvironment.monaco.editor; const instance = editor.getModels()[editorIndex]; - instance.setValue(JSON.parse(codeEditorValue)); + instance.setValue(codeEditorValue); }, nthIndex, value diff --git a/x-pack/test/functional/page_objects/security_page.ts b/x-pack/test/functional/page_objects/security_page.ts index 074ecafddf381..37fe0eccea31a 100644 --- a/x-pack/test/functional/page_objects/security_page.ts +++ b/x-pack/test/functional/page_objects/security_page.ts @@ -485,10 +485,7 @@ export class SecurityPageObject extends FtrService { if (roleObj.elasticsearch.indices[0].query) { await this.testSubjects.click('restrictDocumentsQuery0'); - await this.monacoEditor.setCodeEditorValue( - 0, - JSON.stringify(roleObj.elasticsearch.indices[0].query) - ); + await this.monacoEditor.setCodeEditorValue(roleObj.elasticsearch.indices[0].query); } const globalPrivileges = (roleObj.kibana as any).global;