Skip to content

Commit 198f65f

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

2 files changed

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

0 commit comments

Comments
 (0)