""" Tests for the justify directive support in edit_schematic_component fieldPositions and set_schematic_component_property. Covers: - _set_justify_on_property() helper (unit tests, no file I/O) - fieldPositions[field].justify via edit_schematic_component (integration) - justify forwarding via set_schematic_component_property (integration) - Backward-compat: existing calls without justify must be unaffected """ import re import sys from pathlib import Path from typing import Any import pytest sys.path.insert(0, str(Path(__file__).parent.parent / "python")) TEMPLATE_SCH = Path(__file__).parent.parent / "python" / "templates" / "empty.kicad_sch" # Minimal placed-symbol block used across integration tests PLACED_RESISTOR_BLOCK = """\ (symbol (lib_id "Device:R") (at 50 50 0) (unit 1) (in_bom yes) (on_board yes) (dnp no) (uuid "aaaaaaaa-bbbb-cccc-dddd-ffffffffffff") (property "Reference" "R2" (at 51.27 47.46 0) (effects (font (size 1.27 1.27))) ) (property "Value" "4k7" (at 51.27 52.54 0) (effects (font (size 1.27 1.27))) ) (property "Footprint" "Resistor_SMD:R_0603_1608Metric" (at 50 50 0) (effects (font (size 1.27 1.27)) hide) ) (property "Datasheet" "~" (at 50 50 0) (effects (font (size 1.27 1.27)) hide) ) ) """ def _make_test_schematic(tmp_dir: Path, extra_block: str = "") -> Path: dest = tmp_dir / "justify_test.kicad_sch" src_content = TEMPLATE_SCH.read_text(encoding="utf-8") if extra_block: src_content = src_content.rstrip() if src_content.endswith(")"): src_content = src_content[:-1] + "\n" + extra_block + ")\n" dest.write_text(src_content, encoding="utf-8") return dest # --------------------------------------------------------------------------- # Unit tests — exercise _set_justify_on_property() directly, no file I/O # --------------------------------------------------------------------------- @pytest.mark.unit class TestSetJustifyOnPropertyHelper: """Unit tests for the _set_justify_on_property helper.""" def _get_interface(self) -> Any: from kicad_interface import KiCADInterface return KiCADInterface() def test_adds_justify_left(self) -> None: iface = self._get_interface() block = PLACED_RESISTOR_BLOCK result = iface._set_justify_on_property(block, "Reference", "left") assert "(justify left)" in result def test_adds_justify_right(self) -> None: iface = self._get_interface() block = PLACED_RESISTOR_BLOCK result = iface._set_justify_on_property(block, "Reference", "right") assert "(justify right)" in result def test_adds_combined_justify(self) -> None: iface = self._get_interface() block = PLACED_RESISTOR_BLOCK result = iface._set_justify_on_property(block, "Reference", "right top") assert "(justify right top)" in result def test_center_removes_directive(self) -> None: """Passing 'center' should remove any (justify ...) token (KiCad default).""" iface = self._get_interface() block = PLACED_RESISTOR_BLOCK # First set a non-center justify so there is something to remove block = iface._set_justify_on_property(block, "Reference", "left") assert "(justify left)" in block # Now reset to center block = iface._set_justify_on_property(block, "Reference", "center") assert "(justify" not in block def test_replaces_existing_justify(self) -> None: iface = self._get_interface() block = PLACED_RESISTOR_BLOCK block = iface._set_justify_on_property(block, "Reference", "left") block = iface._set_justify_on_property(block, "Reference", "right") # Should have exactly one justify token and it should be 'right' matches = re.findall(r"\(justify\s+[^)]+\)", block) assert len(matches) == 1 assert "right" in matches[0] def test_does_not_affect_other_fields(self) -> None: iface = self._get_interface() block = PLACED_RESISTOR_BLOCK result = iface._set_justify_on_property(block, "Reference", "left") # Value field must not have acquired a justify token value_section = result[result.find('(property "Value"') :] ref_section = result[result.find('(property "Reference"') :] # The justify token should appear before the Value property section justify_pos = result.find("(justify left)") value_prop_pos = result.find('(property "Value"') assert justify_pos < value_prop_pos def test_unknown_field_returns_block_unchanged(self) -> None: iface = self._get_interface() block = PLACED_RESISTOR_BLOCK result = iface._set_justify_on_property(block, "NoSuchField", "left") assert result == block # --------------------------------------------------------------------------- # Integration tests — real file I/O through edit_schematic_component # --------------------------------------------------------------------------- @pytest.mark.integration class TestFieldPositionsJustify: """Integration tests for justify via fieldPositions in edit_schematic_component.""" @pytest.fixture def sch_with_r2(self, tmp_path: Any) -> Any: return _make_test_schematic(tmp_path, PLACED_RESISTOR_BLOCK) def _get_interface(self) -> Any: from kicad_interface import KiCADInterface return KiCADInterface() def test_set_justify_left_on_reference(self, sch_with_r2: Any) -> None: iface = self._get_interface() result = iface.handle_command( "edit_schematic_component", { "schematicPath": str(sch_with_r2), "reference": "R2", "fieldPositions": {"Reference": {"x": 51.27, "y": 47.46, "justify": "left"}}, }, ) assert result["success"] is True content = sch_with_r2.read_text(encoding="utf-8") assert "(justify left)" in content def test_set_justify_right_on_value(self, sch_with_r2: Any) -> None: iface = self._get_interface() result = iface.handle_command( "edit_schematic_component", { "schematicPath": str(sch_with_r2), "reference": "R2", "fieldPositions": {"Value": {"x": 51.27, "y": 52.54, "justify": "right"}}, }, ) assert result["success"] is True content = sch_with_r2.read_text(encoding="utf-8") assert "(justify right)" in content def test_set_justify_center_removes_directive(self, sch_with_r2: Any) -> None: iface = self._get_interface() # First add a left justify so there is something to remove iface.handle_command( "edit_schematic_component", { "schematicPath": str(sch_with_r2), "reference": "R2", "fieldPositions": {"Reference": {"x": 51.27, "y": 47.46, "justify": "left"}}, }, ) # Now reset to center (default) result = iface.handle_command( "edit_schematic_component", { "schematicPath": str(sch_with_r2), "reference": "R2", "fieldPositions": {"Reference": {"x": 51.27, "y": 47.46, "justify": "center"}}, }, ) assert result["success"] is True content = sch_with_r2.read_text(encoding="utf-8") # Check that the Reference property block itself has no (justify ...) token. # (The lib_symbols section may legitimately contain justify directives.) ref_prop_start = content.find('(property "Reference" "R2"') assert ref_prop_start >= 0, "Reference property block not found" ref_prop_end = content.find("\n )", ref_prop_start) + len("\n )") ref_prop_block = content[ref_prop_start:ref_prop_end] assert "(justify" not in ref_prop_block def test_fieldpositions_without_justify_unchanged(self, sch_with_r2: Any) -> None: """Omitting justify must not alter any existing justify on the field.""" iface = self._get_interface() # Set a justify first iface.handle_command( "edit_schematic_component", { "schematicPath": str(sch_with_r2), "reference": "R2", "fieldPositions": {"Reference": {"x": 51.27, "y": 47.46, "justify": "left"}}, }, ) # Reposition without passing justify — the left directive must survive iface.handle_command( "edit_schematic_component", { "schematicPath": str(sch_with_r2), "reference": "R2", "fieldPositions": {"Reference": {"x": 60.0, "y": 47.46}}, }, ) content = sch_with_r2.read_text(encoding="utf-8") assert "(justify left)" in content def test_position_and_justify_applied_together(self, sch_with_r2: Any) -> None: """Both position and justify should be applied in a single call.""" iface = self._get_interface() result = iface.handle_command( "edit_schematic_component", { "schematicPath": str(sch_with_r2), "reference": "R2", "fieldPositions": {"Reference": {"x": 99.0, "y": 88.0, "justify": "right"}}, }, ) assert result["success"] is True content = sch_with_r2.read_text(encoding="utf-8") assert "(justify right)" in content assert "99.0 88.0" in content # --------------------------------------------------------------------------- # Integration tests — justify via set_schematic_component_property # --------------------------------------------------------------------------- @pytest.mark.integration class TestSetPropertyJustify: """Integration tests for justify via set_schematic_component_property.""" @pytest.fixture def sch_with_r2(self, tmp_path: Any) -> Any: return _make_test_schematic(tmp_path, PLACED_RESISTOR_BLOCK) def _get_interface(self) -> Any: from kicad_interface import KiCADInterface return KiCADInterface() def test_set_property_with_justify(self, sch_with_r2: Any) -> None: iface = self._get_interface() result = iface.handle_command( "set_schematic_component_property", { "schematicPath": str(sch_with_r2), "reference": "R2", "name": "Tolerance", "value": "1%", "justify": "left", }, ) assert result["success"] is True content = sch_with_r2.read_text(encoding="utf-8") assert "(justify left)" in content def test_set_property_without_justify_no_regression(self, sch_with_r2: Any) -> None: """Omitting justify on set_property must not add any justify token.""" iface = self._get_interface() result = iface.handle_command( "set_schematic_component_property", { "schematicPath": str(sch_with_r2), "reference": "R2", "name": "Rating", "value": "100mW", }, ) assert result["success"] is True content = sch_with_r2.read_text(encoding="utf-8") # No justify should appear in the newly added Rating property rating_start = content.find('(property "Rating"') rating_end = content.find(")", rating_start) assert "(justify" not in content[rating_start:rating_end]