[7.12] [ML] Data Frame Analytics: Stabilize canvas element functional tests. (#96311)

- Fix color assertion with risk of percentage being rounded to 0.
- Better naming of attributes of expected values.
- Adds assertions to use the sample size dropdown and randomize query switch.
This commit is contained in:
Walter Rafelsberger 2021-04-07 17:33:40 +02:00 committed by GitHub
parent f9feddb721
commit bf055e5256
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 532 additions and 198 deletions

View file

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

View file

@ -267,7 +267,7 @@ export const ScatterplotMatrix: FC<ScatterplotMatrixProps> = ({
{splom === undefined || vegaSpec === undefined ? (
<VegaChartLoading />
) : (
<div data-test-subj="mlScatterplotMatrix">
<div data-test-subj={`mlScatterplotMatrix ${isLoading ? 'loading' : 'loaded'}`}>
<EuiFlexGroup>
<EuiFlexItem>
<EuiFormRow
@ -316,6 +316,7 @@ export const ScatterplotMatrix: FC<ScatterplotMatrixProps> = ({
fullWidth
>
<EuiSelect
data-test-subj="mlScatterplotMatrixSampleSizeSelect"
compressed
options={sampleSizeOptions}
value={fetchSize}
@ -340,6 +341,7 @@ export const ScatterplotMatrix: FC<ScatterplotMatrixProps> = ({
fullWidth
>
<EuiSwitch
data-test-subj="mlScatterplotMatrixRandomizeQuerySwitch"
name="mlScatterplotMatrixRandomizeQuery"
label={randomizeQuery ? TOGGLE_ON : TOGGLE_OFF}
checked={randomizeQuery}

View file

@ -120,14 +120,6 @@ export const ConfigurationStepForm: FC<CreateAnalyticsStepProps> = ({
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<CreateAnalyticsStepProps> = ({
[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<CreateAnalyticsStepProps> = ({
paddingSize="m"
data-test-subj="mlAnalyticsCreateJobWizardScatterplotMatrixPanel"
>
<ScatterplotMatrix
fields={scatterplotFieldOptions}
index={currentIndexPattern.title}
color={
jobType === ANALYSIS_CONFIG_TYPE.REGRESSION ||
jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION
? dependentVariable
: undefined
}
legendType={getScatterplotMatrixLegendType(jobType)}
searchQuery={jobConfigQuery}
/>
<ScatterplotMatrix {...scatterplotMatrixProps} />
</EuiPanel>
<EuiSpacer />
</>

View file

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

View file

@ -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,19 @@ 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 },
{ color: '#DDDDDD', percentage: 50 },
// line
{ key: '#6092C0', value: 1 },
{ color: '#98A2B3', percentage: 30 },
],
scatterplotMatrixColorStats: [
// background
{ key: '#000000', value: 94 },
// marker colors
{ color: '#7FC6B3', percentage: 1 },
{ color: '#88ADD0', percentage: 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 },
{ color: '#DDDDDD', percentage: 8 },
{ color: '#D3DAE6', percentage: 8 },
{ color: '#F5F7FA', percentage: 15 },
],
row: {
type: 'classification',
@ -83,6 +77,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(
@ -111,9 +109,18 @@ export default function ({ getService }: FtrProviderContext) {
await ml.testExecution.logTestStep('displays the include fields selection');
await ml.dataFrameAnalyticsCreation.assertIncludeFieldsSelectionExists();
await ml.testExecution.logTestStep(
'sets the sample size to 10000 for the scatterplot matrix'
);
await ml.dataFrameAnalyticsCreation.setScatterplotMatrixSampleSizeSelectValue('10000');
await ml.testExecution.logTestStep(
'sets the randomize query switch to true for the scatterplot matrix'
);
await ml.dataFrameAnalyticsCreation.setScatterplotMatrixRandomizeQueryCheckState(true);
await ml.testExecution.logTestStep('displays the scatterplot matrix');
await ml.dataFrameAnalyticsCanvasElement.assertCanvasElement(
'mlAnalyticsCreateJobWizardScatterplotMatrixFormRow',
await ml.dataFrameAnalyticsCreation.assertScatterplotMatrix(
testData.expected.scatterplotMatrixColorStats
);
@ -231,18 +238,39 @@ 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(
'mlDFAnalyticsClassificationExplorationRocCurveChart',
testData.expected.rocCurveColorState
);
await ml.dataFrameAnalyticsResults.assertClassificationTablePanelExists();
await ml.dataFrameAnalyticsResults.assertResultsTableExists();
await ml.dataFrameAnalyticsResults.assertResultsTableTrainingFiltersExist();
await ml.dataFrameAnalyticsResults.assertResultsTableNotEmpty();
await ml.dataFrameAnalyticsCanvasElement.assertCanvasElement(
'mlDFExpandableSection-splom',
await ml.testExecution.logTestStep('displays the ROC curve chart');
await ml.commonUI.assertColorsInCanvasElement(
'mlDFAnalyticsClassificationExplorationRocCurveChart',
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.testExecution.logTestStep(
'sets the sample size to 10000 for the scatterplot matrix'
);
await ml.dataFrameAnalyticsResults.setScatterplotMatrixSampleSizeSelectValue('10000');
await ml.testExecution.logTestStep(
'sets the randomize query switch to true for the scatterplot matrix'
);
await ml.dataFrameAnalyticsResults.setScatterplotMatrixRandomizeQueryCheckState(true);
await ml.testExecution.logTestStep('displays the scatterplot matrix');
await ml.dataFrameAnalyticsResults.assertScatterplotMatrix(
testData.expected.scatterplotMatrixColorStats
);
await ml.commonUI.resetAntiAliasing();
});
it('displays the analytics job in the map view', async () => {

View file

@ -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
{ color: '#52B398', percentage: 15 },
// grey boilerplate
{ color: '#6A717D', percentage: 33 },
],
scatterplotMatrixColorStatsResults: [
// background
{ key: '#000000', value: 91 },
// red markers
{ color: '#D98071', percentage: 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 },
{ color: '#6A717D', percentage: 33 },
{ color: '#D3DAE6', percentage: 8 },
{ color: '#98A1B3', percentage: 12 },
// anti-aliasing
{ color: '#F5F7FA', percentage: 30 },
],
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(
@ -127,10 +126,19 @@ export default function ({ getService }: FtrProviderContext) {
await ml.testExecution.logTestStep('displays the include fields selection');
await ml.dataFrameAnalyticsCreation.assertIncludeFieldsSelectionExists();
await ml.testExecution.logTestStep(
'sets the sample size to 10000 for the scatterplot matrix'
);
await ml.dataFrameAnalyticsCreation.setScatterplotMatrixSampleSizeSelectValue('10000');
await ml.testExecution.logTestStep(
'sets the randomize query switch to true for the scatterplot matrix'
);
await ml.dataFrameAnalyticsCreation.setScatterplotMatrixRandomizeQueryCheckState(true);
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');
@ -250,10 +258,23 @@ 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.testExecution.logTestStep(
'sets the sample size to 10000 for the scatterplot matrix'
);
await ml.dataFrameAnalyticsResults.setScatterplotMatrixSampleSizeSelectValue('10000');
await ml.testExecution.logTestStep(
'sets the randomize query switch to true for the scatterplot matrix'
);
await ml.dataFrameAnalyticsResults.setScatterplotMatrixRandomizeQueryCheckState(true);
await ml.testExecution.logTestStep('displays the scatterplot matrix');
await ml.dataFrameAnalyticsResults.assertScatterplotMatrix(
testData.expected.scatterplotMatrixColorStatsResults
);
await ml.commonUI.resetAntiAliasing();
});
it('displays the analytics job in the map view', async () => {

View file

@ -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
{ color: '#61AFA3', percentage: 2 },
{ color: '#D1E5E0', percentage: 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.
{ color: '#6A717D', percentage: 10 },
{ color: '#F5F7FA', percentage: 10 },
{ color: '#D3DAE6', percentage: 3 },
],
row: {
type: 'regression',
@ -72,6 +71,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(
@ -100,9 +103,18 @@ export default function ({ getService }: FtrProviderContext) {
await ml.testExecution.logTestStep('displays the include fields selection');
await ml.dataFrameAnalyticsCreation.assertIncludeFieldsSelectionExists();
await ml.testExecution.logTestStep(
'sets the sample size to 10000 for the scatterplot matrix'
);
await ml.dataFrameAnalyticsCreation.setScatterplotMatrixSampleSizeSelectValue('10000');
await ml.testExecution.logTestStep(
'sets the randomize query switch to true for the scatterplot matrix'
);
await ml.dataFrameAnalyticsCreation.setScatterplotMatrixRandomizeQueryCheckState(true);
await ml.testExecution.logTestStep('displays the scatterplot matrix');
await ml.dataFrameAnalyticsCanvasElement.assertCanvasElement(
'mlAnalyticsCreateJobWizardScatterplotMatrixFormRow',
await ml.dataFrameAnalyticsCreation.assertScatterplotMatrix(
testData.expected.scatterplotMatrixColorStats
);
@ -224,10 +236,23 @@ 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.testExecution.logTestStep(
'sets the sample size to 10000 for the scatterplot matrix'
);
await ml.dataFrameAnalyticsResults.setScatterplotMatrixSampleSizeSelectValue('10000');
await ml.testExecution.logTestStep(
'sets the randomize query switch to true for the scatterplot matrix'
);
await ml.dataFrameAnalyticsResults.setScatterplotMatrixRandomizeQueryCheckState(true);
await ml.testExecution.logTestStep('displays the scatterplot matrix');
await ml.dataFrameAnalyticsResults.assertScatterplotMatrix(
testData.expected.scatterplotMatrixColorStats
);
await ml.commonUI.resetAntiAliasing();
});
it('displays the analytics job in the map view', async () => {

View file

@ -161,8 +161,8 @@ export default function ({ getService }: FtrProviderContext) {
id: 'currency',
legend: '1 category',
colorStats: [
{ key: '#000000', value: 10 },
{ key: '#54B399', value: 90 },
{ color: '#000000', percentage: 10 },
{ color: '#54B399', percentage: 90 },
],
},
{
@ -177,8 +177,8 @@ export default function ({ getService }: FtrProviderContext) {
id: 'customer_gender',
legend: '2 categories',
colorStats: [
{ key: '#000000', value: 15 },
{ key: '#54B399', value: 85 },
{ color: '#000000', percentage: 15 },
{ color: '#54B399', percentage: 85 },
],
},
{
@ -186,8 +186,8 @@ export default function ({ getService }: FtrProviderContext) {
id: 'customer_id',
legend: 'top 20 of 46 categories',
colorStats: [
{ key: '#54B399', value: 35 },
{ key: '#000000', value: 60 },
{ color: '#54B399', percentage: 35 },
{ color: '#000000', percentage: 60 },
],
},
{ chartAvailable: false, id: 'customer_last_name', legend: 'Chart not supported.' },
@ -196,8 +196,8 @@ export default function ({ getService }: FtrProviderContext) {
id: 'customer_phone',
legend: '1 category',
colorStats: [
{ key: '#000000', value: 10 },
{ key: '#54B399', value: 90 },
{ color: '#000000', percentage: 10 },
{ color: '#54B399', percentage: 90 },
],
},
{
@ -205,8 +205,8 @@ export default function ({ getService }: FtrProviderContext) {
id: 'day_of_week',
legend: '7 categories',
colorStats: [
{ key: '#000000', value: 20 },
{ key: '#54B399', value: 75 },
{ color: '#000000', percentage: 20 },
{ color: '#54B399', percentage: 75 },
],
},
],

View file

@ -8,26 +8,13 @@
import { rgb, nest } from 'd3';
interface ColorStat {
key: string;
value: number;
color: string;
percentage: number;
pixels?: number;
withinTolerance?: boolean;
}
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;
}
export type CanvasElementColorStats = ColorStat[];
import { FtrProviderContext } from '../ftr_provider_context';
@ -35,6 +22,28 @@ 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 +69,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
): Promise<ColorStats> {
expectedColorStats?: CanvasElementColorStats,
exclude?: string[],
percentageThreshold = 5,
channelTolerance = 10,
valueTolerance = 10
): Promise<CanvasElementColorStats> {
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<string, number>)
: {};
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 +107,133 @@ export async function CanvasElementProvider({ getService }: FtrProviderContext)
return nest<string>()
.key((d) => d)
.entries(colors)
.filter((s) => getPixelPercentage(s.values.length) >= threshold)
.map((s) => {
const value = getPixelPercentage(s.values.length);
.filter((s) => getPixelPercentage(s.values.length) >= percentageThreshold)
.sort((a, b) => a.key.localeCompare(b.key))
.map((s, i) => {
const percentage = getPixelPercentage(s.values.length);
const pixels = s.values.length;
return {
key: s.key,
value,
color: s.key,
percentage,
pixels,
...(expectedColorStats !== undefined
? { withinTolerance: isValueWithinTolerance(value, expectedColorStatsMap[s.key]) }
? {
withinTolerance:
this.isValueWithinTolerance(
percentage,
pixels,
expectedColorStats[i]?.percentage,
valueTolerance
) &&
this.isColorWithinTolerance(
s.key,
expectedColorStats[i]?.color,
channelTolerance
),
}
: {}),
};
});
}
/**
* Same as getColorStats() but also checks if each supplied
* expected color lies within channelTolerance.
*/
public async getColorStatsWithColorTolerance(
selector: string,
expectedColorStats: CanvasElementColorStats,
exclude?: string[],
percentageThreshold = 0,
channelTolerance = 10,
valueTolerance = 10
): Promise<CanvasElementColorStats> {
const actualColorStats = await this.getColorStats(
selector,
undefined,
exclude,
percentageThreshold,
channelTolerance,
valueTolerance
);
return expectedColorStats.map((expectedColor) => {
const colorsWithinTolerance = actualColorStats.filter((d) =>
this.isColorWithinTolerance(d.color, expectedColor.color, channelTolerance)
);
const colorPercentageWithinTolerance = colorsWithinTolerance.reduce(
(sum, x) => sum + x.percentage,
0
);
const pixelsWithinTolerance = colorsWithinTolerance.reduce(
(sum, x) => sum + (x.pixels || 0),
0
);
return {
color: expectedColor.color,
percentage: colorPercentageWithinTolerance,
pixels: pixelsWithinTolerance,
withinTolerance: this.isValueWithinTolerance(
colorPercentageWithinTolerance,
pixelsWithinTolerance,
expectedColor.percentage,
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 actualPercentage
* @param actualPixels
* @param expectedPercentage
* @param toleranceRange
* @returns if actualValue is within the tolerance of expectedValue
*/
public isValueWithinTolerance(
actualPercentage: number,
actualPixels: number,
expectedPercentage: number,
toleranceRange = 10
) {
const lower = expectedPercentage - toleranceRange / 2;
const upper = expectedPercentage + toleranceRange / 2;
return (
// actualPercentage could be rounded to 0 so we check against actualPixels if they are above 0.
actualPixels > 0 && lower <= actualPercentage && upper >= actualPercentage
);
}
})();
}

View file

@ -10,6 +10,8 @@ import { ProvidedType } from '@kbn/test/types/ftr';
import { FtrProviderContext } from '../../ftr_provider_context';
import type { CanvasElementColorStats } from '../canvas_element';
interface SetValueOptions {
clearWithKeyboard?: boolean;
typeCharByChar?: boolean;
@ -18,6 +20,7 @@ interface SetValueOptions {
export type MlCommonUI = ProvidedType<typeof MachineLearningCommonUIProvider>;
export function MachineLearningCommonUIProvider({ getService }: FtrProviderContext) {
const canvasElement = getService('canvasElement');
const log = getService('log');
const retry = getService('retry');
const testSubjects = getService('testSubjects');
@ -212,5 +215,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)}')`
);
});
},
};
}

View file

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

View file

@ -9,7 +9,8 @@ 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 } from '../canvas_element';
import type { MlCommonUI } from './common_ui';
import { MlApi } from './api';
import {
isRegressionAnalysis,
@ -255,6 +256,62 @@ 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 setScatterplotMatrixSampleSizeSelectValue(selectValue: string) {
await testSubjects.selectValue('mlScatterplotMatrixSampleSizeSelect', selectValue);
const actualSelectState = await testSubjects.getAttribute(
'mlScatterplotMatrixSampleSizeSelect',
'value'
);
expect(actualSelectState).to.eql(
selectValue,
`Sample size should be '${selectValue}' (got '${actualSelectState}')`
);
},
async getScatterplotMatrixRandomizeQuerySwitchCheckState(): Promise<boolean> {
const state = await testSubjects.getAttribute(
'mlScatterplotMatrixRandomizeQuerySwitch',
'aria-checked'
);
return state === 'true';
},
async assertScatterplotMatrixRandomizeQueryCheckState(expectedCheckState: boolean) {
const actualCheckState = await this.getScatterplotMatrixRandomizeQuerySwitchCheckState();
expect(actualCheckState).to.eql(
expectedCheckState,
`Randomize query check state should be '${expectedCheckState}' (got '${actualCheckState}')`
);
},
async setScatterplotMatrixRandomizeQueryCheckState(checkState: boolean) {
await retry.tryForTime(30000, async () => {
if ((await this.getScatterplotMatrixRandomizeQuerySwitchCheckState()) !== checkState) {
await testSubjects.click('mlScatterplotMatrixRandomizeQuerySwitch');
}
await this.assertScatterplotMatrixRandomizeQueryCheckState(checkState);
});
},
async assertTrainingPercentInputExists() {
await testSubjects.existOrFail('mlAnalyticsCreateJobWizardTrainingPercentSlider');
},

View file

@ -10,9 +10,13 @@ import { WebElementWrapper } from 'test/functional/services/lib/web_element_wrap
import { FtrProviderContext } from '../../ftr_provider_context';
export function MachineLearningDataFrameAnalyticsResultsProvider({
getService,
}: FtrProviderContext) {
import type { CanvasElementColorStats } from '../canvas_element';
import type { MlCommonUI } from './common_ui';
export function MachineLearningDataFrameAnalyticsResultsProvider(
{ getService }: FtrProviderContext,
mlCommonUI: MlCommonUI
) {
const retry = getService('retry');
const testSubjects = getService('testSubjects');
@ -81,6 +85,62 @@ export function MachineLearningDataFrameAnalyticsResultsProvider({
});
},
async setScatterplotMatrixSampleSizeSelectValue(selectValue: string) {
await testSubjects.selectValue('mlScatterplotMatrixSampleSizeSelect', selectValue);
const actualSelectState = await testSubjects.getAttribute(
'mlScatterplotMatrixSampleSizeSelect',
'value'
);
expect(actualSelectState).to.eql(
selectValue,
`Sample size should be '${selectValue}' (got '${actualSelectState}')`
);
},
async getScatterplotMatrixRandomizeQuerySwitchCheckState(): Promise<boolean> {
const state = await testSubjects.getAttribute(
'mlScatterplotMatrixRandomizeQuerySwitch',
'aria-checked'
);
return state === 'true';
},
async assertScatterplotMatrixRandomizeQueryCheckState(expectedCheckState: boolean) {
const actualCheckState = await this.getScatterplotMatrixRandomizeQuerySwitchCheckState();
expect(actualCheckState).to.eql(
expectedCheckState,
`Randomize query check state should be '${expectedCheckState}' (got '${actualCheckState}')`
);
},
async setScatterplotMatrixRandomizeQueryCheckState(checkState: boolean) {
await retry.tryForTime(30000, async () => {
if ((await this.getScatterplotMatrixRandomizeQuerySwitchCheckState()) !== checkState) {
await testSubjects.click('mlScatterplotMatrixRandomizeQuerySwitch');
}
await this.assertScatterplotMatrixRandomizeQueryCheckState(checkState);
});
},
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'],
undefined,
undefined,
// increased tolerance up from 10 to 20
// since the returned randomized colors vary quite a bit on each run.
20
);
},
async assertFeatureImportanceDecisionPathChartElementsExists() {
await testSubjects.existOrFail('mlDFADecisionPathChart', {
timeout: 5000,

View file

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

View file

@ -10,6 +10,15 @@ import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
import type { CanvasElementColorStats } from '../canvas_element';
export type HistogramCharts = Array<{
chartAvailable: boolean;
id: string;
legend?: string;
colorStats?: CanvasElementColorStats;
}>;
export function TransformWizardProvider({ getService }: FtrProviderContext) {
const aceEditor = getService('aceEditor');
const canvasElement = getService('canvasElement');
@ -197,14 +206,7 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) {
);
},
async assertIndexPreviewHistogramCharts(
expectedHistogramCharts: Array<{
chartAvailable: boolean;
id: string;
legend: string;
colorStats?: any[];
}>
) {
async assertIndexPreviewHistogramCharts(expectedHistogramCharts: HistogramCharts) {
// For each chart, get the content of each header cell and assert
// the legend text and column id and if the chart should be present or not.
await retry.tryForTime(5000, async () => {
@ -215,17 +217,25 @@ export function TransformWizardProvider({ getService }: FtrProviderContext) {
await testSubjects.existOrFail(`mlDataGridChart-${index}-histogram`);
if (expected.colorStats !== undefined) {
const actualColorStats = await canvasElement.getColorStats(
`[data-test-subj="mlDataGridChart-${index}-histogram"] .echCanvasRenderer`,
expected.colorStats
const sortedExpectedColorStats = [...expected.colorStats].sort((a, b) =>
a.color.localeCompare(b.color)
);
const actualColorStats = await canvasElement.getColorStats(
`[data-test-subj="mlDataGridChart-${index}-histogram"] .echCanvasRenderer`,
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)}')`
);
}