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");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -53,12 +53,12 @@
|
|||
|
||||
## 9. i18n
|
||||
|
||||
- [ ] 9.1 Add translation keys for all overlay names, POI category names, and UI strings (en + de)
|
||||
- [x] 9.1 Add translation keys for all overlay names, POI category names, and UI strings (en + de)
|
||||
|
||||
## 10. Testing
|
||||
|
||||
- [ ] 10.1 Unit tests for Overpass client: query building, response parsing, deduplication
|
||||
- [ ] 10.2 Unit tests for POI cache: tile quantization, TTL expiry, cache hit/miss
|
||||
- [ ] 10.3 Unit tests for profile-to-overlay mapping
|
||||
- [x] 10.1 Unit tests for Overpass client: query building, response parsing, deduplication
|
||||
- [x] 10.2 Unit tests for POI cache: tile quantization, TTL expiry, cache hit/miss
|
||||
- [x] 10.3 Unit tests for profile-to-overlay mapping
|
||||
- [ ] 10.4 E2E test: enable hillshading overlay, verify tile requests
|
||||
- [ ] 10.5 E2E test: enable POI category, verify markers appear (mock Overpass response)
|
||||
|
|
|
|||
|
|
@ -69,6 +69,23 @@ export default {
|
|||
ascent: "Anstieg",
|
||||
descent: "Abstieg",
|
||||
},
|
||||
poi: {
|
||||
toggle: "Sehenswürdigkeiten",
|
||||
title: "Sehenswürdigkeiten",
|
||||
loading: "Laden...",
|
||||
zoomIn: "Näher heranzoomen für POIs",
|
||||
rateLimited: "POI-Daten vorübergehend nicht verfügbar",
|
||||
error: "POIs konnten nicht geladen werden",
|
||||
drinkingWater: "Trinkwasser",
|
||||
shelter: "Unterstand",
|
||||
camping: "Camping",
|
||||
food: "Essen & Trinken",
|
||||
groceries: "Lebensmittel",
|
||||
bikeInfra: "Fahrrad-Infrastruktur",
|
||||
accommodation: "Unterkunft",
|
||||
viewpoints: "Aussichtspunkte",
|
||||
toilets: "Toiletten",
|
||||
},
|
||||
noGoAreas: {
|
||||
draw: "Sperrgebiet zeichnen",
|
||||
cancel: "Sperrgebiet abbrechen",
|
||||
|
|
|
|||
|
|
@ -69,6 +69,23 @@ export default {
|
|||
ascent: "Ascent",
|
||||
descent: "Descent",
|
||||
},
|
||||
poi: {
|
||||
toggle: "Points of Interest",
|
||||
title: "Points of Interest",
|
||||
loading: "Loading...",
|
||||
zoomIn: "Zoom in to see POIs",
|
||||
rateLimited: "POI data temporarily unavailable",
|
||||
error: "Failed to load POIs",
|
||||
drinkingWater: "Drinking water",
|
||||
shelter: "Shelter",
|
||||
camping: "Camping",
|
||||
food: "Food & drink",
|
||||
groceries: "Groceries",
|
||||
bikeInfra: "Bike infrastructure",
|
||||
accommodation: "Accommodation",
|
||||
viewpoints: "Viewpoints",
|
||||
toilets: "Toilets",
|
||||
},
|
||||
noGoAreas: {
|
||||
draw: "Draw no-go area",
|
||||
cancel: "Cancel no-go area",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue