Skip to main content

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​

MethodTool
Accessibility, AccessibilitySetting, SetAccessibilityapp_accessibility
Alert, AnswerAlert, AnswerPromptapp_alert
Appearanceapp_appearance
Auditapp_audit
Backgroundapp_background
Batchapp_batch
Batteryapp_battery
Biometricapp_biometric
Bootapp_boot
IncomingCallapp_call
Checkapp_check
ClearData, ResetFromBundleapp_clear_data
Clipboard, SetClipboardapp_clipboard
Contextapp_context
Contextsapp_contexts
ClearCookies, Cookies, SetCookiesapp_cookies
Crash, Crashesapp_crashes
Currentapp_current
Devicesapp_devices
AddDialogRule, ClearDialogRules, DialogRulesapp_dialogs
Doctorapp_doctor
Download, DownloadBytes, Downloads, PullPathapp_download
Drag, DragForapp_drag
Evalapp_eval
Fillapp_fill
Findapp_find
Grantapp_grant
HitTestapp_hit_test
Hookapp_hook
Installapp_install
Keyboardapp_keyboard
Launch, LaunchWithGrayBox, LaunchWithHitTestapp_launch
Appsapp_list_apps
AppLocale, SetAppLocaleapp_locale
ClearLocation, FollowGPX, FollowRoute, Location, SetLocationapp_location
ScreenLocked, SetScreenLockedapp_lock
DeviceLogs, Logsapp_logs
LongPressapp_long_press
Map, MapDiffapp_map
Network, ResetNetwork, SetOffline, ShapeNetworkapp_network
Notifications, PostNotification, SetShadeapp_notifications
OpenURLapp_open_url
Orientation, SetOrientationapp_orientation
Back, Pressapp_press
PressDragapp_press_drag
PressTapapp_press_tap
Recordapp_record
ResetPermissionsapp_reset_permissions
Revokeapp_revoke
Rotateapp_rotate
Screenapp_screen
Screenshotapp_screenshot
ScrollToapp_scroll_to
Quit, Sessions, Startapp_session
Shakeapp_shake
Shutdownapp_shutdown
SendSMSapp_sms
Sourceapp_source
AppStateapp_state
ClearStorage, SetStorage, Storageapp_storage
Swipe, SwipeOn, SwipePointsapp_swipe
DoubleTap, DoubleTapPoint, Tap, TapFingers, TapPointapp_tap
Terminateapp_terminate
Textapp_text
DeviceTimeapp_time
SetTimezone, Timezoneapp_timezone
TraceStart, TraceStatus, TraceStopapp_trace
Typeapp_type
Uninstallapp_uninstall
PushPath, Uploadapp_upload
WaitForapp_wait_for
Zoomapp_zoom

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​

TypeWhat it is
CodeCode classifies a failure.
BoundsBounds is an on-screen rectangle in device pixels, on both platforms.
LocatorLocator is how a ref resolves on a later screen.
ElementElement is one actionable thing on screen.
DeviceInfoDeviceInfo is one attached device or simulator.
DeviceDevice is a connection to mobium.
OptionOption configures Connect and Start.
SessionSession is what Start found: the device and how it is driven.
BootedBooted is a virtual device Boot started, or found already running.
MapDiffMapDiff 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.
MapChangeMapChange is one element in both maps that differs between them.
WaitOptionsWaitOptions tunes WaitFor.
AppApp is one installed app.
PageSourcePageSource is the raw hierarchy, as the device-side server sent it.
DialogRuleDialogRule is a declared answer to a dialog: when one whose text contains When is in an action's way, press the button captioned Press.
ClearedDataClearedData 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.
TransferTransfer is one file moved between this machine and the device, as read back at both ends.
DeviceFileDeviceFile is one file in the folder the device keeps downloads in.
TransferOptionsTransferOptions tunes Upload.
StepStep is one call in a Batch: a tool and the arguments it takes on its own.
StepResultStepResult 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.
BatteryStatusBatteryStatus is the battery, from Battery.
DeviceClockDeviceClock is the device's clock, from DeviceTime.
NetworkStatusNetworkStatus is the device's network, read back by Network and each call that changes it.
BiometricStatusBiometricStatus is what Biometric found or did.
HitTestResultHitTestResult is HitTest's answer when the touch reaches its target.
AuditFindingAuditFinding is one thing the platform's accessibility audit found.
AuditResultAuditResult is Audit's answer.
AppStatusAppStatus is one app's state, from AppState.
ScreenScreen is one device's screen, and what is wrong with the layout on it.
FindingFinding is one thing wrong with a layout at one screen size.
LocationLocation is where the device believes it is.
BackResultBackResult is what a back did.
ConsoleEntryConsoleEntry is one line a page logged.
DeviceLogEntryDeviceLogEntry is one line of the device's own log.
RecordingRecording is what app_record reports: whether one is running, and on stop the saved file's frames and duration, read from its own header.
TraceTrace 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.
TraceOptionsTraceOptions tunes TraceStart.
KeyboardFieldKeyboardField is the field with keyboard focus.
KeyboardStateKeyboardState is the soft keyboard after an app_keyboard call.
KeyboardOptionsKeyboardOptions says what to do with the keyboard.
CrashReportCrashReport is one crash the device recorded.
CookieCookie is one of a page's cookies.
StorageItemStorageItem is one key of localStorage or sessionStorage.
OriginStorageOriginStorage is one origin's web storage.
StorageStateStorageState is a page's cookies and web storage, in the shape Vibium saves.
NotificationNotification is one entry in the shade.
ErrorError is a tool reporting that it could not do what was asked.