From 5b1dee3779bd91880e75a47a23440c58f0fc6376 Mon Sep 17 00:00:00 2001 From: Max Grossman Date: Thu, 2 Aug 2018 12:47:47 -0400 Subject: [PATCH] add tests for mapcss checks ref #remote-presets --- modules/core/context.js | 19 +- modules/core/history.js | 2 +- modules/presets/index.js | 4 +- modules/util/mapcss_rule.js | 86 ++++++++- modules/validations/mapcss_checks.js | 4 +- test/spec/util/mapcss_rule.js | 275 +++++++++++++++++++++++++++ 6 files changed, 373 insertions(+), 17 deletions(-) create mode 100644 test/spec/util/mapcss_rule.js diff --git a/modules/core/context.js b/modules/core/context.js index 6c6d5ac81..a69b5f882 100644 --- a/modules/core/context.js +++ b/modules/core/context.js @@ -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; diff --git a/modules/core/history.js b/modules/core/history.js index e8d47a02d..9e45f17cb 100644 --- a/modules/core/history.js +++ b/modules/core/history.js @@ -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); diff --git a/modules/presets/index.js b/modules/presets/index.js index a21a0dee3..04c61cfba 100644 --- a/modules/presets/index.js +++ b/modules/presets/index.js @@ -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); }; diff --git a/modules/util/mapcss_rule.js b/modules/util/mapcss_rule.js index 712c75474..5429a9673 100644 --- a/modules/util/mapcss_rule.js +++ b/modules/util/mapcss_rule.js @@ -1 +1,85 @@ -export function mapcssRule() {} \ No newline at end of file +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; +} diff --git a/modules/validations/mapcss_checks.js b/modules/validations/mapcss_checks.js index 21d57ba1d..f7c508ae8 100644 --- a/modules/validations/mapcss_checks.js +++ b/modules/validations/mapcss_checks.js @@ -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); } } } diff --git a/test/spec/util/mapcss_rule.js b/test/spec/util/mapcss_rule.js new file mode 100644 index 000000000..5be819a16 --- /dev/null +++ b/test/spec/util/mapcss_rule.js @@ -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); + }); + }); + }); +}); +