Files
httprunner/hrp/internal/uixt/android_driver.go
2022-09-26 11:00:18 +08:00

1204 lines
35 KiB
Go

package uixt
import (
"bytes"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"net/url"
"os"
"path"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
"github.com/electricbubble/gadb"
)
type uiaDriver struct {
Driver
adbDevice gadb.Device
localPort int
}
func NewUIADriver(capabilities Capabilities, urlPrefix string) (driver *uiaDriver, err error) {
if capabilities == nil {
capabilities = NewCapabilities()
}
driver = new(uiaDriver)
if driver.urlPrefix, err = url.Parse(urlPrefix); err != nil {
return nil, err
}
if driver.sessionId, err = driver.NewSession(capabilities); err != nil {
return nil, err
}
return
}
func (d *uiaDriver) NewSession(capabilities Capabilities) (sessionID string, err error) {
// register(postHandler, new NewSession("/wd/hub/session"))
var rawResp rawResponse
data := map[string]interface{}{"capabilities": capabilities}
if rawResp, err = d.httpPOST(data, "/session"); err != nil {
return "", err
}
reply := new(struct{ Value struct{ SessionId string } })
if err = json.Unmarshal(rawResp, reply); err != nil {
return "", err
}
sessionID = reply.Value.SessionId
// d.sessionIdCache[sessionID] = true
return
}
func (d *uiaDriver) Quit() (err error) {
// register(deleteHandler, new DeleteSession("/wd/hub/session/:sessionId"))
if d.sessionId == "" {
return nil
}
if _, err = d.httpDELETE("/session", d.sessionId); err == nil {
d.sessionId = ""
}
return err
}
func (d *uiaDriver) ActiveSessionID() string {
return d.sessionId
}
func (d *uiaDriver) SessionIDs() (sessionIDs []string, err error) {
// register(getHandler, new GetSessions("/wd/hub/sessions"))
var rawResp rawResponse
if rawResp, err = d.httpGET("/sessions"); err != nil {
return nil, err
}
reply := new(struct{ Value []struct{ SessionId string } })
if err = json.Unmarshal(rawResp, reply); err != nil {
return nil, err
}
sessionIDs = make([]string, len(reply.Value))
for i := range reply.Value {
sessionIDs[i] = reply.Value[i].SessionId
}
return
}
func (d *uiaDriver) SessionDetails() (scrollData map[string]interface{}, err error) {
// register(getHandler, new GetSessionDetails("/wd/hub/session/:sessionId"))
var rawResp rawResponse
if rawResp, err = d.httpGET("/session", d.sessionId); err != nil {
return nil, err
}
reply := new(struct{ Value map[string]interface{} })
if err = json.Unmarshal(rawResp, reply); err != nil {
return nil, err
}
scrollData = reply.Value
return
}
func (d *uiaDriver) Status() (ready bool, err error) {
// register(getHandler, new Status("/wd/hub/status"))
var rawResp rawResponse
if rawResp, err = d.httpGET("/status"); err != nil {
return false, err
}
reply := new(struct {
Value struct {
// Message string
Ready bool
}
})
if err = json.Unmarshal(rawResp, reply); err != nil {
return false, err
}
ready = reply.Value.Ready
return
}
// Screenshot grab device screenshot
func (d *uiaDriver) Screenshot() (raw *bytes.Buffer, err error) {
// register(getHandler, new CaptureScreenshot("/wd/hub/session/:sessionId/screenshot"))
var rawResp rawResponse
if rawResp, err = d.httpGET("/session", d.sessionId, "screenshot"); err != nil {
return nil, err
}
reply := new(struct{ Value string })
if err = json.Unmarshal(rawResp, reply); err != nil {
return nil, err
}
var decodeStr []byte
if decodeStr, err = base64.StdEncoding.DecodeString(reply.Value); err != nil {
return nil, err
}
raw = bytes.NewBuffer(decodeStr)
return
}
func (d *uiaDriver) Orientation() (orientation Orientation, err error) {
// register(getHandler, new GetOrientation("/wd/hub/session/:sessionId/orientation"))
var rawResp rawResponse
if rawResp, err = d.httpGET("/session", d.sessionId, "orientation"); err != nil {
return "", err
}
reply := new(struct{ Value Orientation })
if err = json.Unmarshal(rawResp, reply); err != nil {
return "", err
}
orientation = reply.Value
return
}
func (d *uiaDriver) Rotation() (rotation Rotation, err error) {
// register(getHandler, new GetRotation("/wd/hub/session/:sessionId/rotation"))
var rawResp rawResponse
if rawResp, err = d.httpGET("/session", d.sessionId, "rotation"); err != nil {
return Rotation{}, err
}
reply := new(struct{ Value Rotation })
if err = json.Unmarshal(rawResp, reply); err != nil {
return Rotation{}, err
}
rotation = reply.Value
return
}
// DeviceSize get window size of the device
func (d *uiaDriver) DeviceSize() (deviceSize Size, err error) {
// register(getHandler, new GetDeviceSize("/wd/hub/session/:sessionId/window/:windowHandle/size"))
var rawResp rawResponse
if rawResp, err = d.httpGET("/session", d.sessionId, "window/:windowHandle/size"); err != nil {
return Size{}, err
}
reply := new(struct{ Value Size })
if err = json.Unmarshal(rawResp, reply); err != nil {
return Size{}, err
}
deviceSize = reply.Value
return
}
// Source get page source
func (d *uiaDriver) Source() (sXML string, err error) {
// register(getHandler, new Source("/wd/hub/session/:sessionId/source"))
var rawResp rawResponse
if rawResp, err = d.httpGET("/session", d.sessionId, "source"); err != nil {
return "", err
}
reply := new(struct{ Value string })
if err = json.Unmarshal(rawResp, reply); err != nil {
return "", err
}
sXML = reply.Value
return
}
// StatusBarHeight get status bar height of the device
func (d *uiaDriver) StatusBarHeight() (height int, err error) {
// register(getHandler, new GetSystemBars("/wd/hub/session/:sessionId/appium/device/system_bars"))
var rawResp rawResponse
if rawResp, err = d.httpGET("/session", d.sessionId, "appium/device/system_bars"); err != nil {
return 0, err
}
reply := new(struct{ Value struct{ StatusBar int } })
if err = json.Unmarshal(rawResp, reply); err != nil {
return 0, err
}
height = reply.Value.StatusBar
return
}
func (d *uiaDriver) check() error {
if d.adbDevice.Serial() == "" {
return errors.New("adb daemon: the device is not ready")
}
return nil
}
// Dispose corresponds to the command:
// adb -s $serial forward --remove $localPort
func (d *uiaDriver) Dispose() (err error) {
if err = d.check(); err != nil {
return err
}
if d.localPort == 0 {
return nil
}
return d.adbDevice.ForwardKill(d.localPort)
}
func (d *uiaDriver) ActiveAppActivity() (appActivity string, err error) {
if err = d.check(); err != nil {
return "", err
}
var sOutput string
if sOutput, err = d.adbDevice.RunShellCommand("dumpsys activity activities | grep mResumedActivity"); err != nil {
return "", err
}
re := regexp.MustCompile(`\{(.+?)\}`)
if !re.MatchString(sOutput) {
return "", fmt.Errorf("active app activity: %s", strings.TrimSpace(sOutput))
}
fields := strings.Fields(re.FindStringSubmatch(sOutput)[1])
appActivity = fields[2]
return
}
func (d *uiaDriver) ActiveAppPackageName() (appPackageName string, err error) {
var activity string
if activity, err = d.ActiveAppActivity(); err != nil {
return "", err
}
appPackageName = strings.Split(activity, "/")[0]
return
}
func (d *uiaDriver) AppLaunch(appPackageName string, waitForComplete ...AndroidBySelector) (err error) {
if err = d.check(); err != nil {
return err
}
var sOutput string
if sOutput, err = d.adbDevice.RunShellCommand("monkey -p", appPackageName, "-c android.intent.category.LAUNCHER 1"); err != nil {
return err
}
if strings.Contains(sOutput, "monkey aborted") {
return fmt.Errorf("app launch: %s", strings.TrimSpace(sOutput))
}
if len(waitForComplete) != 0 {
var ce error
exists := func(d *uiaDriver) (bool, error) {
for i := range waitForComplete {
_, ce = d.FindElement(waitForComplete[i])
if ce == nil {
return true, nil
}
}
return false, nil
}
if err = d.WaitWithTimeoutAndInterval(exists, 45, 1.5); err != nil {
return fmt.Errorf("app launch (waitForComplete): %s: %w", err.Error(), ce)
}
}
return
}
func (d *uiaDriver) AppTerminate(appPackageName string) (err error) {
if err = d.check(); err != nil {
return err
}
_, err = d.adbDevice.RunShellCommand("am force-stop", appPackageName)
return
}
func (d *uiaDriver) AppInstall(apkPath string, reinstall ...bool) (err error) {
if err = d.check(); err != nil {
return err
}
apkName := filepath.Base(apkPath)
if !strings.HasSuffix(strings.ToLower(apkName), ".apk") {
return fmt.Errorf("apk file must have an extension of '.apk': %s", apkPath)
}
var apkFile *os.File
if apkFile, err = os.Open(apkPath); err != nil {
return fmt.Errorf("apk file: %w", err)
}
remotePath := path.Join(DeviceTempPath, apkName)
if err = d.adbDevice.PushFile(apkFile, remotePath); err != nil {
return fmt.Errorf("apk push: %w", err)
}
var shellOutput string
if len(reinstall) != 0 && reinstall[0] {
shellOutput, err = d.adbDevice.RunShellCommand("pm install", "-r", remotePath)
} else {
shellOutput, err = d.adbDevice.RunShellCommand("pm install", remotePath)
}
if err != nil {
return fmt.Errorf("apk install: %w", err)
}
if !strings.Contains(shellOutput, "Success") {
return fmt.Errorf("apk installed: %s", shellOutput)
}
return
}
func (d *uiaDriver) AppUninstall(appPackageName string, keepDataAndCache ...bool) (err error) {
if err = d.check(); err != nil {
return err
}
var shellOutput string
if len(keepDataAndCache) != 0 && keepDataAndCache[0] {
shellOutput, err = d.adbDevice.RunShellCommand("pm uninstall", "-k", appPackageName)
} else {
shellOutput, err = d.adbDevice.RunShellCommand("pm uninstall", appPackageName)
}
if err != nil {
return fmt.Errorf("apk uninstall: %w", err)
}
if !strings.Contains(shellOutput, "Success") {
return fmt.Errorf("apk uninstalled: %s", shellOutput)
}
return
}
type BatteryStatus int
const (
_ = iota
BatteryStatusUnknown BatteryStatus = iota
BatteryStatusCharging
BatteryStatusDischarging
BatteryStatusNotCharging
BatteryStatusFull
)
func (bs BatteryStatus) String() string {
switch bs {
case BatteryStatusUnknown:
return "unknown"
case BatteryStatusCharging:
return "charging"
case BatteryStatusDischarging:
return "discharging"
case BatteryStatusNotCharging:
return "not charging"
case BatteryStatusFull:
return "full"
default:
return fmt.Sprintf("unknown status code (%d)", bs)
}
}
func (d *uiaDriver) BatteryInfo() (info BatteryInfo, err error) {
// register(getHandler, new GetBatteryInfo("/wd/hub/session/:sessionId/appium/device/battery_info"))
var rawResp rawResponse
if rawResp, err = d.httpGET("/session", d.sessionId, "appium/device/battery_info"); err != nil {
return BatteryInfo{}, err
}
reply := new(struct{ Value BatteryInfo })
if err = json.Unmarshal(rawResp, reply); err != nil {
return BatteryInfo{}, err
}
info = reply.Value
if info.Level == -1 || info.Status == -1 {
return info, errors.New("cannot be retrieved from the system")
}
return
}
func (d *uiaDriver) GetAppiumSettings() (settings map[string]interface{}, err error) {
// register(getHandler, new GetSettings("/wd/hub/session/:sessionId/appium/settings"))
var rawResp rawResponse
if rawResp, err = d.httpGET("/session", d.sessionId, "appium/settings"); err != nil {
return nil, err
}
reply := new(struct{ Value map[string]interface{} })
if err = json.Unmarshal(rawResp, reply); err != nil {
return nil, err
}
settings = reply.Value
return
}
// DeviceScaleRatio get device pixel ratio
func (d *uiaDriver) DeviceScaleRatio() (scale float64, err error) {
// register(getHandler, new GetDevicePixelRatio("/wd/hub/session/:sessionId/appium/device/pixel_ratio"))
var rawResp rawResponse
if rawResp, err = d.httpGET("/session", d.sessionId, "appium/device/pixel_ratio"); err != nil {
return 0, err
}
reply := new(struct{ Value float64 })
if err = json.Unmarshal(rawResp, reply); err != nil {
return 0, err
}
scale = reply.Value
return
}
type (
AndroidDeviceInfo struct {
// ANDROID_ID A 64-bit number (as a hex string) that is uniquely generated when the user
// first sets up the device and should remain constant for the lifetime of the user's device. The value
// may change if a factory reset is performed on the device.
AndroidID string `json:"androidId"`
// Build.MANUFACTURER value
Manufacturer string `json:"manufacturer"`
// Build.MODEL value
Model string `json:"model"`
// Build.BRAND value
Brand string `json:"brand"`
// Current running OS's API VERSION
APIVersion string `json:"apiVersion"`
// The current version string, for example "1.0" or "3.4b5"
PlatformVersion string `json:"platformVersion"`
// the name of the current celluar network carrier
CarrierName string `json:"carrierName"`
// the real size of the default display
RealDisplaySize string `json:"realDisplaySize"`
// The logical density of the display in Density Independent Pixel units.
DisplayDensity int `json:"displayDensity"`
// available networks
Networks []networkInfo `json:"networks"`
// current system locale
Locale string `json:"locale"`
// current system timezone
// e.g. "Asia/Tokyo", "America/Caracas", "Asia/Shanghai"
TimeZone string `json:"timeZone"`
Bluetooth struct {
State string `json:"state"`
} `json:"bluetooth"`
}
networkCapabilities struct {
TransportTypes string `json:"transportTypes"`
NetworkCapabilities string `json:"networkCapabilities"`
LinkUpstreamBandwidthKbps int `json:"linkUpstreamBandwidthKbps"`
LinkDownBandwidthKbps int `json:"linkDownBandwidthKbps"`
SignalStrength int `json:"signalStrength"`
SSID string `json:"SSID"`
}
networkInfo struct {
Type int `json:"type"`
TypeName string `json:"typeName"`
Subtype int `json:"subtype"`
SubtypeName string `json:"subtypeName"`
IsConnected bool `json:"isConnected"`
DetailedState string `json:"detailedState"`
State string `json:"state"`
ExtraInfo string `json:"extraInfo"`
IsAvailable bool `json:"isAvailable"`
IsRoaming bool `json:"isRoaming"`
IsFailover bool `json:"isFailover"`
Capabilities networkCapabilities `json:"capabilities"`
}
)
func (d *uiaDriver) DeviceInfo() (info AndroidDeviceInfo, err error) {
// register(getHandler, new GetDeviceInfo("/wd/hub/session/:sessionId/appium/device/info"))
var rawResp rawResponse
if rawResp, err = d.httpGET("/session", d.sessionId, "appium/device/info"); err != nil {
return AndroidDeviceInfo{}, err
}
reply := new(struct{ Value AndroidDeviceInfo })
if err = json.Unmarshal(rawResp, reply); err != nil {
return AndroidDeviceInfo{}, err
}
info = reply.Value
return
}
// AlertText get text of the on-screen dialog
func (d *uiaDriver) AlertText() (text string, err error) {
// register(getHandler, new GetAlertText("/wd/hub/session/:sessionId/alert/text"))
var rawResp rawResponse
if rawResp, err = d.httpGET("/session", d.sessionId, "alert/text"); err != nil {
return "", err
}
reply := new(struct{ Value string })
if err = json.Unmarshal(rawResp, reply); err != nil {
return "", err
}
text = reply.Value
return
}
// Tap perform a click at arbitrary coordinates specified
func (d *uiaDriver) Tap(x, y int) (err error) {
return d.TapFloat(float64(x), float64(y))
}
func (d *uiaDriver) TapFloat(x, y float64) (err error) {
// register(postHandler, new Tap("/wd/hub/session/:sessionId/appium/tap"))
data := map[string]interface{}{
"x": x,
"y": y,
}
_, err = d.httpPOST(data, "/session", d.sessionId, "appium/tap")
return
}
func (d *uiaDriver) TapPoint(point Point) (err error) {
return d.Tap(point.X, point.Y)
}
func (d *uiaDriver) TapPointF(point PointF) (err error) {
return d.TapFloat(point.X, point.Y)
}
func (d *uiaDriver) _swipe(startX, startY, endX, endY interface{}, steps int, elementID ...string) (err error) {
// register(postHandler, new Swipe("/wd/hub/session/:sessionId/touch/perform"))
data := map[string]interface{}{
"startX": startX,
"startY": startY,
"endX": endX,
"endY": endY,
"steps": steps,
}
if len(elementID) != 0 {
data["elementId"] = elementID[0]
}
_, err = d.httpPOST(data, "/session", d.sessionId, "touch/perform")
return
}
// Swipe performs a swipe from one coordinate to another using the number of steps
// to determine smoothness and speed. Each step execution is throttled to 5ms
// per step. So for a 100 steps, the swipe will take about 1/2 second to complete.
// `steps` is the number of move steps sent to the system
func (d *uiaDriver) Swipe(startX, startY, endX, endY int, steps ...int) (err error) {
return d.SwipeFloat(float64(startX), float64(startY), float64(endX), float64(endY), steps...)
}
func (d *uiaDriver) SwipeFloat(startX, startY, endX, endY float64, steps ...int) (err error) {
if len(steps) == 0 {
steps = []int{12}
}
return d._swipe(startX, startY, endX, endY, steps[0])
}
func (d *uiaDriver) SwipePoint(startPoint, endPoint Point, steps ...int) (err error) {
return d.Swipe(startPoint.X, startPoint.Y, endPoint.X, endPoint.Y, steps...)
}
func (d *uiaDriver) SwipePointF(startPoint, endPoint PointF, steps ...int) (err error) {
return d.SwipeFloat(startPoint.X, startPoint.Y, endPoint.X, endPoint.Y, steps...)
}
func (d *uiaDriver) _drag(data map[string]interface{}) (err error) {
// register(postHandler, new Drag("/wd/hub/session/:sessionId/touch/drag"))
_, err = d.httpPOST(data, "/session", d.sessionId, "touch/drag")
return
}
// Drag performs a swipe from one coordinate to another coordinate. You can control
// the smoothness and speed of the swipe by specifying the number of steps.
// Each step execution is throttled to 5 milliseconds per step, so for a 100
// steps, the swipe will take around 0.5 seconds to complete.
func (d *uiaDriver) Drag(startX, startY, endX, endY int, steps ...int) (err error) {
return d.DragFloat(float64(startX), float64(startY), float64(endX), float64(endY), steps...)
}
func (d *uiaDriver) DragFloat(startX, startY, endX, endY float64, steps ...int) error {
if len(steps) == 0 {
steps = []int{12}
}
data := map[string]interface{}{
"startX": startX,
"startY": startY,
"endX": endX,
"endY": endY,
"steps": steps[0],
}
return d._drag(data)
}
func (d *uiaDriver) DragPoint(startPoint Point, endPoint Point, steps ...int) error {
return d.Drag(startPoint.X, startPoint.Y, endPoint.X, endPoint.Y, steps...)
}
func (d *uiaDriver) DragPointF(startPoint PointF, endPoint PointF, steps ...int) (err error) {
return d.DragFloat(startPoint.X, startPoint.Y, endPoint.X, endPoint.Y, steps...)
}
func (d *uiaDriver) TouchLongClick(x, y int, duration ...float64) (err error) {
if len(duration) == 0 {
duration = []float64{1.0}
}
// register(postHandler, new TouchLongClick("/wd/hub/session/:sessionId/touch/longclick"))
data := map[string]interface{}{
"params": map[string]interface{}{
"x": x,
"y": y,
"duration": int(duration[0] * 1000),
},
}
_, err = d.httpPOST(data, "/session", d.sessionId, "touch/longclick")
return
}
func (d *uiaDriver) TouchLongClickPoint(point Point, duration ...float64) (err error) {
return d.TouchLongClick(point.X, point.Y, duration...)
}
func (d *uiaDriver) SendKeys(text string, isReplace ...bool) (err error) {
if len(isReplace) == 0 {
isReplace = []bool{true}
}
// register(postHandler, new SendKeysToElement("/wd/hub/session/:sessionId/keys"))
// https://github.com/appium/appium-uiautomator2-server/blob/master/app/src/main/java/io/appium/uiautomator2/handler/SendKeysToElement.java#L76-L85
data := map[string]interface{}{
"text": text,
"replace": isReplace[0],
}
_, err = d.httpPOST(data, "/session", d.sessionId, "keys")
return
}
// PressBack simulates a short press on the BACK button.
func (d *uiaDriver) PressBack() (err error) {
// register(postHandler, new PressBack("/wd/hub/session/:sessionId/back"))
_, err = d.httpPOST(nil, "/session", d.sessionId, "back")
return
}
// public class KeyCodeModel extends BaseModel {
// @RequiredField
// public Integer keycode;
// public Integer metastate;
// public Integer flags;
// }
func (d *uiaDriver) LongPressKeyCode(keyCode KeyCode, metaState KeyMeta, flags ...KeyFlag) (err error) {
if len(flags) == 0 {
flags = []KeyFlag{KFFromSystem}
}
data := map[string]interface{}{
"keycode": keyCode,
"metastate": metaState,
"flags": flags[0],
}
// register(postHandler, new LongPressKeyCode("/wd/hub/session/:sessionId/appium/device/long_press_keycode"))
_, err = d.httpPOST(data, "/session", d.sessionId, "/appium/device/long_press_keycode")
return
}
func (d *uiaDriver) _pressKeyCode(keyCode KeyCode, metaState KeyMeta, flags ...KeyFlag) (err error) {
// register(postHandler, new PressKeyCodeAsync("/wd/hub/session/:sessionId/appium/device/press_keycode"))
data := map[string]interface{}{
"keycode": keyCode,
}
if metaState != KMEmpty {
data["metastate"] = metaState
}
if len(flags) != 0 {
data["flags"] = flags[0]
}
_, err = d.httpPOST(data, "/session", d.sessionId, "appium/device/press_keycode")
return
}
func (d *uiaDriver) PressKeyCode(keyCode KeyCode, metaState KeyMeta, flags ...KeyFlag) (err error) {
if len(flags) == 0 {
flags = []KeyFlag{KFFromSystem}
}
return d._pressKeyCode(keyCode, metaState, KFFromSystem)
}
// PressKeyCodeAsync simulates a short press using a key code.
func (d *uiaDriver) PressKeyCodeAsync(keyCode KeyCode, metaState ...KeyMeta) (err error) {
if len(metaState) == 0 {
metaState = []KeyMeta{KMEmpty}
}
return d._pressKeyCode(keyCode, metaState[0])
}
func (d *uiaDriver) TouchDown(x, y int) (err error) {
// register(postHandler, new TouchDown("/wd/hub/session/:sessionId/touch/down"))
data := map[string]interface{}{
"params": map[string]interface{}{
"x": x,
"y": y,
},
}
_, err = d.httpPOST(data, "/session", d.sessionId, "touch/down")
return
}
func (d *uiaDriver) TouchDownPoint(point Point) error {
return d.TouchDown(point.X, point.Y)
}
func (d *uiaDriver) TouchUp(x, y int) (err error) {
// register(postHandler, new TouchUp("/wd/hub/session/:sessionId/touch/up"))
data := map[string]interface{}{
"params": map[string]interface{}{
"x": x,
"y": y,
},
}
_, err = d.httpPOST(data, "/session", d.sessionId, "touch/up")
return
}
func (d *uiaDriver) TouchUpPoint(point Point) error {
return d.TouchUp(point.X, point.Y)
}
func (d *uiaDriver) TouchMove(x, y int) (err error) {
// register(postHandler, new TouchMove("/wd/hub/session/:sessionId/touch/move"))
data := map[string]interface{}{
"params": map[string]interface{}{
"x": x,
"y": y,
},
}
_, err = d.httpPOST(data, "/session", d.sessionId, "touch/move")
return
}
func (d *uiaDriver) TouchMovePoint(point Point) error {
return d.TouchMove(point.X, point.Y)
}
// OpenNotification opens the notification shade.
func (d *uiaDriver) OpenNotification() (err error) {
// register(postHandler, new OpenNotification("/wd/hub/session/:sessionId/appium/device/open_notifications"))
_, err = d.httpPOST(nil, "/session", d.sessionId, "appium/device/open_notifications")
return
}
func (d *uiaDriver) _flick(data map[string]interface{}) (err error) {
// register(postHandler, new Flick("/wd/hub/session/:sessionId/touch/flick"))
_, err = d.httpPOST(data, "/session", d.sessionId, "touch/flick")
return
}
func (d *uiaDriver) Flick(xSpeed, ySpeed int) (err error) {
data := map[string]interface{}{
"xspeed": xSpeed,
"yspeed": ySpeed,
}
if xSpeed == 0 && ySpeed == 0 {
return errors.New("both 'xSpeed' and 'ySpeed' cannot be zero")
}
return d._flick(data)
}
func (d *uiaDriver) _scrollTo(method, selector string, maxSwipes int, elementID ...string) (err error) {
// register(postHandler, new ScrollTo("/wd/hub/session/:sessionId/touch/scroll"))
params := map[string]interface{}{
"strategy": method,
"selector": selector,
}
if maxSwipes > 0 {
params["maxSwipes"] = maxSwipes
}
data := map[string]interface{}{"params": params}
if len(elementID) != 0 {
data["origin"] = map[string]string{
legacyWebElementIdentifier: elementID[0],
webElementIdentifier: elementID[0],
}
}
_, err = d.httpPOST(data, "/session", d.sessionId, "touch/scroll")
return
}
func (d *uiaDriver) ScrollTo(by AndroidBySelector, maxSwipes ...int) (err error) {
if len(maxSwipes) == 0 {
maxSwipes = []int{0}
}
method, selector := by.getMethodAndSelector()
return d._scrollTo(method, selector, maxSwipes[0])
}
type W3CMouseButtonType int
const (
MBTLeft W3CMouseButtonType = 0
MBTMiddle W3CMouseButtonType = 1
MBTRight W3CMouseButtonType = 2
)
func (g *W3CGestures) PointerDown(button ...W3CMouseButtonType) *W3CGestures {
if len(button) == 0 {
button = []W3CMouseButtonType{MBTLeft}
}
*g = append(*g, _newW3CGesture().pointerDown(int(button[0])))
return g
}
func (g *W3CGestures) PointerUp(button ...W3CMouseButtonType) *W3CGestures {
if len(button) == 0 {
button = []W3CMouseButtonType{MBTLeft}
}
*g = append(*g, _newW3CGesture().pointerUp(int(button[0])))
return g
}
type W3CPointerMoveType string
const (
PMTViewport W3CPointerMoveType = "viewport"
PMTPointer W3CPointerMoveType = "pointer"
)
func (g *W3CGestures) PointerMove(x, y float64, origin interface{}, duration float64, pressure, size float64) *W3CGestures {
val := ""
switch v := origin.(type) {
case string:
val = v
case W3CPointerMoveType:
val = string(v)
case *uiaElement:
val = v.id
default:
val = string(PMTViewport)
}
*g = append(*g, _newW3CGesture().pointerMove(x, y, val, duration, pressure, size))
return g
}
func (g *W3CGestures) PointerMoveTo(x, y float64, duration ...float64) *W3CGestures {
if len(duration) == 0 || duration[0] < 0 {
duration = []float64{0.5}
}
*g = append(*g, _newW3CGesture().pointerMove(x, y, string(PMTViewport), duration[0]*1000))
return g
}
func (g *W3CGestures) PointerMoveRelative(x, y float64, duration ...float64) *W3CGestures {
if len(duration) == 0 || duration[0] < 0 {
duration = []float64{0.5}
}
*g = append(*g, _newW3CGesture().pointerMove(x, y, string(PMTPointer), duration[0]*1000))
return g
}
func (g *W3CGestures) PointerMouseOver(x, y float64, element *uiaElement, duration ...float64) *W3CGestures {
if len(duration) == 0 || duration[0] < 0 {
duration = []float64{0.5}
}
*g = append(*g, _newW3CGesture().pointerMove(x, y, element.id, duration[0]*1000))
return g
}
type W3CAction map[string]interface{}
type W3CActionType string
const (
_ W3CActionType = "none"
ATKey W3CActionType = "key"
ATPointer W3CActionType = "pointer"
)
type W3CPointerType string
const (
PTMouse W3CPointerType = "mouse"
PTPen W3CPointerType = "pen"
PTTouch W3CPointerType = "touch"
)
func NewW3CAction(actionType W3CActionType, gestures *W3CGestures, pointerType ...W3CPointerType) W3CAction {
w3cAction := make(W3CAction)
w3cAction["type"] = actionType
w3cAction["actions"] = gestures
if actionType != ATPointer {
return w3cAction
}
if len(pointerType) == 0 {
pointerType = []W3CPointerType{PTTouch}
}
type W3CItemParameters struct {
PointerType W3CPointerType `json:"pointerType"`
}
w3cAction["parameters"] = W3CItemParameters{PointerType: pointerType[0]}
return w3cAction
}
func (d *uiaDriver) PerformW3CActions(action W3CAction, acts ...W3CAction) (err error) {
var actionId uint64 = 1
acts = append([]W3CAction{action}, acts...)
for i := range acts {
item := acts[i]
item["id"] = strconv.FormatUint(actionId, 10)
actionId++
acts[i] = item
}
data := map[string]interface{}{
"actions": acts,
}
// register(postHandler, new W3CActions("/wd/hub/session/:sessionId/actions"))
_, err = d.httpPOST(data, "/session", d.sessionId, "/actions")
return
}
type ClipDataType string
const ClipDataTypePlaintext ClipDataType = "PLAINTEXT"
func (d *uiaDriver) GetClipboard(contentType ...ClipDataType) (content string, err error) {
if len(contentType) == 0 {
contentType = []ClipDataType{ClipDataTypePlaintext}
}
// register(postHandler, new GetClipboard("/wd/hub/session/:sessionId/appium/device/get_clipboard"))
data := map[string]interface{}{
"contentType": contentType[0],
}
var rawResp rawResponse
if rawResp, err = d.httpPOST(data, "/session", d.sessionId, "appium/device/get_clipboard"); err != nil {
return "", err
}
reply := new(struct{ Value string })
if err = json.Unmarshal(rawResp, reply); err != nil {
return "", err
}
content = reply.Value
if data, err := base64.StdEncoding.DecodeString(content); err != nil {
return content, err
} else {
content = string(data)
}
return
}
func (d *uiaDriver) SetClipboard(contentType ClipDataType, content string, label ...string) (err error) {
lbl := content
if len(label) != 0 {
lbl = label[0]
}
const defaultLabelLen = 10
if len(lbl) > defaultLabelLen {
lbl = lbl[:defaultLabelLen]
}
data := map[string]interface{}{
"contentType": contentType,
"label": lbl,
"content": base64.StdEncoding.EncodeToString([]byte(content)),
}
// register(postHandler, new SetClipboard("/wd/hub/session/:sessionId/appium/device/set_clipboard"))
_, err = d.httpPOST(data, "/session", d.sessionId, "appium/device/set_clipboard")
return
}
func (d *uiaDriver) AlertAccept(buttonLabel ...string) (err error) {
data := map[string]interface{}{
"buttonLabel": nil,
}
if len(buttonLabel) != 0 {
data["buttonLabel"] = buttonLabel[0]
}
// register(postHandler, new AcceptAlert("/wd/hub/session/:sessionId/alert/accept"))
_, err = d.httpPOST(data, "/session", d.sessionId, "alert/accept")
return
}
func (d *uiaDriver) AlertDismiss(buttonLabel ...string) (err error) {
data := map[string]interface{}{
"buttonLabel": nil,
}
if len(buttonLabel) != 0 {
data["buttonLabel"] = buttonLabel[0]
}
// register(postHandler, new DismissAlert("/wd/hub/session/:sessionId/alert/dismiss"))
_, err = d.httpPOST(data, "/session", d.sessionId, "alert/dismiss")
return
}
func (d *uiaDriver) SetAppiumSettings(settings map[string]interface{}) (err error) {
data := map[string]interface{}{
"settings": settings,
}
// register(postHandler, new UpdateSettings("/wd/hub/session/:sessionId/appium/settings"))
_, err = d.httpPOST(data, "/session", d.sessionId, "appium/settings")
return
}
func (d *uiaDriver) SetOrientation(orientation Orientation) (err error) {
data := map[string]interface{}{
"orientation": orientation,
}
// register(postHandler, new SetOrientation("/wd/hub/session/:sessionId/orientation"))
_, err = d.httpPOST(data, "/session", d.sessionId, "orientation")
return
}
// SetRotation
// `x` and `y` are ignored. We only care about `z`
// 0/90/180/270
func (d *uiaDriver) SetRotation(rotation Rotation) (err error) {
data := map[string]interface{}{
"z": rotation.Z,
}
// register(postHandler, new SetRotation("/wd/hub/session/:sessionId/rotation"))
_, err = d.httpPOST(data, "/session", d.sessionId, "rotation")
return
}
type NetworkType int
const (
NetworkTypeWifi NetworkType = 2
// NetworkTypeNone NetworkType = iota
// NetworkTypeAirplane
// NetworkTypeWifi
// _
// NetworkTypeData
// _
// NetworkTypeAll
)
// NetworkConnection always turn on
func (d *uiaDriver) NetworkConnection(networkType NetworkType) (err error) {
// register(postHandler, new NetworkConnection("/wd/hub/session/:sessionId/network_connection"))
data := map[string]interface{}{
"type": networkType,
}
_, err = d.httpPOST(data, "/session", d.sessionId, "network_connection")
return
}
func (d *uiaDriver) _findElements(method, selector string, elementID ...string) (elements []*uiaElement, err error) {
// register(postHandler, new FindElements("/wd/hub/session/:sessionId/elements"))
data := map[string]interface{}{
"strategy": method,
"selector": selector,
}
if len(elementID) != 0 {
data["context"] = elementID[0]
}
var rawResp rawResponse
if rawResp, err = d.httpPOST(data, "/session", d.sessionId, "/elements"); err != nil {
return nil, err
}
reply := new(struct{ Value []map[string]string })
if err = json.Unmarshal(rawResp, reply); err != nil {
return nil, err
}
if len(reply.Value) == 0 {
return nil, fmt.Errorf("no such element: unable to find an element using '%s', value '%s'", method, selector)
}
elements = make([]*uiaElement, len(reply.Value))
for i, elem := range reply.Value {
var id string
if id = elementIDFromValue(elem); id == "" {
return nil, fmt.Errorf("invalid element returned: %+v", reply)
}
elements[i] = &uiaElement{parent: d, id: id}
}
return
}
func (d *uiaDriver) _findElement(method, selector string, elementID ...string) (elem *uiaElement, err error) {
// register(postHandler, new FindElement("/wd/hub/session/:sessionId/element"))
data := map[string]interface{}{
"strategy": method,
"selector": selector,
}
if len(elementID) != 0 {
data["context"] = elementID[0]
}
var rawResp rawResponse
if rawResp, err = d.httpPOST(data, "/session", d.sessionId, "/element"); err != nil {
return nil, err
}
reply := new(struct{ Value map[string]string })
if err = json.Unmarshal(rawResp, reply); err != nil {
return nil, err
}
if len(reply.Value) == 0 {
return nil, fmt.Errorf("no such element: unable to find an element using '%s', value '%s'", method, selector)
}
var id string
if id = elementIDFromValue(reply.Value); id == "" {
return nil, fmt.Errorf("invalid element returned: %+v", reply)
}
elem = &uiaElement{parent: d, id: id}
return
}
func (d *uiaDriver) FindElements(by AndroidBySelector) (elements []*uiaElement, err error) {
return d._findElements(by.getMethodAndSelector())
}
func (d *uiaDriver) FindElement(by AndroidBySelector) (elem *uiaElement, err error) {
return d._findElement(by.getMethodAndSelector())
}
func (d *uiaDriver) ActiveElement() (elem *uiaElement, err error) {
// register(getHandler, new ActiveElement("/wd/hub/session/:sessionId/element/active"))
var rawResp rawResponse
if rawResp, err = d.httpGET("/session", d.sessionId, "/element/active"); err != nil {
return nil, err
}
reply := new(struct{ Value map[string]string })
if err = json.Unmarshal(rawResp, reply); err != nil {
return nil, err
}
if len(reply.Value) == 0 {
return nil, errors.New("no such element")
}
var id string
if id = elementIDFromValue(reply.Value); id == "" {
return nil, fmt.Errorf("invalid element returned: %+v", reply)
}
elem = &uiaElement{parent: d, id: id}
return
}
type AndroidCondition func(d *uiaDriver) (bool, error)
func (d *uiaDriver) _waitWithTimeoutAndInterval(condition AndroidCondition, timeout, interval time.Duration) (err error) {
startTime := time.Now()
for {
done, err := condition(d)
if err != nil {
return err
}
if done {
return nil
}
if elapsed := time.Since(startTime); elapsed > timeout {
return fmt.Errorf("timeout after %v", elapsed)
}
time.Sleep(interval)
}
}
// WaitWithTimeoutAndInterval waits for the condition to evaluate to true.
func (d *uiaDriver) WaitWithTimeoutAndInterval(condition AndroidCondition, timeout, interval float64) (err error) {
dTimeout := time.Millisecond * time.Duration(timeout*1000)
dInterval := time.Millisecond * time.Duration(interval*1000)
return d._waitWithTimeoutAndInterval(condition, dTimeout, dInterval)
}
// WaitWithTimeout works like WaitWithTimeoutAndInterval, but with default polling interval.
func (d *uiaDriver) WaitWithTimeout(condition AndroidCondition, timeout float64) error {
dTimeout := time.Millisecond * time.Duration(timeout*1000)
return d._waitWithTimeoutAndInterval(condition, dTimeout, DefaultWaitInterval)
}
// Wait works like WaitWithTimeoutAndInterval, but using the default timeout and polling interval.
func (d *uiaDriver) Wait(condition AndroidCondition) error {
return d._waitWithTimeoutAndInterval(condition, DefaultWaitTimeout, DefaultWaitInterval)
}