refactor: move uixt from hrp internal to pkg

This commit is contained in:
debugtalk
2022-10-10 21:50:07 +08:00
parent f4584db139
commit 59b06fcf65
44 changed files with 54 additions and 21 deletions

51
hrp/pkg/uixt/README.md Normal file
View File

@@ -0,0 +1,51 @@
# uixt
From v4.3.0HttpRunner will support mobile UI automation testing:
- iOS: based on [appium/WebDriverAgent], with forked client library [electricbubble/gwda] in golang
- Android: based on [appium-uiautomator2-server], with forked client library [electricbubble/guia2] in golang
Some UI recognition algorithms are also introduced for both iOS and Android:
- OpenCV: based on [OpenCV 4], with golang bindings [hybridgroup/gocv] and helper utils [electricbubble/gwda-ext-opencv]
- OCR: based on OCR API service from [volcengine], other API service may be extended
## Dependencies
### OpenCV
[OpenCV 4] should be pre-installed.
You can install OpenCV 4.6.0 using Homebrew on macOS.
```bash
$ brew install opencv
```
You can get more installation introduction on [hybridgroup/gocv].
### OCR
OCR API is a paid service, you need to pre-purchase and configure the account key.
```bash
$ make build tags=ocr
```
## Thanks
This uixt module is initially forked from the following repos and made a lot of changes.
- [electricbubble/gwda-ext-opencv]
- [electricbubble/gwda]
- [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/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
[appium-uiautomator2-server]: https://github.com/appium/appium-uiautomator2-server

View File

@@ -0,0 +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 (ud *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 = ud.httpPOST(data, "/session", ud.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
}

View File

@@ -0,0 +1,686 @@
package uixt
import (
"bytes"
"context"
"fmt"
"net"
"os/exec"
"reflect"
"strings"
"syscall"
"github.com/electricbubble/gadb"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
"github.com/httprunner/httprunner/v4/hrp/internal/json"
)
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
driverExt, err = Extend(driver)
if err != nil {
return nil, errors.Wrap(err, "failed to extend UIA Driver")
}
if device.LogOn {
err = driverExt.Driver.StartCaptureLog("hrp_adb_log")
if err != nil {
return nil, err
}
}
driverExt.UUID = androidDevice.UUID()
return driverExt, err
}
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.d = dev
device.logcat = NewAdbLogcat(serialNumber)
return device, nil
}
return nil, fmt.Errorf("device %s not found", device.SerialNumber)
}
type AndroidDevice struct {
d gadb.Device
logcat *DeviceLogcat
SerialNumber string `json:"serial,omitempty" yaml:"serial,omitempty"`
IP string `json:"ip,omitempty" yaml:"ip,omitempty"`
Port int `json:"port,omitempty" yaml:"port,omitempty"`
MjpegPort int `json:"mjpeg_port,omitempty" yaml:"mjpeg_port,omitempty"`
LogOn bool `json:"log_on,omitempty" yaml:"log_on,omitempty"`
}
func (o AndroidDevice) UUID() string {
return o.SerialNumber
}
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.d.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.d.ForwardKill(localPort)
return nil, err
}
driver.adbDevice = dev.d
driver.logcat = dev.logcat
driver.localPort = localPort
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.d
return driver, nil
}
func getFreePort() (int, error) {
addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
if err != nil {
return 0, errors.Wrap(err, "resolve tcp addr failed")
}
l, err := net.ListenTCP("tcp", addr)
if err != nil {
return 0, errors.Wrap(err, "listen tcp addr failed")
}
defer func() { _ = l.Close() }()
return l.Addr().(*net.TCPAddr).Port, nil
}
type DeviceLogcat struct {
serial string
logBuffer *bytes.Buffer
errs []error
stopping chan struct{}
done chan struct{}
cmd *exec.Cmd
}
func NewAdbLogcat(serial string) *DeviceLogcat {
return &DeviceLogcat{
serial: serial,
logBuffer: new(bytes.Buffer),
stopping: make(chan struct{}),
done: make(chan struct{}),
}
}
// CatchLogcatContext starts logcat with timeout context
func (l *DeviceLogcat) CatchLogcatContext(timeoutCtx context.Context) (err error) {
if err = l.CatchLogcat(); err != nil {
return
}
go func() {
select {
case <-timeoutCtx.Done():
_ = l.Stop()
case <-l.stopping:
}
}()
return
}
func (l *DeviceLogcat) Stop() error {
select {
case <-l.stopping:
default:
close(l.stopping)
<-l.done
close(l.done)
}
return l.Errors()
}
func (l *DeviceLogcat) Errors() (err error) {
for _, e := range l.errs {
if err != nil {
err = fmt.Errorf("%v |[DeviceLogcatErr] %v", err, e)
} else {
err = fmt.Errorf("[DeviceLogcatErr] %v", e)
}
}
return
}
func (l *DeviceLogcat) CatchLogcat() (err error) {
if l.cmd != nil {
err = fmt.Errorf("logcat already start")
}
cmdLine := fmt.Sprintf("adb -s %s logcat -c && adb -s %s logcat -v time -s iesqaMonitor:V", l.serial, l.serial)
l.cmd = builtin.Command(cmdLine)
l.cmd.Stderr = l.logBuffer
l.cmd.Stdout = l.logBuffer
l.cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
if err = l.cmd.Start(); err != nil {
return
}
go func() {
<-l.stopping
if e := syscall.Kill(-l.cmd.Process.Pid, syscall.SIGKILL); e != nil {
l.errs = append(l.errs, fmt.Errorf("kill logcat process err:%v", e))
}
l.done <- struct{}{}
}()
return
}
func (l *DeviceLogcat) BufferedLogcat() (err error) {
// -d: dump the current buffered logcat result and exits
cmdLine := fmt.Sprintf("adb -s %s logcat -d", l.serial)
cmd := builtin.Command(cmdLine)
cmd.Stdout = l.logBuffer
cmd.Stderr = l.logBuffer
if err = cmd.Run(); err != nil {
return
}
return
}
type ExportPoint struct {
Start int `json:"start" yaml:"start"`
End int `json:"end" yaml:"end"`
From interface{} `json:"from" yaml:"from"`
To interface{} `json:"to" yaml:"to"`
Operation string `json:"operation" yaml:"operation"`
Ext string `json:"ext" yaml:"ext"`
RunTime int `json:"run_time,omitempty" yaml:"run_time,omitempty"`
}
func ConvertPoints(data string) (eps []ExportPoint) {
lines := strings.Split(data, "\n")
for _, line := range lines {
if strings.Contains(line, "ext") {
idx := strings.Index(line, "{")
line = line[idx:]
p := ExportPoint{}
err := json.Unmarshal([]byte(line), &p)
if err != nil {
log.Error().Msg("failed to parse point data")
continue
}
eps = append(eps, p)
}
}
return
}
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
}

View File

@@ -0,0 +1,18 @@
package uixt
import (
"fmt"
"testing"
"github.com/httprunner/httprunner/v4/hrp/internal/json"
)
func TestConvertPoints(t *testing.T) {
data := "10-09 20:16:48.216 I/iesqaMonitor(17845): {\"duration\":0,\"end\":1665317808206,\"ext\":\"输入\",\"from\":{\"x\":0.0,\"y\":0.0},\"operation\":\"Gtf-SendKeys\",\"run_time\":627,\"start\":1665317807579,\"start_first\":0,\"start_last\":0,\"to\":{\"x\":0.0,\"y\":0.0}}\n10-09 20:18:22.899 I/iesqaMonitor(17845): {\"duration\":0,\"end\":1665317902898,\"ext\":\"进入直播间\",\"from\":{\"x\":717.0,\"y\":2117.5},\"operation\":\"Gtf-Tap\",\"run_time\":121,\"start\":1665317902777,\"start_first\":0,\"start_last\":0,\"to\":{\"x\":717.0,\"y\":2117.5}}\n10-09 20:18:32.063 I/iesqaMonitor(17845): {\"duration\":0,\"end\":1665317912062,\"ext\":\"第一次上划\",\"from\":{\"x\":1437.0,\"y\":2409.9},\"operation\":\"Gtf-Swipe\",\"run_time\":32,\"start\":1665317912030,\"start_first\":0,\"start_last\":0,\"to\":{\"x\":1437.0,\"y\":2409.9}}"
eps := ConvertPoints(data)
if len(eps) != 3 {
t.Fatal()
}
jsons, _ := json.Marshal(eps)
println(fmt.Sprintf("%v", string(jsons)))
}

View File

@@ -0,0 +1,996 @@
package uixt
import (
"bytes"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"net"
"net/url"
"strconv"
"strings"
"time"
"github.com/electricbubble/gadb"
"github.com/rs/zerolog/log"
)
var errDriverNotImplemented = errors.New("driver method not implemented")
type uiaDriver struct {
Driver
adbDevice gadb.Device
logcat *DeviceLogcat
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
}
var localPort int
{
tmpURL, _ := url.Parse(driver.urlPrefix.String())
hostname := tmpURL.Hostname()
if strings.HasPrefix(hostname, forwardToPrefix) {
localPort, _ = strconv.Atoi(strings.TrimPrefix(hostname, forwardToPrefix))
}
}
conn, err := net.Dial("tcp", fmt.Sprintf(":%d", localPort))
if err != nil {
return nil, fmt.Errorf("adb forward: %w", err)
}
driver.client = convertToHTTPClient(conn)
if session, err := driver.NewSession(capabilities); err != nil {
return nil, err
} else {
driver.sessionId = session.SessionId
}
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 (ud *uiaDriver) Close() (err error) {
if ud.sessionId == "" {
return nil
}
if _, err = ud.httpDELETE("/session", ud.sessionId); err == nil {
ud.sessionId = ""
}
return err
}
func (ud *uiaDriver) NewSession(capabilities Capabilities) (sessionInfo SessionInfo, err error) {
// register(postHandler, new NewSession("/wd/hub/session"))
var rawResp rawResponse
data := map[string]interface{}{"capabilities": capabilities}
if rawResp, err = ud.httpPOST(data, "/session"); err != nil {
return SessionInfo{SessionId: ""}, err
}
reply := new(struct{ Value struct{ SessionId string } })
if err = json.Unmarshal(rawResp, reply); err != nil {
return SessionInfo{SessionId: ""}, err
}
sessionID := reply.Value.SessionId
// d.sessionIdCache[sessionID] = true
return SessionInfo{SessionId: sessionID}, nil
}
func (ud *uiaDriver) ActiveSession() (sessionInfo SessionInfo, err error) {
// [[FBRoute GET:@""] respondWithTarget:self action:@selector(handleGetActiveSession:)]
return SessionInfo{SessionId: ud.sessionId}, nil
}
func (ud *uiaDriver) SessionIDs() (sessionIDs []string, err error) {
// register(getHandler, new GetSessions("/wd/hub/sessions"))
var rawResp rawResponse
if rawResp, err = ud.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 (ud *uiaDriver) SessionDetails() (scrollData map[string]interface{}, err error) {
// register(getHandler, new GetSessionDetails("/wd/hub/session/:sessionId"))
var rawResp rawResponse
if rawResp, err = ud.httpGET("/session", ud.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 (ud *uiaDriver) DeleteSession() (err error) {
// TODO
return errDriverNotImplemented
}
func (ud *uiaDriver) Status() (deviceStatus DeviceStatus, err error) {
// register(getHandler, new Status("/wd/hub/status"))
var rawResp rawResponse
if rawResp, err = ud.httpGET("/status"); err != nil {
return DeviceStatus{Ready: false}, err
}
reply := new(struct {
Value struct {
// Message string
Ready bool
}
})
if err = json.Unmarshal(rawResp, reply); err != nil {
return DeviceStatus{Ready: false}, err
}
return DeviceStatus{Ready: true}, nil
}
func (ud *uiaDriver) DeviceInfo() (deviceInfo DeviceInfo, err error) {
// register(getHandler, new GetDeviceInfo("/wd/hub/session/:sessionId/appium/device/info"))
var rawResp rawResponse
if rawResp, err = ud.httpGET("/session", ud.sessionId, "appium/device/info"); err != nil {
return DeviceInfo{}, err
}
reply := new(struct{ Value struct{ DeviceInfo } })
if err = json.Unmarshal(rawResp, reply); err != nil {
return DeviceInfo{}, err
}
deviceInfo = reply.Value.DeviceInfo
return
}
func (ud *uiaDriver) Location() (location Location, err error) {
// TODO
return location, errDriverNotImplemented
}
func (ud *uiaDriver) BatteryInfo() (batteryInfo BatteryInfo, err error) {
// register(getHandler, new GetBatteryInfo("/wd/hub/session/:sessionId/appium/device/battery_info"))
var rawResp rawResponse
if rawResp, err = ud.httpGET("/session", ud.sessionId, "appium/device/battery_info"); err != nil {
return BatteryInfo{}, err
}
reply := new(struct{ Value struct{ BatteryInfo } })
if err = json.Unmarshal(rawResp, reply); err != nil {
return BatteryInfo{}, err
}
if reply.Value.Level == -1 || reply.Value.Status == -1 {
return reply.Value.BatteryInfo, errors.New("cannot be retrieved from the system")
}
batteryInfo = reply.Value.BatteryInfo
return
}
func (ud *uiaDriver) WindowSize() (size Size, err error) {
// register(getHandler, new GetDeviceSize("/wd/hub/session/:sessionId/window/:windowHandle/size"))
var rawResp rawResponse
if rawResp, err = ud.httpGET("/session", ud.sessionId, "window/:windowHandle/size"); err != nil {
return Size{}, err
}
reply := new(struct{ Value struct{ Size } })
if err = json.Unmarshal(rawResp, reply); err != nil {
return Size{}, err
}
size = reply.Value.Size
return
}
func (ud *uiaDriver) Screen() (screen Screen, err error) {
// TODO
return screen, errDriverNotImplemented
}
func (ud *uiaDriver) Scale() (scale float64, err error) {
return 1, nil
}
// PressBack simulates a short press on the BACK button.
func (ud *uiaDriver) PressBack() (err error) {
// register(postHandler, new PressBack("/wd/hub/session/:sessionId/back"))
_, err = ud.httpPOST(nil, "/session", ud.sessionId, "back")
return
}
func (ud *uiaDriver) StartCamera() (err error) {
if _, err = ud.adbDevice.RunShellCommand("rm", "-r", "/sdcard/DCIM/Camera"); err != nil {
return err
}
time.Sleep(5 * time.Second)
var version string
if version, err = ud.adbDevice.RunShellCommand("getprop", "ro.build.version.release"); err != nil {
return err
}
if version == "11" || version == "12" {
if _, err = ud.adbDevice.RunShellCommand("am", "start", "-a", "android.media.action.STILL_IMAGE_CAMERA"); err != nil {
return err
}
time.Sleep(5 * time.Second)
if _, err = ud.adbDevice.RunShellCommand("input", "swipe", "750", "1000", "250", "1000"); err != nil {
return err
}
time.Sleep(5 * time.Second)
if _, err = ud.adbDevice.RunShellCommand("input", "keyevent", "27"); err != nil {
return err
}
return
} else {
if _, err = ud.adbDevice.RunShellCommand("am", "start", "-a", "android.media.action.VIDEO_CAPTURE"); err != nil {
return err
}
time.Sleep(5 * time.Second)
if _, err = ud.adbDevice.RunShellCommand("input", "keyevent", "27"); err != nil {
return err
}
return
}
}
func (ud *uiaDriver) StopCamera() (err error) {
err = ud.PressBack()
if err != nil {
return err
}
err = ud.Homescreen()
if err != nil {
return err
}
// kill samsung shell command
if _, err = ud.adbDevice.RunShellCommand("am", "force-stop", "com.sec.android.app.camera"); err != nil {
return err
}
// kill other camera (huawei mi)
if _, err = ud.adbDevice.RunShellCommand("am", "force-stop", "com.android.camera2"); err != nil {
return err
}
return
}
func (ud *uiaDriver) ActiveAppInfo() (info AppInfo, err error) {
// TODO
return info, errDriverNotImplemented
}
func (ud *uiaDriver) ActiveAppsList() (appsList []AppBaseInfo, err error) {
// TODO
return appsList, errDriverNotImplemented
}
func (ud *uiaDriver) AppState(bundleId string) (runState AppState, err error) {
// TODO
return runState, errDriverNotImplemented
}
func (ud *uiaDriver) IsLocked() (locked bool, err error) {
// TODO
return locked, errDriverNotImplemented
}
func (ud *uiaDriver) Unlock() (err error) {
// TODO
return errDriverNotImplemented
}
func (ud *uiaDriver) Lock() (err error) {
// TODO
return errDriverNotImplemented
}
func (ud *uiaDriver) Homescreen() (err error) {
return ud.PressKeyCode(KCHome, KMEmpty)
}
func (ud *uiaDriver) PressKeyCode(keyCode KeyCode, metaState KeyMeta, flags ...KeyFlag) (err error) {
if len(flags) == 0 {
flags = []KeyFlag{KFFromSystem}
}
return ud._pressKeyCode(keyCode, metaState, KFFromSystem)
}
func (ud *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 = ud.httpPOST(data, "/session", ud.sessionId, "appium/device/press_keycode")
return
}
func (ud *uiaDriver) AlertText() (text string, err error) {
// register(getHandler, new GetAlertText("/wd/hub/session/:sessionId/alert/text"))
var rawResp rawResponse
if rawResp, err = ud.httpGET("/session", ud.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
}
func (ud *uiaDriver) AlertButtons() (btnLabels []string, err error) {
// TODO
return btnLabels, errDriverNotImplemented
}
func (ud *uiaDriver) AlertAccept(label ...string) (err error) {
data := map[string]interface{}{
"buttonLabel": nil,
}
if len(label) != 0 {
data["buttonLabel"] = label[0]
}
// register(postHandler, new AcceptAlert("/wd/hub/session/:sessionId/alert/accept"))
_, err = ud.httpPOST(data, "/session", ud.sessionId, "alert/accept")
return
}
func (ud *uiaDriver) AlertDismiss(label ...string) (err error) {
data := map[string]interface{}{
"buttonLabel": nil,
}
if len(label) != 0 {
data["buttonLabel"] = label[0]
}
// register(postHandler, new DismissAlert("/wd/hub/session/:sessionId/alert/dismiss"))
_, err = ud.httpPOST(data, "/session", ud.sessionId, "alert/dismiss")
return
}
func (ud *uiaDriver) AlertSendKeys(text string) (err error) {
// TODO
return errDriverNotImplemented
}
func (ud *uiaDriver) check() error {
if ud.adbDevice.Serial() == "" {
return errors.New("adb daemon: the device is not ready")
}
return nil
}
func (ud *uiaDriver) AppLaunch(bundleId string, launchOpt ...AppLaunchOption) (err error) {
if err = ud.check(); err != nil {
return err
}
var sOutput string
if sOutput, err = ud.adbDevice.RunShellCommand("monkey -p", bundleId, "-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(launchOpt) != 0 {
var ce error
exists := func(ud WebDriver) (bool, error) {
for _, opt := range launchOpt {
if waitForComplete, ok := opt["androidBySelector"]; ok {
for _, e := range waitForComplete.([]BySelector) {
_, ce = ud.FindElement(e)
if ce == nil {
return true, nil
}
}
}
}
return false, nil
}
if err = ud.WaitWithTimeoutAndInterval(exists, 45, 1); err != nil {
return fmt.Errorf("app launch (waitForComplete): %s: %w", err.Error(), ce)
}
}
return
}
func (ud *uiaDriver) AppLaunchUnattached(bundleId string) (err error) {
// TODO
return errDriverNotImplemented
}
// Dispose corresponds to the command:
// adb -s $serial forward --remove $localPort
func (ud *uiaDriver) Dispose() (err error) {
if err = ud.check(); err != nil {
return err
}
if ud.localPort == 0 {
return nil
}
return ud.adbDevice.ForwardKill(ud.localPort)
}
func (ud *uiaDriver) AppTerminate(bundleId string) (successful bool, err error) {
if err = ud.check(); err != nil {
return false, err
}
_, err = ud.adbDevice.RunShellCommand("am force-stop", bundleId)
return err == nil, err
}
func (ud *uiaDriver) AppActivate(bundleId string) (err error) {
// TODO
return errDriverNotImplemented
}
func (ud *uiaDriver) AppDeactivate(second float64) (err error) {
// TODO
return errDriverNotImplemented
}
func (ud *uiaDriver) AppAuthReset(resource ProtectedResource) (err error) {
// TODO
return errDriverNotImplemented
}
func (ud *uiaDriver) Tap(x, y int, options ...DataOption) error {
return ud.TapFloat(float64(x), float64(y), options...)
}
func (ud *uiaDriver) TapFloat(x, y float64, options ...DataOption) (err error) {
// register(postHandler, new Tap("/wd/hub/session/:sessionId/appium/tap"))
data := map[string]interface{}{
"x": x,
"y": y,
}
// append options in post data for extra uiautomator configurations
for _, option := range options {
option(data)
}
_, err = ud.httpPOST(data, "/session", ud.sessionId, "appium/tap")
return
}
func (ud *uiaDriver) DoubleTap(x, y int) error {
return ud.DoubleTapFloat(float64(x), float64(y))
}
func (ud *uiaDriver) DoubleTapFloat(x, y float64) (err error) {
// TODO
return errDriverNotImplemented
}
func (ud *uiaDriver) TouchAndHold(x, y int, second ...float64) (err error) {
return ud.TouchAndHoldFloat(float64(x), float64(y), second...)
}
func (ud *uiaDriver) TouchAndHoldFloat(x, y float64, second ...float64) (err error) {
if len(second) == 0 {
second = []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(second[0] * 1000),
},
}
_, err = ud.httpPOST(data, "/session", ud.sessionId, "touch/longclick")
return
}
func (ud *uiaDriver) _drag(data map[string]interface{}) (err error) {
// register(postHandler, new Drag("/wd/hub/session/:sessionId/touch/drag"))
_, err = ud.httpPOST(data, "/session", ud.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 (ud *uiaDriver) Drag(fromX, fromY, toX, toY int, options ...DataOption) error {
return ud.DragFloat(float64(fromX), float64(fromY), float64(toX), float64(toY), options...)
}
func (ud *uiaDriver) DragFloat(fromX, fromY, toX, toY float64, options ...DataOption) (err error) {
data := map[string]interface{}{
"startX": fromX,
"startY": fromY,
"endX": toX,
"endY": toY,
}
// append options in post data for extra uiautomator configurations
for _, option := range options {
option(data)
}
if _, ok := data["steps"]; !ok {
data["steps"] = 12 // default steps
}
return ud._drag(data)
}
func (ud *uiaDriver) _swipe(startX, startY, endX, endY interface{}, options ...DataOption) (err error) {
// register(postHandler, new Swipe("/wd/hub/session/:sessionId/touch/perform"))
data := map[string]interface{}{
"startX": startX,
"startY": startY,
"endX": endX,
"endY": endY,
}
// append options in post data for extra uiautomator configurations
// e.g. use WithPressDuration to set pressForDuration
for _, option := range options {
option(data)
}
if _, ok := data["steps"]; !ok {
data["steps"] = 12 // default steps
}
_, err = ud.httpPOST(data, "/session", ud.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 (ud *uiaDriver) Swipe(fromX, fromY, toX, toY int, options ...DataOption) error {
options = append(options, WithSteps(12))
return ud.SwipeFloat(float64(fromX), float64(fromY), float64(toX), float64(toY), options...)
}
func (ud *uiaDriver) SwipeFloat(fromX, fromY, toX, toY float64, options ...DataOption) error {
return ud._swipe(fromX, fromY, toX, toY, options...)
}
func (ud *uiaDriver) ForceTouch(x, y int, pressure float64, second ...float64) error {
return ud.ForceTouchFloat(float64(x), float64(y), pressure, second...)
}
func (ud *uiaDriver) ForceTouchFloat(x, y, pressure float64, second ...float64) (err error) {
// TODO
return errDriverNotImplemented
}
func (ud *uiaDriver) PerformW3CActions(actions *W3CActions) (err error) {
data := map[string]interface{}{
"actions": actions,
}
// register(postHandler, new W3CActions("/wd/hub/session/:sessionId/actions"))
_, err = ud.httpPOST(data, "/session", ud.sessionId, "/actions")
return
}
func (ud *uiaDriver) PerformAppiumTouchActions(touchActs *TouchActions) (err error) {
// TODO
return errDriverNotImplemented
}
func (ud *uiaDriver) SetPasteboard(contentType PasteboardType, content string) (err error) {
lbl := content
const defaultLabelLen = 10
if len(lbl) > defaultLabelLen {
lbl = lbl[:defaultLabelLen]
}
data := map[string]interface{}{
"contentType": contentType,
"label": lbl,
"content": base64.StdEncoding.EncodeToString([]byte(content)),
}
// register(postHandler, new SetClipboard("/wd/hub/session/:sessionId/appium/device/set_clipboard"))
_, err = ud.httpPOST(data, "/session", ud.sessionId, "appium/device/set_clipboard")
return
}
func (ud *uiaDriver) GetPasteboard(contentType PasteboardType) (raw *bytes.Buffer, err error) {
if len(contentType) == 0 {
contentType = PasteboardTypePlaintext
}
// 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 = ud.httpPOST(data, "/session", ud.sessionId, "appium/device/get_clipboard"); err != nil {
return
}
reply := new(struct{ Value string })
if err = json.Unmarshal(rawResp, reply); err != nil {
return
}
if data, err := base64.StdEncoding.DecodeString(reply.Value); err != nil {
raw.Write([]byte(reply.Value))
} else {
raw.Write(data)
}
return
}
func (ud *uiaDriver) SendKeys(text string, options ...DataOption) (err error) {
// 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,
}
// append options in post data for extra uiautomator configurations
for _, option := range options {
option(data)
}
if _, ok := data["isReplace"]; !ok {
data["isReplace"] = true // default true
}
_, err = ud.httpPOST(data, "/session", ud.sessionId, "keys")
return
}
func (ud *uiaDriver) Input(text string, options ...DataOption) (err error) {
data := map[string]interface{}{
"view": text,
}
// append options in post data for extra uiautomator configurations
for _, option := range options {
option(data)
}
var element WebElement
if valuetext, ok := data["text"]; ok {
element, err = ud.FindElement(BySelector{UiAutomator: NewUiSelectorHelper().TextContains(fmt.Sprintf("%v", valuetext)).String()})
} else if valueid, ok := data["id"]; ok {
element, err = ud.FindElement(BySelector{ResourceIdID: fmt.Sprintf("%v", valueid)})
} else if valuedesc, ok := data["description"]; ok {
element, err = ud.FindElement(BySelector{UiAutomator: NewUiSelectorHelper().Description(fmt.Sprintf("%v", valuedesc)).String()})
} else {
element, err = ud.FindElement(BySelector{ClassName: ElementType{EditText: true}})
}
if err != nil {
return err
}
return element.SendKeys(text, options...)
}
func (ud *uiaDriver) KeyboardDismiss(keyNames ...string) (err error) {
// TODO
return errDriverNotImplemented
}
func (ud *uiaDriver) PressButton(devBtn DeviceButton) (err error) {
// TODO
return errDriverNotImplemented
}
func (ud *uiaDriver) IOHIDEvent(pageID EventPageID, usageID EventUsageID, duration ...float64) (err error) {
// TODO
return errDriverNotImplemented
}
func (ud *uiaDriver) ExpectNotification(notifyName string, notifyType NotificationType, second ...int) (err error) {
// register(postHandler, new OpenNotification("/wd/hub/session/:sessionId/appium/device/open_notifications"))
_, err = ud.httpPOST(nil, "/session", ud.sessionId, "appium/device/open_notifications")
return
}
func (ud *uiaDriver) SiriActivate(text string) (err error) {
// TODO
return errDriverNotImplemented
}
func (ud *uiaDriver) SiriOpenUrl(url string) (err error) {
// TODO
return errDriverNotImplemented
}
func (ud *uiaDriver) Orientation() (orientation Orientation, err error) {
// register(getHandler, new GetOrientation("/wd/hub/session/:sessionId/orientation"))
var rawResp rawResponse
if rawResp, err = ud.httpGET("/session", ud.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 (ud *uiaDriver) SetOrientation(orientation Orientation) (err error) {
// TODO
return errDriverNotImplemented
}
func (ud *uiaDriver) Rotation() (rotation Rotation, err error) {
// register(getHandler, new GetRotation("/wd/hub/session/:sessionId/rotation"))
var rawResp rawResponse
if rawResp, err = ud.httpGET("/session", ud.sessionId, "rotation"); err != nil {
return Rotation{}, err
}
reply := new(struct{ Value Rotation })
if err = json.Unmarshal(rawResp, reply); err != nil {
return Rotation{}, err
}
rotation = reply.Value
return
}
func (ud *uiaDriver) SetRotation(rotation Rotation) (err error) {
// TODO
return errDriverNotImplemented
}
func (ud *uiaDriver) MatchTouchID(isMatch bool) (err error) {
// TODO
return errDriverNotImplemented
}
func (ud *uiaDriver) _findElements(method, selector string, elementID ...string) (elements []WebElement, 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 = ud.httpPOST(data, "/session", ud.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([]WebElement, 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)
}
uie := WebElement(uiaElement{parent: ud, id: id})
elements[i] = uie
}
return
}
func (ud *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 = ud.httpPOST(data, "/session", ud.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: ud, id: id}
return
}
func (ud *uiaDriver) ActiveElement() (element WebElement, err error) {
// TODO
return element, errDriverNotImplemented
}
func (ud *uiaDriver) FindElement(by BySelector) (element WebElement, err error) {
return ud._findElement(by.getUsingAndValue())
}
func (ud *uiaDriver) FindElements(by BySelector) (elements []WebElement, err error) {
// [[FBRoute POST:@"/elements"] respondWithTarget:self action:@selector(handleFindElements:)]
using, value := by.getUsingAndValue()
data := map[string]interface{}{
"using": using,
"value": value,
}
var rawResp rawResponse
if rawResp, err = ud.httpPOST(data, "/session", ud.sessionId, "/elements"); err != nil {
return nil, err
}
var elementIDs []string
if elementIDs, err = rawResp.valueConvertToElementIDs(); err != nil {
if errors.Is(err, errNoSuchElement) {
return nil, fmt.Errorf("%w: unable to find an element using '%s', value '%s'", err, using, value)
}
return nil, err
}
elements = make([]WebElement, len(elementIDs))
for i := range elementIDs {
elements[i] = WebElement(uiaElement{parent: ud, id: elementIDs[i]})
}
return
}
func (ud *uiaDriver) Screenshot() (raw *bytes.Buffer, err error) {
// register(getHandler, new CaptureScreenshot("/wd/hub/session/:sessionId/screenshot"))
var rawResp rawResponse
if rawResp, err = ud.httpGET("/session", ud.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 (ud *uiaDriver) Source(srcOpt ...SourceOption) (source string, err error) {
// register(getHandler, new Source("/wd/hub/session/:sessionId/source"))
var rawResp rawResponse
if rawResp, err = ud.httpGET("/session", ud.sessionId, "source"); err != nil {
return "", err
}
reply := new(struct{ Value string })
if err = json.Unmarshal(rawResp, reply); err != nil {
return "", err
}
source = reply.Value
return
}
func (ud *uiaDriver) AccessibleSource() (source string, err error) {
// TODO
return source, errDriverNotImplemented
}
func (ud *uiaDriver) HealthCheck() (err error) {
// TODO
return errDriverNotImplemented
}
func (ud *uiaDriver) GetAppiumSettings() (settings map[string]interface{}, err error) {
// register(getHandler, new GetSettings("/wd/hub/session/:sessionId/appium/settings"))
var rawResp rawResponse
if rawResp, err = ud.httpGET("/session", ud.sessionId, "appium/settings"); err != nil {
return nil, err
}
reply := new(struct{ Value map[string]interface{} })
if err = json.Unmarshal(rawResp, reply); err != nil {
return nil, err
}
settings = reply.Value
return
}
func (ud *uiaDriver) SetAppiumSettings(settings map[string]interface{}) (ret map[string]interface{}, err error) {
data := map[string]interface{}{
"settings": settings,
}
// register(postHandler, new UpdateSettings("/wd/hub/session/:sessionId/appium/settings"))
_, err = ud.httpPOST(data, "/session", ud.sessionId, "appium/settings")
return
}
func (ud *uiaDriver) IsHealthy() (healthy bool, err error) {
// TODO
return healthy, errDriverNotImplemented
}
func (ud *uiaDriver) WaitWithTimeoutAndInterval(condition Condition, timeout, interval time.Duration) error {
startTime := time.Now()
for {
done, err := condition(ud)
if err != nil {
return err
}
if done {
return nil
}
if elapsed := time.Since(startTime); elapsed > timeout {
return fmt.Errorf("timeout after %v", elapsed)
}
time.Sleep(interval)
}
}
func (ud *uiaDriver) WaitWithTimeout(condition Condition, timeout time.Duration) error {
return ud.WaitWithTimeoutAndInterval(condition, timeout, DefaultWaitInterval)
}
func (ud *uiaDriver) Wait(condition Condition) error {
return ud.WaitWithTimeoutAndInterval(condition, DefaultWaitTimeout, DefaultWaitInterval)
}
func (ud *uiaDriver) StartCaptureLog(identifier ...string) (err error) {
log.Info().Msg("start adb log recording")
err = ud.logcat.CatchLogcat()
return
}
func (ud *uiaDriver) StopCaptureLog() (result interface{}, err error) {
log.Info().Msg("stop adb log recording")
err = ud.logcat.Stop()
if err != nil {
log.Error().Err(err).Msg("failed to get adb log recording")
return "", err
}
content := ud.logcat.logBuffer.String()
return ConvertPoints(content), nil
}

View File

@@ -0,0 +1,312 @@
package uixt
import (
"bytes"
"encoding/base64"
"encoding/json"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
)
var errElementNotImplemented = errors.New("element method not implemented")
type uiaElement struct {
parent *uiaDriver
id string
}
func (ue uiaElement) Click() (err error) {
// register(postHandler, new Click("/wd/hub/session/:sessionId/element/:id/click"))
_, err = ue.parent.httpPOST(nil, "/session", ue.parent.sessionId, "/element", ue.id, "/click")
return
}
func (ue uiaElement) SendKeys(text string, options ...DataOption) (err error) {
// register(postHandler, new SendKeysToElement("/wd/hub/session/:sessionId/element/:id/value"))
// https://github.com/appium/appium-uiutomator2-server/blob/master/app/src/main/java/io/appium/uiutomator2/handler/SendKeysToElement.java#L76-L85
data := map[string]interface{}{
"text": text,
}
// append options in post data for extra uiautomator configurations
for _, option := range options {
option(data)
}
if _, ok := data["isReplace"]; !ok {
data["isReplace"] = true // default true
}
_, err = ue.parent.httpPOST(data, "/session", ue.parent.sessionId, "/element", ue.id, "/value")
return
}
func (ue uiaElement) Clear() (err error) {
// TODO
return errElementNotImplemented
}
func (ue uiaElement) Tap(x, y int) (err error) {
// TODO
return errElementNotImplemented
}
func (ue uiaElement) TapFloat(x, y float64) (err error) {
// TODO
return errElementNotImplemented
}
func (ue uiaElement) DoubleTap() (err error) {
// TODO
return errElementNotImplemented
}
func (ue uiaElement) TouchAndHold(second ...float64) (err error) {
// TODO
return errElementNotImplemented
}
func (ue uiaElement) TwoFingerTap() (err error) {
// TODO
return errElementNotImplemented
}
func (ue uiaElement) TapWithNumberOfTaps(numberOfTaps, numberOfTouches int) (err error) {
// Todo: implement
log.Fatal().Msg("not support")
return
}
func (ue uiaElement) ForceTouch(pressure float64, second ...float64) (err error) {
// TODO
return errElementNotImplemented
}
func (ue uiaElement) ForceTouchFloat(x, y, pressure float64, second ...float64) (err error) {
// TODO
return errElementNotImplemented
}
func (ue uiaElement) Drag(fromX, fromY, toX, toY int, steps ...float64) (err error) {
return ue.DragFloat(float64(fromX), float64(fromY), float64(toX), float64(toY), steps...)
}
func (ue uiaElement) DragFloat(fromX, fromY, toX, toY float64, steps ...float64) (err error) {
if len(steps) == 0 {
steps = []float64{12 * 10}
} else {
steps[0] = 12 * 10
}
data := map[string]interface{}{
"elementId": ue.id,
"endX": toX,
"endY": toY,
"steps": steps[0],
}
return ue.parent._drag(data)
}
func (ue uiaElement) Swipe(fromX, fromY, toX, toY int) error {
return ue.SwipeFloat(float64(fromX), float64(fromY), float64(toX), float64(toY))
}
func (ue uiaElement) SwipeFloat(fromX, fromY, toX, toY float64) error {
options := []DataOption{
WithSteps(12),
WithCustomOption("elementId", ue.id),
}
return ue.parent._swipe(fromX, fromY, toX, toY, options...)
}
func (ue uiaElement) SwipeDirection(direction Direction, velocity ...float64) (err error) {
// TODO
return errElementNotImplemented
}
func (ue uiaElement) Pinch(scale, velocity float64) (err error) {
// TODO
return errElementNotImplemented
}
func (ue uiaElement) PinchToZoomOutByW3CAction(scale ...float64) (err error) {
// TODO
return errElementNotImplemented
}
func (ue uiaElement) Rotate(rotation float64, velocity ...float64) (err error) {
// TODO
return errElementNotImplemented
}
func (ue uiaElement) PickerWheelSelect(order PickerWheelOrder, offset ...int) (err error) {
// TODO
return errElementNotImplemented
}
func (ue uiaElement) scroll(data interface{}) (err error) {
// TODO
return errElementNotImplemented
}
func (ue uiaElement) ScrollElementByName(name string) (err error) {
// TODO
return errElementNotImplemented
}
func (ue uiaElement) ScrollElementByPredicate(predicate string) (err error) {
// TODO
return errElementNotImplemented
}
func (ue uiaElement) ScrollToVisible() (err error) {
// TODO
return errElementNotImplemented
}
func (ue uiaElement) ScrollDirection(direction Direction, distance ...float64) (err error) {
// TODO
return errElementNotImplemented
}
func (ue uiaElement) FindElement(by BySelector) (element WebElement, err error) {
method, selector := by.getMethodAndSelector()
return ue.parent._findElement(method, selector, ue.id)
}
func (ue uiaElement) FindElements(by BySelector) (elements []WebElement, err error) {
method, selector := by.getMethodAndSelector()
return ue.parent._findElements(method, selector, ue.id)
}
func (ue uiaElement) FindVisibleCells() (elements []WebElement, err error) {
// TODO
return elements, errElementNotImplemented
}
func (ue uiaElement) Rect() (rect Rect, err error) {
// register(getHandler, new GetRect("/wd/hub/session/:sessionId/element/:id/rect"))
var rawResp rawResponse
if rawResp, err = ue.parent.httpGET("/session", ue.parent.sessionId, "/element", ue.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 (ue uiaElement) Location() (point Point, err error) {
// register(getHandler, new Location("/wd/hub/session/:sessionId/element/:id/location"))
var rawResp rawResponse
if rawResp, err = ue.parent.httpGET("/session", ue.parent.sessionId, "/element", ue.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 (ue uiaElement) Size() (size Size, err error) {
// register(getHandler, new GetSize("/wd/hub/session/:sessionId/element/:id/size"))
var rawResp rawResponse
if rawResp, err = ue.parent.httpGET("/session", ue.parent.sessionId, "/element", ue.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 (ue uiaElement) Text() (text string, err error) {
// register(getHandler, new GetText("/wd/hub/session/:sessionId/element/:id/text"))
var rawResp rawResponse
if rawResp, err = ue.parent.httpGET("/session", ue.parent.sessionId, "/element", ue.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 (ue uiaElement) Type() (elemType string, err error) {
// TODO
return elemType, errElementNotImplemented
}
func (ue uiaElement) IsEnabled() (enabled bool, err error) {
// TODO
return enabled, errElementNotImplemented
}
func (ue uiaElement) IsDisplayed() (displayed bool, err error) {
// TODO
return displayed, errElementNotImplemented
}
func (ue uiaElement) IsSelected() (selected bool, err error) {
// TODO
return selected, errElementNotImplemented
}
func (ue uiaElement) IsAccessible() (accessible bool, err error) {
// TODO
return accessible, errElementNotImplemented
}
func (ue uiaElement) IsAccessibilityContainer() (isAccessibilityContainer bool, err error) {
// TODO
return isAccessibilityContainer, errElementNotImplemented
}
func (ue uiaElement) GetAttribute(attr ElementAttribute) (value string, err error) {
// register(getHandler, new GetElementAttribute("/wd/hub/session/:sessionId/element/:id/attribute/:name"))
var rawResp rawResponse
if rawResp, err = ue.parent.httpGET("/session", ue.parent.sessionId, "/element", ue.id, "/attribute", attr.getAttributeName()); err != nil {
return "", err
}
reply := new(struct{ Value string })
if err = json.Unmarshal(rawResp, reply); err != nil {
return "", err
}
value = reply.Value
return
}
func (ue uiaElement) UID() (uid string) {
return ue.id
}
func (ue 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 = ue.parent.httpGET("/session", ue.parent.sessionId, "/element", ue.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
}

879
hrp/pkg/uixt/android_key.go Normal file
View File

@@ -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
)

1307
hrp/pkg/uixt/android_test.go Normal file

File diff suppressed because it is too large Load Diff

104
hrp/pkg/uixt/client.go Normal file
View File

@@ -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 driver agent")
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 driver agent 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,
}
}

30
hrp/pkg/uixt/drag.go Normal file
View File

@@ -0,0 +1,30 @@
package uixt
func (dExt *DriverExt) Drag(pathname string, toX, toY int, pressForDuration ...float64) (err error) {
return dExt.DragFloat(pathname, float64(toX), float64(toY), pressForDuration...)
}
func (dExt *DriverExt) DragFloat(pathname string, toX, toY float64, pressForDuration ...float64) (err error) {
return dExt.DragOffsetFloat(pathname, toX, toY, 0.5, 0.5, pressForDuration...)
}
func (dExt *DriverExt) DragOffset(pathname string, toX, toY int, xOffset, yOffset float64, pressForDuration ...float64) (err error) {
return dExt.DragOffsetFloat(pathname, float64(toX), float64(toY), xOffset, yOffset, pressForDuration...)
}
func (dExt *DriverExt) DragOffsetFloat(pathname string, toX, toY, xOffset, yOffset float64, pressForDuration ...float64) (err error) {
if len(pressForDuration) == 0 {
pressForDuration = []float64{1.0}
}
var x, y, width, height float64
if x, y, width, height, err = dExt.FindUIRectInUIKit(pathname); err != nil {
return err
}
fromX := x + width*xOffset
fromY := y + height*yOffset
return dExt.Driver.DragFloat(fromX, fromY, toX, toY,
WithPressDuration(pressForDuration[0]))
}

20
hrp/pkg/uixt/drag_test.go Normal file
View File

@@ -0,0 +1,20 @@
//go:build localtest
package uixt
import (
"testing"
)
func TestDriverExt_Drag(t *testing.T) {
driverExt, err := InitWDAClient(nil)
checkErr(t, err)
pathSearch := "/Users/hero/Documents/temp/2020-05/opencv/IMG_map.png"
// err = driverExt.Drag(pathSearch, 300, 500, 2)
// checkErr(t, err)
err = driverExt.DragOffset(pathSearch, 300, 500, 2.1, 0.5, 2)
checkErr(t, err)
}

593
hrp/pkg/uixt/ext.go Normal file
View File

@@ -0,0 +1,593 @@
package uixt
import (
"bytes"
"encoding/json"
"fmt"
"image"
"image/jpeg"
"image/png"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
)
type MobileMethod string
const (
AppInstall MobileMethod = "install"
AppUninstall MobileMethod = "uninstall"
AppStart MobileMethod = "app_start"
AppLaunch MobileMethod = "app_launch" // 等待 app 打开并堵塞到 app 首屏加载完成,可以传入 app 的启动参数、环境变量
AppLaunchUnattached MobileMethod = "app_launch_unattached" // 只负责通知打开 app不堵塞等待不可传入启动参数
AppTerminate MobileMethod = "app_terminate"
AppStop MobileMethod = "app_stop"
CtlScreenShot MobileMethod = "screenshot"
CtlSleep MobileMethod = "sleep"
CtlStartCamera MobileMethod = "camera_start" // alias for app_launch camera
CtlStopCamera MobileMethod = "camera_stop" // alias for app_terminate camera
RecordStart MobileMethod = "record_start"
RecordStop MobileMethod = "record_stop"
// UI validation
SelectorName string = "ui_name"
SelectorLabel string = "ui_label"
SelectorOCR string = "ui_ocr"
SelectorImage string = "ui_image"
AssertionExists string = "exists"
AssertionNotExists string = "not_exists"
// UI handling
ACTION_Home MobileMethod = "home"
ACTION_TapXY MobileMethod = "tap_xy"
ACTION_TapAbsXY MobileMethod = "tap_abs_xy"
ACTION_TapByOCR MobileMethod = "tap_ocr"
ACTION_TapByCV MobileMethod = "tap_cv"
ACTION_Tap MobileMethod = "tap"
ACTION_DoubleTapXY MobileMethod = "double_tap_xy"
ACTION_DoubleTap MobileMethod = "double_tap"
ACTION_Swipe MobileMethod = "swipe"
ACTION_Input MobileMethod = "input"
// custom actions
ACTION_SwipeToTapApp MobileMethod = "swipe_to_tap_app" // swipe left & right to find app and tap
ACTION_SwipeToTapText MobileMethod = "swipe_to_tap_text" // swipe up & down to find text and tap
)
type MobileAction struct {
Method MobileMethod `json:"method,omitempty" yaml:"method,omitempty"`
Params interface{} `json:"params,omitempty" yaml:"params,omitempty"`
Identifier string `json:"identifier,omitempty" yaml:"identifier,omitempty"` // used to identify the action in log
MaxRetryTimes int `json:"max_retry_times,omitempty" yaml:"max_retry_times,omitempty"` // max retry times
Index int `json:"index,omitempty" yaml:"index,omitempty"` // index of the target element, should start from 1
Timeout int `json:"timeout,omitempty" yaml:"timeout,omitempty"` // TODO: wait timeout in seconds for mobile action
IgnoreNotFoundError bool `json:"ignore_NotFoundError,omitempty" yaml:"ignore_NotFoundError,omitempty"` // ignore error if target element not found
Text string `json:"text,omitempty" yaml:"text,omitempty"`
ID string `json:"id,omitempty" yaml:"id,omitempty"`
Description string `json:"description,omitempty" yaml:"description,omitempty"`
}
type ActionOption func(o *MobileAction)
func WithIdentifier(identifier string) ActionOption {
return func(o *MobileAction) {
o.Identifier = identifier
}
}
func WithIndex(index int) ActionOption {
return func(o *MobileAction) {
o.Index = index
}
}
func WithText(text string) ActionOption {
return func(o *MobileAction) {
o.Text = text
}
}
func WithID(id string) ActionOption {
return func(o *MobileAction) {
o.ID = id
}
}
func WithDescription(description string) ActionOption {
return func(o *MobileAction) {
o.Description = description
}
}
func WithMaxRetryTimes(maxRetryTimes int) ActionOption {
return func(o *MobileAction) {
o.MaxRetryTimes = maxRetryTimes
}
}
func WithTimeout(timeout int) ActionOption {
return func(o *MobileAction) {
o.Timeout = timeout
}
}
func WithIgnoreNotFoundError(ignoreError bool) ActionOption {
return func(o *MobileAction) {
o.IgnoreNotFoundError = ignoreError
}
}
// TemplateMatchMode is the type of the template matching operation.
type TemplateMatchMode int
type CVArgs struct {
matchMode TemplateMatchMode
threshold float64
}
type CVOption func(*CVArgs)
func WithTemplateMatchMode(mode TemplateMatchMode) CVOption {
return func(args *CVArgs) {
args.matchMode = mode
}
}
func WithThreshold(threshold float64) CVOption {
return func(args *CVArgs) {
args.threshold = threshold
}
}
type DriverExt struct {
UUID string // ios udid or android serial
Driver WebDriver
windowSize Size
frame *bytes.Buffer
doneMjpegStream chan bool
scale float64
StartTime time.Time // used to associate screenshots name
ScreenShots []string // save screenshots path
perfStop chan struct{} // stop performance monitor
perfData []string // save perf data
CVArgs
}
func extend(driver WebDriver) (dExt *DriverExt, err error) {
dExt = &DriverExt{Driver: driver}
dExt.doneMjpegStream = make(chan bool, 1)
// get device window size
dExt.windowSize, err = dExt.Driver.WindowSize()
if err != nil {
return nil, errors.Wrap(err, "failed to get windows size")
}
if dExt.scale, err = dExt.Driver.Scale(); err != nil {
return nil, err
}
return dExt, nil
}
func (dExt *DriverExt) GetPerfData() []string {
if dExt.perfStop == nil {
return nil
}
close(dExt.perfStop)
return dExt.perfData
}
func (dExt *DriverExt) takeScreenShot() (raw *bytes.Buffer, err error) {
// wait for action done
time.Sleep(500 * time.Millisecond)
// 优先使用 MJPEG 流进行截图,性能最优
// 如果 MJPEG 流未开启,则使用 WebDriver 的截图接口
if dExt.frame != nil {
return dExt.frame, nil
}
if raw, err = dExt.Driver.Screenshot(); err != nil {
log.Error().Err(err).Msg("takeScreenShot failed")
return nil, err
}
return raw, nil
}
// saveScreenShot saves image file to $CWD/screenshots/ folder
func (dExt *DriverExt) saveScreenShot(raw *bytes.Buffer, fileName string) (string, error) {
img, format, err := image.Decode(raw)
if err != nil {
return "", errors.Wrap(err, "decode screenshot image failed")
}
dir, _ := os.Getwd()
screenshotsDir := filepath.Join(dir, "screenshots")
if err = os.MkdirAll(screenshotsDir, os.ModePerm); err != nil {
return "", errors.Wrap(err, "create screenshots directory failed")
}
screenshotPath := filepath.Join(screenshotsDir,
fmt.Sprintf("%s.%s", fileName, format))
file, err := os.Create(screenshotPath)
if err != nil {
return "", errors.Wrap(err, "create screenshot image file failed")
}
defer func() {
_ = file.Close()
}()
switch format {
case "png":
err = png.Encode(file, img)
case "jpeg":
err = jpeg.Encode(file, img, nil)
default:
return "", fmt.Errorf("unsupported image format: %s", format)
}
if err != nil {
return "", errors.Wrap(err, "encode screenshot image failed")
}
return screenshotPath, nil
}
// ScreenShot takes screenshot and saves image file to $CWD/screenshots/ folder
func (dExt *DriverExt) ScreenShot(fileName string) (string, error) {
raw, err := dExt.takeScreenShot()
if err != nil {
return "", errors.Wrap(err, "screenshot by WDA failed")
}
path, err := dExt.saveScreenShot(raw, fileName)
if err != nil {
return "", errors.Wrap(err, "save screenshot failed")
}
return path, nil
}
// isPathExists returns true if path exists, whether path is file or dir
func isPathExists(path string) bool {
if _, err := os.Stat(path); os.IsNotExist(err) {
return false
}
return true
}
func (dExt *DriverExt) FindUIElement(param string) (ele WebElement, err error) {
var selector BySelector
if strings.HasPrefix(param, "/") {
// xpath
selector = BySelector{
XPath: param,
}
} else if strings.HasPrefix(param, "com.") {
// name
selector = BySelector{
ResourceIdID: param,
}
} else {
// name
selector = BySelector{
LinkText: NewElementAttribute().WithName(param),
}
}
return dExt.Driver.FindElement(selector)
}
func (dExt *DriverExt) FindUIRectInUIKit(search string, index ...int) (x, y, width, height float64, err error) {
// click on text, using OCR
if !isPathExists(search) {
return dExt.FindTextByOCR(search, index...)
}
// click on image, using opencv
return dExt.FindImageRectInUIKit(search, index...)
}
func (dExt *DriverExt) MappingToRectInUIKit(rect image.Rectangle) (x, y, width, height float64) {
x, y = float64(rect.Min.X)/dExt.scale, float64(rect.Min.Y)/dExt.scale
width, height = float64(rect.Dx())/dExt.scale, float64(rect.Dy())/dExt.scale
return
}
func (dExt *DriverExt) PerformTouchActions(touchActions *TouchActions) error {
return dExt.Driver.PerformAppiumTouchActions(touchActions)
}
func (dExt *DriverExt) PerformActions(actions *W3CActions) error {
return dExt.Driver.PerformW3CActions(actions)
}
func (dExt *DriverExt) IsNameExist(name string) bool {
selector := BySelector{
LinkText: NewElementAttribute().WithName(name),
}
_, err := dExt.Driver.FindElement(selector)
return err == nil
}
func (dExt *DriverExt) IsLabelExist(label string) bool {
selector := BySelector{
LinkText: NewElementAttribute().WithLabel(label),
}
_, err := dExt.Driver.FindElement(selector)
return err == nil
}
func (dExt *DriverExt) IsOCRExist(text string) bool {
_, _, _, _, err := dExt.FindTextByOCR(text)
return err == nil
}
func (dExt *DriverExt) IsImageExist(text string) bool {
_, _, _, _, err := dExt.FindImageRectInUIKit(text)
return err == nil
}
var errActionNotImplemented = errors.New("UI action not implemented")
func (dExt *DriverExt) DoAction(action MobileAction) error {
log.Info().Str("method", string(action.Method)).Interface("params", action.Params).Msg("start UI action")
switch action.Method {
case AppInstall:
// TODO
return errActionNotImplemented
case AppLaunch:
if bundleId, ok := action.Params.(string); ok {
return dExt.Driver.AppLaunch(bundleId)
}
return fmt.Errorf("invalid %s params, should be bundleId(string), got %v",
AppLaunch, action.Params)
case AppLaunchUnattached:
if bundleId, ok := action.Params.(string); ok {
return dExt.Driver.AppLaunchUnattached(bundleId)
}
return fmt.Errorf("invalid %s params, should be bundleId(string), got %v",
AppLaunchUnattached, action.Params)
case ACTION_SwipeToTapApp:
if appName, ok := action.Params.(string); ok {
var point PointF
findApp := func(d *DriverExt) error {
var err error
point, err = d.GetTextXY(appName, action.Index)
return err
}
foundAppAction := func(d *DriverExt) error {
// click app to launch
return d.TapAbsXY(point.X, point.Y-20, action.Identifier)
}
// go to home screen
if err := dExt.Driver.Homescreen(); err != nil {
return errors.Wrap(err, "go to home screen failed")
}
// swipe to first screen
for i := 0; i < 5; i++ {
dExt.SwipeRight()
}
// default to retry 5 times
if action.MaxRetryTimes == 0 {
action.MaxRetryTimes = 5
}
// swipe next screen until app found
return dExt.SwipeUntil("left", findApp, foundAppAction, action.MaxRetryTimes)
}
return fmt.Errorf("invalid %s params, should be app name(string), got %v",
ACTION_SwipeToTapApp, action.Params)
case ACTION_SwipeToTapText:
if text, ok := action.Params.(string); ok {
var point PointF
findText := func(d *DriverExt) error {
var err error
point, err = d.GetTextXY(text, action.Index)
return err
}
foundTextAction := func(d *DriverExt) error {
// tap text
return d.TapAbsXY(point.X, point.Y, action.Identifier)
}
// default to retry 10 times
if action.MaxRetryTimes == 0 {
action.MaxRetryTimes = 10
}
// swipe until live room found
return dExt.SwipeUntil("up", findText, foundTextAction, action.MaxRetryTimes)
}
return fmt.Errorf("invalid %s params, should be app text(string), got %v",
ACTION_SwipeToTapText, action.Params)
case AppTerminate:
if bundleId, ok := action.Params.(string); ok {
success, err := dExt.Driver.AppTerminate(bundleId)
if err != nil {
return errors.Wrap(err, "failed to terminate app")
}
if !success {
log.Warn().Str("bundleId", bundleId).Msg("app was not running")
}
return nil
}
return fmt.Errorf("app_terminate params should be bundleId(string), got %v", action.Params)
case ACTION_Home:
return dExt.Driver.Homescreen()
case ACTION_TapXY:
if location, ok := action.Params.([]interface{}); ok {
// relative x,y of window size: [0.5, 0.5]
if len(location) != 2 {
return fmt.Errorf("invalid tap location params: %v", location)
}
x, _ := location[0].(float64)
y, _ := location[1].(float64)
return dExt.TapXY(x, y, action.Identifier)
}
return fmt.Errorf("invalid %s params: %v", ACTION_TapXY, action.Params)
case ACTION_TapAbsXY:
if location, ok := action.Params.([]interface{}); ok {
// absolute coordinates x,y of window size: [100, 300]
if len(location) != 2 {
return fmt.Errorf("invalid tap location params: %v", location)
}
x, _ := location[0].(float64)
y, _ := location[1].(float64)
return dExt.TapAbsXY(x, y, action.Identifier)
}
return fmt.Errorf("invalid %s params: %v", ACTION_TapAbsXY, action.Params)
case ACTION_Tap:
if param, ok := action.Params.(string); ok {
return dExt.Tap(param, action.Identifier, action.IgnoreNotFoundError, action.Index)
}
return fmt.Errorf("invalid %s params: %v", ACTION_Tap, action.Params)
case ACTION_TapByOCR:
if ocrText, ok := action.Params.(string); ok {
return dExt.TapByOCR(ocrText, action.Identifier, action.IgnoreNotFoundError, action.Index)
}
return fmt.Errorf("invalid %s params: %v", ACTION_TapByOCR, action.Params)
case ACTION_TapByCV:
if imagePath, ok := action.Params.(string); ok {
return dExt.TapByCV(imagePath, action.Identifier, action.IgnoreNotFoundError, action.Index)
}
return fmt.Errorf("invalid %s params: %v", ACTION_TapByCV, action.Params)
case ACTION_DoubleTapXY:
if location, ok := action.Params.([]interface{}); ok {
// relative x,y of window size: [0.5, 0.5]
if len(location) != 2 {
return fmt.Errorf("invalid tap location params: %v", location)
}
x, _ := location[0].(float64)
y, _ := location[1].(float64)
return dExt.DoubleTapXY(x, y)
}
return fmt.Errorf("invalid %s params: %v", ACTION_DoubleTapXY, action.Params)
case ACTION_DoubleTap:
if param, ok := action.Params.(string); ok {
return dExt.DoubleTap(param)
}
return fmt.Errorf("invalid %s params: %v", ACTION_DoubleTap, action.Params)
case ACTION_Swipe:
if positions, ok := action.Params.([]interface{}); ok {
// relative fromX, fromY, toX, toY of window size: [0.5, 0.9, 0.5, 0.1]
if len(positions) != 4 {
return fmt.Errorf("invalid swipe params [fromX, fromY, toX, toY]: %v", positions)
}
fromX, _ := positions[0].(float64)
fromY, _ := positions[1].(float64)
toX, _ := positions[2].(float64)
toY, _ := positions[3].(float64)
return dExt.SwipeRelative(fromX, fromY, toX, toY, action.Identifier)
}
if direction, ok := action.Params.(string); ok {
return dExt.SwipeTo(direction, action.Identifier)
}
return fmt.Errorf("invalid %s params: %v", ACTION_Swipe, action.Params)
case ACTION_Input:
// input text on current active element
// append \n to send text with enter
// send \b\b\b to delete 3 chars
param := fmt.Sprintf("%v", action.Params)
options := []DataOption{}
if action.Text != "" {
options = append(options, WithCustomOption("text", action.Text))
}
if action.ID != "" {
options = append(options, WithCustomOption("id", action.ID))
}
if action.Description != "" {
options = append(options, WithCustomOption("description", action.Description))
}
if action.Identifier != "" {
options = append(options, WithCustomOption("log", map[string]interface{}{
"enable": true,
"data": action.Identifier,
}))
}
return dExt.Driver.Input(param, options...)
case CtlSleep:
if param, ok := action.Params.(json.Number); ok {
seconds, _ := param.Float64()
time.Sleep(time.Duration(seconds*1000) * time.Millisecond)
return nil
} else if param, ok := action.Params.(float64); ok {
time.Sleep(time.Duration(param*1000) * time.Millisecond)
return nil
} else if param, ok := action.Params.(int64); ok {
time.Sleep(time.Duration(param) * time.Second)
return nil
}
return fmt.Errorf("invalid sleep params: %v(%T)", action.Params, action.Params)
case CtlScreenShot:
// take snapshot
log.Info().Msg("take snapshot for current screen")
screenshotPath, err := dExt.ScreenShot(fmt.Sprintf("%d_screenshot_%d",
dExt.StartTime.Unix(), time.Now().Unix()))
if err != nil {
return errors.Wrap(err, "take screenshot failed")
}
log.Info().Str("path", screenshotPath).Msg("take screenshot")
dExt.ScreenShots = append(dExt.ScreenShots, screenshotPath)
return err
case CtlStartCamera:
return dExt.Driver.StartCamera()
case CtlStopCamera:
return dExt.Driver.StopCamera()
}
return nil
}
func (dExt *DriverExt) DoValidation(check, assert, expected string, message ...string) bool {
var exists bool
if assert == AssertionExists {
exists = true
} else {
exists = false
}
var result bool
switch check {
case SelectorName:
result = (dExt.IsNameExist(expected) == exists)
case SelectorLabel:
result = (dExt.IsLabelExist(expected) == exists)
case SelectorOCR:
result = (dExt.IsOCRExist(expected) == exists)
case SelectorImage:
result = (dExt.IsImageExist(expected) == exists)
}
if !result {
if message == nil {
message = []string{""}
}
log.Error().
Str("assert", assert).
Str("expect", expected).
Str("msg", message[0]).
Msg("validate UI failed")
return false
}
log.Info().
Str("assert", assert).
Str("expect", expected).
Msg("validate UI success")
return true
}
func checkErr(t *testing.T, err error, msg ...string) {
if err != nil {
if len(msg) == 0 {
t.Fatal(err)
} else {
t.Fatal(msg, err)
}
}
}

44
hrp/pkg/uixt/gesture.go Normal file
View File

@@ -0,0 +1,44 @@
//go:build opencv
package uixt
import (
"image"
"sort"
)
func (dExt *DriverExt) GesturePassword(pathname string, password ...int) (err error) {
var rects []image.Rectangle
if rects, err = dExt.FindAllImageRect(pathname); err != nil {
return err
}
sort.Slice(rects, func(i, j int) bool {
if rects[i].Min.Y < rects[j].Min.Y {
return true
} else if rects[i].Min.Y == rects[j].Min.Y {
if rects[i].Min.X < rects[j].Min.X {
return true
}
}
return false
})
touchActions := NewTouchActions(len(password)*2 + 1)
for i := range password {
x, y, width, height := dExt.MappingToRectInUIKit(rects[password[i]])
x = x + width*0.5
y = y + height*0.5
if i == 0 {
touchActions.Press(NewTouchActionPress().WithXYFloat(x, y)).
Wait(0.2)
} else {
touchActions.MoveTo(NewTouchActionMoveTo().WithXYFloat(x, y)).
Wait(0.2)
}
}
touchActions.Release()
return dExt.PerformTouchActions(touchActions)
}

View File

@@ -0,0 +1,25 @@
//go:build opencv
package uixt
import (
"strconv"
"strings"
"testing"
)
func TestDriverExt_GesturePassword(t *testing.T) {
split := strings.Split("6304258", "")
password := make([]int, len(split))
for i := range split {
password[i], _ = strconv.Atoi(split[i])
}
driverExt, err := InitWDAClient(nil)
checkErr(t, err)
pathSearch := "/Users/hero/Documents/temp/2020-05/opencv/IMG_5.png"
err = driverExt.GesturePassword(pathSearch, password...)
checkErr(t, err)
}

1094
hrp/pkg/uixt/interface.go Normal file

File diff suppressed because it is too large Load Diff

373
hrp/pkg/uixt/ios_action.go Normal file
View File

@@ -0,0 +1,373 @@
package uixt
import (
"strconv"
"strings"
)
type W3CActions []map[string]interface{}
func NewW3CActions(capacity ...int) *W3CActions {
if len(capacity) == 0 || capacity[0] <= 0 {
capacity = []int{8}
}
tmp := make(W3CActions, 0, capacity[0])
return &tmp
}
func (act *W3CActions) SendKeys(text string) *W3CActions {
keyboard := make(map[string]interface{})
keyboard["type"] = "key"
keyboard["id"] = "keyboard" + strconv.FormatInt(int64(len(*act)+1), 10)
ss := strings.Split(text, "")
type KeyEvent struct {
Type string `json:"type"`
Value string `json:"value"`
}
actOptKey := make([]KeyEvent, 0, len(ss)+1)
for i := range ss {
actOptKey = append(
actOptKey,
KeyEvent{Type: "keyDown", Value: ss[i]},
KeyEvent{Type: "keyUp", Value: ss[i]},
)
}
keyboard["actions"] = actOptKey
*act = append(*act, keyboard)
return act
}
func (act *W3CActions) _newFinger() map[string]interface{} {
pointer := make(map[string]interface{})
pointer["type"] = "pointer"
pointer["id"] = "finger" + strconv.FormatInt(int64(len(*act)+1), 10)
pointer["parameters"] = map[string]string{"pointerType": "touch"}
return pointer
}
func (act *W3CActions) FingerAction(fingerAct *FingerAction, fActs ...*FingerAction) *W3CActions {
fActs = append([]*FingerAction{fingerAct}, fActs...)
for i := range fActs {
pointer := act._newFinger()
pointer["actions"] = *fActs[i]
*act = append(*act, pointer)
}
return act
}
type FingerAction []map[string]interface{}
func NewFingerAction(capacity ...int) *FingerAction {
if len(capacity) == 0 || capacity[0] <= 0 {
capacity = []int{8}
}
tmp := make(FingerAction, 0, capacity[0])
return &tmp
}
type FingerMove map[string]interface{}
func NewFingerMove() FingerMove {
return map[string]interface{}{"type": "pointerMove"}
}
func (fm FingerMove) WithXY(x, y int) FingerMove {
fm["x"] = x
fm["y"] = y
return fm
}
func (fm FingerMove) WithXYFloat(x, y float64) FingerMove {
fm["x"] = x
fm["y"] = y
return fm
}
func (fm FingerMove) WithOrigin(element WebElement) FingerMove {
fm["origin"] = element.UID()
return fm
}
func (fm FingerMove) WithDuration(second float64) FingerMove {
fm["duration"] = second
return fm
}
func (fa *FingerAction) Move(fm FingerMove) *FingerAction {
*fa = append(*fa, fm)
return fa
}
func (fa *FingerAction) Down() *FingerAction {
*fa = append(*fa, map[string]interface{}{"type": "pointerDown"})
return fa
}
func (fa *FingerAction) Up() *FingerAction {
*fa = append(*fa, map[string]interface{}{"type": "pointerUp"})
return fa
}
func (fa *FingerAction) Pause(second ...float64) *FingerAction {
if len(second) == 0 || second[0] < 0 {
second = []float64{0.5}
}
tmp := map[string]interface{}{
"type": "pause",
"duration": second[0] * 1000,
}
*fa = append(*fa, tmp)
return fa
}
func (act *W3CActions) Tap(x, y int, element ...WebElement) *W3CActions {
fm := NewFingerMove().WithXY(x, y)
if len(element) != 0 {
fm.WithOrigin(element[0])
}
fingerAction := NewFingerAction().
Move(fm).
Down().
Pause(0.1).
Up()
return act.FingerAction(fingerAction)
}
func (act *W3CActions) DoubleTap(x, y int, element ...WebElement) *W3CActions {
fm := NewFingerMove().WithXY(x, y)
if len(element) != 0 {
fm.WithOrigin(element[0])
}
fingerAction := NewFingerAction().
Move(fm).
Down().
Pause(0.1).
Up().
Pause(0.04).
Down().
Pause(0.1).
Up()
return act.FingerAction(fingerAction)
}
func (act *W3CActions) Press(x, y int, second float64, element ...WebElement) *W3CActions {
fm := NewFingerMove().WithXY(x, y)
if len(element) != 0 {
fm.WithOrigin(element[0])
}
fingerAction := NewFingerAction().
Move(fm).
Down().
Pause(second).
Up()
return act.FingerAction(fingerAction)
}
func (act *W3CActions) Swipe(fromX, fromY, toX, toY int, element ...WebElement) *W3CActions {
fmFrom := NewFingerMove().WithXY(fromX, fromY)
fmTo := NewFingerMove().WithXY(toX, toY)
if len(element) != 0 {
fmFrom.WithOrigin(element[0])
fmTo.WithOrigin(element[0])
}
fingerAction := NewFingerAction().
Move(fmFrom).
Down().
Pause(0.25).
Move(fmTo).
Pause(0.25).
Up()
return act.FingerAction(fingerAction)
}
func (act *W3CActions) SwipeFloat(fromX, fromY, toX, toY float64, element ...WebElement) *W3CActions {
fmFrom := NewFingerMove().WithXYFloat(fromX, fromY)
fmTo := NewFingerMove().WithXYFloat(toX, toY)
if len(element) != 0 {
fmFrom.WithOrigin(element[0])
fmTo.WithOrigin(element[0])
}
fingerAction := NewFingerAction().
Move(fmFrom).
Down().
Pause(0.25).
Move(fmTo).
Pause(0.25).
Up()
return act.FingerAction(fingerAction)
}
/* ---------------------------------------------------------------------------------------------------------------- */
type TouchActions []map[string]interface{}
func NewTouchActions(capacity ...int) *TouchActions {
if len(capacity) == 0 || capacity[0] <= 0 {
capacity = []int{8}
}
tmp := make(TouchActions, 0, capacity[0])
return &tmp
}
func (act *TouchActions) MoveTo(opt TouchActionMoveTo) *TouchActions {
tmp := map[string]interface{}{
"action": "moveTo",
"options": opt,
}
*act = append(*act, tmp)
return act
}
func (act *TouchActions) Tap(opt TouchActionTap) *TouchActions {
tmp := map[string]interface{}{
"action": "tap",
"options": opt,
}
*act = append(*act, tmp)
return act
}
func (act *TouchActions) Press(opt TouchActionPress) *TouchActions {
tmp := map[string]interface{}{
"action": "press",
"options": opt,
}
*act = append(*act, tmp)
return act
}
func (act *TouchActions) LongPress(opt TouchActionLongPress) *TouchActions {
tmp := map[string]interface{}{
"action": "longPress",
"options": opt,
}
*act = append(*act, tmp)
return act
}
func (act *TouchActions) Wait(second ...float64) *TouchActions {
if len(second) == 0 || second[0] < 0 {
second = []float64{0.5}
}
tmp := map[string]interface{}{
"action": "wait",
"options": map[string]interface{}{"ms": second[0] * 1000},
}
*act = append(*act, tmp)
return act
}
func (act *TouchActions) Release() *TouchActions {
tmp := map[string]interface{}{"action": "release"}
*act = append(*act, tmp)
return act
}
func (act *TouchActions) Cancel() *TouchActions {
tmp := map[string]interface{}{"action": "cancel"}
*act = append(*act, tmp)
return act
}
type TouchActionMoveTo map[string]interface{}
func NewTouchActionMoveTo() TouchActionMoveTo {
return make(map[string]interface{})
}
func (opt TouchActionMoveTo) WithXY(x, y int) TouchActionMoveTo {
opt["x"] = x
opt["y"] = y
return opt
}
func (opt TouchActionMoveTo) WithXYFloat(x, y float64) TouchActionMoveTo {
opt["x"] = x
opt["y"] = y
return opt
}
func (opt TouchActionMoveTo) WithElement(element WebElement) TouchActionMoveTo {
opt["element"] = element.UID()
return opt
}
type TouchActionTap map[string]interface{}
func NewTouchActionTap() TouchActionTap {
return make(map[string]interface{})
}
func (opt TouchActionTap) WithXY(x, y int) TouchActionTap {
opt["x"] = x
opt["y"] = y
return opt
}
func (opt TouchActionTap) WithXYFloat(x, y float64) TouchActionTap {
opt["x"] = x
opt["y"] = y
return opt
}
func (opt TouchActionTap) WithElement(element WebElement) TouchActionTap {
opt["element"] = element.UID()
return opt
}
func (opt TouchActionTap) WithCount(count int) TouchActionTap {
opt["count"] = count
return opt
}
type TouchActionPress map[string]interface{}
func NewTouchActionPress() TouchActionPress {
return make(map[string]interface{})
}
func (opt TouchActionPress) WithXY(x, y int) TouchActionPress {
opt["x"] = x
opt["y"] = y
return opt
}
func (opt TouchActionPress) WithXYFloat(x, y float64) TouchActionPress {
opt["x"] = x
opt["y"] = y
return opt
}
func (opt TouchActionPress) WithElement(element WebElement) TouchActionPress {
opt["element"] = element.UID()
return opt
}
func (opt TouchActionPress) WithPressure(pressure float64) TouchActionPress {
opt["pressure"] = pressure
return opt
}
type TouchActionLongPress map[string]interface{}
func NewTouchActionLongPress() TouchActionLongPress {
return make(map[string]interface{})
}
func (opt TouchActionLongPress) WithXY(x, y int) TouchActionLongPress {
opt["x"] = x
opt["y"] = y
return opt
}
func (opt TouchActionLongPress) WithXYFloat(x, y float64) TouchActionLongPress {
opt["x"] = x
opt["y"] = y
return opt
}
func (opt TouchActionLongPress) WithElement(element WebElement) TouchActionLongPress {
opt["element"] = element.UID()
return opt
}

560
hrp/pkg/uixt/ios_device.go Normal file
View File

@@ -0,0 +1,560 @@
package uixt
import (
"bytes"
"encoding/base64"
builtinJSON "encoding/json"
"fmt"
"io"
"mime"
"mime/multipart"
"net"
"net/http"
"net/url"
"os"
"regexp"
"strings"
"time"
giDevice "github.com/electricbubble/gidevice"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v4/hrp/internal/json"
)
const (
// Changes the value of maximum depth for traversing elements source tree.
// It may help to prevent out of memory or timeout errors while getting the elements source tree,
// but it might restrict the depth of source tree.
// A part of elements source tree might be lost if the value was too small. Defaults to 50
snapshotMaxDepth = 10
// Allows to customize accept/dismiss alert button selector.
// It helps you to handle an arbitrary element as accept button in accept alert command.
// The selector should be a valid class chain expression, where the search root is the alert element itself.
// The default button location algorithm is used if the provided selector is wrong or does not match any element.
// e.g. **/XCUIElementTypeButton[`label CONTAINS[c] accept`]
acceptAlertButtonSelector = "**/XCUIElementTypeButton[`label IN {'允许','好','仅在使用应用期间','稍后再说'}`]"
dismissAlertButtonSelector = "**/XCUIElementTypeButton[`label IN {'不允许','暂不'}`]"
)
const (
defaultWDAPort = 8100
defaultMjpegPort = 9100
)
func InitWDAClient(device *IOSDevice) (*DriverExt, error) {
// init wda device
iosDevice, err := NewIOSDevice(device.opitons()...)
if err != nil {
return nil, err
}
// init WDA driver
capabilities := NewCapabilities()
capabilities.WithDefaultAlertAction(AlertActionAccept)
var driver WebDriver
if os.Getenv("WDA_USB_DRIVER") == "" {
// default use http driver
driver, err = iosDevice.NewHTTPDriver(capabilities)
} else {
driver, err = iosDevice.NewUSBDriver(capabilities)
}
if err != nil {
return nil, errors.Wrap(err, "failed to init WDA driver")
}
// switch to iOS springboard before init WDA session
// aviod getting stuck when some super app is activate such as douyin or wexin
log.Info().Msg("go back to home screen")
if err = driver.Homescreen(); err != nil {
return nil, errors.Wrap(err, "failed to go back to home screen")
}
driverExt, err := Extend(driver)
if err != nil {
return nil, errors.Wrap(err, "failed to extend WebDriver")
}
settings, err := driverExt.Driver.SetAppiumSettings(map[string]interface{}{
"snapshotMaxDepth": snapshotMaxDepth,
"acceptAlertButtonSelector": acceptAlertButtonSelector,
})
if err != nil {
return nil, errors.Wrap(err, "failed to set appium WDA settings")
}
log.Info().Interface("appiumWDASettings", settings).Msg("set appium WDA settings")
if device.LogOn {
err = driverExt.Driver.StartCaptureLog("hrp_wda_log")
if err != nil {
return nil, err
}
}
if device.PerfOptions != nil {
data, err := iosDevice.d.PerfStart(device.perfOpitons()...)
if err != nil {
return nil, err
}
driverExt.perfStop = make(chan struct{})
// start performance monitor
go func() {
for {
select {
case <-driverExt.perfStop:
iosDevice.d.PerfStop()
return
case d := <-data:
fmt.Println(string(d))
driverExt.perfData = append(driverExt.perfData, string(d))
}
}
}()
}
driverExt.UUID = iosDevice.UUID()
return driverExt, nil
}
type IOSDeviceOption func(*IOSDevice)
func WithUDID(udid string) IOSDeviceOption {
return func(device *IOSDevice) {
device.UDID = udid
}
}
func WithWDAPort(port int) IOSDeviceOption {
return func(device *IOSDevice) {
device.Port = port
}
}
func WithWDAMjpegPort(port int) IOSDeviceOption {
return func(device *IOSDevice) {
device.MjpegPort = port
}
}
func WithLogOn(logOn bool) IOSDeviceOption {
return func(device *IOSDevice) {
device.LogOn = logOn
}
}
func WithPerfOptions(options ...giDevice.PerfOption) IOSDeviceOption {
return func(device *IOSDevice) {
device.PerfOptions = &giDevice.PerfOptions{}
for _, option := range options {
option(device.PerfOptions)
}
}
}
func IOSDevices(udid ...string) (devices []giDevice.Device, err error) {
var usbmux giDevice.Usbmux
if usbmux, err = giDevice.NewUsbmux(); err != nil {
return nil, fmt.Errorf("init usbmux failed: %v", err)
}
if devices, err = usbmux.Devices(); err != nil {
return nil, fmt.Errorf("list ios devices failed: %v", err)
}
// filter by udid
var deviceList []giDevice.Device
for _, d := range devices {
for _, u := range udid {
if u != "" && u != d.Properties().SerialNumber {
continue
}
deviceList = append(deviceList, d)
}
}
return deviceList, nil
}
func NewIOSDevice(options ...IOSDeviceOption) (device *IOSDevice, err error) {
device = &IOSDevice{
Port: defaultWDAPort,
MjpegPort: defaultMjpegPort,
}
for _, option := range options {
option(device)
}
deviceList, err := IOSDevices(device.UDID)
if err != nil {
return nil, err
}
if len(deviceList) > 0 {
device.UDID = deviceList[0].Properties().SerialNumber
log.Info().Str("udid", device.UDID).Msg("select device")
device.d = deviceList[0]
return device, nil
}
return nil, fmt.Errorf("device %s not found", device.UDID)
}
type IOSDevice struct {
d giDevice.Device
PerfOptions *giDevice.PerfOptions `json:"perf_options,omitempty" yaml:"perf_options,omitempty"`
UDID string `json:"udid,omitempty" yaml:"udid,omitempty"`
Port int `json:"port,omitempty" yaml:"port,omitempty"` // WDA remote port
MjpegPort int `json:"mjpeg_port,omitempty" yaml:"mjpeg_port,omitempty"` // WDA remote MJPEG port
LogOn bool `json:"log_on,omitempty" yaml:"log_on,omitempty"`
}
func (dev *IOSDevice) UUID() string {
return dev.UDID
}
func (dev *IOSDevice) forward(localPort, remotePort int) error {
log.Info().Int("localPort", localPort).Int("remotePort", remotePort).
Str("udid", dev.UDID).Msg("forward tcp port")
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", localPort))
if err != nil {
log.Error().Err(err).Msg("listen tcp error")
return err
}
go func(listener net.Listener, device giDevice.Device) {
for {
accept, err := listener.Accept()
if err != nil {
log.Error().Err(err).Msg("accept error")
continue
}
rInnerConn, err := device.NewConnect(remotePort)
if err != nil {
log.Error().Err(err).Msg("connect to device failed")
os.Exit(1)
}
rConn := rInnerConn.RawConn()
_ = rConn.SetDeadline(time.Time{})
go func(lConn net.Conn) {
go func(lConn, rConn net.Conn) {
if _, err := io.Copy(lConn, rConn); err != nil {
log.Error().Err(err).Msg("copy local -> remote")
}
}(lConn, rConn)
go func(lConn, rConn net.Conn) {
if _, err := io.Copy(rConn, lConn); err != nil {
log.Error().Err(err).Msg("copy local <- remote")
}
}(lConn, rConn)
}(accept)
}
}(listener, dev.d)
return nil
}
func (dev *IOSDevice) opitons() (deviceOptions []IOSDeviceOption) {
if dev.UDID != "" {
deviceOptions = append(deviceOptions, WithUDID(dev.UDID))
}
if dev.Port != 0 {
deviceOptions = append(deviceOptions, WithWDAPort(dev.Port))
}
if dev.MjpegPort != 0 {
deviceOptions = append(deviceOptions, WithWDAMjpegPort(dev.MjpegPort))
}
return
}
func (dev *IOSDevice) perfOpitons() (perfOptions []giDevice.PerfOption) {
if dev.PerfOptions == nil {
return
}
// system
if dev.PerfOptions.SysCPU {
perfOptions = append(perfOptions, giDevice.WithPerfSystemCPU(true))
}
if dev.PerfOptions.SysMem {
perfOptions = append(perfOptions, giDevice.WithPerfSystemMem(true))
}
if dev.PerfOptions.SysDisk {
perfOptions = append(perfOptions, giDevice.WithPerfSystemDisk(true))
}
if dev.PerfOptions.SysNetwork {
perfOptions = append(perfOptions, giDevice.WithPerfSystemNetwork(true))
}
if dev.PerfOptions.FPS {
perfOptions = append(perfOptions, giDevice.WithPerfFPS(true))
}
if dev.PerfOptions.Network {
perfOptions = append(perfOptions, giDevice.WithPerfNetwork(true))
}
// process
if dev.PerfOptions.BundleID != "" {
perfOptions = append(perfOptions,
giDevice.WithPerfBundleID(dev.PerfOptions.BundleID))
}
if dev.PerfOptions.Pid != 0 {
perfOptions = append(perfOptions,
giDevice.WithPerfPID(dev.PerfOptions.Pid))
}
// config
if dev.PerfOptions.OutputInterval != 0 {
perfOptions = append(perfOptions,
giDevice.WithPerfOutputInterval(dev.PerfOptions.OutputInterval))
}
if dev.PerfOptions.SystemAttributes != nil {
perfOptions = append(perfOptions,
giDevice.WithPerfSystemAttributes(dev.PerfOptions.SystemAttributes...))
}
if dev.PerfOptions.ProcessAttributes != nil {
perfOptions = append(perfOptions,
giDevice.WithPerfProcessAttributes(dev.PerfOptions.ProcessAttributes...))
}
return
}
// NewHTTPDriver creates new remote HTTP client, this will also start a new session.
func (dev *IOSDevice) NewHTTPDriver(capabilities Capabilities) (driver WebDriver, err error) {
localPort, err := getFreePort()
if err != nil {
return nil, errors.Wrap(err, "get free port failed")
}
if err = dev.forward(localPort, dev.Port); err != nil {
return nil, errors.Wrap(err, "forward tcp port failed")
}
localMjpegPort, err := getFreePort()
if err != nil {
return nil, errors.Wrap(err, "get free port failed")
}
if err = dev.forward(localMjpegPort, dev.MjpegPort); err != nil {
return nil, errors.Wrap(err, "forward tcp port failed")
}
log.Info().Interface("capabilities", capabilities).
Int("localPort", localPort).Int("localMjpegPort", localMjpegPort).
Msg("init WDA HTTP driver")
wd := new(wdaDriver)
wd.client = http.DefaultClient
host := "127.0.0.1"
if wd.urlPrefix, err = url.Parse(fmt.Sprintf("http://%s:%d", host, localPort)); err != nil {
return nil, err
}
var sessionInfo SessionInfo
if sessionInfo, err = wd.NewSession(capabilities); err != nil {
return nil, err
}
wd.sessionId = sessionInfo.SessionId
if wd.mjpegHTTPConn, err = net.Dial(
"tcp",
fmt.Sprintf("%s:%d", host, localMjpegPort),
); err != nil {
return nil, err
}
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) {
log.Info().Interface("capabilities", capabilities).
Str("udid", dev.UDID).Msg("init WDA USB driver")
wd := new(wdaDriver)
if wd.defaultConn, err = dev.d.NewConnect(dev.Port, 0); err != nil {
return nil, fmt.Errorf("connect port %d failed: %w",
dev.Port, err)
}
wd.client = convertToHTTPClient(wd.defaultConn.RawConn())
if wd.mjpegUSBConn, err = dev.d.NewConnect(dev.MjpegPort, 0); err != nil {
return nil, fmt.Errorf("connect MJPEG port %d failed: %w",
dev.MjpegPort, err)
}
wd.mjpegClient = convertToHTTPClient(wd.mjpegUSBConn.RawConn())
if wd.urlPrefix, err = url.Parse("http://" + dev.UDID); err != nil {
return nil, err
}
_, err = wd.NewSession(capabilities)
return wd, err
}
func (dExt *DriverExt) ConnectMjpegStream(httpClient *http.Client) (err error) {
if httpClient == nil {
return errors.New(`'httpClient' can't be nil`)
}
var req *http.Request
if req, err = http.NewRequest(http.MethodGet, "http://*", nil); err != nil {
return err
}
var resp *http.Response
if resp, err = httpClient.Do(req); err != nil {
return err
}
// defer func() { _ = resp.Body.Close() }()
var boundary string
if _, param, err := mime.ParseMediaType(resp.Header.Get("Content-Type")); err != nil {
return err
} else {
boundary = strings.Trim(param["boundary"], "-")
}
mjpegReader := multipart.NewReader(resp.Body, boundary)
go func() {
for {
select {
case <-dExt.doneMjpegStream:
_ = resp.Body.Close()
return
default:
var part *multipart.Part
if part, err = mjpegReader.NextPart(); err != nil {
dExt.frame = nil
continue
}
raw := new(bytes.Buffer)
if _, err = raw.ReadFrom(part); err != nil {
dExt.frame = nil
continue
}
dExt.frame = raw
}
}
}()
return
}
func (dExt *DriverExt) CloseMjpegStream() {
dExt.doneMjpegStream <- true
}
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"` // wda
Stacktrace string `json:"stacktrace"` // uia
}
})
if err = json.Unmarshal(r, reply); err != nil {
return err
}
if reply.Value.Err != "" {
errText := reply.Value.Message
re := regexp.MustCompile(`{.+?=(.+?)}`)
if re.MatchString(reply.Value.Message) {
subMatch := re.FindStringSubmatch(reply.Value.Message)
errText = subMatch[len(subMatch)-1]
}
return fmt.Errorf("%s: %s", reply.Value.Err, errText)
}
return
}
func (r rawResponse) valueConvertToString() (s string, err error) {
reply := new(struct{ Value string })
if err = json.Unmarshal(r, reply); err != nil {
return "", errors.Wrapf(err, "json.Unmarshal failed, rawResponse: %s", string(r))
}
s = reply.Value
return
}
func (r rawResponse) valueConvertToBool() (b bool, err error) {
reply := new(struct{ Value bool })
if err = json.Unmarshal(r, reply); err != nil {
return false, err
}
b = reply.Value
return
}
func (r rawResponse) valueConvertToSessionInfo() (sessionInfo SessionInfo, err error) {
reply := new(struct{ Value struct{ SessionInfo } })
if err = json.Unmarshal(r, reply); err != nil {
return SessionInfo{}, err
}
sessionInfo = reply.Value.SessionInfo
return
}
func (r rawResponse) valueConvertToJsonRawMessage() (raw builtinJSON.RawMessage, err error) {
reply := new(struct{ Value builtinJSON.RawMessage })
if err = json.Unmarshal(r, reply); err != nil {
return nil, err
}
raw = reply.Value
return
}
func (r rawResponse) valueDecodeAsBase64() (raw *bytes.Buffer, err error) {
str, err := r.valueConvertToString()
if err != nil {
return nil, errors.Wrap(err, "failed to convert value to string")
}
decodeString, err := base64.StdEncoding.DecodeString(str)
if err != nil {
return nil, errors.Wrap(err, "failed to decode base64 string")
}
raw = bytes.NewBuffer(decodeString)
return
}
var errNoSuchElement = errors.New("no such element")
func (r rawResponse) valueConvertToElementID() (id string, err error) {
reply := new(struct{ Value map[string]string })
if err = json.Unmarshal(r, reply); err != nil {
return "", err
}
if len(reply.Value) == 0 {
return "", errNoSuchElement
}
if id = elementIDFromValue(reply.Value); id == "" {
return "", fmt.Errorf("invalid element returned: %+v", reply)
}
return
}
func (r rawResponse) valueConvertToElementIDs() (IDs []string, err error) {
reply := new(struct{ Value []map[string]string })
if err = json.Unmarshal(r, reply); err != nil {
return nil, err
}
if len(reply.Value) == 0 {
return nil, errNoSuchElement
}
IDs = make([]string, len(reply.Value))
for i, elem := range reply.Value {
var id string
if id = elementIDFromValue(elem); id == "" {
return nil, fmt.Errorf("invalid element returned: %+v", reply)
}
IDs[i] = id
}
return
}

898
hrp/pkg/uixt/ios_driver.go Normal file
View File

@@ -0,0 +1,898 @@
package uixt
import (
"bytes"
"encoding/base64"
builtinJSON "encoding/json"
"fmt"
"net"
"net/http"
"net/url"
"strings"
"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 {
Driver
// default port
defaultConn giDevice.InnerConn
// mjpeg port
mjpegUSBConn giDevice.InnerConn // via USB
mjpegHTTPConn net.Conn // via HTTP
mjpegClient *http.Client
}
func (wd *wdaDriver) GetMjpegClient() *http.Client {
return wd.mjpegClient
}
func (wd *wdaDriver) Close() error {
if wd.defaultConn != nil {
wd.defaultConn.Close()
}
if wd.mjpegUSBConn != nil {
wd.mjpegUSBConn.Close()
}
if wd.mjpegClient != nil {
wd.mjpegClient.CloseIdleConnections()
}
return wd.mjpegHTTPConn.Close()
}
func (wd *wdaDriver) NewSession(capabilities Capabilities) (sessionInfo SessionInfo, err error) {
// [[FBRoute POST:@"/session"].withoutSession respondWithTarget:self action:@selector(handleCreateSession:)]
data := make(map[string]interface{})
if len(capabilities) == 0 {
data["capabilities"] = make(map[string]interface{})
} else {
data["capabilities"] = map[string]interface{}{"alwaysMatch": capabilities}
}
var rawResp rawResponse
if rawResp, err = wd.httpPOST(data, "/session"); err != nil {
return SessionInfo{}, err
}
if sessionInfo, err = rawResp.valueConvertToSessionInfo(); err != nil {
return SessionInfo{}, err
}
wd.sessionId = sessionInfo.SessionId
return
}
func (wd *wdaDriver) ActiveSession() (sessionInfo SessionInfo, err error) {
// [[FBRoute GET:@""] respondWithTarget:self action:@selector(handleGetActiveSession:)]
var rawResp rawResponse
if rawResp, err = wd.httpGET("/session", wd.sessionId); err != nil {
return SessionInfo{}, err
}
if sessionInfo, err = rawResp.valueConvertToSessionInfo(); err != nil {
return SessionInfo{}, err
}
return
}
func (wd *wdaDriver) DeleteSession() (err error) {
// [[FBRoute DELETE:@""] respondWithTarget:self action:@selector(handleDeleteSession:)]
_, err = wd.httpDELETE("/session", wd.sessionId)
return
}
func (wd *wdaDriver) Status() (deviceStatus DeviceStatus, err error) {
// [[FBRoute GET:@"/status"].withoutSession respondWithTarget:self action:@selector(handleGetStatus:)]
var rawResp rawResponse
if rawResp, err = wd.httpGET("/status"); err != nil {
return DeviceStatus{}, err
}
reply := new(struct{ Value struct{ DeviceStatus } })
if err = json.Unmarshal(rawResp, reply); err != nil {
return DeviceStatus{}, err
}
deviceStatus = reply.Value.DeviceStatus
return
}
func (wd *wdaDriver) DeviceInfo() (deviceInfo DeviceInfo, err error) {
// [[FBRoute GET:@"/wda/device/info"] respondWithTarget:self action:@selector(handleGetDeviceInfo:)]
// [[FBRoute GET:@"/wda/device/info"].withoutSession
var rawResp rawResponse
if rawResp, err = wd.httpGET("/session", wd.sessionId, "/wda/device/info"); err != nil {
return DeviceInfo{}, err
}
reply := new(struct{ Value struct{ DeviceInfo } })
if err = json.Unmarshal(rawResp, reply); err != nil {
return DeviceInfo{}, err
}
deviceInfo = reply.Value.DeviceInfo
return
}
func (wd *wdaDriver) Location() (location Location, err error) {
// [[FBRoute GET:@"/wda/device/location"] respondWithTarget:self action:@selector(handleGetLocation:)]
// [[FBRoute GET:@"/wda/device/location"].withoutSession
var rawResp rawResponse
if rawResp, err = wd.httpGET("/session", wd.sessionId, "/wda/device/location"); err != nil {
return Location{}, err
}
reply := new(struct{ Value struct{ Location } })
if err = json.Unmarshal(rawResp, reply); err != nil {
return Location{}, err
}
location = reply.Value.Location
return
}
func (wd *wdaDriver) BatteryInfo() (batteryInfo BatteryInfo, err error) {
// [[FBRoute GET:@"/wda/batteryInfo"] respondWithTarget:self action:@selector(handleGetBatteryInfo:)]
var rawResp rawResponse
if rawResp, err = wd.httpGET("/session", wd.sessionId, "/wda/batteryInfo"); err != nil {
return BatteryInfo{}, err
}
reply := new(struct{ Value struct{ BatteryInfo } })
if err = json.Unmarshal(rawResp, reply); err != nil {
return BatteryInfo{}, err
}
batteryInfo = reply.Value.BatteryInfo
return
}
func (wd *wdaDriver) WindowSize() (size Size, err error) {
// [[FBRoute GET:@"/window/size"] respondWithTarget:self action:@selector(handleGetWindowSize:)]
var rawResp rawResponse
if rawResp, err = wd.httpGET("/session", wd.sessionId, "/window/size"); err != nil {
return Size{}, err
}
reply := new(struct{ Value struct{ Size } })
if err = json.Unmarshal(rawResp, reply); err != nil {
return Size{}, err
}
size = reply.Value.Size
return
}
func (wd *wdaDriver) Screen() (screen Screen, err error) {
// [[FBRoute GET:@"/wda/screen"] respondWithTarget:self action:@selector(handleGetScreen:)]
var rawResp rawResponse
if rawResp, err = wd.httpGET("/session", wd.sessionId, "/wda/screen"); err != nil {
return Screen{}, err
}
reply := new(struct{ Value struct{ Screen } })
if err = json.Unmarshal(rawResp, reply); err != nil {
return Screen{}, err
}
screen = reply.Value.Screen
return
}
func (wd *wdaDriver) Scale() (float64, error) {
screen, err := wd.Screen()
if err != nil {
return 0, err
}
return screen.Scale, nil
}
func (wd *wdaDriver) ActiveAppInfo() (info AppInfo, err error) {
// [[FBRoute GET:@"/wda/activeAppInfo"] respondWithTarget:self action:@selector(handleActiveAppInfo:)]
// [[FBRoute GET:@"/wda/activeAppInfo"].withoutSession
var rawResp rawResponse
if rawResp, err = wd.httpGET("/session", wd.sessionId, "/wda/activeAppInfo"); err != nil {
return AppInfo{}, err
}
reply := new(struct{ Value struct{ AppInfo } })
if err = json.Unmarshal(rawResp, reply); err != nil {
return AppInfo{}, err
}
info = reply.Value.AppInfo
return
}
func (wd *wdaDriver) ActiveAppsList() (appsList []AppBaseInfo, err error) {
// [[FBRoute GET:@"/wda/apps/list"] respondWithTarget:self action:@selector(handleGetActiveAppsList:)]
var rawResp rawResponse
if rawResp, err = wd.httpGET("/session", wd.sessionId, "/wda/apps/list"); err != nil {
return nil, err
}
reply := new(struct{ Value []AppBaseInfo })
if err = json.Unmarshal(rawResp, reply); err != nil {
return nil, err
}
appsList = reply.Value
return
}
func (wd *wdaDriver) AppState(bundleId string) (runState AppState, err error) {
// [[FBRoute POST:@"/wda/apps/state"] respondWithTarget:self action:@selector(handleSessionAppState:)]
data := map[string]interface{}{"bundleId": bundleId}
var rawResp rawResponse
if rawResp, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/apps/state"); err != nil {
return 0, err
}
reply := new(struct{ Value AppState })
if err = json.Unmarshal(rawResp, reply); err != nil {
return 0, err
}
runState = reply.Value
_ = rawResp
return
}
func (wd *wdaDriver) IsLocked() (locked bool, err error) {
// [[FBRoute GET:@"/wda/locked"] respondWithTarget:self action:@selector(handleIsLocked:)]
// [[FBRoute GET:@"/wda/locked"].withoutSession
var rawResp rawResponse
if rawResp, err = wd.httpGET("/session", wd.sessionId, "/wda/locked"); err != nil {
return false, err
}
if locked, err = rawResp.valueConvertToBool(); err != nil {
return false, err
}
return
}
func (wd *wdaDriver) Unlock() (err error) {
// [[FBRoute POST:@"/wda/unlock"] respondWithTarget:self action:@selector(handleUnlock:)]
// [[FBRoute POST:@"/wda/unlock"].withoutSession
_, err = wd.httpPOST(nil, "/session", wd.sessionId, "/wda/unlock")
return
}
func (wd *wdaDriver) Lock() (err error) {
// [[FBRoute POST:@"/wda/lock"] respondWithTarget:self action:@selector(handleLock:)]
// [[FBRoute POST:@"/wda/lock"].withoutSession
_, err = wd.httpPOST(nil, "/session", wd.sessionId, "/wda/lock")
return
}
func (wd *wdaDriver) Homescreen() (err error) {
// [[FBRoute POST:@"/wda/homescreen"].withoutSession respondWithTarget:self action:@selector(handleHomescreenCommand:)]
_, err = wd.httpPOST(nil, "/wda/homescreen")
return
}
func (wd *wdaDriver) AlertText() (text string, err error) {
// [[FBRoute GET:@"/alert/text"] respondWithTarget:self action:@selector(handleAlertGetTextCommand:)]
// [[FBRoute GET:@"/alert/text"].withoutSession
var rawResp rawResponse
if rawResp, err = wd.httpGET("/session", wd.sessionId, "/alert/text"); err != nil {
return "", err
}
if text, err = rawResp.valueConvertToString(); err != nil {
return "", err
}
return
}
func (wd *wdaDriver) AlertButtons() (btnLabels []string, err error) {
// [[FBRoute GET:@"/wda/alert/buttons"] respondWithTarget:self action:@selector(handleGetAlertButtonsCommand:)]
var rawResp rawResponse
if rawResp, err = wd.httpGET("/session", wd.sessionId, "/wda/alert/buttons"); err != nil {
return nil, err
}
reply := new(struct{ Value []string })
if err = json.Unmarshal(rawResp, reply); err != nil {
return nil, err
}
btnLabels = reply.Value
return
}
func (wd *wdaDriver) AlertAccept(label ...string) (err error) {
// [[FBRoute POST:@"/alert/accept"] respondWithTarget:self action:@selector(handleAlertAcceptCommand:)]
// [[FBRoute POST:@"/alert/accept"].withoutSession
data := make(map[string]interface{})
if len(label) != 0 && label[0] != "" {
data["name"] = label[0]
}
_, err = wd.httpPOST(data, "/alert/accept")
return
}
func (wd *wdaDriver) AlertDismiss(label ...string) (err error) {
// [[FBRoute POST:@"/alert/dismiss"] respondWithTarget:self action:@selector(handleAlertDismissCommand:)]
// [[FBRoute POST:@"/alert/dismiss"].withoutSession
data := make(map[string]interface{})
if len(label) != 0 && label[0] != "" {
data["name"] = label[0]
}
_, err = wd.httpPOST(data, "/alert/dismiss")
return
}
func (wd *wdaDriver) AlertSendKeys(text string) (err error) {
// [[FBRoute POST:@"/alert/text"] respondWithTarget:self action:@selector(handleAlertSetTextCommand:)]
data := map[string]interface{}{"value": strings.Split(text, "")}
_, err = wd.httpPOST(data, "/session", wd.sessionId, "/alert/text")
return
}
func (wd *wdaDriver) AppLaunch(bundleId string, launchOpt ...AppLaunchOption) (err error) {
// [[FBRoute POST:@"/wda/apps/launch"] respondWithTarget:self action:@selector(handleSessionAppLaunch:)]
data := make(map[string]interface{})
if len(launchOpt) != 0 {
data = launchOpt[0]
}
data["bundleId"] = bundleId
_, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/apps/launch")
return
}
func (wd *wdaDriver) AppLaunchUnattached(bundleId string) (err error) {
// [[FBRoute POST:@"/wda/apps/launchUnattached"].withoutSession respondWithTarget:self action:@selector(handleLaunchUnattachedApp:)]
data := map[string]interface{}{"bundleId": bundleId}
_, err = wd.httpPOST(data, "/wda/apps/launchUnattached")
return
}
func (wd *wdaDriver) AppTerminate(bundleId string) (successful bool, err error) {
// [[FBRoute POST:@"/wda/apps/terminate"] respondWithTarget:self action:@selector(handleSessionAppTerminate:)]
data := map[string]interface{}{"bundleId": bundleId}
var rawResp rawResponse
if rawResp, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/apps/terminate"); err != nil {
return false, err
}
if successful, err = rawResp.valueConvertToBool(); err != nil {
return false, err
}
return
}
func (wd *wdaDriver) AppActivate(bundleId string) (err error) {
// [[FBRoute POST:@"/wda/apps/activate"] respondWithTarget:self action:@selector(handleSessionAppActivate:)]
data := map[string]interface{}{"bundleId": bundleId}
_, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/apps/activate")
return
}
func (wd *wdaDriver) AppDeactivate(second float64) (err error) {
// [[FBRoute POST:@"/wda/deactivateApp"] respondWithTarget:self action:@selector(handleDeactivateAppCommand:)]
if second < 3 {
second = 3.0
}
data := map[string]interface{}{"duration": second}
_, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/deactivateApp")
return
}
func (wd *wdaDriver) AppAuthReset(resource ProtectedResource) (err error) {
// [[FBRoute POST:@"/wda/resetAppAuth"] respondWithTarget:self action:@selector(handleResetAppAuth:)]
data := map[string]interface{}{"resource": resource}
_, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/resetAppAuth")
return
}
func (wd *wdaDriver) Tap(x, y int, options ...DataOption) error {
return wd.TapFloat(float64(x), float64(y), options...)
}
func (wd *wdaDriver) TapFloat(x, y float64, options ...DataOption) (err error) {
// [[FBRoute POST:@"/wda/tap/:uuid"] respondWithTarget:self action:@selector(handleTap:)]
data := map[string]interface{}{
"x": x,
"y": y,
}
// append options in post data for extra WDA configurations
// e.g. add identifier in tap event logs
for _, option := range options {
option(data)
}
_, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/tap/0")
return
}
func (wd *wdaDriver) DoubleTap(x, y int) error {
return wd.DoubleTapFloat(float64(x), float64(y))
}
func (wd *wdaDriver) DoubleTapFloat(x, y float64) (err error) {
// [[FBRoute POST:@"/wda/doubleTap"] respondWithTarget:self action:@selector(handleDoubleTapCoordinate:)]
data := map[string]interface{}{
"x": x,
"y": y,
}
_, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/doubleTap")
return
}
func (wd *wdaDriver) TouchAndHold(x, y int, second ...float64) error {
return wd.TouchAndHoldFloat(float64(x), float64(y), second...)
}
func (wd *wdaDriver) TouchAndHoldFloat(x, y float64, second ...float64) (err error) {
// [[FBRoute POST:@"/wda/touchAndHold"] respondWithTarget:self action:@selector(handleTouchAndHoldCoordinate:)]
data := map[string]interface{}{
"x": x,
"y": y,
}
if len(second) == 0 || second[0] <= 0 {
second = []float64{1.0}
}
data["duration"] = second[0]
_, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/touchAndHold")
return
}
func (wd *wdaDriver) Drag(fromX, fromY, toX, toY int, options ...DataOption) error {
return wd.DragFloat(float64(fromX), float64(fromY), float64(toX), float64(toY), options...)
}
func (wd *wdaDriver) DragFloat(fromX, fromY, toX, toY float64, options ...DataOption) (err error) {
// [[FBRoute POST:@"/wda/dragfromtoforduration"] respondWithTarget:self action:@selector(handleDragCoordinate:)]
data := map[string]interface{}{
"fromX": fromX,
"fromY": fromY,
"toX": toX,
"toY": toY,
}
// append options in post data for extra WDA configurations
// e.g. use WithPressDuration to set pressForDuration
for _, option := range options {
option(data)
}
if _, ok := data["duration"]; !ok {
data["duration"] = 1.0 // default duration
}
_, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/dragfromtoforduration")
return
}
func (wd *wdaDriver) Swipe(fromX, fromY, toX, toY int, options ...DataOption) error {
options = append(options, WithPressDuration(0))
return wd.SwipeFloat(float64(fromX), float64(fromY), float64(toX), float64(toY), options...)
}
func (wd *wdaDriver) SwipeFloat(fromX, fromY, toX, toY float64, options ...DataOption) error {
options = append(options, WithPressDuration(0))
return wd.DragFloat(fromX, fromY, toX, toY, options...)
}
func (wd *wdaDriver) ForceTouch(x, y int, pressure float64, second ...float64) error {
return wd.ForceTouchFloat(float64(x), float64(y), pressure, second...)
}
func (wd *wdaDriver) ForceTouchFloat(x, y, pressure float64, second ...float64) error {
if len(second) == 0 || second[0] <= 0 {
second = []float64{1.0}
}
actions := NewTouchActions().
Press(
NewTouchActionPress().WithXYFloat(x, y).WithPressure(pressure)).
Wait(second[0]).
Release()
return wd.PerformAppiumTouchActions(actions)
}
func (wd *wdaDriver) PerformW3CActions(actions *W3CActions) (err error) {
// [[FBRoute POST:@"/actions"] respondWithTarget:self action:@selector(handlePerformW3CTouchActions:)]
data := map[string]interface{}{"actions": actions}
_, err = wd.httpPOST(data, "/session", wd.sessionId, "/actions")
return
}
func (wd *wdaDriver) PerformAppiumTouchActions(touchActs *TouchActions) (err error) {
// [[FBRoute POST:@"/wda/touch/perform"] respondWithTarget:self action:@selector(handlePerformAppiumTouchActions:)]
// [[FBRoute POST:@"/wda/touch/multi/perform"]
data := map[string]interface{}{"actions": touchActs}
_, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/touch/multi/perform")
return
}
func (wd *wdaDriver) SetPasteboard(contentType PasteboardType, content string) (err error) {
// [[FBRoute POST:@"/wda/setPasteboard"] respondWithTarget:self action:@selector(handleSetPasteboard:)]
data := map[string]interface{}{
"contentType": contentType,
"content": base64.StdEncoding.EncodeToString([]byte(content)),
}
_, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/setPasteboard")
return
}
func (wd *wdaDriver) GetPasteboard(contentType PasteboardType) (raw *bytes.Buffer, err error) {
// [[FBRoute POST:@"/wda/getPasteboard"] respondWithTarget:self action:@selector(handleGetPasteboard:)]
data := map[string]interface{}{"contentType": contentType}
var rawResp rawResponse
if rawResp, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/getPasteboard"); err != nil {
return nil, err
}
if raw, err = rawResp.valueDecodeAsBase64(); err != nil {
return nil, err
}
return
}
func (wd *wdaDriver) SendKeys(text string, options ...DataOption) (err error) {
// [[FBRoute POST:@"/wda/keys"] respondWithTarget:self action:@selector(handleKeys:)]
data := map[string]interface{}{"value": strings.Split(text, "")}
// append options in post data for extra WDA configurations
// e.g. use WithFrequency to set frequency of typing
for _, option := range options {
option(data)
}
if _, ok := data["frequency"]; !ok {
data["frequency"] = 60 // default frequency
}
_, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/keys")
return
}
func (wd *wdaDriver) Input(text string, options ...DataOption) (err error) {
return wd.SendKeys(text, options...)
}
func (wd *wdaDriver) KeyboardDismiss(keyNames ...string) (err error) {
// [[FBRoute POST:@"/wda/keyboard/dismiss"] respondWithTarget:self action:@selector(handleDismissKeyboardCommand:)]
if len(keyNames) == 0 {
keyNames = []string{"return"}
}
data := map[string]interface{}{"keyNames": keyNames}
_, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/keyboard/dismiss")
return
}
func (wd *wdaDriver) PressButton(devBtn DeviceButton) (err error) {
// [[FBRoute POST:@"/wda/pressButton"] respondWithTarget:self action:@selector(handlePressButtonCommand:)]
data := map[string]interface{}{"name": devBtn}
_, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/pressButton")
return
}
func (wd *wdaDriver) IOHIDEvent(pageID EventPageID, usageID EventUsageID, duration ...float64) (err error) {
// [[FBRoute POST:@"/wda/performIoHidEvent"] respondWithTarget:self action:@selector(handlePeformIOHIDEvent:)]
if len(duration) == 0 || duration[0] <= 0 {
duration = []float64{0.005}
}
data := map[string]interface{}{
"page": pageID,
"usage": usageID,
"duration": duration[0],
}
_, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/performIoHidEvent")
return
}
func (wd *wdaDriver) StartCamera() (err error) {
// start camera, alias for app_launch com.apple.camera
return wd.AppLaunch("com.apple.camera")
}
func (wd *wdaDriver) StopCamera() (err error) {
// stop camera, alias for app_terminate com.apple.camera
success, err := wd.AppTerminate("com.apple.camera")
if err != nil {
return errors.Wrap(err, "failed to terminate camera")
}
if !success {
log.Warn().Msg("camera was not running")
}
return nil
}
func (wd *wdaDriver) ExpectNotification(notifyName string, notifyType NotificationType, second ...int) (err error) {
// [[FBRoute POST:@"/wda/expectNotification"] respondWithTarget:self action:@selector(handleExpectNotification:)]
if len(second) == 0 {
second = []int{60}
}
data := map[string]interface{}{
"name": notifyName,
"type": notifyType,
"timeout": second[0],
}
_, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/expectNotification")
return
}
func (wd *wdaDriver) SiriActivate(text string) (err error) {
// [[FBRoute POST:@"/wda/siri/activate"] respondWithTarget:self action:@selector(handleActivateSiri:)]
data := map[string]interface{}{"text": text}
_, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/siri/activate")
return
}
func (wd *wdaDriver) SiriOpenUrl(url string) (err error) {
// [[FBRoute POST:@"/url"] respondWithTarget:self action:@selector(handleOpenURL:)]
data := map[string]interface{}{"url": url}
_, err = wd.httpPOST(data, "/session", wd.sessionId, "/url")
return
}
func (wd *wdaDriver) Orientation() (orientation Orientation, err error) {
// [[FBRoute GET:@"/orientation"] respondWithTarget:self action:@selector(handleGetOrientation:)]
var rawResp rawResponse
if rawResp, err = wd.httpGET("/session", wd.sessionId, "/orientation"); err != nil {
return "", err
}
reply := new(struct{ Value Orientation })
if err = json.Unmarshal(rawResp, reply); err != nil {
return "", err
}
orientation = reply.Value
return
}
func (wd *wdaDriver) SetOrientation(orientation Orientation) (err error) {
// [[FBRoute POST:@"/orientation"] respondWithTarget:self action:@selector(handleSetOrientation:)]
data := map[string]interface{}{"orientation": orientation}
_, err = wd.httpPOST(data, "/session", wd.sessionId, "/orientation")
return
}
func (wd *wdaDriver) Rotation() (rotation Rotation, err error) {
// [[FBRoute GET:@"/rotation"] respondWithTarget:self action:@selector(handleGetRotation:)]
var rawResp rawResponse
if rawResp, err = wd.httpGET("/session", wd.sessionId, "/rotation"); err != nil {
return Rotation{}, err
}
reply := new(struct{ Value Rotation })
if err = json.Unmarshal(rawResp, reply); err != nil {
return Rotation{}, err
}
rotation = reply.Value
return
}
func (wd *wdaDriver) SetRotation(rotation Rotation) (err error) {
// [[FBRoute POST:@"/rotation"] respondWithTarget:self action:@selector(handleSetRotation:)]
_, err = wd.httpPOST(rotation, "/session", wd.sessionId, "/rotation")
return
}
func (wd *wdaDriver) MatchTouchID(isMatch bool) (err error) {
// [FBRoute POST:@"/wda/touch_id"]
data := map[string]interface{}{"match": isMatch}
_, err = wd.httpPOST(data, "/session", wd.sessionId, "/wda/touch_id")
return
}
func (wd *wdaDriver) ActiveElement() (element WebElement, err error) {
// [[FBRoute GET:@"/element/active"] respondWithTarget:self action:@selector(handleGetActiveElement:)]
var rawResp rawResponse
if rawResp, err = wd.httpGET("/session", wd.sessionId, "/element/active"); err != nil {
return nil, err
}
var elementID string
if elementID, err = rawResp.valueConvertToElementID(); err != nil {
return nil, err
}
element = &wdaElement{parent: wd, id: elementID}
return
}
func (wd *wdaDriver) FindElement(by BySelector) (element WebElement, err error) {
// [[FBRoute POST:@"/element"] respondWithTarget:self action:@selector(handleFindElement:)]
using, value := by.getUsingAndValue()
data := map[string]interface{}{
"using": using,
"value": value,
}
var rawResp rawResponse
if rawResp, err = wd.httpPOST(data, "/session", wd.sessionId, "/element"); err != nil {
return nil, err
}
var elementID string
if elementID, err = rawResp.valueConvertToElementID(); err != nil {
if errors.Is(err, errNoSuchElement) {
return nil, fmt.Errorf("%w: unable to find an element using '%s', value '%s'", err, using, value)
}
return nil, err
}
element = &wdaElement{parent: wd, id: elementID}
return
}
func (wd *wdaDriver) FindElements(by BySelector) (elements []WebElement, err error) {
// [[FBRoute POST:@"/elements"] respondWithTarget:self action:@selector(handleFindElements:)]
using, value := by.getUsingAndValue()
data := map[string]interface{}{
"using": using,
"value": value,
}
var rawResp rawResponse
if rawResp, err = wd.httpPOST(data, "/session", wd.sessionId, "/elements"); err != nil {
return nil, err
}
var elementIDs []string
if elementIDs, err = rawResp.valueConvertToElementIDs(); err != nil {
if errors.Is(err, errNoSuchElement) {
return nil, fmt.Errorf("%w: unable to find an element using '%s', value '%s'", err, using, value)
}
return nil, err
}
elements = make([]WebElement, len(elementIDs))
for i := range elementIDs {
elements[i] = &wdaElement{parent: wd, id: elementIDs[i]}
}
return
}
func (wd *wdaDriver) Screenshot() (raw *bytes.Buffer, err error) {
// [[FBRoute GET:@"/screenshot"] respondWithTarget:self action:@selector(handleGetScreenshot:)]
// [[FBRoute GET:@"/screenshot"].withoutSession respondWithTarget:self action:@selector(handleGetScreenshot:)]
var rawResp rawResponse
if rawResp, err = wd.httpGET("/session", wd.sessionId, "/screenshot"); err != nil {
return nil, errors.Wrap(err, "get WDA screenshot data failed")
}
if raw, err = rawResp.valueDecodeAsBase64(); err != nil {
return nil, errors.Wrap(err, "decode WDA screenshot data failed")
}
return
}
func (wd *wdaDriver) Source(srcOpt ...SourceOption) (source string, err error) {
// [[FBRoute GET:@"/source"] respondWithTarget:self action:@selector(handleGetSourceCommand:)]
// [[FBRoute GET:@"/source"].withoutSession
tmp, _ := url.Parse(wd.concatURL(nil, "/session", wd.sessionId))
toJsonRaw := false
if len(srcOpt) != 0 {
q := tmp.Query()
for k, val := range srcOpt[0] {
v := val.(string)
q.Set(k, v)
if k == "format" && v == "json" {
toJsonRaw = true
}
}
tmp.RawQuery = q.Encode()
}
var rawResp rawResponse
if rawResp, err = wd.httpRequest(http.MethodGet, wd.concatURL(tmp, "/source"), nil); err != nil {
return "", nil
}
if toJsonRaw {
var jr builtinJSON.RawMessage
if jr, err = rawResp.valueConvertToJsonRawMessage(); err != nil {
return "", err
}
return string(jr), nil
}
if source, err = rawResp.valueConvertToString(); err != nil {
return "", err
}
return
}
func (wd *wdaDriver) AccessibleSource() (source string, err error) {
// [[FBRoute GET:@"/wda/accessibleSource"] respondWithTarget:self action:@selector(handleGetAccessibleSourceCommand:)]
// [[FBRoute GET:@"/wda/accessibleSource"].withoutSession
var rawResp rawResponse
if rawResp, err = wd.httpGET("/session", wd.sessionId, "/wda/accessibleSource"); err != nil {
return "", err
}
var jr builtinJSON.RawMessage
if jr, err = rawResp.valueConvertToJsonRawMessage(); err != nil {
return "", err
}
source = string(jr)
return
}
func (wd *wdaDriver) HealthCheck() (err error) {
// [[FBRoute GET:@"/wda/healthcheck"].withoutSession respondWithTarget:self action:@selector(handleGetHealthCheck:)]
_, err = wd.httpGET("/wda/healthcheck")
return
}
func (wd *wdaDriver) GetAppiumSettings() (settings map[string]interface{}, err error) {
// [[FBRoute GET:@"/appium/settings"] respondWithTarget:self action:@selector(handleGetSettings:)]
var rawResp rawResponse
if rawResp, err = wd.httpGET("/session", wd.sessionId, "/appium/settings"); err != nil {
return nil, err
}
reply := new(struct{ Value map[string]interface{} })
if err = json.Unmarshal(rawResp, reply); err != nil {
return nil, err
}
settings = reply.Value
return
}
func (wd *wdaDriver) SetAppiumSettings(settings map[string]interface{}) (ret map[string]interface{}, err error) {
// [[FBRoute POST:@"/appium/settings"] respondWithTarget:self action:@selector(handleSetSettings:)]
data := map[string]interface{}{"settings": settings}
var rawResp rawResponse
if rawResp, err = wd.httpPOST(data, "/session", wd.sessionId, "/appium/settings"); err != nil {
return nil, err
}
reply := new(struct{ Value map[string]interface{} })
if err = json.Unmarshal(rawResp, reply); err != nil {
return nil, err
}
ret = reply.Value
return
}
func (wd *wdaDriver) IsHealthy() (healthy bool, err error) {
var rawResp rawResponse
if rawResp, err = wd.httpGET("/health"); err != nil {
return false, err
}
if string(rawResp) != "I-AM-ALIVE" {
return false, nil
}
return true, nil
}
func (wd *wdaDriver) WdaShutdown() (err error) {
_, err = wd.httpGET("/wda/shutdown")
return
}
func (wd *wdaDriver) WaitWithTimeoutAndInterval(condition Condition, timeout, interval time.Duration) error {
startTime := time.Now()
for {
done, err := condition(wd)
if err != nil {
return err
}
if done {
return nil
}
if elapsed := time.Since(startTime); elapsed > timeout {
return fmt.Errorf("timeout after %v", elapsed)
}
time.Sleep(interval)
}
}
func (wd *wdaDriver) WaitWithTimeout(condition Condition, timeout time.Duration) error {
return wd.WaitWithTimeoutAndInterval(condition, timeout, DefaultWaitInterval)
}
func (wd *wdaDriver) Wait(condition Condition) error {
return wd.WaitWithTimeoutAndInterval(condition, DefaultWaitTimeout, DefaultWaitInterval)
}
func (wd *wdaDriver) triggerWDALog(data map[string]interface{}) (rawResp []byte, err error) {
// [[FBRoute POST:@"/gtf/automation/log"].withoutSession respondWithTarget:self action:@selector(handleAutomationLog:)]
return wd.httpPOST(data, "/gtf/automation/log")
}
func (wd *wdaDriver) StartCaptureLog(identifier ...string) error {
log.Info().Msg("start WDA log recording")
if identifier == nil {
identifier = []string{""}
}
data := map[string]interface{}{"action": "start", "type": 2, "identifier": identifier[0]}
_, err := wd.triggerWDALog(data)
if err != nil {
return errors.Wrap(err, "failed to start WDA log recording")
}
return nil
}
type wdaResponse struct {
Value interface{} `json:"value"`
SessionID string `json:"sessionId"`
}
func (wd *wdaDriver) StopCaptureLog() (result interface{}, err error) {
log.Info().Msg("stop log recording")
data := map[string]interface{}{"action": "stop"}
rawResp, err := wd.triggerWDALog(data)
if err != nil {
log.Error().Err(err).Bytes("rawResp", rawResp).Msg("failed to get WDA logs")
return "", errors.Wrap(err, "failed to get WDA logs")
}
reply := new(wdaResponse)
if err = json.Unmarshal(rawResp, reply); err != nil {
log.Error().Err(err).Bytes("rawResp", rawResp).Msg("failed to json.Unmarshal WDA logs")
return reply, err
}
log.Info().Interface("value", reply.Value).Msg("get WDA log response")
return reply.Value, nil
}

484
hrp/pkg/uixt/ios_element.go Normal file
View File

@@ -0,0 +1,484 @@
package uixt
import (
"bytes"
"fmt"
"math"
"strings"
"github.com/pkg/errors"
"github.com/httprunner/httprunner/v4/hrp/internal/json"
)
// All elements returned by search endpoints have assigned element_id.
// Given element_id you can query properties like:
// enabled, rect, size, location, text, displayed, accessible, name
type wdaElement struct {
parent *wdaDriver
id string // element_id
}
func (we wdaElement) Click() (err error) {
// [[FBRoute POST:@"/element/:uuid/click"] respondWithTarget:self action:@selector(handleClick:)]
_, err = we.parent.httpPOST(nil, "/session", we.parent.sessionId, "/element", we.id, "/click")
return
}
func (we wdaElement) SendKeys(text string, options ...DataOption) (err error) {
// [[FBRoute POST:@"/element/:uuid/value"] respondWithTarget:self action:@selector(handleSetValue:)]
data := map[string]interface{}{
"value": strings.Split(text, ""),
}
// append options in post data for extra uiautomator configurations
for _, option := range options {
option(data)
}
if _, ok := data["frequency"]; !ok {
data["frequency"] = 60
}
_, err = we.parent.httpPOST(data, "/session", we.parent.sessionId, "/element", we.id, "/value")
return
}
func (we wdaElement) Clear() (err error) {
// [[FBRoute POST:@"/element/:uuid/clear"] respondWithTarget:self action:@selector(handleClear:)]
_, err = we.parent.httpPOST(nil, "/session", we.parent.sessionId, "/element", we.id, "/clear")
return
}
func (we wdaElement) Tap(x, y int) error {
return we.TapFloat(float64(x), float64(y))
}
func (we wdaElement) TapFloat(x, y float64) (err error) {
// [[FBRoute POST:@"/wda/tap/:uuid"] respondWithTarget:self action:@selector(handleTap:)]
data := map[string]interface{}{
"x": x,
"y": y,
}
_, err = we.parent.httpPOST(data, "/session", we.parent.sessionId, "/wda/tap/", we.id)
return
}
func (we wdaElement) DoubleTap() (err error) {
// [[FBRoute POST:@"/wda/element/:uuid/doubleTap"] respondWithTarget:self action:@selector(handleDoubleTap:)]
_, err = we.parent.httpPOST(nil, "/session", we.parent.sessionId, "/wda/element", we.id, "/doubleTap")
return
}
func (we wdaElement) TouchAndHold(second ...float64) (err error) {
// [[FBRoute POST:@"/wda/element/:uuid/touchAndHold"] respondWithTarget:self action:@selector(handleTouchAndHold:)]
data := make(map[string]interface{})
if len(second) == 0 || second[0] <= 0 {
second = []float64{1.0}
}
data["duration"] = second[0]
_, err = we.parent.httpPOST(data, "/session", we.parent.sessionId, "/wda/element", we.id, "/touchAndHold")
return
}
func (we wdaElement) TwoFingerTap() (err error) {
// [[FBRoute POST:@"/wda/element/:uuid/twoFingerTap"] respondWithTarget:self action:@selector(handleTwoFingerTap:)]
_, err = we.parent.httpPOST(nil, "/session", we.parent.sessionId, "/wda/element", we.id, "/twoFingerTap")
return
}
func (we wdaElement) TapWithNumberOfTaps(numberOfTaps, numberOfTouches int) (err error) {
// [[FBRoute POST:@"/wda/element/:uuid/tapWithNumberOfTaps"] respondWithTarget:self action:@selector(handleTapWithNumberOfTaps:)]
if numberOfTouches <= 0 {
return errors.New("'numberOfTouches' must be greater than zero")
}
if numberOfTouches > 5 {
return errors.New("'numberOfTouches' cannot be greater than 5")
}
if numberOfTaps <= 0 {
return errors.New("'numberOfTaps' must be greater than zero")
}
if numberOfTaps > 10 {
return errors.New("'numberOfTaps' cannot be greater than 10")
}
data := map[string]interface{}{
"numberOfTaps": numberOfTaps,
"numberOfTouches": numberOfTouches,
}
_, err = we.parent.httpPOST(data, "/session", we.parent.sessionId, "/wda/element", we.id, "/tapWithNumberOfTaps")
return
}
func (we wdaElement) ForceTouch(pressure float64, second ...float64) (err error) {
return we.ForceTouchFloat(-1, -1, pressure, second...)
}
func (we wdaElement) ForceTouchFloat(x, y, pressure float64, second ...float64) (err error) {
// [[FBRoute POST:@"/wda/element/:uuid/forceTouch"] respondWithTarget:self action:@selector(handleForceTouch:)]
data := make(map[string]interface{})
if x != -1 && y != -1 {
data["x"] = x
data["y"] = y
}
if len(second) == 0 || second[0] <= 0 {
second = []float64{1.0}
}
data["pressure"] = pressure
data["duration"] = second[0]
_, err = we.parent.httpPOST(data, "/session", we.parent.sessionId, "/wda/element", we.id, "/forceTouch")
return
}
func (we wdaElement) Drag(fromX, fromY, toX, toY int, pressForDuration ...float64) error {
return we.DragFloat(float64(fromX), float64(fromY), float64(toX), float64(toY), pressForDuration...)
}
func (we wdaElement) DragFloat(fromX, fromY, toX, toY float64, pressForDuration ...float64) (err error) {
// [[FBRoute POST:@"/wda/element/:uuid/dragfromtoforduration"] respondWithTarget:self action:@selector(handleDrag:)]
data := map[string]interface{}{
"fromX": fromX,
"fromY": fromY,
"toX": toX,
"toY": toY,
}
if len(pressForDuration) == 0 || pressForDuration[0] < 0 {
pressForDuration = []float64{1.0}
}
data["duration"] = pressForDuration[0]
_, err = we.parent.httpPOST(data, "/session", we.parent.sessionId, "/wda/element", we.id, "/dragfromtoforduration")
return
}
func (we wdaElement) Swipe(fromX, fromY, toX, toY int) error {
return we.SwipeFloat(float64(fromX), float64(fromY), float64(toX), float64(toY))
}
func (we wdaElement) SwipeFloat(fromX, fromY, toX, toY float64) error {
return we.DragFloat(fromX, fromY, toX, toY, 0)
}
func (we wdaElement) SwipeDirection(direction Direction, velocity ...float64) (err error) {
// [[FBRoute POST:@"/wda/element/:uuid/swipe"] respondWithTarget:self action:@selector(handleSwipe:)]
data := map[string]interface{}{"direction": direction}
if len(velocity) != 0 && velocity[0] > 0 {
data["velocity"] = velocity[0]
}
_, err = we.parent.httpPOST(data, "/session", we.parent.sessionId, "/wda/element", we.id, "/swipe")
return
}
func (we wdaElement) Pinch(scale, velocity float64) (err error) {
// [[FBRoute POST:@"/wda/element/:uuid/pinch"] respondWithTarget:self action:@selector(handlePinch:)]
if scale <= 0 {
return errors.New("'scale' must be greater than zero")
}
if scale == 1 {
return errors.New("'scale' must be greater or less than 1")
}
if scale < 1 && velocity > 0 {
return errors.New("'velocity' must be less than zero when 'scale' is less than 1")
}
if scale > 1 && velocity <= 0 {
return errors.New("'velocity' must be greater than zero when 'scale' is greater than 1")
}
data := map[string]interface{}{
"scale": scale,
"velocity": velocity,
}
_, err = we.parent.httpPOST(data, "/session", we.parent.sessionId, "/wda/element", we.id, "/pinch")
return
}
func (we wdaElement) PinchToZoomOutByW3CAction(scale ...float64) (err error) {
if len(scale) == 0 {
scale = []float64{1.0}
} else if scale[0] > 23 {
scale[0] = 23
}
var size Size
if size, err = we.Size(); err != nil {
return err
}
r := scale[0] * 2 / 100.0
offsetX, offsetY := float64(size.Width)*r, float64(size.Height)*r
actions := NewW3CActions().SwipeFloat(0-offsetX, 0-offsetY, 0, 0, we).SwipeFloat(offsetX, offsetY, 0, 0, we)
return we.parent.PerformW3CActions(actions)
}
func (we wdaElement) Rotate(rotation float64, velocity ...float64) (err error) {
// [[FBRoute POST:@"/wda/element/:uuid/rotate"] respondWithTarget:self action:@selector(handleRotate:)]
if rotation > math.Pi*2 || rotation < math.Pi*-2 {
return errors.New("'rotation' must not be more than 2π or less than -2π")
}
if len(velocity) == 0 || velocity[0] == 0 {
velocity = []float64{rotation}
}
if rotation > 0 && velocity[0] < 0 || rotation < 0 && velocity[0] > 0 {
return errors.New("'rotation' and 'velocity' must have the same sign")
}
data := map[string]interface{}{
"rotation": rotation,
"velocity": velocity[0],
}
_, err = we.parent.httpPOST(data, "/session", we.parent.sessionId, "/wda/element", we.id, "/rotate")
return
}
func (we wdaElement) PickerWheelSelect(order PickerWheelOrder, offset ...int) (err error) {
// [[FBRoute POST:@"/wda/pickerwheel/:uuid/select"] respondWithTarget:self action:@selector(handleWheelSelect:)]
if len(offset) == 0 {
offset = []int{2}
} else if offset[0] <= 0 || offset[0] > 5 {
return fmt.Errorf("'offset' value is expected to be in range (0, 5]. '%d' was given instead", offset[0])
}
data := map[string]interface{}{
"order": order,
"offset": float64(offset[0]) * 0.1,
}
_, err = we.parent.httpPOST(data, "/session", we.parent.sessionId, "/wda/pickerwheel", we.id, "/select")
return
}
func (we wdaElement) scroll(data interface{}) (err error) {
// [[FBRoute POST:@"/wda/element/:uuid/scroll"] respondWithTarget:self action:@selector(handleScroll:)]
_, err = we.parent.httpPOST(data, "/session", we.parent.sessionId, "/wda/element", we.id, "/scroll")
return
}
func (we wdaElement) ScrollElementByName(name string) error {
data := map[string]interface{}{"name": name}
return we.scroll(data)
}
func (we wdaElement) ScrollElementByPredicate(predicate string) error {
data := map[string]interface{}{"predicateString": predicate}
return we.scroll(data)
}
func (we wdaElement) ScrollToVisible() error {
data := map[string]interface{}{"toVisible": true}
return we.scroll(data)
}
func (we wdaElement) ScrollDirection(direction Direction, distance ...float64) error {
if len(distance) == 0 || distance[0] <= 0 {
distance = []float64{0.5}
}
data := map[string]interface{}{
"direction": direction,
"distance": distance[0],
}
return we.scroll(data)
}
func (we wdaElement) FindElement(by BySelector) (element WebElement, err error) {
// [[FBRoute POST:@"/element/:uuid/element"] respondWithTarget:self action:@selector(handleFindSubElement:)]
using, value := by.getUsingAndValue()
data := map[string]interface{}{
"using": using,
"value": value,
}
var rawResp rawResponse
if rawResp, err = we.parent.httpPOST(data, "/session", we.parent.sessionId, "/element", we.id, "/element"); err != nil {
return nil, err
}
var elementID string
if elementID, err = rawResp.valueConvertToElementID(); err != nil {
if errors.Is(err, errNoSuchElement) {
return nil, fmt.Errorf("%w: unable to find an element using '%s', value '%s'", err, using, value)
}
return nil, err
}
element = &wdaElement{parent: we.parent, id: elementID}
return
}
func (we wdaElement) FindElements(by BySelector) (elements []WebElement, err error) {
// [[FBRoute POST:@"/element/:uuid/elements"] respondWithTarget:self action:@selector(handleFindSubElements:)]
using, value := by.getUsingAndValue()
data := map[string]interface{}{
"using": using,
"value": value,
}
var rawResp rawResponse
if rawResp, err = we.parent.httpPOST(data, "/session", we.parent.sessionId, "/element", we.id, "/elements"); err != nil {
return nil, err
}
var elementIDs []string
if elementIDs, err = rawResp.valueConvertToElementIDs(); err != nil {
if errors.Is(err, errNoSuchElement) {
return nil, fmt.Errorf("%w: unable to find an element using '%s', value '%s'", err, using, value)
}
return nil, err
}
elements = make([]WebElement, len(elementIDs))
for i := range elementIDs {
elements[i] = &wdaElement{parent: we.parent, id: elementIDs[i]}
}
return
}
func (we wdaElement) FindVisibleCells() (elements []WebElement, err error) {
// [[FBRoute GET:@"/wda/element/:uuid/getVisibleCells"] respondWithTarget:self action:@selector(handleFindVisibleCells:)]
var rawResp rawResponse
if rawResp, err = we.parent.httpGET("/session", we.parent.sessionId, "/wda/element", we.id, "/getVisibleCells"); err != nil {
return nil, err
}
var elementIDs []string
if elementIDs, err = rawResp.valueConvertToElementIDs(); err != nil {
if errors.Is(err, errNoSuchElement) {
return nil, fmt.Errorf("%w: unable to find a cell element in this element", err)
}
return nil, err
}
elements = make([]WebElement, len(elementIDs))
for i := range elementIDs {
elements[i] = &wdaElement{parent: we.parent, id: elementIDs[i]}
}
return
}
func (we wdaElement) Rect() (rect Rect, err error) {
// [[FBRoute GET:@"/element/:uuid/rect"] respondWithTarget:self action:@selector(handleGetRect:)]
var rawResp rawResponse
if rawResp, err = we.parent.httpGET("/session", we.parent.sessionId, "/element", we.id, "/rect"); err != nil {
return Rect{}, err
}
reply := new(struct{ Value struct{ Rect } })
if err = json.Unmarshal(rawResp, reply); err != nil {
return Rect{}, err
}
rect = reply.Value.Rect
return
}
func (we wdaElement) Location() (Point, error) {
rect, err := we.Rect()
if err != nil {
return Point{}, err
}
return rect.Point, nil
}
func (we wdaElement) Size() (Size, error) {
rect, err := we.Rect()
if err != nil {
return Size{}, err
}
return rect.Size, nil
}
func (we wdaElement) Text() (text string, err error) {
// [[FBRoute GET:@"/element/:uuid/text"] respondWithTarget:self action:@selector(handleGetText:)]
var rawResp rawResponse
if rawResp, err = we.parent.httpGET("/session", we.parent.sessionId, "/element", we.id, "/text"); err != nil {
return "", err
}
if text, err = rawResp.valueConvertToString(); err != nil {
return "", err
}
return
}
func (we wdaElement) Type() (elemType string, err error) {
// [[FBRoute GET:@"/element/:uuid/name"] respondWithTarget:self action:@selector(handleGetName:)]
var rawResp rawResponse
if rawResp, err = we.parent.httpGET("/session", we.parent.sessionId, "/element", we.id, "/name"); err != nil {
return "", err
}
if elemType, err = rawResp.valueConvertToString(); err != nil {
return "", err
}
return
}
func (we wdaElement) IsEnabled() (enabled bool, err error) {
// [[FBRoute GET:@"/element/:uuid/enabled"] respondWithTarget:self action:@selector(handleGetEnabled:)]
var rawResp rawResponse
if rawResp, err = we.parent.httpGET("/session", we.parent.sessionId, "/element", we.id, "/enabled"); err != nil {
return false, err
}
if enabled, err = rawResp.valueConvertToBool(); err != nil {
return false, err
}
return
}
func (we wdaElement) IsDisplayed() (displayed bool, err error) {
// [[FBRoute GET:@"/element/:uuid/displayed"] respondWithTarget:self action:@selector(handleGetDisplayed:)]
var rawResp rawResponse
if rawResp, err = we.parent.httpGET("/session", we.parent.sessionId, "/element", we.id, "/displayed"); err != nil {
return false, err
}
if displayed, err = rawResp.valueConvertToBool(); err != nil {
return false, err
}
return
}
func (we wdaElement) IsSelected() (selected bool, err error) {
// [[FBRoute GET:@"/element/:uuid/selected"] respondWithTarget:self action:@selector(handleGetSelected:)]
var rawResp rawResponse
if rawResp, err = we.parent.httpGET("/session", we.parent.sessionId, "/element", we.id, "/selected"); err != nil {
return false, err
}
if selected, err = rawResp.valueConvertToBool(); err != nil {
return false, err
}
return
}
func (we wdaElement) IsAccessible() (accessible bool, err error) {
// [[FBRoute GET:@"/wda/element/:uuid/accessible"] respondWithTarget:self action:@selector(handleGetAccessible:)]
var rawResp rawResponse
if rawResp, err = we.parent.httpGET("/session", we.parent.sessionId, "/wda/element", we.id, "/accessible"); err != nil {
return false, err
}
if accessible, err = rawResp.valueConvertToBool(); err != nil {
return false, err
}
return
}
func (we wdaElement) IsAccessibilityContainer() (isAccessibilityContainer bool, err error) {
// [[FBRoute GET:@"/wda/element/:uuid/accessibilityContainer"] respondWithTarget:self action:@selector(handleGetIsAccessibilityContainer:)]
var rawResp rawResponse
if rawResp, err = we.parent.httpGET("/session", we.parent.sessionId, "/wda/element", we.id, "/accessibilityContainer"); err != nil {
return false, err
}
if isAccessibilityContainer, err = rawResp.valueConvertToBool(); err != nil {
return false, err
}
return
}
func (we wdaElement) GetAttribute(attr ElementAttribute) (value string, err error) {
// [[FBRoute GET:@"/element/:uuid/attribute/:name"] respondWithTarget:self action:@selector(handleGetAttribute:)]
var rawResp rawResponse
if rawResp, err = we.parent.httpGET("/session", we.parent.sessionId, "/element", we.id, "/attribute", attr.getAttributeName()); err != nil {
return "", err
}
if value, err = rawResp.valueConvertToString(); err != nil {
return "", err
}
return
}
func (we wdaElement) UID() (uid string) {
return we.id
}
func (we wdaElement) Screenshot() (raw *bytes.Buffer, err error) {
// W3C element screenshot
// [[FBRoute GET:@"/element/:uuid/screenshot"] respondWithTarget:self action:@selector(handleElementScreenshot:)]
// JSONWP element screenshot
// [[FBRoute GET:@"/screenshot/:uuid"] respondWithTarget:self action:@selector(handleElementScreenshot:)]
var rawResp rawResponse
if rawResp, err = we.parent.httpGET("/session", we.parent.sessionId, "/element", we.id, "/screenshot"); err != nil {
return nil, err
}
if raw, err = rawResp.valueDecodeAsBase64(); err != nil {
return nil, err
}
return
}

1174
hrp/pkg/uixt/ios_test.go Normal file

File diff suppressed because it is too large Load Diff

10
hrp/pkg/uixt/ocr_off.go Normal file
View File

@@ -0,0 +1,10 @@
//go:build !ocr
package uixt
import "github.com/rs/zerolog/log"
func (dExt *DriverExt) FindTextByOCR(ocrText string, index ...int) (x, y, width, height float64, err error) {
log.Fatal().Msg("OCR is not supported")
return
}

179
hrp/pkg/uixt/ocr_on.go Normal file
View File

@@ -0,0 +1,179 @@
//go:build ocr
package uixt
import (
"bytes"
"encoding/base64"
"fmt"
"image"
"io/ioutil"
"mime/multipart"
"net/http"
"strings"
"time"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v4/hrp/internal/json"
)
var client = &http.Client{
Timeout: time.Second * 10,
}
type OCRResult struct {
Text string `json:"text"`
Points []PointF `json:"points"`
}
type ResponseOCR struct {
Code int `json:"code"`
Message string `json:"message"`
OCRResult []OCRResult `json:"ocrResult"`
}
type veDEMOCRService struct{}
func (s *veDEMOCRService) getOCRResult(imageBuf []byte) ([]OCRResult, error) {
bodyBuf := &bytes.Buffer{}
bodyWriter := multipart.NewWriter(bodyBuf)
bodyWriter.WriteField("withDet", "true")
// bodyWriter.WriteField("timestampOnly", "true")
formWriter, err := bodyWriter.CreateFormFile("image", "screenshot.png")
if err != nil {
return nil, fmt.Errorf("create form file error: %v", err)
}
_, err = formWriter.Write(imageBuf)
if err != nil {
return nil, fmt.Errorf("write form error: %v", err)
}
err = bodyWriter.Close()
if err != nil {
return nil, fmt.Errorf("close body writer error: %v", err)
}
url, _ := base64.StdEncoding.DecodeString("aHR0cHM6Ly9ndGZ0YXNrLmJ5dGVkYW5jZS5jb20vYXBpL3YxL2FsZ29yaXRobS9vY3I=")
req, err := http.NewRequest("POST", string(url), bodyBuf)
if err != nil {
return nil, fmt.Errorf("construct request error: %v", err)
}
req.Header.Add("Content-Type", bodyWriter.FormDataContentType())
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("http reqeust OCR server error: %v", err)
}
defer resp.Body.Close()
results, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("read response body error: %v", err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected response status code: %d, results: %v", resp.StatusCode, string(results))
}
var ocrResult ResponseOCR
err = json.Unmarshal(results, &ocrResult)
if err != nil {
return nil, fmt.Errorf("json unmarshal response body error: %v", err)
}
return ocrResult.OCRResult, nil
}
func (s *veDEMOCRService) FindText(text string, imageBuf []byte, index ...int) (rect image.Rectangle, err error) {
if len(index) == 0 {
index = []int{0} // index not specified
}
ocrResults, err := s.getOCRResult(imageBuf)
if err != nil {
log.Error().Err(err).Msg("getOCRResult failed")
return
}
var rects []image.Rectangle
var ocrTexts []string
for _, ocrResult := range ocrResults {
ocrTexts = append(ocrTexts, ocrResult.Text)
// not contains text
if !strings.Contains(ocrResult.Text, text) {
continue
}
rect = image.Rectangle{
// ocrResult.Points 顺序:左上 -> 右上 -> 右下 -> 左下
Min: image.Point{
X: int(ocrResult.Points[0].X),
Y: int(ocrResult.Points[0].Y),
},
Max: image.Point{
X: int(ocrResult.Points[2].X),
Y: int(ocrResult.Points[2].Y),
},
}
rects = append(rects, rect)
// contains text while not match exactly
if ocrResult.Text != text {
continue
}
// match exactly, and not specify index, return the first one
if index[0] == 0 {
return rect, nil
}
}
if len(rects) == 0 {
return image.Rectangle{},
fmt.Errorf("text %s not found in %v", text, ocrTexts)
}
// get index
idx := index[0]
if idx > 0 {
// NOTICE: index start from 1
idx = idx - 1
} else if idx < 0 {
idx = len(rects) + idx
}
// index out of range
if idx >= len(rects) {
return image.Rectangle{}, fmt.Errorf("text %s found %d, index %d out of range",
text, len(rects), idx)
}
return rects[idx], nil
}
type OCRService interface {
FindText(text string, imageBuf []byte, index ...int) (rect image.Rectangle, err error)
}
func (dExt *DriverExt) FindTextByOCR(ocrText string, index ...int) (x, y, width, height float64, err error) {
var bufSource *bytes.Buffer
if bufSource, err = dExt.takeScreenShot(); err != nil {
err = fmt.Errorf("takeScreenShot error: %v", err)
return
}
service := &veDEMOCRService{}
rect, err := service.FindText(ocrText, bufSource.Bytes(), index...)
if err != nil {
log.Warn().Msgf("FindText failed: %s", err.Error())
err = fmt.Errorf("FindText failed: %v", err)
return
}
log.Info().Str("ocrText", ocrText).Msgf("FindText success")
x, y, width, height = dExt.MappingToRectInUIKit(rect)
return
}

18
hrp/pkg/uixt/ocr_test.go Normal file
View File

@@ -0,0 +1,18 @@
//go:build ocr
package uixt
import (
"testing"
)
func TestDriverExtOCR(t *testing.T) {
driverExt, err := InitWDAClient(nil)
checkErr(t, err)
x, y, width, height, err := driverExt.FindTextByOCR("抖音")
checkErr(t, err)
t.Logf("x: %v, y: %v, width: %v, height: %v", x, y, width, height)
driverExt.Driver.TapFloat(x+width*0.5, y+height*0.5-20)
}

View File

@@ -0,0 +1,23 @@
//go:build !opencv
package uixt
import (
"image"
"github.com/rs/zerolog/log"
)
func Extend(driver WebDriver, options ...CVOption) (dExt *DriverExt, err error) {
return extend(driver)
}
func (dExt *DriverExt) FindAllImageRect(search string) (rects []image.Rectangle, err error) {
log.Fatal().Msg("opencv is not supported")
return
}
func (dExt *DriverExt) FindImageRectInUIKit(imagePath string, index ...int) (x, y, width, height float64, err error) {
log.Fatal().Msg("opencv is not supported")
return
}

146
hrp/pkg/uixt/opencv_on.go Normal file
View File

@@ -0,0 +1,146 @@
//go:build opencv
package uixt
import (
"bytes"
"image"
"io/ioutil"
"os"
cvHelper "github.com/electricbubble/opencv-helper"
)
const (
// TmCcoeffNormed maps to TM_CCOEFF_NORMED
TmCcoeffNormed TemplateMatchMode = iota
// TmSqdiff maps to TM_SQDIFF
TmSqdiff
// TmSqdiffNormed maps to TM_SQDIFF_NORMED
TmSqdiffNormed
// TmCcorr maps to TM_CCORR
TmCcorr
// TmCcorrNormed maps to TM_CCORR_NORMED
TmCcorrNormed
// TmCcoeff maps to TM_CCOEFF
TmCcoeff
)
type DebugMode int
const (
// DmOff no output
DmOff DebugMode = iota
// DmEachMatch output matched and mismatched values
DmEachMatch
// DmNotMatch output only values that do not match
DmNotMatch
)
// Extend 获得扩展后的 Driver
// 并指定匹配阀值,
// 获取当前设备的 Scale
// 默认匹配模式为 TmCcoeffNormed
// 默认关闭 OpenCV 匹配值计算后的输出
func Extend(driver WebDriver, options ...CVOption) (dExt *DriverExt, err error) {
dExt, err = extend(driver)
if err != nil {
return nil, err
}
for _, option := range options {
option(&dExt.CVArgs)
}
if dExt.threshold == 0 {
dExt.threshold = 0.95 // default threshold
}
if dExt.matchMode == 0 {
dExt.matchMode = TmCcoeffNormed // default match mode
}
cvHelper.Debug(cvHelper.DebugMode(DmOff))
return
}
func (dExt *DriverExt) Debug(dm DebugMode) {
cvHelper.Debug(cvHelper.DebugMode(dm))
}
func (dExt *DriverExt) OnlyOnceThreshold(threshold float64) (newExt *DriverExt) {
newExt = new(DriverExt)
newExt.Driver = dExt.Driver
newExt.scale = dExt.scale
newExt.matchMode = dExt.matchMode
newExt.threshold = threshold
return
}
func (dExt *DriverExt) OnlyOnceMatchMode(matchMode TemplateMatchMode) (newExt *DriverExt) {
newExt = new(DriverExt)
newExt.Driver = dExt.Driver
newExt.scale = dExt.scale
newExt.matchMode = matchMode
newExt.threshold = dExt.threshold
return
}
// func (sExt *DriverExt) findImgRect(search string) (rect image.Rectangle, err error) {
// pathSource := filepath.Join(sExt.pathname, cvHelper.GenFilename())
// if err = sExt.driver.ScreenshotToDisk(pathSource); err != nil {
// return image.Rectangle{}, err
// }
//
// if rect, err = cvHelper.FindImageRectFromDisk(pathSource, search, float32(sExt.Threshold), cvHelper.TemplateMatchMode(sExt.MatchMode)); err != nil {
// return image.Rectangle{}, err
// }
// return
// }
func (dExt *DriverExt) FindAllImageRect(search string) (rects []image.Rectangle, err error) {
var bufSource, bufSearch *bytes.Buffer
if bufSearch, err = getBufFromDisk(search); err != nil {
return nil, err
}
if bufSource, err = dExt.takeScreenShot(); err != nil {
return nil, err
}
if rects, err = cvHelper.FindAllImageRectsFromRaw(bufSource, bufSearch, float32(dExt.threshold), cvHelper.TemplateMatchMode(dExt.matchMode)); err != nil {
return nil, err
}
return
}
func (dExt *DriverExt) FindImageRectInUIKit(imagePath string, index ...int) (x, y, width, height float64, err error) {
var bufSource, bufSearch *bytes.Buffer
if bufSearch, err = getBufFromDisk(imagePath); err != nil {
return 0, 0, 0, 0, err
}
if bufSource, err = dExt.takeScreenShot(); err != nil {
return 0, 0, 0, 0, err
}
var rect image.Rectangle
if rect, err = cvHelper.FindImageRectFromRaw(bufSource, bufSearch, float32(dExt.threshold), cvHelper.TemplateMatchMode(dExt.matchMode)); err != nil {
return 0, 0, 0, 0, err
}
// if rect, err = dExt.findImgRect(search); err != nil {
// return 0, 0, 0, 0, err
// }
x, y, width, height = dExt.MappingToRectInUIKit(rect)
return
}
func getBufFromDisk(name string) (*bytes.Buffer, error) {
var f *os.File
var err error
if f, err = os.Open(name); err != nil {
return nil, err
}
var all []byte
if all, err = ioutil.ReadAll(f); err != nil {
return nil, err
}
return bytes.NewBuffer(all), nil
}

86
hrp/pkg/uixt/swipe.go Normal file
View File

@@ -0,0 +1,86 @@
package uixt
import (
"fmt"
"github.com/rs/zerolog/log"
)
func assertRelative(p float64) bool {
return p >= 0 && p <= 1
}
// SwipeRelative swipe from relative position [fromX, fromY] to relative position [toX, toY]
func (dExt *DriverExt) SwipeRelative(fromX, fromY, toX, toY float64, identifier ...string) error {
width := dExt.windowSize.Width
height := dExt.windowSize.Height
if !assertRelative(fromX) || !assertRelative(fromY) ||
!assertRelative(toX) || !assertRelative(toY) {
return fmt.Errorf("fromX(%f), fromY(%f), toX(%f), toY(%f) must be less than 1",
fromX, fromY, toX, toY)
}
fromX = float64(width) * fromX
fromY = float64(height) * fromY
toX = float64(width) * toX
toY = float64(height) * toY
if len(identifier) > 0 && identifier[0] != "" {
option := WithCustomOption("log", map[string]interface{}{
"enable": true,
"data": identifier[0],
})
return dExt.Driver.SwipeFloat(fromX, fromY, toX, toY, option)
}
return dExt.Driver.SwipeFloat(fromX, fromY, toX, toY)
}
func (dExt *DriverExt) SwipeTo(direction string, identifier ...string) (err error) {
switch direction {
case "up":
return dExt.SwipeUp(identifier...)
case "down":
return dExt.SwipeDown(identifier...)
case "left":
return dExt.SwipeLeft(identifier...)
case "right":
return dExt.SwipeRight(identifier...)
}
return fmt.Errorf("unexpected direction: %s", direction)
}
func (dExt *DriverExt) SwipeUp(identifier ...string) (err error) {
return dExt.SwipeRelative(0.5, 0.5, 0.5, 0.1, identifier...)
}
func (dExt *DriverExt) SwipeDown(identifier ...string) (err error) {
return dExt.SwipeRelative(0.5, 0.5, 0.5, 0.9, identifier...)
}
func (dExt *DriverExt) SwipeLeft(identifier ...string) (err error) {
return dExt.SwipeRelative(0.5, 0.5, 0.1, 0.5, identifier...)
}
func (dExt *DriverExt) SwipeRight(identifier ...string) (err error) {
return dExt.SwipeRelative(0.5, 0.5, 0.9, 0.5, identifier...)
}
// FindCondition indicates the condition to find a UI element
type FindCondition func(driver *DriverExt) error
// FoundAction indicates the action to do after a UI element is found
type FoundAction func(driver *DriverExt) error
func (dExt *DriverExt) SwipeUntil(direction string, condition FindCondition, action FoundAction, maxTimes int) error {
for i := 0; i < maxTimes; i++ {
if err := condition(dExt); err == nil {
// do action after found
return action(dExt)
}
if err := dExt.SwipeTo(direction); err != nil {
log.Error().Err(err).Msgf("swipe %s failed", direction)
}
}
return fmt.Errorf("swipe %s %d times, match condition failed", direction, maxTimes)
}

View File

@@ -0,0 +1,48 @@
//go:build localtest
package uixt
import (
"testing"
)
func TestSwipeUntil(t *testing.T) {
driverExt, err := InitWDAClient(nil)
checkErr(t, err)
var point PointF
findApp := func(d *DriverExt) error {
var err error
point, err = d.GetTextXY("抖音")
return err
}
foundAppAction := func(d *DriverExt) error {
// click app, launch douyin
return d.TapAbsXY(point.X, point.Y, "")
}
driverExt.Driver.Homescreen()
// swipe to first screen
for i := 0; i < 5; i++ {
driverExt.SwipeRight()
}
// swipe until app found
err = driverExt.SwipeUntil("left", findApp, foundAppAction, 10)
checkErr(t, err)
findLive := func(d *DriverExt) error {
var err error
point, err = d.GetTextXY("点击进入直播间")
return err
}
foundLiveAction := func(d *DriverExt) error {
// enter live room
return d.TapAbsXY(point.X, point.Y, "")
}
// swipe until live room found
err = driverExt.SwipeUntil("up", findLive, foundLiveAction, 20)
checkErr(t, err)
}

152
hrp/pkg/uixt/tap.go Normal file
View File

@@ -0,0 +1,152 @@
package uixt
import (
"fmt"
)
func (dExt *DriverExt) TapAbsXY(x, y float64, identifier string) error {
// tap on absolute coordinate [x, y]
if len(identifier) > 0 {
option := WithCustomOption("log", map[string]interface{}{
"enable": true,
"data": identifier,
})
return dExt.Driver.TapFloat(x, y, option)
}
return dExt.Driver.TapFloat(x, y)
}
func (dExt *DriverExt) TapXY(x, y float64, identifier string) error {
// tap on [x, y] percent of window size
if x > 1 || y > 1 {
return fmt.Errorf("x, y percentage should be < 1, got x=%v, y=%v", x, y)
}
x = x * float64(dExt.windowSize.Width)
y = y * float64(dExt.windowSize.Height)
return dExt.TapAbsXY(x, y, identifier)
}
func (dExt *DriverExt) GetTextXY(ocrText string, index ...int) (point PointF, err error) {
x, y, width, height, err := dExt.FindTextByOCR(ocrText, index...)
if err != nil {
return PointF{}, err
}
point = PointF{
X: x + width*0.5,
Y: y + height*0.5,
}
return point, nil
}
func (dExt *DriverExt) GetImageXY(imagePath string, index ...int) (point PointF, err error) {
x, y, width, height, err := dExt.FindImageRectInUIKit(imagePath, index...)
if err != nil {
return PointF{}, err
}
point = PointF{
X: x + width*0.5,
Y: y + height*0.5,
}
return point, nil
}
func (dExt *DriverExt) TapByOCR(ocrText string, identifier string, ignoreNotFoundError bool, index ...int) error {
point, err := dExt.GetTextXY(ocrText, index...)
if err != nil {
if ignoreNotFoundError {
return nil
}
return err
}
return dExt.TapAbsXY(point.X, point.Y, identifier)
}
func (dExt *DriverExt) TapByCV(imagePath string, identifier string, ignoreNotFoundError bool, index ...int) error {
point, err := dExt.GetImageXY(imagePath, index...)
if err != nil {
if ignoreNotFoundError {
return nil
}
return err
}
return dExt.TapAbsXY(point.X, point.Y, identifier)
}
func (dExt *DriverExt) Tap(param string, identifier string, ignoreNotFoundError bool, index ...int) error {
return dExt.TapOffset(param, 0.5, 0.5, identifier, ignoreNotFoundError, index...)
}
func (dExt *DriverExt) TapOffset(param string, xOffset, yOffset float64, identifier string, ignoreNotFoundError bool, index ...int) (err error) {
// click on element, find by name attribute
ele, err := dExt.FindUIElement(param)
if err == nil {
return ele.Click()
}
x, y, width, height, err := dExt.FindUIRectInUIKit(param, index...)
if err != nil {
if ignoreNotFoundError {
return nil
}
return err
}
return dExt.TapAbsXY(x+width*xOffset, y+height*yOffset, identifier)
}
func (dExt *DriverExt) DoubleTapXY(x, y float64) error {
// double tap on coordinate: [x, y] should be relative
if x > 1 || y > 1 {
return fmt.Errorf("x, y percentage should be < 1, got x=%v, y=%v", x, y)
}
x = x * float64(dExt.windowSize.Width)
y = y * float64(dExt.windowSize.Height)
return dExt.Driver.DoubleTapFloat(x, y)
}
func (dExt *DriverExt) DoubleTap(param string) (err error) {
return dExt.DoubleTapOffset(param, 0.5, 0.5)
}
func (dExt *DriverExt) DoubleTapOffset(param string, xOffset, yOffset float64) (err error) {
// click on element, find by name attribute
ele, err := dExt.FindUIElement(param)
if err == nil {
return ele.DoubleTap()
}
var x, y, width, height float64
if x, y, width, height, err = dExt.FindUIRectInUIKit(param); err != nil {
return err
}
return dExt.Driver.DoubleTapFloat(x+width*xOffset, y+height*yOffset)
}
// TapWithNumber sends one or more taps
func (dExt *DriverExt) TapWithNumber(param string, numberOfTaps int) (err error) {
return dExt.TapWithNumberOffset(param, numberOfTaps, 0.5, 0.5)
}
func (dExt *DriverExt) TapWithNumberOffset(param string, numberOfTaps int, xOffset, yOffset float64) (err error) {
if numberOfTaps <= 0 {
numberOfTaps = 1
}
var x, y, width, height float64
if x, y, width, height, err = dExt.FindUIRectInUIKit(param); err != nil {
return err
}
x = x + width*xOffset
y = y + height*yOffset
touchActions := NewTouchActions().Tap(NewTouchActionTap().WithXYFloat(x, y).WithCount(numberOfTaps))
return dExt.PerformTouchActions(touchActions)
}

45
hrp/pkg/uixt/tap_test.go Normal file
View File

@@ -0,0 +1,45 @@
//go:build localtest
package uixt
import (
"testing"
)
func TestDriverExt_TapWithNumber(t *testing.T) {
driverExt, err := InitWDAClient(nil)
checkErr(t, err)
pathSearch := "/Users/hero/Documents/temp/2020-05/opencv/flag7.png"
err = driverExt.TapWithNumber(pathSearch, 3)
checkErr(t, err)
err = driverExt.TapWithNumberOffset(pathSearch, 3, 0.5, 0.75)
checkErr(t, err)
}
func TestDriverExt_TapXY(t *testing.T) {
driverExt, err := InitWDAClient(nil)
checkErr(t, err)
err = driverExt.TapXY(0.4, 0.5, "")
checkErr(t, err)
}
func TestDriverExt_TapAbsXY(t *testing.T) {
driverExt, err := InitWDAClient(nil)
checkErr(t, err)
err = driverExt.TapAbsXY(100, 300, "")
checkErr(t, err)
}
func TestDriverExt_TapWithOCR(t *testing.T) {
driverExt, err := InitWDAClient(nil)
checkErr(t, err)
// 需要点击文字上方的图标
err = driverExt.TapOffset("抖音", 0.5, -1, "", false)
checkErr(t, err)
}

33
hrp/pkg/uixt/touch.go Normal file
View File

@@ -0,0 +1,33 @@
package uixt
func (dExt *DriverExt) ForceTouch(pathname string, pressure float64, duration ...float64) (err error) {
return dExt.ForceTouchOffset(pathname, pressure, 0.5, 0.5, duration...)
}
func (dExt *DriverExt) ForceTouchOffset(pathname string, pressure, xOffset, yOffset float64, duration ...float64) (err error) {
if len(duration) == 0 {
duration = []float64{1.0}
}
var x, y, width, height float64
if x, y, width, height, err = dExt.FindUIRectInUIKit(pathname); err != nil {
return err
}
return dExt.Driver.ForceTouchFloat(x+width*xOffset, y+height*yOffset, pressure, duration[0])
}
func (dExt *DriverExt) TouchAndHold(pathname string, duration ...float64) (err error) {
return dExt.TouchAndHoldOffset(pathname, 0.5, 0.5, duration...)
}
func (dExt *DriverExt) TouchAndHoldOffset(pathname string, xOffset, yOffset float64, duration ...float64) (err error) {
if len(duration) == 0 {
duration = []float64{1.0}
}
var x, y, width, height float64
if x, y, width, height, err = dExt.FindUIRectInUIKit(pathname); err != nil {
return err
}
return dExt.Driver.TouchAndHoldFloat(x+width*xOffset, y+height*yOffset, duration[0])
}

View File

@@ -0,0 +1,39 @@
//go:build localtest
package uixt
import (
"testing"
)
func TestDriverExt_ForceTouch(t *testing.T) {
driverExt, err := InitWDAClient(nil)
checkErr(t, err)
pathSearch := "/Users/hero/Documents/temp/2020-05/opencv/IMG_ft.png"
err = driverExt.ForceTouch(pathSearch, 0.5, 3)
checkErr(t, err)
// err = driverExt.ForceTouchOffset(pathSearch, 0.5, 0.1, 0.9)
// checkErr(t, err)
// err = driverExt.ForceTouchOffset(pathSearch, 0.2, 1.1, -1)
// checkErr(t, err)
}
func TestDriverExt_TouchAndHold(t *testing.T) {
driverExt, err := InitWDAClient(nil)
checkErr(t, err)
pathSearch := "/Users/hero/Documents/temp/2020-05/opencv/IMG_ft.png"
// err = driverExt.TouchAndHold(pathSearch)
// checkErr(t, err)
// err = driverExt.TouchAndHold(pathSearch, 3)
// checkErr(t, err)
err = driverExt.TouchAndHoldOffset(pathSearch, 0.8, 0.1)
checkErr(t, err)
}