Files
gods-eye-view/src/data/rocketLaunches.test.mjs
T

1503 lines
57 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import * as Cesium from 'cesium';
import { gstime } from 'satellite.js';
import { createModel } from '../layers/launches/model.js';
import rocketLaunchesLayer, {
_setRocketMissionOverlayHostForTest,
_setSelectedRocketMissionForTest,
approximateOrbitPath,
buildMissionPaths,
cameraHeadingForPath,
compactLaunchSiteName,
createMissionRosterPreviewOwnership,
createRocketMissionElementOverlayEntry,
createRocketMissionMarkerOverlayEntry,
formatMissionEventTime,
LAUNCH_PAD_ZONE_RADIUS_M,
launchPadZoneVisible,
launchStatusAllowsOrbit,
missionAnchorHorizonVisible,
missionAnchorVisible,
missionHoverPreviewRange,
missionDataCompleteness,
missionMarkerColor,
missionPathPresentation,
missionRosterEntries,
missionZoomPitch,
orbitProgressAtTime,
normalizeReplaySpeed,
normalizeRocketLaunches,
parseMissionDurationSeconds,
replayState,
replayCameraView,
replayChaseCameraHeading,
replayOrbitFrameSphere,
replayOrbitGlobeAnchor,
replayOrbitCameraTarget,
replayOrbitCameraPose,
replayOrbitGlobeRange,
replayOverlayMode,
replayInitialCameraHeading,
smoothReplayWindowPosition,
replayAscentDurationSeconds,
replayStartAfterPause,
replayVehicleScreenRotation,
resolveMissionRosterPreviewLaunch,
releaseAircraftTracking,
ROCKET_MISSION_AMBIENT_OVERLAY_COHORT_LIMIT,
ROCKET_MISSION_AMBIENT_OVERLAY_COLLISION_CAPACITY,
ROCKET_MISSION_AMBIENT_OVERLAY_SOURCE_ID,
ROCKET_MISSION_SELECTED_OVERLAY_SOURCE_ID,
ROCKET_MISSION_SELECTED_OVERLAY_SOURCE_OPTIONS,
samplePath,
satelliteParamsAfterSpaceMissions,
satelliteParamsForSpaceMissions,
shouldRetryAfterActiveTle,
selectRocketMissionMarkerOverlayCohort,
smoothReplayCameraHeading,
} from './rocketLaunches.js';
import {
findSatelliteOrbitTrackInTle,
orbitFrameModelMatrix,
satelliteCatalogModeChanged,
scoreSatelliteNameMatch,
} from './satellites.js';
const NOW = new Date('2026-07-27T00:00:00Z');
test('mission anchors are hidden behind Earth and restored on the facing hemisphere', () => {
const camera = Cesium.Cartesian3.fromDegrees(-75, 20, 18000000);
const front = Cesium.Cartesian3.fromDegrees(-75, 20);
const rear = Cesium.Cartesian3.fromDegrees(105, -20);
assert.equal(missionAnchorHorizonVisible(camera, front), true);
assert.equal(missionAnchorHorizonVisible(camera, rear), false);
});
test('selecting one mission hides every other front-facing launch anchor', () => {
const camera = Cesium.Cartesian3.fromDegrees(-75, 20, 18000000);
const first = Cesium.Cartesian3.fromDegrees(-75, 20);
const second = Cesium.Cartesian3.fromDegrees(-80, 25);
assert.equal(missionAnchorVisible(camera, first, 'first', null), true);
assert.equal(missionAnchorVisible(camera, second, 'second', null), true);
assert.equal(missionAnchorVisible(camera, first, 'first', 'first'), true);
assert.equal(missionAnchorVisible(camera, second, 'second', 'first'), false);
});
test('advances the live orbit marker between whole seconds', () => {
assert.equal(orbitProgressAtTime(10_000, 100), 0.1);
assert.equal(orbitProgressAtTime(10_250, 100), 0.1025);
assert.ok(orbitProgressAtTime(10_250, 100) > orbitProgressAtTime(10_000, 100));
});
test('samples replay paths uniformly by distance rather than vertex count', () => {
const path = [
new Cesium.Cartesian3(0, 0, 0),
new Cesium.Cartesian3(1, 0, 0),
new Cesium.Cartesian3(101, 0, 0),
];
assert.ok(Math.abs(samplePath(path, 0.5).x - 50.5) < 1e-9);
assert.ok(Math.abs(samplePath(path, 0.75).x - 75.75) < 1e-9);
});
test('transitions selected mission zoom from globe nadir to an oblique site view', () => {
assert.ok(Math.abs(missionZoomPitch(5000000) - Cesium.Math.toRadians(-90)) < 1e-10);
assert.ok(Math.abs(missionZoomPitch(180000) - Cesium.Math.toRadians(-42)) < 1e-10);
const midPitch = Cesium.Math.toDegrees(missionZoomPitch(1000000));
assert.ok(midPitch < -42 && midPitch > -90);
});
test('pulls replay camera from close ascent tracking into a globe-scale orbit view', () => {
const ascent = replayCameraView({ ascending: true, phaseProgress: 0.9 }, 120000);
const contextualAscent = replayCameraView({ ascending: true, phaseProgress: 0.9 }, 420000);
const orbitStart = replayCameraView({ ascending: false, phaseProgress: 0 }, 550000);
const orbitMidPullback = replayCameraView({ ascending: false, phaseProgress: 0.1 }, 550000);
const orbitGlobe = replayCameraView({ ascending: false, phaseProgress: 0.2 }, 550000);
assert.ok(ascent.pitch < Cesium.Math.toRadians(-20));
assert.ok(ascent.pitch > Cesium.Math.toRadians(-34));
assert.ok(contextualAscent.range > 1000000);
assert.equal(contextualAscent.pitch, Cesium.Math.toRadians(-34));
assert.equal(orbitStart.pitch, Cesium.Math.toRadians(-34));
assert.ok(orbitMidPullback.range > orbitStart.range);
assert.ok(orbitMidPullback.pitch < orbitStart.pitch);
assert.equal(orbitGlobe.range, 18000000);
assert.equal(orbitGlobe.pitch, Cesium.Math.toRadians(-45));
});
test('limits replay camera yaw through heading wraps', () => {
const previous = Cesium.Math.toRadians(359);
const desired = Cesium.Math.toRadians(181);
const next = smoothReplayCameraHeading(previous, desired);
assert.ok(Math.abs(Cesium.Math.negativePiToPi(next - previous)) <= Cesium.Math.toRadians(2));
const wrapped = smoothReplayCameraHeading(
Cesium.Math.toRadians(359),
Cesium.Math.toRadians(1),
);
assert.ok(Cesium.Math.negativePiToPi(wrapped - previous) > 0);
});
test('starts replay camera broadside to the ascent path', () => {
const path = [
Cesium.Cartesian3.fromDegrees(-120, 34, 0),
Cesium.Cartesian3.fromDegrees(-120, 35, 1000),
];
const chaseHeading = cameraHeadingForPath(path, 0);
const heading = replayInitialCameraHeading(path);
assert.ok(Math.abs(Cesium.Math.negativePiToPi(chaseHeading)) < 0.02);
assert.ok(Math.abs(Cesium.Math.negativePiToPi(heading - Cesium.Math.toRadians(90))) < 0.02);
});
test('keeps the chase camera in a rear-quarter view of the forward path', () => {
assert.ok(Math.abs(
Cesium.Math.negativePiToPi(replayChaseCameraHeading(0, 0) - Cesium.Math.toRadians(30)),
) < 1e-10);
assert.ok(Math.abs(
Cesium.Math.negativePiToPi(replayChaseCameraHeading(0, 1) - Cesium.Math.toRadians(45)),
) < 1e-10);
});
test('centers orbit follow on a globe-side anchor while retaining vehicle clearance', () => {
const position = Cesium.Cartesian3.fromDegrees(20, 10, 550000);
const anchor = replayOrbitGlobeAnchor(position, 1);
const anchorHeight = Cesium.Ellipsoid.WGS84.cartesianToCartographic(anchor).height;
assert.ok(Math.abs(anchorHeight - 55000) < 1);
const target = replayOrbitCameraTarget(anchor, Cesium.Cartesian3.ZERO, 1);
assert.ok(Cesium.Cartesian3.magnitude(target) > Cesium.Ellipsoid.WGS84.maximumRadius * 0.3);
assert.ok(Cesium.Cartesian3.distance(target, anchor) > 0);
assert.ok(Cesium.Cartesian3.distance(target, anchor) < Cesium.Cartesian3.magnitude(anchor));
assert.equal(replayOrbitGlobeRange(18000000, 550000, 1), 18000000);
assert.ok(replayOrbitGlobeRange(18000000, 35786000, 1) > 50000000);
});
test('keeps the forward orbit tangent moving toward screen-left', () => {
const earthRadius = Cesium.Ellipsoid.WGS84.maximumRadius;
const position = new Cesium.Cartesian3(earthRadius + 550000, 0, 0);
const tangentPosition = new Cesium.Cartesian3(earthRadius + 550000, 10000, 0);
const target = new Cesium.Cartesian3(earthRadius * 0.35, 0, 0);
const pose = replayOrbitCameraPose(
position,
tangentPosition,
target,
18000000,
Cesium.Math.toRadians(-45),
);
assert.ok(pose);
const screenRight = Cesium.Cartesian3.normalize(
Cesium.Cartesian3.cross(pose.direction, pose.up, new Cesium.Cartesian3()),
new Cesium.Cartesian3(),
);
const tangent = Cesium.Cartesian3.normalize(
Cesium.Cartesian3.subtract(
tangentPosition,
position,
new Cesium.Cartesian3(),
),
new Cesium.Cartesian3(),
);
assert.ok(Cesium.Cartesian3.dot(screenRight, tangent) < -0.999);
assert.ok(Math.abs(
Cesium.Cartesian3.distance(pose.destination, target) - 18000000,
) < 1e-5);
});
test('frames the complete high-apogee orbit together with Earth', () => {
const earthRadius = Cesium.Ellipsoid.WGS84.maximumRadius;
const orbit = [
new Cesium.Cartesian3(earthRadius + 300000, 0, 0),
new Cesium.Cartesian3(0, earthRadius + 35786000, 0),
new Cesium.Cartesian3(-(earthRadius + 300000), 0, 0),
new Cesium.Cartesian3(0, -(earthRadius + 35786000), 0),
];
const frame = replayOrbitFrameSphere(orbit);
const range = replayOrbitGlobeRange(18000000, 300000, 1, frame.radius);
assert.ok(frame.radius > earthRadius + 30000000);
assert.ok(range >= frame.radius * 3);
});
test('holds the completed replay on its final orbit frame instead of wrapping to launch', () => {
const launch = { launchTime: '2026-07-01T00:00:00Z', timeline: [] };
const completed = replayState(
launch,
0,
12,
28,
5400,
1,
40000,
0,
false,
);
assert.equal(completed.ascending, false);
assert.ok(completed.phaseProgress > 0.999);
});
test('smooths small replay marker reprojection jitter but snaps camera jumps', () => {
const smoothed = smoothReplayWindowPosition({ x: 100, y: 100 }, { x: 104, y: 103 });
assert.ok(smoothed.x > 100 && smoothed.x < 104);
assert.deepEqual(
smoothReplayWindowPosition({ x: 100, y: 100 }, { x: 140, y: 100 }),
{ x: 140, y: 100 },
);
});
test('shows the screen-space rocket only while replay is active', () => {
assert.equal(replayOverlayMode({
replayActive: false,
closeSelected: false,
ascending: true,
countdownActive: false,
}), null);
assert.equal(replayOverlayMode({
replayActive: false,
closeSelected: true,
ascending: false,
countdownActive: false,
}), null);
assert.equal(replayOverlayMode({
replayActive: true,
closeSelected: false,
ascending: true,
countdownActive: true,
}), 'countdown');
assert.equal(replayOverlayMode({
replayActive: true,
closeSelected: false,
ascending: true,
countdownActive: false,
}), 'ascent');
assert.equal(replayOverlayMode({
replayActive: true,
closeSelected: false,
ascending: false,
countdownActive: false,
}), 'orbit');
});
test('aligns the replay rocket nose to the projected path tangent', () => {
assert.equal(replayVehicleScreenRotation({ x: 10, y: 10 }, { x: 10, y: 0 }), 0);
assert.ok(Math.abs(
replayVehicleScreenRotation({ x: 10, y: 10 }, { x: 20, y: 10 })
- Math.PI / 2,
) < 1e-10);
assert.ok(Math.abs(
replayVehicleScreenRotation({ x: 10, y: 10 }, { x: 0, y: 10 })
+ Math.PI / 2,
) < 1e-10);
});
test('clamps and snaps ascent replay speed to supported quarter steps', () => {
assert.equal(normalizeReplaySpeed(0.1), 0.25);
assert.equal(normalizeReplaySpeed(0.62), 0.5);
assert.equal(normalizeReplaySpeed('1.75'), 1.75);
assert.equal(normalizeReplaySpeed(8), 4);
assert.equal(normalizeReplaySpeed('invalid'), 1);
});
test('shifts replay start time so pause duration does not advance the mission', () => {
assert.equal(replayStartAfterPause(1000, 4000, 9500), 6500);
assert.equal(replayStartAfterPause(1000, 9500, 4000), 1000);
assert.equal(replayStartAfterPause(1000, Number.NaN, 9500), 1000);
});
test('restores the complete standalone Satellite style after mission mode', () => {
const standalone = {
catalog: 'core',
showPoints: true,
showOrbits: true,
labelDensity: 'operator',
};
assert.deepEqual(satelliteParamsForSpaceMissions(standalone), {
...standalone,
catalog: 'dense',
showPoints: false,
showOrbits: false,
});
assert.deepEqual(satelliteParamsAfterSpaceMissions(standalone), standalone);
assert.deepEqual(satelliteParamsAfterSpaceMissions(null), {
catalog: 'core',
showPoints: true,
showOrbits: true,
});
});
test('suppresses every orbital representation for failed launches', () => {
assert.equal(launchStatusAllowsOrbit('Launch Successful'), true);
assert.equal(launchStatusAllowsOrbit('Go for Launch'), true);
assert.equal(launchStatusAllowsOrbit('Launch Failure'), false);
assert.equal(launchStatusAllowsOrbit('Partial Failure'), false);
assert.equal(launchStatusAllowsOrbit('Failed'), false);
});
test('keeps failed-launch mission details truthful without inventing paths', () => {
assert.deepEqual(missionPathPresentation({
status: 'Launch Failure',
orbit: { name: 'Geostationary Transfer Orbit' },
trajectory: [],
}, false), {
orbit: 'PLANNED · Geostationary Transfer Orbit',
ascent: 'UNAVAILABLE',
replayAvailable: false,
});
assert.deepEqual(missionPathPresentation({
status: 'Partial Failure',
orbit: { name: 'Low Earth Orbit' },
trajectory: [
{ latitude: 28.5, longitude: -80.5 },
{ latitude: 29, longitude: -79.5 },
],
}, false), {
orbit: 'PLANNED · Low Earth Orbit',
ascent: 'SUPPLIED TRAJECTORY POINTS',
replayAvailable: false,
});
});
test('describes only renderable successful mission paths', () => {
assert.deepEqual(missionPathPresentation({
status: 'Launch Successful',
orbit: { name: 'Low Earth Orbit' },
trajectory: [],
}, true), {
orbit: 'Low Earth Orbit',
ascent: 'RECONSTRUCTED ESTIMATE',
replayAvailable: true,
});
assert.deepEqual(missionPathPresentation({
status: 'Launch Successful',
orbit: null,
trajectory: [{ latitude: 28.5, longitude: -80.5 }],
}, false), {
orbit: null,
ascent: 'UNAVAILABLE',
replayAvailable: false,
});
});
test('bounds the post-TLE refresh to one resolved-catalog rebuild', () => {
const base = {
enabled: true,
retryCount: 0,
activeTleText: 'ACTIVE TLE',
renderedTleText: null,
};
assert.equal(shouldRetryAfterActiveTle(base), true);
assert.equal(shouldRetryAfterActiveTle({ ...base, retryCount: 1 }), false);
assert.equal(shouldRetryAfterActiveTle({ ...base, activeTleText: null }), false);
assert.equal(shouldRetryAfterActiveTle({ ...base, renderedTleText: 'ACTIVE TLE' }), false);
assert.equal(shouldRetryAfterActiveTle({ ...base, enabled: false }), false);
});
test('replay releases aircraft tracking through both owning layer APIs', () => {
const calls = [];
const dataManager = {
layers: new Map([
['flights', { module: { stopTracking: () => calls.push('flights') } }],
['military', { module: { stopTracking: () => calls.push('military') } }],
['satellites', { module: { stopTracking: () => calls.push('satellites') } }],
]),
};
assert.equal(releaseAircraftTracking(dataManager), 2);
assert.deepEqual(calls, ['flights', 'military']);
});
test('uses the core GMST frame transform for mission orbit primitives', () => {
const bakeDate = new Date('2026-07-20T10:00:00Z');
const nowDate = new Date('2026-07-20T10:10:00Z');
const gmstAtBake = gstime(bakeDate);
const matrix = orbitFrameModelMatrix(gmstAtBake, nowDate);
const actual = Cesium.Matrix4.multiplyByPoint(
matrix,
Cesium.Cartesian3.UNIT_X,
new Cesium.Cartesian3(),
);
const expectedAngle = -(gstime(nowDate) - gmstAtBake);
assert.ok(Math.abs(actual.x - Math.cos(expectedAngle)) < 1e-12);
assert.ok(Math.abs(actual.y - Math.sin(expectedAngle)) < 1e-12);
assert.ok(Math.abs(actual.z) < 1e-12);
});
test('treats an already-active Satellite catalog mode as idempotent', () => {
assert.equal(satelliteCatalogModeChanged('dense', 'dense'), false);
assert.equal(satelliteCatalogModeChanged('core', 'core'), false);
assert.equal(satelliteCatalogModeChanged('core', 'dense'), true);
assert.equal(satelliteCatalogModeChanged('dense', 'core'), true);
assert.equal(satelliteCatalogModeChanged('core', undefined), false);
});
test('holds replay at the pad for a real-time countdown independent of replay speed', () => {
const launch = { launchTime: '2026-07-20T10:00:00Z', timeline: [] };
const tilePreparation = replayState(launch, 115000, 12, 28, 5400, 4, 100000, 5);
assert.equal(tilePreparation.preCountdownActive, true);
assert.equal(tilePreparation.countdownActive, false);
const countdown = replayState(launch, 110000, 12, 28, 5400, 4, 100000);
assert.equal(countdown.countdownActive, true);
assert.equal(countdown.countdownSeconds, 10);
assert.equal(countdown.elapsedSinceStart, 0);
assert.equal(countdown.phaseProgress, 0);
const liftoff = replayState(launch, 110000, 12, 28, 5400, 4, 110500);
assert.equal(liftoff.countdownActive, false);
assert.equal(liftoff.countdownSeconds, 0);
assert.equal(liftoff.elapsedSinceStart, 2);
assert.ok(liftoff.phaseProgress > 0);
});
test('runs the five-second tile hold before a full T-10 countdown', () => {
const launch = { launchTime: '2026-07-20T10:00:00Z', timeline: [] };
// REPLAY ASCENT puts liftoff 5 s (tile hold) + 10 s (countdown) ahead.
const hold = replayState(launch, 115000, 12, 28, 5400, 1, 104500, 5);
assert.equal(hold.preCountdownActive, true);
assert.equal(hold.countdownActive, false);
const countdown = replayState(launch, 115000, 12, 28, 5400, 1, 105000, 5);
assert.equal(countdown.preCountdownActive, false);
assert.equal(countdown.countdownActive, true);
assert.equal(countdown.countdownSeconds, 10);
});
test('hands replay directly from ascent into orbit', () => {
const launch = { launchTime: '2026-07-20T10:00:00Z', timeline: [] };
const orbit = replayState(launch, 0, 12, 28, 5400, 1, 16_000);
assert.equal(orbit.ascending, false);
assert.equal(orbit.phaseProgress, 4 / 28);
});
test('shows launch-pad zone only for the selected close-range mission', () => {
assert.equal(LAUNCH_PAD_ZONE_RADIUS_M, 500);
const closeSelected = {
layerActive: true,
selectedLaunchId: 'mission-a',
launchId: 'mission-a',
cameraHeightM: 12000,
cameraDistanceM: 14000,
};
assert.equal(launchPadZoneVisible(closeSelected), true);
assert.equal(launchPadZoneVisible({ ...closeSelected, layerActive: false }), false);
assert.equal(launchPadZoneVisible({ ...closeSelected, selectedLaunchId: 'mission-b' }), false);
assert.equal(launchPadZoneVisible({ ...closeSelected, cameraHeightM: 120001 }), false);
assert.equal(launchPadZoneVisible({ ...closeSelected, cameraDistanceM: 180001 }), false);
});
test('orders mission roster newest-first without losing navigation indices', () => {
const entries = missionRosterEntries([
{ id: 'oldest', launchTime: '2026-07-01T00:00:00Z' },
{ id: 'newest', launchTime: '2026-07-20T00:00:00Z' },
{ id: 'middle', launchTime: '2026-07-10T00:00:00Z' },
]);
assert.deepEqual(entries.map((entry) => entry.launch.id), ['newest', 'middle', 'oldest']);
assert.deepEqual(entries.map((entry) => entry.index), [1, 2, 0]);
});
test('prioritizes data-rich missions before newer sparse records', () => {
const entries = missionRosterEntries([
{ id: 'rich', launchTime: '2026-07-01T00:00:00Z', provider: 'Agency', mission: 'Detailed mission', orbit: { name: 'LEO' }, payloads: [{ name: 'Payload' }], timeline: [{ name: 'Liftoff' }] },
{ id: 'sparse-new', launchTime: '2026-07-20T00:00:00Z' },
]);
assert.ok(missionDataCompleteness(entries[0].launch) > missionDataCompleteness(entries[1].launch));
assert.equal(entries[0].launch.id, 'rich');
});
test('preserves globe scale for roster hover previews', () => {
assert.equal(missionHoverPreviewRange(18178265), 18178265);
assert.equal(missionHoverPreviewRange(12000), 180000);
assert.equal(missionHoverPreviewRange(Number.NaN), 5000000);
});
test('keyboard focus previews roster missions and clears without selecting them', () => {
const calls = [];
const ownership = createMissionRosterPreviewOwnership({
preview: (index) => calls.push(['preview', index]),
clear: () => calls.push(['clear']),
});
ownership.focus(2);
ownership.blur(3);
ownership.blur();
assert.deepEqual(calls, [
['preview', 2],
['preview', 3],
['clear'],
]);
});
test('leaving a hovered row for an internal roster gap restores keyboard preview', () => {
const calls = [];
const ownership = createMissionRosterPreviewOwnership({
preview: (index) => calls.push(['preview', index]),
clear: () => calls.push(['clear']),
});
ownership.focus(1);
ownership.pointerEnter(0);
ownership.pointerLeave();
assert.deepEqual(calls, [
['preview', 1],
['preview', 0],
['preview', 1],
]);
});
test('pending mission preview resolves the current refreshed record by identity', () => {
const stale = { id: 'mission-a', name: 'Stale' };
const current = { id: 'mission-a', name: 'Current' };
assert.equal(resolveMissionRosterPreviewLaunch([current], stale.id), current);
assert.equal(resolveMissionRosterPreviewLaunch([], stale.id), null);
});
test('latest roster input owns preview and restores the remaining owner on leave', () => {
const calls = [];
const ownership = createMissionRosterPreviewOwnership({
preview: (index) => calls.push(['preview', index]),
clear: () => calls.push(['clear']),
});
ownership.pointerEnter(0);
ownership.focus(1);
ownership.blur();
ownership.pointerLeave();
assert.deepEqual(calls, [
['preview', 0],
['preview', 1],
['preview', 0],
['clear'],
]);
});
test('assigns distinct stable colors to mission operators', () => {
assert.equal(missionMarkerColor({ provider: 'NASA', name: 'Science Flight' }).toCssColorString(), 'rgb(255,159,67)');
assert.equal(missionMarkerColor({ provider: 'SpaceX', name: 'Starlink Group' }).toCssColorString(), 'rgb(76,201,240)');
assert.equal(missionMarkerColor({ provider: 'Private Launch Co.', name: 'Test Flight' }).toCssColorString(), 'rgb(192,132,252)');
});
test('normalizes recent launches and preserves supplied trajectory/orbit data', () => {
const launches = normalizeRocketLaunches({ results: [{
id: 'recent-1',
name: 'Test Flight',
net: '2026-07-20T10:00:00Z',
status: { name: 'Launch Successful' },
pad: { name: 'Pad A', location: { name: 'Test Range', coordinates: '12.5,45.5' } },
trajectory: [{ latitude: 45.5, longitude: 12.5, altitude: 0 }],
timeline: [{ type: { abbrev: 'SECO-1' }, relative_time: 'PT8M40S' }],
mission: { description: 'Payload test', orbit: { name: 'LEO' } },
}] }, NOW);
assert.equal(launches.length, 1);
assert.equal(launches[0].lat, 45.5);
assert.equal(launches[0].lon, 12.5);
assert.equal(launches[0].orbit.name, 'LEO');
assert.equal(launches[0].trajectory.length, 1);
assert.equal(launches[0].timeline[0].offsetSeconds, 520);
});
test('uses Launch Library 2 pad latitude/longitude fields', () => {
const launches = normalizeRocketLaunches({ results: [{
id: 'pad-fields',
net: '2026-07-20T10:00:00Z',
pad: { latitude: '34.632', longitude: '-120.611', location: { name: 'Vandenberg' } },
}] }, NOW);
assert.equal(launches.length, 1);
assert.equal(launches[0].lat, 34.632);
assert.equal(launches[0].lon, -120.611);
});
test('null or blank pad coordinates stay unknown instead of becoming 0', () => {
const base = { net: '2026-07-20T10:00:00Z' };
for (const blank of [null, '', ' ', false]) {
assert.deepEqual(
normalizeRocketLaunches([{ ...base, id: 'blank', pad: { latitude: blank, longitude: blank } }], NOW),
[],
);
}
});
test('null pad coordinates use the location fallback instead of 0,0', () => {
const base = { net: '2026-07-20T10:00:00Z' };
const [fallback] = normalizeRocketLaunches([{ ...base, id: 'fallback',
pad: { latitude: null, longitude: null, location: { coordinates: '-80.6,28.6' } } }], NOW);
assert.equal(fallback.lat, 28.6);
assert.equal(fallback.lon, -80.6);
});
test('an undisclosed landing keeps top-level position and stage numbers unknown', () => {
const base = { net: '2026-07-20T10:00:00Z' };
const [launch] = normalizeRocketLaunches([{ ...base, id: 'landing', pad: { latitude: 1, longitude: 2 },
rocket: { launcher_stage: [{ launcher_flight_number: null,
landing: { attempt: true, latitude: null, longitude: null, downrange_distance: null,
landing_location: { latitude: null, longitude: null } } }] } }], NOW);
const [stage] = launch.recoveryStages;
assert.equal(stage.lat, null);
assert.equal(stage.lon, null);
assert.equal(stage.downrangeKm, null);
assert.equal(stage.flightNumber, null);
});
test('normalizes detailed payload and stage recovery records', () => {
const launches = normalizeRocketLaunches({ results: [{
id: 'recovery-details',
name: 'Falcon 9 | Test Payload',
net: '2026-07-20T10:00:00Z',
pad: { latitude: '28.608', longitude: '-80.604', name: 'LC-39A' },
rocket: {
payloads: [{
id: 501,
destination: 'Low Earth Orbit',
amount: 2,
payload: {
id: 91,
name: 'TestSat',
type: { name: 'Earth Observation Satellite' },
manufacturer: { name: 'Example Space' },
operator: { name: 'Example Operator' },
mass: 420,
},
}],
launcher_stage: [{
id: 77,
type: 'Core',
reused: true,
launcher_flight_number: 8,
launcher: { serial_number: 'B1099' },
landing: {
attempt: true,
success: true,
downrange_distance: 610,
type: { name: 'Autonomous Spaceport Drone Ship' },
landing_location: {
name: 'A Shortfall of Gravitas',
latitude: '30.1',
longitude: '-76.2',
},
},
}],
},
mission: { orbit: { name: 'Low Earth Orbit' } },
}] }, NOW);
assert.equal(launches[0].payloads[0].name, 'TestSat');
assert.equal(launches[0].payloads[0].amount, 2);
assert.equal(launches[0].payloads[0].massKg, 420);
assert.equal(launches[0].recoveryStages[0].name, 'Core · B1099');
assert.equal(launches[0].recoveryStages[0].status, 'RECOVERED');
assert.equal(launches[0].recoveryStages[0].downrangeKm, 610);
assert.equal(launches[0].recoveryStages[0].lat, 30.1);
assert.equal(launches[0].recoveryStages[0].lon, -76.2);
});
test('finiteCoordinate normalizes finite numbers and rejects non-numeric or non-finite inputs', () => {
const model = createModel({});
assert.equal(model.finiteCoordinate(42.5), 42.5);
assert.equal(model.finiteCoordinate('28.5619'), 28.5619);
assert.equal(model.finiteCoordinate('-80.5772'), -80.5772);
assert.equal(model.finiteCoordinate(0), 0);
assert.equal(model.finiteCoordinate('0'), 0);
assert.equal(model.finiteCoordinate(undefined), null);
assert.equal(model.finiteCoordinate('invalid'), null);
assert.equal(model.finiteCoordinate('TBD'), null);
assert.equal(model.finiteCoordinate(Number.NaN), null);
assert.equal(model.finiteCoordinate(Number.POSITIVE_INFINITY), null);
assert.equal(model.finiteCoordinate(Number.NEGATIVE_INFINITY), null);
});
test('normalizes recovery stage coordinates, downrange distance, and flight numbers with non-finite fallbacks', () => {
const launches = normalizeRocketLaunches(
{
results: [
{
id: 'recovery-fallbacks',
name: 'Falcon 9 | Edge Case Stage Normalization',
net: '2026-07-20T10:00:00Z',
pad: { latitude: '28.608', longitude: '-80.604' },
rocket: {
launcher_stage: [
{
id: 88,
type: { name: 'Booster' },
launcher_flight_number: 'invalid-flight',
landing: {
attempt: true,
success: false,
downrange_distance: 'TBD',
landing_location: {
name: 'Ocean Impact',
latitude: 'N/A',
longitude: undefined,
},
},
},
{
id: 89,
type: { name: 'Expended Stage' },
landing: {
attempt: false,
type: { name: 'Expendable' },
},
},
],
},
},
],
},
NOW,
);
assert.equal(launches[0].recoveryStages.length, 2);
const booster = launches[0].recoveryStages[0];
assert.equal(booster.status, 'LOST');
assert.equal(booster.success, false);
assert.equal(booster.attempted, true);
assert.equal(booster.flightNumber, null);
assert.equal(booster.downrangeKm, null);
assert.equal(booster.lat, null);
assert.equal(booster.lon, null);
const expendable = launches[0].recoveryStages[1];
assert.equal(expendable.status, 'EXPENDABLE');
assert.equal(expendable.attempted, false);
assert.equal(expendable.success, null);
});
test('normalizes payload amounts and fractional string masses', () => {
const launches = normalizeRocketLaunches(
{
results: [
{
id: 'payload-edge-cases',
name: 'Electron | Mixed Payloads',
net: '2026-07-20T10:00:00Z',
pad: { latitude: '-39.26', longitude: '177.86' },
rocket: {
payloads: [
{
id: 601,
amount: 'not-a-number',
payload: { id: 11, name: 'Unknown Amount Payload' },
},
{
id: 602,
amount: 3,
payload: { id: 12, name: 'Triple Payload' },
},
{
id: 603,
payload: {
id: 13,
name: 'Valid Fractional Mass Payload',
mass: '12.75',
},
},
],
},
},
],
},
NOW,
);
// Blank, negative and non-numeric masses are pinned by "missing payload
// details stay unknown without inventing mass or classification".
const payloads = launches[0].payloads;
assert.equal(payloads.length, 3);
assert.equal(payloads[0].amount, 1);
assert.equal(payloads[1].amount, 3);
assert.equal(payloads[2].massKg, 12.75);
});
test('keeps payload and recovery collections empty when LL2 does not disclose them', () => {
const launches = normalizeRocketLaunches({ results: [{
id: 'undisclosed',
net: '2026-07-20T10:00:00Z',
pad: { latitude: '28.608', longitude: '-80.604' },
rocket: {},
}] }, NOW);
assert.deepEqual(launches[0].payloads, []);
assert.deepEqual(launches[0].recoveryStages, []);
});
test('excludes launches outside the rolling 30-day window and missing coordinates', () => {
const launches = normalizeRocketLaunches({ results: [
{ id: 'old', net: '2026-06-26T00:00:00Z', pad: { location: { coordinates: '1,1' } } },
{ id: 'no-coordinates', net: '2026-07-20T00:00:00Z', pad: { location: {} } },
{ id: 'future', net: '2026-07-28T00:00:00Z', pad: { location: { coordinates: '1,1' } } },
] }, NOW);
assert.deepEqual(launches, []);
});
test('accepts an array payload for proxy and fixture flexibility', () => {
const launches = normalizeRocketLaunches([
{ id: 'array-1', net: '2026-07-01T00:00:00Z', pad: { location: { coordinates: '2,3' } } },
], NOW);
assert.equal(launches[0].id, 'array-1');
});
test('builds a surface-safe ascent that meets the orbit without a phase jump', () => {
const launch = Cesium.Cartesian3.fromDegrees(-120.61, 34.63, 0);
const orbit = Array.from({ length: 37 }, (_, index) => {
const longitude = -180 + index * 10;
const latitude = Math.sin(Cesium.Math.toRadians(longitude)) * 35;
return Cesium.Cartesian3.fromDegrees(longitude, latitude, 550000);
});
const paths = buildMissionPaths(launch, [], orbit);
assert.ok(paths.ascentPath.length > 2);
assert.equal(paths.ascentPath.at(-1), orbit[paths.insertionIndex]);
assert.equal(paths.animatedOrbitPath[0], orbit[paths.insertionIndex]);
assert.equal(paths.animatedOrbitPath.at(-1), paths.animatedOrbitPath[0]);
const launchCartographic = Cesium.Cartographic.fromCartesian(paths.ascentPath[0]);
const verticalCartographic = Cesium.Cartographic.fromCartesian(paths.ascentPath[12]);
const earlyHorizontalDistance = new Cesium.EllipsoidGeodesic(
launchCartographic,
verticalCartographic,
).surfaceDistance;
assert.ok(earlyHorizontalDistance < 2000);
assert.ok(verticalCartographic.height > launchCartographic.height + 10000);
const maximumTurn = Math.max(...paths.ascentPath.slice(1, -13).map((position, index) => {
const incoming = Cesium.Cartesian3.normalize(
Cesium.Cartesian3.subtract(position, paths.ascentPath[index], new Cesium.Cartesian3()),
new Cesium.Cartesian3(),
);
const outgoing = Cesium.Cartesian3.normalize(
Cesium.Cartesian3.subtract(paths.ascentPath[index + 2], position, new Cesium.Cartesian3()),
new Cesium.Cartesian3(),
);
return Cesium.Math.acosClamped(Cesium.Cartesian3.dot(incoming, outgoing));
}));
assert.ok(Cesium.Math.toDegrees(maximumTurn) < 5);
const ascentTangent = Cesium.Cartesian3.normalize(
Cesium.Cartesian3.subtract(paths.ascentPath.at(-1), paths.ascentPath.at(-2), new Cesium.Cartesian3()),
new Cesium.Cartesian3(),
);
const orbitTangent = Cesium.Cartesian3.normalize(
Cesium.Cartesian3.subtract(paths.animatedOrbitPath[1], paths.animatedOrbitPath[0], new Cesium.Cartesian3()),
new Cesium.Cartesian3(),
);
assert.ok(Cesium.Cartesian3.dot(ascentTangent, orbitTangent) > 0.7);
for (const position of paths.ascentPath) {
assert.ok(Cesium.Cartographic.fromCartesian(position).height >= -1);
}
});
test('keeps a tangent-blended inclined ascent outside the globe', () => {
const launch = Cesium.Cartesian3.fromDegrees(80, -60, 0);
const radius = Cesium.Ellipsoid.WGS84.maximumRadius + 200000;
const inclination = Cesium.Math.toRadians(30);
const orbit = Array.from({ length: 97 }, (_, index) => {
const angle = (index / 96) * Cesium.Math.TWO_PI;
return new Cesium.Cartesian3(
radius * Math.cos(angle),
radius * Math.sin(angle) * Math.cos(inclination),
radius * Math.sin(angle) * Math.sin(inclination),
);
});
const paths = buildMissionPaths(launch, [], orbit);
const minimumHeight = Math.min(
...paths.ascentPath.map((position) => Cesium.Cartographic.fromCartesian(position).height),
);
assert.ok(minimumHeight >= -1);
assert.equal(paths.ascentPath.at(-1), orbit[paths.insertionIndex]);
});
test('uses a propagated insertion reference instead of the radial nearest orbit point', () => {
const launch = Cesium.Cartesian3.fromDegrees(-80.6, 28.5, 0);
const orbit = approximateOrbitPath({
lat: 28.5,
lon: -80.6,
orbit: { name: 'Low Earth Orbit' },
});
const targetIndex = 42;
const paths = buildMissionPaths(launch, [], orbit, orbit[targetIndex]);
assert.equal(paths.insertionIndex, targetIndex);
assert.equal(paths.ascentPath.at(-1), orbit[targetIndex]);
const finalAscent = Cesium.Cartesian3.normalize(
Cesium.Cartesian3.subtract(paths.ascentPath.at(-1), paths.ascentPath.at(-2), new Cesium.Cartesian3()),
new Cesium.Cartesian3(),
);
const firstOrbit = Cesium.Cartesian3.normalize(
Cesium.Cartesian3.subtract(paths.animatedOrbitPath[1], paths.animatedOrbitPath[0], new Cesium.Cartesian3()),
new Cesium.Cartesian3(),
);
assert.ok(Cesium.Cartesian3.dot(finalAscent, firstOrbit) > 0.7);
});
test('estimated mission orbit is a smooth planar ring', () => {
const orbit = approximateOrbitPath({
lat: 34.63,
lon: -120.61,
orbit: { name: 'Polar Orbit' },
});
const normal = Cesium.Cartesian3.normalize(
Cesium.Cartesian3.cross(orbit[0], orbit[24], new Cesium.Cartesian3()),
new Cesium.Cartesian3(),
);
const maximumPlaneResidual = Math.max(...orbit.map((position) => Math.abs(
Cesium.Cartesian3.dot(
normal,
Cesium.Cartesian3.normalize(position, new Cesium.Cartesian3()),
),
)));
assert.ok(maximumPlaneResidual < 1e-12);
assert.ok(Cesium.Cartesian3.distance(orbit[0], orbit.at(-1)) < 1e-6);
});
test('projects a west-coast ascent forward into orbit without reversing course', () => {
const launchInfo = {
lat: 34.63,
lon: -120.61,
orbit: { name: 'Low Earth Orbit' },
};
const launch = Cesium.Cartesian3.fromDegrees(launchInfo.lon, launchInfo.lat, 0);
const orbit = approximateOrbitPath(launchInfo);
const firstDownrange = Cesium.Cartographic.fromCartesian(orbit[1]);
assert.ok(Cesium.Math.toDegrees(firstDownrange.latitude) < launchInfo.lat);
assert.ok(Cesium.Math.toDegrees(firstDownrange.longitude) < launchInfo.lon);
const paths = buildMissionPaths(launch, [], orbit, orbit[10]);
const minimumDirectionContinuity = Math.min(
...paths.ascentPath.slice(1, -1).map((position, index) => {
const incoming = Cesium.Cartesian3.normalize(
Cesium.Cartesian3.subtract(position, paths.ascentPath[index], new Cesium.Cartesian3()),
new Cesium.Cartesian3(),
);
const outgoing = Cesium.Cartesian3.normalize(
Cesium.Cartesian3.subtract(paths.ascentPath[index + 2], position, new Cesium.Cartesian3()),
new Cesium.Cartesian3(),
);
return Cesium.Cartesian3.dot(incoming, outgoing);
}),
);
assert.ok(minimumDirectionContinuity > Math.cos(Cesium.Math.toRadians(5)));
});
test('formats the launch epoch for ascent and orbit replay labels', () => {
assert.equal(
formatMissionEventTime('2026-06-29T02:25:00Z'),
'2026-06-29\n02:25:00 UTC',
);
assert.equal(formatMissionEventTime(null), 'UNAVAILABLE');
});
test('reduces generic launch-site names to their identifying suffix', () => {
assert.equal(compactLaunchSiteName('Orbital Launch Pad 2'), '2');
assert.equal(compactLaunchSiteName('Space Launch Complex 4E'), '4E');
assert.equal(compactLaunchSiteName('Launch Area 130'), '130');
assert.equal(
compactLaunchSiteName('Satish Dhawan Space Centre First Launch Pad'),
'Satish Dhawan Space Centre First Launch Pad',
);
assert.equal(compactLaunchSiteName('Unknown'), null);
});
test('mission overlay factories preserve all four source-formatted label roles and lane policy', () => {
const position = Cesium.Cartesian3.fromDegrees(-80.604, 28.608);
const launch = {
id: 'mission-1',
name: 'Falcon 9 | Gauntlet Payload',
launchSite: 'Space Launch Complex 39A',
launchTime: '2026-07-20T10:00:00Z',
};
const ambient = createRocketMissionMarkerOverlayEntry(launch, position);
assert.deepEqual({
title: ambient.title,
details: ambient.details,
paintLane: ambient.paintLane,
protected: ambient.protected,
edgeFade: ambient.edgeFade,
}, {
title: 'FALCON 9',
details: [],
paintLane: 'ambient-label',
protected: false,
edgeFade: 'keyhole',
});
const selected = createRocketMissionMarkerOverlayEntry(launch, position, true);
assert.equal(selected.title, 'FALCON 9');
assert.deepEqual(selected.details, ['LAUNCH SITE · 39A']);
assert.equal(selected.paintLane, 'selected');
assert.equal(selected.protected, true);
const roles = [
['reentry', 'STAGE RE-ENTRY', '#ffd166', ['STAGE RE-ENTRY', []]],
['payload', 'EST. ORBIT POSITION\n2026-07-20\n10:00:00 UTC', '#ffd166', [
'EST. ORBIT POSITION',
['2026-07-20', '10:00:00 UTC'],
]],
['orbit', 'PROJECTED ORBIT', '#c084fc', ['PROJECTED ORBIT', []]],
];
for (const [id, text, accent, [title, details]] of roles) {
const entry = createRocketMissionElementOverlayEntry({ id, position, text, accent });
assert.equal(entry.title, title);
assert.deepEqual(entry.details, details);
assert.equal(entry.accent, accent);
assert.equal(entry.paintLane, 'selected');
assert.equal(entry.protected, true);
assert.equal(entry.edgeFade, 'keyhole');
}
const surplus = Array.from(
{ length: ROCKET_MISSION_AMBIENT_OVERLAY_COHORT_LIMIT + 12 },
(_, index) => ({ id: `mission-${String(index).padStart(3, '0')}`, priority: index }),
);
const cohort = selectRocketMissionMarkerOverlayCohort(surplus);
assert.equal(cohort.length, ROCKET_MISSION_AMBIENT_OVERLAY_COHORT_LIMIT);
assert.equal(cohort[0].id, 'mission-059');
assert.equal(cohort.at(-1).id, 'mission-012');
});
test('parses signed Launch Library timeline durations', () => {
assert.equal(parseMissionDurationSeconds('PT1H1M46S'), 3706);
assert.equal(parseMissionDurationSeconds('-PT35M'), -2100);
assert.equal(parseMissionDurationSeconds('P0D'), 0);
assert.equal(parseMissionDurationSeconds('unknown'), null);
});
test('derives replay ascent duration from mission timing instead of a fixed constant', () => {
const path = [
Cesium.Cartesian3.fromDegrees(-120, 34, 0),
Cesium.Cartesian3.fromDegrees(-116, 34, 200000),
];
const fast = replayAscentDurationSeconds({
timeline: [{ name: 'SECO-1', offsetSeconds: 360 }],
}, path);
const slow = replayAscentDurationSeconds({
timeline: [{ name: 'Orbit insertion', offsetSeconds: 1200 }],
}, path);
assert.ok(slow > fast);
assert.ok(fast >= 8 && slow <= 36);
});
test('matches compact payload identifiers without accepting an arbitrary constellation member', () => {
assert.ok(scoreSatelliteNameMatch('Sirius SXM-11', 'SXM-11') >= 200);
assert.ok(scoreSatelliteNameMatch('Starlink Group 17-40', 'STARLINK-1008') < 12);
});
test('finds a newly launched payload in the active TLE fallback catalog', () => {
const tle = `SXM-11
1 69728U 26148A 26208.65166678 .00000015 00000+0 00000+0 0 9990
2 69728 0.0784 264.9826 0001797 240.4588 273.9506 1.00271527 440`;
const track = findSatelliteOrbitTrackInTle(tle, 'Sirius SXM-11', {
launchTime: '2026-06-29T02:25:00Z',
});
assert.equal(track?.noradId, 69728);
assert.equal(track?.name, 'SXM-11');
assert.ok(track?.orbitPath.length > 100);
assert.ok(track?.periodSec > 80000);
assert.ok(track?.current.speedMps > 1000);
assert.ok(track?.current.speedMps < 12000);
assert.equal(typeof track?.positionAt, 'function');
});
test('real mission build, select, refresh, deselect, disable, and destroy paths publish no native labels', async () => {
const realDocument = globalThis.document;
const realFetch = globalThis.fetch;
const realHtmlCanvasElement = globalThis.HTMLCanvasElement;
const realHtmlImageElement = globalThis.HTMLImageElement;
const realImageBitmap = globalThis.ImageBitmap;
const realOffscreenCanvas = globalThis.OffscreenCanvas;
const listeners = new Map();
const context = {
strokeStyle: '',
lineWidth: 1,
lineCap: '',
shadowColor: '',
shadowBlur: 0,
beginPath() {},
moveTo() {},
lineTo() {},
stroke() {},
};
class FakeElement {
constructor(tagName = 'div') {
this.tagName = tagName.toUpperCase();
this.children = [];
this.parentElement = null;
this.style = { setProperty() {} };
this.classList = { add() {}, remove() {}, toggle() {} };
this.dataset = {};
this.hidden = false;
this.clientWidth = 1600;
this.clientHeight = 900;
this.width = 1600;
this.height = 900;
this.disableRootEvents = false;
}
addEventListener(type, handler) { listeners.set(`${this.tagName}:${type}`, handler); }
removeEventListener(type) { listeners.delete(`${this.tagName}:${type}`); }
appendChild(child) { child.parentElement = this; this.children.push(child); return child; }
remove() {
if (this.parentElement) {
this.parentElement.children = this.parentElement.children.filter((child) => child !== this);
}
this.parentElement = null;
}
setAttribute() {}
// This fixture has no parsed DOM children; it exercises native label
// ownership, not the replay overlay's separately rendered icon children.
querySelectorAll() { return []; }
querySelector() { return null; }
getBoundingClientRect() { return { left: 0, top: 0, width: 1600, height: 900 }; }
getContext() { return this.tagName === 'CANVAS' ? context : null; }
}
const body = new FakeElement('body');
const document = {
body,
onmousewheel: undefined,
createElement: (tagName) => new FakeElement(tagName),
getElementById: () => null,
addEventListener(type, handler) { listeners.set(`document:${type}`, handler); },
removeEventListener(type) { listeners.delete(`document:${type}`); },
};
const canvas = new FakeElement('canvas');
const dataSources = [];
const camera = {
positionCartographic: null,
positionWC: Cesium.Cartesian3.fromDegrees(-80.604, 28.608, 18_000_000),
cancelFlight() {},
lookAtTransform() {},
};
const scene = {
canvas,
camera,
frameState: { frameNumber: 1 },
postRender: new Cesium.Event(),
preRender: new Cesium.Event(),
preUpdate: new Cesium.Event(),
primitives: { add: (primitive) => primitive, remove: () => true },
drillPick: () => [],
};
const viewer = {
camera,
scene,
dataSources: {
add(dataSource) { dataSources.push(dataSource); return dataSource; },
remove(dataSource) {
const index = dataSources.indexOf(dataSource);
if (index >= 0) dataSources.splice(index, 1);
return index >= 0;
},
},
selectedEntity: undefined,
};
const hostCalls = [];
const host = {
setEntries: (...args) => hostCalls.push(['entries', ...args]),
setVisible: (...args) => hostCalls.push(['visible', ...args]),
clearSource: (...args) => hostCalls.push(['clear', ...args]),
};
const launchTime = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
let launchName = 'Falcon 9 | Gauntlet Payload';
const launchPayload = () => ({ results: [{
id: 'mission-runtime',
name: launchName,
net: launchTime,
status: { name: 'Launch Successful' },
pad: {
latitude: '28.608',
longitude: '-80.604',
name: 'Space Launch Complex 39A',
},
rocket: {
launcher_stage: [{
id: 'booster-1',
type: 'Core',
landing: {
attempt: true,
success: true,
downrange_distance: 600,
landing_location: { latitude: '30.1', longitude: '-76.2' },
},
}],
},
mission: {
name: 'Gauntlet Payload',
orbit: { name: 'Low Earth Orbit' },
},
}] });
globalThis.document = document;
globalThis.HTMLCanvasElement = FakeElement;
globalThis.HTMLImageElement = class {};
globalThis.ImageBitmap = class {};
globalThis.OffscreenCanvas = class {};
globalThis.fetch = async (url) => {
if (url === '/api/celestrak/active') {
return { ok: true, text: async () => '' };
}
assert.equal(url, '/api/launches');
return { ok: true, json: async () => launchPayload() };
};
_setRocketMissionOverlayHostForTest(host);
let initialized = false;
try {
rocketLaunchesLayer.init(viewer);
initialized = true;
await rocketLaunchesLayer.enable();
await rocketLaunchesLayer.update();
const entities = dataSources[0].entities.values;
assert.ok(entities.length >= 7, 'runtime guard requires the populated mission build path');
assert.ok(entities.every((entity) => entity.label === undefined));
const ambientPublication = hostCalls.findLast(([type, sourceId]) => (
type === 'entries' && sourceId === ROCKET_MISSION_AMBIENT_OVERLAY_SOURCE_ID
));
assert.ok(ambientPublication);
assert.equal(ambientPublication[2].length, 1);
assert.equal(ambientPublication[2][0].title, 'FALCON 9');
assert.deepEqual(ambientPublication[3], {
cohortLimit: ROCKET_MISSION_AMBIENT_OVERLAY_COHORT_LIMIT,
collisionCapacity: ROCKET_MISSION_AMBIENT_OVERLAY_COLLISION_CAPACITY,
moving: false,
});
const launchEntity = dataSources[0].entities.getById('rocket-launch:mission-runtime');
assert.deepEqual(
ambientPublication[2][0].position,
launchEntity.position.getValue(Cesium.JulianDate.now()),
'ambient host marker must reuse the entity anchor Cartesian',
);
const frameTime = Cesium.JulianDate.now();
scene.preRender.raiseEvent(scene, frameTime);
assert.equal(launchEntity.point.show.getValue(frameTime), true);
camera.positionWC = Cesium.Cartesian3.fromDegrees(99.396, -28.608, 18_000_000);
scene.preRender.raiseEvent(scene, frameTime);
assert.equal(
launchEntity.point.show.getValue(frameTime),
false,
'pre-render horizon pass must hide a rear-side depth-free mission dot before draw and pick',
);
camera.positionWC = Cesium.Cartesian3.fromDegrees(-80.604, 28.608, 18_000_000);
scene.preRender.raiseEvent(scene, frameTime);
assert.equal(
launchEntity.point.show.getValue(frameTime),
true,
'mission dot must return when its surface anchor faces the camera again',
);
_setSelectedRocketMissionForTest('mission-runtime');
const selectedPublication = hostCalls.findLast(([type, sourceId]) => (
type === 'entries' && sourceId === ROCKET_MISSION_SELECTED_OVERLAY_SOURCE_ID
));
assert.ok(selectedPublication);
assert.deepEqual(selectedPublication[3], ROCKET_MISSION_SELECTED_OVERLAY_SOURCE_OPTIONS);
assert.deepEqual(selectedPublication[2].map(({ title }) => title), [
'FALCON 9',
'STAGE RE-ENTRY',
'EST. ORBIT POSITION',
'PROJECTED ORBIT',
]);
assert.deepEqual(selectedPublication[2][0].details, ['LAUNCH SITE · 39A']);
assert.match(selectedPublication[2][2].details[0], /^\d{4}-\d{2}-\d{2}$/);
assert.match(selectedPublication[2][2].details[1], /^\d{2}:\d{2}:\d{2} UTC$/);
assert.ok(selectedPublication[2].every((entry) => (
entry.selected === true
&& entry.protected === true
&& entry.paintLane === 'selected'
&& entry.edgeFade === 'keyhole'
)));
const satelliteEntity = dataSources[0].entities.getById('rocket-satellite:mission-runtime');
const satellitePosition = satelliteEntity.position.getValue(Cesium.JulianDate.now());
assert.equal(
selectedPublication[2][2].position(),
satellitePosition,
'payload host getter must return the exact per-frame live-position cache',
);
// Jitter regression net: live-state propagation advances at most once per
// frameNumber, and the host getter is a pure read that never advances it.
// Without both halves, the host label and the native dot can propagate to
// different wall-clock instants inside one frame — the documented
// label-separation jitter class.
const payloadEntry = selectedPublication[2][2];
const realDateNow = Date.now;
try {
let nowMs = realDateNow();
Date.now = () => nowMs;
scene.frameState.frameNumber = 41;
const frameA = Cesium.Cartesian3.clone(
satelliteEntity.position.getValue(Cesium.JulianDate.now()),
);
nowMs += 30_000;
assert.deepEqual(
Cesium.Cartesian3.clone(satelliteEntity.position.getValue(Cesium.JulianDate.now())),
frameA,
'a second native read in the same frame must not re-propagate',
);
nowMs += 30_000;
scene.frameState.frameNumber = 42;
assert.deepEqual(
Cesium.Cartesian3.clone(payloadEntry.position()),
frameA,
'host getter must not advance propagation, even on a new frame',
);
const frameB = Cesium.Cartesian3.clone(
satelliteEntity.position.getValue(Cesium.JulianDate.now()),
);
assert.notDeepEqual(frameB, frameA, 'native read on a new frame propagates (non-vacuous)');
assert.deepEqual(
Cesium.Cartesian3.clone(payloadEntry.position()),
frameB,
'host getter reads the advanced cache after native propagation',
);
} finally {
Date.now = realDateNow;
scene.frameState.frameNumber = 1;
}
const reentryEntity = dataSources[0].entities.getById('rocket-reentry:mission-runtime:0');
assert.deepEqual(
selectedPublication[2][1].position,
reentryEntity.polyline.positions.getValue(Cesium.JulianDate.now())[0],
're-entry host label must reuse the recovery-path interface Cartesian',
);
const transferEntity = dataSources[0].entities.getById('rocket-transfer:mission-runtime');
assert.deepEqual(
selectedPublication[2][3].position,
transferEntity.polyline.positions.getValue(Cesium.JulianDate.now()).at(-1),
'orbit host label must reuse the insertion Cartesian',
);
launchName = 'Mission Refresh | Gauntlet Payload';
await rocketLaunchesLayer.update();
assert.ok(dataSources[0].entities.values.every((entity) => entity.label === undefined));
const refreshPublication = hostCalls.findLast(([type, sourceId]) => (
type === 'entries' && sourceId === ROCKET_MISSION_SELECTED_OVERLAY_SOURCE_ID
));
assert.equal(refreshPublication[2][0].title, 'MISSION REFRESH');
assert.deepEqual(refreshPublication[2][0].details, ['LAUNCH SITE · 39A']);
_setSelectedRocketMissionForTest(null);
const deselectedPublication = hostCalls.findLast(([type, sourceId]) => (
type === 'entries' && sourceId === ROCKET_MISSION_AMBIENT_OVERLAY_SOURCE_ID
));
assert.equal(deselectedPublication[2][0].title, 'MISSION REFRESH');
assert.equal(deselectedPublication[2][0].protected, false);
await rocketLaunchesLayer.disable();
assert.deepEqual(hostCalls.slice(-4), [
['clear', ROCKET_MISSION_AMBIENT_OVERLAY_SOURCE_ID],
['visible', ROCKET_MISSION_AMBIENT_OVERLAY_SOURCE_ID, false],
['clear', ROCKET_MISSION_SELECTED_OVERLAY_SOURCE_ID],
['visible', ROCKET_MISSION_SELECTED_OVERLAY_SOURCE_ID, false],
]);
await rocketLaunchesLayer.destroy(viewer);
initialized = false;
assert.equal(dataSources.length, 0);
assert.deepEqual(hostCalls.slice(-4), [
['clear', ROCKET_MISSION_AMBIENT_OVERLAY_SOURCE_ID],
['visible', ROCKET_MISSION_AMBIENT_OVERLAY_SOURCE_ID, false],
['clear', ROCKET_MISSION_SELECTED_OVERLAY_SOURCE_ID],
['visible', ROCKET_MISSION_SELECTED_OVERLAY_SOURCE_ID, false],
]);
} finally {
if (initialized) await rocketLaunchesLayer.destroy(viewer);
_setRocketMissionOverlayHostForTest();
globalThis.fetch = realFetch;
globalThis.document = realDocument;
globalThis.HTMLCanvasElement = realHtmlCanvasElement;
globalThis.HTMLImageElement = realHtmlImageElement;
globalThis.ImageBitmap = realImageBitmap;
globalThis.OffscreenCanvas = realOffscreenCanvas;
}
});
test('enable is transactional: a failed satellites dependency rolls the module back and a retry recaptures', async (t) => {
const calls = [];
let satellitesEnabled = false;
let failNextActivation = true;
const fakeManager = {
isEnabled: (id) => (id === 'satellites' ? satellitesEnabled : false),
getLayerParams: () => ({}),
setLayerParams: () => {},
setEnabled: (id, enabled) => {
calls.push(['setEnabled', id, enabled]);
if (id === 'satellites' && enabled && failNextActivation) {
return Promise.resolve(false); // activation refused/failed
}
if (id === 'satellites') satellitesEnabled = enabled;
return Promise.resolve(true);
},
};
const priorDocument = globalThis.document;
globalThis.document = {
getElementById: () => null,
querySelector: () => null,
querySelectorAll: () => [],
};
rocketLaunchesLayer.attachDataManager(fakeManager);
t.after(() => {
globalThis.document = priorDocument;
rocketLaunchesLayer.attachDataManager(null);
});
// First enable: dependency activation fails -> enable() must reject and the
// module must roll itself back (satellite snapshot restored AND cleared).
await assert.rejects(() => rocketLaunchesLayer.enable(), /satellites layer/);
const restoreCall = calls.filter(([, id, enabled]) => id === 'satellites' && enabled === false);
assert.ok(restoreCall.length >= 1, 'rollback restored the pre-mission satellites state');
// Retry with a healthy dependency: capture must run AGAIN (a retained
// snapshot would skip it) and enable must succeed.
calls.length = 0;
failNextActivation = false;
await rocketLaunchesLayer.enable();
assert.deepEqual(
calls.filter(([, id, enabled]) => id === 'satellites' && enabled === true).length >= 1,
true,
'retry recaptured the satellites dependency',
);
await rocketLaunchesLayer.disable();
});
test('disable reports a semantic failure while restoring the satellites dependency', async (t) => {
let satellitesEnabled = false;
let failRestore = false;
const fakeManager = {
isEnabled: (id) => (id === 'satellites' ? satellitesEnabled : false),
isEffectivelyEnabled: (id) => (id === 'satellites' ? satellitesEnabled : false),
getLayerParams: () => ({}),
setLayerParams: () => {},
setEnabled: (id, enabled) => {
if (id === 'satellites' && !enabled && failRestore) return Promise.resolve(false);
if (id === 'satellites') satellitesEnabled = enabled;
return Promise.resolve(true);
},
};
const priorDocument = globalThis.document;
globalThis.document = {
getElementById: () => null,
querySelector: () => null,
querySelectorAll: () => [],
};
rocketLaunchesLayer.attachDataManager(fakeManager);
t.after(() => {
globalThis.document = priorDocument;
rocketLaunchesLayer.attachDataManager(null);
});
await rocketLaunchesLayer.enable();
failRestore = true;
await assert.rejects(
() => rocketLaunchesLayer.disable(),
/could not restore the satellites layer/,
);
});
test('missing payload details stay unknown without inventing mass or classification', () => {
for (const [mass, expected] of [[null, null], [undefined, null], ['', null], [' ', null],
[false, null], [-1, null], ['invalid', null], [0, 0], ['125', 125]]) {
const [launch] = normalizeRocketLaunches([{ id: 'mass', pad: { latitude: 1, longitude: 2 }, net: '2026-07-20T10:00:00Z',
payloads: [{ mass }] }], NOW);
assert.equal(launch.payloads[0].massKg, expected);
assert.equal(launch.payloads[0].name, 'Unnamed payload');
}
const [launch] = normalizeRocketLaunches([{ id: 'missing', pad: { latitude: 1, longitude: 2 }, net: '2026-07-20T10:00:00Z' }], NOW);
assert.deepEqual(launch.payloads, []);
});