mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
test: check the destination preset table as data (#506)
Supersedes #168 by @daikaginza, ported onto the current component layout with fixes from review. Verified before merge: format, package boundaries, unit suite, build, and the browser tracking suite; a browser harness for the change where one applies; adversarial review applied. Co-authored-by: daikaginza <[email protected]>
This commit is contained in:
1 parent
e6deeaeca8
commit
687edc4da7
3 files changed
+207
-1
No files matched your search
@@ -283,6 +283,15 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md
|
|||||||
- Remove the annotation GeoJSON conversion module and its tests. Nothing in the
|
- Remove the annotation GeoJSON conversion module and its tests. Nothing in the
|
||||||
application read or wrote it, so it carried no behavior. Annotations are
|
application read or wrote it, so it carried no behavior. Annotations are
|
||||||
unchanged. Contributed by raiyan22 (#293).
|
unchanged. Contributed by raiyan22 (#293).
|
||||||
|
- Check the destination preset table as data: every entry carries the keys the
|
||||||
|
camera reads, its numbers are finite, its coordinates are on Earth, its camera
|
||||||
|
angle is one a camera can hold, `viewBounds` latitudes are not swapped, every
|
||||||
|
landmark lies inside its own destination's `viewBounds` (wrapped, so a view
|
||||||
|
across the antimeridian is valid), and every `LOCATIONS` row matches the
|
||||||
|
destination it names. This is a structural guard on the hand-written table for
|
||||||
|
whoever adds the next destination; it passes on the current entries and makes
|
||||||
|
no claim about how well any destination is framed. Contributed by daikaginza
|
||||||
|
(#168).
|
||||||
|
|
||||||
- Extract application lifecycle and viewer exports. Split standalone startup into
|
- Extract application lifecycle and viewer exports. Split standalone startup into
|
||||||
scene setup, controls, layer registration, tools and loading UI. Startup failure
|
scene setup, controls, layer registration, tools and loading UI. Startup failure
|
||||||
|
|||||||
@@ -611,5 +611,6 @@
|
|||||||
"src/layers/vessels/recordPolicy.js",
|
"src/layers/vessels/recordPolicy.js",
|
||||||
"src/layers/vessels/snapshotRenderer.js",
|
"src/layers/vessels/snapshotRenderer.js",
|
||||||
"src/layers/vessels/records.test.mjs",
|
"src/layers/vessels/records.test.mjs",
|
||||||
"src/layers/vessels/ingestion.test.mjs"
|
"src/layers/vessels/ingestion.test.mjs",
|
||||||
|
"src/cityPresets.test.mjs"
|
||||||
]
|
]
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
// CITY PRESETS — the destination table, checked as data.
|
||||||
|
//
|
||||||
|
// CITY_POIS is written by hand and read directly by the camera, so a malformed
|
||||||
|
// row is not a crash: the pill lights up, the camera flies, and it frames the
|
||||||
|
// wrong thing. These checks are structural. They say a row is well formed —
|
||||||
|
// present keys, finite numbers, coordinates on Earth, a camera angle a camera
|
||||||
|
// can hold, a landmark inside the rectangle its own destination opens on — and
|
||||||
|
// say nothing about whether a destination is framed well.
|
||||||
|
import { test } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { CITY_POIS, LOCATIONS } from './locations.js';
|
||||||
|
|
||||||
|
const DESTINATIONS = Object.entries(CITY_POIS);
|
||||||
|
const REQUIRED_POI_KEYS = ['name', 'lat', 'lon', 'alt', 'pitch', 'heading'];
|
||||||
|
|
||||||
|
/** `id · POI name`, so a failure names the row instead of an index. */
|
||||||
|
const where = (id, poi) => `${id} · ${poi.name}`;
|
||||||
|
|
||||||
|
/** Longitude folded into [-180, 180]. */
|
||||||
|
const wrapLon = (lon) => ((((lon + 180) % 360) + 360) % 360) - 180;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether a longitude lies in the west→east span, the way navigation reads it.
|
||||||
|
*
|
||||||
|
* A span that crosses the antimeridian has a west greater than its east
|
||||||
|
* (179 → -179), and everything outside that pair is what falls between them.
|
||||||
|
*/
|
||||||
|
export function lonWithin(lon, west, east) {
|
||||||
|
const value = wrapLon(lon);
|
||||||
|
const from = wrapLon(west);
|
||||||
|
const to = wrapLon(east);
|
||||||
|
return from <= to
|
||||||
|
? value >= from && value <= to
|
||||||
|
: value >= from || value <= to;
|
||||||
|
}
|
||||||
|
|
||||||
|
test('the table is not empty, and every destination offers at least one place', () => {
|
||||||
|
assert.ok(DESTINATIONS.length > 0);
|
||||||
|
for (const [id, city] of DESTINATIONS) {
|
||||||
|
assert.ok(
|
||||||
|
typeof city.name === 'string' && city.name.trim().length > 0,
|
||||||
|
`${id} has no name`,
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
Array.isArray(city.pois) && city.pois.length > 0,
|
||||||
|
`${id} has no POIs`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('every POI carries the keys the camera reads', () => {
|
||||||
|
for (const [id, city] of DESTINATIONS) {
|
||||||
|
for (const poi of city.pois) {
|
||||||
|
for (const key of REQUIRED_POI_KEYS) {
|
||||||
|
assert.notEqual(
|
||||||
|
poi[key],
|
||||||
|
undefined,
|
||||||
|
`${where(id, poi)} is missing ${key}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// A blank name reaches the pill as an unlabelled button.
|
||||||
|
assert.ok(
|
||||||
|
typeof poi.name === 'string' && poi.name.trim().length > 0,
|
||||||
|
`${id} has a place with no name`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('every coordinate is a finite point on Earth', () => {
|
||||||
|
for (const [id, city] of DESTINATIONS) {
|
||||||
|
for (const poi of city.pois) {
|
||||||
|
assert.ok(
|
||||||
|
Number.isFinite(poi.lat) && Math.abs(poi.lat) <= 90,
|
||||||
|
`${where(id, poi)} lat ${poi.lat}`,
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
Number.isFinite(poi.lon) && Math.abs(poi.lon) <= 180,
|
||||||
|
`${where(id, poi)} lon ${poi.lon}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('every camera angle is one a camera can hold', () => {
|
||||||
|
for (const [id, city] of DESTINATIONS) {
|
||||||
|
for (const poi of city.pois) {
|
||||||
|
// Looking down, not up and not along the horizon: these are aerial frames.
|
||||||
|
assert.ok(
|
||||||
|
Number.isFinite(poi.pitch) && poi.pitch < 0 && poi.pitch >= -90,
|
||||||
|
`${where(id, poi)} pitch ${poi.pitch}`,
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
Number.isFinite(poi.heading) && poi.heading >= 0 && poi.heading < 360,
|
||||||
|
`${where(id, poi)} heading ${poi.heading}`,
|
||||||
|
);
|
||||||
|
// A non-finite altitude is a camera at no distance at all.
|
||||||
|
assert.ok(
|
||||||
|
Number.isFinite(poi.alt) && poi.alt > 0,
|
||||||
|
`${where(id, poi)} alt ${poi.alt}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('every viewBounds is a finite rectangle on Earth', () => {
|
||||||
|
for (const [id, city] of DESTINATIONS) {
|
||||||
|
assert.ok(city.viewBounds, `${id} has no viewBounds`);
|
||||||
|
const { southwest: sw, northeast: ne } = city.viewBounds;
|
||||||
|
for (const [corner, point] of [
|
||||||
|
['southwest', sw],
|
||||||
|
['northeast', ne],
|
||||||
|
]) {
|
||||||
|
assert.ok(
|
||||||
|
Number.isFinite(point?.lat) && Math.abs(point.lat) <= 90,
|
||||||
|
`${id} ${corner} lat ${point?.lat}`,
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
Number.isFinite(point?.lng) && Math.abs(point.lng) <= 180,
|
||||||
|
`${id} ${corner} lng ${point?.lng}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Swapped latitudes produce an inverted-but-plausible box: the framing code
|
||||||
|
// still runs, and the camera frames nothing. Longitudes are not ordered,
|
||||||
|
// because a span that crosses the antimeridian runs west > east.
|
||||||
|
assert.ok(sw.lat < ne.lat, `${id} viewBounds latitudes are inverted`);
|
||||||
|
assert.notEqual(
|
||||||
|
wrapLon(sw.lng),
|
||||||
|
wrapLon(ne.lng),
|
||||||
|
`${id} viewBounds has no width`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('every POI lies inside the frame its own destination opens on', () => {
|
||||||
|
// The failure this exists for: a landmark the destination lists but its
|
||||||
|
// overview never shows.
|
||||||
|
for (const [id, city] of DESTINATIONS) {
|
||||||
|
const { southwest: sw, northeast: ne } = city.viewBounds;
|
||||||
|
for (const poi of city.pois) {
|
||||||
|
assert.ok(
|
||||||
|
poi.lat >= sw.lat &&
|
||||||
|
poi.lat <= ne.lat &&
|
||||||
|
lonWithin(poi.lon, sw.lng, ne.lng),
|
||||||
|
`${where(id, poi)} at ${poi.lat}, ${poi.lon} is outside ${id}'s viewBounds`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('every LOCATIONS row matches the destination it names', () => {
|
||||||
|
assert.equal(LOCATIONS.length, DESTINATIONS.length);
|
||||||
|
for (const [index, [id, city]] of DESTINATIONS.entries()) {
|
||||||
|
assert.deepEqual(LOCATIONS[index], {
|
||||||
|
id,
|
||||||
|
name: city.name,
|
||||||
|
lat: city.pois[0].lat,
|
||||||
|
lon: city.pois[0].lon,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('containment follows a span across the antimeridian', () => {
|
||||||
|
// No current destination crosses 180°, so the wrapped case is exercised here
|
||||||
|
// rather than left to whoever adds the first one. southwest (-1, 179) to
|
||||||
|
// northeast (1, -179) is a real 2° box straddling the line.
|
||||||
|
const west = 179;
|
||||||
|
const east = -179;
|
||||||
|
assert.equal(lonWithin(179.5, west, east), true);
|
||||||
|
assert.equal(lonWithin(-179.5, west, east), true);
|
||||||
|
assert.equal(lonWithin(180, west, east), true);
|
||||||
|
assert.equal(lonWithin(0, west, east), false);
|
||||||
|
assert.equal(lonWithin(178.9, west, east), false);
|
||||||
|
assert.equal(lonWithin(-178.9, west, east), false);
|
||||||
|
// An ordinary span is unaffected by the wrapping.
|
||||||
|
assert.equal(lonWithin(-97.7, -97.95, -97.55), true);
|
||||||
|
assert.equal(lonWithin(-98.4, -97.95, -97.55), false);
|
||||||
|
// A whole POI checked against a synthetic destination, the way the table
|
||||||
|
// check reads it.
|
||||||
|
const destination = {
|
||||||
|
viewBounds: {
|
||||||
|
southwest: { lat: -1, lng: west },
|
||||||
|
northeast: { lat: 1, lng: east },
|
||||||
|
},
|
||||||
|
pois: [{ name: 'Dateline overlook', lat: 0, lon: 179.5 }],
|
||||||
|
};
|
||||||
|
const { southwest: sw, northeast: ne } = destination.viewBounds;
|
||||||
|
for (const poi of destination.pois) {
|
||||||
|
assert.ok(
|
||||||
|
poi.lat >= sw.lat &&
|
||||||
|
poi.lat <= ne.lat &&
|
||||||
|
lonWithin(poi.lon, sw.lng, ne.lng),
|
||||||
|
'a landmark inside a wrapped view must read as inside',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in new issue
Block a user