Skip to content

Commit 31f9e86

Browse files
committed
fix(component editor): ensure "Notes" is always the last field
1 parent 907408a commit 31f9e86

2 files changed

Lines changed: 159 additions & 0 deletions

File tree

ardupilot_methodic_configurator/data_model_vehicle_components_base.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,7 @@ def _reorder_components(self, existing_components: ComponentData) -> ComponentDa
432432
reordered_components.update(remaining_components)
433433

434434
# Second step: for each component, ensure Product fields are in correct order (Version before URL)
435+
# and ensure "Notes" is always the last field
435436
for component_name, component_data in reordered_components.items():
436437
if "Product" in component_data and isinstance(component_data["Product"], dict):
437438
product = component_data["Product"]
@@ -448,6 +449,10 @@ def _reorder_components(self, existing_components: ComponentData) -> ComponentDa
448449
ordered_product[field] = value
449450
reordered_components[component_name]["Product"] = ordered_product
450451

452+
if "Notes" in component_data:
453+
notes_value = component_data.pop("Notes")
454+
component_data["Notes"] = notes_value
455+
451456
return reordered_components
452457

453458
def init_battery_chemistry(self) -> None:

tests/test_data_model_vehicle_components_base.py

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1529,3 +1529,157 @@ def test_user_sees_complex_product_sections_with_extra_fields_preserved(self, mo
15291529
assert product["Custom Field 1"] == "Custom Value 1"
15301530
assert product["Custom Field 2"] == "Custom Value 2"
15311531
assert product["Custom Field 3"] == "Custom Value 3"
1532+
1533+
def test_user_sees_notes_field_always_last_in_component(self, model_instance) -> None:
1534+
"""
1535+
User sees the Notes field displayed at the bottom of every component editor section.
1536+
1537+
GIVEN: A component where "Notes" appears before other fields
1538+
WHEN: The system processes the component data
1539+
THEN: "Notes" is moved to be the very last field in that component
1540+
AND: All other field values and their order are preserved
1541+
"""
1542+
# Arrange: Component with Notes appearing before connection data
1543+
existing_components = {
1544+
"Telemetry": {
1545+
"Notes": "SiK radio on Telem1",
1546+
"Product": {"Manufacturer": "RFDesign", "Model": "RFD900x"},
1547+
"FC Connection": {"Type": "SERIAL1", "Protocol": "MAVLink2"},
1548+
}
1549+
}
1550+
1551+
# Act
1552+
result = model_instance._reorder_components(existing_components)
1553+
1554+
# Assert: Notes is last
1555+
fields = list(result["Telemetry"].keys())
1556+
assert fields[-1] == "Notes", "Notes should be the last field in the component"
1557+
assert result["Telemetry"]["Notes"] == "SiK radio on Telem1"
1558+
# Other fields are preserved in their original relative order
1559+
assert fields.index("Product") < fields.index("FC Connection")
1560+
1561+
def test_user_sees_notes_field_last_when_already_at_end(self, model_instance) -> None:
1562+
"""
1563+
User sees Notes remain last when it is already the last field.
1564+
1565+
GIVEN: A component where "Notes" is already the last field
1566+
WHEN: The system processes the component data
1567+
THEN: "Notes" stays last and all other fields are untouched
1568+
"""
1569+
# Arrange: Notes already last
1570+
existing_components = {
1571+
"GNSS Receiver": {
1572+
"Product": {"Manufacturer": "u-blox", "Model": "NEO-M8N"},
1573+
"FC Connection": {"Type": "SERIAL3", "Protocol": "AUTO"},
1574+
"Notes": "Primary GPS",
1575+
}
1576+
}
1577+
1578+
# Act
1579+
result = model_instance._reorder_components(existing_components)
1580+
1581+
# Assert
1582+
fields = list(result["GNSS Receiver"].keys())
1583+
assert fields[-1] == "Notes"
1584+
assert result["GNSS Receiver"]["Notes"] == "Primary GPS"
1585+
1586+
def test_user_sees_components_without_notes_field_unaffected(self, model_instance) -> None:
1587+
"""
1588+
User sees components that have no Notes field remain completely unaffected by Notes ordering.
1589+
1590+
GIVEN: Components with no Notes field
1591+
WHEN: The system processes the component data
1592+
THEN: All component fields are preserved without modification
1593+
"""
1594+
# Arrange
1595+
existing_components = {
1596+
"Motors": {
1597+
"Product": {"Manufacturer": "T-Motor", "Model": "F60 Pro IV"},
1598+
"Specifications": {"Poles": 14},
1599+
},
1600+
"Propellers": {
1601+
"Product": {"Manufacturer": "HQProp", "Model": "5.1x4.6"},
1602+
"Specifications": {"Diameter_inches": 5.1},
1603+
},
1604+
}
1605+
1606+
# Act
1607+
result = model_instance._reorder_components(existing_components)
1608+
1609+
# Assert: No Notes key injected, all fields unchanged
1610+
assert "Notes" not in result["Motors"]
1611+
assert "Notes" not in result["Propellers"]
1612+
assert list(result["Motors"].keys()) == ["Product", "Specifications"]
1613+
assert list(result["Propellers"].keys()) == ["Product", "Specifications"]
1614+
1615+
def test_user_sees_notes_last_across_all_components_simultaneously(self, model_instance) -> None:
1616+
"""
1617+
User sees Notes consistently last in every component when multiple components are processed together.
1618+
1619+
GIVEN: Multiple components each with Notes in different positions
1620+
WHEN: The system processes all component data at once
1621+
THEN: Notes is the last field in every component that has it
1622+
AND: Components without Notes are unaffected
1623+
"""
1624+
# Arrange: Multiple components with Notes in varying positions
1625+
existing_components = {
1626+
"Flight Controller": {
1627+
"Notes": "flight controller notes",
1628+
"Product": {"Manufacturer": "Holybro", "Model": "Pixhawk 6C"},
1629+
"Firmware": {"Type": "ArduCopter", "Version": "4.6.x"},
1630+
},
1631+
"Battery": {
1632+
"Product": {"Manufacturer": "Tattu"},
1633+
"Notes": "battery notes",
1634+
"Specifications": {"Chemistry": "Lipo"},
1635+
},
1636+
"ESC": {
1637+
"Product": {"Manufacturer": "Mamba"},
1638+
"FC->ESC Connection": {"Type": "Main Out", "Protocol": "DShot600"},
1639+
"ESC->FC Telemetry": {"Type": "Main Out", "Protocol": "BDShot"},
1640+
"Notes": "esc notes",
1641+
},
1642+
"Motors": {
1643+
"Specifications": {"Poles": 14},
1644+
# No Notes field
1645+
},
1646+
}
1647+
1648+
# Act
1649+
result = model_instance._reorder_components(existing_components)
1650+
1651+
# Assert: Notes is last in every component that has it
1652+
for component_name in ["Flight Controller", "Battery", "ESC"]:
1653+
fields = list(result[component_name].keys())
1654+
assert fields[-1] == "Notes", f"Notes should be last in {component_name}, got: {fields}"
1655+
1656+
# Motors has no Notes and should be unaffected
1657+
assert "Notes" not in result["Motors"]
1658+
1659+
def test_system_moves_notes_last_during_update_json_structure(self, model_instance) -> None:
1660+
"""
1661+
System moves Notes to be the last field during a full update_json_structure call.
1662+
1663+
GIVEN: A stored vehicle_components.json where Notes appears before connection data
1664+
WHEN: update_json_structure is called on load
1665+
THEN: Notes is the last field in the affected component after migration
1666+
"""
1667+
# Arrange: Inject raw data with Notes in the middle
1668+
model_instance._data = {
1669+
"Format version": 1,
1670+
"Components": {
1671+
"RC Receiver": {
1672+
"Notes": "FrSky receiver",
1673+
"Product": {"Manufacturer": "FrSky", "Model": "R-XSR"},
1674+
"FC Connection": {"Type": "RCin/SBUS", "Protocol": "SBUS"},
1675+
}
1676+
},
1677+
}
1678+
1679+
# Act
1680+
model_instance.update_json_structure()
1681+
1682+
# Assert
1683+
rc_fields = list(model_instance._data["Components"]["RC Receiver"].keys())
1684+
assert rc_fields[-1] == "Notes", f"Notes should be last after update_json_structure, got: {rc_fields}"
1685+
assert model_instance._data["Components"]["RC Receiver"]["Notes"] == "FrSky receiver"

0 commit comments

Comments
 (0)