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';
6 formatNumberFieldValue,
7 resolveNumberFieldBlur,
9} from './InspectorControls';
11 createDechirpReference,
14 DECHIRP_REFERENCE_SOURCE_OPTIONS,
15 getAvailableDechirpReferenceSourceOptions,
16 getCompatibleWaveforms,
19 getPulsedRadarFieldLabels,
21 resolveWaveformSelectValue,
22 shouldClearWaveformForRadarType,
23} from './PlatformComponentInspector';
25 ensureCubicPositionWaypoints,
26 ensureCubicRotationWaypoints,
27} from './PlatformInspector';
29 createWaveformForType,
30 getVisibleWaveformFieldLabels,
31 WAVEFORM_TYPE_OPTIONS,
32} from './WaveformInspector';
34describe('InspectorControls blur resolution', () => {
35 test('reverts required numeric fields when blurred empty', () => {
36 expect(resolveNumberFieldBlur('', 42, 'revert')).toEqual({
38 error: 'Value required.',
43 test('commits null for unsettable numeric fields when blurred empty', () => {
44 expect(resolveNumberFieldBlur('', 42, 'null')).toEqual({
51 test('reverts timing noise-entry drafts instead of producing empty commits', () => {
52 expect(resolveNumberFieldBlur('', 0.5, 'revert')).toEqual({
54 error: 'Value required.',
59 test('rejects invalid numeric drafts on blur', () => {
60 expect(resolveNumberFieldBlur('-', 7, 'revert')).toEqual({
62 error: 'Invalid number.',
67 test('parses valid numeric drafts on blur', () => {
68 expect(resolveNumberFieldBlur('1e3', 7, 'revert')).toEqual({
75 test('reverts required text fields when blurred empty', () => {
76 expect(resolveTextFieldBlur(' ', 'Timing A', false)).toEqual({
78 error: 'Value required.',
79 nextDraft: 'Timing A',
83 test('commits non-empty text drafts', () => {
85 resolveTextFieldBlur('Updated Timing', 'Timing A', false)
88 nextDraft: 'Updated Timing',
89 value: 'Updated Timing',
93 test('formats null numeric values as an empty draft', () => {
94 expect(formatNumberFieldValue(null)).toBe('');
98describe('Waveform inspector authoring options', () => {
99 test('offers file waveform types first and generated alternatives after them', () => {
100 expect(WAVEFORM_TYPE_OPTIONS).toEqual([
101 { value: 'pulsed_from_file', label: 'Pulse File' },
102 { value: 'cw_from_file', label: 'CW File' },
103 { value: 'fmcw_from_file', label: 'FMCW File' },
104 { value: 'cw', label: 'CW Tone (Generated)' },
106 value: 'fmcw_linear_chirp',
107 label: 'FMCW Linear Chirp (Generated)',
109 { value: 'fmcw_triangle', label: 'FMCW Triangle (Generated)' },
110 { value: 'stepped_frequency', label: 'SFCW' },
114 test('creates file waveform defaults and shows only the file field', () => {
115 for (const waveformType of [
119 const waveform = createWaveformForType(
123 name: 'File waveform',
126 carrier_frequency: 1e9,
130 expect(waveform).toMatchObject({ waveformType, filename: '' });
131 expect(getVisibleWaveformFieldLabels(waveformType)).toEqual([
132 'Waveform File (.h5)',
137 test('creates FMCW defaults while preserving common waveform fields', () => {
138 const fmcwWaveform = createWaveformForType(
142 name: 'Search chirp',
145 carrier_frequency: 9.6e9,
147 start_frequency_offset: 125,
152 expect(fmcwWaveform).toMatchObject({
155 name: 'Search chirp',
156 waveformType: 'fmcw_linear_chirp',
159 carrier_frequency: 9.6e9,
160 chirp_bandwidth: 4e3,
161 chirp_duration: 1e-3,
163 start_frequency_offset: 125,
168 test('creates SFCW defaults while preserving common waveform fields', () => {
169 const sfcwWaveform = createWaveformForType(
176 carrier_frequency: 5.8e9,
181 expect(sfcwWaveform).toMatchObject({
185 waveformType: 'stepped_frequency',
187 carrier_frequency: 5.8e9,
188 start_frequency_offset: 0,
197 test('reports FMCW chirp fields only for FMCW waveforms', () => {
198 expect(getVisibleWaveformFieldLabels('fmcw_linear_chirp')).toEqual([
200 'Chirp Bandwidth (Hz)',
201 'Chirp Duration (s)',
203 'Start Frequency Offset (Hz)',
206 expect(getVisibleWaveformFieldLabels('fmcw_triangle')).toEqual([
207 'Chirp Bandwidth (Hz)',
208 'Chirp Duration (s)',
209 'Start Frequency Offset (Hz)',
212 expect(getVisibleWaveformFieldLabels('stepped_frequency')).toEqual([
213 'Start Frequency Offset (Hz)',
220 expect(getVisibleWaveformFieldLabels('cw')).toEqual([]);
224describe('Platform inspector authoring helpers', () => {
225 test('adds a second position waypoint before selecting cubic interpolation', () => {
236 const next = ensureCubicPositionWaypoints(waypoints);
238 expect(next).toHaveLength(2);
239 expect(next[0]).toEqual(waypoints[0]);
240 expect(next[1]).toMatchObject({
246 expect(next[1].id).not.toBe('position-1');
249 test('adds a second rotation waypoint before selecting cubic interpolation', () => {
259 const next = ensureCubicRotationWaypoints(waypoints);
261 expect(next).toHaveLength(2);
262 expect(next[0]).toEqual(waypoints[0]);
263 expect(next[1]).toMatchObject({
268 expect(next[1].id).not.toBe('rotation-1');
272describe('Platform component inspector waveform compatibility', () => {
274 { id: '1', name: 'Pulse', waveformType: 'pulsed_from_file' },
275 { id: '2', name: 'Tone', waveformType: 'cw' },
276 { id: '3', name: 'Chirp', waveformType: 'fmcw_linear_chirp' },
277 { id: '4', name: 'Triangle', waveformType: 'fmcw_triangle' },
278 { id: '5', name: 'Steps', waveformType: 'stepped_frequency' },
279 { id: '6', name: 'CW File', waveformType: 'cw_from_file' },
280 { id: '7', name: 'FMCW File', waveformType: 'fmcw_from_file' },
283 test('offers FMCW radar mode and hides pulsed-only fields for FMCW/CW', () => {
284 expect(RADAR_MODE_OPTIONS).toEqual([
285 { value: 'pulsed', label: 'Pulsed' },
286 { value: 'cw', label: 'CW' },
287 { value: 'fmcw', label: 'FMCW' },
288 { value: 'sfcw', label: 'SFCW' },
291 expect(getPulsedRadarFieldLabels('pulsed')).toEqual([
296 expect(getPulsedRadarFieldLabels('cw')).toEqual([]);
297 expect(getPulsedRadarFieldLabels('fmcw')).toEqual([]);
298 expect(getPulsedRadarFieldLabels('sfcw')).toEqual([]);
301 test('filters waveform dropdown choices by radar mode', () => {
302 expect(getCompatibleWaveforms(waveforms, 'pulsed')).toEqual([
305 expect(getCompatibleWaveforms(waveforms, 'cw')).toEqual([
309 expect(getCompatibleWaveforms(waveforms, 'fmcw')).toEqual([
314 expect(getCompatibleWaveforms(waveforms, 'sfcw')).toEqual([
319 test('clears and blocks incompatible waveform selection values', () => {
320 expect(shouldClearWaveformForRadarType('1', waveforms, 'fmcw')).toBe(
323 expect(shouldClearWaveformForRadarType('3', waveforms, 'fmcw')).toBe(
326 expect(shouldClearWaveformForRadarType('4', waveforms, 'fmcw')).toBe(
329 expect(resolveWaveformSelectValue('1', waveforms, 'fmcw')).toBe('');
330 expect(resolveWaveformSelectValue('3', waveforms, 'fmcw')).toBe('3');
331 expect(resolveWaveformSelectValue('4', waveforms, 'fmcw')).toBe('4');
332 expect(shouldClearWaveformForRadarType('3', waveforms, 'sfcw')).toBe(
335 expect(shouldClearWaveformForRadarType('5', waveforms, 'sfcw')).toBe(
338 expect(resolveWaveformSelectValue('5', waveforms, 'sfcw')).toBe('5');
341 test('offers dechirp modes and source options for receiver FMCW mode', () => {
342 expect(DECHIRP_MODE_OPTIONS).toEqual([
343 { value: 'none', label: 'None' },
344 { value: 'physical', label: 'Physical' },
345 { value: 'ideal', label: 'Ideal' },
347 expect(DECHIRP_REFERENCE_SOURCE_OPTIONS).toEqual([
348 { value: 'attached', label: 'Attached' },
349 { value: 'transmitter', label: 'Transmitter' },
350 { value: 'custom', label: 'Custom Waveform' },
352 expect(getAvailableDechirpReferenceSourceOptions('monostatic')).toEqual(
353 [...DECHIRP_REFERENCE_SOURCE_OPTIONS]
355 expect(getAvailableDechirpReferenceSourceOptions('receiver')).toEqual([
356 { value: 'transmitter', label: 'Transmitter' },
357 { value: 'custom', label: 'Custom Waveform' },
360 getAvailableDechirpReferenceSourceOptions('receiver', 'attached')
361 ).toEqual([...DECHIRP_REFERENCE_SOURCE_OPTIONS]);
364 test('builds backend-valid dechirp config shapes', () => {
365 expect(createFmcwModeConfig('none')).toEqual({});
366 expect(createFmcwModeConfig('physical')).toEqual({
367 dechirp_mode: 'physical',
368 dechirp_reference: { source: 'attached' },
371 createFmcwModeConfig('ideal', {
372 dechirp_mode: 'physical',
374 source: 'transmitter',
375 transmitter_name: 'TX A',
379 dechirp_mode: 'ideal',
381 source: 'transmitter',
382 transmitter_name: 'TX A',
386 createFmcwModeConfig('ideal', {
387 dechirp_mode: 'physical',
388 dechirp_reference: { source: 'attached' },
390 if_filter_bandwidth: 4e5,
391 if_filter_transition_width: 1e5,
394 dechirp_mode: 'ideal',
395 dechirp_reference: { source: 'attached' },
397 if_filter_bandwidth: 4e5,
398 if_filter_transition_width: 1e5,
401 createDechirpReference('transmitter', {
403 waveform_name: 'Bad carryover',
404 transmitter_name: 'TX B',
407 source: 'transmitter',
408 transmitter_name: 'TX B',
411 createDechirpReference('custom', {
412 source: 'transmitter',
413 transmitter_name: 'Bad carryover',
414 waveform_name: 'LO Waveform',
418 waveform_name: 'LO Waveform',
422 test('lists only FMCW waveforms and FMCW emitters for dechirp references', () => {
423 const fullWaveforms = [
426 type: 'Waveform' as const,
428 waveformType: 'pulsed_from_file' as const,
430 carrier_frequency: 1,
431 filename: 'pulse.h5',
435 type: 'Waveform' as const,
437 waveformType: 'fmcw_linear_chirp' as const,
438 direction: 'up' as const,
440 carrier_frequency: 1,
444 start_frequency_offset: 0,
449 type: 'Waveform' as const,
450 name: 'FMCW File LO',
451 waveformType: 'fmcw_from_file' as const,
453 carrier_frequency: 1,
460 type: 'Platform' as const,
463 interpolation: 'static' as const,
475 type: 'fixed' as const,
484 type: 'transmitter' as const,
486 radarType: 'fmcw' as const,
495 type: 'transmitter' as const,
497 radarType: 'pulsed' as const,
508 expect(getFmcwWaveformNames(fullWaveforms)).toEqual([
512 expect(getFmcwEmitterNames(platforms, fullWaveforms)).toEqual([