diff --git a/x-pack/plugins/ml/public/application/_hacks.scss b/x-pack/plugins/ml/public/application/_hacks.scss index 63fec4e74b796..13fabcb8045aa 100644 --- a/x-pack/plugins/ml/public/application/_hacks.scss +++ b/x-pack/plugins/ml/public/application/_hacks.scss @@ -1,6 +1,6 @@ .tab-datavisualizer_index_select, .tab-timeseriesexplorer, -.tab-explorer, { +.tab-explorer { // Make all page background white until More of the pages use EuiPage to wrap in panel-like components background-color: $euiColorEmptyShade; } @@ -22,3 +22,12 @@ .clear, .clearfix { clear: both; } + +// Helper class for functional tests to disable anti-aliasing for canvas elements +.mlDisableAntiAliasing { + -webkit-font-smoothing : none; + + * canvas { + image-rendering: pixelated; + } +} diff --git a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx index 344464bfe9590..4e9fd3baebe7b 100644 --- a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx +++ b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx @@ -267,7 +267,7 @@ export const ScatterplotMatrix: FC = ({ {splom === undefined || vegaSpec === undefined ? ( ) : ( -
+
= ({ language: SEARCH_QUERY_LANGUAGE.KUERY, }); - const scatterplotFieldOptions = useMemo( - () => - includesTableItems - .filter((d) => d.feature_type === 'numerical' && d.is_included) - .map((d) => d.name), - [includesTableItems] - ); - const toastNotifications = getToastNotifications(); const setJobConfigQuery: ExplorationQueryBarProps['setSearchQuery'] = (update) => { @@ -341,16 +333,37 @@ export const ConfigurationStepForm: FC = ({ [currentIndexPattern.fields] ); + const scatterplotMatrixProps = useMemo( + () => ({ + color: isJobTypeWithDepVar ? dependentVariable : undefined, + fields: includesTableItems + .filter((d) => d.feature_type === 'numerical' && d.is_included) + .map((d) => d.name), + index: currentIndexPattern.title, + legendType: getScatterplotMatrixLegendType(jobType), + searchQuery: jobConfigQuery, + }), + [ + currentIndexPattern.title, + dependentVariable, + includesTableItems, + isJobTypeWithDepVar, + jobConfigQuery, + jobType, + ] + ); + // Show the Scatterplot Matrix only if // - There's more than one suitable field available // - The job type is outlier detection, or // - The job type is regression or classification and the dependent variable has been set - const showScatterplotMatrix = - (jobType === ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION || - ((jobType === ANALYSIS_CONFIG_TYPE.REGRESSION || - jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION) && - !dependentVariableEmpty)) && - scatterplotFieldOptions.length > 1; + const showScatterplotMatrix = useMemo( + () => + (jobType === ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION || + (isJobTypeWithDepVar && !dependentVariableEmpty)) && + scatterplotMatrixProps.fields.length > 1, + [dependentVariableEmpty, jobType, scatterplotMatrixProps.fields.length] + ); // Don't render until `savedSearchQuery` has been initialized. // `undefined` means uninitialized, `null` means initialized but not used. @@ -550,18 +563,7 @@ export const ConfigurationStepForm: FC = ({ paddingSize="m" data-test-subj="mlAnalyticsCreateJobWizardScatterplotMatrixPanel" > - + diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/get_roc_curve_chart_vega_lite_spec.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/get_roc_curve_chart_vega_lite_spec.tsx index b9e9c5720e5aa..7e279308c6879 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/get_roc_curve_chart_vega_lite_spec.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/get_roc_curve_chart_vega_lite_spec.tsx @@ -53,6 +53,7 @@ export const getRocCurveChartVegaLiteSpec = ( return { $schema: 'https://vega.github.io/schema/vega-lite/v4.8.1.json', + background: 'transparent', // Left padding of 45px to align the left axis of the chart with the confusion matrix above. padding: { left: 45, top: 0, right: 0, bottom: 0 }, config: { diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts index 50aa3d1498997..90705a73e3839 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/classification_creation.ts @@ -12,8 +12,7 @@ export default function ({ getService }: FtrProviderContext) { const ml = getService('ml'); const editedDescription = 'Edited description'; - // Failing: See https://github.com/elastic/kibana/issues/91450 - describe.skip('classification creation', function () { + describe('classification creation', function () { before(async () => { await esArchiver.loadIfNeeded('ml/bm_classification'); await ml.testResources.createIndexPatternIfNeeded('ft_bank_marketing', '@timestamp'); @@ -43,24 +42,21 @@ export default function ({ getService }: FtrProviderContext) { createIndexPattern: true, expected: { rocCurveColorState: [ - // background - { key: '#FFFFFF', value: 93 }, // tick/grid/axis - { key: '#98A2B3', value: 1 }, - { key: '#DDDDDD', value: 3 }, - // line - { key: '#6092C0', value: 1 }, + { key: '#DDDDDD', value: 50 }, + // lines + { key: '#98A2B3', value: 30 }, + { key: '#6092C0', value: 10 }, + { key: '#5F92C0', value: 6 }, ], scatterplotMatrixColorStats: [ - // background - { key: '#000000', value: 94 }, + // marker colors + { key: '#7FC6B3', value: 1 }, + { key: '#88ADD0', value: 0.03 }, // tick/grid/axis - { key: '#DDDDDD', value: 1 }, - { key: '#D3DAE6', value: 1 }, - { key: '#F5F7FA', value: 1 }, - // scatterplot circles - { key: '#6A717D', value: 1 }, - { key: '#54B39A', value: 1 }, + { key: '#DDDDDD', value: 8 }, + { key: '#D3DAE6', value: 8 }, + { key: '#F5F7FA', value: 20 }, ], row: { type: 'classification', @@ -112,8 +108,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.dataFrameAnalyticsCreation.assertIncludeFieldsSelectionExists(); await ml.testExecution.logTestStep('displays the scatterplot matrix'); - await ml.dataFrameAnalyticsCanvasElement.assertCanvasElement( - 'mlAnalyticsCreateJobWizardScatterplotMatrixFormRow', + await ml.dataFrameAnalyticsCreation.assertScatterplotMatrix( testData.expected.scatterplotMatrixColorStats ); @@ -157,7 +152,12 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('checks validation callouts exist'); await ml.dataFrameAnalyticsCreation.assertValidationCalloutsExists(); - await ml.dataFrameAnalyticsCreation.assertAllValidationCalloutsPresent(3); + // Expect the follow callouts: + // - ✓ Dependent variable + // - ✓ Training percent + // - ✓ Top classes + // - ⚠ Analysis fields + await ml.dataFrameAnalyticsCreation.assertAllValidationCalloutsPresent(4); await ml.testExecution.logTestStep('continues to the create step'); await ml.dataFrameAnalyticsCreation.continueToCreateStep(); @@ -238,16 +238,21 @@ export default function ({ getService }: FtrProviderContext) { await ml.testExecution.logTestStep('displays the results view for created job'); await ml.dataFrameAnalyticsTable.openResultsView(testData.jobId); await ml.dataFrameAnalyticsResults.assertClassificationEvaluatePanelElementsExists(); - await ml.dataFrameAnalyticsCanvasElement.assertCanvasElement( + await ml.commonUI.assertColorsInCanvasElement( 'mlDFAnalyticsClassificationExplorationRocCurveChart', - testData.expected.rocCurveColorState + testData.expected.rocCurveColorState, + ['#000000'], + undefined, + undefined, + // increased tolerance for ROC curve chart up from 10 to 20 + // since the returned colors vary quite a bit on each run. + 20 ); await ml.dataFrameAnalyticsResults.assertClassificationTablePanelExists(); await ml.dataFrameAnalyticsResults.assertResultsTableExists(); await ml.dataFrameAnalyticsResults.assertResultsTableTrainingFiltersExist(); await ml.dataFrameAnalyticsResults.assertResultsTableNotEmpty(); - await ml.dataFrameAnalyticsCanvasElement.assertCanvasElement( - 'mlDFExpandableSection-splom', + await ml.dataFrameAnalyticsResults.assertScatterplotMatrix( testData.expected.scatterplotMatrixColorStats ); }); diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts index 524b0205914bd..d8b52130d307c 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts @@ -50,26 +50,21 @@ export default function ({ getService }: FtrProviderContext) { { chartAvailable: true, id: 'Exterior2nd', legend: '3 categories' }, { chartAvailable: true, id: 'Fireplaces', legend: '0 - 3' }, ], - scatterplotMatrixColorStatsWizard: [ - // background - { key: '#000000', value: 91 }, - // tick/grid/axis - { key: '#6A717D', value: 2 }, - { key: '#F5F7FA', value: 2 }, - { key: '#D3DAE6', value: 1 }, - // scatterplot circles - { key: '#54B399', value: 1 }, - { key: '#54B39A', value: 1 }, + scatterplotMatrixColorsWizard: [ + // markers + { key: '#52B398', value: 25 }, + // grey boilerplate + { key: '#6A717D', value: 30 }, ], scatterplotMatrixColorStatsResults: [ - // background - { key: '#000000', value: 91 }, + // red markers + { key: '#D98071', value: 1 }, // tick/grid/axis, grey markers - // the red outlier color is not above the 1% threshold. - { key: '#6A717D', value: 2 }, - { key: '#98A2B3', value: 1 }, - { key: '#F5F7FA', value: 2 }, - { key: '#D3DAE6', value: 1 }, + { key: '#6A717D', value: 30 }, + { key: '#D3DAE6', value: 8 }, + { key: '#98A1B3', value: 25 }, + // anti-aliasing + { key: '#F5F7FA', value: 27 }, ], row: { type: 'outlier_detection', @@ -93,6 +88,10 @@ export default function ({ getService }: FtrProviderContext) { await ml.navigation.navigateToDataFrameAnalytics(); await ml.testExecution.logTestStep('loads the source selection modal'); + + // Disable anti-aliasing to stabilize canvas image rendering assertions + await ml.commonUI.disableAntiAliasing(); + await ml.dataFrameAnalytics.startAnalyticsCreation(); await ml.testExecution.logTestStep( @@ -128,9 +127,8 @@ export default function ({ getService }: FtrProviderContext) { await ml.dataFrameAnalyticsCreation.assertIncludeFieldsSelectionExists(); await ml.testExecution.logTestStep('displays the scatterplot matrix'); - await ml.dataFrameAnalyticsCanvasElement.assertCanvasElement( - 'mlAnalyticsCreateJobWizardScatterplotMatrixFormRow', - testData.expected.scatterplotMatrixColorStatsWizard + await ml.dataFrameAnalyticsCreation.assertScatterplotMatrix( + testData.expected.scatterplotMatrixColorsWizard ); await ml.testExecution.logTestStep('continues to the additional options step'); @@ -257,8 +255,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.dataFrameAnalyticsResults.assertResultsTableExists(); await ml.dataFrameAnalyticsResults.assertResultsTableNotEmpty(); await ml.dataFrameAnalyticsResults.assertFeatureInfluenceCellNotEmpty(); - await ml.dataFrameAnalyticsCanvasElement.assertCanvasElement( - 'mlDFExpandableSection-splom', + await ml.dataFrameAnalyticsResults.assertScatterplotMatrix( testData.expected.scatterplotMatrixColorStatsResults ); }); diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts index c88867aa95de1..43dd5ad78a072 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts @@ -41,14 +41,13 @@ export default function ({ getService }: FtrProviderContext) { createIndexPattern: true, expected: { scatterplotMatrixColorStats: [ - // background - { key: '#000000', value: 80 }, + // some marker colors of the continuous color scale + { key: '#61AFA3', value: 2 }, + { key: '#D1E5E0', value: 2 }, // tick/grid/axis - { key: '#6A717D', value: 1 }, - { key: '#F5F7FA', value: 2 }, - { key: '#D3DAE6', value: 1 }, - // because a continuous color scale is used for the scatterplot circles, - // none of the generated colors is above the 1% threshold. + { key: '#6A717D', value: 10 }, + { key: '#F5F7FA', value: 12 }, + { key: '#D3DAE6', value: 3 }, ], row: { type: 'regression', @@ -101,8 +100,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.dataFrameAnalyticsCreation.assertIncludeFieldsSelectionExists(); await ml.testExecution.logTestStep('displays the scatterplot matrix'); - await ml.dataFrameAnalyticsCanvasElement.assertCanvasElement( - 'mlAnalyticsCreateJobWizardScatterplotMatrixFormRow', + await ml.dataFrameAnalyticsCreation.assertScatterplotMatrix( testData.expected.scatterplotMatrixColorStats ); @@ -231,8 +229,7 @@ export default function ({ getService }: FtrProviderContext) { await ml.dataFrameAnalyticsResults.assertResultsTableExists(); await ml.dataFrameAnalyticsResults.assertResultsTableTrainingFiltersExist(); await ml.dataFrameAnalyticsResults.assertResultsTableNotEmpty(); - await ml.dataFrameAnalyticsCanvasElement.assertCanvasElement( - 'mlDFExpandableSection-splom', + await ml.dataFrameAnalyticsResults.assertScatterplotMatrix( testData.expected.scatterplotMatrixColorStats ); }); diff --git a/x-pack/test/functional/services/canvas_element.ts b/x-pack/test/functional/services/canvas_element.ts index 300874d1e1a21..b6d1239b34169 100644 --- a/x-pack/test/functional/services/canvas_element.ts +++ b/x-pack/test/functional/services/canvas_element.ts @@ -15,26 +15,34 @@ interface ColorStat { type ColorStats = ColorStat[]; -/** - * Returns if a given value is within the tolerated range of an expected value - * - * @param actualValue - * @param expectedValue - * @param toleranceRange - * @returns if actualValue is within the tolerance of expectedValue - */ -function isValueWithinTolerance(actualValue: number, expectedValue: number, toleranceRange = 10) { - const lower = expectedValue - toleranceRange / 2; - const upper = expectedValue + toleranceRange / 2; - return lower <= actualValue && upper >= actualValue; -} - import { FtrProviderContext } from '../ftr_provider_context'; export async function CanvasElementProvider({ getService }: FtrProviderContext) { const { driver } = await getService('__webdriver__').init(); return new (class CanvasElementService { + // disable font anti-aliasing to be more resilient + // against OS rendering differences + public async disableAntiAliasing() { + await driver.executeScript( + ` + document.body.style["font-smooth"] = "never"; + document.body.style["-webkit-font-smoothing"] = "none"; + document.body.classList.add("mlDisableAntiAliasing"); + ` + ); + } + + public async resetAntiAliasing() { + await driver.executeScript( + ` + document.body.style["font-smooth"] = ""; + document.body.style["-webkit-font-smoothing"] = ""; + document.body.classList.remove("mlDisableAntiAliasing"); + ` + ); + } + /** * Gets the image data of a canvas element * @param selector querySelector to access the canvas element. @@ -60,36 +68,33 @@ export async function CanvasElementProvider({ getService }: FtrProviderContext) * * @param selector querySelector to access the canvas element. * @param expectedColorStats - optional stats to compare against and check if the percentage is within the tolerance. - * @param threshold - colors below this percentage threshold will be filtered from the returned list of colors + * @param percentageThreshold - colors below this percentage threshold will be filtered from the returned list of colors + * @param channelTolerance - tolerance for each RGB channel value + * @param exclude - colors to exclude, useful for e.g. known background color values * @returns an array of colors and their percentage of appearance in the given image data */ public async getColorStats( selector: string, expectedColorStats?: ColorStats, - threshold = 5 + exclude?: string[], + percentageThreshold = 5, + channelTolerance = 10, + valueTolerance = 10 ): Promise { const imageData = await this.getImageData(selector); // transform the array of RGBA numbers to an array of hex values const colors: string[] = []; for (let i = 0; i < imageData.length; i += 4) { // uses d3's `rgb` method create a color object, `toString()` returns the hex value - colors.push( - rgb(imageData[i], imageData[i + 1], imageData[i + 2]) - .toString() - .toUpperCase() - ); + const r = imageData[i]; + const g = imageData[i + 1]; + const b = imageData[i + 2]; + const color = rgb(r, g, b).toString().toUpperCase(); + if (exclude === undefined || !exclude.includes(color)) colors.push(color); } - const expectedColorStatsMap = - expectedColorStats !== undefined - ? expectedColorStats.reduce((p, c) => { - p[c.key] = c.value; - return p; - }, {} as Record) - : {}; - function getPixelPercentage(pixelsNum: number): number { - return Math.round((pixelsNum / colors.length) * 100); + return (pixelsNum / colors.length) * 100; } // - d3's nest/key/entries methods will group the array of hex values so we can count @@ -101,17 +106,111 @@ export async function CanvasElementProvider({ getService }: FtrProviderContext) return nest() .key((d) => d) .entries(colors) - .filter((s) => getPixelPercentage(s.values.length) >= threshold) - .map((s) => { + .filter((s) => getPixelPercentage(s.values.length) >= percentageThreshold) + .sort((a, b) => a.key.localeCompare(b.key)) + .map((s, i) => { const value = getPixelPercentage(s.values.length); return { key: s.key, value, ...(expectedColorStats !== undefined - ? { withinTolerance: isValueWithinTolerance(value, expectedColorStatsMap[s.key]) } + ? { + withinTolerance: + this.isValueWithinTolerance( + value, + expectedColorStats[i]?.value, + valueTolerance + ) && + this.isColorWithinTolerance( + s.key, + expectedColorStats[i]?.key, + channelTolerance + ), + } : {}), }; }); } + + /** + * Same as getColorStats() but also checks if each supplied + * expected color lies within channelTolerance. + */ + public async getColorStatsWithColorTolerance( + selector: string, + expectedColorStats: ColorStats, + exclude?: string[], + percentageThreshold = 0, + channelTolerance = 10, + valueTolerance = 10 + ) { + const actualColorStats = await this.getColorStats( + selector, + undefined, + exclude, + percentageThreshold, + channelTolerance, + valueTolerance + ); + + return expectedColorStats.map((expectedColor) => { + const colorPercentageWithinTolerance = actualColorStats + .filter((d) => this.isColorWithinTolerance(d.key, expectedColor.key, channelTolerance)) + .reduce((sum, x) => sum + x.value, 0); + + return { + key: expectedColor.key, + value: colorPercentageWithinTolerance, + withinTolerance: this.isValueWithinTolerance( + colorPercentageWithinTolerance, + expectedColor.value, + valueTolerance + ), + }; + }); + } + + /** + * Returns if a given color is within the tolerated range of an expected color + * + * @param actualColor + * @param expectedColor + * @param toleranceRange + * @returns if actualColor is within the tolerance of expectedColor + */ + public isColorWithinTolerance(actualColor: string, expectedColor: string, toleranceRange = 10) { + const actualRGB = rgb(actualColor); + const expectedRGB = rgb(expectedColor); + + const lowerR = expectedRGB.r - toleranceRange / 2; + const upperR = expectedRGB.r + toleranceRange / 2; + const lowerG = expectedRGB.g - toleranceRange / 2; + const upperG = expectedRGB.g + toleranceRange / 2; + const lowerB = expectedRGB.b - toleranceRange / 2; + const upperB = expectedRGB.b + toleranceRange / 2; + + return ( + lowerR <= actualRGB.r && + upperR >= actualRGB.r && + lowerG <= actualRGB.g && + upperG >= actualRGB.g && + lowerB <= actualRGB.b && + upperB >= actualRGB.b + ); + } + + /** + * Returns if a given value is within the tolerated range of an expected value + * + * @param actualValue + * @param expectedValue + * @param toleranceRange + * @returns if actualValue is within the tolerance of expectedValue + */ + public isValueWithinTolerance(actualValue: number, expectedValue: number, toleranceRange = 10) { + const lower = expectedValue - toleranceRange / 2; + const upper = expectedValue + toleranceRange / 2; + return actualValue > 0 && lower <= actualValue && upper >= actualValue; + } })(); } diff --git a/x-pack/test/functional/services/ml/common_ui.ts b/x-pack/test/functional/services/ml/common_ui.ts index 70e3d7c1b9b15..e7aa7ad5d3c89 100644 --- a/x-pack/test/functional/services/ml/common_ui.ts +++ b/x-pack/test/functional/services/ml/common_ui.ts @@ -15,9 +15,17 @@ interface SetValueOptions { typeCharByChar?: boolean; } +// key: color hex code, e.g. #FF3344 +// value: the expected percentage of the color to be present in the canvas element +export type CanvasElementColorStats = Array<{ + key: string; + value: number; +}>; + export type MlCommonUI = ProvidedType; export function MachineLearningCommonUIProvider({ getService }: FtrProviderContext) { + const canvasElement = getService('canvasElement'); const log = getService('log'); const retry = getService('retry'); const testSubjects = getService('testSubjects'); @@ -205,5 +213,42 @@ export function MachineLearningCommonUIProvider({ getService }: FtrProviderConte `${testDataSubj} slider value should be '${expectedValue}' (got '${actualValue}')` ); }, + + async disableAntiAliasing() { + await canvasElement.disableAntiAliasing(); + }, + + async resetAntiAliasing() { + await canvasElement.resetAntiAliasing(); + }, + + async assertColorsInCanvasElement( + dataTestSubj: string, + expectedColorStats: CanvasElementColorStats, + exclude?: string[], + percentageThreshold = 0, + channelTolerance = 10, + valueTolerance = 10 + ) { + await retry.tryForTime(30 * 1000, async () => { + await testSubjects.existOrFail(dataTestSubj); + + const actualColorStatsWithTolerance = await canvasElement.getColorStatsWithColorTolerance( + `[data-test-subj="${dataTestSubj}"] canvas`, + expectedColorStats, + exclude, + percentageThreshold, + channelTolerance, + valueTolerance + ); + + expect(actualColorStatsWithTolerance.every((d) => d.withinTolerance)).to.eql( + true, + `Color stats for '${dataTestSubj}' should be within tolerance. Expected: '${JSON.stringify( + expectedColorStats + )}' (got '${JSON.stringify(actualColorStatsWithTolerance)}')` + ); + }); + }, }; } diff --git a/x-pack/test/functional/services/ml/data_frame_analytics_canvas_element.ts b/x-pack/test/functional/services/ml/data_frame_analytics_canvas_element.ts deleted file mode 100644 index a354e0723d377..0000000000000 --- a/x-pack/test/functional/services/ml/data_frame_analytics_canvas_element.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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 { FtrProviderContext } from '../../ftr_provider_context'; - -export function MachineLearningDataFrameAnalyticsCanvasElementProvider({ - getService, -}: FtrProviderContext) { - const canvasElement = getService('canvasElement'); - const testSubjects = getService('testSubjects'); - - return new (class AnalyticsCanvasElement { - public async assertCanvasElement( - dataTestSubj: string, - expectedColorStats: Array<{ - key: string; - value: number; - }> - ) { - await testSubjects.existOrFail(dataTestSubj); - - const actualColorStats = await canvasElement.getColorStats( - `[data-test-subj="${dataTestSubj}"] canvas`, - expectedColorStats, - 1 - ); - expect(actualColorStats.every((d) => d.withinTolerance)).to.eql( - true, - `Color stats for canvas element should be within tolerance. Expected: '${JSON.stringify( - expectedColorStats - )}' (got '${JSON.stringify(actualColorStats)}')` - ); - } - })(); -} diff --git a/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts b/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts index 89a19016c9abd..d2c1291e69af8 100644 --- a/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts +++ b/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts @@ -9,7 +9,7 @@ import expect from '@kbn/expect'; import { DataFrameAnalyticsConfig } from '../../../../plugins/ml/public/application/data_frame_analytics/common'; import { FtrProviderContext } from '../../ftr_provider_context'; -import { MlCommonUI } from './common_ui'; +import type { CanvasElementColorStats, MlCommonUI } from './common_ui'; import { MlApi } from './api'; import { isRegressionAnalysis, @@ -256,6 +256,23 @@ export function MachineLearningDataFrameAnalyticsCreationProvider( await this.assertDependentVariableSelection([dependentVariable]); }, + async assertScatterplotMatrix(expectedValue: CanvasElementColorStats) { + await testSubjects.existOrFail( + 'mlAnalyticsCreateJobWizardScatterplotMatrixPanel > mlScatterplotMatrix loaded', + { + timeout: 5000, + } + ); + await testSubjects.scrollIntoView( + 'mlAnalyticsCreateJobWizardScatterplotMatrixPanel > mlScatterplotMatrix loaded' + ); + await mlCommonUI.assertColorsInCanvasElement( + 'mlAnalyticsCreateJobWizardScatterplotMatrixPanel', + expectedValue, + ['#000000'] + ); + }, + async assertTrainingPercentInputExists() { await testSubjects.existOrFail('mlAnalyticsCreateJobWizardTrainingPercentSlider'); }, diff --git a/x-pack/test/functional/services/ml/data_frame_analytics_results.ts b/x-pack/test/functional/services/ml/data_frame_analytics_results.ts index bf4e4778d1441..c3fb75799fe64 100644 --- a/x-pack/test/functional/services/ml/data_frame_analytics_results.ts +++ b/x-pack/test/functional/services/ml/data_frame_analytics_results.ts @@ -10,9 +10,12 @@ import { WebElementWrapper } from 'test/functional/services/lib/web_element_wrap import { FtrProviderContext } from '../../ftr_provider_context'; -export function MachineLearningDataFrameAnalyticsResultsProvider({ - getService, -}: FtrProviderContext) { +import type { CanvasElementColorStats, MlCommonUI } from './common_ui'; + +export function MachineLearningDataFrameAnalyticsResultsProvider( + { getService }: FtrProviderContext, + mlCommonUI: MlCommonUI +) { const retry = getService('retry'); const testSubjects = getService('testSubjects'); @@ -81,6 +84,16 @@ export function MachineLearningDataFrameAnalyticsResultsProvider({ }); }, + async assertScatterplotMatrix(expectedValue: CanvasElementColorStats) { + await testSubjects.existOrFail('mlDFExpandableSection-splom > mlScatterplotMatrix loaded', { + timeout: 5000, + }); + await testSubjects.scrollIntoView('mlDFExpandableSection-splom > mlScatterplotMatrix loaded'); + await mlCommonUI.assertColorsInCanvasElement('mlDFExpandableSection-splom', expectedValue, [ + '#000000', + ]); + }, + async assertFeatureImportanceDecisionPathChartElementsExists() { await testSubjects.existOrFail('mlDFADecisionPathChart', { timeout: 5000, diff --git a/x-pack/test/functional/services/ml/index.ts b/x-pack/test/functional/services/ml/index.ts index ceee1ba7dc1ac..894ba3d6ef07d 100644 --- a/x-pack/test/functional/services/ml/index.ts +++ b/x-pack/test/functional/services/ml/index.ts @@ -18,7 +18,6 @@ import { MachineLearningDataFrameAnalyticsProvider } from './data_frame_analytic import { MachineLearningDataFrameAnalyticsCreationProvider } from './data_frame_analytics_creation'; import { MachineLearningDataFrameAnalyticsEditProvider } from './data_frame_analytics_edit'; import { MachineLearningDataFrameAnalyticsResultsProvider } from './data_frame_analytics_results'; -import { MachineLearningDataFrameAnalyticsCanvasElementProvider } from './data_frame_analytics_canvas_element'; import { MachineLearningDataFrameAnalyticsMapProvider } from './data_frame_analytics_map'; import { MachineLearningDataFrameAnalyticsTableProvider } from './data_frame_analytics_table'; import { MachineLearningDataVisualizerProvider } from './data_visualizer'; @@ -63,12 +62,12 @@ export function MachineLearningProvider(context: FtrProviderContext) { api ); const dataFrameAnalyticsEdit = MachineLearningDataFrameAnalyticsEditProvider(context, commonUI); - const dataFrameAnalyticsResults = MachineLearningDataFrameAnalyticsResultsProvider(context); + const dataFrameAnalyticsResults = MachineLearningDataFrameAnalyticsResultsProvider( + context, + commonUI + ); const dataFrameAnalyticsMap = MachineLearningDataFrameAnalyticsMapProvider(context); const dataFrameAnalyticsTable = MachineLearningDataFrameAnalyticsTableProvider(context); - const dataFrameAnalyticsCanvasElement = MachineLearningDataFrameAnalyticsCanvasElementProvider( - context - ); const dataVisualizer = MachineLearningDataVisualizerProvider(context); const dataVisualizerTable = MachineLearningDataVisualizerTableProvider(context, commonUI); @@ -113,7 +112,6 @@ export function MachineLearningProvider(context: FtrProviderContext) { dataFrameAnalyticsResults, dataFrameAnalyticsMap, dataFrameAnalyticsTable, - dataFrameAnalyticsCanvasElement, dataVisualizer, dataVisualizerFileBased, dataVisualizerIndexBased, diff --git a/x-pack/test/functional/services/transform/wizard.ts b/x-pack/test/functional/services/transform/wizard.ts index 40cc8644625eb..d9ba127bc16ce 100644 --- a/x-pack/test/functional/services/transform/wizard.ts +++ b/x-pack/test/functional/services/transform/wizard.ts @@ -243,17 +243,25 @@ export function TransformWizardProvider({ getService, getPageObjects }: FtrProvi await testSubjects.existOrFail(`mlDataGridChart-${id}-histogram`); if (expected.colorStats !== undefined) { + const sortedExpectedColorStats = [...expected.colorStats].sort((a, b) => + a.key.localeCompare(b.key) + ); + const actualColorStats = await canvasElement.getColorStats( `[data-test-subj="mlDataGridChart-${id}-histogram"] .echCanvasRenderer`, - expected.colorStats + sortedExpectedColorStats ); + expect(actualColorStats.length).to.eql( + sortedExpectedColorStats.length, + `Expected and actual color stats for column '${expected.id}' should have the same amount of elements. Expected: ${sortedExpectedColorStats.length} (got ${actualColorStats.length})` + ); expect(actualColorStats.every((d) => d.withinTolerance)).to.eql( true, `Color stats for column '${ expected.id }' should be within tolerance. Expected: '${JSON.stringify( - expected.colorStats + sortedExpectedColorStats )}' (got '${JSON.stringify(actualColorStats)}')` ); }