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.
708 lines
19 KiB
Markdown
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.
|