The Go client
It speaks to the same tool layer as the CLI and the MCP server, by spawning
mobium pipe, so the mobium binary has to be on PATH or named by
MOBIUM_BIN_PATH. The quick start goes from nothing to a
running script.
Install
go get github.com/mobiumdev/mobium/clients/go
Connect
dev, err := mobium.Start(ctx, mobium.WithPlatform("android"), mobium.WithApp("com.android.settings"))
if err != nil {
return err
}
defer dev.Quit(ctx)
els, err := dev.Map(ctx)
if err != nil {
return err
}
for _, el := range els {
fmt.Println(el.Ref, el.Label)
}
dev below is a *mobium.Device from Start or Connect, and ctx a context.Context. Each example is the body of a function that returns an error.
Every tool, by method
Connecting, ending, and errors
Start
func Start(ctx context.Context, opts ...Option) (*Device, error)
Start connects and opens the session on the device: the device-side server is started now, and the app, if one was named with WithApp, launched and in front. End it with Quit.
Nothing requires it — every call opens a session on first use — but it puts the slow first start (installing UiAutomator2, building WebDriverAgent on an iPhone) where it was asked for, and says which device it got.
Start a session on Android and launch an app
dev, err := mobium.Start(ctx, mobium.WithPlatform("android"), mobium.WithApp("com.example.shop"))
if err != nil {
log.Fatal(err)
}
defer dev.Quit(ctx)
Start on an iOS simulator and see what you got
dev, err := mobium.Start(ctx, mobium.WithPlatform("ios"))
if err != nil {
return err
}
defer dev.Quit(ctx)
s := dev.Session()
fmt.Println(s.Device, s.Platform, s.Driver, s.Reused)
Quit
func (d *Device) Quit(ctx context.Context) error
Quit ends the session on the device and closes the connection. The session's teardown is the daemon's own: accessibility settings put back, a recording or route stopped, WebViews detached, the device-side server stopped, and the app Start launched, if any, stopped too. Quitting a session that is not open succeeds, and a second Quit — a deferred one after an explicit one, say — does nothing.
A program that exits without Quit or Close has the sessions it started ended for it, the same way: mobium sees the client go.
End the session, even when the test fails
dev, err := mobium.Start(ctx)
if err != nil {
return err
}
defer dev.Quit(ctx) // a second Quit does nothing
Connect
func Connect(opts ...Option) (*Device, error)
Connect opens a connection to mobium. It does not touch the device: the session there opens on the first call that needs it, or with Start.
The transport is mobium pipe, which forwards to the shared daemon rather
than starting a session of its own: a device-side server holds one session
at a time, so a client with its own would invalidate the CLI's.
Connect without touching the device
dev, err := mobium.Connect()
if err != nil {
return err
}
defer dev.Close() // leaves the session open for the CLI
Connect to one device, on a daemon of its own
dev, err := mobium.Connect(mobium.WithDevice("emulator-5554"), mobium.WithSession("pixel"))
if err != nil {
return err
}
defer dev.Close()
Close
func (d *Device) Close() error
Close closes the connection. The device's session lives in the daemon and stays open, for the next Connect or the CLI; Quit ends it.
Close the connection and keep the session
if err := dev.Close(); err != nil {
return err
}
Session
type Session struct {
Device string `json:"device"`
Platform string `json:"platform"`
Driver string `json:"driver"`
// Reused says a session was already open on the device and was kept.
Reused bool `json:"reused"`
// App is the app Start launched, if one was asked for.
App string `json:"app"`
}
func (d *Device) Session() *Session
Session is what Start found: the device and how it is driven.
Ask which device Start opened
if s := dev.Session(); s != nil {
fmt.Println(s.Device, s.Driver)
} // nil for a Device from Connect
Sessions
func (d *Device) Sessions(ctx context.Context) ([]Session, error)
Sessions lists the sessions open on the daemon: device, platform, driver.
List the sessions open on the daemon
sessions, err := dev.Sessions(ctx)
if err != nil {
return err
}
for _, s := range sessions {
fmt.Println(s.Device, s.Platform, s.Driver)
}
WithBinary
func WithBinary(path string) Option
WithBinary pins the mobium executable, ahead of MOBIUM_BIN_PATH and PATH.
Pin the mobium binary
dev, err := mobium.Connect(mobium.WithBinary("/usr/local/bin/mobium"))
if err != nil {
return err
}
defer dev.Close()
WithDevice
func WithDevice(serial string) Option
WithDevice targets one device by serial or UDID. Omit it when only one device is running.
Target one device by serial or UDID
dev, err := mobium.Start(ctx, mobium.WithDevice("emulator-5554"))
if err != nil {
return err
}
defer dev.Quit(ctx)
WithDriver
func WithDriver(name string) Option
WithDriver chooses the driver: "uiautomator2" (default on Android), "uiautomator" (installs nothing, slower, cannot type) or "wda" (iOS simulators and iPhones).
Use the zero-install uiautomator driver
dev, err := mobium.Connect(mobium.WithDriver("uiautomator"))
if err != nil {
return err
}
defer dev.Close()
WithPlatform
func WithPlatform(name string) Option
WithPlatform names the platform for Start: "android" or "ios". "ios" picks wda, so the driver need not be named.
Start on iOS, which picks the wda driver
dev, err := mobium.Start(ctx, mobium.WithPlatform("ios"))
if err != nil {
return err
}
defer dev.Quit(ctx)
WithSession
func WithSession(name string) Option
WithSession gives this connection a daemon of its own, named name, as MOBIUM_SESSION does. One daemon serves one call at a time across every device, so two test runs driving two devices at once should each have one: sharing, an emulator's 15 maps took 22.3s behind a simulator's, against 0.3s on a daemon of its own. The name is part of a socket path, so keep it short.
Give each of two parallel runs its own daemon
a, err := mobium.Start(ctx, mobium.WithDevice("emulator-5554"), mobium.WithSession("a"))
if err != nil {
return err
}
defer a.Quit(ctx)
b, err := mobium.Start(ctx, mobium.WithPlatform("ios"), mobium.WithSession("b"))
if err != nil {
return err
}
defer b.Quit(ctx)
WithApp
func WithApp(id string) Option
WithApp is an app for Start to launch once the session is up, by package name (Android) or bundle id (iOS).
Launch an app fresh when the session starts
dev, err := mobium.Start(ctx, mobium.WithApp("com.android.settings"))
if err != nil {
return err
}
defer dev.Quit(ctx)
FindBinary
func FindBinary(explicit string) (string, error)
FindBinary locates the mobium executable: the explicit path, then MOBIUM_BIN_PATH, then PATH — and nothing else.
MOBIUM_BIN_PATH wins, so a test run can pin a specific build — the same escape hatch the other clients have. The current directory is never searched, not even through a relative PATH entry: a library that runs whatever ./bin/mobium happens to sit where a test was started runs a binary anyone could have planted there. exec.LookPath already refuses a result from a relative PATH entry (exec.ErrDot).
Find the mobium binary the client would run
path, err := mobium.FindBinary("") // MOBIUM_BIN_PATH, then PATH
if err != nil {
return err
}
fmt.Println(path)
Call
func (d *Device) Call(ctx context.Context, tool string, args map[string]any, out any) error
Call runs any tool by name, for anything this package does not wrap yet. The tool's structured answer is decoded into out, which may be nil.
Call a tool by name, decoding its answer
var out struct {
Rules []mobium.DialogRule `json:"rules"`
}
if err := dev.Call(ctx, "app_dialogs", map[string]any{}, &out); err != nil {
return err
}
fmt.Println(len(out.Rules), "rules")
Call a tool for its effect alone
args := map[string]any{"action": "accept", "text": "Allow"}
if err := dev.Call(ctx, "app_alert", args, nil); err != nil {
return err
}
Is
func (e *Error) Is(target error) bool
Is makes the sentinels above match by code.
Decide by the error's code, never its message
err := dev.Tap(ctx, "text=Sign in")
if errors.Is(err, mobium.ErrNoSuchElement) {
fmt.Println("not on this screen")
} else if err != nil {
return err
}
Tell a refusal from a failure
err := dev.PressTap(ctx, "@e3", "@e7")
if errors.Is(err, mobium.ErrUnsupported) {
fmt.Println("skipped: this platform cannot land a second finger mid-gesture")
} else if err != nil {
return err
}
Error
type Error struct {
Tool string
Reason string
Code Code
Remedy string
Retryable bool
Details map[string]any
}
func (e *Error) Error() string
Error is a tool reporting that it could not do what was asked.
A failing tool answers with isError and its reason in the content rather than with a JSON-RPC error, so the reason has to be lifted out deliberately; this is what that becomes. Code says what kind of failure it was — compare with errors.Is and the Err sentinels in errors.go — Remedy what to do about it, and Retryable whether the same call can succeed if made again.
Read the code, remedy and details of a failure
var merr *mobium.Error
if err := dev.Tap(ctx, "@e9"); errors.As(err, &merr) {
fmt.Println(merr.Code, merr.Remedy, merr.Retryable)
return fmt.Errorf("tap failed: %w", err)
}
Center
func (b Bounds) Center() (int, int)
Center is the point a tap targets.
Find the middle of an element
elements, err := dev.Map(ctx)
if err != nil || len(elements) == 0 {
return err
}
x, y := elements[0].Bounds.Center()
if err := dev.TapPoint(ctx, x, y); err != nil {
return err
}
Width
func (b Bounds) Width() int
Width and Height are the rectangle's size in device pixels.
Measure an element in device pixels
el, err := dev.WaitFor(ctx, "testid=search", nil)
if err != nil {
return err
}
fmt.Println(el.Bounds.Width(), "x", el.Bounds.Height())
Height
func (b Bounds) Height() int
Flag a touch target shorter than 48 pixels
buttons, err := dev.Find(ctx, "role=button")
if err != nil {
return err
}
for _, b := range buttons {
if b.Bounds.Height() < 48 {
fmt.Println("short:", b.Label)
}
}
String
func (l Locator) String() string
String renders the locator the way the tools accept it.
Keep a ref's locator for a later screen
el, err := dev.WaitFor(ctx, "text=Sign in", nil)
if err != nil || el.Locator == nil {
return err
}
locator := el.Locator.String() // e.g. "text=Sign in"
if err := dev.Tap(ctx, locator); err != nil {
return err
}
Types
| Type | What it is |
|---|---|
Code | Code classifies a failure. |
Bounds | Bounds is an on-screen rectangle in device pixels, on both platforms. |
Locator | Locator is how a ref resolves on a later screen. |
Element | Element is one actionable thing on screen. |
DeviceInfo | DeviceInfo is one attached device or simulator. |
Device | Device is a connection to mobium. |
Option | Option configures Connect and Start. |
Session | Session is what Start found: the device and how it is driven. |
Booted | Booted is a virtual device Boot started, or found already running. |
MapDiff | MapDiff is what changed on the screen since the last map of this device: what appeared, what went away, and what changed its label, its checked state or its place. |
MapChange | MapChange is one element in both maps that differs between them. |
WaitOptions | WaitOptions tunes WaitFor. |
App | App is one installed app. |
PageSource | PageSource is the raw hierarchy, as the device-side server sent it. |
DialogRule | DialogRule is a declared answer to a dialog: when one whose text contains When is in an action's way, press the button captioned Press. |
ClearedData | ClearedData is what ClearData did: the stores read back empty, what was kept, on Android the runtime permissions still granted afterwards, and on a real iPhone what the reset changed that nothing can read back. |
Transfer | Transfer is one file moved between this machine and the device, as read back at both ends. |
DeviceFile | DeviceFile is one file in the folder the device keeps downloads in. |
TransferOptions | TransferOptions tunes Upload. |
Step | Step is one call in a Batch: a tool and the arguments it takes on its own. |
StepResult | StepResult is one step's answer in a Batch: its text, and its data as the tool's own result, to decode with json.Unmarshal into what that tool returns. |
BatteryStatus | BatteryStatus is the battery, from Battery. |
DeviceClock | DeviceClock is the device's clock, from DeviceTime. |
NetworkStatus | NetworkStatus is the device's network, read back by Network and each call that changes it. |
BiometricStatus | BiometricStatus is what Biometric found or did. |
HitTestResult | HitTestResult is HitTest's answer when the touch reaches its target. |
AuditFinding | AuditFinding is one thing the platform's accessibility audit found. |
AuditResult | AuditResult is Audit's answer. |
AppStatus | AppStatus is one app's state, from AppState. |
Screen | Screen is one device's screen, and what is wrong with the layout on it. |
Finding | Finding is one thing wrong with a layout at one screen size. |
Location | Location is where the device believes it is. |
BackResult | BackResult is what a back did. |
ConsoleEntry | ConsoleEntry is one line a page logged. |
DeviceLogEntry | DeviceLogEntry is one line of the device's own log. |
Recording | Recording is what app_record reports: whether one is running, and on stop the saved file's frames and duration, read from its own header. |
Trace | Trace is what app_trace reports: whether a trace is running on the device, how many calls it holds and for how long, and on stop where the zip went and its size. |
TraceOptions | TraceOptions tunes TraceStart. |
KeyboardField | KeyboardField is the field with keyboard focus. |
KeyboardState | KeyboardState is the soft keyboard after an app_keyboard call. |
KeyboardOptions | KeyboardOptions says what to do with the keyboard. |
CrashReport | CrashReport is one crash the device recorded. |
Cookie | Cookie is one of a page's cookies. |
StorageItem | StorageItem is one key of localStorage or sessionStorage. |
OriginStorage | OriginStorage is one origin's web storage. |
StorageState | StorageState is a page's cookies and web storage, in the shape Vibium saves. |
Notification | Notification is one entry in the shade. |
Error | Error is a tool reporting that it could not do what was asked. |