"""Integration tests for FreeCAD MCP GUI mode. These tests verify that the MCP bridge works correctly when FreeCAD is running in GUI mode (with full graphical interface). They test GUI-specific features like visibility, display modes, colors, and camera operations. Note: These tests require a running FreeCAD GUI server. Start it with: just run-gui To run these tests: pytest tests/integration/test_gui_mode.py -v """ from __future__ import annotations import tempfile from pathlib import Path from typing import TYPE_CHECKING, Any import pytest if TYPE_CHECKING: import xmlrpc.client from collections.abc import Generator # Mark all tests in this module as integration tests and gui tests pytestmark = [pytest.mark.integration, pytest.mark.gui] # Note: xmlrpc_proxy fixture is defined in conftest.py @pytest.fixture(scope="module") def temp_dir() -> Generator[str, None, None]: """Create a temporary directory for test files.""" with tempfile.TemporaryDirectory() as tmpdir: yield tmpdir def execute_code(proxy: xmlrpc.client.ServerProxy, code: str) -> dict[str, Any]: """Execute Python code via the MCP bridge and return the result.""" result: dict[str, Any] = proxy.execute(code) # type: ignore[assignment] assert result.get("success"), f"Execution failed: {result.get('error_traceback')}" return result class TestGUIConnection: """Tests for GUI mode connectivity and detection.""" def test_ping(self, xmlrpc_proxy: xmlrpc.client.ServerProxy) -> None: """Test that the server responds to ping.""" result: dict[str, Any] = xmlrpc_proxy.ping() # type: ignore[assignment] assert result["pong"] is True assert "timestamp" in result def test_gui_mode_detected(self, xmlrpc_proxy: xmlrpc.client.ServerProxy) -> None: """Test that FreeCAD is running in GUI mode (GuiUp=True).""" result = execute_code( xmlrpc_proxy, """ import FreeCAD _result_ = {"gui_up": FreeCAD.GuiUp} """, ) # In GUI mode, GuiUp should be True (1) assert result["result"]["gui_up"] == 1 or result["result"]["gui_up"] is True class TestGUIObjectCreation: """Tests for object creation in GUI mode.""" @pytest.fixture(autouse=True) def setup_document(self, xmlrpc_proxy: xmlrpc.client.ServerProxy) -> None: """Create a fresh document for each test.""" execute_code( xmlrpc_proxy, """ import FreeCAD # Close any existing GUITestDoc if "GUITestDoc" in FreeCAD.listDocuments(): FreeCAD.closeDocument("GUITestDoc") doc = FreeCAD.newDocument("GUITestDoc") _result_ = True """, ) def test_create_box_with_view_object( self, xmlrpc_proxy: xmlrpc.client.ServerProxy ) -> None: """Test creating a box with ViewObject in GUI mode.""" result = execute_code( xmlrpc_proxy, """ import FreeCAD import Part doc = FreeCAD.ActiveDocument box = Part.makeBox(10, 20, 30) obj = doc.addObject("Part::Feature", "GUIBox") obj.Shape = box doc.recompute() has_view_object = hasattr(obj, "ViewObject") and obj.ViewObject is not None _result_ = { "name": obj.Name, "volume": obj.Shape.Volume, "has_view_object": has_view_object, "valid": obj.Shape.isValid() } """, ) assert result["result"]["name"] == "GUIBox" assert abs(result["result"]["volume"] - 6000.0) < 0.01 assert result["result"]["has_view_object"] is True assert result["result"]["valid"] is True class TestVisibility: """Tests for object visibility operations in GUI mode.""" @pytest.fixture(autouse=True) def setup_document(self, xmlrpc_proxy: xmlrpc.client.ServerProxy) -> None: """Create a document with test objects.""" execute_code( xmlrpc_proxy, """ import FreeCAD import Part if "VisibilityTestDoc" in FreeCAD.listDocuments(): FreeCAD.closeDocument("VisibilityTestDoc") doc = FreeCAD.newDocument("VisibilityTestDoc") box = Part.makeBox(10, 10, 10) obj = doc.addObject("Part::Feature", "VisBox") obj.Shape = box doc.recompute() _result_ = True """, ) def test_hide_object(self, xmlrpc_proxy: xmlrpc.client.ServerProxy) -> None: """Test hiding an object.""" result = execute_code( xmlrpc_proxy, """ import FreeCAD doc = FreeCAD.ActiveDocument obj = doc.getObject("VisBox") # Hide the object obj.ViewObject.Visibility = False _result_ = { "visible": obj.ViewObject.Visibility } """, ) assert result["result"]["visible"] is False def test_show_object(self, xmlrpc_proxy: xmlrpc.client.ServerProxy) -> None: """Test showing an object.""" result = execute_code( xmlrpc_proxy, """ import FreeCAD doc = FreeCAD.ActiveDocument obj = doc.getObject("VisBox") # First hide, then show obj.ViewObject.Visibility = False obj.ViewObject.Visibility = True _result_ = { "visible": obj.ViewObject.Visibility } """, ) assert result["result"]["visible"] is True class TestDisplayMode: """Tests for display mode operations in GUI mode.""" @pytest.fixture(autouse=True) def setup_document(self, xmlrpc_proxy: xmlrpc.client.ServerProxy) -> None: """Create a document with test objects.""" execute_code( xmlrpc_proxy, """ import FreeCAD import Part if "DisplayModeTestDoc" in FreeCAD.listDocuments(): FreeCAD.closeDocument("DisplayModeTestDoc") doc = FreeCAD.newDocument("DisplayModeTestDoc") box = Part.makeBox(10, 10, 10) obj = doc.addObject("Part::Feature", "DisplayBox") obj.Shape = box doc.recompute() _result_ = True """, ) def test_get_display_modes(self, xmlrpc_proxy: xmlrpc.client.ServerProxy) -> None: """Test getting available display modes.""" result = execute_code( xmlrpc_proxy, """ import FreeCAD doc = FreeCAD.ActiveDocument obj = doc.getObject("DisplayBox") modes = obj.ViewObject.listDisplayModes() _result_ = { "modes": list(modes), "current": obj.ViewObject.DisplayMode } """, ) # Typical modes: "Flat Lines", "Shaded", "Wireframe", etc. assert len(result["result"]["modes"]) > 0 assert result["result"]["current"] is not None def test_set_display_mode(self, xmlrpc_proxy: xmlrpc.client.ServerProxy) -> None: """Test setting display mode.""" result = execute_code( xmlrpc_proxy, """ import FreeCAD doc = FreeCAD.ActiveDocument obj = doc.getObject("DisplayBox") modes = obj.ViewObject.listDisplayModes() # Set to a different mode if available target_mode = "Wireframe" if "Wireframe" in modes else modes[0] obj.ViewObject.DisplayMode = target_mode _result_ = { "mode_set": obj.ViewObject.DisplayMode, "target_mode": target_mode } """, ) assert result["result"]["mode_set"] == result["result"]["target_mode"] class TestObjectColor: """Tests for object color operations in GUI mode.""" @pytest.fixture(autouse=True) def setup_document(self, xmlrpc_proxy: xmlrpc.client.ServerProxy) -> None: """Create a document with test objects.""" execute_code( xmlrpc_proxy, """ import FreeCAD import Part if "ColorTestDoc" in FreeCAD.listDocuments(): FreeCAD.closeDocument("ColorTestDoc") doc = FreeCAD.newDocument("ColorTestDoc") box = Part.makeBox(10, 10, 10) obj = doc.addObject("Part::Feature", "ColorBox") obj.Shape = box doc.recompute() _result_ = True """, ) def test_set_shape_color(self, xmlrpc_proxy: xmlrpc.client.ServerProxy) -> None: """Test setting object shape color.""" result = execute_code( xmlrpc_proxy, """ import FreeCAD doc = FreeCAD.ActiveDocument obj = doc.getObject("ColorBox") # Set color to red (RGB as tuple of floats 0-1) obj.ViewObject.ShapeColor = (1.0, 0.0, 0.0) color = obj.ViewObject.ShapeColor _result_ = { "r": color[0], "g": color[1], "b": color[2] } """, ) assert result["result"]["r"] == pytest.approx(1.0, rel=0.01) assert result["result"]["g"] == pytest.approx(0.0, abs=0.01) assert result["result"]["b"] == pytest.approx(0.0, abs=0.01) def test_set_transparency(self, xmlrpc_proxy: xmlrpc.client.ServerProxy) -> None: """Test setting object transparency.""" result = execute_code( xmlrpc_proxy, """ import FreeCAD doc = FreeCAD.ActiveDocument obj = doc.getObject("ColorBox") # Set 50% transparency obj.ViewObject.Transparency = 50 _result_ = { "transparency": obj.ViewObject.Transparency } """, ) assert result["result"]["transparency"] == 50 class TestCameraOperations: """Tests for camera/view operations in GUI mode.""" @pytest.fixture(autouse=True) def setup_document(self, xmlrpc_proxy: xmlrpc.client.ServerProxy) -> None: """Create a document with test objects.""" execute_code( xmlrpc_proxy, """ import FreeCAD import Part if "CameraTestDoc" in FreeCAD.listDocuments(): FreeCAD.closeDocument("CameraTestDoc") doc = FreeCAD.newDocument("CameraTestDoc") box = Part.makeBox(10, 10, 10) obj = doc.addObject("Part::Feature", "CameraBox") obj.Shape = box doc.recompute() _result_ = True """, ) def test_fit_all(self, xmlrpc_proxy: xmlrpc.client.ServerProxy) -> None: """Test fit all view operation.""" result = execute_code( xmlrpc_proxy, """ import FreeCAD import FreeCADGui doc = FreeCAD.ActiveDocument view = FreeCADGui.ActiveDocument.ActiveView # Fit all objects in view view.fitAll() _result_ = {"success": True} """, ) assert result["result"]["success"] is True def test_set_view_direction(self, xmlrpc_proxy: xmlrpc.client.ServerProxy) -> None: """Test setting view direction (front, top, etc.).""" result = execute_code( xmlrpc_proxy, """ import FreeCAD import FreeCADGui doc = FreeCAD.ActiveDocument view = FreeCADGui.ActiveDocument.ActiveView # Set to front view view.viewFront() # Get camera orientation cam = view.getCameraOrientation() _result_ = { "success": True, "camera_orientation": [cam.Q[0], cam.Q[1], cam.Q[2], cam.Q[3]] } """, ) assert result["result"]["success"] is True class TestScreenshot: """Tests for screenshot functionality in GUI mode.""" def test_capture_screenshot( self, xmlrpc_proxy: xmlrpc.client.ServerProxy, temp_dir: str ) -> None: """Test capturing a screenshot.""" # First create a document with objects execute_code( xmlrpc_proxy, """ import FreeCAD import Part if "ScreenshotTestDoc" in FreeCAD.listDocuments(): FreeCAD.closeDocument("ScreenshotTestDoc") doc = FreeCAD.newDocument("ScreenshotTestDoc") box = Part.makeBox(20, 20, 20) obj = doc.addObject("Part::Feature", "ScreenshotBox") obj.Shape = box obj.ViewObject.ShapeColor = (0.0, 0.5, 1.0) # Blue color doc.recompute() _result_ = True """, ) screenshot_path = Path(temp_dir) / "test_screenshot.png" result = execute_code( xmlrpc_proxy, f""" import FreeCAD import FreeCADGui import os doc = FreeCAD.ActiveDocument view = FreeCADGui.ActiveDocument.ActiveView # Fit objects in view view.fitAll() # Save screenshot view.saveImage({screenshot_path!r}, 800, 600, "White") _result_ = {{ "saved": os.path.exists({screenshot_path!r}), "path": {screenshot_path!r} }} """, ) assert result["result"]["saved"] is True assert screenshot_path.exists() # Verify file has content assert screenshot_path.stat().st_size > 0 class TestComplexGUIWorkflow: """Tests for complex workflows that combine GUI and modeling features.""" def test_create_assembly_with_colors( self, xmlrpc_proxy: xmlrpc.client.ServerProxy ) -> None: """Test creating multiple objects with different colors.""" result = execute_code( xmlrpc_proxy, """ import FreeCAD import Part if "AssemblyTestDoc" in FreeCAD.listDocuments(): FreeCAD.closeDocument("AssemblyTestDoc") doc = FreeCAD.newDocument("AssemblyTestDoc") # Create base plate (gray) base = Part.makeBox(100, 100, 5) base_obj = doc.addObject("Part::Feature", "BasePlate") base_obj.Shape = base base_obj.ViewObject.ShapeColor = (0.5, 0.5, 0.5) # Create pillar 1 (red) pillar1 = Part.makeBox(10, 10, 50, FreeCAD.Vector(10, 10, 5)) pillar1_obj = doc.addObject("Part::Feature", "Pillar1") pillar1_obj.Shape = pillar1 pillar1_obj.ViewObject.ShapeColor = (1.0, 0.0, 0.0) # Create pillar 2 (green) pillar2 = Part.makeBox(10, 10, 50, FreeCAD.Vector(80, 10, 5)) pillar2_obj = doc.addObject("Part::Feature", "Pillar2") pillar2_obj.Shape = pillar2 pillar2_obj.ViewObject.ShapeColor = (0.0, 1.0, 0.0) # Create pillar 3 (blue) pillar3 = Part.makeBox(10, 10, 50, FreeCAD.Vector(80, 80, 5)) pillar3_obj = doc.addObject("Part::Feature", "Pillar3") pillar3_obj.Shape = pillar3 pillar3_obj.ViewObject.ShapeColor = (0.0, 0.0, 1.0) # Create pillar 4 (yellow) pillar4 = Part.makeBox(10, 10, 50, FreeCAD.Vector(10, 80, 5)) pillar4_obj = doc.addObject("Part::Feature", "Pillar4") pillar4_obj.Shape = pillar4 pillar4_obj.ViewObject.ShapeColor = (1.0, 1.0, 0.0) # Create top plate (semi-transparent gray) top = Part.makeBox(100, 100, 5, FreeCAD.Vector(0, 0, 55)) top_obj = doc.addObject("Part::Feature", "TopPlate") top_obj.Shape = top top_obj.ViewObject.ShapeColor = (0.5, 0.5, 0.5) top_obj.ViewObject.Transparency = 30 doc.recompute() _result_ = { "object_count": len(doc.Objects), "objects": [obj.Name for obj in doc.Objects], "all_visible": all(obj.ViewObject.Visibility for obj in doc.Objects) } """, ) assert result["result"]["object_count"] == 6 assert "BasePlate" in result["result"]["objects"] assert "TopPlate" in result["result"]["objects"] assert result["result"]["all_visible"] is True def test_export_with_screenshot( self, xmlrpc_proxy: xmlrpc.client.ServerProxy, temp_dir: str ) -> None: """Test creating model, setting view, taking screenshot, and exporting.""" step_path = Path(temp_dir) / "workflow_export.step" screenshot_path = Path(temp_dir) / "workflow_screenshot.png" result = execute_code( xmlrpc_proxy, f""" import FreeCAD import FreeCADGui import Part import os if "WorkflowTestDoc" in FreeCAD.listDocuments(): FreeCAD.closeDocument("WorkflowTestDoc") doc = FreeCAD.newDocument("WorkflowTestDoc") # Create a simple bracket shape # Base base = Part.makeBox(50, 30, 5) # Vertical support vertical = Part.makeBox(5, 30, 40, FreeCAD.Vector(0, 0, 5)) # Top flange with hole top_flange = Part.makeBox(20, 30, 5, FreeCAD.Vector(0, 0, 45)) hole = Part.makeCylinder(4, 10, FreeCAD.Vector(10, 15, 40)) # Combine and cut bracket = base.fuse(vertical).fuse(top_flange).cut(hole) bracket_obj = doc.addObject("Part::Feature", "Bracket") bracket_obj.Shape = bracket bracket_obj.ViewObject.ShapeColor = (0.2, 0.4, 0.8) doc.recompute() # Set isometric view view = FreeCADGui.ActiveDocument.ActiveView view.viewIsometric() view.fitAll() # Take screenshot view.saveImage({screenshot_path!r}, 800, 600, "White") # Export to STEP bracket_obj.Shape.exportStep({step_path!r}) _result_ = {{ "bracket_valid": bracket_obj.Shape.isValid(), "bracket_volume": bracket_obj.Shape.Volume, "screenshot_exists": os.path.exists({screenshot_path!r}), "step_exists": os.path.exists({step_path!r}) }} """, ) assert result["result"]["bracket_valid"] is True assert result["result"]["bracket_volume"] > 0 assert result["result"]["screenshot_exists"] is True assert result["result"]["step_exists"] is True assert screenshot_path.exists() assert step_path.exists()