nigig-org/MAKEPAD_STUDIO_REMOTE_TESTING.md
andodeki eeffa2af06 docs: add Studio Remote Protocol testing guide (2026)
This guide documents the current Makepad testing approach using the
Studio Remote Protocol, based on the latest dev branch and AGENTS.md.

Key discoveries:
- Makepad does NOT have a makepad-test crate
- Testing is done via Studio Remote bridge (JSON protocol)
- Uses script_mod! syntax (not live_design!)
- Supports screenshots, widget queries, clicks, typing, code search

Includes:
- Complete protocol reference
- Python and Bash test scripts
- Current Makepad syntax (2026)
- Testing best practices
- Example test cases for map widget

This supersedes the previous MAKEPAD_TESTING_BEST_PRACTICES.md
which was based on older Makepad patterns.
2026-07-27 17:18:58 +00:00

708 lines
19 KiB
Markdown

# 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!) |
|-------------------|-------------------|
| `<BaseWidget>` | `mod.widgets.BaseWidget{}` |
| `{{StructName}}` | `#(Struct::register_widget(vm))` |
| `Name = value` | `Name: value` |
| `name: Type{...}` | `name := Type{...}` |
| `(THEME_COLOR_X)` | `theme.color_x` |
| `<THEME_FONT>` | `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.