Adds screenshot tests for the region maps. This also closes https://github.com/elastic/kibana/pull/15483, https://github.com/elastic/kibana/issues/13293
After Width: | Height: | Size: 122 KiB |
BIN
src/core_plugins/region_map/public/__tests__/afterdatachange.png
Normal file
After Width: | Height: | Size: 122 KiB |
After Width: | Height: | Size: 25 KiB |
BIN
src/core_plugins/region_map/public/__tests__/afterresize.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
src/core_plugins/region_map/public/__tests__/changestartup.png
Normal file
After Width: | Height: | Size: 55 KiB |
BIN
src/core_plugins/region_map/public/__tests__/initial.png
Normal file
After Width: | Height: | Size: 123 KiB |
|
@ -0,0 +1,387 @@
|
|||
import expect from 'expect.js';
|
||||
import ngMock from 'ng_mock';
|
||||
import _ from 'lodash';
|
||||
import { RegionMapsVisualizationProvider } from '../region_map_visualization';
|
||||
import ChoroplethLayer from '../choropleth_layer';
|
||||
import LogstashIndexPatternStubProvider from 'fixtures/stubbed_logstash_index_pattern';
|
||||
import * as visModule from 'ui/vis';
|
||||
import { ImageComparator } from 'test_utils/image_comparator';
|
||||
import sinon from 'sinon';
|
||||
import worldJson from './world.json';
|
||||
|
||||
import initialPng from './initial.png';
|
||||
import toiso3Png from './toiso3.png';
|
||||
import afterresizePng from './afterresize.png';
|
||||
import afterdatachangePng from './afterdatachange.png';
|
||||
import afterdatachangeandresizePng from './afterdatachangeandresize.png';
|
||||
import aftercolorchangePng from './aftercolorchange.png';
|
||||
import changestartupPng from './changestartup.png';
|
||||
|
||||
const manifestUrl = 'https://staging-dot-catalogue-dot-elastic-layer.appspot.com/v1/manifest';
|
||||
const tmsManifestUrl = `"https://tiles-maps-stage.elastic.co/v2/manifest`;
|
||||
const vectorManifestUrl = `"https://staging-dot-elastic-layer.appspot.com/v1/manifest`;
|
||||
const manifest = {
|
||||
'services': [{
|
||||
'id': 'tiles_v2',
|
||||
'name': 'Elastic Tile Service',
|
||||
'manifest': tmsManifestUrl,
|
||||
'type': 'tms'
|
||||
},
|
||||
{
|
||||
'id': 'geo_layers',
|
||||
'name': 'Elastic Layer Service',
|
||||
'manifest': vectorManifestUrl,
|
||||
'type': 'file'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const tmsManifest = {
|
||||
'services': [{
|
||||
'id': 'road_map',
|
||||
'url': 'https://tiles.elastic.co/v2/default/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana',
|
||||
'minZoom': 0,
|
||||
'maxZoom': 10,
|
||||
'attribution': '© [OpenStreetMap](http://www.openstreetmap.org/copyright) © [Elastic Maps Service](https://www.elastic.co/elastic-maps-service)'
|
||||
}]
|
||||
};
|
||||
|
||||
const vectorManifest = {
|
||||
'layers': [{
|
||||
'attribution': '',
|
||||
'name': 'US States',
|
||||
'format': 'geojson',
|
||||
'url': 'https://storage.googleapis.com/elastic-layer.appspot.com/L2FwcGhvc3RpbmdfcHJvZC9ibG9icy9BRW5CMlVvNGJ0aVNidFNJR2dEQl9rbTBjeXhKMU01WjRBeW1kN3JMXzM2Ry1qc3F6QjF4WE5XdHY2ODlnQkRpZFdCY2g1T2dqUGRHSFhSRTU3amlxTVFwZjNBSFhycEFwV2lYR29vTENjZjh1QTZaZnRpaHBzby5VXzZoNk1paGJYSkNPalpI?elastic_tile_service_tos=agree',
|
||||
'fields': [{ 'name': 'postal', 'description': 'Two letter abbreviation' }, {
|
||||
'name': 'name',
|
||||
'description': 'State name'
|
||||
}],
|
||||
'created_at': '2017-04-26T19:45:22.377820',
|
||||
'id': 5086441721823232
|
||||
}, {
|
||||
'attribution': '© [Elastic Tile Service](https://www.elastic.co/elastic-maps-service)',
|
||||
'name': 'World Countries',
|
||||
'format': 'geojson',
|
||||
'url': 'https://storage.googleapis.com/elastic-layer.appspot.com/L2FwcGhvc3RpbmdfcHJvZC9ibG9icy9BRW5CMlVwWTZTWnhRRzNmUk9HUE93TENjLXNVd2IwdVNpc09SRXRyRzBVWWdqOU5qY2hldGJLOFNZSFpUMmZmZWdNZGx0NWprT1R1ZkZ0U1JEdFBtRnkwUWo0S0JuLTVYY1I5RFdSMVZ5alBIZkZuME1qVS04TS5oQTRNTl9yRUJCWk9tMk03?elastic_tile_service_tos=agree',
|
||||
'fields': [{ 'name': 'iso2', 'description': 'Two letter abbreviation' }, {
|
||||
'name': 'name',
|
||||
'description': 'Country name'
|
||||
}, { 'name': 'iso3', 'description': 'Three letter abbreviation' }],
|
||||
'created_at': '2017-04-26T17:12:15.978370',
|
||||
'id': 5659313586569216
|
||||
}]
|
||||
};
|
||||
|
||||
|
||||
const THRESHOLD = 0.25;
|
||||
const PIXEL_DIFF = 64;
|
||||
|
||||
describe('RegionMapsVisualizationTests', function () {
|
||||
|
||||
let domNode;
|
||||
let RegionMapsVisualization;
|
||||
let Vis;
|
||||
let indexPattern;
|
||||
let vis;
|
||||
|
||||
let imageComparator;
|
||||
|
||||
const _makeJsonAjaxCallOld = ChoroplethLayer.prototype._makeJsonAjaxCall;
|
||||
|
||||
const dummyTableGroup = {
|
||||
tables: [
|
||||
{
|
||||
columns: [{
|
||||
'aggConfig': {
|
||||
'id': '2',
|
||||
'enabled': true,
|
||||
'type': 'terms',
|
||||
'schema': 'segment',
|
||||
'params': { 'field': 'geo.dest', 'size': 5, 'order': 'desc', 'orderBy': '1' }
|
||||
}, 'title': 'geo.dest: Descending'
|
||||
}, {
|
||||
'aggConfig': { 'id': '1', 'enabled': true, 'type': 'count', 'schema': 'metric', 'params': {} },
|
||||
'title': 'Count'
|
||||
}],
|
||||
rows: [['CN', 26], ['IN', 17], ['US', 6], ['DE', 4], ['BR', 3]]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
beforeEach(ngMock.module('kibana'));
|
||||
beforeEach(ngMock.inject((Private, $injector) => {
|
||||
|
||||
Vis = Private(visModule.VisProvider);
|
||||
RegionMapsVisualization = Private(RegionMapsVisualizationProvider);
|
||||
indexPattern = Private(LogstashIndexPatternStubProvider);
|
||||
|
||||
ChoroplethLayer.prototype._makeJsonAjaxCall = async function () {
|
||||
//simulate network call
|
||||
return new Promise((resolve)=> {
|
||||
setTimeout(() => {
|
||||
resolve(worldJson);
|
||||
}, 10);
|
||||
});
|
||||
};
|
||||
|
||||
const serviceSettings = $injector.get('serviceSettings');
|
||||
sinon.stub(serviceSettings, '_getManifest', function (url) {
|
||||
let contents = null;
|
||||
if (url.startsWith(tmsManifestUrl)) {
|
||||
contents = tmsManifest;
|
||||
} else if (url.startsWith(vectorManifestUrl)) {
|
||||
contents = vectorManifest;
|
||||
} else if (url.startsWith(manifestUrl)) {
|
||||
contents = manifest;
|
||||
}
|
||||
return {
|
||||
data: contents
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
|
||||
}));
|
||||
|
||||
|
||||
afterEach(function () {
|
||||
ChoroplethLayer.prototype._makeJsonAjaxCall = _makeJsonAjaxCallOld;
|
||||
});
|
||||
|
||||
|
||||
describe('RegionMapVisualization - basics', function () {
|
||||
|
||||
beforeEach(async function () {
|
||||
setupDOM('512px', '512px');
|
||||
|
||||
imageComparator = new ImageComparator();
|
||||
|
||||
|
||||
vis = new Vis(indexPattern, {
|
||||
type: 'region_map'
|
||||
});
|
||||
|
||||
vis.params.selectedJoinField = { 'name': 'iso2', 'description': 'Two letter abbreviation' };
|
||||
vis.params.selectedLayer = {
|
||||
'attribution': '<p><a href="http://www.naturalearthdata.com/about/terms-of-use">Made with NaturalEarth</a> | <a href="https://www.elastic.co/elastic-maps-service">Elastic Maps Service</a></p> ',
|
||||
'name': 'World Countries',
|
||||
'format': 'geojson',
|
||||
'url': 'https://staging-dot-elastic-layer.appspot.com/blob/5715999101812736?elastic_tile_service_tos=agree&my_app_version=7.0.0-alpha1',
|
||||
'fields': [{ 'name': 'iso2', 'description': 'Two letter abbreviation' }, {
|
||||
'name': 'iso3',
|
||||
'description': 'Three letter abbreviation'
|
||||
}, { 'name': 'name', 'description': 'Country name' }],
|
||||
'created_at': '2017-07-31T16:00:19.996450',
|
||||
'id': 5715999101812736,
|
||||
'layerId': 'elastic_maps_service.World Countries'
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
teardownDOM();
|
||||
imageComparator.destroy();
|
||||
});
|
||||
|
||||
|
||||
it('should instantiate at zoom level 2', async function () {
|
||||
const regionMapsVisualization = new RegionMapsVisualization(domNode, vis);
|
||||
await regionMapsVisualization.render(dummyTableGroup, {
|
||||
resize: false,
|
||||
params: true,
|
||||
aggs: true,
|
||||
data: true,
|
||||
uiState: false
|
||||
});
|
||||
const mismatchedPixels = await compareImage(initialPng);
|
||||
regionMapsVisualization.destroy();
|
||||
expect(mismatchedPixels).to.be.lessThan(PIXEL_DIFF);
|
||||
});
|
||||
|
||||
it('should update after resetting join field', async function () {
|
||||
const regionMapsVisualization = new RegionMapsVisualization(domNode, vis);
|
||||
await regionMapsVisualization.render(dummyTableGroup, {
|
||||
resize: false,
|
||||
params: true,
|
||||
aggs: true,
|
||||
data: true,
|
||||
uiState: false
|
||||
});
|
||||
|
||||
//this will actually create an empty image
|
||||
vis.params.selectedJoinField = { 'name': 'iso3', 'description': 'Three letter abbreviation' };
|
||||
vis.params.isDisplayWarning = false;//so we don't get notifications
|
||||
await regionMapsVisualization.render(dummyTableGroup, {
|
||||
resize: false,
|
||||
params: true,
|
||||
aggs: false,
|
||||
data: false,
|
||||
uiState: false
|
||||
});
|
||||
|
||||
const mismatchedPixels = await compareImage(toiso3Png);
|
||||
regionMapsVisualization.destroy();
|
||||
expect(mismatchedPixels).to.be.lessThan(PIXEL_DIFF);
|
||||
|
||||
});
|
||||
|
||||
it('should resize', async function () {
|
||||
|
||||
const regionMapsVisualization = new RegionMapsVisualization(domNode, vis);
|
||||
await regionMapsVisualization.render(dummyTableGroup, {
|
||||
resize: false,
|
||||
params: true,
|
||||
aggs: true,
|
||||
data: true,
|
||||
uiState: false
|
||||
});
|
||||
|
||||
domNode.style.width = '256px';
|
||||
domNode.style.height = '128px';
|
||||
await regionMapsVisualization.render(dummyTableGroup, {
|
||||
resize: true,
|
||||
params: false,
|
||||
aggs: false,
|
||||
data: false,
|
||||
uiState: false
|
||||
});
|
||||
const mismatchedPixelsAfterFirstResize = await compareImage(afterresizePng);
|
||||
|
||||
domNode.style.width = '512px';
|
||||
domNode.style.height = '512px';
|
||||
await regionMapsVisualization.render(dummyTableGroup, {
|
||||
resize: true,
|
||||
params: false,
|
||||
aggs: false,
|
||||
data: false,
|
||||
uiState: false
|
||||
});
|
||||
const mismatchedPixelsAfterSecondResize = await compareImage(initialPng);
|
||||
|
||||
regionMapsVisualization.destroy();
|
||||
expect(mismatchedPixelsAfterFirstResize).to.be.lessThan(PIXEL_DIFF);
|
||||
expect(mismatchedPixelsAfterSecondResize).to.be.lessThan(PIXEL_DIFF);
|
||||
});
|
||||
|
||||
it('should redo data', async function () {
|
||||
|
||||
const regionMapsVisualization = new RegionMapsVisualization(domNode, vis);
|
||||
await regionMapsVisualization.render(dummyTableGroup, {
|
||||
resize: false,
|
||||
params: true,
|
||||
aggs: true,
|
||||
data: true,
|
||||
uiState: false
|
||||
});
|
||||
|
||||
const newTableGroup = _.cloneDeep(dummyTableGroup);
|
||||
newTableGroup.tables[0].rows.pop();//remove one shape
|
||||
|
||||
await regionMapsVisualization.render(newTableGroup, {
|
||||
resize: false,
|
||||
params: false,
|
||||
aggs: false,
|
||||
data: true,
|
||||
uiState: false
|
||||
});
|
||||
const mismatchedPixelsAfterDataChange = await compareImage(afterdatachangePng);
|
||||
|
||||
|
||||
const anoterTableGroup = _.cloneDeep(newTableGroup);
|
||||
anoterTableGroup.tables[0].rows.pop();//remove one shape
|
||||
domNode.style.width = '412px';
|
||||
domNode.style.height = '112px';
|
||||
await regionMapsVisualization.render(anoterTableGroup, {
|
||||
resize: true,
|
||||
params: false,
|
||||
aggs: false,
|
||||
data: true,
|
||||
uiState: false
|
||||
});
|
||||
const mismatchedPixelsAfterDataChangeAndResize = await compareImage(afterdatachangeandresizePng);
|
||||
|
||||
regionMapsVisualization.destroy();
|
||||
expect(mismatchedPixelsAfterDataChange).to.be.lessThan(PIXEL_DIFF);
|
||||
expect(mismatchedPixelsAfterDataChangeAndResize).to.be.lessThan(PIXEL_DIFF);
|
||||
|
||||
});
|
||||
|
||||
it('should redo data and color ramp', async function () {
|
||||
|
||||
const regionMapsVisualization = new RegionMapsVisualization(domNode, vis);
|
||||
await regionMapsVisualization.render(dummyTableGroup, {
|
||||
resize: false,
|
||||
params: true,
|
||||
aggs: true,
|
||||
data: true,
|
||||
uiState: false
|
||||
});
|
||||
|
||||
const newTableGroup = _.cloneDeep(dummyTableGroup);
|
||||
newTableGroup.tables[0].rows.pop();//remove one shape
|
||||
vis.params.colorSchema = 'Blues';
|
||||
await regionMapsVisualization.render(newTableGroup, {
|
||||
resize: false,
|
||||
params: true,
|
||||
aggs: false,
|
||||
data: true,
|
||||
uiState: false
|
||||
});
|
||||
const mismatchedPixelsAfterDataAndColorChange = await compareImage(aftercolorchangePng);
|
||||
|
||||
regionMapsVisualization.destroy();
|
||||
expect(mismatchedPixelsAfterDataAndColorChange).to.be.lessThan(PIXEL_DIFF);
|
||||
|
||||
});
|
||||
|
||||
|
||||
it('should zoom and center elsewhere', async function () {
|
||||
|
||||
vis.params.mapZoom = 4;
|
||||
vis.params.mapCenter = [36, -85];
|
||||
const regionMapsVisualization = new RegionMapsVisualization(domNode, vis);
|
||||
await regionMapsVisualization.render(dummyTableGroup, {
|
||||
resize: false,
|
||||
params: true,
|
||||
aggs: true,
|
||||
data: true,
|
||||
uiState: false
|
||||
});
|
||||
|
||||
const mismatchedPixels = await compareImage(changestartupPng);
|
||||
regionMapsVisualization.destroy();
|
||||
|
||||
expect(mismatchedPixels).to.be.lessThan(PIXEL_DIFF);
|
||||
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
async function compareImage(expectedImageSource) {
|
||||
const elementList = domNode.querySelectorAll('canvas');
|
||||
expect(elementList.length).to.equal(1);
|
||||
const firstCanvasOnMap = elementList[0];
|
||||
return imageComparator.compareImage(firstCanvasOnMap, expectedImageSource, THRESHOLD);
|
||||
}
|
||||
|
||||
|
||||
function setupDOM(width, height) {
|
||||
domNode = document.createElement('div');
|
||||
domNode.style.top = '0';
|
||||
domNode.style.left = '0';
|
||||
domNode.style.width = width;
|
||||
domNode.style.height = height;
|
||||
domNode.style.position = 'fixed';
|
||||
domNode.style.border = '1px solid blue';
|
||||
domNode.style['pointer-events'] = 'none';
|
||||
document.body.appendChild(domNode);
|
||||
}
|
||||
|
||||
function teardownDOM() {
|
||||
domNode.innerHTML = '';
|
||||
document.body.removeChild(domNode);
|
||||
}
|
||||
|
||||
});
|
||||
|
BIN
src/core_plugins/region_map/public/__tests__/toiso3.png
Normal file
After Width: | Height: | Size: 119 KiB |
1
src/core_plugins/region_map/public/__tests__/world.json
Normal file
|
@ -47,25 +47,28 @@ export default class ChoroplethLayer extends KibanaMapLayer {
|
|||
|
||||
this._loaded = false;
|
||||
this._error = false;
|
||||
this._whenDataLoaded = new Promise((resolve) => {
|
||||
$.ajax({
|
||||
dataType: 'json',
|
||||
url: geojsonUrl,
|
||||
success: (data) => {
|
||||
this._leafletLayer.addData(data);
|
||||
this._loaded = true;
|
||||
this._setStyle();
|
||||
resolve();
|
||||
},
|
||||
error: () => {
|
||||
this._loaded = true;
|
||||
this._error = true;
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
this._whenDataLoaded = new Promise(async (resolve) => {
|
||||
try {
|
||||
const data = await this._makeJsonAjaxCall(geojsonUrl);
|
||||
this._leafletLayer.addData(data);
|
||||
this._loaded = true;
|
||||
this._setStyle();
|
||||
resolve();
|
||||
} catch (e) {
|
||||
this._loaded = true;
|
||||
this._error = true;
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//This method is stubbed in the tests to avoid network request during unit tests.
|
||||
async _makeJsonAjaxCall(url) {
|
||||
return await $.ajax({
|
||||
dataType: 'json',
|
||||
url: url
|
||||
});
|
||||
}
|
||||
|
||||
_setStyle() {
|
||||
if (this._error || (!this._loaded || !this._metrics || !this._joinField)) {
|
||||
|
|
|
@ -24,13 +24,13 @@ export function RegionMapsVisualizationProvider(Private, Notifier, config) {
|
|||
|
||||
async render(esReponse, status) {
|
||||
await super.render(esReponse, status);
|
||||
|
||||
await this._choroplethLayer.whenDataLoaded();
|
||||
if (this._choroplethLayer) {
|
||||
await this._choroplethLayer.whenDataLoaded();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async _updateData(tableGroup) {
|
||||
|
||||
let results;
|
||||
if (!tableGroup || !tableGroup.tables || !tableGroup.tables.length || tableGroup.tables[0].columns.length !== 2) {
|
||||
results = [];
|
||||
|
|
|
@ -29,7 +29,7 @@ window.__KBN__ = {
|
|||
layers: []
|
||||
},
|
||||
mapConfig: {
|
||||
manifestServiceUrl: 'https://geo.elastic.co/v1/manifest'
|
||||
manifestServiceUrl: 'https://staging-dot-catalogue-dot-elastic-layer.appspot.com/v1/manifest'
|
||||
}
|
||||
},
|
||||
uiSettings: {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import expect from 'expect.js';
|
||||
import pixelmatch from 'pixelmatch';
|
||||
import { KibanaMap } from '../kibana_map';
|
||||
import { GeohashLayer } from '../geohash_layer';
|
||||
import { GeoHashSampleData } from './geohash_sample_data';
|
||||
|
@ -7,12 +6,14 @@ import heatmapPng from './heatmap.png';
|
|||
import scaledCircleMarkersPng from './scaledCircleMarkers.png';
|
||||
import shadedCircleMarkersPng from './shadedCircleMarkers.png';
|
||||
import shadedGeohashGridPng from './shadedGeohashGrid.png';
|
||||
import { ImageComparator } from 'test_utils/image_comparator';
|
||||
|
||||
describe('kibana_map tests', function () {
|
||||
|
||||
let domNode;
|
||||
let expectCanvas;
|
||||
let kibanaMap;
|
||||
let imageComparator;
|
||||
|
||||
function setupDOM() {
|
||||
domNode = document.createElement('div');
|
||||
|
@ -38,6 +39,7 @@ describe('kibana_map tests', function () {
|
|||
|
||||
beforeEach(async function () {
|
||||
setupDOM();
|
||||
imageComparator = new ImageComparator();
|
||||
kibanaMap = new KibanaMap(domNode, {
|
||||
minZoom: 1,
|
||||
maxZoom: 10
|
||||
|
@ -52,6 +54,7 @@ describe('kibana_map tests', function () {
|
|||
afterEach(function () {
|
||||
kibanaMap.destroy();
|
||||
teardownDOM();
|
||||
imageComparator.destroy();
|
||||
});
|
||||
|
||||
[
|
||||
|
@ -78,57 +81,18 @@ describe('kibana_map tests', function () {
|
|||
}
|
||||
].forEach(function (test) {
|
||||
|
||||
it(test.options.mapType, function (done) {
|
||||
it(test.options.mapType, async function () {
|
||||
|
||||
const geohashGridOptions = test.options;
|
||||
const geohashLayer = new GeohashLayer(GeoHashSampleData, geohashGridOptions, kibanaMap.getZoomLevel(), kibanaMap);
|
||||
kibanaMap.addLayer(geohashLayer);
|
||||
|
||||
// Give time for canvas to render before checking output
|
||||
window.setTimeout(() => {
|
||||
// Extract image data from live map
|
||||
const elementList = domNode.querySelectorAll('canvas');
|
||||
expect(elementList.length).to.equal(1);
|
||||
const canvas = elementList[0];
|
||||
const ctx = canvas.getContext('2d');
|
||||
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
const elementList = domNode.querySelectorAll('canvas');
|
||||
expect(elementList.length).to.equal(1);
|
||||
const canvas = elementList[0];
|
||||
|
||||
// convert expect PNG into pixel data by drawing in new canvas element
|
||||
expectCanvas.id = 'expectCursor';
|
||||
expectCanvas.width = canvas.width;
|
||||
expectCanvas.height = canvas.height;
|
||||
const imageEl = new Image();
|
||||
imageEl.onload = () => {
|
||||
const expectCtx = expectCanvas.getContext('2d');
|
||||
expectCtx.drawImage(imageEl, 0, 0, canvas.width, canvas.height); // draw reference image to size of generated image
|
||||
const expectImageData = expectCtx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// compare live map vs expected pixel data
|
||||
const diffImage = expectCtx.createImageData(canvas.width, canvas.height);
|
||||
const mismatchedPixels = pixelmatch(
|
||||
imageData.data,
|
||||
expectImageData.data,
|
||||
diffImage.data,
|
||||
canvas.width,
|
||||
canvas.height,
|
||||
{ threshold: 0.1 });
|
||||
expect(mismatchedPixels < 16).to.equal(true);
|
||||
// Display difference image for refernce
|
||||
expectCtx.putImageData(diffImage, 0, 0);
|
||||
|
||||
done();
|
||||
};
|
||||
imageEl.src = test.expected;
|
||||
|
||||
// Instructions for creating expected image PNGs
|
||||
// Comment out imageEl creation and image loading
|
||||
// Comment out teardown line that removes expectCanvas from DOM
|
||||
// Uncomment out below lines. Run test, right click canvas and select "Save Image As"
|
||||
// const expectCtx = expectCanvas.getContext('2d');
|
||||
// expectCtx.putImageData(imageData, 0, 0);
|
||||
// done();
|
||||
|
||||
}, 200);
|
||||
const mismatchedPixels = await imageComparator.compareImage(canvas, test.expected, 0.1);
|
||||
expect(mismatchedPixels).to.be.lessThan(16);
|
||||
|
||||
});
|
||||
});
|
||||
|
@ -144,5 +108,29 @@ describe('kibana_map tests', function () {
|
|||
kibanaMap.fitToData();
|
||||
}).to.not.throwException();
|
||||
});
|
||||
|
||||
it('should not throw when resizing to 0 on heatmap', function () {
|
||||
|
||||
const geohashGridOptions = {
|
||||
mapType: 'Heatmap',
|
||||
heatmap: {
|
||||
heatClusterSize: '2'
|
||||
}
|
||||
};
|
||||
|
||||
const geohashLayer = new GeohashLayer(GeoHashSampleData, geohashGridOptions, kibanaMap.getZoomLevel(), kibanaMap);
|
||||
kibanaMap.addLayer(geohashLayer);
|
||||
domNode.style.width = 0;
|
||||
domNode.style.height = 0;
|
||||
expect(() => {
|
||||
kibanaMap.resize();
|
||||
}).to.not.throwException();
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
});
|
||||
});
|
||||
|
|
|
@ -98,7 +98,7 @@ describe('kibana_map tests', function () {
|
|||
});
|
||||
});
|
||||
|
||||
describe('no map height', function () {
|
||||
describe('no map height (should default to size of 1px for height)', function () {
|
||||
beforeEach(async function () {
|
||||
setupDOM('386px', '256px');
|
||||
const noHeightNode = createDiv('386px', '0px');
|
||||
|
@ -111,18 +111,24 @@ describe('kibana_map tests', function () {
|
|||
});
|
||||
});
|
||||
|
||||
it('should calculate map dimensions based on parent element dimensions', function () {
|
||||
it('should calculate map dimensions based on enforcement of single pixel min-width CSS-rule', function () {
|
||||
const bounds = kibanaMap.getUntrimmedBounds();
|
||||
expect(bounds).to.have.property('bottom_right');
|
||||
expect(bounds.bottom_right.lon.toFixed(2)).to.equal('0.27');
|
||||
expect(bounds.bottom_right.lat.toFixed(2)).to.equal('-0.18');
|
||||
expect(round(bounds.bottom_right.lon, 2)).to.equal(0.27);
|
||||
expect(round(bounds.bottom_right.lat, 2)).to.equal(0);
|
||||
expect(bounds).to.have.property('top_left');
|
||||
expect(bounds.top_left.lon.toFixed(2)).to.equal('-0.27');
|
||||
expect(bounds.top_left.lat.toFixed(2)).to.equal('0.18');
|
||||
expect(round(bounds.top_left.lon, 2)).to.equal(-0.27);
|
||||
expect(round(bounds.top_left.lat, 2)).to.equal(0);
|
||||
});
|
||||
|
||||
function round(num, dig) {
|
||||
return Math.round(num * Math.pow(10, dig)) / Math.pow(10, dig);
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
|
||||
describe('no map width', function () {
|
||||
describe('no map width (should default to size of 1px for width)', function () {
|
||||
beforeEach(async function () {
|
||||
setupDOM('386px', '256px');
|
||||
const noWidthNode = createDiv('0px', '256px');
|
||||
|
@ -135,13 +141,13 @@ describe('kibana_map tests', function () {
|
|||
});
|
||||
});
|
||||
|
||||
it('should calculate map dimensions based on parent element dimensions', function () {
|
||||
it('should calculate map dimensions based on enforcement of single pixel min-width CSS-rule', function () {
|
||||
const bounds = kibanaMap.getUntrimmedBounds();
|
||||
expect(bounds).to.have.property('bottom_right');
|
||||
expect(bounds.bottom_right.lon.toFixed(2)).to.equal('0.27');
|
||||
expect(Math.round(bounds.bottom_right.lon)).to.equal(0);
|
||||
expect(bounds.bottom_right.lat.toFixed(2)).to.equal('-0.18');
|
||||
expect(bounds).to.have.property('top_left');
|
||||
expect(bounds.top_left.lon.toFixed(2)).to.equal('-0.27');
|
||||
expect(Math.round(bounds.top_left.lon)).to.equal(0);
|
||||
expect(bounds.top_left.lat.toFixed(2)).to.equal('0.18');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -24,6 +24,7 @@ export function BaseMapsVisualizationProvider(serviceSettings) {
|
|||
destroy() {
|
||||
if (this._kibanaMap) {
|
||||
this._kibanaMap.destroy();
|
||||
this._kibanaMap = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -44,6 +45,11 @@ export function BaseMapsVisualizationProvider(serviceSettings) {
|
|||
*/
|
||||
async render(esResponse, status) {
|
||||
|
||||
if (!this._kibanaMap) {
|
||||
//the visualization has been destroyed;
|
||||
return;
|
||||
}
|
||||
|
||||
await this._mapIsLoaded;
|
||||
|
||||
if (status.resize) {
|
||||
|
@ -78,8 +84,9 @@ export function BaseMapsVisualizationProvider(serviceSettings) {
|
|||
const uiState = this.vis.getUiState();
|
||||
const zoomFromUiState = parseInt(uiState.get('mapZoom'));
|
||||
const centerFromUIState = uiState.get('mapCenter');
|
||||
options.zoom = !isNaN(zoomFromUiState) ? zoomFromUiState : this.vis.type.visConfig.defaults.mapZoom;
|
||||
options.center = centerFromUIState ? centerFromUIState : this.vis.type.visConfig.defaults.mapCenter;
|
||||
options.zoom = !isNaN(zoomFromUiState) ? zoomFromUiState : this.vis.params.mapZoom;
|
||||
options.center = centerFromUIState ? centerFromUIState : this.vis.params.mapCenter;
|
||||
|
||||
this._kibanaMap = new KibanaMap(this._container, options);
|
||||
|
||||
this._kibanaMap.addLegendControl();
|
||||
|
@ -95,8 +102,6 @@ export function BaseMapsVisualizationProvider(serviceSettings) {
|
|||
|
||||
const mapparams = this._getMapsParams();
|
||||
await this._updateBaseLayer(mapparams);
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@ -111,7 +116,6 @@ export function BaseMapsVisualizationProvider(serviceSettings) {
|
|||
this._notify.warning(e.message);
|
||||
}
|
||||
const { minZoom, maxZoom } = this._getMinMaxZoom();
|
||||
|
||||
if (mapParams.wms.enabled) {
|
||||
// Switch to WMS
|
||||
if (maxZoom > this._kibanaMap.getMaxZoomLevel()) {
|
||||
|
|
|
@ -396,47 +396,14 @@ export class KibanaMap extends EventEmitter {
|
|||
|
||||
const southEast = bounds.getSouthEast();
|
||||
const northWest = bounds.getNorthWest();
|
||||
let southEastLng = southEast.lng;
|
||||
let northWestLng = northWest.lng;
|
||||
let southEastLat = southEast.lat;
|
||||
let northWestLat = northWest.lat;
|
||||
const southEastLng = southEast.lng;
|
||||
const northWestLng = northWest.lng;
|
||||
const southEastLat = southEast.lat;
|
||||
const northWestLat = northWest.lat;
|
||||
|
||||
// When map has not width or height, calculate map dimensions based on parent dimensions
|
||||
if (southEastLat === northWestLat || southEastLng === northWestLng) {
|
||||
let parent = this._containerNode.parentNode;
|
||||
while (parent && (parent.clientWidth === 0 || parent.clientHeight === 0)) {
|
||||
parent = parent.parentNode;
|
||||
}
|
||||
let width = 512;
|
||||
let height = 512;
|
||||
if (parent && parent.clientWidth !== 0) {
|
||||
width = parent.clientWidth;
|
||||
}
|
||||
if (parent && parent.clientHeight !== 0) {
|
||||
height = parent.clientHeight;
|
||||
}
|
||||
|
||||
let top = 0;
|
||||
let left = 0;
|
||||
let bottom = height;
|
||||
let right = width;
|
||||
// no height - top is center of map and needs to be adjusted
|
||||
if (southEastLat === northWestLat) {
|
||||
top = height / 2 * -1;
|
||||
bottom = height / 2;
|
||||
}
|
||||
// no width - left is center of map and needs to be adjusted
|
||||
if (southEastLng === northWestLng) {
|
||||
left = width / 2 * -1;
|
||||
right = width / 2;
|
||||
}
|
||||
const containerSouthEast = this._leafletMap.layerPointToLatLng(L.point(right, bottom));
|
||||
const containerNorthWest = this._leafletMap.layerPointToLatLng(L.point(left, top));
|
||||
southEastLng = containerSouthEast.lng;
|
||||
northWestLng = containerNorthWest.lng;
|
||||
southEastLat = containerSouthEast.lat;
|
||||
northWestLat = containerNorthWest.lat;
|
||||
}
|
||||
// When map has not width or height, the map has no dimensions.
|
||||
// These dimensions are enforced due to CSS style rules that enforce min-width/height of 0
|
||||
// that enforcement also resolves errors with the heatmap layer plugin.
|
||||
|
||||
return {
|
||||
bottom_right: {
|
||||
|
|
|
@ -97,6 +97,10 @@
|
|||
.leaflet-container {
|
||||
background: @tilemap-leaflet-container-bg !important;
|
||||
outline: 0 !important;
|
||||
|
||||
//the heatmap layer plugin logs an error to the console when the map is in a 0-sized container
|
||||
min-width: 1px !important;
|
||||
min-height: 1px !important;
|
||||
}
|
||||
|
||||
.leaflet-popup-content-wrapper {
|
||||
|
|
|
@ -15,8 +15,6 @@ const TimelionRequestHandlerProvider = function (Private, Notifier, $http, $root
|
|||
handler: function (vis /*, appState, uiState, queryFilter*/) {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log('[timelion] get');
|
||||
|
||||
const expression = vis.params.expression;
|
||||
if (!expression) return;
|
||||
|
||||
|
|
103
src/test_utils/public/image_comparator.js
Normal file
|
@ -0,0 +1,103 @@
|
|||
import pixelmatch from 'pixelmatch';
|
||||
|
||||
/**
|
||||
* Utility to compare pixels of two images
|
||||
* Adds the snapshots and comparison to the corners of the HTML-body to help with human inspection.
|
||||
*/
|
||||
export class ImageComparator {
|
||||
|
||||
constructor() {
|
||||
|
||||
this._expectCanvas = document.createElement('canvas');
|
||||
this._expectCanvas.style.position = 'fixed';
|
||||
this._expectCanvas.style.right = 0;
|
||||
this._expectCanvas.style.top = 0;
|
||||
this._expectCanvas.style.border = '1px solid green';
|
||||
document.body.appendChild(this._expectCanvas);
|
||||
|
||||
this._diffCanvas = document.createElement('canvas');
|
||||
this._diffCanvas.style.position = 'fixed';
|
||||
this._diffCanvas.style.right = 0;
|
||||
this._diffCanvas.style.bottom = 0;
|
||||
this._diffCanvas.style.border = '1px solid red';
|
||||
document.body.appendChild(this._diffCanvas);
|
||||
|
||||
this._actualCanvas = document.createElement('canvas');
|
||||
this._actualCanvas.style.position = 'fixed';
|
||||
this._actualCanvas.style.left = 0;
|
||||
this._actualCanvas.style.bottom = 0;
|
||||
this._actualCanvas.style.border = '1px solid yellow';
|
||||
document.body.appendChild(this._actualCanvas);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Do pixel-comparison of two images
|
||||
* @param actualCanvasFromUser HTMl5 canvas
|
||||
* @param expectedImageSourcePng Img to compare to
|
||||
* @param threshold number between 0-1. A lower number indicates a lower tolerance for pixel-differences.
|
||||
* @return number
|
||||
*/
|
||||
async compareImage(actualCanvasFromUser, expectedImageSourcePng, threshold) {
|
||||
|
||||
console.log('compare image:actual ', actualCanvasFromUser);
|
||||
console.log('compare image:expected ', expectedImageSourcePng);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
|
||||
|
||||
window.setTimeout(() => {
|
||||
|
||||
const actualContextFromUser = actualCanvasFromUser.getContext('2d');
|
||||
const actualImageDataFromUser = actualContextFromUser.getImageData(0, 0, actualCanvasFromUser.width, actualCanvasFromUser.height);
|
||||
const actualContext = this._actualCanvas.getContext('2d');
|
||||
this._actualCanvas.width = actualCanvasFromUser.width;
|
||||
this._actualCanvas.height = actualCanvasFromUser.height;
|
||||
actualContext.putImageData(actualImageDataFromUser, 0, 0);
|
||||
|
||||
// convert expect PNG into pixel data by drawing in new canvas element
|
||||
this._expectCanvas.width = this._actualCanvas.width;
|
||||
this._expectCanvas.height = this._actualCanvas.height;
|
||||
|
||||
const expectedImage = new Image();
|
||||
expectedImage.onload = () => {
|
||||
|
||||
const expectCtx = this._expectCanvas.getContext('2d');
|
||||
expectCtx.drawImage(expectedImage, 0, 0, this._actualCanvas.width, this._actualCanvas.height); // draw reference image to size of generated image
|
||||
|
||||
const expectImageData = expectCtx.getImageData(0, 0, this._actualCanvas.width, this._actualCanvas.height);
|
||||
|
||||
// compare live map vs expected pixel data
|
||||
const diffImage = expectCtx.createImageData(this._actualCanvas.width, this._actualCanvas.height);
|
||||
const mismatchedPixels = pixelmatch(
|
||||
actualImageDataFromUser.data,
|
||||
expectImageData.data,
|
||||
diffImage.data,
|
||||
this._actualCanvas.width,
|
||||
this._actualCanvas.height,
|
||||
{ threshold: threshold });
|
||||
|
||||
|
||||
const diffContext = this._diffCanvas.getContext('2d');
|
||||
this._diffCanvas.width = this._actualCanvas.width;
|
||||
this._diffCanvas.height = this._actualCanvas.height;
|
||||
diffContext.putImageData(diffImage, 0, 0);
|
||||
|
||||
resolve(mismatchedPixels);
|
||||
};
|
||||
expectedImage.src = expectedImageSourcePng;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
destroy() {
|
||||
document.body.removeChild(this._expectCanvas);
|
||||
document.body.removeChild(this._diffCanvas);
|
||||
document.body.removeChild(this._actualCanvas);
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -87,7 +87,6 @@ uiModules.get('kibana')
|
|||
const tmsService = catalogue.services.filter((service) => service.type === 'tms')[0];
|
||||
const manifest = await this._getManifest(tmsService.manifest, this._queryParams);
|
||||
const services = manifest.data.services;
|
||||
|
||||
const firstService = _.cloneDeep(services[0]);
|
||||
if (!firstService) {
|
||||
throw new Error('Manifest response does not include sufficient service data.');
|
||||
|
@ -108,6 +107,9 @@ uiModules.get('kibana')
|
|||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* this internal method is overridden by the tests to simulate custom manifest.
|
||||
*/
|
||||
async _getManifest(manifestUrl) {
|
||||
return $http({
|
||||
url: extendUrl(manifestUrl, { query: this._queryParams }),
|
||||
|
|