feat: Maestro driver integration + DeviceDriver architecture (#32)

* proto(driver): drop launcher_activity from LaunchRequest

* refactor(driver): rename Driver to DeviceDriver, drop launcherActivity from Launch

* refactor(driver): rename package maestro to sidecar

* feat(driver): add ChromeDriver backed by chromedp

* feat(hierarchy): replace XML parser with TreeNode JSON parser

* refactor(driver): update mock and runner to DeviceDriver, drop launcherActivity

* feat(runner): add platform routing for web vs sidecar

* test(verifier): update hierarchy fixtures from XML to TreeNode JSON

* feat(sidecar): extract readLogcat/readProcMetrics; add MaestroDriverBackend

- Extract readLogcat() and readProcMetrics() as internal package-level
  helpers parameterised by serial
- Add MaestroDriverBackend wiring maestro-client AndroidDriver
- Drop launcherActivity from DriverBackend interface and StubDriverBackend
- Add maestro-utils and micrometer-core as explicit compile deps

* refactor(sidecar): inject DriverBackend into DriverService; drop launcherActivity

- Remove serial and default backend from DriverService constructor
- Make backend a required parameter
- Drop launcherActivity from launch RPC handler
- Create MaestroDriverBackend in Main.kt when platform is android

* test(sidecar): update tests for dropped launcherActivity and required backend

* chore(sidecar): remove unnecessary micrometer-core direct dependency
This commit is contained in:
pj authored and GitHub committed 2026-04-22 19:02:39 +07:00
1 parent b667abbbca
commit d5b6f6ccaf
26 files changed
+880 -450

No files matched your search

+92 -14
View File
@@ -2,14 +2,30 @@ package hierarchy
import "testing"
const sampleDump = `<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<hierarchy rotation="0">
<node class="android.widget.LinearLayout" package="app" bounds="[0,0][1080,2340]">
<node resource-id="app:id/title" text="Hello" bounds="[10,20][200,60]" clickable="false" enabled="true"/>
<node resource-id="app:id/row" text="Alice" content-desc="row" bounds="[0,100][1080,200]" clickable="true" enabled="true"/>
<node resource-id="app:id/row" text="Bob" content-desc="row" bounds="[0,200][1080,300]" clickable="true" enabled="true"/>
</node>
</hierarchy>`
// sampleDump is a Maestro TreeNode JSON equivalent of the old XML fixture.
const sampleDump = `{
"attributes": {"class": "android.widget.LinearLayout", "package": "app", "bounds": "[0,0,1080,2340]"},
"children": [
{
"attributes": {"resource-id": "app:id/title", "text": "Hello", "bounds": "[10,20,200,60]"},
"children": [],
"clickable": false,
"enabled": true
},
{
"attributes": {"resource-id": "app:id/row", "text": "Alice", "content-desc": "row", "bounds": "[0,100,1080,200]"},
"children": [],
"clickable": true,
"enabled": true
},
{
"attributes": {"resource-id": "app:id/row", "text": "Bob", "content-desc": "row", "bounds": "[0,200,1080,300]"},
"children": [],
"clickable": true,
"enabled": true
}
]
}`
func TestParseCountsNodes(t *testing.T) {
tree, err := Parse(sampleDump)
@@ -75,14 +91,76 @@ func TestUnknownSelector(t *testing.T) {
}
func TestDescPrefix(t *testing.T) {
xml := `<hierarchy>
<node content-desc="customer_row_abc-123" bounds="[0,0][100,100]"/>
<node content-desc="customer_row_def-456" bounds="[0,100][100,200]"/>
<node content-desc="supplier_row_xyz" bounds="[0,200][100,300]"/>
</hierarchy>`
tree, _ := Parse(xml)
input := `{
"attributes": {},
"children": [
{"attributes": {"content-desc": "customer_row_abc-123", "bounds": "[0,0,100,100]"}, "children": []},
{"attributes": {"content-desc": "customer_row_def-456", "bounds": "[0,100,100,200]"}, "children": []},
{"attributes": {"content-desc": "supplier_row_xyz", "bounds": "[0,200,100,300]"}, "children": []}
]
}`
tree, _ := Parse(input)
rows := tree.FindAll("descPrefix:customer_row_")
if len(rows) != 2 {
t.Fatalf("want 2 customer rows, got %d", len(rows))
}
}
func TestBoolFieldsFromNode(t *testing.T) {
input := `{
"attributes": {"resource-id": "x", "bounds": "[0,0,100,100]"},
"children": [],
"clickable": true,
"enabled": false,
"focused": true,
"checked": true,
"selected": false
}`
tree, err := Parse(input)
if err != nil {
t.Fatalf("Parse: %v", err)
}
if len(tree.Elements) != 1 {
t.Fatalf("want 1 element, got %d", len(tree.Elements))
}
el := tree.Elements[0]
if !el.Clickable {
t.Error("expected clickable=true")
}
if el.Enabled {
t.Error("expected enabled=false")
}
if !el.Focused {
t.Error("expected focused=true")
}
if !el.Checked {
t.Error("expected checked=true")
}
if el.Selected {
t.Error("expected selected=false")
}
}
func TestIdentifierFallback(t *testing.T) {
input := `{
"attributes": {"identifier": "my-button", "bounds": "[0,0,100,100]"},
"children": []
}`
tree, _ := Parse(input)
el := tree.Find("id:my-button")
if el == nil {
t.Fatal("expected match via identifier fallback")
}
}
func TestAccessibilityTextFallback(t *testing.T) {
input := `{
"attributes": {"accessibilityText": "Close dialog", "bounds": "[0,0,50,50]"},
"children": []
}`
tree, _ := Parse(input)
el := tree.Find("desc:Close dialog")
if el == nil {
t.Fatal("expected match via accessibilityText fallback")
}
}