2026-05-18 23:16:37 +00:00
|
|
|
"""Tests for form_descriptors module."""
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
from pydantic import BaseModel
|
|
|
|
|
from typing import Optional
|
|
|
|
|
|
2026-05-19 00:38:06 +00:00
|
|
|
from central.gui.form_descriptors import describe_fields, FieldDescriptor, _type_to_widget_and_options
|
2026-05-18 23:16:37 +00:00
|
|
|
from central.config_models import RegionConfig
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SimpleSettings(BaseModel):
|
|
|
|
|
"""Simple settings model for testing."""
|
|
|
|
|
name: str
|
|
|
|
|
count: int
|
|
|
|
|
enabled: bool
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SettingsWithOptional(BaseModel):
|
|
|
|
|
"""Settings with optional fields."""
|
|
|
|
|
required_field: str
|
|
|
|
|
optional_field: Optional[str] = None
|
|
|
|
|
with_default: str = "default_value"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SettingsWithList(BaseModel):
|
|
|
|
|
"""Settings with list field."""
|
|
|
|
|
tags: list[str]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SettingsWithRegion(BaseModel):
|
|
|
|
|
"""Settings with region config."""
|
|
|
|
|
region: Optional[RegionConfig] = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestTypeToWidget:
|
2026-05-19 00:38:06 +00:00
|
|
|
"""Tests for _type_to_widget_and_options function."""
|
2026-05-18 23:16:37 +00:00
|
|
|
|
|
|
|
|
def test_str_maps_to_text(self):
|
2026-05-19 00:38:06 +00:00
|
|
|
assert _type_to_widget_and_options("field", str) == ("text", None)
|
2026-05-18 23:16:37 +00:00
|
|
|
|
|
|
|
|
def test_int_maps_to_number(self):
|
2026-05-19 00:38:06 +00:00
|
|
|
assert _type_to_widget_and_options("field", int) == ("number", None)
|
2026-05-18 23:16:37 +00:00
|
|
|
|
|
|
|
|
def test_bool_maps_to_checkbox(self):
|
2026-05-19 00:38:06 +00:00
|
|
|
assert _type_to_widget_and_options("field", bool) == ("checkbox", None)
|
2026-05-18 23:16:37 +00:00
|
|
|
|
|
|
|
|
def test_list_str_maps_to_csv(self):
|
2026-05-19 00:38:06 +00:00
|
|
|
assert _type_to_widget_and_options("field", list[str]) == ("csv", None)
|
2026-05-18 23:16:37 +00:00
|
|
|
|
|
|
|
|
def test_region_config_maps_to_region(self):
|
2026-05-19 00:38:06 +00:00
|
|
|
assert _type_to_widget_and_options("field", RegionConfig) == ("region", None)
|
2026-05-18 23:16:37 +00:00
|
|
|
|
|
|
|
|
def test_optional_region_maps_to_region(self):
|
2026-05-19 00:38:06 +00:00
|
|
|
assert _type_to_widget_and_options("field", Optional[RegionConfig]) == ("region", None)
|
2026-05-18 23:16:37 +00:00
|
|
|
|
|
|
|
|
def test_optional_str_maps_to_text(self):
|
|
|
|
|
"""Optional[str] should map to text widget."""
|
2026-05-19 00:38:06 +00:00
|
|
|
assert _type_to_widget_and_options("field", Optional[str]) == ("text", None)
|
2026-05-18 23:16:37 +00:00
|
|
|
|
|
|
|
|
def test_optional_int_maps_to_number(self):
|
|
|
|
|
"""Optional[int] should map to number widget."""
|
2026-05-19 00:38:06 +00:00
|
|
|
assert _type_to_widget_and_options("field", Optional[int]) == ("number", None)
|
2026-05-18 23:16:37 +00:00
|
|
|
|
|
|
|
|
def test_unsupported_type_raises(self):
|
|
|
|
|
class CustomType:
|
|
|
|
|
pass
|
|
|
|
|
with pytest.raises(NotImplementedError):
|
2026-05-19 00:38:06 +00:00
|
|
|
_type_to_widget_and_options("field", CustomType)
|
2026-05-18 23:16:37 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestDescribeFields:
|
|
|
|
|
"""Tests for describe_fields function."""
|
|
|
|
|
|
|
|
|
|
def test_simple_model_fields(self):
|
|
|
|
|
"""describe_fields returns correct descriptors for simple model."""
|
|
|
|
|
fields = describe_fields(SimpleSettings, {"name": "test", "count": 5, "enabled": True})
|
|
|
|
|
|
|
|
|
|
assert len(fields) == 3
|
|
|
|
|
|
|
|
|
|
name_field = next(f for f in fields if f.name == "name")
|
|
|
|
|
assert name_field.label == "Name"
|
|
|
|
|
assert name_field.widget == "text"
|
|
|
|
|
assert name_field.current_value == "test"
|
|
|
|
|
|
|
|
|
|
count_field = next(f for f in fields if f.name == "count")
|
|
|
|
|
assert count_field.label == "Count"
|
|
|
|
|
assert count_field.widget == "number"
|
|
|
|
|
assert count_field.current_value == 5
|
|
|
|
|
|
|
|
|
|
enabled_field = next(f for f in fields if f.name == "enabled")
|
|
|
|
|
assert enabled_field.label == "Enabled"
|
|
|
|
|
assert enabled_field.widget == "checkbox"
|
|
|
|
|
assert enabled_field.current_value is True
|
|
|
|
|
|
|
|
|
|
def test_uses_current_values(self):
|
|
|
|
|
"""Current values from dict are used."""
|
|
|
|
|
fields = describe_fields(SimpleSettings, {"name": "current_name", "count": 42, "enabled": False})
|
|
|
|
|
|
|
|
|
|
name_field = next(f for f in fields if f.name == "name")
|
|
|
|
|
assert name_field.current_value == "current_name"
|
|
|
|
|
|
|
|
|
|
count_field = next(f for f in fields if f.name == "count")
|
|
|
|
|
assert count_field.current_value == 42
|
|
|
|
|
|
|
|
|
|
def test_missing_values_use_defaults(self):
|
|
|
|
|
"""Missing values fall back to model defaults."""
|
|
|
|
|
fields = describe_fields(SettingsWithOptional, {"required_field": "value"})
|
|
|
|
|
|
|
|
|
|
optional_field = next(f for f in fields if f.name == "optional_field")
|
|
|
|
|
assert optional_field.current_value is None
|
|
|
|
|
assert optional_field.widget == "text" # Optional[str] -> text
|
|
|
|
|
|
|
|
|
|
default_field = next(f for f in fields if f.name == "with_default")
|
|
|
|
|
assert default_field.current_value == "default_value"
|
|
|
|
|
|
|
|
|
|
def test_list_field_returns_csv_widget(self):
|
|
|
|
|
"""List[str] fields get csv widget."""
|
|
|
|
|
fields = describe_fields(SettingsWithList, {"tags": ["a", "b", "c"]})
|
|
|
|
|
|
|
|
|
|
tags_field = next(f for f in fields if f.name == "tags")
|
|
|
|
|
assert tags_field.widget == "csv"
|
|
|
|
|
assert tags_field.current_value == ["a", "b", "c"]
|
|
|
|
|
|
|
|
|
|
def test_region_field_returns_region_widget(self):
|
|
|
|
|
"""RegionConfig fields get region widget."""
|
|
|
|
|
fields = describe_fields(SettingsWithRegion, {
|
|
|
|
|
"region": {"north": 50.0, "south": 40.0, "east": -100.0, "west": -120.0}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
region_field = next(f for f in fields if f.name == "region")
|
|
|
|
|
assert region_field.widget == "region"
|
|
|
|
|
|
|
|
|
|
def test_empty_current_dict(self):
|
|
|
|
|
"""Works with empty current values dict."""
|
|
|
|
|
fields = describe_fields(SettingsWithOptional, {})
|
|
|
|
|
|
|
|
|
|
required_field = next(f for f in fields if f.name == "required_field")
|
|
|
|
|
assert required_field.current_value is None
|
|
|
|
|
assert required_field.widget == "text"
|
|
|
|
|
|
|
|
|
|
def test_field_descriptor_attributes(self):
|
|
|
|
|
"""FieldDescriptor has all expected attributes."""
|
|
|
|
|
fields = describe_fields(SimpleSettings, {"name": "test", "count": 1, "enabled": True})
|
|
|
|
|
field = fields[0]
|
|
|
|
|
|
|
|
|
|
assert hasattr(field, "name")
|
|
|
|
|
assert hasattr(field, "label")
|
|
|
|
|
assert hasattr(field, "widget")
|
|
|
|
|
assert hasattr(field, "current_value")
|
|
|
|
|
assert hasattr(field, "default")
|
|
|
|
|
assert hasattr(field, "description")
|
|
|
|
|
assert hasattr(field, "required")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestRealAdapterSchemas:
|
|
|
|
|
"""Test with actual adapter settings schemas."""
|
|
|
|
|
|
|
|
|
|
def test_nws_settings(self):
|
|
|
|
|
"""NWSSettings generates correct field descriptors."""
|
|
|
|
|
from central.adapters.nws import NWSSettings
|
|
|
|
|
|
|
|
|
|
fields = describe_fields(NWSSettings, {"contact_email": "test@example.com"})
|
|
|
|
|
|
|
|
|
|
assert len(fields) >= 1
|
|
|
|
|
email_field = next(f for f in fields if f.name == "contact_email")
|
|
|
|
|
assert email_field.widget == "text"
|
|
|
|
|
assert email_field.current_value == "test@example.com"
|
|
|
|
|
|
|
|
|
|
def test_firms_settings(self):
|
|
|
|
|
"""FIRMSSettings generates correct field descriptors."""
|
|
|
|
|
from central.adapters.firms import FIRMSSettings
|
|
|
|
|
|
|
|
|
|
fields = describe_fields(FIRMSSettings, {
|
|
|
|
|
"api_key_alias": "firms_key",
|
2026-05-19 00:38:06 +00:00
|
|
|
"satellites": ["VIIRS_SNPP_NRT"]
|
2026-05-18 23:16:37 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
key_field = next(f for f in fields if f.name == "api_key_alias")
|
|
|
|
|
assert key_field.widget == "text"
|
|
|
|
|
|
|
|
|
|
sat_field = next(f for f in fields if f.name == "satellites")
|
2026-05-19 00:38:06 +00:00
|
|
|
assert sat_field.widget == "checkboxes"
|
|
|
|
|
assert sat_field.current_value == ["VIIRS_SNPP_NRT"]
|
|
|
|
|
assert sat_field.options is not None
|
|
|
|
|
assert "VIIRS_SNPP_NRT" in sat_field.options
|
2026-05-18 23:16:37 +00:00
|
|
|
|
|
|
|
|
def test_usgs_quake_settings(self):
|
|
|
|
|
"""USGSQuakeSettings generates correct field descriptors."""
|
|
|
|
|
from central.adapters.usgs_quake import USGSQuakeSettings
|
|
|
|
|
|
|
|
|
|
fields = describe_fields(USGSQuakeSettings, {"feed": "all_hour"})
|
|
|
|
|
|
|
|
|
|
feed_field = next(f for f in fields if f.name == "feed")
|
2026-05-19 00:38:06 +00:00
|
|
|
assert feed_field.widget == "select"
|
2026-05-18 23:16:37 +00:00
|
|
|
assert feed_field.current_value == "all_hour"
|
2026-05-19 00:38:06 +00:00
|
|
|
assert feed_field.options is not None
|
|
|
|
|
assert "all_hour" in feed_field.options
|
|
|
|
|
assert "all_day" in feed_field.options
|
2026-05-18 23:16:37 +00:00
|
|
|
|
|
|
|
|
def test_all_adapters_have_region_field(self):
|
|
|
|
|
"""All adapter settings schemas include region field."""
|
|
|
|
|
from central.adapters.nws import NWSSettings
|
|
|
|
|
from central.adapters.firms import FIRMSSettings
|
|
|
|
|
from central.adapters.usgs_quake import USGSQuakeSettings
|
|
|
|
|
|
|
|
|
|
for schema in [NWSSettings, FIRMSSettings, USGSQuakeSettings]:
|
|
|
|
|
fields = describe_fields(schema, {})
|
|
|
|
|
region_field = next((f for f in fields if f.name == "region"), None)
|
|
|
|
|
assert region_field is not None, f"{schema.__name__} should have region field"
|
|
|
|
|
assert region_field.widget == "region"
|
2026-05-19 00:38:06 +00:00
|
|
|
|
|
|
|
|
class TestLiteralTypes:
|
|
|
|
|
"""Tests for Literal type support."""
|
|
|
|
|
|
|
|
|
|
def test_literal_maps_to_select(self):
|
|
|
|
|
"""Literal type maps to select widget with options."""
|
|
|
|
|
from typing import Literal
|
|
|
|
|
|
|
|
|
|
widget, options = _type_to_widget_and_options("field", Literal["a", "b", "c"])
|
|
|
|
|
assert widget == "select"
|
|
|
|
|
assert options == ["a", "b", "c"]
|
|
|
|
|
|
|
|
|
|
def test_list_literal_maps_to_checkboxes(self):
|
|
|
|
|
"""list[Literal] maps to checkboxes widget with options."""
|
|
|
|
|
from typing import Literal
|
|
|
|
|
|
|
|
|
|
widget, options = _type_to_widget_and_options("field", list[Literal["x", "y", "z"]])
|
|
|
|
|
assert widget == "checkboxes"
|
|
|
|
|
assert options == ["x", "y", "z"]
|
|
|
|
|
|
|
|
|
|
def test_optional_literal_maps_to_select(self):
|
|
|
|
|
"""Optional[Literal] maps to select widget."""
|
|
|
|
|
from typing import Literal, Optional
|
|
|
|
|
|
|
|
|
|
widget, options = _type_to_widget_and_options("field", Optional[Literal["one", "two"]])
|
|
|
|
|
assert widget == "select"
|
|
|
|
|
assert options == ["one", "two"]
|
|
|
|
|
|