forked from MapComplete/MapComplete
More unit tests
This commit is contained in:
parent
4f4fc650b1
commit
82e59bc1eb
16 changed files with 433 additions and 375 deletions
|
@ -67,6 +67,30 @@ export class RegexTag extends TagsFilter {
|
|||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this tag matches the given properties
|
||||
*
|
||||
* const isNotEmpty = new RegexTag("key","^$", true);
|
||||
* isNotEmpty.matchesProperties({"key": "value"}) // => true
|
||||
* isNotEmpty.matchesProperties({"key": "other_value"}) // => true
|
||||
* isNotEmpty.matchesProperties({"key": ""}) // => false
|
||||
* isNotEmpty.matchesProperties({"other_key": ""}) // => false
|
||||
* isNotEmpty.matchesProperties({"other_key": "value"}) // => false
|
||||
*
|
||||
* const isNotEmpty = new RegexTag("key","^..*$", true);
|
||||
* isNotEmpty.matchesProperties({"key": "value"}) // => false
|
||||
* isNotEmpty.matchesProperties({"key": "other_value"}) // => false
|
||||
* isNotEmpty.matchesProperties({"key": ""}) // => true
|
||||
* isNotEmpty.matchesProperties({"other_key": ""}) // => true
|
||||
* isNotEmpty.matchesProperties({"other_key": "value"}) // => true
|
||||
*
|
||||
* const notRegex = new RegexTag("x", /^y$/, true)
|
||||
* notRegex.matchesProperties({"x": "y"}) // => false
|
||||
* notRegex.matchesProperties({"x": "z"}) // => true
|
||||
* notRegex.matchesProperties({"x": ""}) // => true
|
||||
* notRegex.matchesProperties({}) // => true
|
||||
|
||||
*/
|
||||
matchesProperties(tags: any): boolean {
|
||||
if (typeof this.key === "string") {
|
||||
const value = tags[this.key] ?? ""
|
||||
|
|
|
@ -22,6 +22,20 @@ export class Tag extends TagsFilter {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* const tag = new Tag("key","value")
|
||||
* tag.matchesProperties({"key": "value"}) // => true
|
||||
* tag.matchesProperties({"key": "z"}) // => false
|
||||
* tag.matchesProperties({"key": ""}) // => false
|
||||
* tag.matchesProperties({"other_key": ""}) // => false
|
||||
* tag.matchesProperties({"other_key": "value"}) // => false
|
||||
*
|
||||
* const isEmpty = new Tag("key","")
|
||||
* isEmpty.matchesProperties({"key": "value"}) // => false
|
||||
* isEmpty.matchesProperties({"key": ""}) // => true
|
||||
* isEmpty.matchesProperties({"other_key": ""}) // => true
|
||||
* isEmpty.matchesProperties({"other_key": "value"}) // => true
|
||||
*/
|
||||
matchesProperties(properties: any): boolean {
|
||||
const foundValue = properties[this.key]
|
||||
if (foundValue === undefined && (this.value === "" || this.value === undefined)) {
|
||||
|
|
|
@ -266,6 +266,9 @@ export class TagUtils {
|
|||
if (tag.indexOf("!=") >= 0) {
|
||||
const split = Utils.SplitFirst(tag, "!=");
|
||||
if (split[1] === "*") {
|
||||
throw "At "+context+": invalid tag "+tag+". To indicate a missing tag, use '"+split[0]+"!=' instead"
|
||||
}
|
||||
if(split[1] === "") {
|
||||
split[1] = "..*"
|
||||
}
|
||||
return new RegexTag(
|
||||
|
|
|
@ -389,8 +389,9 @@ export default class TagRenderingConfig {
|
|||
const freeformValues = tags[this.freeform.key].split(";")
|
||||
const leftovers = freeformValues.filter(v => !usedFreeformValues.has(v))
|
||||
for (const leftover of leftovers) {
|
||||
applicableMappings.push({then: this.render.OnEveryLanguage(str =>
|
||||
str.replace(new RegExp("{"+this.freeform.key+"}", "g"), leftover))})
|
||||
applicableMappings.push({then:
|
||||
this.render.replace("{"+this.freeform.key+"}", leftover)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -174,11 +174,19 @@ export class Translation extends BaseUIElement {
|
|||
return new Translation(newTranslations)
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the given string with the given text in the language.
|
||||
* Other substitutions are left in place
|
||||
*
|
||||
* const tr = new Translation(
|
||||
* {"nl": "Een voorbeeldtekst met {key} en {key1}, en nogmaals {key}",
|
||||
* "en": "Just a single {key}"})
|
||||
* const r = tr.replace("{key}", "value")
|
||||
* r.textFor("nl") // => "Een voorbeeldtekst met value en {key1}, en nogmaals value"
|
||||
* r.textFor("en") // => "Just a single value"
|
||||
*/
|
||||
public replace(a: string, b: string) {
|
||||
if (a.startsWith("{") && a.endsWith("}")) {
|
||||
a = a.substr(1, a.length - 2);
|
||||
}
|
||||
return this.Subs({[a]: b});
|
||||
return this.OnEveryLanguage(str => str.replace(new RegExp(a, "g"), b))
|
||||
}
|
||||
|
||||
public Clone() {
|
||||
|
|
51
Utils.ts
51
Utils.ts
|
@ -295,14 +295,44 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
/**
|
||||
* Copies all key-value pairs of the source into the target. This will change the target
|
||||
* If the key starts with a '+', the values of the list will be appended to the target instead of overwritten
|
||||
* @param source
|
||||
* @param target
|
||||
* @constructor
|
||||
* @return the second parameter as is
|
||||
*
|
||||
* const obj = {someValue: 42};
|
||||
* const override = {someValue: null};
|
||||
* Utils.Merge(override, obj);
|
||||
* obj.someValue // => null
|
||||
*
|
||||
* const obj = {someValue: 42};
|
||||
* const override = {someValue: null};
|
||||
* const returned = Utils.Merge(override, obj);
|
||||
* returned == obj // => true
|
||||
*
|
||||
* const source = {
|
||||
* abc: "def",
|
||||
* foo: "bar",
|
||||
* list0: ["overwritten"],
|
||||
* "list1+": ["appended"]
|
||||
* }
|
||||
* const target = {
|
||||
* "xyz": "omega",
|
||||
* "list0": ["should-be-gone"],
|
||||
* "list1": ["should-be-kept"],
|
||||
* "list2": ["should-be-untouched"]
|
||||
* }
|
||||
* const result = Utils.Merge(source, target)
|
||||
* result.abc // => "def"
|
||||
* result.foo // => "bar"
|
||||
* result.xyz // => "omega"
|
||||
* result.list0.length // => 1
|
||||
* result.list0[0] // => "overwritten"
|
||||
* result.list1.length // => 2
|
||||
* result.list1[0] // => "should-be-kept"
|
||||
* result.list1[1] // => "appended"
|
||||
* result.list2.length // => 1
|
||||
* result.list2[0] // => "should-be-untouched"
|
||||
*/
|
||||
static Merge(source: any, target: any) {
|
||||
static Merge<T, S>(source: S, target: T): (T & S) {
|
||||
if (target === null) {
|
||||
return source
|
||||
return <T & S> source
|
||||
}
|
||||
|
||||
for (const key in source) {
|
||||
|
@ -322,6 +352,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
|
||||
let newList: any[];
|
||||
if (key.startsWith("+")) {
|
||||
// @ts-ignore
|
||||
newList = sourceV.concat(targetV)
|
||||
} else {
|
||||
newList = targetV.concat(sourceV)
|
||||
|
@ -332,21 +363,26 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
}
|
||||
|
||||
const sourceV = source[key]
|
||||
// @ts-ignore
|
||||
const targetV = target[key]
|
||||
if (typeof sourceV === "object") {
|
||||
if (sourceV === null) {
|
||||
// @ts-ignore
|
||||
target[key] = null
|
||||
} else if (targetV === undefined) {
|
||||
// @ts-ignore
|
||||
target[key] = sourceV;
|
||||
} else {
|
||||
Utils.Merge(sourceV, targetV);
|
||||
}
|
||||
|
||||
} else {
|
||||
// @ts-ignore
|
||||
target[key] = sourceV;
|
||||
}
|
||||
|
||||
}
|
||||
// @ts-ignore
|
||||
return target;
|
||||
}
|
||||
|
||||
|
@ -471,6 +507,9 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
return dict.get(k);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to minify the given JSON by applying some compression
|
||||
*/
|
||||
public static MinifyJSON(stringified: string): string {
|
||||
stringified = stringified.replace(/\|/g, "||");
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
"watch:css": "tailwindcss -i index.css -o css/index-tailwind-output.css --watch",
|
||||
"generate:css": "tailwindcss -i index.css -o css/index-tailwind-output.css",
|
||||
"generate:doctests": "doctest-ts --mocha Logic/**/*.ts && doctest-ts --mocha UI/**/*.ts && doctest-ts --mocha *.ts && doctest-ts --mocha Models/**/*.ts",
|
||||
"test": "(npm run generate:doctests 2>&1 | grep -v \"No doctests found in\") && mocha --require ts-node/register \"Logic/**/*.doctest.ts\" \"tests/*\" \"tests/**/*.ts\"",
|
||||
"test": "(npm run generate:doctests 2>&1 | grep -v \"No doctests found in\") && mocha --require ts-node/register \"./**/*.doctest.ts\" \"tests/*\" \"tests/**/*.ts\"",
|
||||
"init": "npm ci && npm run generate && npm run generate:editor-layer-index && npm run generate:layouts && npm run clean",
|
||||
"add-weblate-upstream": "git remote add weblate-layers https://hosted.weblate.org/git/mapcomplete/layer-translations/ ; git remote add weblate-core https://hosted.weblate.org/git/mapcomplete/layer-core/; git remote add weblate-themes https://hosted.weblate.org/git/mapcomplete/layer-themes/; git remote add weblate-github git@github.com:weblate/MapComplete.git",
|
||||
"fix-weblate": "git remote update weblate-layers; git merge weblate-layers/master",
|
||||
|
|
|
@ -1,49 +0,0 @@
|
|||
import T from "./TestHelper";
|
||||
import {exec} from "child_process";
|
||||
|
||||
export default class CodeQualitySpec extends T {
|
||||
|
||||
constructor() {
|
||||
super([
|
||||
[
|
||||
"no constructor.name in compiled code", () => {
|
||||
CodeQualitySpec.detectInCode("constructor\\.name", "This is not allowed, as minification does erase names.")
|
||||
}],
|
||||
[
|
||||
"no reverse in compiled code", () => {
|
||||
CodeQualitySpec.detectInCode("reverse()", "Reverse is stateful and changes the source list. This often causes subtle bugs")
|
||||
}]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param forbidden: a GREP-regex. This means that '.' is a wildcard and should be escaped to match a literal dot
|
||||
* @param reason
|
||||
* @private
|
||||
*/
|
||||
private static detectInCode(forbidden: string, reason: string) {
|
||||
|
||||
const excludedDirs = [".git", "node_modules", "dist", ".cache", ".parcel-cache", "assets"]
|
||||
|
||||
exec("grep -n \"" + forbidden + "\" -r . " + excludedDirs.map(d => "--exclude-dir=" + d).join(" "), ((error, stdout, stderr) => {
|
||||
if (error?.message?.startsWith("Command failed: grep")) {
|
||||
console.warn("Command failed!")
|
||||
return;
|
||||
}
|
||||
if (error !== null) {
|
||||
throw error
|
||||
|
||||
}
|
||||
if (stderr !== "") {
|
||||
throw stderr
|
||||
}
|
||||
|
||||
const found = stdout.split("\n").filter(s => s !== "").filter(s => !s.startsWith("./test/"));
|
||||
if (found.length > 0) {
|
||||
throw `Found a '${forbidden}' at \n ${found.join("\n ")}.\n ${reason}`
|
||||
}
|
||||
|
||||
}))
|
||||
}
|
||||
}
|
|
@ -9,190 +9,14 @@ import {Tag} from "../Logic/Tags/Tag";
|
|||
import {And} from "../Logic/Tags/And";
|
||||
import {TagUtils} from "../Logic/Tags/TagUtils";
|
||||
import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig";
|
||||
import {TagsFilter} from "../Logic/Tags/TagsFilter";
|
||||
import {Or} from "../Logic/Tags/Or";
|
||||
import {RegexTag} from "../Logic/Tags/RegexTag";
|
||||
|
||||
export default class TagSpec extends T {
|
||||
|
||||
constructor() {
|
||||
super([
|
||||
["Tag replacement works in translation", () => {
|
||||
const tr = new Translation({
|
||||
"en": "Test {key} abc"
|
||||
}).replace("{key}", "value");
|
||||
equal(tr.txt, "Test value abc");
|
||||
|
||||
}],
|
||||
["Optimize tags", () => {
|
||||
|
||||
let t : TagsFilter= new And(
|
||||
[
|
||||
new And([
|
||||
new Tag("x", "y")
|
||||
]),
|
||||
new Tag("a", "b")
|
||||
]
|
||||
)
|
||||
let opt =<TagsFilter> t.optimize()
|
||||
console.log(TagUtils.toString(opt))
|
||||
T.equals(`a=b&x=y`,TagUtils.toString(opt), "Optimization failed")
|
||||
|
||||
|
||||
// foo&bar & (x=y | a=b) & (x=y | c=d) & foo=bar is equivalent too foo=bar & ((x=y) | (a=b & c=d))
|
||||
t = new And([
|
||||
new Tag("foo","bar"),
|
||||
new Or([
|
||||
new Tag("x", "y"),
|
||||
new Tag("a", "b")
|
||||
]),
|
||||
new Or([
|
||||
new Tag("x", "y"),
|
||||
new Tag("c", "d")
|
||||
])
|
||||
])
|
||||
opt =<TagsFilter> t.optimize()
|
||||
console.log(TagUtils.toString(opt))
|
||||
T.equals(TagUtils.toString(opt), "foo=bar& (x=y| (a=b&c=d) )")
|
||||
|
||||
t = new Or([
|
||||
new Tag("foo","bar"),
|
||||
new And([
|
||||
new Tag("foo", "bar"),
|
||||
new Tag("x", "y"),
|
||||
])
|
||||
])
|
||||
opt =<TagsFilter> t.optimize()
|
||||
console.log(TagUtils.toString(opt))
|
||||
T.equals("foo=bar", TagUtils.toString(opt), "Optimizing away an unneeded factor failed")
|
||||
|
||||
|
||||
t = new And([
|
||||
new RegexTag("x","y"),
|
||||
new Tag("a","b")
|
||||
])
|
||||
opt =<TagsFilter> t.optimize()
|
||||
T.equals("a=b&x~^y$", TagUtils.toString(opt), "Regexes go to the end")
|
||||
|
||||
t = new And([
|
||||
new Tag("bicycle","yes"),
|
||||
new Tag("amenity","binoculars")
|
||||
])
|
||||
opt =<TagsFilter> t.optimize()
|
||||
T.equals("amenity=binoculars&bicycle=yes", TagUtils.toString(opt), "Common keys go to the end")
|
||||
|
||||
|
||||
|
||||
|
||||
const filter = TagUtils.Tag( {or: [
|
||||
{
|
||||
"and": [
|
||||
{
|
||||
"or": ["amenity=charging_station","disused:amenity=charging_station","planned:amenity=charging_station","construction:amenity=charging_station"]
|
||||
},
|
||||
"bicycle=yes"
|
||||
]
|
||||
},
|
||||
{
|
||||
"and": [
|
||||
{
|
||||
"or": ["amenity=charging_station","disused:amenity=charging_station","planned:amenity=charging_station","construction:amenity=charging_station"]
|
||||
},
|
||||
]
|
||||
},
|
||||
"amenity=toilets",
|
||||
"amenity=bench",
|
||||
"leisure=picnic_table",
|
||||
{
|
||||
"and": [
|
||||
"tower:type=observation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"and": [
|
||||
"amenity=bicycle_repair_station"
|
||||
]
|
||||
},
|
||||
{
|
||||
"and": [
|
||||
{
|
||||
"or": [
|
||||
"amenity=bicycle_rental",
|
||||
"bicycle_rental~*",
|
||||
"service:bicycle:rental=yes",
|
||||
"rental~.*bicycle.*"
|
||||
]
|
||||
},
|
||||
"bicycle_rental!=docking_station"
|
||||
]
|
||||
},
|
||||
{
|
||||
"and": [
|
||||
"leisure=playground",
|
||||
"playground!=forest"
|
||||
]
|
||||
}
|
||||
]});
|
||||
|
||||
opt = <TagsFilter> filter.optimize()
|
||||
console.log(TagUtils.toString(opt))
|
||||
T.equals(("amenity=charging_station|" +
|
||||
"amenity=toilets|" +
|
||||
"amenity=bench|" +
|
||||
"amenity=bicycle_repair_station" +
|
||||
"|construction:amenity=charging_station|" +
|
||||
"disused:amenity=charging_station|" +
|
||||
"leisure=picnic_table|" +
|
||||
"planned:amenity=charging_station|" +
|
||||
"tower:type=observation| " +
|
||||
"( (amenity=bicycle_rental|service:bicycle:rental=yes|bicycle_rental~^..*$|rental~^.*bicycle.*$) &bicycle_rental!~^docking_station$) |" +
|
||||
" (leisure=playground&playground!~^forest$)").replace(/ /g, ""),
|
||||
TagUtils.toString(opt).replace(/ /g, ""), "Advanced case failed")
|
||||
}],
|
||||
|
||||
|
||||
["Parse tag config", (() => {
|
||||
const tag = TagUtils.Tag("key=value") as Tag;
|
||||
equal(tag.key, "key");
|
||||
equal(tag.value, "value");
|
||||
equal(tag.matchesProperties({"key": "value"}), true)
|
||||
equal(tag.matchesProperties({"key": "z"}), false)
|
||||
equal(tag.matchesProperties({"key": ""}), false)
|
||||
equal(tag.matchesProperties({"other_key": ""}), false)
|
||||
equal(tag.matchesProperties({"other_key": "value"}), false)
|
||||
|
||||
const isEmpty = TagUtils.Tag("key=") as Tag;
|
||||
equal(isEmpty.matchesProperties({"key": "value"}), false)
|
||||
equal(isEmpty.matchesProperties({"key": ""}), true)
|
||||
equal(isEmpty.matchesProperties({"other_key": ""}), true)
|
||||
equal(isEmpty.matchesProperties({"other_key": "value"}), true)
|
||||
|
||||
const isNotEmpty = TagUtils.Tag("key!=");
|
||||
equal(isNotEmpty.matchesProperties({"key": "value"}), true)
|
||||
equal(isNotEmpty.matchesProperties({"key": "other_value"}), true)
|
||||
equal(isNotEmpty.matchesProperties({"key": ""}), false)
|
||||
equal(isNotEmpty.matchesProperties({"other_key": ""}), false)
|
||||
equal(isNotEmpty.matchesProperties({"other_key": "value"}), false)
|
||||
|
||||
|
||||
const and = TagUtils.Tag({"and": ["key=value", "x=y"]}) as And;
|
||||
equal((and.and[0] as Tag).key, "key");
|
||||
equal((and.and[1] as Tag).value, "y");
|
||||
|
||||
|
||||
const notReg = TagUtils.Tag("x!~y") as And;
|
||||
equal(notReg.matchesProperties({"x": "y"}), false)
|
||||
equal(notReg.matchesProperties({"x": "z"}), true)
|
||||
equal(notReg.matchesProperties({"x": ""}), true)
|
||||
equal(notReg.matchesProperties({}), true)
|
||||
|
||||
const noMatch = TagUtils.Tag("key!=value") as Tag;
|
||||
equal(noMatch.matchesProperties({"key": "value"}), false)
|
||||
equal(noMatch.matchesProperties({"key": "otherValue"}), true)
|
||||
equal(noMatch.matchesProperties({"key": ""}), true)
|
||||
equal(noMatch.matchesProperties({"otherKey": ""}), true)
|
||||
|
||||
|
||||
|
||||
const multiMatch = TagUtils.Tag("vending~.*bicycle_tube.*") as Tag;
|
||||
equal(multiMatch.matchesProperties({"vending": "bicycle_tube"}), true)
|
||||
equal(multiMatch.matchesProperties({"vending": "something;bicycle_tube"}), true)
|
||||
|
|
|
@ -1,132 +0,0 @@
|
|||
import T from "./TestHelper";
|
||||
import {Utils} from "../Utils";
|
||||
import {equal} from "assert";
|
||||
import LZString from "lz-string";
|
||||
|
||||
export default class UtilsSpec extends T {
|
||||
private static readonly example = {
|
||||
"id": "bookcases",
|
||||
"maintainer": "MapComplete",
|
||||
"version": "2020-08-29",
|
||||
"language": [
|
||||
"en",
|
||||
"nl",
|
||||
"de",
|
||||
"fr"
|
||||
],
|
||||
"title": {
|
||||
"en": "Open Bookcase Map",
|
||||
"nl": "Open Boekenruilkastenkaart",
|
||||
"de": "Öffentliche Bücherschränke Karte",
|
||||
"fr": "Carte des microbibliothèques"
|
||||
},
|
||||
"description": {
|
||||
"en": "A public bookcase is a small streetside cabinet, box, old phone boot or some other objects where books are stored. Everyone can place or take a book. This map aims to collect all these bookcases. You can discover new bookcases nearby and, with a free OpenStreetMap account, quickly add your favourite bookcases.",
|
||||
"nl": "Een boekenruilkast is een kastje waar iedereen een boek kan nemen of achterlaten. Op deze kaart kan je deze boekenruilkasten terugvinden en met een gratis OpenStreetMap-account, ook boekenruilkasten toevoegen of informatie verbeteren",
|
||||
"de": "Ein öffentlicher Bücherschrank ist ein kleiner Bücherschrank am Straßenrand, ein Kasten, eine alte Telefonzelle oder andere Gegenstände, in denen Bücher aufbewahrt werden. Jeder kann ein Buch hinstellen oder mitnehmen. Diese Karte zielt darauf ab, all diese Bücherschränke zu sammeln. Sie können neue Bücherschränke in der Nähe entdecken und mit einem kostenlosen OpenStreetMap-Account schnell Ihre Lieblingsbücherschränke hinzufügen.",
|
||||
"fr": "Une microbibliothèques, également appelée boite à livre, est un élément de mobilier urbain (étagère, armoire, etc) dans lequel sont stockés des livres et autres objets en accès libre. Découvrez les boites à livres prêt de chez vous, ou ajouter en une nouvelle à l'aide de votre compte OpenStreetMap."
|
||||
},
|
||||
"icon": "./assets/themes/bookcases/bookcase.svg",
|
||||
"socialImage": null,
|
||||
"startLat": 0,
|
||||
"startLon": 0,
|
||||
"startZoom": 1,
|
||||
"widenFactor": 0.05,
|
||||
"roamingRenderings": [],
|
||||
"layers": [
|
||||
"public_bookcase"
|
||||
]
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super([
|
||||
["Sort object keys", () => {
|
||||
const o = {
|
||||
x: 'x',
|
||||
abc: {'x': 'x', 'a': 'a'},
|
||||
def: 'def'
|
||||
}
|
||||
equal('{"x":"x","abc":{"x":"x","a":"a"},"def":"def"}', JSON.stringify(o))
|
||||
const copy = Utils.sortKeys(o)
|
||||
equal('{"abc":{"a":"a","x":"x"},"def":"def","x":"x"}', JSON.stringify(copy))
|
||||
}],
|
||||
|
||||
["Minify-json", () => {
|
||||
const str = JSON.stringify({title: "abc", "and": "xyz", "render": "somevalue"}, null, 0);
|
||||
const minified = Utils.MinifyJSON(str);
|
||||
console.log(minified)
|
||||
console.log("Minified version has ", minified.length, "chars")
|
||||
const restored = Utils.UnMinify(minified)
|
||||
console.log(restored)
|
||||
console.log("Restored version has ", restored.length, "chars")
|
||||
equal(str, restored)
|
||||
|
||||
}],
|
||||
["Minify-json of the bookcases", () => {
|
||||
const str = JSON.stringify(UtilsSpec.example, null, 0)
|
||||
const minified = Utils.MinifyJSON(str);
|
||||
console.log("Minified version has ", minified.length, "chars")
|
||||
const restored = Utils.UnMinify(minified)
|
||||
console.log("Restored version has ", restored.length, "chars")
|
||||
equal(str, restored)
|
||||
|
||||
}],
|
||||
["Minify-json with LZ-string of the bookcases", () => {
|
||||
const str = JSON.stringify(UtilsSpec.example, null, 0)
|
||||
const minified = LZString.compressToBase64(Utils.MinifyJSON(str));
|
||||
console.log("Minified version has ", minified.length, "chars")
|
||||
const restored = Utils.UnMinify(LZString.decompressFromBase64(minified))
|
||||
console.log("Restored version has ", restored.length, "chars")
|
||||
equal(str, restored)
|
||||
|
||||
}],
|
||||
["Minify-json with only LZ-string of the bookcases", () => {
|
||||
const str = JSON.stringify(UtilsSpec.example, null, 0)
|
||||
const minified = LZString.compressToBase64(str);
|
||||
console.log("Minified version has ", minified.length, "chars")
|
||||
const restored = LZString.decompressFromBase64(minified)
|
||||
console.log("Restored version has ", restored.length, "chars")
|
||||
equal(str, restored)
|
||||
|
||||
}],
|
||||
["TestMerge", () => {
|
||||
|
||||
const source = {
|
||||
abc: "def",
|
||||
foo: "bar",
|
||||
list0: ["overwritten"],
|
||||
"list1+": ["appended"]
|
||||
}
|
||||
const target = {
|
||||
"xyz": "omega",
|
||||
"list0": ["should-be-gone"],
|
||||
"list1": ["should-be-kept"],
|
||||
"list2": ["should-be-untouched"]
|
||||
}
|
||||
const result = Utils.Merge(source, target)
|
||||
|
||||
equal(result.abc, "def")
|
||||
equal(result.foo, "bar")
|
||||
equal(result.xyz, "omega")
|
||||
equal(result.list0.length, 1)
|
||||
equal(result.list0[0], "overwritten")
|
||||
equal(result.list1.length, 2)
|
||||
equal(result.list1[0], "should-be-kept")
|
||||
equal(result.list1[1], "appended")
|
||||
equal(result.list2.length, 1)
|
||||
equal(result.list2[0], "should-be-untouched")
|
||||
}],
|
||||
["Test merge with null", () => {
|
||||
const obj = {
|
||||
someValue: 42
|
||||
}
|
||||
const override = {
|
||||
someValue: null
|
||||
}
|
||||
Utils.Merge(override, obj)
|
||||
equal(obj.someValue, null)
|
||||
}]
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
45
tests/CodeQuality.spec.ts
Normal file
45
tests/CodeQuality.spec.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
import {describe} from 'mocha'
|
||||
import {exec} from "child_process";
|
||||
|
||||
/**
|
||||
*
|
||||
* @param forbidden: a GREP-regex. This means that '.' is a wildcard and should be escaped to match a literal dot
|
||||
* @param reason
|
||||
* @private
|
||||
*/
|
||||
function detectInCode(forbidden: string, reason: string) {
|
||||
|
||||
const excludedDirs = [".git", "node_modules", "dist", ".cache", ".parcel-cache", "assets"]
|
||||
|
||||
exec("grep -n \"" + forbidden + "\" -r . " + excludedDirs.map(d => "--exclude-dir=" + d).join(" "), ((error, stdout, stderr) => {
|
||||
if (error?.message?.startsWith("Command failed: grep")) {
|
||||
console.warn("Command failed!")
|
||||
return;
|
||||
}
|
||||
if (error !== null) {
|
||||
throw error
|
||||
|
||||
}
|
||||
if (stderr !== "") {
|
||||
throw stderr
|
||||
}
|
||||
|
||||
const found = stdout.split("\n").filter(s => s !== "").filter(s => !s.startsWith("./tests/") && !s.startsWith("./testLegacy/"));
|
||||
if (found.length > 0) {
|
||||
throw `Found a '${forbidden}' at \n ${found.join("\n ")}.\n ${reason}`
|
||||
}
|
||||
|
||||
}))
|
||||
}
|
||||
|
||||
describe("Code quality", () => {
|
||||
it("should not contain reverse", () => {
|
||||
detectInCode("reverse()", "Reverse is stateful and changes the source list. This often causes subtle bugs")
|
||||
})
|
||||
|
||||
it("should not contain 'constructor.name'", () => {
|
||||
detectInCode("constructor\\.name", "This is not allowed, as minification does erase names.")
|
||||
})
|
||||
|
||||
})
|
||||
|
150
tests/Logic/Tags/OptimzeTags.spec.ts
Normal file
150
tests/Logic/Tags/OptimzeTags.spec.ts
Normal file
|
@ -0,0 +1,150 @@
|
|||
import {describe} from 'mocha'
|
||||
import {expect} from 'chai'
|
||||
import {TagsFilter} from "../../../Logic/Tags/TagsFilter";
|
||||
import {And} from "../../../Logic/Tags/And";
|
||||
import {Tag} from "../../../Logic/Tags/Tag";
|
||||
import {TagUtils} from "../../../Logic/Tags/TagUtils";
|
||||
import {Or} from "../../../Logic/Tags/Or";
|
||||
import {RegexTag} from "../../../Logic/Tags/RegexTag";
|
||||
|
||||
describe("Tag optimalization", () => {
|
||||
|
||||
describe("And", () => {
|
||||
it("with condition and nested and should be flattened", () => {
|
||||
const t = new And(
|
||||
[
|
||||
new And([
|
||||
new Tag("x", "y")
|
||||
]),
|
||||
new Tag("a", "b")
|
||||
]
|
||||
)
|
||||
const opt =<TagsFilter> t.optimize()
|
||||
expect(TagUtils.toString(opt)).eq(`a=b&x=y`)
|
||||
})
|
||||
|
||||
it("with nested ors and commons property should be extracted", () => {
|
||||
|
||||
// foo&bar & (x=y | a=b) & (x=y | c=d) & foo=bar is equivalent too foo=bar & ((x=y) | (a=b & c=d))
|
||||
const t = new And([
|
||||
new Tag("foo","bar"),
|
||||
new Or([
|
||||
new Tag("x", "y"),
|
||||
new Tag("a", "b")
|
||||
]),
|
||||
new Or([
|
||||
new Tag("x", "y"),
|
||||
new Tag("c", "d")
|
||||
])
|
||||
])
|
||||
const opt =<TagsFilter> t.optimize()
|
||||
expect(TagUtils.toString(opt)).eq("foo=bar& (x=y| (a=b&c=d) )")
|
||||
})
|
||||
|
||||
it("should move regextag to the end", () => {
|
||||
const t = new And([
|
||||
new RegexTag("x","y"),
|
||||
new Tag("a","b")
|
||||
])
|
||||
const opt =<TagsFilter> t.optimize()
|
||||
expect(TagUtils.toString(opt)).eq("a=b&x~^y$")
|
||||
|
||||
})
|
||||
|
||||
it("should sort tags by their popularity (least popular first)", () => {
|
||||
const t = new And([
|
||||
new Tag("bicycle","yes"),
|
||||
new Tag("amenity","binoculars")
|
||||
])
|
||||
const opt =<TagsFilter> t.optimize()
|
||||
expect(TagUtils.toString(opt)).eq("amenity=binoculars&bicycle=yes")
|
||||
|
||||
})
|
||||
|
||||
it("should optimize an advanced, real world case", () => {
|
||||
const filter = TagUtils.Tag( {or: [
|
||||
{
|
||||
"and": [
|
||||
{
|
||||
"or": ["amenity=charging_station","disused:amenity=charging_station","planned:amenity=charging_station","construction:amenity=charging_station"]
|
||||
},
|
||||
"bicycle=yes"
|
||||
]
|
||||
},
|
||||
{
|
||||
"and": [
|
||||
{
|
||||
"or": ["amenity=charging_station","disused:amenity=charging_station","planned:amenity=charging_station","construction:amenity=charging_station"]
|
||||
},
|
||||
]
|
||||
},
|
||||
"amenity=toilets",
|
||||
"amenity=bench",
|
||||
"leisure=picnic_table",
|
||||
{
|
||||
"and": [
|
||||
"tower:type=observation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"and": [
|
||||
"amenity=bicycle_repair_station"
|
||||
]
|
||||
},
|
||||
{
|
||||
"and": [
|
||||
{
|
||||
"or": [
|
||||
"amenity=bicycle_rental",
|
||||
"bicycle_rental~*",
|
||||
"service:bicycle:rental=yes",
|
||||
"rental~.*bicycle.*"
|
||||
]
|
||||
},
|
||||
"bicycle_rental!=docking_station"
|
||||
]
|
||||
},
|
||||
{
|
||||
"and": [
|
||||
"leisure=playground",
|
||||
"playground!=forest"
|
||||
]
|
||||
}
|
||||
]});
|
||||
const opt = <TagsFilter> filter.optimize()
|
||||
const expected = "amenity=charging_station|" +
|
||||
"amenity=toilets|" +
|
||||
"amenity=bench|" +
|
||||
"amenity=bicycle_repair_station" +
|
||||
"|construction:amenity=charging_station|" +
|
||||
"disused:amenity=charging_station|" +
|
||||
"leisure=picnic_table|" +
|
||||
"planned:amenity=charging_station|" +
|
||||
"tower:type=observation| " +
|
||||
"( (amenity=bicycle_rental|service:bicycle:rental=yes|bicycle_rental~^..*$|rental~^.*bicycle.*$) &bicycle_rental!~^docking_station$) |" +
|
||||
" (leisure=playground&playground!~^forest$)"
|
||||
|
||||
expect(TagUtils.toString(opt).replace(/ /g, ""))
|
||||
.eq(expected.replace(/ /g, ""))
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
describe("Or", () => {
|
||||
it("with nested And which has a common property should be dropped", () => {
|
||||
|
||||
const t = new Or([
|
||||
new Tag("foo","bar"),
|
||||
new And([
|
||||
new Tag("foo", "bar"),
|
||||
new Tag("x", "y"),
|
||||
])
|
||||
])
|
||||
const opt =<TagsFilter> t.optimize()
|
||||
expect(TagUtils.toString(opt)).eq("foo=bar")
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
})
|
56
tests/Logic/Tags/TagUtils.spec.ts
Normal file
56
tests/Logic/Tags/TagUtils.spec.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
import {describe} from 'mocha'
|
||||
import {expect} from 'chai'
|
||||
import {TagUtils} from "../../../Logic/Tags/TagUtils";
|
||||
import {Tag} from "../../../Logic/Tags/Tag";
|
||||
import {RegexTag} from "../../../Logic/Tags/RegexTag";
|
||||
import {And} from "../../../Logic/Tags/And";
|
||||
|
||||
describe("TagUtils", () => {
|
||||
|
||||
describe("ParseTag", () => {
|
||||
it("should parse a simple key=value tag", () => {
|
||||
const tag = TagUtils.Tag("key=value") as Tag;
|
||||
expect(tag.key).eq( "key");
|
||||
expect(tag.value).eq("value");
|
||||
})
|
||||
|
||||
it("should parse an 'empty spec' key= tag", () => {
|
||||
const tag = TagUtils.Tag("key=") as Tag;
|
||||
expect(tag.key).eq( "key");
|
||||
expect(tag.value).eq("");
|
||||
})
|
||||
it("should parse a 'not-empty spec' key!= tag", () => {
|
||||
const tag = TagUtils.Tag("key!=") as RegexTag;
|
||||
expect(tag.invert).true
|
||||
expect(tag.key).eq( "key");
|
||||
expect(tag.value["source"]).eq("^..*$");
|
||||
})
|
||||
|
||||
it("should parse a normal 'not-equals-value' key!=value tag", () => {
|
||||
const tag = TagUtils.Tag("key!=value") as RegexTag
|
||||
expect(tag.invert).true
|
||||
expect(tag.key).eq( "key");
|
||||
expect(tag.value["source"]).eq("^value$");
|
||||
})
|
||||
|
||||
it("should refuse a key!=* tag", () => {
|
||||
expect(() => TagUtils.Tag("key!=*")).to.throw();
|
||||
})
|
||||
|
||||
it("should parse regextags", () => {
|
||||
const notReg = TagUtils.Tag("x!~y") as RegexTag;
|
||||
expect(notReg.key).eq("x")
|
||||
expect(notReg.value["source"]).eq("^y$")
|
||||
expect(notReg.invert).true
|
||||
})
|
||||
|
||||
it("should parse and", () => {
|
||||
const and = TagUtils.Tag({"and": ["key=value", "x=y"]}) as And;
|
||||
expect(and.and[0]["key"]).eq("key")
|
||||
expect(and.and[0]["value"]).eq("value")
|
||||
expect(and.and[1]["key"]).eq("x")
|
||||
expect(and.and[1]["value"]).eq("y")
|
||||
|
||||
})
|
||||
})
|
||||
})
|
|
@ -7473,7 +7473,7 @@ Utils.injectJsonDownloadForTests( "https://www.wikidata.org/wiki/Special:Enti
|
|||
|
||||
describe("Wikidata", () => {
|
||||
|
||||
it("Download lion",
|
||||
it("should download Q140 (lion)",
|
||||
async () => {
|
||||
|
||||
const wikidata = await Wikidata.LoadWikidataEntryAsync("Q140")
|
||||
|
@ -7481,14 +7481,14 @@ describe("Wikidata", () => {
|
|||
}
|
||||
)
|
||||
|
||||
it("download wikidata",
|
||||
it("should download wikidata",
|
||||
async () => {
|
||||
const wdata = await Wikidata.LoadWikidataEntryAsync(14517013)
|
||||
expect(wdata.wikisites).to.have.key("nl")
|
||||
expect(wdata.wikisites.get("nl")).eq("Vredesmolen")
|
||||
})
|
||||
|
||||
it("Download a lexeme", async () => {
|
||||
it("should download a lexeme", async () => {
|
||||
|
||||
const response = await Wikidata.LoadWikidataEntryAsync("https://www.wikidata.org/wiki/Lexeme:L614072")
|
||||
expect(response).not.undefined
|
||||
|
|
|
@ -5,7 +5,7 @@ import ValidatedTextField from "../../UI/Input/ValidatedTextField";
|
|||
|
||||
describe("ValidatedTextFields", () => {
|
||||
|
||||
it("All validated text fields should have a name and description", () => {
|
||||
it("should all have description in the translations", () => {
|
||||
const ts = Translations.t.validation;
|
||||
const missingTranslations = Array.from(ValidatedTextField.allTypes.keys())
|
||||
.filter(key => ts[key] === undefined || ts[key].description === undefined)
|
||||
|
|
75
tests/Utils.MinifyJson.spec.ts
Normal file
75
tests/Utils.MinifyJson.spec.ts
Normal file
|
@ -0,0 +1,75 @@
|
|||
import {Utils} from "../Utils";
|
||||
import LZString from "lz-string";
|
||||
import {describe} from 'mocha'
|
||||
import {expect} from 'chai'
|
||||
|
||||
const example = {
|
||||
"id": "bookcases",
|
||||
"maintainer": "MapComplete",
|
||||
"version": "2020-08-29",
|
||||
"language": [
|
||||
"en",
|
||||
"nl",
|
||||
"de",
|
||||
"fr"
|
||||
],
|
||||
"title": {
|
||||
"en": "Open Bookcase Map",
|
||||
"nl": "Open Boekenruilkastenkaart",
|
||||
"de": "Öffentliche Bücherschränke Karte",
|
||||
"fr": "Carte des microbibliothèques"
|
||||
},
|
||||
"description": {
|
||||
"en": "A public bookcase is a small streetside cabinet, box, old phone boot or some other objects where books are stored. Everyone can place or take a book. This map aims to collect all these bookcases. You can discover new bookcases nearby and, with a free OpenStreetMap account, quickly add your favourite bookcases.",
|
||||
"nl": "Een boekenruilkast is een kastje waar iedereen een boek kan nemen of achterlaten. Op deze kaart kan je deze boekenruilkasten terugvinden en met een gratis OpenStreetMap-account, ook boekenruilkasten toevoegen of informatie verbeteren",
|
||||
"de": "Ein öffentlicher Bücherschrank ist ein kleiner Bücherschrank am Straßenrand, ein Kasten, eine alte Telefonzelle oder andere Gegenstände, in denen Bücher aufbewahrt werden. Jeder kann ein Buch hinstellen oder mitnehmen. Diese Karte zielt darauf ab, all diese Bücherschränke zu sammeln. Sie können neue Bücherschränke in der Nähe entdecken und mit einem kostenlosen OpenStreetMap-Account schnell Ihre Lieblingsbücherschränke hinzufügen.",
|
||||
"fr": "Une microbibliothèques, également appelée boite à livre, est un élément de mobilier urbain (étagère, armoire, etc) dans lequel sont stockés des livres et autres objets en accès libre. Découvrez les boites à livres prêt de chez vous, ou ajouter en une nouvelle à l'aide de votre compte OpenStreetMap."
|
||||
},
|
||||
"icon": "./assets/themes/bookcases/bookcase.svg",
|
||||
"socialImage": null,
|
||||
"startLat": 0,
|
||||
"startLon": 0,
|
||||
"startZoom": 1,
|
||||
"widenFactor": 0.05,
|
||||
"roamingRenderings": [],
|
||||
"layers": [
|
||||
"public_bookcase"
|
||||
]
|
||||
}
|
||||
|
||||
describe("Utils", () => {
|
||||
describe("Minify-json", () => {
|
||||
|
||||
|
||||
it("should work in two directions", () => {
|
||||
const str = JSON.stringify({title: "abc", "and": "xyz", "render": "somevalue"});
|
||||
const minified = Utils.MinifyJSON(str);
|
||||
const restored = Utils.UnMinify(minified)
|
||||
expect(str).eq(restored)
|
||||
})
|
||||
it("should minify and restore the bookcase example", () => {
|
||||
const str = JSON.stringify(example, null, 0)
|
||||
const minified = Utils.MinifyJSON(str);
|
||||
const restored = Utils.UnMinify(minified)
|
||||
expect(str).eq(restored)
|
||||
|
||||
})
|
||||
it("should LZ-compress a theme", () => {
|
||||
const str = JSON.stringify(example, null, 0)
|
||||
const minified = LZString.compressToBase64(Utils.MinifyJSON(str));
|
||||
const restored = Utils.UnMinify(LZString.decompressFromBase64(minified))
|
||||
expect(str).eq(restored)
|
||||
|
||||
})
|
||||
it("shoud be able to decode the LZ-compression of a theme", () => {
|
||||
const str = JSON.stringify(example, null, 0)
|
||||
const minified = LZString.compressToBase64(str);
|
||||
const restored = LZString.decompressFromBase64(minified)
|
||||
expect(str).eq(restored)
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
|
Loading…
Reference in a new issue