1// SPDX-License-Identifier: GPL-2.0-only
2// Copyright (c) 2026-present FERS Contributors (see AUTHORS.md).
4import { describe, expect, test } from 'bun:test';
5import { defaultGlobalParameters } from './defaults';
6import { buildHydratedScenarioState, parseScenarioData } from './hydration';
7import { ScenarioData, ScenarioState } from './types';
9function createScenarioData(): ScenarioData {
12 ...defaultGlobalParameters,
25 interpolation: 'static',
61function createState(overrides: Partial<ScenarioState> = {}): ScenarioState {
63 globalParameters: defaultGlobalParameters,
69 selectedComponentId: null,
73 targetPlaybackDuration: null,
74 isBackendSyncing: false,
76 scenarioFilePath: null,
77 outputDirectory: null,
78 antennaPreviewErrors: {},
79 notificationSnackbar: {
84 notificationQueue: [],
85 viewControlAction: { type: null, timestamp: 0 },
92 showLinkMonostatic: true,
93 showLinkIlluminator: true,
94 showLinkScattered: true,
98 showPlatformLabels: true,
99 showMotionPaths: true,
105describe('buildHydratedScenarioState', () => {
106 test('preserves selection and clamps current time during backend recovery', () => {
107 const hydrated = buildHydratedScenarioState(
109 selectedItemId: 'platform-1',
110 selectedComponentId: 'component-1',
113 createScenarioData(),
116 preserveSelection: true,
117 preserveCurrentTime: true,
121 expect(hydrated.selectedItemId).toBe('platform-1');
122 expect(hydrated.selectedComponentId).toBe('component-1');
123 expect(hydrated.currentTime).toBe(20);
124 expect(hydrated.isDirty).toBe(false);
127 test('clears stale selection and resets time for import-style loads', () => {
128 const hydrated = buildHydratedScenarioState(
130 selectedItemId: 'missing-platform',
131 selectedComponentId: 'missing-component',
134 createScenarioData(),
140 expect(hydrated.selectedItemId).toBeNull();
141 expect(hydrated.selectedComponentId).toBeNull();
142 expect(hydrated.currentTime).toBe(5);
143 expect(hydrated.isDirty).toBe(true);
147describe('parseScenarioData FMCW hydration', () => {
148 test('hydrates CW and FMCW file waveform variants', () => {
149 const scenario = parseScenarioData({
150 name: 'File waveforms',
157 carrier_frequency: 1e9,
158 cw_from_file: { filename: 'cw.h5' },
164 carrier_frequency: 2e9,
165 fmcw_from_file: { filename: 'fmcw.h5' },
173 expect(scenario?.waveforms).toEqual([
174 expect.objectContaining({
176 waveformType: 'cw_from_file',
179 expect.objectContaining({
181 waveformType: 'fmcw_from_file',
187 test('hydrates FMCW waveform and component mode from backend data', () => {
188 const scenario = parseScenarioData({
189 name: 'FMCW Scenario',
194 name: 'FMCW Down Chirp',
196 carrier_frequency: 10e9,
199 chirp_bandwidth: 20e6,
200 chirp_duration: 250e-6,
201 chirp_period: 300e-6,
202 start_frequency_offset: -10e6,
208 name: 'FMCW Triangle',
210 carrier_frequency: 10e9,
212 chirp_bandwidth: 20e6,
213 chirp_duration: 250e-6,
214 start_frequency_offset: -10e6,
224 name: 'Radar Platform',
226 interpolation: 'static',
228 { x: 0, y: 0, altitude: 0, time: 0 },
243 dechirp_mode: 'ideal',
246 waveform_name: 'FMCW Down Chirp',
249 if_filter_bandwidth: 4e5,
250 if_filter_transition_width: 1e5,
259 expect(scenario).not.toBeNull();
260 expect(scenario?.waveforms[0]).toMatchObject({
262 waveformType: 'fmcw_linear_chirp',
264 chirp_bandwidth: 20e6,
265 chirp_duration: 250e-6,
266 chirp_period: 300e-6,
267 start_frequency_offset: -10e6,
270 expect(scenario?.waveforms[1]).toMatchObject({
272 waveformType: 'fmcw_triangle',
273 chirp_bandwidth: 20e6,
274 chirp_duration: 250e-6,
275 start_frequency_offset: -10e6,
278 expect(scenario?.platforms[0].components[0]).toMatchObject({
283 dechirp_mode: 'ideal',
286 waveform_name: 'FMCW Down Chirp',
289 if_filter_bandwidth: 4e5,
290 if_filter_transition_width: 1e5,
295 test('hydrates receiver and monostatic dechirp references by backend names', () => {
296 const scenario = parseScenarioData({
297 name: 'FMCW Dechirp Scenario',
304 carrier_frequency: 10e9,
307 chirp_bandwidth: 20e6,
308 chirp_duration: 250e-6,
309 chirp_period: 300e-6,
318 name: 'Radar Platform',
320 interpolation: 'static',
322 { x: 0, y: 0, altitude: 0, time: 0 },
335 name: 'Reference TX',
345 dechirp_mode: 'physical',
347 source: 'transmitter',
348 transmitter_name: 'Reference TX',
361 dechirp_mode: 'ideal',
373 expect(scenario).not.toBeNull();
374 expect(scenario?.platforms[0].components[1]).toMatchObject({
379 dechirp_mode: 'physical',
381 source: 'transmitter',
382 transmitter_name: 'Reference TX',
386 expect(scenario?.platforms[0].components[2]).toMatchObject({
393 dechirp_mode: 'ideal',