feat: 支持shoots协议,新增UIAgent驱动接口

This commit is contained in:
余泓铮
2024-07-16 20:30:39 +08:00
parent 9cf1809ac5
commit e2a7c29acf
19 changed files with 1149 additions and 76 deletions

View File

@@ -292,6 +292,33 @@ func (d *Device) ReverseForwardKill(remoteInterface interface{}) error {
return err
}
func (d *Device) RunShootsCommand(command []byte, processName string) (res string, err error) {
var tp transport
if tp, err = d.createDeviceTransport(); err != nil {
return "", err
}
defer func() { _ = tp.Close() }()
if err = tp.SendWithCheck(fmt.Sprintf("localabstract:%s", processName)); err != nil {
return "", err
}
if err = tp.SendBytes(command); err != nil {
return "", err
}
lenBuf, err := tp.ReadBytesN(4)
if err != nil {
return "", err
}
length := binary.LittleEndian.Uint32(lenBuf)
result, err := tp.ReadBytesN(int(length) - 4)
if err != nil {
return "", err
}
return string(result), nil
}
func (d *Device) ReverseForwardKillAll() error {
_, err := d.executeCommand("reverse:killforward-all")
return err

View File

@@ -0,0 +1,18 @@
package server
// 常见的错误代码和消息
const (
InternalServerErrorCode = 100001
InternalServerErrorMsg = "Invalid Server Error"
InvalidParamErrorCode = 100002
InvalidParamErrorMsg = "Invalid %s Param"
CodeNotFound = 1004
MsgNotFound = "Resource not found"
)
const (
DeviceNotFoundCode = 110001
DeviceNotFoundMsg = "Device %s Not Found"
)

44
hrp/pkg/server/model.go Normal file
View File

@@ -0,0 +1,44 @@
package server
type HttpResponse struct {
Result interface{} `json:"result,omitempty"`
ErrorCode int `json:"errorCode"`
ErrorMsg string `json:"errorMsg"`
}
type TapRequest struct {
X float64 `json:"x"`
Y float64 `json:"y"`
Duration float64 `json:"duration"`
}
type DragRequest struct {
FromX float64 `json:"from_x"`
FromY float64 `json:"from_y"`
ToX float64 `json:"to_x"`
ToY float64 `json:"to_y"`
Duration float64 `json:"duration"`
}
type InputRequest struct {
Text string `json:"text"`
Frequency int `json:"frequency"` // only iOS
}
type KeycodeRequest struct {
Keycode int `json:"keycode"`
}
type AppLaunchRequest struct {
PackageName string `json:"packageName"`
}
type AppTerminalRequest struct {
PackageName string `json:"packageName"`
}
type LoginRequest struct {
PackageName string `json:"packageName"`
PhoneNumber string `json:"phoneNumber"`
Captcha string `json:"captcha"`
}

514
hrp/pkg/server/server.go Normal file
View File

@@ -0,0 +1,514 @@
package server
import (
"encoding/base64"
"fmt"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v4/hrp/pkg/gadb"
"github.com/httprunner/httprunner/v4/hrp/pkg/uixt"
)
func NewServer(port int) error {
router := gin.Default()
router.GET("/ping", pingHandler)
router.GET("/api/v1/:platform/devices", listDeviceHandler)
router.POST("/api/v1/:platform/:serial/ui/tap", parseDeviceInfo(), tapHandler)
router.POST("/api/v1/:platform/:serial/ui/drag", parseDeviceInfo(), dragHandler)
router.POST("/api/v1/:platform/:serial/ui/input", parseDeviceInfo(), inputHandler)
router.POST("/api/v1/:platform/:serial/key/unlock", parseDeviceInfo(), unlockHandler)
router.POST("/api/v1/:platform/:serial/key/home", parseDeviceInfo(), homeHandler)
router.POST("/api/v1/:platform/:serial/key", parseDeviceInfo(), keycodeHandler)
router.GET("/api/v1/:platform/:serial/app/foreground", parseDeviceInfo(), foregroundAppHandler)
router.POST("/api/v1/:platform/:serial/app/launch", parseDeviceInfo(), launchAppHandler)
router.POST("/api/v1/:platform/:serial/app/terminal", parseDeviceInfo(), terminalAppHandler)
router.GET("/api/v1/:platform/:serial/screenshot", parseDeviceInfo(), screenshotHandler)
router.GET("/api/v1/:platform/:serial/shoots/source", parseDeviceInfo(), sourceHandler)
router.GET("/api/v1/:platform/:serial/adb/source", parseDeviceInfo(), adbSourceHandler)
router.POST("/api/v1/:platform/:serial/shoots/login", parseDeviceInfo(), loginHandler)
err := router.Run(fmt.Sprintf("127.0.0.1:%d", port))
if err != nil {
log.Err(err).Msg("failed to start http server")
return err
}
return nil
}
func pingHandler(c *gin.Context) {
c.JSON(http.StatusOK, HttpResponse{Result: true})
return
}
func listDeviceHandler(c *gin.Context) {
platform := c.Param("platform")
switch strings.ToLower(platform) {
case "android":
{
client, err := gadb.NewClient()
if err != nil {
log.Err(err).Msg("failed to init adb client")
c.JSON(http.StatusInternalServerError, HttpResponse{Result: false, ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg})
c.Abort()
return
}
devices, err := client.DeviceList()
if err != nil && strings.Contains(err.Error(), "no android device found") {
c.JSON(http.StatusOK, HttpResponse{Result: nil})
return
} else if err != nil {
log.Err(err).Msg("failed to list devices")
c.JSON(http.StatusInternalServerError, HttpResponse{Result: false, ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg})
c.Abort()
return
}
var deviceList []interface{}
for _, device := range devices {
brand, err := device.Brand()
if err != nil {
log.Err(err).Msg("failed to get device brand")
c.JSON(http.StatusInternalServerError, HttpResponse{Result: false, ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg})
c.Abort()
return
}
model, err := device.Model()
if err != nil {
log.Err(err).Msg("failed to get device model")
c.JSON(http.StatusInternalServerError, HttpResponse{Result: false, ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg})
c.Abort()
return
}
deviceInfo := map[string]interface{}{
"serial": device.Serial(),
"brand": brand,
"model": model,
"platform": "android",
}
deviceList = append(deviceList, deviceInfo)
}
c.JSON(http.StatusOK, HttpResponse{Result: deviceList})
return
}
default:
{
c.JSON(http.StatusBadRequest, HttpResponse{
ErrorCode: InvalidParamErrorCode,
ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "platform"),
})
c.Abort()
return
}
}
}
func tapHandler(c *gin.Context) {
var tapReq TapRequest
if err := c.ShouldBindJSON(&tapReq); err != nil {
log.Err(err).Msg(fmt.Sprintf("[%s]: Invalid Request", c.HandlerName()))
c.JSON(http.StatusBadRequest, HttpResponse{Result: false, ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "request")})
c.Abort()
return
}
driverObj, exists := c.Get("driver")
if !exists {
log.Error().Msg(fmt.Sprintf("[%s]: Driver Not exsit", c.HandlerName()))
c.JSON(http.StatusBadRequest, HttpResponse{Result: false, ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "driver")})
c.Abort()
return
}
dExt := driverObj.(*uixt.DriverExt)
if tapReq.X < 1 && tapReq.Y < 1 {
err := dExt.TapXY(tapReq.X, tapReq.Y, uixt.WithPressDuration(tapReq.Duration))
if err != nil {
log.Err(err).Msg(fmt.Sprintf("[%s]: failed to tap %f, %f", c.HandlerName(), tapReq.X, tapReq.Y))
c.JSON(http.StatusInternalServerError, HttpResponse{Result: false, ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg})
c.Abort()
return
}
} else {
err := dExt.TapAbsXY(tapReq.X, tapReq.Y, uixt.WithPressDuration(tapReq.Duration))
if err != nil {
log.Err(err).Msg(fmt.Sprintf("[%s]: failed to tap %f, %f", c.HandlerName(), tapReq.X, tapReq.Y))
c.JSON(http.StatusInternalServerError, HttpResponse{Result: false, ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg})
c.Abort()
return
}
}
c.JSON(http.StatusOK, HttpResponse{Result: true})
return
}
func dragHandler(c *gin.Context) {
var dragReq DragRequest
if err := c.ShouldBindJSON(&dragReq); err != nil {
log.Err(err).Msg(fmt.Sprintf("[%s]: Invalid Request", c.HandlerName()))
c.JSON(http.StatusBadRequest, HttpResponse{Result: false, ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "request")})
c.Abort()
return
}
driverObj, exists := c.Get("driver")
if !exists {
log.Error().Msg(fmt.Sprintf("[%s]: Driver Not exsit", c.HandlerName()))
c.JSON(http.StatusBadRequest, HttpResponse{Result: false, ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "driver")})
c.Abort()
return
}
dExt := driverObj.(*uixt.DriverExt)
if dragReq.FromX < 1 && dragReq.FromY < 1 && dragReq.ToX < 1 && dragReq.ToY < 1 {
err := dExt.SwipeRelative(dragReq.FromX, dragReq.FromY, dragReq.ToX, dragReq.ToY, uixt.WithPressDuration(dragReq.Duration))
if err != nil {
log.Err(err).Msg(fmt.Sprintf("[%s]: failed to drag from %f, %f to %f, %f", c.HandlerName(), dragReq.FromX, dragReq.FromY, dragReq.ToX, dragReq.ToY))
c.JSON(http.StatusInternalServerError, HttpResponse{Result: false, ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg})
c.Abort()
return
}
} else {
err := dExt.Driver.SwipeFloat(dragReq.FromX, dragReq.FromY, dragReq.ToX, dragReq.ToY, uixt.WithPressDuration(dragReq.Duration))
if err != nil {
log.Err(err).Msg(fmt.Sprintf("[%s]: failed to drag from %f, %f to %f, %f", c.HandlerName(), dragReq.FromX, dragReq.FromY, dragReq.ToX, dragReq.ToY))
c.JSON(http.StatusInternalServerError, HttpResponse{Result: false, ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg})
c.Abort()
return
}
}
c.JSON(http.StatusOK, HttpResponse{Result: true})
return
}
func inputHandler(c *gin.Context) {
var inputReq InputRequest
if err := c.ShouldBindJSON(&inputReq); err != nil {
log.Err(err).Msg(fmt.Sprintf("[%s]: Invalid Request", c.HandlerName()))
c.JSON(http.StatusBadRequest, HttpResponse{Result: false, ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "request")})
c.Abort()
return
}
driverObj, exists := c.Get("driver")
if !exists {
log.Error().Msg(fmt.Sprintf("[%s]: Driver Not exsit", c.HandlerName()))
c.JSON(http.StatusBadRequest, HttpResponse{Result: false, ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "driver")})
c.Abort()
return
}
dExt := driverObj.(*uixt.DriverExt)
err := dExt.Driver.SendKeys(inputReq.Text, uixt.WithFrequency(inputReq.Frequency))
if err != nil {
log.Err(err).Msg(fmt.Sprintf("[%s]: failed to input text %s", c.HandlerName(), inputReq.Text))
c.JSON(http.StatusInternalServerError, HttpResponse{Result: false, ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg})
c.Abort()
return
}
c.JSON(http.StatusOK, HttpResponse{Result: true})
return
}
func unlockHandler(c *gin.Context) {
driverObj, exists := c.Get("driver")
if !exists {
log.Error().Msg(fmt.Sprintf("[%s]: Driver Not exsit", c.HandlerName()))
c.JSON(http.StatusBadRequest, HttpResponse{Result: false, ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "driver")})
c.Abort()
return
}
dExt := driverObj.(*uixt.DriverExt)
err := dExt.Driver.Unlock()
if err != nil {
log.Err(err).Msg(fmt.Sprintf("[%s]: failed to unlick screen", c.HandlerName()))
c.JSON(http.StatusInternalServerError, HttpResponse{Result: false, ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg})
c.Abort()
return
}
c.JSON(http.StatusOK, HttpResponse{Result: true})
return
}
func homeHandler(c *gin.Context) {
driverObj, exists := c.Get("driver")
if !exists {
log.Error().Msg(fmt.Sprintf("[%s]: Driver Not exsit", c.HandlerName()))
c.JSON(http.StatusBadRequest, HttpResponse{Result: false, ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "driver")})
c.Abort()
return
}
dExt := driverObj.(*uixt.DriverExt)
err := dExt.Driver.Homescreen()
if err != nil {
log.Err(err).Msg(fmt.Sprintf("[%s]: failed to enter homescreen", c.HandlerName()))
c.JSON(http.StatusInternalServerError, HttpResponse{Result: false, ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg})
c.Abort()
return
}
c.JSON(http.StatusOK, HttpResponse{Result: true})
return
}
func keycodeHandler(c *gin.Context) {
var keycodeReq KeycodeRequest
if err := c.ShouldBindJSON(&keycodeReq); err != nil {
log.Err(err).Msg(fmt.Sprintf("[%s]: Invalid Request", c.HandlerName()))
c.JSON(http.StatusBadRequest, HttpResponse{Result: false, ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "request")})
c.Abort()
return
}
driverObj, exists := c.Get("driver")
if !exists {
log.Error().Msg(fmt.Sprintf("[%s]: Driver Not exsit", c.HandlerName()))
c.JSON(http.StatusBadRequest, HttpResponse{Result: false, ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "driver")})
c.Abort()
return
}
dExt := driverObj.(*uixt.DriverExt)
err := dExt.Driver.PressKeyCode(uixt.KeyCode(keycodeReq.Keycode))
if err != nil {
log.Err(err).Msg(fmt.Sprintf("[%s]: failed to input keycode %d", c.HandlerName(), keycodeReq.Keycode))
c.JSON(http.StatusInternalServerError, HttpResponse{Result: false, ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg})
c.Abort()
return
}
c.JSON(http.StatusOK, HttpResponse{Result: true})
return
}
func foregroundAppHandler(c *gin.Context) {
driverObj, exists := c.Get("driver")
if !exists {
log.Error().Msg(fmt.Sprintf("[%s]: Driver Not exsit", c.HandlerName()))
c.JSON(http.StatusBadRequest, HttpResponse{Result: false, ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "driver")})
c.Abort()
return
}
dExt := driverObj.(*uixt.DriverExt)
appInfo, err := dExt.Driver.GetForegroundApp()
if err != nil {
log.Err(err).Msg(fmt.Sprintf("[%s]: failed to unlick screen", c.HandlerName()))
c.JSON(http.StatusInternalServerError, HttpResponse{Result: false, ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg})
c.Abort()
return
}
c.JSON(http.StatusOK, HttpResponse{Result: appInfo})
return
}
func launchAppHandler(c *gin.Context) {
var appLaunchReq AppLaunchRequest
if err := c.ShouldBindJSON(&appLaunchReq); err != nil {
log.Err(err).Msg(fmt.Sprintf("[%s]: Invalid Request", c.HandlerName()))
c.JSON(http.StatusBadRequest, HttpResponse{Result: "", ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "request")})
c.Abort()
return
}
driverObj, exists := c.Get("driver")
if !exists {
log.Error().Msg(fmt.Sprintf("[%s]: Driver Not exsit", c.HandlerName()))
c.JSON(http.StatusBadRequest, HttpResponse{Result: "", ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "driver")})
c.Abort()
return
}
dExt := driverObj.(*uixt.DriverExt)
err := dExt.Driver.AppLaunch(appLaunchReq.PackageName)
if err != nil {
log.Err(err).Msg(fmt.Sprintf("[%s]: failed to launch app %s", c.HandlerName(), appLaunchReq.PackageName))
c.JSON(http.StatusInternalServerError, HttpResponse{Result: "", ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg})
c.Abort()
return
}
c.JSON(http.StatusOK, HttpResponse{Result: true})
return
}
func terminalAppHandler(c *gin.Context) {
var appTerminalReq AppTerminalRequest
if err := c.ShouldBindJSON(&appTerminalReq); err != nil {
log.Err(err).Msg(fmt.Sprintf("[%s]: Invalid Request", c.HandlerName()))
c.JSON(http.StatusBadRequest, HttpResponse{Result: "", ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "request")})
c.Abort()
return
}
driverObj, exists := c.Get("driver")
if !exists {
log.Error().Msg(fmt.Sprintf("[%s]: Driver Not exsit", c.HandlerName()))
c.JSON(http.StatusBadRequest, HttpResponse{Result: "", ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "driver")})
c.Abort()
return
}
dExt := driverObj.(*uixt.DriverExt)
success, err := dExt.Driver.AppTerminate(appTerminalReq.PackageName)
if !success {
log.Err(err).Msg(fmt.Sprintf("[%s]: failed to launch app %s", c.HandlerName(), appTerminalReq.PackageName))
c.JSON(http.StatusInternalServerError, HttpResponse{Result: "", ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg})
c.Abort()
return
}
c.JSON(http.StatusOK, HttpResponse{Result: true})
return
}
func screenshotHandler(c *gin.Context) {
driverObj, exists := c.Get("driver")
if !exists {
log.Error().Msg(fmt.Sprintf("[%s]: Driver Not exsit", c.HandlerName()))
c.JSON(http.StatusBadRequest, HttpResponse{Result: "", ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "driver")})
c.Abort()
return
}
dExt := driverObj.(*uixt.DriverExt)
raw, err := dExt.Driver.Screenshot()
if err != nil {
log.Err(err).Msg(fmt.Sprintf("[%s]: failed to get screenshot", c.HandlerName()))
c.JSON(http.StatusInternalServerError, HttpResponse{Result: "", ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg})
c.Abort()
return
}
c.JSON(http.StatusOK, HttpResponse{Result: base64.StdEncoding.EncodeToString(raw.Bytes())})
return
}
func sourceHandler(c *gin.Context) {
driverObj, exists := c.Get("driver")
if !exists {
log.Error().Msg(fmt.Sprintf("[%s]: Driver Not exsit", c.HandlerName()))
c.JSON(http.StatusBadRequest, HttpResponse{Result: "", ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "driver")})
c.Abort()
return
}
dExt := driverObj.(*uixt.DriverExt)
app, err := dExt.Driver.GetForegroundApp()
if err != nil {
log.Err(err).Msg(fmt.Sprintf("[%s]: failed to get foreground app", c.HandlerName()))
c.JSON(http.StatusInternalServerError, HttpResponse{Result: "", ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg})
c.Abort()
return
}
source, err := dExt.Driver.Source(uixt.NewSourceOption().WithProcessName(app.PackageName))
if err != nil {
log.Err(err).Msg(fmt.Sprintf("[%s]: failed to get source %s", c.HandlerName(), app.PackageName))
c.JSON(http.StatusInternalServerError, HttpResponse{Result: "", ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg})
c.Abort()
return
}
c.JSON(http.StatusOK, HttpResponse{Result: source})
return
}
func adbSourceHandler(c *gin.Context) {
deviceObj, exists := c.Get("device")
if !exists {
log.Error().Msg(fmt.Sprintf("[%s]: Driver Not exsit", c.HandlerName()))
c.JSON(http.StatusBadRequest, HttpResponse{Result: "", ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "device")})
c.Abort()
return
}
device := deviceObj.(*uixt.AndroidDevice)
driver, err := device.NewAdbDriver()
if err != nil {
log.Err(err).Msg(fmt.Sprintf("[%s]: failed to new adb driver", c.HandlerName()))
c.JSON(http.StatusInternalServerError, HttpResponse{Result: "", ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg})
c.Abort()
return
}
source, err := driver.Source()
if err != nil {
log.Err(err).Msg(fmt.Sprintf("[%s]: failed to get adb source", c.HandlerName()))
c.JSON(http.StatusInternalServerError, HttpResponse{Result: "", ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg})
c.Abort()
return
}
c.JSON(http.StatusOK, HttpResponse{Result: source})
return
}
func loginHandler(c *gin.Context) {
var loginReq LoginRequest
if err := c.ShouldBindJSON(&loginReq); err != nil {
log.Err(err).Msg(fmt.Sprintf("[%s]: Invalid Request", c.HandlerName()))
c.JSON(http.StatusBadRequest, HttpResponse{Result: "", ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "request")})
c.Abort()
return
}
driverObj, exists := c.Get("driver")
if !exists {
log.Error().Msg(fmt.Sprintf("[%s]: Driver Not exsit", c.HandlerName()))
c.JSON(http.StatusBadRequest, HttpResponse{Result: "", ErrorCode: InvalidParamErrorCode, ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "driver")})
c.Abort()
return
}
dExt := driverObj.(*uixt.DriverExt)
err := dExt.Driver.LoginNoneUI(loginReq.PackageName, loginReq.PhoneNumber, loginReq.Captcha)
if err != nil {
log.Err(err).Msg(fmt.Sprintf("[%s]: failed to get foreground app", c.HandlerName()))
c.JSON(http.StatusInternalServerError, HttpResponse{Result: "", ErrorCode: InternalServerErrorCode, ErrorMsg: InternalServerErrorMsg})
c.Abort()
return
}
c.JSON(http.StatusOK, HttpResponse{Result: true})
return
}
func parseDeviceInfo() gin.HandlerFunc {
return func(c *gin.Context) {
platform := c.Param("platform")
switch strings.ToLower(platform) {
case "android":
serial := c.Param("serial")
if serial == "" {
log.Error().Str("platform", platform).Msg(fmt.Sprintf("[%s]: serial is empty", c.HandlerName()))
c.JSON(http.StatusBadRequest, HttpResponse{
ErrorCode: InvalidParamErrorCode,
ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "serial"),
})
c.Abort()
return
}
device, err := uixt.NewAndroidDevice(uixt.WithSerialNumber(serial), uixt.WithShoots(true))
if err != nil {
log.Error().Err(err).Str("platform", platform).Str("serial", serial).Msg(fmt.Sprintf("[%s]: Device Not Found", c.HandlerName()))
c.JSON(http.StatusBadRequest, HttpResponse{
ErrorCode: DeviceNotFoundCode,
ErrorMsg: fmt.Sprintf(DeviceNotFoundMsg, serial),
})
c.Abort()
return
}
c.Set("device", device)
driver, err := device.NewDriver(uixt.WithDriverImageService(false), uixt.WithDriverResultFolder(false))
if err != nil {
log.Error().Err(err).Str("platform", platform).Str("serial", serial).Msg(fmt.Sprintf("[%s]: Failed New Driver", c.HandlerName()))
c.JSON(http.StatusInternalServerError, HttpResponse{
ErrorCode: InternalServerErrorCode,
ErrorMsg: err.Error(),
})
c.Abort()
return
}
c.Set("driver", driver)
default:
c.JSON(http.StatusBadRequest, HttpResponse{
ErrorCode: InvalidParamErrorCode,
ErrorMsg: fmt.Sprintf(InvalidParamErrorMsg, "platform"),
})
c.Abort()
return
}
c.Next()
}
}

View File

@@ -29,6 +29,7 @@ const (
ACTION_SetClipboard ActionMethod = "set_clipboard"
ACTION_GetClipboard ActionMethod = "get_clipboard"
ACTION_SetIme ActionMethod = "set_ime"
ACTION_GetSource ActionMethod = "get_source"
// UI validation
// selectors
@@ -616,6 +617,15 @@ func (dExt *DriverExt) DoAction(action MobileAction) (err error) {
}
return nil
}
case ACTION_GetSource:
if packageName, ok := action.Params.(string); ok {
source := NewSourceOption().WithProcessName(packageName)
_, err = dExt.Driver.Source(source)
if err != nil {
return errors.Wrap(err, "failed to set ime")
}
return nil
}
case ACTION_TapXY:
if location, ok := action.Params.([]interface{}); ok {
// relative x,y of window size: [0.5, 0.5]

View File

@@ -3,6 +3,7 @@ package uixt
import (
"bufio"
"bytes"
"encoding/json"
"encoding/xml"
"fmt"
"io/fs"
@@ -218,10 +219,18 @@ func (ad *adbDriver) Orientation() (orientation Orientation, err error) {
}
func (ad *adbDriver) Homescreen() (err error) {
return ad.PressKeyCode(KCHome, KMEmpty)
return ad.PressKeyCodes(KCHome, KMEmpty)
}
func (ad *adbDriver) PressKeyCode(keyCode KeyCode, metaState KeyMeta) (err error) {
func (ad *adbDriver) Unlock() (err error) {
return ad.PressKeyCodes(KCMenu, KMEmpty)
}
func (ad *adbDriver) PressKeyCode(keyCode KeyCode) (err error) {
return ad.PressKeyCodes(keyCode, KMEmpty)
}
func (ad *adbDriver) PressKeyCodes(keyCode KeyCode, metaState KeyMeta) (err error) {
// adb shell input keyevent <keyCode>
_, err = ad.adbClient.RunShellCommand(
"input", "keyevent", fmt.Sprintf("%d", keyCode))
@@ -507,6 +516,10 @@ func (ad *adbDriver) Source(srcOpt ...SourceOption) (source string, err error) {
return
}
func (ad *adbDriver) LoginNoneUI(packageName, phoneNumber string, captcha string) error {
return errDriverNotImplemented
}
func (ad *adbDriver) sourceTree(srcOpt ...SourceOption) (sourceTree *Hierarchy, err error) {
source, err := ad.Source()
if err != nil {
@@ -693,51 +706,21 @@ func (ad *adbDriver) GetDriverResults() []*DriverResult {
}
func (ad *adbDriver) GetForegroundApp() (app AppInfo, err error) {
// adb shell dumpsys activity activities
output, err := ad.adbClient.RunShellCommand("dumpsys", "activity", "activities")
packageInfo, err := ad.adbClient.RunShellCommand("CLASSPATH=/data/local/tmp/eval_tool", "app_process", "/", "com.bytedance.iesqa.eval_process.PackageService")
if err != nil {
log.Error().Err(err).Msg("failed to dumpsys activities")
return AppInfo{}, errors.Wrap(err, "dumpsys activities failed")
return app, err
}
lines := strings.Split(string(output), "\n")
for _, line := range lines {
trimmedLine := strings.TrimSpace(line)
// grep mResumedActivity|ResumedActivity
if strings.HasPrefix(trimmedLine, "mResumedActivity:") || strings.HasPrefix(trimmedLine, "ResumedActivity:") {
// mResumedActivity: ActivityRecord{9656d74 u0 com.android.settings/.Settings t407}
// ResumedActivity: ActivityRecord{8265c25 u0 com.android.settings/.Settings t73}
strs := strings.Split(trimmedLine, " ")
for _, str := range strs {
if strings.Contains(str, "/") {
// com.android.settings/.Settings
s := strings.Split(str, "/")
app := AppInfo{
AppBaseInfo: AppBaseInfo{
PackageName: s[0],
Activity: s[1],
},
}
return app, nil
}
}
}
}
return AppInfo{}, errors.Wrap(code.MobileUIAssertForegroundAppError, "get foreground app failed")
log.Info().Msg(packageInfo)
err = json.Unmarshal([]byte(strings.TrimSpace(packageInfo)), &app)
return
}
func (ad *adbDriver) GetFocusedPackage() (packageName string, err error) {
res, err := ad.adbClient.RunShellCommand("dumpsys", "window", "windows", "|", "grep", "-E", "'mCurrentFocus|mFocusedApp'")
res, err := ad.adbClient.RunShellCommand("dumpsys", "activity", "activities", "|", "grep", "-E", "'mResumedActivity'")
if err != nil {
return "", err
}
match := regexp.MustCompile("mCurrentFocus.+\\s([^\\s/}]+)/[^\\s/}]+(\\.[^\\s/}]+)}").FindStringSubmatch(res)
if len(match) > 1 {
packageName = match[1]
return
}
match = regexp.MustCompile("mFocusedApp.+Record\\{.*\\s([^\\s/}]+)/([^\\s/}]+)(\\s[^\\s/}]+)*}").FindStringSubmatch(res)
match := regexp.MustCompile(`mResumedActivity:.*? (\S+)/`).FindStringSubmatch(res)
if len(match) > 1 {
packageName = match[1]
return
@@ -778,7 +761,7 @@ func (ad *adbDriver) SetIme(imeRegx string) error {
currentPackage, err := ad.GetFocusedPackage()
log.Info().Str("beforeFocusedPackage", focusedPackage).Str("afterFocusedPackage", currentPackage).Msg("")
if err == nil && currentPackage != focusedPackage {
_ = ad.PressKeyCode(KCBack, KMEmpty)
_ = ad.PressKeyCodes(KCBack, KMEmpty)
}
}
}

View File

@@ -4,9 +4,11 @@ import (
"bufio"
"bytes"
"context"
"embed"
"fmt"
"os/exec"
"strings"
"time"
"github.com/httprunner/funplugin/myexec"
"github.com/pkg/errors"
@@ -25,6 +27,9 @@ var (
UIA2ServerPort = 6790
)
//go:embed eval_tool
var evalTool embed.FS
const forwardToPrefix = "forward-to-"
type AndroidDeviceOption func(*AndroidDevice)
@@ -41,6 +46,12 @@ func WithUIA2(uia2On bool) AndroidDeviceOption {
}
}
func WithShoots(shootsOn bool) AndroidDeviceOption {
return func(device *AndroidDevice) {
device.SHOOTS = shootsOn
}
}
func WithUIA2IP(ip string) AndroidDeviceOption {
return func(device *AndroidDevice) {
device.UIA2IP = ip
@@ -109,7 +120,14 @@ func NewAndroidDevice(options ...AndroidDeviceOption) (device *AndroidDevice, er
device.d = dev
device.logcat = NewAdbLogcat(device.SerialNumber)
evalToolRaw, err := evalTool.ReadFile("eval_tool")
if err != nil {
return nil, errors.Wrap(code.LoadFileError, err.Error())
}
err = dev.Push(bytes.NewReader(evalToolRaw), "/data/local/tmp/eval_tool", time.Now())
if err != nil {
return nil, errors.Wrap(code.AndroidShellExecError, err.Error())
}
log.Info().Str("serial", device.SerialNumber).Msg("init android device")
return device, nil
}
@@ -152,6 +170,7 @@ type AndroidDevice struct {
logcat *AdbLogcat
SerialNumber string `json:"serial,omitempty" yaml:"serial,omitempty"`
UIA2 bool `json:"uia2,omitempty" yaml:"uia2,omitempty"` // use uiautomator2
SHOOTS bool `json:"shoots,omitempty" yaml:"uia2,omitempty"` // use uiautomator2
UIA2IP string `json:"uia2_ip,omitempty" yaml:"uia2_ip,omitempty"` // uiautomator2 server ip
UIA2Port int `json:"uia2_port,omitempty" yaml:"uia2_port,omitempty"` // uiautomator2 server port
LogOn bool `json:"log_on,omitempty" yaml:"log_on,omitempty"`
@@ -174,7 +193,7 @@ func (dev *AndroidDevice) LogEnabled() bool {
}
func (dev *AndroidDevice) NewDriver(options ...DriverOption) (driverExt *DriverExt, err error) {
driverOptions := &DriverOptions{}
driverOptions := NewDriverOptions()
for _, option := range options {
option(driverOptions)
}
@@ -182,6 +201,8 @@ func (dev *AndroidDevice) NewDriver(options ...DriverOption) (driverExt *DriverE
var driver WebDriver
if dev.UIA2 || dev.LogOn {
driver, err = dev.NewUSBDriver(driverOptions.capabilities)
} else if dev.SHOOTS {
driver, err = dev.NewShootsDriver(driverOptions.capabilities)
} else {
driver, err = dev.NewAdbDriver()
}
@@ -189,7 +210,7 @@ func (dev *AndroidDevice) NewDriver(options ...DriverOption) (driverExt *DriverE
return nil, errors.Wrap(err, "failed to init UIA driver")
}
driverExt, err = newDriverExt(dev, driver, driverOptions.plugin)
driverExt, err = newDriverExt(dev, driver, options...)
if err != nil {
return nil, err
}
@@ -226,6 +247,25 @@ func (dev *AndroidDevice) NewUSBDriver(capabilities Capabilities) (driver WebDri
return uiaDriver, nil
}
func (dev *AndroidDevice) NewShootsDriver(capabilities Capabilities) (driver *ShootsAndroidDriver, err error) {
localPort, err := dev.d.Forward(ShootsSocketName)
if err != nil {
return nil, errors.Wrap(code.AndroidDeviceConnectionError,
fmt.Sprintf("forward port %d->%s failed: %v",
localPort, ShootsSocketName, err))
}
shootsDriver, err := newShootsAndroidDriver(fmt.Sprintf("127.0.0.1:%d", localPort))
if err != nil {
_ = dev.d.ForwardKill(localPort)
return nil, errors.Wrap(code.AndroidDeviceConnectionError, err.Error())
}
shootsDriver.adbClient = dev.d
shootsDriver.logcat = dev.logcat
return shootsDriver, nil
}
// NewHTTPDriver creates new remote HTTP client, this will also start a new session.
func (dev *AndroidDevice) NewHTTPDriver(capabilities Capabilities) (driver WebDriver, err error) {
rawURL := fmt.Sprintf("http://%s:%d/wd/hub", dev.UIA2IP, dev.UIA2Port)

View File

@@ -0,0 +1,235 @@
package uixt
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"net"
"time"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v4/hrp/internal/json"
)
type ShootsAndroidDriver struct {
socket net.Conn
seq int
timeout time.Duration
adbDriver
}
const ShootsSocketName = "com.bytest.device"
// newShootsAndroidDriver
// 创建shoots Driver address为forward后的端口格式127.0.0.1:${port}
func newShootsAndroidDriver(address string, readTimeout ...time.Duration) (*ShootsAndroidDriver, error) {
timeout := 10 * time.Second
if len(readTimeout) > 0 {
timeout = readTimeout[0]
}
conn, err := net.Dial("tcp", address)
if err != nil {
log.Err(err).Msg(fmt.Sprintf("failed to connect %s", address))
return nil, err
}
return &ShootsAndroidDriver{
socket: conn,
timeout: timeout,
}, nil
}
func (sad *ShootsAndroidDriver) NewSession(capabilities Capabilities) (SessionInfo, error) {
return SessionInfo{}, errDriverNotImplemented
}
func (sad *ShootsAndroidDriver) sendCommand(packageName string, cmdType string, params map[string]interface{}, readTimeout ...time.Duration) (interface{}, error) {
sad.seq++
packet := map[string]interface{}{
"Seq": sad.seq,
"Cmd": cmdType,
"v": "",
}
for key, value := range params {
if key == "Cmd" || key == "Seq" {
return "", errors.New("params cannot be Cmd or Seq")
}
packet[key] = value
}
data, err := json.Marshal(packet)
if err != nil {
return nil, err
}
res, err := sad.adbClient.RunShootsCommand(append(data, '\n'), packageName)
if err != nil {
return nil, err
}
var resultMap map[string]interface{}
if err := json.Unmarshal([]byte(res), &resultMap); err != nil {
return nil, err
}
if resultMap["Error"] != nil {
return nil, fmt.Errorf("failed to call shoots command: %s", resultMap["Error"].(string))
}
return resultMap["Result"], nil
}
func (sad *ShootsAndroidDriver) send(data []byte, readTimeout ...time.Duration) (map[string]interface{}, error) {
timeout := sad.timeout
if len(readTimeout) > 0 {
timeout = readTimeout[0]
}
_ = sad.socket.SetReadDeadline(time.Now().Add(timeout))
err := _send(sad.socket, append(data, '\n'))
if err != nil {
sad.close()
return nil, err
}
raw, err := _readAll(sad.socket)
if err != nil {
return nil, err
}
var result map[string]interface{}
if err := json.Unmarshal(raw, &result); err != nil {
log.Printf("error when parse json response: %s\n", raw)
return nil, err
}
return result, nil
}
func _send(writer io.Writer, msg []byte) (err error) {
for totalSent := 0; totalSent < len(msg); {
var sent int
if sent, err = writer.Write(msg[totalSent:]); err != nil {
return err
}
if sent == 0 {
return errors.New("socket connection broken")
}
totalSent += sent
}
return
}
func _readN(reader io.Reader, size int) (raw []byte, err error) {
raw = make([]byte, 0, size)
for len(raw) < size {
buf := make([]byte, size-len(raw))
var n int
if n, err = io.ReadFull(reader, buf); err != nil {
return nil, err
}
if n == 0 {
return nil, errors.New("socket connection broken")
}
raw = append(raw, buf...)
}
return
}
func _readAll(reader io.Reader) (raw []byte, err error) {
buffer := new(bytes.Buffer)
for true {
lengthBuf := make([]byte, 4)
_, err := io.ReadFull(reader, lengthBuf)
if err != nil {
if err == io.EOF {
return buffer.Bytes(), nil
} else if errors.Is(err, io.ErrUnexpectedEOF) {
err = fmt.Errorf("reached unexpected EOF, read partial data: %s %v", string(buffer.Bytes()), err)
return nil, err
} else {
return nil, err
}
}
length := binary.BigEndian.Uint32(lengthBuf)
data, err := _readN(reader, int(length)-4)
if err != nil {
return nil, err
}
buffer.Write(data)
}
return buffer.Bytes(), nil
}
func (sad *ShootsAndroidDriver) DeleteSession() error {
return sad.close()
}
func (sad *ShootsAndroidDriver) close() error {
if sad.socket != nil {
return sad.socket.Close()
}
return nil
}
func (sad *ShootsAndroidDriver) Status() (DeviceStatus, error) {
app, err := sad.GetForegroundApp()
res, err := sad.sendCommand(app.PackageName, "Hello", nil)
if err != nil {
return DeviceStatus{}, err
}
log.Info().Msg(fmt.Sprintf("pint shoots result :%v", res))
return DeviceStatus{}, nil
}
func (sad *ShootsAndroidDriver) Source(srcOpt ...SourceOption) (source string, err error) {
app, err := sad.GetForegroundApp()
params := map[string]interface{}{
"ClassName": "com.bytedance.byteinsight.MockOperator",
"Method": "getLayout",
"RetType": "",
"Args": []string{},
}
res, err := sad.sendCommand(app.PackageName, "CallStaticMethod", params)
if err != nil {
return "", err
}
return res.(string), nil
}
func (sad *ShootsAndroidDriver) LoginNoneUI(packageName, phoneNumber string, captcha string) error {
_, err := sad.adbClient.RunShellCommand("am", "broadcast", "-a", fmt.Sprintf("%s.util.crony.action_login", packageName), "-e", "phone", phoneNumber, "-e", "code", captcha)
time.Sleep(5 * time.Second)
login, err := sad.isLogin(packageName)
if err != nil || !login {
log.Err(err).Msg("failed to login")
return fmt.Errorf("failed to login")
}
return err
}
func (sad *ShootsAndroidDriver) isLogin(packageName string) (login bool, err error) {
params := map[string]interface{}{
"ClassName": "com.ss.android.ugc.aweme.account.AccountProxyService",
"Method": "userService",
"RetType": "",
"Args": []string{},
"CacheObject": true,
}
id, err := sad.sendCommand(packageName, "CallStaticMethod", params)
if err != nil {
return false, err
}
params = map[string]interface{}{
"Method": "isLogin",
"RetType": "",
"Args": []string{},
"ObjectId": int(id.(float64)),
}
loginObj, err := sad.sendCommand(packageName, "CallMethod", params)
if err != nil {
return false, err
}
return loginObj.(bool), nil
}

View File

@@ -0,0 +1,40 @@
package uixt
import "testing"
var driver *ShootsAndroidDriver
func setupAndroid(t *testing.T) {
device, err := NewAndroidDevice()
checkErr(t, err)
device.SHOOTS = true
driver, err = device.NewShootsDriver(Capabilities{})
checkErr(t, err)
}
func TestHello(t *testing.T) {
setupAndroid(t)
status, err := driver.Status()
if err != nil {
t.Fatal(err)
}
t.Log(status)
}
func TestSource(t *testing.T) {
setupAndroid(t)
source, err := driver.Source()
if err != nil {
t.Fatal(err)
}
t.Log(source)
}
func TestLogin(t *testing.T) {
setupAndroid(t)
res, err := driver.isLogin("com.ss.android.ugc.aweme")
if err != nil {
t.Fatal(err)
}
t.Log(res)
}

View File

@@ -250,10 +250,14 @@ func (ud *uiaDriver) PressBack(options ...ActionOption) (err error) {
}
func (ud *uiaDriver) Homescreen() (err error) {
return ud.PressKeyCode(KCHome, KMEmpty)
return ud.PressKeyCodes(KCHome, KMEmpty)
}
func (ud *uiaDriver) PressKeyCode(keyCode KeyCode, metaState KeyMeta, flags ...KeyFlag) (err error) {
func (ud *uiaDriver) PressKeyCode(keyCode KeyCode) (err error) {
return ud.PressKeyCodes(keyCode, KMEmpty)
}
func (ud *uiaDriver) 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,

BIN
hrp/pkg/uixt/eval_tool Normal file

Binary file not shown.

View File

@@ -155,11 +155,16 @@ type DriverExt struct {
plugin funplugin.IPlugin
}
func newDriverExt(device Device, driver WebDriver, plugin funplugin.IPlugin) (dExt *DriverExt, err error) {
func newDriverExt(device Device, driver WebDriver, options ...DriverOption) (dExt *DriverExt, err error) {
driverOptions := &DriverOptions{}
for _, option := range options {
option(driverOptions)
}
dExt = &DriverExt{
Device: device,
Driver: driver,
plugin: plugin,
plugin: driverOptions.plugin,
cacheStepData: cacheStepData{},
interruptSignal: make(chan os.Signal, 1),
}
@@ -179,17 +184,19 @@ func newDriverExt(device Device, driver WebDriver, plugin funplugin.IPlugin) (dE
if err != nil {
return nil, errors.Wrap(err, "get screen resolution failed")
}
if dExt.ImageService, err = newVEDEMImageService(); err != nil {
return nil, err
if driverOptions.withImageService {
if dExt.ImageService, err = newVEDEMImageService(); err != nil {
return nil, err
}
}
// create results directory
if err = builtin.EnsureFolderExists(env.ResultsPath); err != nil {
return nil, errors.Wrap(err, "create results directory failed")
}
if err = builtin.EnsureFolderExists(env.ScreenShotsPath); err != nil {
return nil, errors.Wrap(err, "create screenshots directory failed")
if driverOptions.withResultFolder {
// create results directory
if err = builtin.EnsureFolderExists(env.ResultsPath); err != nil {
return nil, errors.Wrap(err, "create results directory failed")
}
if err = builtin.EnsureFolderExists(env.ScreenShotsPath); err != nil {
return nil, errors.Wrap(err, "create screenshots directory failed")
}
}
return dExt, nil
}

View File

@@ -252,11 +252,7 @@ type Screen struct {
}
type AppInfo struct {
ProcessArguments struct {
Env interface{} `json:"env"`
Args []interface{} `json:"args"`
} `json:"processArguments"`
Name string `json:"name"`
Name string `json:"name,omitempty"`
AppBaseInfo
}
@@ -266,6 +262,10 @@ type AppBaseInfo struct {
ViewController string `json:"viewController,omitempty"` // ios view controller
PackageName string `json:"packageName,omitempty"` // android package name
Activity string `json:"activity,omitempty"` // android activity
VersionName string `json:"versionName,omitempty"`
VersionCode int `json:"versionCode,omitempty"`
AppName string `json:"appName,omitempty"`
// AppIcon string `json:"appIcon,omitempty"`
}
type AppState int
@@ -376,6 +376,11 @@ func (opt SourceOption) WithFormatAsJson() SourceOption {
return opt
}
func (opt SourceOption) WithProcessName(processName string) SourceOption {
opt["processName"] = processName
return opt
}
// WithFormatAsXml Application elements tree in form of xml string
func (opt SourceOption) WithFormatAsXml() SourceOption {
opt["format"] = "xml"
@@ -448,8 +453,17 @@ type Rect struct {
}
type DriverOptions struct {
capabilities Capabilities
plugin funplugin.IPlugin
capabilities Capabilities
plugin funplugin.IPlugin
withImageService bool
withResultFolder bool
}
func NewDriverOptions() *DriverOptions {
return &DriverOptions{
withImageService: true,
withResultFolder: true,
}
}
type DriverOption func(*DriverOptions)
@@ -460,6 +474,18 @@ func WithDriverCapabilities(capabilities Capabilities) DriverOption {
}
}
func WithDriverImageService(withImageService bool) DriverOption {
return func(options *DriverOptions) {
options.withImageService = withImageService
}
}
func WithDriverResultFolder(withResultFolder bool) DriverOption {
return func(options *DriverOptions) {
options.withResultFolder = withResultFolder
}
}
func WithDriverPlugin(plugin funplugin.IPlugin) DriverOption {
return func(options *DriverOptions) {
options.plugin = plugin
@@ -526,6 +552,8 @@ type WebDriver interface {
// Homescreen Forces the device under test to switch to the home screen
Homescreen() error
Unlock() (err error)
// AppLaunch Launch an application with given bundle identifier in scope of current session.
// !This method is only available since Xcode9 SDK
AppLaunch(packageName string) error
@@ -588,11 +616,15 @@ type WebDriver interface {
// PressBack Presses the back button
PressBack(options ...ActionOption) error
PressKeyCode(keyCode KeyCode) (err error)
Screenshot() (*bytes.Buffer, error)
// Source Return application elements tree
Source(srcOpt ...SourceOption) (string, error)
LoginNoneUI(packageName, phoneNumber string, captcha string) error
TapByText(text string, options ...ActionOption) error
TapByTexts(actions ...TapTextAction) error

View File

@@ -308,7 +308,7 @@ func (dev *IOSDevice) LogEnabled() bool {
}
func (dev *IOSDevice) NewDriver(options ...DriverOption) (driverExt *DriverExt, err error) {
driverOptions := &DriverOptions{}
driverOptions := NewDriverOptions()
for _, option := range options {
option(driverOptions)
}
@@ -339,7 +339,7 @@ func (dev *IOSDevice) NewDriver(options ...DriverOption) (driverExt *DriverExt,
}
}
driverExt, err = newDriverExt(dev, driver, driverOptions.plugin)
driverExt, err = newDriverExt(dev, driver, options...)
if err != nil {
return nil, err
}

View File

@@ -595,6 +595,10 @@ func (wd *wdaDriver) SetIme(ime string) error {
return errDriverNotImplemented
}
func (wd *wdaDriver) PressKeyCode(keyCode KeyCode) (err error) {
return errDriverNotImplemented
}
func (wd *wdaDriver) SendKeys(text string, options ...ActionOption) (err error) {
// [[FBRoute POST:@"/wda/keys"] respondWithTarget:self action:@selector(handleKeys:)]
actionOptions := NewActionOptions(options...)
@@ -655,6 +659,10 @@ func (wd *wdaDriver) PressButton(devBtn DeviceButton) (err error) {
return
}
func (wd *wdaDriver) LoginNoneUI(packageName, phoneNumber string, captcha string) error {
return errDriverNotImplemented
}
func (wd *wdaDriver) StartCamera() (err error) {
// start camera, alias for app_launch com.apple.camera
return wd.AppLaunch("com.apple.camera")