blob: b3408c249455fa66f2d2281b67b3aa1ad5985150 [file] [log] [blame]
// Copyright 2018 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package ui
import (
"context"
"time"
"chromiumos/tast/errors"
"chromiumos/tast/local/coords"
)
// Available RPC methods are listed at:
// https://github.com/xiaocong/android-uiautomator-server/blob/master/app/src/androidTest/java/com/github/uiautomator/stub/AutomatorService.java
// ErrorTimeout defines an error for the ui timeout.
type ErrorTimeout struct {
*errors.E
}
// IsTimeout returns true if the given error is of type ErrorTimeout.
func IsTimeout(err error) bool {
_, ok := err.(*ErrorTimeout)
return ok
}
var errTimeout = &ErrorTimeout{E: errors.New("timeout")}
// Object is a representation of an Android view.
//
// An instantiated Object does NOT uniquely identify an Android view. Instead,
// it holds a selector to locate a matching view when its methods are called.
// Once you create an instance of Object, it can be reused for different views
// matching the selector.
//
// This object corresponds to UiObject in UI Automator API:
// https://developer.android.com/reference/android/support/test/uiautomator/UiObject
type Object struct {
// id is a string ID representing the Android UI Object.
// It can be assigned by GetObject() or GetChild() method, and will be used by methods
// which require the object ID as one of the parameters.
id string
d *Device
s *selector
}
// rect represents a rectangle, as defined here:
// https://developer.android.com/reference/android/graphics/Rect
// It differs from coords.Rect in that it uses bottom & right instead of width & height.
type rect struct {
Top int `json:"top"`
Left int `json:"left"`
Bottom int `json:"bottom"`
Right int `json:"right"`
}
// objectInfo represents a collection of attributes that belongs to the object as defined
// by UI Automator server. See:
// https://github.com/lnishan/android-uiautomator-server/blob/master/app/src/androidTest/java/com/github/uiautomator/stub/ObjInfo.java
type objectInfo struct {
Text string `json:"text"`
ContentDescription string `json:"contentDescription"`
PackageName string `json:"packageName"`
ClassName string `json:"className"`
Bounds rect `json:"bounds"`
Checkable bool `json:"checkable"`
Checked bool `json:"checked"`
Clickable bool `json:"clickable"`
Enabled bool `json:"enabled"`
Focusable bool `json:"focusable"`
Focused bool `json:"focused"`
LongClickable bool `json:"longClickable"`
Scrollable bool `json:"scrollable"`
Selected bool `json:"selected"`
ChildCount int `json:"childCount"`
}
// Object creates an Object from given selectors.
//
// Example:
// btn := d.Object(ui.ID("foo_button"), ui.Text("bar"))
func (d *Device) Object(opts ...SelectorOption) *Object {
return &Object{d: d, s: newSelector(opts)}
}
// GetObject assigns the UI Object ID to the object so it can be used in subsequent calls
// to the same object.
//
// It is the interface for getUiObject() method of UI Automator server.
func (o *Object) GetObject(ctx context.Context) error {
// Always call JSONRPC to get new id.
method := "getUiObject"
if err := o.d.call(ctx, method, &o.id, o.s); err != nil {
return wrapMethodError(method, o.s, err)
}
return nil
}
// WaitForExists waits for a view matching the selector to appear.
//
// This method corresponds to UiObject.waitForExists().
// https://developer.android.com/reference/android/support/test/uiautomator/UiObject.html#waitForExists(long)
func (o *Object) WaitForExists(ctx context.Context, timeout time.Duration) error {
return o.callSimple(ctx, "waitForExists", o.s, timeout/time.Millisecond)
}
// SwipeUp performs the swipe up action on the UiObject
// steps indicates the number of injected move steps into the system. Steps are injected about 5ms apart. So a 100 steps may take about 1/2 second to complete.
//
// This method corresponds to UiObject.SwipeUp().
// https://github.com/lnishan/android-uiautomator-server/blob/master/app/src/androidTest/java/com/github/uiautomator/stub/AutomatorServiceImpl.java
func (o *Object) SwipeUp(ctx context.Context, steps int) error {
return o.callSimple(ctx, "swipe", o.s, "up", steps)
}
// SwipeDown performs the swipe down action on the UiObject
// steps indicates the number of injected move steps into the system. Steps are injected about 5ms apart. So a 100 steps may take about 1/2 second to complete.
//
// This method corresponds to UiObject.SwipeDown().
// https://github.com/lnishan/android-uiautomator-server/blob/master/app/src/androidTest/java/com/github/uiautomator/stub/AutomatorServiceImpl.java
func (o *Object) SwipeDown(ctx context.Context, steps int) error {
return o.callSimple(ctx, "swipe", o.s, "down", steps)
}
// SwipeRight performs the swipe right action on the UiObject
// steps indicates the number of injected move steps into the system. Steps are injected about 5ms apart. So a 100 steps may take about 1/2 second to complete.
//
// This method corresponds to UiObject.SwipeRight().
// https://github.com/lnishan/android-uiautomator-server/blob/master/app/src/androidTest/java/com/github/uiautomator/stub/AutomatorServiceImpl.java
func (o *Object) SwipeRight(ctx context.Context, steps int) error {
return o.callSimple(ctx, "swipe", o.s, "right", steps)
}
// SwipeLeft performs the swipe left action on the UiObject
// steps indicates the number of injected move steps into the system. Steps are injected about 5ms apart. So a 100 steps may take about 1/2 second to complete.
//
// This method corresponds to UiObject.SwipeLeft().
// https://github.com/lnishan/android-uiautomator-server/blob/master/app/src/androidTest/java/com/github/uiautomator/stub/AutomatorServiceImpl.java
func (o *Object) SwipeLeft(ctx context.Context, steps int) error {
return o.callSimple(ctx, "swipe", o.s, "left", steps)
}
// WaitForText waits for a text view matching the selector to have the expected text.
func (o *Object) WaitForText(ctx context.Context, expected string, timeout time.Duration) error {
s := *o.s
s.Text = expected
s.Mask |= maskText
terr := o.callSimple(ctx, "waitForExists", &s, timeout/time.Millisecond)
if terr != nil {
if actual, err := o.GetText(ctx); err == nil {
return errors.Errorf("text does not match: got %q; want %q", actual, expected)
}
return terr
}
return nil
}
// WaitUntilGone waits for a view matching the selector to disappear.
//
// This method corresponds to UiObject.waitUntilGone().
// https://developer.android.com/reference/android/support/test/uiautomator/UiObject.html#waitUntilGone(long)
func (o *Object) WaitUntilGone(ctx context.Context, timeout time.Duration) error {
return o.callSimple(ctx, "waitUntilGone", o.s, timeout/time.Millisecond)
}
// Click clicks a view matching the selector.
//
// This method corresponds to UiObject.click().
// https://developer.android.com/reference/android/support/test/uiautomator/UiObject.html#click
func (o *Object) Click(ctx context.Context) error {
return o.callSimple(ctx, "click", o.s)
}
// ScrollBackward returns true if scrolled, false if can't scroll anymore.
//
// ScrollBackward performs a backward scroll action with n number of scroll steps in vertical direction from top to bottom.
// This method corresponds to UiScrollable.scrollBackward().
// https://developer.android.com/reference/androidx/test/uiautomator/UiScrollable#scrollbackward
func (o *Object) ScrollBackward(ctx context.Context, n int) (bool, error) {
return o.callSimpleBool(ctx, "scrollBackward", o.s, true, n)
}
// ScrollForward returns true if scrolled, false if can't scroll anymore.
//
// ScrollForward performs a forward scroll action with n number of scroll steps in vertical direction from bottom to top.
// This method corresponds to UiScrollable.scrollForward().
// https://developer.android.com/reference/androidx/test/uiautomator/UiScrollable#scrollforward
func (o *Object) ScrollForward(ctx context.Context, n int) (bool, error) {
return o.callSimpleBool(ctx, "scrollForward", o.s, true, n)
}
// callSimpleBool is a method to call a RPC method that returns boolean as an output.
func (o *Object) callSimpleBool(ctx context.Context, method string, params ...interface{}) (bool, error) {
var output bool
if err := o.d.call(ctx, method, &output, params...); err != nil {
return output, wrapMethodError(method, o.s, err)
}
return output, nil
}
// ScrollTo performs a forward scroll action to move through the scrollable layout element until a view matching the target selector is found.
//
// This method corresponds to UiScrollable.scrollintoview().
// https://developer.android.com/reference/androidx/test/uiautomator/UiScrollable.html#scrollintoview
func (o *Object) ScrollTo(ctx context.Context, target *Object) error {
return o.callSimple(ctx, "scrollTo", o.s, target.s, true)
}
// GetChild Creates a new UiObject for a child view that is under the present UiObject
//
// This method corresponds to UiObject.getChild().
// https://developer.android.com/reference/androidx/test/uiautomator/UiObject#getchild
func (o *Object) GetChild(ctx context.Context, target *Object) error {
if o.id == "" {
if err := o.GetObject(ctx); err != nil {
return errors.Wrap(err, "failed to get parent object")
}
}
method := "getChild"
if err := o.d.call(ctx, method, &target.id, o.id, target.s); err != nil {
return wrapMethodError(method, o.s, err)
}
return nil
}
// Exists returns if an object exists.
//
// This method corresponds to UiObject.exists().
// https://developer.android.com/reference/android/support/test/uiautomator/UiObject.html#exists
func (o *Object) Exists(ctx context.Context) error {
return o.callSimple(ctx, "exist", o.s)
}
// SetText sets the text property of a view.
//
// This method corresponds to UiObject.setText().
// https://developer.android.com/reference/android/support/test/uiautomator/UiObject.html#settext
func (o *Object) SetText(ctx context.Context, s string) error {
return o.callSimple(ctx, "setText", o.s, s)
}
// GetText returns the text property of a view.
//
// This method corresponds to UiObject.getText().
// https://developer.android.com/reference/android/support/test/uiautomator/UiObject.html#gettext
func (o *Object) GetText(ctx context.Context) (string, error) {
info, err := o.info(ctx)
if err != nil {
return "", errors.Wrap(err, "GetText failed")
}
return info.Text, nil
}
// GetContentDescription returns the content description property of a view.
//
// This method corresponds to UiObject.getContentDescription().
// https://developer.android.com/reference/android/support/test/uiautomator/UiObject.html#getcontentdescription
func (o *Object) GetContentDescription(ctx context.Context) (string, error) {
info, err := o.info(ctx)
if err != nil {
return "", errors.Wrap(err, "GetContentDescription failed")
}
return info.ContentDescription, nil
}
// GetPackageName returns the package name of a view.
//
// This method corresponds to UiObject.getPackageName().
// https://developer.android.com/reference/android/support/test/uiautomator/UiObject.html#getpackagename
func (o *Object) GetPackageName(ctx context.Context) (string, error) {
info, err := o.info(ctx)
if err != nil {
return "", errors.Wrap(err, "GetPackageName failed")
}
return info.PackageName, nil
}
// GetClassName returns the class name of a view.
//
// This method corresponds to UiObject.getClassName().
// https://developer.android.com/reference/android/support/test/uiautomator/UiObject.html#getclassname
func (o *Object) GetClassName(ctx context.Context) (string, error) {
info, err := o.info(ctx)
if err != nil {
return "", errors.Wrap(err, "GetClassName failed")
}
return info.ClassName, nil
}
// GetChildCount returns the count of child views immediately under the present UiObject.
//
// This method corresponds to UiObject.getChildCount().
// https://developer.android.com/reference/androidx/test/uiautomator/UiObject#getchildcount
func (o *Object) GetChildCount(ctx context.Context) (int, error) {
info, err := o.info(ctx)
if err != nil {
return 0, errors.Wrap(err, "GetChildCount failed")
}
return info.ChildCount, nil
}
// GetBounds returns the bounds of a view.
//
// This method corresponds to UiObject.getBounds().
// https://developer.android.com/reference/android/support/test/uiautomator/UiObject.html#getbounds
func (o *Object) GetBounds(ctx context.Context) (coords.Rect, error) {
info, err := o.info(ctx)
if err != nil {
return coords.Rect{}, errors.Wrap(err, "GetBounds failed")
}
// Bounds contains a "graphics rect". Needs to be converted to an coords.Rect.
gr := info.Bounds
r := coords.NewRectLTRB(gr.Left, gr.Top, gr.Right, gr.Bottom)
return r, nil
}
// IsCheckable returns if a view is checkable.
//
// This method corresponds to UiObject.isCheckable().
// https://developer.android.com/reference/android/support/test/uiautomator/UiObject.html#ischeckable
func (o *Object) IsCheckable(ctx context.Context) (bool, error) {
info, err := o.info(ctx)
if err != nil {
return false, errors.Wrap(err, "IsCheckable failed")
}
return info.Checkable, nil
}
// IsChecked returns if a view is checked.
//
// This method corresponds to UiObject.isChecked().
// https://developer.android.com/reference/android/support/test/uiautomator/UiObject.html#ischecked
func (o *Object) IsChecked(ctx context.Context) (bool, error) {
info, err := o.info(ctx)
if err != nil {
return false, errors.Wrap(err, "IsChecked failed")
}
return info.Checked, nil
}
// IsClickable returns if a view is clickable.
//
// This method corresponds to UiObject.isClickable().
// https://developer.android.com/reference/android/support/test/uiautomator/UiObject.html#isclickable
func (o *Object) IsClickable(ctx context.Context) (bool, error) {
info, err := o.info(ctx)
if err != nil {
return false, errors.Wrap(err, "IsClickable failed")
}
return info.Clickable, nil
}
// IsEnabled returns if a view is enabled.
//
// This method corresponds to UiObject.isEnabled().
// https://developer.android.com/reference/android/support/test/uiautomator/UiObject.html#isenabled
func (o *Object) IsEnabled(ctx context.Context) (bool, error) {
info, err := o.info(ctx)
if err != nil {
return false, errors.Wrap(err, "IsEnabled failed")
}
return info.Enabled, nil
}
// IsFocusable returns if a view is focusable.
//
// This method corresponds to UiObject.isFocusable().
// https://developer.android.com/reference/android/support/test/uiautomator/UiObject.html#isfocusable
func (o *Object) IsFocusable(ctx context.Context) (bool, error) {
info, err := o.info(ctx)
if err != nil {
return false, errors.Wrap(err, "IsFocusable failed")
}
return info.Focusable, nil
}
// IsFocused returns if a view is focused.
//
// This method corresponds to UiObject.isFocused().
// https://developer.android.com/reference/android/support/test/uiautomator/UiObject.html#isfocused
func (o *Object) IsFocused(ctx context.Context) (bool, error) {
info, err := o.info(ctx)
if err != nil {
return false, errors.Wrap(err, "IsFocused failed")
}
return info.Focused, nil
}
// IsLongClickable returns if a view is longClickable.
//
// This method corresponds to UiObject.isLongClickable().
// https://developer.android.com/reference/android/support/test/uiautomator/UiObject.html#islongclickable
func (o *Object) IsLongClickable(ctx context.Context) (bool, error) {
info, err := o.info(ctx)
if err != nil {
return false, errors.Wrap(err, "IsLongClickable failed")
}
return info.LongClickable, nil
}
// IsScrollable returns if a view is scrollable.
//
// This method corresponds to UiObject.isScrollable().
// https://developer.android.com/reference/android/support/test/uiautomator/UiObject.html#isscrollable
func (o *Object) IsScrollable(ctx context.Context) (bool, error) {
info, err := o.info(ctx)
if err != nil {
return false, errors.Wrap(err, "IsScrollable failed")
}
return info.Scrollable, nil
}
// IsSelected returns if a view is selected.
//
// This method corresponds to UiObject.isSelected().
// https://developer.android.com/reference/android/support/test/uiautomator/UiObject.html#isselected
func (o *Object) IsSelected(ctx context.Context) (bool, error) {
info, err := o.info(ctx)
if err != nil {
return false, errors.Wrap(err, "IsSelected failed")
}
return info.Selected, nil
}
// callSimple is a common method to call a RPC method that returns a boolean indicating success.
func (o *Object) callSimple(ctx context.Context, method string, params ...interface{}) error {
var success bool
if err := o.d.call(ctx, method, &success, params...); err != nil {
return wrapMethodError(method, o.s, err)
}
if !success {
return wrapMethodError(method, o.s, errTimeout)
}
return nil
}
// info returns an objectInfo of a view matched by the selector.
func (o *Object) info(ctx context.Context) (*objectInfo, error) {
const method = "objInfo"
var info objectInfo
if o.id != "" {
// Use object ID to get info.
if err := o.d.call(ctx, method, &info, o.id); err != nil {
return nil, wrapMethodError(method, o.s, err)
}
return &info, nil
}
// Use select to get info.
if err := o.d.call(ctx, method, &info, o.s); err != nil {
return nil, wrapMethodError(method, o.s, err)
}
return &info, nil
}
// wrapMethodError wraps an error returned from an RPC method.
func wrapMethodError(method string, s *selector, err error) error {
newErr := errors.Wrapf(err, "%s (selector=%v) failed", method, s)
if IsTimeout(err) {
// Still return an ErrorTimeout instance so the caller can check the error type.
return &ErrorTimeout{E: newErr}
}
return newErr
}