How the code is organized and how to extend it.
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ YAML │────▶│ Executor │────▶│ Report │
│ Parser │ │ (Driver) │ │ Generator │
└─────────────┘ └─────────────┘ └─────────────┘
flow/ core/ + driver/ report/
- YAML Parser (
pkg/flow) — Parses Maestro flow files into typed step structures - Executor (
pkg/executor) — Orchestrates flow execution, delegates commands to Driver implementations - Report (
pkg/report) — Generates test reports (JSON, HTML)
| Package | Purpose |
|---|---|
pkg/cli |
CLI commands and argument parsing |
pkg/config |
Configuration file loading (config.yaml) |
pkg/core |
Core types: Driver interface, CommandResult, Status, Artifacts |
pkg/device |
Android device management via ADB |
pkg/driver/appium |
Appium driver (Android/iOS, local and cloud) |
pkg/driver/uiautomator2 |
UIAutomator2 driver (Android, direct) |
pkg/driver/wda |
WebDriverAgent driver (iOS) |
pkg/driver/mock |
Mock driver for testing |
pkg/executor |
Flow runner — orchestrates step execution and callbacks |
pkg/flow |
YAML parsing, Step types, Selectors |
pkg/jsengine |
JavaScript evaluation engine (evalScript, assertTrue) |
pkg/report |
JSON and HTML report generation |
pkg/uiautomator2 |
UIAutomator2 HTTP protocol client |
pkg/validator |
Pre-execution flow validation and tag filtering |
The abstraction all backends implement:
type Driver interface {
Execute(step flow.Step) *CommandResult
Screenshot() ([]byte, error)
Hierarchy() ([]byte, error)
GetState() *StateSnapshot
GetPlatformInfo() *PlatformInfo
SetFindTimeout(ms int)
SetWaitForIdleTimeout(ms int) error
}All flow steps implement:
type Step interface {
Type() StepType
IsOptional() bool
Label() string
Describe() string
}pkg/driver/mydriver/
├── driver.go # Driver implementation
├── driver_test.go # Tests
└── commands.go # Command implementations
package mydriver
import (
"github.com/devicelab-dev/maestro-runner/pkg/core"
"github.com/devicelab-dev/maestro-runner/pkg/flow"
)
type Driver struct {
findTimeout int
}
func (d *Driver) Execute(step flow.Step) *core.CommandResult {
switch s := step.(type) {
case *flow.TapOnStep:
return d.executeTap(s)
case *flow.InputTextStep:
return d.executeInputText(s)
default:
return &core.CommandResult{
Success: false,
Error: fmt.Errorf("unsupported step: %s", step.Type()),
}
}
}
func (d *Driver) Screenshot() ([]byte, error) { /* capture screenshot */ }
func (d *Driver) Hierarchy() ([]byte, error) { /* capture UI hierarchy */ }
func (d *Driver) GetState() *core.StateSnapshot { /* return current state */ }
func (d *Driver) GetPlatformInfo() *core.PlatformInfo { /* return platform info */ }
func (d *Driver) SetFindTimeout(ms int) { d.findTimeout = ms }
func (d *Driver) SetWaitForIdleTimeout(ms int) error { return nil }Wire it up in pkg/cli/test.go where drivers are selected based on the --driver flag.
const (
StepMyNewCommand StepType = "myNewCommand"
)type MyNewCommandStep struct {
BaseStep `yaml:",inline"`
Target string `yaml:"target"`
}
func (s *MyNewCommandStep) Describe() string {
return fmt.Sprintf("myNewCommand on %s", s.Target)
}Add a case to parseStep() for the new command key.
In each driver's Execute() method, add a case *flow.MyNewCommandStep branch.
- Parser test in
pkg/flow/parser_test.go - Driver test in
pkg/driver/<driver>/driver_test.go
SuiteResult
├── Flows []FlowResult
│ ├── PlatformInfo
│ ├── OnFlowStart []StepResult
│ ├── Steps []StepResult
│ │ ├── Status (passed/failed/skipped/warned/errored)
│ │ ├── Duration
│ │ ├── Error
│ │ ├── Attachments (screenshots, hierarchy)
│ │ └── SubFlowResult (for runFlow steps)
│ └── OnFlowComplete []StepResult
└── Summary (passed/failed/skipped counts)
| Status | Meaning |
|---|---|
pending |
Not yet executed |
running |
Currently executing |
passed |
Completed successfully |
failed |
Assertion/expectation failed |
errored |
Unexpected error occurred |
skipped |
Skipped (condition not met) |
warned |
Passed with warnings |