Files
gods-eye-view/src/data/bhoteKoshiEvent.test.mjs
T
Sameh Khamis fd0d51c450 feat(bhote-koshi): render the reconstruction panel from a store
The Bhote Koshi layer keeps its panel controls as plain records and
presents them to a store; the context rail renders the panel as its last
child and marks itself while it is on. Controls are bound to the enable
that published them, and an unchanged frame is not sent again.
2026-10-31 21:12:08 -07:00

2995 lines
124 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 { useBhoteKoshiPanelStore } from '../ui/stores/bhoteKoshiPanel.js';
import { useImagerySplitStore } from '../ui/stores/imagerySplit.js';
import { applicationPinia } from '../ui/vueRuntime.js';
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);
/** The event's divider as the imagery split store holds it, or undefined. */
const eventSplit = () =>
useImagerySplitStore(applicationPinia()).splits.findLast(
(split) => split.view.id === 'bhote-koshi-split-line',
);
// The panel controls the layer presents, by the selector the panel view gives each.
const PANEL_REFS = {
'[data-role="imagery-comparison"]': 'imagerySection',
'[data-role="time"]': 'time',
'[data-role="caveat"]': 'caveat',
'[data-role="imagery-cloud-note"]': 'imageryCloudNote',
'[data-role="progress"]': 'progressInput',
'[data-role="split"]': 'splitInput',
'[data-role="timeline-label"]': 'timelineLabel',
'[data-role="beat-index"]': 'beatIndex',
'[data-role="beat-title"]': 'beatTitle',
'[data-role="beat-meta"]': 'beatMeta',
'[data-action="play"]': 'playButton',
'[data-action="play-scene"]': 'playSceneButton',
'[data-action="cinematic"]': 'cinematicButton',
'[data-action="story-replay"]': 'storyReplayButton',
'[data-action="corridor"]': 'corridorButton',
'[data-action="previous-beat"]': 'previousButton',
'[data-action="next-beat"]': 'nextButton',
'[data-action="open-source"]': 'openSourceButton',
};
// What each control calls, as the panel view wires it.
const PANEL_EVENTS = {
playButton: { click: 'play' },
playSceneButton: { click: 'playScene' },
cinematicButton: { click: 'toggleCinematic' },
storyReplayButton: { click: 'replayCinematic' },
corridorButton: { click: 'focusCorridor' },
previousButton: { click: 'previousBeat' },
nextButton: { click: 'nextBeat' },
openSourceButton: { click: 'openActiveSource' },
progressInput: {
input: (controls, value) => controls.previewProgress(Number(value) / 1000),
change: (controls, value) => controls.commitProgress(Number(value) / 1000, { final: true }),
},
splitInput: { input: (controls, value) => controls.setSplit(Number(value) / 100) },
};
/**
* The event panel as the layer presents it to the panel store, or undefined
* while it is off. Controls read the latest presentation, and dispatching an
* event calls the connected controls as the panel view does.
*/
function eventPanel() {
const store = useBhoteKoshiPanelStore(applicationPinia());
if (!store.panel) return undefined;
const control = (key) => {
let typed = null;
const record = () => store.panel.refs[key];
return {
get textContent() { return record().textContent; },
get hidden() { return record().hidden; },
get disabled() { return record().disabled; },
get value() { return typed ?? record().value; },
set value(value) { typed = value; },
getAttribute: (name) => record().attributes[name] ?? null,
dispatch(type) {
const value = typed ?? record().value;
typed = null;
const handler = PANEL_EVENTS[key]?.[type];
return typeof handler === 'function'
? handler(store.controls, value)
: store.controls[handler]?.();
},
};
};
const named = (name) => ({ dispatch: () => store.controls[name]() });
return {
querySelector(selector) {
if (selector === '.bhote-event-report-list') {
return {
children: store.panel.reports.map((_, index) => ({
dispatch: () => store.controls.openReport(index),
})),
};
}
if (selector === '[data-action="geolocation-map"]') return named('openGeolocationMap');
if (selector === '[data-action="close"]') return named('close');
return control(PANEL_REFS[selector]);
},
};
}
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();
assert.equal(eventSplit(), undefined, 'no divider is left from an earlier test');
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.ok(eventPanel());
assert.ok(eventSplit());
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.equal(eventPanel(), undefined);
assert.equal(eventSplit(), undefined);
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 = eventPanel();
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();
assert.equal(eventSplit(), undefined, 'no divider is left from an earlier test');
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 = eventPanel();
assert.ok(panel);
assert.ok(eventSplit());
assert.equal(eventSplit().view.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);
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(eventSplit().view.hidden, false);
assert.equal(panel.querySelector('[data-role="imagery-comparison"]').hidden, false);
assert.ok(viewer._test.imagery.every((imageryLayer) => imageryLayer.show === true));
assert.equal(eventSplit().view.handleClass, 'bhote-koshi-split-handle');
assert.equal(eventSplit().view.valueNow, 62);
const splitHandle = eventSplit().controls;
splitHandle.pointerdown({
button: 0,
clientX: 250,
pointerId: 4,
preventDefault() {},
});
splitHandle.pointermove({
clientX: 750,
pointerId: 4,
preventDefault() {},
});
splitHandle.pointerup({ pointerId: 4 });
assert.equal(viewer.scene.splitPosition, 0.75);
assert.equal(eventSplit().view.valueNow, 75);
splitHandle.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(eventSplit().view.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.ok(eventPanel());
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 = eventPanel();
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 = eventPanel();
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.ok(eventPanel());
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 = eventPanel();
assert.equal(panel.querySelector('[data-role="timeline-label"]').textContent, 'RECONSTRUCTION CLOCK');
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 republish no panel and churn no camera', 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 = eventPanel();
const cinematic = panel.querySelector('[data-action="cinematic"]');
const progress = panel.querySelector('[data-role="progress"]');
cinematic.dispatch('click');
progress.value = '500';
progress.dispatch('input');
const panelStore = useBhoteKoshiPanelStore(applicationPinia());
const published = panelStore.panel;
const cameraLookCount = viewer._test.cameraLooks.length;
for (let index = 0; index < 20; index += 1) progress.dispatch('input');
assert.ok(panelStore.panel === published, 'unchanged frames do not republish the panel');
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 = eventPanel();
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 = eventPanel();
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;
}
});