From 3a661a5be5380e69db7eb3fd9ed6bceb54a76b39 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Wed, 21 Sep 2022 17:50:08 +0800 Subject: [PATCH 1/9] fix: run ocr alone without opencv --- hrp/internal/uixt/opencv_on.go | 1 - 1 file changed, 1 deletion(-) diff --git a/hrp/internal/uixt/opencv_on.go b/hrp/internal/uixt/opencv_on.go index d4acda45..2153283f 100644 --- a/hrp/internal/uixt/opencv_on.go +++ b/hrp/internal/uixt/opencv_on.go @@ -5,7 +5,6 @@ package uixt import ( "bytes" "image" - "io/ioutil" "os" cvHelper "github.com/electricbubble/opencv-helper" From 5753955668643f043ad69b639e637a5e36331e4e Mon Sep 17 00:00:00 2001 From: xucong053 Date: Wed, 28 Sep 2022 13:46:34 +0800 Subject: [PATCH 2/9] refactor: android ui automation --- examples/uitest/demo_android_douyin_test.go | 48 + hrp/config.go | 21 + hrp/internal/uixt/android_action.go | 4 +- hrp/internal/uixt/android_device.go | 27 +- hrp/internal/uixt/android_driver.go | 1581 ++++++++----------- hrp/internal/uixt/android_elment.go | 423 ++--- hrp/internal/uixt/android_test.go | 1165 +++++++------- hrp/internal/uixt/client.go | 4 +- hrp/internal/uixt/ext.go | 22 +- hrp/internal/uixt/interface.go | 87 + hrp/internal/uixt/ios_device.go | 17 +- hrp/internal/uixt/ios_driver.go | 28 + hrp/step_android_ui.go | 257 ++- 13 files changed, 1912 insertions(+), 1772 deletions(-) create mode 100644 examples/uitest/demo_android_douyin_test.go diff --git a/examples/uitest/demo_android_douyin_test.go b/examples/uitest/demo_android_douyin_test.go new file mode 100644 index 00000000..4bf95693 --- /dev/null +++ b/examples/uitest/demo_android_douyin_test.go @@ -0,0 +1,48 @@ +package uitest + +import ( + "testing" + + "github.com/httprunner/httprunner/v4/hrp" +) + +func TestIOSDouYinLive(t *testing.T) { + testCase := &hrp.TestCase{ + Config: hrp.NewConfig("通过 feed 头像进入抖音直播间"). + SetAndroid(hrp.WithAdbLogOn(true), hrp.WithMjpegPortA(9100)), + TestSteps: []hrp.IStep{ + hrp.NewStep("启动抖音"). + Android(). + Home().StartCamera().Sleep(10).StopCamera(). + AppTerminate("com.ss.android.ugc.aweme"). // 关闭已运行的抖音,确保启动抖音后在「抖音」首页 + SwipeToTapApp("抖音", hrp.WithMaxRetryTimes(5)). + Sleep(10), + hrp.NewStep("处理青少年弹窗"). + Android(). + Tap("推荐"). + TapByOCR("我知道了", hrp.WithIgnoreNotFoundError(true)). + Validate(). + AssertOCRExists("首页", "抖音启动失败,「首页」不存在"), + hrp.NewStep("在推荐页上划,直到出现 feed 头像「直播」"). + Android(). + SwipeToTapText("直播", hrp.WithMaxRetryTimes(10), hrp.WithIdentifier("进入直播间")), + hrp.NewStep("向上滑动,等待 10s"). + Android(). + SwipeUp(hrp.WithIdentifier("第一次上划")).Sleep(10).ScreenShot(). // 上划 1 次,等待 10s,截图保存 + SwipeUp(hrp.WithIdentifier("第二次上划")).Sleep(10).ScreenShot(), // 再上划 1 次,等待 10s,截图保存 + }, + } + + if err := testCase.Dump2JSON("android_demo_douyin_live.json"); err != nil { + t.Fatal(err) + } + if err := testCase.Dump2YAML("android_demo_douyin_live.yaml"); err != nil { + t.Fatal(err) + } + + runner := hrp.NewRunner(t).SetSaveTests(true) + err := runner.Run(testCase) + if err != nil { + t.Fatal(err) + } +} diff --git a/hrp/config.go b/hrp/config.go index d5caffcf..3d1e1aa1 100644 --- a/hrp/config.go +++ b/hrp/config.go @@ -123,6 +123,27 @@ func (c *TConfig) SetIOS(options ...uixt.IOSDeviceOption) *TConfig { return c } +func (c *TConfig) SetAndroid(options ...uixt.AndroidDeviceOption) *TConfig { + uiaOptions := &uixt.AndroidDevice{} + for _, option := range options { + option(uiaOptions) + } + + // each device can have its own settings + if uiaOptions.SerialNumber != "" { + c.Android = append(c.Android, uiaOptions) + return c + } + + // device UDID is not specified, settings will be shared + if len(c.Android) == 0 { + c.Android = append(c.Android, uiaOptions) + } else { + c.Android[0] = uiaOptions + } + return c +} + type ThinkTimeConfig struct { Strategy thinkTimeStrategy `json:"strategy,omitempty" yaml:"strategy,omitempty"` // default、random、multiply、ignore Setting interface{} `json:"setting,omitempty" yaml:"setting,omitempty"` // random(map): {"min_percentage": 0.5, "max_percentage": 1.5}; 10、multiply(float64): 1.5 diff --git a/hrp/internal/uixt/android_action.go b/hrp/internal/uixt/android_action.go index 254972d3..b8081614 100644 --- a/hrp/internal/uixt/android_action.go +++ b/hrp/internal/uixt/android_action.go @@ -42,7 +42,7 @@ func (ta *TouchAction) AddPointF(point PointF, startTime ...float64) *TouchActio return ta.AddFloat(point.X, point.Y, startTime...) } -func (d *uiaDriver) MultiPointerGesture(gesture1 *TouchAction, gesture2 *TouchAction, tas ...*TouchAction) (err error) { +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) @@ -53,7 +53,7 @@ func (d *uiaDriver) MultiPointerGesture(gesture1 *TouchAction, gesture2 *TouchAc "actions": actions, } // register(postHandler, new MultiPointerGesture("/wd/hub/session/:sessionId/touch/multi/perform")) - _, err = d.httpPOST(data, "/session", d.sessionId, "/touch/multi/perform") + _, err = ud.httpPOST(data, "/session", ud.sessionId, "/touch/multi/perform") return } diff --git a/hrp/internal/uixt/android_device.go b/hrp/internal/uixt/android_device.go index f00880ad..275e4cb9 100644 --- a/hrp/internal/uixt/android_device.go +++ b/hrp/internal/uixt/android_device.go @@ -44,11 +44,15 @@ func InitUIAClient(device *AndroidDevice) (*DriverExt, error) { fmt.Println(driver) var driverExt *DriverExt - // TODO - // driverExt, err = Extend(driver) - // if err != nil { - // return nil, errors.Wrap(err, "failed to extend UIA Driver") - // } + + driverExt, err = Extend(driver) + if err != nil { + return nil, errors.Wrap(err, "failed to extend UIA Driver") + } + + if device.LogOn { + // TODO + } return driverExt, nil } @@ -61,6 +65,12 @@ func WithSerialNumber(serial string) AndroidDeviceOption { } } +func WithMjpegPortA(port int) AndroidDeviceOption { + return func(device *AndroidDevice) { + device.MjpegPort = port + } +} + func WithAdbIP(ip string) AndroidDeviceOption { return func(device *AndroidDevice) { device.IP = ip @@ -113,6 +123,7 @@ type AndroidDevice struct { 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"` } @@ -149,12 +160,6 @@ func (dev *AndroidDevice) NewUSBDriver(capabilities Capabilities) (driver *uiaDr driver.adbDevice = dev.d driver.localPort = localPort - conn, err := net.Dial("tcp", fmt.Sprintf(":%d", localPort)) - if err != nil { - return nil, fmt.Errorf("adb forward: %w", err) - } - driver.client = convertToHTTPClient(conn) - return driver, nil } diff --git a/hrp/internal/uixt/android_driver.go b/hrp/internal/uixt/android_driver.go index 7851a3da..77501047 100644 --- a/hrp/internal/uixt/android_driver.go +++ b/hrp/internal/uixt/android_driver.go @@ -6,18 +6,18 @@ import ( "encoding/json" "errors" "fmt" + "net" "net/url" - "os" - "path" - "path/filepath" - "regexp" "strconv" "strings" "time" "github.com/electricbubble/gadb" + "github.com/rs/zerolog/log" ) +var errDriverNotImplemented = errors.New("driver method not implemented") + type uiaDriver struct { Driver @@ -33,339 +33,24 @@ func NewUIADriver(capabilities Capabilities, urlPrefix string) (driver *uiaDrive if driver.urlPrefix, err = url.Parse(urlPrefix); err != nil { return nil, err } - if driver.sessionId, err = driver.NewSession(capabilities); err != nil { - return nil, err - } - return -} - -func (d *uiaDriver) NewSession(capabilities Capabilities) (sessionID string, err error) { - // register(postHandler, new NewSession("/wd/hub/session")) - var rawResp rawResponse - data := map[string]interface{}{"capabilities": capabilities} - if rawResp, err = d.httpPOST(data, "/session"); err != nil { - return "", err - } - reply := new(struct{ Value struct{ SessionId string } }) - if err = json.Unmarshal(rawResp, reply); err != nil { - return "", err - } - sessionID = reply.Value.SessionId - // d.sessionIdCache[sessionID] = true - return -} - -func (d *uiaDriver) Quit() (err error) { - // register(deleteHandler, new DeleteSession("/wd/hub/session/:sessionId")) - if d.sessionId == "" { - return nil - } - if _, err = d.httpDELETE("/session", d.sessionId); err == nil { - d.sessionId = "" - } - - return err -} - -func (d *uiaDriver) ActiveSessionID() string { - return d.sessionId -} - -func (d *uiaDriver) SessionIDs() (sessionIDs []string, err error) { - // register(getHandler, new GetSessions("/wd/hub/sessions")) - var rawResp rawResponse - if rawResp, err = d.httpGET("/sessions"); err != nil { - return nil, err - } - reply := new(struct{ Value []struct{ SessionId string } }) - if err = json.Unmarshal(rawResp, reply); err != nil { - return nil, err - } - - sessionIDs = make([]string, len(reply.Value)) - for i := range reply.Value { - sessionIDs[i] = reply.Value[i].SessionId - } - return -} - -func (d *uiaDriver) SessionDetails() (scrollData map[string]interface{}, err error) { - // register(getHandler, new GetSessionDetails("/wd/hub/session/:sessionId")) - var rawResp rawResponse - if rawResp, err = d.httpGET("/session", d.sessionId); err != nil { - return nil, err - } - reply := new(struct{ Value map[string]interface{} }) - if err = json.Unmarshal(rawResp, reply); err != nil { - return nil, err - } - - scrollData = reply.Value - return -} - -func (d *uiaDriver) Status() (ready bool, err error) { - // register(getHandler, new Status("/wd/hub/status")) - var rawResp rawResponse - if rawResp, err = d.httpGET("/status"); err != nil { - return false, err - } - reply := new(struct { - Value struct { - // Message string - Ready bool - } - }) - if err = json.Unmarshal(rawResp, reply); err != nil { - return false, err - } - ready = reply.Value.Ready - return -} - -// Screenshot grab device screenshot -func (d *uiaDriver) Screenshot() (raw *bytes.Buffer, err error) { - // register(getHandler, new CaptureScreenshot("/wd/hub/session/:sessionId/screenshot")) - var rawResp rawResponse - if rawResp, err = d.httpGET("/session", d.sessionId, "screenshot"); err != nil { - return nil, err - } - reply := new(struct{ Value string }) - if err = json.Unmarshal(rawResp, reply); err != nil { - return nil, err - } - - var decodeStr []byte - if decodeStr, err = base64.StdEncoding.DecodeString(reply.Value); err != nil { - return nil, err - } - - raw = bytes.NewBuffer(decodeStr) - return -} - -func (d *uiaDriver) Orientation() (orientation Orientation, err error) { - // register(getHandler, new GetOrientation("/wd/hub/session/:sessionId/orientation")) - var rawResp rawResponse - if rawResp, err = d.httpGET("/session", d.sessionId, "orientation"); err != nil { - return "", err - } - reply := new(struct{ Value Orientation }) - if err = json.Unmarshal(rawResp, reply); err != nil { - return "", err - } - - orientation = reply.Value - return -} - -func (d *uiaDriver) Rotation() (rotation Rotation, err error) { - // register(getHandler, new GetRotation("/wd/hub/session/:sessionId/rotation")) - var rawResp rawResponse - if rawResp, err = d.httpGET("/session", d.sessionId, "rotation"); err != nil { - return Rotation{}, err - } - reply := new(struct{ Value Rotation }) - if err = json.Unmarshal(rawResp, reply); err != nil { - return Rotation{}, err - } - - rotation = reply.Value - return -} - -// DeviceSize get window size of the device -func (d *uiaDriver) DeviceSize() (deviceSize Size, err error) { - // register(getHandler, new GetDeviceSize("/wd/hub/session/:sessionId/window/:windowHandle/size")) - var rawResp rawResponse - if rawResp, err = d.httpGET("/session", d.sessionId, "window/:windowHandle/size"); err != nil { - return Size{}, err - } - reply := new(struct{ Value Size }) - if err = json.Unmarshal(rawResp, reply); err != nil { - return Size{}, err - } - - deviceSize = reply.Value - return -} - -// Source get page source -func (d *uiaDriver) Source() (sXML string, err error) { - // register(getHandler, new Source("/wd/hub/session/:sessionId/source")) - var rawResp rawResponse - if rawResp, err = d.httpGET("/session", d.sessionId, "source"); err != nil { - return "", err - } - reply := new(struct{ Value string }) - if err = json.Unmarshal(rawResp, reply); err != nil { - return "", err - } - - sXML = reply.Value - return -} - -// StatusBarHeight get status bar height of the device -func (d *uiaDriver) StatusBarHeight() (height int, err error) { - // register(getHandler, new GetSystemBars("/wd/hub/session/:sessionId/appium/device/system_bars")) - var rawResp rawResponse - if rawResp, err = d.httpGET("/session", d.sessionId, "appium/device/system_bars"); err != nil { - return 0, err - } - reply := new(struct{ Value struct{ StatusBar int } }) - if err = json.Unmarshal(rawResp, reply); err != nil { - return 0, err - } - - height = reply.Value.StatusBar - return -} - -func (d *uiaDriver) check() error { - if d.adbDevice.Serial() == "" { - return errors.New("adb daemon: the device is not ready") - } - return nil -} - -// Dispose corresponds to the command: -// adb -s $serial forward --remove $localPort -func (d *uiaDriver) Dispose() (err error) { - if err = d.check(); err != nil { - return err - } - if d.localPort == 0 { - return nil - } - return d.adbDevice.ForwardKill(d.localPort) -} - -func (d *uiaDriver) ActiveAppActivity() (appActivity string, err error) { - if err = d.check(); err != nil { - return "", err - } - - var sOutput string - if sOutput, err = d.adbDevice.RunShellCommand("dumpsys activity activities | grep mResumedActivity"); err != nil { - return "", err - } - re := regexp.MustCompile(`\{(.+?)\}`) - if !re.MatchString(sOutput) { - return "", fmt.Errorf("active app activity: %s", strings.TrimSpace(sOutput)) - } - fields := strings.Fields(re.FindStringSubmatch(sOutput)[1]) - appActivity = fields[2] - return -} - -func (d *uiaDriver) ActiveAppPackageName() (appPackageName string, err error) { - var activity string - if activity, err = d.ActiveAppActivity(); err != nil { - return "", err - } - appPackageName = strings.Split(activity, "/")[0] - return -} - -func (d *uiaDriver) AppLaunch(appPackageName string, waitForComplete ...AndroidBySelector) (err error) { - if err = d.check(); err != nil { - return err - } - - var sOutput string - if sOutput, err = d.adbDevice.RunShellCommand("monkey -p", appPackageName, "-c android.intent.category.LAUNCHER 1"); err != nil { - return err - } - if strings.Contains(sOutput, "monkey aborted") { - return fmt.Errorf("app launch: %s", strings.TrimSpace(sOutput)) - } - - if len(waitForComplete) != 0 { - var ce error - exists := func(d *uiaDriver) (bool, error) { - for i := range waitForComplete { - _, ce = d.FindElement(waitForComplete[i]) - if ce == nil { - return true, nil - } - } - return false, nil - } - if err = d.WaitWithTimeoutAndInterval(exists, 45, 1.5); err != nil { - return fmt.Errorf("app launch (waitForComplete): %s: %w", err.Error(), ce) + var localPort int + { + tmpURL, _ := url.Parse(driver.urlPrefix.String()) + hostname := tmpURL.Hostname() + if strings.HasPrefix(hostname, forwardToPrefix) { + localPort, _ = strconv.Atoi(strings.TrimPrefix(hostname, forwardToPrefix)) } } - return -} - -func (d *uiaDriver) AppTerminate(appPackageName string) (err error) { - if err = d.check(); err != nil { - return err - } - - _, err = d.adbDevice.RunShellCommand("am force-stop", appPackageName) - return -} - -func (d *uiaDriver) AppInstall(apkPath string, reinstall ...bool) (err error) { - if err = d.check(); err != nil { - return err - } - - apkName := filepath.Base(apkPath) - if !strings.HasSuffix(strings.ToLower(apkName), ".apk") { - return fmt.Errorf("apk file must have an extension of '.apk': %s", apkPath) - } - - var apkFile *os.File - if apkFile, err = os.Open(apkPath); err != nil { - return fmt.Errorf("apk file: %w", err) - } - - remotePath := path.Join(DeviceTempPath, apkName) - if err = d.adbDevice.PushFile(apkFile, remotePath); err != nil { - return fmt.Errorf("apk push: %w", err) - } - - var shellOutput string - if len(reinstall) != 0 && reinstall[0] { - shellOutput, err = d.adbDevice.RunShellCommand("pm install", "-r", remotePath) - } else { - shellOutput, err = d.adbDevice.RunShellCommand("pm install", remotePath) - } - + conn, err := net.Dial("tcp", fmt.Sprintf(":%d", localPort)) if err != nil { - return fmt.Errorf("apk install: %w", err) + return nil, fmt.Errorf("adb forward: %w", err) } - - if !strings.Contains(shellOutput, "Success") { - return fmt.Errorf("apk installed: %s", shellOutput) - } - - return -} - -func (d *uiaDriver) AppUninstall(appPackageName string, keepDataAndCache ...bool) (err error) { - if err = d.check(); err != nil { - return err - } - - var shellOutput string - if len(keepDataAndCache) != 0 && keepDataAndCache[0] { - shellOutput, err = d.adbDevice.RunShellCommand("pm uninstall", "-k", appPackageName) + driver.client = convertToHTTPClient(conn) + if session, err := driver.NewSession(capabilities); err != nil { + return nil, err } else { - shellOutput, err = d.adbDevice.RunShellCommand("pm uninstall", appPackageName) + driver.sessionId = session.SessionId } - - if err != nil { - return fmt.Errorf("apk uninstall: %w", err) - } - - if !strings.Contains(shellOutput, "Success") { - return fmt.Errorf("apk uninstalled: %s", shellOutput) - } - return } @@ -397,132 +82,267 @@ func (bs BatteryStatus) String() string { } } -func (d *uiaDriver) BatteryInfo() (info BatteryInfo, err error) { - // register(getHandler, new GetBatteryInfo("/wd/hub/session/:sessionId/appium/device/battery_info")) - var rawResp rawResponse - if rawResp, err = d.httpGET("/session", d.sessionId, "appium/device/battery_info"); err != nil { - return BatteryInfo{}, err +func (ud *uiaDriver) Close() (err error) { + if ud.sessionId == "" { + return nil } - reply := new(struct{ Value BatteryInfo }) - if err = json.Unmarshal(rawResp, reply); err != nil { - return BatteryInfo{}, err + if _, err = ud.httpDELETE("/session", ud.sessionId); err == nil { + ud.sessionId = "" } - info = reply.Value - if info.Level == -1 || info.Status == -1 { - return info, errors.New("cannot be retrieved from the system") - } - return + return err } -func (d *uiaDriver) GetAppiumSettings() (settings map[string]interface{}, err error) { - // register(getHandler, new GetSettings("/wd/hub/session/:sessionId/appium/settings")) +func (ud *uiaDriver) NewSession(capabilities Capabilities) (sessionInfo SessionInfo, err error) { + // register(postHandler, new NewSession("/wd/hub/session")) var rawResp rawResponse - if rawResp, err = d.httpGET("/session", d.sessionId, "appium/settings"); err != nil { + 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 map[string]interface{} }) + reply := new(struct{ Value []struct{ SessionId string } }) if err = json.Unmarshal(rawResp, reply); err != nil { return nil, err } - settings = reply.Value + sessionIDs = make([]string, len(reply.Value)) + for i := range reply.Value { + sessionIDs[i] = reply.Value[i].SessionId + } return } -// DeviceScaleRatio get device pixel ratio -func (d *uiaDriver) DeviceScaleRatio() (scale float64, err error) { - // register(getHandler, new GetDevicePixelRatio("/wd/hub/session/:sessionId/appium/device/pixel_ratio")) +func (ud *uiaDriver) SessionDetails() (scrollData map[string]interface{}, err error) { + // register(getHandler, new GetSessionDetails("/wd/hub/session/:sessionId")) var rawResp rawResponse - if rawResp, err = d.httpGET("/session", d.sessionId, "appium/device/pixel_ratio"); err != nil { - return 0, err + if rawResp, err = ud.httpGET("/session", ud.sessionId); err != nil { + return nil, err } - reply := new(struct{ Value float64 }) + var reply = new(struct{ Value map[string]interface{} }) if err = json.Unmarshal(rawResp, reply); err != nil { - return 0, err + return nil, err } - scale = reply.Value + scrollData = reply.Value return } -type ( - AndroidDeviceInfo struct { - // ANDROID_ID A 64-bit number (as a hex string) that is uniquely generated when the user - // first sets up the device and should remain constant for the lifetime of the user's device. The value - // may change if a factory reset is performed on the device. - AndroidID string `json:"androidId"` - // Build.MANUFACTURER value - Manufacturer string `json:"manufacturer"` - // Build.MODEL value - Model string `json:"model"` - // Build.BRAND value - Brand string `json:"brand"` - // Current running OS's API VERSION - APIVersion string `json:"apiVersion"` - // The current version string, for example "1.0" or "3.4b5" - PlatformVersion string `json:"platformVersion"` - // the name of the current celluar network carrier - CarrierName string `json:"carrierName"` - // the real size of the default display - RealDisplaySize string `json:"realDisplaySize"` - // The logical density of the display in Density Independent Pixel units. - DisplayDensity int `json:"displayDensity"` - // available networks - Networks []networkInfo `json:"networks"` - // current system locale - Locale string `json:"locale"` - // current system timezone - // e.g. "Asia/Tokyo", "America/Caracas", "Asia/Shanghai" - TimeZone string `json:"timeZone"` - Bluetooth struct { - State string `json:"state"` - } `json:"bluetooth"` - } - networkCapabilities struct { - TransportTypes string `json:"transportTypes"` - NetworkCapabilities string `json:"networkCapabilities"` - LinkUpstreamBandwidthKbps int `json:"linkUpstreamBandwidthKbps"` - LinkDownBandwidthKbps int `json:"linkDownBandwidthKbps"` - SignalStrength int `json:"signalStrength"` - SSID string `json:"SSID"` - } - networkInfo struct { - Type int `json:"type"` - TypeName string `json:"typeName"` - Subtype int `json:"subtype"` - SubtypeName string `json:"subtypeName"` - IsConnected bool `json:"isConnected"` - DetailedState string `json:"detailedState"` - State string `json:"state"` - ExtraInfo string `json:"extraInfo"` - IsAvailable bool `json:"isAvailable"` - IsRoaming bool `json:"isRoaming"` - IsFailover bool `json:"isFailover"` - Capabilities networkCapabilities `json:"capabilities"` - } -) +func (ud *uiaDriver) DeleteSession() (err error) { + // TODO + return errDriverNotImplemented +} -func (d *uiaDriver) DeviceInfo() (info AndroidDeviceInfo, err error) { +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 = d.httpGET("/session", d.sessionId, "appium/device/info"); err != nil { - return AndroidDeviceInfo{}, err + if rawResp, err = ud.httpGET("/session", ud.sessionId, "appium/device/info"); err != nil { + return DeviceInfo{}, err } - reply := new(struct{ Value AndroidDeviceInfo }) + reply := new(struct{ Value struct{ DeviceInfo } }) if err = json.Unmarshal(rawResp, reply); err != nil { - return AndroidDeviceInfo{}, err + return DeviceInfo{}, err } - - info = reply.Value + deviceInfo = reply.Value.DeviceInfo return } -// AlertText get text of the on-screen dialog -func (d *uiaDriver) AlertText() (text string, err error) { +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("am", "start", "-a", "android.media.action.VIDEO_CAPTURE"); 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) StartRecording() (err error) { + var res string + if res, err = ud.adbDevice.RunShellCommand("input", "keyevent", "27"); err != nil { + return err + } + log.Info().Str("shell", res) + return +} + +func (ud *uiaDriver) StopRecording() (err error) { + var res string + if res, err = ud.adbDevice.RunShellCommand("input", "keyevent", "27"); err != nil { + return err + } + log.Info().Str("shell", res) + 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 = d.httpGET("/session", d.sessionId, "alert/text"); err != nil { + if rawResp, err = ud.httpGET("/session", ud.sessionId, "alert/text"); err != nil { return "", err } reply := new(struct{ Value string }) @@ -534,30 +354,202 @@ func (d *uiaDriver) AlertText() (text string, err error) { return } -// Tap perform a click at arbitrary coordinates specified -func (d *uiaDriver) Tap(x, y int) (err error) { - return d.TapFloat(float64(x), float64(y)) +func (ud *uiaDriver) AlertButtons() (btnLabels []string, err error) { + // TODO + return btnLabels, errDriverNotImplemented } -func (d *uiaDriver) TapFloat(x, y float64) (err error) { +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)) +} + +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, } - _, err = d.httpPOST(data, "/session", d.sessionId, "appium/tap") + _, err = ud.httpPOST(data, "/session", ud.sessionId, "appium/tap") return } -func (d *uiaDriver) TapPoint(point Point) (err error) { - return d.Tap(point.X, point.Y) +func (ud *uiaDriver) DoubleTap(x, y int) error { + return ud.DoubleTapFloat(float64(x), float64(y)) } -func (d *uiaDriver) TapPointF(point PointF) (err error) { - return d.TapFloat(point.X, point.Y) +func (ud *uiaDriver) DoubleTapFloat(x, y float64) (err error) { + // TODO + return errDriverNotImplemented } -func (d *uiaDriver) _swipe(startX, startY, endX, endY interface{}, steps int, elementID ...string) (err error) { +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 WDA configurations + // e.g. use WithPressDuration to set pressForDuration + 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{}, steps int, elementID ...string) (err error) { // register(postHandler, new Swipe("/wd/hub/session/:sessionId/touch/perform")) data := map[string]interface{}{ "startX": startX, @@ -569,7 +561,7 @@ func (d *uiaDriver) _swipe(startX, startY, endX, endY interface{}, steps int, el if len(elementID) != 0 { data["elementId"] = elementID[0] } - _, err = d.httpPOST(data, "/session", d.sessionId, "touch/perform") + _, err = ud.httpPOST(data, "/session", ud.sessionId, "touch/perform") return } @@ -577,412 +569,52 @@ func (d *uiaDriver) _swipe(startX, startY, endX, endY interface{}, steps int, el // to determine smoothness and speed. Each step execution is throttled to 5ms // per step. So for a 100 steps, the swipe will take about 1/2 second to complete. // `steps` is the number of move steps sent to the system -func (d *uiaDriver) Swipe(startX, startY, endX, endY int, steps ...int) (err error) { - return d.SwipeFloat(float64(startX), float64(startY), float64(endX), float64(endY), steps...) +func (ud *uiaDriver) Swipe(fromX, fromY, toX, toY int, options ...DataOption) error { + options = append(options, WithPressDuration(0)) + return ud.SwipeFloat(float64(fromX), float64(fromY), float64(toX), float64(toY), options...) } -func (d *uiaDriver) SwipeFloat(startX, startY, endX, endY float64, steps ...int) (err error) { - if len(steps) == 0 { - steps = []int{12} +func (ud *uiaDriver) SwipeFloat(fromX, fromY, toX, toY float64, options ...DataOption) error { + data := map[string]interface{}{} + // append options in post data for extra WDA configurations + // e.g. use WithPressDuration to set pressForDuration + for _, option := range options { + option(data) } - return d._swipe(startX, startY, endX, endY, steps[0]) -} -func (d *uiaDriver) SwipePoint(startPoint, endPoint Point, steps ...int) (err error) { - return d.Swipe(startPoint.X, startPoint.Y, endPoint.X, endPoint.Y, steps...) -} - -func (d *uiaDriver) SwipePointF(startPoint, endPoint PointF, steps ...int) (err error) { - return d.SwipeFloat(startPoint.X, startPoint.Y, endPoint.X, endPoint.Y, steps...) -} - -func (d *uiaDriver) _drag(data map[string]interface{}) (err error) { - // register(postHandler, new Drag("/wd/hub/session/:sessionId/touch/drag")) - _, err = d.httpPOST(data, "/session", d.sessionId, "touch/drag") - return -} - -// Drag performs a swipe from one coordinate to another coordinate. You can control -// the smoothness and speed of the swipe by specifying the number of steps. -// Each step execution is throttled to 5 milliseconds per step, so for a 100 -// steps, the swipe will take around 0.5 seconds to complete. -func (d *uiaDriver) Drag(startX, startY, endX, endY int, steps ...int) (err error) { - return d.DragFloat(float64(startX), float64(startY), float64(endX), float64(endY), steps...) -} - -func (d *uiaDriver) DragFloat(startX, startY, endX, endY float64, steps ...int) error { - if len(steps) == 0 { - steps = []int{12} + if _, ok := data["steps"]; !ok { + data["steps"] = 12 // default steps } + + return ud._swipe(fromX, fromY, toX, toY, data["steps"].(int)) +} + +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{}{ - "startX": startX, - "startY": startY, - "endX": endX, - "endY": endY, - "steps": steps[0], - } - return d._drag(data) -} - -func (d *uiaDriver) DragPoint(startPoint Point, endPoint Point, steps ...int) error { - return d.Drag(startPoint.X, startPoint.Y, endPoint.X, endPoint.Y, steps...) -} - -func (d *uiaDriver) DragPointF(startPoint PointF, endPoint PointF, steps ...int) (err error) { - return d.DragFloat(startPoint.X, startPoint.Y, endPoint.X, endPoint.Y, steps...) -} - -func (d *uiaDriver) TouchLongClick(x, y int, duration ...float64) (err error) { - if len(duration) == 0 { - duration = []float64{1.0} - } - // register(postHandler, new TouchLongClick("/wd/hub/session/:sessionId/touch/longclick")) - data := map[string]interface{}{ - "params": map[string]interface{}{ - "x": x, - "y": y, - "duration": int(duration[0] * 1000), - }, - } - _, err = d.httpPOST(data, "/session", d.sessionId, "touch/longclick") - return -} - -func (d *uiaDriver) TouchLongClickPoint(point Point, duration ...float64) (err error) { - return d.TouchLongClick(point.X, point.Y, duration...) -} - -func (d *uiaDriver) SendKeys(text string, isReplace ...bool) (err error) { - if len(isReplace) == 0 { - isReplace = []bool{true} - } - // register(postHandler, new SendKeysToElement("/wd/hub/session/:sessionId/keys")) - // https://github.com/appium/appium-uiautomator2-server/blob/master/app/src/main/java/io/appium/uiautomator2/handler/SendKeysToElement.java#L76-L85 - data := map[string]interface{}{ - "text": text, - "replace": isReplace[0], - } - _, err = d.httpPOST(data, "/session", d.sessionId, "keys") - return -} - -// PressBack simulates a short press on the BACK button. -func (d *uiaDriver) PressBack() (err error) { - // register(postHandler, new PressBack("/wd/hub/session/:sessionId/back")) - _, err = d.httpPOST(nil, "/session", d.sessionId, "back") - return -} - -// public class KeyCodeModel extends BaseModel { -// @RequiredField -// public Integer keycode; -// public Integer metastate; -// public Integer flags; -// } -func (d *uiaDriver) LongPressKeyCode(keyCode KeyCode, metaState KeyMeta, flags ...KeyFlag) (err error) { - if len(flags) == 0 { - flags = []KeyFlag{KFFromSystem} - } - data := map[string]interface{}{ - "keycode": keyCode, - "metastate": metaState, - "flags": flags[0], - } - // register(postHandler, new LongPressKeyCode("/wd/hub/session/:sessionId/appium/device/long_press_keycode")) - _, err = d.httpPOST(data, "/session", d.sessionId, "/appium/device/long_press_keycode") - return -} - -func (d *uiaDriver) _pressKeyCode(keyCode KeyCode, metaState KeyMeta, flags ...KeyFlag) (err error) { - // register(postHandler, new PressKeyCodeAsync("/wd/hub/session/:sessionId/appium/device/press_keycode")) - data := map[string]interface{}{ - "keycode": keyCode, - } - if metaState != KMEmpty { - data["metastate"] = metaState - } - if len(flags) != 0 { - data["flags"] = flags[0] - } - _, err = d.httpPOST(data, "/session", d.sessionId, "appium/device/press_keycode") - return -} - -func (d *uiaDriver) PressKeyCode(keyCode KeyCode, metaState KeyMeta, flags ...KeyFlag) (err error) { - if len(flags) == 0 { - flags = []KeyFlag{KFFromSystem} - } - return d._pressKeyCode(keyCode, metaState, KFFromSystem) -} - -// PressKeyCodeAsync simulates a short press using a key code. -func (d *uiaDriver) PressKeyCodeAsync(keyCode KeyCode, metaState ...KeyMeta) (err error) { - if len(metaState) == 0 { - metaState = []KeyMeta{KMEmpty} - } - return d._pressKeyCode(keyCode, metaState[0]) -} - -func (d *uiaDriver) TouchDown(x, y int) (err error) { - // register(postHandler, new TouchDown("/wd/hub/session/:sessionId/touch/down")) - data := map[string]interface{}{ - "params": map[string]interface{}{ - "x": x, - "y": y, - }, - } - _, err = d.httpPOST(data, "/session", d.sessionId, "touch/down") - return -} - -func (d *uiaDriver) TouchDownPoint(point Point) error { - return d.TouchDown(point.X, point.Y) -} - -func (d *uiaDriver) TouchUp(x, y int) (err error) { - // register(postHandler, new TouchUp("/wd/hub/session/:sessionId/touch/up")) - data := map[string]interface{}{ - "params": map[string]interface{}{ - "x": x, - "y": y, - }, - } - _, err = d.httpPOST(data, "/session", d.sessionId, "touch/up") - return -} - -func (d *uiaDriver) TouchUpPoint(point Point) error { - return d.TouchUp(point.X, point.Y) -} - -func (d *uiaDriver) TouchMove(x, y int) (err error) { - // register(postHandler, new TouchMove("/wd/hub/session/:sessionId/touch/move")) - data := map[string]interface{}{ - "params": map[string]interface{}{ - "x": x, - "y": y, - }, - } - _, err = d.httpPOST(data, "/session", d.sessionId, "touch/move") - return -} - -func (d *uiaDriver) TouchMovePoint(point Point) error { - return d.TouchMove(point.X, point.Y) -} - -// OpenNotification opens the notification shade. -func (d *uiaDriver) OpenNotification() (err error) { - // register(postHandler, new OpenNotification("/wd/hub/session/:sessionId/appium/device/open_notifications")) - _, err = d.httpPOST(nil, "/session", d.sessionId, "appium/device/open_notifications") - return -} - -func (d *uiaDriver) _flick(data map[string]interface{}) (err error) { - // register(postHandler, new Flick("/wd/hub/session/:sessionId/touch/flick")) - _, err = d.httpPOST(data, "/session", d.sessionId, "touch/flick") - return -} - -func (d *uiaDriver) Flick(xSpeed, ySpeed int) (err error) { - data := map[string]interface{}{ - "xspeed": xSpeed, - "yspeed": ySpeed, - } - if xSpeed == 0 && ySpeed == 0 { - return errors.New("both 'xSpeed' and 'ySpeed' cannot be zero") - } - - return d._flick(data) -} - -func (d *uiaDriver) _scrollTo(method, selector string, maxSwipes int, elementID ...string) (err error) { - // register(postHandler, new ScrollTo("/wd/hub/session/:sessionId/touch/scroll")) - params := map[string]interface{}{ - "strategy": method, - "selector": selector, - } - if maxSwipes > 0 { - params["maxSwipes"] = maxSwipes - } - data := map[string]interface{}{"params": params} - if len(elementID) != 0 { - data["origin"] = map[string]string{ - legacyWebElementIdentifier: elementID[0], - webElementIdentifier: elementID[0], - } - } - _, err = d.httpPOST(data, "/session", d.sessionId, "touch/scroll") - return -} - -func (d *uiaDriver) ScrollTo(by AndroidBySelector, maxSwipes ...int) (err error) { - if len(maxSwipes) == 0 { - maxSwipes = []int{0} - } - method, selector := by.getMethodAndSelector() - return d._scrollTo(method, selector, maxSwipes[0]) -} - -type W3CMouseButtonType int - -const ( - MBTLeft W3CMouseButtonType = 0 - MBTMiddle W3CMouseButtonType = 1 - MBTRight W3CMouseButtonType = 2 -) - -func (g *W3CGestures) PointerDown(button ...W3CMouseButtonType) *W3CGestures { - if len(button) == 0 { - button = []W3CMouseButtonType{MBTLeft} - } - *g = append(*g, _newW3CGesture().pointerDown(int(button[0]))) - return g -} - -func (g *W3CGestures) PointerUp(button ...W3CMouseButtonType) *W3CGestures { - if len(button) == 0 { - button = []W3CMouseButtonType{MBTLeft} - } - *g = append(*g, _newW3CGesture().pointerUp(int(button[0]))) - return g -} - -type W3CPointerMoveType string - -const ( - PMTViewport W3CPointerMoveType = "viewport" - PMTPointer W3CPointerMoveType = "pointer" -) - -func (g *W3CGestures) PointerMove(x, y float64, origin interface{}, duration float64, pressure, size float64) *W3CGestures { - val := "" - switch v := origin.(type) { - case string: - val = v - case W3CPointerMoveType: - val = string(v) - case *uiaElement: - val = v.id - default: - val = string(PMTViewport) - } - *g = append(*g, _newW3CGesture().pointerMove(x, y, val, duration, pressure, size)) - return g -} - -func (g *W3CGestures) PointerMoveTo(x, y float64, duration ...float64) *W3CGestures { - if len(duration) == 0 || duration[0] < 0 { - duration = []float64{0.5} - } - *g = append(*g, _newW3CGesture().pointerMove(x, y, string(PMTViewport), duration[0]*1000)) - return g -} - -func (g *W3CGestures) PointerMoveRelative(x, y float64, duration ...float64) *W3CGestures { - if len(duration) == 0 || duration[0] < 0 { - duration = []float64{0.5} - } - *g = append(*g, _newW3CGesture().pointerMove(x, y, string(PMTPointer), duration[0]*1000)) - return g -} - -func (g *W3CGestures) PointerMouseOver(x, y float64, element *uiaElement, duration ...float64) *W3CGestures { - if len(duration) == 0 || duration[0] < 0 { - duration = []float64{0.5} - } - *g = append(*g, _newW3CGesture().pointerMove(x, y, element.id, duration[0]*1000)) - return g -} - -type W3CAction map[string]interface{} - -type W3CActionType string - -const ( - _ W3CActionType = "none" - ATKey W3CActionType = "key" - ATPointer W3CActionType = "pointer" -) - -type W3CPointerType string - -const ( - PTMouse W3CPointerType = "mouse" - PTPen W3CPointerType = "pen" - PTTouch W3CPointerType = "touch" -) - -func NewW3CAction(actionType W3CActionType, gestures *W3CGestures, pointerType ...W3CPointerType) W3CAction { - w3cAction := make(W3CAction) - w3cAction["type"] = actionType - w3cAction["actions"] = gestures - if actionType != ATPointer { - return w3cAction - } - - if len(pointerType) == 0 { - pointerType = []W3CPointerType{PTTouch} - } - type W3CItemParameters struct { - PointerType W3CPointerType `json:"pointerType"` - } - w3cAction["parameters"] = W3CItemParameters{PointerType: pointerType[0]} - return w3cAction -} - -func (d *uiaDriver) PerformW3CActions(action W3CAction, acts ...W3CAction) (err error) { - var actionId uint64 = 1 - acts = append([]W3CAction{action}, acts...) - for i := range acts { - item := acts[i] - item["id"] = strconv.FormatUint(actionId, 10) - actionId++ - acts[i] = item - } - data := map[string]interface{}{ - "actions": acts, + "actions": actions, } // register(postHandler, new W3CActions("/wd/hub/session/:sessionId/actions")) - _, err = d.httpPOST(data, "/session", d.sessionId, "/actions") + _, err = ud.httpPOST(data, "/session", ud.sessionId, "/actions") return } -type ClipDataType string - -const ClipDataTypePlaintext ClipDataType = "PLAINTEXT" - -func (d *uiaDriver) GetClipboard(contentType ...ClipDataType) (content string, err error) { - if len(contentType) == 0 { - contentType = []ClipDataType{ClipDataTypePlaintext} - } - // register(postHandler, new GetClipboard("/wd/hub/session/:sessionId/appium/device/get_clipboard")) - data := map[string]interface{}{ - "contentType": contentType[0], - } - var rawResp rawResponse - if rawResp, err = d.httpPOST(data, "/session", d.sessionId, "appium/device/get_clipboard"); err != nil { - return "", err - } - reply := new(struct{ Value string }) - if err = json.Unmarshal(rawResp, reply); err != nil { - return "", err - } - - content = reply.Value - if data, err := base64.StdEncoding.DecodeString(content); err != nil { - return content, err - } else { - content = string(data) - } - return +func (ud *uiaDriver) PerformAppiumTouchActions(touchActs *TouchActions) (err error) { + // TODO + return errDriverNotImplemented } -func (d *uiaDriver) SetClipboard(contentType ClipDataType, content string, label ...string) (err error) { +func (ud *uiaDriver) SetPasteboard(contentType PasteboardType, content string) (err error) { lbl := content - if len(label) != 0 { - lbl = label[0] - } + const defaultLabelLen = 10 if len(lbl) > defaultLabelLen { lbl = lbl[:defaultLabelLen] @@ -994,89 +626,132 @@ func (d *uiaDriver) SetClipboard(contentType ClipDataType, content string, label "content": base64.StdEncoding.EncodeToString([]byte(content)), } // register(postHandler, new SetClipboard("/wd/hub/session/:sessionId/appium/device/set_clipboard")) - _, err = d.httpPOST(data, "/session", d.sessionId, "appium/device/set_clipboard") + _, err = ud.httpPOST(data, "/session", ud.sessionId, "appium/device/set_clipboard") return } -func (d *uiaDriver) AlertAccept(buttonLabel ...string) (err error) { +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{}{ - "buttonLabel": nil, + "contentType": contentType[0], } - if len(buttonLabel) != 0 { - data["buttonLabel"] = buttonLabel[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) } - // register(postHandler, new AcceptAlert("/wd/hub/session/:sessionId/alert/accept")) - _, err = d.httpPOST(data, "/session", d.sessionId, "alert/accept") return } -func (d *uiaDriver) AlertDismiss(buttonLabel ...string) (err error) { +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{}{ - "buttonLabel": nil, + "text": text, } - if len(buttonLabel) != 0 { - data["buttonLabel"] = buttonLabel[0] + // append options in post data for extra WDA configurations + // e.g. use WithPressDuration to set pressForDuration + for _, option := range options { + option(data) } - // register(postHandler, new DismissAlert("/wd/hub/session/:sessionId/alert/dismiss")) - _, err = d.httpPOST(data, "/session", d.sessionId, "alert/dismiss") + + if _, ok := data["isReplace"]; !ok { + data["isReplace"] = true // default true + } + + _, err = ud.httpPOST(data, "/session", ud.sessionId, "keys") return } -func (d *uiaDriver) SetAppiumSettings(settings map[string]interface{}) (err error) { - data := map[string]interface{}{ - "settings": settings, - } - // register(postHandler, new UpdateSettings("/wd/hub/session/:sessionId/appium/settings")) - _, err = d.httpPOST(data, "/session", d.sessionId, "appium/settings") +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 (d *uiaDriver) SetOrientation(orientation Orientation) (err error) { - data := map[string]interface{}{ - "orientation": orientation, +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 } - // register(postHandler, new SetOrientation("/wd/hub/session/:sessionId/orientation")) - _, err = d.httpPOST(data, "/session", d.sessionId, "orientation") + reply := new(struct{ Value Orientation }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return "", err + } + + orientation = reply.Value return } -// SetRotation -// `x` and `y` are ignored. We only care about `z` -// 0/90/180/270 -func (d *uiaDriver) SetRotation(rotation Rotation) (err error) { - data := map[string]interface{}{ - "z": rotation.Z, +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 } - // register(postHandler, new SetRotation("/wd/hub/session/:sessionId/rotation")) - _, err = d.httpPOST(data, "/session", d.sessionId, "rotation") + reply := new(struct{ Value Rotation }) + if err = json.Unmarshal(rawResp, reply); err != nil { + return Rotation{}, err + } + + rotation = reply.Value return } -type NetworkType int - -const ( - NetworkTypeWifi NetworkType = 2 - - // NetworkTypeNone NetworkType = iota - // NetworkTypeAirplane - // NetworkTypeWifi - // _ - // NetworkTypeData - // _ - // NetworkTypeAll -) - -// NetworkConnection always turn on -func (d *uiaDriver) NetworkConnection(networkType NetworkType) (err error) { - // register(postHandler, new NetworkConnection("/wd/hub/session/:sessionId/network_connection")) - data := map[string]interface{}{ - "type": networkType, - } - _, err = d.httpPOST(data, "/session", d.sessionId, "network_connection") - return +func (ud *uiaDriver) SetRotation(rotation Rotation) (err error) { + // TODO + return errDriverNotImplemented } -func (d *uiaDriver) _findElements(method, selector string, elementID ...string) (elements []*uiaElement, err error) { +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, @@ -1086,7 +761,7 @@ func (d *uiaDriver) _findElements(method, selector string, elementID ...string) data["context"] = elementID[0] } var rawResp rawResponse - if rawResp, err = d.httpPOST(data, "/session", d.sessionId, "/elements"); err != nil { + if rawResp, err = ud.httpPOST(data, "/session", ud.sessionId, "/elements"); err != nil { return nil, err } reply := new(struct{ Value []map[string]string }) @@ -1096,18 +771,19 @@ func (d *uiaDriver) _findElements(method, selector string, elementID ...string) if len(reply.Value) == 0 { return nil, fmt.Errorf("no such element: unable to find an element using '%s', value '%s'", method, selector) } - elements = make([]*uiaElement, len(reply.Value)) + 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) } - elements[i] = &uiaElement{parent: d, id: id} + uie := WebElement(uiaElement{parent: ud, id: id}) + elements[i] = uie } return } -func (d *uiaDriver) _findElement(method, selector string, elementID ...string) (elem *uiaElement, err error) { +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, @@ -1117,7 +793,7 @@ func (d *uiaDriver) _findElement(method, selector string, elementID ...string) ( data["context"] = elementID[0] } var rawResp rawResponse - if rawResp, err = d.httpPOST(data, "/session", d.sessionId, "/element"); err != nil { + if rawResp, err = ud.httpPOST(data, "/session", ud.sessionId, "/element"); err != nil { return nil, err } reply := new(struct{ Value map[string]string }) @@ -1131,45 +807,122 @@ func (d *uiaDriver) _findElement(method, selector string, elementID ...string) ( if id = elementIDFromValue(reply.Value); id == "" { return nil, fmt.Errorf("invalid element returned: %+v", reply) } - elem = &uiaElement{parent: d, id: id} + elem = &uiaElement{parent: ud, id: id} return } -func (d *uiaDriver) FindElements(by AndroidBySelector) (elements []*uiaElement, err error) { - return d._findElements(by.getMethodAndSelector()) +func (ud *uiaDriver) ActiveElement() (element WebElement, err error) { + // TODO + return element, errDriverNotImplemented } -func (d *uiaDriver) FindElement(by AndroidBySelector) (elem *uiaElement, err error) { - return d._findElement(by.getMethodAndSelector()) +func (ud *uiaDriver) FindElement(by BySelector) (element WebElement, err error) { + return ud._findElement(by.getUsingAndValue()) } -func (d *uiaDriver) ActiveElement() (elem *uiaElement, err error) { - // register(getHandler, new ActiveElement("/wd/hub/session/:sessionId/element/active")) +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 = d.httpGET("/session", d.sessionId, "/element/active"); err != nil { + if rawResp, err = ud.httpPOST(data, "/session", ud.sessionId, "/elements"); err != nil { return nil, err } - reply := new(struct{ Value map[string]string }) + 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 } - if len(reply.Value) == 0 { - return nil, errors.New("no such element") + + var decodeStr []byte + if decodeStr, err = base64.StdEncoding.DecodeString(reply.Value); err != nil { + return nil, err } - var id string - if id = elementIDFromValue(reply.Value); id == "" { - return nil, fmt.Errorf("invalid element returned: %+v", reply) - } - elem = &uiaElement{parent: d, id: id} + + raw = bytes.NewBuffer(decodeStr) return } -type AndroidCondition func(d *uiaDriver) (bool, error) +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 + } -func (d *uiaDriver) _waitWithTimeoutAndInterval(condition AndroidCondition, timeout, interval time.Duration) (err error) { + 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(d) + done, err := condition(ud) if err != nil { return err } @@ -1184,20 +937,10 @@ func (d *uiaDriver) _waitWithTimeoutAndInterval(condition AndroidCondition, time } } -// WaitWithTimeoutAndInterval waits for the condition to evaluate to true. -func (d *uiaDriver) WaitWithTimeoutAndInterval(condition AndroidCondition, timeout, interval float64) (err error) { - dTimeout := time.Millisecond * time.Duration(timeout*1000) - dInterval := time.Millisecond * time.Duration(interval*1000) - return d._waitWithTimeoutAndInterval(condition, dTimeout, dInterval) +func (ud *uiaDriver) WaitWithTimeout(condition Condition, timeout time.Duration) error { + return ud.WaitWithTimeoutAndInterval(condition, timeout, DefaultWaitInterval) } -// WaitWithTimeout works like WaitWithTimeoutAndInterval, but with default polling interval. -func (d *uiaDriver) WaitWithTimeout(condition AndroidCondition, timeout float64) error { - dTimeout := time.Millisecond * time.Duration(timeout*1000) - return d._waitWithTimeoutAndInterval(condition, dTimeout, DefaultWaitInterval) -} - -// Wait works like WaitWithTimeoutAndInterval, but using the default timeout and polling interval. -func (d *uiaDriver) Wait(condition AndroidCondition) error { - return d._waitWithTimeoutAndInterval(condition, DefaultWaitTimeout, DefaultWaitInterval) +func (ud *uiaDriver) Wait(condition Condition) error { + return ud.WaitWithTimeoutAndInterval(condition, DefaultWaitTimeout, DefaultWaitInterval) } diff --git a/hrp/internal/uixt/android_elment.go b/hrp/internal/uixt/android_elment.go index 533217f0..f14fac40 100644 --- a/hrp/internal/uixt/android_elment.go +++ b/hrp/internal/uixt/android_elment.go @@ -4,73 +4,179 @@ 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 (e *uiaElement) Text() (text string, err error) { - // register(getHandler, new GetText("/wd/hub/session/:sessionId/element/:id/text")) - var rawResp rawResponse - if rawResp, err = e.parent.httpGET("/session", e.parent.sessionId, "/element", e.id, "/text"); err != nil { - return "", err - } - reply := new(struct{ Value string }) - if err = json.Unmarshal(rawResp, reply); err != nil { - return "", err - } - text = reply.Value +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 (e *uiaElement) GetAttribute(name string) (attribute string, err error) { - // register(getHandler, new GetElementAttribute("/wd/hub/session/:sessionId/element/:id/attribute/:name")) - var rawResp rawResponse - if rawResp, err = e.parent.httpGET("/session", e.parent.sessionId, "/element", e.id, "/attribute", name); err != nil { - return "", err +func (ue uiaElement) SendKeys(text string, isReplace ...int) (err error) { + if len(isReplace) == 0 { + isReplace = []int{1} } - reply := new(struct{ Value string }) - if err = json.Unmarshal(rawResp, reply); err != nil { - return "", err + // 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, + "replace": isReplace[0] == 1, } - attribute = reply.Value + _, err = ue.parent.httpPOST(data, "/session", ue.parent.sessionId, "/element", ue.id, "/value") return } -func (e *uiaElement) ContentDescription() (name string, err error) { - // register(getHandler, new GetName("/wd/hub/session/:sessionId/element/:id/name")) - var rawResp rawResponse - if rawResp, err = e.parent.httpGET("/session", e.parent.sessionId, "/element", e.id, "/name"); err != nil { - return "", err - } - reply := new(struct{ Value string }) - if err = json.Unmarshal(rawResp, reply); err != nil { - return "", err - } - name = reply.Value +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 (e *uiaElement) Size() (size Size, err error) { - // register(getHandler, new GetSize("/wd/hub/session/:sessionId/element/:id/size")) - var rawResp rawResponse - if rawResp, err = e.parent.httpGET("/session", e.parent.sessionId, "/element", e.id, "/size"); err != nil { - return Size{-1, -1}, err - } - reply := new(struct{ Value Size }) - if err = json.Unmarshal(rawResp, reply); err != nil { - return Size{-1, -1}, err - } - size = reply.Value - return +func (ue uiaElement) ForceTouch(pressure float64, second ...float64) (err error) { + // TODO + return errElementNotImplemented } -func (e *uiaElement) Rect() (rect Rect, err error) { +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 { + return ue.parent._swipe(fromX, fromY, toX, toY, 12, ue.id) +} + +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 = e.parent.httpGET("/session", e.parent.sessionId, "/element", e.id, "/rect"); err != nil { + if rawResp, err = ue.parent.httpGET("/session", ue.parent.sessionId, "/element", ue.id, "/rect"); err != nil { return Rect{}, err } reply := new(struct{ Value Rect }) @@ -81,13 +187,103 @@ func (e *uiaElement) Rect() (rect Rect, err error) { return } -func (e *uiaElement) Screenshot() (raw *bytes.Buffer, err error) { +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 = e.parent.httpGET("/session", e.parent.sessionId, "/element", e.id, "/screenshot"); err != nil { + if rawResp, err = ue.parent.httpGET("/session", ue.parent.sessionId, "/element", ue.id, "/screenshot"); err != nil { return nil, err } reply := new(struct{ Value string }) @@ -103,136 +299,3 @@ func (e *uiaElement) Screenshot() (raw *bytes.Buffer, err error) { raw = bytes.NewBuffer(decodeStr) return } - -func (e *uiaElement) Location() (point Point, err error) { - // register(getHandler, new Location("/wd/hub/session/:sessionId/element/:id/location")) - var rawResp rawResponse - if rawResp, err = e.parent.httpGET("/session", e.parent.sessionId, "/element", e.id, "/location"); err != nil { - return Point{-1, -1}, err - } - reply := new(struct{ Value Point }) - if err = json.Unmarshal(rawResp, reply); err != nil { - return Point{-1, -1}, err - } - point = reply.Value - return -} - -func (e *uiaElement) Click() (err error) { - // register(postHandler, new Click("/wd/hub/session/:sessionId/element/:id/click")) - _, err = e.parent.httpPOST(nil, "/session", e.parent.sessionId, "/element", e.id, "/click") - return -} - -func (e *uiaElement) Clear() (err error) { - // register(postHandler, new Clear("/wd/hub/session/:sessionId/element/:id/clear")) - _, err = e.parent.httpPOST(nil, "/session", e.parent.sessionId, "/element", e.id, "/clear") - return -} - -func (e *uiaElement) SendKeys(text string, isReplace ...bool) (err error) { - if len(isReplace) == 0 { - isReplace = []bool{true} - } - // register(postHandler, new SendKeysToElement("/wd/hub/session/:sessionId/element/:id/value")) - // https://github.com/appium/appium-uiautomator2-server/blob/master/app/src/main/java/io/appium/uiautomator2/handler/SendKeysToElement.java#L76-L85 - data := map[string]interface{}{ - "text": text, - "replace": isReplace[0], - } - _, err = e.parent.httpPOST(data, "/session", e.parent.sessionId, "/element", e.id, "/value") - return -} - -func (e *uiaElement) FindElements(by AndroidBySelector) (elements []*uiaElement, err error) { - method, selector := by.getMethodAndSelector() - return e.parent._findElements(method, selector, e.id) -} - -func (e *uiaElement) FindElement(by AndroidBySelector) (elem *uiaElement, err error) { - method, selector := by.getMethodAndSelector() - return e.parent._findElement(method, selector, e.id) -} - -func (e *uiaElement) Swipe(startX, startY, endX, endY int, steps ...int) (err error) { - return e.SwipeFloat(float64(startX), float64(startY), float64(endX), float64(endY), steps...) -} - -func (e *uiaElement) SwipeFloat(startX, startY, endX, endY float64, steps ...int) (err error) { - if len(steps) == 0 { - steps = []int{12} - } - return e.parent._swipe(startX, startY, endX, endY, steps[0], e.id) -} - -func (e *uiaElement) SwipePoint(startPoint, endPoint Point, steps ...int) (err error) { - return e.Swipe(startPoint.X, startPoint.Y, endPoint.X, endPoint.Y, steps...) -} - -func (e *uiaElement) SwipePointF(startPoint, endPoint PointF, steps ...int) (err error) { - return e.SwipeFloat(startPoint.X, startPoint.Y, endPoint.X, endPoint.Y, steps...) -} - -func (e *uiaElement) Drag(endX, endY int, steps ...int) (err error) { - return e.DragFloat(float64(endX), float64(endY), steps...) -} - -func (e *uiaElement) DragFloat(endX, endY float64, steps ...int) error { - if len(steps) == 0 { - steps = []int{12 * 10} - } else { - steps[0] = 12 * 10 - } - data := map[string]interface{}{ - "elementId": e.id, - "endX": endX, - "endY": endY, - "steps": steps[0], - } - return e.parent._drag(data) -} - -func (e *uiaElement) DragPoint(endPoint Point, steps ...int) error { - return e.Drag(endPoint.X, endPoint.Y, steps...) -} - -func (e *uiaElement) DragPointF(endPoint PointF, steps ...int) (err error) { - return e.DragFloat(endPoint.X, endPoint.Y, steps...) -} - -func (e *uiaElement) DragTo(destElem *uiaElement, steps ...int) error { - if len(steps) == 0 { - steps = []int{12} - } - data := map[string]interface{}{ - "elementId": e.id, - "destElId": destElem.id, - "steps": steps[0], - } - return e.parent._drag(data) -} - -func (e *uiaElement) Flick(xOffset, yOffset, speed int) (err error) { - data := map[string]interface{}{ - legacyWebElementIdentifier: e.id, - webElementIdentifier: e.id, - "xoffset": xOffset, - "yoffset": yOffset, - "speed": speed, - } - return e.parent._flick(data) -} - -func (e *uiaElement) ScrollTo(by AndroidBySelector, maxSwipes ...int) (err error) { - if len(maxSwipes) == 0 { - maxSwipes = []int{0} - } - method, selector := by.getMethodAndSelector() - return e.parent._scrollTo(method, selector, maxSwipes[0], e.id) -} - -func (e *uiaElement) ScrollToElement(element *uiaElement) (err error) { - // register(postHandler, new ScrollToElement("/wd/hub/session/:sessionId/appium/element/:id/scroll_to/:id2")) - _, err = e.parent.httpPOST(nil, "/session", e.parent.sessionId, "/appium/element", e.id, "/scroll_to", element.id) - return -} diff --git a/hrp/internal/uixt/android_test.go b/hrp/internal/uixt/android_test.go index 2efa9c66..08a71c17 100644 --- a/hrp/internal/uixt/android_test.go +++ b/hrp/internal/uixt/android_test.go @@ -21,11 +21,11 @@ func TestDriver_NewSession(t *testing.T) { "firstMatch": []interface{}{firstMatchEntry}, "alwaysMatch": struct{}{}, } - sessionID, err := driver.NewSession(caps) + session, err := driver.NewSession(caps) if err != nil { t.Fatal(err) } - if len(sessionID) == 0 { + if len(session.SessionId) == 0 { t.Fatal("should not be empty") } } @@ -45,7 +45,7 @@ func TestDriver_Quit(t *testing.T) { t.Fatal(err) } - if err = driver.Quit(); err != nil { + if err = driver.Close(); err != nil { t.Fatal(err) } } @@ -147,7 +147,7 @@ func TestDriver_DeviceSize(t *testing.T) { t.Fatal(err) } - deviceSize, err := driver.DeviceSize() + deviceSize, err := driver.WindowSize() if err != nil { t.Fatal(err) } @@ -169,20 +169,6 @@ func TestDriver_Source(t *testing.T) { t.Log(source) } -func TestDriver_StatusBarHeight(t *testing.T) { - driver, err := NewUIADriver(nil, uiaServerURL) - if err != nil { - t.Fatal(err) - } - - statusBarHeight, err := driver.StatusBarHeight() - if err != nil { - t.Fatal(err) - } - - t.Log(statusBarHeight) -} - func TestDriver_BatteryInfo(t *testing.T) { driver, err := NewUIADriver(nil, uiaServerURL) if err != nil { @@ -220,7 +206,7 @@ func TestDriver_DeviceScaleRatio(t *testing.T) { t.Fatal(err) } - scaleRatio, err := driver.DeviceScaleRatio() + scaleRatio, err := driver.Scale() if err != nil { t.Fatal(err) } @@ -275,17 +261,6 @@ func TestDriver_Tap(t *testing.T) { t.Fatal(err) } time.Sleep(time.Second) - - err = driver.TapPoint(Point{X: 150, Y: 340}) - if err != nil { - t.Fatal(err) - } - time.Sleep(time.Second) - - err = driver.TapPointF(PointF{X: 60.5, Y: 125.5}) - if err != nil { - t.Fatal(err) - } } func TestDriver_Swipe(t *testing.T) { @@ -294,7 +269,7 @@ func TestDriver_Swipe(t *testing.T) { t.Fatal(err) } - err = driver.Swipe(400, 1000, 400, 500, 10) + err = driver.Swipe(400, 1000, 400, 500) if err != nil { t.Fatal(err) } @@ -303,20 +278,6 @@ func TestDriver_Swipe(t *testing.T) { if err != nil { t.Fatal(err) } - - startPoint := Point{400, 1000} - endPoint := Point{400, 500} - err = driver.SwipePoint(startPoint, endPoint) - if err != nil { - t.Fatal(err) - } - - startPointF := PointF{400, 555.5} - endPointF := PointF{400, 1255.5} - err = driver.SwipePointF(startPointF, endPointF) - if err != nil { - t.Fatal(err) - } } func TestDriver_Drag(t *testing.T) { @@ -325,50 +286,17 @@ func TestDriver_Drag(t *testing.T) { t.Fatal(err) } - err = driver.Drag(400, 260, 400, 500, 10) + err = driver.Drag(400, 260, 400, 500) if err != nil { t.Fatal(err) } time.Sleep(time.Millisecond * 200) - err = driver.DragFloat(400, 501.5, 400, 261.5, 10) + err = driver.DragFloat(400, 501.5, 400, 261.5) if err != nil { t.Fatal(err) } time.Sleep(time.Millisecond * 200) - - startPoint := Point{400, 260} - endPoint := Point{400, 500} - err = driver.DragPoint(startPoint, endPoint) - if err != nil { - t.Fatal(err) - } - time.Sleep(time.Millisecond * 200) - - startPointF := PointF{400.5, 501.5} - endPointF := PointF{400.5, 261.5} - err = driver.DragPointF(startPointF, endPointF) - if err != nil { - t.Fatal(err) - } -} - -func TestDriver_TouchLongClick(t *testing.T) { - driver, err := NewUIADriver(nil, uiaServerURL) - if err != nil { - t.Fatal(err) - } - - err = driver.TouchLongClick(400, 260, 1.2222) - if err != nil { - t.Fatal(err) - } - time.Sleep(time.Millisecond * 200) - - err = driver.TouchLongClickPoint(Point{X: 400, Y: 260}) - if err != nil { - t.Fatal(err) - } } func TestDriver_SendKeys(t *testing.T) { @@ -383,7 +311,7 @@ func TestDriver_SendKeys(t *testing.T) { } time.Sleep(time.Second * 2) - err = driver.SendKeys("def", false) + err = driver.SendKeys("def") if err != nil { t.Fatal(err) } @@ -396,270 +324,270 @@ func TestDriver_SendKeys(t *testing.T) { } } -func TestDriver_PressBack(t *testing.T) { - driver, err := NewUIADriver(nil, uiaServerURL) - if err != nil { - t.Fatal(err) - } +//func TestDriver_PressBack(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// err = driver.PressBack() +// if err != nil { +// t.Fatal(err) +// } +//} - err = driver.PressBack() - if err != nil { - t.Fatal(err) - } -} +//func TestDriver_PressKeyCode(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// err = driver.PressKeyCodeAsync(KCx) +// if err != nil { +// t.Fatal(err) +// } +// err = driver.PressKeyCodeAsync(KCx, KMCapLocked) +// if err != nil { +// t.Fatal(err) +// } +// // err = driver.PressKeyCodeAsync(KCExplorer) +// // if err != nil { +// // t.Fatal(err) +// // } +// +// err = driver.PressKeyCode(KCExplorer, KMEmpty) +// if err != nil { +// t.Fatal(err) +// } +//} -func TestDriver_PressKeyCode(t *testing.T) { - driver, err := NewUIADriver(nil, uiaServerURL) - if err != nil { - t.Fatal(err) - } +//func TestDriver_LongPressKeyCode(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// err = driver.LongPressKeyCode(KCAt, KMEmpty) +// if err != nil { +// t.Fatal(err) +// } +//} +// +//func TestDriver_TouchDown(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// doTouchUp := func() { +// err = driver.TouchUp(400, 260) +// if err != nil { +// t.Fatal(err) +// } +// } +// +// err = driver.TouchDown(400, 260) +// if err != nil { +// t.Fatal(err) +// } +// +// // _ = driver.TapPoint(Point{400, 500}) +// doTouchUp() +// +// err = driver.TouchDownPoint(Point{400, 260}) +// if err != nil { +// t.Fatal(err) +// } +// +// doTouchUp() +//} +// +//func TestDriver_TouchUp(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// err = driver.TouchDown(400, 260) +// if err != nil { +// t.Fatal(err) +// } +// +// // err = driver.TouchUp(400, 260) +// err = driver.TouchUpPoint(Point{400, 260}) +// if err != nil { +// t.Fatal(err) +// } +//} +// +//func TestDriver_TouchMove(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// doTouchDown := func(x, y int) { +// err = driver.TouchDown(x, y) +// if err != nil { +// t.Fatal(err) +// } +// } +// +// doTouchUp := func(x, y int) { +// err = driver.TouchUp(x, y) +// if err != nil { +// t.Fatal(err) +// } +// } +// +// doTouchDown(400, 260) +// +// err = driver.TouchMove(400, 500) +// if err != nil { +// t.Fatal(err) +// } +// +// doTouchUp(400, 500) +// +// doTouchDown(400, 500) +// +// err = driver.TouchMove(400, 260) +// if err != nil { +// t.Fatal(err) +// } +// +// doTouchUp(400, 260) +//} +// +//func TestDriver_OpenNotification(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// err = driver.OpenNotification() +// if err != nil { +// t.Fatal(err) +// } +//} +// +//func TestDriver_Flick(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// err = driver.Flick(50, -100) +// if err != nil { +// t.Fatal(err) +// } +//} +// +//func TestDriver_ScrollTo(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// err = driver.ScrollTo(BySelector{ClassName: "android.widget.SeekBar"}) +// if err != nil { +// t.Fatal(err) +// } +//} - err = driver.PressKeyCodeAsync(KCx) - if err != nil { - t.Fatal(err) - } - err = driver.PressKeyCodeAsync(KCx, KMCapLocked) - if err != nil { - t.Fatal(err) - } - // err = driver.PressKeyCodeAsync(KCExplorer) - // if err != nil { - // t.Fatal(err) - // } - - err = driver.PressKeyCode(KCExplorer, KMEmpty) - if err != nil { - t.Fatal(err) - } -} - -func TestDriver_LongPressKeyCode(t *testing.T) { - driver, err := NewUIADriver(nil, uiaServerURL) - if err != nil { - t.Fatal(err) - } - - err = driver.LongPressKeyCode(KCAt, KMEmpty) - if err != nil { - t.Fatal(err) - } -} - -func TestDriver_TouchDown(t *testing.T) { - driver, err := NewUIADriver(nil, uiaServerURL) - if err != nil { - t.Fatal(err) - } - - doTouchUp := func() { - err = driver.TouchUp(400, 260) - if err != nil { - t.Fatal(err) - } - } - - err = driver.TouchDown(400, 260) - if err != nil { - t.Fatal(err) - } - - // _ = driver.TapPoint(Point{400, 500}) - doTouchUp() - - err = driver.TouchDownPoint(Point{400, 260}) - if err != nil { - t.Fatal(err) - } - - doTouchUp() -} - -func TestDriver_TouchUp(t *testing.T) { - driver, err := NewUIADriver(nil, uiaServerURL) - if err != nil { - t.Fatal(err) - } - - err = driver.TouchDown(400, 260) - if err != nil { - t.Fatal(err) - } - - // err = driver.TouchUp(400, 260) - err = driver.TouchUpPoint(Point{400, 260}) - if err != nil { - t.Fatal(err) - } -} - -func TestDriver_TouchMove(t *testing.T) { - driver, err := NewUIADriver(nil, uiaServerURL) - if err != nil { - t.Fatal(err) - } - - doTouchDown := func(x, y int) { - err = driver.TouchDown(x, y) - if err != nil { - t.Fatal(err) - } - } - - doTouchUp := func(x, y int) { - err = driver.TouchUp(x, y) - if err != nil { - t.Fatal(err) - } - } - - doTouchDown(400, 260) - - err = driver.TouchMove(400, 500) - if err != nil { - t.Fatal(err) - } - - doTouchUp(400, 500) - - doTouchDown(400, 500) - - err = driver.TouchMove(400, 260) - if err != nil { - t.Fatal(err) - } - - doTouchUp(400, 260) -} - -func TestDriver_OpenNotification(t *testing.T) { - driver, err := NewUIADriver(nil, uiaServerURL) - if err != nil { - t.Fatal(err) - } - - err = driver.OpenNotification() - if err != nil { - t.Fatal(err) - } -} - -func TestDriver_Flick(t *testing.T) { - driver, err := NewUIADriver(nil, uiaServerURL) - if err != nil { - t.Fatal(err) - } - - err = driver.Flick(50, -100) - if err != nil { - t.Fatal(err) - } -} - -func TestDriver_ScrollTo(t *testing.T) { - driver, err := NewUIADriver(nil, uiaServerURL) - if err != nil { - t.Fatal(err) - } - - err = driver.ScrollTo(AndroidBySelector{ClassName: "android.widget.SeekBar"}) - if err != nil { - t.Fatal(err) - } -} - -func TestDriver_MultiPointerGesture(t *testing.T) { - driver, err := NewUIADriver(nil, uiaServerURL) - if err != nil { - t.Fatal(err) - } - - gesture1 := NewTouchAction().Add(150, 340, 0.35).AddFloat(50, 300) - gesture2 := NewTouchAction().Add(200, 340).AddFloat(300, 300) - gesture3 := NewTouchAction().Add(300, 500).AddFloat(350, 500).AddPoint(Point{300, 550}).AddPointF(PointF{350, 550}) - _ = gesture3 - - // err = driver.MultiPointerGesture(gesture1, gesture2) - err = driver.MultiPointerGesture(gesture1, gesture2, gesture3) - if err != nil { - t.Fatal(err) - } -} - -func TestDriver_PerformW3CActions(t *testing.T) { - driver, err := NewUIADriver(nil, uiaServerURL) - if err != nil { - t.Fatal(err) - } - - // actionKey := NewW3CAction(ATKey, NewW3CGestures().KeyDown("g").KeyUp("g").Pause().KeyDown("o").KeyUp("o")) - // actionKey := NewW3CAction(ATKey, NewW3CGestures().SendKeys("golang")) - // err = driver.PerformW3CActions(actionKey) - // if err != nil { - // t.Fatal(err) - // } - - // var queryField map[string]string - // queryField = make(map[string]string) - // { - // queryField = map[string]string{ - // "a": "", - // } - // } - - elem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "com.android.settings:id/search"}) - if err != nil { - t.Fatal(err) - } - // actionPointer := NewW3CAction(ATPointer, NewW3CGestures().PointerMove(0, 0, elem.id).PointerDown().Pause(3).PointerUp()) - // actionPointer := NewW3CAction(ATPointer, - // NewW3CGestures().PointerMove(400, 500, "viewport").PointerDown().Pause(2). - // PointerMove(0, 0, elem.id).Pause(2). - // PointerMove(20, 0, "pointer").Pause(2). - // PointerUp(), - // ) - actionPointer := NewW3CAction(ATPointer, - NewW3CGestures().PointerMoveTo(400, 500).PointerDown(). - PointerMouseOver(0, 0, elem). - PointerMoveRelative(20, 0).PointerUp()) - err = driver.PerformW3CActions(actionPointer) - if err != nil { - t.Fatal(err) - } -} - -func TestDriver_GetClipboard(t *testing.T) { - driver, err := NewUIADriver(nil, uiaServerURL) - if err != nil { - t.Fatal(err) - } - - text, err := driver.GetClipboard() - if err != nil { - t.Fatal(err) - } - t.Log(text) -} - -func TestDriver_SetClipboard(t *testing.T) { - driver, err := NewUIADriver(nil, uiaServerURL) - if err != nil { - t.Fatal(err) - } - - content := "test123" - err = driver.SetClipboard(ClipDataTypePlaintext, content) - if err != nil { - t.Fatal(err) - } - - text, err := driver.GetClipboard() - if err != nil { - t.Fatal(err) - } - if text != content { - t.Fatal("should be the same") - } -} +//func TestDriver_MultiPointerGesture(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// gesture1 := NewTouchAction().Add(150, 340, 0.35).AddFloat(50, 300) +// gesture2 := NewTouchAction().Add(200, 340).AddFloat(300, 300) +// gesture3 := NewTouchAction().Add(300, 500).AddFloat(350, 500).AddPoint(Point{300, 550}).AddPointF(PointF{350, 550}) +// _ = gesture3 +// +// // err = driver.MultiPointerGesture(gesture1, gesture2) +// err = driver.MultiPointerGesture(gesture1, gesture2, gesture3) +// if err != nil { +// t.Fatal(err) +// } +//} +// +//func TestDriver_PerformW3CActions(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// // actionKey := NewW3CAction(ATKey, NewW3CGestures().KeyDown("g").KeyUp("g").Pause().KeyDown("o").KeyUp("o")) +// // actionKey := NewW3CAction(ATKey, NewW3CGestures().SendKeys("golang")) +// // err = driver.PerformW3CActions(actionKey) +// // if err != nil { +// // t.Fatal(err) +// // } +// +// // var queryField map[string]string +// // queryField = make(map[string]string) +// // { +// // queryField = map[string]string{ +// // "a": "", +// // } +// // } +// +// elem, err := driver.FindElement(BySelector{ResourceIdID: "com.android.settings:id/search"}) +// if err != nil { +// t.Fatal(err) +// } +// // actionPointer := NewW3CAction(ATPointer, NewW3CGestures().PointerMove(0, 0, elem.id).PointerDown().Pause(3).PointerUp()) +// // actionPointer := NewW3CAction(ATPointer, +// // NewW3CGestures().PointerMove(400, 500, "viewport").PointerDown().Pause(2). +// // PointerMove(0, 0, elem.id).Pause(2). +// // PointerMove(20, 0, "pointer").Pause(2). +// // PointerUp(), +// // ) +// actionPointer := NewW3CAction(ATPointer, +// NewW3CGestures().PointerMoveTo(400, 500).PointerDown(). +// PointerMouseOver(0, 0, elem). +// PointerMoveRelative(20, 0).PointerUp()) +// err = driver.PerformW3CActions(actionPointer) +// if err != nil { +// t.Fatal(err) +// } +//} +// +//func TestDriver_GetClipboard(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// text, err := driver.GetClipboard() +// if err != nil { +// t.Fatal(err) +// } +// t.Log(text) +//} +// +//func TestDriver_SetClipboard(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// content := "test123" +// err = driver.SetClipboard(ClipDataTypePlaintext, content) +// if err != nil { +// t.Fatal(err) +// } +// +// text, err := driver.GetClipboard() +// if err != nil { +// t.Fatal(err) +// } +// if text != content { +// t.Fatal("should be the same") +// } +//} func TestDriver_AlertAccept(t *testing.T) { driver, err := NewUIADriver(nil, uiaServerURL) @@ -687,33 +615,33 @@ func TestDriver_AlertDismiss(t *testing.T) { } } -func TestDriver_SetAppiumSettings(t *testing.T) { - driver, err := NewUIADriver(nil, uiaServerURL) - if err != nil { - t.Fatal(err) - } - - appiumSettings, err := driver.GetAppiumSettings() - if err != nil { - t.Fatal(err) - } - sdopd := appiumSettings["shutdownOnPowerDisconnect"] - t.Log("shutdownOnPowerDisconnect:", sdopd) - - err = driver.SetAppiumSettings(map[string]interface{}{"shutdownOnPowerDisconnect": !sdopd.(bool)}) - if err != nil { - t.Fatal(err) - } - - appiumSettings, err = driver.GetAppiumSettings() - if err != nil { - t.Fatal(err) - } - if appiumSettings["shutdownOnPowerDisconnect"] == sdopd.(bool) { - t.Fatal("should not be equal") - } - t.Log("shutdownOnPowerDisconnect:", appiumSettings["shutdownOnPowerDisconnect"]) -} +//func TestDriver_SetAppiumSettings(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// appiumSettings, err := driver.GetAppiumSettings() +// if err != nil { +// t.Fatal(err) +// } +// sdopd := appiumSettings["shutdownOnPowerDisconnect"] +// t.Log("shutdownOnPowerDisconnect:", sdopd) +// +// err = driver.SetAppiumSettings(map[string]interface{}{"shutdownOnPowerDisconnect": !sdopd.(bool)}) +// if err != nil { +// t.Fatal(err) +// } +// +// appiumSettings, err = driver.GetAppiumSettings() +// if err != nil { +// t.Fatal(err) +// } +// if appiumSettings["shutdownOnPowerDisconnect"] == sdopd.(bool) { +// t.Fatal("should not be equal") +// } +// t.Log("shutdownOnPowerDisconnect:", appiumSettings["shutdownOnPowerDisconnect"]) +//} func TestDriver_SetOrientation(t *testing.T) { driver, err := NewUIADriver(nil, uiaServerURL) @@ -741,17 +669,17 @@ func TestDriver_SetRotation(t *testing.T) { } } -func TestDriver_NetworkConnection(t *testing.T) { - driver, err := NewUIADriver(nil, uiaServerURL) - if err != nil { - t.Fatal(err) - } - - err = driver.NetworkConnection(NetworkTypeWifi) - if err != nil { - t.Fatal(err) - } -} +//func TestDriver_NetworkConnection(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// err = driver.NetworkConnection(NetworkTypeWifi) +// if err != nil { +// t.Fatal(err) +// } +//} func TestDriver_FindElement(t *testing.T) { driver, err := NewUIADriver(nil, uiaServerURL) @@ -759,12 +687,12 @@ func TestDriver_FindElement(t *testing.T) { t.Fatal(err) } - elem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "android:id/content"}) + elem, err := driver.FindElement(BySelector{ResourceIdID: "android:id/content"}) if err != nil { t.Fatal(err) } - - t.Log(elem.GetAttribute("class")) + e := ElementAttribute{}.WithLabel("class") + t.Log(elem.GetAttribute(e)) } func TestDriver_FindElements(t *testing.T) { @@ -773,8 +701,8 @@ func TestDriver_FindElements(t *testing.T) { t.Fatal(err) } - // elements, err := driver.FindElements(AndroidBySelector{ResourceIdID: "com.android.settings:id/title"}) - elements, err := driver.FindElements(AndroidBySelector{UiAutomator: "new UiSelector().textStartsWith(\"应\");"}) + // elements, err := driver.FindElements(BySelector{ResourceIdID: "com.android.settings:id/title"}) + elements, err := driver.FindElements(BySelector{UiAutomator: "new UiSelector().textStartsWith(\"应\");"}) if err != nil { t.Fatal(err) } @@ -786,12 +714,12 @@ func TestDriver_WaitWithTimeoutAndInterval(t *testing.T) { if err != nil { t.Fatal(err) } - element, err := driver.FindElement(AndroidBySelector{UiAutomator: "new UiSelector().className(\"android.view.ViewGroup\");"}) + element, err := driver.FindElement(BySelector{UiAutomator: "new UiSelector().className(\"android.view.ViewGroup\");"}) if err != nil { t.Fatal(err) } - elem, err := element.FindElement(AndroidBySelector{UiAutomator: "new UiSelector().className(\"android.widget.LinearLayout\").index(6);"}) + elem, err := element.FindElement(BySelector{UiAutomator: "new UiSelector().className(\"android.widget.LinearLayout\").index(6);"}) if err != nil { t.Fatal(err) } @@ -808,8 +736,8 @@ func TestDriver_WaitWithTimeoutAndInterval(t *testing.T) { t.Fatal(err) } - by := AndroidBySelector{UiAutomator: "new UiSelector().text(\"科技\");"} - exists := func(d *uiaDriver) (bool, error) { + by := BySelector{UiAutomator: "new UiSelector().text(\"科技\");"} + exists := func(d WebDriver) (bool, error) { element, err = d.FindElement(by) if err == nil { return true, nil @@ -817,7 +745,7 @@ func TestDriver_WaitWithTimeoutAndInterval(t *testing.T) { return false, nil } - err = driver.WaitWithTimeoutAndInterval(exists, 1, 0.1) + err = driver.WaitWithTimeoutAndInterval(exists, 1, 1) if err != nil { t.Fatal(err) } @@ -833,25 +761,25 @@ func TestDriver_WaitWithTimeoutAndInterval(t *testing.T) { } } -func TestDriver_ActiveElement(t *testing.T) { - device, _ := NewAndroidDevice() - driver, err := device.NewUSBDriver(nil) - if err != nil { - t.Fatal(err) - } - defer func() { - _ = driver.Dispose() - }() - - element, err := driver.ActiveElement() - if err != nil { - t.Fatal(err) - } - - if err = element.SendKeys("test"); err != nil { - t.Fatal(err) - } -} +//func TestDriver_ActiveElement(t *testing.T) { +// device, _ := NewAndroidDevice() +// driver, err := device.NewUSBDriver(nil) +// if err != nil { +// t.Fatal(err) +// } +// defer func() { +// _ = driver.Dispose() +// }() +// +// element, err := driver.ActiveElement() +// if err != nil { +// t.Fatal(err) +// } +// +// if err = element.SendKeys("test"); err != nil { +// t.Fatal(err) +// } +//} func TestUiSelectorHelper_NewUiSelectorHelper(t *testing.T) { uiSelector := NewUiSelectorHelper().Text("a").String() @@ -898,38 +826,38 @@ func TestDeviceList(t *testing.T) { } } -func TestAndroidNewUSBDriver(t *testing.T) { - device, _ := NewAndroidDevice() - driver, err := device.NewUSBDriver(nil) - if err != nil { - t.Fatal(err) - } - defer driver.Dispose() +//func TestAndroidNewUSBDriver(t *testing.T) { +// device, _ := NewAndroidDevice() +// driver, err := device.NewUSBDriver(nil) +// if err != nil { +// t.Fatal(err) +// } +// defer driver.Dispose() +// +// ready, err := driver.Status() +// if err != nil { +// t.Fatal(err) +// } +// if !ready { +// t.Fatal("should be 'true'") +// } +//} - ready, err := driver.Status() - if err != nil { - t.Fatal(err) - } - if !ready { - t.Fatal("should be 'true'") - } -} - -func TestDriver_ActiveAppPackageName(t *testing.T) { - device, _ := NewAndroidDevice() - driver, err := device.NewUSBDriver(nil) - if err != nil { - t.Fatal(err) - } - defer driver.Dispose() - - appPackageName, err := driver.ActiveAppPackageName() - if err != nil { - t.Fatal(err) - } - - t.Log(appPackageName) -} +//func TestDriver_ActiveAppPackageName(t *testing.T) { +// device, _ := NewAndroidDevice() +// driver, err := device.NewUSBDriver(nil) +// if err != nil { +// t.Fatal(err) +// } +// defer driver.Dispose() +// +// appPackageName, err := driver.ActiveAppPackageName() +// if err != nil { +// t.Fatal(err) +// } +// +// t.Log(appPackageName) +//} func TestDriver_AppLaunch(t *testing.T) { device, _ := NewAndroidDevice() @@ -937,10 +865,9 @@ func TestDriver_AppLaunch(t *testing.T) { if err != nil { t.Fatal(err) } - defer driver.Dispose() - // err = driver.AppLaunch("tv.danmaku.bili", AndroidBySelector{ResourceIdID: "tv.danmaku.bili:id/action_bar_root"}) - err = driver.AppLaunch("com.android.settings", AndroidBySelector{ResourceIdID: "android:id/list"}) + // err = driver.AppLaunch("tv.danmaku.bili", BySelector{ResourceIdID: "tv.danmaku.bili:id/action_bar_root"}) + err = driver.AppLaunch("com.android.settings", AppLaunchOption{}.WithAndroidBySelector(AndroidBySelector{ResourceIdID: "android:id/list"})) if err != nil { t.Fatal(err) } @@ -960,63 +887,63 @@ func TestDriver_AppTerminate(t *testing.T) { } defer driver.Dispose() - err = driver.AppTerminate("tv.danmaku.bili") + _, err = driver.AppTerminate("tv.danmaku.bili") if err != nil { t.Fatal(err) } } -func TestNewWiFiDriver(t *testing.T) { - device, _ := NewAndroidDevice(WithAdbIP("192.168.1.28")) - driver, err := device.NewHTTPDriver(nil) - if err != nil { - t.Fatal(err) - } +//func TestNewWiFiDriver(t *testing.T) { +// device, _ := NewAndroidDevice(WithAdbIP("192.168.1.28")) +// driver, err := device.NewHTTPDriver(nil) +// if err != nil { +// t.Fatal(err) +// } +// +// // SetDebug(false, true) +// _, err = driver.ActiveAppActivity() +// if err != nil { +// t.Fatal(err) +// } +//} - // SetDebug(false, true) - _, err = driver.ActiveAppActivity() - if err != nil { - t.Fatal(err) - } -} +//func TestDriver_AppInstall(t *testing.T) { +// device, _ := NewAndroidDevice() +// driver, err := device.NewUSBDriver(nil) +// if err != nil { +// t.Fatal(err) +// } +// defer driver.Dispose() +// +// err = driver.AppInstall("/Users/hero/Desktop/xuexi_android_10002068.apk") +// if err != nil { +// t.Fatal(err) +// } +//} -func TestDriver_AppInstall(t *testing.T) { - device, _ := NewAndroidDevice() - driver, err := device.NewUSBDriver(nil) - if err != nil { - t.Fatal(err) - } - defer driver.Dispose() - - err = driver.AppInstall("/Users/hero/Desktop/xuexi_android_10002068.apk") - if err != nil { - t.Fatal(err) - } -} - -func TestDriver_AppUninstall(t *testing.T) { - device, _ := NewAndroidDevice() - driver, err := device.NewUSBDriver(nil) - if err != nil { - t.Fatal(err) - } - defer driver.Dispose() - - err = driver.AppUninstall("cn.xuexi.android") - if err != nil { - t.Fatal(err) - } -} +//func TestDriver_AppUninstall(t *testing.T) { +// device, _ := NewAndroidDevice() +// driver, err := device.NewUSBDriver(nil) +// if err != nil { +// t.Fatal(err) +// } +// defer driver.Dispose() +// +// err = driver.AppUninstall("cn.xuexi.android") +// if err != nil { +// t.Fatal(err) +// } +//} func TestBySelector_getMethodAndSelector(t *testing.T) { testVal := "test id" - bySelector := AndroidBySelector{ResourceIdID: testVal} + bySelector := BySelector{ResourceIdID: testVal} method, selector := bySelector.getMethodAndSelector() if method != "id" || selector != testVal { t.Fatal(method, "=", selector) } - bySelector = AndroidBySelector{ContentDescription: testVal} + bySelector = BySelector{ContentDescription: testVal} method, selector = bySelector.getMethodAndSelector() if method != "accessibility id" || selector != testVal { t.Fatal(method, "=", selector) @@ -1029,7 +956,7 @@ func TestElement_Text(t *testing.T) { t.Fatal(err) } - elem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "com.android.settings:id/category_title"}) + elem, err := driver.FindElement(BySelector{ResourceIdID: "com.android.settings:id/category_title"}) if err != nil { t.Fatal(err) } @@ -1048,12 +975,13 @@ func TestElement_GetAttribute(t *testing.T) { t.Fatal(err) } - elem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "com.android.settings:id/category_title"}) + elem, err := driver.FindElement(BySelector{ResourceIdID: "com.android.settings:id/category_title"}) if err != nil { t.Fatal(err) } - attribute, err := elem.GetAttribute("class") + e := ElementAttribute{}.WithName("class") + attribute, err := elem.GetAttribute(e) if err != nil { t.Fatal(err) } @@ -1061,24 +989,24 @@ func TestElement_GetAttribute(t *testing.T) { t.Log(attribute) } -func TestElement_ContentDescription(t *testing.T) { - driver, err := NewUIADriver(nil, uiaServerURL) - if err != nil { - t.Fatal(err) - } - - elem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "com.android.settings:id/search"}) - if err != nil { - t.Fatal(err) - } - - name, err := elem.ContentDescription() - if err != nil { - t.Fatal(err) - } - - t.Log(name) -} +//func TestElement_ContentDescription(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// elem, err := driver.FindElement(BySelector{ResourceIdID: "com.android.settings:id/search"}) +// if err != nil { +// t.Fatal(err) +// } +// +// name, err := elem.ContentDescription() +// if err != nil { +// t.Fatal(err) +// } +// +// t.Log(name) +//} func TestElement_Size(t *testing.T) { driver, err := NewUIADriver(nil, uiaServerURL) @@ -1086,7 +1014,7 @@ func TestElement_Size(t *testing.T) { t.Fatal(err) } - elem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "com.android.settings:id/search"}) + elem, err := driver.FindElement(BySelector{ResourceIdID: "com.android.settings:id/search"}) if err != nil { t.Fatal(err) } @@ -1105,7 +1033,7 @@ func TestElement_Rect(t *testing.T) { t.Fatal(err) } - elem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "com.android.settings:id/category_title"}) + elem, err := driver.FindElement(BySelector{ResourceIdID: "com.android.settings:id/category_title"}) if err != nil { t.Fatal(err) } @@ -1124,7 +1052,7 @@ func TestElement_Screenshot(t *testing.T) { t.Fatal(err) } - elem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "com.android.settings:id/category_title"}) + elem, err := driver.FindElement(BySelector{ResourceIdID: "com.android.settings:id/category_title"}) if err != nil { t.Fatal(err) } @@ -1143,7 +1071,7 @@ func TestElement_Location(t *testing.T) { t.Fatal(err) } - elem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "com.android.settings:id/category_title"}) + elem, err := driver.FindElement(BySelector{ResourceIdID: "com.android.settings:id/category_title"}) if err != nil { t.Fatal(err) } @@ -1162,7 +1090,7 @@ func TestElement_Click(t *testing.T) { t.Fatal(err) } - elem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "com.android.settings:id/title"}) + elem, err := driver.FindElement(BySelector{ResourceIdID: "com.android.settings:id/title"}) if err != nil { t.Fatal(err) } @@ -1179,7 +1107,7 @@ func TestElement_Clear(t *testing.T) { t.Fatal(err) } - elem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "android:id/search_src_text"}) + elem, err := driver.FindElement(BySelector{ResourceIdID: "android:id/search_src_text"}) if err != nil { t.Fatal(err) } @@ -1196,7 +1124,7 @@ func TestElement_SendKeys(t *testing.T) { t.Fatal(err) } - elem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "android:id/search_src_text"}) + elem, err := driver.FindElement(BySelector{ResourceIdID: "android:id/search_src_text"}) if err != nil { t.Fatal(err) } @@ -1204,7 +1132,7 @@ func TestElement_SendKeys(t *testing.T) { // return // err = elem.SendKeys("abc") - err = elem.SendKeys("456", false) + err = elem.SendKeys("456", 0) if err != nil { t.Fatal(err) } @@ -1216,12 +1144,12 @@ func TestElement_FindElements(t *testing.T) { t.Fatal(err) } - parentElem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "com.android.settings:id/main_content"}) + parentElem, err := driver.FindElement(BySelector{ResourceIdID: "com.android.settings:id/main_content"}) if err != nil { t.Fatal(err) } - elements, err := parentElem.FindElements(AndroidBySelector{ResourceIdID: "com.android.settings:id/category"}) + elements, err := parentElem.FindElements(BySelector{ResourceIdID: "com.android.settings:id/category"}) if err != nil { t.Fatal(err) } @@ -1234,12 +1162,12 @@ func TestElement_FindElement(t *testing.T) { t.Fatal(err) } - parentElem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "com.android.settings:id/main_content"}) + parentElem, err := driver.FindElement(BySelector{ResourceIdID: "com.android.settings:id/main_content"}) if err != nil { t.Fatal(err) } - elem, err := parentElem.FindElement(AndroidBySelector{ResourceIdID: "com.android.settings:id/category_title"}) + elem, err := parentElem.FindElement(BySelector{ResourceIdID: "com.android.settings:id/category_title"}) if err != nil { t.Fatal(err) } @@ -1253,7 +1181,7 @@ func TestElement_Swipe(t *testing.T) { t.Fatal(err) } - elem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "com.android.settings:id/category_title"}) + elem, err := driver.FindElement(BySelector{ResourceIdID: "com.android.settings:id/category_title"}) if err != nil { t.Fatal(err) } @@ -1274,111 +1202,104 @@ func TestElement_Swipe(t *testing.T) { if err != nil { t.Fatal(err) } - - startPoint := PointF{X: float64(rect.X + rect.Width/20 + 30), Y: float64(startY / 2)} - endPoint := PointF{X: startPoint.X, Y: startPoint.Y + startPoint.Y} - err = elem.SwipePointF(startPoint, endPoint) - if err != nil { - t.Fatal(err) - } } -func TestElement_Drag(t *testing.T) { - driver, err := NewUIADriver(nil, uiaServerURL) - if err != nil { - t.Fatal(err) - } +//func TestElement_Drag(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// elements, err := driver.FindElements(BySelector{ClassName: "android.widget.TextView"}) +// if err != nil { +// t.Fatal(err) +// } +// +// for i, elem := range elements { +// text, _ := elem.Text() +// t.Log(i, text) +// } +// +// rect, err := elements[0].Rect() +// if err != nil { +// t.Fatal(err) +// } +// +// // err = elements[0].Drag(300, 450, 256) +// err = elements[0].Drag(300, 450, 256) +// if err != nil { +// t.Fatal(err) +// } +// +// err = elements[0].DragTo(elements[1], 256) +// if err != nil { +// t.Fatal(err) +// } +// +// endPoint := PointF{X: float64(rect.X + rect.Width/3*2), Y: float64(rect.Y + rect.Height/2)} +// err = elements[0].DragPointF(endPoint, 256) +// if err != nil { +// t.Fatal() +// } +//} - elements, err := driver.FindElements(AndroidBySelector{ClassName: "android.widget.TextView"}) - if err != nil { - t.Fatal(err) - } +//func TestElement_Flick(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// elem, err := driver.FindElement(BySelector{UiAutomator: "new UiSelector().text(\"提示音和通知\");"}) +// if err != nil { +// t.Fatal(err) +// } +// +// err = elem.Flick(36, 20, 100) +// if err != nil { +// t.Fatal(err) +// } +//} - for i, elem := range elements { - text, _ := elem.Text() - t.Log(i, text) - } +//func TestElement_ScrollTo(t *testing.T) { +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// // how to make it work? +// // parentElem, err := driver.FindElement(BySelector{ClassName: "android.widget.ScrollView"}) +// // parentElem, err := driver.FindElement(BySelector{ResourceIdID: "com.cyanogenmod.filemanager:id/navigation_view_layout"}) +// parentElem, err := driver.FindElement(BySelector{ResourceIdID: "com.android.settings:id/dashboard"}) +// if err != nil { +// t.Fatal(err) +// } +// +// err = parentElem.ScrollTo(BySelector{ContentDescription: "电池"}) +// if err != nil { +// t.Fatal(err) +// } +//} - rect, err := elements[0].Rect() - if err != nil { - t.Fatal(err) - } - - // err = elements[0].Drag(300, 450, 256) - err = elements[0].Drag(300, 450, 256) - if err != nil { - t.Fatal(err) - } - - err = elements[0].DragTo(elements[1], 256) - if err != nil { - t.Fatal(err) - } - - endPoint := PointF{X: float64(rect.X + rect.Width/3*2), Y: float64(rect.Y + rect.Height/2)} - err = elements[0].DragPointF(endPoint, 256) - if err != nil { - t.Fatal() - } -} - -func TestElement_Flick(t *testing.T) { - driver, err := NewUIADriver(nil, uiaServerURL) - if err != nil { - t.Fatal(err) - } - - elem, err := driver.FindElement(AndroidBySelector{UiAutomator: "new UiSelector().text(\"提示音和通知\");"}) - if err != nil { - t.Fatal(err) - } - - err = elem.Flick(36, 20, 100) - if err != nil { - t.Fatal(err) - } -} - -func TestElement_ScrollTo(t *testing.T) { - driver, err := NewUIADriver(nil, uiaServerURL) - if err != nil { - t.Fatal(err) - } - - // how to make it work? - // parentElem, err := driver.FindElement(AndroidBySelector{ClassName: "android.widget.ScrollView"}) - // parentElem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "com.cyanogenmod.filemanager:id/navigation_view_layout"}) - parentElem, err := driver.FindElement(AndroidBySelector{ResourceIdID: "com.android.settings:id/dashboard"}) - if err != nil { - t.Fatal(err) - } - - err = parentElem.ScrollTo(AndroidBySelector{ContentDescription: "电池"}) - if err != nil { - t.Fatal(err) - } -} - -func TestElement_ScrollToElement(t *testing.T) { - // android.widget.HorizontalScrollView - driver, err := NewUIADriver(nil, uiaServerURL) - if err != nil { - t.Fatal(err) - } - - // how to make it work? - parentElem, err := driver.FindElement(AndroidBySelector{UiAutomator: "new UiSelector().resourceId(\"com.android.settings:id/dashboard\");"}) - if err != nil { - t.Fatal(err) - } - - element, err := driver.FindElement(AndroidBySelector{UiAutomator: "new UiSelector().text(\"电池\");"}) - if err != nil { - t.Fatal(err) - } - - err = parentElem.ScrollToElement(element) - if err != nil { - t.Fatal(err) - } -} +//func TestElement_ScrollToElement(t *testing.T) { +// // android.widget.HorizontalScrollView +// driver, err := NewUIADriver(nil, uiaServerURL) +// if err != nil { +// t.Fatal(err) +// } +// +// // how to make it work? +// parentElem, err := driver.FindElement(BySelector{UiAutomator: "new UiSelector().resourceId(\"com.android.settings:id/dashboard\");"}) +// if err != nil { +// t.Fatal(err) +// } +// +// element, err := driver.FindElement(BySelector{UiAutomator: "new UiSelector().text(\"电池\");"}) +// if err != nil { +// t.Fatal(err) +// } +// +// err = parentElem.ScrollToElement(element) +// if err != nil { +// t.Fatal(err) +// } +//} diff --git a/hrp/internal/uixt/client.go b/hrp/internal/uixt/client.go index befa38d6..5ebd9309 100644 --- a/hrp/internal/uixt/client.go +++ b/hrp/internal/uixt/client.go @@ -51,7 +51,7 @@ func (wd *Driver) httpDELETE(pathElem ...string) (rawResp rawResponse, err error } func (wd *Driver) httpRequest(method string, rawURL string, rawBody []byte) (rawResp rawResponse, err error) { - log.Debug().Str("method", method).Str("url", rawURL).Str("body", string(rawBody)).Msg("request WDA") + 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 { @@ -77,7 +77,7 @@ func (wd *Driver) httpRequest(method string, rawURL string, rawBody []byte) (raw // avoid printing screenshot data logger.Str("response", string(rawResp)) } - logger.Msg("get WDA response") + logger.Msg("get driver agent response") if err != nil { return nil, err } diff --git a/hrp/internal/uixt/ext.go b/hrp/internal/uixt/ext.go index e333ed48..41dea6d9 100644 --- a/hrp/internal/uixt/ext.go +++ b/hrp/internal/uixt/ext.go @@ -237,6 +237,11 @@ func (dExt *DriverExt) FindUIElement(param string) (ele WebElement, err error) { selector = BySelector{ XPath: param, } + } else if strings.HasPrefix(param, "com.") { + // name + selector = BySelector{ + ResourceIdID: param, + } } else { // name selector = BySelector{ @@ -473,18 +478,13 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { dExt.ScreenShots = append(dExt.ScreenShots, screenshotPath) return err case CtlStartCamera: - // start camera, alias for app_launch com.apple.camera - return dExt.Driver.AppLaunch("com.apple.camera") + return dExt.Driver.StartCamera() case CtlStopCamera: - // stop camera, alias for app_terminate com.apple.camera - success, err := dExt.Driver.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 + return dExt.Driver.StopCamera() + case RecordStart: + return dExt.Driver.StartRecording() + case RecordStop: + return dExt.Driver.StopRecording() } return nil } diff --git a/hrp/internal/uixt/interface.go b/hrp/internal/uixt/interface.go index 03fd7a19..346233f4 100644 --- a/hrp/internal/uixt/interface.go +++ b/hrp/internal/uixt/interface.go @@ -148,6 +148,55 @@ type DeviceInfo struct { Name string `json:"name"` IsSimulator bool `json:"isSimulator"` ThermalState int `json:"thermalState"` + // ANDROID_ID A 64-bit number (as a hex string) that is uniquely generated when the user + // first sets up the device and should remain constant for the lifetime of the user's device. The value + // may change if a factory reset is performed on the device. + AndroidID string `json:"androidId"` + // Build.MANUFACTURER value + Manufacturer string `json:"manufacturer"` + // Build.BRAND value + Brand string `json:"brand"` + // Current running OS's API VERSION + APIVersion string `json:"apiVersion"` + // The current version string, for example "1.0" or "3.4b5" + PlatformVersion string `json:"platformVersion"` + // the name of the current celluar network carrier + CarrierName string `json:"carrierName"` + // the real size of the default display + RealDisplaySize string `json:"realDisplaySize"` + // The logical density of the display in Density Independent Pixel units. + DisplayDensity int `json:"displayDensity"` + // available networks + Networks []networkInfo `json:"networks"` + // current system locale + Locale string `json:"locale"` + Bluetooth struct { + State string `json:"state"` + } `json:"bluetooth"` +} + +type networkCapabilities struct { + TransportTypes string `json:"transportTypes"` + NetworkCapabilities string `json:"networkCapabilities"` + LinkUpstreamBandwidthKbps int `json:"linkUpstreamBandwidthKbps"` + LinkDownBandwidthKbps int `json:"linkDownBandwidthKbps"` + SignalStrength int `json:"signalStrength"` + SSID string `json:"SSID"` +} + +type networkInfo struct { + Type int `json:"type"` + TypeName string `json:"typeName"` + Subtype int `json:"subtype"` + SubtypeName string `json:"subtypeName"` + IsConnected bool `json:"isConnected"` + DetailedState string `json:"detailedState"` + State string `json:"state"` + ExtraInfo string `json:"extraInfo"` + IsAvailable bool `json:"isAvailable"` + IsRoaming bool `json:"isRoaming"` + IsFailover bool `json:"isFailover"` + Capabilities networkCapabilities `json:"capabilities"` } type Location struct { @@ -267,6 +316,11 @@ func (opt AppLaunchOption) WithEnvironment(env map[string]string) AppLaunchOptio return opt } +func (opt AppLaunchOption) WithAndroidBySelector(waitForComplete ...AndroidBySelector) AppLaunchOption { + opt["androidBySelector"] = waitForComplete + return opt +} + // PasteboardType The type of the item on the pasteboard. type PasteboardType string @@ -426,6 +480,13 @@ type BySelector struct { ClassChain string `json:"class chain"` XPath string `json:"xpath"` // not recommended, it's slow because it is not supported by XCTest natively + + // 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"` + + UiAutomator string `json:"-android uiautomator"` } func (wl BySelector) getUsingAndValue() (using, value string) { @@ -449,6 +510,24 @@ func (wl BySelector) getUsingAndValue() (using, value string) { return } +func (by BySelector) 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 +} + type ElementAttribute map[string]interface{} func (ea ElementAttribute) String() string { @@ -800,6 +879,14 @@ type WebDriver interface { // AppAuthReset Resets the authorization status for a protected resource. Available since Xcode 11.4 AppAuthReset(ProtectedResource) error + // StartCamera Starts a new camera for recording + StartCamera() error + // StopCamera Stops the camera for recording + StopCamera() error + + StartRecording() error + StopRecording() error + // Tap Sends a tap event at the coordinate. Tap(x, y int, options ...DataOption) error TapFloat(x, y float64, options ...DataOption) error diff --git a/hrp/internal/uixt/ios_device.go b/hrp/internal/uixt/ios_device.go index bfacfb72..da9de53e 100644 --- a/hrp/internal/uixt/ios_device.go +++ b/hrp/internal/uixt/ios_device.go @@ -241,14 +241,17 @@ func (dExt *DriverExt) StartLogRecording(identifier string) error { func (dExt *DriverExt) GetLogs() (interface{}, error) { log.Info().Msg("stop WDA log recording") - data := map[string]interface{}{"action": "stop"} - reply, err := dExt.triggerWDALog(data) - if err != nil { - log.Error().Err(err).Msg("failed to get WDA logs") - return "", errors.Wrap(err, "failed to get WDA logs") + if _, ok := dExt.Driver.(*wdaDriver); ok { + data := map[string]interface{}{"action": "stop"} + reply, err := dExt.triggerWDALog(data) + if err != nil { + return "", errors.Wrap(err, "failed to get WDA logs") + } + return reply.Value, nil + } else { + // TODO: Android log recording } - - return reply.Value, nil + return "", nil } func (dExt *DriverExt) triggerWDALog(data map[string]interface{}) (*wdaResponse, error) { diff --git a/hrp/internal/uixt/ios_driver.go b/hrp/internal/uixt/ios_driver.go index 9e5eda75..771d7d36 100644 --- a/hrp/internal/uixt/ios_driver.go +++ b/hrp/internal/uixt/ios_driver.go @@ -13,6 +13,7 @@ import ( giDevice "github.com/electricbubble/gidevice" "github.com/pkg/errors" + "github.com/rs/zerolog/log" "github.com/httprunner/httprunner/v4/hrp/internal/json" ) @@ -557,6 +558,33 @@ func (wd *wdaDriver) IOHIDEvent(pageID EventPageID, usageID EventUsageID, durati 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) StartRecording() (err error) { + // TODO + return errDriverNotImplemented +} + +func (wd *wdaDriver) StopRecording() (err error) { + // TODO + return errDriverNotImplemented +} + 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 { diff --git a/hrp/step_android_ui.go b/hrp/step_android_ui.go index b5b74f72..5397f1b2 100644 --- a/hrp/step_android_ui.go +++ b/hrp/step_android_ui.go @@ -9,6 +9,14 @@ import ( "github.com/httprunner/httprunner/v4/hrp/internal/uixt" ) +var ( + WithSerialNumber = uixt.WithSerialNumber + WithAdbIP = uixt.WithAdbIP + WithAdbPort = uixt.WithAdbPort + WithAdbLogOn = uixt.WithAdbLogOn + WithMjpegPortA = uixt.WithMjpegPortA +) + type AndroidStep struct { uixt.AndroidDevice `yaml:",inline"` // inline refers to https://pkg.go.dev/gopkg.in/yaml.v3#Marshal uixt.MobileAction @@ -33,6 +41,38 @@ func (s *StepAndroid) InstallApp(path string) *StepAndroid { return &StepAndroid{step: s.step} } +func (s *StepAndroid) AppLaunch(bundleId string) *StepAndroid { + s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{ + Method: uixt.AppLaunch, + Params: bundleId, + }) + return s +} + +func (s *StepAndroid) AppLaunchUnattached(bundleId string) *StepAndroid { + s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{ + Method: uixt.AppLaunchUnattached, + Params: bundleId, + }) + return s +} + +func (s *StepAndroid) AppTerminate(bundleId string) *StepAndroid { + s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{ + Method: uixt.AppTerminate, + Params: bundleId, + }) + return s +} + +func (s *StepAndroid) Home() *StepAndroid { + s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{ + Method: uixt.ACTION_Home, + Params: nil, + }) + return &StepAndroid{step: s.step} +} + func (s *StepAndroid) StartAppByIntent(activity string) *StepAndroid { s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{ Method: uixt.AppStart, @@ -81,51 +121,101 @@ func (s *StepAndroid) Tap(params interface{}) *StepAndroid { return &StepAndroid{step: s.step} } -func (s *StepAndroid) DoubleTap(params interface{}) *StepAndroid { - s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{ +// Tap taps on the target element by OCR recognition +func (s *StepAndroid) TapByOCR(ocrText string, options ...uixt.ActionOption) *StepAndroid { + action := uixt.MobileAction{ + Method: uixt.ACTION_TapByOCR, + Params: ocrText, + } + for _, option := range options { + option(&action) + } + s.step.Android.Actions = append(s.step.Android.Actions, action) + return &StepAndroid{step: s.step} +} + +// Tap taps on the target element by CV recognition +func (s *StepAndroid) TapByCV(imagePath string, options ...uixt.ActionOption) *StepAndroid { + action := uixt.MobileAction{ + Method: uixt.ACTION_TapByCV, + Params: imagePath, + } + for _, option := range options { + option(&action) + } + s.step.Android.Actions = append(s.step.Android.Actions, action) + return &StepAndroid{step: s.step} +} + +func (s *StepAndroid) DoubleTap(params string, options ...uixt.ActionOption) *StepAndroid { + action := uixt.MobileAction{ Method: uixt.ACTION_DoubleTap, Params: params, - }) + } + for _, option := range options { + option(&action) + } + s.step.Android.Actions = append(s.step.Android.Actions, action) return &StepAndroid{step: s.step} } -func (s *StepAndroid) Swipe(sx, sy, ex, ey int) *StepAndroid { - s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{ +func (s *StepAndroid) Swipe(sx, sy, ex, ey int, options ...uixt.ActionOption) *StepAndroid { + action := uixt.MobileAction{ Method: uixt.ACTION_Swipe, Params: []int{sx, sy, ex, ey}, - }) + } + for _, option := range options { + option(&action) + } + s.step.Android.Actions = append(s.step.Android.Actions, action) return &StepAndroid{step: s.step} } -func (s *StepAndroid) SwipeUp() *StepAndroid { - s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{ +func (s *StepAndroid) SwipeUp(options ...uixt.ActionOption) *StepAndroid { + action := uixt.MobileAction{ Method: uixt.ACTION_Swipe, Params: "up", - }) + } + for _, option := range options { + option(&action) + } + s.step.Android.Actions = append(s.step.Android.Actions, action) return &StepAndroid{step: s.step} } -func (s *StepAndroid) SwipeDown() *StepAndroid { - s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{ +func (s *StepAndroid) SwipeDown(options ...uixt.ActionOption) *StepAndroid { + action := uixt.MobileAction{ Method: uixt.ACTION_Swipe, Params: "down", - }) + } + for _, option := range options { + option(&action) + } + s.step.Android.Actions = append(s.step.Android.Actions, action) return &StepAndroid{step: s.step} } -func (s *StepAndroid) SwipeLeft() *StepAndroid { - s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{ +func (s *StepAndroid) SwipeLeft(options ...uixt.ActionOption) *StepAndroid { + action := uixt.MobileAction{ Method: uixt.ACTION_Swipe, Params: "left", - }) + } + for _, option := range options { + option(&action) + } + s.step.Android.Actions = append(s.step.Android.Actions, action) return &StepAndroid{step: s.step} } -func (s *StepAndroid) SwipeRight() *StepAndroid { - s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{ +func (s *StepAndroid) SwipeRight(options ...uixt.ActionOption) *StepAndroid { + action := uixt.MobileAction{ Method: uixt.ACTION_Swipe, Params: "right", - }) + } + for _, option := range options { + option(&action) + } + s.step.Android.Actions = append(s.step.Android.Actions, action) return &StepAndroid{step: s.step} } @@ -137,6 +227,47 @@ func (s *StepAndroid) Input(text string) *StepAndroid { return &StepAndroid{step: s.step} } +// Sleep specify sleep seconds after last action +func (s *StepAndroid) Sleep(n float64) *StepAndroid { + s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{ + Method: uixt.CtlSleep, + Params: n, + }) + return &StepAndroid{step: s.step} +} + +func (s *StepAndroid) ScreenShot() *StepAndroid { + s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{ + Method: uixt.CtlScreenShot, + Params: nil, + }) + return &StepAndroid{step: s.step} +} + +func (s *StepAndroid) SwipeToTapApp(appName string, options ...uixt.ActionOption) *StepAndroid { + action := uixt.MobileAction{ + Method: uixt.ACTION_SwipeToTapApp, + Params: appName, + } + for _, option := range options { + option(&action) + } + s.step.Android.Actions = append(s.step.Android.Actions, action) + return &StepAndroid{step: s.step} +} + +func (s *StepAndroid) SwipeToTapText(text string, options ...uixt.ActionOption) *StepAndroid { + action := uixt.MobileAction{ + Method: uixt.ACTION_SwipeToTapText, + Params: text, + } + for _, option := range options { + option(&action) + } + s.step.Android.Actions = append(s.step.Android.Actions, action) + return &StepAndroid{step: s.step} +} + // Validate switches to step validation. func (s *StepAndroid) Validate() *StepAndroidValidation { return &StepAndroidValidation{ @@ -195,6 +326,96 @@ func (s *StepAndroidValidation) AssertNameNotExists(expectedName string, msg ... return s } +func (s *StepAndroidValidation) AssertLabelExists(expectedLabel string, msg ...string) *StepAndroidValidation { + v := Validator{ + Check: uixt.SelectorLabel, + Assert: uixt.AssertionExists, + Expect: expectedLabel, + } + if len(msg) > 0 { + v.Message = msg[0] + } else { + v.Message = fmt.Sprintf("attribute label [%s] not found", expectedLabel) + } + s.step.Validators = append(s.step.Validators, v) + return s +} + +func (s *StepAndroidValidation) AssertLabelNotExists(expectedLabel string, msg ...string) *StepAndroidValidation { + v := Validator{ + Check: uixt.SelectorLabel, + Assert: uixt.AssertionNotExists, + Expect: expectedLabel, + } + if len(msg) > 0 { + v.Message = msg[0] + } else { + v.Message = fmt.Sprintf("attribute label [%s] should not exist", expectedLabel) + } + s.step.Validators = append(s.step.Validators, v) + return s +} + +func (s *StepAndroidValidation) AssertOCRExists(expectedText string, msg ...string) *StepAndroidValidation { + v := Validator{ + Check: uixt.SelectorOCR, + Assert: uixt.AssertionExists, + Expect: expectedText, + } + if len(msg) > 0 { + v.Message = msg[0] + } else { + v.Message = fmt.Sprintf("ocr text [%s] not found", expectedText) + } + s.step.Validators = append(s.step.Validators, v) + return s +} + +func (s *StepAndroidValidation) AssertOCRNotExists(expectedText string, msg ...string) *StepAndroidValidation { + v := Validator{ + Check: uixt.SelectorOCR, + Assert: uixt.AssertionNotExists, + Expect: expectedText, + } + if len(msg) > 0 { + v.Message = msg[0] + } else { + v.Message = fmt.Sprintf("ocr text [%s] should not exist", expectedText) + } + s.step.Validators = append(s.step.Validators, v) + return s +} + +func (s *StepAndroidValidation) AssertImageExists(expectedImagePath string, msg ...string) *StepAndroidValidation { + v := Validator{ + Check: uixt.SelectorImage, + Assert: uixt.AssertionExists, + Expect: expectedImagePath, + } + if len(msg) > 0 { + v.Message = msg[0] + } else { + v.Message = fmt.Sprintf("cv image [%s] not found", expectedImagePath) + } + s.step.Validators = append(s.step.Validators, v) + return s +} + +func (s *StepAndroidValidation) AssertImageNotExists(expectedImagePath string, msg ...string) *StepAndroidValidation { + v := Validator{ + Check: uixt.SelectorImage, + Assert: uixt.AssertionNotExists, + Expect: expectedImagePath, + } + if len(msg) > 0 { + v.Message = msg[0] + } else { + v.Message = fmt.Sprintf("cv image [%s] should not exist", expectedImagePath) + } + s.step.Validators = append(s.step.Validators, v) + return s +} + func (s *StepAndroidValidation) Name() string { return s.step.Name } From 2ad38c20318bc5830047aefcb3340db5886f7767 Mon Sep 17 00:00:00 2001 From: xucong053 Date: Wed, 28 Sep 2022 14:36:00 +0800 Subject: [PATCH 3/9] update: android douyin demo --- examples/uitest/demo_android_douyin_test.go | 13 ++++++++++--- hrp/internal/uixt/ext.go | 2 +- hrp/internal/uixt/ios_device.go | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/examples/uitest/demo_android_douyin_test.go b/examples/uitest/demo_android_douyin_test.go index 4bf95693..9e5f2477 100644 --- a/examples/uitest/demo_android_douyin_test.go +++ b/examples/uitest/demo_android_douyin_test.go @@ -11,9 +11,16 @@ func TestIOSDouYinLive(t *testing.T) { Config: hrp.NewConfig("通过 feed 头像进入抖音直播间"). SetAndroid(hrp.WithAdbLogOn(true), hrp.WithMjpegPortA(9100)), TestSteps: []hrp.IStep{ + hrp.NewStep("打开网页"). + Android(). + Home(). + AppTerminate("com.google.android.apps.chrome.Main").Sleep(1). // 关闭已运行的抖音,确保启动抖音后在「抖音」首页 + SwipeToTapApp("Chrome", hrp.WithMaxRetryTimes(5)).TapByOCR("Search").Input("https://gtftask.bytedance.com/local-time").TapByOCR("前往").Sleep(5). + Validate(). + AssertOCRExists("1664", "网页打开失败"), hrp.NewStep("启动抖音"). Android(). - Home().StartCamera().Sleep(10).StopCamera(). + Home(). AppTerminate("com.ss.android.ugc.aweme"). // 关闭已运行的抖音,确保启动抖音后在「抖音」首页 SwipeToTapApp("抖音", hrp.WithMaxRetryTimes(5)). Sleep(10), @@ -33,10 +40,10 @@ func TestIOSDouYinLive(t *testing.T) { }, } - if err := testCase.Dump2JSON("android_demo_douyin_live.json"); err != nil { + if err := testCase.Dump2JSON("demo_android_douyin_live.json"); err != nil { t.Fatal(err) } - if err := testCase.Dump2YAML("android_demo_douyin_live.yaml"); err != nil { + if err := testCase.Dump2YAML("demo_android_douyin_live.yaml"); err != nil { t.Fatal(err) } diff --git a/hrp/internal/uixt/ext.go b/hrp/internal/uixt/ext.go index 41dea6d9..b2daad9c 100644 --- a/hrp/internal/uixt/ext.go +++ b/hrp/internal/uixt/ext.go @@ -304,7 +304,7 @@ func (dExt *DriverExt) IsImageExist(text string) bool { 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 iOS UI action") + log.Info().Str("method", string(action.Method)).Interface("params", action.Params).Msg("start UI action") switch action.Method { case AppInstall: diff --git a/hrp/internal/uixt/ios_device.go b/hrp/internal/uixt/ios_device.go index da9de53e..9a5f6272 100644 --- a/hrp/internal/uixt/ios_device.go +++ b/hrp/internal/uixt/ios_device.go @@ -240,7 +240,7 @@ func (dExt *DriverExt) StartLogRecording(identifier string) error { } func (dExt *DriverExt) GetLogs() (interface{}, error) { - log.Info().Msg("stop WDA log recording") + log.Info().Msg("stop log recording") if _, ok := dExt.Driver.(*wdaDriver); ok { data := map[string]interface{}{"action": "stop"} reply, err := dExt.triggerWDALog(data) From 2a8e8a965ceba65c880aa3cc422b8a473dccc86b Mon Sep 17 00:00:00 2001 From: xucong053 Date: Wed, 28 Sep 2022 14:57:41 +0800 Subject: [PATCH 4/9] fix: remove android mjpeg option --- examples/uitest/demo_android_douyin_test.go | 6 +++--- hrp/internal/uixt/android_device.go | 6 ------ hrp/internal/uixt/opencv_on.go | 1 + hrp/step_android_ui.go | 1 - 4 files changed, 4 insertions(+), 10 deletions(-) diff --git a/examples/uitest/demo_android_douyin_test.go b/examples/uitest/demo_android_douyin_test.go index 9e5f2477..a0d946f2 100644 --- a/examples/uitest/demo_android_douyin_test.go +++ b/examples/uitest/demo_android_douyin_test.go @@ -6,16 +6,16 @@ import ( "github.com/httprunner/httprunner/v4/hrp" ) -func TestIOSDouYinLive(t *testing.T) { +func TestAndroidDouYinLive(t *testing.T) { testCase := &hrp.TestCase{ Config: hrp.NewConfig("通过 feed 头像进入抖音直播间"). - SetAndroid(hrp.WithAdbLogOn(true), hrp.WithMjpegPortA(9100)), + SetAndroid(hrp.WithAdbLogOn(true)), TestSteps: []hrp.IStep{ hrp.NewStep("打开网页"). Android(). Home(). AppTerminate("com.google.android.apps.chrome.Main").Sleep(1). // 关闭已运行的抖音,确保启动抖音后在「抖音」首页 - SwipeToTapApp("Chrome", hrp.WithMaxRetryTimes(5)).TapByOCR("Search").Input("https://gtftask.bytedance.com/local-time").TapByOCR("前往").Sleep(5). + SwipeToTapApp("Chrome", hrp.WithMaxRetryTimes(5)).TapByOCR("搜索").Input("https://gtftask.bytedance.com/local-time").TapByOCR("前往").Sleep(5). Validate(). AssertOCRExists("1664", "网页打开失败"), hrp.NewStep("启动抖音"). diff --git a/hrp/internal/uixt/android_device.go b/hrp/internal/uixt/android_device.go index 275e4cb9..568adcdf 100644 --- a/hrp/internal/uixt/android_device.go +++ b/hrp/internal/uixt/android_device.go @@ -65,12 +65,6 @@ func WithSerialNumber(serial string) AndroidDeviceOption { } } -func WithMjpegPortA(port int) AndroidDeviceOption { - return func(device *AndroidDevice) { - device.MjpegPort = port - } -} - func WithAdbIP(ip string) AndroidDeviceOption { return func(device *AndroidDevice) { device.IP = ip diff --git a/hrp/internal/uixt/opencv_on.go b/hrp/internal/uixt/opencv_on.go index 2153283f..d4acda45 100644 --- a/hrp/internal/uixt/opencv_on.go +++ b/hrp/internal/uixt/opencv_on.go @@ -5,6 +5,7 @@ package uixt import ( "bytes" "image" + "io/ioutil" "os" cvHelper "github.com/electricbubble/opencv-helper" diff --git a/hrp/step_android_ui.go b/hrp/step_android_ui.go index 5397f1b2..53168cf2 100644 --- a/hrp/step_android_ui.go +++ b/hrp/step_android_ui.go @@ -14,7 +14,6 @@ var ( WithAdbIP = uixt.WithAdbIP WithAdbPort = uixt.WithAdbPort WithAdbLogOn = uixt.WithAdbLogOn - WithMjpegPortA = uixt.WithMjpegPortA ) type AndroidStep struct { From 0e822053297556b912364b6e5301f4da734ccd70 Mon Sep 17 00:00:00 2001 From: xucong053 Date: Wed, 28 Sep 2022 16:14:00 +0800 Subject: [PATCH 5/9] fix: update unittest for action --- .github/workflows/unittest.yml | 2 -- hrp/internal/uixt/android_test.go | 2 ++ hrp/internal/uixt/drag_test.go | 2 ++ hrp/internal/uixt/ios_test.go | 1 + hrp/internal/uixt/swipe_test.go | 2 ++ hrp/internal/uixt/tap_test.go | 2 ++ hrp/internal/uixt/touch_test.go | 2 ++ hrp/step_android_ui_test.go | 2 ++ hrp/step_ios_ui_test.go | 3 ++- 9 files changed, 15 insertions(+), 3 deletions(-) diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index 77e221fc..e8b5d983 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -63,8 +63,6 @@ jobs: fail-fast: false matrix: go-version: - - 1.16.x - - 1.17.x - 1.18.x os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} diff --git a/hrp/internal/uixt/android_test.go b/hrp/internal/uixt/android_test.go index 08a71c17..71c3790c 100644 --- a/hrp/internal/uixt/android_test.go +++ b/hrp/internal/uixt/android_test.go @@ -1,3 +1,5 @@ +//go:build localtest + package uixt import ( diff --git a/hrp/internal/uixt/drag_test.go b/hrp/internal/uixt/drag_test.go index 258c515c..f2a628c5 100644 --- a/hrp/internal/uixt/drag_test.go +++ b/hrp/internal/uixt/drag_test.go @@ -1,3 +1,5 @@ +//go:build localtest + package uixt import ( diff --git a/hrp/internal/uixt/ios_test.go b/hrp/internal/uixt/ios_test.go index 62c52161..efc61518 100644 --- a/hrp/internal/uixt/ios_test.go +++ b/hrp/internal/uixt/ios_test.go @@ -1,3 +1,4 @@ +//go:build localtest package uixt import ( diff --git a/hrp/internal/uixt/swipe_test.go b/hrp/internal/uixt/swipe_test.go index 5314a663..61ded0af 100644 --- a/hrp/internal/uixt/swipe_test.go +++ b/hrp/internal/uixt/swipe_test.go @@ -1,3 +1,5 @@ +//go:build localtest + package uixt import ( diff --git a/hrp/internal/uixt/tap_test.go b/hrp/internal/uixt/tap_test.go index faaaa14e..c5dcafa7 100644 --- a/hrp/internal/uixt/tap_test.go +++ b/hrp/internal/uixt/tap_test.go @@ -1,3 +1,5 @@ +//go:build localtest + package uixt import ( diff --git a/hrp/internal/uixt/touch_test.go b/hrp/internal/uixt/touch_test.go index 9ec38aee..a4b25ade 100644 --- a/hrp/internal/uixt/touch_test.go +++ b/hrp/internal/uixt/touch_test.go @@ -1,3 +1,5 @@ +//go:build localtest + package uixt import ( diff --git a/hrp/step_android_ui_test.go b/hrp/step_android_ui_test.go index 7ca077f9..7162eef9 100644 --- a/hrp/step_android_ui_test.go +++ b/hrp/step_android_ui_test.go @@ -1,3 +1,5 @@ +//go:build localtest + package hrp import ( diff --git a/hrp/step_ios_ui_test.go b/hrp/step_ios_ui_test.go index 3de1f6db..4b60b04c 100644 --- a/hrp/step_ios_ui_test.go +++ b/hrp/step_ios_ui_test.go @@ -1,3 +1,4 @@ +//go:build localtest package hrp import ( @@ -85,7 +86,7 @@ func TestIOSWeixinLive(t *testing.T) { NewStep("进入直播页"). IOS(). Tap("发现").Sleep(5). // 进入「发现页」;等待 5 秒确保加载完成 - TapByOCR("直播"). // 通过 OCR 识别「直播」 + TapByOCR("直播"). // 通过 OCR 识别「直播」 Validate(). AssertLabelExists("直播"), NewStep("向上滑动 5 次"). From d59fb4477f504554212a4142864e6624804a2026 Mon Sep 17 00:00:00 2001 From: xucong053 Date: Wed, 28 Sep 2022 16:39:23 +0800 Subject: [PATCH 6/9] fix: unittest --- hrp/runner.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hrp/runner.go b/hrp/runner.go index 2524ff8c..bbfd8655 100644 --- a/hrp/runner.go +++ b/hrp/runner.go @@ -215,7 +215,7 @@ func (r *HRPRunner) Run(testcases ...ITestCase) error { s.appendCaseSummary(caseSummary) if err1 != nil || err2 != nil { log.Error().Err(err1).Msg("[Run] run testcase failed") - runErr = err + runErr = err1 break } } From 1e52e5b4da4ad5fd712ccb699c597cc673b039df Mon Sep 17 00:00:00 2001 From: xucong053 Date: Wed, 28 Sep 2022 16:43:54 +0800 Subject: [PATCH 7/9] fix: workflows runs only in go version 1.18 --- .github/workflows/hrp-scaffold.yml | 6 ++--- .github/workflows/smoketest.yml | 2 +- .../plugin/debugtalk_gen.go | 16 ------------- examples/demo-with-go-plugin/proj.json | 4 ++-- .../demo-with-py-plugin/.debugtalk_gen.py | 23 ------------------- examples/demo-with-py-plugin/proj.json | 4 ++-- 6 files changed, 8 insertions(+), 47 deletions(-) delete mode 100644 examples/demo-with-go-plugin/plugin/debugtalk_gen.go delete mode 100644 examples/demo-with-py-plugin/.debugtalk_gen.py diff --git a/.github/workflows/hrp-scaffold.yml b/.github/workflows/hrp-scaffold.yml index e513435a..2d0ab07d 100644 --- a/.github/workflows/hrp-scaffold.yml +++ b/.github/workflows/hrp-scaffold.yml @@ -17,7 +17,7 @@ jobs: fail-fast: false matrix: go-version: - - 1.17.x + - 1.18.x os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: @@ -44,7 +44,7 @@ jobs: fail-fast: false matrix: go-version: - - 1.17.x + - 1.18.x os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: @@ -76,7 +76,7 @@ jobs: fail-fast: false matrix: go-version: - - 1.17.x + - 1.18.x os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: diff --git a/.github/workflows/smoketest.yml b/.github/workflows/smoketest.yml index f914858a..efca234e 100644 --- a/.github/workflows/smoketest.yml +++ b/.github/workflows/smoketest.yml @@ -56,7 +56,7 @@ jobs: fail-fast: false matrix: go-version: - - 1.17.x + - 1.18.x os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: diff --git a/examples/demo-with-go-plugin/plugin/debugtalk_gen.go b/examples/demo-with-go-plugin/plugin/debugtalk_gen.go deleted file mode 100644 index 0ee1ae22..00000000 --- a/examples/demo-with-go-plugin/plugin/debugtalk_gen.go +++ /dev/null @@ -1,16 +0,0 @@ -// NOTE: Generated By hrp v4.1.5, DO NOT EDIT! -package main - -import ( - "github.com/httprunner/funplugin/fungo" -) - -func main() { - fungo.Register("SumTwoInt", SumTwoInt) - fungo.Register("SumInts", SumInts) - fungo.Register("Sum", Sum) - fungo.Register("SetupHookExample", SetupHookExample) - fungo.Register("TeardownHookExample", TeardownHookExample) - fungo.Register("GetUserAgent", GetUserAgent) - fungo.Serve() -} diff --git a/examples/demo-with-go-plugin/proj.json b/examples/demo-with-go-plugin/proj.json index ecc3509d..3225b92e 100644 --- a/examples/demo-with-go-plugin/proj.json +++ b/examples/demo-with-go-plugin/proj.json @@ -1,5 +1,5 @@ { "project_name": "demo-with-go-plugin", - "create_time": "2022-07-26T10:30:29.31361+08:00", - "hrp_version": "v4.2.0" + "create_time": "2022-09-28T16:40:14.674398+08:00", + "hrp_version": "v4.3.0" } diff --git a/examples/demo-with-py-plugin/.debugtalk_gen.py b/examples/demo-with-py-plugin/.debugtalk_gen.py deleted file mode 100644 index 50f50e5f..00000000 --- a/examples/demo-with-py-plugin/.debugtalk_gen.py +++ /dev/null @@ -1,23 +0,0 @@ -# NOTE: Generated By hrp v4.1.6, DO NOT EDIT! - -import sys -import os - -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) - -from debugtalk import * - - -if __name__ == "__main__": - import funppy - funppy.register("get_user_agent", get_user_agent) - funppy.register("sleep", sleep) - funppy.register("sum", sum) - funppy.register("sum_ints", sum_ints) - funppy.register("sum_two_int", sum_two_int) - funppy.register("sum_two_string", sum_two_string) - funppy.register("sum_strings", sum_strings) - funppy.register("concatenate", concatenate) - funppy.register("setup_hook_example", setup_hook_example) - funppy.register("teardown_hook_example", teardown_hook_example) - funppy.serve() diff --git a/examples/demo-with-py-plugin/proj.json b/examples/demo-with-py-plugin/proj.json index 8140ae8b..a2b30841 100644 --- a/examples/demo-with-py-plugin/proj.json +++ b/examples/demo-with-py-plugin/proj.json @@ -1,5 +1,5 @@ { "project_name": "demo-with-py-plugin", - "create_time": "2022-07-26T10:30:30.601095+08:00", - "hrp_version": "v4.2.0" + "create_time": "2022-09-28T16:40:15.283869+08:00", + "hrp_version": "v4.3.0" } From 8e2f856393e26d0b24026e915f6feb1dfe8b0ae0 Mon Sep 17 00:00:00 2001 From: xucong053 Date: Wed, 28 Sep 2022 19:58:39 +0800 Subject: [PATCH 8/9] fix: unittest --- examples/uitest/demo_android_douyin_test.go | 2 ++ examples/uitest/demo_douyin_test.go | 2 ++ examples/uitest/demo_weixin_test.go | 2 ++ 3 files changed, 6 insertions(+) diff --git a/examples/uitest/demo_android_douyin_test.go b/examples/uitest/demo_android_douyin_test.go index a0d946f2..b43f8b9f 100644 --- a/examples/uitest/demo_android_douyin_test.go +++ b/examples/uitest/demo_android_douyin_test.go @@ -1,3 +1,5 @@ +//go:build localtest + package uitest import ( diff --git a/examples/uitest/demo_douyin_test.go b/examples/uitest/demo_douyin_test.go index b9ca8fa9..273be52e 100644 --- a/examples/uitest/demo_douyin_test.go +++ b/examples/uitest/demo_douyin_test.go @@ -1,3 +1,5 @@ +//go:build localtest + package uitest import ( diff --git a/examples/uitest/demo_weixin_test.go b/examples/uitest/demo_weixin_test.go index 112b6ffd..c4cd4380 100644 --- a/examples/uitest/demo_weixin_test.go +++ b/examples/uitest/demo_weixin_test.go @@ -1,3 +1,5 @@ +//go:build localtest + package uitest import ( From 7bad3a99d104a57224c896e48ed096c0b4c7791c Mon Sep 17 00:00:00 2001 From: xucong053 Date: Wed, 28 Sep 2022 20:25:19 +0800 Subject: [PATCH 9/9] fix: failed to generate html report --- hrp/internal/scaffold/templates/report/template.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hrp/internal/scaffold/templates/report/template.html b/hrp/internal/scaffold/templates/report/template.html index 4bff6c65..0575c021 100644 --- a/hrp/internal/scaffold/templates/report/template.html +++ b/hrp/internal/scaffold/templates/report/template.html @@ -338,14 +338,14 @@ - {{ if .Attachment }} + {{ if .Attachments }} traceback