From 58289557b6abd83bdaca7cd71ee3fa71c7351ba7 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Sun, 25 Sep 2022 00:47:12 +0800 Subject: [PATCH] refactor: move guia2 into uixt --- go.mod | 1 + go.sum | 2 + hrp/internal/uixt/README.md | 4 +- hrp/internal/uixt/android_action.go | 157 +++ hrp/internal/uixt/android_device.go | 544 +++++++++- hrp/internal/uixt/android_driver.go | 1203 ++++++++++++++++++++ hrp/internal/uixt/android_elment.go | 238 ++++ hrp/internal/uixt/android_key.go | 879 +++++++++++++++ hrp/internal/uixt/android_test.go | 1384 ++++++++++++++++++++++++ hrp/internal/uixt/android_webdriver.go | 3 - hrp/internal/uixt/android_webelment.go | 1 - hrp/internal/uixt/client.go | 104 ++ hrp/internal/uixt/gesture.go | 1 + hrp/internal/uixt/interface.go | 12 + hrp/internal/uixt/ios_device.go | 50 +- hrp/internal/uixt/ios_driver.go | 125 +-- hrp/internal/uixt/ios_test.go | 2 - hrp/internal/uixt/tap_test.go | 2 - 18 files changed, 4557 insertions(+), 155 deletions(-) create mode 100644 hrp/internal/uixt/android_driver.go create mode 100644 hrp/internal/uixt/android_elment.go create mode 100644 hrp/internal/uixt/android_key.go create mode 100644 hrp/internal/uixt/android_test.go delete mode 100644 hrp/internal/uixt/android_webdriver.go delete mode 100644 hrp/internal/uixt/android_webelment.go create mode 100644 hrp/internal/uixt/client.go diff --git a/go.mod b/go.mod index 57d30aeb..9148a7aa 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.18 require ( github.com/andybalholm/brotli v1.0.4 github.com/denisbrodbeck/machineid v1.0.1 + github.com/electricbubble/gadb v0.0.7 github.com/electricbubble/gidevice v0.6.2 github.com/electricbubble/opencv-helper v0.0.3 github.com/fatih/color v1.13.0 diff --git a/go.sum b/go.sum index 6b9beca0..1f3a0ba3 100644 --- a/go.sum +++ b/go.sum @@ -98,6 +98,8 @@ 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/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ= github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI= +github.com/electricbubble/gadb v0.0.7 h1:fxvVLVNs3IFKuYAEXDF2tDZUjT9jNCltoTSirjM5dgo= +github.com/electricbubble/gadb v0.0.7/go.mod h1:3293YJ6OWHv/Q6NA5dwSbK43MbmYm8+Vz2d7h5J3IA8= github.com/electricbubble/gidevice v0.6.2 h1:eIeCHH7Xn5fTwnUv3qL8c7L4anKIHtjlTBkgr1LDVTc= github.com/electricbubble/gidevice v0.6.2/go.mod h1:bRHL2M9qgeEKju8KRvKMZUVEg7t5zMnTiG3SJ3QDH5o= github.com/electricbubble/opencv-helper v0.0.3 h1:p0sHTUPPPm8GqzVUtYH+wQbJoguzotUXVRAS7Ibk7nI= diff --git a/hrp/internal/uixt/README.md b/hrp/internal/uixt/README.md index bfb7d26f..6fc5449a 100644 --- a/hrp/internal/uixt/README.md +++ b/hrp/internal/uixt/README.md @@ -38,13 +38,13 @@ This uixt module is initially forked from the following repos and made a lot of - [electricbubble/gwda-ext-opencv] - [electricbubble/gwda] -- [electricbubble/guia] +- [electricbubble/guia2] [electricbubble/gwda-ext-opencv]: https://github.com/electricbubble/gwda-ext-opencv [appium/WebDriverAgent]: https://github.com/appium/WebDriverAgent [electricbubble/gwda]: https://github.com/electricbubble/gwda -[electricbubble/guia]: https://github.com/electricbubble/guia2 +[electricbubble/guia2]: https://github.com/electricbubble/guia2 [OpenCV 4]: https://opencv.org/ [hybridgroup/gocv]: https://github.com/hybridgroup/gocv [volcengine]: https://www.volcengine.com/product/text-recognition diff --git a/hrp/internal/uixt/android_action.go b/hrp/internal/uixt/android_action.go index 64c90179..254972d3 100644 --- a/hrp/internal/uixt/android_action.go +++ b/hrp/internal/uixt/android_action.go @@ -1 +1,158 @@ package uixt + +import "strings" + +type touchGesture struct { + Touch PointF `json:"touch"` + Time float64 `json:"time"` +} + +type TouchAction []touchGesture + +func NewTouchAction(cap ...int) *TouchAction { + if len(cap) == 0 || cap[0] <= 0 { + cap = []int{8} + } + tmp := make(TouchAction, 0, cap[0]) + return &tmp +} + +func (ta *TouchAction) Add(x, y int, startTime ...float64) *TouchAction { + return ta.AddFloat(float64(x), float64(y), startTime...) +} + +func (ta *TouchAction) AddFloat(x, y float64, startTime ...float64) *TouchAction { + if len(startTime) == 0 { + var tmp float64 = 0 + if len(*ta) != 0 { + g := (*ta)[len(*ta)-1] + tmp = g.Time + 0.05 + } + startTime = []float64{tmp} + } + *ta = append(*ta, touchGesture{Touch: PointF{x, y}, Time: startTime[0]}) + return ta +} + +func (ta *TouchAction) AddPoint(point Point, startTime ...float64) *TouchAction { + return ta.AddFloat(float64(point.X), float64(point.Y), startTime...) +} + +func (ta *TouchAction) AddPointF(point PointF, startTime ...float64) *TouchAction { + return ta.AddFloat(point.X, point.Y, startTime...) +} + +func (d *uiaDriver) MultiPointerGesture(gesture1 *TouchAction, gesture2 *TouchAction, tas ...*TouchAction) (err error) { + // Must provide coordinates for at least 2 pointers + actions := make([]*TouchAction, 0) + actions = append(actions, gesture1, gesture2) + if len(tas) != 0 { + actions = append(actions, tas...) + } + data := map[string]interface{}{ + "actions": actions, + } + // register(postHandler, new MultiPointerGesture("/wd/hub/session/:sessionId/touch/multi/perform")) + _, err = d.httpPOST(data, "/session", d.sessionId, "/touch/multi/perform") + return +} + +type w3cGesture map[string]interface{} + +func _newW3CGesture() w3cGesture { + return make(w3cGesture) +} + +func (g w3cGesture) _set(key string, value interface{}) w3cGesture { + g[key] = value + return g +} + +func (g w3cGesture) pause(duration float64) w3cGesture { + return g._set("type", "pause"). + _set("duration", duration) +} + +func (g w3cGesture) keyDown(value string) w3cGesture { + return g._set("type", "keyDown"). + _set("value", value) +} + +func (g w3cGesture) keyUp(value string) w3cGesture { + return g._set("type", "keyUp"). + _set("value", value) +} + +func (g w3cGesture) pointerDown(button int) w3cGesture { + return g._set("type", "pointerDown")._set("button", button) +} + +func (g w3cGesture) pointerUp(button int) w3cGesture { + return g._set("type", "pointerUp")._set("button", button) +} + +func (g w3cGesture) pointerMove(x, y float64, origin string, duration float64, pressureAndSize ...float64) w3cGesture { + switch len(pressureAndSize) { + case 1: + g._set("pressure", pressureAndSize[0]) + case 2: + g._set("pressure", pressureAndSize[0]) + g._set("size", pressureAndSize[1]) + } + return g._set("type", "pointerMove"). + _set("duration", duration). + _set("origin", origin). + _set("x", x). + _set("y", y) +} + +func (g w3cGesture) size(size ...float64) w3cGesture { + if len(size) == 0 { + size = []float64{1.0} + } + return g._set("size", size[0]) +} + +func (g w3cGesture) pressure(pressure ...float64) w3cGesture { + if len(pressure) == 0 { + pressure = []float64{1.0} + } + return g._set("pressure", pressure[0]) +} + +type W3CGestures []w3cGesture + +func NewW3CGestures(cap ...int) *W3CGestures { + if len(cap) == 0 || cap[0] <= 0 { + cap = []int{8} + } + tmp := make(W3CGestures, 0, cap[0]) + return &tmp +} + +func (g *W3CGestures) Pause(duration ...float64) *W3CGestures { + if len(duration) == 0 || duration[0] < 0 { + duration = []float64{0.5} + } + *g = append(*g, _newW3CGesture().pause(duration[0]*1000)) + return g +} + +func (g *W3CGestures) KeyDown(value string) *W3CGestures { + *g = append(*g, _newW3CGesture().keyDown(value)) + return g +} + +func (g *W3CGestures) KeyUp(value string) *W3CGestures { + *g = append(*g, _newW3CGesture().keyUp(value)) + return g +} + +func (g *W3CGestures) SendKeys(text string) *W3CGestures { + ss := strings.Split(text, "") + for i := range ss { + g.KeyDown(ss[i]) + g.KeyUp(ss[i]) + } + return g +} diff --git a/hrp/internal/uixt/android_device.go b/hrp/internal/uixt/android_device.go index b7a3a332..ae394be4 100644 --- a/hrp/internal/uixt/android_device.go +++ b/hrp/internal/uixt/android_device.go @@ -1,7 +1,117 @@ package uixt +import ( + "bytes" + "fmt" + "net" + "reflect" + + "github.com/electricbubble/gadb" + "github.com/pkg/errors" +) + +var ( + AdbServerHost = "localhost" + AdbServerPort = gadb.AdbServerPort // 5037 + UIA2ServerPort = 6790 + DeviceTempPath = "/data/local/tmp" +) + +const forwardToPrefix = "forward-to-" + +func InitUIAClient(device *AndroidDevice) (*DriverExt, error) { + var deviceOptions []AndroidDeviceOption + if device.SerialNumber != "" { + deviceOptions = append(deviceOptions, WithSerialNumber(device.SerialNumber)) + } + if device.IP != "" { + deviceOptions = append(deviceOptions, WithAdbIP(device.IP)) + } + if device.Port != 0 { + deviceOptions = append(deviceOptions, WithAdbPort(device.Port)) + } + + // init uia device + androidDevice, err := NewAndroidDevice(deviceOptions...) + if err != nil { + return nil, err + } + + driver, err := androidDevice.NewUSBDriver(nil) + if err != nil { + return nil, errors.Wrap(err, "failed to init UIA driver") + } + fmt.Println(driver) + + var driverExt *DriverExt + // TODO + // driverExt, err = Extend(driver) + // if err != nil { + // return nil, errors.Wrap(err, "failed to extend UIA Driver") + // } + + return driverExt, nil +} + +type AndroidDeviceOption func(*AndroidDevice) + +func WithSerialNumber(serial string) AndroidDeviceOption { + return func(device *AndroidDevice) { + device.SerialNumber = serial + } +} + +func WithAdbIP(ip string) AndroidDeviceOption { + return func(device *AndroidDevice) { + device.IP = ip + } +} + +func WithAdbPort(port int) AndroidDeviceOption { + return func(device *AndroidDevice) { + device.Port = port + } +} + +func WithAdbLogOn(logOn bool) AndroidDeviceOption { + return func(device *AndroidDevice) { + device.LogOn = logOn + } +} + +func NewAndroidDevice(options ...AndroidDeviceOption) (device *AndroidDevice, err error) { + deviceList, err := DeviceList() + if err != nil { + return nil, fmt.Errorf("get attached devices failed: %v", err) + } + + device = &AndroidDevice{ + Port: UIA2ServerPort, + IP: AdbServerHost, + } + for _, option := range options { + option(device) + } + + serialNumber := device.SerialNumber + for _, dev := range deviceList { + // find device by serial number if specified + if serialNumber != "" && dev.Serial() != serialNumber { + continue + } + + device.SerialNumber = dev.Serial() + device.Device = dev + return device, nil + } + + return nil, fmt.Errorf("device %s not found", device.SerialNumber) +} + type AndroidDevice struct { + gadb.Device SerialNumber string `json:"serial,omitempty" yaml:"serial,omitempty"` + IP string `json:"ip,omitempty" yaml:"ip,omitempty"` Port int `json:"port,omitempty" yaml:"port,omitempty"` LogOn bool `json:"log_on,omitempty" yaml:"log_on,omitempty"` } @@ -10,6 +120,436 @@ func (o AndroidDevice) UUID() string { return o.SerialNumber } -func InitUIAClient(device *AndroidDevice) (*DriverExt, error) { - return nil, nil +func DeviceList() (devices []gadb.Device, err error) { + var adbClient gadb.Client + if adbClient, err = gadb.NewClientWith(AdbServerHost, AdbServerPort); err != nil { + return nil, err + } + + return adbClient.DeviceList() +} + +// NewUSBDriver creates new client via USB connected device, this will also start a new session. +// TODO: replace uiaDriver with WebDriver +func (dev *AndroidDevice) NewUSBDriver(capabilities Capabilities) (driver *uiaDriver, err error) { + var localPort int + if localPort, err = getFreePort(); err != nil { + return nil, err + } + if err = dev.Forward(localPort, UIA2ServerPort); err != nil { + return nil, err + } + + rawURL := fmt.Sprintf("http://%s%d:6790/wd/hub", forwardToPrefix, localPort) + driver, err = NewUIADriver(capabilities, rawURL) + if err != nil { + _ = dev.ForwardKill(localPort) + return nil, err + } + driver.adbDevice = dev.Device + driver.localPort = localPort + + conn, err := net.Dial("tcp", fmt.Sprintf(":%d", localPort)) + if err != nil { + return nil, fmt.Errorf("adb forward: %w", err) + } + driver.client = convertToHTTPClient(conn) + + return driver, nil +} + +// NewHTTPDriver creates new remote HTTP client, this will also start a new session. +// TODO: replace uiaDriver with WebDriver +func (dev *AndroidDevice) NewHTTPDriver(capabilities Capabilities) (driver *uiaDriver, err error) { + rawURL := fmt.Sprintf("http://%s:%d/wd/hub", dev.IP, dev.Port) + if driver, err = NewUIADriver(capabilities, rawURL); err != nil { + return nil, err + } + driver.adbDevice = dev.Device + return driver, nil +} + +func getFreePort() (int, error) { + addr, err := net.ResolveTCPAddr("tcp", "localhost:0") + if err != nil { + return 0, fmt.Errorf("free port: %w", err) + } + + l, err := net.ListenTCP("tcp", addr) + if err != nil { + return 0, fmt.Errorf("free port: %w", err) + } + defer func() { _ = l.Close() }() + return l.Addr().(*net.TCPAddr).Port, nil +} + +type UiSelectorHelper struct { + value *bytes.Buffer +} + +func NewUiSelectorHelper() UiSelectorHelper { + return UiSelectorHelper{value: bytes.NewBufferString("new UiSelector()")} +} + +func (s UiSelectorHelper) String() string { + return s.value.String() + ";" +} + +// Text Set the search criteria to match the visible text displayed +// in a widget (for example, the text label to launch an app). +// +// The text for the element must match exactly with the string in your input +// argument. Matching is case-sensitive. +func (s UiSelectorHelper) Text(text string) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.text("%s")`, text)) + return s +} + +// TextMatches Set the search criteria to match the visible text displayed in a layout +// element, using a regular expression. +// +// The text in the widget must match exactly with the string in your +// input argument. +func (s UiSelectorHelper) TextMatches(regex string) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.textMatches("%s")`, regex)) + return s +} + +// TextStartsWith Set the search criteria to match visible text in a widget that is +// prefixed by the text parameter. +// +// The matching is case-insensitive. +func (s UiSelectorHelper) TextStartsWith(text string) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.textStartsWith("%s")`, text)) + return s +} + +// TextContains Set the search criteria to match the visible text in a widget +// where the visible text must contain the string in your input argument. +// +// The matching is case-sensitive. +func (s UiSelectorHelper) TextContains(text string) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.textContains("%s")`, text)) + return s +} + +// ClassName Set the search criteria to match the class property +// for a widget (for example, "android.widget.Button"). +func (s UiSelectorHelper) ClassName(className string) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.className("%s")`, className)) + return s +} + +// ClassNameMatches Set the search criteria to match the class property +// for a widget, using a regular expression. +func (s UiSelectorHelper) ClassNameMatches(regex string) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.classNameMatches("%s")`, regex)) + return s +} + +// Description Set the search criteria to match the content-description +// property for a widget. +// +// The content-description is typically used +// by the Android Accessibility framework to +// provide an audio prompt for the widget when +// the widget is selected. The content-description +// for the widget must match exactly +// with the string in your input argument. +// +// Matching is case-sensitive. +func (s UiSelectorHelper) Description(desc string) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.description("%s")`, desc)) + return s +} + +// DescriptionMatches Set the search criteria to match the content-description +// property for a widget. +// +// The content-description is typically used +// by the Android Accessibility framework to +// provide an audio prompt for the widget when +// the widget is selected. The content-description +// for the widget must match exactly +// with the string in your input argument. +func (s UiSelectorHelper) DescriptionMatches(regex string) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.descriptionMatches("%s")`, regex)) + return s +} + +// DescriptionStartsWith Set the search criteria to match the content-description +// property for a widget. +// +// The content-description is typically used +// by the Android Accessibility framework to +// provide an audio prompt for the widget when +// the widget is selected. The content-description +// for the widget must start +// with the string in your input argument. +// +// Matching is case-insensitive. +func (s UiSelectorHelper) DescriptionStartsWith(desc string) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.descriptionStartsWith("%s")`, desc)) + return s +} + +// DescriptionContains Set the search criteria to match the content-description +// property for a widget. +// +// The content-description is typically used +// by the Android Accessibility framework to +// provide an audio prompt for the widget when +// the widget is selected. The content-description +// for the widget must contain +// the string in your input argument. +// +// Matching is case-insensitive. +func (s UiSelectorHelper) DescriptionContains(desc string) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.descriptionContains("%s")`, desc)) + return s +} + +// ResourceId Set the search criteria to match the given resource ID. +func (s UiSelectorHelper) ResourceId(id string) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.resourceId("%s")`, id)) + return s +} + +// ResourceIdMatches Set the search criteria to match the resource ID +// of the widget, using a regular expression. +func (s UiSelectorHelper) ResourceIdMatches(regex string) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.resourceIdMatches("%s")`, regex)) + return s +} + +// Index Set the search criteria to match the widget by its node +// index in the layout hierarchy. +// +// The index value must be 0 or greater. +// +// Using the index can be unreliable and should only +// be used as a last resort for matching. Instead, +// consider using the `Instance(int)` method. +func (s UiSelectorHelper) Index(index int) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.index(%d)`, index)) + return s +} + +// Instance Set the search criteria to match the +// widget by its instance number. +// +// The instance value must be 0 or greater, where +// the first instance is 0. +// +// For example, to simulate a user click on +// the third image that is enabled in a UI screen, you +// could specify a a search criteria where the instance is +// 2, the `className(String)` matches the image +// widget class, and `enabled(boolean)` is true. +// The code would look like this: +// `new UiSelector().className("android.widget.ImageView") +// .enabled(true).instance(2);` +func (s UiSelectorHelper) Instance(instance int) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.instance(%d)`, instance)) + return s +} + +// Enabled Set the search criteria to match widgets that are enabled. +// +// Typically, using this search criteria alone is not useful. +// You should also include additional criteria, such as text, +// content-description, or the class name for a widget. +// +// If no other search criteria is specified, and there is more +// than one matching widget, the first widget in the tree +// is selected. +func (s UiSelectorHelper) Enabled(b bool) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.enabled(%t)`, b)) + return s +} + +// Focused Set the search criteria to match widgets that have focus. +// +// Typically, using this search criteria alone is not useful. +// You should also include additional criteria, such as text, +// content-description, or the class name for a widget. +// +// If no other search criteria is specified, and there is more +// than one matching widget, the first widget in the tree +// is selected. +func (s UiSelectorHelper) Focused(b bool) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.focused(%t)`, b)) + return s +} + +// Focusable Set the search criteria to match widgets that are focusable. +// +// Typically, using this search criteria alone is not useful. +// You should also include additional criteria, such as text, +// content-description, or the class name for a widget. +// +// If no other search criteria is specified, and there is more +// than one matching widget, the first widget in the tree +// is selected. +func (s UiSelectorHelper) Focusable(b bool) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.focusable(%t)`, b)) + return s +} + +// Scrollable Set the search criteria to match widgets that are scrollable. +// +// Typically, using this search criteria alone is not useful. +// You should also include additional criteria, such as text, +// content-description, or the class name for a widget. +// +// If no other search criteria is specified, and there is more +// than one matching widget, the first widget in the tree +// is selected. +func (s UiSelectorHelper) Scrollable(b bool) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.scrollable(%t)`, b)) + return s +} + +// Selected Set the search criteria to match widgets that +// are currently selected. +// +// Typically, using this search criteria alone is not useful. +// You should also include additional criteria, such as text, +// content-description, or the class name for a widget. +// +// If no other search criteria is specified, and there is more +// than one matching widget, the first widget in the tree +// is selected. +func (s UiSelectorHelper) Selected(b bool) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.selected(%t)`, b)) + return s +} + +// Checked Set the search criteria to match widgets that +// are currently checked (usually for checkboxes). +// +// Typically, using this search criteria alone is not useful. +// You should also include additional criteria, such as text, +// content-description, or the class name for a widget. +// +// If no other search criteria is specified, and there is more +// than one matching widget, the first widget in the tree +// is selected. +func (s UiSelectorHelper) Checked(b bool) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.checked(%t)`, b)) + return s +} + +// Checkable Set the search criteria to match widgets that are checkable. +// +// Typically, using this search criteria alone is not useful. +// You should also include additional criteria, such as text, +// content-description, or the class name for a widget. +// +// If no other search criteria is specified, and there is more +// than one matching widget, the first widget in the tree +// is selected. +func (s UiSelectorHelper) Checkable(b bool) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.checkable(%t)`, b)) + return s +} + +// Clickable Set the search criteria to match widgets that are clickable. +// +// Typically, using this search criteria alone is not useful. +// You should also include additional criteria, such as text, +// content-description, or the class name for a widget. +// +// If no other search criteria is specified, and there is more +// than one matching widget, the first widget in the tree +// is selected. +func (s UiSelectorHelper) Clickable(b bool) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.clickable(%t)`, b)) + return s +} + +// LongClickable Set the search criteria to match widgets that are long-clickable. +// +// Typically, using this search criteria alone is not useful. +// You should also include additional criteria, such as text, +// content-description, or the class name for a widget. +// +// If no other search criteria is specified, and there is more +// than one matching widget, the first widget in the tree +// is selected. +func (s UiSelectorHelper) LongClickable(b bool) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.longClickable(%t)`, b)) + return s +} + +// packageName Set the search criteria to match the package name +// of the application that contains the widget. +func (s UiSelectorHelper) packageName(name string) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.packageName(%s)`, name)) + return s +} + +// PackageNameMatches Set the search criteria to match the package name +// of the application that contains the widget. +func (s UiSelectorHelper) PackageNameMatches(regex string) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.packageNameMatches(%s)`, regex)) + return s +} + +// ChildSelector Adds a child UiSelector criteria to this selector. +// +// Use this selector to narrow the search scope to +// child widgets under a specific parent widget. +func (s UiSelectorHelper) ChildSelector(selector UiSelectorHelper) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.childSelector(%s)`, selector.value.String())) + return s +} + +func (s UiSelectorHelper) PatternSelector(selector UiSelectorHelper) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.patternSelector(%s)`, selector.value.String())) + return s +} + +func (s UiSelectorHelper) ContainerSelector(selector UiSelectorHelper) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.containerSelector(%s)`, selector.value.String())) + return s +} + +// FromParent Adds a child UiSelector criteria to this selector which is used to +// start search from the parent widget. +// +// Use this selector to narrow the search scope to +// sibling widgets as well all child widgets under a parent. +func (s UiSelectorHelper) FromParent(selector UiSelectorHelper) UiSelectorHelper { + s.value.WriteString(fmt.Sprintf(`.fromParent(%s)`, selector.value.String())) + return s +} + +type AndroidBySelector struct { + // Set the search criteria to match the given resource ResourceIdID. + ResourceIdID string `json:"id"` + // Set the search criteria to match the content-description property for a widget. + ContentDescription string `json:"accessibility id"` + XPath string `json:"xpath"` + // Set the search criteria to match the class property for a widget (for example, "android.widget.Button"). + ClassName string `json:"class name"` + UiAutomator string `json:"-android uiautomator"` +} + +func (by AndroidBySelector) getMethodAndSelector() (method, selector string) { + vBy := reflect.ValueOf(by) + tBy := reflect.TypeOf(by) + for i := 0; i < vBy.NumField(); i++ { + vi := vBy.Field(i).Interface() + // switch vi := vi.(type) { + // case string: + // selector = vi + // } + selector = vi.(string) + if selector != "" && selector != "UNKNOWN" { + method = tBy.Field(i).Tag.Get("json") + return + } + } + return } diff --git a/hrp/internal/uixt/android_driver.go b/hrp/internal/uixt/android_driver.go new file mode 100644 index 00000000..7851a3da --- /dev/null +++ b/hrp/internal/uixt/android_driver.go @@ -0,0 +1,1203 @@ +package uixt + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "net/url" + "os" + "path" + "path/filepath" + "regexp" + "strconv" + "strings" + "time" + + "github.com/electricbubble/gadb" +) + +type uiaDriver struct { + Driver + + adbDevice gadb.Device + localPort int +} + +func NewUIADriver(capabilities Capabilities, urlPrefix string) (driver *uiaDriver, err error) { + if capabilities == nil { + capabilities = NewCapabilities() + } + driver = new(uiaDriver) + if driver.urlPrefix, err = url.Parse(urlPrefix); err != nil { + return nil, err + } + if driver.sessionId, err = driver.NewSession(capabilities); err != nil { + return nil, err + } + return +} + +func (d *uiaDriver) NewSession(capabilities Capabilities) (sessionID string, err error) { + // register(postHandler, new NewSession("/wd/hub/session")) + var rawResp rawResponse + data := map[string]interface{}{"capabilities": capabilities} + if rawResp, err = d.httpPOST(data, "/session"); err != nil { + return "", err + } + reply := new(struct{ Value struct{ SessionId string } }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return "", err + } + sessionID = reply.Value.SessionId + // d.sessionIdCache[sessionID] = true + return +} + +func (d *uiaDriver) Quit() (err error) { + // register(deleteHandler, new DeleteSession("/wd/hub/session/:sessionId")) + if d.sessionId == "" { + return nil + } + if _, err = d.httpDELETE("/session", d.sessionId); err == nil { + d.sessionId = "" + } + + return err +} + +func (d *uiaDriver) ActiveSessionID() string { + return d.sessionId +} + +func (d *uiaDriver) SessionIDs() (sessionIDs []string, err error) { + // register(getHandler, new GetSessions("/wd/hub/sessions")) + var rawResp rawResponse + if rawResp, err = d.httpGET("/sessions"); err != nil { + return nil, err + } + reply := new(struct{ Value []struct{ SessionId string } }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return nil, err + } + + sessionIDs = make([]string, len(reply.Value)) + for i := range reply.Value { + sessionIDs[i] = reply.Value[i].SessionId + } + return +} + +func (d *uiaDriver) SessionDetails() (scrollData map[string]interface{}, err error) { + // register(getHandler, new GetSessionDetails("/wd/hub/session/:sessionId")) + var rawResp rawResponse + if rawResp, err = d.httpGET("/session", d.sessionId); err != nil { + return nil, err + } + reply := new(struct{ Value map[string]interface{} }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return nil, err + } + + scrollData = reply.Value + return +} + +func (d *uiaDriver) Status() (ready bool, err error) { + // register(getHandler, new Status("/wd/hub/status")) + var rawResp rawResponse + if rawResp, err = d.httpGET("/status"); err != nil { + return false, err + } + reply := new(struct { + Value struct { + // Message string + Ready bool + } + }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return false, err + } + ready = reply.Value.Ready + return +} + +// Screenshot grab device screenshot +func (d *uiaDriver) Screenshot() (raw *bytes.Buffer, err error) { + // register(getHandler, new CaptureScreenshot("/wd/hub/session/:sessionId/screenshot")) + var rawResp rawResponse + if rawResp, err = d.httpGET("/session", d.sessionId, "screenshot"); err != nil { + return nil, err + } + reply := new(struct{ Value string }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return nil, err + } + + var decodeStr []byte + if decodeStr, err = base64.StdEncoding.DecodeString(reply.Value); err != nil { + return nil, err + } + + raw = bytes.NewBuffer(decodeStr) + return +} + +func (d *uiaDriver) Orientation() (orientation Orientation, err error) { + // register(getHandler, new GetOrientation("/wd/hub/session/:sessionId/orientation")) + var rawResp rawResponse + if rawResp, err = d.httpGET("/session", d.sessionId, "orientation"); err != nil { + return "", err + } + reply := new(struct{ Value Orientation }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return "", err + } + + orientation = reply.Value + return +} + +func (d *uiaDriver) Rotation() (rotation Rotation, err error) { + // register(getHandler, new GetRotation("/wd/hub/session/:sessionId/rotation")) + var rawResp rawResponse + if rawResp, err = d.httpGET("/session", d.sessionId, "rotation"); err != nil { + return Rotation{}, err + } + reply := new(struct{ Value Rotation }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return Rotation{}, err + } + + rotation = reply.Value + return +} + +// DeviceSize get window size of the device +func (d *uiaDriver) DeviceSize() (deviceSize Size, err error) { + // register(getHandler, new GetDeviceSize("/wd/hub/session/:sessionId/window/:windowHandle/size")) + var rawResp rawResponse + if rawResp, err = d.httpGET("/session", d.sessionId, "window/:windowHandle/size"); err != nil { + return Size{}, err + } + reply := new(struct{ Value Size }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return Size{}, err + } + + deviceSize = reply.Value + return +} + +// Source get page source +func (d *uiaDriver) Source() (sXML string, err error) { + // register(getHandler, new Source("/wd/hub/session/:sessionId/source")) + var rawResp rawResponse + if rawResp, err = d.httpGET("/session", d.sessionId, "source"); err != nil { + return "", err + } + reply := new(struct{ Value string }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return "", err + } + + sXML = reply.Value + return +} + +// StatusBarHeight get status bar height of the device +func (d *uiaDriver) StatusBarHeight() (height int, err error) { + // register(getHandler, new GetSystemBars("/wd/hub/session/:sessionId/appium/device/system_bars")) + var rawResp rawResponse + if rawResp, err = d.httpGET("/session", d.sessionId, "appium/device/system_bars"); err != nil { + return 0, err + } + reply := new(struct{ Value struct{ StatusBar int } }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return 0, err + } + + height = reply.Value.StatusBar + return +} + +func (d *uiaDriver) check() error { + if d.adbDevice.Serial() == "" { + return errors.New("adb daemon: the device is not ready") + } + return nil +} + +// Dispose corresponds to the command: +// adb -s $serial forward --remove $localPort +func (d *uiaDriver) Dispose() (err error) { + if err = d.check(); err != nil { + return err + } + if d.localPort == 0 { + return nil + } + return d.adbDevice.ForwardKill(d.localPort) +} + +func (d *uiaDriver) ActiveAppActivity() (appActivity string, err error) { + if err = d.check(); err != nil { + return "", err + } + + var sOutput string + if sOutput, err = d.adbDevice.RunShellCommand("dumpsys activity activities | grep mResumedActivity"); err != nil { + return "", err + } + re := regexp.MustCompile(`\{(.+?)\}`) + if !re.MatchString(sOutput) { + return "", fmt.Errorf("active app activity: %s", strings.TrimSpace(sOutput)) + } + fields := strings.Fields(re.FindStringSubmatch(sOutput)[1]) + appActivity = fields[2] + return +} + +func (d *uiaDriver) ActiveAppPackageName() (appPackageName string, err error) { + var activity string + if activity, err = d.ActiveAppActivity(); err != nil { + return "", err + } + appPackageName = strings.Split(activity, "/")[0] + return +} + +func (d *uiaDriver) AppLaunch(appPackageName string, waitForComplete ...AndroidBySelector) (err error) { + if err = d.check(); err != nil { + return err + } + + var sOutput string + if sOutput, err = d.adbDevice.RunShellCommand("monkey -p", appPackageName, "-c android.intent.category.LAUNCHER 1"); err != nil { + return err + } + if strings.Contains(sOutput, "monkey aborted") { + return fmt.Errorf("app launch: %s", strings.TrimSpace(sOutput)) + } + + if len(waitForComplete) != 0 { + var ce error + exists := func(d *uiaDriver) (bool, error) { + for i := range waitForComplete { + _, ce = d.FindElement(waitForComplete[i]) + if ce == nil { + return true, nil + } + } + return false, nil + } + if err = d.WaitWithTimeoutAndInterval(exists, 45, 1.5); err != nil { + return fmt.Errorf("app launch (waitForComplete): %s: %w", err.Error(), ce) + } + } + return +} + +func (d *uiaDriver) AppTerminate(appPackageName string) (err error) { + if err = d.check(); err != nil { + return err + } + + _, err = d.adbDevice.RunShellCommand("am force-stop", appPackageName) + return +} + +func (d *uiaDriver) AppInstall(apkPath string, reinstall ...bool) (err error) { + if err = d.check(); err != nil { + return err + } + + apkName := filepath.Base(apkPath) + if !strings.HasSuffix(strings.ToLower(apkName), ".apk") { + return fmt.Errorf("apk file must have an extension of '.apk': %s", apkPath) + } + + var apkFile *os.File + if apkFile, err = os.Open(apkPath); err != nil { + return fmt.Errorf("apk file: %w", err) + } + + remotePath := path.Join(DeviceTempPath, apkName) + if err = d.adbDevice.PushFile(apkFile, remotePath); err != nil { + return fmt.Errorf("apk push: %w", err) + } + + var shellOutput string + if len(reinstall) != 0 && reinstall[0] { + shellOutput, err = d.adbDevice.RunShellCommand("pm install", "-r", remotePath) + } else { + shellOutput, err = d.adbDevice.RunShellCommand("pm install", remotePath) + } + + if err != nil { + return fmt.Errorf("apk install: %w", err) + } + + if !strings.Contains(shellOutput, "Success") { + return fmt.Errorf("apk installed: %s", shellOutput) + } + + return +} + +func (d *uiaDriver) AppUninstall(appPackageName string, keepDataAndCache ...bool) (err error) { + if err = d.check(); err != nil { + return err + } + + var shellOutput string + if len(keepDataAndCache) != 0 && keepDataAndCache[0] { + shellOutput, err = d.adbDevice.RunShellCommand("pm uninstall", "-k", appPackageName) + } else { + shellOutput, err = d.adbDevice.RunShellCommand("pm uninstall", appPackageName) + } + + if err != nil { + return fmt.Errorf("apk uninstall: %w", err) + } + + if !strings.Contains(shellOutput, "Success") { + return fmt.Errorf("apk uninstalled: %s", shellOutput) + } + + return +} + +type BatteryStatus int + +const ( + _ = iota + BatteryStatusUnknown BatteryStatus = iota + BatteryStatusCharging + BatteryStatusDischarging + BatteryStatusNotCharging + BatteryStatusFull +) + +func (bs BatteryStatus) String() string { + switch bs { + case BatteryStatusUnknown: + return "unknown" + case BatteryStatusCharging: + return "charging" + case BatteryStatusDischarging: + return "discharging" + case BatteryStatusNotCharging: + return "not charging" + case BatteryStatusFull: + return "full" + default: + return fmt.Sprintf("unknown status code (%d)", bs) + } +} + +func (d *uiaDriver) BatteryInfo() (info BatteryInfo, err error) { + // register(getHandler, new GetBatteryInfo("/wd/hub/session/:sessionId/appium/device/battery_info")) + var rawResp rawResponse + if rawResp, err = d.httpGET("/session", d.sessionId, "appium/device/battery_info"); err != nil { + return BatteryInfo{}, err + } + reply := new(struct{ Value BatteryInfo }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return BatteryInfo{}, err + } + + info = reply.Value + if info.Level == -1 || info.Status == -1 { + return info, errors.New("cannot be retrieved from the system") + } + return +} + +func (d *uiaDriver) GetAppiumSettings() (settings map[string]interface{}, err error) { + // register(getHandler, new GetSettings("/wd/hub/session/:sessionId/appium/settings")) + var rawResp rawResponse + if rawResp, err = d.httpGET("/session", d.sessionId, "appium/settings"); err != nil { + return nil, err + } + reply := new(struct{ Value map[string]interface{} }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return nil, err + } + + settings = reply.Value + return +} + +// DeviceScaleRatio get device pixel ratio +func (d *uiaDriver) DeviceScaleRatio() (scale float64, err error) { + // register(getHandler, new GetDevicePixelRatio("/wd/hub/session/:sessionId/appium/device/pixel_ratio")) + var rawResp rawResponse + if rawResp, err = d.httpGET("/session", d.sessionId, "appium/device/pixel_ratio"); err != nil { + return 0, err + } + reply := new(struct{ Value float64 }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return 0, err + } + + scale = reply.Value + return +} + +type ( + AndroidDeviceInfo struct { + // ANDROID_ID A 64-bit number (as a hex string) that is uniquely generated when the user + // first sets up the device and should remain constant for the lifetime of the user's device. The value + // may change if a factory reset is performed on the device. + AndroidID string `json:"androidId"` + // Build.MANUFACTURER value + Manufacturer string `json:"manufacturer"` + // Build.MODEL value + Model string `json:"model"` + // Build.BRAND value + Brand string `json:"brand"` + // Current running OS's API VERSION + APIVersion string `json:"apiVersion"` + // The current version string, for example "1.0" or "3.4b5" + PlatformVersion string `json:"platformVersion"` + // the name of the current celluar network carrier + CarrierName string `json:"carrierName"` + // the real size of the default display + RealDisplaySize string `json:"realDisplaySize"` + // The logical density of the display in Density Independent Pixel units. + DisplayDensity int `json:"displayDensity"` + // available networks + Networks []networkInfo `json:"networks"` + // current system locale + Locale string `json:"locale"` + // current system timezone + // e.g. "Asia/Tokyo", "America/Caracas", "Asia/Shanghai" + TimeZone string `json:"timeZone"` + Bluetooth struct { + State string `json:"state"` + } `json:"bluetooth"` + } + networkCapabilities struct { + TransportTypes string `json:"transportTypes"` + NetworkCapabilities string `json:"networkCapabilities"` + LinkUpstreamBandwidthKbps int `json:"linkUpstreamBandwidthKbps"` + LinkDownBandwidthKbps int `json:"linkDownBandwidthKbps"` + SignalStrength int `json:"signalStrength"` + SSID string `json:"SSID"` + } + networkInfo struct { + Type int `json:"type"` + TypeName string `json:"typeName"` + Subtype int `json:"subtype"` + SubtypeName string `json:"subtypeName"` + IsConnected bool `json:"isConnected"` + DetailedState string `json:"detailedState"` + State string `json:"state"` + ExtraInfo string `json:"extraInfo"` + IsAvailable bool `json:"isAvailable"` + IsRoaming bool `json:"isRoaming"` + IsFailover bool `json:"isFailover"` + Capabilities networkCapabilities `json:"capabilities"` + } +) + +func (d *uiaDriver) DeviceInfo() (info AndroidDeviceInfo, err error) { + // register(getHandler, new GetDeviceInfo("/wd/hub/session/:sessionId/appium/device/info")) + var rawResp rawResponse + if rawResp, err = d.httpGET("/session", d.sessionId, "appium/device/info"); err != nil { + return AndroidDeviceInfo{}, err + } + reply := new(struct{ Value AndroidDeviceInfo }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return AndroidDeviceInfo{}, err + } + + info = reply.Value + return +} + +// AlertText get text of the on-screen dialog +func (d *uiaDriver) AlertText() (text string, err error) { + // register(getHandler, new GetAlertText("/wd/hub/session/:sessionId/alert/text")) + var rawResp rawResponse + if rawResp, err = d.httpGET("/session", d.sessionId, "alert/text"); err != nil { + return "", err + } + reply := new(struct{ Value string }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return "", err + } + + text = reply.Value + return +} + +// Tap perform a click at arbitrary coordinates specified +func (d *uiaDriver) Tap(x, y int) (err error) { + return d.TapFloat(float64(x), float64(y)) +} + +func (d *uiaDriver) TapFloat(x, y float64) (err error) { + // register(postHandler, new Tap("/wd/hub/session/:sessionId/appium/tap")) + data := map[string]interface{}{ + "x": x, + "y": y, + } + _, err = d.httpPOST(data, "/session", d.sessionId, "appium/tap") + return +} + +func (d *uiaDriver) TapPoint(point Point) (err error) { + return d.Tap(point.X, point.Y) +} + +func (d *uiaDriver) TapPointF(point PointF) (err error) { + return d.TapFloat(point.X, point.Y) +} + +func (d *uiaDriver) _swipe(startX, startY, endX, endY interface{}, steps int, elementID ...string) (err error) { + // register(postHandler, new Swipe("/wd/hub/session/:sessionId/touch/perform")) + data := map[string]interface{}{ + "startX": startX, + "startY": startY, + "endX": endX, + "endY": endY, + "steps": steps, + } + if len(elementID) != 0 { + data["elementId"] = elementID[0] + } + _, err = d.httpPOST(data, "/session", d.sessionId, "touch/perform") + return +} + +// Swipe performs a swipe from one coordinate to another using the number of steps +// to determine smoothness and speed. Each step execution is throttled to 5ms +// per step. So for a 100 steps, the swipe will take about 1/2 second to complete. +// `steps` is the number of move steps sent to the system +func (d *uiaDriver) Swipe(startX, startY, endX, endY int, steps ...int) (err error) { + return d.SwipeFloat(float64(startX), float64(startY), float64(endX), float64(endY), steps...) +} + +func (d *uiaDriver) SwipeFloat(startX, startY, endX, endY float64, steps ...int) (err error) { + if len(steps) == 0 { + steps = []int{12} + } + return d._swipe(startX, startY, endX, endY, steps[0]) +} + +func (d *uiaDriver) SwipePoint(startPoint, endPoint Point, steps ...int) (err error) { + return d.Swipe(startPoint.X, startPoint.Y, endPoint.X, endPoint.Y, steps...) +} + +func (d *uiaDriver) SwipePointF(startPoint, endPoint PointF, steps ...int) (err error) { + return d.SwipeFloat(startPoint.X, startPoint.Y, endPoint.X, endPoint.Y, steps...) +} + +func (d *uiaDriver) _drag(data map[string]interface{}) (err error) { + // register(postHandler, new Drag("/wd/hub/session/:sessionId/touch/drag")) + _, err = d.httpPOST(data, "/session", d.sessionId, "touch/drag") + return +} + +// Drag performs a swipe from one coordinate to another coordinate. You can control +// the smoothness and speed of the swipe by specifying the number of steps. +// Each step execution is throttled to 5 milliseconds per step, so for a 100 +// steps, the swipe will take around 0.5 seconds to complete. +func (d *uiaDriver) Drag(startX, startY, endX, endY int, steps ...int) (err error) { + return d.DragFloat(float64(startX), float64(startY), float64(endX), float64(endY), steps...) +} + +func (d *uiaDriver) DragFloat(startX, startY, endX, endY float64, steps ...int) error { + if len(steps) == 0 { + steps = []int{12} + } + data := map[string]interface{}{ + "startX": startX, + "startY": startY, + "endX": endX, + "endY": endY, + "steps": steps[0], + } + return d._drag(data) +} + +func (d *uiaDriver) DragPoint(startPoint Point, endPoint Point, steps ...int) error { + return d.Drag(startPoint.X, startPoint.Y, endPoint.X, endPoint.Y, steps...) +} + +func (d *uiaDriver) DragPointF(startPoint PointF, endPoint PointF, steps ...int) (err error) { + return d.DragFloat(startPoint.X, startPoint.Y, endPoint.X, endPoint.Y, steps...) +} + +func (d *uiaDriver) TouchLongClick(x, y int, duration ...float64) (err error) { + if len(duration) == 0 { + duration = []float64{1.0} + } + // register(postHandler, new TouchLongClick("/wd/hub/session/:sessionId/touch/longclick")) + data := map[string]interface{}{ + "params": map[string]interface{}{ + "x": x, + "y": y, + "duration": int(duration[0] * 1000), + }, + } + _, err = d.httpPOST(data, "/session", d.sessionId, "touch/longclick") + return +} + +func (d *uiaDriver) TouchLongClickPoint(point Point, duration ...float64) (err error) { + return d.TouchLongClick(point.X, point.Y, duration...) +} + +func (d *uiaDriver) SendKeys(text string, isReplace ...bool) (err error) { + if len(isReplace) == 0 { + isReplace = []bool{true} + } + // register(postHandler, new SendKeysToElement("/wd/hub/session/:sessionId/keys")) + // https://github.com/appium/appium-uiautomator2-server/blob/master/app/src/main/java/io/appium/uiautomator2/handler/SendKeysToElement.java#L76-L85 + data := map[string]interface{}{ + "text": text, + "replace": isReplace[0], + } + _, err = d.httpPOST(data, "/session", d.sessionId, "keys") + return +} + +// PressBack simulates a short press on the BACK button. +func (d *uiaDriver) PressBack() (err error) { + // register(postHandler, new PressBack("/wd/hub/session/:sessionId/back")) + _, err = d.httpPOST(nil, "/session", d.sessionId, "back") + return +} + +// public class KeyCodeModel extends BaseModel { +// @RequiredField +// public Integer keycode; +// public Integer metastate; +// public Integer flags; +// } +func (d *uiaDriver) LongPressKeyCode(keyCode KeyCode, metaState KeyMeta, flags ...KeyFlag) (err error) { + if len(flags) == 0 { + flags = []KeyFlag{KFFromSystem} + } + data := map[string]interface{}{ + "keycode": keyCode, + "metastate": metaState, + "flags": flags[0], + } + // register(postHandler, new LongPressKeyCode("/wd/hub/session/:sessionId/appium/device/long_press_keycode")) + _, err = d.httpPOST(data, "/session", d.sessionId, "/appium/device/long_press_keycode") + return +} + +func (d *uiaDriver) _pressKeyCode(keyCode KeyCode, metaState KeyMeta, flags ...KeyFlag) (err error) { + // register(postHandler, new PressKeyCodeAsync("/wd/hub/session/:sessionId/appium/device/press_keycode")) + data := map[string]interface{}{ + "keycode": keyCode, + } + if metaState != KMEmpty { + data["metastate"] = metaState + } + if len(flags) != 0 { + data["flags"] = flags[0] + } + _, err = d.httpPOST(data, "/session", d.sessionId, "appium/device/press_keycode") + return +} + +func (d *uiaDriver) PressKeyCode(keyCode KeyCode, metaState KeyMeta, flags ...KeyFlag) (err error) { + if len(flags) == 0 { + flags = []KeyFlag{KFFromSystem} + } + return d._pressKeyCode(keyCode, metaState, KFFromSystem) +} + +// PressKeyCodeAsync simulates a short press using a key code. +func (d *uiaDriver) PressKeyCodeAsync(keyCode KeyCode, metaState ...KeyMeta) (err error) { + if len(metaState) == 0 { + metaState = []KeyMeta{KMEmpty} + } + return d._pressKeyCode(keyCode, metaState[0]) +} + +func (d *uiaDriver) TouchDown(x, y int) (err error) { + // register(postHandler, new TouchDown("/wd/hub/session/:sessionId/touch/down")) + data := map[string]interface{}{ + "params": map[string]interface{}{ + "x": x, + "y": y, + }, + } + _, err = d.httpPOST(data, "/session", d.sessionId, "touch/down") + return +} + +func (d *uiaDriver) TouchDownPoint(point Point) error { + return d.TouchDown(point.X, point.Y) +} + +func (d *uiaDriver) TouchUp(x, y int) (err error) { + // register(postHandler, new TouchUp("/wd/hub/session/:sessionId/touch/up")) + data := map[string]interface{}{ + "params": map[string]interface{}{ + "x": x, + "y": y, + }, + } + _, err = d.httpPOST(data, "/session", d.sessionId, "touch/up") + return +} + +func (d *uiaDriver) TouchUpPoint(point Point) error { + return d.TouchUp(point.X, point.Y) +} + +func (d *uiaDriver) TouchMove(x, y int) (err error) { + // register(postHandler, new TouchMove("/wd/hub/session/:sessionId/touch/move")) + data := map[string]interface{}{ + "params": map[string]interface{}{ + "x": x, + "y": y, + }, + } + _, err = d.httpPOST(data, "/session", d.sessionId, "touch/move") + return +} + +func (d *uiaDriver) TouchMovePoint(point Point) error { + return d.TouchMove(point.X, point.Y) +} + +// OpenNotification opens the notification shade. +func (d *uiaDriver) OpenNotification() (err error) { + // register(postHandler, new OpenNotification("/wd/hub/session/:sessionId/appium/device/open_notifications")) + _, err = d.httpPOST(nil, "/session", d.sessionId, "appium/device/open_notifications") + return +} + +func (d *uiaDriver) _flick(data map[string]interface{}) (err error) { + // register(postHandler, new Flick("/wd/hub/session/:sessionId/touch/flick")) + _, err = d.httpPOST(data, "/session", d.sessionId, "touch/flick") + return +} + +func (d *uiaDriver) Flick(xSpeed, ySpeed int) (err error) { + data := map[string]interface{}{ + "xspeed": xSpeed, + "yspeed": ySpeed, + } + if xSpeed == 0 && ySpeed == 0 { + return errors.New("both 'xSpeed' and 'ySpeed' cannot be zero") + } + + return d._flick(data) +} + +func (d *uiaDriver) _scrollTo(method, selector string, maxSwipes int, elementID ...string) (err error) { + // register(postHandler, new ScrollTo("/wd/hub/session/:sessionId/touch/scroll")) + params := map[string]interface{}{ + "strategy": method, + "selector": selector, + } + if maxSwipes > 0 { + params["maxSwipes"] = maxSwipes + } + data := map[string]interface{}{"params": params} + if len(elementID) != 0 { + data["origin"] = map[string]string{ + legacyWebElementIdentifier: elementID[0], + webElementIdentifier: elementID[0], + } + } + _, err = d.httpPOST(data, "/session", d.sessionId, "touch/scroll") + return +} + +func (d *uiaDriver) ScrollTo(by AndroidBySelector, maxSwipes ...int) (err error) { + if len(maxSwipes) == 0 { + maxSwipes = []int{0} + } + method, selector := by.getMethodAndSelector() + return d._scrollTo(method, selector, maxSwipes[0]) +} + +type W3CMouseButtonType int + +const ( + MBTLeft W3CMouseButtonType = 0 + MBTMiddle W3CMouseButtonType = 1 + MBTRight W3CMouseButtonType = 2 +) + +func (g *W3CGestures) PointerDown(button ...W3CMouseButtonType) *W3CGestures { + if len(button) == 0 { + button = []W3CMouseButtonType{MBTLeft} + } + *g = append(*g, _newW3CGesture().pointerDown(int(button[0]))) + return g +} + +func (g *W3CGestures) PointerUp(button ...W3CMouseButtonType) *W3CGestures { + if len(button) == 0 { + button = []W3CMouseButtonType{MBTLeft} + } + *g = append(*g, _newW3CGesture().pointerUp(int(button[0]))) + return g +} + +type W3CPointerMoveType string + +const ( + PMTViewport W3CPointerMoveType = "viewport" + PMTPointer W3CPointerMoveType = "pointer" +) + +func (g *W3CGestures) PointerMove(x, y float64, origin interface{}, duration float64, pressure, size float64) *W3CGestures { + val := "" + switch v := origin.(type) { + case string: + val = v + case W3CPointerMoveType: + val = string(v) + case *uiaElement: + val = v.id + default: + val = string(PMTViewport) + } + *g = append(*g, _newW3CGesture().pointerMove(x, y, val, duration, pressure, size)) + return g +} + +func (g *W3CGestures) PointerMoveTo(x, y float64, duration ...float64) *W3CGestures { + if len(duration) == 0 || duration[0] < 0 { + duration = []float64{0.5} + } + *g = append(*g, _newW3CGesture().pointerMove(x, y, string(PMTViewport), duration[0]*1000)) + return g +} + +func (g *W3CGestures) PointerMoveRelative(x, y float64, duration ...float64) *W3CGestures { + if len(duration) == 0 || duration[0] < 0 { + duration = []float64{0.5} + } + *g = append(*g, _newW3CGesture().pointerMove(x, y, string(PMTPointer), duration[0]*1000)) + return g +} + +func (g *W3CGestures) PointerMouseOver(x, y float64, element *uiaElement, duration ...float64) *W3CGestures { + if len(duration) == 0 || duration[0] < 0 { + duration = []float64{0.5} + } + *g = append(*g, _newW3CGesture().pointerMove(x, y, element.id, duration[0]*1000)) + return g +} + +type W3CAction map[string]interface{} + +type W3CActionType string + +const ( + _ W3CActionType = "none" + ATKey W3CActionType = "key" + ATPointer W3CActionType = "pointer" +) + +type W3CPointerType string + +const ( + PTMouse W3CPointerType = "mouse" + PTPen W3CPointerType = "pen" + PTTouch W3CPointerType = "touch" +) + +func NewW3CAction(actionType W3CActionType, gestures *W3CGestures, pointerType ...W3CPointerType) W3CAction { + w3cAction := make(W3CAction) + w3cAction["type"] = actionType + w3cAction["actions"] = gestures + if actionType != ATPointer { + return w3cAction + } + + if len(pointerType) == 0 { + pointerType = []W3CPointerType{PTTouch} + } + type W3CItemParameters struct { + PointerType W3CPointerType `json:"pointerType"` + } + w3cAction["parameters"] = W3CItemParameters{PointerType: pointerType[0]} + return w3cAction +} + +func (d *uiaDriver) PerformW3CActions(action W3CAction, acts ...W3CAction) (err error) { + var actionId uint64 = 1 + acts = append([]W3CAction{action}, acts...) + for i := range acts { + item := acts[i] + item["id"] = strconv.FormatUint(actionId, 10) + actionId++ + acts[i] = item + } + data := map[string]interface{}{ + "actions": acts, + } + // register(postHandler, new W3CActions("/wd/hub/session/:sessionId/actions")) + _, err = d.httpPOST(data, "/session", d.sessionId, "/actions") + return +} + +type ClipDataType string + +const ClipDataTypePlaintext ClipDataType = "PLAINTEXT" + +func (d *uiaDriver) GetClipboard(contentType ...ClipDataType) (content string, err error) { + if len(contentType) == 0 { + contentType = []ClipDataType{ClipDataTypePlaintext} + } + // register(postHandler, new GetClipboard("/wd/hub/session/:sessionId/appium/device/get_clipboard")) + data := map[string]interface{}{ + "contentType": contentType[0], + } + var rawResp rawResponse + if rawResp, err = d.httpPOST(data, "/session", d.sessionId, "appium/device/get_clipboard"); err != nil { + return "", err + } + reply := new(struct{ Value string }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return "", err + } + + content = reply.Value + if data, err := base64.StdEncoding.DecodeString(content); err != nil { + return content, err + } else { + content = string(data) + } + return +} + +func (d *uiaDriver) SetClipboard(contentType ClipDataType, content string, label ...string) (err error) { + lbl := content + if len(label) != 0 { + lbl = label[0] + } + const defaultLabelLen = 10 + if len(lbl) > defaultLabelLen { + lbl = lbl[:defaultLabelLen] + } + + data := map[string]interface{}{ + "contentType": contentType, + "label": lbl, + "content": base64.StdEncoding.EncodeToString([]byte(content)), + } + // register(postHandler, new SetClipboard("/wd/hub/session/:sessionId/appium/device/set_clipboard")) + _, err = d.httpPOST(data, "/session", d.sessionId, "appium/device/set_clipboard") + return +} + +func (d *uiaDriver) AlertAccept(buttonLabel ...string) (err error) { + data := map[string]interface{}{ + "buttonLabel": nil, + } + if len(buttonLabel) != 0 { + data["buttonLabel"] = buttonLabel[0] + } + // register(postHandler, new AcceptAlert("/wd/hub/session/:sessionId/alert/accept")) + _, err = d.httpPOST(data, "/session", d.sessionId, "alert/accept") + return +} + +func (d *uiaDriver) AlertDismiss(buttonLabel ...string) (err error) { + data := map[string]interface{}{ + "buttonLabel": nil, + } + if len(buttonLabel) != 0 { + data["buttonLabel"] = buttonLabel[0] + } + // register(postHandler, new DismissAlert("/wd/hub/session/:sessionId/alert/dismiss")) + _, err = d.httpPOST(data, "/session", d.sessionId, "alert/dismiss") + return +} + +func (d *uiaDriver) SetAppiumSettings(settings map[string]interface{}) (err error) { + data := map[string]interface{}{ + "settings": settings, + } + // register(postHandler, new UpdateSettings("/wd/hub/session/:sessionId/appium/settings")) + _, err = d.httpPOST(data, "/session", d.sessionId, "appium/settings") + return +} + +func (d *uiaDriver) SetOrientation(orientation Orientation) (err error) { + data := map[string]interface{}{ + "orientation": orientation, + } + // register(postHandler, new SetOrientation("/wd/hub/session/:sessionId/orientation")) + _, err = d.httpPOST(data, "/session", d.sessionId, "orientation") + return +} + +// SetRotation +// `x` and `y` are ignored. We only care about `z` +// 0/90/180/270 +func (d *uiaDriver) SetRotation(rotation Rotation) (err error) { + data := map[string]interface{}{ + "z": rotation.Z, + } + // register(postHandler, new SetRotation("/wd/hub/session/:sessionId/rotation")) + _, err = d.httpPOST(data, "/session", d.sessionId, "rotation") + return +} + +type NetworkType int + +const ( + NetworkTypeWifi NetworkType = 2 + + // NetworkTypeNone NetworkType = iota + // NetworkTypeAirplane + // NetworkTypeWifi + // _ + // NetworkTypeData + // _ + // NetworkTypeAll +) + +// NetworkConnection always turn on +func (d *uiaDriver) NetworkConnection(networkType NetworkType) (err error) { + // register(postHandler, new NetworkConnection("/wd/hub/session/:sessionId/network_connection")) + data := map[string]interface{}{ + "type": networkType, + } + _, err = d.httpPOST(data, "/session", d.sessionId, "network_connection") + return +} + +func (d *uiaDriver) _findElements(method, selector string, elementID ...string) (elements []*uiaElement, err error) { + // register(postHandler, new FindElements("/wd/hub/session/:sessionId/elements")) + data := map[string]interface{}{ + "strategy": method, + "selector": selector, + } + if len(elementID) != 0 { + data["context"] = elementID[0] + } + var rawResp rawResponse + if rawResp, err = d.httpPOST(data, "/session", d.sessionId, "/elements"); err != nil { + return nil, err + } + reply := new(struct{ Value []map[string]string }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return nil, err + } + if len(reply.Value) == 0 { + return nil, fmt.Errorf("no such element: unable to find an element using '%s', value '%s'", method, selector) + } + elements = make([]*uiaElement, len(reply.Value)) + for i, elem := range reply.Value { + var id string + if id = elementIDFromValue(elem); id == "" { + return nil, fmt.Errorf("invalid element returned: %+v", reply) + } + elements[i] = &uiaElement{parent: d, id: id} + } + return +} + +func (d *uiaDriver) _findElement(method, selector string, elementID ...string) (elem *uiaElement, err error) { + // register(postHandler, new FindElement("/wd/hub/session/:sessionId/element")) + data := map[string]interface{}{ + "strategy": method, + "selector": selector, + } + if len(elementID) != 0 { + data["context"] = elementID[0] + } + var rawResp rawResponse + if rawResp, err = d.httpPOST(data, "/session", d.sessionId, "/element"); err != nil { + return nil, err + } + reply := new(struct{ Value map[string]string }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return nil, err + } + if len(reply.Value) == 0 { + return nil, fmt.Errorf("no such element: unable to find an element using '%s', value '%s'", method, selector) + } + var id string + if id = elementIDFromValue(reply.Value); id == "" { + return nil, fmt.Errorf("invalid element returned: %+v", reply) + } + elem = &uiaElement{parent: d, id: id} + return +} + +func (d *uiaDriver) FindElements(by AndroidBySelector) (elements []*uiaElement, err error) { + return d._findElements(by.getMethodAndSelector()) +} + +func (d *uiaDriver) FindElement(by AndroidBySelector) (elem *uiaElement, err error) { + return d._findElement(by.getMethodAndSelector()) +} + +func (d *uiaDriver) ActiveElement() (elem *uiaElement, err error) { + // register(getHandler, new ActiveElement("/wd/hub/session/:sessionId/element/active")) + var rawResp rawResponse + if rawResp, err = d.httpGET("/session", d.sessionId, "/element/active"); err != nil { + return nil, err + } + reply := new(struct{ Value map[string]string }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return nil, err + } + if len(reply.Value) == 0 { + return nil, errors.New("no such element") + } + var id string + if id = elementIDFromValue(reply.Value); id == "" { + return nil, fmt.Errorf("invalid element returned: %+v", reply) + } + elem = &uiaElement{parent: d, id: id} + return +} + +type AndroidCondition func(d *uiaDriver) (bool, error) + +func (d *uiaDriver) _waitWithTimeoutAndInterval(condition AndroidCondition, timeout, interval time.Duration) (err error) { + startTime := time.Now() + for { + done, err := condition(d) + if err != nil { + return err + } + if done { + return nil + } + + if elapsed := time.Since(startTime); elapsed > timeout { + return fmt.Errorf("timeout after %v", elapsed) + } + time.Sleep(interval) + } +} + +// WaitWithTimeoutAndInterval waits for the condition to evaluate to true. +func (d *uiaDriver) WaitWithTimeoutAndInterval(condition AndroidCondition, timeout, interval float64) (err error) { + dTimeout := time.Millisecond * time.Duration(timeout*1000) + dInterval := time.Millisecond * time.Duration(interval*1000) + return d._waitWithTimeoutAndInterval(condition, dTimeout, dInterval) +} + +// WaitWithTimeout works like WaitWithTimeoutAndInterval, but with default polling interval. +func (d *uiaDriver) WaitWithTimeout(condition AndroidCondition, timeout float64) error { + dTimeout := time.Millisecond * time.Duration(timeout*1000) + return d._waitWithTimeoutAndInterval(condition, dTimeout, DefaultWaitInterval) +} + +// Wait works like WaitWithTimeoutAndInterval, but using the default timeout and polling interval. +func (d *uiaDriver) Wait(condition AndroidCondition) error { + return d._waitWithTimeoutAndInterval(condition, DefaultWaitTimeout, DefaultWaitInterval) +} diff --git a/hrp/internal/uixt/android_elment.go b/hrp/internal/uixt/android_elment.go new file mode 100644 index 00000000..533217f0 --- /dev/null +++ b/hrp/internal/uixt/android_elment.go @@ -0,0 +1,238 @@ +package uixt + +import ( + "bytes" + "encoding/base64" + "encoding/json" +) + +type uiaElement struct { + parent *uiaDriver + id string +} + +func (e *uiaElement) Text() (text string, err error) { + // register(getHandler, new GetText("/wd/hub/session/:sessionId/element/:id/text")) + var rawResp rawResponse + if rawResp, err = e.parent.httpGET("/session", e.parent.sessionId, "/element", e.id, "/text"); err != nil { + return "", err + } + reply := new(struct{ Value string }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return "", err + } + text = reply.Value + return +} + +func (e *uiaElement) GetAttribute(name string) (attribute string, err error) { + // register(getHandler, new GetElementAttribute("/wd/hub/session/:sessionId/element/:id/attribute/:name")) + var rawResp rawResponse + if rawResp, err = e.parent.httpGET("/session", e.parent.sessionId, "/element", e.id, "/attribute", name); err != nil { + return "", err + } + reply := new(struct{ Value string }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return "", err + } + attribute = reply.Value + return +} + +func (e *uiaElement) ContentDescription() (name string, err error) { + // register(getHandler, new GetName("/wd/hub/session/:sessionId/element/:id/name")) + var rawResp rawResponse + if rawResp, err = e.parent.httpGET("/session", e.parent.sessionId, "/element", e.id, "/name"); err != nil { + return "", err + } + reply := new(struct{ Value string }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return "", err + } + name = reply.Value + return +} + +func (e *uiaElement) Size() (size Size, err error) { + // register(getHandler, new GetSize("/wd/hub/session/:sessionId/element/:id/size")) + var rawResp rawResponse + if rawResp, err = e.parent.httpGET("/session", e.parent.sessionId, "/element", e.id, "/size"); err != nil { + return Size{-1, -1}, err + } + reply := new(struct{ Value Size }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return Size{-1, -1}, err + } + size = reply.Value + return +} + +func (e *uiaElement) Rect() (rect Rect, err error) { + // register(getHandler, new GetRect("/wd/hub/session/:sessionId/element/:id/rect")) + var rawResp rawResponse + if rawResp, err = e.parent.httpGET("/session", e.parent.sessionId, "/element", e.id, "/rect"); err != nil { + return Rect{}, err + } + reply := new(struct{ Value Rect }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return Rect{}, err + } + rect = reply.Value + return +} + +func (e *uiaElement) Screenshot() (raw *bytes.Buffer, err error) { + // W3C endpoint + // register(getHandler, new GetElementScreenshot("/wd/hub/session/:sessionId/element/:id/screenshot")) + // JSONWP endpoint + // register(getHandler, new GetElementScreenshot("/wd/hub/session/:sessionId/screenshot/:id")) + var rawResp rawResponse + if rawResp, err = e.parent.httpGET("/session", e.parent.sessionId, "/element", e.id, "/screenshot"); err != nil { + return nil, err + } + reply := new(struct{ Value string }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return nil, err + } + + var decodeStr []byte + if decodeStr, err = base64.StdEncoding.DecodeString(reply.Value); err != nil { + return nil, err + } + + raw = bytes.NewBuffer(decodeStr) + return +} + +func (e *uiaElement) Location() (point Point, err error) { + // register(getHandler, new Location("/wd/hub/session/:sessionId/element/:id/location")) + var rawResp rawResponse + if rawResp, err = e.parent.httpGET("/session", e.parent.sessionId, "/element", e.id, "/location"); err != nil { + return Point{-1, -1}, err + } + reply := new(struct{ Value Point }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return Point{-1, -1}, err + } + point = reply.Value + return +} + +func (e *uiaElement) Click() (err error) { + // register(postHandler, new Click("/wd/hub/session/:sessionId/element/:id/click")) + _, err = e.parent.httpPOST(nil, "/session", e.parent.sessionId, "/element", e.id, "/click") + return +} + +func (e *uiaElement) Clear() (err error) { + // register(postHandler, new Clear("/wd/hub/session/:sessionId/element/:id/clear")) + _, err = e.parent.httpPOST(nil, "/session", e.parent.sessionId, "/element", e.id, "/clear") + return +} + +func (e *uiaElement) SendKeys(text string, isReplace ...bool) (err error) { + if len(isReplace) == 0 { + isReplace = []bool{true} + } + // register(postHandler, new SendKeysToElement("/wd/hub/session/:sessionId/element/:id/value")) + // https://github.com/appium/appium-uiautomator2-server/blob/master/app/src/main/java/io/appium/uiautomator2/handler/SendKeysToElement.java#L76-L85 + data := map[string]interface{}{ + "text": text, + "replace": isReplace[0], + } + _, err = e.parent.httpPOST(data, "/session", e.parent.sessionId, "/element", e.id, "/value") + return +} + +func (e *uiaElement) FindElements(by AndroidBySelector) (elements []*uiaElement, err error) { + method, selector := by.getMethodAndSelector() + return e.parent._findElements(method, selector, e.id) +} + +func (e *uiaElement) FindElement(by AndroidBySelector) (elem *uiaElement, err error) { + method, selector := by.getMethodAndSelector() + return e.parent._findElement(method, selector, e.id) +} + +func (e *uiaElement) Swipe(startX, startY, endX, endY int, steps ...int) (err error) { + return e.SwipeFloat(float64(startX), float64(startY), float64(endX), float64(endY), steps...) +} + +func (e *uiaElement) SwipeFloat(startX, startY, endX, endY float64, steps ...int) (err error) { + if len(steps) == 0 { + steps = []int{12} + } + return e.parent._swipe(startX, startY, endX, endY, steps[0], e.id) +} + +func (e *uiaElement) SwipePoint(startPoint, endPoint Point, steps ...int) (err error) { + return e.Swipe(startPoint.X, startPoint.Y, endPoint.X, endPoint.Y, steps...) +} + +func (e *uiaElement) SwipePointF(startPoint, endPoint PointF, steps ...int) (err error) { + return e.SwipeFloat(startPoint.X, startPoint.Y, endPoint.X, endPoint.Y, steps...) +} + +func (e *uiaElement) Drag(endX, endY int, steps ...int) (err error) { + return e.DragFloat(float64(endX), float64(endY), steps...) +} + +func (e *uiaElement) DragFloat(endX, endY float64, steps ...int) error { + if len(steps) == 0 { + steps = []int{12 * 10} + } else { + steps[0] = 12 * 10 + } + data := map[string]interface{}{ + "elementId": e.id, + "endX": endX, + "endY": endY, + "steps": steps[0], + } + return e.parent._drag(data) +} + +func (e *uiaElement) DragPoint(endPoint Point, steps ...int) error { + return e.Drag(endPoint.X, endPoint.Y, steps...) +} + +func (e *uiaElement) DragPointF(endPoint PointF, steps ...int) (err error) { + return e.DragFloat(endPoint.X, endPoint.Y, steps...) +} + +func (e *uiaElement) DragTo(destElem *uiaElement, steps ...int) error { + if len(steps) == 0 { + steps = []int{12} + } + data := map[string]interface{}{ + "elementId": e.id, + "destElId": destElem.id, + "steps": steps[0], + } + return e.parent._drag(data) +} + +func (e *uiaElement) Flick(xOffset, yOffset, speed int) (err error) { + data := map[string]interface{}{ + legacyWebElementIdentifier: e.id, + webElementIdentifier: e.id, + "xoffset": xOffset, + "yoffset": yOffset, + "speed": speed, + } + return e.parent._flick(data) +} + +func (e *uiaElement) ScrollTo(by AndroidBySelector, maxSwipes ...int) (err error) { + if len(maxSwipes) == 0 { + maxSwipes = []int{0} + } + method, selector := by.getMethodAndSelector() + return e.parent._scrollTo(method, selector, maxSwipes[0], e.id) +} + +func (e *uiaElement) ScrollToElement(element *uiaElement) (err error) { + // register(postHandler, new ScrollToElement("/wd/hub/session/:sessionId/appium/element/:id/scroll_to/:id2")) + _, err = e.parent.httpPOST(nil, "/session", e.parent.sessionId, "/appium/element", e.id, "/scroll_to", element.id) + return +} diff --git a/hrp/internal/uixt/android_key.go b/hrp/internal/uixt/android_key.go new file mode 100644 index 00000000..07f0d5b7 --- /dev/null +++ b/hrp/internal/uixt/android_key.go @@ -0,0 +1,879 @@ +package uixt + +type KeyMeta int + +const ( + KMEmpty KeyMeta = 0 // As a `null` + KMCapLocked KeyMeta = 0x100 // SHIFT key locked in CAPS mode. + KMAltLocked KeyMeta = 0x200 // ALT key locked. + KMSymLocked KeyMeta = 0x400 // SYM key locked. + KMSelecting KeyMeta = 0x800 // Text is in selection mode. + // KMAltOn KeyMeta = 0x02 // This mask is used to check whether one of the ALT meta keys is pressed. + // KMAltLeftOn KeyMeta = 0x10 // This mask is used to check whether the left ALT meta key is pressed. + // KMAltRightOn KeyMeta = 0x20 // This mask is used to check whether the right the ALT meta key is pressed. + // KMShiftOn KeyMeta = 0x1 // This mask is used to check whether one of the SHIFT meta keys is pressed. + // KMShiftLeftOn KeyMeta = 0x40 // This mask is used to check whether the left SHIFT meta key is pressed. + // KMShiftRightOn KeyMeta = 0x80 // This mask is used to check whether the right SHIFT meta key is pressed. + // KMSymOn KeyMeta = 0x4 // This mask is used to check whether the SYM meta key is pressed. + // KMFunctionOn KeyMeta = 0x8 // This mask is used to check whether the FUNCTION meta key is pressed. + // KMCtrlOn KeyMeta = 0x1000 // This mask is used to check whether one of the CTRL meta keys is pressed. + // KMCtrlLeftOn KeyMeta = 0x2000 // This mask is used to check whether the left CTRL meta key is pressed. + // KMCtrlRightOn KeyMeta = 0x4000 // This mask is used to check whether the right CTRL meta key is pressed. + // KMMetaOn KeyMeta = 0x10000 // This mask is used to check whether one of the META meta keys is pressed. + // KMMetaLeftOn KeyMeta = 0x20000 // This mask is used to check whether the left META meta key is pressed. + // KMMetaRightOn KeyMeta = 0x40000 // This mask is used to check whether the right META meta key is pressed. + // KMCapsLockOn KeyMeta = 0x100000 // This mask is used to check whether the CAPS LOCK meta key is on. + // KMNumLockOn KeyMeta = 0x200000 // This mask is used to check whether the NUM LOCK meta key is on. + // KMScrollLockOn KeyMeta = 0x400000 // This mask is used to check whether the SCROLL LOCK meta key is on. + // KMShiftMask = KMShiftOn | KMShiftLeftOn | KMShiftRightOn + // KMAltMask = KMAltOn | KMAltLeftOn | KMAltRightOn + // KMCtrlMask = KMCtrlOn | KMCtrlLeftOn | KMCtrlRightOn + // KMMetaMask = KMMetaOn | KMMetaLeftOn | KMMetaRightOn +) + +type KeyFlag int + +const ( + // KFWokeHere This mask is set if the device woke because of this key event. + // Deprecated + KFWokeHere KeyFlag = 0x1 + + // KFSoftKeyboard This mask is set if the key event was generated by a software keyboard. + KFSoftKeyboard KeyFlag = 0x2 + + // KFKeepTouchMode This mask is set if we don't want the key event to cause us to leave touch mode. + KFKeepTouchMode KeyFlag = 0x4 + + // KFFromSystem This mask is set if an event was known to come from a trusted part + // of the system. That is, the event is known to come from the user, + // and could not have been spoofed by a third party component. + KFFromSystem KeyFlag = 0x8 + + // KFEditorAction This mask is used for compatibility, to identify enter keys that are + // coming from an IME whose enter key has been auto-labelled "next" or + // "done". This allows TextView to dispatch these as normal enter keys + // for old applications, but still do the appropriate action when receiving them. + KFEditorAction KeyFlag = 0x10 + + // KFCanceled When associated with up key events, this indicates that the key press + // has been canceled. Typically this is used with virtual touch screen + // keys, where the user can slide from the virtual key area on to the + // display: in that case, the application will receive a canceled up + // event and should not perform the action normally associated with the + // key. Note that for this to work, the application can not perform an + // action for a key until it receives an up or the long press timeout has expired. + KFCanceled KeyFlag = 0x20 + + // KFVirtualHardKey This key event was generated by a virtual (on-screen) hard key area. + // Typically this is an area of the touchscreen, outside of the regular + // display, dedicated to "hardware" buttons. + KFVirtualHardKey KeyFlag = 0x40 + + // KFLongPress This flag is set for the first key repeat that occurs after the long press timeout. + KFLongPress KeyFlag = 0x80 + + // KFCanceledLongPress Set when a key event has `KFCanceled` set because a long + // press action was executed while it was down. + KFCanceledLongPress KeyFlag = 0x100 + + // KFTracking Set for `ACTION_UP` when this event's key code is still being + // tracked from its initial down. That is, somebody requested that tracking + // started on the key down and a long press has not caused + // the tracking to be canceled. + KFTracking KeyFlag = 0x200 + + // KFFallback Set when a key event has been synthesized to implement default behavior + // for an event that the application did not handle. + // Fallback key events are generated by unhandled trackball motions + // (to emulate a directional keypad) and by certain unhandled key presses + // that are declared in the key map (such as special function numeric keypad + // keys when numlock is off). + KFFallback KeyFlag = 0x400 + + // KFPredispatch Signifies that the key is being predispatched. + // KFPredispatch KeyFlag = 0x20000000 + + // KFStartTracking Private control to determine when an app is tracking a key sequence. + // KFStartTracking KeyFlag = 0x40000000 + + // KFTainted Private flag that indicates when the system has detected that this key event + // may be inconsistent with respect to the sequence of previously delivered key events, + // such as when a key up event is sent but the key was not down. + // KFTainted KeyFlag = 0x80000000 +) + +type KeyCode int + +const ( + _ KeyCode = 0 // Unknown key code. + + // KCSoftLeft Soft Left key + // Usually situated below the display on phones and used as a multi-function + // feature key for selecting a software defined function shown on the bottom left + // of the display. + KCSoftLeft KeyCode = 1 + + // KCSoftRight Soft Right key. + // Usually situated below the display on phones and used as a multi-function + // feature key for selecting a software defined function shown on the bottom right + // of the display. + KCSoftRight KeyCode = 2 + + // KCHome Home key. + // This key is handled by the framework and is never delivered to applications. + KCHome KeyCode = 3 + + KCBack KeyCode = 4 // Back key + KCCall KeyCode = 5 // Call key + KCEndCall KeyCode = 6 // End Call key + KC0 KeyCode = 7 // '0' key + KC1 KeyCode = 8 // '1' key + KC2 KeyCode = 9 // '2' key + KC3 KeyCode = 10 // '3' key + KC4 KeyCode = 11 // '4' key + KC5 KeyCode = 12 // '5' key + KC6 KeyCode = 13 // '6' key + KC7 KeyCode = 14 // '7' key + KC8 KeyCode = 15 // '8' key + KC9 KeyCode = 16 // '9' key + KCStar KeyCode = 17 // '*' key + KCPound KeyCode = 18 // '#' key + + // KCDPadUp KeycodeDPadUp Directional Pad Up key. + // May also be synthesized from trackball motions. + KCDPadUp KeyCode = 19 + + // KCDPadDown Directional Pad Down key. + // May also be synthesized from trackball motions. + KCDPadDown KeyCode = 20 + + // KCDPadLeft Directional Pad Left key. + // May also be synthesized from trackball motions. + KCDPadLeft KeyCode = 21 + + // KCDPadRight Directional Pad Right key. + // May also be synthesized from trackball motions. + KCDPadRight KeyCode = 22 + + // KCDPadCenter Directional Pad Center key. + // May also be synthesized from trackball motions. + KCDPadCenter KeyCode = 23 + + // KCVolumeUp Volume Up key. + // Adjusts the speaker volume up. + KCVolumeUp KeyCode = 24 + + // KCVolumeDown Volume Down key. + // Adjusts the speaker volume down. + KCVolumeDown KeyCode = 25 + + // KCPower Power key. + KCPower KeyCode = 26 + + // KCCamera Camera key. + // Used to launch a camera application or take pictures. + KCCamera KeyCode = 27 + + KCClear KeyCode = 28 // Clear key + KCa KeyCode = 29 // 'a' key + KCb KeyCode = 30 // 'b' key + KCc KeyCode = 31 // 'c' key + KCd KeyCode = 32 // 'd' key + KCe KeyCode = 33 // 'e' key + KCf KeyCode = 34 // 'f' key + KCg KeyCode = 35 // 'g' key + KCh KeyCode = 36 // 'h' key + KCi KeyCode = 37 // 'i' key + KCj KeyCode = 38 // 'j' key + KCk KeyCode = 39 // 'k' key + KCl KeyCode = 40 // 'l' key + KCm KeyCode = 41 // 'm' key + KCn KeyCode = 42 // 'n' key + KCo KeyCode = 43 // 'o' key + KCp KeyCode = 44 // 'p' key + KCq KeyCode = 45 // 'q' key + KCr KeyCode = 46 // 'r' key + KCs KeyCode = 47 // 's' key + KCt KeyCode = 48 // 't' key + KCu KeyCode = 49 // 'u' key + KCv KeyCode = 50 // 'v' key + KCw KeyCode = 51 // 'w' key + KCx KeyCode = 52 // 'x' key + KCy KeyCode = 53 // 'y' key + KCz KeyCode = 54 // 'z' key + KCComma KeyCode = 55 // ',' key + KCPeriod KeyCode = 56 // '.' key + KCAltLeft KeyCode = 57 // Left Alt modifier key + KCAltRight KeyCode = 58 // Right Alt modifier key + KCShiftLeft KeyCode = 59 // Left Shift modifier key + KCShiftRight KeyCode = 60 // Right Shift modifier key + KCTab KeyCode = 61 // Tab key + KCSpace KeyCode = 62 // Space key + + // KCSym Symbol modifier key. + // Used to enter alternate symbols. + KCSym KeyCode = 63 + + // KCExplorer Explorer special function key. + // Used to launch a browser application. + KCExplorer KeyCode = 64 + + // KCEnvelope Envelope special function key. + // Used to launch a mail application. + KCEnvelope KeyCode = 65 + + // KCEnter Enter key. + KCEnter KeyCode = 66 + + // KCDel Backspace key. + // Deletes characters before the insertion point, unlike `KCForwardDel`. + KCDel KeyCode = 67 + + KCGrave KeyCode = 68 // '`' (backtick) key + KCMinus KeyCode = 69 // '-' + KCEquals KeyCode = 70 // '=' key + KCLeftBracket KeyCode = 71 // '[' key + KCRightBracket KeyCode = 72 // ']' key + KCBackslash KeyCode = 73 // '\' key + KCSemicolon KeyCode = 74 // '' key + KCApostrophe KeyCode = 75 // ''' (apostrophe) key + KCSlash KeyCode = 76 // '/' key + KCAt KeyCode = 77 // '@' key + + // KCNum Number modifier key. + // Used to enter numeric symbols. + // This key is not Num Lock; it is more like `KCAltLeft` and is + // interpreted as an ALT key by {@link android.text.method.MetaKeyKeyListener}. + KCNum KeyCode = 78 + + // KCHeadsetHook Headset Hook key. + // Used to hang up calls and stop media. + KCHeadsetHook KeyCode = 79 + + // KCFocus Camera Focus key. + // Used to focus the camera. + // *Camera* focus + KCFocus KeyCode = 80 + + KCPlus KeyCode = 81 // '+' key. + KCMenu KeyCode = 82 // Menu key. + KCNotification KeyCode = 83 // Notification key. + KCSearch KeyCode = 84 // Search key. + KCMediaPlayPause KeyCode = 85 // Play/Pause media key. + KCMediaStop KeyCode = 86 // Stop media key. + KCMediaNext KeyCode = 87 // Play Next media key. + KCMediaPrevious KeyCode = 88 // Play Previous media key. + KCMediaRewind KeyCode = 89 // Rewind media key. + KCMediaFastForward KeyCode = 90 // Fast Forward media key. + + // KCMute Mute key. + // Mutes the microphone, unlike `KCVolumeMute` + KCMute KeyCode = 91 + + // KCPageUp Page Up key. + KCPageUp KeyCode = 92 + + // KCPageDown Page Down key. + KCPageDown KeyCode = 93 + + // KCPictSymbols Picture Symbols modifier key. + // Used to switch symbol sets (Emoji, Kao-moji). + // switch symbol-sets (Emoji,Kao-moji) + KCPictSymbols KeyCode = 94 + + // KCSwitchCharset Switch Charset modifier key. + // Used to switch character sets (Kanji, Katakana). + // switch char-sets (Kanji,Katakana) + KCSwitchCharset KeyCode = 95 + + // KCButtonA A Button key. + // On a game controller, the A button should be either the button labeled A + // or the first button on the bottom row of controller buttons. + KCButtonA KeyCode = 96 + + // KCButtonB B Button key. + // On a game controller, the B button should be either the button labeled B + // or the second button on the bottom row of controller buttons. + KCButtonB KeyCode = 97 + + // KCButtonC C Button key. + // On a game controller, the C button should be either the button labeled C + // or the third button on the bottom row of controller buttons. + KCButtonC KeyCode = 98 + + // KCButtonX X Button key. + // On a game controller, the X button should be either the button labeled X + // or the first button on the upper row of controller buttons. + KCButtonX KeyCode = 99 + + // KCButtonY Y Button key. + // On a game controller, the Y button should be either the button labeled Y + // or the second button on the upper row of controller buttons. + KCButtonY KeyCode = 100 + + // KCButtonZ Z Button key. + // On a game controller, the Z button should be either the button labeled Z + // or the third button on the upper row of controller buttons. + KCButtonZ KeyCode = 101 + + // KCButtonL1 L1 Button key. + // On a game controller, the L1 button should be either the button labeled L1 (or L) + // or the top left trigger button. + KCButtonL1 KeyCode = 102 + + // KCButtonR1 R1 Button key. + // On a game controller, the R1 button should be either the button labeled R1 (or R) + // or the top right trigger button. + KCButtonR1 KeyCode = 103 + + // KCButtonL2 L2 Button key. + // On a game controller, the L2 button should be either the button labeled L2 + // or the bottom left trigger button. + KCButtonL2 KeyCode = 104 + + // KCButtonR2 R2 Button key. + // On a game controller, the R2 button should be either the button labeled R2 + // or the bottom right trigger button. + KCButtonR2 KeyCode = 105 + + // KCButtonTHUMBL Left Thumb Button key. + // On a game controller, the left thumb button indicates that the left (or only) + // joystick is pressed. + KCButtonTHUMBL KeyCode = 106 + + // KCButtonTHUMBR Right Thumb Button key. + // On a game controller, the right thumb button indicates that the right + // joystick is pressed. + KCButtonTHUMBR KeyCode = 107 + + // KCButtonStart Start Button key. + // On a game controller, the button labeled Start. + KCButtonStart KeyCode = 108 + + // KCButtonSelect Select Button key. + // On a game controller, the button labeled Select. + KCButtonSelect KeyCode = 109 + + // KCButtonMode Mode Button key. + // On a game controller, the button labeled Mode. + KCButtonMode KeyCode = 110 + + // KCEscape Escape key. + KCEscape KeyCode = 111 + + // KCForwardDel Forward Delete key. + // Deletes characters ahead of the insertion point, unlike `KCDel`. + KCForwardDel KeyCode = 112 + + KCCtrlLeft KeyCode = 113 // Left Control modifier key + KCCtrlRight KeyCode = 114 // Right Control modifier key + KCCapsLock KeyCode = 115 // Caps Lock key + KCScrollLock KeyCode = 116 // Scroll Lock key + KCMetaLeft KeyCode = 117 // Left Meta modifier key + KCMetaRight KeyCode = 118 // Right Meta modifier key + KCFunction KeyCode = 119 // Function modifier key + KCSysRq KeyCode = 120 // System Request / Print Screen key + KCBreak KeyCode = 121 // Break / Pause key + + // KCMoveHome Home Movement key. + // Used for scrolling or moving the cursor around to the start of a line + // or to the top of a list. + KCMoveHome KeyCode = 122 + + // KCMoveEnd End Movement key. + // Used for scrolling or moving the cursor around to the end of a line + // or to the bottom of a list. + KCMoveEnd KeyCode = 123 + + // KCInsert Insert key. + // Toggles insert / overwrite edit mode. + KCInsert KeyCode = 124 + + // KCForward Forward key. + // Navigates forward in the history stack. Complement of `KCBack`. + KCForward KeyCode = 125 + + // KCMediaPlay Play media key. + KCMediaPlay KeyCode = 126 + + // KCMediaPause Pause media key. + KCMediaPause KeyCode = 127 + + // KCMediaClose Close media key. + // May be used to close a CD tray, for example. + KCMediaClose KeyCode = 128 + + // KCMediaEject Eject media key. + // May be used to eject a CD tray, for example. + KCMediaEject KeyCode = 129 + + // KCMediaRecord Record media key. + KCMediaRecord KeyCode = 130 + + KCF1 KeyCode = 131 // F1 key. + KCF2 KeyCode = 132 // F2 key. + KCF3 KeyCode = 133 // F3 key. + KCF4 KeyCode = 134 // F4 key. + KCF5 KeyCode = 135 // F5 key. + KCF6 KeyCode = 136 // F6 key. + KCF7 KeyCode = 137 // F7 key. + KCF8 KeyCode = 138 // F8 key. + KCF9 KeyCode = 139 // F9 key. + KCF10 KeyCode = 140 // F10 key. + KCF11 KeyCode = 141 // F11 key. + KCF12 KeyCode = 142 // F12 key. + + // KCNumLock Num Lock key. + // This is the Num Lock key; it is different from `KCNum`. + // This key alters the behavior of other keys on the numeric keypad. + KCNumLock KeyCode = 143 + + KCNumpad0 KeyCode = 144 // Numeric keypad '0' key + KCNumpad1 KeyCode = 145 // Numeric keypad '1' key + KCNumpad2 KeyCode = 146 // Numeric keypad '2' key + KCNumpad3 KeyCode = 147 // Numeric keypad '3' key + KCNumpad4 KeyCode = 148 // Numeric keypad '4' key + KCNumpad5 KeyCode = 149 // Numeric keypad '5' key + KCNumpad6 KeyCode = 150 // Numeric keypad '6' key + KCNumpad7 KeyCode = 151 // Numeric keypad '7' key + KCNumpad8 KeyCode = 152 // Numeric keypad '8' key + KCNumpad9 KeyCode = 153 // Numeric keypad '9' key + KCNumpadDivide KeyCode = 154 // Numeric keypad '/' key (for division) + KCNumpadMultiply KeyCode = 155 // Numeric keypad '*' key (for multiplication) + KCNumpadSubtract KeyCode = 156 // Numeric keypad '-' key (for subtraction) + KCNumpadAdd KeyCode = 157 // Numeric keypad '+' key (for addition) + KCNumpadDot KeyCode = 158 // Numeric keypad '.' key (for decimals or digit grouping) + KCNumpadComma KeyCode = 159 // Numeric keypad ',' key (for decimals or digit grouping) + KCNumpadEnter KeyCode = 160 // Numeric keypad Enter key + KCNumpadEquals KeyCode = 161 // Numeric keypad 'KeyCode =' key + KCNumpadLeftParen KeyCode = 162 // Numeric keypad '(' key + KCNumpadRightParen KeyCode = 163 // Numeric keypad ')' key + + // KCVolumeMute Volume Mute key. + // Mutes the speaker, unlike `KCMute`. + // This key should normally be implemented as a toggle such that the first press + // mutes the speaker and the second press restores the original volume. + KCVolumeMute KeyCode = 164 + + // KCInfo Info key. + // Common on TV remotes to show additional information related to what is + // currently being viewed. + KCInfo KeyCode = 165 + + // KCChannelUp Channel up key. + // On TV remotes, increments the television channel. + KCChannelUp KeyCode = 166 + + // KCChannelDown Channel down key. + // On TV remotes, decrements the television channel. + KCChannelDown KeyCode = 167 + + // KCZoomIn Zoom in key. + KCZoomIn KeyCode = 168 + + // KCZoomOut Zoom out key. + KCZoomOut KeyCode = 169 + + // KCTv TV key. + // On TV remotes, switches to viewing live TV. + KCTv KeyCode = 170 + + // KCWindow Window key. + // On TV remotes, toggles picture-in-picture mode or other windowing functions. + // On Android Wear devices, triggers a display offset. + KCWindow KeyCode = 171 + + // KCGuide Guide key. + // On TV remotes, shows a programming guide. + KCGuide KeyCode = 172 + + // KCDvr DVR key. + // On some TV remotes, switches to a DVR mode for recorded shows. + KCDvr KeyCode = 173 + + // KCBookmark Bookmark key. + // On some TV remotes, bookmarks content or web pages. + KCBookmark KeyCode = 174 + + // KCCaptions Toggle captions key. + // Switches the mode for closed-captioning text, for example during television shows. + KCCaptions KeyCode = 175 + + // KCSettings Settings key. + // Starts the system settings activity. + KCSettings KeyCode = 176 + + // KCTvPower TV power key. + // On TV remotes, toggles the power on a television screen. + KCTvPower KeyCode = 177 + + // KCTvInput TV input key. + // On TV remotes, switches the input on a television screen. + KCTvInput KeyCode = 178 + + // KCStbPower Set-top-box power key. + // On TV remotes, toggles the power on an external Set-top-box. + KCStbPower KeyCode = 179 + + // KCStbInput Set-top-box input key. + // On TV remotes, switches the input mode on an external Set-top-box. + KCStbInput KeyCode = 180 + + // KCAvrPower A/V Receiver power key. + // On TV remotes, toggles the power on an external A/V Receiver. + KCAvrPower KeyCode = 181 + + // KCAvrInput A/V Receiver input key. + // On TV remotes, switches the input mode on an external A/V Receiver. + KCAvrInput KeyCode = 182 + + // KCProgRed Red "programmable" key. + // On TV remotes, acts as a contextual/programmable key. + KCProgRed KeyCode = 183 + + // KCProgGreen Green "programmable" key. + // On TV remotes, actsas a contextual/programmable key. + KCProgGreen KeyCode = 184 + + // KCProgYellow Yellow "programmable" key. + // On TV remotes, acts as a contextual/programmable key. + KCProgYellow KeyCode = 185 + + // KCProgBlue Blue "programmable" key. + // On TV remotes, acts as a contextual/programmable key. + KCProgBlue KeyCode = 186 + + // KCAppSwitch App switch key. + // Should bring up the application switcher dialog. + KCAppSwitch KeyCode = 187 + + KCButton1 KeyCode = 188 // Generic Game Pad Button #1 + KCButton2 KeyCode = 189 // Generic Game Pad Button #2 + KCButton3 KeyCode = 190 // Generic Game Pad Button #3 + KCButton4 KeyCode = 191 // Generic Game Pad Button #4 + KCButton5 KeyCode = 192 // Generic Game Pad Button #5 + KCButton6 KeyCode = 193 // Generic Game Pad Button #6 + KCButton7 KeyCode = 194 // Generic Game Pad Button #7 + KCButton8 KeyCode = 195 // Generic Game Pad Button #8 + KCButton9 KeyCode = 196 // Generic Game Pad Button #9 + KCButton10 KeyCode = 197 // Generic Game Pad Button #10 + KCButton11 KeyCode = 198 // Generic Game Pad Button #11 + KCButton12 KeyCode = 199 // Generic Game Pad Button #12 + KCButton13 KeyCode = 200 // Generic Game Pad Button #13 + KCButton14 KeyCode = 201 // Generic Game Pad Button #14 + KCButton15 KeyCode = 202 // Generic Game Pad Button #15 + KCButton16 KeyCode = 203 // Generic Game Pad Button #16 + + // KCLanguageSwitch Language Switch key. + // Toggles the current input language such as switching between English and Japanese on + // a QWERTY keyboard. On some devices, the same function may be performed by + // pressing Shift+Spacebar. + KCLanguageSwitch KeyCode = 204 + + // Manner Mode key. + // Toggles silent or vibrate mode on and off to make the device behave more politely + // in certain settings such as on a crowded train. On some devices, the key may only + // operate when long-pressed. + KCMannerMode KeyCode = 205 + + // 3D Mode key. + // Toggles the display between 2D and 3D mode. + KC3dMode KeyCode = 206 + + // Contacts special function key. + // Used to launch an address book application. + KCContacts KeyCode = 207 + + // Calendar special function key. + // Used to launch a calendar application. + KCCalendar KeyCode = 208 + + // Music special function key. + // Used to launch a music player application. + KCMusic KeyCode = 209 + + // Calculator special function key. + // Used to launch a calculator application. + KCCalculator KeyCode = 210 + + // Japanese full-width / half-width key. + KCZenkakuHankaku KeyCode = 211 + + // Japanese alphanumeric key. + KCEisu KeyCode = 212 + + // Japanese non-conversion key. + KCMuhenkan KeyCode = 213 + + // Japanese conversion key. + KCHenkan KeyCode = 214 + + // Japanese katakana / hiragana key. + KCKatakanaHiragana KeyCode = 215 + + // Japanese Yen key. + KCYen KeyCode = 216 + + // Japanese Ro key. + KCRo KeyCode = 217 + + // Japanese kana key. + KCKana KeyCode = 218 + + // Assist key. + // Launches the global assist activity. Not delivered to applications. + KCAssist KeyCode = 219 + + // Brightness Down key. + // Adjusts the screen brightness down. + KCBrightnessDown KeyCode = 220 + + // Brightness Up key. + // Adjusts the screen brightness up. + KCBrightnessUp KeyCode = 221 + + // Audio Track key. + // Switches the audio tracks. + KCMediaAudioTrack KeyCode = 222 + + // Sleep key. + // Puts the device to sleep. Behaves somewhat like {@link #KEYCODE_POWER} but it + // has no effect if the device is already asleep. + KCSleep KeyCode = 223 + + // Wakeup key. + // Wakes up the device. Behaves somewhat like {@link #KEYCODE_POWER} but it + // has no effect if the device is already awake. + KCWakeup KeyCode = 224 + + // Pairing key. + // Initiates peripheral pairing mode. Useful for pairing remote control + // devices or game controllers, especially if no other input mode is + // available. + KCPairing KeyCode = 225 + + // Media Top Menu key. + // Goes to the top of media menu. + KCMediaTopMenu KeyCode = 226 + + // '11' key. + KC11 KeyCode = 227 + + // '12' key. + KC12 KeyCode = 228 + + // Last Channel key. + // Goes to the last viewed channel. + KCLastChannel KeyCode = 229 + + // TV data service key. + // Displays data services like weather, sports. + KCTvDataService KeyCode = 230 + + // Voice Assist key. + // Launches the global voice assist activity. Not delivered to applications. + KCVoiceAssist KeyCode = 231 + + // Radio key. + // Toggles TV service / Radio service. + KCTvRadioService KeyCode = 232 + + // Teletext key. + // Displays Teletext service. + KCTvTeletext KeyCode = 233 + + // Number entry key. + // Initiates to enter multi-digit channel nubmber when each digit key is assigned + // for selecting separate channel. Corresponds to Number Entry Mode (0x1D) of CEC + // User Control Code. + KCTvNumberEntry KeyCode = 234 + + // Analog Terrestrial key. + // Switches to analog terrestrial broadcast service. + KCTvTerrestrialAnalog KeyCode = 235 + + // Digital Terrestrial key. + // Switches to digital terrestrial broadcast service. + KCTvTerrestrialDigital KeyCode = 236 + + // Satellite key. + // Switches to digital satellite broadcast service. + KCTvSatellite KeyCode = 237 + + // BS key. + // Switches to BS digital satellite broadcasting service available in Japan. + KCTvSatelliteBs KeyCode = 238 + + // CS key. + // Switches to CS digital satellite broadcasting service available in Japan. + KCTvSatelliteCs KeyCode = 239 + + // BS/CS key. + // Toggles between BS and CS digital satellite services. + KCTvSatelliteService KeyCode = 240 + + // Toggle Network key. + // Toggles selecting broacast services. + KCTvNetwork KeyCode = 241 + + // Antenna/Cable key. + // Toggles broadcast input source between antenna and cable. + KCTvAntennaCable KeyCode = 242 + + // HDMI #1 key. + // Switches to HDMI input #1. + KCTvInputHdmi1 KeyCode = 243 + + // HDMI #2 key. + // Switches to HDMI input #2. + KCTvInputHdmi2 KeyCode = 244 + + // HDMI #3 key. + // Switches to HDMI input #3. + KCTvInputHdmi3 KeyCode = 245 + + // HDMI #4 key. + // Switches to HDMI input #4. + KCTvInputHdmi4 KeyCode = 246 + + // Composite #1 key. + // Switches to composite video input #1. + KCTvInputComposite1 KeyCode = 247 + + // Composite #2 key. + // Switches to composite video input #2. + KCTvInputComposite2 KeyCode = 248 + + // Component #1 key. + // Switches to component video input #1. + KCTvInputComponent1 KeyCode = 249 + + // Component #2 key. + // Switches to component video input #2. + KCTvInputComponent2 KeyCode = 250 + + // VGA #1 key. + // Switches to VGA (analog RGB) input #1. + KCTvInputVga1 KeyCode = 251 + + // Audio description key. + // Toggles audio description off / on. + KCTvAudioDescription KeyCode = 252 + + // Audio description mixing volume up key. + // Louden audio description volume as compared with normal audio volume. + KCTvAudioDescriptionMixUp KeyCode = 253 + + // Audio description mixing volume down key. + // Lessen audio description volume as compared with normal audio volume. + KCTvAudioDescriptionMixDown KeyCode = 254 + + // Zoom mode key. + // Changes Zoom mode (Normal, Full, Zoom, Wide-zoom, etc.) + KCTvZoomMode KeyCode = 255 + + // Contents menu key. + // Goes to the title list. Corresponds to Contents Menu (0x0B) of CEC User Control + // Code + KCTvContentsMenu KeyCode = 256 + + // Media context menu key. + // Goes to the context menu of media contents. Corresponds to Media Context-sensitive + // Menu (0x11) of CEC User Control Code. + KCTvMediaContextMenu KeyCode = 257 + + // Timer programming key. + // Goes to the timer recording menu. Corresponds to Timer Programming (0x54) of + // CEC User Control Code. + KCTvTimerProgramming KeyCode = 258 + + // Help key. + KCHelp KeyCode = 259 + + // Navigate to previous key. + // Goes backward by one item in an ordered collection of items. + KCNavigatePrevious KeyCode = 260 + + // Navigate to next key. + // Advances to the next item in an ordered collection of items. + KCNavigateNext KeyCode = 261 + + // Navigate in key. + // Activates the item that currently has focus or expands to the next level of a navigation + // hierarchy. + KCNavigateIn KeyCode = 262 + + // Navigate out key. + // Backs out one level of a navigation hierarchy or collapses the item that currently has + // focus. + KCNavigateOut KeyCode = 263 + + // Primary stem key for Wear + // Main power/reset button on watch. + KCStemPrimary KeyCode = 264 + + // Generic stem key 1 for Wear + KCStem1 KeyCode = 265 + + // Generic stem key 2 for Wear + KCStem2 KeyCode = 266 + + // Generic stem key 3 for Wear + KCStem3 KeyCode = 267 + + // Directional Pad Up-Left + KCDPadUpLeft KeyCode = 268 + + // Directional Pad Down-Left + KCDPadDownLeft KeyCode = 269 + + // Directional Pad Up-Right + KCDPadUpRight KeyCode = 270 + + // Directional Pad Down-Right + KCDPadDownRight KeyCode = 271 + + // Skip forward media key. + KCMediaSkipForward KeyCode = 272 + + // Skip backward media key. + KCMediaSkipBackward KeyCode = 273 + + // Step forward media key. + // Steps media forward, one frame at a time. + KCMediaStepForward KeyCode = 274 + + // Step backward media key. + // Steps media backward, one frame at a time. + KCMediaStepBackward KeyCode = 275 + + // put device to sleep unless a wakelock is held. + KCSoftSleep KeyCode = 276 + + // Cut key. + KCCut KeyCode = 277 + + // Copy key. + KCCopy KeyCode = 278 + + // Paste key. + KCPaste KeyCode = 279 + + // Consumed by the system for navigation up + KCSystemNavigationUp KeyCode = 280 + + // Consumed by the system for navigation down + KCSystemNavigationDown KeyCode = 281 + + // Consumed by the system for navigation left*/ + KCSystemNavigationLeft KeyCode = 282 + + // Consumed by the system for navigation right + KCSystemNavigationRight KeyCode = 283 + + // Show all apps + KCAllApps KeyCode = 284 + + // Refresh key. + KCRefresh KeyCode = 285 +) diff --git a/hrp/internal/uixt/android_test.go b/hrp/internal/uixt/android_test.go new file mode 100644 index 00000000..2efa9c66 --- /dev/null +++ b/hrp/internal/uixt/android_test.go @@ -0,0 +1,1384 @@ +package uixt + +import ( + "io/ioutil" + "testing" + "time" +) + +var uiaServerURL = "http://localhost:6790/wd/hub" + +func TestDriver_NewSession(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + firstMatchEntry := make(map[string]interface{}) + firstMatchEntry["package"] = "com.android.settings" + firstMatchEntry["activity"] = "com.android.settings/.Settings" + caps := Capabilities{ + "firstMatch": []interface{}{firstMatchEntry}, + "alwaysMatch": struct{}{}, + } + sessionID, err := driver.NewSession(caps) + if err != nil { + t.Fatal(err) + } + if len(sessionID) == 0 { + t.Fatal("should not be empty") + } +} + +func TestNewDriver(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + t.Log(driver.sessionId) +} + +func TestDriver_Quit(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + if err = driver.Quit(); err != nil { + t.Fatal(err) + } +} + +func TestDriver_Status(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + _, err = driver.Status() + if err != nil { + t.Fatal(err) + } +} + +func TestDriver_SessionIDs(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + sessions, err := driver.SessionIDs() + if err != nil { + t.Fatal(err) + } + if len(sessions) == 0 { + t.Fatal("should have at least one") + } + t.Log(len(sessions), sessions) +} + +func TestDriver_SessionDetails(t *testing.T) { + // firstMatchEntry := make(map[string]interface{}) + // firstMatchEntry["package"] = "com.android.settings" + // firstMatchEntry["activity"] = "com.android.settings/.Settings" + // caps = Capabilities{ + // "firstMatch": []interface{}{firstMatchEntry}, + // "alwaysMatch": struct{}{}, + // } + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + scrollData, err := driver.SessionDetails() + if err != nil { + t.Fatal(err) + } + + t.Log(scrollData) +} + +func TestDriver_Screenshot(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + screenshot, err := driver.Screenshot() + if err != nil { + t.Fatal(err) + } + + t.Log(ioutil.WriteFile("/Users/hero/Desktop/s1.png", screenshot.Bytes(), 0o600)) +} + +func TestDriver_Orientation(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + orientation, err := driver.Orientation() + if err != nil { + t.Fatal(err) + } + + t.Log(orientation) +} + +func TestDriver_Rotation(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + rotation, err := driver.Rotation() + if err != nil { + t.Fatal(err) + } + + t.Logf("x = %d\ty = %d\tz = %d", rotation.X, rotation.Y, rotation.Z) +} + +func TestDriver_DeviceSize(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + deviceSize, err := driver.DeviceSize() + if err != nil { + t.Fatal(err) + } + + t.Logf("width = %d\theight = %d", deviceSize.Width, deviceSize.Height) +} + +func TestDriver_Source(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + source, err := driver.Source() + if err != nil { + t.Fatal(err) + } + + t.Log(source) +} + +func TestDriver_StatusBarHeight(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + statusBarHeight, err := driver.StatusBarHeight() + if err != nil { + t.Fatal(err) + } + + t.Log(statusBarHeight) +} + +func TestDriver_BatteryInfo(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + batteryInfo, err := driver.BatteryInfo() + if err != nil { + t.Fatal(err) + } + + t.Log(batteryInfo) +} + +func TestDriver_GetAppiumSettings(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + appiumSettings, err := driver.GetAppiumSettings() + if err != nil { + t.Fatal(err) + } + + for k := range appiumSettings { + t.Logf("key: %s\tvalue: %v", k, appiumSettings[k]) + } + // t.Log(appiumSettings) +} + +func TestDriver_DeviceScaleRatio(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + scaleRatio, err := driver.DeviceScaleRatio() + if err != nil { + t.Fatal(err) + } + + t.Log(scaleRatio) +} + +func TestDriver_DeviceInfo(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + devInfo, err := driver.DeviceInfo() + if err != nil { + t.Fatal(err) + } + + t.Logf("api version: %s", devInfo.APIVersion) + t.Logf("platform version: %s", devInfo.PlatformVersion) + t.Logf("bluetooth state: %s", devInfo.Bluetooth.State) +} + +func TestDriver_AlertText(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + alertText, err := driver.AlertText() + if err != nil { + t.Fatal(err) + } + + t.Log(alertText) +} + +func TestDriver_Tap(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + err = driver.Tap(150, 340) + if err != nil { + t.Fatal(err) + } + time.Sleep(time.Second) + + err = driver.TapFloat(60.5, 125.5) + if err != nil { + t.Fatal(err) + } + time.Sleep(time.Second) + + err = driver.TapPoint(Point{X: 150, Y: 340}) + if err != nil { + t.Fatal(err) + } + time.Sleep(time.Second) + + err = driver.TapPointF(PointF{X: 60.5, Y: 125.5}) + if err != nil { + t.Fatal(err) + } +} + +func TestDriver_Swipe(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + err = driver.Swipe(400, 1000, 400, 500, 10) + if err != nil { + t.Fatal(err) + } + + err = driver.SwipeFloat(400, 555.5, 400, 1255.5) + if err != nil { + t.Fatal(err) + } + + startPoint := Point{400, 1000} + endPoint := Point{400, 500} + err = driver.SwipePoint(startPoint, endPoint) + if err != nil { + t.Fatal(err) + } + + startPointF := PointF{400, 555.5} + endPointF := PointF{400, 1255.5} + err = driver.SwipePointF(startPointF, endPointF) + if err != nil { + t.Fatal(err) + } +} + +func TestDriver_Drag(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + err = driver.Drag(400, 260, 400, 500, 10) + if err != nil { + t.Fatal(err) + } + time.Sleep(time.Millisecond * 200) + + err = driver.DragFloat(400, 501.5, 400, 261.5, 10) + if err != nil { + t.Fatal(err) + } + time.Sleep(time.Millisecond * 200) + + startPoint := Point{400, 260} + endPoint := Point{400, 500} + err = driver.DragPoint(startPoint, endPoint) + if err != nil { + t.Fatal(err) + } + time.Sleep(time.Millisecond * 200) + + startPointF := PointF{400.5, 501.5} + endPointF := PointF{400.5, 261.5} + err = driver.DragPointF(startPointF, endPointF) + if err != nil { + t.Fatal(err) + } +} + +func TestDriver_TouchLongClick(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + err = driver.TouchLongClick(400, 260, 1.2222) + if err != nil { + t.Fatal(err) + } + time.Sleep(time.Millisecond * 200) + + err = driver.TouchLongClickPoint(Point{X: 400, Y: 260}) + if err != nil { + t.Fatal(err) + } +} + +func TestDriver_SendKeys(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + err = driver.SendKeys("abc") + if err != nil { + t.Fatal(err) + } + time.Sleep(time.Second * 2) + + err = driver.SendKeys("def", false) + if err != nil { + t.Fatal(err) + } + time.Sleep(time.Second * 2) + + err = driver.SendKeys("\\n") + // err = driver.SendKeys(`\n`, false) + if err != nil { + t.Fatal(err) + } +} + +func TestDriver_PressBack(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + err = driver.PressBack() + if err != nil { + t.Fatal(err) + } +} + +func TestDriver_PressKeyCode(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + err = driver.PressKeyCodeAsync(KCx) + if err != nil { + t.Fatal(err) + } + err = driver.PressKeyCodeAsync(KCx, KMCapLocked) + if err != nil { + t.Fatal(err) + } + // err = driver.PressKeyCodeAsync(KCExplorer) + // if err != nil { + // t.Fatal(err) + // } + + err = driver.PressKeyCode(KCExplorer, KMEmpty) + if err != nil { + t.Fatal(err) + } +} + +func TestDriver_LongPressKeyCode(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + err = driver.LongPressKeyCode(KCAt, KMEmpty) + if err != nil { + t.Fatal(err) + } +} + +func TestDriver_TouchDown(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + doTouchUp := func() { + err = driver.TouchUp(400, 260) + if err != nil { + t.Fatal(err) + } + } + + err = driver.TouchDown(400, 260) + if err != nil { + t.Fatal(err) + } + + // _ = driver.TapPoint(Point{400, 500}) + doTouchUp() + + err = driver.TouchDownPoint(Point{400, 260}) + if err != nil { + t.Fatal(err) + } + + doTouchUp() +} + +func TestDriver_TouchUp(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + err = driver.TouchDown(400, 260) + if err != nil { + t.Fatal(err) + } + + // err = driver.TouchUp(400, 260) + err = driver.TouchUpPoint(Point{400, 260}) + if err != nil { + t.Fatal(err) + } +} + +func TestDriver_TouchMove(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + doTouchDown := func(x, y int) { + err = driver.TouchDown(x, y) + if err != nil { + t.Fatal(err) + } + } + + doTouchUp := func(x, y int) { + err = driver.TouchUp(x, y) + if err != nil { + t.Fatal(err) + } + } + + doTouchDown(400, 260) + + err = driver.TouchMove(400, 500) + if err != nil { + t.Fatal(err) + } + + doTouchUp(400, 500) + + doTouchDown(400, 500) + + err = driver.TouchMove(400, 260) + if err != nil { + t.Fatal(err) + } + + doTouchUp(400, 260) +} + +func TestDriver_OpenNotification(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + err = driver.OpenNotification() + if err != nil { + t.Fatal(err) + } +} + +func TestDriver_Flick(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + err = driver.Flick(50, -100) + if err != nil { + t.Fatal(err) + } +} + +func TestDriver_ScrollTo(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + err = driver.ScrollTo(AndroidBySelector{ClassName: "android.widget.SeekBar"}) + if err != nil { + t.Fatal(err) + } +} + +func TestDriver_MultiPointerGesture(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + gesture1 := NewTouchAction().Add(150, 340, 0.35).AddFloat(50, 300) + gesture2 := NewTouchAction().Add(200, 340).AddFloat(300, 300) + gesture3 := NewTouchAction().Add(300, 500).AddFloat(350, 500).AddPoint(Point{300, 550}).AddPointF(PointF{350, 550}) + _ = gesture3 + + // err = driver.MultiPointerGesture(gesture1, gesture2) + err = driver.MultiPointerGesture(gesture1, gesture2, gesture3) + if err != nil { + t.Fatal(err) + } +} + +func TestDriver_PerformW3CActions(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + // actionKey := NewW3CAction(ATKey, NewW3CGestures().KeyDown("g").KeyUp("g").Pause().KeyDown("o").KeyUp("o")) + // actionKey := NewW3CAction(ATKey, NewW3CGestures().SendKeys("golang")) + // err = driver.PerformW3CActions(actionKey) + // if err != nil { + // t.Fatal(err) + // } + + // var queryField map[string]string + // queryField = make(map[string]string) + // { + // queryField = map[string]string{ + // "a": "", + // } + // } + + elem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "com.android.settings:id/search"}) + if err != nil { + t.Fatal(err) + } + // actionPointer := NewW3CAction(ATPointer, NewW3CGestures().PointerMove(0, 0, elem.id).PointerDown().Pause(3).PointerUp()) + // actionPointer := NewW3CAction(ATPointer, + // NewW3CGestures().PointerMove(400, 500, "viewport").PointerDown().Pause(2). + // PointerMove(0, 0, elem.id).Pause(2). + // PointerMove(20, 0, "pointer").Pause(2). + // PointerUp(), + // ) + actionPointer := NewW3CAction(ATPointer, + NewW3CGestures().PointerMoveTo(400, 500).PointerDown(). + PointerMouseOver(0, 0, elem). + PointerMoveRelative(20, 0).PointerUp()) + err = driver.PerformW3CActions(actionPointer) + if err != nil { + t.Fatal(err) + } +} + +func TestDriver_GetClipboard(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + text, err := driver.GetClipboard() + if err != nil { + t.Fatal(err) + } + t.Log(text) +} + +func TestDriver_SetClipboard(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + content := "test123" + err = driver.SetClipboard(ClipDataTypePlaintext, content) + if err != nil { + t.Fatal(err) + } + + text, err := driver.GetClipboard() + if err != nil { + t.Fatal(err) + } + if text != content { + t.Fatal("should be the same") + } +} + +func TestDriver_AlertAccept(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + err = driver.AlertAccept() + // err = driver.AlertAccept("是") + if err != nil { + t.Fatal(err) + } +} + +func TestDriver_AlertDismiss(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + // err = driver.AlertDismiss() + err = driver.AlertDismiss("否") + if err != nil { + t.Fatal(err) + } +} + +func TestDriver_SetAppiumSettings(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + appiumSettings, err := driver.GetAppiumSettings() + if err != nil { + t.Fatal(err) + } + sdopd := appiumSettings["shutdownOnPowerDisconnect"] + t.Log("shutdownOnPowerDisconnect:", sdopd) + + err = driver.SetAppiumSettings(map[string]interface{}{"shutdownOnPowerDisconnect": !sdopd.(bool)}) + if err != nil { + t.Fatal(err) + } + + appiumSettings, err = driver.GetAppiumSettings() + if err != nil { + t.Fatal(err) + } + if appiumSettings["shutdownOnPowerDisconnect"] == sdopd.(bool) { + t.Fatal("should not be equal") + } + t.Log("shutdownOnPowerDisconnect:", appiumSettings["shutdownOnPowerDisconnect"]) +} + +func TestDriver_SetOrientation(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + err = driver.SetOrientation(OrientationLandscapeLeft) + // err = driver.SetOrientation(OrientationPortrait) + if err != nil { + t.Fatal(err) + } +} + +func TestDriver_SetRotation(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + // err = driver.SetRotation(Rotation{Z: 0}) + err = driver.SetRotation(Rotation{Z: 270}) + if err != nil { + t.Fatal(err) + } +} + +func TestDriver_NetworkConnection(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + err = driver.NetworkConnection(NetworkTypeWifi) + if err != nil { + t.Fatal(err) + } +} + +func TestDriver_FindElement(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + elem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "android:id/content"}) + if err != nil { + t.Fatal(err) + } + + t.Log(elem.GetAttribute("class")) +} + +func TestDriver_FindElements(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + // elements, err := driver.FindElements(AndroidBySelector{ResourceIdID: "com.android.settings:id/title"}) + elements, err := driver.FindElements(AndroidBySelector{UiAutomator: "new UiSelector().textStartsWith(\"应\");"}) + if err != nil { + t.Fatal(err) + } + t.Log(len(elements)) +} + +func TestDriver_WaitWithTimeoutAndInterval(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + element, err := driver.FindElement(AndroidBySelector{UiAutomator: "new UiSelector().className(\"android.view.ViewGroup\");"}) + if err != nil { + t.Fatal(err) + } + + elem, err := element.FindElement(AndroidBySelector{UiAutomator: "new UiSelector().className(\"android.widget.LinearLayout\").index(6);"}) + if err != nil { + t.Fatal(err) + } + + rect, err := elem.Rect() + if err != nil { + t.Fatal(err) + } + + x := rect.X + int(float64(rect.Width)*2) + y := rect.Y + rect.Height/2 + err = driver.Tap(x, y) + if err != nil { + t.Fatal(err) + } + + by := AndroidBySelector{UiAutomator: "new UiSelector().text(\"科技\");"} + exists := func(d *uiaDriver) (bool, error) { + element, err = d.FindElement(by) + if err == nil { + return true, nil + } + return false, nil + } + + err = driver.WaitWithTimeoutAndInterval(exists, 1, 0.1) + if err != nil { + t.Fatal(err) + } + + // element, err = driver.FindElement(by) + // if err != nil { + // t.Fatal(err) + // } + + err = element.Click() + if err != nil { + t.Fatal(err) + } +} + +func TestDriver_ActiveElement(t *testing.T) { + device, _ := NewAndroidDevice() + driver, err := device.NewUSBDriver(nil) + if err != nil { + t.Fatal(err) + } + defer func() { + _ = driver.Dispose() + }() + + element, err := driver.ActiveElement() + if err != nil { + t.Fatal(err) + } + + if err = element.SendKeys("test"); err != nil { + t.Fatal(err) + } +} + +func TestUiSelectorHelper_NewUiSelectorHelper(t *testing.T) { + uiSelector := NewUiSelectorHelper().Text("a").String() + if uiSelector != `new UiSelector().text("a");` { + t.Fatal("[ERROR]", uiSelector) + } + + uiSelector = NewUiSelectorHelper().Text("a").TextStartsWith("b").String() + if uiSelector != `new UiSelector().text("a").textStartsWith("b");` { + t.Fatal("[ERROR]", uiSelector) + } + + uiSelector = NewUiSelectorHelper().ClassName("android.widget.LinearLayout").Index(6).String() + if uiSelector != `new UiSelector().className("android.widget.LinearLayout").index(6);` { + t.Fatal("[ERROR]", uiSelector) + } + + uiSelector = NewUiSelectorHelper().Focused(false).Instance(6).String() + if uiSelector != `new UiSelector().focused(false).instance(6);` { + t.Fatal("[ERROR]", uiSelector) + } + + uiSelector = NewUiSelectorHelper().ChildSelector(NewUiSelectorHelper().Enabled(true)).String() + if uiSelector != `new UiSelector().childSelector(new UiSelector().enabled(true));` { + t.Fatal("[ERROR]", uiSelector) + } +} + +func Test_getFreePort(t *testing.T) { + freePort, err := getFreePort() + if err != nil { + t.Fatal(err) + } + t.Log(freePort) +} + +func TestDeviceList(t *testing.T) { + devices, err := DeviceList() + if err != nil { + t.Fatal(err) + } + for i := range devices { + t.Log(devices[i].Serial()) + } +} + +func TestAndroidNewUSBDriver(t *testing.T) { + device, _ := NewAndroidDevice() + driver, err := device.NewUSBDriver(nil) + if err != nil { + t.Fatal(err) + } + defer driver.Dispose() + + ready, err := driver.Status() + if err != nil { + t.Fatal(err) + } + if !ready { + t.Fatal("should be 'true'") + } +} + +func TestDriver_ActiveAppPackageName(t *testing.T) { + device, _ := NewAndroidDevice() + driver, err := device.NewUSBDriver(nil) + if err != nil { + t.Fatal(err) + } + defer driver.Dispose() + + appPackageName, err := driver.ActiveAppPackageName() + if err != nil { + t.Fatal(err) + } + + t.Log(appPackageName) +} + +func TestDriver_AppLaunch(t *testing.T) { + device, _ := NewAndroidDevice() + driver, err := device.NewUSBDriver(nil) + if err != nil { + t.Fatal(err) + } + defer driver.Dispose() + + // err = driver.AppLaunch("tv.danmaku.bili", AndroidBySelector{ResourceIdID: "tv.danmaku.bili:id/action_bar_root"}) + err = driver.AppLaunch("com.android.settings", AndroidBySelector{ResourceIdID: "android:id/list"}) + if err != nil { + t.Fatal(err) + } + + // screenshot, err := driver.Screenshot() + // if err != nil { + // t.Fatal(err) + // } + // t.Log(ioutil.WriteFile("/Users/hero/Desktop/s1.png", screenshot.Bytes(), 0600)) +} + +func TestDriver_AppTerminate(t *testing.T) { + device, _ := NewAndroidDevice() + driver, err := device.NewUSBDriver(nil) + if err != nil { + t.Fatal(err) + } + defer driver.Dispose() + + err = driver.AppTerminate("tv.danmaku.bili") + if err != nil { + t.Fatal(err) + } +} + +func TestNewWiFiDriver(t *testing.T) { + device, _ := NewAndroidDevice(WithAdbIP("192.168.1.28")) + driver, err := device.NewHTTPDriver(nil) + if err != nil { + t.Fatal(err) + } + + // SetDebug(false, true) + _, err = driver.ActiveAppActivity() + if err != nil { + t.Fatal(err) + } +} + +func TestDriver_AppInstall(t *testing.T) { + device, _ := NewAndroidDevice() + driver, err := device.NewUSBDriver(nil) + if err != nil { + t.Fatal(err) + } + defer driver.Dispose() + + err = driver.AppInstall("/Users/hero/Desktop/xuexi_android_10002068.apk") + if err != nil { + t.Fatal(err) + } +} + +func TestDriver_AppUninstall(t *testing.T) { + device, _ := NewAndroidDevice() + driver, err := device.NewUSBDriver(nil) + if err != nil { + t.Fatal(err) + } + defer driver.Dispose() + + err = driver.AppUninstall("cn.xuexi.android") + if err != nil { + t.Fatal(err) + } +} + +func TestBySelector_getMethodAndSelector(t *testing.T) { + testVal := "test id" + bySelector := AndroidBySelector{ResourceIdID: testVal} + method, selector := bySelector.getMethodAndSelector() + if method != "id" || selector != testVal { + t.Fatal(method, "=", selector) + } + + bySelector = AndroidBySelector{ContentDescription: testVal} + method, selector = bySelector.getMethodAndSelector() + if method != "accessibility id" || selector != testVal { + t.Fatal(method, "=", selector) + } +} + +func TestElement_Text(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + elem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "com.android.settings:id/category_title"}) + if err != nil { + t.Fatal(err) + } + + text, err := elem.Text() + if err != nil { + t.Fatal(err) + } + + t.Log(text) +} + +func TestElement_GetAttribute(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + elem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "com.android.settings:id/category_title"}) + if err != nil { + t.Fatal(err) + } + + attribute, err := elem.GetAttribute("class") + if err != nil { + t.Fatal(err) + } + + t.Log(attribute) +} + +func TestElement_ContentDescription(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + elem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "com.android.settings:id/search"}) + if err != nil { + t.Fatal(err) + } + + name, err := elem.ContentDescription() + if err != nil { + t.Fatal(err) + } + + t.Log(name) +} + +func TestElement_Size(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + elem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "com.android.settings:id/search"}) + if err != nil { + t.Fatal(err) + } + + size, err := elem.Size() + if err != nil { + t.Fatal(err) + } + + t.Log(size) +} + +func TestElement_Rect(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + elem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "com.android.settings:id/category_title"}) + if err != nil { + t.Fatal(err) + } + + rect, err := elem.Rect() + if err != nil { + t.Fatal(err) + } + + t.Log(rect) +} + +func TestElement_Screenshot(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + elem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "com.android.settings:id/category_title"}) + if err != nil { + t.Fatal(err) + } + + screenshot, err := elem.Screenshot() + if err != nil { + t.Fatal(err) + } + + t.Log(ioutil.WriteFile("/Users/hero/Desktop/e1.png", screenshot.Bytes(), 0o600)) +} + +func TestElement_Location(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + elem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "com.android.settings:id/category_title"}) + if err != nil { + t.Fatal(err) + } + + location, err := elem.Location() + if err != nil { + t.Fatal(err) + } + + t.Log(location) +} + +func TestElement_Click(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + elem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "com.android.settings:id/title"}) + if err != nil { + t.Fatal(err) + } + + err = elem.Click() + if err != nil { + t.Fatal(err) + } +} + +func TestElement_Clear(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + elem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "android:id/search_src_text"}) + if err != nil { + t.Fatal(err) + } + + err = elem.Clear() + if err != nil { + t.Fatal(err) + } +} + +func TestElement_SendKeys(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + elem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "android:id/search_src_text"}) + if err != nil { + t.Fatal(err) + } + + // return + + // err = elem.SendKeys("abc") + err = elem.SendKeys("456", false) + if err != nil { + t.Fatal(err) + } +} + +func TestElement_FindElements(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + parentElem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "com.android.settings:id/main_content"}) + if err != nil { + t.Fatal(err) + } + + elements, err := parentElem.FindElements(AndroidBySelector{ResourceIdID: "com.android.settings:id/category"}) + if err != nil { + t.Fatal(err) + } + t.Log(len(elements)) +} + +func TestElement_FindElement(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + parentElem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "com.android.settings:id/main_content"}) + if err != nil { + t.Fatal(err) + } + + elem, err := parentElem.FindElement(AndroidBySelector{ResourceIdID: "com.android.settings:id/category_title"}) + if err != nil { + t.Fatal(err) + } + + t.Log(elem.Text()) +} + +func TestElement_Swipe(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + elem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "com.android.settings:id/category_title"}) + if err != nil { + t.Fatal(err) + } + + rect, err := elem.Rect() + if err != nil { + t.Fatal(err) + } + + t.Log(rect) + + var startX, startY, endX, endY int + startX = rect.X + rect.Width/20 + startY = rect.Y + rect.Height/2 + endX = startX + endY = startY - startY/2 + err = elem.Swipe(startX, startY, endX, endY) + if err != nil { + t.Fatal(err) + } + + startPoint := PointF{X: float64(rect.X + rect.Width/20 + 30), Y: float64(startY / 2)} + endPoint := PointF{X: startPoint.X, Y: startPoint.Y + startPoint.Y} + err = elem.SwipePointF(startPoint, endPoint) + if err != nil { + t.Fatal(err) + } +} + +func TestElement_Drag(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + elements, err := driver.FindElements(AndroidBySelector{ClassName: "android.widget.TextView"}) + if err != nil { + t.Fatal(err) + } + + for i, elem := range elements { + text, _ := elem.Text() + t.Log(i, text) + } + + rect, err := elements[0].Rect() + if err != nil { + t.Fatal(err) + } + + // err = elements[0].Drag(300, 450, 256) + err = elements[0].Drag(300, 450, 256) + if err != nil { + t.Fatal(err) + } + + err = elements[0].DragTo(elements[1], 256) + if err != nil { + t.Fatal(err) + } + + endPoint := PointF{X: float64(rect.X + rect.Width/3*2), Y: float64(rect.Y + rect.Height/2)} + err = elements[0].DragPointF(endPoint, 256) + if err != nil { + t.Fatal() + } +} + +func TestElement_Flick(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + elem, err := driver.FindElement(AndroidBySelector{UiAutomator: "new UiSelector().text(\"提示音和通知\");"}) + if err != nil { + t.Fatal(err) + } + + err = elem.Flick(36, 20, 100) + if err != nil { + t.Fatal(err) + } +} + +func TestElement_ScrollTo(t *testing.T) { + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + // how to make it work? + // parentElem, err := driver.FindElement(AndroidBySelector{ClassName: "android.widget.ScrollView"}) + // parentElem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "com.cyanogenmod.filemanager:id/navigation_view_layout"}) + parentElem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "com.android.settings:id/dashboard"}) + if err != nil { + t.Fatal(err) + } + + err = parentElem.ScrollTo(AndroidBySelector{ContentDescription: "电池"}) + if err != nil { + t.Fatal(err) + } +} + +func TestElement_ScrollToElement(t *testing.T) { + // android.widget.HorizontalScrollView + driver, err := NewUIADriver(nil, uiaServerURL) + if err != nil { + t.Fatal(err) + } + + // how to make it work? + parentElem, err := driver.FindElement(AndroidBySelector{UiAutomator: "new UiSelector().resourceId(\"com.android.settings:id/dashboard\");"}) + if err != nil { + t.Fatal(err) + } + + element, err := driver.FindElement(AndroidBySelector{UiAutomator: "new UiSelector().text(\"电池\");"}) + if err != nil { + t.Fatal(err) + } + + err = parentElem.ScrollToElement(element) + if err != nil { + t.Fatal(err) + } +} diff --git a/hrp/internal/uixt/android_webdriver.go b/hrp/internal/uixt/android_webdriver.go deleted file mode 100644 index 91dd4070..00000000 --- a/hrp/internal/uixt/android_webdriver.go +++ /dev/null @@ -1,3 +0,0 @@ -package uixt - -type uiaWebDriver struct{} diff --git a/hrp/internal/uixt/android_webelment.go b/hrp/internal/uixt/android_webelment.go deleted file mode 100644 index 64c90179..00000000 --- a/hrp/internal/uixt/android_webelment.go +++ /dev/null @@ -1 +0,0 @@ -package uixt diff --git a/hrp/internal/uixt/client.go b/hrp/internal/uixt/client.go new file mode 100644 index 00000000..befa38d6 --- /dev/null +++ b/hrp/internal/uixt/client.go @@ -0,0 +1,104 @@ +package uixt + +import ( + "bytes" + "context" + "encoding/json" + "io" + "io/ioutil" + "net" + "net/http" + "net/url" + "path" + "strings" + "time" + + "github.com/rs/zerolog/log" +) + +type Driver struct { + urlPrefix *url.URL + sessionId string + client *http.Client +} + +func (wd *Driver) concatURL(u *url.URL, elem ...string) string { + var tmp *url.URL + if u == nil { + u = wd.urlPrefix + } + tmp, _ = url.Parse(u.String()) + tmp.Path = path.Join(append([]string{u.Path}, elem...)...) + return tmp.String() +} + +func (wd *Driver) httpGET(pathElem ...string) (rawResp rawResponse, err error) { + return wd.httpRequest(http.MethodGet, wd.concatURL(nil, pathElem...), nil) +} + +func (wd *Driver) httpPOST(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.httpRequest(http.MethodPost, wd.concatURL(nil, pathElem...), bsJSON) +} + +func (wd *Driver) httpDELETE(pathElem ...string) (rawResp rawResponse, err error) { + return wd.httpRequest(http.MethodDelete, wd.concatURL(nil, pathElem...), nil) +} + +func (wd *Driver) httpRequest(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 = http.NewRequest(method, rawURL, bytes.NewBuffer(rawBody)); err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json;charset=UTF-8") + req.Header.Set("Accept", "application/json") + + start := time.Now() + var resp *http.Response + if resp, err = wd.client.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.Str("response", string(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 +} + +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, + } +} diff --git a/hrp/internal/uixt/gesture.go b/hrp/internal/uixt/gesture.go index ce3b6b21..7642425c 100644 --- a/hrp/internal/uixt/gesture.go +++ b/hrp/internal/uixt/gesture.go @@ -4,6 +4,7 @@ package uixt import ( "image" + "sort" ) func (dExt *DriverExt) GesturePassword(pathname string, password ...int) (err error) { diff --git a/hrp/internal/uixt/interface.go b/hrp/internal/uixt/interface.go index cbe7a0e1..0101d2d9 100644 --- a/hrp/internal/uixt/interface.go +++ b/hrp/internal/uixt/interface.go @@ -164,6 +164,8 @@ type BatteryInfo struct { // Battery state ( 1: on battery, discharging; 2: plugged in, less than 100%, 3: plugged in, at 100% ) State BatteryState `json:"state"` + + Status BatteryStatus `json:"status"` } type BatteryState int @@ -683,6 +685,11 @@ type Point struct { Y int `json:"y"` // upper left Y coordinate of selected element } +type PointF struct { + X float64 `json:"x"` + Y float64 `json:"y"` +} + type Rect struct { Point Size @@ -708,6 +715,11 @@ func WithFrequency(frequency int) DataOption { } } +// current implemeted device: IOSDevice, AndroidDevice +type Device interface { + UUID() string +} + // WebDriver defines methods supported by WebDriver drivers. type WebDriver interface { // NewSession starts a new session and returns the SessionInfo. diff --git a/hrp/internal/uixt/ios_device.go b/hrp/internal/uixt/ios_device.go index eeee5e5f..9a243ce3 100644 --- a/hrp/internal/uixt/ios_device.go +++ b/hrp/internal/uixt/ios_device.go @@ -2,7 +2,6 @@ package uixt import ( "bytes" - "context" "encoding/base64" builtinJSON "encoding/json" "fmt" @@ -14,7 +13,6 @@ import ( "net/url" "regexp" "strings" - "sync" "time" giDevice "github.com/electricbubble/gidevice" @@ -40,7 +38,7 @@ const ( ) const ( - defaultPort = 8100 + defaultWDAPort = 8100 defaultMjpegPort = 9100 ) @@ -102,10 +100,6 @@ func InitWDAClient(device *IOSDevice) (*DriverExt, error) { return driverExt, nil } -type Device interface { - UUID() string -} - type IOSDeviceOption func(*IOSDevice) func WithUDID(udid string) IOSDeviceOption { @@ -144,7 +138,7 @@ func NewIOSDevice(options ...IOSDeviceOption) (device *IOSDevice, err error) { } device = &IOSDevice{ - Port: defaultPort, + Port: defaultWDAPort, MjpegPort: defaultMjpegPort, } for _, option := range options { @@ -191,37 +185,33 @@ func (dev *IOSDevice) NewHTTPDriver(capabilities Capabilities) (driver WebDriver return nil, err } wd.sessionId = sessionInfo.SessionId + wd.client = http.DefaultClient - if wd.mjpegConn, err = net.Dial( + if wd.mjpegHTTPConn, err = net.Dial( "tcp", fmt.Sprintf("%s:%d", wd.urlPrefix.Hostname(), dev.MjpegPort), ); err != nil { return nil, err } - wd.mjpegClient = convertToHTTPClient(wd.mjpegConn) + wd.mjpegClient = convertToHTTPClient(wd.mjpegHTTPConn) return wd, nil } // NewUSBDriver creates new client via USB connected device, this will also start a new session. func (dev *IOSDevice) NewUSBDriver(capabilities Capabilities) (driver WebDriver, err error) { - wd := &wdaDriver{ - usbCli: &struct { - httpCli *http.Client - defaultConn, mjpegConn giDevice.InnerConn - sync.Mutex - }{}, - } - if wd.usbCli.defaultConn, err = dev.NewConnect(dev.Port, 0); err != nil { + wd := new(wdaDriver) + + if wd.defaultConn, err = dev.NewConnect(dev.Port, 0); err != nil { return nil, fmt.Errorf("create connection: %w", err) } - wd.usbCli.httpCli = convertToHTTPClient(wd.usbCli.defaultConn.RawConn()) + wd.client = convertToHTTPClient(wd.defaultConn.RawConn()) - if wd.usbCli.mjpegConn, err = dev.NewConnect(dev.MjpegPort, 0); err != nil { + if wd.mjpegUSBConn, err = dev.NewConnect(dev.MjpegPort, 0); err != nil { return nil, fmt.Errorf("create connection MJPEG: %w", err) } - wd.mjpegClient = convertToHTTPClient(wd.usbCli.mjpegConn.RawConn()) + wd.mjpegClient = convertToHTTPClient(wd.mjpegUSBConn.RawConn()) if wd.urlPrefix, err = url.Parse("http://" + dev.UDID); err != nil { return nil, err @@ -245,17 +235,6 @@ func (dev *IOSDevice) NewUSBDriver(capabilities Capabilities) (driver WebDriver, return wd, err } -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"` @@ -374,9 +353,10 @@ 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"` + Err string `json:"error"` + Message string `json:"message"` + Traceback string `json:"traceback"` // wda + Stacktrace string `json:"stacktrace"` // uia } }) if err = json.Unmarshal(r, reply); err != nil { diff --git a/hrp/internal/uixt/ios_driver.go b/hrp/internal/uixt/ios_driver.go index 6464fd2d..e99e33bc 100644 --- a/hrp/internal/uixt/ios_driver.go +++ b/hrp/internal/uixt/ios_driver.go @@ -5,57 +5,46 @@ import ( "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" ) type wdaDriver struct { - urlPrefix *url.URL - sessionId string + Driver - usbCli *struct { - httpCli *http.Client - defaultConn, mjpegConn giDevice.InnerConn - sync.Mutex - } + // default port + defaultConn giDevice.InnerConn - mjpegClient *http.Client - mjpegConn net.Conn + // mjpeg port + mjpegUSBConn giDevice.InnerConn // via USB + mjpegHTTPConn net.Conn // via HTTP + mjpegClient *http.Client } -func (wd *wdaDriver) GetMjpegHTTPClient() *http.Client { +func (wd *wdaDriver) GetMjpegClient() *http.Client { return wd.mjpegClient } func (wd *wdaDriver) Close() error { - if wd.usbCli == nil { + if wd.defaultConn != nil { + wd.defaultConn.Close() + } + if wd.mjpegUSBConn != nil { + wd.mjpegUSBConn.Close() + } + + if wd.mjpegClient != 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 + return wd.mjpegHTTPConn.Close() } func (wd *wdaDriver) NewSession(capabilities Capabilities) (sessionInfo SessionInfo, err error) { @@ -844,83 +833,3 @@ func (wd *wdaDriver) WaitWithTimeout(condition Condition, timeout time.Duration) func (wd *wdaDriver) Wait(condition Condition) error { return wd.WaitWithTimeoutAndInterval(condition, DefaultWaitTimeout, DefaultWaitInterval) } - -func (wd *wdaDriver) concatURL(u *url.URL, elem ...string) string { - var tmp *url.URL - if u == nil { - u = wd.urlPrefix - } - tmp, _ = url.Parse(u.String()) - tmp.Path = path.Join(append([]string{u.Path}, elem...)...) - return tmp.String() -} - -func (wd *wdaDriver) httpGET(pathElem ...string) (rawResp rawResponse, err error) { - return wd.httpRequest(http.MethodGet, wd.concatURL(nil, pathElem...), nil) -} - -func (wd *wdaDriver) httpPOST(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.httpRequest(http.MethodPost, wd.concatURL(nil, pathElem...), bsJSON) -} - -func (wd *wdaDriver) httpDELETE(pathElem ...string) (rawResp rawResponse, err error) { - return wd.httpRequest(http.MethodDelete, wd.concatURL(nil, pathElem...), nil) -} - -func (wd *wdaDriver) httpRequest(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 = http.NewRequest(method, rawURL, bytes.NewBuffer(rawBody)); err != nil { - return nil, err - } - req.Header.Set("Content-Type", "application/json;charset=UTF-8") - req.Header.Set("Accept", "application/json") - - var httpCli *http.Client - if wd.usbCli != nil { - wd.usbCli.Lock() - defer wd.usbCli.Unlock() - httpCli = wd.usbCli.httpCli - } else { - httpCli = http.DefaultClient - } - 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_test.go b/hrp/internal/uixt/ios_test.go index 9f6f8236..62c52161 100644 --- a/hrp/internal/uixt/ios_test.go +++ b/hrp/internal/uixt/ios_test.go @@ -351,8 +351,6 @@ func Test_remoteWD_Homescreen(t *testing.T) { func Test_remoteWD_AppLaunch(t *testing.T) { setup(t) - // SetDebug(true) - // bundleId = "com.hustlzp.xcz" // bundleId = "com.github.stormbreaker.prod" // bundleId = "com.360buy.jdmobile" diff --git a/hrp/internal/uixt/tap_test.go b/hrp/internal/uixt/tap_test.go index 6fc499e1..bde3c861 100644 --- a/hrp/internal/uixt/tap_test.go +++ b/hrp/internal/uixt/tap_test.go @@ -10,8 +10,6 @@ func TestDriverExt_TapWithNumber(t *testing.T) { pathSearch := "/Users/hero/Documents/temp/2020-05/opencv/flag7.png" - // SetDebug(true) - err = driverExt.TapWithNumber(pathSearch, 3) checkErr(t, err)