add tests for mapcss checks

ref #remote-presets
This commit is contained in:
Max Grossman
2018-08-02 12:47:47 -04:00
parent 2bf5eaf6e7
commit 5b1dee3779
6 changed files with 373 additions and 17 deletions
+9 -10
View File
@@ -5,6 +5,7 @@ import _find from 'lodash-es/find';
import _forOwn from 'lodash-es/forOwn';
import _isObject from 'lodash-es/isObject';
import _isString from 'lodash-es/isString';
import _map from 'lodash-es/map';
import { dispatch as d3_dispatch } from 'd3-dispatch';
@@ -15,8 +16,6 @@ import {
import { select as d3_select } from 'd3-selection';
import mapcssParse from 'mapcss-parse/source/index';
import {
t,
currentLocale,
@@ -456,14 +455,14 @@ export function coreContext() {
locale = locale.split('-')[0];
}
if (utilExternalValidationRules()) {
var validationsUrl = utilStringQs(window.location.hash)['validations'];
d3_text(validationsUrl, function (err, mapcss) {
if (err) return;
var validations = _map(mapcssParse(mapcss), function(mapcssConfig) { return utilMapCSSRule(mapcsS) });
context.validationRules = function() { return validations; };
});
}
// if (utilExternalValidationRules()) {
// var validationsUrl = utilStringQs(window.location.hash).validations;
// d3_text(validationsUrl, function (err, mapcss) {
// if (err) return;
// var validations = _map(mapcssParse(mapcss), function(mapcssConfig) { return utilMapCSSRule(mapcssConfig); });
// context.validationRules = function() { return validations; };
// });
// }
history = coreHistory(context);
context.graph = history.graph;
+1 -1
View File
@@ -284,7 +284,7 @@ export function coreHistory(context) {
return _flatten(
_map(Validations, function(fn) {
var warnings;
if (fn === Validations.validationMapCSSChecks) {
if (fn === Validations.validationMapCSSChecks && context.hasOwnProperty('validationRules')) {
warnings = fn()(changes, _stack[_index].graph, context.validationRules());
} else {
warnings = fn()(changes, _stack[_index].graph);
+2 -2
View File
@@ -183,7 +183,7 @@ export function presetIndex() {
};
all.fromExternal = function() {
var presetsUrl = utilStringQs(window.location.hash)['presets'];
var presetsUrl = utilStringQs(window.location.hash).presets;
d3_json(presetsUrl, function(err, presets) {
if (err) all.init();
all.overwrite(presets);
@@ -213,7 +213,7 @@ export function presetIndex() {
all.defaults = function(geometry, n) {
var rec = _recent.matchGeometry(geometry).collection.slice(0, 4);
var def = _uniq(rec.concat(_defaults[geometry].collection)).slice(0, n - 1);
var fin = _uniq(rec.concat(def).concat(all.item(geometry))).filter(i => i !== undefined);
var fin = _uniq(rec.concat(def).concat(all.item(geometry))).filter(function(d) { return d !== undefined; });
return presetCollection(fin);
};
+85 -1
View File
@@ -1 +1,85 @@
export function mapcssRule() {}
import _isMatch from 'lodash-es/isMatch';
export function utilMapCSSRule(selector) {
var ruleChecks = {
equals: function (tags) {
return _isMatch(tags, selector.equals);
},
notEquals: function (tags) {
return !_isMatch(tags, selector.notEquals);
},
absence: function(tags) {
return Object.keys(tags).indexOf(selector.absence) === -1;
},
presence: function(tags) {
return Object.keys(tags).indexOf(selector.presence) > -1;
},
greaterThan: function(tags) {
var key = Object.keys(selector.greaterThan)[0];
var value = selector.greaterThan[key];
return tags[key] > value;
},
greaterThanEqual: function(tags) {
var key = Object.keys(selector.greaterThanEqual)[0];
var value = selector.greaterThanEqual[key];
return tags[key] >= value;
},
lessThan: function(tags) {
var key = Object.keys(selector.lessThan)[0];
var value = selector.lessThan[key];
return tags[key] < value;
},
lessThanEqual: function(tags) {
var key = Object.keys(selector.lessThanEqual)[0];
var value = selector.lessThanEqual[key];
return tags[key] <= value;
},
positiveRegex: function(tags) {
var tagKey = Object.keys(selector.positiveRegex)[0];
var expression = selector.positiveRegex[tagKey].join('|');
var regex = new RegExp(expression);
return regex.test(tags[tagKey]);
},
negativeRegex: function(tags) {
var tagKey = Object.keys(selector.negativeRegex)[0];
var expression = selector.negativeRegex[tagKey].join('|');
var regex = new RegExp(expression);
return !regex.test(tags[tagKey]);
}
};
var rule = {
ruleChecks: ruleChecks,
type: Object.keys(selector).indexOf('error') > -1 ? 'error' : 'warning',
buildChecks: function() {
return Object.keys(selector)
.filter(function(key) { return key !== 'geometry' && key !== 'error' && key !== 'warning'; })
.map(function(key) { return ruleChecks[key]; });
},
matches: function(entity) {
return this.buildChecks().every(function(check) { return check(entity.tags); });
},
geometryMatches: function(entity, graph) {
if (entity.type === 'node' || entity.type === 'relation') {
return selector.geometry === entity.type;
} else if (entity.type === 'way') {
return selector.geometry === entity.geometry(graph);
}
},
findWarnings: function (entity, graph, warnings) {
if (this.geometryMatches(entity, graph) && this.matches(entity)) {
warnings.push({
id: 'mapcss_' + rule.type,
message: selector[rule.type],
entity: entity
});
}
}
};
return rule;
}
+1 -3
View File
@@ -1,5 +1,3 @@
// import { t } from '../util/locale';
export function validationMapCSSChecks() {
var validation = function(changes, graph, rules) {
var warnings = [];
@@ -10,7 +8,7 @@ export function validationMapCSSChecks() {
var entity = entities[i];
for (var k = 0; k < rules.length; k++) {
var rule = rules[k];
rules.findWarnings(entity, rules);
rule.findWarnings(entity, graph, warnings);
}
}
}
+275
View File
@@ -0,0 +1,275 @@
describe('iD.utilMapCSSRule', function() {
var entities = [
iD.Entity({ type: 'node', tags: { amenity: 'marketplace' }}),
iD.Entity({ type: 'node', tags: { man_made: 'water_tap' }}),
iD.Entity({ type: 'node', tags: { amenity: 'marketplace', height: 0 }}),
iD.Entity({ type: 'node', tags: { amenity: 'school', height: 5, width: 3 }}),
iD.Entity({ type: 'node', tags: { amenity: 'healthcare' }}),
iD.Entity({ type: 'node', tags: { amenity: 'place_of_worship' }}),
];
var selectors = [
{
'geometry':'node',
'equals':{'amenity':'marketplace'},
'absence':'name',
'warning':'throwWarning: "[amenity=marketplace]: MapRules preset \'Market\': must be coupled with name";'
},
{
'geometry':'node',
'equals':{'man_made':'water_tap'},
'absence':'name',
'warning':'throwWarning: "[amenity=drinking_water][man_made=water_tap]: MapRules preset \'Water Tap\': must be coupled with name";'
},
{
'geometry':'node',
'equals':{'amenity':'marketplace'},
'presence':'height',
'lessThanEqual': { 'height': 0 },
'warning':'throwWarning: "[amenity=marketplace]: height must be greater than 0";'
},
{
'geometry': 'node',
'equals': {'amenity': 'school'},
'greaterThan': { 'height': 0 },
'greaterThanEqual': { 'width': 1 },
'lessThanEqual': { 'width': 10 },
'lessThan': { 'height': 10 },
'warning': 'this is the warning!'
},
{
'geometry': 'node',
'presence': 'amenity',
'positiveRegex': { amenity: ['^school$', '^healthcare$'] },
'error': 'amenity cannot be healthcare or school!'
},
{
'geometry': 'node',
'presence': 'amenity',
'negativeRegex': { amenity: ['^school$', '^healthcare$'] },
'error': 'amenity must be healtcare or school!'
}
];
var rules = selectors.map(function(s) { return iD.utilMapCSSRule(s); });
it ('turns selector object in mapcssRule', function () {
var ruleKeys = ['ruleChecks', 'type','buildChecks','matches','geometryMatches','findWarnings'];
rules.forEach(function(rule) {
expect(Object.keys(rule)).to.eql(ruleKeys);
});
});
describe('#type', function() {
it('is either error or warning', function() {
selectors.forEach(function(s) {
expect(['error', 'warning'].indexOf(iD.utilMapCSSRule(s).type)).to.be.greaterThan(-1);
});
});
});
describe('#geometryMatches', function() {
it('determines if entity and rule geometries match', function() {
var node = iD.Entity({ type: 'node'});
var way = iD.Entity({ type: 'way'});
var graph = iD.Graph([node, way]);
rules.forEach(function(rule) {
expect(rule.geometryMatches(node, graph)).to.be.true;
expect(rule.geometryMatches(way, graph)).to.be.false;
});
});
});
describe('#buildsChecks', function() {
it('builds array of MapCSS rule check functions to run entities against', function() {
rules.forEach(function(rule) {
expect(rule.buildChecks().every(function(fn) { return fn instanceof Function; })).to.be.true;
});
});
});
describe('#matches', function() {
it('determines if an entity matches the MapCSS rule checks', function() {
var node = iD.Entity({ type: 'node', tags: { power: 'tower' }});
rules.forEach(function(rule, i) {
expect(rule.matches(entities[i])).to.be.true;
expect(rule.matches(node)).to.be.false;
});
});
});
describe('#ruleChecks', function() {
describe('equals', function() {
it('is true when entity.tags intersects selector.equals', function() {
var pseudoSelector = { equals: {'amenity': 'school'} };
var pseudoRule = iD.utilMapCSSRule(pseudoSelector);
var school = iD.Entity({ type: 'node', tags: { amenity: 'school' }});
expect(pseudoRule.ruleChecks.equals(school.tags)).to.be.true;
});
it('is false when entity.tags intersects selector.equals', function() {
var pseudoSelector = { equals: { 'man_made': 'water_tap'} };
var pseudoRule = iD.utilMapCSSRule(pseudoSelector);
var school = iD.Entity({ type: 'node', tags: { amenity: 'school' } } );
expect(pseudoRule.ruleChecks.equals(school.tags)).to.be.false;
});
});
describe('notEquals', function() {
it('is true when entity.tags does not intersect selector.notEquals', function() {
var pseudoSelector = { notEquals: { 'man_made': 'water_tap'} };
var pseudoRule = iD.utilMapCSSRule(pseudoSelector);
var school = iD.Entity({ type: 'node', tags: { amenity: 'school' } } );
expect(pseudoRule.ruleChecks.notEquals(school.tags)).to.be.true;
});
it('is false when entity.tags does not intersect selector.notEquals', function() {
var pseudoSelector = { notEquals: { 'amenity': 'school'} };
var pseudoRule = iD.utilMapCSSRule(pseudoSelector);
var school = iD.Entity({ type: 'node', tags: { amenity: 'school' } } );
expect(pseudoRule.ruleChecks.notEquals(school.tags)).to.be.false;
});
});
describe('presence', function() {
it('is true when entity.tags\' key s include selector.presence', function() {
var pseudoSelector = { presence: 'name' };
var pseudoRule = iD.utilMapCSSRule(pseudoSelector);
var kHouse = iD.Entity({ type: 'node', tags: { amenity: 'marketplace', name: 'Kensington Square' }});
expect(pseudoRule.ruleChecks.presence(kHouse.tags)).to.be.true;
});
it('is false when entity tags\' keys do not include selector.presence', function() {
var pseudoSelector = { presence: 'name' };
var pseudoRule = iD.utilMapCSSRule(pseudoSelector);
var notKHouse = iD.Entity({ type: 'node', tags: { amenity: 'marketplace' }});
expect(pseudoRule.ruleChecks.presence(notKHouse.tags)).to.be.false;
});
});
describe('absence', function() {
it('is true when entity.tags\' keys do not include selector.absence', function() {
var pseudoSelector = { absence: 'name' };
var pseudoRule = iD.utilMapCSSRule(pseudoSelector);
var notKHouse = iD.Entity({ type: 'node', tags: { amenity: 'marketplace' }});
expect(pseudoRule.ruleChecks.absence(notKHouse.tags)).to.be.true;
});
it('is false when entity.tags\' keys include selector.absence', function() {
var pseudoSelector = { absence: 'name' };
var pseudoRule = iD.utilMapCSSRule(pseudoSelector);
var kHouse = iD.Entity({ type: 'node', tags: { amenity: 'marketplace', name: 'Kensington Square' }});
expect(pseudoRule.ruleChecks.presence(kHouse.tags)).to.be.false;
});
});
describe('greaterThan', function() {
it('is true when entity.tags\' equivalent value is greater than selector.greaterThan', function() {
var pseudoSelector = { greaterThan: { height: 10 }};
var pseudoRule = iD.utilMapCSSRule(pseudoSelector);
var tallSchool = iD.Entity({ type: 'node', tags: { amenity: 'school', height: 9000 }});
expect(pseudoRule.ruleChecks.greaterThan(tallSchool.tags)).to.be.true;
});
it('is false when entity.tags\' equivalent value is less than or equal to selector.greaterThan', function() {
var pseudoSelector = { greaterThan: { height: 10 }};
var pseudoRule = iD.utilMapCSSRule(pseudoSelector);
var smallSchool = iD.Entity({ type: 'node', tags: { amenity: 'school', height: 9 }});
expect(pseudoRule.ruleChecks.greaterThan(smallSchool.tags)).to.be.false;
});
});
describe('greaterThanEqual', function() {
it('is true when entity.tags\' equivalent value is greater than or equal to selector.greaterThanEqual', function() {
var pseudoSelector = { greaterThanEqual: { height: 10 } };
var pseudoRule = iD.utilMapCSSRule(pseudoSelector);
var okHeightSchool = iD.Entity({ type: 'node', tags: { amenity: 'school', height: 10 }});
expect(pseudoRule.ruleChecks.greaterThanEqual(okHeightSchool.tags)).to.be.true;
});
it('is false when entity.tags\' equivalent value is less than to selector.greaterThanEqual', function() {
var pseudoSelector = { greaterThanEqual: { height: 10 }};
var pseudoRule = iD.utilMapCSSRule(pseudoSelector);
var smallSchool = iD.Entity({ type: 'node', tags: { amenity: 'school', height: 9 }});
expect(pseudoRule.ruleChecks.greaterThanEqual(smallSchool.tags)).to.be.false;
});
});
describe('lessThan', function() {
it('is true when entity.tags\' equivalent value is less than to selector.lessThan', function() {
var pseudoSelector = { lessThan: { height: 10 } };
var pseudoRule = iD.utilMapCSSRule(pseudoSelector);
var smallSchool = iD.Entity({ type: 'node', tags: { amenity: 'school', height: 3 }});
expect(pseudoRule.ruleChecks.lessThan(smallSchool.tags)).to.be.tru;
});
it('is false when entity.tags\' equivalent value is greater than or equal to selector.lessThan', function() {
var pseudoSelector = { lessThan: { height: 10 } };
var pseudoRule = iD.utilMapCSSRule(pseudoSelector);
var notOkHeightSchool = iD.Entity({ type: 'node', tags: { amenity: 'school', height: 10 }});
expect(pseudoRule.ruleChecks.lessThan(notOkHeightSchool.tags)).to.be.false;
});
});
describe('lessThanEqual', function() {
it('is true when entity.tags\' equivalent value is less than or equal to to selector.lessThan', function() {
var pseudoSelector = { lessThanEqual: { height: 10 } };
var pseudoRule = iD.utilMapCSSRule(pseudoSelector);
var okHeightSchool = iD.Entity({ type: 'node', tags: { 'amenity': 'school', 'height': 10 }});
expect(pseudoRule.ruleChecks.lessThanEqual(okHeightSchool.tags)).to.be.true;
});
it('is false when entity.tags\' equivalent value is greater than to selector.lessThan', function() {
var pseudoSelector = { lessThanEqual: { height: 10 } };
var pseudoRule = iD.utilMapCSSRule(pseudoSelector);
var notOkHeightSchool = iD.Entity({ type: 'node', tags: { amenity: 'school', height: 11 }});
expect(pseudoRule.ruleChecks.lessThanEqual(notOkHeightSchool.tags)).to.be.false;
});
});
describe('positiveRegex', function() {
it('is true when entity.tags\' equivalent value matches regular expression built from selector.positiveRegex', function() {
var pseudoSelector = { positiveRegex: { amenity: ['^school$', '^healthcare$'] }};
var pseudoRule = iD.utilMapCSSRule(pseudoSelector);
var okAmenities = [
iD.Entity({ type: 'node', tags: { amenity: 'school' }}),
iD.Entity({ type: 'node', tags: { amenity: 'healthcare' }})
];
okAmenities.forEach(function(amenity) {
expect(pseudoRule.ruleChecks.positiveRegex(amenity.tags)).to.be.true;
});
});
it('is false when entity.tags\' equivalent value does not match regular expression built from selector.positiveRegex', function() {
var pseudoSelector = { positiveRegex: { amenity: ['^school$', '^healthcare$'] }};
var pseudoRule = iD.utilMapCSSRule(pseudoSelector);
var notOkAmenities = [
iD.Entity({ type: 'node', tags: { amenity: 'parking' }}),
iD.Entity({ type: 'node', tags: { amenity: 'place_of_worship' }})
];
notOkAmenities.forEach(function(amenity) {
expect(pseudoRule.ruleChecks.positiveRegex(amenity.tags)).to.be.false;
});
});
});
describe('negativeRegex', function() {
it('is true when entity.tags\' equivalent value does not match regular exprsesion built from selector.negativeRegex', function() {
var pseudoSelector = { negativeRegex: { amenity: ['^school$', '^healthcare$'] }};
var pseudoRule = iD.utilMapCSSRule(pseudoSelector);
var notOkAmenities = [
iD.Entity({ type: 'node', tags: { amenity: 'parking' }}),
iD.Entity({ type: 'node', tags: { amenity: 'place_of_worship' }})
];
notOkAmenities.forEach(function(amenity) {
expect(pseudoRule.ruleChecks.negativeRegex(amenity.tags)).to.be.true;
});
});
it('is false when entity.tags\' equivalent value matches regular expression built from selector.negativeRegex', function() {
var pseudoSelector = { negativeRegex: { amenity: ['^school$', '^healthcare$'] }};
var pseudoRule = iD.utilMapCSSRule(pseudoSelector);
var okAmenities = [
iD.Entity({ type: 'node', tags: { amenity: 'school' }}),
iD.Entity({ type: 'node', tags: { amenity: 'healthcare' }})
];
okAmenities.forEach(function(amenity) {
expect(pseudoRule.ruleChecks.negativeRegex(amenity.tags)).to.be.false;
});
});
});
});
describe('#findWarnings', function() {
it('adds found warnings to warnings array', function() {
var graph = iD.Graph([entities]);
var warnings = [];
rules.forEach(function(rule) {
entities.forEach(function(entity) {
rule.findWarnings(entity, graph, warnings);
});
});
warnings.forEach(function(warning) {
// console.log(warning);
// expect(warning.message).to.not.be.null;
// expect(['mapcss_warning', 'mapcss_error'].indexOf(warning.id)).to.be.greaterThan(-1);
// expect(warning.entity).to.be.instanceOf(iD.Entity);
});
});
});
});