Extract vessel components and preserve source lifecycle

This commit is contained in:
Sameh Khamis committed 2026-09-13 10:54:32 -07:00
1 parent 8b3fad0672
commit 2f4839e8f2
23 files changed
+2967 -2182

No files matched your search

+163
View File
@@ -0,0 +1,163 @@
import { DEFAULT_AIS_RUNTIME } from './policy.js';
export function createTesting({ vesselState, services, parts: components, layer, options }) {
const { state } = vesselState;
const aisLiveVesselsLayer = layer;
/**
* Bind the production interaction callbacks to mockable viewer/handler
* surfaces. Test-only seam; behavior is shared with installInteraction().
* @param {Object} viewer - Viewer-like object with scene.pick().
* @param {Object} handler - Handler-like object with setInputAction().
* @param {Object} keyTarget - EventTarget-like object with add/removeEventListener().
* @returns {void}
*/
function _bindVesselInteractionForTest(viewer, handler, keyTarget) {
components.selection.bindVesselInteraction(viewer, handler, keyTarget);
}
/**
* Prime the minimum live state needed by interaction/lifecycle wire tests.
* @param {Object} [options={}] - Test state values.
* @returns {void}
*/
function _setVesselStateForTest(options = {}) {
components.lifecycle.resetState();
const records = Array.isArray(options.records) ? options.records : [];
state.viewer = options.viewer || null;
state.enabled = options.enabled !== false;
state.loaded = options.loaded === true;
state.loading = options.loading === true;
state.stale = options.stale === true;
state.error = options.error || null;
state.lastUpdate = options.lastUpdate ?? null;
state.vesselRecords = records;
state.count = records.length;
state.vesselMap = new Map(
records.filter((record) => record?.mmsi).map((record) => [record.mmsi, record])
);
state.selectedRecord = options.selectedRecord || null;
state.billboardCollection = options.billboardCollection || { remove() {} };
state.trail = options.trail || null;
state.trailMmsi = options.trailMmsi || null;
state.trailPositions = Array.isArray(options.trailPositions) ? [...options.trailPositions] : [];
state.transportStatus = options.transportStatus || null;
state.lastMessageAt = options.lastMessageAt ?? null;
state.rawRowCount = Number.isFinite(options.rawRowCount) ? options.rawRowCount : 0;
state.acceptedRowCount = Number.isFinite(options.acceptedRowCount) ? options.acceptedRowCount : records.length;
state.firstConnectPhase = options.firstConnectPhase || 'idle';
state.firstConnectStartedAt = options.firstConnectStartedAt ?? null;
state.firstConnectDeadline = options.firstConnectDeadline ?? null;
state.interactionHandlerFactory = options.interactionHandlerFactory || null;
state.interactionKeyTarget = options.interactionKeyTarget || null;
}
/** Inject a host recorder for lifecycle/contract tests; null restores production. */
function _setVesselOverlayHostForTest(host = null) {
vesselState._vesselOverlayHost = host || vesselState.DEFAULT_VESSEL_OVERLAY_HOST;
}
/** Exercise the production selector/publisher through a test-owned state. */
function _updateVesselCardsForTest(records = []) {
components.rendering.updateClusteredLabels(records);
}
/**
* Reconcile AIS rows through the production lifecycle. Test-only seam.
* @param {Object} viewer - Viewer-like object.
* @param {Array<Object>} rows - Raw AIS rows.
* @returns {void}
*/
function _reconcileVesselsForTest(viewer, rows) {
components.store.reconcileVessels(viewer, rows);
}
/** Apply one server snapshot through the production pre-reconcile health gate. */
function _applyAisFeedSnapshotForTest(viewer, payload) {
return components.ingestion.applyAisFeedSnapshot(viewer, payload);
}
/** Exercise the request-owned live loader with a test-controlled fetch. */
function _loadLivePositionsForTest(viewer) {
return components.ingestion.loadLivePositions(viewer);
}
/** Start the production first-connect grace state without installing UI. */
function _beginAisSessionForTest() {
components.lifecycle.beginAisSession();
}
/** Inject a deterministic clock/scheduler; null restores production runtime. */
function _setAisRuntimeForTest(runtime = null) {
components.lifecycle.clearFirstConnectTimer();
vesselState._aisRuntime = runtime
? {
now: runtime.now,
setTimeout: runtime.setTimeout,
clearTimeout: runtime.clearTimeout,
}
: DEFAULT_AIS_RUNTIME;
}
/** Read feed-health fields without exposing mutable production state. */
function _getVesselFeedStateForTest() {
const stats = aisLiveVesselsLayer.getStats();
return {
count: state.count,
loaded: state.loaded,
loading: stats.loading,
loadingLabel: stats.loadingLabel,
stale: state.stale,
error: state.error,
status: stats.status,
lastUpdate: state.lastUpdate,
transportStatus: state.transportStatus,
lastMessageAt: state.lastMessageAt,
rawRowCount: state.rawRowCount,
acceptedRowCount: state.acceptedRowCount,
selectedMmsi: state.selectedRecord?.mmsi || null,
trailMmsi: state.trailMmsi,
trailPositionCount: state.trailPositions.length,
sessionId: state.sessionId,
firstConnectPhase: state.firstConnectPhase,
firstConnectStartedAt: state.firstConnectStartedAt,
firstConnectDeadline: state.firstConnectDeadline,
};
}
/**
* Read lifecycle ownership state without exposing the mutable state object.
* Test-only seam.
* @returns {{trailMmsi: string|null, trailPositionCount: number, vesselCount: number}}
*/
function _getVesselStateForTest() {
return {
trailMmsi: state.trailMmsi,
trailPositionCount: state.trailPositions.length,
vesselCount: state.vesselMap.size,
};
}
return { _bindVesselInteractionForTest, _setVesselStateForTest, _setVesselOverlayHostForTest, _updateVesselCardsForTest, _reconcileVesselsForTest, _applyAisFeedSnapshotForTest, _loadLivePositionsForTest, _beginAisSessionForTest, _setAisRuntimeForTest, _getVesselFeedStateForTest, _getVesselStateForTest };
}