From 1daa3c64ba8e86e4cf8556c7d8c6e52cd345d31d Mon Sep 17 00:00:00 2001 From: debugtalk Date: Fri, 23 Sep 2022 22:39:43 +0800 Subject: [PATCH] refactor: move gwda into uixt --- go.mod | 4 +- go.sum | 2 - hrp/config.go | 8 +- .../uixt/{android.go => android_action.go} | 0 hrp/internal/uixt/android_device.go | 15 + hrp/internal/uixt/android_webdriver.go | 1 + hrp/internal/uixt/android_webelment.go | 1 + hrp/internal/uixt/drag.go | 4 +- hrp/internal/uixt/ext.go | 29 +- hrp/internal/uixt/gesture.go | 9 +- hrp/internal/uixt/interface.go | 983 +++++++++++++- hrp/internal/uixt/ios.go | 187 --- hrp/internal/uixt/ios_action.go | 373 ++++++ hrp/internal/uixt/ios_device.go | 470 +++++++ hrp/internal/uixt/ios_test.go | 1187 +++++++++++++++++ hrp/internal/uixt/ios_webdriver.go | 928 +++++++++++++ hrp/internal/uixt/ios_webelement.go | 477 +++++++ hrp/internal/uixt/ocr_on.go | 5 - hrp/internal/uixt/opencv_off.go | 3 +- hrp/internal/uixt/opencv_on.go | 3 +- hrp/internal/uixt/swipe.go | 3 +- hrp/internal/uixt/tap.go | 6 +- hrp/internal/uixt/tap_test.go | 2 +- hrp/step_android_ui.go | 7 +- hrp/step_ios_ui.go | 16 +- 25 files changed, 4475 insertions(+), 248 deletions(-) rename hrp/internal/uixt/{android.go => android_action.go} (100%) create mode 100644 hrp/internal/uixt/android_device.go create mode 100644 hrp/internal/uixt/android_webdriver.go create mode 100644 hrp/internal/uixt/android_webelment.go delete mode 100644 hrp/internal/uixt/ios.go create mode 100644 hrp/internal/uixt/ios_action.go create mode 100644 hrp/internal/uixt/ios_device.go create mode 100644 hrp/internal/uixt/ios_test.go create mode 100644 hrp/internal/uixt/ios_webdriver.go create mode 100644 hrp/internal/uixt/ios_webelement.go diff --git a/go.mod b/go.mod index 9eaee6e2..57d30aeb 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index e615a6ac..6b9beca0 100644 --- a/go.sum +++ b/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= diff --git a/hrp/config.go b/hrp/config.go index 99ebcbad..d5caffcf 100644 --- a/hrp/config.go +++ b/hrp/config.go @@ -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) } diff --git a/hrp/internal/uixt/android.go b/hrp/internal/uixt/android_action.go similarity index 100% rename from hrp/internal/uixt/android.go rename to hrp/internal/uixt/android_action.go diff --git a/hrp/internal/uixt/android_device.go b/hrp/internal/uixt/android_device.go new file mode 100644 index 00000000..b7a3a332 --- /dev/null +++ b/hrp/internal/uixt/android_device.go @@ -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 +} diff --git a/hrp/internal/uixt/android_webdriver.go b/hrp/internal/uixt/android_webdriver.go new file mode 100644 index 00000000..64c90179 --- /dev/null +++ b/hrp/internal/uixt/android_webdriver.go @@ -0,0 +1 @@ +package uixt diff --git a/hrp/internal/uixt/android_webelment.go b/hrp/internal/uixt/android_webelment.go new file mode 100644 index 00000000..64c90179 --- /dev/null +++ b/hrp/internal/uixt/android_webelment.go @@ -0,0 +1 @@ +package uixt diff --git a/hrp/internal/uixt/drag.go b/hrp/internal/uixt/drag.go index 510b5915..27a13501 100644 --- a/hrp/internal/uixt/drag.go +++ b/hrp/internal/uixt/drag.go @@ -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])) } diff --git a/hrp/internal/uixt/ext.go b/hrp/internal/uixt/ext.go index 76ffc417..a705d5b8 100644 --- a/hrp/internal/uixt/ext.go +++ b/hrp/internal/uixt/ext.go @@ -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 diff --git a/hrp/internal/uixt/gesture.go b/hrp/internal/uixt/gesture.go index 5edd8147..ce3b6b21 100644 --- a/hrp/internal/uixt/gesture.go +++ b/hrp/internal/uixt/gesture.go @@ -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) } } diff --git a/hrp/internal/uixt/interface.go b/hrp/internal/uixt/interface.go index b788c6e1..cbe7a0e1 100644 --- a/hrp/internal/uixt/interface.go +++ b/hrp/internal/uixt/interface.go @@ -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) } diff --git a/hrp/internal/uixt/ios.go b/hrp/internal/uixt/ios.go deleted file mode 100644 index d79caf27..00000000 --- a/hrp/internal/uixt/ios.go +++ /dev/null @@ -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 -} diff --git a/hrp/internal/uixt/ios_action.go b/hrp/internal/uixt/ios_action.go new file mode 100644 index 00000000..0541f827 --- /dev/null +++ b/hrp/internal/uixt/ios_action.go @@ -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 +} diff --git a/hrp/internal/uixt/ios_device.go b/hrp/internal/uixt/ios_device.go new file mode 100644 index 00000000..10b37d74 --- /dev/null +++ b/hrp/internal/uixt/ios_device.go @@ -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 +} diff --git a/hrp/internal/uixt/ios_test.go b/hrp/internal/uixt/ios_test.go new file mode 100644 index 00000000..a93b0bec --- /dev/null +++ b/hrp/internal/uixt/ios_test.go @@ -0,0 +1,1187 @@ +package uixt + +import ( + "bytes" + "fmt" + "math" + "testing" + "time" +) + +var ( + urlPrefix = "http://localhost:8100" + bundleId = "com.apple.Preferences" + driver WebDriver +) + +func setup(t *testing.T) { + var err error + driver, err = NewUSBDriver(nil) + if err != nil { + t.Fatal(err) + } +} + +func TestViaUSB(t *testing.T) { + devices, err := DeviceList() + if err != nil { + t.Fatal(err) + } + + drivers := make([]WebDriver, 0, len(devices)) + + for _, dev := range devices { + d, err := NewUSBDriver(nil, dev) + if err != nil { + t.Errorf("%s: %s", dev.UUID(), err) + continue + } + drivers = append(drivers, d) + } + + for _, d := range drivers { + t.Log(d.Status()) + } +} + +func TestNewDevice(t *testing.T) { + device, _ := NewDevice() + if device != nil { + t.Log(device) + } + + device, _ = NewDevice(WithUDID("xxxx")) + if device != nil { + t.Log(device) + } + + device, _ = NewDevice(WithPort(8700), WithMjpegPort(8800)) + if device != nil { + t.Log(device) + } + + device, _ = NewDevice(WithUDID("xxxx"), WithPort(8700), WithMjpegPort(8800)) + if device != nil { + t.Log(device) + } +} + +func TestNewDriver(t *testing.T) { + var err error + driver, err = NewDriver(nil, urlPrefix) + if err != nil { + t.Fatal(err) + } +} + +func TestNewUSBDriver(t *testing.T) { + setup(t) + + // t.Log(driver.IsWdaHealthy()) +} + +func Test_remoteWD_NewSession(t *testing.T) { + setup(t) + + // sessionInfo, err := driver.NewSession(nil) + sessionInfo, err := driver.NewSession( + NewCapabilities().WithAppLaunchOption( + NewAppLaunchOption().WithBundleId(bundleId).WithArguments([]string{"-AppleLanguages", "(Russian)"}), + ), + ) + if err != nil { + t.Fatal(err) + } + if len(sessionInfo.SessionId) == 0 { + t.Fatal(sessionInfo) + } +} + +func Test_remoteWD_ActiveSession(t *testing.T) { + setup(t) + + sessionInfo, err := driver.ActiveSession() + if err != nil { + t.Fatal(err) + } + if len(sessionInfo.SessionId) == 0 { + t.Fatal(sessionInfo) + } +} + +func Test_remoteWD_DeleteSession(t *testing.T) { + setup(t) + + err := driver.DeleteSession() + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_HealthCheck(t *testing.T) { + setup(t) + + err := driver.HealthCheck() + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_GetAppiumSettings(t *testing.T) { + setup(t) + + settings, err := driver.GetAppiumSettings() + if err != nil { + t.Fatal(err) + } + t.Log(settings) +} + +func Test_remoteWD_SetAppiumSettings(t *testing.T) { + setup(t) + + const _acceptAlertButtonSelector = "**/XCUIElementTypeButton[`label IN {'允许','好','仅在使用应用期间','暂不'}`]" + const _dismissAlertButtonSelector = "**/XCUIElementTypeButton[`label IN {'不允许','暂不'}`]" + + key := "acceptAlertButtonSelector" + value := _acceptAlertButtonSelector + + // settings, err := driver.SetAppiumSettings(map[string]interface{}{"dismissAlertButtonSelector": "暂不"}) + settings, err := driver.SetAppiumSettings(map[string]interface{}{key: value}) + if err != nil { + t.Fatal(err) + } + if settings[key] != value { + t.Fatal(settings[key]) + } +} + +func Test_remoteWD_IsWdaHealthy(t *testing.T) { + setup(t) + + healthy, err := driver.IsHealthy() + if err != nil { + t.Fatal(err) + } + if healthy == false { + t.Fatal("healthy =", healthy) + } +} + +// func Test_remoteWD_WdaShutdown(t *testing.T) { +// setup(t) +// +// if err := driver.WdaShutdown(); err != nil { +// t.Fatal(err) +// } +// } + +func Test_remoteWD_Status(t *testing.T) { + setup(t) + + status, err := driver.Status() + if err != nil { + t.Fatal(err) + } + if status.Ready == false { + t.Fatal("deviceStatus =", status) + } +} + +func Test_remoteWD_DeviceInfo(t *testing.T) { + setup(t) + + info, err := driver.DeviceInfo() + if err != nil { + t.Fatal(err) + } + if len(info.Model) == 0 { + t.Fatal(info) + } +} + +func Test_remoteWD_BatteryInfo(t *testing.T) { + setup(t) + + batteryInfo, err := driver.BatteryInfo() + if err != nil { + t.Fatal() + } + t.Log(batteryInfo) +} + +func Test_remoteWD_WindowSize(t *testing.T) { + setup(t) + + size, err := driver.WindowSize() + if err != nil { + t.Fatal() + } + t.Log(size) +} + +func Test_remoteWD_Screen(t *testing.T) { + setup(t) + + screen, err := driver.Screen() + if err != nil { + t.Fatal(err) + } + t.Log(screen) +} + +func Test_remoteWD_ActiveAppInfo(t *testing.T) { + setup(t) + + appInfo, err := driver.ActiveAppInfo() + if err != nil { + t.Fatal(err) + } + if len(appInfo.BundleId) == 0 { + t.Fatal(appInfo) + } + t.Log(appInfo) +} + +func Test_remoteWD_ActiveAppsList(t *testing.T) { + setup(t) + + appsList, err := driver.ActiveAppsList() + if err != nil { + t.Fatal(err) + } + if len(appsList) == 0 { + t.Fatal(appsList) + } + t.Log(appsList) +} + +func Test_remoteWD_AppState(t *testing.T) { + setup(t) + + runState, err := driver.AppState(bundleId) + if err != nil { + t.Fatal(err) + } + t.Log(runState) +} + +func Test_remoteWD_IsLocked(t *testing.T) { + setup(t) + + locked, err := driver.IsLocked() + if err != nil { + t.Fatal(err) + } + t.Log(locked) +} + +func Test_remoteWD_Unlock(t *testing.T) { + setup(t) + + err := driver.Unlock() + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_Lock(t *testing.T) { + setup(t) + + err := driver.Lock() + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_AlertText(t *testing.T) { + setup(t) + + text, err := driver.AlertText() + if err != nil { + t.Fatal(err) + } + _ = text + t.Log(text) +} + +func Test_remoteWD_AlertButtons(t *testing.T) { + setup(t) + + btnLabels, err := driver.AlertButtons() + if err != nil { + t.Fatal(err) + } + t.Log(btnLabels) +} + +func Test_remoteWD_AlertAccept(t *testing.T) { + // Test_remoteWD_AppAuthReset(t) + // return + + setup(t) + + err := driver.AlertAccept() + // err := driver.AlertAccept("") + // err := driver.AlertAccept("好") + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_AlertDismiss(t *testing.T) { + // Test_remoteWD_AppAuthReset(t) + // return + + setup(t) + + err := driver.AlertDismiss() + // err := driver.AlertDismiss("") + // err := driver.AlertDismiss("不允许") + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_AlertSendKeys(t *testing.T) { + setup(t) + + err := driver.AlertSendKeys("todo") + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_Homescreen(t *testing.T) { + setup(t) + + err := driver.Homescreen() + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_AppLaunch(t *testing.T) { + setup(t) + + // SetDebug(true) + + // bundleId = "com.hustlzp.xcz" + // bundleId = "com.github.stormbreaker.prod" + // bundleId = "com.360buy.jdmobile" + // bundleId = "com.zhihu.ios" + // bundleId = "com.tencent.xin" + // bundleId = "com.jsmcc.ZP7267A6ES" + err := driver.AppLaunch(bundleId) + // err := driver.AppLaunch(bundleId, NewAppLaunchOption().WithShouldWaitForQuiescence(true)) + // err := driver.AppLaunch(bundleId, NewAppLaunchOption().WithArguments([]string{"-AppleLanguages", "(Russian)"})) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_AppLaunchUnattached(t *testing.T) { + setup(t) + + err := driver.AppLaunchUnattached(bundleId) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_AppTerminate(t *testing.T) { + setup(t) + + _, err := driver.AppTerminate(bundleId) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_AppActivate(t *testing.T) { + setup(t) + + err := driver.AppActivate(bundleId) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_AppDeactivate(t *testing.T) { + setup(t) + + err := driver.AppDeactivate(2) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_AppAuthReset(t *testing.T) { + setup(t) + + err := driver.AppAuthReset(ProtectedResourceCamera) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_Tap(t *testing.T) { + setup(t) + + err := driver.Tap(200, 300) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_DoubleTap(t *testing.T) { + setup(t) + + err := driver.DoubleTap(200, 300) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_TouchAndHold(t *testing.T) { + setup(t) + + // err := driver.TouchAndHold(200, 300) + err := driver.TouchAndHold(200, 300, -1) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_Drag(t *testing.T) { + setup(t) + + // err := driver.Drag(200, 300, 200, 500, WithPressDuration(0.5)) + err := driver.Swipe(200, 300, 200, 500) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_ForceTouch(t *testing.T) { + setup(t) + + err := driver.ForceTouch(256, 400, 0.8, -1) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_SetPasteboard(t *testing.T) { + setup(t) + + // err := driver.SetPasteboard(PasteboardTypePlaintext, "gwda") + err := driver.SetPasteboard(PasteboardTypeUrl, "Clock-stopwatch://") + // userHomeDir, _ := os.UserHomeDir() + // bytesImg, _ := ioutil.ReadFile(userHomeDir + "/Pictures/IMG_0806.jpg") + // err := driver.SetPasteboard(PasteboardTypeImage, string(bytesImg)) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_GetPasteboard(t *testing.T) { + setup(t) + + var buffer *bytes.Buffer + var err error + + buffer, err = driver.GetPasteboard(PasteboardTypePlaintext) + // buffer, err = driver.GetPasteboard(PasteboardTypeUrl) + if err != nil { + t.Fatal(err) + } + t.Log(buffer.String()) + + // buffer, err = driver.GetPasteboard(PasteboardTypeImage) + // if err != nil { + // t.Fatal(err) + // } + // userHomeDir, _ := os.UserHomeDir() + // if err = ioutil.WriteFile(userHomeDir+"/Desktop/p1.png", buffer.Bytes(), 0600); err != nil { + // t.Error(err) + // } +} + +func Test_remoteWD_SendKeys(t *testing.T) { + setup(t) + + err := driver.SendKeys("App Store") + // err := driver.SendKeys("App Store", WithFrequency(3)) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_PressButton(t *testing.T) { + setup(t) + + err := driver.PressButton(DeviceButtonVolumeUp) + if err != nil { + t.Fatal(err) + } + time.Sleep(time.Second * 1) + err = driver.PressButton(DeviceButtonVolumeDown) + if err != nil { + t.Fatal(err) + } + time.Sleep(time.Second * 1) + err = driver.PressButton(DeviceButtonHome) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_SiriActivate(t *testing.T) { + setup(t) + + err := driver.SiriActivate("What's the weather like today") + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_SiriOpenUrl(t *testing.T) { + setup(t) + + err := driver.SiriOpenUrl("Prefs:root=Bluetooth") + // err := driver.SiriOpenUrl("Prefs:root=WIFI![]()") + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_Orientation(t *testing.T) { + setup(t) + + orientation, err := driver.Orientation() + if err != nil { + t.Fatal(err) + } + if orientation == "" { + t.Fatal(orientation) + } +} + +func Test_remoteWD_SetOrientation(t *testing.T) { + setup(t) + + var err error + err = driver.SetOrientation(OrientationLandscapeLeft) + err = driver.SetOrientation(OrientationLandscapeRight) + err = driver.SetOrientation(OrientationPortrait) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_Rotation(t *testing.T) { + setup(t) + + rotation, err := driver.Rotation() + if err != nil { + t.Fatal() + } + t.Log(rotation) +} + +func Test_remoteWD_SetRotation(t *testing.T) { + setup(t) + + err := driver.SetRotation(Rotation{X: 0, Y: 0, Z: 270}) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_PerformW3CActions(t *testing.T) { + // setup(t) + // actions := NewW3CActions().SendKeys("App Store") + + element := setupElement(t, BySelector{Name: "touchableView"}) + actions := NewW3CActions().FingerAction( + NewFingerAction(). + Move(NewFingerMove().WithXY(-15, -85).WithOrigin(element)). + Down(). + Pause(0.25). + Move(NewFingerMove().WithOrigin(element)). + Pause(0.25). + Up(), + NewFingerAction(). + Move(NewFingerMove().WithXY(15, 85).WithOrigin(element)). + Down(). + Pause(0.25). + Move(NewFingerMove().WithOrigin(element)). + Pause(0.25). + Up(), + ) + err := driver.PerformW3CActions(actions) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_PerformAppiumTouchActions(t *testing.T) { + element := setupElement(t, BySelector{Name: "touchableView"}) + + actions := NewTouchActions(). + Press(NewTouchActionPress().WithElement(element).WithXY(100, 150).WithPressure(0.2)). + Wait(0.2). + MoveTo(NewTouchActionMoveTo().WithXY(300, 150)). + Wait(0.2). + MoveTo(NewTouchActionMoveTo().WithElement(element)). + Wait(0.2). + MoveTo(NewTouchActionMoveTo().WithElement(element).WithXY(300, 400)). + Release() + + err := driver.PerformAppiumTouchActions(actions) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_ActiveElement(t *testing.T) { + setup(t) + + element, err := driver.ActiveElement() + if err != nil { + t.Fatal(err) + } + _ = element + // t.Log(element) +} + +func Test_remoteWD_FindElement(t *testing.T) { + setup(t) + + element, err := driver.FindElement(BySelector{Name: "设置"}) + if err != nil { + t.Fatal(err) + } + _ = element + // t.Log(element) +} + +func Test_remoteWD_FindElements(t *testing.T) { + setup(t) + + elements, err := driver.FindElements(BySelector{ClassName: ElementType{Icon: true}}) + if err != nil { + t.Fatal(err) + } + _ = elements + t.Log(elements) +} + +func Test_remoteWD_Screenshot(t *testing.T) { + setup(t) + + screenshot, err := driver.Screenshot() + if err != nil { + t.Fatal(err) + } + _ = screenshot + + // img, format, err := image.Decode(screenshot) + // if err != nil { + // t.Fatal(err) + // } + // userHomeDir, _ := os.UserHomeDir() + // file, err := os.Create(userHomeDir + "/Desktop/s1." + format) + // if err != nil { + // t.Fatal(err) + // } + // defer func() { _ = file.Close() }() + // switch format { + // case "png": + // err = png.Encode(file, img) + // case "jpeg": + // err = jpeg.Encode(file, img, nil) + // } + // if err != nil { + // t.Fatal(err) + // } + // t.Log(file.Name()) +} + +func Test_remoteWD_Source(t *testing.T) { + setup(t) + + var source string + var err error + + // source, err = driver.Source() + // if err != nil { + // t.Fatal(err) + // } + + source, err = driver.Source(NewSourceOption().WithScope("AppiumAUT")) + if err != nil { + t.Fatal(err) + } + + // source, err = driver.Source(NewSourceOption().WithFormatAsJson()) + // if err != nil { + // t.Fatal(err) + // } + + // source, err = driver.Source(NewSourceOption().WithFormatAsDescription()) + // if err != nil { + // t.Fatal(err) + // } + + // source, err = driver.Source(NewSourceOption().WithFormatAsXml().WithExcludedAttributes([]string{"label", "type", "index"})) + // if err != nil { + // t.Fatal(err) + // } + + _ = source + fmt.Println(source) +} + +func Test_remoteWD_AccessibleSource(t *testing.T) { + setup(t) + + source, err := driver.AccessibleSource() + if err != nil { + t.Fatal(err) + } + _ = source + fmt.Println(source) +} + +func Test_remoteWD_Wait(t *testing.T) { + setup(t) + + var element WebElement + var err error + + by := BySelector{Name: "通知"} + // driver.AppLaunch() + exists := func(d WebDriver) (bool, error) { + element, err = d.FindElement(by) + if err == nil { + return true, nil + } + return false, nil + } + _ = exists + _ = element + + err = driver.AppLaunchUnattached(bundleId) + if err != nil { + t.Fatal(err) + } + // element, err = driver.FindElement(by) + err = driver.WaitWithTimeoutAndInterval(exists, time.Second*10, time.Millisecond*10) + if err != nil { + t.Fatal(err) + } + + // t.Log(element.Rect()) +} + +func Test_remoteWD_Location(t *testing.T) { + setup(t) + + location, err := driver.Location() + if err != nil { + t.Fatal(err) + } + t.Log(location) +} + +func Test_remoteWD_KeyboardDismiss(t *testing.T) { + setup(t) + + err := driver.KeyboardDismiss() + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWD_ExpectNotification(t *testing.T) { + setup(t) + + // bundleId = "com.apple.shortcuts" + // err := driver.ExpectNotification("shortcuts", NotificationTypePlain, 10) + // if err != nil { + // t.Fatal(err) + // } +} + +func Test_remoteWD_IOHIDEvent(t *testing.T) { + setup(t) + + err := driver.IOHIDEvent(EventPageIDConsumer, EventUsageIDCsmrVolumeDown) + if err != nil { + t.Fatal(err) + } +} + +func setupElement(t *testing.T, by BySelector) WebElement { + setup(t) + element, err := driver.FindElement(by) + if err != nil { + t.Fatal(err) + } + return element +} + +func Test_remoteWE_Click(t *testing.T) { + element := setupElement(t, BySelector{LinkText: NewElementAttribute().WithLabel("设置")}) + + err := element.Click() + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_SendKeys(t *testing.T) { + element := setupElement(t, BySelector{ClassName: ElementType{SearchField: true}}) + + err := element.SendKeys("App Store") + // err := element.SendKeys("App Store", 3) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_Clear(t *testing.T) { + element := setupElement(t, BySelector{ClassName: ElementType{SearchField: true}}) + + err := element.Clear() + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_Tap(t *testing.T) { + element := setupElement(t, BySelector{Name: "touchableView"}) + + err := element.Tap(10, 20) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_DoubleTap(t *testing.T) { + element := setupElement(t, BySelector{Name: "touchableView"}) + + err := element.DoubleTap() + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_TouchAndHold(t *testing.T) { + element := setupElement(t, BySelector{Name: "touchableView"}) + + err := element.TouchAndHold(-1) + // err := element.TouchAndHold(5) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_TwoFingerTap(t *testing.T) { + element := setupElement(t, BySelector{Name: "touchableView"}) + + err := element.TwoFingerTap() + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_TapWithNumberOfTaps(t *testing.T) { + element := setupElement(t, BySelector{Name: "touchableView"}) + + err := element.TapWithNumberOfTaps(3, 3) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_ForceTouch(t *testing.T) { + element := setupElement(t, BySelector{Name: "touchableView"}) + + // err := element.ForceTouch(1, -1) + err := element.ForceTouchFloat(10, 20, 1, -1) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_Drag(t *testing.T) { + element := setupElement(t, BySelector{Name: "touchableView"}) + + // err := element.Drag(10, 20, 10, 300, -1) + err := element.Swipe(10, 20, 10, 300) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_SwipeDirection(t *testing.T) { + element := setupElement(t, BySelector{Name: "touchableView"}) + + // err := element.SwipeDirection(DirectionUp, -1) + err := element.SwipeDirection(DirectionDown, 120) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_Pinch(t *testing.T) { + element := setupElement(t, BySelector{Name: "touchableView"}) + + // zoom in + // err := element.Pinch(2,10) + // zoom out + err := element.Pinch(0.9, -4.5) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_PinchToZoomOutByW3CAction(t *testing.T) { + element := setupElement(t, BySelector{Name: "touchableView"}) + + err := element.PinchToZoomOutByW3CAction(15) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_Rotate(t *testing.T) { + element := setupElement(t, BySelector{Name: "touchableView"}) + + // 90 CW + // err := element.Rotate(math.Pi / 2) + // 180 CCW + err := element.Rotate(math.Pi * -2) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_PickerWheelSelect(t *testing.T) { + element := setupElement(t, BySelector{ClassName: ElementType{PickerWheel: true}}) + + err := element.PickerWheelSelect(PickerWheelOrderNext, 3) + if err != nil { + t.Fatal(err) + } + err = element.PickerWheelSelect(PickerWheelOrderPrevious) + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_scroll(t *testing.T) { + element := setupElement(t, BySelector{ClassName: ElementType{Table: true}}) + + var err error + // err = element.ScrollElementByName("电池") + // err = element.ScrollElementByPredicate("type == 'XCUIElementTypeCell' AND name LIKE 'Safari*'") + err = element.ScrollDirection(DirectionDown, 0.8) + + // element, err = driver.FindElement(BySelector{PartialLinkText: NewElementAttribute().WithLabel("Safari")}) + // if err != nil { + // t.Fatal(err) + // } + // err = element.ScrollToVisible() + + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_FindElement(t *testing.T) { + element := setupElement(t, BySelector{ClassName: ElementType{Table: true}}) + + var err error + element, err = element.FindElement(BySelector{PartialLinkText: NewElementAttribute().WithLabel("Safari")}) + if err != nil { + t.Fatal(err) + } + + err = element.Click() + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_FindElements(t *testing.T) { + element := setupElement(t, BySelector{ClassName: ElementType{Table: true}}) + + elements, err := element.FindElements(BySelector{ClassName: ElementType{Cell: true}}) + if err != nil { + t.Fatal(err) + } + + err = elements[0].Click() + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_FindVisibleCells(t *testing.T) { + element := setupElement(t, BySelector{ClassName: ElementType{Table: true}}) + + cells, err := element.FindVisibleCells() + if err != nil { + t.Fatal(err) + } + + err = cells[0].Click() + if err != nil { + t.Fatal(err) + } +} + +func Test_remoteWE_Rect(t *testing.T) { + element := setupElement(t, BySelector{ClassName: ElementType{Switch: true}}) + + rect, err := element.Rect() + if err != nil { + t.Fatal(err) + } + location, err := element.Location() + if err != nil { + t.Fatal(err) + } + size, err := element.Size() + if err != nil { + t.Fatal(err) + } + _, _, _ = rect, location, size + t.Log(rect, location, size) +} + +func Test_remoteWE_Text(t *testing.T) { + element := setupElement(t, BySelector{ClassName: ElementType{Switch: true}}) + + text, err := element.Text() + if err != nil { + t.Fatal(err) + } + _ = text + // t.Log(text) +} + +func Test_remoteWE_Type(t *testing.T) { + element := setupElement(t, BySelector{ClassName: ElementType{Switch: true}}) + + elemType, err := element.Type() + if err != nil { + t.Fatal(err) + } + _ = elemType + // t.Log(elemType) +} + +func Test_remoteWE_IsEnabled(t *testing.T) { + element := setupElement(t, BySelector{ClassName: ElementType{Switch: true}}) + + enabled, err := element.IsEnabled() + if err != nil { + t.Fatal(err) + } + _ = enabled + // t.Log(enabled) +} + +func Test_remoteWE_IsDisplayed(t *testing.T) { + element := setupElement(t, BySelector{PartialLinkText: NewElementAttribute().WithLabel("Safari")}) + + displayed, err := element.IsDisplayed() + if err != nil { + t.Fatal(err) + } + _ = displayed + // t.Log(displayed) +} + +func Test_remoteWE_IsSelected(t *testing.T) { + element := setupElement(t, BySelector{ClassName: ElementType{Switch: true}}) + // element := setupElement(t, BySelector{Name: "添加到主屏幕"}) + // element := setupElement(t, BySelector{Name: "仅App资源库"}) + + selected, err := element.IsSelected() + if err != nil { + t.Fatal(err) + } + _ = selected + // t.Log(selected) +} + +func Test_remoteWE_IsAccessible(t *testing.T) { + element := setupElement(t, BySelector{ClassName: ElementType{Switch: true}}) + + accessible, err := element.IsAccessible() + if err != nil { + t.Fatal(err) + } + _ = accessible + // t.Log(accessible) +} + +func Test_remoteWE_IsAccessibilityContainer(t *testing.T) { + // element := setupElement(t, BySelector{ClassName: ElementType{Switch: true}}) + element := setupElement(t, BySelector{ClassName: ElementType{Table: true}}) + + isAccessibilityContainer, err := element.IsAccessibilityContainer() + if err != nil { + t.Fatal(err) + } + _ = isAccessibilityContainer + // t.Log(isAccessibilityContainer) +} + +func Test_remoteWE_GetAttribute(t *testing.T) { + element := setupElement(t, BySelector{ClassName: ElementType{StaticText: true}}) + + value, err := element.GetAttribute(NewElementAttribute().WithValue("")) + if err != nil { + t.Fatal(err) + } + _ = value + // t.Log(value) +} + +func Test_remoteWE_Screenshot(t *testing.T) { + element := setupElement(t, BySelector{ClassName: ElementType{TextView: true}}) + + screenshot, err := element.Screenshot() + if err != nil { + t.Fatal(err) + } + _ = screenshot + + // img, format, err := image.Decode(screenshot) + // if err != nil { + // t.Fatal(err) + // } + // userHomeDir, _ := os.UserHomeDir() + // file, err := os.Create(userHomeDir + "/Desktop/e1." + format) + // if err != nil { + // t.Fatal(err) + // } + // defer func() { _ = file.Close() }() + // switch format { + // case "png": + // err = png.Encode(file, img) + // case "jpeg": + // err = jpeg.Encode(file, img, nil) + // } + // if err != nil { + // t.Fatal(err) + // } + // t.Log(file.Name()) +} diff --git a/hrp/internal/uixt/ios_webdriver.go b/hrp/internal/uixt/ios_webdriver.go new file mode 100644 index 00000000..fb1c5337 --- /dev/null +++ b/hrp/internal/uixt/ios_webdriver.go @@ -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 +} diff --git a/hrp/internal/uixt/ios_webelement.go b/hrp/internal/uixt/ios_webelement.go new file mode 100644 index 00000000..805a67c2 --- /dev/null +++ b/hrp/internal/uixt/ios_webelement.go @@ -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 +} diff --git a/hrp/internal/uixt/ocr_on.go b/hrp/internal/uixt/ocr_on.go index 68739172..5809020b 100644 --- a/hrp/internal/uixt/ocr_on.go +++ b/hrp/internal/uixt/ocr_on.go @@ -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"` diff --git a/hrp/internal/uixt/opencv_off.go b/hrp/internal/uixt/opencv_off.go index 18045295..4fc747c1 100644 --- a/hrp/internal/uixt/opencv_off.go +++ b/hrp/internal/uixt/opencv_off.go @@ -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) } diff --git a/hrp/internal/uixt/opencv_on.go b/hrp/internal/uixt/opencv_on.go index 326e9277..5fdc7197 100644 --- a/hrp/internal/uixt/opencv_on.go +++ b/hrp/internal/uixt/opencv_on.go @@ -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 diff --git a/hrp/internal/uixt/swipe.go b/hrp/internal/uixt/swipe.go index 4d29e05b..7973b53a 100644 --- a/hrp/internal/uixt/swipe.go +++ b/hrp/internal/uixt/swipe.go @@ -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], }) diff --git a/hrp/internal/uixt/tap.go b/hrp/internal/uixt/tap.go index ac6c51fc..1d4eb168 100644 --- a/hrp/internal/uixt/tap.go +++ b/hrp/internal/uixt/tap.go @@ -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) } diff --git a/hrp/internal/uixt/tap_test.go b/hrp/internal/uixt/tap_test.go index 482c06f4..6fc499e1 100644 --- a/hrp/internal/uixt/tap_test.go +++ b/hrp/internal/uixt/tap_test.go @@ -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) diff --git a/hrp/step_android_ui.go b/hrp/step_android_ui.go index b6f44c97..b5b74f72 100644 --- a/hrp/step_android_ui.go +++ b/hrp/step_android_ui.go @@ -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 } diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index 7a6a4327..588b3728 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -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 }