mirror of
https://github.com/httprunner/httprunner.git
synced 2026-05-13 08:59:44 +08:00
refactor: move gwda into uixt
This commit is contained in:
4
go.mod
4
go.mod
@@ -5,7 +5,7 @@ go 1.18
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.0.4
|
||||
github.com/denisbrodbeck/machineid v1.0.1
|
||||
github.com/electricbubble/gwda v0.4.0
|
||||
github.com/electricbubble/gidevice v0.6.2
|
||||
github.com/electricbubble/opencv-helper v0.0.3
|
||||
github.com/fatih/color v1.13.0
|
||||
github.com/getsentry/sentry-go v0.13.0
|
||||
@@ -42,7 +42,6 @@ require (
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/electricbubble/gidevice v0.6.2 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.0 // indirect
|
||||
@@ -88,4 +87,3 @@ require (
|
||||
)
|
||||
|
||||
// replace github.com/httprunner/funplugin => ../funplugin
|
||||
replace github.com/electricbubble/gwda => github.com/debugtalk/gwda v0.0.0-20220920103757-8c05b6218f45
|
||||
|
||||
2
go.sum
2
go.sum
@@ -96,8 +96,6 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/debugtalk/gwda v0.0.0-20220920103757-8c05b6218f45 h1:n/O+tMRl7XmuP778Oy2wunq8QpftRS0rlBkKumaJSbc=
|
||||
github.com/debugtalk/gwda v0.0.0-20220920103757-8c05b6218f45/go.mod h1:kyzKpP1/iKJ2i4AxmT8sEmSvB8Pz5NcDVwc/m/Jsg6k=
|
||||
github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ=
|
||||
github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI=
|
||||
github.com/electricbubble/gidevice v0.6.2 h1:eIeCHH7Xn5fTwnUv3qL8c7L4anKIHtjlTBkgr1LDVTc=
|
||||
|
||||
@@ -30,8 +30,8 @@ type TConfig struct {
|
||||
ParametersSetting *TParamsConfig `json:"parameters_setting,omitempty" yaml:"parameters_setting,omitempty"`
|
||||
ThinkTimeSetting *ThinkTimeConfig `json:"think_time,omitempty" yaml:"think_time,omitempty"`
|
||||
WebSocketSetting *WebSocketConfig `json:"websocket,omitempty" yaml:"websocket,omitempty"`
|
||||
IOS []*uixt.WDAOptions `json:"ios,omitempty" yaml:"ios,omitempty"`
|
||||
Android []*uixt.UIAOptions `json:"android,omitempty" yaml:"android,omitempty"`
|
||||
IOS []*uixt.IOSDevice `json:"ios,omitempty" yaml:"ios,omitempty"`
|
||||
Android []*uixt.AndroidDevice `json:"android,omitempty" yaml:"android,omitempty"`
|
||||
Timeout float64 `json:"timeout,omitempty" yaml:"timeout,omitempty"` // global timeout in seconds
|
||||
Export []string `json:"export,omitempty" yaml:"export,omitempty"`
|
||||
Weight int `json:"weight,omitempty" yaml:"weight,omitempty"`
|
||||
@@ -102,8 +102,8 @@ func (c *TConfig) SetWebSocket(times, interval, timeout, size int64) *TConfig {
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *TConfig) SetIOS(options ...uixt.WDAOption) *TConfig {
|
||||
wdaOptions := &uixt.WDAOptions{}
|
||||
func (c *TConfig) SetIOS(options ...uixt.IOSDeviceOption) *TConfig {
|
||||
wdaOptions := &uixt.IOSDevice{}
|
||||
for _, option := range options {
|
||||
option(wdaOptions)
|
||||
}
|
||||
|
||||
15
hrp/internal/uixt/android_device.go
Normal file
15
hrp/internal/uixt/android_device.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package uixt
|
||||
|
||||
type AndroidDevice struct {
|
||||
SerialNumber string `json:"serial,omitempty" yaml:"serial,omitempty"`
|
||||
Port int `json:"port,omitempty" yaml:"port,omitempty"`
|
||||
LogOn bool `json:"log_on,omitempty" yaml:"log_on,omitempty"`
|
||||
}
|
||||
|
||||
func (o AndroidDevice) UUID() string {
|
||||
return o.SerialNumber
|
||||
}
|
||||
|
||||
func InitUIAClient(device *AndroidDevice) (*DriverExt, error) {
|
||||
return nil, nil
|
||||
}
|
||||
1
hrp/internal/uixt/android_webdriver.go
Normal file
1
hrp/internal/uixt/android_webdriver.go
Normal file
@@ -0,0 +1 @@
|
||||
package uixt
|
||||
1
hrp/internal/uixt/android_webelment.go
Normal file
1
hrp/internal/uixt/android_webelment.go
Normal file
@@ -0,0 +1 @@
|
||||
package uixt
|
||||
@@ -1,7 +1,5 @@
|
||||
package uixt
|
||||
|
||||
import "github.com/electricbubble/gwda"
|
||||
|
||||
func (dExt *DriverExt) Drag(pathname string, toX, toY int, pressForDuration ...float64) (err error) {
|
||||
return dExt.DragFloat(pathname, float64(toX), float64(toY), pressForDuration...)
|
||||
}
|
||||
@@ -28,5 +26,5 @@ func (dExt *DriverExt) DragOffsetFloat(pathname string, toX, toY, xOffset, yOffs
|
||||
fromY := y + height*yOffset
|
||||
|
||||
return dExt.Driver.DragFloat(fromX, fromY, toX, toY,
|
||||
gwda.WithPressDuration(pressForDuration[0]))
|
||||
WithPressDuration(pressForDuration[0]))
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/electricbubble/gwda"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
@@ -120,8 +119,8 @@ func WithThreshold(threshold float64) CVOption {
|
||||
}
|
||||
|
||||
type DriverExt struct {
|
||||
Driver gwda.WebDriver
|
||||
windowSize gwda.Size
|
||||
Driver WebDriver
|
||||
windowSize Size
|
||||
frame *bytes.Buffer
|
||||
doneMjpegStream chan bool
|
||||
scale float64
|
||||
@@ -132,7 +131,7 @@ type DriverExt struct {
|
||||
CVArgs
|
||||
}
|
||||
|
||||
func extend(driver gwda.WebDriver) (dExt *DriverExt, err error) {
|
||||
func extend(driver WebDriver) (dExt *DriverExt, err error) {
|
||||
dExt = &DriverExt{Driver: driver}
|
||||
dExt.doneMjpegStream = make(chan bool, 1)
|
||||
|
||||
@@ -273,17 +272,17 @@ func isPathExists(path string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (dExt *DriverExt) FindUIElement(param string) (ele gwda.WebElement, err error) {
|
||||
var selector gwda.BySelector
|
||||
func (dExt *DriverExt) FindUIElement(param string) (ele WebElement, err error) {
|
||||
var selector BySelector
|
||||
if strings.HasPrefix(param, "/") {
|
||||
// xpath
|
||||
selector = gwda.BySelector{
|
||||
selector = BySelector{
|
||||
XPath: param,
|
||||
}
|
||||
} else {
|
||||
// name
|
||||
selector = gwda.BySelector{
|
||||
LinkText: gwda.NewElementAttribute().WithName(param),
|
||||
selector = BySelector{
|
||||
LinkText: NewElementAttribute().WithName(param),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,25 +304,25 @@ func (dExt *DriverExt) MappingToRectInUIKit(rect image.Rectangle) (x, y, width,
|
||||
return
|
||||
}
|
||||
|
||||
func (dExt *DriverExt) PerformTouchActions(touchActions *gwda.TouchActions) error {
|
||||
func (dExt *DriverExt) PerformTouchActions(touchActions *TouchActions) error {
|
||||
return dExt.Driver.PerformAppiumTouchActions(touchActions)
|
||||
}
|
||||
|
||||
func (dExt *DriverExt) PerformActions(actions *gwda.W3CActions) error {
|
||||
func (dExt *DriverExt) PerformActions(actions *W3CActions) error {
|
||||
return dExt.Driver.PerformW3CActions(actions)
|
||||
}
|
||||
|
||||
func (dExt *DriverExt) IsNameExist(name string) bool {
|
||||
selector := gwda.BySelector{
|
||||
LinkText: gwda.NewElementAttribute().WithName(name),
|
||||
selector := BySelector{
|
||||
LinkText: NewElementAttribute().WithName(name),
|
||||
}
|
||||
_, err := dExt.Driver.FindElement(selector)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (dExt *DriverExt) IsLabelExist(label string) bool {
|
||||
selector := gwda.BySelector{
|
||||
LinkText: gwda.NewElementAttribute().WithLabel(label),
|
||||
selector := BySelector{
|
||||
LinkText: NewElementAttribute().WithLabel(label),
|
||||
}
|
||||
_, err := dExt.Driver.FindElement(selector)
|
||||
return err == nil
|
||||
|
||||
@@ -4,9 +4,6 @@ package uixt
|
||||
|
||||
import (
|
||||
"image"
|
||||
"sort"
|
||||
|
||||
"github.com/electricbubble/gwda"
|
||||
)
|
||||
|
||||
func (dExt *DriverExt) GesturePassword(pathname string, password ...int) (err error) {
|
||||
@@ -26,17 +23,17 @@ func (dExt *DriverExt) GesturePassword(pathname string, password ...int) (err er
|
||||
return false
|
||||
})
|
||||
|
||||
touchActions := gwda.NewTouchActions(len(password)*2 + 1)
|
||||
touchActions := NewTouchActions(len(password)*2 + 1)
|
||||
for i := range password {
|
||||
x, y, width, height := dExt.MappingToRectInUIKit(rects[password[i]])
|
||||
x = x + width*0.5
|
||||
y = y + height*0.5
|
||||
|
||||
if i == 0 {
|
||||
touchActions.Press(gwda.NewTouchActionPress().WithXYFloat(x, y)).
|
||||
touchActions.Press(NewTouchActionPress().WithXYFloat(x, y)).
|
||||
Wait(0.2)
|
||||
} else {
|
||||
touchActions.MoveTo(gwda.NewTouchActionMoveTo().WithXYFloat(x, y)).
|
||||
touchActions.MoveTo(NewTouchActionMoveTo().WithXYFloat(x, y)).
|
||||
Wait(0.2)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,985 @@
|
||||
package uixt
|
||||
|
||||
type WebDriver interface {
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
DefaultWaitTimeout = 60 * time.Second
|
||||
DefaultWaitInterval = 400 * time.Millisecond
|
||||
DefaultKeepAliveInterval = 30 * time.Second
|
||||
)
|
||||
|
||||
type AlertAction string
|
||||
|
||||
const (
|
||||
AlertActionAccept AlertAction = "accept"
|
||||
AlertActionDismiss AlertAction = "dismiss"
|
||||
)
|
||||
|
||||
type Capabilities map[string]interface{}
|
||||
|
||||
func NewCapabilities() Capabilities {
|
||||
return make(Capabilities)
|
||||
}
|
||||
|
||||
func (caps Capabilities) WithAppLaunchOption(launchOpt AppLaunchOption) Capabilities {
|
||||
for k, v := range launchOpt {
|
||||
caps[k] = v
|
||||
}
|
||||
return caps
|
||||
}
|
||||
|
||||
// WithDefaultAlertAction
|
||||
func (caps Capabilities) WithDefaultAlertAction(alertAction AlertAction) Capabilities {
|
||||
caps["defaultAlertAction"] = alertAction
|
||||
return caps
|
||||
}
|
||||
|
||||
// WithMaxTypingFrequency
|
||||
// Defaults to `60`.
|
||||
func (caps Capabilities) WithMaxTypingFrequency(n int) Capabilities {
|
||||
if n <= 0 {
|
||||
n = 60
|
||||
}
|
||||
caps["maxTypingFrequency"] = n
|
||||
return caps
|
||||
}
|
||||
|
||||
// WithWaitForIdleTimeout
|
||||
// Defaults to `10`
|
||||
func (caps Capabilities) WithWaitForIdleTimeout(second float64) Capabilities {
|
||||
caps["waitForIdleTimeout"] = second
|
||||
return caps
|
||||
}
|
||||
|
||||
// WithShouldUseTestManagerForVisibilityDetection If set to YES will ask TestManagerDaemon for element visibility
|
||||
// Defaults to `false`
|
||||
func (caps Capabilities) WithShouldUseTestManagerForVisibilityDetection(b bool) Capabilities {
|
||||
caps["shouldUseTestManagerForVisibilityDetection"] = b
|
||||
return caps
|
||||
}
|
||||
|
||||
// WithShouldUseCompactResponses If set to YES will use compact (standards-compliant) & faster responses
|
||||
// Defaults to `true`
|
||||
func (caps Capabilities) WithShouldUseCompactResponses(b bool) Capabilities {
|
||||
caps["shouldUseCompactResponses"] = b
|
||||
return caps
|
||||
}
|
||||
|
||||
// WithElementResponseAttributes If shouldUseCompactResponses == NO,
|
||||
// is the comma-separated list of fields to return with each element.
|
||||
// Defaults to `type,label`.
|
||||
func (caps Capabilities) WithElementResponseAttributes(s string) Capabilities {
|
||||
caps["elementResponseAttributes"] = s
|
||||
return caps
|
||||
}
|
||||
|
||||
// WithShouldUseSingletonTestManager
|
||||
// Defaults to `true`
|
||||
func (caps Capabilities) WithShouldUseSingletonTestManager(b bool) Capabilities {
|
||||
caps["shouldUseSingletonTestManager"] = b
|
||||
return caps
|
||||
}
|
||||
|
||||
// WithDisableAutomaticScreenshots
|
||||
// Defaults to `true`
|
||||
func (caps Capabilities) WithDisableAutomaticScreenshots(b bool) Capabilities {
|
||||
caps["disableAutomaticScreenshots"] = b
|
||||
return caps
|
||||
}
|
||||
|
||||
// WithShouldTerminateApp
|
||||
// Defaults to `true`
|
||||
func (caps Capabilities) WithShouldTerminateApp(b bool) Capabilities {
|
||||
caps["shouldTerminateApp"] = b
|
||||
return caps
|
||||
}
|
||||
|
||||
// WithEventloopIdleDelaySec
|
||||
// Delays the invocation of '-[XCUIApplicationProcess setEventLoopHasIdled:]' by the timer interval passed.
|
||||
// which is skipped on setting it to zero.
|
||||
func (caps Capabilities) WithEventloopIdleDelaySec(second float64) Capabilities {
|
||||
caps["eventloopIdleDelaySec"] = second
|
||||
return caps
|
||||
}
|
||||
|
||||
type SessionInfo struct {
|
||||
SessionId string `json:"sessionId"`
|
||||
Capabilities struct {
|
||||
Device string `json:"device"`
|
||||
BrowserName string `json:"browserName"`
|
||||
SdkVersion string `json:"sdkVersion"`
|
||||
CFBundleIdentifier string `json:"CFBundleIdentifier"`
|
||||
} `json:"capabilities"`
|
||||
}
|
||||
|
||||
type DeviceStatus struct {
|
||||
Message string `json:"message"`
|
||||
State string `json:"state"`
|
||||
OS struct {
|
||||
TestmanagerdVersion int `json:"testmanagerdVersion"`
|
||||
Name string `json:"name"`
|
||||
SdkVersion string `json:"sdkVersion"`
|
||||
Version string `json:"version"`
|
||||
} `json:"os"`
|
||||
IOS struct {
|
||||
IP string `json:"ip"`
|
||||
SimulatorVersion string `json:"simulatorVersion"`
|
||||
} `json:"ios"`
|
||||
Ready bool `json:"ready"`
|
||||
Build struct {
|
||||
Time string `json:"time"`
|
||||
ProductBundleIdentifier string `json:"productBundleIdentifier"`
|
||||
} `json:"build"`
|
||||
}
|
||||
|
||||
type DeviceInfo struct {
|
||||
TimeZone string `json:"timeZone"`
|
||||
CurrentLocale string `json:"currentLocale"`
|
||||
Model string `json:"model"`
|
||||
UUID string `json:"uuid"`
|
||||
UserInterfaceIdiom int `json:"userInterfaceIdiom"`
|
||||
UserInterfaceStyle string `json:"userInterfaceStyle"`
|
||||
Name string `json:"name"`
|
||||
IsSimulator bool `json:"isSimulator"`
|
||||
ThermalState int `json:"thermalState"`
|
||||
}
|
||||
|
||||
type Location struct {
|
||||
AuthorizationStatus int `json:"authorizationStatus"`
|
||||
Longitude float64 `json:"longitude"`
|
||||
Latitude float64 `json:"latitude"`
|
||||
Altitude float64 `json:"altitude"`
|
||||
}
|
||||
|
||||
type BatteryInfo struct {
|
||||
// Battery level in range [0.0, 1.0], where 1.0 means 100% charge.
|
||||
Level float64 `json:"level"`
|
||||
|
||||
// Battery state ( 1: on battery, discharging; 2: plugged in, less than 100%, 3: plugged in, at 100% )
|
||||
State BatteryState `json:"state"`
|
||||
}
|
||||
|
||||
type BatteryState int
|
||||
|
||||
const (
|
||||
_ = iota
|
||||
BatteryStateUnplugged BatteryState = iota // on battery, discharging
|
||||
BatteryStateCharging // plugged in, less than 100%
|
||||
BatteryStateFull // plugged in, at 100%
|
||||
)
|
||||
|
||||
func (v BatteryState) String() string {
|
||||
switch v {
|
||||
case BatteryStateUnplugged:
|
||||
return "On battery, discharging"
|
||||
case BatteryStateCharging:
|
||||
return "Plugged in, less than 100%"
|
||||
case BatteryStateFull:
|
||||
return "Plugged in, at 100%"
|
||||
default:
|
||||
return "UNKNOWN"
|
||||
}
|
||||
}
|
||||
|
||||
type Size struct {
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
}
|
||||
|
||||
type Screen struct {
|
||||
StatusBarSize Size `json:"statusBarSize"`
|
||||
Scale float64 `json:"scale"`
|
||||
}
|
||||
|
||||
type AppInfo struct {
|
||||
ProcessArguments struct {
|
||||
Env interface{} `json:"env"`
|
||||
Args []interface{} `json:"args"`
|
||||
} `json:"processArguments"`
|
||||
Name string `json:"name"`
|
||||
AppBaseInfo
|
||||
}
|
||||
|
||||
type AppBaseInfo struct {
|
||||
Pid int `json:"pid"`
|
||||
BundleId string `json:"bundleId"`
|
||||
}
|
||||
|
||||
type AppState int
|
||||
|
||||
const (
|
||||
AppStateNotRunning AppState = 1 << iota
|
||||
AppStateRunningBack
|
||||
AppStateRunningFront
|
||||
)
|
||||
|
||||
func (v AppState) String() string {
|
||||
switch v {
|
||||
case AppStateNotRunning:
|
||||
return "Not Running"
|
||||
case AppStateRunningBack:
|
||||
return "Running (Back)"
|
||||
case AppStateRunningFront:
|
||||
return "Running (Front)"
|
||||
default:
|
||||
return "UNKNOWN"
|
||||
}
|
||||
}
|
||||
|
||||
// AppLaunchOption Configure app launch parameters
|
||||
type AppLaunchOption map[string]interface{}
|
||||
|
||||
func NewAppLaunchOption() AppLaunchOption {
|
||||
return make(AppLaunchOption)
|
||||
}
|
||||
|
||||
func (opt AppLaunchOption) WithBundleId(bundleId string) AppLaunchOption {
|
||||
opt["bundleId"] = bundleId
|
||||
return opt
|
||||
}
|
||||
|
||||
// WithShouldWaitForQuiescence whether to wait for quiescence on application startup
|
||||
// Defaults to `true`
|
||||
func (opt AppLaunchOption) WithShouldWaitForQuiescence(b bool) AppLaunchOption {
|
||||
opt["shouldWaitForQuiescence"] = b
|
||||
return opt
|
||||
}
|
||||
|
||||
// WithArguments The optional array of application command line arguments.
|
||||
// The arguments are going to be applied if the application was not running before.
|
||||
func (opt AppLaunchOption) WithArguments(args []string) AppLaunchOption {
|
||||
opt["arguments"] = args
|
||||
return opt
|
||||
}
|
||||
|
||||
// WithEnvironment The optional dictionary of environment variables for the application, which is going to be executed.
|
||||
// The environment variables are going to be applied if the application was not running before.
|
||||
func (opt AppLaunchOption) WithEnvironment(env map[string]string) AppLaunchOption {
|
||||
opt["environment"] = env
|
||||
return opt
|
||||
}
|
||||
|
||||
// PasteboardType The type of the item on the pasteboard.
|
||||
type PasteboardType string
|
||||
|
||||
const (
|
||||
PasteboardTypePlaintext PasteboardType = "plaintext"
|
||||
PasteboardTypeImage PasteboardType = "image"
|
||||
PasteboardTypeUrl PasteboardType = "url"
|
||||
)
|
||||
|
||||
const (
|
||||
TextBackspace string = "\u0008"
|
||||
TextDelete string = "\u007F"
|
||||
)
|
||||
|
||||
// type KeyboardKeyLabel string
|
||||
//
|
||||
// const (
|
||||
// KeyboardKeyReturn = "return"
|
||||
// )
|
||||
|
||||
// DeviceButton A physical button on an iOS device.
|
||||
type DeviceButton string
|
||||
|
||||
const (
|
||||
DeviceButtonHome DeviceButton = "home"
|
||||
DeviceButtonVolumeUp DeviceButton = "volumeUp"
|
||||
DeviceButtonVolumeDown DeviceButton = "volumeDown"
|
||||
)
|
||||
|
||||
type NotificationType string
|
||||
|
||||
const (
|
||||
NotificationTypePlain NotificationType = "plain"
|
||||
NotificationTypeDarwin NotificationType = "darwin"
|
||||
)
|
||||
|
||||
// EventPageID The event page identifier
|
||||
type EventPageID int
|
||||
|
||||
const EventPageIDConsumer EventPageID = 0x0C
|
||||
|
||||
// EventUsageID The event usage identifier (usages are defined per-page)
|
||||
type EventUsageID int
|
||||
|
||||
const (
|
||||
EventUsageIDCsmrVolumeUp EventUsageID = 0xE9
|
||||
EventUsageIDCsmrVolumeDown EventUsageID = 0xEA
|
||||
EventUsageIDCsmrHome EventUsageID = 0x40
|
||||
EventUsageIDCsmrPower EventUsageID = 0x30
|
||||
EventUsageIDCsmrSnapshot EventUsageID = 0x65 // Power + Home
|
||||
)
|
||||
|
||||
type Orientation string
|
||||
|
||||
const (
|
||||
// OrientationPortrait Device oriented vertically, home button on the bottom
|
||||
OrientationPortrait Orientation = "PORTRAIT"
|
||||
|
||||
// OrientationPortraitUpsideDown Device oriented vertically, home button on the top
|
||||
OrientationPortraitUpsideDown Orientation = "UIA_DEVICE_ORIENTATION_PORTRAIT_UPSIDEDOWN"
|
||||
|
||||
// OrientationLandscapeLeft Device oriented horizontally, home button on the right
|
||||
OrientationLandscapeLeft Orientation = "LANDSCAPE"
|
||||
|
||||
// OrientationLandscapeRight Device oriented horizontally, home button on the left
|
||||
OrientationLandscapeRight Orientation = "UIA_DEVICE_ORIENTATION_LANDSCAPERIGHT"
|
||||
)
|
||||
|
||||
type Rotation struct {
|
||||
X int `json:"x"`
|
||||
Y int `json:"y"`
|
||||
Z int `json:"z"`
|
||||
}
|
||||
|
||||
// SourceOption Configure the format or attribute of the Source
|
||||
type SourceOption map[string]interface{}
|
||||
|
||||
func NewSourceOption() SourceOption {
|
||||
return make(SourceOption)
|
||||
}
|
||||
|
||||
// WithFormatAsJson Application elements tree in form of json string
|
||||
func (opt SourceOption) WithFormatAsJson() SourceOption {
|
||||
opt["format"] = "json"
|
||||
return opt
|
||||
}
|
||||
|
||||
// WithFormatAsXml Application elements tree in form of xml string
|
||||
func (opt SourceOption) WithFormatAsXml() SourceOption {
|
||||
opt["format"] = "xml"
|
||||
return opt
|
||||
}
|
||||
|
||||
// WithFormatAsDescription Application elements tree in form of internal XCTest debugDescription string
|
||||
func (opt SourceOption) WithFormatAsDescription() SourceOption {
|
||||
opt["format"] = "description"
|
||||
return opt
|
||||
}
|
||||
|
||||
// WithScope Allows to provide XML scope.
|
||||
// only `xml` is supported.
|
||||
func (opt SourceOption) WithScope(scope string) SourceOption {
|
||||
if vFormat, ok := opt["format"]; ok && vFormat != "xml" {
|
||||
return opt
|
||||
}
|
||||
opt["scope"] = scope
|
||||
return opt
|
||||
}
|
||||
|
||||
// WithExcludedAttributes Excludes the given attribute names.
|
||||
// only `xml` is supported.
|
||||
func (opt SourceOption) WithExcludedAttributes(attributes []string) SourceOption {
|
||||
if vFormat, ok := opt["format"]; ok && vFormat != "xml" {
|
||||
return opt
|
||||
}
|
||||
opt["excluded_attributes"] = strings.Join(attributes, ",")
|
||||
return opt
|
||||
}
|
||||
|
||||
const (
|
||||
// legacyWebElementIdentifier is the string constant used in the old
|
||||
// WebDriver JSON protocol that is the key for the map that contains an
|
||||
// unique element identifier.
|
||||
legacyWebElementIdentifier = "ELEMENT"
|
||||
|
||||
// webElementIdentifier is the string constant defined by the W3C
|
||||
// specification that is the key for the map that contains a unique element identifier.
|
||||
webElementIdentifier = "element-6066-11e4-a52e-4f735466cecf"
|
||||
)
|
||||
|
||||
func elementIDFromValue(val map[string]string) string {
|
||||
for _, key := range []string{webElementIdentifier, legacyWebElementIdentifier} {
|
||||
if v, ok := val[key]; ok && v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// performance ranking: class name > accessibility id > link text > predicate > class chain > xpath
|
||||
type BySelector struct {
|
||||
ClassName ElementType `json:"class name"`
|
||||
|
||||
// isSearchByIdentifier
|
||||
Name string `json:"name"`
|
||||
Id string `json:"id"`
|
||||
AccessibilityId string `json:"accessibility id"`
|
||||
// isSearchByIdentifier
|
||||
|
||||
// partialSearch
|
||||
LinkText ElementAttribute `json:"link text"`
|
||||
PartialLinkText ElementAttribute `json:"partial link text"`
|
||||
// partialSearch
|
||||
|
||||
Predicate string `json:"predicate string"`
|
||||
|
||||
ClassChain string `json:"class chain"`
|
||||
|
||||
XPath string `json:"xpath"` // not recommended, it's slow because it is not supported by XCTest natively
|
||||
}
|
||||
|
||||
func (wl BySelector) getUsingAndValue() (using, value string) {
|
||||
vBy := reflect.ValueOf(wl)
|
||||
tBy := reflect.TypeOf(wl)
|
||||
for i := 0; i < vBy.NumField(); i++ {
|
||||
vi := vBy.Field(i).Interface()
|
||||
switch vi := vi.(type) {
|
||||
case ElementType:
|
||||
value = vi.String()
|
||||
case string:
|
||||
value = vi
|
||||
case ElementAttribute:
|
||||
value = vi.String()
|
||||
}
|
||||
if value != "" && value != "UNKNOWN" {
|
||||
using = tBy.Field(i).Tag.Get("json")
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type ElementAttribute map[string]interface{}
|
||||
|
||||
func (ea ElementAttribute) String() string {
|
||||
for k, v := range ea {
|
||||
switch v := v.(type) {
|
||||
case bool:
|
||||
return k + "=" + strconv.FormatBool(v)
|
||||
case string:
|
||||
return k + "=" + v
|
||||
default:
|
||||
return k + "=" + fmt.Sprintf("%v", v)
|
||||
}
|
||||
}
|
||||
return "UNKNOWN"
|
||||
}
|
||||
|
||||
func (ea ElementAttribute) getAttributeName() string {
|
||||
for k := range ea {
|
||||
return k
|
||||
}
|
||||
return "UNKNOWN"
|
||||
}
|
||||
|
||||
func NewElementAttribute() ElementAttribute {
|
||||
return make(ElementAttribute)
|
||||
}
|
||||
|
||||
// WithUID Element's unique identifier
|
||||
func (ea ElementAttribute) WithUID(uid string) ElementAttribute {
|
||||
ea["UID"] = uid
|
||||
return ea
|
||||
}
|
||||
|
||||
// WithAccessibilityContainer Whether element is an accessibility container
|
||||
// (contains children of any depth that are accessible)
|
||||
func (ea ElementAttribute) WithAccessibilityContainer(b bool) ElementAttribute {
|
||||
ea["accessibilityContainer"] = b
|
||||
return ea
|
||||
}
|
||||
|
||||
// WithAccessible Whether element is accessible
|
||||
func (ea ElementAttribute) WithAccessible(b bool) ElementAttribute {
|
||||
ea["accessible"] = b
|
||||
return ea
|
||||
}
|
||||
|
||||
// WithEnabled Whether element is enabled
|
||||
func (ea ElementAttribute) WithEnabled(b bool) ElementAttribute {
|
||||
ea["enabled"] = b
|
||||
return ea
|
||||
}
|
||||
|
||||
// WithLabel Element's label
|
||||
func (ea ElementAttribute) WithLabel(s string) ElementAttribute {
|
||||
ea["label"] = s
|
||||
return ea
|
||||
}
|
||||
|
||||
// WithName Element's name
|
||||
func (ea ElementAttribute) WithName(s string) ElementAttribute {
|
||||
ea["name"] = s
|
||||
return ea
|
||||
}
|
||||
|
||||
// WithSelected Element's selected state
|
||||
func (ea ElementAttribute) WithSelected(b bool) ElementAttribute {
|
||||
ea["selected"] = b
|
||||
return ea
|
||||
}
|
||||
|
||||
// WithType Element's type
|
||||
func (ea ElementAttribute) WithType(elemType ElementType) ElementAttribute {
|
||||
ea["type"] = elemType
|
||||
return ea
|
||||
}
|
||||
|
||||
// WithValue Element's value
|
||||
func (ea ElementAttribute) WithValue(s string) ElementAttribute {
|
||||
ea["value"] = s
|
||||
return ea
|
||||
}
|
||||
|
||||
// WithVisible
|
||||
//
|
||||
// Whether element is visible
|
||||
func (ea ElementAttribute) WithVisible(b bool) ElementAttribute {
|
||||
ea["visible"] = b
|
||||
return ea
|
||||
}
|
||||
|
||||
func (et ElementType) String() string {
|
||||
vBy := reflect.ValueOf(et)
|
||||
tBy := reflect.TypeOf(et)
|
||||
for i := 0; i < vBy.NumField(); i++ {
|
||||
if vBy.Field(i).Bool() {
|
||||
return tBy.Field(i).Tag.Get("json")
|
||||
}
|
||||
}
|
||||
return "UNKNOWN"
|
||||
}
|
||||
|
||||
// ElementType
|
||||
// !!! This mapping should be updated if there are changes after each new XCTest release"`
|
||||
type ElementType struct {
|
||||
Any bool `json:"XCUIElementTypeAny"`
|
||||
Other bool `json:"XCUIElementTypeOther"`
|
||||
Application bool `json:"XCUIElementTypeApplication"`
|
||||
Group bool `json:"XCUIElementTypeGroup"`
|
||||
Window bool `json:"XCUIElementTypeWindow"`
|
||||
Sheet bool `json:"XCUIElementTypeSheet"`
|
||||
Drawer bool `json:"XCUIElementTypeDrawer"`
|
||||
Alert bool `json:"XCUIElementTypeAlert"`
|
||||
Dialog bool `json:"XCUIElementTypeDialog"`
|
||||
Button bool `json:"XCUIElementTypeButton"`
|
||||
RadioButton bool `json:"XCUIElementTypeRadioButton"`
|
||||
RadioGroup bool `json:"XCUIElementTypeRadioGroup"`
|
||||
CheckBox bool `json:"XCUIElementTypeCheckBox"`
|
||||
DisclosureTriangle bool `json:"XCUIElementTypeDisclosureTriangle"`
|
||||
PopUpButton bool `json:"XCUIElementTypePopUpButton"`
|
||||
ComboBox bool `json:"XCUIElementTypeComboBox"`
|
||||
MenuButton bool `json:"XCUIElementTypeMenuButton"`
|
||||
ToolbarButton bool `json:"XCUIElementTypeToolbarButton"`
|
||||
Popover bool `json:"XCUIElementTypePopover"`
|
||||
Keyboard bool `json:"XCUIElementTypeKeyboard"`
|
||||
Key bool `json:"XCUIElementTypeKey"`
|
||||
NavigationBar bool `json:"XCUIElementTypeNavigationBar"`
|
||||
TabBar bool `json:"XCUIElementTypeTabBar"`
|
||||
TabGroup bool `json:"XCUIElementTypeTabGroup"`
|
||||
Toolbar bool `json:"XCUIElementTypeToolbar"`
|
||||
StatusBar bool `json:"XCUIElementTypeStatusBar"`
|
||||
Table bool `json:"XCUIElementTypeTable"`
|
||||
TableRow bool `json:"XCUIElementTypeTableRow"`
|
||||
TableColumn bool `json:"XCUIElementTypeTableColumn"`
|
||||
Outline bool `json:"XCUIElementTypeOutline"`
|
||||
OutlineRow bool `json:"XCUIElementTypeOutlineRow"`
|
||||
Browser bool `json:"XCUIElementTypeBrowser"`
|
||||
CollectionView bool `json:"XCUIElementTypeCollectionView"`
|
||||
Slider bool `json:"XCUIElementTypeSlider"`
|
||||
PageIndicator bool `json:"XCUIElementTypePageIndicator"`
|
||||
ProgressIndicator bool `json:"XCUIElementTypeProgressIndicator"`
|
||||
ActivityIndicator bool `json:"XCUIElementTypeActivityIndicator"`
|
||||
SegmentedControl bool `json:"XCUIElementTypeSegmentedControl"`
|
||||
Picker bool `json:"XCUIElementTypePicker"`
|
||||
PickerWheel bool `json:"XCUIElementTypePickerWheel"`
|
||||
Switch bool `json:"XCUIElementTypeSwitch"`
|
||||
Toggle bool `json:"XCUIElementTypeToggle"`
|
||||
Link bool `json:"XCUIElementTypeLink"`
|
||||
Image bool `json:"XCUIElementTypeImage"`
|
||||
Icon bool `json:"XCUIElementTypeIcon"`
|
||||
SearchField bool `json:"XCUIElementTypeSearchField"`
|
||||
ScrollView bool `json:"XCUIElementTypeScrollView"`
|
||||
ScrollBar bool `json:"XCUIElementTypeScrollBar"`
|
||||
StaticText bool `json:"XCUIElementTypeStaticText"`
|
||||
TextField bool `json:"XCUIElementTypeTextField"`
|
||||
SecureTextField bool `json:"XCUIElementTypeSecureTextField"`
|
||||
DatePicker bool `json:"XCUIElementTypeDatePicker"`
|
||||
TextView bool `json:"XCUIElementTypeTextView"`
|
||||
Menu bool `json:"XCUIElementTypeMenu"`
|
||||
MenuItem bool `json:"XCUIElementTypeMenuItem"`
|
||||
MenuBar bool `json:"XCUIElementTypeMenuBar"`
|
||||
MenuBarItem bool `json:"XCUIElementTypeMenuBarItem"`
|
||||
Map bool `json:"XCUIElementTypeMap"`
|
||||
WebView bool `json:"XCUIElementTypeWebView"`
|
||||
IncrementArrow bool `json:"XCUIElementTypeIncrementArrow"`
|
||||
DecrementArrow bool `json:"XCUIElementTypeDecrementArrow"`
|
||||
Timeline bool `json:"XCUIElementTypeTimeline"`
|
||||
RatingIndicator bool `json:"XCUIElementTypeRatingIndicator"`
|
||||
ValueIndicator bool `json:"XCUIElementTypeValueIndicator"`
|
||||
SplitGroup bool `json:"XCUIElementTypeSplitGroup"`
|
||||
Splitter bool `json:"XCUIElementTypeSplitter"`
|
||||
RelevanceIndicator bool `json:"XCUIElementTypeRelevanceIndicator"`
|
||||
ColorWell bool `json:"XCUIElementTypeColorWell"`
|
||||
HelpTag bool `json:"XCUIElementTypeHelpTag"`
|
||||
Matte bool `json:"XCUIElementTypeMatte"`
|
||||
DockItem bool `json:"XCUIElementTypeDockItem"`
|
||||
Ruler bool `json:"XCUIElementTypeRuler"`
|
||||
RulerMarker bool `json:"XCUIElementTypeRulerMarker"`
|
||||
Grid bool `json:"XCUIElementTypeGrid"`
|
||||
LevelIndicator bool `json:"XCUIElementTypeLevelIndicator"`
|
||||
Cell bool `json:"XCUIElementTypeCell"`
|
||||
LayoutArea bool `json:"XCUIElementTypeLayoutArea"`
|
||||
LayoutItem bool `json:"XCUIElementTypeLayoutItem"`
|
||||
Handle bool `json:"XCUIElementTypeHandle"`
|
||||
Stepper bool `json:"XCUIElementTypeStepper"`
|
||||
Tab bool `json:"XCUIElementTypeTab"`
|
||||
TouchBar bool `json:"XCUIElementTypeTouchBar"`
|
||||
StatusItem bool `json:"XCUIElementTypeStatusItem"`
|
||||
}
|
||||
|
||||
// ProtectedResource A system resource that requires user authorization to access.
|
||||
type ProtectedResource int
|
||||
|
||||
// https://developer.apple.com/documentation/xctest/xcuiprotectedresource?language=objc
|
||||
const (
|
||||
ProtectedResourceContacts ProtectedResource = 1
|
||||
ProtectedResourceCalendar ProtectedResource = 2
|
||||
ProtectedResourceReminders ProtectedResource = 3
|
||||
ProtectedResourcePhotos ProtectedResource = 4
|
||||
ProtectedResourceMicrophone ProtectedResource = 5
|
||||
ProtectedResourceCamera ProtectedResource = 6
|
||||
ProtectedResourceMediaLibrary ProtectedResource = 7
|
||||
ProtectedResourceHomeKit ProtectedResource = 8
|
||||
ProtectedResourceSystemRootDirectory ProtectedResource = 0x40000000
|
||||
ProtectedResourceUserDesktopDirectory ProtectedResource = 0x40000001
|
||||
ProtectedResourceUserDownloadsDirectory ProtectedResource = 0x40000002
|
||||
ProtectedResourceUserDocumentsDirectory ProtectedResource = 0x40000003
|
||||
ProtectedResourceBluetooth ProtectedResource = -0x40000000
|
||||
ProtectedResourceKeyboardNetwork ProtectedResource = -0x40000001
|
||||
ProtectedResourceLocation ProtectedResource = -0x40000002
|
||||
ProtectedResourceHealth ProtectedResource = -0x40000003
|
||||
)
|
||||
|
||||
type Condition func(wd WebDriver) (bool, error)
|
||||
|
||||
type Direction string
|
||||
|
||||
const (
|
||||
DirectionUp Direction = "up"
|
||||
DirectionDown Direction = "down"
|
||||
DirectionLeft Direction = "left"
|
||||
DirectionRight Direction = "right"
|
||||
)
|
||||
|
||||
type PickerWheelOrder string
|
||||
|
||||
const (
|
||||
PickerWheelOrderNext PickerWheelOrder = "next"
|
||||
PickerWheelOrderPrevious PickerWheelOrder = "previous"
|
||||
)
|
||||
|
||||
type Point struct {
|
||||
X int `json:"x"` // upper left X coordinate of selected element
|
||||
Y int `json:"y"` // upper left Y coordinate of selected element
|
||||
}
|
||||
|
||||
type Rect struct {
|
||||
Point
|
||||
Size
|
||||
}
|
||||
|
||||
type DataOption func(data map[string]interface{})
|
||||
|
||||
func WithCustomOption(key string, value interface{}) DataOption {
|
||||
return func(data map[string]interface{}) {
|
||||
data[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
func WithPressDuration(duraion float64) DataOption {
|
||||
return func(data map[string]interface{}) {
|
||||
data["duration"] = duraion
|
||||
}
|
||||
}
|
||||
|
||||
func WithFrequency(frequency int) DataOption {
|
||||
return func(data map[string]interface{}) {
|
||||
data["frequency"] = frequency
|
||||
}
|
||||
}
|
||||
|
||||
// WebDriver defines methods supported by WebDriver drivers.
|
||||
type WebDriver interface {
|
||||
// NewSession starts a new session and returns the SessionInfo.
|
||||
NewSession(capabilities Capabilities) (SessionInfo, error)
|
||||
|
||||
ActiveSession() (SessionInfo, error)
|
||||
// DeleteSession Kills application associated with that session and removes session
|
||||
// 1) alertsMonitor disable
|
||||
// 2) testedApplicationBundleId terminate
|
||||
DeleteSession() error
|
||||
|
||||
Status() (DeviceStatus, error)
|
||||
|
||||
DeviceInfo() (DeviceInfo, error)
|
||||
|
||||
// Location Returns device location data.
|
||||
//
|
||||
// It requires to configure location access permission by manual.
|
||||
// The response of 'latitude', 'longitude' and 'altitude' are always zero (0) without authorization.
|
||||
// 'authorizationStatus' indicates current authorization status. '3' is 'Always'.
|
||||
// https://developer.apple.com/documentation/corelocation/clauthorizationstatus
|
||||
//
|
||||
// Settings -> Privacy -> Location Service -> WebDriverAgent-Runner -> Always
|
||||
//
|
||||
// The return value could be zero even if the permission is set to 'Always'
|
||||
// since the location service needs some time to update the location data.
|
||||
Location() (Location, error)
|
||||
BatteryInfo() (BatteryInfo, error)
|
||||
WindowSize() (Size, error)
|
||||
Screen() (Screen, error)
|
||||
Scale() (float64, error)
|
||||
ActiveAppInfo() (AppInfo, error)
|
||||
// ActiveAppsList Retrieves the information about the currently active apps
|
||||
ActiveAppsList() ([]AppBaseInfo, error)
|
||||
// AppState Get the state of the particular application in scope of the current session.
|
||||
// !This method is only returning reliable results since Xcode9 SDK
|
||||
AppState(bundleId string) (AppState, error)
|
||||
|
||||
// IsLocked Checks if the screen is locked or not.
|
||||
IsLocked() (bool, error)
|
||||
// Unlock Forces the device under test to unlock.
|
||||
// An immediate return will happen if the device is already unlocked
|
||||
// and an error is going to be thrown if the screen has not been unlocked after the timeout.
|
||||
Unlock() error
|
||||
// Lock Forces the device under test to switch to the lock screen.
|
||||
// An immediate return will happen if the device is already locked
|
||||
// and an error is going to be thrown if the screen has not been locked after the timeout.
|
||||
Lock() error
|
||||
|
||||
// Homescreen Forces the device under test to switch to the home screen
|
||||
Homescreen() error
|
||||
|
||||
// AlertText Returns alert's title and description separated by new lines
|
||||
AlertText() (string, error)
|
||||
// AlertButtons Gets the labels of the buttons visible in the alert
|
||||
AlertButtons() ([]string, error)
|
||||
// AlertAccept Accepts alert, if present
|
||||
AlertAccept(label ...string) error
|
||||
// AlertDismiss Dismisses alert, if present
|
||||
AlertDismiss(label ...string) error
|
||||
// AlertSendKeys Types a text into an input inside the alert container, if it is present
|
||||
AlertSendKeys(text string) error
|
||||
|
||||
// AppLaunch Launch an application with given bundle identifier in scope of current session.
|
||||
// !This method is only available since Xcode9 SDK
|
||||
AppLaunch(bundleId string, launchOpt ...AppLaunchOption) error
|
||||
// AppLaunchUnattached Launch the app with the specified bundle ID.
|
||||
AppLaunchUnattached(bundleId string) error
|
||||
// AppTerminate Terminate an application with the given bundle id.
|
||||
// Either `true` if the app has been successfully terminated or `false` if it was not running
|
||||
AppTerminate(bundleId string) (bool, error)
|
||||
// AppActivate Activate an application with given bundle identifier in scope of current session.
|
||||
// !This method is only available since Xcode9 SDK
|
||||
AppActivate(bundleId string) error
|
||||
// AppDeactivate Deactivates application for given time and then activate it again
|
||||
// The minimum application switch wait is 3 seconds
|
||||
AppDeactivate(second float64) error
|
||||
|
||||
// AppAuthReset Resets the authorization status for a protected resource. Available since Xcode 11.4
|
||||
AppAuthReset(ProtectedResource) error
|
||||
|
||||
// Tap Sends a tap event at the coordinate.
|
||||
Tap(x, y int, options ...DataOption) error
|
||||
TapFloat(x, y float64, options ...DataOption) error
|
||||
|
||||
// DoubleTap Sends a double tap event at the coordinate.
|
||||
DoubleTap(x, y int) error
|
||||
DoubleTapFloat(x, y float64) error
|
||||
|
||||
// TouchAndHold Initiates a long-press gesture at the coordinate, holding for the specified duration.
|
||||
// second: The default value is 1
|
||||
TouchAndHold(x, y int, second ...float64) error
|
||||
TouchAndHoldFloat(x, y float64, second ...float64) error
|
||||
|
||||
// Drag Initiates a press-and-hold gesture at the coordinate, then drags to another coordinate.
|
||||
// WithPressDuration option can be used to set pressForDuration (default to 1 second).
|
||||
Drag(fromX, fromY, toX, toY int, options ...DataOption) error
|
||||
DragFloat(fromX, fromY, toX, toY float64, options ...DataOption) error
|
||||
|
||||
// Swipe works like Drag, but `pressForDuration` value is 0
|
||||
Swipe(fromX, fromY, toX, toY int, options ...DataOption) error
|
||||
SwipeFloat(fromX, fromY, toX, toY float64, options ...DataOption) error
|
||||
|
||||
ForceTouch(x, y int, pressure float64, second ...float64) error
|
||||
ForceTouchFloat(x, y, pressure float64, second ...float64) error
|
||||
|
||||
// PerformW3CActions Perform complex touch action in scope of the current application.
|
||||
PerformW3CActions(actions *W3CActions) error
|
||||
PerformAppiumTouchActions(touchActs *TouchActions) error
|
||||
|
||||
// SetPasteboard Sets data to the general pasteboard
|
||||
SetPasteboard(contentType PasteboardType, content string) error
|
||||
// GetPasteboard Gets the data contained in the general pasteboard.
|
||||
// It worked when `WDA` was foreground. https://github.com/appium/WebDriverAgent/issues/330
|
||||
GetPasteboard(contentType PasteboardType) (raw *bytes.Buffer, err error)
|
||||
|
||||
// SendKeys Types a string into active element. There must be element with keyboard focus,
|
||||
// otherwise an error is raised.
|
||||
// WithFrequency option can be used to set frequency of typing (letters per sec). The default value is 60
|
||||
SendKeys(text string, options ...DataOption) error
|
||||
|
||||
// KeyboardDismiss Tries to dismiss the on-screen keyboard
|
||||
KeyboardDismiss(keyNames ...string) error
|
||||
|
||||
// PressButton Presses the corresponding hardware button on the device
|
||||
PressButton(devBtn DeviceButton) error
|
||||
|
||||
// IOHIDEvent Emulated triggering of the given low-level IOHID device event.
|
||||
// duration: The event duration in float seconds (XCTest uses 0.005 for a single press event)
|
||||
IOHIDEvent(pageID EventPageID, usageID EventUsageID, duration ...float64) error
|
||||
|
||||
// ExpectNotification Creates an expectation that is fulfilled when an expected Notification is received
|
||||
ExpectNotification(notifyName string, notifyType NotificationType, second ...int) error
|
||||
|
||||
// SiriActivate Activates Siri service voice recognition with the given text to parse
|
||||
SiriActivate(text string) error
|
||||
// SiriOpenUrl Opens the particular url scheme using Siri voice recognition helpers.
|
||||
// !This will only work since XCode 8.3/iOS 10.3
|
||||
// It doesn't actually work, right?
|
||||
SiriOpenUrl(url string) error
|
||||
|
||||
Orientation() (Orientation, error)
|
||||
// SetOrientation Sets requested device interface orientation.
|
||||
SetOrientation(Orientation) error
|
||||
|
||||
Rotation() (Rotation, error)
|
||||
// SetRotation Sets the devices orientation to the rotation passed.
|
||||
SetRotation(Rotation) error
|
||||
|
||||
// MatchTouchID Matches or mismatches TouchID request
|
||||
MatchTouchID(isMatch bool) error
|
||||
|
||||
// ActiveElement Returns the element, which currently holds the keyboard input focus or nil if there are no such elements.
|
||||
ActiveElement() (WebElement, error)
|
||||
FindElement(by BySelector) (WebElement, error)
|
||||
FindElements(by BySelector) ([]WebElement, error)
|
||||
|
||||
Screenshot() (*bytes.Buffer, error)
|
||||
|
||||
// Source Return application elements tree
|
||||
Source(srcOpt ...SourceOption) (string, error)
|
||||
// AccessibleSource Return application elements accessibility tree
|
||||
AccessibleSource() (string, error)
|
||||
|
||||
// HealthCheck Health check might modify simulator state so it should only be called in-between testing sessions
|
||||
// Checks health of XCTest by:
|
||||
// 1) Querying application for some elements,
|
||||
// 2) Triggering some device events.
|
||||
HealthCheck() error
|
||||
GetAppiumSettings() (map[string]interface{}, error)
|
||||
SetAppiumSettings(settings map[string]interface{}) (map[string]interface{}, error)
|
||||
|
||||
IsHealthy() (bool, error)
|
||||
|
||||
// WaitWithTimeoutAndInterval waits for the condition to evaluate to true.
|
||||
WaitWithTimeoutAndInterval(condition Condition, timeout, interval time.Duration) error
|
||||
// WaitWithTimeout works like WaitWithTimeoutAndInterval, but with default polling interval.
|
||||
WaitWithTimeout(condition Condition, timeout time.Duration) error
|
||||
// Wait works like WaitWithTimeoutAndInterval, but using the default timeout and polling interval.
|
||||
Wait(condition Condition) error
|
||||
|
||||
// Close inner connections properly
|
||||
Close() error
|
||||
}
|
||||
|
||||
// WebElement defines method supported by web elements.
|
||||
type WebElement interface {
|
||||
// Click Waits for element to become stable (not move) and performs sync tap on element.
|
||||
Click() error
|
||||
// SendKeys Types a text into element. It will try to activate keyboard on element,
|
||||
// if element has no keyboard focus.
|
||||
// frequency: Frequency of typing (letters per sec). The default value is 60
|
||||
SendKeys(text string, frequency ...int) error
|
||||
// Clear Clears text on element. It will try to activate keyboard on element,
|
||||
// if element has no keyboard focus.
|
||||
Clear() error
|
||||
|
||||
// Tap Waits for element to become stable (not move) and performs sync tap on element,
|
||||
// relative to the current element position
|
||||
Tap(x, y int) error
|
||||
TapFloat(x, y float64) error
|
||||
|
||||
// DoubleTap Sends a double tap event to a hittable point computed for the element.
|
||||
DoubleTap() error
|
||||
|
||||
// TouchAndHold Sends a long-press gesture to a hittable point computed for the element,
|
||||
// holding for the specified duration.
|
||||
// second: The default value is 1
|
||||
TouchAndHold(second ...float64) error
|
||||
// TwoFingerTap Sends a two finger tap event to a hittable point computed for the element.
|
||||
TwoFingerTap() error
|
||||
// TapWithNumberOfTaps Sends one or more taps with one or more touch points.
|
||||
TapWithNumberOfTaps(numberOfTaps, numberOfTouches int) error
|
||||
// ForceTouch Waits for element to become stable (not move) and performs sync force touch on element.
|
||||
// second: The default value is 1
|
||||
ForceTouch(pressure float64, second ...float64) error
|
||||
// ForceTouchFloat works like ForceTouch, but relative to the current element position
|
||||
ForceTouchFloat(x, y, pressure float64, second ...float64) error
|
||||
|
||||
// Drag Initiates a press-and-hold gesture at the coordinate, then drags to another coordinate.
|
||||
// relative to the current element position
|
||||
// pressForDuration: The default value is 1 second.
|
||||
Drag(fromX, fromY, toX, toY int, pressForDuration ...float64) error
|
||||
DragFloat(fromX, fromY, toX, toY float64, pressForDuration ...float64) error
|
||||
|
||||
// Swipe works like Drag, but `pressForDuration` value is 0.
|
||||
// relative to the current element position
|
||||
Swipe(fromX, fromY, toX, toY int) error
|
||||
SwipeFloat(fromX, fromY, toX, toY float64) error
|
||||
// SwipeDirection Performs swipe gesture on the element.
|
||||
// velocity: swipe speed in pixels per second. Custom velocity values are only supported since Xcode SDK 11.4.
|
||||
SwipeDirection(direction Direction, velocity ...float64) error
|
||||
|
||||
// Pinch Sends a pinching gesture with two touches.
|
||||
// scale: The scale of the pinch gesture. Use a scale between 0 and 1 to "pinch close" or zoom out
|
||||
// and a scale greater than 1 to "pinch open" or zoom in.
|
||||
// velocity: The velocity of the pinch in scale factor per second.
|
||||
Pinch(scale, velocity float64) error
|
||||
PinchToZoomOutByW3CAction(scale ...float64) error
|
||||
|
||||
// Rotate Sends a rotation gesture with two touches.
|
||||
// rotation: The rotation of the gesture in radians.
|
||||
// velocity: The velocity of the rotation gesture in radians per second.
|
||||
Rotate(rotation float64, velocity ...float64) error
|
||||
|
||||
// PickerWheelSelect
|
||||
// offset: The default value is 2
|
||||
PickerWheelSelect(order PickerWheelOrder, offset ...int) error
|
||||
|
||||
ScrollElementByName(name string) error
|
||||
ScrollElementByPredicate(predicate string) error
|
||||
ScrollToVisible() error
|
||||
// ScrollDirection
|
||||
// distance: The default value is 0.5
|
||||
ScrollDirection(direction Direction, distance ...float64) error
|
||||
|
||||
FindElement(by BySelector) (element WebElement, err error)
|
||||
FindElements(by BySelector) (elements []WebElement, err error)
|
||||
FindVisibleCells() (elements []WebElement, err error)
|
||||
|
||||
Rect() (rect Rect, err error)
|
||||
Location() (Point, error)
|
||||
Size() (Size, error)
|
||||
Text() (text string, err error)
|
||||
Type() (elemType string, err error)
|
||||
IsEnabled() (enabled bool, err error)
|
||||
IsDisplayed() (displayed bool, err error)
|
||||
IsSelected() (selected bool, err error)
|
||||
IsAccessible() (accessible bool, err error)
|
||||
IsAccessibilityContainer() (isAccessibilityContainer bool, err error)
|
||||
GetAttribute(attr ElementAttribute) (value string, err error)
|
||||
UID() (uid string)
|
||||
|
||||
Screenshot() (raw *bytes.Buffer, err error)
|
||||
}
|
||||
|
||||
@@ -1,187 +0,0 @@
|
||||
package uixt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"github.com/electricbubble/gwda"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/json"
|
||||
)
|
||||
|
||||
const (
|
||||
// Changes the value of maximum depth for traversing elements source tree.
|
||||
// It may help to prevent out of memory or timeout errors while getting the elements source tree,
|
||||
// but it might restrict the depth of source tree.
|
||||
// A part of elements source tree might be lost if the value was too small. Defaults to 50
|
||||
snapshotMaxDepth = 10
|
||||
// Allows to customize accept/dismiss alert button selector.
|
||||
// It helps you to handle an arbitrary element as accept button in accept alert command.
|
||||
// The selector should be a valid class chain expression, where the search root is the alert element itself.
|
||||
// The default button location algorithm is used if the provided selector is wrong or does not match any element.
|
||||
// e.g. **/XCUIElementTypeButton[`label CONTAINS[c] ‘accept’`]
|
||||
acceptAlertButtonSelector = "**/XCUIElementTypeButton[`label IN {'允许','好','仅在使用应用期间','稍后再说'}`]"
|
||||
dismissAlertButtonSelector = "**/XCUIElementTypeButton[`label IN {'不允许','暂不'}`]"
|
||||
)
|
||||
|
||||
type Options interface {
|
||||
UUID() string
|
||||
}
|
||||
|
||||
type WDAOptions struct {
|
||||
UDID string `json:"udid,omitempty" yaml:"udid,omitempty"`
|
||||
Port int `json:"port,omitempty" yaml:"port,omitempty"`
|
||||
MjpegPort int `json:"mjpeg_port,omitempty" yaml:"mjpeg_port,omitempty"`
|
||||
LogOn bool `json:"log_on,omitempty" yaml:"log_on,omitempty"`
|
||||
}
|
||||
|
||||
func (o WDAOptions) UUID() string {
|
||||
return o.UDID
|
||||
}
|
||||
|
||||
type WDAOption func(*WDAOptions)
|
||||
|
||||
func WithUDID(udid string) WDAOption {
|
||||
return func(device *WDAOptions) {
|
||||
device.UDID = udid
|
||||
}
|
||||
}
|
||||
|
||||
func WithPort(port int) WDAOption {
|
||||
return func(device *WDAOptions) {
|
||||
device.Port = port
|
||||
}
|
||||
}
|
||||
|
||||
func WithMjpegPort(port int) WDAOption {
|
||||
return func(device *WDAOptions) {
|
||||
device.MjpegPort = port
|
||||
}
|
||||
}
|
||||
|
||||
func WithLogOn(logOn bool) WDAOption {
|
||||
return func(device *WDAOptions) {
|
||||
device.LogOn = logOn
|
||||
}
|
||||
}
|
||||
|
||||
func InitWDAClient(options *WDAOptions) (*DriverExt, error) {
|
||||
var deviceOptions []gwda.DeviceOption
|
||||
if options.UDID != "" {
|
||||
deviceOptions = append(deviceOptions, gwda.WithSerialNumber(options.UDID))
|
||||
}
|
||||
if options.Port != 0 {
|
||||
deviceOptions = append(deviceOptions, gwda.WithPort(options.Port))
|
||||
}
|
||||
if options.MjpegPort != 0 {
|
||||
deviceOptions = append(deviceOptions, gwda.WithMjpegPort(options.MjpegPort))
|
||||
}
|
||||
|
||||
// init wda device
|
||||
targetDevice, err := gwda.NewDevice(deviceOptions...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// switch to iOS springboard before init WDA session
|
||||
// aviod getting stuck when some super app is activate such as douyin or wexin
|
||||
log.Info().Msg("switch to iOS springboard")
|
||||
bundleID := "com.apple.springboard"
|
||||
_, err = targetDevice.GIDevice().AppLaunch(bundleID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "launch springboard failed")
|
||||
}
|
||||
|
||||
// init WDA driver
|
||||
gwda.SetDebug(true)
|
||||
capabilities := gwda.NewCapabilities()
|
||||
capabilities.WithDefaultAlertAction(gwda.AlertActionAccept)
|
||||
driver, err := gwda.NewUSBDriver(capabilities, *targetDevice)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to init WDA driver")
|
||||
}
|
||||
driverExt, err := Extend(driver)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to extend gwda.WebDriver")
|
||||
}
|
||||
settings, err := driverExt.Driver.SetAppiumSettings(map[string]interface{}{
|
||||
"snapshotMaxDepth": snapshotMaxDepth,
|
||||
"acceptAlertButtonSelector": acceptAlertButtonSelector,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to set appium WDA settings")
|
||||
}
|
||||
log.Info().Interface("appiumWDASettings", settings).Msg("set appium WDA settings")
|
||||
|
||||
driverExt.host = fmt.Sprintf("http://127.0.0.1:%d", targetDevice.Port)
|
||||
if options.LogOn {
|
||||
err = driverExt.StartLogRecording("hrp_wda_log")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return driverExt, nil
|
||||
}
|
||||
|
||||
type wdaResponse struct {
|
||||
Value string `json:"value"`
|
||||
SessionID string `json:"sessionId"`
|
||||
}
|
||||
|
||||
func (dExt *DriverExt) StartLogRecording(identifier string) error {
|
||||
log.Info().Msg("start WDA log recording")
|
||||
data := map[string]interface{}{"action": "start", "type": 2, "identifier": identifier}
|
||||
_, err := dExt.triggerWDALog(data)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to start WDA log recording")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dExt *DriverExt) GetLogs() (string, error) {
|
||||
log.Info().Msg("stop WDA log recording")
|
||||
data := map[string]interface{}{"action": "stop"}
|
||||
reply, err := dExt.triggerWDALog(data)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to get WDA logs")
|
||||
}
|
||||
|
||||
return reply.Value, nil
|
||||
}
|
||||
|
||||
func (dExt *DriverExt) triggerWDALog(data map[string]interface{}) (*wdaResponse, error) {
|
||||
// [[FBRoute POST:@"/gtf/automation/log"].withoutSession respondWithTarget:self action:@selector(handleAutomationLog:)]
|
||||
postJSON, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/gtf/automation/log", dExt.host)
|
||||
log.Info().Str("url", url).Interface("data", data).Msg("trigger WDA log")
|
||||
resp, err := http.DefaultClient.Post(url, "application/json", bytes.NewBuffer(postJSON))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, errors.Errorf("failed to trigger wda log, response status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
rawResp, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reply := new(wdaResponse)
|
||||
if err = json.Unmarshal(rawResp, reply); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return reply, nil
|
||||
}
|
||||
373
hrp/internal/uixt/ios_action.go
Normal file
373
hrp/internal/uixt/ios_action.go
Normal file
@@ -0,0 +1,373 @@
|
||||
package uixt
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type W3CActions []map[string]interface{}
|
||||
|
||||
func NewW3CActions(capacity ...int) *W3CActions {
|
||||
if len(capacity) == 0 || capacity[0] <= 0 {
|
||||
capacity = []int{8}
|
||||
}
|
||||
tmp := make(W3CActions, 0, capacity[0])
|
||||
return &tmp
|
||||
}
|
||||
|
||||
func (act *W3CActions) SendKeys(text string) *W3CActions {
|
||||
keyboard := make(map[string]interface{})
|
||||
keyboard["type"] = "key"
|
||||
keyboard["id"] = "keyboard" + strconv.FormatInt(int64(len(*act)+1), 10)
|
||||
|
||||
ss := strings.Split(text, "")
|
||||
type KeyEvent struct {
|
||||
Type string `json:"type"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
actOptKey := make([]KeyEvent, 0, len(ss)+1)
|
||||
for i := range ss {
|
||||
actOptKey = append(
|
||||
actOptKey,
|
||||
KeyEvent{Type: "keyDown", Value: ss[i]},
|
||||
KeyEvent{Type: "keyUp", Value: ss[i]},
|
||||
)
|
||||
}
|
||||
keyboard["actions"] = actOptKey
|
||||
*act = append(*act, keyboard)
|
||||
return act
|
||||
}
|
||||
|
||||
func (act *W3CActions) _newFinger() map[string]interface{} {
|
||||
pointer := make(map[string]interface{})
|
||||
pointer["type"] = "pointer"
|
||||
pointer["id"] = "finger" + strconv.FormatInt(int64(len(*act)+1), 10)
|
||||
pointer["parameters"] = map[string]string{"pointerType": "touch"}
|
||||
return pointer
|
||||
}
|
||||
|
||||
func (act *W3CActions) FingerAction(fingerAct *FingerAction, fActs ...*FingerAction) *W3CActions {
|
||||
fActs = append([]*FingerAction{fingerAct}, fActs...)
|
||||
for i := range fActs {
|
||||
pointer := act._newFinger()
|
||||
pointer["actions"] = *fActs[i]
|
||||
*act = append(*act, pointer)
|
||||
}
|
||||
return act
|
||||
}
|
||||
|
||||
type FingerAction []map[string]interface{}
|
||||
|
||||
func NewFingerAction(capacity ...int) *FingerAction {
|
||||
if len(capacity) == 0 || capacity[0] <= 0 {
|
||||
capacity = []int{8}
|
||||
}
|
||||
tmp := make(FingerAction, 0, capacity[0])
|
||||
return &tmp
|
||||
}
|
||||
|
||||
type FingerMove map[string]interface{}
|
||||
|
||||
func NewFingerMove() FingerMove {
|
||||
return map[string]interface{}{"type": "pointerMove"}
|
||||
}
|
||||
|
||||
func (fm FingerMove) WithXY(x, y int) FingerMove {
|
||||
fm["x"] = x
|
||||
fm["y"] = y
|
||||
return fm
|
||||
}
|
||||
|
||||
func (fm FingerMove) WithXYFloat(x, y float64) FingerMove {
|
||||
fm["x"] = x
|
||||
fm["y"] = y
|
||||
return fm
|
||||
}
|
||||
|
||||
func (fm FingerMove) WithOrigin(element WebElement) FingerMove {
|
||||
fm["origin"] = element.UID()
|
||||
return fm
|
||||
}
|
||||
|
||||
func (fm FingerMove) WithDuration(second float64) FingerMove {
|
||||
fm["duration"] = second
|
||||
return fm
|
||||
}
|
||||
|
||||
func (fa *FingerAction) Move(fm FingerMove) *FingerAction {
|
||||
*fa = append(*fa, fm)
|
||||
return fa
|
||||
}
|
||||
|
||||
func (fa *FingerAction) Down() *FingerAction {
|
||||
*fa = append(*fa, map[string]interface{}{"type": "pointerDown"})
|
||||
return fa
|
||||
}
|
||||
|
||||
func (fa *FingerAction) Up() *FingerAction {
|
||||
*fa = append(*fa, map[string]interface{}{"type": "pointerUp"})
|
||||
return fa
|
||||
}
|
||||
|
||||
func (fa *FingerAction) Pause(second ...float64) *FingerAction {
|
||||
if len(second) == 0 || second[0] < 0 {
|
||||
second = []float64{0.5}
|
||||
}
|
||||
tmp := map[string]interface{}{
|
||||
"type": "pause",
|
||||
"duration": second[0] * 1000,
|
||||
}
|
||||
*fa = append(*fa, tmp)
|
||||
return fa
|
||||
}
|
||||
|
||||
func (act *W3CActions) Tap(x, y int, element ...WebElement) *W3CActions {
|
||||
fm := NewFingerMove().WithXY(x, y)
|
||||
if len(element) != 0 {
|
||||
fm.WithOrigin(element[0])
|
||||
}
|
||||
fingerAction := NewFingerAction().
|
||||
Move(fm).
|
||||
Down().
|
||||
Pause(0.1).
|
||||
Up()
|
||||
return act.FingerAction(fingerAction)
|
||||
}
|
||||
|
||||
func (act *W3CActions) DoubleTap(x, y int, element ...WebElement) *W3CActions {
|
||||
fm := NewFingerMove().WithXY(x, y)
|
||||
if len(element) != 0 {
|
||||
fm.WithOrigin(element[0])
|
||||
}
|
||||
fingerAction := NewFingerAction().
|
||||
Move(fm).
|
||||
Down().
|
||||
Pause(0.1).
|
||||
Up().
|
||||
Pause(0.04).
|
||||
Down().
|
||||
Pause(0.1).
|
||||
Up()
|
||||
return act.FingerAction(fingerAction)
|
||||
}
|
||||
|
||||
func (act *W3CActions) Press(x, y int, second float64, element ...WebElement) *W3CActions {
|
||||
fm := NewFingerMove().WithXY(x, y)
|
||||
if len(element) != 0 {
|
||||
fm.WithOrigin(element[0])
|
||||
}
|
||||
fingerAction := NewFingerAction().
|
||||
Move(fm).
|
||||
Down().
|
||||
Pause(second).
|
||||
Up()
|
||||
return act.FingerAction(fingerAction)
|
||||
}
|
||||
|
||||
func (act *W3CActions) Swipe(fromX, fromY, toX, toY int, element ...WebElement) *W3CActions {
|
||||
fmFrom := NewFingerMove().WithXY(fromX, fromY)
|
||||
fmTo := NewFingerMove().WithXY(toX, toY)
|
||||
if len(element) != 0 {
|
||||
fmFrom.WithOrigin(element[0])
|
||||
fmTo.WithOrigin(element[0])
|
||||
}
|
||||
fingerAction := NewFingerAction().
|
||||
Move(fmFrom).
|
||||
Down().
|
||||
Pause(0.25).
|
||||
Move(fmTo).
|
||||
Pause(0.25).
|
||||
Up()
|
||||
return act.FingerAction(fingerAction)
|
||||
}
|
||||
|
||||
func (act *W3CActions) SwipeFloat(fromX, fromY, toX, toY float64, element ...WebElement) *W3CActions {
|
||||
fmFrom := NewFingerMove().WithXYFloat(fromX, fromY)
|
||||
fmTo := NewFingerMove().WithXYFloat(toX, toY)
|
||||
if len(element) != 0 {
|
||||
fmFrom.WithOrigin(element[0])
|
||||
fmTo.WithOrigin(element[0])
|
||||
}
|
||||
fingerAction := NewFingerAction().
|
||||
Move(fmFrom).
|
||||
Down().
|
||||
Pause(0.25).
|
||||
Move(fmTo).
|
||||
Pause(0.25).
|
||||
Up()
|
||||
return act.FingerAction(fingerAction)
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------------------------------------------- */
|
||||
|
||||
type TouchActions []map[string]interface{}
|
||||
|
||||
func NewTouchActions(capacity ...int) *TouchActions {
|
||||
if len(capacity) == 0 || capacity[0] <= 0 {
|
||||
capacity = []int{8}
|
||||
}
|
||||
tmp := make(TouchActions, 0, capacity[0])
|
||||
return &tmp
|
||||
}
|
||||
|
||||
func (act *TouchActions) MoveTo(opt TouchActionMoveTo) *TouchActions {
|
||||
tmp := map[string]interface{}{
|
||||
"action": "moveTo",
|
||||
"options": opt,
|
||||
}
|
||||
*act = append(*act, tmp)
|
||||
return act
|
||||
}
|
||||
|
||||
func (act *TouchActions) Tap(opt TouchActionTap) *TouchActions {
|
||||
tmp := map[string]interface{}{
|
||||
"action": "tap",
|
||||
"options": opt,
|
||||
}
|
||||
*act = append(*act, tmp)
|
||||
return act
|
||||
}
|
||||
|
||||
func (act *TouchActions) Press(opt TouchActionPress) *TouchActions {
|
||||
tmp := map[string]interface{}{
|
||||
"action": "press",
|
||||
"options": opt,
|
||||
}
|
||||
*act = append(*act, tmp)
|
||||
return act
|
||||
}
|
||||
|
||||
func (act *TouchActions) LongPress(opt TouchActionLongPress) *TouchActions {
|
||||
tmp := map[string]interface{}{
|
||||
"action": "longPress",
|
||||
"options": opt,
|
||||
}
|
||||
*act = append(*act, tmp)
|
||||
return act
|
||||
}
|
||||
|
||||
func (act *TouchActions) Wait(second ...float64) *TouchActions {
|
||||
if len(second) == 0 || second[0] < 0 {
|
||||
second = []float64{0.5}
|
||||
}
|
||||
tmp := map[string]interface{}{
|
||||
"action": "wait",
|
||||
"options": map[string]interface{}{"ms": second[0] * 1000},
|
||||
}
|
||||
*act = append(*act, tmp)
|
||||
return act
|
||||
}
|
||||
|
||||
func (act *TouchActions) Release() *TouchActions {
|
||||
tmp := map[string]interface{}{"action": "release"}
|
||||
*act = append(*act, tmp)
|
||||
return act
|
||||
}
|
||||
|
||||
func (act *TouchActions) Cancel() *TouchActions {
|
||||
tmp := map[string]interface{}{"action": "cancel"}
|
||||
*act = append(*act, tmp)
|
||||
return act
|
||||
}
|
||||
|
||||
type TouchActionMoveTo map[string]interface{}
|
||||
|
||||
func NewTouchActionMoveTo() TouchActionMoveTo {
|
||||
return make(map[string]interface{})
|
||||
}
|
||||
|
||||
func (opt TouchActionMoveTo) WithXY(x, y int) TouchActionMoveTo {
|
||||
opt["x"] = x
|
||||
opt["y"] = y
|
||||
return opt
|
||||
}
|
||||
|
||||
func (opt TouchActionMoveTo) WithXYFloat(x, y float64) TouchActionMoveTo {
|
||||
opt["x"] = x
|
||||
opt["y"] = y
|
||||
return opt
|
||||
}
|
||||
|
||||
func (opt TouchActionMoveTo) WithElement(element WebElement) TouchActionMoveTo {
|
||||
opt["element"] = element.UID()
|
||||
return opt
|
||||
}
|
||||
|
||||
type TouchActionTap map[string]interface{}
|
||||
|
||||
func NewTouchActionTap() TouchActionTap {
|
||||
return make(map[string]interface{})
|
||||
}
|
||||
|
||||
func (opt TouchActionTap) WithXY(x, y int) TouchActionTap {
|
||||
opt["x"] = x
|
||||
opt["y"] = y
|
||||
return opt
|
||||
}
|
||||
|
||||
func (opt TouchActionTap) WithXYFloat(x, y float64) TouchActionTap {
|
||||
opt["x"] = x
|
||||
opt["y"] = y
|
||||
return opt
|
||||
}
|
||||
|
||||
func (opt TouchActionTap) WithElement(element WebElement) TouchActionTap {
|
||||
opt["element"] = element.UID()
|
||||
return opt
|
||||
}
|
||||
|
||||
func (opt TouchActionTap) WithCount(count int) TouchActionTap {
|
||||
opt["count"] = count
|
||||
return opt
|
||||
}
|
||||
|
||||
type TouchActionPress map[string]interface{}
|
||||
|
||||
func NewTouchActionPress() TouchActionPress {
|
||||
return make(map[string]interface{})
|
||||
}
|
||||
|
||||
func (opt TouchActionPress) WithXY(x, y int) TouchActionPress {
|
||||
opt["x"] = x
|
||||
opt["y"] = y
|
||||
return opt
|
||||
}
|
||||
|
||||
func (opt TouchActionPress) WithXYFloat(x, y float64) TouchActionPress {
|
||||
opt["x"] = x
|
||||
opt["y"] = y
|
||||
return opt
|
||||
}
|
||||
|
||||
func (opt TouchActionPress) WithElement(element WebElement) TouchActionPress {
|
||||
opt["element"] = element.UID()
|
||||
return opt
|
||||
}
|
||||
|
||||
func (opt TouchActionPress) WithPressure(pressure float64) TouchActionPress {
|
||||
opt["pressure"] = pressure
|
||||
return opt
|
||||
}
|
||||
|
||||
type TouchActionLongPress map[string]interface{}
|
||||
|
||||
func NewTouchActionLongPress() TouchActionLongPress {
|
||||
return make(map[string]interface{})
|
||||
}
|
||||
|
||||
func (opt TouchActionLongPress) WithXY(x, y int) TouchActionLongPress {
|
||||
opt["x"] = x
|
||||
opt["y"] = y
|
||||
return opt
|
||||
}
|
||||
|
||||
func (opt TouchActionLongPress) WithXYFloat(x, y float64) TouchActionLongPress {
|
||||
opt["x"] = x
|
||||
opt["y"] = y
|
||||
return opt
|
||||
}
|
||||
|
||||
func (opt TouchActionLongPress) WithElement(element WebElement) TouchActionLongPress {
|
||||
opt["element"] = element.UID()
|
||||
return opt
|
||||
}
|
||||
470
hrp/internal/uixt/ios_device.go
Normal file
470
hrp/internal/uixt/ios_device.go
Normal file
@@ -0,0 +1,470 @@
|
||||
package uixt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
builtinJSON "encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
giDevice "github.com/electricbubble/gidevice"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/json"
|
||||
)
|
||||
|
||||
const (
|
||||
// Changes the value of maximum depth for traversing elements source tree.
|
||||
// It may help to prevent out of memory or timeout errors while getting the elements source tree,
|
||||
// but it might restrict the depth of source tree.
|
||||
// A part of elements source tree might be lost if the value was too small. Defaults to 50
|
||||
snapshotMaxDepth = 10
|
||||
// Allows to customize accept/dismiss alert button selector.
|
||||
// It helps you to handle an arbitrary element as accept button in accept alert command.
|
||||
// The selector should be a valid class chain expression, where the search root is the alert element itself.
|
||||
// The default button location algorithm is used if the provided selector is wrong or does not match any element.
|
||||
// e.g. **/XCUIElementTypeButton[`label CONTAINS[c] ‘accept’`]
|
||||
acceptAlertButtonSelector = "**/XCUIElementTypeButton[`label IN {'允许','好','仅在使用应用期间','稍后再说'}`]"
|
||||
dismissAlertButtonSelector = "**/XCUIElementTypeButton[`label IN {'不允许','暂不'}`]"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultPort = 8100
|
||||
defaultMjpegPort = 9100
|
||||
)
|
||||
|
||||
func InitWDAClient(device *IOSDevice) (*DriverExt, error) {
|
||||
var deviceOptions []IOSDeviceOption
|
||||
if device.UDID != "" {
|
||||
deviceOptions = append(deviceOptions, WithUDID(device.UDID))
|
||||
}
|
||||
if device.Port != 0 {
|
||||
deviceOptions = append(deviceOptions, WithPort(device.Port))
|
||||
}
|
||||
if device.MjpegPort != 0 {
|
||||
deviceOptions = append(deviceOptions, WithMjpegPort(device.MjpegPort))
|
||||
}
|
||||
|
||||
// init wda device
|
||||
targetDevice, err := NewDevice(deviceOptions...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// switch to iOS springboard before init WDA session
|
||||
// aviod getting stuck when some super app is activate such as douyin or wexin
|
||||
log.Info().Msg("switch to iOS springboard")
|
||||
bundleID := "com.apple.springboard"
|
||||
_, err = targetDevice.GIDevice().AppLaunch(bundleID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "launch springboard failed")
|
||||
}
|
||||
|
||||
// init WDA driver
|
||||
capabilities := NewCapabilities()
|
||||
capabilities.WithDefaultAlertAction(AlertActionAccept)
|
||||
driver, err := NewUSBDriver(capabilities, *targetDevice)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to init WDA driver")
|
||||
}
|
||||
driverExt, err := Extend(driver)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to extend WebDriver")
|
||||
}
|
||||
settings, err := driverExt.Driver.SetAppiumSettings(map[string]interface{}{
|
||||
"snapshotMaxDepth": snapshotMaxDepth,
|
||||
"acceptAlertButtonSelector": acceptAlertButtonSelector,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to set appium WDA settings")
|
||||
}
|
||||
log.Info().Interface("appiumWDASettings", settings).Msg("set appium WDA settings")
|
||||
|
||||
driverExt.host = fmt.Sprintf("http://127.0.0.1:%d", targetDevice.Port)
|
||||
if device.LogOn {
|
||||
err = driverExt.StartLogRecording("hrp_wda_log")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return driverExt, nil
|
||||
}
|
||||
|
||||
type Device interface {
|
||||
UUID() string
|
||||
}
|
||||
|
||||
type IOSDevice struct {
|
||||
UDID string `json:"udid,omitempty" yaml:"udid,omitempty"`
|
||||
Port int `json:"port,omitempty" yaml:"port,omitempty"`
|
||||
MjpegPort int `json:"mjpeg_port,omitempty" yaml:"mjpeg_port,omitempty"`
|
||||
LogOn bool `json:"log_on,omitempty" yaml:"log_on,omitempty"`
|
||||
|
||||
d giDevice.Device
|
||||
}
|
||||
|
||||
func (d IOSDevice) UUID() string {
|
||||
return d.UDID
|
||||
}
|
||||
|
||||
func (d IOSDevice) GIDevice() giDevice.Device {
|
||||
return d.d
|
||||
}
|
||||
|
||||
type IOSDeviceOption func(*IOSDevice)
|
||||
|
||||
func WithUDID(udid string) IOSDeviceOption {
|
||||
return func(device *IOSDevice) {
|
||||
device.UDID = udid
|
||||
}
|
||||
}
|
||||
|
||||
func WithPort(port int) IOSDeviceOption {
|
||||
return func(device *IOSDevice) {
|
||||
device.Port = port
|
||||
}
|
||||
}
|
||||
|
||||
func WithMjpegPort(port int) IOSDeviceOption {
|
||||
return func(device *IOSDevice) {
|
||||
device.MjpegPort = port
|
||||
}
|
||||
}
|
||||
|
||||
func WithLogOn(logOn bool) IOSDeviceOption {
|
||||
return func(device *IOSDevice) {
|
||||
device.LogOn = logOn
|
||||
}
|
||||
}
|
||||
|
||||
func NewDevice(options ...IOSDeviceOption) (device *IOSDevice, err error) {
|
||||
var usbmux giDevice.Usbmux
|
||||
if usbmux, err = giDevice.NewUsbmux(); err != nil {
|
||||
return nil, fmt.Errorf("init usbmux failed: %v", err)
|
||||
}
|
||||
|
||||
var deviceList []giDevice.Device
|
||||
if deviceList, err = usbmux.Devices(); err != nil {
|
||||
return nil, fmt.Errorf("get attached devices failed: %v", err)
|
||||
}
|
||||
|
||||
device = &IOSDevice{
|
||||
Port: defaultPort,
|
||||
MjpegPort: defaultMjpegPort,
|
||||
}
|
||||
for _, option := range options {
|
||||
option(device)
|
||||
}
|
||||
|
||||
serialNumber := device.UDID
|
||||
for _, d := range deviceList {
|
||||
// find device by serial number if specified
|
||||
if serialNumber != "" && d.Properties().SerialNumber != serialNumber {
|
||||
continue
|
||||
}
|
||||
|
||||
device.UDID = d.Properties().SerialNumber
|
||||
device.d = d
|
||||
return device, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("device %s not found", device.UDID)
|
||||
}
|
||||
|
||||
func DeviceList() (devices []IOSDevice, err error) {
|
||||
var usbmux giDevice.Usbmux
|
||||
if usbmux, err = giDevice.NewUsbmux(); err != nil {
|
||||
return nil, fmt.Errorf("usbmuxd: %w", err)
|
||||
}
|
||||
|
||||
var deviceList []giDevice.Device
|
||||
if deviceList, err = usbmux.Devices(); err != nil {
|
||||
return nil, fmt.Errorf("device list: %w", err)
|
||||
}
|
||||
|
||||
devices = make([]IOSDevice, len(deviceList))
|
||||
|
||||
for i := range devices {
|
||||
devices[i].UDID = deviceList[i].Properties().SerialNumber
|
||||
devices[i].Port = defaultPort
|
||||
devices[i].MjpegPort = defaultMjpegPort
|
||||
devices[i].d = deviceList[i]
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// NewDriver creates new remote client, this will also start a new session.
|
||||
func NewDriver(capabilities Capabilities, urlPrefix string, mjpegPort ...int) (driver WebDriver, err error) {
|
||||
if len(mjpegPort) == 0 {
|
||||
mjpegPort = []int{defaultMjpegPort}
|
||||
}
|
||||
wd := new(remoteWD)
|
||||
if wd.urlPrefix, err = url.Parse(urlPrefix); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var sessionInfo SessionInfo
|
||||
if sessionInfo, err = wd.NewSession(capabilities); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wd.sessionId = sessionInfo.SessionId
|
||||
|
||||
if wd.mjpegConn, err = net.Dial("tcp", fmt.Sprintf("%s:%d", wd.urlPrefix.Hostname(), mjpegPort[0])); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wd.mjpegClient = convertToHTTPClient(wd.mjpegConn)
|
||||
|
||||
return wd, nil
|
||||
}
|
||||
|
||||
// NewUSBDriver creates new client via USB connected device, this will also start a new session.
|
||||
func NewUSBDriver(capabilities Capabilities, device ...IOSDevice) (driver WebDriver, err error) {
|
||||
if len(device) == 0 {
|
||||
if device, err = DeviceList(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(device) == 0 {
|
||||
return nil, errors.New("no device")
|
||||
}
|
||||
}
|
||||
dev := device[0]
|
||||
|
||||
wd := &remoteWD{
|
||||
usbCli: &struct {
|
||||
httpCli *http.Client
|
||||
defaultConn, mjpegConn giDevice.InnerConn
|
||||
sync.Mutex
|
||||
}{},
|
||||
}
|
||||
if wd.usbCli.defaultConn, err = dev.d.NewConnect(dev.Port, 0); err != nil {
|
||||
return nil, fmt.Errorf("create connection: %w", err)
|
||||
}
|
||||
wd.usbCli.httpCli = convertToHTTPClient(wd.usbCli.defaultConn.RawConn())
|
||||
|
||||
if wd.usbCli.mjpegConn, err = dev.d.NewConnect(dev.MjpegPort, 0); err != nil {
|
||||
return nil, fmt.Errorf("create connection MJPEG: %w", err)
|
||||
}
|
||||
wd.mjpegClient = convertToHTTPClient(wd.usbCli.mjpegConn.RawConn())
|
||||
|
||||
if wd.urlPrefix, err = url.Parse("http://" + dev.UDID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = wd.NewSession(capabilities)
|
||||
|
||||
go func() {
|
||||
if DefaultKeepAliveInterval <= 0 {
|
||||
return
|
||||
}
|
||||
ticker := time.NewTicker(DefaultKeepAliveInterval)
|
||||
for {
|
||||
<-ticker.C
|
||||
if healthy, err := wd.IsHealthy(); err != nil || !healthy {
|
||||
ticker.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return wd, err
|
||||
}
|
||||
|
||||
func newRequest(method string, url string, rawBody []byte) (request *http.Request, err error) {
|
||||
header := map[string]string{
|
||||
"Content-Type": "application/json;charset=UTF-8",
|
||||
"Accept": "application/json",
|
||||
}
|
||||
if request, err = http.NewRequest(method, url, bytes.NewBuffer(rawBody)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for k, v := range header {
|
||||
request.Header.Set(k, v)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func convertToHTTPClient(_conn net.Conn) *http.Client {
|
||||
return &http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
|
||||
return _conn, nil
|
||||
},
|
||||
},
|
||||
Timeout: 0,
|
||||
}
|
||||
}
|
||||
|
||||
type wdaResponse struct {
|
||||
Value string `json:"value"`
|
||||
SessionID string `json:"sessionId"`
|
||||
}
|
||||
|
||||
func (dExt *DriverExt) StartLogRecording(identifier string) error {
|
||||
log.Info().Msg("start WDA log recording")
|
||||
data := map[string]interface{}{"action": "start", "type": 2, "identifier": identifier}
|
||||
_, err := dExt.triggerWDALog(data)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to start WDA log recording")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dExt *DriverExt) GetLogs() (string, error) {
|
||||
log.Info().Msg("stop WDA log recording")
|
||||
data := map[string]interface{}{"action": "stop"}
|
||||
reply, err := dExt.triggerWDALog(data)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to get WDA logs")
|
||||
}
|
||||
|
||||
return reply.Value, nil
|
||||
}
|
||||
|
||||
func (dExt *DriverExt) triggerWDALog(data map[string]interface{}) (*wdaResponse, error) {
|
||||
// [[FBRoute POST:@"/gtf/automation/log"].withoutSession respondWithTarget:self action:@selector(handleAutomationLog:)]
|
||||
postJSON, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/gtf/automation/log", dExt.host)
|
||||
log.Info().Str("url", url).Interface("data", data).Msg("trigger WDA log")
|
||||
resp, err := http.DefaultClient.Post(url, "application/json", bytes.NewBuffer(postJSON))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, errors.Errorf("failed to trigger wda log, response status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
rawResp, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reply := new(wdaResponse)
|
||||
if err = json.Unmarshal(rawResp, reply); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return reply, nil
|
||||
}
|
||||
|
||||
type rawResponse []byte
|
||||
|
||||
func (r rawResponse) checkErr() (err error) {
|
||||
reply := new(struct {
|
||||
Value struct {
|
||||
Err string `json:"error"`
|
||||
Message string `json:"message"`
|
||||
Traceback string `json:"traceback"`
|
||||
}
|
||||
})
|
||||
if err = json.Unmarshal(r, reply); err != nil {
|
||||
return err
|
||||
}
|
||||
if reply.Value.Err != "" {
|
||||
errText := reply.Value.Message
|
||||
re := regexp.MustCompile(`{.+?=(.+?)}`)
|
||||
if re.MatchString(reply.Value.Message) {
|
||||
subMatch := re.FindStringSubmatch(reply.Value.Message)
|
||||
errText = subMatch[len(subMatch)-1]
|
||||
}
|
||||
return fmt.Errorf("%s: %s", reply.Value.Err, errText)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (r rawResponse) valueConvertToString() (s string, err error) {
|
||||
reply := new(struct{ Value string })
|
||||
if err = json.Unmarshal(r, reply); err != nil {
|
||||
return "", err
|
||||
}
|
||||
s = reply.Value
|
||||
return
|
||||
}
|
||||
|
||||
func (r rawResponse) valueConvertToBool() (b bool, err error) {
|
||||
reply := new(struct{ Value bool })
|
||||
if err = json.Unmarshal(r, reply); err != nil {
|
||||
return false, err
|
||||
}
|
||||
b = reply.Value
|
||||
return
|
||||
}
|
||||
|
||||
func (r rawResponse) valueConvertToSessionInfo() (sessionInfo SessionInfo, err error) {
|
||||
reply := new(struct{ Value struct{ SessionInfo } })
|
||||
if err = json.Unmarshal(r, reply); err != nil {
|
||||
return SessionInfo{}, err
|
||||
}
|
||||
sessionInfo = reply.Value.SessionInfo
|
||||
return
|
||||
}
|
||||
|
||||
func (r rawResponse) valueConvertToJsonRawMessage() (raw builtinJSON.RawMessage, err error) {
|
||||
reply := new(struct{ Value builtinJSON.RawMessage })
|
||||
if err = json.Unmarshal(r, reply); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
raw = reply.Value
|
||||
return
|
||||
}
|
||||
|
||||
func (r rawResponse) valueDecodeAsBase64() (raw *bytes.Buffer, err error) {
|
||||
var str string
|
||||
if str, err = r.valueConvertToString(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var decodeString []byte
|
||||
if decodeString, err = base64.StdEncoding.DecodeString(str); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
raw = bytes.NewBuffer(decodeString)
|
||||
return
|
||||
}
|
||||
|
||||
var errNoSuchElement = errors.New("no such element")
|
||||
|
||||
func (r rawResponse) valueConvertToElementID() (id string, err error) {
|
||||
reply := new(struct{ Value map[string]string })
|
||||
if err = json.Unmarshal(r, reply); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(reply.Value) == 0 {
|
||||
return "", errNoSuchElement
|
||||
}
|
||||
if id = elementIDFromValue(reply.Value); id == "" {
|
||||
return "", fmt.Errorf("invalid element returned: %+v", reply)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (r rawResponse) valueConvertToElementIDs() (IDs []string, err error) {
|
||||
reply := new(struct{ Value []map[string]string })
|
||||
if err = json.Unmarshal(r, reply); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(reply.Value) == 0 {
|
||||
return nil, errNoSuchElement
|
||||
}
|
||||
IDs = make([]string, 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)
|
||||
}
|
||||
IDs[i] = id
|
||||
}
|
||||
return
|
||||
}
|
||||
1187
hrp/internal/uixt/ios_test.go
Normal file
1187
hrp/internal/uixt/ios_test.go
Normal file
File diff suppressed because it is too large
Load Diff
928
hrp/internal/uixt/ios_webdriver.go
Normal file
928
hrp/internal/uixt/ios_webdriver.go
Normal file
@@ -0,0 +1,928 @@
|
||||
package uixt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
builtinJSON "encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
giDevice "github.com/electricbubble/gidevice"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/json"
|
||||
)
|
||||
|
||||
// var _ WebDriver = (*remoteWD)(nil)
|
||||
|
||||
type remoteWD struct {
|
||||
urlPrefix *url.URL
|
||||
sessionId string
|
||||
|
||||
usbCli *struct {
|
||||
httpCli *http.Client
|
||||
defaultConn, mjpegConn giDevice.InnerConn
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
mjpegClient *http.Client
|
||||
mjpegConn net.Conn
|
||||
}
|
||||
|
||||
func (wd *remoteWD) _requestURL(tmpURL *url.URL, elem ...string) string {
|
||||
var tmp *url.URL
|
||||
if tmpURL == nil {
|
||||
tmpURL = wd.urlPrefix
|
||||
}
|
||||
tmp, _ = url.Parse(tmpURL.String())
|
||||
tmp.Path = path.Join(append([]string{tmpURL.Path}, elem...)...)
|
||||
return tmp.String()
|
||||
}
|
||||
|
||||
func (wd *remoteWD) executeGet(pathElem ...string) (rawResp rawResponse, err error) {
|
||||
return wd.executeHTTP(http.MethodGet, wd._requestURL(nil, pathElem...), nil)
|
||||
}
|
||||
|
||||
func (wd *remoteWD) executePost(data interface{}, pathElem ...string) (rawResp rawResponse, err error) {
|
||||
var bsJSON []byte = nil
|
||||
if data != nil {
|
||||
if bsJSON, err = json.Marshal(data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return wd.executeHTTP(http.MethodPost, wd._requestURL(nil, pathElem...), bsJSON)
|
||||
}
|
||||
|
||||
func (wd *remoteWD) executeDelete(pathElem ...string) (rawResp rawResponse, err error) {
|
||||
return wd.executeHTTP(http.MethodDelete, wd._requestURL(nil, pathElem...), nil)
|
||||
}
|
||||
|
||||
func (wd *remoteWD) GetMjpegHTTPClient() *http.Client {
|
||||
return wd.mjpegClient
|
||||
}
|
||||
|
||||
func (wd *remoteWD) Close() error {
|
||||
if wd.usbCli == nil {
|
||||
wd.mjpegClient.CloseIdleConnections()
|
||||
return wd.mjpegConn.Close()
|
||||
}
|
||||
|
||||
wd.usbCli.Lock()
|
||||
defer wd.usbCli.Unlock()
|
||||
|
||||
if wd.usbCli.defaultConn != nil {
|
||||
wd.usbCli.defaultConn.Close()
|
||||
}
|
||||
if wd.usbCli.mjpegConn != nil {
|
||||
wd.usbCli.mjpegConn.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (wd *remoteWD) NewSession(capabilities Capabilities) (sessionInfo SessionInfo, err error) {
|
||||
// [[FBRoute POST:@"/session"].withoutSession respondWithTarget:self action:@selector(handleCreateSession:)]
|
||||
data := make(map[string]interface{})
|
||||
if len(capabilities) == 0 {
|
||||
data["capabilities"] = make(map[string]interface{})
|
||||
} else {
|
||||
data["capabilities"] = map[string]interface{}{"alwaysMatch": capabilities}
|
||||
}
|
||||
|
||||
var rawResp rawResponse
|
||||
if rawResp, err = wd.executePost(data, "/session"); err != nil {
|
||||
return SessionInfo{}, err
|
||||
}
|
||||
if sessionInfo, err = rawResp.valueConvertToSessionInfo(); err != nil {
|
||||
return SessionInfo{}, err
|
||||
}
|
||||
wd.sessionId = sessionInfo.SessionId
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *remoteWD) ActiveSession() (sessionInfo SessionInfo, err error) {
|
||||
// [[FBRoute GET:@""] respondWithTarget:self action:@selector(handleGetActiveSession:)]
|
||||
var rawResp rawResponse
|
||||
if rawResp, err = wd.executeGet("/session", wd.sessionId); err != nil {
|
||||
return SessionInfo{}, err
|
||||
}
|
||||
if sessionInfo, err = rawResp.valueConvertToSessionInfo(); err != nil {
|
||||
return SessionInfo{}, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *remoteWD) DeleteSession() (err error) {
|
||||
// [[FBRoute DELETE:@""] respondWithTarget:self action:@selector(handleDeleteSession:)]
|
||||
_, err = wd.executeDelete("/session", wd.sessionId)
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *remoteWD) Status() (deviceStatus DeviceStatus, err error) {
|
||||
// [[FBRoute GET:@"/status"].withoutSession respondWithTarget:self action:@selector(handleGetStatus:)]
|
||||
var rawResp rawResponse
|
||||
if rawResp, err = wd.executeGet("/status"); err != nil {
|
||||
return DeviceStatus{}, err
|
||||
}
|
||||
reply := new(struct{ Value struct{ DeviceStatus } })
|
||||
if err = json.Unmarshal(rawResp, reply); err != nil {
|
||||
return DeviceStatus{}, err
|
||||
}
|
||||
deviceStatus = reply.Value.DeviceStatus
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *remoteWD) DeviceInfo() (deviceInfo DeviceInfo, err error) {
|
||||
// [[FBRoute GET:@"/wda/device/info"] respondWithTarget:self action:@selector(handleGetDeviceInfo:)]
|
||||
// [[FBRoute GET:@"/wda/device/info"].withoutSession
|
||||
var rawResp rawResponse
|
||||
if rawResp, err = wd.executeGet("/session", wd.sessionId, "/wda/device/info"); err != nil {
|
||||
return DeviceInfo{}, err
|
||||
}
|
||||
reply := new(struct{ Value struct{ DeviceInfo } })
|
||||
if err = json.Unmarshal(rawResp, reply); err != nil {
|
||||
return DeviceInfo{}, err
|
||||
}
|
||||
deviceInfo = reply.Value.DeviceInfo
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *remoteWD) Location() (location Location, err error) {
|
||||
// [[FBRoute GET:@"/wda/device/location"] respondWithTarget:self action:@selector(handleGetLocation:)]
|
||||
// [[FBRoute GET:@"/wda/device/location"].withoutSession
|
||||
var rawResp rawResponse
|
||||
if rawResp, err = wd.executeGet("/session", wd.sessionId, "/wda/device/location"); err != nil {
|
||||
return Location{}, err
|
||||
}
|
||||
reply := new(struct{ Value struct{ Location } })
|
||||
if err = json.Unmarshal(rawResp, reply); err != nil {
|
||||
return Location{}, err
|
||||
}
|
||||
location = reply.Value.Location
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *remoteWD) BatteryInfo() (batteryInfo BatteryInfo, err error) {
|
||||
// [[FBRoute GET:@"/wda/batteryInfo"] respondWithTarget:self action:@selector(handleGetBatteryInfo:)]
|
||||
var rawResp rawResponse
|
||||
if rawResp, err = wd.executeGet("/session", wd.sessionId, "/wda/batteryInfo"); err != nil {
|
||||
return BatteryInfo{}, err
|
||||
}
|
||||
reply := new(struct{ Value struct{ BatteryInfo } })
|
||||
if err = json.Unmarshal(rawResp, reply); err != nil {
|
||||
return BatteryInfo{}, err
|
||||
}
|
||||
batteryInfo = reply.Value.BatteryInfo
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *remoteWD) WindowSize() (size Size, err error) {
|
||||
// [[FBRoute GET:@"/window/size"] respondWithTarget:self action:@selector(handleGetWindowSize:)]
|
||||
var rawResp rawResponse
|
||||
if rawResp, err = wd.executeGet("/session", wd.sessionId, "/window/size"); err != nil {
|
||||
return Size{}, err
|
||||
}
|
||||
reply := new(struct{ Value struct{ Size } })
|
||||
if err = json.Unmarshal(rawResp, reply); err != nil {
|
||||
return Size{}, err
|
||||
}
|
||||
size = reply.Value.Size
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *remoteWD) Screen() (screen Screen, err error) {
|
||||
// [[FBRoute GET:@"/wda/screen"] respondWithTarget:self action:@selector(handleGetScreen:)]
|
||||
var rawResp rawResponse
|
||||
if rawResp, err = wd.executeGet("/session", wd.sessionId, "/wda/screen"); err != nil {
|
||||
return Screen{}, err
|
||||
}
|
||||
reply := new(struct{ Value struct{ Screen } })
|
||||
if err = json.Unmarshal(rawResp, reply); err != nil {
|
||||
return Screen{}, err
|
||||
}
|
||||
screen = reply.Value.Screen
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *remoteWD) Scale() (float64, error) {
|
||||
screen, err := wd.Screen()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return screen.Scale, nil
|
||||
}
|
||||
|
||||
func (wd *remoteWD) ActiveAppInfo() (info AppInfo, err error) {
|
||||
// [[FBRoute GET:@"/wda/activeAppInfo"] respondWithTarget:self action:@selector(handleActiveAppInfo:)]
|
||||
// [[FBRoute GET:@"/wda/activeAppInfo"].withoutSession
|
||||
var rawResp rawResponse
|
||||
if rawResp, err = wd.executeGet("/session", wd.sessionId, "/wda/activeAppInfo"); err != nil {
|
||||
return AppInfo{}, err
|
||||
}
|
||||
reply := new(struct{ Value struct{ AppInfo } })
|
||||
if err = json.Unmarshal(rawResp, reply); err != nil {
|
||||
return AppInfo{}, err
|
||||
}
|
||||
info = reply.Value.AppInfo
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *remoteWD) ActiveAppsList() (appsList []AppBaseInfo, err error) {
|
||||
// [[FBRoute GET:@"/wda/apps/list"] respondWithTarget:self action:@selector(handleGetActiveAppsList:)]
|
||||
var rawResp rawResponse
|
||||
if rawResp, err = wd.executeGet("/session", wd.sessionId, "/wda/apps/list"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reply := new(struct{ Value []AppBaseInfo })
|
||||
if err = json.Unmarshal(rawResp, reply); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
appsList = reply.Value
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *remoteWD) AppState(bundleId string) (runState AppState, err error) {
|
||||
// [[FBRoute POST:@"/wda/apps/state"] respondWithTarget:self action:@selector(handleSessionAppState:)]
|
||||
data := map[string]interface{}{"bundleId": bundleId}
|
||||
var rawResp rawResponse
|
||||
if rawResp, err = wd.executePost(data, "/session", wd.sessionId, "/wda/apps/state"); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
reply := new(struct{ Value AppState })
|
||||
if err = json.Unmarshal(rawResp, reply); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
runState = reply.Value
|
||||
_ = rawResp
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *remoteWD) IsLocked() (locked bool, err error) {
|
||||
// [[FBRoute GET:@"/wda/locked"] respondWithTarget:self action:@selector(handleIsLocked:)]
|
||||
// [[FBRoute GET:@"/wda/locked"].withoutSession
|
||||
var rawResp rawResponse
|
||||
if rawResp, err = wd.executeGet("/session", wd.sessionId, "/wda/locked"); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if locked, err = rawResp.valueConvertToBool(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *remoteWD) Unlock() (err error) {
|
||||
// [[FBRoute POST:@"/wda/unlock"] respondWithTarget:self action:@selector(handleUnlock:)]
|
||||
// [[FBRoute POST:@"/wda/unlock"].withoutSession
|
||||
_, err = wd.executePost(nil, "/session", wd.sessionId, "/wda/unlock")
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *remoteWD) Lock() (err error) {
|
||||
// [[FBRoute POST:@"/wda/lock"] respondWithTarget:self action:@selector(handleLock:)]
|
||||
// [[FBRoute POST:@"/wda/lock"].withoutSession
|
||||
_, err = wd.executePost(nil, "/session", wd.sessionId, "/wda/lock")
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *remoteWD) Homescreen() (err error) {
|
||||
// [[FBRoute POST:@"/wda/homescreen"].withoutSession respondWithTarget:self action:@selector(handleHomescreenCommand:)]
|
||||
_, err = wd.executePost(nil, "/wda/homescreen")
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *remoteWD) AlertText() (text string, err error) {
|
||||
// [[FBRoute GET:@"/alert/text"] respondWithTarget:self action:@selector(handleAlertGetTextCommand:)]
|
||||
// [[FBRoute GET:@"/alert/text"].withoutSession
|
||||
var rawResp rawResponse
|
||||
if rawResp, err = wd.executeGet("/session", wd.sessionId, "/alert/text"); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if text, err = rawResp.valueConvertToString(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *remoteWD) AlertButtons() (btnLabels []string, err error) {
|
||||
// [[FBRoute GET:@"/wda/alert/buttons"] respondWithTarget:self action:@selector(handleGetAlertButtonsCommand:)]
|
||||
var rawResp rawResponse
|
||||
if rawResp, err = wd.executeGet("/session", wd.sessionId, "/wda/alert/buttons"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reply := new(struct{ Value []string })
|
||||
if err = json.Unmarshal(rawResp, reply); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
btnLabels = reply.Value
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *remoteWD) AlertAccept(label ...string) (err error) {
|
||||
// [[FBRoute POST:@"/alert/accept"] respondWithTarget:self action:@selector(handleAlertAcceptCommand:)]
|
||||
// [[FBRoute POST:@"/alert/accept"].withoutSession
|
||||
data := make(map[string]interface{})
|
||||
if len(label) != 0 && label[0] != "" {
|
||||
data["name"] = label[0]
|
||||
}
|
||||
_, err = wd.executePost(data, "/alert/accept")
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *remoteWD) AlertDismiss(label ...string) (err error) {
|
||||
// [[FBRoute POST:@"/alert/dismiss"] respondWithTarget:self action:@selector(handleAlertDismissCommand:)]
|
||||
// [[FBRoute POST:@"/alert/dismiss"].withoutSession
|
||||
data := make(map[string]interface{})
|
||||
if len(label) != 0 && label[0] != "" {
|
||||
data["name"] = label[0]
|
||||
}
|
||||
_, err = wd.executePost(data, "/alert/dismiss")
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *remoteWD) AlertSendKeys(text string) (err error) {
|
||||
// [[FBRoute POST:@"/alert/text"] respondWithTarget:self action:@selector(handleAlertSetTextCommand:)]
|
||||
data := map[string]interface{}{"value": strings.Split(text, "")}
|
||||
_, err = wd.executePost(data, "/session", wd.sessionId, "/alert/text")
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *remoteWD) AppLaunch(bundleId string, launchOpt ...AppLaunchOption) (err error) {
|
||||
// [[FBRoute POST:@"/wda/apps/launch"] respondWithTarget:self action:@selector(handleSessionAppLaunch:)]
|
||||
data := make(map[string]interface{})
|
||||
if len(launchOpt) != 0 {
|
||||
data = launchOpt[0]
|
||||
}
|
||||
data["bundleId"] = bundleId
|
||||
_, err = wd.executePost(data, "/session", wd.sessionId, "/wda/apps/launch")
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *remoteWD) AppLaunchUnattached(bundleId string) (err error) {
|
||||
// [[FBRoute POST:@"/wda/apps/launchUnattached"].withoutSession respondWithTarget:self action:@selector(handleLaunchUnattachedApp:)]
|
||||
data := map[string]interface{}{"bundleId": bundleId}
|
||||
_, err = wd.executePost(data, "/wda/apps/launchUnattached")
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *remoteWD) AppTerminate(bundleId string) (successful bool, err error) {
|
||||
// [[FBRoute POST:@"/wda/apps/terminate"] respondWithTarget:self action:@selector(handleSessionAppTerminate:)]
|
||||
data := map[string]interface{}{"bundleId": bundleId}
|
||||
var rawResp rawResponse
|
||||
if rawResp, err = wd.executePost(data, "/session", wd.sessionId, "/wda/apps/terminate"); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if successful, err = rawResp.valueConvertToBool(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *remoteWD) AppActivate(bundleId string) (err error) {
|
||||
// [[FBRoute POST:@"/wda/apps/activate"] respondWithTarget:self action:@selector(handleSessionAppActivate:)]
|
||||
data := map[string]interface{}{"bundleId": bundleId}
|
||||
_, err = wd.executePost(data, "/session", wd.sessionId, "/wda/apps/activate")
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *remoteWD) AppDeactivate(second float64) (err error) {
|
||||
// [[FBRoute POST:@"/wda/deactivateApp"] respondWithTarget:self action:@selector(handleDeactivateAppCommand:)]
|
||||
if second < 3 {
|
||||
second = 3.0
|
||||
}
|
||||
data := map[string]interface{}{"duration": second}
|
||||
_, err = wd.executePost(data, "/session", wd.sessionId, "/wda/deactivateApp")
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *remoteWD) AppAuthReset(resource ProtectedResource) (err error) {
|
||||
// [[FBRoute POST:@"/wda/resetAppAuth"] respondWithTarget:self action:@selector(handleResetAppAuth:)]
|
||||
data := map[string]interface{}{"resource": resource}
|
||||
_, err = wd.executePost(data, "/session", wd.sessionId, "/wda/resetAppAuth")
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *remoteWD) Tap(x, y int, options ...DataOption) error {
|
||||
return wd.TapFloat(float64(x), float64(y), options...)
|
||||
}
|
||||
|
||||
func (wd *remoteWD) TapFloat(x, y float64, options ...DataOption) (err error) {
|
||||
// [[FBRoute POST:@"/wda/tap/:uuid"] respondWithTarget:self action:@selector(handleTap:)]
|
||||
data := map[string]interface{}{
|
||||
"x": x,
|
||||
"y": y,
|
||||
}
|
||||
// append options in post data for extra WDA configurations
|
||||
// e.g. add identifier in tap event logs
|
||||
for _, option := range options {
|
||||
option(data)
|
||||
}
|
||||
_, err = wd.executePost(data, "/session", wd.sessionId, "/wda/tap/0")
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *remoteWD) DoubleTap(x, y int) error {
|
||||
return wd.DoubleTapFloat(float64(x), float64(y))
|
||||
}
|
||||
|
||||
func (wd *remoteWD) DoubleTapFloat(x, y float64) (err error) {
|
||||
// [[FBRoute POST:@"/wda/doubleTap"] respondWithTarget:self action:@selector(handleDoubleTapCoordinate:)]
|
||||
data := map[string]interface{}{
|
||||
"x": x,
|
||||
"y": y,
|
||||
}
|
||||
_, err = wd.executePost(data, "/session", wd.sessionId, "/wda/doubleTap")
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *remoteWD) TouchAndHold(x, y int, second ...float64) error {
|
||||
return wd.TouchAndHoldFloat(float64(x), float64(y), second...)
|
||||
}
|
||||
|
||||
func (wd *remoteWD) TouchAndHoldFloat(x, y float64, second ...float64) (err error) {
|
||||
// [[FBRoute POST:@"/wda/touchAndHold"] respondWithTarget:self action:@selector(handleTouchAndHoldCoordinate:)]
|
||||
data := map[string]interface{}{
|
||||
"x": x,
|
||||
"y": y,
|
||||
}
|
||||
if len(second) == 0 || second[0] <= 0 {
|
||||
second = []float64{1.0}
|
||||
}
|
||||
data["duration"] = second[0]
|
||||
_, err = wd.executePost(data, "/session", wd.sessionId, "/wda/touchAndHold")
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *remoteWD) Drag(fromX, fromY, toX, toY int, options ...DataOption) error {
|
||||
return wd.DragFloat(float64(fromX), float64(fromY), float64(toX), float64(toY), options...)
|
||||
}
|
||||
|
||||
func (wd *remoteWD) DragFloat(fromX, fromY, toX, toY float64, options ...DataOption) (err error) {
|
||||
// [[FBRoute POST:@"/wda/dragfromtoforduration"] respondWithTarget:self action:@selector(handleDragCoordinate:)]
|
||||
data := map[string]interface{}{
|
||||
"fromX": fromX,
|
||||
"fromY": fromY,
|
||||
"toX": toX,
|
||||
"toY": toY,
|
||||
}
|
||||
|
||||
// append options in post data for extra WDA configurations
|
||||
// e.g. use WithPressDuration to set pressForDuration
|
||||
for _, option := range options {
|
||||
option(data)
|
||||
}
|
||||
|
||||
if _, ok := data["duration"]; !ok {
|
||||
data["duration"] = 1.0 // default duration
|
||||
}
|
||||
_, err = wd.executePost(data, "/session", wd.sessionId, "/wda/dragfromtoforduration")
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *remoteWD) Swipe(fromX, fromY, toX, toY int, options ...DataOption) error {
|
||||
options = append(options, WithPressDuration(0))
|
||||
return wd.SwipeFloat(float64(fromX), float64(fromY), float64(toX), float64(toY), options...)
|
||||
}
|
||||
|
||||
func (wd *remoteWD) SwipeFloat(fromX, fromY, toX, toY float64, options ...DataOption) error {
|
||||
options = append(options, WithPressDuration(0))
|
||||
return wd.DragFloat(fromX, fromY, toX, toY, options...)
|
||||
}
|
||||
|
||||
func (wd *remoteWD) ForceTouch(x, y int, pressure float64, second ...float64) error {
|
||||
return wd.ForceTouchFloat(float64(x), float64(y), pressure, second...)
|
||||
}
|
||||
|
||||
func (wd *remoteWD) ForceTouchFloat(x, y, pressure float64, second ...float64) error {
|
||||
if len(second) == 0 || second[0] <= 0 {
|
||||
second = []float64{1.0}
|
||||
}
|
||||
actions := NewTouchActions().
|
||||
Press(
|
||||
NewTouchActionPress().WithXYFloat(x, y).WithPressure(pressure)).
|
||||
Wait(second[0]).
|
||||
Release()
|
||||
return wd.PerformAppiumTouchActions(actions)
|
||||
}
|
||||
|
||||
func (wd *remoteWD) PerformW3CActions(actions *W3CActions) (err error) {
|
||||
// [[FBRoute POST:@"/actions"] respondWithTarget:self action:@selector(handlePerformW3CTouchActions:)]
|
||||
data := map[string]interface{}{"actions": actions}
|
||||
_, err = wd.executePost(data, "/session", wd.sessionId, "/actions")
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *remoteWD) PerformAppiumTouchActions(touchActs *TouchActions) (err error) {
|
||||
// [[FBRoute POST:@"/wda/touch/perform"] respondWithTarget:self action:@selector(handlePerformAppiumTouchActions:)]
|
||||
// [[FBRoute POST:@"/wda/touch/multi/perform"]
|
||||
data := map[string]interface{}{"actions": touchActs}
|
||||
_, err = wd.executePost(data, "/session", wd.sessionId, "/wda/touch/multi/perform")
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *remoteWD) SetPasteboard(contentType PasteboardType, content string) (err error) {
|
||||
// [[FBRoute POST:@"/wda/setPasteboard"] respondWithTarget:self action:@selector(handleSetPasteboard:)]
|
||||
data := map[string]interface{}{
|
||||
"contentType": contentType,
|
||||
"content": base64.StdEncoding.EncodeToString([]byte(content)),
|
||||
}
|
||||
_, err = wd.executePost(data, "/session", wd.sessionId, "/wda/setPasteboard")
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *remoteWD) GetPasteboard(contentType PasteboardType) (raw *bytes.Buffer, err error) {
|
||||
// [[FBRoute POST:@"/wda/getPasteboard"] respondWithTarget:self action:@selector(handleGetPasteboard:)]
|
||||
data := map[string]interface{}{"contentType": contentType}
|
||||
var rawResp rawResponse
|
||||
if rawResp, err = wd.executePost(data, "/session", wd.sessionId, "/wda/getPasteboard"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if raw, err = rawResp.valueDecodeAsBase64(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *remoteWD) SendKeys(text string, options ...DataOption) (err error) {
|
||||
// [[FBRoute POST:@"/wda/keys"] respondWithTarget:self action:@selector(handleKeys:)]
|
||||
data := map[string]interface{}{"value": strings.Split(text, "")}
|
||||
|
||||
// append options in post data for extra WDA configurations
|
||||
// e.g. use WithFrequency to set frequency of typing
|
||||
for _, option := range options {
|
||||
option(data)
|
||||
}
|
||||
|
||||
if _, ok := data["frequency"]; !ok {
|
||||
data["frequency"] = 60 // default frequency
|
||||
}
|
||||
_, err = wd.executePost(data, "/session", wd.sessionId, "/wda/keys")
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *remoteWD) KeyboardDismiss(keyNames ...string) (err error) {
|
||||
// [[FBRoute POST:@"/wda/keyboard/dismiss"] respondWithTarget:self action:@selector(handleDismissKeyboardCommand:)]
|
||||
if len(keyNames) == 0 {
|
||||
keyNames = []string{"return"}
|
||||
}
|
||||
data := map[string]interface{}{"keyNames": keyNames}
|
||||
_, err = wd.executePost(data, "/session", wd.sessionId, "/wda/keyboard/dismiss")
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *remoteWD) PressButton(devBtn DeviceButton) (err error) {
|
||||
// [[FBRoute POST:@"/wda/pressButton"] respondWithTarget:self action:@selector(handlePressButtonCommand:)]
|
||||
data := map[string]interface{}{"name": devBtn}
|
||||
_, err = wd.executePost(data, "/session", wd.sessionId, "/wda/pressButton")
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *remoteWD) IOHIDEvent(pageID EventPageID, usageID EventUsageID, duration ...float64) (err error) {
|
||||
// [[FBRoute POST:@"/wda/performIoHidEvent"] respondWithTarget:self action:@selector(handlePeformIOHIDEvent:)]
|
||||
if len(duration) == 0 || duration[0] <= 0 {
|
||||
duration = []float64{0.005}
|
||||
}
|
||||
data := map[string]interface{}{
|
||||
"page": pageID,
|
||||
"usage": usageID,
|
||||
"duration": duration[0],
|
||||
}
|
||||
_, err = wd.executePost(data, "/session", wd.sessionId, "/wda/performIoHidEvent")
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *remoteWD) ExpectNotification(notifyName string, notifyType NotificationType, second ...int) (err error) {
|
||||
// [[FBRoute POST:@"/wda/expectNotification"] respondWithTarget:self action:@selector(handleExpectNotification:)]
|
||||
if len(second) == 0 {
|
||||
second = []int{60}
|
||||
}
|
||||
data := map[string]interface{}{
|
||||
"name": notifyName,
|
||||
"type": notifyType,
|
||||
"timeout": second[0],
|
||||
}
|
||||
_, err = wd.executePost(data, "/session", wd.sessionId, "/wda/expectNotification")
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *remoteWD) SiriActivate(text string) (err error) {
|
||||
// [[FBRoute POST:@"/wda/siri/activate"] respondWithTarget:self action:@selector(handleActivateSiri:)]
|
||||
data := map[string]interface{}{"text": text}
|
||||
_, err = wd.executePost(data, "/session", wd.sessionId, "/wda/siri/activate")
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *remoteWD) SiriOpenUrl(url string) (err error) {
|
||||
// [[FBRoute POST:@"/url"] respondWithTarget:self action:@selector(handleOpenURL:)]
|
||||
data := map[string]interface{}{"url": url}
|
||||
_, err = wd.executePost(data, "/session", wd.sessionId, "/url")
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *remoteWD) Orientation() (orientation Orientation, err error) {
|
||||
// [[FBRoute GET:@"/orientation"] respondWithTarget:self action:@selector(handleGetOrientation:)]
|
||||
var rawResp rawResponse
|
||||
if rawResp, err = wd.executeGet("/session", wd.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 (wd *remoteWD) SetOrientation(orientation Orientation) (err error) {
|
||||
// [[FBRoute POST:@"/orientation"] respondWithTarget:self action:@selector(handleSetOrientation:)]
|
||||
data := map[string]interface{}{"orientation": orientation}
|
||||
_, err = wd.executePost(data, "/session", wd.sessionId, "/orientation")
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *remoteWD) Rotation() (rotation Rotation, err error) {
|
||||
// [[FBRoute GET:@"/rotation"] respondWithTarget:self action:@selector(handleGetRotation:)]
|
||||
var rawResp rawResponse
|
||||
if rawResp, err = wd.executeGet("/session", wd.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
|
||||
}
|
||||
|
||||
func (wd *remoteWD) SetRotation(rotation Rotation) (err error) {
|
||||
// [[FBRoute POST:@"/rotation"] respondWithTarget:self action:@selector(handleSetRotation:)]
|
||||
_, err = wd.executePost(rotation, "/session", wd.sessionId, "/rotation")
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *remoteWD) MatchTouchID(isMatch bool) (err error) {
|
||||
// [FBRoute POST:@"/wda/touch_id"]
|
||||
data := map[string]interface{}{"match": isMatch}
|
||||
_, err = wd.executePost(data, "/session", wd.sessionId, "/wda/touch_id")
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *remoteWD) ActiveElement() (element WebElement, err error) {
|
||||
// [[FBRoute GET:@"/element/active"] respondWithTarget:self action:@selector(handleGetActiveElement:)]
|
||||
var rawResp rawResponse
|
||||
if rawResp, err = wd.executeGet("/session", wd.sessionId, "/element/active"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var elementID string
|
||||
if elementID, err = rawResp.valueConvertToElementID(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
element = &remoteWE{parent: wd, id: elementID}
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *remoteWD) FindElement(by BySelector) (element WebElement, err error) {
|
||||
// [[FBRoute POST:@"/element"] respondWithTarget:self action:@selector(handleFindElement:)]
|
||||
using, value := by.getUsingAndValue()
|
||||
data := map[string]interface{}{
|
||||
"using": using,
|
||||
"value": value,
|
||||
}
|
||||
var rawResp rawResponse
|
||||
if rawResp, err = wd.executePost(data, "/session", wd.sessionId, "/element"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var elementID string
|
||||
if elementID, err = rawResp.valueConvertToElementID(); err != nil {
|
||||
if errors.Is(err, errNoSuchElement) {
|
||||
return nil, fmt.Errorf("%w: unable to find an element using '%s', value '%s'", err, using, value)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
element = &remoteWE{parent: wd, id: elementID}
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *remoteWD) FindElements(by BySelector) (elements []WebElement, err error) {
|
||||
// [[FBRoute POST:@"/elements"] respondWithTarget:self action:@selector(handleFindElements:)]
|
||||
using, value := by.getUsingAndValue()
|
||||
data := map[string]interface{}{
|
||||
"using": using,
|
||||
"value": value,
|
||||
}
|
||||
var rawResp rawResponse
|
||||
if rawResp, err = wd.executePost(data, "/session", wd.sessionId, "/elements"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var elementIDs []string
|
||||
if elementIDs, err = rawResp.valueConvertToElementIDs(); err != nil {
|
||||
if errors.Is(err, errNoSuchElement) {
|
||||
return nil, fmt.Errorf("%w: unable to find an element using '%s', value '%s'", err, using, value)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
elements = make([]WebElement, len(elementIDs))
|
||||
for i := range elementIDs {
|
||||
elements[i] = &remoteWE{parent: wd, id: elementIDs[i]}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *remoteWD) Screenshot() (raw *bytes.Buffer, err error) {
|
||||
// [[FBRoute GET:@"/screenshot"] respondWithTarget:self action:@selector(handleGetScreenshot:)]
|
||||
// [[FBRoute GET:@"/screenshot"].withoutSession respondWithTarget:self action:@selector(handleGetScreenshot:)]
|
||||
var rawResp rawResponse
|
||||
if rawResp, err = wd.executeGet("/session", wd.sessionId, "/screenshot"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if raw, err = rawResp.valueDecodeAsBase64(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *remoteWD) Source(srcOpt ...SourceOption) (source string, err error) {
|
||||
// [[FBRoute GET:@"/source"] respondWithTarget:self action:@selector(handleGetSourceCommand:)]
|
||||
// [[FBRoute GET:@"/source"].withoutSession
|
||||
tmp, _ := url.Parse(wd._requestURL(nil, "/session", wd.sessionId))
|
||||
toJsonRaw := false
|
||||
if len(srcOpt) != 0 {
|
||||
q := tmp.Query()
|
||||
for k, val := range srcOpt[0] {
|
||||
v := val.(string)
|
||||
q.Set(k, v)
|
||||
if k == "format" && v == "json" {
|
||||
toJsonRaw = true
|
||||
}
|
||||
}
|
||||
tmp.RawQuery = q.Encode()
|
||||
}
|
||||
|
||||
var rawResp rawResponse
|
||||
if rawResp, err = wd.executeHTTP(http.MethodGet, wd._requestURL(tmp, "/source"), nil); err != nil {
|
||||
return "", nil
|
||||
}
|
||||
if toJsonRaw {
|
||||
var jr builtinJSON.RawMessage
|
||||
if jr, err = rawResp.valueConvertToJsonRawMessage(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(jr), nil
|
||||
}
|
||||
if source, err = rawResp.valueConvertToString(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *remoteWD) AccessibleSource() (source string, err error) {
|
||||
// [[FBRoute GET:@"/wda/accessibleSource"] respondWithTarget:self action:@selector(handleGetAccessibleSourceCommand:)]
|
||||
// [[FBRoute GET:@"/wda/accessibleSource"].withoutSession
|
||||
var rawResp rawResponse
|
||||
if rawResp, err = wd.executeGet("/session", wd.sessionId, "/wda/accessibleSource"); err != nil {
|
||||
return "", err
|
||||
}
|
||||
var jr builtinJSON.RawMessage
|
||||
if jr, err = rawResp.valueConvertToJsonRawMessage(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
source = string(jr)
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *remoteWD) HealthCheck() (err error) {
|
||||
// [[FBRoute GET:@"/wda/healthcheck"].withoutSession respondWithTarget:self action:@selector(handleGetHealthCheck:)]
|
||||
_, err = wd.executeGet("/wda/healthcheck")
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *remoteWD) GetAppiumSettings() (settings map[string]interface{}, err error) {
|
||||
// [[FBRoute GET:@"/appium/settings"] respondWithTarget:self action:@selector(handleGetSettings:)]
|
||||
var rawResp rawResponse
|
||||
if rawResp, err = wd.executeGet("/session", wd.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
|
||||
}
|
||||
|
||||
func (wd *remoteWD) SetAppiumSettings(settings map[string]interface{}) (ret map[string]interface{}, err error) {
|
||||
// [[FBRoute POST:@"/appium/settings"] respondWithTarget:self action:@selector(handleSetSettings:)]
|
||||
data := map[string]interface{}{"settings": settings}
|
||||
var rawResp rawResponse
|
||||
if rawResp, err = wd.executePost(data, "/session", wd.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
|
||||
}
|
||||
ret = reply.Value
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *remoteWD) IsHealthy() (healthy bool, err error) {
|
||||
var rawResp rawResponse
|
||||
if rawResp, err = wd.executeGet("/health"); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if string(rawResp) != "I-AM-ALIVE" {
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (wd *remoteWD) WdaShutdown() (err error) {
|
||||
_, err = wd.executeGet("/wda/shutdown")
|
||||
return
|
||||
}
|
||||
|
||||
func (wd *remoteWD) WaitWithTimeoutAndInterval(condition Condition, timeout, interval time.Duration) error {
|
||||
startTime := time.Now()
|
||||
for {
|
||||
done, err := condition(wd)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func (wd *remoteWD) WaitWithTimeout(condition Condition, timeout time.Duration) error {
|
||||
return wd.WaitWithTimeoutAndInterval(condition, timeout, DefaultWaitInterval)
|
||||
}
|
||||
|
||||
func (wd *remoteWD) Wait(condition Condition) error {
|
||||
return wd.WaitWithTimeoutAndInterval(condition, DefaultWaitTimeout, DefaultWaitInterval)
|
||||
}
|
||||
|
||||
// HTTPClient The default client to use to communicate with the WebDriver server.
|
||||
var HTTPClient = http.DefaultClient
|
||||
|
||||
func (wd *remoteWD) executeHTTP(method string, rawURL string, rawBody []byte) (rawResp rawResponse, err error) {
|
||||
log.Debug().Str("method", method).Str("url", rawURL).Str("body", string(rawBody)).Msg("request WDA")
|
||||
var req *http.Request
|
||||
if req, err = newRequest(method, rawURL, rawBody); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var httpCli *http.Client
|
||||
if wd.usbCli != nil {
|
||||
wd.usbCli.Lock()
|
||||
defer wd.usbCli.Unlock()
|
||||
httpCli = wd.usbCli.httpCli
|
||||
} else {
|
||||
httpCli = HTTPClient
|
||||
}
|
||||
httpCli.Timeout = 0
|
||||
|
||||
start := time.Now()
|
||||
var resp *http.Response
|
||||
if resp, err = httpCli.Do(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
// https://github.com/etcd-io/etcd/blob/v3.3.25/pkg/httputil/httputil.go#L16-L22
|
||||
_, _ = io.Copy(ioutil.Discard, resp.Body)
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
rawResp, err = ioutil.ReadAll(resp.Body)
|
||||
logger := log.Debug().Int("statusCode", resp.StatusCode).Str("duration", time.Since(start).String())
|
||||
if !strings.HasSuffix(rawURL, "screenshot") {
|
||||
// avoid printing screenshot data
|
||||
logger.Bytes("response", rawResp)
|
||||
}
|
||||
logger.Msg("get WDA response")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = rawResp.checkErr(); err != nil {
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return rawResp, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
477
hrp/internal/uixt/ios_webelement.go
Normal file
477
hrp/internal/uixt/ios_webelement.go
Normal file
@@ -0,0 +1,477 @@
|
||||
package uixt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/json"
|
||||
)
|
||||
|
||||
// All elements returned by search endpoints have assigned element_id.
|
||||
// Given element_id you can query properties like:
|
||||
// enabled, rect, size, location, text, displayed, accessible, name
|
||||
type remoteWE struct {
|
||||
parent *remoteWD
|
||||
id string // element_id
|
||||
}
|
||||
|
||||
func (we remoteWE) Click() (err error) {
|
||||
// [[FBRoute POST:@"/element/:uuid/click"] respondWithTarget:self action:@selector(handleClick:)]
|
||||
_, err = we.parent.executePost(nil, "/session", we.parent.sessionId, "/element", we.id, "/click")
|
||||
return
|
||||
}
|
||||
|
||||
func (we remoteWE) SendKeys(text string, frequency ...int) (err error) {
|
||||
// [[FBRoute POST:@"/element/:uuid/value"] respondWithTarget:self action:@selector(handleSetValue:)]
|
||||
data := map[string]interface{}{"value": strings.Split(text, "")}
|
||||
if len(frequency) == 0 || frequency[0] <= 0 {
|
||||
frequency = []int{60}
|
||||
}
|
||||
data["frequency"] = frequency[0]
|
||||
_, err = we.parent.executePost(data, "/session", we.parent.sessionId, "/element", we.id, "/value")
|
||||
return
|
||||
}
|
||||
|
||||
func (we remoteWE) Clear() (err error) {
|
||||
// [[FBRoute POST:@"/element/:uuid/clear"] respondWithTarget:self action:@selector(handleClear:)]
|
||||
_, err = we.parent.executePost(nil, "/session", we.parent.sessionId, "/element", we.id, "/clear")
|
||||
return
|
||||
}
|
||||
|
||||
func (we remoteWE) Tap(x, y int) error {
|
||||
return we.TapFloat(float64(x), float64(y))
|
||||
}
|
||||
|
||||
func (we remoteWE) TapFloat(x, y float64) (err error) {
|
||||
// [[FBRoute POST:@"/wda/tap/:uuid"] respondWithTarget:self action:@selector(handleTap:)]
|
||||
data := map[string]interface{}{
|
||||
"x": x,
|
||||
"y": y,
|
||||
}
|
||||
_, err = we.parent.executePost(data, "/session", we.parent.sessionId, "/wda/tap/", we.id)
|
||||
return
|
||||
}
|
||||
|
||||
func (we remoteWE) DoubleTap() (err error) {
|
||||
// [[FBRoute POST:@"/wda/element/:uuid/doubleTap"] respondWithTarget:self action:@selector(handleDoubleTap:)]
|
||||
_, err = we.parent.executePost(nil, "/session", we.parent.sessionId, "/wda/element", we.id, "/doubleTap")
|
||||
return
|
||||
}
|
||||
|
||||
func (we remoteWE) TouchAndHold(second ...float64) (err error) {
|
||||
// [[FBRoute POST:@"/wda/element/:uuid/touchAndHold"] respondWithTarget:self action:@selector(handleTouchAndHold:)]
|
||||
data := make(map[string]interface{})
|
||||
if len(second) == 0 || second[0] <= 0 {
|
||||
second = []float64{1.0}
|
||||
}
|
||||
data["duration"] = second[0]
|
||||
_, err = we.parent.executePost(data, "/session", we.parent.sessionId, "/wda/element", we.id, "/touchAndHold")
|
||||
return
|
||||
}
|
||||
|
||||
func (we remoteWE) TwoFingerTap() (err error) {
|
||||
// [[FBRoute POST:@"/wda/element/:uuid/twoFingerTap"] respondWithTarget:self action:@selector(handleTwoFingerTap:)]
|
||||
_, err = we.parent.executePost(nil, "/session", we.parent.sessionId, "/wda/element", we.id, "/twoFingerTap")
|
||||
return
|
||||
}
|
||||
|
||||
func (we remoteWE) TapWithNumberOfTaps(numberOfTaps, numberOfTouches int) (err error) {
|
||||
// [[FBRoute POST:@"/wda/element/:uuid/tapWithNumberOfTaps"] respondWithTarget:self action:@selector(handleTapWithNumberOfTaps:)]
|
||||
if numberOfTouches <= 0 {
|
||||
return errors.New("'numberOfTouches' must be greater than zero")
|
||||
}
|
||||
if numberOfTouches > 5 {
|
||||
return errors.New("'numberOfTouches' cannot be greater than 5")
|
||||
}
|
||||
if numberOfTaps <= 0 {
|
||||
return errors.New("'numberOfTaps' must be greater than zero")
|
||||
}
|
||||
if numberOfTaps > 10 {
|
||||
return errors.New("'numberOfTaps' cannot be greater than 10")
|
||||
}
|
||||
data := map[string]interface{}{
|
||||
"numberOfTaps": numberOfTaps,
|
||||
"numberOfTouches": numberOfTouches,
|
||||
}
|
||||
_, err = we.parent.executePost(data, "/session", we.parent.sessionId, "/wda/element", we.id, "/tapWithNumberOfTaps")
|
||||
return
|
||||
}
|
||||
|
||||
func (we remoteWE) ForceTouch(pressure float64, second ...float64) (err error) {
|
||||
return we.ForceTouchFloat(-1, -1, pressure, second...)
|
||||
}
|
||||
|
||||
func (we remoteWE) ForceTouchFloat(x, y, pressure float64, second ...float64) (err error) {
|
||||
// [[FBRoute POST:@"/wda/element/:uuid/forceTouch"] respondWithTarget:self action:@selector(handleForceTouch:)]
|
||||
data := make(map[string]interface{})
|
||||
if x != -1 && y != -1 {
|
||||
data["x"] = x
|
||||
data["y"] = y
|
||||
}
|
||||
if len(second) == 0 || second[0] <= 0 {
|
||||
second = []float64{1.0}
|
||||
}
|
||||
data["pressure"] = pressure
|
||||
data["duration"] = second[0]
|
||||
_, err = we.parent.executePost(data, "/session", we.parent.sessionId, "/wda/element", we.id, "/forceTouch")
|
||||
return
|
||||
}
|
||||
|
||||
func (we remoteWE) Drag(fromX, fromY, toX, toY int, pressForDuration ...float64) error {
|
||||
return we.DragFloat(float64(fromX), float64(fromY), float64(toX), float64(toY), pressForDuration...)
|
||||
}
|
||||
|
||||
func (we remoteWE) DragFloat(fromX, fromY, toX, toY float64, pressForDuration ...float64) (err error) {
|
||||
// [[FBRoute POST:@"/wda/element/:uuid/dragfromtoforduration"] respondWithTarget:self action:@selector(handleDrag:)]
|
||||
data := map[string]interface{}{
|
||||
"fromX": fromX,
|
||||
"fromY": fromY,
|
||||
"toX": toX,
|
||||
"toY": toY,
|
||||
}
|
||||
if len(pressForDuration) == 0 || pressForDuration[0] < 0 {
|
||||
pressForDuration = []float64{1.0}
|
||||
}
|
||||
data["duration"] = pressForDuration[0]
|
||||
_, err = we.parent.executePost(data, "/session", we.parent.sessionId, "/wda/element", we.id, "/dragfromtoforduration")
|
||||
return
|
||||
}
|
||||
|
||||
func (we remoteWE) Swipe(fromX, fromY, toX, toY int) error {
|
||||
return we.SwipeFloat(float64(fromX), float64(fromY), float64(toX), float64(toY))
|
||||
}
|
||||
|
||||
func (we remoteWE) SwipeFloat(fromX, fromY, toX, toY float64) error {
|
||||
return we.DragFloat(fromX, fromY, toX, toY, 0)
|
||||
}
|
||||
|
||||
func (we remoteWE) SwipeDirection(direction Direction, velocity ...float64) (err error) {
|
||||
// [[FBRoute POST:@"/wda/element/:uuid/swipe"] respondWithTarget:self action:@selector(handleSwipe:)]
|
||||
data := map[string]interface{}{"direction": direction}
|
||||
if len(velocity) != 0 && velocity[0] > 0 {
|
||||
data["velocity"] = velocity[0]
|
||||
}
|
||||
_, err = we.parent.executePost(data, "/session", we.parent.sessionId, "/wda/element", we.id, "/swipe")
|
||||
return
|
||||
}
|
||||
|
||||
func (we remoteWE) Pinch(scale, velocity float64) (err error) {
|
||||
// [[FBRoute POST:@"/wda/element/:uuid/pinch"] respondWithTarget:self action:@selector(handlePinch:)]
|
||||
if scale <= 0 {
|
||||
return errors.New("'scale' must be greater than zero")
|
||||
}
|
||||
if scale == 1 {
|
||||
return errors.New("'scale' must be greater or less than 1")
|
||||
}
|
||||
if scale < 1 && velocity > 0 {
|
||||
return errors.New("'velocity' must be less than zero when 'scale' is less than 1")
|
||||
}
|
||||
if scale > 1 && velocity <= 0 {
|
||||
return errors.New("'velocity' must be greater than zero when 'scale' is greater than 1")
|
||||
}
|
||||
data := map[string]interface{}{
|
||||
"scale": scale,
|
||||
"velocity": velocity,
|
||||
}
|
||||
_, err = we.parent.executePost(data, "/session", we.parent.sessionId, "/wda/element", we.id, "/pinch")
|
||||
return
|
||||
}
|
||||
|
||||
func (we remoteWE) PinchToZoomOutByW3CAction(scale ...float64) (err error) {
|
||||
if len(scale) == 0 {
|
||||
scale = []float64{1.0}
|
||||
} else if scale[0] > 23 {
|
||||
scale[0] = 23
|
||||
}
|
||||
var size Size
|
||||
if size, err = we.Size(); err != nil {
|
||||
return err
|
||||
}
|
||||
r := scale[0] * 2 / 100.0
|
||||
offsetX, offsetY := float64(size.Width)*r, float64(size.Height)*r
|
||||
|
||||
actions := NewW3CActions().SwipeFloat(0-offsetX, 0-offsetY, 0, 0, we).SwipeFloat(offsetX, offsetY, 0, 0, we)
|
||||
return we.parent.PerformW3CActions(actions)
|
||||
}
|
||||
|
||||
func (we remoteWE) Rotate(rotation float64, velocity ...float64) (err error) {
|
||||
// [[FBRoute POST:@"/wda/element/:uuid/rotate"] respondWithTarget:self action:@selector(handleRotate:)]
|
||||
if rotation > math.Pi*2 || rotation < math.Pi*-2 {
|
||||
return errors.New("'rotation' must not be more than 2π or less than -2π")
|
||||
}
|
||||
if len(velocity) == 0 || velocity[0] == 0 {
|
||||
velocity = []float64{rotation}
|
||||
}
|
||||
if rotation > 0 && velocity[0] < 0 || rotation < 0 && velocity[0] > 0 {
|
||||
return errors.New("'rotation' and 'velocity' must have the same sign")
|
||||
}
|
||||
data := map[string]interface{}{
|
||||
"rotation": rotation,
|
||||
"velocity": velocity[0],
|
||||
}
|
||||
_, err = we.parent.executePost(data, "/session", we.parent.sessionId, "/wda/element", we.id, "/rotate")
|
||||
return
|
||||
}
|
||||
|
||||
func (we remoteWE) PickerWheelSelect(order PickerWheelOrder, offset ...int) (err error) {
|
||||
// [[FBRoute POST:@"/wda/pickerwheel/:uuid/select"] respondWithTarget:self action:@selector(handleWheelSelect:)]
|
||||
if len(offset) == 0 {
|
||||
offset = []int{2}
|
||||
} else if offset[0] <= 0 || offset[0] > 5 {
|
||||
return fmt.Errorf("'offset' value is expected to be in range (0, 5]. '%d' was given instead", offset[0])
|
||||
}
|
||||
data := map[string]interface{}{
|
||||
"order": order,
|
||||
"offset": float64(offset[0]) * 0.1,
|
||||
}
|
||||
_, err = we.parent.executePost(data, "/session", we.parent.sessionId, "/wda/pickerwheel", we.id, "/select")
|
||||
return
|
||||
}
|
||||
|
||||
func (we remoteWE) scroll(data interface{}) (err error) {
|
||||
// [[FBRoute POST:@"/wda/element/:uuid/scroll"] respondWithTarget:self action:@selector(handleScroll:)]
|
||||
_, err = we.parent.executePost(data, "/session", we.parent.sessionId, "/wda/element", we.id, "/scroll")
|
||||
return
|
||||
}
|
||||
|
||||
func (we remoteWE) ScrollElementByName(name string) error {
|
||||
data := map[string]interface{}{"name": name}
|
||||
return we.scroll(data)
|
||||
}
|
||||
|
||||
func (we remoteWE) ScrollElementByPredicate(predicate string) error {
|
||||
data := map[string]interface{}{"predicateString": predicate}
|
||||
return we.scroll(data)
|
||||
}
|
||||
|
||||
func (we remoteWE) ScrollToVisible() error {
|
||||
data := map[string]interface{}{"toVisible": true}
|
||||
return we.scroll(data)
|
||||
}
|
||||
|
||||
func (we remoteWE) ScrollDirection(direction Direction, distance ...float64) error {
|
||||
if len(distance) == 0 || distance[0] <= 0 {
|
||||
distance = []float64{0.5}
|
||||
}
|
||||
data := map[string]interface{}{
|
||||
"direction": direction,
|
||||
"distance": distance[0],
|
||||
}
|
||||
return we.scroll(data)
|
||||
}
|
||||
|
||||
func (we remoteWE) FindElement(by BySelector) (element WebElement, err error) {
|
||||
// [[FBRoute POST:@"/element/:uuid/element"] respondWithTarget:self action:@selector(handleFindSubElement:)]
|
||||
using, value := by.getUsingAndValue()
|
||||
data := map[string]interface{}{
|
||||
"using": using,
|
||||
"value": value,
|
||||
}
|
||||
var rawResp rawResponse
|
||||
if rawResp, err = we.parent.executePost(data, "/session", we.parent.sessionId, "/element", we.id, "/element"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var elementID string
|
||||
if elementID, err = rawResp.valueConvertToElementID(); err != nil {
|
||||
if errors.Is(err, errNoSuchElement) {
|
||||
return nil, fmt.Errorf("%w: unable to find an element using '%s', value '%s'", err, using, value)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
element = &remoteWE{parent: we.parent, id: elementID}
|
||||
return
|
||||
}
|
||||
|
||||
func (we remoteWE) FindElements(by BySelector) (elements []WebElement, err error) {
|
||||
// [[FBRoute POST:@"/element/:uuid/elements"] respondWithTarget:self action:@selector(handleFindSubElements:)]
|
||||
using, value := by.getUsingAndValue()
|
||||
data := map[string]interface{}{
|
||||
"using": using,
|
||||
"value": value,
|
||||
}
|
||||
var rawResp rawResponse
|
||||
if rawResp, err = we.parent.executePost(data, "/session", we.parent.sessionId, "/element", we.id, "/elements"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var elementIDs []string
|
||||
if elementIDs, err = rawResp.valueConvertToElementIDs(); err != nil {
|
||||
if errors.Is(err, errNoSuchElement) {
|
||||
return nil, fmt.Errorf("%w: unable to find an element using '%s', value '%s'", err, using, value)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
elements = make([]WebElement, len(elementIDs))
|
||||
for i := range elementIDs {
|
||||
elements[i] = &remoteWE{parent: we.parent, id: elementIDs[i]}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (we remoteWE) FindVisibleCells() (elements []WebElement, err error) {
|
||||
// [[FBRoute GET:@"/wda/element/:uuid/getVisibleCells"] respondWithTarget:self action:@selector(handleFindVisibleCells:)]
|
||||
var rawResp rawResponse
|
||||
if rawResp, err = we.parent.executeGet("/session", we.parent.sessionId, "/wda/element", we.id, "/getVisibleCells"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var elementIDs []string
|
||||
if elementIDs, err = rawResp.valueConvertToElementIDs(); err != nil {
|
||||
if errors.Is(err, errNoSuchElement) {
|
||||
return nil, fmt.Errorf("%w: unable to find a cell element in this element", err)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
elements = make([]WebElement, len(elementIDs))
|
||||
for i := range elementIDs {
|
||||
elements[i] = &remoteWE{parent: we.parent, id: elementIDs[i]}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (we remoteWE) Rect() (rect Rect, err error) {
|
||||
// [[FBRoute GET:@"/element/:uuid/rect"] respondWithTarget:self action:@selector(handleGetRect:)]
|
||||
var rawResp rawResponse
|
||||
if rawResp, err = we.parent.executeGet("/session", we.parent.sessionId, "/element", we.id, "/rect"); err != nil {
|
||||
return Rect{}, err
|
||||
}
|
||||
reply := new(struct{ Value struct{ Rect } })
|
||||
if err = json.Unmarshal(rawResp, reply); err != nil {
|
||||
return Rect{}, err
|
||||
}
|
||||
rect = reply.Value.Rect
|
||||
return
|
||||
}
|
||||
|
||||
func (we remoteWE) Location() (Point, error) {
|
||||
rect, err := we.Rect()
|
||||
if err != nil {
|
||||
return Point{}, err
|
||||
}
|
||||
return rect.Point, nil
|
||||
}
|
||||
|
||||
func (we remoteWE) Size() (Size, error) {
|
||||
rect, err := we.Rect()
|
||||
if err != nil {
|
||||
return Size{}, err
|
||||
}
|
||||
return rect.Size, nil
|
||||
}
|
||||
|
||||
func (we remoteWE) Text() (text string, err error) {
|
||||
// [[FBRoute GET:@"/element/:uuid/text"] respondWithTarget:self action:@selector(handleGetText:)]
|
||||
var rawResp rawResponse
|
||||
if rawResp, err = we.parent.executeGet("/session", we.parent.sessionId, "/element", we.id, "/text"); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if text, err = rawResp.valueConvertToString(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (we remoteWE) Type() (elemType string, err error) {
|
||||
// [[FBRoute GET:@"/element/:uuid/name"] respondWithTarget:self action:@selector(handleGetName:)]
|
||||
var rawResp rawResponse
|
||||
if rawResp, err = we.parent.executeGet("/session", we.parent.sessionId, "/element", we.id, "/name"); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if elemType, err = rawResp.valueConvertToString(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (we remoteWE) IsEnabled() (enabled bool, err error) {
|
||||
// [[FBRoute GET:@"/element/:uuid/enabled"] respondWithTarget:self action:@selector(handleGetEnabled:)]
|
||||
var rawResp rawResponse
|
||||
if rawResp, err = we.parent.executeGet("/session", we.parent.sessionId, "/element", we.id, "/enabled"); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if enabled, err = rawResp.valueConvertToBool(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (we remoteWE) IsDisplayed() (displayed bool, err error) {
|
||||
// [[FBRoute GET:@"/element/:uuid/displayed"] respondWithTarget:self action:@selector(handleGetDisplayed:)]
|
||||
var rawResp rawResponse
|
||||
if rawResp, err = we.parent.executeGet("/session", we.parent.sessionId, "/element", we.id, "/displayed"); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if displayed, err = rawResp.valueConvertToBool(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (we remoteWE) IsSelected() (selected bool, err error) {
|
||||
// [[FBRoute GET:@"/element/:uuid/selected"] respondWithTarget:self action:@selector(handleGetSelected:)]
|
||||
var rawResp rawResponse
|
||||
if rawResp, err = we.parent.executeGet("/session", we.parent.sessionId, "/element", we.id, "/selected"); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if selected, err = rawResp.valueConvertToBool(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (we remoteWE) IsAccessible() (accessible bool, err error) {
|
||||
// [[FBRoute GET:@"/wda/element/:uuid/accessible"] respondWithTarget:self action:@selector(handleGetAccessible:)]
|
||||
var rawResp rawResponse
|
||||
if rawResp, err = we.parent.executeGet("/session", we.parent.sessionId, "/wda/element", we.id, "/accessible"); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if accessible, err = rawResp.valueConvertToBool(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (we remoteWE) IsAccessibilityContainer() (isAccessibilityContainer bool, err error) {
|
||||
// [[FBRoute GET:@"/wda/element/:uuid/accessibilityContainer"] respondWithTarget:self action:@selector(handleGetIsAccessibilityContainer:)]
|
||||
var rawResp rawResponse
|
||||
if rawResp, err = we.parent.executeGet("/session", we.parent.sessionId, "/wda/element", we.id, "/accessibilityContainer"); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if isAccessibilityContainer, err = rawResp.valueConvertToBool(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (we remoteWE) GetAttribute(attr ElementAttribute) (value string, err error) {
|
||||
// [[FBRoute GET:@"/element/:uuid/attribute/:name"] respondWithTarget:self action:@selector(handleGetAttribute:)]
|
||||
var rawResp rawResponse
|
||||
if rawResp, err = we.parent.executeGet("/session", we.parent.sessionId, "/element", we.id, "/attribute", attr.getAttributeName()); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if value, err = rawResp.valueConvertToString(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (we remoteWE) UID() (uid string) {
|
||||
return we.id
|
||||
}
|
||||
|
||||
func (we remoteWE) Screenshot() (raw *bytes.Buffer, err error) {
|
||||
// W3C element screenshot
|
||||
// [[FBRoute GET:@"/element/:uuid/screenshot"] respondWithTarget:self action:@selector(handleElementScreenshot:)]
|
||||
// JSONWP element screenshot
|
||||
// [[FBRoute GET:@"/screenshot/:uuid"] respondWithTarget:self action:@selector(handleElementScreenshot:)]
|
||||
var rawResp rawResponse
|
||||
if rawResp, err = we.parent.executeGet("/session", we.parent.sessionId, "/element", we.id, "/screenshot"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if raw, err = rawResp.valueDecodeAsBase64(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -20,11 +20,6 @@ var client = &http.Client{
|
||||
Timeout: time.Second * 10,
|
||||
}
|
||||
|
||||
type Point struct {
|
||||
X float32 `json:"x"`
|
||||
Y float32 `json:"y"`
|
||||
}
|
||||
|
||||
type OCRResult struct {
|
||||
Text string `json:"text"`
|
||||
Points []Point `json:"points"`
|
||||
|
||||
@@ -5,11 +5,10 @@ package uixt
|
||||
import (
|
||||
"image"
|
||||
|
||||
"github.com/electricbubble/gwda"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func Extend(driver gwda.WebDriver, options ...CVOption) (dExt *DriverExt, err error) {
|
||||
func Extend(driver WebDriver, options ...CVOption) (dExt *DriverExt, err error) {
|
||||
return extend(driver)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/electricbubble/gwda"
|
||||
cvHelper "github.com/electricbubble/opencv-helper"
|
||||
)
|
||||
|
||||
@@ -43,7 +42,7 @@ const (
|
||||
// 获取当前设备的 Scale,
|
||||
// 默认匹配模式为 TmCcoeffNormed,
|
||||
// 默认关闭 OpenCV 匹配值计算后的输出
|
||||
func Extend(driver gwda.WebDriver, options ...CVOption) (dExt *DriverExt, err error) {
|
||||
func Extend(driver WebDriver, options ...CVOption) (dExt *DriverExt, err error) {
|
||||
dExt, err = extend(driver)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/electricbubble/gwda"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
@@ -29,7 +28,7 @@ func (dExt *DriverExt) SwipeRelative(fromX, fromY, toX, toY float64, identifier
|
||||
toY = float64(height) * toY
|
||||
|
||||
if len(identifier) > 0 && identifier[0] != "" {
|
||||
option := gwda.WithCustomOption("log", map[string]interface{}{
|
||||
option := WithCustomOption("log", map[string]interface{}{
|
||||
"enable": true,
|
||||
"data": identifier[0],
|
||||
})
|
||||
|
||||
@@ -2,13 +2,11 @@ package uixt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/electricbubble/gwda"
|
||||
)
|
||||
|
||||
func (dExt *DriverExt) tapFloat(x, y float64, identifier string) error {
|
||||
if len(identifier) > 0 {
|
||||
option := gwda.WithCustomOption("log", map[string]interface{}{
|
||||
option := WithCustomOption("log", map[string]interface{}{
|
||||
"enable": true,
|
||||
"data": identifier,
|
||||
})
|
||||
@@ -122,6 +120,6 @@ func (dExt *DriverExt) TapWithNumberOffset(param string, numberOfTaps int, xOffs
|
||||
x = x + width*xOffset
|
||||
y = y + height*yOffset
|
||||
|
||||
touchActions := gwda.NewTouchActions().Tap(gwda.NewTouchActionTap().WithXYFloat(x, y).WithCount(numberOfTaps))
|
||||
touchActions := NewTouchActions().Tap(NewTouchActionTap().WithXYFloat(x, y).WithCount(numberOfTaps))
|
||||
return dExt.PerformTouchActions(touchActions)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ func TestDriverExt_TapWithNumber(t *testing.T) {
|
||||
|
||||
pathSearch := "/Users/hero/Documents/temp/2020-05/opencv/flag7.png"
|
||||
|
||||
// gwda.SetDebug(true)
|
||||
// SetDebug(true)
|
||||
|
||||
err = driverExt.TapWithNumber(pathSearch, 3)
|
||||
checkErr(t, err)
|
||||
|
||||
@@ -4,12 +4,13 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/uixt"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/httprunner/httprunner/v4/hrp/internal/uixt"
|
||||
)
|
||||
|
||||
type AndroidStep struct {
|
||||
uixt.UIAOptions `yaml:",inline"` // inline refers to https://pkg.go.dev/gopkg.in/yaml.v3#Marshal
|
||||
uixt.AndroidDevice `yaml:",inline"` // inline refers to https://pkg.go.dev/gopkg.in/yaml.v3#Marshal
|
||||
uixt.MobileAction
|
||||
Actions []uixt.MobileAction `json:"actions,omitempty" yaml:"actions,omitempty"`
|
||||
}
|
||||
@@ -220,7 +221,7 @@ func runStepAndroid(s *SessionRunner, step *TStep) (stepResult *StepResult, err
|
||||
screenshots := make([]string, 0)
|
||||
|
||||
// init uiaClient driver
|
||||
uiaClient, err := s.hrpRunner.initUIClient(&step.Android.UIAOptions)
|
||||
uiaClient, err := s.hrpRunner.initUIClient(&step.Android.AndroidDevice)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ var (
|
||||
)
|
||||
|
||||
type IOSStep struct {
|
||||
uixt.WDAOptions `yaml:",inline"` // inline refers to https://pkg.go.dev/gopkg.in/yaml.v3#Marshal
|
||||
uixt.IOSDevice `yaml:",inline"` // inline refers to https://pkg.go.dev/gopkg.in/yaml.v3#Marshal
|
||||
uixt.MobileAction `yaml:",inline"`
|
||||
Actions []uixt.MobileAction `json:"actions,omitempty" yaml:"actions,omitempty"`
|
||||
}
|
||||
@@ -455,8 +455,8 @@ func (s *StepIOSValidation) Run(r *SessionRunner) (*StepResult, error) {
|
||||
return runStepIOS(r, s.step)
|
||||
}
|
||||
|
||||
func (r *HRPRunner) initUIClient(options uixt.Options) (client *uixt.DriverExt, err error) {
|
||||
uuid := options.UUID()
|
||||
func (r *HRPRunner) initUIClient(device uixt.Device) (client *uixt.DriverExt, err error) {
|
||||
uuid := device.UUID()
|
||||
|
||||
// avoid duplicate init
|
||||
if uuid == "" && len(r.uiClients) == 1 {
|
||||
@@ -472,10 +472,10 @@ func (r *HRPRunner) initUIClient(options uixt.Options) (client *uixt.DriverExt,
|
||||
}
|
||||
}
|
||||
|
||||
if wdaOptions, ok := options.(*uixt.WDAOptions); ok {
|
||||
client, err = uixt.InitWDAClient(wdaOptions)
|
||||
} else if uiaOptions, ok := options.(*uixt.UIAOptions); ok {
|
||||
client, err = uixt.InitUIAClient(uiaOptions)
|
||||
if iosDevice, ok := device.(*uixt.IOSDevice); ok {
|
||||
client, err = uixt.InitWDAClient(iosDevice)
|
||||
} else if androidDevice, ok := device.(*uixt.AndroidDevice); ok {
|
||||
client, err = uixt.InitUIAClient(androidDevice)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -500,7 +500,7 @@ func runStepIOS(s *SessionRunner, step *TStep) (stepResult *StepResult, err erro
|
||||
screenshots := make([]string, 0)
|
||||
|
||||
// init wdaClient driver
|
||||
wdaClient, err := s.hrpRunner.initUIClient(&step.IOS.WDAOptions)
|
||||
wdaClient, err := s.hrpRunner.initUIClient(&step.IOS.IOSDevice)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user