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

2914 lines
121 KiB
JavaScript

import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import test from 'node:test';
import { claimPointer, releasePointer } from './inputOwnership.js';
import * as Cesium from 'cesium';
import { SOURCE_PATH_BEAT_IDS } from './bhoteKoshiShotPaths.js';
import {
BHOTE_KOSHI_LAYER_ID,
activeEvidenceIndex,
adjacentEvidenceIndex,
buildCinematicKeyframes,
buildEvidenceTimeline,
clampUnit,
createBhoteKoshiEventLayer,
evidenceCardLayout,
createEvidenceFrame,
elapsedLabel,
evidenceBeatProgress,
evidenceBeatPresentation,
evidenceCorroborationLabel,
evidenceSequenceWindow,
evidenceSummaryLines,
floodProgressForStoryProgress,
interpolateHeadingDegrees,
sampleCinematicPose,
updateEvidenceFrame,
updateCorridorPositionCache,
usesProviderEmbeddedEvidence,
visibleSegmentCount,
} from './bhoteKoshiEvent.js';
import { SCENE_RECIPES } from '../scenes/recipes.js';
import { LayerLifecycle } from './lifecycle.js';
import { createBhoteKoshiEmbeddedMedia } from './bhoteKoshiEmbeddedMedia.js';
import { SceneDirector } from '../scenes/director.js';
const eventUrl = new URL('../../public/events/bhote-koshi-2026/event.json', import.meta.url);
const eventModuleUrl = new URL('./bhoteKoshiEvent.js', import.meta.url);
class FakeElement {
constructor(tagName = 'div') {
this.tagName = tagName;
this.children = [];
this.removed = false;
this._value = '';
this._textContent = '';
this._disabled = false;
this._writeCount = 0;
this._queryCount = 0;
this._attributes = new Map();
this._selectors = new Map();
this._listeners = new Map();
}
set value(value) {
if (this._value === value) return;
this._value = value;
this._writeCount += 1;
}
get value() { return this._value; }
set textContent(value) {
if (this._textContent === value) return;
this._textContent = value;
this._writeCount += 1;
}
get textContent() { return this._textContent; }
set disabled(value) {
if (this._disabled === value) return;
this._disabled = value;
this._writeCount += 1;
}
get disabled() { return this._disabled; }
setAttribute(name, value) {
if (this._attributes.get(name) === value) return;
this._attributes.set(name, value);
this._writeCount += 1;
}
getAttribute(name) { return this._attributes.get(name) ?? null; }
addEventListener(type, handler) {
this._listeners.set(type, handler);
}
removeEventListener(type, handler) {
if (this._listeners.get(type) === handler) this._listeners.delete(type);
}
dispatch(type, event = { target: this }) {
return this._listeners.get(type)?.(event);
}
append(...children) {
this.children.push(...children);
}
appendChild(child) {
this.children.push(child);
return child;
}
querySelector(selector) {
this._queryCount += 1;
if (!this._selectors.has(selector)) this._selectors.set(selector, new FakeElement());
return this._selectors.get(selector);
}
remove() {
this.removed = true;
}
set innerHTML(value) {
this._innerHTML = value;
}
get innerHTML() {
return this._innerHTML || '';
}
}
function installEventDom() {
const body = new FakeElement('body');
const removedProperties = [];
globalThis.document = {
body,
createElement: (tagName) => new FakeElement(tagName),
documentElement: {
clientWidth: 1000,
style: {
setProperty() {},
removeProperty(name) { removedProperties.push(name); },
},
},
};
return { body, removedProperties };
}
function eventViewer(splitPosition = 0.37) {
const imagery = [];
const removedImagery = [];
const dataSources = [];
const removedDataSources = [];
const canvas = new FakeElement('canvas');
const cameraLooks = [];
const cameraTransforms = [];
return {
scene: { splitPosition, canvas },
camera: {
cancelFlight() {},
lookAt(target, offset) {
cameraLooks.push({
target: { x: target.x, y: target.y, z: target.z },
offset: { heading: offset.heading, pitch: offset.pitch, range: offset.range },
});
},
lookAtTransform(transform) { cameraTransforms.push(transform); },
},
imageryLayers: {
addImageryProvider(provider) {
const layer = { provider };
imagery.push(layer);
return layer;
},
remove(layer) { removedImagery.push(layer); },
},
dataSources: {
add(source) { dataSources.push(source); },
remove(source) { removedDataSources.push(source); },
},
_test: {
imagery,
removedImagery,
dataSources,
removedDataSources,
canvas,
cameraLooks,
cameraTransforms,
},
};
}
test('public lifecycle restores Nepal scene ownership without forwarding origin to enable', async () => {
const previous = { document: globalThis.document, window: globalThis.window };
const event = JSON.parse(await readFile(eventUrl, 'utf8'));
try {
for (const initialMap of ['photoreal', 'esri-imagery']) {
for (const origin of ['scene', 'local-restore', 'share-restore']) {
installEventDom();
const timers = [];
globalThis.window = {
setTimeout(handler, delay) { timers.push({ handler, delay }); return timers.length; },
clearTimeout() {},
};
const viewer = eventViewer();
viewer.camera.flyToBoundingSphere = () => viewer._test.cameraLooks.push('corridor');
let activeMap = initialMap;
const mapCalls = [];
const layer = createBhoteKoshiEventLayer({
eventLoader: async () => event,
imageryProviderFactory: async (url) => ({ url }),
terrainSampler: async (_viewer, observations) => observations.map(() => ({ height: 800 })),
mediaLoader: async () => { const error = new Error('No bundled clip'); error.name = 'AbortError'; throw error; },
overlayHost: { setEntries() {}, setVisible() {}, clearSource() {} },
renderHost: { request() {}, hold() {}, release() {} },
});
layer.attachMapStackController({
getActiveId: () => activeMap,
getSwitchGeneration: () => 1,
async setStack(id) { mapCalls.push(id); activeMap = id; },
});
const manager = new LayerLifecycle(viewer);
manager.register(layer);
manager.finalizeRegistrations([{ id: layer.id, disposition: 'enabled-only' }]);
try {
const result = await manager.restoreLayerState(layer.id, {
enabled: true,
params: { presentation: 'scene-beat', sceneSurface: 'panel-only', progress: 0.34 },
}, { origin });
assert.equal(result.succeeded, true);
assert.equal(layer.getPlaybackState().enabled, true);
assert.deepEqual(mapCalls, [], `${initialMap}/${origin}: preserve the shot map`);
assert.equal(activeMap, initialMap);
const floodSource = viewer._test.dataSources.at(-1);
for (const id of ['bhote-koshi-flood-halo', 'bhote-koshi-flood-core']) {
const line = floodSource.entities.getById(id).polyline;
assert.equal(line.clampToGround.getValue(), true, 'dynamic trail follows the rendered surface');
assert.equal(line.classificationType.getValue(), initialMap === 'photoreal'
? Cesium.ClassificationType.BOTH : Cesium.ClassificationType.TERRAIN);
assert.equal(line.depthFailMaterial, undefined, 'do not rely on unsupported dynamic depth-fail rendering');
}
assert.equal(floodSource.entities.getById('bhote-koshi-surge-front').point.heightReference.getValue(),
Cesium.HeightReference.CLAMP_TO_GROUND);
assert.deepEqual(viewer._test.cameraLooks, [], 'enable must not focus the corridor');
assert.deepEqual(timers, [], 'enable must not schedule independent playback');
assert.equal(layer.getPlaybackState().playing, false);
assert.equal(await manager.setEnabled(layer.id, false, { origin: 'user' }), true);
assert.deepEqual(mapCalls, [], 'disable must not restore a map it never owned');
} finally {
await manager.destroyLayer(layer.id);
}
}
}
} finally {
Object.assign(globalThis, previous);
}
});
test('trimmed scene media holds through provider startup, playback and fade, then releases the camera', async () => {
const previous = { document: globalThis.document, window: globalThis.window };
installEventDom();
const timers = new Map();
let nextId = 0;
globalThis.window = {
setTimeout(fn, delay) { const id = ++nextId; timers.set(id, { fn, delay }); return id; },
clearTimeout(id) { timers.delete(id); },
};
let phase = 'starting';
let hidden = 0;
const event = JSON.parse(await readFile(eventUrl, 'utf8'));
const layer = createBhoteKoshiEventLayer({
eventLoader: async () => event,
imageryProviderFactory: async (url) => ({ url }),
terrainSampler: async (_viewer, observations) => observations.map(() => ({ height: 800 })),
mediaLoader: async () => { const error = new Error('No bundled clip'); error.name = 'AbortError'; throw error; },
overlayHost: { setEntries() {}, setVisible() {}, clearSource() {} },
renderHost: { request() {}, hold() {}, release() {} },
embeddedMediaFactory: () => ({ warm: () => true, show: () => true, play() {}, pause() {},
getPlaybackState: () => ({ phase }), hide() { hidden++; }, destroy() {} }),
});
const fire = (delay) => {
const entry = [...timers].find(([, value]) => value.delay === delay);
assert.ok(entry, `expected ${delay}ms owned timer`);
timers.delete(entry[0]); entry[1].fn();
};
try {
const viewer = eventViewer();
await layer.init(viewer);
layer.setSceneMediaPlayback({ sceneId: 'scene', shotId: 'shot', token: { cancelled: false } });
layer.setParams({ presentation: 'scene-beat', sceneSurface: 'evidence-beat', beatId: 'mailung-bazzar',
sceneContext: { sceneId: 'scene', shotId: 'shot', holdSec: 7.65 },
sceneControls: { evidenceMediaAutoplay: true, cameraSettled: true, mediaExitDurationSec: 0.65 } }, { origin: 'scene' });
await layer.enable(viewer, { origin: 'scene' });
assert.equal(layer.getSceneShotMediaHold('mailung-bazzar').pending, true);
assert.equal(layer.getSceneShotMediaHold('different-beat'), null);
fire(100);
assert.equal(layer.getSceneShotMediaHold('mailung-bazzar').pending, true, 'startup is not treated as playback');
phase = 'playing'; fire(100);
assert.equal(layer.getSceneShotMediaHold('mailung-bazzar').pending, true);
const hiddenBeforeEnd = hidden;
phase = 'completed'; fire(100);
assert.ok(hidden > hiddenBeforeEnd);
assert.equal(layer.getSceneShotMediaHold('mailung-bazzar').pending, true, 'the fade still owns the hold');
fire(650);
assert.equal(layer.getSceneShotMediaHold('mailung-bazzar').pending, false);
await layer.disable();
assert.equal(layer.getSceneShotMediaHold('mailung-bazzar'), null);
} finally {
await layer.destroy();
globalThis.document = previous.document; globalThis.window = previous.window;
}
});
test('Pinokio source-card fallback selects authored Director dwell, not an impossible player hold', async () => {
const previous = { document: globalThis.document, window: globalThis.window };
installEventDom();
globalThis.window = { setTimeout: () => 1, clearTimeout() {} };
const event = JSON.parse(await readFile(eventUrl, 'utf8'));
const layer = createBhoteKoshiEventLayer({
eventLoader: async () => event,
imageryProviderFactory: async url => ({ url }),
terrainSampler: async (_viewer, observations) => observations.map(() => ({ height: 800 })),
mediaLoader: async () => { throw Object.assign(new Error('No bundled clip'), { name: 'AbortError' }); },
overlayHost: { setEntries() {}, setVisible() {}, clearSource() {} },
renderHost: { request() {}, hold() {}, release() {} },
embeddedMediaFactory: options => createBhoteKoshiEmbeddedMedia({ ...options,
globalRef: { navigator: { userAgent: 'Chrome Pinokio/8.0.40' } },
documentRef: { createElement() { assert.fail('unsupported host must allocate no provider DOM'); } },
}),
});
const token = { cancelled: false, signal: new AbortController().signal };
const params = { presentation: 'scene-beat', sceneSurface: 'evidence-beat', beatId: 'mailung-bazzar',
sceneContext: { sceneId: 'scene', shotId: 'shot', holdSec: 7.65 },
sceneControls: { evidenceMediaAutoplay: true, mediaPlaybackHoldSec: 7, cameraSettled: true } };
try {
const viewer = eventViewer();
await layer.init(viewer);
layer.setSceneMediaPlayback({ sceneId: 'scene', shotId: 'shot', token });
layer.setParams(params, { origin: 'scene' });
await layer.enable(viewer, { origin: 'scene' });
assert.equal(layer.getSceneShotMediaHold('mailung-bazzar'), null);
const waits = [];
await SceneDirector.prototype._holdShot.call({
_layerStatesForShot: () => ({ [layer.id]: { enabled: true, params } }),
dataManager: { layers: new Map([[layer.id, { module: layer }]]) },
_effectiveShotHoldSec: () => 7.65,
_sleep: async ms => waits.push(ms),
stopScene: () => assert.fail('a source card must not time out'),
}, { id: 'scene' }, { id: 'shot' }, token);
assert.deepEqual(waits, [7650], 'use the authored dwell instead of completing instantly or polling 20 seconds');
event.evidenceSpine.find(item => item.id === 'mailung-bazzar').media.videoPath = 'fixture-approved.mp4';
assert.equal(layer.getSceneShotMediaHold('mailung-bazzar').pending, true,
'an approved local clip remains a real playback owner even with the provider suppressed');
} finally { await layer.destroy(); Object.assign(globalThis, previous); }
});
test('saved autoplay controls cannot authorize media; live shot ownership is cancellable and never serialized', async () => {
const previous = { document: globalThis.document, window: globalThis.window };
installEventDom();
const timers = new Map(); let timerId = 0;
globalThis.window = { setTimeout: fn => { timers.set(++timerId, fn); return timerId; }, clearTimeout: id => timers.delete(id) };
const calls = [];
const event = JSON.parse(await readFile(eventUrl, 'utf8'));
const layer = createBhoteKoshiEventLayer({
eventLoader: async () => event,
imageryProviderFactory: async url => ({ url }),
terrainSampler: async (_viewer, observations) => observations.map(() => ({ height: 800 })),
mediaLoader: async () => { throw Object.assign(new Error('No bundled clip'), { name: 'AbortError' }); },
overlayHost: { setEntries() {}, setVisible() {}, clearSource() {} },
renderHost: { request() {}, hold() {}, release() {} },
embeddedMediaFactory: () => ({ supportsPlayback: true, warm: () => true,
show: options => { calls.push(options.autoplay); return true; },
getPlaybackState: () => null, play() {}, pause() {}, hide() {}, destroy() {} }),
});
const params = { presentation: 'scene-beat', sceneSurface: 'evidence-beat', beatId: 'mailung-bazzar',
sceneContext: { sceneId: 'scene', shotId: 'shot', holdSec: 7.65 },
sceneControls: { evidenceMediaAutoplay: true, cameraSettled: true, mediaPlaybackHoldSec: 7 } };
try {
const viewer = eventViewer(); await layer.init(viewer);
layer.setParams(params, { origin: 'local-restore' }); await layer.enable(viewer);
assert.ok(calls.length); assert.ok(calls.every(value => value === false));
assert.equal(layer.getSceneShotMediaHold('mailung-bazzar'), null);
const saved = layer.getParams();
const abort = new AbortController();
layer.setSceneMediaPlayback({ sceneId: 'scene', shotId: 'shot', token: { cancelled: false, signal: abort.signal } });
layer.setParams(params, { origin: 'scene' });
assert.equal(calls.at(-1), true);
assert.equal(layer.getSceneShotMediaHold('mailung-bazzar').pending, true,
'a temporarily null supported-provider state is still startup, not unsupported');
assert.deepEqual(layer.getParams(), saved, 'live playback authority must not enter saved params');
const late = [...timers.values()];
abort.abort(); late.forEach(fn => fn());
assert.equal(layer.getSceneShotMediaHold('mailung-bazzar'), null);
layer.setParams(params, { origin: 'local-restore' });
assert.equal(calls.at(-1), false, 'late callbacks and restored flags cannot restart revoked playback');
layer.setSceneMediaPlayback({ sceneId: 'scene', shotId: 'different-shot', token: { cancelled: false } });
layer.setParams(params, { origin: 'scene' });
assert.equal(calls.at(-1), false, 'a different shot cannot borrow this playback owner');
} finally { await layer.destroy(); Object.assign(globalThis, previous); }
});
test('Bhote Koshi timeline helpers clamp untrusted UI values', () => {
assert.equal(clampUnit(-1), 0);
assert.equal(clampUnit(0.42), 0.42);
assert.equal(clampUnit(3), 1);
assert.equal(elapsedLabel(0.5, 4170), '34:45');
assert.equal(elapsedLabel(1, 4170), '69:30');
});
test('Bhote Koshi flood reveal is monotonic and bounded', () => {
assert.equal(visibleSegmentCount(0, 144), 0);
assert.equal(visibleSegmentCount(Number.NaN, 144), 0);
assert.equal(visibleSegmentCount(0.001, 144), 1);
assert.equal(visibleSegmentCount(0.5, 144), 72);
assert.equal(visibleSegmentCount(2, 144), 144);
});
test('Bhote Koshi flood corridor grows continuously to the animated surge head', () => {
const terrainPositions = Array.from(
{ length: 11 },
(_, index) => new Cesium.Cartesian3(index, 0, 0),
);
const cache = { visibleCount: -1, positions: [] };
assert.equal(updateCorridorPositionCache(0.201, terrainPositions, cache), true);
const firstPositions = cache.positions;
assert.equal(cache.visibleCount, 3);
assert.equal(firstPositions.length, 4);
assert.ok(Math.abs(cache.headPosition.x - 2.01) < 1e-9);
assert.equal(firstPositions.at(-1).x, cache.headPosition.x);
assert.equal(updateCorridorPositionCache(0.299, terrainPositions, cache), true);
assert.notEqual(cache.positions, firstPositions, 'the trail advances within the segment');
assert.ok(Math.abs(cache.headPosition.x - 2.99) < 1e-9);
assert.equal(cache.positions.at(-1).x, cache.headPosition.x);
const repeatedPositions = cache.positions;
assert.equal(updateCorridorPositionCache(0.299, terrainPositions, cache), false);
assert.equal(cache.positions, repeatedPositions, 'an unchanged frame reuses its positions');
assert.equal(updateCorridorPositionCache(0.301, terrainPositions, cache), true);
assert.equal(cache.visibleCount, 4);
assert.equal(cache.positions.length, 5);
assert.ok(Math.abs(cache.headPosition.x - 3.01) < 1e-9);
assert.equal(cache.positions.at(-1).x, cache.headPosition.x);
});
test('Bhote Koshi story clock holds the flood for the cause beats then completes the corridor', () => {
assert.equal(floodProgressForStoryProgress(0), 0);
assert.equal(floodProgressForStoryProgress(0.11), 0);
assert.equal(floodProgressForStoryProgress(0.22), 0);
assert.ok(floodProgressForStoryProgress(0.5) > 0.4);
assert.equal(floodProgressForStoryProgress(0.84), 1);
assert.equal(floodProgressForStoryProgress(1), 1);
});
test('Bhote Koshi camera holds its evidence beat, then travels by the next activation', () => {
assert.equal(interpolateHeadingDegrees(350, 10, 0.5), 0);
const reusableTarget = { x: 99, y: 99, z: 99 };
const result = { target: reusableTarget };
const keyframes = [
{
id: 'cause',
progress: 0,
target: { x: 0, y: 0, z: 0 },
headingDeg: 350,
pitchDeg: -50,
rangeM: 1000,
},
{
id: 'border',
progress: 1,
target: { x: 10, y: 20, z: 30 },
headingDeg: 10,
pitchDeg: -30,
rangeM: 3000,
},
];
const held = sampleCinematicPose(0.5, keyframes, result);
assert.deepEqual(held.target, { x: 0, y: 0, z: 0 });
assert.equal(held.headingDeg, 350);
assert.equal(held.pitchDeg, -50);
assert.equal(held.rangeM, 1000);
const sampled = sampleCinematicPose(0.81, keyframes, result);
assert.equal(sampled, result);
assert.equal(sampled.target, reusableTarget);
assert.ok(Math.abs(sampled.target.x - 5) < 1e-9);
assert.ok(Math.abs(sampled.target.y - 10) < 1e-9);
assert.ok(Math.abs(sampled.target.z - 15) < 1e-9);
assert.equal(sampled.headingDeg, 0);
assert.equal(sampled.pitchDeg, -40);
assert.equal(sampled.rangeM, 2000);
});
test('Bhote Koshi card summaries wrap on words instead of clipping mid-sentence', () => {
assert.deepEqual(
evidenceSummaryLines('Five geolocated public viewpoints document the flood passage.', 34, 2),
['Five geolocated public viewpoints', 'document the flood passage.'],
);
assert.deepEqual(evidenceSummaryLines('one two three four five', 9, 1), ['one two…']);
});
test('Bhote Koshi evidence timeline is ordered by phase and corridor chainage with one active beat', async () => {
const event = JSON.parse(await readFile(eventUrl, 'utf8'));
const timeline = buildEvidenceTimeline(event.evidenceSpine, event.reconstruction.corridor);
assert.deepEqual(
timeline.map((beat) => beat.observation.id),
[
'immediate-collapse-viewpoint',
'debris-dammed-lake',
'second-landslide',
'gyirong-border-gate',
'timure-cluster',
'syabru-besi',
'dhunche',
'mailung-upper-trishuli',
'mailung-bazzar',
'dandagaun',
'dandagaun-viewpoint',
'betrawati-bazaar',
'bhainse',
'bidur-trishuli-bridge',
'devighat-taadi-khola-bridge',
'charaudi',
],
);
assert.equal(timeline[0].activationProgress, 0);
assert.equal(timeline[1].activationProgress, 0.065);
assert.equal(timeline[2].activationProgress, 0.13);
assert.ok(timeline[3].activationProgress > timeline[2].activationProgress);
assert.ok(timeline[4].activationProgress > timeline[3].activationProgress);
assert.equal(timeline[5].activationProgress, 0.325);
for (let index = 0; index < timeline.length; index += 1) {
assert.equal(activeEvidenceIndex(timeline[index].activationProgress, timeline), index);
const midpoint = (
timeline[index].activationProgress + Math.min(1, timeline[index].deactivationProgress)
) / 2;
assert.equal(activeEvidenceIndex(midpoint, timeline), index);
const dwellSeconds = (
Math.min(1, timeline[index].deactivationProgress) - timeline[index].activationProgress
) * 90;
assert.ok(dwellSeconds >= 2, `${timeline[index].observation.id} dwell is too short`);
}
const anchors = timeline.map((_, index) => ({ x: index, y: index + 1, z: index + 2 }));
const cameraFrames = buildCinematicKeyframes(timeline, anchors);
assert.deepEqual(cameraFrames.slice(0, 16).map((frame) => frame.id), timeline.map((beat) => beat.observation.id));
assert.deepEqual(cameraFrames.slice(0, 16).map((frame) => frame.progress), timeline.map((beat) => beat.activationProgress));
assert.equal(cameraFrames.at(-1).id, 'charaudi');
assert.equal(cameraFrames.at(-1).progress, 1);
});
test('Bhote Koshi scene beats resolve stable ids inside their reveal windows', async () => {
const event = JSON.parse(await readFile(eventUrl, 'utf8'));
const timeline = buildEvidenceTimeline(event.evidenceSpine, event.reconstruction.corridor);
for (const beat of timeline) {
const progress = evidenceBeatProgress(beat.observation.id, timeline, 0.36);
assert.ok(progress > beat.activationProgress || beat.deactivationProgress <= beat.activationProgress);
assert.ok(progress < Math.min(1.000001, beat.deactivationProgress));
assert.equal(activeEvidenceIndex(progress, timeline), timeline.indexOf(beat));
}
assert.equal(evidenceBeatProgress('missing-beat', timeline), null);
});
test('Bhote Koshi scene shots continue the standalone evidence clock from Border Gate onward', async () => {
const event = JSON.parse(await readFile(eventUrl, 'utf8'));
const timeline = buildEvidenceTimeline(event.evidenceSpine, event.reconstruction.corridor);
const border = evidenceSequenceWindow('gyirong-border-gate', timeline);
assert.deepEqual(border, {
startProgress: timeline[3].activationProgress,
targetProgress: timeline[3].deactivationProgress,
});
for (const beatId of [
'timure-cluster',
'syabru-besi',
'bidur-trishuli-bridge',
]) {
const beat = timeline.find(({ observation }) => observation.id === beatId);
const window = evidenceSequenceWindow(beatId, timeline);
assert.equal(window.startProgress, beat.activationProgress);
assert.equal(window.targetProgress, Math.min(1, beat.deactivationProgress));
assert.ok(window.targetProgress > window.startProgress);
}
assert.equal(evidenceSequenceWindow('missing-beat', timeline), null);
});
test('Bhote Koshi evidence presentation reveals dot, leader, and card then tears down in reverse', () => {
const beat = { activationProgress: 0.2, deactivationProgress: 0.4 };
assert.deepEqual(evidenceBeatPresentation(0.2, beat), {
alpha: 0,
scale: 0.96,
leaderProgress: 0,
contentAlpha: 0,
localProgress: 0,
});
const revealed = evidenceBeatPresentation(0.2075, beat);
assert.equal(revealed.alpha, 1);
assert.equal(revealed.leaderProgress, 1);
assert.ok(revealed.contentAlpha > 0 && revealed.contentAlpha < 1);
assert.ok(revealed.scale > 0.96 && revealed.scale < 1);
const held = evidenceBeatPresentation(0.3, beat);
assert.equal(held.alpha, 1);
assert.equal(held.contentAlpha, 1);
assert.equal(held.scale, 1);
const receding = evidenceBeatPresentation(0.35, beat);
assert.equal(receding.alpha, 1, 'the terrain dot remains after card recession starts');
assert.ok(receding.contentAlpha < receding.leaderProgress);
assert.ok(receding.scale < held.scale);
const late = evidenceBeatPresentation(0.375, beat);
assert.equal(late.contentAlpha, 0, 'card content clears first');
assert.ok(late.leaderProgress > 0 && late.leaderProgress < late.alpha);
assert.equal(evidenceBeatPresentation(0.4, beat).alpha, 0);
});
test('Bhote Koshi evidence cards freeze useful landscape, portrait, and link-only footprints', () => {
assert.equal(evidenceCardLayout({
media: { posterUrl: 'poster.jpg', width: 1920, height: 1080 },
}).kind, 'landscape');
assert.deepEqual(
{
kind: evidenceCardLayout({
media: { videoPath: 'portrait.mp4', width: 240, height: 422 },
}).kind,
width: evidenceCardLayout({
media: { videoPath: 'portrait.mp4', width: 240, height: 422 },
}).thumbnailWidth,
height: evidenceCardLayout({
media: { videoPath: 'portrait.mp4', width: 240, height: 422 },
}).thumbnailHeight,
},
{ kind: 'portrait', width: 168, height: 260 },
);
assert.equal(evidenceCardLayout({ media: { posterUrl: null } }).kind, 'link');
});
test('Bhote Koshi gives YouTube and Facebook players priority over local review clips', () => {
assert.equal(usesProviderEmbeddedEvidence({
media: {
sourceUrl: 'https://www.youtube.com/watch?v=DbqRexFxv3k',
videoPath: 'evidence-01.mp4',
},
}), true);
assert.equal(usesProviderEmbeddedEvidence({
media: {
sourceUrl: 'https://www.facebook.com/reel/1571491657757829',
videoPath: 'evidence-15.mp4',
},
}), true);
assert.equal(usesProviderEmbeddedEvidence({
media: {
sourceUrl: 'https://example.com/evidence',
videoPath: 'evidence-01.mp4',
},
}), false);
});
test('Bhote Koshi beat navigation is deterministic in both directions and clamps at its ends', async () => {
const event = JSON.parse(await readFile(eventUrl, 'utf8'));
const timeline = buildEvidenceTimeline(event.evidenceSpine, event.reconstruction.corridor);
assert.equal(adjacentEvidenceIndex(0, timeline, -1), 0);
assert.equal(adjacentEvidenceIndex(0, timeline, 1), 1);
assert.equal(adjacentEvidenceIndex(timeline[3].activationProgress, timeline, -1), 2);
assert.equal(adjacentEvidenceIndex(timeline[3].activationProgress, timeline, 1), 4);
assert.equal(adjacentEvidenceIndex(1, timeline, 1), 15);
assert.equal(adjacentEvidenceIndex(0.5, [], 1), -1);
});
test('Bhote Koshi evidence fallback stays media-first and source-linked without card copy overload', async () => {
const event = JSON.parse(await readFile(eventUrl, 'utf8'));
const timure = event.evidenceSpine.find((observation) => observation.id === 'timure-cluster');
const originalDocument = globalThis.document;
const contexts = [];
globalThis.document = {
createElement() {
const text = [];
const context = {
text,
beginPath() {},
clearRect() {},
moveTo() {},
lineTo() {},
stroke() {},
fillRect() {},
fillText(value) { text.push(String(value)); },
drawImage() { throw new Error('fixture poster decode failure'); },
};
contexts.push(context);
return { width: 0, height: 0, getContext: () => context };
},
};
try {
const missingPosterFrame = createEvidenceFrame(timure);
const failedPosterFrame = createEvidenceFrame(timure, { width: 640, height: 400 });
assert.ok(missingPosterFrame);
assert.ok(failedPosterFrame);
for (const context of contexts) {
assert.ok(context.text.includes('NO PREVIEW'));
assert.ok(context.text.includes('TIMURE'));
assert.ok(context.text.includes('YOUTUBE SOURCE / OPEN'));
assert.doesNotMatch(context.text.join(' '), /CAPTURE TIME|GEOLOCATED|public viewpoints/);
}
assert.match(timure.media.sourceUrl, /^https:\/\//);
} finally {
globalThis.document = originalDocument;
}
});
test('Bhote Koshi evidence compositor contains portrait video from its decoded dimensions', async () => {
const event = JSON.parse(await readFile(eventUrl, 'utf8'));
const observation = event.evidenceSpine[0];
const drawCalls = [];
const context = {
beginPath() {},
clearRect() {},
moveTo() {},
lineTo() {},
stroke() {},
fillRect() {},
fillText() {},
drawImage(...args) { drawCalls.push(args); },
};
const layout = evidenceCardLayout(observation);
const canvas = {
width: layout.canvasWidth,
height: layout.canvasHeight,
_bhoteEvidenceLayout: layout,
getContext: () => context,
};
const video = { videoWidth: 240, videoHeight: 422, width: 0, height: 0 };
assert.equal(updateEvidenceFrame(canvas, observation, video, { videoActive: true }), true);
assert.equal(drawCalls.length, 1);
const [, destX, destY, destWidth, destHeight] = drawCalls[0];
assert.ok(destX > 0, 'portrait video keeps narrow side gutters instead of being cropped');
assert.equal(destY, 0);
assert.ok(destWidth < layout.canvasWidth);
assert.equal(destHeight, layout.mediaHeight);
assert.equal(context.textAlign, 'left', 'repeat paints cannot inherit the outside badge alignment');
assert.equal(updateEvidenceFrame(canvas, observation, video, { videoActive: true }), true);
assert.equal(context.textAlign, 'left');
});
test('Bhote Koshi failed direct enable restores the prior map and shared split', async () => {
const event = JSON.parse(await readFile(eventUrl, 'utf8'));
const mapCalls = [];
const overlayCalls = [];
const originalDocument = globalThis.document;
const originalWindow = globalThis.window;
let activeMap = 'photoreal';
let generation = 7;
const viewer = {
scene: { splitPosition: 0.37 },
imageryLayers: { remove() {} },
dataSources: { remove() {} },
};
const layer = createBhoteKoshiEventLayer({
eventLoader: async () => event,
imageryProviderFactory: async () => { throw new Error('fixture image failure'); },
overlayHost: {
setEntries() {},
setVisible(id, visible) { overlayCalls.push(['visible', id, visible]); },
clearSource(id) { overlayCalls.push(['clear', id]); },
},
renderHost: { request() {}, hold() {}, release() {} },
});
layer.attachMapStackController({
getActiveId: () => activeMap,
getSwitchGeneration: () => generation,
async setStack(id) {
activeMap = id;
generation += 1;
mapCalls.push(id);
},
});
globalThis.document = { documentElement: { style: { removeProperty() {} } } };
globalThis.window = { clearTimeout() {} };
try {
await assert.rejects(
layer.enable(viewer, { origin: 'scene' }),
/fixture image failure/,
);
assert.deepEqual(mapCalls, ['esri-imagery', 'photoreal']);
assert.equal(viewer.scene.splitPosition, 0.37);
assert.deepEqual(layer.getPlaybackState(), {
enabled: false,
playing: false,
progress: 0,
split: 0.5,
});
assert.ok(overlayCalls.some(([kind]) => kind === 'clear'));
} finally {
globalThis.document = originalDocument;
globalThis.window = originalWindow;
}
});
test('Bhote Koshi cancellation during the map switch restores the prior stack', async () => {
const event = JSON.parse(await readFile(eventUrl, 'utf8'));
const originalDocument = globalThis.document;
const originalWindow = globalThis.window;
const controller = new AbortController();
const viewer = eventViewer();
let activeMap = 'photoreal';
let generation = 12;
let finishSwitch;
const switchGate = new Promise((resolve) => { finishSwitch = resolve; });
const mapCalls = [];
const layer = createBhoteKoshiEventLayer({
eventLoader: async () => event,
overlayHost: { setEntries() {}, setVisible() {}, clearSource() {} },
renderHost: { request() {}, hold() {}, release() {} },
});
layer.attachMapStackController({
getActiveId: () => activeMap,
getSwitchGeneration: () => generation,
async setStack(id) {
activeMap = id;
generation += 1;
mapCalls.push(id);
if (id === 'esri-imagery') await switchGate;
},
});
installEventDom();
globalThis.window = { clearTimeout() {} };
try {
const enabling = layer.enable(viewer, { origin: 'scene', signal: controller.signal });
await new Promise((resolve) => setTimeout(resolve, 0));
controller.abort();
finishSwitch();
await assert.rejects(enabling, (error) => error?.name === 'AbortError');
assert.deepEqual(mapCalls, ['esri-imagery', 'photoreal']);
assert.equal(viewer.scene.splitPosition, 0.37);
assert.equal(layer.getPlaybackState().enabled, false);
} finally {
globalThis.document = originalDocument;
globalThis.window = originalWindow;
}
});
test('Bhote Koshi disable preserves an operator-selected map and tears down every surface', async () => {
const event = JSON.parse(await readFile(eventUrl, 'utf8'));
const originalDocument = globalThis.document;
const originalWindow = globalThis.window;
const { body, removedProperties } = installEventDom();
const clearedTimeouts = [];
globalThis.window = {
setTimeout: () => 71,
clearTimeout: (id) => clearedTimeouts.push(id),
};
const viewer = eventViewer();
let activeMap = 'photoreal';
let generation = 20;
const mapCalls = [];
const overlayCalls = [];
const renderCalls = [];
const layer = createBhoteKoshiEventLayer({
eventLoader: async () => event,
imageryProviderFactory: async (url) => ({ url }),
overlayHost: {
setEntries() {},
setVisible(id, visible) { overlayCalls.push(['visible', id, visible]); },
clearSource(id) { overlayCalls.push(['clear', id]); },
},
renderHost: {
request(reason) { renderCalls.push(reason); },
hold() {},
release() {},
},
});
layer.attachMapStackController({
getActiveId: () => activeMap,
getSwitchGeneration: () => generation,
async setStack(id) {
activeMap = id;
generation += 1;
mapCalls.push(id);
},
});
try {
assert.equal(await layer.enable(viewer, { origin: 'scene' }), true);
assert.equal(viewer._test.imagery.length, 2);
assert.equal(viewer._test.dataSources.length, 1);
assert.equal(body.children.length, 2);
activeMap = 'operator-choice';
generation += 1;
await layer.disable();
assert.deepEqual(mapCalls, ['esri-imagery']);
assert.equal(viewer._test.removedImagery.length, 2);
assert.equal(viewer._test.removedDataSources.length, 1);
assert.ok(body.children.every((element) => element.removed));
assert.equal(viewer.scene.splitPosition, 0.37);
assert.deepEqual(clearedTimeouts, [71]);
assert.ok(overlayCalls.some(([kind]) => kind === 'clear'));
assert.ok(removedProperties.includes('--bhote-koshi-split'));
assert.ok(renderCalls.includes('bhote-koshi-disable'));
} finally {
globalThis.document = originalDocument;
globalThis.window = originalWindow;
}
});
test('Bhote Koshi disable stops active playback and releases its global render hold', async () => {
const event = JSON.parse(await readFile(eventUrl, 'utf8'));
const originalDocument = globalThis.document;
const originalWindow = globalThis.window;
const originalPerformance = globalThis.performance;
const originalRequestAnimationFrame = globalThis.requestAnimationFrame;
const originalCancelAnimationFrame = globalThis.cancelAnimationFrame;
const { body } = installEventDom();
const holds = [];
const releases = [];
const cancelledFrames = [];
globalThis.window = { setTimeout: () => 71, clearTimeout() {} };
globalThis.performance = { now: () => 1000 };
globalThis.requestAnimationFrame = () => 33;
globalThis.cancelAnimationFrame = (id) => cancelledFrames.push(id);
const viewer = eventViewer();
const layer = createBhoteKoshiEventLayer({
eventLoader: async () => event,
imageryProviderFactory: async (url) => ({ url }),
overlayHost: { setEntries() {}, setVisible() {}, clearSource() {} },
renderHost: {
request() {},
hold(id) { holds.push(id); },
release(id) { releases.push(id); },
},
});
layer.attachMapStackController({
getActiveId: () => 'esri-imagery',
getSwitchGeneration: () => 1,
async setStack() {},
});
try {
await layer.enable(viewer, { origin: 'scene' });
const panel = body.children.find((element) => element.tagName === 'aside');
assert.equal(panel.querySelector('[data-action="play-scene"]').hidden, true);
assert.equal(await panel.querySelector('[data-action="play-scene"]').dispatch('click'), false);
panel.querySelector('[data-action="play"]').dispatch('click');
assert.equal(layer.getPlaybackState().playing, true);
await layer.disable();
assert.deepEqual(holds, ['bhote-koshi-playback']);
assert.deepEqual(releases, ['bhote-koshi-playback']);
assert.deepEqual(cancelledFrames, [33]);
assert.deepEqual(layer.getPlaybackState(), {
enabled: false,
playing: false,
progress: 0,
split: 0.5,
});
} finally {
globalThis.document = originalDocument;
globalThis.window = originalWindow;
globalThis.performance = originalPerformance;
globalThis.requestAnimationFrame = originalRequestAnimationFrame;
globalThis.cancelAnimationFrame = originalCancelAnimationFrame;
}
});
test('Bhote Koshi passive restores do not start off-screen playback', async () => {
const event = JSON.parse(await readFile(eventUrl, 'utf8'));
const originalDocument = globalThis.document;
const originalWindow = globalThis.window;
installEventDom();
const timers = [];
globalThis.window = {
setTimeout(handler, delay) {
timers.push({ handler, delay });
return timers.length;
},
clearTimeout() {},
};
const viewer = eventViewer();
const layer = createBhoteKoshiEventLayer({
eventLoader: async () => event,
imageryProviderFactory: async (url) => ({ url }),
embeddedMediaFactory: () => ({ show() {}, hide() {}, pause() {}, destroy() {} }),
overlayHost: { setEntries() {}, setVisible() {}, clearSource() {} },
renderHost: { request() {}, hold() {}, release() {} },
});
layer.attachMapStackController({
getActiveId: () => 'esri-imagery',
getSwitchGeneration: () => 1,
async setStack() {},
});
const manager = new LayerLifecycle(viewer);
manager.register(layer);
try {
assert.equal(await manager.setEnabled(layer.id, true, { origin: 'local-restore' }), true);
assert.deepEqual(timers, []);
assert.deepEqual(viewer._test.cameraLooks, []);
assert.equal(layer.getPlaybackState().playing, false);
} finally {
await manager.destroyLayer(layer.id);
globalThis.document = originalDocument;
globalThis.window = originalWindow;
}
});
test('Bhote Koshi standalone Scene keeps its Esri cinematic contract', async () => {
const event = JSON.parse(await readFile(eventUrl, 'utf8'));
const originalDocument = globalThis.document;
const originalWindow = globalThis.window;
const originalPerformance = globalThis.performance;
const originalRequestAnimationFrame = globalThis.requestAnimationFrame;
const originalCancelAnimationFrame = globalThis.cancelAnimationFrame;
installEventDom();
const timers = [];
globalThis.window = {
setTimeout(handler, delay) {
timers.push({ handler, delay });
return timers.length;
},
clearTimeout() {},
};
globalThis.performance = { now: () => 1000 };
globalThis.requestAnimationFrame = () => 44;
globalThis.cancelAnimationFrame = () => {};
const viewer = eventViewer();
let activeMap = 'photoreal';
const mapCalls = [];
const layer = createBhoteKoshiEventLayer({
eventLoader: async () => event,
imageryProviderFactory: async (url) => ({ url }),
terrainSampler: async (_viewer, observations) => (
observations.map((observation) => ({ height: observation.fallbackElevationM }))
),
mediaLoader: async () => {
const error = new Error('fixture media intentionally unavailable');
error.name = 'AbortError';
throw error;
},
overlayHost: { setEntries() {}, setVisible() {}, clearSource() {} },
renderHost: { request() {}, hold() {}, release() {} },
});
layer.attachMapStackController({
getActiveId: () => activeMap,
getSwitchGeneration: () => mapCalls.length,
async setStack(id) {
activeMap = id;
mapCalls.push(id);
},
});
try {
await layer.enable(viewer, { origin: 'scene' });
assert.deepEqual(mapCalls, ['esri-imagery']);
assert.equal(viewer._test.imagery.length, 2);
assert.equal(timers.length, 1);
assert.equal(timers[0].delay, 2400);
timers[0].handler();
assert.equal(layer.getPlaybackState().playing, true);
assert.equal(layer.getCinematicState().active, true);
assert.equal(viewer._test.cameraLooks.length, 1);
await layer.disable();
assert.deepEqual(mapCalls, ['esri-imagery', 'photoreal']);
} finally {
globalThis.document = originalDocument;
globalThis.window = originalWindow;
globalThis.performance = originalPerformance;
globalThis.requestAnimationFrame = originalRequestAnimationFrame;
globalThis.cancelAnimationFrame = originalCancelAnimationFrame;
}
});
test('Bhote Koshi scene beats preserve photoreal and never take camera ownership', async () => {
const event = JSON.parse(await readFile(eventUrl, 'utf8'));
const originalDocument = globalThis.document;
const originalWindow = globalThis.window;
const originalPerformance = globalThis.performance;
const originalRequestAnimationFrame = globalThis.requestAnimationFrame;
const originalCancelAnimationFrame = globalThis.cancelAnimationFrame;
const { body } = installEventDom();
const timers = [];
const openedUrls = [];
const cancelledFrames = [];
globalThis.window = {
setTimeout(handler, delay) {
timers.push({ handler, delay });
return timers.length;
},
clearTimeout() {},
open(url) {
openedUrls.push(url);
return { opener: 'fixture' };
},
};
globalThis.performance = { now: () => 1000 };
globalThis.requestAnimationFrame = () => 91;
globalThis.cancelAnimationFrame = (id) => cancelledFrames.push(id);
const viewer = eventViewer();
let activeMap = 'photoreal';
const mapCalls = [];
const overlayPublications = [];
const overlayVisibility = [];
const layer = createBhoteKoshiEventLayer({
eventLoader: async () => event,
imageryProviderFactory: async (url) => ({ url }),
terrainSampler: async (_viewer, observations) => (
observations.map((observation) => ({ height: observation.fallbackElevationM }))
),
mediaLoader: async () => {
const error = new Error('fixture media intentionally unavailable');
error.name = 'AbortError';
throw error;
},
overlayHost: {
setEntries(id, entries, options) {
overlayPublications.push({ id, entries, options });
},
setVisible(id, visible) {
overlayVisibility.push({ id, visible });
},
clearSource() {},
},
renderHost: { request() {}, hold() {}, release() {} },
});
layer.attachMapStackController({
getActiveId: () => activeMap,
getSwitchGeneration: () => 1,
async setStack(id) {
activeMap = id;
mapCalls.push(id);
},
});
const sceneCalls = [];
let finishScene;
let publishSceneClock;
layer.attachSceneController({
subscribeSceneClock(listener) {
publishSceneClock = listener;
return () => { publishSceneClock = null; };
},
continueScene(sceneId, shotId) {
sceneCalls.push({ action: 'scene', sceneId, shotId });
return new Promise((resolve) => { finishScene = resolve; });
},
replayShot(sceneId, shotId) {
sceneCalls.push({ action: 'play', sceneId, shotId });
return true;
},
loadAdjacentShot(sceneId, shotId, direction) {
sceneCalls.push({ action: 'adjacent', sceneId, shotId, direction });
return true;
},
seekScene(sceneId, progress) {
sceneCalls.push({ action: 'seek', sceneId, progress });
return true;
},
});
try {
assert.equal(layer.setParams({
presentation: 'scene-beat',
sceneSurface: 'panel-only',
progress: 0.34,
split: 0.62,
cinematic: true,
autoPlay: true,
sceneContext: {
sceneId: 'nepal-scene',
sceneTitle: 'Nepal Flood Incident',
shotId: 'global-context',
shotTitle: 'Global Incident Context',
shotIndex: 0,
shotCount: 14,
durationSec: 4,
holdSec: 0.9,
sceneElapsedSec: 0,
sceneDurationSec: 120,
sceneProgress: 0,
},
}, { origin: 'scene' }), true);
await layer.enable(viewer, { origin: 'scene' });
assert.deepEqual(mapCalls, []);
assert.equal(activeMap, 'photoreal');
assert.deepEqual(timers, []);
assert.equal(viewer._test.imagery.length, 2, 'the event surface keeps its cached imagery pair');
assert.ok(viewer._test.imagery.every((imageryLayer) => imageryLayer.show === false));
const panel = body.children.find((element) => element.tagName === 'aside');
assert.ok(panel);
const splitLine = body.children.find((element) => element.id === 'bhote-koshi-split-line');
assert.ok(splitLine);
assert.equal(splitLine.hidden, true);
assert.equal(panel.querySelector('[data-role="imagery-comparison"]').hidden, true);
assert.match(panel.querySelector('[data-role="caveat"]').textContent, /SCHEMATIC CORRIDOR/i);
assert.doesNotMatch(panel.querySelector('[data-role="caveat"]').textContent, /historical/i);
assert.equal(panel.querySelector('[data-role="imagery-cloud-note"]').hidden, true);
const sceneClock = panel.querySelector('[data-role="progress"]');
assert.equal(sceneClock.hidden, false);
assert.equal(sceneClock.disabled, false);
assert.equal(sceneClock.getAttribute('aria-label'), 'Seek Nepal scene clock');
assert.equal(panel.querySelector('[data-role="timeline-label"]').textContent, 'NEPAL SCENE CLOCK');
assert.equal(panel.querySelector('[data-role="time"]').textContent, '~0:00 / 2:00');
publishSceneClock({
sceneId: 'nepal-scene',
shotId: 'global-context',
shotIndex: 0,
shotCount: 14,
sceneElapsedSec: 30,
sceneDurationSec: 120,
sceneProgress: 0.25,
running: true,
});
assert.equal(sceneClock.value, '250');
assert.equal(panel.querySelector('[data-role="time"]').textContent, '~0:30 / 2:00');
assert.equal(panel.querySelector('[data-role="beat-index"]').textContent, '01 / 14');
assert.equal(panel.querySelector('[data-role="beat-title"]').textContent, 'Global Incident Context');
assert.equal(panel.querySelector('[data-action="play"]').textContent, '▶ PLAY SHOT');
const playScene = panel.querySelector('[data-action="play-scene"]');
assert.equal(playScene.hidden, false);
assert.equal(playScene.disabled, false);
assert.equal(playScene.textContent, '▶ PLAY SCENE');
assert.equal(playScene.getAttribute('aria-label'), 'Continue Nepal Flood Incident from next shot');
assert.equal(panel.querySelector('[data-action="cinematic"]').hidden, true);
assert.equal(panel.querySelector('[data-action="cinematic"]').disabled, true);
assert.equal(panel.querySelector('[data-action="corridor"]').hidden, false);
assert.equal(panel.querySelector('[data-action="corridor"]').disabled, false);
assert.doesNotMatch(panel.innerHTML, /data-action="witness"|◎ WITNESS/);
assert.match(panel.innerHTML, /data-action="open-source"/);
const fullStory = panel.querySelector('[data-action="story-replay"]');
assert.equal(fullStory.textContent, '↺ FULL STORY');
assert.equal(fullStory.hidden, true, 'PLAY SCENE owns authored scene playback');
assert.equal(fullStory.disabled, true);
assert.equal(panel.querySelector('[data-action="open-source"]').hidden, true);
assert.deepEqual(layer.getParams(), {
presentation: 'scene-beat',
sceneSurface: 'panel-only',
progress: 0.34,
split: 0.62,
});
assert.equal(viewer.scene.splitPosition, 0.62);
layer.setParams({
presentation: 'scene-beat',
sceneSurface: 'evidence-beat',
beatId: 'timure-cluster',
sceneControls: { imageryComparison: true },
}, { origin: 'scene' });
assert.equal(splitLine.hidden, false);
assert.equal(panel.querySelector('[data-role="imagery-comparison"]').hidden, false);
assert.ok(viewer._test.imagery.every((imageryLayer) => imageryLayer.show === true));
const splitHandle = splitLine.children.find(
(element) => element.className === 'bhote-koshi-split-handle',
);
assert.ok(splitHandle);
assert.equal(splitHandle.textContent, '↔');
assert.equal(splitHandle.getAttribute('aria-valuenow'), '62');
splitHandle.dispatch('pointerdown', {
button: 0,
clientX: 250,
pointerId: 4,
preventDefault() {},
});
splitHandle.dispatch('pointermove', {
clientX: 750,
pointerId: 4,
preventDefault() {},
});
splitHandle.dispatch('pointerup', { pointerId: 4 });
assert.equal(viewer.scene.splitPosition, 0.75);
assert.equal(splitHandle.getAttribute('aria-valuenow'), '75');
splitHandle.dispatch('keydown', {
key: 'ArrowLeft',
shiftKey: true,
preventDefault() {},
});
assert.equal(viewer.scene.splitPosition, 0.7);
layer.setParams({
presentation: 'scene-beat',
sceneSurface: 'panel-only',
progress: 0.34,
split: 0.62,
sceneControls: {},
}, { origin: 'scene' });
assert.equal(splitLine.hidden, true);
assert.ok(viewer._test.imagery.every((imageryLayer) => imageryLayer.show === false));
assert.equal(viewer.scene.splitPosition, 0.62);
assert.equal(overlayVisibility.at(-1).visible, false);
for (const id of [
'bhote-koshi-flood-halo',
'bhote-koshi-flood-core',
'bhote-koshi-surge-front',
]) {
assert.equal(viewer._test.dataSources[0].entities.getById(id).show, false);
}
panel.querySelector('[data-action="play"]').dispatch('click');
await Promise.resolve();
panel.querySelector('[data-action="next-beat"]').dispatch('click');
await Promise.resolve();
assert.deepEqual(sceneCalls, [
{ action: 'play', sceneId: 'nepal-scene', shotId: 'global-context' },
{ action: 'adjacent', sceneId: 'nepal-scene', shotId: 'global-context', direction: 1 },
]);
const run = playScene.dispatch('click');
assert.equal(playScene.disabled, true);
assert.equal(playScene.textContent, '… PLAYING SCENE');
assert.equal(panel.querySelector('[data-action="play"]').disabled, true);
assert.equal(panel.querySelector('[data-action="play"]').textContent, '▶ PLAY SHOT');
await playScene.dispatch('click');
assert.deepEqual(sceneCalls.at(-1), {
action: 'scene', sceneId: 'nepal-scene', shotId: 'global-context',
});
assert.equal(sceneCalls.length, 3, 'duplicate clicks cannot start another scene run');
finishScene({ started: true });
await run;
assert.equal(playScene.disabled, false);
assert.equal(playScene.textContent, '▶ PLAY SCENE');
sceneClock.value = '500';
sceneClock.dispatch('input');
assert.equal(panel.querySelector('[data-role="time"]').textContent, '~1:00 / 2:00');
sceneClock.dispatch('change');
await Promise.resolve();
assert.deepEqual(sceneCalls.at(-1), { action: 'seek', sceneId: 'nepal-scene', progress: 0.5 });
assert.equal(sceneCalls.length, 4);
layer.attachSceneController({ continueScene: async () => ({ started: false, reason: 'already-running' }) });
assert.deepEqual(await playScene.dispatch('click'), { started: false, reason: 'already-running' });
assert.equal(playScene.disabled, false, 'a refused start releases the button');
layer.attachSceneController({ continueScene: async () => { throw new Error('test scene failure'); } });
const originalWarn = console.warn;
const warnings = [];
try {
console.warn = (...args) => warnings.push(args);
assert.equal(await playScene.dispatch('click'), false);
assert.equal(playScene.disabled, false, 'a rejected start releases the button');
assert.equal(warnings.length, 1);
} finally {
console.warn = originalWarn;
}
layer.attachSceneController(null);
assert.equal(playScene.disabled, true);
await playScene.dispatch('click');
assert.equal(sceneCalls.length, 4);
assert.equal(layer.getPlaybackState().playing, false);
for (const reportButton of panel.querySelector('.bhote-event-report-list').children) {
reportButton.dispatch('click');
}
panel.querySelector('[data-action="geolocation-map"]').dispatch('click');
assert.deepEqual(openedUrls, [
...event.fieldReports.map(({ url }) => url),
event.geolocationMap.sourceUrl,
]);
assert.equal(layer.getPlaybackState().playing, false);
assert.equal(layer.getCinematicState().active, false);
assert.equal(viewer._test.cameraLooks.length, 0);
assert.deepEqual(mapCalls, []);
assert.equal(body.children.filter((element) => element.tagName === 'aside').length, 1);
assert.equal(viewer.scene.splitPosition, 0.62);
for (const beatId of [
'debris-dammed-lake',
'gyirong-border-gate',
'timure-cluster',
'syabru-besi',
'bidur-trishuli-bridge',
]) {
layer.setParams({
presentation: 'scene-beat',
sceneSurface: 'evidence-beat',
beatId,
beatReveal: 0.36,
}, { origin: 'scene' });
assert.equal(layer.getParams().beatId, beatId);
assert.equal(layer.getParams().sceneSurface, 'evidence-beat');
assert.equal(panel.querySelector('[data-action="open-source"]').hidden, false);
assert.equal(overlayVisibility.at(-1).visible, true);
const entries = overlayPublications.at(-1).entries;
const visibleCards = entries.filter((entry) => (
entry.id.startsWith('evidence-card-') && entry.position() != null
));
assert.deepEqual(visibleCards.map(({ id }) => id), [`evidence-card-${beatId}`]);
assert.ok(entries
.filter((entry) => entry.id.startsWith('evidence-breadcrumb-'))
.every((entry) => entry.position() == null));
assert.equal(viewer._test.dataSources.filter((source) => (
!viewer._test.removedDataSources.includes(source)
)).length, 1, 'surface transitions leave exactly one active event surface');
assert.equal(viewer._test.cameraLooks.length, 0, 'consecutive beats never write the camera');
}
panel.querySelector('[data-action="open-source"]').dispatch('click');
assert.equal(openedUrls.at(-1), event.evidenceSpine.find(({ id }) => id === 'bidur-trishuli-bridge').media.sourceUrl);
layer.setParams({
presentation: 'scene-beat',
progress: 0.2,
}, { origin: 'scene' });
assert.equal(layer.getParams().sceneSurface, 'panel-only', 'missing ownership fails closed');
assert.equal(panel.querySelector('[data-action="open-source"]').hidden, true);
assert.equal(overlayVisibility.at(-1).visible, false);
await layer.disable();
assert.deepEqual(
cancelledFrames,
[91],
'disable revokes the pending real-time scene-clock scrub frame',
);
assert.deepEqual(mapCalls, []);
assert.equal(viewer._test.removedDataSources.length, viewer._test.dataSources.length);
} finally {
globalThis.document = originalDocument;
globalThis.window = originalWindow;
globalThis.performance = originalPerformance;
globalThis.requestAnimationFrame = originalRequestAnimationFrame;
globalThis.cancelAnimationFrame = originalCancelAnimationFrame;
}
});
test('Nepal hybrid comparison preserves native split and terrain-clamped flood trail', async () => {
const event = JSON.parse(await readFile(eventUrl, 'utf8'));
const originalDocument = globalThis.document;
const originalWindow = globalThis.window;
installEventDom();
globalThis.window = { setTimeout: () => 71, clearTimeout() {} };
const viewer = eventViewer();
const mapCalls = [];
const comparisonCalls = [];
const layer = createBhoteKoshiEventLayer({
eventLoader: async () => event,
imageryProviderFactory: async (url) => ({ url }),
terrainSampler: async (_viewer, observations) => (
observations.map((observation) => ({ height: observation.fallbackElevationM }))
),
mediaLoader: async () => {
const error = new Error('fixture media intentionally unavailable');
error.name = 'AbortError';
throw error;
},
overlayHost: { setEntries() {}, setVisible() {}, clearSource() {} },
renderHost: { request() {}, hold() {}, release() {} },
});
layer.attachMapStackController({
getActiveId: () => 'photoreal',
getSwitchGeneration: () => 1,
async setStack(id) { mapCalls.push(id); },
async setTerrainComparison(rectangle) { comparisonCalls.push(rectangle); return true; },
clearTerrainComparison() { comparisonCalls.push(null); },
});
try {
layer.setParams({
presentation: 'scene-beat',
sceneSurface: 'evidence-beat',
beatId: 'gyirong-border-gate',
sceneControls: { imageryComparison: true },
}, { origin: 'scene' });
await layer.enable(viewer, { origin: 'scene' });
assert.deepEqual(mapCalls, [], 'comparison does not switch the authored Google base');
assert.deepEqual(comparisonCalls, [event.imagery.rectangle]);
assert.equal(viewer._test.imagery.length, 2);
assert.ok(viewer._test.imagery.every((imageryLayer) => imageryLayer.show === true));
assert.equal(viewer.scene.splitPosition, 0.5);
assert.equal(viewer._test.imagery[0].splitDirection, Cesium.SplitDirection.LEFT);
assert.equal(viewer._test.imagery[1].splitDirection, Cesium.SplitDirection.RIGHT);
layer.setParams({ split: 0.25 });
assert.equal(viewer.scene.splitPosition, 0.25);
const flood = viewer._test.dataSources[0].entities.getById('bhote-koshi-flood-core');
const surge = viewer._test.dataSources[0].entities.getById('bhote-koshi-surge-front');
assert.equal(flood.polyline.clampToGround.getValue(), true);
assert.equal(
surge.point.heightReference.getValue(),
Cesium.HeightReference.CLAMP_TO_GROUND,
);
layer.setParams({
presentation: 'scene-beat',
sceneSurface: 'evidence-beat',
beatId: 'timure',
sceneControls: { imageryComparison: false },
}, { origin: 'scene' });
assert.ok(viewer._test.imagery.every((imageryLayer) => imageryLayer.show === false));
assert.equal(comparisonCalls.at(-1), null);
const tileFlood = viewer._test.dataSources.at(-1).entities.getById('bhote-koshi-flood-core');
assert.equal(tileFlood.polyline.clampToGround.getValue(), true);
assert.equal(tileFlood.polyline.classificationType.getValue(), Cesium.ClassificationType.BOTH);
await layer.disable();
assert.equal(viewer._test.removedImagery.length, 2);
} finally {
globalThis.document = originalDocument;
globalThis.window = originalWindow;
}
});
test('Bhote Koshi event pack keeps observations separate from reconstruction', async () => {
const event = JSON.parse(await readFile(eventUrl, 'utf8'));
assert.equal(BHOTE_KOSHI_LAYER_ID, event.id);
assert.equal(event.imagery.before.observedAt, '2021-10-16');
assert.equal(event.imagery.after.observedAt, '2026-08-27');
assert.equal(
event.reconstruction.label,
'SCHEMATIC DOWNSTREAM PROGRESSION, NOT MODELED ARRIVAL TIME',
);
assert.match(event.imagery.before.label, /^2021 HISTORICAL REFERENCE/);
assert.match(event.imagery.after.label, /^2026 POST-EVENT/);
assert.equal(
event.geolocationMap.sourceUrl,
'https://x.com/geogeorgeology/status/2093632283442053371',
);
assert.ok(event.reconstruction.corridor.length >= 100);
assert.equal(event.witnessAnchors.length, 1);
assert.ok(Number.isFinite(event.witnessAnchors[0].elevationM));
assert.match(event.witnessAnchors[0].title, /SOURCE GEO/);
assert.equal(event.viewpoints.witness.imagery, 'before');
assert.equal(
event.fieldReports.filter((report) => report.status.includes('WORLD-PINNED')).length,
1,
);
assert.ok(event.fieldReports.some((report) => report.status.includes('UNVERIFIED')));
assert.equal(event.evidenceSpine.length, 16);
assert.equal(new Set(event.evidenceSpine.map(({ id }) => id)).size, 16);
assert.deepEqual(event.evidenceSpine.map(({ sequence }) => sequence),
Array.from({ length: 16 }, (_, index) => index + 1));
assert.equal(floodProgressForStoryProgress(0.195, event.reconstruction.storyFloodWindow), 0);
assert.equal(floodProgressForStoryProgress(0.39, event.reconstruction.storyFloodWindow), 1);
assert.equal(floodProgressForStoryProgress(0.9, event.reconstruction.storyFloodWindow), 1);
for (const { media } of event.evidenceSpine) {
for (const asset of [media.videoPath, media.posterPath].filter(Boolean)) {
assert.match(asset, /^evidence-\d+\.(mp4|jpg)$/);
assert.ok((await readFile(new URL(asset, eventUrl))).length > 0, asset);
}
}
assert.ok(event.evidenceSpine.every((observation) => observation.timing.capturedAt === null));
assert.ok(event.evidenceSpine.every((observation) => (
observation.timing.note === 'capture time unverified'
)));
assert.ok(event.evidenceSpine.every((observation) => /^https:\/\//.test(observation.media.sourceUrl)));
assert.ok(event.evidenceSpine.filter((observation) => observation.media.posterUrl).every((observation) => (
observation.media.posterUrl.startsWith('https://i.ytimg.com/')
)));
assert.equal(
event.evidenceSpine.find(({ id }) => id === 'gyirong-border-gate').media.clipInSec,
26,
);
assert.ok(event.evidenceSpine.every(({ media }) => !media.videoPath && !media.posterPath),
'Public event records must not reference privately cached social clips');
assert.equal(event.evidenceSpine.filter((observation) => (
observation.imageryCoverage === 'outside'
)).length, 13);
const timure = event.evidenceSpine.find((observation) => observation.id === 'timure-cluster');
assert.equal(timure.corroboration.kind, 'multi-source geolocation cluster');
assert.equal(timure.corroboration.sourceCount, 5);
assert.equal(timure.corroboration.displayedSourceCount, 1);
assert.equal(evidenceCorroborationLabel(timure), '5 GEOLOCATED SOURCE-MAP PLACEMENTS');
assert.match(event.reconstruction.caveat, /2021 historical reference versus 2026 post-event/i);
assert.match(event.reconstruction.caveat, /not modeled arrival time/i);
assert.match(event.reconstruction.caveat, /2025 Rasuwagadhi flood/i);
assert.equal(SCENE_RECIPES.some((recipe) => recipe.id === 'bhote-koshi-flood'), false);
assert.ok(SCENE_RECIPES.some((recipe) => recipe.id === 'bhote-koshi-nepal-scene'));
});
test('Bhote Koshi publishes the evidence spine once and scrubs through cards and breadcrumbs', async () => {
const event = JSON.parse(await readFile(eventUrl, 'utf8'));
const originalDocument = globalThis.document;
const originalWindow = globalThis.window;
const { body } = installEventDom();
globalThis.window = { setTimeout: () => 71, clearTimeout() {} };
const viewer = eventViewer();
const publications = [];
const layer = createBhoteKoshiEventLayer({
eventLoader: async () => event,
imageryProviderFactory: async (url) => ({ url }),
terrainSampler: async (_viewer, observations) => (
observations.map((observation) => ({ height: observation.fallbackElevationM }))
),
mediaLoader: async () => {
const error = new Error('fixture media intentionally unavailable');
error.name = 'AbortError';
throw error;
},
evidenceFrameFactory: (observation) => ({ fixture: observation.id }),
overlayHost: {
setEntries(id, entries, options) { publications.push({ id, entries, options }); },
setVisible() {},
clearSource() {},
},
renderHost: { request() {}, hold() {}, release() {} },
});
layer.attachMapStackController({
getActiveId: () => 'esri-imagery',
getSwitchGeneration: () => 1,
async setStack() {},
});
try {
await layer.enable(viewer, { origin: 'local-restore' });
assert.equal(publications.length, 1);
assert.equal(publications[0].entries.length, 32);
const cards = publications[0].entries.filter((entry) => entry.variant === 'thumbnail');
const breadcrumbs = publications[0].entries.filter((entry) => entry.variant === 'label');
assert.equal(cards.filter((entry) => entry.position()).length, 1);
assert.match(cards.find((entry) => entry.position()).id, /immediate-collapse/);
assert.equal(breadcrumbs.filter((entry) => entry.position()).length, 0);
assert.ok(cards.every((entry) => entry.requireImage && entry.image.stamp === 1));
const panel = body.children.find((element) => element.tagName === 'aside');
const progress = panel.querySelector('[data-role="progress"]');
progress.value = '285';
progress.dispatch('input');
assert.equal(publications.length, 1);
assert.equal(cards.filter((entry) => entry.position()).length, 1);
assert.match(cards.find((entry) => entry.position()).id, /timure-cluster/);
assert.equal(breadcrumbs.filter((entry) => entry.position()).length, 4);
await layer.disable();
} finally {
globalThis.document = originalDocument;
globalThis.window = originalWindow;
}
});
test('Bhote Koshi keeps one local fallback video active and unloads it on beat change and disable', async () => {
const event = JSON.parse(await readFile(eventUrl, 'utf8'));
// Synthetic clips exercise the player; public event data never bundles these.
event.evidenceSpine.find(({ id }) => id === 'immediate-collapse-viewpoint').media.videoPath = 'evidence-01.mp4';
event.evidenceSpine.find(({ id }) => id === 'timure-cluster').media.videoPath = 'evidence-04.mp4';
event.evidenceSpine.find(({ id }) => id === 'immediate-collapse-viewpoint').media.sourceUrl =
'https://example.com/immediate-collapse';
event.evidenceSpine.find(({ id }) => id === 'timure-cluster').media.sourceUrl =
'https://example.com/timure';
const originalDocument = globalThis.document;
const originalWindow = globalThis.window;
const { body } = installEventDom();
globalThis.window = { setTimeout: () => 71, clearTimeout() {} };
const viewer = eventViewer();
const videos = [];
const videoFactory = () => {
const listeners = new Map();
const video = {
muted: false,
playsInline: false,
preload: '',
loop: false,
paused: true,
currentTime: 0,
duration: 10,
playbackRate: 1,
pauseCalls: 0,
playCalls: 0,
loadCalls: 0,
removedSources: 0,
addEventListener(type, handler) { listeners.set(type, handler); },
load() { this.loadCalls += 1; },
pause() { this.pauseCalls += 1; this.paused = true; },
play() { this.playCalls += 1; this.paused = false; return Promise.resolve(); },
removeAttribute(name) {
if (name === 'src') {
this.src = '';
this.removedSources += 1;
}
},
dispatch(type) { listeners.get(type)?.(); },
};
videos.push(video);
return video;
};
const layer = createBhoteKoshiEventLayer({
eventLoader: async () => event,
imageryProviderFactory: async (url) => ({ url }),
terrainSampler: async (_viewer, observations) => (
observations.map((observation) => ({ height: observation.fallbackElevationM }))
),
mediaLoader: async () => {
const error = new Error('fixture media intentionally unavailable');
error.name = 'AbortError';
throw error;
},
evidenceFrameFactory: (observation) => ({ fixture: observation.id }),
evidenceFrameUpdater: () => true,
videoFactory,
overlayHost: { setEntries() {}, setVisible() {}, clearSource() {} },
renderHost: { request() {}, hold() {}, release() {} },
});
layer.attachMapStackController({
getActiveId: () => 'esri-imagery',
getSwitchGeneration: () => 1,
async setStack() {},
});
try {
await layer.enable(viewer, { origin: 'local-restore' });
assert.equal(videos.length, 1);
assert.match(videos[0].src, /evidence-01\.mp4$/);
videos[0].dispatch('loadeddata');
assert.equal(layer.getCinematicState().mediaReady, true);
const panel = body.children.find((element) => element.tagName === 'aside');
const progress = panel.querySelector('[data-role="progress"]');
progress.value = '285';
progress.dispatch('input');
assert.equal(videos.length, 2, 'the new beat replaces rather than stacks a decoder');
assert.equal(videos[0].removedSources, 1);
assert.ok(videos[0].pauseCalls >= 1);
assert.match(videos[1].src, /evidence-04\.mp4$/);
await layer.disable();
assert.equal(videos[1].removedSources, 1);
assert.ok(videos[1].pauseCalls >= 1);
assert.equal(layer.getCinematicState().mediaEvidenceId, null);
} finally {
globalThis.document = originalDocument;
globalThis.window = originalWindow;
}
});
test('Upper Valley Collapse autoplays its muted local fallback without starting the event clock', async () => {
const event = JSON.parse(await readFile(eventUrl, 'utf8'));
event.evidenceSpine.find(({ id }) => id === 'immediate-collapse-viewpoint').media.videoPath = 'evidence-01.mp4';
event.evidenceSpine.find(({ id }) => id === 'immediate-collapse-viewpoint').media.sourceUrl =
'https://example.com/immediate-collapse';
const originalDocument = globalThis.document;
const originalWindow = globalThis.window;
const { body } = installEventDom();
globalThis.window = { setTimeout: () => 71, clearTimeout() {} };
const viewer = eventViewer();
const videos = [];
let frameUpdates = 0;
const videoFactory = () => {
const listeners = new Map();
const frameCallbacks = new Map();
const video = {
muted: false,
playsInline: false,
preload: '',
loop: false,
paused: true,
currentTime: 0,
duration: 8.6,
playbackRate: 1,
playCalls: 0,
pauseCalls: 0,
cancelledFrameHandles: [],
addEventListener(type, handler) { listeners.set(type, handler); },
load() {},
pause() { this.pauseCalls += 1; this.paused = true; },
play() { this.playCalls += 1; this.paused = false; return Promise.resolve(); },
removeAttribute(name) { if (name === 'src') this.src = ''; },
requestVideoFrameCallback(callback) {
const handle = frameCallbacks.size + 1;
frameCallbacks.set(handle, callback);
return handle;
},
cancelVideoFrameCallback(handle) {
this.cancelledFrameHandles.push(handle);
frameCallbacks.delete(handle);
},
dispatch(type) { listeners.get(type)?.(); },
drawFrame(handle = [...frameCallbacks.keys()][0]) {
const callback = frameCallbacks.get(handle);
frameCallbacks.delete(handle);
callback?.();
},
};
videos.push(video);
return video;
};
const layer = createBhoteKoshiEventLayer({
eventLoader: async () => event,
imageryProviderFactory: async (url) => ({ url }),
terrainSampler: async (_viewer, observations) => (
observations.map((observation) => ({ height: observation.fallbackElevationM }))
),
mediaLoader: async () => {
const error = new Error('fixture media intentionally unavailable');
error.name = 'AbortError';
throw error;
},
evidenceFrameFactory: (observation) => ({ fixture: observation.id }),
evidenceFrameUpdater: () => { frameUpdates += 1; return true; },
videoFactory,
overlayHost: { setEntries() {}, setVisible() {}, clearSource() {} },
renderHost: { request() {}, hold() {}, release() {} },
});
layer.attachMapStackController({
getActiveId: () => 'photoreal',
getSwitchGeneration: () => 1,
async setStack() {},
});
try {
layer.setParams({
presentation: 'scene-beat',
sceneSurface: 'evidence-beat',
sceneControls: { evidenceMediaAutoplay: true },
beatId: 'immediate-collapse-viewpoint',
beatReveal: 0.36,
sceneContext: { sceneId: 'scene', shotId: 'shot' },
}, { origin: 'scene' });
layer.setSceneMediaPlayback({ sceneId: 'scene', shotId: 'shot', token: { cancelled: false } });
await layer.enable(viewer, { origin: 'scene' });
assert.equal(videos.length, 1);
const video = videos[0];
assert.equal(video.muted, true);
assert.equal(video.loop, true);
assert.equal(video.playCalls, 0, 'play waits until decoded media is ready');
video.dispatch('loadeddata');
assert.equal(video.playCalls, 1);
assert.equal(video.playbackRate, 1);
assert.equal(layer.getPlaybackState().playing, false);
const updatesBeforeFrame = frameUpdates;
video.currentTime = 1.2;
video.drawFrame();
assert.ok(frameUpdates > updatesBeforeFrame, 'decoded frames repaint the anchored callout');
layer.setParams({ presentation: 'scene-beat', progress: 0.2 }, { origin: 'scene' });
assert.ok(video.pauseCalls >= 1);
assert.ok(video.cancelledFrameHandles.length >= 1);
assert.equal(layer.getCinematicState().mediaEvidenceId, null);
assert.equal(body.children.find((element) => element.tagName === 'aside')?.removed, false);
await layer.disable();
} finally {
globalThis.document = originalDocument;
globalThis.window = originalWindow;
}
});
test('Debris-Dammed Lake card opens its source and is armed for approved local video autoplay', async () => {
const event = JSON.parse(await readFile(eventUrl, 'utf8'));
const originalDocument = globalThis.document;
const originalWindow = globalThis.window;
installEventDom();
const opened = [];
globalThis.window = {
setTimeout: () => 71,
clearTimeout() {},
open(...args) {
opened.push(args);
return { opener: 'fixture' };
},
};
const viewer = eventViewer();
let activeCard = null;
const layer = createBhoteKoshiEventLayer({
eventLoader: async () => event,
imageryProviderFactory: async (url) => ({ url }),
terrainSampler: async (_viewer, observations) => (
observations.map((observation) => ({ height: observation.fallbackElevationM }))
),
mediaLoader: async () => {
const error = new Error('fixture media intentionally unavailable');
error.name = 'AbortError';
throw error;
},
overlayHost: {
setEntries(_id, entries) {
activeCard = entries.find((entry) => entry.id === 'evidence-card-debris-dammed-lake');
},
setVisible() {},
clearSource() {},
hitTest(_x, _y, options) {
if (!activeCard || options?.filter?.(activeCard) === false) return null;
return { entry: activeCard };
},
},
renderHost: { request() {}, hold() {}, release() {} },
});
layer.attachMapStackController({
getActiveId: () => 'photoreal',
getSwitchGeneration: () => 1,
async setStack() {},
});
try {
layer.setParams({
presentation: 'scene-beat',
sceneSurface: 'evidence-beat',
sceneControls: { evidenceMediaAutoplay: true },
beatId: 'debris-dammed-lake',
beatReveal: 0.36,
}, { origin: 'scene' });
await layer.enable(viewer, { origin: 'scene' });
const lease = claimPointer('draw');
try {
viewer._test.canvas.dispatch('click', { offsetX: 640, offsetY: 420 });
assert.equal(opened.length, 0, 'drawing owns the pointer over evidence cards');
} finally {
releasePointer(lease);
}
viewer._test.canvas.dispatch('click', { offsetX: 640, offsetY: 420 });
assert.deepEqual(opened, [[
event.evidenceSpine[1].media.sourceUrl,
'_blank',
'noopener,noreferrer',
]]);
assert.equal(opened.length, 1);
await layer.disable();
viewer._test.canvas.dispatch('click', { offsetX: 640, offsetY: 420 });
assert.equal(opened.length, 1, 'disabled layer releases card click ownership');
} finally {
globalThis.document = originalDocument;
globalThis.window = originalWindow;
}
});
test('Nepal evidence shots resume the standalone callout and flood sequence from Border Gate', async () => {
const event = JSON.parse(await readFile(eventUrl, 'utf8'));
const timeline = buildEvidenceTimeline(event.evidenceSpine, event.reconstruction.corridor);
const originalDocument = globalThis.document;
const originalWindow = globalThis.window;
const originalPerformance = globalThis.performance;
const originalRequestAnimationFrame = globalThis.requestAnimationFrame;
const originalCancelAnimationFrame = globalThis.cancelAnimationFrame;
installEventDom();
let now = 1000;
let nextFrameId = 0;
const frames = new Map();
const cancelledFrames = [];
globalThis.window = { setTimeout: () => 71, clearTimeout() {} };
globalThis.performance = { now: () => now };
globalThis.requestAnimationFrame = (callback) => {
const id = ++nextFrameId;
frames.set(id, callback);
return id;
};
globalThis.cancelAnimationFrame = (id) => {
cancelledFrames.push(id);
frames.delete(id);
};
const viewer = eventViewer();
let entries = [];
const embeddedCalls = [];
const holds = [];
const releases = [];
const layer = createBhoteKoshiEventLayer({
eventLoader: async () => event,
imageryProviderFactory: async (url) => ({ url }),
terrainSampler: async (_viewer, observations) => (
observations.map((observation) => ({ height: observation.fallbackElevationM }))
),
mediaLoader: async () => {
const error = new Error('fixture media intentionally unavailable');
error.name = 'AbortError';
throw error;
},
overlayHost: {
setEntries(_id, published) { entries = published; },
setVisible() {},
clearSource() {},
},
renderHost: {
request() {},
hold(id) { holds.push(id); },
release(id) { releases.push(id); },
},
embeddedMediaFactory: () => ({
warm(options) {
embeddedCalls.push(['warm', options?.observation?.id || null]);
return Boolean(options?.observation);
},
show(options) {
embeddedCalls.push(['show', options?.observation?.id || null]);
return false;
},
hide() { embeddedCalls.push(['hide']); },
destroy() {},
}),
});
layer.attachMapStackController({
getActiveId: () => 'photoreal',
getSwitchGeneration: () => 1,
async setStack() {},
});
const params = {
presentation: 'scene-beat',
sceneSurface: 'evidence-beat',
beatId: 'gyirong-border-gate',
beatReveal: 0.36,
sceneContext: { sceneId: 'scene', shotId: 'shot' },
sceneControls: {
evidenceSequence: true,
evidenceSequenceDurationSec: 0.9,
deferEvidenceUntilCameraSettled: true,
evidenceRevealDurationSec: 0.9,
cameraSettled: false,
},
};
try {
await layer.init(viewer);
layer.setParams(params, { origin: 'scene' });
layer.setSceneMediaPlayback({ sceneId: 'scene', shotId: 'shot', token: { cancelled: false } });
await layer.enable(viewer, { origin: 'scene' });
const card = entries.find(({ id }) => id === 'evidence-card-gyirong-border-gate');
const flood = viewer._test.dataSources[0].entities.getById('bhote-koshi-flood-core');
const surge = viewer._test.dataSources[0].entities.getById('bhote-koshi-surge-front');
assert.equal(card.position(), null, 'the evidence card is absent during the camera flight');
assert.equal(flood.show, false);
assert.equal(surge.show, false);
assert.ok(
embeddedCalls.some(([action, id]) => action === 'warm' && id === 'gyirong-border-gate'),
'the upcoming Border Gate provider player warms while the camera is travelling',
);
assert.equal(
embeddedCalls.some(([action]) => action === 'show'),
false,
'warming does not expose or autoplay the embedded card before camera settle',
);
layer.setParams({
...params,
sceneControls: { ...params.sceneControls, cameraSettled: true },
}, { origin: 'scene' });
assert.ok(
embeddedCalls.some(([action, id]) => action === 'show' && id === 'gyirong-border-gate'),
'camera settle promotes the warmed source into the visible callout',
);
assert.ok(card.position(), 'camera settle publishes the anchored card');
assert.equal(card.sourceAlpha(), 0, 'the card starts from a hidden reveal state');
assert.ok(holds.includes('bhote-koshi-scene-beat-reveal'));
now = 1050;
const earlyReveal = frames.get(Math.max(...frames.keys()));
frames.delete(Math.max(...frames.keys()));
earlyReveal(now);
assert.ok(card.sourceAlpha() > 0 && card.sourceAlpha() < 1);
assert.equal(flood.show, true, 'the amber corridor grows with the callout reveal');
assert.equal(surge.show, true, 'the amber surge circle advances with the corridor');
now = 1900;
const finalFrameId = Math.max(...frames.keys());
const finalFrame = frames.get(finalFrameId);
frames.delete(finalFrameId);
finalFrame(now);
assert.equal(
layer.getPlaybackState().progress,
timeline.find(({ observation }) => observation.id === 'timure-cluster').activationProgress,
'the Border Gate surge reaches the next evidence point',
);
assert.equal(card.sourceAlpha(), 0, 'the Border Gate card recedes at the next beat');
assert.ok(releases.includes('bhote-koshi-scene-beat-reveal'));
const timureBeat = timeline.find(({ observation }) => observation.id === 'timure-cluster');
layer.setParams({
presentation: 'scene-beat',
sceneSurface: 'evidence-beat',
beatId: 'timure-cluster',
beatReveal: 0.36,
sceneControls: {
imageryComparison: true,
evidenceSequence: true,
evidenceSequenceDurationSec: 4,
},
}, { origin: 'scene' });
const timureCard = entries.find(({ id }) => id === 'evidence-card-timure-cluster');
assert.equal(layer.getPlaybackState().progress, timureBeat.activationProgress);
assert.equal(timureCard.sourceAlpha(), 0, 'the next callout begins below its activation');
now = 3900;
const timureReveal = frames.get(Math.max(...frames.keys()));
frames.delete(Math.max(...frames.keys()));
timureReveal(now);
assert.ok(timureCard.sourceAlpha() > 0, 'the Timure callout enters during its authored shot');
now = 5900;
const timureFinalId = Math.max(...frames.keys());
const timureFinal = frames.get(timureFinalId);
frames.delete(timureFinalId);
timureFinal(now);
assert.equal(layer.getPlaybackState().progress, timureBeat.deactivationProgress);
assert.equal(timureCard.sourceAlpha(), 0);
await layer.disable();
} finally {
globalThis.document = originalDocument;
globalThis.window = originalWindow;
globalThis.performance = originalPerformance;
globalThis.requestAnimationFrame = originalRequestAnimationFrame;
globalThis.cancelAnimationFrame = originalCancelAnimationFrame;
}
});
test('Bhote Koshi beat controls keep cards, flood, camera, and panel on the one clock', async () => {
const event = JSON.parse(await readFile(eventUrl, 'utf8'));
const timeline = buildEvidenceTimeline(event.evidenceSpine, event.reconstruction.corridor);
const originalDocument = globalThis.document;
const originalWindow = globalThis.window;
const originalPerformance = globalThis.performance;
const originalRequestAnimationFrame = globalThis.requestAnimationFrame;
const originalCancelAnimationFrame = globalThis.cancelAnimationFrame;
const { body } = installEventDom();
const openedUrls = [];
const cancelledFrames = [];
globalThis.window = {
setTimeout: () => 71,
clearTimeout() {},
open(url) {
openedUrls.push(url);
return { opener: 'fixture' };
},
};
globalThis.performance = { now: () => 1000 };
globalThis.requestAnimationFrame = () => 91;
globalThis.cancelAnimationFrame = (id) => cancelledFrames.push(id);
const viewer = eventViewer();
const publications = [];
const layer = createBhoteKoshiEventLayer({
eventLoader: async () => event,
imageryProviderFactory: async (url) => ({ url }),
terrainSampler: async (_viewer, observations) => (
observations.map((observation) => ({ height: observation.fallbackElevationM }))
),
mediaLoader: async () => {
const error = new Error('fixture media intentionally unavailable');
error.name = 'AbortError';
throw error;
},
evidenceFrameFactory: (observation) => ({ fixture: observation.id }),
overlayHost: {
setEntries(id, entries, options) { publications.push({ id, entries, options }); },
setVisible() {},
clearSource() {},
},
renderHost: { request() {}, hold() {}, release() {} },
});
layer.attachMapStackController({
getActiveId: () => 'esri-imagery',
getSwitchGeneration: () => 1,
async setStack() {},
});
try {
await layer.enable(viewer, { origin: 'local-restore' });
const panel = body.children.find((element) => element.tagName === 'aside');
assert.match(panel.innerHTML, /2021 HISTORICAL REFERENCE \/ 2026 POST-EVENT/);
assert.match(panel.innerHTML, /RECONSTRUCTION CLOCK/);
assert.match(panel.innerHTML, /OPEN PUBLIC MAP/);
const entries = publications[0].entries;
const cards = entries.filter((entry) => entry.variant === 'thumbnail');
const breadcrumbs = entries.filter((entry) => entry.variant === 'label');
const next = panel.querySelector('[data-action="next-beat"]');
const previous = panel.querySelector('[data-action="previous-beat"]');
next.dispatch('click');
assert.equal(layer.getPlaybackState().progress, timeline[1].activationProgress);
assert.equal(layer.getPlaybackState().playing, false);
assert.equal(layer.getCinematicState().active, true);
assert.match(cards.find((entry) => entry.position()).id, /debris-dammed-lake/);
assert.equal(breadcrumbs.filter((entry) => entry.position()).length, 1);
assert.equal(panel.querySelector('[data-role="beat-index"]').textContent, '02 / 16');
assert.equal(panel.querySelector('[data-role="beat-title"]').textContent, 'Dammed lake');
next.dispatch('click');
next.dispatch('click');
next.dispatch('click');
assert.equal(layer.getPlaybackState().progress, timeline[4].activationProgress);
assert.equal(layer.getCinematicState().evidenceId, 'timure-cluster');
assert.match(cards.find((entry) => entry.position()).id, /timure-cluster/);
assert.equal(breadcrumbs.filter((entry) => entry.position()).length, 4);
assert.equal(
panel.querySelector('[data-role="beat-meta"]').textContent,
'PROPAGATION / TIME UNVERIFIED / 5 SOURCES',
);
const flood = viewer._test.dataSources[0].entities.getById('bhote-koshi-flood-core');
assert.ok(flood.polyline.positions.getValue().length > 1);
assert.equal(
body.children.some((element) => element.id === 'bhote-koshi-story-strap'),
false,
);
panel.querySelector('[data-action="open-source"]').dispatch('click');
assert.deepEqual(openedUrls, [timeline[4].observation.media.sourceUrl]);
panel.querySelector('[data-action="geolocation-map"]').dispatch('click');
assert.deepEqual(openedUrls, [
timeline[4].observation.media.sourceUrl,
event.geolocationMap.sourceUrl,
]);
previous.dispatch('click');
assert.equal(layer.getPlaybackState().progress, timeline[3].activationProgress);
assert.equal(layer.getCinematicState().evidenceId, 'gyirong-border-gate');
assert.equal(panel.querySelector('[data-role="beat-index"]').textContent, '04 / 16');
assert.equal(publications.length, 1, 'beat navigation never republishes overlay entries');
viewer._test.canvas.dispatch('pointerdown');
assert.equal(layer.getCinematicState().active, false);
const cameraLookCount = viewer._test.cameraLooks.length;
const progress = panel.querySelector('[data-role="progress"]');
progress.value = '500';
progress.dispatch('input');
assert.equal(layer.getCinematicState().active, false, 'manual release survives slider changes');
assert.equal(viewer._test.cameraLooks.length, cameraLookCount);
previous.dispatch('click');
assert.equal(layer.getCinematicState().active, true, 'explicit beat navigation reclaims camera');
assert.equal(layer.getCinematicState().evidenceId, 'dhunche');
panel.querySelector('[data-action="story-replay"]').dispatch('click');
assert.equal(layer.getPlaybackState().progress, 0);
assert.equal(layer.getPlaybackState().playing, true);
assert.equal(layer.getCinematicState().active, true);
assert.equal(layer.getCinematicState().evidenceId, 'immediate-collapse-viewpoint');
assert.equal(panel.querySelector('[data-role="beat-index"]').textContent, '01 / 16');
assert.equal(publications.length, 1);
await layer.disable();
assert.deepEqual(cancelledFrames, [91]);
assert.equal(viewer._test.canvas._listeners.size, 0);
} finally {
globalThis.document = originalDocument;
globalThis.window = originalWindow;
globalThis.performance = originalPerformance;
globalThis.requestAnimationFrame = originalRequestAnimationFrame;
globalThis.cancelAnimationFrame = originalCancelAnimationFrame;
}
});
test('Bhote Koshi repeated event frames perform no panel lookup, DOM write, or camera churn', async () => {
const event = JSON.parse(await readFile(eventUrl, 'utf8'));
const originalDocument = globalThis.document;
const originalWindow = globalThis.window;
const originalPerformance = globalThis.performance;
const originalRequestAnimationFrame = globalThis.requestAnimationFrame;
const originalCancelAnimationFrame = globalThis.cancelAnimationFrame;
const { body } = installEventDom();
globalThis.window = { setTimeout: () => 71, clearTimeout() {} };
globalThis.performance = { now: () => 1000 };
globalThis.requestAnimationFrame = () => 93;
globalThis.cancelAnimationFrame = () => {};
const viewer = eventViewer();
const layer = createBhoteKoshiEventLayer({
eventLoader: async () => event,
imageryProviderFactory: async (url) => ({ url }),
terrainSampler: async (_viewer, observations) => (
observations.map((observation) => ({ height: observation.fallbackElevationM }))
),
mediaLoader: async () => {
const error = new Error('fixture media intentionally unavailable');
error.name = 'AbortError';
throw error;
},
overlayHost: { setEntries() {}, setVisible() {}, clearSource() {} },
renderHost: { request() {}, hold() {}, release() {} },
});
layer.attachMapStackController({
getActiveId: () => 'esri-imagery',
getSwitchGeneration: () => 1,
async setStack() {},
});
try {
await layer.enable(viewer, { origin: 'local-restore' });
const panel = body.children.find((element) => element.tagName === 'aside');
const cinematic = panel.querySelector('[data-action="cinematic"]');
const progress = panel.querySelector('[data-role="progress"]');
cinematic.dispatch('click');
progress.value = '500';
progress.dispatch('input');
const queryCount = panel._queryCount;
const writeCount = [...panel._selectors.values()].reduce((sum, element) => (
sum + element._writeCount
), 0);
const cameraLookCount = viewer._test.cameraLooks.length;
for (let index = 0; index < 20; index += 1) progress.dispatch('input');
assert.equal(panel._queryCount, queryCount, 'hot path uses cached panel references');
assert.equal(
[...panel._selectors.values()].reduce((sum, element) => sum + element._writeCount, 0),
writeCount,
'unchanged frames do not rewrite panel state',
);
assert.equal(
viewer._test.cameraLooks.length,
cameraLookCount,
'unchanged progress does not reapply the cinematic camera object',
);
await layer.disable();
} finally {
globalThis.document = originalDocument;
globalThis.window = originalWindow;
globalThis.performance = originalPerformance;
globalThis.requestAnimationFrame = originalRequestAnimationFrame;
globalThis.cancelAnimationFrame = originalCancelAnimationFrame;
}
});
test('Bhote Koshi frame path keeps evidence lookup scalar and wrapper-free', async () => {
const source = await readFile(eventModuleUrl, 'utf8');
assert.doesNotMatch(source, /#ffd166/i, 'event UI no longer publishes the prototype yellow');
assert.doesNotMatch(
source,
/function\s+activeEvidenceEntry\s*\(/,
'the event frame path must not restore an object-returning evidence wrapper',
);
assert.doesNotMatch(
source,
/return\s*\{\s*(?:index\s*,\s*observation|observation\s*,\s*index)\s*\}/,
'evidence lookup must not allocate an index/observation pair',
);
assert.match(
source,
/const beatIndex = currentEvidenceIndex\(\);\s*const sceneBeatIndex = sceneDirected\s*\? _evidenceTimeline\.findIndex\(\s*\(\{ observation: item \}\) => item\.id === _sceneBeatId,?\s*\)\s*:\s*beatIndex;\s*const observation =\s*_evidenceTimeline\[sceneBeatIndex\]\?\.observation \|\| null;/,
'syncPanel must resolve authored scene beats to one scalar index before reading the observation',
);
});
test('Bhote Koshi cinematic camera stays synchronized through pause and deterministic scrubbing', async () => {
const event = JSON.parse(await readFile(eventUrl, 'utf8'));
const originalDocument = globalThis.document;
const originalWindow = globalThis.window;
const originalPerformance = globalThis.performance;
const originalRequestAnimationFrame = globalThis.requestAnimationFrame;
const originalCancelAnimationFrame = globalThis.cancelAnimationFrame;
const { body } = installEventDom();
globalThis.window = { setTimeout: () => 71, clearTimeout() {} };
globalThis.performance = { now: () => 1000 };
globalThis.requestAnimationFrame = () => 33;
globalThis.cancelAnimationFrame = () => {};
const viewer = eventViewer();
const layer = createBhoteKoshiEventLayer({
eventLoader: async () => event,
imageryProviderFactory: async (url) => ({ url }),
terrainSampler: async (_viewer, observations) => (
observations.map((observation) => ({ height: observation.fallbackElevationM }))
),
mediaLoader: async () => {
const error = new Error('fixture media intentionally unavailable');
error.name = 'AbortError';
throw error;
},
overlayHost: { setEntries() {}, setVisible() {}, clearSource() {} },
renderHost: { request() {}, hold() {}, release() {} },
});
layer.attachMapStackController({
getActiveId: () => 'esri-imagery',
getSwitchGeneration: () => 1,
async setStack() {},
});
try {
await layer.enable(viewer, { origin: 'local-restore' });
const panel = body.children.find((element) => element.tagName === 'aside');
panel.querySelector('[data-action="cinematic"]').dispatch('click');
assert.equal(layer.getCinematicState().active, true);
assert.equal(layer.getPlaybackState().playing, true);
assert.equal(viewer._test.cameraLooks.length, 1);
assert.equal(
body.children.some((element) => element.id === 'bhote-koshi-story-strap'),
false,
);
panel.querySelector('[data-action="play"]').dispatch('click');
assert.equal(layer.getPlaybackState().playing, false);
const progress = panel.querySelector('[data-role="progress"]');
progress.value = '500';
progress.dispatch('input');
const firstPose = viewer._test.cameraLooks.at(-1);
progress.dispatch('input');
assert.deepEqual(viewer._test.cameraLooks.at(-1), firstPose);
assert.deepEqual(layer.getCinematicState(), {
active: true,
progress: 0.5,
evidenceId: 'mailung-upper-trishuli',
mediaReady: false,
mediaEvidenceId: null,
});
viewer._test.canvas.dispatch('pointerdown');
assert.equal(layer.getCinematicState().active, false);
assert.equal(viewer._test.cameraTransforms.length, 1);
await layer.disable();
assert.equal(viewer._test.canvas._listeners.size, 0);
} finally {
globalThis.document = originalDocument;
globalThis.window = originalWindow;
globalThis.performance = originalPerformance;
globalThis.requestAnimationFrame = originalRequestAnimationFrame;
globalThis.cancelAnimationFrame = originalCancelAnimationFrame;
}
});
test('Bhote Koshi replay restores cinematic ownership unless the operator released it', async () => {
const event = JSON.parse(await readFile(eventUrl, 'utf8'));
const originalDocument = globalThis.document;
const originalWindow = globalThis.window;
const originalPerformance = globalThis.performance;
const originalRequestAnimationFrame = globalThis.requestAnimationFrame;
const originalCancelAnimationFrame = globalThis.cancelAnimationFrame;
const { body } = installEventDom();
const animationCallbacks = [];
const cancelledFrames = [];
const releases = [];
globalThis.window = { setTimeout: () => 71, clearTimeout() {} };
globalThis.performance = { now: () => 1000 };
globalThis.requestAnimationFrame = (callback) => {
animationCallbacks.push(callback);
return animationCallbacks.length;
};
globalThis.cancelAnimationFrame = (id) => cancelledFrames.push(id);
const viewer = eventViewer();
const layer = createBhoteKoshiEventLayer({
eventLoader: async () => event,
imageryProviderFactory: async (url) => ({ url }),
terrainSampler: async (_viewer, observations) => (
observations.map((observation) => ({ height: observation.fallbackElevationM }))
),
mediaLoader: async () => {
const error = new Error('fixture media intentionally unavailable');
error.name = 'AbortError';
throw error;
},
overlayHost: { setEntries() {}, setVisible() {}, clearSource() {} },
renderHost: {
request() {},
hold() {},
release(id) { releases.push(id); },
},
});
layer.attachMapStackController({
getActiveId: () => 'esri-imagery',
getSwitchGeneration: () => 1,
async setStack() {},
});
try {
await layer.enable(viewer, { origin: 'local-restore' });
const panel = body.children.find((element) => element.tagName === 'aside');
panel.querySelector('[data-action="cinematic"]').dispatch('click');
animationCallbacks[0](91001);
assert.equal(layer.getPlaybackState().progress, 1);
assert.equal(layer.getPlaybackState().playing, false);
assert.equal(layer.getCinematicState().active, false);
assert.equal(viewer._test.cameraTransforms.length, 1);
assert.deepEqual(releases, ['bhote-koshi-playback']);
const play = panel.querySelector('[data-action="play"]');
assert.equal(play.textContent, '↺ REPLAY CINEMATIC');
play.dispatch('click');
assert.equal(layer.getCinematicState().active, true);
assert.equal(layer.getPlaybackState().progress, 0);
assert.equal(layer.getPlaybackState().playing, true);
assert.equal(
body.children.some((element) => element.id === 'bhote-koshi-story-strap'),
false,
);
panel.querySelector('[data-action="cinematic"]').dispatch('click');
assert.equal(layer.getCinematicState().active, false);
assert.equal(viewer._test.cameraTransforms.length, 2);
animationCallbacks[1](91001);
assert.equal(play.textContent, '↺ REPLAY');
const cameraLookCount = viewer._test.cameraLooks.length;
play.dispatch('click');
assert.equal(layer.getPlaybackState().playing, true);
assert.equal(layer.getCinematicState().active, false);
assert.equal(viewer._test.cameraLooks.length, cameraLookCount);
await layer.disable();
assert.equal(layer.getCinematicState().active, false);
assert.equal(viewer._test.cameraTransforms.length, 2);
assert.deepEqual(cancelledFrames, [1, 2, 3]);
assert.equal(viewer._test.canvas._listeners.size, 0);
} finally {
globalThis.document = originalDocument;
globalThis.window = originalWindow;
globalThis.performance = originalPerformance;
globalThis.requestAnimationFrame = originalRequestAnimationFrame;
globalThis.cancelAnimationFrame = originalCancelAnimationFrame;
}
});
test('Bhote Koshi closes a poster that resolves after disable', async () => {
const event = JSON.parse(await readFile(eventUrl, 'utf8'));
event.evidenceSpine = event.evidenceSpine.slice(0, 1);
event.evidenceSpine[0].media.posterPath = 'evidence-09.jpg';
const originalDocument = globalThis.document;
const originalWindow = globalThis.window;
installEventDom();
globalThis.window = { setTimeout: () => 71, clearTimeout() {} };
const viewer = eventViewer();
let resolvePoster;
const poster = { closed: false, close() { this.closed = true; } };
const layer = createBhoteKoshiEventLayer({
eventLoader: async () => event,
imageryProviderFactory: async (url) => ({ url }),
terrainSampler: async () => [{ height: 2800 }],
mediaLoader: async (url) => {
assert.equal(url, '/events/bhote-koshi-2026/evidence-09.jpg');
return new Promise((resolve) => { resolvePoster = resolve; });
},
evidenceFrameFactory: (observation) => ({ fixture: observation.id }),
overlayHost: { setEntries() {}, setVisible() {}, clearSource() {} },
renderHost: { request() {}, hold() {}, release() {} },
});
layer.attachMapStackController({
getActiveId: () => 'esri-imagery',
getSwitchGeneration: () => 1,
async setStack() {},
});
try {
await layer.enable(viewer, { origin: 'local-restore' });
await layer.disable();
resolvePoster(poster);
await new Promise((resolve) => setTimeout(resolve, 0));
assert.equal(poster.closed, true);
} finally {
globalThis.document = originalDocument;
globalThis.window = originalWindow;
}
});
test('every sourced shot grows its tail with its head, keeps its own card, and clears on exit', async () => {
const event = JSON.parse(await readFile(eventUrl, 'utf8'));
const originals = Object.fromEntries(['document', 'window', 'performance', 'requestAnimationFrame', 'cancelAnimationFrame']
.map((key) => [key, globalThis[key]]));
installEventDom();
let now = 1000;
let nextFrame = 0;
const frames = new Map();
globalThis.window = { setTimeout: () => 71, clearTimeout() {} };
globalThis.performance = { now: () => now };
globalThis.requestAnimationFrame = (callback) => { frames.set(++nextFrame, callback); return nextFrame; };
globalThis.cancelAnimationFrame = (id) => frames.delete(id);
const viewer = eventViewer();
let heightSamples = 0;
viewer.scene.sampleHeight = () => { heightSamples++; return 1500; };
let entries = [];
const layer = createBhoteKoshiEventLayer({
eventLoader: async () => event,
imageryProviderFactory: async (url) => ({ url }),
terrainSampler: async () => null,
mediaLoader: async () => { throw Object.assign(new Error('fixture'), { name: 'AbortError' }); },
overlayHost: { setEntries(_id, value) { entries = value; }, setVisible() {}, clearSource() {} },
renderHost: { request() {}, hold() {}, release() {} },
});
layer.attachMapStackController({ getActiveId: () => 'photoreal', getSwitchGeneration: () => 1 });
const params = (beatId, cameraSettled, evidencePath = 'source', evidencePathElevation = null) => ({
presentation: 'scene-beat', sceneSurface: 'evidence-beat', beatId,
sceneControls: { cameraSettled, evidencePath, evidencePathElevation, evidencePathDurationSec: 4,
deferEvidenceUntilCameraSettled: true, evidenceHoldCard: true },
});
const advance = (elapsed) => {
const callback = frames.get(nextFrame);
frames.delete(nextFrame);
callback(now + elapsed);
};
try {
layer.setParams(params(SOURCE_PATH_BEAT_IDS[0], false));
await layer.enable(viewer, { origin: 'scene' });
const source = viewer._test.dataSources.at(-1);
const flood = source.entities.getById('bhote-koshi-flood-core');
const surge = source.entities.getById('bhote-koshi-surge-front');
for (const id of SOURCE_PATH_BEAT_IDS) {
layer.setParams(params(id, false));
assert.equal(flood.show, false, `${id}: hide the previous route during flight`);
assert.equal(surge.show, false);
layer.setParams(params(id, true));
const sampleCount = heightSamples;
let previousLength = 0;
for (const elapsed of [400, 1800, 4000]) {
advance(elapsed);
const points = flood.polyline.positions.getValue();
assert.ok(points.length >= previousLength);
assert.equal(flood.show, true);
assert.equal(surge.show, true);
assert.ok(Cesium.Cartesian3.equals(points.at(-1), surge.position.getValue()), `${id}: tail must reach head in every frame`);
previousLength = points.length;
}
assert.equal(heightSamples, sampleCount, 'do not resample geometry during motion');
const visibleCards = entries.filter((entry) => entry.id.startsWith('evidence-card-') && entry.position());
assert.deepEqual(visibleCards.map(({ id: cardId }) => cardId), [`evidence-card-${id}`]);
assert.ok(visibleCards[0].sourceAlpha() > 0, 'keep the source readable after the path settles');
now += 5000;
}
// A settled-to-settled entry must not paint the last shot's completed
// progress on the new route for a frame, then jump back to its start.
layer.setParams(params('dhunche', true));
assert.equal(flood.show, false, 'new shot clears its tail before the first animation frame');
assert.equal(surge.show, false);
advance(1800);
const headBeforeSync = Cesium.Cartesian3.clone(surge.position.getValue());
const frameBeforeSync = nextFrame;
layer.setParams(params('dhunche', true));
assert.equal(nextFrame, frameBeforeSync, 'repeated settled notification does not restart the reveal');
assert.ok(Cesium.Cartesian3.equals(headBeforeSync, surge.position.getValue()));
advance(2200);
assert.ok(!Cesium.Cartesian3.equals(headBeforeSync, surge.position.getValue()));
// The comparison exit can expose coarse photogrammetry samples a kilometre
// above the river. Dhunche's schematic profile must not dive through them.
viewer.scene.sampleHeight = () => { heightSamples++; return 2703; };
viewer.scene.globe = { getHeight: () => 0 };
layer.setParams(params('dhunche', false, 'source', 'source'));
const samplesBeforeProfile = heightSamples;
layer.setParams(params('dhunche', true, 'source', 'source'));
for (const elapsed of [40, 400, 1800, 4000]) {
advance(elapsed);
const points = flood.polyline.positions.getValue();
for (const position of points) {
const height = Cesium.Cartographic.fromCartesian(position).height;
assert.ok(height >= 1307 && height <= 1332, `source profile stays near river elevation, got ${height}`);
}
assert.ok(Cesium.Cartesian3.equals(points.at(-1), surge.position.getValue()));
}
assert.equal(heightSamples, samplesBeforeProfile, 'source elevation does not sample the changing mesh');
layer.setParams(params('charaudi', false, 'none'));
layer.setParams(params('charaudi', true, 'none'));
advance(1000);
assert.equal(flood.show, false);
assert.equal(surge.show, false);
await layer.disable();
assert.equal(frames.size, 0);
} finally {
for (const [key, value] of Object.entries(originals)) globalThis[key] = value;
}
});
test('media-led upper-valley shots retain only their upstream prefix during camera travel', async () => {
const event = JSON.parse(await readFile(eventUrl, 'utf8'));
const originals = Object.fromEntries(['document', 'window', 'performance', 'requestAnimationFrame', 'cancelAnimationFrame']
.map((key) => [key, globalThis[key]]));
installEventDom();
let now = 1000;
let nextFrame = 0;
const frames = new Map();
globalThis.window = { setTimeout: () => 71, clearTimeout() {} };
globalThis.performance = { now: () => now };
globalThis.requestAnimationFrame = callback => { frames.set(++nextFrame, callback); return nextFrame; };
globalThis.cancelAnimationFrame = id => frames.delete(id);
const viewer = eventViewer();
const layer = createBhoteKoshiEventLayer({
eventLoader: async () => event,
imageryProviderFactory: async url => ({ url }),
terrainSampler: async () => null,
mediaLoader: async () => { throw Object.assign(new Error('fixture'), { name: 'AbortError' }); },
overlayHost: { setEntries() {}, setVisible() {}, clearSource() {} },
renderHost: { request() {}, hold() {}, release() {} },
});
layer.attachMapStackController({ getActiveId: () => 'esri-imagery', getSwitchGeneration: () => 1 });
const params = (beatId, cameraSettled, extras = {}) => ({
presentation: 'scene-beat', sceneSurface: 'evidence-beat', beatId,
sceneControls: {
evidencePath: 'source', evidencePathHistory: true, evidencePathPersistent: true,
evidencePathDuringMedia: true, evidencePathDurationSec: 4.8,
deferEvidenceUntilCameraSettled: true, evidenceHoldCard: true,
cameraSettled, ...extras,
},
});
const finishReveal = () => {
now += 7000;
const callback = frames.get(nextFrame);
frames.delete(nextFrame);
callback(now);
};
try {
layer.setParams(params('debris-dammed-lake', false));
await layer.enable(viewer, { origin: 'scene' });
const source = viewer._test.dataSources.at(-1);
const flood = source.entities.getById('bhote-koshi-flood-core');
const surge = source.entities.getById('bhote-koshi-surge-front');
layer.setParams(params('debris-dammed-lake', true));
finishReveal();
const lakeTail = flood.polyline.positions.getValue().map(point => Cesium.Cartesian3.clone(point));
assert.ok(lakeTail.length > 1);
const origin = Cesium.Cartographic.fromCartesian(lakeTail[0]);
assert.ok(Math.abs(Cesium.Math.toDegrees(origin.longitude) - 85.48476) < 1e-6);
assert.ok(Math.abs(Cesium.Math.toDegrees(origin.latitude) - 28.33255) < 1e-6,
'the rendered trail starts at the lake reach, not the earlier trigger reach');
layer.setParams(params('second-landslide', false));
assert.equal(flood.show, true, 'lake trail survives departure before the next camera settles');
assert.deepEqual(flood.polyline.positions.getValue(), lakeTail, 'retain the prefix without revealing the next segment');
assert.ok(Cesium.Cartesian3.equals(surge.position.getValue(), lakeTail.at(-1)));
layer.setParams(params('second-landslide', true));
assert.deepEqual(flood.polyline.positions.getValue(), lakeTail, 'arrival does not clear the prefix');
finishReveal();
assert.ok(flood.polyline.positions.getValue().length > lakeTail.length);
layer.setParams(params('debris-dammed-lake', false));
assert.equal(flood.show, false, 'backward replay trims future segments instead of retaining stale history');
layer.setParams(params('second-landslide', false));
assert.deepEqual(flood.polyline.positions.getValue(), lakeTail, 'direct load reconstructs the same prefix');
layer.setParams(params('second-landslide', false, { evidencePathPersistent: false }));
assert.equal(flood.show, false, 'nonpersistent shots retain their arrival gate');
for (const beatId of ['devighat-taadi-khola-bridge', 'charaudi', 'final-view']) {
const historyControls = {
evidencePath: 'history', evidencePathHistoryBeatId: SOURCE_PATH_BEAT_IDS.at(-1),
evidencePathDuringMedia: false, evidenceMediaAutoplay: beatId !== 'final-view',
};
layer.setParams(params(beatId, false, historyControls));
const completedTail = [...flood.polyline.positions.getValue()];
assert.ok(completedTail.length > 100, `${beatId}: direct load reconstructs full sourced history`);
layer.setParams(params(beatId, true, historyControls));
assert.deepEqual(flood.polyline.positions.getValue(), completedTail,
`${beatId}: arrival must not reset the completed history`);
finishReveal();
assert.deepEqual(flood.polyline.positions.getValue(), completedTail,
`${beatId}: media/card reveal must not erase or regrow completed history`);
}
await layer.disable();
assert.equal(frames.size, 0);
} finally {
for (const [key, value] of Object.entries(originals)) globalThis[key] = value;
}
});
test('camera-led Nepal flood motion preserves its prefix and stays ahead of shot travel', async () => {
const event = JSON.parse(await readFile(eventUrl, 'utf8'));
const originals = Object.fromEntries(['document', 'window', 'performance', 'requestAnimationFrame', 'cancelAnimationFrame']
.map((key) => [key, globalThis[key]]));
installEventDom();
let now = 1000;
let nextFrame = 0;
let nextTimer = 0;
const frames = new Map();
const timers = new Map();
const holds = [];
const releases = [];
globalThis.window = {
setTimeout(callback, delay) {
const id = ++nextTimer;
timers.set(id, { callback, delay });
return id;
},
clearTimeout(id) { timers.delete(id); },
};
globalThis.performance = { now: () => now };
globalThis.requestAnimationFrame = (callback) => { frames.set(++nextFrame, callback); return nextFrame; };
globalThis.cancelAnimationFrame = (id) => frames.delete(id);
const viewer = eventViewer();
const layer = createBhoteKoshiEventLayer({
eventLoader: async () => event,
imageryProviderFactory: async (url) => ({ url }),
terrainSampler: async () => null,
mediaLoader: async () => { throw Object.assign(new Error('fixture'), { name: 'AbortError' }); },
overlayHost: { setEntries() {}, setVisible() {}, clearSource() {} },
renderHost: {
request() {},
hold(id) { holds.push(id); },
release(id) { releases.push(id); },
},
});
layer.attachMapStackController({ getActiveId: () => 'photoreal', getSwitchGeneration: () => 1 });
const travel = (id, durationSec = 4.2, overrides = {}) => ({
id, durationSec, active: true, completed: false, cancelled: false, ...overrides,
});
const params = (beatId, evidencePath, cameraTravel, extras = {}) => ({
presentation: 'scene-beat',
sceneSurface: 'evidence-beat',
beatId,
sceneControls: {
evidencePath,
evidencePathDuringCamera: true,
evidencePathPersistent: true,
deferEvidenceUntilCameraSettled: true,
cameraSettled: false,
cameraTravel,
...extras,
},
});
const advance = (elapsed) => {
now += elapsed;
const id = Math.max(...frames.keys());
const callback = frames.get(id);
frames.delete(id);
assert.equal(typeof callback, 'function');
callback(now);
};
try {
layer.setParams(params('timure-cluster', 'comparison', travel(1), {
evidenceSequence: true,
}));
await layer.enable(viewer, { origin: 'scene' });
let source = viewer._test.dataSources.at(-1);
let flood = source.entities.getById('bhote-koshi-flood-core');
let surge = source.entities.getById('bhote-koshi-surge-front');
const timureStart = flood.polyline.positions.getValue().length;
assert.ok(timureStart > 1, 'the completed Border Gate prefix is already visible during travel');
assert.equal(surge.show, true);
advance(420);
const earlyTimureLength = flood.polyline.positions.getValue().length;
assert.ok(earlyTimureLength > timureStart,
'the trail leads during the opening tenth of camera travel instead of waiting for arrival');
advance(2880);
const timureEnd = flood.polyline.positions.getValue().length;
assert.ok(timureEnd > timureStart);
assert.equal(flood.show, true, 'the completed tail remains visible at the next point');
assert.ok(Cesium.Cartesian3.equals(flood.polyline.positions.getValue().at(-1), surge.position.getValue()));
layer.setParams(params('syabru-besi', 'comparison', travel(2), {
evidenceSequence: true,
}));
assert.ok(flood.polyline.positions.getValue().length >= timureEnd,
'the Timure segment remains painted when Syabru travel starts');
advance(3300);
assert.equal(surge.show, true, 'the amber head remains visible at comparison completion');
assert.ok(Cesium.Cartesian3.equals(flood.polyline.positions.getValue().at(-1), surge.position.getValue()));
layer.setParams(params('dhunche', 'source', travel(3), {
evidencePathHistory: true,
evidencePathElevation: 'source',
evidenceHoldCard: true,
}));
source = viewer._test.dataSources.at(-1);
flood = source.entities.getById('bhote-koshi-flood-core');
surge = source.entities.getById('bhote-koshi-surge-front');
const dhuncheStart = flood.polyline.positions.getValue().length;
assert.ok(dhuncheStart >= event.reconstruction.corridor.length,
'the comparison corridor survives the transition onto Google photogrammetry');
advance(3300);
const dhuncheEnd = flood.polyline.positions.getValue().length;
assert.ok(dhuncheEnd > dhuncheStart);
assert.equal(surge.show, true);
layer.setParams(params('mailung-upper-trishuli', 'source', travel(4), {
evidencePathHistory: true,
evidencePathElevation: 'source',
evidenceHoldCard: true,
}));
assert.ok(flood.polyline.positions.getValue().length >= dhuncheEnd,
'the completed Dhunche segment never disappears');
advance(900);
const frozenHead = Cesium.Cartesian3.clone(surge.position.getValue());
const frozenLength = flood.polyline.positions.getValue().length;
layer.setParams(params('mailung-upper-trishuli', 'source', travel(4, 4.2, {
active: false, cancelled: true,
}), {
evidencePathHistory: true,
evidencePathElevation: 'source',
evidenceHoldCard: true,
}));
assert.equal(frames.size, 0, 'cancelling camera travel revokes the path animation frame');
assert.equal(flood.polyline.positions.getValue().length, frozenLength);
assert.ok(Cesium.Cartesian3.equals(surge.position.getValue(), frozenHead));
assert.ok(holds.includes('bhote-koshi-scene-path-travel'));
assert.ok(releases.includes('bhote-koshi-scene-path-travel'));
let previousCompleteLength = dhuncheEnd;
let travelId = 5;
for (const beatId of SOURCE_PATH_BEAT_IDS.slice(3)) {
layer.setParams(params(beatId, 'source', travel(travelId++), {
evidencePathHistory: true,
evidencePathElevation: 'source',
evidenceHoldCard: true,
}));
const startLength = flood.polyline.positions.getValue().length;
assert.ok(startLength >= previousCompleteLength,
`${beatId} retains every completed upstream segment while travel begins`);
advance(3300);
const completeLength = flood.polyline.positions.getValue().length;
assert.ok(completeLength > startLength, `${beatId} grows its own segment during travel`);
assert.equal(flood.show, true);
assert.equal(surge.show, true);
assert.ok(Cesium.Cartesian3.equals(
flood.polyline.positions.getValue().at(-1), surge.position.getValue(),
));
previousCompleteLength = completeLength;
}
const bidurCompleteLength = previousCompleteLength;
// Direct entry must reconstruct its supported prefix without relying on
// earlier playback, while backward navigation must trim later segments.
layer.setParams({
presentation: 'scene-beat', sceneSurface: 'evidence-beat', beatId: 'devighat-taadi-khola-bridge',
sceneControls: { evidencePath: 'none', deferEvidenceUntilCameraSettled: true, cameraSettled: false },
});
assert.equal(flood.show, false, 'Devighat does not leak the supported route');
assert.equal(surge.show, false);
layer.setParams(params('bidur-trishuli-bridge', 'source', travel(travelId++), {
evidencePathHistory: true,
evidencePathElevation: 'source',
evidenceHoldCard: true,
}));
assert.ok(flood.polyline.positions.getValue().length > dhuncheEnd,
'a direct Bidur load reconstructs its complete upstream prefix');
advance(3300);
assert.equal(flood.polyline.positions.getValue().length, bidurCompleteLength);
layer.setParams(params('dhunche', 'source', travel(travelId++), {
evidencePathHistory: true,
evidencePathElevation: 'source',
evidenceHoldCard: true,
}));
assert.ok(flood.polyline.positions.getValue().length < bidurCompleteLength,
'backward navigation trims unsupported downstream history immediately');
advance(3300);
assert.equal(flood.polyline.positions.getValue().length, dhuncheEnd);
// The deadline is independent of RAF, so a throttled render loop still
// completes the active segment before the camera's longer flight ends.
layer.setParams(params('mailung-upper-trishuli', 'source', travel(travelId++), {
evidencePathHistory: true,
evidencePathElevation: 'source',
evidenceHoldCard: true,
}));
const deadlineEntry = [...timers.entries()].find(([, entry]) => entry.delay >= 2400);
assert.ok(deadlineEntry, 'camera-led travel registers a pre-arrival completion deadline');
now += deadlineEntry[1].delay;
deadlineEntry[1].callback();
assert.equal(frames.size, 0, 'the deadline cancels a stalled animation frame');
assert.ok(flood.polyline.positions.getValue().length > dhuncheEnd);
layer.setParams({
presentation: 'scene-beat', sceneSurface: 'evidence-beat', beatId: 'charaudi',
sceneControls: { evidencePath: 'none', deferEvidenceUntilCameraSettled: true, cameraSettled: false },
});
assert.equal(flood.show, false, 'unsupported downstream shots do not invent or leak a route');
assert.equal(surge.show, false);
await layer.disable();
} finally {
for (const [key, value] of Object.entries(originals)) globalThis[key] = value;
}
});
test('Bhote Koshi corridor remains ordered downstream and inside the imagery rectangle', async () => {
const event = JSON.parse(await readFile(eventUrl, 'utf8'));
const { west, south, east, north } = event.imagery.rectangle;
let priorChainage = Number.NEGATIVE_INFINITY;
for (const point of event.reconstruction.corridor) {
assert.ok(point.chainageM > priorChainage);
assert.ok(point.lon >= west && point.lon <= east);
assert.ok(point.lat >= south && point.lat <= north);
priorChainage = point.chainageM;
}
});