Add i18n keys and unit tests for overlays
- i18n: POI category names, panel UI strings (en + de) - overpass.test.ts: Query building, response parsing, deduplication - poi-cache.test.ts: Tile quantization, cache hit/miss, bbox filtering - poi-categories.test.ts: Profile-to-overlay mapping, category validation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6aa2229b39
commit
c23c325965
6 changed files with 249 additions and 4 deletions
104
apps/planner/app/lib/overpass.test.ts
Normal file
104
apps/planner/app/lib/overpass.test.ts
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { buildQuery, parseResponse, deduplicateById, type Poi } from "./overpass.ts";
|
||||
import { poiCategories } from "./poi-categories.ts";
|
||||
|
||||
describe("buildQuery", () => {
|
||||
it("builds Overpass QL with bbox and category union", () => {
|
||||
const bbox = { south: 52.5, west: 13.3, north: 52.6, east: 13.5 };
|
||||
const categories = poiCategories.filter((c) => c.id === "drinking_water");
|
||||
const query = buildQuery(bbox, categories);
|
||||
|
||||
expect(query).toContain("[out:json]");
|
||||
expect(query).toContain("[bbox:52.5,13.3,52.6,13.5]");
|
||||
expect(query).toContain('amenity"="drinking_water"');
|
||||
expect(query).toContain("out center 200");
|
||||
});
|
||||
|
||||
it("combines multiple categories into a single union", () => {
|
||||
const bbox = { south: 52.5, west: 13.3, north: 52.6, east: 13.5 };
|
||||
const categories = poiCategories.filter((c) => ["drinking_water", "camping"].includes(c.id));
|
||||
const query = buildQuery(bbox, categories);
|
||||
|
||||
expect(query).toContain("drinking_water");
|
||||
expect(query).toContain("camp_site");
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseResponse", () => {
|
||||
it("parses nodes into Poi objects", () => {
|
||||
const data = {
|
||||
elements: [
|
||||
{
|
||||
type: "node",
|
||||
id: 123,
|
||||
lat: 52.52,
|
||||
lon: 13.405,
|
||||
tags: { amenity: "drinking_water", name: "Brunnen" },
|
||||
},
|
||||
],
|
||||
};
|
||||
const categories = poiCategories.filter((c) => c.id === "drinking_water");
|
||||
const pois = parseResponse(data, categories);
|
||||
|
||||
expect(pois).toHaveLength(1);
|
||||
expect(pois[0]!.id).toBe(123);
|
||||
expect(pois[0]!.name).toBe("Brunnen");
|
||||
expect(pois[0]!.category).toBe("drinking_water");
|
||||
expect(pois[0]!.lat).toBe(52.52);
|
||||
});
|
||||
|
||||
it("uses center for way/relation elements", () => {
|
||||
const data = {
|
||||
elements: [
|
||||
{
|
||||
type: "way",
|
||||
id: 456,
|
||||
center: { lat: 52.51, lon: 13.39 },
|
||||
tags: { tourism: "camp_site", name: "Zeltplatz" },
|
||||
},
|
||||
],
|
||||
};
|
||||
const categories = poiCategories.filter((c) => c.id === "camping");
|
||||
const pois = parseResponse(data, categories);
|
||||
|
||||
expect(pois).toHaveLength(1);
|
||||
expect(pois[0]!.lat).toBe(52.51);
|
||||
expect(pois[0]!.lon).toBe(13.39);
|
||||
});
|
||||
|
||||
it("skips elements without coordinates", () => {
|
||||
const data = {
|
||||
elements: [
|
||||
{ type: "node", id: 789, tags: { amenity: "drinking_water" } },
|
||||
],
|
||||
};
|
||||
const categories = poiCategories.filter((c) => c.id === "drinking_water");
|
||||
const pois = parseResponse(data, categories);
|
||||
expect(pois).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("skips elements that don't match any category", () => {
|
||||
const data = {
|
||||
elements: [
|
||||
{ type: "node", id: 100, lat: 52.52, lon: 13.4, tags: { amenity: "bank" } },
|
||||
],
|
||||
};
|
||||
const categories = poiCategories.filter((c) => c.id === "drinking_water");
|
||||
const pois = parseResponse(data, categories);
|
||||
expect(pois).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("deduplicateById", () => {
|
||||
it("removes duplicate POIs by id", () => {
|
||||
const pois: Poi[] = [
|
||||
{ id: 1, lat: 52.5, lon: 13.4, category: "drinking_water", tags: {} },
|
||||
{ id: 1, lat: 52.5, lon: 13.4, category: "camping", tags: {} },
|
||||
{ id: 2, lat: 52.6, lon: 13.5, category: "shelter", tags: {} },
|
||||
];
|
||||
const result = deduplicateById(pois);
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0]!.id).toBe(1);
|
||||
expect(result[1]!.id).toBe(2);
|
||||
});
|
||||
});
|
||||
61
apps/planner/app/lib/poi-cache.test.ts
Normal file
61
apps/planner/app/lib/poi-cache.test.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import { describe, it, expect, beforeEach } from "vitest";
|
||||
import { quantize, getCellKeys, getCached, setCached, clearCache } from "./poi-cache.ts";
|
||||
import type { Poi } from "./overpass.ts";
|
||||
|
||||
beforeEach(() => {
|
||||
clearCache();
|
||||
});
|
||||
|
||||
describe("quantize", () => {
|
||||
it("rounds down to 0.1 degree grid", () => {
|
||||
expect(quantize(52.537)).toBe(52.5);
|
||||
expect(quantize(13.405)).toBe(13.4);
|
||||
expect(quantize(0.0)).toBe(0);
|
||||
expect(quantize(-0.15)).toBe(-0.2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getCellKeys", () => {
|
||||
it("returns cell keys covering the bbox", () => {
|
||||
const bbox = { south: 52.5, west: 13.3, north: 52.6, east: 13.4 };
|
||||
const keys = getCellKeys(bbox);
|
||||
expect(keys.length).toBeGreaterThan(0);
|
||||
expect(keys).toContain("52.5,13.3");
|
||||
expect(keys).toContain("52.5,13.4");
|
||||
expect(keys).toContain("52.6,13.3");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getCached / setCached", () => {
|
||||
const bbox = { south: 52.5, west: 13.3, north: 52.6, east: 13.4 };
|
||||
const categoriesKey = "drinking_water";
|
||||
const pois: Poi[] = [
|
||||
{ id: 1, lat: 52.52, lon: 13.35, category: "drinking_water", tags: {} },
|
||||
{ id: 2, lat: 52.55, lon: 13.38, category: "drinking_water", tags: {} },
|
||||
];
|
||||
|
||||
it("returns null on cache miss", () => {
|
||||
expect(getCached(bbox, categoriesKey)).toBeNull();
|
||||
});
|
||||
|
||||
it("returns cached POIs on cache hit", () => {
|
||||
setCached(bbox, categoriesKey, pois);
|
||||
const result = getCached(bbox, categoriesKey);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("filters POIs to requested bbox", () => {
|
||||
setCached(bbox, categoriesKey, [
|
||||
...pois,
|
||||
{ id: 3, lat: 52.9, lon: 13.35, category: "drinking_water", tags: {} }, // outside bbox
|
||||
]);
|
||||
const result = getCached(bbox, categoriesKey);
|
||||
expect(result).toHaveLength(2); // only the 2 within bbox
|
||||
});
|
||||
|
||||
it("returns null for different categories key", () => {
|
||||
setCached(bbox, categoriesKey, pois);
|
||||
expect(getCached(bbox, "camping")).toBeNull();
|
||||
});
|
||||
});
|
||||
46
apps/planner/app/lib/poi-categories.test.ts
Normal file
46
apps/planner/app/lib/poi-categories.test.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { getCategoriesForProfile, profileOverlayDefaults, poiCategories } from "./poi-categories.ts";
|
||||
|
||||
describe("getCategoriesForProfile", () => {
|
||||
it("returns bike infra for cycling profiles", () => {
|
||||
expect(getCategoriesForProfile("fastbike")).toContain("bike_infra");
|
||||
expect(getCategoriesForProfile("safety")).toContain("bike_infra");
|
||||
});
|
||||
|
||||
it("returns shelter and viewpoints for hiking", () => {
|
||||
const cats = getCategoriesForProfile("trekking");
|
||||
expect(cats).toContain("shelter");
|
||||
expect(cats).toContain("viewpoints");
|
||||
});
|
||||
|
||||
it("returns empty for unknown profiles", () => {
|
||||
expect(getCategoriesForProfile("car")).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("profileOverlayDefaults", () => {
|
||||
it("maps cycling profiles to waymarked-cycling", () => {
|
||||
expect(profileOverlayDefaults.fastbike).toContain("waymarked-cycling");
|
||||
expect(profileOverlayDefaults.safety).toContain("waymarked-cycling");
|
||||
});
|
||||
|
||||
it("maps hiking to waymarked-hiking", () => {
|
||||
expect(profileOverlayDefaults.trekking).toContain("waymarked-hiking");
|
||||
});
|
||||
});
|
||||
|
||||
describe("poiCategories", () => {
|
||||
it("has 9 categories", () => {
|
||||
expect(poiCategories).toHaveLength(9);
|
||||
});
|
||||
|
||||
it("all categories have required fields", () => {
|
||||
for (const cat of poiCategories) {
|
||||
expect(cat.id).toBeTruthy();
|
||||
expect(cat.name).toBeTruthy();
|
||||
expect(cat.icon).toBeTruthy();
|
||||
expect(cat.color).toMatch(/^#/);
|
||||
expect(cat.query).toContain("nwr");
|
||||
}
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue