Files
gods-eye-view/src/testSupport/streetLevelFakes.mjs
T
Said TurkseverandClaude Opus 5.5 3d4c6de21a test(street-level): harden KartaView tests (mutation score 51% → 95%)
A mutation review (91, then 130 mutants) found tests that could not fail
(quick presses with synchronous fakes, a no-flight check with a provider
that never posed), and untested paths. New and rewritten tests kill 123 of
130 mutants; the 7 survivors are equivalent or harmless redraws.

- viewer: held-back loads for quick presses, stale sharp/flat loads,
  close mid-load, ends checks for an older photo, WebGL retried per mount,
  focus handed back, look direction kept on ›.
- provider: id-ordered pages with out-of-order indexes, page and probe
  failures retried, overtaken lookups, cones re-centred on ‹ ›, leftover
  imagery filters hide no line or cone, coverage hints, tile cache.
- source: 429 backoff and Retry-After under mock timers, hang timeouts,
  the nearest query's parameters. policy: storage-host injection, FOV 360,
  longitudes past ±180. panorama: a stub WebGL context (angles, lost
  context, re-measuring). lineCoverage.test.mjs: tooHigh hint, passes().
- core: openImage({frame:false}) and openNearest leave the camera
  (with a posing provider and a default-open control), KartaView's legend
  and wording end to end; viewerHost supportsRenderModes.
- Fixed tick-count waits replaced by an until() poll.
- Browser gate fixture: odd photos archived (409 at storage2, served by
  the proxy), the first lookup rate-limited (429, Retry-After: 0); the
  line-click step asserts the camera stays and › skips the archived photo.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 20:43:27 +01:00

238 lines
6.6 KiB
JavaScript

/** Shared Street Level test stand-ins: provider snapshot, minimal provider, ray-casting camera. */
import * as Cesium from 'cesium';
import { MAPILLARY_CREDIT_HTML } from '../layers/streetLevel/providers/mapillary/policy.js';
import {
latToTileY,
lonToTileX,
tileBounds,
} from '../layers/streetLevel/tileMath.js';
const RAD = Math.PI / 180;
/** One provider as `getUIState().providers` lists it. */
export function providerSnapshot(overrides = {}) {
return {
id: 'mapillary',
name: 'Mapillary',
label: 'MAPILLARY',
on: true,
configured: true,
keyRequired: false,
requiresKeyId: 'mapillary',
loading: false,
count: 0,
hint: '',
error: null,
color: '#05cb63',
...overrides,
};
}
/**
* The smallest provider the core accepts. Mutate `stats` to change what
* coverageStats() answers; `calls`, `filters` and `context()` record use.
*/
export function fakeStreetLevelProvider({
id = 'mapillary',
pickPrefix = 'mly:',
html = MAPILLARY_CREDIT_HTML,
nearestImage = async () => null,
} = {}) {
const calls = { activate: 0, deactivate: 0, mount: 0, open: [], unmount: 0 };
const filters = [];
const stats = {
count: 0,
zoom: null,
kind: null,
loading: false,
hint: '',
error: null,
keyRequired: false,
};
let context = null;
return {
calls,
filters,
stats,
context: () => context,
id,
name: 'Mapillary',
label: 'MAPILLARY',
requiresKeyId: null,
pickPrefix,
colors: { coverage: '#05cb63' },
credit: { html },
create: (providerContext) => {
context = providerContext;
return {
status: async () => ({ configured: true }),
init() {},
activate: () => calls.activate++,
deactivate: () => calls.deactivate++,
destroy() {},
refreshCoverage() {},
setFilter: (filter) => filters.push(filter),
coverageStats: () => ({ ...stats }),
handlePick: () => false,
nearestImage,
viewer: {
mount: async () => calls.mount++,
open: async (imageId) => calls.open.push(imageId),
close() {},
unmount: () => calls.unmount++,
resize() {},
onPose: () => () => {},
},
};
},
};
}
/**
* A pinhole camera `altitude` m above WGS84, at `heading` (deg from north) and
* `pitch` (deg, negative down); `pickEllipsoid` hits whatever ellipsoid it gets.
*/
export function rayCamera({
lon,
lat,
altitude,
heading = 0,
pitch,
width = 1600,
height = 900,
fovY = 60,
fovX = null,
}) {
const position = Cesium.Cartesian3.fromDegrees(lon, lat, altitude);
const frame = Cesium.Transforms.eastNorthUpToFixedFrame(position);
const h = heading * RAD;
const p = pitch * RAD;
// East-north-up axes of the view: forward, right and up.
const forward = [
Math.sin(h) * Math.cos(p),
Math.cos(h) * Math.cos(p),
Math.sin(p),
];
const right = [Math.cos(h), -Math.sin(h), 0];
const up = [
-Math.sin(h) * Math.sin(p),
-Math.cos(h) * Math.sin(p),
Math.cos(p),
];
const halfY = Math.tan((fovY / 2) * RAD);
const halfX =
fovX == null ? halfY * (width / height) : Math.tan((fovX / 2) * RAD);
return {
positionWC: position,
positionCartographic: Cesium.Cartographic.fromDegrees(lon, lat, altitude),
pickEllipsoid(point, ellipsoid = Cesium.Ellipsoid.WGS84) {
const sx = ((2 * point.x) / width - 1) * halfX;
const sy = (1 - (2 * point.y) / height) * halfY;
const local = [0, 1, 2].map(
(i) => forward[i] + sx * right[i] + sy * up[i],
);
const direction = Cesium.Cartesian3.normalize(
Cesium.Matrix4.multiplyByPointAsVector(
frame,
new Cesium.Cartesian3(...local),
new Cesium.Cartesian3(),
),
new Cesium.Cartesian3(),
);
const ray = new Cesium.Ray(position, direction);
const hit = Cesium.IntersectionTests.rayEllipsoid(ray, ellipsoid);
return hit ? Cesium.Ray.getPoint(ray, hit.start) : undefined;
},
computeViewRectangle: () => undefined,
};
}
/** The centre of the tile holding a point at zoom `z`, so a small view is one tile. */
export function tileCentre(lon, lat, z = 14) {
const x = lonToTileX(lon, z);
const y = latToTileY(lat, z);
const { west, east, south, north } = tileBounds(x, y, z);
return {
tile: { x, y, z },
lon: (west + east) / 2,
lat: (south + north) / 2,
};
}
/** A scene collection that remembers what is on the globe. */
function sceneCollection() {
const items = new Set();
return {
items,
add(primitive) {
items.add(primitive);
return primitive;
},
remove(primitive) {
return items.delete(primitive);
},
};
}
/**
* A viewer looking straight down on a 0.004° square around `view`
* (`{lon, lat, height}`, mutable), enough for line coverage to attach,
* refresh and draw. Cesium's one-time terrain-height download is skipped.
*/
export function coverageViewer(view, t) {
const initialize = Cesium.GroundPolylinePrimitive.initializeTerrainHeights;
Cesium.GroundPolylinePrimitive.initializeTerrainHeights = () =>
Promise.resolve();
t?.after(() => {
Cesium.GroundPolylinePrimitive.initializeTerrainHeights = initialize;
});
// Node has no WebGL context to report line-width limits.
Cesium.ContextLimits._minimumAliasedLineWidth = 1;
Cesium.ContextLimits._maximumAliasedLineWidth = 10;
const listeners = () => {
const set = new Set();
return {
set,
addEventListener(listener) {
set.add(listener);
return () => set.delete(listener);
},
};
};
return {
view,
scene: {
canvas: { clientWidth: 100, clientHeight: 100 },
globe: { show: false, ellipsoid: Cesium.Ellipsoid.WGS84 },
frameState: { mode: Cesium.SceneMode.SCENE3D },
updateHeight: () => () => {},
getHeight: () => undefined,
groundPrimitives: sceneCollection(),
primitives: sceneCollection(),
postRender: listeners(),
preRender: listeners(),
},
camera: {
changed: new Cesium.Event(),
moveEnd: new Cesium.Event(),
get positionWC() {
return Cesium.Cartesian3.fromDegrees(view.lon, view.lat, view.height);
},
get positionCartographic() {
return {
longitude: view.lon * RAD,
latitude: view.lat * RAD,
height: view.height,
};
},
pickEllipsoid(point) {
return Cesium.Cartesian3.fromDegrees(
view.lon - 0.002 + (point.x / 100) * 0.004,
view.lat + 0.002 - (point.y / 100) * 0.004,
);
},
computeViewRectangle: () => undefined,
},
};
}