httprunner/uixt/android_driver_uia2.go

612 lines
20 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package uixt
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"strings"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v5/code"
"github.com/httprunner/httprunner/v5/internal/utf7"
"github.com/httprunner/httprunner/v5/uixt/option"
"github.com/httprunner/httprunner/v5/uixt/types"
)
func NewUIA2Driver(device *AndroidDevice) (*UIA2Driver, error) {
log.Info().Interface("device", device).Msg("init android UIA2 driver")
adbDriver, err := NewADBDriver(device)
if err != nil {
return nil, err
}
driver := &UIA2Driver{
ADBDriver: adbDriver,
}
// setup driver
if err := driver.Setup(); err != nil {
return nil, err
}
// register driver session reset handler
driver.Session.RegisterResetHandler(driver.Setup)
return driver, nil
}
type UIA2Driver struct {
*ADBDriver
// cache to avoid repeated query
windowSize types.Size
}
func (ud *UIA2Driver) Setup() error {
localPort, err := ud.Device.Forward(ud.Device.Options.UIA2Port)
if err != nil {
return errors.Wrap(code.DeviceConnectionError,
fmt.Sprintf("forward port %d->%d failed: %v",
localPort, ud.Device.Options.UIA2Port, err))
}
err = ud.Session.SetupPortForward(localPort)
if err != nil {
return err
}
ud.Session.SetBaseURL(
fmt.Sprintf("http://forward-to-%d:%d/wd/hub",
localPort, ud.Device.Options.UIA2Port))
// uiautomator2 server must be started before
// check uiautomator server package installed
if !ud.Device.IsPackageInstalled(ud.Device.Options.UIA2ServerPackageName) {
return errors.Wrapf(code.MobileUIDriverAppNotInstalled,
"%s not installed", ud.Device.Options.UIA2ServerPackageName)
}
if !ud.Device.IsPackageInstalled(ud.Device.Options.UIA2ServerTestPackageName) {
return errors.Wrapf(code.MobileUIDriverAppNotInstalled,
"%s not installed", ud.Device.Options.UIA2ServerTestPackageName)
}
// TODO: check uiautomator server package running
// if dev.IsPackageRunning(UIA2ServerPackageName) {
// return nil
// }
// start uiautomator2 server
// go func() {
// if err := ud.startUIA2Server(); err != nil {
// log.Fatal().Err(err).Msg("start UIA2 failed")
// }
// }()
// time.Sleep(5 * time.Second) // wait for uiautomator2 server start
// create new session
err = ud.InitSession(nil)
if err != nil {
return err
}
return nil
}
func (ud *UIA2Driver) TearDown() error {
log.Warn().Msg("TearDown not implemented in UIA2Driver")
return nil
}
func (ud *UIA2Driver) InitSession(capabilities option.Capabilities) (err error) {
// register(postHandler, new InitSession("/wd/hub/session"))
var rawResp DriverRawResponse
data := make(map[string]interface{})
if len(capabilities) == 0 {
data["capabilities"] = make(map[string]interface{})
} else {
data["capabilities"] = map[string]interface{}{"alwaysMatch": capabilities}
}
if rawResp, err = ud.Session.POST(data, "/session"); err != nil {
return err
}
reply := new(struct{ Value struct{ SessionId string } })
if err = json.Unmarshal(rawResp, reply); err != nil {
return err
}
ud.Session.ID = reply.Value.SessionId
return nil
}
func (ud *UIA2Driver) DeleteSession() (err error) {
if ud.Session.ID == "" {
return nil
}
urlStr := fmt.Sprintf("/session/%s", ud.Session.ID)
if _, err = ud.Session.DELETE(urlStr); err == nil {
ud.Session.ID = ""
}
return err
}
func (ud *UIA2Driver) Status() (deviceStatus types.DeviceStatus, err error) {
// register(getHandler, new Status("/wd/hub/status"))
var rawResp DriverRawResponse
// Notice: use Driver.GET instead of httpGET to avoid loop calling
if rawResp, err = ud.Session.GET("/status"); err != nil {
return types.DeviceStatus{Ready: false}, err
}
reply := new(struct {
Value struct {
// Message string
Ready bool
}
})
if err = json.Unmarshal(rawResp, reply); err != nil {
return types.DeviceStatus{Ready: false}, err
}
return types.DeviceStatus{Ready: true}, nil
}
func (ud *UIA2Driver) DeviceInfo() (deviceInfo types.DeviceInfo, err error) {
// register(getHandler, new GetDeviceInfo("/wd/hub/session/:sessionId/appium/device/info"))
var rawResp DriverRawResponse
urlStr := fmt.Sprintf("/session/%s/appium/device/info", ud.Session.ID)
if rawResp, err = ud.Session.GET(urlStr); err != nil {
return types.DeviceInfo{}, err
}
reply := new(struct{ Value struct{ types.DeviceInfo } })
if err = json.Unmarshal(rawResp, reply); err != nil {
return types.DeviceInfo{}, err
}
deviceInfo = reply.Value.DeviceInfo
return
}
func (ud *UIA2Driver) BatteryInfo() (batteryInfo types.BatteryInfo, err error) {
// register(getHandler, new GetBatteryInfo("/wd/hub/session/:sessionId/appium/device/battery_info"))
var rawResp DriverRawResponse
urlStr := fmt.Sprintf("/session/%s/appium/device/battery_info", ud.Session.ID)
if rawResp, err = ud.Session.GET(urlStr); err != nil {
return types.BatteryInfo{}, err
}
reply := new(struct{ Value struct{ types.BatteryInfo } })
if err = json.Unmarshal(rawResp, reply); err != nil {
return types.BatteryInfo{}, err
}
if reply.Value.Level == -1 || reply.Value.Status == -1 {
return reply.Value.BatteryInfo, errors.New("cannot be retrieved from the system")
}
batteryInfo = reply.Value.BatteryInfo
return
}
func (ud *UIA2Driver) WindowSize() (size types.Size, err error) {
// register(getHandler, new GetDeviceSize("/wd/hub/session/:sessionId/window/:windowHandle/size"))
if !ud.windowSize.IsNil() {
// use cached window size
return ud.windowSize, nil
}
var rawResp DriverRawResponse
urlStr := fmt.Sprintf("/session/%s/window/:windowHandle/size", ud.Session.ID)
if rawResp, err = ud.Session.GET(urlStr); err != nil {
return types.Size{}, errors.Wrap(err, "get window size failed by UIA2 request")
}
reply := new(struct{ Value struct{ types.Size } })
if err = json.Unmarshal(rawResp, reply); err != nil {
return types.Size{}, errors.Wrap(err, "get window size failed by UIA2 response")
}
size = reply.Value.Size
// check orientation
orientation, err := ud.Orientation()
if err != nil {
log.Warn().Err(err).Msgf("window size get orientation failed, use default orientation")
orientation = types.OrientationPortrait
}
if orientation != types.OrientationPortrait {
size.Width, size.Height = size.Height, size.Width
}
ud.windowSize = size // cache window size
return size, nil
}
// Back simulates a short press on the BACK button.
func (ud *UIA2Driver) Back() (err error) {
log.Info().Msg("UIA2Driver.Back")
// register(postHandler, new PressBack("/wd/hub/session/:sessionId/back"))
urlStr := fmt.Sprintf("/session/%s/back", ud.Session.ID)
_, err = ud.Session.POST(nil, urlStr)
return
}
func (ud *UIA2Driver) PressKeyCodes(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]
}
urlStr := fmt.Sprintf("/session/%s/appium/device/press_keycode", ud.Session.ID)
_, err = ud.Session.POST(data, urlStr)
return
}
func (ud *UIA2Driver) Orientation() (orientation types.Orientation, err error) {
// [[FBRoute GET:@"/orientation"] respondWithTarget:self action:@selector(handleGetOrientation:)]
var rawResp DriverRawResponse
urlStr := fmt.Sprintf("/session/%s/orientation", ud.Session.ID)
if rawResp, err = ud.Session.GET(urlStr); err != nil {
return "", err
}
reply := new(struct{ Value types.Orientation })
if err = json.Unmarshal(rawResp, reply); err != nil {
return "", err
}
orientation = reply.Value
return
}
func (ud *UIA2Driver) DoubleTap(x, y float64, opts ...option.ActionOption) error {
log.Info().Float64("x", x).Float64("y", y).Msg("UIA2Driver.DoubleTap")
var err error
x, y, err = convertToAbsolutePoint(ud, x, y)
if err != nil {
return err
}
actionOptions := option.NewActionOptions(opts...)
x, y = actionOptions.ApplyTapOffset(x, y)
data := map[string]interface{}{
"actions": []interface{}{
map[string]interface{}{
"type": "pointer",
"parameters": map[string]string{"pointerType": "touch"},
"id": "touch",
"actions": []interface{}{
map[string]interface{}{"type": "pointerMove", "duration": 0, "x": x, "y": y, "origin": "viewport"},
map[string]interface{}{"type": "pointerDown", "duration": 0, "button": 0},
map[string]interface{}{"type": "pointerUp", "duration": 0, "button": 0},
map[string]interface{}{"type": "pointerDown", "duration": 0, "button": 0},
map[string]interface{}{"type": "pointerUp", "duration": 0, "button": 0},
},
},
},
}
urlStr := fmt.Sprintf("/session/%s/actions/tap", ud.Session.ID)
_, err = ud.Session.POST(data, urlStr)
return err
}
func (ud *UIA2Driver) TapXY(x, y float64, opts ...option.ActionOption) error {
log.Info().Float64("x", x).Float64("y", y).Msg("UIA2Driver.TapXY")
// register(postHandler, new Tap("/wd/hub/session/:sessionId/appium/tap"))
absX, absY, err := convertToAbsolutePoint(ud, x, y)
if err != nil {
return err
}
return ud.TapAbsXY(absX, absY, opts...)
}
func (ud *UIA2Driver) TapAbsXY(x, y float64, opts ...option.ActionOption) error {
log.Info().Float64("x", x).Float64("y", y).Msg("UIA2Driver.TapAbsXY")
// register(postHandler, new Tap("/wd/hub/session/:sessionId/appium/tap"))
actionOptions := option.NewActionOptions(opts...)
x, y = actionOptions.ApplyTapOffset(x, y)
duration := 100.0
if actionOptions.PressDuration > 0 {
duration = actionOptions.PressDuration * 1000 // convert to ms
}
data := map[string]interface{}{
"actions": []interface{}{
map[string]interface{}{
"type": "pointer",
"parameters": map[string]string{"pointerType": "touch"},
"id": "touch",
"actions": []interface{}{
map[string]interface{}{"type": "pointerMove", "duration": 0, "x": x, "y": y, "origin": "viewport"},
map[string]interface{}{"type": "pointerDown", "duration": 0, "button": 0},
map[string]interface{}{"type": "pause", "duration": duration},
map[string]interface{}{"type": "pointerUp", "duration": 0, "button": 0},
},
},
},
}
option.MergeOptions(data, opts...)
urlStr := fmt.Sprintf("/session/%s/actions/tap", ud.Session.ID)
_, err := ud.Session.POST(data, urlStr)
return err
}
func (ud *UIA2Driver) TouchAndHold(x, y float64, opts ...option.ActionOption) (err error) {
log.Info().Float64("x", x).Float64("y", y).Msg("UIA2Driver.TouchAndHold")
actionOptions := option.NewActionOptions(opts...)
x, y = actionOptions.ApplyTapOffset(x, y)
duration := actionOptions.Duration
if duration == 0 {
duration = 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 * 1000),
},
}
urlStr := fmt.Sprintf("/session/%s/touch/longclick", ud.Session.ID)
_, err = ud.Session.POST(data, urlStr)
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 (ud *UIA2Driver) Drag(fromX, fromY, toX, toY float64, opts ...option.ActionOption) error {
log.Info().Float64("fromX", fromX).Float64("fromY", fromY).
Float64("toX", toX).Float64("toY", toY).Msg("UIA2Driver.Drag")
var err error
fromX, fromY, toX, toY, err = convertToAbsoluteCoordinates(ud, fromX, fromY, toX, toY)
if err != nil {
return err
}
actionOptions := option.NewActionOptions(opts...)
fromX, fromY, toX, toY = actionOptions.ApplySwipeOffset(fromX, fromY, toX, toY)
data := map[string]interface{}{
"startX": fromX,
"startY": fromY,
"endX": toX,
"endY": toY,
}
option.MergeOptions(data, opts...)
// register(postHandler, new Drag("/wd/hub/session/:sessionId/touch/drag"))
urlStr := fmt.Sprintf("/session/%s/touch/drag", ud.Session.ID)
_, err = ud.Session.POST(data, urlStr)
return err
}
// 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 (ud *UIA2Driver) Swipe(fromX, fromY, toX, toY float64, opts ...option.ActionOption) error {
// register(postHandler, new Swipe("/wd/hub/session/:sessionId/touch/perform"))
log.Info().Float64("fromX", fromX).Float64("fromY", fromY).
Float64("toX", toX).Float64("toY", toY).Msg("UIA2Driver.Swipe")
var err error
actionOptions := option.NewActionOptions(opts...)
fromX, fromY, toX, toY, err = convertToAbsoluteCoordinates(ud, fromX, fromY, toX, toY)
if err != nil {
return err
}
fromX, fromY, toX, toY = actionOptions.ApplySwipeOffset(fromX, fromY, toX, toY)
duration := 200.0
if actionOptions.PressDuration > 0 {
duration = actionOptions.PressDuration * 1000 // ms
}
data := map[string]interface{}{
"actions": []interface{}{
map[string]interface{}{
"type": "pointer",
"parameters": map[string]string{"pointerType": "touch"},
"id": "touch",
"actions": []interface{}{
map[string]interface{}{"type": "pointerMove", "duration": 0, "x": fromX, "y": fromY, "origin": "viewport"},
map[string]interface{}{"type": "pointerDown", "duration": 0, "button": 0},
map[string]interface{}{"type": "pointerMove", "duration": duration, "x": toX, "y": toY, "origin": "viewport"},
map[string]interface{}{"type": "pointerUp", "duration": 0, "button": 0},
},
},
},
}
option.MergeOptions(data, opts...)
urlStr := fmt.Sprintf("/session/%s/actions/swipe", ud.Session.ID)
_, err = ud.Session.POST(data, urlStr)
return err
}
func (ud *UIA2Driver) SetPasteboard(contentType types.PasteboardType, content string) (err error) {
log.Info().Str("contentType", string(contentType)).
Str("content", content).Msg("UIA2Driver.SetPasteboard")
lbl := content
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"))
urlStr := fmt.Sprintf("/session/%s/appium/device/set_clipboard", ud.Session.ID)
_, err = ud.Session.POST(data, urlStr)
return
}
func (ud *UIA2Driver) GetPasteboard(contentType types.PasteboardType) (raw *bytes.Buffer, err error) {
if len(contentType) == 0 {
contentType = types.PasteboardTypePlaintext
}
// register(postHandler, new GetClipboard("/wd/hub/session/:sessionId/appium/device/get_clipboard"))
data := map[string]interface{}{
"contentType": contentType[0],
}
var rawResp DriverRawResponse
urlStr := fmt.Sprintf("/session/%s/appium/device/get_clipboard", ud.Session.ID)
if rawResp, err = ud.Session.POST(data, urlStr); err != nil {
return
}
reply := new(struct{ Value string })
if err = json.Unmarshal(rawResp, reply); err != nil {
return
}
if data, err := base64.StdEncoding.DecodeString(reply.Value); err != nil {
raw.Write([]byte(reply.Value))
} else {
raw.Write(data)
}
return
}
// SendKeys Android input does not support setting frequency.
func (ud *UIA2Driver) Input(text string, opts ...option.ActionOption) (err error) {
log.Info().Str("text", text).Msg("UIA2Driver.Input")
// 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
err = ud.SendUnicodeKeys(text, opts...)
if err == nil {
return nil
}
data := map[string]interface{}{
"text": text,
}
option.MergeOptions(data, opts...)
urlStr := fmt.Sprintf("/session/%s/keys", ud.Session.ID)
_, err = ud.Session.POST(data, urlStr)
return
}
func (ud *UIA2Driver) SendUnicodeKeys(text string, opts ...option.ActionOption) (err error) {
log.Info().Str("text", text).Msg("UIA2Driver.SendUnicodeKeys")
// If the Unicode IME is not installed, fall back to the old interface.
// There might be differences in the tracking schemes across different phones, and it is pending further verification.
// In release version: without the Unicode IME installed, the test cannot execute.
if !ud.IsUnicodeIMEInstalled() {
return fmt.Errorf("appium unicode ime not installed")
}
currentIme, err := ud.ADBDriver.GetIme()
if err != nil {
return
}
if currentIme != option.UnicodeImePackageName {
defer func() {
_ = ud.ADBDriver.SetIme(currentIme)
}()
err = ud.ADBDriver.SetIme(option.UnicodeImePackageName)
if err != nil {
log.Warn().Err(err).Msgf("set Unicode Ime failed")
return
}
}
encodedStr, err := utf7.Encoding.NewEncoder().String(text)
if err != nil {
log.Warn().Err(err).Msgf("encode text with modified utf7 failed")
return
}
err = ud.SendActionKey(encodedStr, opts...)
return
}
func (ud *UIA2Driver) SendActionKey(text string, opts ...option.ActionOption) (err error) {
log.Info().Str("text", text).Msg("UIA2Driver.SendActionKey")
var actions []interface{}
for i, c := range text {
actions = append(actions, map[string]interface{}{"type": "keyDown", "value": string(c)},
map[string]interface{}{"type": "keyUp", "value": string(c)})
if i != len(text)-1 {
actions = append(actions, map[string]interface{}{"type": "pause", "duration": 40})
}
}
data := map[string]interface{}{
"actions": []interface{}{
map[string]interface{}{
"type": "key",
"id": "key",
"actions": actions,
},
},
}
option.MergeOptions(data, opts...)
urlStr := fmt.Sprintf("/session/%s/actions/keys", ud.Session.ID)
_, err = ud.Session.POST(data, urlStr)
return
}
func (ud *UIA2Driver) Rotation() (rotation types.Rotation, err error) {
// register(getHandler, new GetRotation("/wd/hub/session/:sessionId/rotation"))
var rawResp DriverRawResponse
urlStr := fmt.Sprintf("/session/%s/rotation", ud.Session.ID)
if rawResp, err = ud.Session.GET(urlStr); err != nil {
return types.Rotation{}, err
}
reply := new(struct{ Value types.Rotation })
if err = json.Unmarshal(rawResp, reply); err != nil {
return types.Rotation{}, err
}
rotation = reply.Value
return
}
func (ud *UIA2Driver) ScreenShot(opts ...option.ActionOption) (raw *bytes.Buffer, err error) {
// https://bytedance.larkoffice.com/docx/C8qEdmSHnoRvMaxZauocMiYpnLh
// ui2截图受内存影响改为adb截图
return ud.ADBDriver.ScreenShot(opts...)
}
func (ud *UIA2Driver) Source(srcOpt ...option.SourceOption) (source string, err error) {
// register(getHandler, new Source("/wd/hub/session/:sessionId/source"))
var rawResp DriverRawResponse
urlStr := fmt.Sprintf("/session/%s/source", ud.Session.ID)
if rawResp, err = ud.Session.GET(urlStr); err != nil {
return "", err
}
reply := new(struct{ Value string })
if err = json.Unmarshal(rawResp, reply); err != nil {
return "", err
}
source = reply.Value
return
}
func (ud *UIA2Driver) startUIA2Server() error {
const maxRetries = 3
for attempt := 1; attempt <= maxRetries; attempt++ {
log.Info().Str("package", ud.Device.Options.UIA2ServerTestPackageName).
Int("attempt", attempt).Msg("start uiautomator server")
// $ adb shell am instrument -w $UIA2ServerTestPackageName
// -w: wait for instrumentation to finish before returning.
// Required for test runners.
out, err := ud.Device.RunShellCommand("am", "instrument", "-w",
ud.Device.Options.UIA2ServerTestPackageName)
if err != nil {
return errors.Wrap(err, "start uiautomator server failed")
}
if strings.Contains(out, "Process crashed") {
log.Error().Msg("uiautomator server crashed, retrying...")
}
}
return errors.Wrapf(code.MobileUIDriverAppCrashed,
"uiautomator server crashed %d times", maxRetries)
}
func (ud *UIA2Driver) stopUIA2Server() error {
_, err := ud.Device.RunShellCommand("am", "force-stop",
ud.Device.Options.UIA2ServerPackageName)
return err
}