# Makepad Testing Guide - Studio Remote Protocol (2026) ## Executive Summary Makepad uses the **Studio Remote Protocol** for automated UI testing. This is a JSON-based protocol that allows external tools to: - Launch and control Makepad applications - Take screenshots - Query widget trees - Simulate user interactions (clicks, typing, scrolling) - Inspect application state **Key Discovery:** Makepad does NOT have a `makepad-test` crate. Instead, testing is done through the Studio Remote bridge. ## Studio Remote Protocol Overview ### Architecture ``` ┌─────────────────┐ │ Test Script │ (Python, Bash, Rust, etc.) │ (JSON client) │ └────────┬────────┘ │ stdin/stdout (JSON lines) ▼ ┌─────────────────┐ │ Studio Remote │ target/release/cargo-makepad studio --studio=127.0.0.1:8001 │ Bridge │ └────────┬────────┘ │ WebSocket ▼ ┌─────────────────┐ │ Makepad Studio │ (Running on desktop) │ (Desktop) │ └────────┬────────┘ │ ▼ ┌─────────────────┐ │ Makepad App │ (Your application) │ (Under Test) │ └─────────────────┘ ``` ### Starting the Studio Remote Bridge ```bash # Start the bridge (one-time setup) target/release/cargo-makepad studio --studio=127.0.0.1:8001 # Or use port 8002 if 8001 is occupied target/release/cargo-makepad studio --studio=127.0.0.1:8002 ``` The bridge communicates via **newline-delimited JSON** on stdin/stdout. ## Testing Workflow ### 1. List Available Builds ```json {"ListBuilds":[]} ``` Response: ```json {"Builds":[{"build_id":[6],"name":"makepad-example-map","status":"running"}]} ``` ### 2. Clear Old Build (if exists) ```json {"ClearBuild":{"build_id":[6]}} ``` This stops the running build and clears Studio UI tabs. ### 3. Launch Application ```json {"RunItem":{"mount":"makepad","name":"makepad-example-map"}} ``` Wait for responses: ```json {"BuildStarted":{"build_id":[7]}} {"AppStarted":{"build_id":[7]}} ``` ### 4. Take Screenshot ```json {"Screenshot":{"build_id":[7],"kind_id":0}} ``` Response: ```json {"Screenshot":{"build_id":[7],"path":"/tmp/screenshot_7.png","width":1280,"height":840}} ``` ### 5. Query Widget Tree ```json {"WidgetTreeDump":{"build_id":[7]}} ``` Response includes widget hierarchy with coordinates for clicking. ### 6. Query Specific Widget ```json {"WidgetQuery":{"build_id":[7],"query":"id:map_view"}} ``` ### 7. Simulate Click ```json {"Click":{"build_id":[7],"x":640,"y":420}} ``` ### 8. Type Text ```json {"TypeText":{"build_id":[7],"text":"Hello World"}} ``` ### 9. Press Enter ```json {"Return":{"build_id":[7],"auto_dump":false}} ``` ### 10. Search Code ```json {"FindInFiles":{"mount":"makepad","pattern":"MapView","is_regex":false,"glob":"**/*.rs","max_results":200}} ``` Response: ```json {"SearchFileResults":{"results":[{"path":"widgets/src/map_view.rs","line":42,"column":5,"line_text":"pub struct MapView {...}"}],"done":true}} ``` ### 11. Read Code Range ```json {"ReadTextRange":{"path":"makepad/widgets/src/map_view.rs","start_line":40,"end_line":100}} ``` Response: ```json {"TextFileRange":{"path":"makepad/widgets/src/map_view.rs","start_line":40,"end_line":100,"total_lines":250,"content":"..."}} ``` ## Complete Testing Example ### Python Test Script ```python #!/usr/bin/env python3 """ Makepad Map Widget Test Suite Uses Studio Remote Protocol for automated UI testing """ import json import subprocess import sys import time from pathlib import Path class MakepadTestRunner: def __init__(self, studio_port=8001): self.studio_port = studio_port self.process = None self.build_id = None def start_bridge(self): """Start the Studio Remote bridge""" print(f"Starting Studio Remote bridge on port {self.studio_port}...") self.process = subprocess.Popen( ["target/release/cargo-makepad", "studio", f"--studio=127.0.0.1:{self.studio_port}"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True ) time.sleep(2) # Wait for bridge to start def send_request(self, request): """Send JSON request and read response""" json_str = json.dumps(request) + "\n" self.process.stdin.write(json_str) self.process.stdin.flush() # Read response (may be multiple lines) response = self.process.stdout.readline() return json.loads(response) def list_builds(self): """List all running builds""" return self.send_request({"ListBuilds": []}) def clear_build(self, build_id): """Clear a build""" return self.send_request({"ClearBuild": {"build_id": [build_id]}}) def run_item(self, mount, name): """Run a Makepad application""" return self.send_request({"RunItem": {"mount": mount, "name": name}}) def screenshot(self, build_id, kind_id=0): """Take a screenshot""" return self.send_request({"Screenshot": {"build_id": [build_id], "kind_id": kind_id}}) def widget_tree_dump(self, build_id): """Dump widget tree""" return self.send_request({"WidgetTreeDump": {"build_id": [build_id]}}) def widget_query(self, build_id, query): """Query a specific widget""" return self.send_request({"WidgetQuery": {"build_id": [build_id], "query": query}}) def click(self, build_id, x, y): """Simulate a click""" return self.send_request({"Click": {"build_id": [build_id], "x": x, "y": y}}) def type_text(self, build_id, text): """Type text""" return self.send_request({"TypeText": {"build_id": [build_id], "text": text}}) def press_return(self, build_id, auto_dump=False): """Press Enter key""" return self.send_request({"Return": {"build_id": [build_id], "auto_dump": auto_dump}}) def find_in_files(self, mount, pattern, is_regex=False, glob=None, max_results=200): """Search for pattern in files""" request = { "FindInFiles": { "mount": mount, "pattern": pattern, "is_regex": is_regex, "glob": glob, "max_results": max_results } } return self.send_request(request) def read_text_range(self, path, start_line, end_line): """Read a range of lines from a file""" return self.send_request({ "ReadTextRange": { "path": path, "start_line": start_line, "end_line": end_line } }) def wait_for_build_started(self, timeout=30): """Wait for BuildStarted response""" start_time = time.time() while time.time() - start_time < timeout: response = self.process.stdout.readline() if not response: continue data = json.loads(response) if "BuildStarted" in data: self.build_id = data["BuildStarted"]["build_id"][0] return self.build_id raise TimeoutError("Build did not start within timeout") def wait_for_app_started(self, timeout=30): """Wait for AppStarted response""" start_time = time.time() while time.time() - start_time < timeout: response = self.process.stdout.readline() if not response: continue data = json.loads(response) if "AppStarted" in data: return data["AppStarted"]["build_id"][0] raise TimeoutError("App did not start within timeout") def cleanup(self): """Clean up the bridge process""" if self.process: self.process.terminate() self.process.wait() def test_map_widget(): """Test the MapView widget""" runner = MakepadTestRunner() try: # Start bridge runner.start_bridge() # Clear any existing builds print("Clearing old builds...") builds = runner.list_builds() if "Builds" in builds: for build in builds["Builds"]: runner.clear_build(build["build_id"][0]) # Launch map example print("Launching map example...") runner.run_item("makepad", "makepad-example-map") # Wait for app to start build_id = runner.wait_for_build_started() runner.wait_for_app_started() print(f"App started with build_id: {build_id}") # Take initial screenshot print("Taking initial screenshot...") screenshot = runner.screenshot(build_id) print(f"Screenshot saved to: {screenshot['Screenshot']['path']}") # Query widget tree print("Querying widget tree...") tree = runner.widget_tree_dump(build_id) print(f"Widget tree dumped") # Query map widget print("Querying map widget...") map_query = runner.widget_query(build_id, "id:map") print(f"Map widget found") # Simulate user interaction: click on map print("Simulating click on map...") runner.click(build_id, 640, 420) # Take screenshot after click time.sleep(0.5) # Wait for UI to update screenshot2 = runner.screenshot(build_id) print(f"Screenshot after click: {screenshot2['Screenshot']['path']}") # Search for MapView in codebase print("Searching for MapView in codebase...") search = runner.find_in_files("makepad", "MapView", glob="**/*.rs") if "SearchFileResults" in search: print(f"Found {len(search['SearchFileResults']['results'])} occurrences of MapView") print("✓ All tests passed!") return True except Exception as e: print(f"✗ Test failed: {e}") import traceback traceback.print_exc() return False finally: runner.cleanup() if __name__ == "__main__": success = test_map_widget() sys.exit(0 if success else 1) ``` ### Bash Test Script ```bash #!/bin/bash # Makepad Map Widget Test (Bash version) STUDIO_PORT=8001 BRIDGE_PID="" cleanup() { if [ -n "$BRIDGE_PID" ]; then kill $BRIDGE_PID 2>/dev/null fi } trap cleanup EXIT # Start bridge echo "Starting Studio Remote bridge..." target/release/cargo-makepad studio --studio=127.0.0.1:$STUDIO_PORT & BRIDGE_PID=$! sleep 2 # Send request function send_request() { echo "$1" | target/release/cargo-makepad studio --studio=127.0.0.1:$STUDIO_PORT } # List builds echo "Listing builds..." send_request '{"ListBuilds":[]}' # Launch map example echo "Launching map example..." send_request '{"RunItem":{"mount":"makepad","name":"makepad-example-map"}}' # Wait for app to start sleep 5 # Take screenshot echo "Taking screenshot..." send_request '{"Screenshot":{"build_id":[1],"kind_id":0}}' # Query widget tree echo "Querying widget tree..." send_request '{"WidgetTreeDump":{"build_id":[1]}}' # Click on map echo "Clicking on map..." send_request '{"Click":{"build_id":[1],"x":640,"y":420}}' sleep 1 # Take another screenshot echo "Taking screenshot after click..." send_request '{"Screenshot":{"build_id":[1],"kind_id":0}}' echo "✓ Tests complete!" ``` ## Current Makepad Syntax (2026) ### Application Structure ```rust // src/main.rs pub use makepad_widgets; use makepad_widgets::*; app_main!(App); script_mod! { use mod.prelude.widgets.* startup() do #(App::script_component(vm)) { ui: Root { main_window := Window { window.inner_size: vec2(1280, 840) pass.clear_color: vec4(0.08, 0.10, 0.12, 1.0) body +: { map := MapView { width: Fill height: Fill } } } } } } #[derive(Script, ScriptHook)] pub struct App { #[live] ui: WidgetRef, } impl MatchEvent for App { fn handle_actions(&mut self, _cx: &mut Cx, _actions: &Actions) { // Handle widget actions } } impl AppMain for App { fn script_mod(vm: &mut ScriptVm) -> ScriptValue { crate::makepad_widgets::script_mod(vm); self::script_mod(vm) } fn handle_event(&mut self, cx: &mut Cx, event: &Event) { self.match_event(cx, event); self.ui.handle_event(cx, event, &mut Scope::empty()); } } ``` ### Key Syntax Changes (Old vs New) | Old (live_design!) | New (script_mod!) | |-------------------|-------------------| | `` | `mod.widgets.BaseWidget{}` | | `{{StructName}}` | `#(Struct::register_widget(vm))` | | `Name = value` | `Name: value` | | `name: Type{...}` | `name := Type{...}` | | `(THEME_COLOR_X)` | `theme.color_x` | | `` | `theme.font_regular` | | `draw_bg: {}` (replace) | `draw_bg +: {}` (merge) | | `instance hover: 0.0` | `hover: instance(0.0)` | | `uniform color: #fff` | `color: uniform(#fff)` | ### Widget Definition Pattern ```rust #[derive(Script, ScriptHook, Widget)] pub struct MyWidget { #[source] source: ScriptObjectRef, #[walk] walk: Walk, #[layout] layout: Layout, #[redraw] #[live] draw_bg: DrawQuad, #[live] draw_text: DrawText, #[rust] my_state: i32, } ``` ### Runtime Property Updates ```rust // Old system (live! macro) item.apply_over(cx, live!{ height: (height) }); // New system (script_apply_eval! macro) script_apply_eval!(cx, item, { height: #(height) }); ``` ## Testing Best Practices ### 1. Separate Business Logic ```rust // Testable business logic (no UI dependencies) pub struct MapLogic { center_lon: f64, center_lat: f64, zoom: f32, } impl MapLogic { pub fn new(lon: f64, lat: f64, zoom: f32) -> Self { Self { center_lon: lon, center_lat: lat, zoom, } } pub fn zoom_in(&mut self) -> f32 { self.zoom = (self.zoom + 1.0).min(18.0); self.zoom } pub fn zoom_out(&mut self) -> f32 { self.zoom = (self.zoom - 1.0).max(1.0); self.zoom } } #[cfg(test)] mod tests { use super::*; #[test] fn test_zoom_in() { let mut logic = MapLogic::new(0.0, 0.0, 10.0); assert_eq!(logic.zoom_in(), 11.0); assert_eq!(logic.zoom_in(), 12.0); } #[test] fn test_zoom_out() { let mut logic = MapLogic::new(0.0, 0.0, 10.0); assert_eq!(logic.zoom_out(), 9.0); assert_eq!(logic.zoom_out(), 8.0); } } ``` ### 2. Use Studio Remote for UI Testing ```python # Test UI interactions def test_map_zoom(): runner = MakepadTestRunner() runner.start_bridge() runner.run_item("makepad", "makepad-example-map") build_id = runner.wait_for_build_started() # Take screenshot at zoom 10 screenshot1 = runner.screenshot(build_id) # Simulate zoom in (scroll up) runner.click(build_id, 640, 420) runner.type_text(build_id, "+") # Or use scroll # Take screenshot at zoom 11 screenshot2 = runner.screenshot(build_id) # Compare screenshots assert screenshots_differ(screenshot1, screenshot2) ``` ### 3. Document Test Cases ```markdown # Map Widget Test Cases ## TC-001: Map Renders **Steps:** 1. Launch map example via Studio Remote 2. Wait for AppStarted 3. Take screenshot **Expected:** Screenshot shows map with tiles loaded **Actual:** Map renders correctly **Status:** PASS ✓ ## TC-002: Map Click Interaction **Steps:** 1. Launch map example 2. Query widget tree for map widget 3. Click on map at coordinates (640, 420) 4. Take screenshot **Expected:** Map responds to click (pan or zoom) **Actual:** Map pans to new location **Status:** PASS ✓ ``` ### 4. Use Release Builds for Testing ```bash # Always use release builds for UI testing cargo build --release # Run bridge with release build target/release/cargo-makepad studio --studio=127.0.0.1:8001 ``` ### 5. Clear Old Builds Before Testing ```python # Always clear old builds before launching new tests builds = runner.list_builds() if "Builds" in builds: for build in builds["Builds"]: runner.clear_build(build["build_id"][0]) # Now launch fresh runner.run_item("makepad", "makepad-example-map") ``` ## Available Studio Remote Commands ### Build Management - `ListBuilds` - List all running builds - `ClearBuild` - Stop and clear a build - `StopBuild` - Stop a build without clearing - `RunItem` - Launch a runnable item ### UI Inspection - `Screenshot` - Take a screenshot - `WidgetTreeDump` - Dump widget hierarchy - `WidgetQuery` - Query specific widget ### User Interaction - `Click` - Simulate mouse click - `TypeText` - Type text - `Return` - Press Enter key ### Code Search - `FindInFiles` - Search for pattern - `ReadTextRange` - Read file range ### Advanced - `ForwardToApp` - Send raw binary message to app ## Limitations ### What Studio Remote Doesn't Have (Yet) 1. **No visual regression comparison** - You need to implement screenshot comparison 2. **No accessibility testing** - No WCAG compliance tools 3. **No performance profiling** - Use external profilers 4. **No cross-platform testing** - Tests run on desktop only ### Workarounds 1. **Visual regression**: Use Python PIL or OpenCV for image comparison 2. **Accessibility**: Manual testing with screen readers 3. **Performance**: Use `perf`, ` Instruments`, or browser DevTools 4. **Cross-platform**: Run tests on each platform separately ## Resources - [Makepad AGENTS.md](https://github.com/makepad/makepad/blob/dev/AGENTS.md) - Official testing guide - [Makepad Examples](https://github.com/makepad/makepad/tree/dev/examples) - Example applications - [Makepad Studio](https://github.com/makepad/makepad/tree/dev/studio) - Studio source code - [Makepad Widgets](https://github.com/makepad/makepad/tree/dev/widgets) - Widget library ## Conclusion Makepad uses the **Studio Remote Protocol** for automated UI testing. This is a powerful JSON-based protocol that allows: 1. **Launching applications** via `RunItem` 2. **Taking screenshots** via `Screenshot` 3. **Querying widgets** via `WidgetTreeDump` and `WidgetQuery` 4. **Simulating interactions** via `Click`, `TypeText`, `Return` 5. **Searching code** via `FindInFiles` and `ReadTextRange` **Key Points:** - No `makepad-test` crate exists - Testing is done through the Studio Remote bridge - Use `script_mod!` syntax (not `live_design!`) - Always use release builds for testing - Clear old builds before launching new tests - Separate business logic from UI code for unit testing This is the **current best practice** for testing Makepad applications in 2026.