From e98569722719509d0402f59f64dce48a1d087a34 Mon Sep 17 00:00:00 2001 From: "lilong.129" Date: Mon, 17 Feb 2025 22:02:12 +0800 Subject: [PATCH] fix: WDA tests --- internal/version/VERSION | 2 +- pkg/uixt/ai/cv.go | 5 - pkg/uixt/android_driver_uia2.go | 38 ++--- pkg/uixt/android_test.go | 5 +- pkg/uixt/driver_action.go | 3 +- pkg/uixt/driver_ext_screenshot.go | 13 +- pkg/uixt/driver_utils.go | 5 +- pkg/uixt/ios_driver_wda.go | 192 ++++++++++++------------ pkg/uixt/ios_test.go | 240 ++++++++++++------------------ pkg/uixt/option/action.go | 13 +- pkg/uixt/option/source.go | 107 +++++++------ server/ext/shoots.go | 2 +- 12 files changed, 298 insertions(+), 327 deletions(-) diff --git a/internal/version/VERSION b/internal/version/VERSION index 05eb3559..02e3e908 100644 --- a/internal/version/VERSION +++ b/internal/version/VERSION @@ -1 +1 @@ -v5.0.0+2502171750 +v5.0.0+2502172202 diff --git a/pkg/uixt/ai/cv.go b/pkg/uixt/ai/cv.go index 2c15d246..5a5cd6b4 100644 --- a/pkg/uixt/ai/cv.go +++ b/pkg/uixt/ai/cv.go @@ -316,8 +316,3 @@ func (p PointF) IsIdentical(p2 PointF) bool { // set the coordinate precision to 1 pixel return math.Abs(p.X-p2.X) < 1 && math.Abs(p.Y-p2.Y) < 1 } - -type Screen struct { - StatusBarSize types.Size `json:"statusBarSize"` - Scale float64 `json:"scale"` -} diff --git a/pkg/uixt/android_driver_uia2.go b/pkg/uixt/android_driver_uia2.go index 0c006e6f..d30a45ef 100644 --- a/pkg/uixt/android_driver_uia2.go +++ b/pkg/uixt/android_driver_uia2.go @@ -239,7 +239,7 @@ func (ud *UIA2Driver) TapAbsXY(x, y float64, opts ...option.ActionOption) error duration := 100.0 if actionOptions.PressDuration > 0 { - duration = actionOptions.PressDuration * 1000 + duration = actionOptions.PressDuration * 1000 // convert to ms } data := map[string]interface{}{ "actions": []interface{}{ @@ -256,9 +256,7 @@ func (ud *UIA2Driver) TapAbsXY(x, y float64, opts ...option.ActionOption) error }, }, } - - // update data options in post data for extra uiautomator configurations - actionOptions.UpdateData(data) + option.MergeOptions(data, opts...) _, err := ud.Session.POST(data, "/session", ud.Session.ID, "actions/tap") return err @@ -301,9 +299,7 @@ func (ud *UIA2Driver) Drag(fromX, fromY, toX, toY float64, opts ...option.Action "endX": toX, "endY": toY, } - - // update data options in post data for extra uiautomator configurations - actionOptions.UpdateData(data) + option.MergeOptions(data, opts...) // register(postHandler, new Drag("/wd/hub/session/:sessionId/touch/drag")) _, err = ud.Session.POST(data, "/session", ud.Session.ID, "touch/drag") @@ -328,7 +324,7 @@ func (ud *UIA2Driver) Swipe(fromX, fromY, toX, toY float64, opts ...option.Actio duration := 200.0 if actionOptions.PressDuration > 0 { - duration = actionOptions.PressDuration * 1000 + duration = actionOptions.PressDuration * 1000 // ms } data := map[string]interface{}{ "actions": []interface{}{ @@ -345,9 +341,7 @@ func (ud *UIA2Driver) Swipe(fromX, fromY, toX, toY float64, opts ...option.Actio }, }, } - - // update data options in post data for extra uiautomator configurations - actionOptions.UpdateData(data) + option.MergeOptions(data, opts...) _, err = ud.Session.POST(data, "/session", ud.Session.ID, "actions/swipe") return err @@ -400,18 +394,16 @@ func (ud *UIA2Driver) GetPasteboard(contentType types.PasteboardType) (raw *byte func (ud *UIA2Driver) Input(text string, opts ...option.ActionOption) (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 - actionOptions := option.NewActionOptions(opts...) err = ud.SendUnicodeKeys(text, opts...) - if err != nil { - data := map[string]interface{}{ - "text": text, - } - - // new data options in post data for extra uiautomator configurations - actionOptions.UpdateData(data) - - _, err = ud.Session.POST(data, "/session", ud.Session.ID, "/keys") + if err == nil { + return nil } + + data := map[string]interface{}{ + "text": text, + } + option.MergeOptions(data, opts...) + _, err = ud.Session.POST(data, "/session", ud.Session.ID, "/keys") return } @@ -446,7 +438,6 @@ func (ud *UIA2Driver) SendUnicodeKeys(text string, opts ...option.ActionOption) } func (ud *UIA2Driver) SendActionKey(text string, opts ...option.ActionOption) (err error) { - actionOptions := option.NewActionOptions(opts...) var actions []interface{} for i, c := range text { actions = append(actions, map[string]interface{}{"type": "keyDown", "value": string(c)}, @@ -465,9 +456,8 @@ func (ud *UIA2Driver) SendActionKey(text string, opts ...option.ActionOption) (e }, }, } + option.MergeOptions(data, opts...) - // new data options in post data for extra uiautomator configurations - actionOptions.UpdateData(data) _, err = ud.Session.POST(data, "/session", ud.Session.ID, "/actions/keys") return } diff --git a/pkg/uixt/android_test.go b/pkg/uixt/android_test.go index 68e3ffa8..723155c9 100644 --- a/pkg/uixt/android_test.go +++ b/pkg/uixt/android_test.go @@ -14,7 +14,10 @@ import ( "github.com/httprunner/httprunner/v5/pkg/uixt/types" ) -var driverExt *XTDriver +var ( + driver IDriver + driverExt *XTDriver +) func setupAndroidAdbDriver(t *testing.T) { device, err := NewAndroidDevice() diff --git a/pkg/uixt/driver_action.go b/pkg/uixt/driver_action.go index e0400d55..12ffdb89 100644 --- a/pkg/uixt/driver_action.go +++ b/pkg/uixt/driver_action.go @@ -176,8 +176,7 @@ func (dExt *XTDriver) DoAction(action MobileAction) (err error) { } case ACTION_GetSource: if packageName, ok := action.Params.(string); ok { - source := option.NewSourceOption().WithProcessName(packageName) - _, err = dExt.Source(source) + _, err = dExt.Source(option.WithProcessName(packageName)) if err != nil { return errors.Wrap(err, "failed to set ime") } diff --git a/pkg/uixt/driver_ext_screenshot.go b/pkg/uixt/driver_ext_screenshot.go index 613071e8..9ac9d31c 100644 --- a/pkg/uixt/driver_ext_screenshot.go +++ b/pkg/uixt/driver_ext_screenshot.go @@ -6,6 +6,7 @@ import ( "image" "image/gif" "image/jpeg" + "image/png" _ "image/png" "os" "path/filepath" @@ -214,14 +215,14 @@ func saveScreenShot(raw *bytes.Buffer, fileName string) (string, error) { // compress image and save to file switch format { - case "jpeg", "png": + case "jpeg": jpegOptions := &jpeg.Options{Quality: 95} err = jpeg.Encode(file, img, jpegOptions) - // case "png": - // encoder := png.Encoder{ - // CompressionLevel: png.BestCompression, - // } - // err = encoder.Encode(file, img) + case "png": + encoder := png.Encoder{ + CompressionLevel: png.BestCompression, + } + err = encoder.Encode(file, img) case "gif": gifOptions := &gif.Options{ NumColors: 256, diff --git a/pkg/uixt/driver_utils.go b/pkg/uixt/driver_utils.go index fb808db7..d343ac6d 100644 --- a/pkg/uixt/driver_utils.go +++ b/pkg/uixt/driver_utils.go @@ -2,6 +2,7 @@ package uixt import ( "fmt" + "math" "math/rand/v2" "time" @@ -43,8 +44,8 @@ func convertToAbsolutePoint(driver IDriver, x, y float64) (absX, absY float64, e return } - absX = float64(windowSize.Width) * x - absY = float64(windowSize.Height) * y + absX = math.Round(float64(windowSize.Width)*x*10) / 10 + absY = math.Round(float64(windowSize.Height)*y*10) / 10 return } diff --git a/pkg/uixt/ios_driver_wda.go b/pkg/uixt/ios_driver_wda.go index 2347a531..2afe13c5 100644 --- a/pkg/uixt/ios_driver_wda.go +++ b/pkg/uixt/ios_driver_wda.go @@ -9,7 +9,6 @@ import ( "math" "net" "net/http" - "net/url" "os" "os/exec" "path/filepath" @@ -25,7 +24,6 @@ import ( "github.com/httprunner/httprunner/v5/internal/builtin" "github.com/httprunner/httprunner/v5/internal/config" "github.com/httprunner/httprunner/v5/internal/json" - "github.com/httprunner/httprunner/v5/pkg/uixt/ai" "github.com/httprunner/httprunner/v5/pkg/uixt/option" "github.com/httprunner/httprunner/v5/pkg/uixt/types" ) @@ -37,58 +35,22 @@ func NewWDADriver(device *IOSDevice) (*WDADriver, error) { Session: NewDriverSession(), } - var err error - // forward local port to device - var localPort int - localPort, err = strconv.Atoi(os.Getenv("WDA_LOCAL_PORT")) - if err != nil { - localPort, err = builtin.GetFreePort() - if err != nil { - return nil, errors.Wrap(code.DeviceHTTPDriverError, - fmt.Sprintf("get free port failed: %v", err)) - } - if err = device.Forward(localPort, device.Options.WDAPort); err != nil { - return nil, errors.Wrap(code.DeviceHTTPDriverError, - fmt.Sprintf("forward tcp port failed: %v", err)) - } - } else { - log.Info().Int("WDA_LOCAL_PORT", localPort).Msg("reuse WDA local port") - } - - var localMjpegPort int - localMjpegPort, err = strconv.Atoi(os.Getenv("WDA_LOCAL_MJPEG_PORT")) - if err != nil { - localMjpegPort, err = builtin.GetFreePort() - if err != nil { - return nil, errors.Wrap(code.DeviceHTTPDriverError, - fmt.Sprintf("get free port failed: %v", err)) - } - if err = device.Forward(localMjpegPort, device.Options.WDAMjpegPort); err != nil { - return nil, errors.Wrap(code.DeviceHTTPDriverError, - fmt.Sprintf("forward tcp port failed: %v", err)) - } - } else { - log.Info().Int("WDA_LOCAL_MJPEG_PORT", localMjpegPort). - Msg("reuse WDA local mjpeg port") - } - host := "localhost" + localPort, err := driver.getLocalPort() + if err != nil { + return nil, err + } driver.Session.SetBaseURL(fmt.Sprintf("http://%s:%d", host, localPort)) + if err = driver.initMjpegClient(); err != nil { + return nil, err + } + // create new session if err = driver.InitSession(nil); err != nil { return nil, errors.Wrap(code.DeviceHTTPDriverError, err.Error()) } - if driver.mjpegHTTPConn, err = net.Dial( - "tcp", - fmt.Sprintf("%s:%d", host, localMjpegPort), - ); err != nil { - return nil, errors.Wrap(code.DeviceHTTPDriverError, err.Error()) - } - driver.mjpegClient = NewHTTPClientWithConnection(driver.mjpegHTTPConn, 30*time.Second) - driver.mjpegUrl = fmt.Sprintf("%s:%d", host, localMjpegPort) - // init WDA scale if driver.scale, err = driver.Scale(); err != nil { return nil, err @@ -110,6 +72,61 @@ type WDADriver struct { mjpegUrl string } +func (wd *WDADriver) getLocalPort() (int, error) { + localPort, err := strconv.Atoi(os.Getenv("WDA_LOCAL_PORT")) + if err != nil { + localPort, err = builtin.GetFreePort() + if err != nil { + return 0, errors.Wrap(code.DeviceHTTPDriverError, + fmt.Sprintf("get free port failed: %v", err)) + } + // forward local port to device + if err = wd.Device.Forward(localPort, wd.Device.Options.WDAPort); err != nil { + return 0, errors.Wrap(code.DeviceHTTPDriverError, + fmt.Sprintf("forward tcp port failed: %v", err)) + } + } else { + log.Info().Int("WDA_LOCAL_PORT", localPort).Msg("reuse WDA local port") + } + return localPort, nil +} + +func (wd *WDADriver) getMjpegLocalPort() (int, error) { + localMjpegPort, err := strconv.Atoi(os.Getenv("WDA_LOCAL_MJPEG_PORT")) + if err != nil { + localMjpegPort, err = builtin.GetFreePort() + if err != nil { + return 0, errors.Wrap(code.DeviceHTTPDriverError, + fmt.Sprintf("get free port failed: %v", err)) + } + if err = wd.Device.Forward(localMjpegPort, wd.Device.Options.WDAMjpegPort); err != nil { + return 0, errors.Wrap(code.DeviceHTTPDriverError, + fmt.Sprintf("forward tcp port failed: %v", err)) + } + } else { + log.Info().Int("WDA_LOCAL_MJPEG_PORT", localMjpegPort). + Msg("reuse WDA local mjpeg port") + } + return localMjpegPort, nil +} + +func (wd *WDADriver) initMjpegClient() error { + host := "localhost" + localMjpegPort, err := wd.getMjpegLocalPort() + if err != nil { + return err + } + if wd.mjpegHTTPConn, err = net.Dial( + "tcp", + fmt.Sprintf("%s:%d", host, localMjpegPort), + ); err != nil { + return errors.Wrap(code.DeviceHTTPDriverError, err.Error()) + } + wd.mjpegClient = NewHTTPClientWithConnection(wd.mjpegHTTPConn, 30*time.Second) + wd.mjpegUrl = fmt.Sprintf("http://%s:%d", host, localMjpegPort) + return nil +} + func (wd *WDADriver) GetMjpegClient() *http.Client { return wd.mjpegClient } @@ -145,6 +162,8 @@ func (wd *WDADriver) DeleteSession() (err error) { // [[FBRoute DELETE:@""] respondWithTarget:self action:@selector(handleDeleteSession:)] _, err = wd.Session.DELETE("/session", wd.Session.ID) + wd.Session.ID = "" + wd.Session.client.CloseIdleConnections() return } @@ -250,15 +269,20 @@ func (wd *WDADriver) Scale() (float64, error) { return screen.Scale, nil } -func (wd *WDADriver) Screen() (screen ai.Screen, err error) { +type Screen struct { + StatusBarSize types.Size `json:"statusBarSize"` + Scale float64 `json:"scale"` +} + +func (wd *WDADriver) Screen() (screen Screen, err error) { // [[FBRoute GET:@"/wda/screen"] respondWithTarget:self action:@selector(handleGetScreen:)] var rawResp DriverRawResponse if rawResp, err = wd.Session.GET("/session", wd.Session.ID, "/wda/screen"); err != nil { - return ai.Screen{}, err + return Screen{}, err } - reply := new(struct{ Value struct{ ai.Screen } }) + reply := new(struct{ Value struct{ Screen } }) if err = json.Unmarshal(rawResp, reply); err != nil { - return ai.Screen{}, err + return Screen{}, err } screen = reply.Value.Screen return @@ -525,6 +549,8 @@ func (wd *WDADriver) TapAbsXY(x, y float64, opts ...option.ActionOption) error { "x": wd.toScale(x), "y": wd.toScale(y), } + option.MergeOptions(data, opts...) + _, err := wd.Session.POST(data, "/session", wd.Session.ID, "/wda/tap/0") return err } @@ -546,10 +572,11 @@ func (wd *WDADriver) DoubleTapXY(x, y float64, opts ...option.ActionOption) erro return err } +// FIXME: hold not work func (wd *WDADriver) TouchAndHold(x, y float64, opts ...option.ActionOption) (err error) { actionOptions := option.NewActionOptions(opts...) if actionOptions.Duration == 0 { - opts = append(opts, option.WithDuration(1)) + opts = append(opts, option.WithPressDuration(1)) } return wd.TapXY(x, y, opts...) } @@ -574,12 +601,7 @@ func (wd *WDADriver) Drag(fromX, fromY, toX, toY float64, opts ...option.ActionO "toX": math.Round(toX*10) / 10, "toY": math.Round(toY*10) / 10, } - if actionOptions.PressDuration > 0 { - data["pressDuration"] = actionOptions.PressDuration - } - - // update data options in post data for extra WDA configurations - actionOptions.UpdateData(data) + option.MergeOptions(data, opts...) // wda 43 version _, err = wd.Session.POST(data, "/session", wd.Session.ID, "/wda/dragfromtoforduration") // _, err = wd.Session.POST(data, "/session", wd.Session.ID, "/wda/drag") @@ -619,12 +641,8 @@ func (wd *WDADriver) SetIme(ime string) error { func (wd *WDADriver) Input(text string, opts ...option.ActionOption) (err error) { // [[FBRoute POST:@"/wda/keys"] respondWithTarget:self action:@selector(handleKeys:)] - actionOptions := option.NewActionOptions(opts...) data := map[string]interface{}{"value": strings.Split(text, "")} - - // new data options in post data for extra WDA configurations - actionOptions.UpdateData(data) - + option.MergeOptions(data, opts...) _, err = wd.Session.POST(data, "/session", wd.Session.ID, "/wda/keys") return } @@ -633,12 +651,8 @@ func (wd *WDADriver) Backspace(count int, opts ...option.ActionOption) (err erro if count == 0 { return nil } - actionOptions := option.NewActionOptions(opts...) data := map[string]interface{}{"count": count} - - // new data options in post data for extra WDA configurations - actionOptions.UpdateData(data) - + option.MergeOptions(data, opts...) _, err = wd.Session.POST(data, "/gtf/interaction/input/backspace") return } @@ -720,39 +734,29 @@ func (wd *WDADriver) SetRotation(rotation types.Rotation) (err error) { func (wd *WDADriver) Source(srcOpt ...option.SourceOption) (source string, err error) { // [[FBRoute GET:@"/source"] respondWithTarget:self action:@selector(handleGetSourceCommand:)] // [[FBRoute GET:@"/source"].withoutSession - urlStr, err := wd.Session.concatURL("/session", wd.Session.ID) - if err != nil { - return "", err - } - tmp, _ := url.Parse(urlStr) - toJsonRaw := false - if len(srcOpt) != 0 { - q := tmp.Query() - for k, val := range srcOpt[0] { - v := val.(string) - q.Set(k, v) - if k == "format" && v == "json" { - toJsonRaw = true - } - } - tmp.RawQuery = q.Encode() - } - - urlStr, err = wd.Session.concatURL(tmp.Path, "/source") - if err != nil { - return "", err + // urlStr, err := wd.Session.concatURL("/session", wd.Session.ID) + // if err != nil { + // return "", err + // } + options := option.NewSourceOptions(srcOpt...) + query := options.Query() + if len(query) > 0 { + query = "?" + query } var rawResp DriverRawResponse - if rawResp, err = wd.Session.GET(http.MethodGet, urlStr); err != nil { - return "", nil + if rawResp, err = wd.Session.GET("/source" + query); err != nil { + return "", err } - if toJsonRaw { + // json format + if options.Format == option.SourceFormatJSON { var jr builtinJSON.RawMessage if jr, err = rawResp.ValueConvertToJsonRawMessage(); err != nil { return "", err } return string(jr), nil } + + // xml/description format if source, err = rawResp.ValueConvertToString(); err != nil { return "", err } @@ -850,7 +854,7 @@ func (wd *WDADriver) ScreenRecord(duration time.Duration) (videoPath string, err "-f", "mjpeg", "-y", "-r", "10", - "-i", "http://"+wd.mjpegUrl, + "-i", wd.mjpegUrl, "-c:v", "libx264", "-vf", "pad=width=ceil(iw/2)*2:height=ceil(ih/2)*2", fileName, @@ -936,7 +940,5 @@ func (wd *WDADriver) Setup() error { } func (wd *WDADriver) TearDown() error { - wd.mjpegClient.CloseIdleConnections() - wd.Session.client.CloseIdleConnections() - return nil + return wd.DeleteSession() } diff --git a/pkg/uixt/ios_test.go b/pkg/uixt/ios_test.go index 794b0e95..8de9e6bf 100644 --- a/pkg/uixt/ios_test.go +++ b/pkg/uixt/ios_test.go @@ -14,13 +14,7 @@ import ( "github.com/httprunner/httprunner/v5/pkg/uixt/types" ) -var ( - bundleId = "com.apple.Preferences" - driver IDriver - iOSDriverExt *XTDriver -) - -func setup(t *testing.T) { +func setupWDADriverExt(t *testing.T) *XTDriver { device, err := NewIOSDevice( option.WithWDAPort(8700), option.WithWDAMjpegPort(8800), @@ -28,21 +22,16 @@ func setup(t *testing.T) { if err != nil { t.Fatal(err) } - driver, err = device.NewDriver() + driver, err := device.NewDriver() if err != nil { t.Fatal(err) } - iOSDriverExt = NewXTDriver(driver, ai.WithCVService(ai.CVServiceTypeVEDEM)) -} - -func TestViaUSB(t *testing.T) { - setup(t) - t.Log(driver.Status()) + return NewXTDriver(driver, ai.WithCVService(ai.CVServiceTypeVEDEM)) } func TestInstall(t *testing.T) { - setup(t) - err := iOSDriverExt.GetDevice().Install("xxx.ipa", + driver := setupWDADriverExt(t) + err := driver.GetDevice().Install("xxx.ipa", option.WithRetryTimes(5)) log.Error().Err(err) if err != nil { @@ -84,28 +73,22 @@ func TestIOSDevice_GetPackageInfo(t *testing.T) { checkErr(t, err) appInfo, err := device.GetPackageInfo("com.ss.iphone.ugc.Aweme") checkErr(t, err) - t.Log(appInfo) -} - -func TestNewUSBDriver(t *testing.T) { - setup(t) - - // t.Log(driver.IsWdaHealthy()) + t.Logf("%+v", appInfo) } func TestDriver_DeviceScaleRatio(t *testing.T) { - setup(t) + driver := setupWDADriverExt(t) - scaleRatio, err := driver.(*WDADriver).Scale() + scaleRatio, err := driver.IDriver.(*WDADriver).Scale() if err != nil { t.Fatal(err) } - t.Log(scaleRatio) + t.Logf("%+v", scaleRatio) } func Test_remoteWD_DeleteSession(t *testing.T) { - setup(t) + driver := setupWDADriverExt(t) err := driver.DeleteSession() if err != nil { @@ -114,26 +97,26 @@ func Test_remoteWD_DeleteSession(t *testing.T) { } func Test_remoteWD_HealthCheck(t *testing.T) { - setup(t) + driver := setupWDADriverExt(t) - err := driver.(*WDADriver).HealthCheck() + err := driver.IDriver.(*WDADriver).HealthCheck() if err != nil { t.Fatal(err) } } func Test_remoteWD_GetAppiumSettings(t *testing.T) { - setup(t) + driver := setupWDADriverExt(t) - settings, err := driver.(*WDADriver).GetAppiumSettings() + settings, err := driver.IDriver.(*WDADriver).GetAppiumSettings() if err != nil { t.Fatal(err) } - t.Log(settings) + t.Logf("%+v", settings) } func Test_remoteWD_SetAppiumSettings(t *testing.T) { - setup(t) + driver := setupWDADriverExt(t) const _acceptAlertButtonSelector = "**/XCUIElementTypeButton[`label IN {'允许','好','仅在使用应用期间','暂不'}`]" const _dismissAlertButtonSelector = "**/XCUIElementTypeButton[`label IN {'不允许','暂不'}`]" @@ -142,7 +125,7 @@ func Test_remoteWD_SetAppiumSettings(t *testing.T) { value := _acceptAlertButtonSelector // settings, err := driver.SetAppiumSettings(map[string]interface{}{"dismissAlertButtonSelector": "暂不"}) - settings, err := driver.(*WDADriver).SetAppiumSettings(map[string]interface{}{key: value}) + settings, err := driver.IDriver.(*WDADriver).SetAppiumSettings(map[string]interface{}{key: value}) if err != nil { t.Fatal(err) } @@ -152,39 +135,31 @@ func Test_remoteWD_SetAppiumSettings(t *testing.T) { } func Test_remoteWD_IsWdaHealthy(t *testing.T) { - setup(t) + driver := setupWDADriverExt(t) - healthy, err := driver.(*WDADriver).IsHealthy() + healthy, err := driver.IDriver.(*WDADriver).IsHealthy() if err != nil { t.Fatal(err) } - if healthy == false { - t.Fatal("healthy =", healthy) + if !healthy { + t.Fatal("assert healthy failed") } } -// func Test_remoteWD_WdaShutdown(t *testing.T) { -// setup(t) -// -// if err := driver.WdaShutdown(); err != nil { -// t.Fatal(err) -// } -// } - func Test_remoteWD_Status(t *testing.T) { - setup(t) + driver := setupWDADriverExt(t) status, err := driver.Status() if err != nil { t.Fatal(err) } - if status.Ready == false { - t.Fatal("deviceStatus =", status) + if !status.Ready { + t.Fatal("assert device status failed") } } func Test_remoteWD_DeviceInfo(t *testing.T) { - setup(t) + driver := setupWDADriverExt(t) info, err := driver.DeviceInfo() if err != nil { @@ -196,7 +171,7 @@ func Test_remoteWD_DeviceInfo(t *testing.T) { } func Test_remoteWD_BatteryInfo(t *testing.T) { - setup(t) + driver := setupWDADriverExt(t) batteryInfo, err := driver.BatteryInfo() if err != nil { @@ -206,7 +181,7 @@ func Test_remoteWD_BatteryInfo(t *testing.T) { } func Test_remoteWD_WindowSize(t *testing.T) { - setup(t) + driver := setupWDADriverExt(t) size, err := driver.WindowSize() if err != nil { @@ -216,9 +191,9 @@ func Test_remoteWD_WindowSize(t *testing.T) { } func Test_remoteWD_Screen(t *testing.T) { - setup(t) + driver := setupWDADriverExt(t) - screen, err := driver.(*WDADriver).Screen() + screen, err := driver.IDriver.(*WDADriver).Screen() if err != nil { t.Fatal(err) } @@ -226,7 +201,7 @@ func Test_remoteWD_Screen(t *testing.T) { } func Test_remoteWD_Homescreen(t *testing.T) { - setup(t) + driver := setupWDADriverExt(t) err := driver.Home() if err != nil { @@ -234,174 +209,154 @@ func Test_remoteWD_Homescreen(t *testing.T) { } } -func Test_remoteWD_AppLaunch(t *testing.T) { - setup(t) +func Test_remoteWD_AppLaunchTerminate(t *testing.T) { + driver := setupWDADriverExt(t) + bundleId := "com.apple.Preferences" err := driver.AppLaunch(bundleId) - // err := driver.AppLaunch(bundleId, NewAppLaunchOption().WithShouldWaitForQuiescence(true)) - // err := driver.AppLaunch(bundleId, NewAppLaunchOption().WithArguments([]string{"-AppleLanguages", "(Russian)"})) if err != nil { t.Fatal(err) } -} + time.Sleep(2 * time.Second) -func Test_remoteWD_AppTerminate(t *testing.T) { - setup(t) - - _, err := driver.AppTerminate(bundleId) + _, err = driver.AppTerminate(bundleId) if err != nil { t.Fatal(err) } } func Test_remoteWD_Tap(t *testing.T) { - setup(t) + driver := setupWDADriverExt(t) - err := driver.TapXY(200, 300) + err := driver.TapXY(0.2, 0.2) if err != nil { t.Fatal(err) } } func Test_remoteWD_DoubleTap(t *testing.T) { - setup(t) + driver := setupWDADriverExt(t) - err := driver.DoubleTapXY(200, 300) + err := driver.DoubleTapXY(0.2, 0.2) if err != nil { t.Fatal(err) } } func Test_remoteWD_TouchAndHold(t *testing.T) { - setup(t) + driver := setupWDADriverExt(t) - // err := driver.TouchAndHold(200, 300) - err := driver.TouchAndHold(200, 300) + err := driver.TouchAndHold(0.2, 0.2) if err != nil { t.Fatal(err) } } func Test_remoteWD_Drag(t *testing.T) { - setup(t) + driver := setupWDADriverExt(t) - // err := driver.Drag(200, 300, 200, 500, WithDataPressDuration(0.5)) - err := driver.Drag(200, 300, 200, 500, - option.WithPressDuration(2), option.WithDuration(3)) + err := driver.Drag(0.8, 0.5, 0.2, 0.5, + option.WithDuration(0.5)) if err != nil { t.Fatal(err) } } -func Test_Relative_Drag(t *testing.T) { - setup(t) +func Test_Relative_Swipe(t *testing.T) { + driver := setupWDADriverExt(t) - // err := driver.Drag(200, 300, 200, 500, WithDataPressDuration(0.5)) - err := iOSDriverExt.Swipe(0.5, 0.7, 0.5, 0.5) + err := driver.Swipe(0.8, 0.5, 0.2, 0.5) if err != nil { t.Fatal(err) } } func Test_remoteWD_SendKeys(t *testing.T) { - setup(t) - // driver.StartCaptureLog("hrp_wda_log") - err := driver.Input("test", option.WithIdentifier("test")) - // result, _ := driver.StopCaptureLog() - // err := driver.SendKeys("App Store", WithFrequency(3)) + driver := setupWDADriverExt(t) + driver.StartCaptureLog("hrp_wda_log") + err := driver.Input("test中文", option.WithIdentifier("test")) + result, _ := driver.StopCaptureLog() if err != nil { t.Fatal(err) } - // t.Log(result) + t.Log(result) } func Test_remoteWD_PressButton(t *testing.T) { - setup(t) + driver := setupWDADriverExt(t) - err := driver.(*WDADriver).PressButton(types.DeviceButtonVolumeUp) + err := driver.IDriver.(*WDADriver).PressButton(types.DeviceButtonVolumeUp) if err != nil { t.Fatal(err) } time.Sleep(time.Second * 1) - err = driver.(*WDADriver).PressButton(types.DeviceButtonVolumeDown) + err = driver.IDriver.(*WDADriver).PressButton(types.DeviceButtonVolumeDown) if err != nil { t.Fatal(err) } time.Sleep(time.Second * 1) - err = driver.(*WDADriver).PressButton(types.DeviceButtonHome) + err = driver.IDriver.(*WDADriver).PressButton(types.DeviceButtonHome) if err != nil { t.Fatal(err) } } func Test_remoteWD_Screenshot(t *testing.T) { - setup(t) + driver := setupWDADriverExt(t) + // without save file screenshot, err := driver.ScreenShot() if err != nil { t.Fatal(err) } _ = screenshot - // img, format, err := image.Decode(screenshot) - // if err != nil { - // t.Fatal(err) - // } - // userHomeDir, _ := os.UserHomeDir() - // file, err := os.Create(userHomeDir + "/Desktop/s1." + format) - // if err != nil { - // t.Fatal(err) - // } - // defer func() { _ = file.Close() }() - // switch format { - // case "png": - // err = png.Encode(file, img) - // case "jpeg": - // err = jpeg.Encode(file, img, nil) - // } - // if err != nil { - // t.Fatal(err) - // } - // t.Log(file.Name()) + // save file + screenshot, err = driver.ScreenShot(option.WithScreenShotFileName("123")) + if err != nil { + t.Fatal(err) + } + _ = screenshot + + path, err := saveScreenShot(screenshot, "1234") + if err != nil { + t.Fatal(err) + } + t.Logf("save screenshot to %s", path) } func Test_remoteWD_Source(t *testing.T) { - setup(t) + driver := setupWDADriverExt(t) var source string var err error - // source, err = driver.Source() - // if err != nil { - // t.Fatal(err) - // } - source, err = driver.Source() if err != nil { t.Fatal(err) } - // source, err = driver.Source(NewSourceOption().WithFormatAsJson()) - // if err != nil { - // t.Fatal(err) - // } + source, err = driver.Source(option.WithFormat(option.SourceFormatJSON)) + if err != nil { + t.Fatal(err) + } - // source, err = driver.Source(NewSourceOption().WithFormatAsDescription()) - // if err != nil { - // t.Fatal(err) - // } + source, err = driver.Source(option.WithFormat(option.SourceFormatDescription)) + if err != nil { + t.Fatal(err) + } - // source, err = driver.Source(NewSourceOption().WithFormatAsXml().WithExcludedAttributes([]string{"label", "type", "index"})) - // if err != nil { - // t.Fatal(err) - // } - - _ = source - fmt.Println(source) + source, err = driver.Source( + option.WithFormat(option.SourceFormatXML), + option.WithExcludedAttributes([]string{"label", "type", "index"})) + if err != nil { + t.Fatal(err) + } + t.Logf("source: %s", source) } func TestGetForegroundApp(t *testing.T) { - setup(t) + driver := setupWDADriverExt(t) app, err := driver.ForegroundInfo() if err != nil { t.Fatal(err) @@ -410,18 +365,17 @@ func TestGetForegroundApp(t *testing.T) { } func Test_remoteWD_AccessibleSource(t *testing.T) { - setup(t) + driver := setupWDADriverExt(t) - source, err := driver.(*WDADriver).AccessibleSource() + source, err := driver.IDriver.(*WDADriver).AccessibleSource() if err != nil { t.Fatal(err) } - _ = source fmt.Println(source) } func TestRecord(t *testing.T) { - setup(t) + driver := setupWDADriverExt(t) path, err := driver.ScreenRecord(5 * time.Second) if err != nil { t.Fatal(err) @@ -429,11 +383,11 @@ func TestRecord(t *testing.T) { println(path) } -// func Test_Backspace(t *testing.T) { -// setup(t) +func Test_Backspace(t *testing.T) { + driver := setupWDADriverExt(t) -// err := driver.Backspace(3) -// if err != nil { -// t.Fatal(err) -// } -// } + err := driver.Backspace(3) + if err != nil { + t.Fatal(err) + } +} diff --git a/pkg/uixt/option/action.go b/pkg/uixt/option/action.go index 735a13ca..863f71e7 100644 --- a/pkg/uixt/option/action.go +++ b/pkg/uixt/option/action.go @@ -13,9 +13,9 @@ type ActionOptions struct { // control related MaxRetryTimes int `json:"max_retry_times,omitempty" yaml:"max_retry_times,omitempty"` // max retry times Interval float64 `json:"interval,omitempty" yaml:"interval,omitempty"` // interval between retries in seconds - Duration float64 `json:"duration,omitempty" yaml:"duration,omitempty"` // used to set duration of ios swipe action - PressDuration float64 `json:"press_duration,omitempty" yaml:"press_duration,omitempty"` // used to set duration of ios swipe action - Steps int `json:"steps,omitempty" yaml:"steps,omitempty"` // used to set steps of android swipe action + Duration float64 `json:"duration,omitempty" yaml:"duration,omitempty"` // used to set duration in seconds + PressDuration float64 `json:"press_duration,omitempty" yaml:"press_duration,omitempty"` // used to set press duration in seconds + Steps int `json:"steps,omitempty" yaml:"steps,omitempty"` // used to set steps of action Direction interface{} `json:"direction,omitempty" yaml:"direction,omitempty"` // used by swipe to tap text or app Timeout int `json:"timeout,omitempty" yaml:"timeout,omitempty"` // TODO: wait timeout in seconds for mobile action Frequency int `json:"frequency,omitempty" yaml:"frequency,omitempty"` @@ -142,7 +142,8 @@ func (o *ActionOptions) GetRandomOffset() float64 { return float64(builtin.GetRandomNumber(minOffset, maxOffset)) + rand.Float64() } -func (o *ActionOptions) UpdateData(data map[string]interface{}) { +func MergeOptions(data map[string]interface{}, opts ...ActionOption) { + o := NewActionOptions(opts...) if o.Identifier != "" { data["log"] = map[string]interface{}{ "enable": true, @@ -164,6 +165,10 @@ func (o *ActionOptions) UpdateData(data map[string]interface{}) { data["duration"] = 0 // default duration } + if o.PressDuration > 0 { + data["pressDuration"] = o.PressDuration + } + if o.Frequency > 0 { data["frequency"] = o.Frequency } diff --git a/pkg/uixt/option/source.go b/pkg/uixt/option/source.go index 0cc79515..5f6a52d4 100644 --- a/pkg/uixt/option/source.go +++ b/pkg/uixt/option/source.go @@ -2,53 +2,74 @@ package option import "strings" -// SourceOption Configure the format or attribute of the Source -type SourceOption map[string]interface{} - -func NewSourceOption() SourceOption { - return make(SourceOption) -} - -// WithFormatAsJson Application elements tree in form of json string -func (opt SourceOption) WithFormatAsJson() SourceOption { - opt["format"] = "json" - return opt -} - -func (opt SourceOption) WithProcessName(processName string) SourceOption { - opt["processName"] = processName - return opt -} - -// WithFormatAsXml Application elements tree in form of xml string -func (opt SourceOption) WithFormatAsXml() SourceOption { - opt["format"] = "xml" - return opt -} - -// WithFormatAsDescription Application elements tree in form of internal XCTest debugDescription string -func (opt SourceOption) WithFormatAsDescription() SourceOption { - opt["format"] = "description" - return opt -} - -// WithScope Allows to provide XML scope. -// -// only `xml` is supported. -func (opt SourceOption) WithScope(scope string) SourceOption { - if vFormat, ok := opt["format"]; ok && vFormat != "xml" { - return opt +func NewSourceOptions(opts ...SourceOption) *SourceOptions { + options := &SourceOptions{} + for _, option := range opts { + option(options) + } + return options +} + +type SourceOptions struct { + Format SourceFormat `json:"format,omitempty"` + ProcessName string `json:"processName,omitempty"` + Scope string `json:"scope,omitempty"` + ExcludedAttributes string `json:"excluded_attributes,omitempty"` +} + +func (o *SourceOptions) Query() string { + query := []string{} + if o.Format != "" { + query = append(query, "format="+string(o.Format)) + } + if o.ProcessName != "" { + query = append(query, "processName="+o.ProcessName) + } + if o.Scope != "" { + query = append(query, "scope="+o.Scope) + } + if o.ExcludedAttributes != "" { + query = append(query, "excluded_attributes="+o.ExcludedAttributes) + } + return strings.Join(query, "&") +} + +type SourceOption func(o *SourceOptions) + +type SourceFormat string + +const ( + SourceFormatJSON SourceFormat = "json" + SourceFormatXML SourceFormat = "xml" + SourceFormatDescription SourceFormat = "description" +) + +// WithFormat specify Application elements tree format +// `json` or `xml` or `description` +func WithFormat(format SourceFormat) SourceOption { + return func(o *SourceOptions) { + o.Format = format + } +} + +func WithProcessName(name string) SourceOption { + return func(o *SourceOptions) { + o.ProcessName = name + } +} + +// WithSourceScope Allows to provide XML scope. +// only `xml` is supported. +func WithSourceScope(scope string) SourceOption { + return func(o *SourceOptions) { + o.Scope = scope } - opt["scope"] = scope - return opt } // WithExcludedAttributes Excludes the given attribute names. // only `xml` is supported. -func (opt SourceOption) WithExcludedAttributes(attributes []string) SourceOption { - if vFormat, ok := opt["format"]; ok && vFormat != "xml" { - return opt +func WithExcludedAttributes(attributes []string) SourceOption { + return func(o *SourceOptions) { + o.ExcludedAttributes = strings.Join(attributes, ",") } - opt["excluded_attributes"] = strings.Join(attributes, ",") - return opt } diff --git a/server/ext/shoots.go b/server/ext/shoots.go index 2a78b4a4..5705d208 100644 --- a/server/ext/shoots.go +++ b/server/ext/shoots.go @@ -50,7 +50,7 @@ func sourceHandler(c *gin.Context) { c.Abort() return } - source, err := dExt.Source(option.NewSourceOption().WithProcessName(app.PackageName)) + source, err := dExt.Source(option.WithProcessName(app.PackageName)) if err != nil { log.Err(err).Msg(fmt.Sprintf("[%s]: failed to get source %s", c.HandlerName(), app.PackageName)) c.JSON(http.StatusInternalServerError,