diff --git a/internal/version/VERSION b/internal/version/VERSION index 467f9619..2e105fa2 100644 --- a/internal/version/VERSION +++ b/internal/version/VERSION @@ -1 +1 @@ -v5.0.0+2502071814 +v5.0.0+2502081446 diff --git a/pkg/ai/ai.go b/pkg/ai/ai.go new file mode 100644 index 00000000..3b7ae92f --- /dev/null +++ b/pkg/ai/ai.go @@ -0,0 +1,63 @@ +package ai + +import ( + "os" + + "github.com/httprunner/httprunner/v5/code" + "github.com/rs/zerolog/log" +) + +func NewAIService(opts ...AIServiceOption) *AIServices { + services := &AIServices{} + for _, option := range opts { + option(services) + } + return services +} + +type AIServices struct { + ICVService + ILLMService +} + +type AIServiceOption func(*AIServices) + +type CVServiceType string + +const ( + CVServiceTypeVEDEM CVServiceType = "vedem" + CVServiceTypeOpenCV CVServiceType = "opencv" +) + +func WithCVService(service CVServiceType) AIServiceOption { + return func(opts *AIServices) { + if service == CVServiceTypeVEDEM { + var err error + opts.ICVService, err = NewVEDEMImageService() + if err != nil { + log.Error().Err(err).Msg("init vedem image service failed") + os.Exit(code.GetErrorCode(err)) + } + } + } +} + +type LLMServiceType string + +const ( + LLMServiceTypeGPT4o LLMServiceType = "gpt-4o" + LLMServiceTypeDeepSeekV3 LLMServiceType = "deepseek-v3" +) + +func WithLLMService(service LLMServiceType) AIServiceOption { + return func(opts *AIServices) { + if service == LLMServiceTypeGPT4o { + var err error + opts.ILLMService, err = NewGPT4oLLMService() + if err != nil { + log.Error().Err(err).Msg("init gpt-4o llm service failed") + os.Exit(code.GetErrorCode(err)) + } + } + } +} diff --git a/pkg/ai/ai_test.go b/pkg/ai/ai_test.go new file mode 100644 index 00000000..bae046ad --- /dev/null +++ b/pkg/ai/ai_test.go @@ -0,0 +1,11 @@ +package ai + +import "testing" + +func TestOption(t *testing.T) { + options := NewAIService( + WithCVService(CVServiceTypeOpenCV), + WithLLMService(LLMServiceTypeDeepSeekV3), + ) + t.Log(options) +} diff --git a/pkg/uixt/ai.go b/pkg/ai/cv.go similarity index 78% rename from pkg/uixt/ai.go rename to pkg/ai/cv.go index a40979b0..63de25e6 100644 --- a/pkg/uixt/ai.go +++ b/pkg/ai/cv.go @@ -1,4 +1,4 @@ -package uixt +package ai import ( "bytes" @@ -7,19 +7,18 @@ import ( "math" "regexp" - "github.com/pkg/errors" - "github.com/httprunner/httprunner/v5/code" "github.com/httprunner/httprunner/v5/internal/builtin" - "github.com/httprunner/httprunner/v5/pkg/uixt/option" + "github.com/pkg/errors" ) -type IImageService interface { - // GetImage returns image result including ocr texts, uploaded image url, etc - GetImage(imageBuf *bytes.Buffer, opts ...option.ActionOption) (imageResult *ImageResult, err error) +type ICVService interface { + // returns CV result including ocr texts, uploaded image url, etc + ReadFromBuffer(imageBuf *bytes.Buffer, opts ...ScreenShotOption) (*CVResult, error) + ReadFromPath(imagePath string, opts ...ScreenShotOption) (*CVResult, error) } -type ImageResult struct { +type CVResult struct { URL string `json:"url,omitempty"` // image uploaded url OCRResult OCRResults `json:"ocrResult,omitempty"` // OCR texts // NoLive(非直播间) @@ -106,7 +105,7 @@ func (t OCRTexts) texts() (texts []string) { return texts } -func (t OCRTexts) FilterScope(scope option.AbsScope) (results OCRTexts) { +func (t OCRTexts) FilterScope(scope AbsScope) (results OCRTexts) { for _, ocrText := range t { rect := ocrText.Rect @@ -128,12 +127,12 @@ func (t OCRTexts) FilterScope(scope option.AbsScope) (results OCRTexts) { // FindText returns matched text with options // Notice: filter scope should be specified with WithAbsScope -func (t OCRTexts) FindText(text string, opts ...option.ActionOption) (result OCRText, err error) { - actionOptions := option.NewActionOptions(opts...) +func (t OCRTexts) FindText(text string, opts ...ScreenFilterOption) (result OCRText, err error) { + options := NewScreenFilterOptions(opts...) var results []OCRText - for _, ocrText := range t.FilterScope(actionOptions.AbsScope) { - if actionOptions.Regex { + for _, ocrText := range t.FilterScope(options.AbsScope) { + if options.Regex { // regex on, check if match regex if !regexp.MustCompile(text).MatchString(ocrText.Text) { continue @@ -148,7 +147,7 @@ func (t OCRTexts) FindText(text string, opts ...option.ActionOption) (result OCR results = append(results, ocrText) // return the first one matched exactly when index not specified - if ocrText.Text == text && actionOptions.Index == 0 { + if ocrText.Text == text && options.Index == 0 { return ocrText, nil } } @@ -159,7 +158,7 @@ func (t OCRTexts) FindText(text string, opts ...option.ActionOption) (result OCR } // get index - idx := actionOptions.Index + idx := options.Index if idx < 0 { idx = len(results) + idx } @@ -173,8 +172,8 @@ func (t OCRTexts) FindText(text string, opts ...option.ActionOption) (result OCR return results[idx], nil } -func (t OCRTexts) FindTexts(texts []string, opts ...option.ActionOption) (results OCRTexts, err error) { - actionOptions := option.NewActionOptions(opts...) +func (t OCRTexts) FindTexts(texts []string, opts ...ScreenFilterOption) (results OCRTexts, err error) { + options := NewScreenFilterOptions(opts...) for _, text := range texts { ocrText, err := t.FindText(text, opts...) if err != nil { @@ -183,7 +182,7 @@ func (t OCRTexts) FindTexts(texts []string, opts ...option.ActionOption) (result results = append(results, ocrText) // found one, skip searching and return - if actionOptions.MatchOne { + if options.MatchOne { return results, nil } } @@ -240,7 +239,7 @@ func (box Box) Center() PointF { type UIResults []UIResult -func (u UIResults) FilterScope(scope option.AbsScope) (results UIResults) { +func (u UIResults) FilterScope(scope AbsScope) (results UIResults) { for _, uiResult := range u { rect := image.Rectangle{ Min: image.Point{ @@ -268,16 +267,15 @@ func (u UIResults) FilterScope(scope option.AbsScope) (results UIResults) { return } -func (u UIResults) GetUIResult(opts ...option.ActionOption) (UIResult, error) { - actionOptions := option.NewActionOptions(opts...) - - uiResults := u.FilterScope(actionOptions.AbsScope) +func (u UIResults) GetUIResult(opts ...ScreenFilterOption) (UIResult, error) { + options := NewScreenFilterOptions(opts...) + uiResults := u.FilterScope(options.AbsScope) if len(uiResults) == 0 { return UIResult{}, errors.Wrap(code.CVResultNotFoundError, "ui types not found in scope") } // get index - idx := actionOptions.Index + idx := options.Index if idx < 0 { idx = len(uiResults) + idx } @@ -301,3 +299,32 @@ type ClosePopupsResult struct { func (c ClosePopupsResult) IsEmpty() bool { return c.PopupArea.IsEmpty() && c.CloseArea.IsEmpty() } + +type Point struct { + X int `json:"x"` // upper left X coordinate of selected element + Y int `json:"y"` // upper left Y coordinate of selected element +} + +type PointF struct { + X float64 `json:"x"` + Y float64 `json:"y"` +} + +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 Size struct { + Width int `json:"width"` + Height int `json:"height"` +} + +func (s Size) IsNil() bool { + return s.Width == 0 && s.Height == 0 +} + +type Screen struct { + StatusBarSize Size `json:"statusBarSize"` + Scale float64 `json:"scale"` +} diff --git a/pkg/uixt/ai_vedem.go b/pkg/ai/cv_vedem.go similarity index 86% rename from pkg/uixt/ai_vedem.go rename to pkg/ai/cv_vedem.go index 4a112e5c..9fb073cd 100644 --- a/pkg/uixt/ai_vedem.go +++ b/pkg/ai/cv_vedem.go @@ -1,4 +1,4 @@ -package uixt +package ai import ( "bytes" @@ -16,7 +16,6 @@ import ( "github.com/httprunner/httprunner/v5/code" "github.com/httprunner/httprunner/v5/internal/builtin" "github.com/httprunner/httprunner/v5/internal/json" - "github.com/httprunner/httprunner/v5/pkg/uixt/option" ) var client = &http.Client{ @@ -24,19 +23,19 @@ var client = &http.Client{ } type APIResponseImage struct { - Code int `json:"code"` - Message string `json:"message"` - Result ImageResult `json:"result"` + Code int `json:"code"` + Message string `json:"message"` + Result CVResult `json:"result"` } -func newVEDEMImageService() (*veDEMImageService, error) { +func NewVEDEMImageService() (*vedemCVService, error) { if err := checkEnv(); err != nil { return nil, err } - return &veDEMImageService{}, nil + return &vedemCVService{}, nil } -// veDEMImageService implements IImageService interface +// vedemCVService implements IImageService interface // actions: // // ocr - get ocr texts @@ -45,11 +44,24 @@ func newVEDEMImageService() (*veDEMImageService, error) { // popup - get popup windows // close - get close popup // ui - get ui position by type(s) -type veDEMImageService struct{} +type vedemCVService struct{} -func (s *veDEMImageService) GetImage(imageBuf *bytes.Buffer, opts ...option.ActionOption) (imageResult *ImageResult, err error) { - actionOptions := option.NewActionOptions(opts...) - screenshotActions := actionOptions.ScreenshotActions() +func (s *vedemCVService) ReadFromPath(imagePath string, opts ...ScreenShotOption) ( + imageResult *CVResult, err error) { + imageBuf, err := os.ReadFile(imagePath) + if err != nil { + err = errors.Wrap(code.CVRequestError, + fmt.Sprintf("read image file error: %v", err)) + return + } + imageResult, err = s.ReadFromBuffer(bytes.NewBuffer(imageBuf), opts...) + return +} + +func (s *vedemCVService) ReadFromBuffer(imageBuf *bytes.Buffer, opts ...ScreenShotOption) ( + imageResult *CVResult, err error) { + actionOptions := NewScreenShotOptions(opts...) + screenshotActions := actionOptions.List() if len(screenshotActions) == 0 { // skip return nil, nil @@ -98,11 +110,7 @@ func (s *veDEMImageService) GetImage(imageBuf *bytes.Buffer, opts ...option.Acti bodyWriter.WriteField("ocrCluster", actionOptions.ScreenShotWithOCRCluster) } - if actionOptions.Timeout > 0 { - bodyWriter.WriteField("timeout", fmt.Sprintf("%v", actionOptions.Timeout)) - } else { - bodyWriter.WriteField("timeout", fmt.Sprintf("%v", 10)) - } + bodyWriter.WriteField("timeout", fmt.Sprintf("%v", 10)) formWriter, err := bodyWriter.CreateFormFile("image", "screenshot.png") if err != nil { diff --git a/pkg/ai/cv_vedem_test.go b/pkg/ai/cv_vedem_test.go new file mode 100644 index 00000000..d8c47cab --- /dev/null +++ b/pkg/ai/cv_vedem_test.go @@ -0,0 +1,41 @@ +//go:build localtest + +package ai + +import ( + "bytes" + "fmt" + "os" + "testing" +) + +func TestGetImageFromBuffer(t *testing.T) { + imagePath := "/Users/debugtalk/Downloads/s1.png" + file, err := os.ReadFile(imagePath) + if err != nil { + t.Fatal(err) + } + buf := new(bytes.Buffer) + buf.Read(file) + + service := NewAIService( + WithCVService(CVServiceTypeVEDEM), + ) + cvResult, err := service.ReadFromBuffer(buf) + if err != nil { + t.Fatal(err) + } + fmt.Println(fmt.Sprintf("cvResult: %v", cvResult)) +} + +func TestGetImageFromPath(t *testing.T) { + imagePath := "/Users/debugtalk/Downloads/s1.png" + service := NewAIService( + WithCVService(CVServiceTypeVEDEM), + ) + cvResult, err := service.ReadFromPath(imagePath) + if err != nil { + t.Fatal(err) + } + fmt.Println(fmt.Sprintf("cvResult: %v", cvResult)) +} diff --git a/pkg/ai/llm.go b/pkg/ai/llm.go new file mode 100644 index 00000000..3b787400 --- /dev/null +++ b/pkg/ai/llm.go @@ -0,0 +1,20 @@ +package ai + +import "context" + +type ILLMService interface { + Call(ctx context.Context, prompt string) (string, error) +} + +func NewGPT4oLLMService() (*openaiLLMService, error) { + if err := checkEnv(); err != nil { + return nil, err + } + return &openaiLLMService{}, nil +} + +type openaiLLMService struct{} + +func (s openaiLLMService) Call(ctx context.Context, prompt string) (string, error) { + return "", nil +} diff --git a/pkg/ai/screen.go b/pkg/ai/screen.go new file mode 100644 index 00000000..483362c3 --- /dev/null +++ b/pkg/ai/screen.go @@ -0,0 +1,213 @@ +package ai + +func NewScreenShotOptions(opts ...ScreenShotOption) *ScreenShotOptions { + options := &ScreenShotOptions{} + for _, option := range opts { + option(options) + } + return options +} + +type ScreenShotOptions struct { + ScreenShotWithOCR bool `json:"screenshot_with_ocr,omitempty" yaml:"screenshot_with_ocr,omitempty"` + ScreenShotWithUpload bool `json:"screenshot_with_upload,omitempty" yaml:"screenshot_with_upload,omitempty"` + ScreenShotWithLiveType bool `json:"screenshot_with_live_type,omitempty" yaml:"screenshot_with_live_type,omitempty"` + ScreenShotWithLivePopularity bool `json:"screenshot_with_live_popularity,omitempty" yaml:"screenshot_with_live_popularity,omitempty"` + ScreenShotWithUITypes []string `json:"screenshot_with_ui_types,omitempty" yaml:"screenshot_with_ui_types,omitempty"` + ScreenShotWithClosePopups bool `json:"screenshot_with_close_popups,omitempty" yaml:"screenshot_with_close_popups,omitempty"` + ScreenShotWithOCRCluster string `json:"screenshot_with_ocr_cluster,omitempty" yaml:"screenshot_with_ocr_cluster,omitempty"` + ScreenShotFileName string `json:"screenshot_file_name,omitempty" yaml:"screenshot_file_name,omitempty"` +} + +func (o *ScreenShotOptions) Options() []ScreenShotOption { + options := make([]ScreenShotOption, 0) + if o == nil { + return options + } + + // screenshot options + if o.ScreenShotWithOCR { + options = append(options, WithScreenShotOCR(true)) + } + if o.ScreenShotWithUpload { + options = append(options, WithScreenShotUpload(true)) + } + if o.ScreenShotWithLiveType { + options = append(options, WithScreenShotLiveType(true)) + } + if o.ScreenShotWithLivePopularity { + options = append(options, WithScreenShotLivePopularity(true)) + } + if len(o.ScreenShotWithUITypes) > 0 { + options = append(options, WithScreenShotUITypes(o.ScreenShotWithUITypes...)) + } + if o.ScreenShotWithClosePopups { + options = append(options, WithScreenShotClosePopups(true)) + } + if o.ScreenShotWithOCRCluster != "" { + options = append(options, WithScreenOCRCluster(o.ScreenShotWithOCRCluster)) + } + if o.ScreenShotFileName != "" { + options = append(options, WithScreenShotFileName(o.ScreenShotFileName)) + } + + return options +} + +func (o *ScreenShotOptions) List() []string { + options := []string{} + if o.ScreenShotWithUpload { + options = append(options, "upload") + } + if o.ScreenShotWithOCR { + options = append(options, "ocr") + } + if o.ScreenShotWithLiveType { + options = append(options, "liveType") + } + if o.ScreenShotWithLivePopularity { + options = append(options, "livePopularity") + } + // UI detection + if len(o.ScreenShotWithUITypes) > 0 { + options = append(options, "ui") + } + if o.ScreenShotWithClosePopups { + options = append(options, "close") + } + return options +} + +type ScreenShotOption func(o *ScreenShotOptions) + +func WithScreenShotOCR(ocrOn bool) ScreenShotOption { + return func(o *ScreenShotOptions) { + o.ScreenShotWithOCR = ocrOn + } +} + +func WithScreenShotUpload(uploadOn bool) ScreenShotOption { + return func(o *ScreenShotOptions) { + o.ScreenShotWithUpload = uploadOn + } +} + +func WithScreenShotLiveType(liveTypeOn bool) ScreenShotOption { + return func(o *ScreenShotOptions) { + o.ScreenShotWithLiveType = liveTypeOn + } +} + +func WithScreenShotLivePopularity(livePopularityOn bool) ScreenShotOption { + return func(o *ScreenShotOptions) { + o.ScreenShotWithLivePopularity = livePopularityOn + } +} + +func WithScreenShotUITypes(uiTypes ...string) ScreenShotOption { + return func(o *ScreenShotOptions) { + o.ScreenShotWithUITypes = uiTypes + } +} + +func WithScreenShotClosePopups(closeOn bool) ScreenShotOption { + return func(o *ScreenShotOptions) { + o.ScreenShotWithClosePopups = closeOn + } +} + +func WithScreenOCRCluster(ocrCluster string) ScreenShotOption { + return func(o *ScreenShotOptions) { + o.ScreenShotWithOCRCluster = ocrCluster + } +} + +func WithScreenShotFileName(fileName string) ScreenShotOption { + return func(o *ScreenShotOptions) { + o.ScreenShotFileName = fileName + } +} + +// (x1, y1) is the top left corner, (x2, y2) is the bottom right corner +// [x1, y1, x2, y2] in percentage of the screen +type Scope []float64 + +func (s Scope) ToAbs(windowSize Size) AbsScope { + x1, y1, x2, y2 := s[0], s[1], s[2], s[3] + // convert relative scope to absolute scope + absX1 := int(x1 * float64(windowSize.Width)) + absY1 := int(y1 * float64(windowSize.Height)) + absX2 := int(x2 * float64(windowSize.Width)) + absY2 := int(y2 * float64(windowSize.Height)) + return AbsScope{absX1, absY1, absX2, absY2} +} + +// [x1, y1, x2, y2] in absolute pixels +type AbsScope []int + +func (s AbsScope) Option() ScreenFilterOption { + return WithAbsScope(s[0], s[1], s[2], s[3]) +} + +func NewScreenFilterOptions(opts ...ScreenFilterOption) *ScreenFilterOptions { + options := &ScreenFilterOptions{} + for _, option := range opts { + option(options) + } + return options +} + +type ScreenFilterOptions struct { + // scope related + Scope Scope `json:"scope,omitempty" yaml:"scope,omitempty"` + AbsScope AbsScope `json:"abs_scope,omitempty" yaml:"abs_scope,omitempty"` + + Regex bool `json:"regex,omitempty" yaml:"regex,omitempty"` // use regex to match text + Offset []int `json:"offset,omitempty" yaml:"offset,omitempty"` // used to tap offset of point + OffsetRandomRange []int `json:"offset_random_range,omitempty" yaml:"offset_random_range,omitempty"` // set random range [min, max] for tap/swipe points + Index int `json:"index,omitempty" yaml:"index,omitempty"` // index of the target element + MatchOne bool `json:"match_one,omitempty" yaml:"match_one,omitempty"` // match one of the targets if existed +} + +type ScreenFilterOption func(o *ScreenFilterOptions) + +// WithScope inputs area of [(x1,y1), (x2,y2)] +// x1, y1, x2, y2 are all in [0, 1], which means the relative position of the screen +func WithScope(x1, y1, x2, y2 float64) ScreenFilterOption { + return func(o *ScreenFilterOptions) { + o.Scope = Scope{x1, y1, x2, y2} + } +} + +// WithAbsScope inputs area of [(x1,y1), (x2,y2)] +// x1, y1, x2, y2 are all absolute position of the screen +func WithAbsScope(x1, y1, x2, y2 int) ScreenFilterOption { + return func(o *ScreenFilterOptions) { + o.AbsScope = AbsScope{x1, y1, x2, y2} + } +} + +// tap [x, y] with offset [offsetX, offsetY] +func WithTapOffset(offsetX, offsetY int) ScreenFilterOption { + return func(o *ScreenFilterOptions) { + o.Offset = []int{offsetX, offsetY} + } +} + +func WithRegex(regex bool) ScreenFilterOption { + return func(o *ScreenFilterOptions) { + o.Regex = regex + } +} + +func WithMatchOne(matchOne bool) ScreenFilterOption { + return func(o *ScreenFilterOptions) { + o.MatchOne = matchOne + } +} + +func WithIndex(index int) ScreenFilterOption { + return func(o *ScreenFilterOptions) { + o.Index = index + } +} diff --git a/pkg/uixt/ai_vedem_test.go b/pkg/uixt/ai_vedem_test.go deleted file mode 100644 index 048af5fe..00000000 --- a/pkg/uixt/ai_vedem_test.go +++ /dev/null @@ -1,80 +0,0 @@ -//go:build localtest - -package uixt - -import ( - "bytes" - "fmt" - "os" - "testing" - - "github.com/httprunner/httprunner/v5/pkg/uixt/option" -) - -func checkOCR(buff *bytes.Buffer) error { - service, err := newVEDEMImageService() - if err != nil { - return err - } - imageResult, err := service.GetImage(buff) - if err != nil { - return err - } - fmt.Println(fmt.Sprintf("imageResult: %v", imageResult)) - return nil -} - -func TestOCRWithScreenshot(t *testing.T) { - setupAndroidAdbDriver(t) - - raw, err := driverExt.Driver.Screenshot() - if err != nil { - t.Fatal(err) - } - - if err := checkOCR(raw); err != nil { - t.Fatal(err) - } -} - -func TestOCRWithLocalFile(t *testing.T) { - imagePath := "/Users/debugtalk/Downloads/s1.png" - - file, err := os.ReadFile(imagePath) - if err != nil { - t.Fatal(err) - } - - buf := new(bytes.Buffer) - buf.Read(file) - - if err := checkOCR(buf); err != nil { - t.Fatal(err) - } -} - -func TestTapUIWithScreenshot(t *testing.T) { - serialNumber := os.Getenv("SERIAL_NUMBER") - device, _ := NewAndroidDevice(option.WithSerialNumber(serialNumber)) - driver, err := device.NewDriver() - if err != nil { - t.Fatal(err) - } - - err = driver.TapByUIDetection( - option.WithScreenShotUITypes("dyhouse", "shoppingbag")) - if err != nil { - t.Fatal(err) - } -} - -func TestDriverExtOCR(t *testing.T) { - driverExt, err := iosDevice.NewDriver() - checkErr(t, err) - - point, err := driverExt.FindScreenText("抖音") - checkErr(t, err) - - t.Logf("point.X: %v, point.Y: %v", point.X, point.Y) - driverExt.Driver.Tap(point.X, point.Y-20) -} diff --git a/pkg/uixt/android_device.go b/pkg/uixt/android_device.go index 87d426ef..4479dc9a 100644 --- a/pkg/uixt/android_device.go +++ b/pkg/uixt/android_device.go @@ -152,7 +152,7 @@ func (dev *AndroidDevice) NewDriver(opts ...option.DriverOption) (driverExt *Dri if dev.UIA2 || dev.LogOn { driver, err = NewUIA2Driver(dev) } else if dev.STUB { - driver, err = NewStubDriver(dev) + driver, err = NewStubAndroidDriver(dev) } else { driver, err = NewADBDriver(dev) } diff --git a/pkg/uixt/android_driver_adb.go b/pkg/uixt/android_driver_adb.go index 44a3562b..b2ab07c4 100644 --- a/pkg/uixt/android_driver_adb.go +++ b/pkg/uixt/android_driver_adb.go @@ -23,6 +23,7 @@ import ( "github.com/httprunner/httprunner/v5/code" "github.com/httprunner/httprunner/v5/internal/config" "github.com/httprunner/httprunner/v5/internal/utf7" + "github.com/httprunner/httprunner/v5/pkg/ai" "github.com/httprunner/httprunner/v5/pkg/uixt/option" ) @@ -39,6 +40,7 @@ func NewADBDriver(device *AndroidDevice) (*ADBDriver, error) { type ADBDriver struct { *AndroidDevice *Session + // DriverExt } func (ad *ADBDriver) runShellCommand(cmd string, args ...string) (output string, err error) { @@ -89,6 +91,10 @@ func (ad *ADBDriver) Status() (deviceStatus DeviceStatus, err error) { return } +func (ad *ADBDriver) GetDevice() IDevice { + return ad.AndroidDevice +} + func (ad *ADBDriver) DeviceInfo() (deviceInfo DeviceInfo, err error) { err = errDriverNotImplemented return @@ -104,7 +110,7 @@ func (ad *ADBDriver) BatteryInfo() (batteryInfo BatteryInfo, err error) { return } -func (ad *ADBDriver) getWindowSize() (size Size, err error) { +func (ad *ADBDriver) getWindowSize() (size ai.Size, err error) { // adb shell wm size output, err := ad.runShellCommand("wm", "size") if err != nil { @@ -130,14 +136,14 @@ func (ad *ADBDriver) getWindowSize() (size Size, err error) { ss := strings.Split(resolution, "x") width, _ := strconv.Atoi(ss[0]) height, _ := strconv.Atoi(ss[1]) - return Size{Width: width, Height: height}, nil + return ai.Size{Width: width, Height: height}, nil } } err = errors.New("physical window size not found by adb") return } -func (ad *ADBDriver) WindowSize() (size Size, err error) { +func (ad *ADBDriver) WindowSize() (size ai.Size, err error) { if !ad.windowSize.IsNil() { // use cached window size return ad.windowSize, nil @@ -163,7 +169,7 @@ func (ad *ADBDriver) WindowSize() (size Size, err error) { return size, nil } -func (ad *ADBDriver) Screen() (screen Screen, err error) { +func (ad *ADBDriver) Screen() (screen ai.Screen, err error) { err = errDriverNotImplemented return } @@ -172,21 +178,6 @@ func (ad *ADBDriver) Scale() (scale float64, err error) { return 1, nil } -func (ad *ADBDriver) GetTimestamp() (timestamp int64, err error) { - // adb shell date +%s - output, err := ad.runShellCommand("date", "+%s") - if err != nil { - return 0, errors.Wrap(err, "failed to get timestamp by adb") - } - - timestamp, err = strconv.ParseInt(strings.TrimSpace(output), 10, 64) - if err != nil { - return 0, errors.Wrap(err, "convert timestamp failed") - } - - return timestamp, nil -} - // PressBack simulates a short press on the BACK button. func (ad *ADBDriver) PressBack(opts ...option.ActionOption) (err error) { // adb shell input keyevent 4 @@ -1088,6 +1079,10 @@ func (ad *ADBDriver) RecordScreen(folderPath string, duration time.Duration) (vi return filepath.Abs(fileName) } +func (ad *ADBDriver) Setup() error { + return nil +} + func (ad *ADBDriver) TearDown() error { return nil } diff --git a/pkg/uixt/android_driver_stub.go b/pkg/uixt/android_driver_stub.go index 9052c237..dd703f85 100644 --- a/pkg/uixt/android_driver_stub.go +++ b/pkg/uixt/android_driver_stub.go @@ -22,7 +22,7 @@ const ( DouyinServerPort = 32316 ) -func NewStubDriver(device *AndroidDevice) (driver *StubAndroidDriver, err error) { +func NewStubAndroidDriver(device *AndroidDevice) (driver *StubAndroidDriver, err error) { socketLocalPort, err := device.Forward(StubSocketName) if err != nil { return nil, errors.Wrap(code.DeviceConnectionError, diff --git a/pkg/uixt/android_driver_stub_test.go b/pkg/uixt/android_driver_stub_test.go index 7688f3f8..c7600252 100644 --- a/pkg/uixt/android_driver_stub_test.go +++ b/pkg/uixt/android_driver_stub_test.go @@ -14,7 +14,7 @@ func setupStubDriver(t *testing.T) { device, err := NewAndroidDevice() checkErr(t, err) device.STUB = true - androidStubDriver, err = NewStubDriver(device) + androidStubDriver, err = NewStubAndroidDriver(device) checkErr(t, err) } diff --git a/pkg/uixt/android_driver_uia2.go b/pkg/uixt/android_driver_uia2.go index 189364ef..563bcce2 100644 --- a/pkg/uixt/android_driver_uia2.go +++ b/pkg/uixt/android_driver_uia2.go @@ -16,6 +16,7 @@ import ( "github.com/httprunner/httprunner/v5/code" "github.com/httprunner/httprunner/v5/internal/utf7" + "github.com/httprunner/httprunner/v5/pkg/ai" "github.com/httprunner/httprunner/v5/pkg/uixt/option" ) @@ -182,7 +183,7 @@ func (ud *UIA2Driver) BatteryInfo() (batteryInfo BatteryInfo, err error) { return } -func (ud *UIA2Driver) WindowSize() (size Size, err error) { +func (ud *UIA2Driver) WindowSize() (size ai.Size, err error) { // register(getHandler, new GetDeviceSize("/wd/hub/session/:sessionId/window/:windowHandle/size")) if !ud.windowSize.IsNil() { // use cached window size @@ -191,11 +192,11 @@ func (ud *UIA2Driver) WindowSize() (size Size, err error) { var rawResp rawResponse if rawResp, err = ud.httpGET("/session", ud.sessionID, "window/:windowHandle/size"); err != nil { - return Size{}, errors.Wrap(err, "get window size failed by UIA2 request") + return ai.Size{}, errors.Wrap(err, "get window size failed by UIA2 request") } - reply := new(struct{ Value struct{ Size } }) + reply := new(struct{ Value struct{ ai.Size } }) if err = json.Unmarshal(rawResp, reply); err != nil { - return Size{}, errors.Wrap(err, "get window size failed by UIA2 response") + return ai.Size{}, errors.Wrap(err, "get window size failed by UIA2 response") } size = reply.Value.Size diff --git a/pkg/uixt/device.go b/pkg/uixt/device.go index 116026d3..89eb1f29 100644 --- a/pkg/uixt/device.go +++ b/pkg/uixt/device.go @@ -6,17 +6,16 @@ import ( // current implemeted device: IOSDevice, AndroidDevice, HarmonyDevice type IDevice interface { + UUID() string // ios udid or android serial + Setup() error Teardown() error - UUID() string // ios udid or android serial - LogEnabled() bool - - // TODO: remove - NewDriver(...option.DriverOption) (driverExt *DriverExt, err error) - Install(appPath string, opts ...option.InstallOption) error Uninstall(packageName string) error GetPackageInfo(packageName string) (AppInfo, error) + + // TODO: remove? + LogEnabled() bool } diff --git a/pkg/uixt/driver.go b/pkg/uixt/driver.go index df6112c9..6bce7132 100644 --- a/pkg/uixt/driver.go +++ b/pkg/uixt/driver.go @@ -16,9 +16,17 @@ 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/ai" "github.com/httprunner/httprunner/v5/pkg/uixt/option" ) +var ( + _ IDriver = (*ADBDriver)(nil) + _ IDriver = (*UIA2Driver)(nil) + _ IDriver = (*WDADriver)(nil) + _ IDriver = (*HDCDriver)(nil) +) + // current implemeted driver: ADBDriver, UIA2Driver, WDADriver, HDCDriver type IDriver interface { // NewSession starts a new session and returns the DriverSession. @@ -34,6 +42,7 @@ type IDriver interface { Status() (DeviceStatus, error) + GetDevice() IDevice DeviceInfo() (DeviceInfo, error) // Location Returns device location data. @@ -53,13 +62,10 @@ type IDriver interface { // WindowSize Return the width and height in portrait mode. // when getting the window size in wda/ui2/adb, if the device is in landscape mode, // the width and height will be reversed. - WindowSize() (Size, error) - Screen() (Screen, error) + WindowSize() (ai.Size, error) + Screen() (ai.Screen, error) Scale() (float64, error) - // GetTimestamp returns the timestamp of the mobile device - GetTimestamp() (timestamp int64, err error) - // Homescreen Forces the device under test to switch to the home screen Homescreen() error @@ -162,40 +168,66 @@ type IDriver interface { GetDriverResults() []*DriverRequests RecordScreen(folderPath string, duration time.Duration) (videoPath string, err error) + Setup() error TearDown() error } +type IDriverExt interface { + GetDriver() IDriver // get original driver + + GetScreenResult(opts ...option.ActionOption) (screenResult *ScreenResult, err error) + GetScreenTexts(opts ...option.ActionOption) (ocrTexts ai.OCRTexts, err error) + + // swipe + SwipeRelative(fromX, fromY, toX, toY float64, opts ...option.ActionOption) error + SwipeUp(opts ...option.ActionOption) error + SwipeDown(opts ...option.ActionOption) error + SwipeLeft(opts ...option.ActionOption) error + SwipeRight(opts ...option.ActionOption) error +} + +func NewDriverExt(driver IDriver, opts ...ai.AIServiceOption) (IDriverExt, error) { + services := ai.NewAIService(opts...) + driverExt := &DriverExt{ + Driver: driver, + ImageService: services.ImageService, + } + // create results directory + if err := builtin.EnsureFolderExists(config.ResultsPath); err != nil { + return nil, errors.Wrap(err, "create results directory failed") + } + if err := builtin.EnsureFolderExists(config.ScreenShotsPath); err != nil { + return nil, errors.Wrap(err, "create screenshots directory failed") + } + return driverExt, nil +} + type DriverExt struct { - Device IDevice Driver IDriver - ImageService IImageService // used to extract image data + ImageService ai.IImageService // used to extract image data } func newDriverExt(device IDevice, driver IDriver, opts ...option.DriverOption) (dExt *DriverExt, err error) { options := option.NewDriverOptions(opts...) dExt = &DriverExt{ - Device: device, Driver: driver, } if options.WithImageService { - if dExt.ImageService, err = newVEDEMImageService(); err != nil { + if dExt.ImageService, err = ai.NewVEDEMImageService(); err != nil { return nil, err } } if options.WithResultFolder { - // create results directory - if err = builtin.EnsureFolderExists(config.ResultsPath); err != nil { - return nil, errors.Wrap(err, "create results directory failed") - } - if err = builtin.EnsureFolderExists(config.ScreenShotsPath); err != nil { - return nil, errors.Wrap(err, "create screenshots directory failed") - } } return dExt, nil } +func (dExt *DriverExt) GetDriver() IDriver { + return dExt.Driver +} + func (dExt *DriverExt) Setup() error { // unlock device screen err := dExt.Driver.Unlock() diff --git a/pkg/uixt/driver_install.go b/pkg/uixt/driver_install.go index 38a5dde5..ef0f950f 100644 --- a/pkg/uixt/driver_install.go +++ b/pkg/uixt/driver_install.go @@ -42,7 +42,7 @@ func (dExt *DriverExt) InstallByUrl(url string, opts ...option.InstallOption) er } func (dExt *DriverExt) Install(filePath string, opts ...option.InstallOption) error { - if _, ok := dExt.Device.(*AndroidDevice); ok { + if _, ok := dExt.Driver.GetDevice().(*AndroidDevice); ok { stopChan := make(chan struct{}) go func() { ticker := time.NewTicker(5 * time.Second) @@ -87,12 +87,12 @@ func (dExt *DriverExt) Install(filePath string, opts ...option.InstallOption) er }() } - return dExt.Device.Install(filePath, opts...) + return dExt.Driver.GetDevice().Install(filePath, opts...) } func (dExt *DriverExt) Uninstall(packageName string, opts ...option.ActionOption) error { actionOptions := option.NewActionOptions(opts...) - err := dExt.Device.Uninstall(packageName) + err := dExt.Driver.GetDevice().Uninstall(packageName) if err != nil { log.Warn().Err(err).Msg("failed to uninstall") } diff --git a/pkg/uixt/driver_popups.go b/pkg/uixt/driver_popups.go index 1c91526a..145cd431 100644 --- a/pkg/uixt/driver_popups.go +++ b/pkg/uixt/driver_popups.go @@ -5,6 +5,7 @@ import ( "github.com/rs/zerolog/log" "github.com/httprunner/httprunner/v5/code" + "github.com/httprunner/httprunner/v5/pkg/ai" "github.com/httprunner/httprunner/v5/pkg/uixt/option" ) @@ -25,7 +26,7 @@ var popups = [][]string{ {"管理使用时间", ".*忽略.*"}, } -func findTextPopup(screenTexts OCRTexts) (closePoint *OCRText) { +func findTextPopup(screenTexts ai.OCRTexts) (closePoint *ai.OCRText) { for _, popup := range popups { if len(popup) != 2 { continue @@ -42,7 +43,7 @@ func findTextPopup(screenTexts OCRTexts) (closePoint *OCRText) { return } -func (dExt *DriverExt) handleTextPopup(screenTexts OCRTexts) error { +func (dExt *DriverExt) handleTextPopup(screenTexts ai.OCRTexts) error { closePoint := findTextPopup(screenTexts) if closePoint == nil { // no popup found @@ -76,13 +77,13 @@ func (dExt *DriverExt) AutoPopupHandler() error { } type PopupInfo struct { - *ClosePopupsResult - ClosePoints []PointF `json:"close_points,omitempty"` // CV 识别的所有关闭按钮(仅关闭按钮,可能存在多个) - PicName string `json:"pic_name"` - PicURL string `json:"pic_url"` + *ai.ClosePopupsResult + ClosePoints []ai.PointF `json:"close_points,omitempty"` // CV 识别的所有关闭按钮(仅关闭按钮,可能存在多个) + PicName string `json:"pic_name"` + PicURL string `json:"pic_url"` } -func (p *PopupInfo) ClosePoint() *PointF { +func (p *PopupInfo) ClosePoint() *ai.PointF { closeResult := p.ClosePopupsResult if closeResult == nil { return nil diff --git a/pkg/uixt/driver_screenshot.go b/pkg/uixt/driver_screenshot.go index 31712ad0..95911b2e 100644 --- a/pkg/uixt/driver_screenshot.go +++ b/pkg/uixt/driver_screenshot.go @@ -18,21 +18,22 @@ import ( "github.com/httprunner/httprunner/v5/code" "github.com/httprunner/httprunner/v5/internal/builtin" "github.com/httprunner/httprunner/v5/internal/config" + "github.com/httprunner/httprunner/v5/pkg/ai" "github.com/httprunner/httprunner/v5/pkg/uixt/option" ) type ScreenResult struct { - bufSource *bytes.Buffer // raw image buffer bytes - ImagePath string `json:"image_path"` // image file path - Resolution Size `json:"resolution"` - UploadedURL string `json:"uploaded_url"` // uploaded image url - Texts OCRTexts `json:"texts"` // dumped raw OCRTexts - Icons UIResultMap `json:"icons"` // CV 识别的图标 - Tags []string `json:"tags"` // tags for image, e.g. ["feed", "ad", "live"] - Popup *PopupInfo `json:"popup,omitempty"` + bufSource *bytes.Buffer // raw image buffer bytes + ImagePath string `json:"image_path"` // image file path + Resolution ai.Size `json:"resolution"` + UploadedURL string `json:"uploaded_url"` // uploaded image url + Texts ai.OCRTexts `json:"texts"` // dumped raw OCRTexts + Icons ai.UIResultMap `json:"icons"` // CV 识别的图标 + Tags []string `json:"tags"` // tags for image, e.g. ["feed", "ad", "live"] + Popup *PopupInfo `json:"popup,omitempty"` } -func (s *ScreenResult) FilterTextsByScope(x1, y1, x2, y2 float64) OCRTexts { +func (s *ScreenResult) FilterTextsByScope(x1, y1, x2, y2 float64) ai.OCRTexts { if x1 > 1 || y1 > 1 || x2 > 1 || y2 > 1 { log.Warn().Msg("x1, y1, x2, y2 should be in percentage, skip filter scope") return s.Texts @@ -46,9 +47,6 @@ func (s *ScreenResult) FilterTextsByScope(x1, y1, x2, y2 float64) OCRTexts { // GetScreenResult takes a screenshot, returns the image recognition result func (dExt *DriverExt) GetScreenResult(opts ...option.ActionOption) (screenResult *ScreenResult, err error) { actionOptions := option.NewActionOptions(opts...) - if actionOptions.MaxRetryTimes == 0 { - actionOptions.MaxRetryTimes = 1 - } var fileName string screenshotActions := actionOptions.ScreenshotActions() @@ -61,9 +59,9 @@ func (dExt *DriverExt) GetScreenResult(opts ...option.ActionOption) (screenResul } var bufSource *bytes.Buffer - var imageResult *ImageResult + var imageResult *ai.ImageResult var imagePath string - var windowSize Size + var windowSize ai.Size var lastErr error // get screenshot info with retry @@ -87,9 +85,9 @@ func (dExt *DriverExt) GetScreenResult(opts ...option.ActionOption) (screenResul Tags: nil, Resolution: windowSize, } - imageResult, err = dExt.ImageService.GetImage(bufSource, opts...) + imageResult, err = dExt.ImageService.GetImageFromBuffer(bufSource, opts...) if err != nil { - log.Error().Err(err).Msg("GetImage from ImageService failed") + log.Error().Err(err).Msg("GetImageFromBuffer from ImageService failed") lastErr = err continue } @@ -130,7 +128,7 @@ func (dExt *DriverExt) GetScreenResult(opts ...option.ActionOption) (screenResul return screenResult, nil } -func (dExt *DriverExt) GetScreenTexts(opts ...option.ActionOption) (ocrTexts OCRTexts, err error) { +func (dExt *DriverExt) GetScreenTexts(opts ...option.ActionOption) (ocrTexts ai.OCRTexts, err error) { actionOptions := option.NewActionOptions(opts...) if actionOptions.ScreenShotFileName == "" { opts = append(opts, option.WithScreenShotFileName("get_screen_texts")) @@ -143,7 +141,7 @@ func (dExt *DriverExt) GetScreenTexts(opts ...option.ActionOption) (ocrTexts OCR return screenResult.Texts, nil } -func (dExt *DriverExt) FindUIRectInUIKit(search string, opts ...option.ActionOption) (point PointF, err error) { +func (dExt *DriverExt) FindUIRectInUIKit(search string, opts ...option.ActionOption) (point ai.PointF, err error) { // find text using OCR if !builtin.IsPathExists(search) { return dExt.FindScreenText(search, opts...) @@ -153,7 +151,7 @@ func (dExt *DriverExt) FindUIRectInUIKit(search string, opts ...option.ActionOpt return } -func (dExt *DriverExt) FindScreenText(text string, opts ...option.ActionOption) (point PointF, err error) { +func (dExt *DriverExt) FindScreenText(text string, opts ...option.ActionOption) (point ai.PointF, err error) { actionOptions := option.NewActionOptions(opts...) if actionOptions.ScreenShotFileName == "" { opts = append(opts, option.WithScreenShotFileName(fmt.Sprintf("find_screen_text_%s", text))) @@ -175,7 +173,7 @@ func (dExt *DriverExt) FindScreenText(text string, opts ...option.ActionOption) return } -func (dExt *DriverExt) FindUIResult(opts ...option.ActionOption) (point PointF, err error) { +func (dExt *DriverExt) FindUIResult(opts ...option.ActionOption) (point ai.PointF, err error) { actionOptions := option.NewActionOptions(opts...) if actionOptions.ScreenShotFileName == "" { opts = append(opts, option.WithScreenShotFileName( diff --git a/pkg/uixt/driver_session.go b/pkg/uixt/driver_session.go index 5d8b5c24..aa62feab 100644 --- a/pkg/uixt/driver_session.go +++ b/pkg/uixt/driver_session.go @@ -12,6 +12,7 @@ import ( "time" "github.com/httprunner/httprunner/v5/internal/json" + "github.com/httprunner/httprunner/v5/pkg/ai" "github.com/rs/zerolog" "github.com/rs/zerolog/log" ) @@ -24,7 +25,7 @@ type Session struct { // cache to avoid repeated query scale float64 - windowSize Size + windowSize ai.Size // cache uia2/wda request and response requests []*DriverRequests diff --git a/pkg/uixt/driver_swipe.go b/pkg/uixt/driver_swipe.go index 1675c594..366d11fa 100644 --- a/pkg/uixt/driver_swipe.go +++ b/pkg/uixt/driver_swipe.go @@ -10,6 +10,7 @@ import ( "github.com/httprunner/httprunner/v5/code" "github.com/httprunner/httprunner/v5/internal/builtin" + "github.com/httprunner/httprunner/v5/pkg/ai" "github.com/httprunner/httprunner/v5/pkg/uixt/option" ) @@ -146,7 +147,7 @@ func (dExt *DriverExt) swipeToTapTexts(texts []string, opts ...option.ActionOpti actionOptions := option.NewActionOptions(opts...) actionOptions.Identifier = "" optionsWithoutIdentifier := actionOptions.Options() - var point PointF + var point ai.PointF findTexts := func(d *DriverExt) error { var err error screenResult, err := d.GetScreenResult( diff --git a/pkg/uixt/driver_test.go b/pkg/uixt/driver_test.go new file mode 100644 index 00000000..5bc1cf01 --- /dev/null +++ b/pkg/uixt/driver_test.go @@ -0,0 +1,29 @@ +package uixt + +import ( + "testing" + + "github.com/httprunner/httprunner/v5/pkg/ai" +) + +func TestNewDriverExt(t *testing.T) { + device, _ := NewAndroidDevice() + var driver IDriver + var err error + if device.UIA2 || device.LogOn { + driver, err = NewUIA2Driver(device) + } else if device.STUB { + driver, err = NewStubAndroidDriver(device) + } else { + driver, err = NewADBDriver(device) + } + if err != nil { + t.Fatal(err) + } + + driverExt, _ := NewDriverExt(driver, + ai.WithCVService(ai.CVServiceTypeVEDEM)) + + driverExt.GetDriver() + t.Log(driverExt) +} diff --git a/pkg/uixt/harmony_device.go b/pkg/uixt/harmony_device.go index db264e6a..e6eaaf2c 100644 --- a/pkg/uixt/harmony_device.go +++ b/pkg/uixt/harmony_device.go @@ -98,31 +98,6 @@ func (dev *HarmonyDevice) LogEnabled() bool { return dev.LogOn } -func (dev *HarmonyDevice) NewDriver(opts ...option.DriverOption) (driverExt *DriverExt, err error) { - driver, err := newHarmonyDriver(dev.Device) - if err != nil { - log.Error().Err(err).Msg("failed to new harmony driver") - return nil, err - } - - driverExt, err = newDriverExt(dev, driver, opts...) - if err != nil { - return nil, err - } - - return driverExt, nil -} - -func (dev *HarmonyDevice) NewUSBDriver(opts ...option.DriverOption) (driver IDriver, err error) { - harmonyDriver, err := newHarmonyDriver(dev.Device) - if err != nil { - log.Error().Err(err).Msg("failed to new harmony driver") - return nil, err - } - - return harmonyDriver, nil -} - func (dev *HarmonyDevice) Install(appPath string, opts ...option.InstallOption) error { return nil } diff --git a/pkg/uixt/harmony_driver_hdc.go b/pkg/uixt/harmony_driver_hdc.go index ddbf30fa..9d14bd75 100644 --- a/pkg/uixt/harmony_driver_hdc.go +++ b/pkg/uixt/harmony_driver_hdc.go @@ -10,12 +10,15 @@ import ( "code.byted.org/iesqa/ghdc" "github.com/rs/zerolog/log" + "github.com/httprunner/httprunner/v5/pkg/ai" "github.com/httprunner/httprunner/v5/pkg/uixt/option" ) -type hdcDriver struct { +type HDCDriver struct { *HarmonyDevice *Session + IDriver + *DriverExt points []ExportPoint uiDriver *ghdc.UIDriver } @@ -28,10 +31,10 @@ const ( POWER_STATUS_ON PowerStatus = "POWER_STATUS_ON" ) -func newHarmonyDriver(device *ghdc.Device) (driver *hdcDriver, err error) { - driver = new(hdcDriver) - driver.Device = device - uiDriver, err := ghdc.NewUIDriver(*device) +func NewHDCDriver(device *HarmonyDevice) (driver *HDCDriver, err error) { + driver = new(HDCDriver) + driver.HarmonyDevice = device + uiDriver, err := ghdc.NewUIDriver(*device.Device) if err != nil { log.Error().Err(err).Msg("failed to new harmony ui driver") return nil, err @@ -41,64 +44,64 @@ func newHarmonyDriver(device *ghdc.Device) (driver *hdcDriver, err error) { return } -func (hd *hdcDriver) NewSession(capabilities option.Capabilities) (Session, error) { +func (hd *HDCDriver) NewSession(capabilities option.Capabilities) (Session, error) { hd.Reset() hd.Unlock() return Session{}, errDriverNotImplemented } -func (hd *hdcDriver) DeleteSession() error { +func (hd *HDCDriver) DeleteSession() error { return errDriverNotImplemented } -func (hd *hdcDriver) GetSession() *Session { +func (hd *HDCDriver) GetSession() *Session { return hd.Session } -func (hd *hdcDriver) Status() (DeviceStatus, error) { +func (hd *HDCDriver) Status() (DeviceStatus, error) { return DeviceStatus{}, errDriverNotImplemented } -func (hd *hdcDriver) DeviceInfo() (DeviceInfo, error) { +func (hd *HDCDriver) GetDevice() IDevice { + return hd.HarmonyDevice +} + +func (hd *HDCDriver) DeviceInfo() (DeviceInfo, error) { return DeviceInfo{}, errDriverNotImplemented } -func (hd *hdcDriver) Location() (Location, error) { +func (hd *HDCDriver) Location() (Location, error) { return Location{}, errDriverNotImplemented } -func (hd *hdcDriver) BatteryInfo() (BatteryInfo, error) { +func (hd *HDCDriver) BatteryInfo() (BatteryInfo, error) { return BatteryInfo{}, errDriverNotImplemented } -func (hd *hdcDriver) WindowSize() (size Size, err error) { +func (hd *HDCDriver) WindowSize() (size ai.Size, err error) { display, err := hd.uiDriver.GetDisplaySize() if err != nil { log.Error().Err(err).Msg("failed to get window size") - return Size{}, err + return ai.Size{}, err } size.Width = display.Width size.Height = display.Height return size, err } -func (hd *hdcDriver) Screen() (Screen, error) { - return Screen{}, errDriverNotImplemented +func (hd *HDCDriver) Screen() (ai.Screen, error) { + return ai.Screen{}, errDriverNotImplemented } -func (hd *hdcDriver) Scale() (float64, error) { +func (hd *HDCDriver) Scale() (float64, error) { return 1, nil } -func (hd *hdcDriver) GetTimestamp() (timestamp int64, err error) { - return 0, errDriverNotImplemented -} - -func (hd *hdcDriver) Homescreen() error { +func (hd *HDCDriver) Homescreen() error { return hd.uiDriver.PressKey(ghdc.KEYCODE_HOME) } -func (hd *hdcDriver) Unlock() (err error) { +func (hd *HDCDriver) Unlock() (err error) { // Todo 检查是否锁屏 hdc shell hidumper -s RenderService -a screen screenInfo, err := hd.RunShellCommand("hidumper", "-s", "RenderService", "-a", "screen") if err != nil { @@ -120,12 +123,12 @@ func (hd *hdcDriver) Unlock() (err error) { return hd.Swipe(500, 1500, 500, 500) } -func (hd *hdcDriver) AppLaunch(packageName string) error { +func (hd *HDCDriver) AppLaunch(packageName string) error { // Todo return errDriverNotImplemented } -func (hd *hdcDriver) AppTerminate(packageName string) (bool, error) { +func (hd *HDCDriver) AppTerminate(packageName string) (bool, error) { _, err := hd.RunShellCommand("aa", "force-stop", packageName) if err != nil { log.Error().Err(err).Msg("failed to terminal app") @@ -134,29 +137,29 @@ func (hd *hdcDriver) AppTerminate(packageName string) (bool, error) { return true, nil } -func (hd *hdcDriver) GetForegroundApp() (app AppInfo, err error) { +func (hd *HDCDriver) GetForegroundApp() (app AppInfo, err error) { // Todo return AppInfo{}, errDriverNotImplemented } -func (hd *hdcDriver) AssertForegroundApp(packageName string, activityType ...string) error { +func (hd *HDCDriver) AssertForegroundApp(packageName string, activityType ...string) error { // Todo return nil } -func (hd *hdcDriver) StartCamera() error { +func (hd *HDCDriver) StartCamera() error { return errDriverNotImplemented } -func (hd *hdcDriver) StopCamera() error { +func (hd *HDCDriver) StopCamera() error { return errDriverNotImplemented } -func (hd *hdcDriver) Orientation() (orientation Orientation, err error) { +func (hd *HDCDriver) Orientation() (orientation Orientation, err error) { return OrientationPortrait, nil } -func (hd *hdcDriver) Tap(x, y float64, opts ...option.ActionOption) error { +func (hd *HDCDriver) Tap(x, y float64, opts ...option.ActionOption) error { actionOptions := option.NewActionOptions(opts...) if len(actionOptions.Offset) == 2 { @@ -173,20 +176,20 @@ func (hd *hdcDriver) Tap(x, y float64, opts ...option.ActionOption) error { return hd.uiDriver.InjectGesture(ghdc.NewGesture().Start(ghdc.Point{X: int(x), Y: int(y)}).Pause(100)) } -func (hd *hdcDriver) DoubleTap(x, y float64, opts ...option.ActionOption) error { +func (hd *HDCDriver) DoubleTap(x, y float64, opts ...option.ActionOption) error { return errDriverNotImplemented } -func (hd *hdcDriver) TouchAndHold(x, y float64, opts ...option.ActionOption) (err error) { +func (hd *HDCDriver) TouchAndHold(x, y float64, opts ...option.ActionOption) (err error) { return errDriverNotImplemented } -func (hd *hdcDriver) Drag(fromX, fromY, toX, toY float64, opts ...option.ActionOption) error { +func (hd *HDCDriver) Drag(fromX, fromY, toX, toY float64, opts ...option.ActionOption) error { return errDriverNotImplemented } // Swipe works like Drag, but `pressForDuration` value is 0 -func (hd *hdcDriver) Swipe(fromX, fromY, toX, toY float64, opts ...option.ActionOption) error { +func (hd *HDCDriver) Swipe(fromX, fromY, toX, toY float64, opts ...option.ActionOption) error { actionOptions := option.NewActionOptions(opts...) if len(actionOptions.Offset) == 4 { fromX += float64(actionOptions.Offset[0]) @@ -210,51 +213,51 @@ func (hd *hdcDriver) Swipe(fromX, fromY, toX, toY float64, opts ...option.Action return hd.uiDriver.InjectGesture(ghdc.NewGesture().Start(ghdc.Point{X: int(fromX), Y: int(fromY)}).MoveTo(ghdc.Point{X: int(toX), Y: int(toY)}, duration)) } -func (hd *hdcDriver) SetPasteboard(contentType PasteboardType, content string) error { +func (hd *HDCDriver) SetPasteboard(contentType PasteboardType, content string) error { return errDriverNotImplemented } -func (hd *hdcDriver) GetPasteboard(contentType PasteboardType) (raw *bytes.Buffer, err error) { +func (hd *HDCDriver) GetPasteboard(contentType PasteboardType) (raw *bytes.Buffer, err error) { return nil, errDriverNotImplemented } -func (hd *hdcDriver) SetIme(ime string) error { +func (hd *HDCDriver) SetIme(ime string) error { return errDriverNotImplemented } -func (hd *hdcDriver) SendKeys(text string, opts ...option.ActionOption) error { +func (hd *HDCDriver) SendKeys(text string, opts ...option.ActionOption) error { return hd.uiDriver.InputText(text) } -func (hd *hdcDriver) Input(text string, opts ...option.ActionOption) error { +func (hd *HDCDriver) Input(text string, opts ...option.ActionOption) error { return hd.uiDriver.InputText(text) } -func (hd *hdcDriver) Clear(packageName string) error { +func (hd *HDCDriver) Clear(packageName string) error { return errDriverNotImplemented } -func (hd *hdcDriver) PressButton(devBtn DeviceButton) error { +func (hd *HDCDriver) PressButton(devBtn DeviceButton) error { return errDriverNotImplemented } -func (hd *hdcDriver) PressBack(opts ...option.ActionOption) error { +func (hd *HDCDriver) PressBack(opts ...option.ActionOption) error { return hd.uiDriver.PressBack() } -func (hd *hdcDriver) Backspace(count int, opts ...option.ActionOption) (err error) { +func (hd *HDCDriver) Backspace(count int, opts ...option.ActionOption) (err error) { return nil } -func (hd *hdcDriver) PressKeyCode(keyCode KeyCode) (err error) { +func (hd *HDCDriver) PressKeyCode(keyCode KeyCode) (err error) { return errDriverNotImplemented } -func (hd *hdcDriver) PressHarmonyKeyCode(keyCode ghdc.KeyCode) (err error) { +func (hd *HDCDriver) PressHarmonyKeyCode(keyCode ghdc.KeyCode) (err error) { return hd.uiDriver.PressKey(keyCode) } -func (hd *hdcDriver) Screenshot() (*bytes.Buffer, error) { +func (hd *HDCDriver) Screenshot() (*bytes.Buffer, error) { tempDir := os.TempDir() screenshotPath := fmt.Sprintf("%s/screenshot_%d.png", tempDir, time.Now().Unix()) err := hd.uiDriver.Screenshot(screenshotPath) @@ -274,74 +277,78 @@ func (hd *hdcDriver) Screenshot() (*bytes.Buffer, error) { return bytes.NewBuffer(raw), nil } -func (hd *hdcDriver) Source(srcOpt ...option.SourceOption) (string, error) { +func (hd *HDCDriver) Source(srcOpt ...option.SourceOption) (string, error) { return "", nil } -func (hd *hdcDriver) LoginNoneUI(packageName, phoneNumber string, captcha, password string) (info AppLoginInfo, err error) { +func (hd *HDCDriver) LoginNoneUI(packageName, phoneNumber string, captcha, password string) (info AppLoginInfo, err error) { err = errDriverNotImplemented return } -func (hd *hdcDriver) LogoutNoneUI(packageName string) error { +func (hd *HDCDriver) LogoutNoneUI(packageName string) error { return errDriverNotImplemented } -func (hd *hdcDriver) TapByText(text string, opts ...option.ActionOption) error { +func (hd *HDCDriver) TapByText(text string, opts ...option.ActionOption) error { return errDriverNotImplemented } -func (hd *hdcDriver) TapByTexts(actions ...TapTextAction) error { +func (hd *HDCDriver) TapByTexts(actions ...TapTextAction) error { return errDriverNotImplemented } -func (hd *hdcDriver) AccessibleSource() (string, error) { +func (hd *HDCDriver) AccessibleSource() (string, error) { return "", errDriverNotImplemented } -func (hd *hdcDriver) HealthCheck() error { +func (hd *HDCDriver) HealthCheck() error { return errDriverNotImplemented } -func (hd *hdcDriver) GetAppiumSettings() (map[string]interface{}, error) { +func (hd *HDCDriver) GetAppiumSettings() (map[string]interface{}, error) { return nil, errDriverNotImplemented } -func (hd *hdcDriver) SetAppiumSettings(settings map[string]interface{}) (map[string]interface{}, error) { +func (hd *HDCDriver) SetAppiumSettings(settings map[string]interface{}) (map[string]interface{}, error) { return nil, errDriverNotImplemented } -func (hd *hdcDriver) IsHealthy() (bool, error) { +func (hd *HDCDriver) IsHealthy() (bool, error) { return false, errDriverNotImplemented } -func (hd *hdcDriver) StartCaptureLog(identifier ...string) (err error) { +func (hd *HDCDriver) StartCaptureLog(identifier ...string) (err error) { return errDriverNotImplemented } -func (hd *hdcDriver) StopCaptureLog() (result interface{}, err error) { +func (hd *HDCDriver) StopCaptureLog() (result interface{}, err error) { // defer clear(hd.points) return hd.points, nil } -func (hd *hdcDriver) GetDriverResults() []*DriverRequests { +func (hd *HDCDriver) GetDriverResults() []*DriverRequests { return nil } -func (hd *hdcDriver) RecordScreen(folderPath string, duration time.Duration) (videoPath string, err error) { +func (hd *HDCDriver) RecordScreen(folderPath string, duration time.Duration) (videoPath string, err error) { return "", nil } -func (hd *hdcDriver) TearDown() error { +func (hd *HDCDriver) Setup() error { return nil } -func (hd *hdcDriver) Rotation() (rotation Rotation, err error) { +func (hd *HDCDriver) TearDown() error { + return nil +} + +func (hd *HDCDriver) Rotation() (rotation Rotation, err error) { err = errDriverNotImplemented return } -func (hd *hdcDriver) SetRotation(rotation Rotation) (err error) { +func (hd *HDCDriver) SetRotation(rotation Rotation) (err error) { err = errDriverNotImplemented return } diff --git a/pkg/uixt/harmony_test.go b/pkg/uixt/harmony_test.go index c4d72ac3..a148caa5 100644 --- a/pkg/uixt/harmony_test.go +++ b/pkg/uixt/harmony_test.go @@ -7,18 +7,14 @@ import ( "testing" ) -var harmonyDriverExt *DriverExt +var hdcDriver *HDCDriver func setupHarmonyDevice(t *testing.T) { device, err := NewHarmonyDevice() if err != nil { t.Fatal(err) } - driver, err = device.NewUSBDriver() - if err != nil { - t.Fatal(err) - } - harmonyDriverExt, err = newDriverExt(device, driver) + hdcDriver, err = NewHDCDriver(device) if err != nil { t.Fatal(err) } @@ -35,7 +31,7 @@ func TestWindowSize(t *testing.T) { func TestHarmonyTap(t *testing.T) { setupHarmonyDevice(t) - err := harmonyDriverExt.TapAbsXY(200, 2000) + err := hdcDriver.TapAbsXY(200, 2000) if err != nil { t.Fatal(err) } @@ -43,7 +39,7 @@ func TestHarmonyTap(t *testing.T) { func TestHarmonySwipe(t *testing.T) { setupHarmonyDevice(t) - err := harmonyDriverExt.SwipeLeft() + err := hdcDriver.SwipeLeft() if err != nil { t.Fatal(err) } @@ -51,7 +47,7 @@ func TestHarmonySwipe(t *testing.T) { func TestHarmonyInput(t *testing.T) { setupHarmonyDevice(t) - err := harmonyDriverExt.Input("test") + err := hdcDriver.Input("test") if err != nil { t.Fatal(err) } diff --git a/pkg/uixt/ios_device.go b/pkg/uixt/ios_device.go index deee7b2b..bd7924ac 100644 --- a/pkg/uixt/ios_device.go +++ b/pkg/uixt/ios_device.go @@ -552,7 +552,7 @@ func (dev *IOSDevice) NewHTTPDriver(capabilities option.Capabilities) (driver ID Int("localPort", localPort).Int("localMjpegPort", localMjpegPort). Msg("init WDA HTTP driver") - wd := new(wdaDriver) + wd := new(WDADriver) wd.IOSDevice = dev wd.udid = dev.UDID wd.client = &http.Client{ diff --git a/pkg/uixt/ios_driver_stub.go b/pkg/uixt/ios_driver_stub.go index 99208c41..e58faabb 100644 --- a/pkg/uixt/ios_driver_stub.go +++ b/pkg/uixt/ios_driver_stub.go @@ -9,11 +9,12 @@ import ( "github.com/rs/zerolog/log" + "github.com/httprunner/httprunner/v5/pkg/ai" "github.com/httprunner/httprunner/v5/pkg/uixt/option" ) type stubIOSDriver struct { - *wdaDriver + *WDADriver bightInsightPrefix string serverPrefix string @@ -38,7 +39,7 @@ func newStubIOSDriver(bightInsightAddr, serverAddr string, dev *IOSDevice, readT } func (s *stubIOSDriver) setUpWda() (err error) { - if s.wdaDriver == nil { + if s.WDADriver == nil { capabilities := option.NewCapabilities() capabilities.WithDefaultAlertAction(option.AlertActionAccept) driver, err := s.device.NewHTTPDriver(capabilities) @@ -46,7 +47,7 @@ func (s *stubIOSDriver) setUpWda() (err error) { log.Error().Err(err).Msg("stub driver failed to init wda driver") return err } - s.wdaDriver = driver.(*wdaDriver) + s.WDADriver = driver.(*WDADriver) } return nil } @@ -57,7 +58,7 @@ func (s *stubIOSDriver) NewSession(capabilities option.Capabilities) (Session, e if err != nil { return Session{}, err } - return s.wdaDriver.NewSession(capabilities) + return s.WDADriver.NewSession(capabilities) } // DeleteSession Kills application associated with that session and removes session @@ -68,7 +69,7 @@ func (s *stubIOSDriver) DeleteSession() error { if err != nil { return err } - return s.wdaDriver.DeleteSession() + return s.WDADriver.DeleteSession() } func (s *stubIOSDriver) Status() (DeviceStatus, error) { @@ -76,7 +77,7 @@ func (s *stubIOSDriver) Status() (DeviceStatus, error) { if err != nil { return DeviceStatus{}, err } - return s.wdaDriver.Status() + return s.WDADriver.Status() } func (s *stubIOSDriver) DeviceInfo() (DeviceInfo, error) { @@ -84,7 +85,7 @@ func (s *stubIOSDriver) DeviceInfo() (DeviceInfo, error) { if err != nil { return DeviceInfo{}, err } - return s.wdaDriver.DeviceInfo() + return s.WDADriver.DeviceInfo() } func (s *stubIOSDriver) Location() (Location, error) { @@ -92,7 +93,7 @@ func (s *stubIOSDriver) Location() (Location, error) { if err != nil { return Location{}, err } - return s.wdaDriver.Location() + return s.WDADriver.Location() } func (s *stubIOSDriver) BatteryInfo() (BatteryInfo, error) { @@ -100,26 +101,26 @@ func (s *stubIOSDriver) BatteryInfo() (BatteryInfo, error) { if err != nil { return BatteryInfo{}, err } - return s.wdaDriver.BatteryInfo() + return s.WDADriver.BatteryInfo() } // WindowSize Return the width and height in portrait mode. // when getting the window size in wda/ui2/adb, if the device is in landscape mode, // the width and height will be reversed. -func (s *stubIOSDriver) WindowSize() (Size, error) { +func (s *stubIOSDriver) WindowSize() (ai.Size, error) { err := s.setUpWda() if err != nil { - return Size{}, err + return ai.Size{}, err } - return s.wdaDriver.WindowSize() + return s.WDADriver.WindowSize() } -func (s *stubIOSDriver) Screen() (Screen, error) { +func (s *stubIOSDriver) Screen() (ai.Screen, error) { err := s.setUpWda() if err != nil { - return Screen{}, err + return ai.Screen{}, err } - return s.wdaDriver.Screen() + return s.WDADriver.Screen() } func (s *stubIOSDriver) Scale() (float64, error) { @@ -127,16 +128,7 @@ func (s *stubIOSDriver) Scale() (float64, error) { if err != nil { return 0, err } - return s.wdaDriver.Scale() -} - -// GetTimestamp returns the timestamp of the mobile device -func (s *stubIOSDriver) GetTimestamp() (timestamp int64, err error) { - err = s.setUpWda() - if err != nil { - return 0, err - } - return s.wdaDriver.GetTimestamp() + return s.WDADriver.Scale() } // Homescreen Forces the device under test to switch to the home screen @@ -145,7 +137,7 @@ func (s *stubIOSDriver) Homescreen() error { if err != nil { return err } - return s.wdaDriver.Homescreen() + return s.WDADriver.Homescreen() } func (s *stubIOSDriver) Unlock() (err error) { @@ -153,7 +145,7 @@ func (s *stubIOSDriver) Unlock() (err error) { if err != nil { return err } - return s.wdaDriver.Unlock() + return s.WDADriver.Unlock() } // AppLaunch Launch an application with given bundle identifier in scope of current session. @@ -163,7 +155,7 @@ func (s *stubIOSDriver) AppLaunch(packageName string) error { if err != nil { return err } - return s.wdaDriver.AppLaunch(packageName) + return s.WDADriver.AppLaunch(packageName) } // AppTerminate Terminate an application with the given package name. @@ -173,7 +165,7 @@ func (s *stubIOSDriver) AppTerminate(packageName string) (bool, error) { if err != nil { return false, err } - return s.wdaDriver.AppTerminate(packageName) + return s.WDADriver.AppTerminate(packageName) } // GetForegroundApp returns current foreground app package name and activity name @@ -182,16 +174,7 @@ func (s *stubIOSDriver) GetForegroundApp() (app AppInfo, err error) { if err != nil { return AppInfo{}, err } - return s.wdaDriver.GetForegroundApp() -} - -// AssertForegroundApp returns nil if the given package and activity are in foreground -func (s *stubIOSDriver) AssertForegroundApp(packageName string, activityType ...string) error { - err := s.setUpWda() - if err != nil { - return err - } - return s.wdaDriver.AssertForegroundApp(packageName, activityType...) + return s.WDADriver.GetForegroundApp() } // StartCamera Starts a new camera for recording @@ -200,7 +183,7 @@ func (s *stubIOSDriver) StartCamera() error { if err != nil { return err } - return s.wdaDriver.StartCamera() + return s.WDADriver.StartCamera() } // StopCamera Stops the camera for recording @@ -209,7 +192,7 @@ func (s *stubIOSDriver) StopCamera() error { if err != nil { return err } - return s.wdaDriver.StopCamera() + return s.WDADriver.StopCamera() } func (s *stubIOSDriver) Orientation() (orientation Orientation, err error) { @@ -217,7 +200,7 @@ func (s *stubIOSDriver) Orientation() (orientation Orientation, err error) { if err != nil { return OrientationPortrait, err } - return s.wdaDriver.Orientation() + return s.WDADriver.Orientation() } // Tap Sends a tap event at the coordinate. @@ -226,7 +209,7 @@ func (s *stubIOSDriver) Tap(x, y float64, opts ...option.ActionOption) error { if err != nil { return err } - return s.wdaDriver.Tap(x, y, opts...) + return s.WDADriver.Tap(x, y, opts...) } // DoubleTap Sends a double tap event at the coordinate. @@ -235,7 +218,7 @@ func (s *stubIOSDriver) DoubleTap(x, y float64, opts ...option.ActionOption) err if err != nil { return err } - return s.wdaDriver.DoubleTap(x, y, opts...) + return s.WDADriver.DoubleTap(x, y, opts...) } // TouchAndHold Initiates a long-press gesture at the coordinate, holding for the specified duration. @@ -246,7 +229,7 @@ func (s *stubIOSDriver) TouchAndHold(x, y float64, opts ...option.ActionOption) if err != nil { return err } - return s.wdaDriver.TouchAndHold(x, y, opts...) + return s.WDADriver.TouchAndHold(x, y, opts...) } // Drag Initiates a press-and-hold gesture at the coordinate, then drags to another coordinate. @@ -256,7 +239,7 @@ func (s *stubIOSDriver) Drag(fromX, fromY, toX, toY float64, opts ...option.Acti if err != nil { return err } - return s.wdaDriver.Drag(fromX, fromY, toX, toY, opts...) + return s.WDADriver.Drag(fromX, fromY, toX, toY, opts...) } // SetPasteboard Sets data to the general pasteboard @@ -265,7 +248,7 @@ func (s *stubIOSDriver) SetPasteboard(contentType PasteboardType, content string if err != nil { return err } - return s.wdaDriver.SetPasteboard(contentType, content) + return s.WDADriver.SetPasteboard(contentType, content) } // GetPasteboard Gets the data contained in the general pasteboard. @@ -276,7 +259,7 @@ func (s *stubIOSDriver) GetPasteboard(contentType PasteboardType) (raw *bytes.Bu if err != nil { return nil, err } - return s.wdaDriver.GetPasteboard(contentType) + return s.WDADriver.GetPasteboard(contentType) } func (s *stubIOSDriver) SetIme(ime string) error { @@ -284,7 +267,7 @@ func (s *stubIOSDriver) SetIme(ime string) error { if err != nil { return err } - return s.wdaDriver.SetIme(ime) + return s.WDADriver.SetIme(ime) } // SendKeys Types a string into active element. There must be element with keyboard focus, @@ -295,7 +278,7 @@ func (s *stubIOSDriver) SendKeys(text string, opts ...option.ActionOption) error if err != nil { return err } - return s.wdaDriver.SendKeys(text, opts...) + return s.WDADriver.SendKeys(text, opts...) } // Input works like SendKeys @@ -304,7 +287,7 @@ func (s *stubIOSDriver) Input(text string, opts ...option.ActionOption) error { if err != nil { return err } - return s.wdaDriver.Input(text, opts...) + return s.WDADriver.Input(text, opts...) } func (s *stubIOSDriver) Clear(packageName string) error { @@ -312,7 +295,7 @@ func (s *stubIOSDriver) Clear(packageName string) error { if err != nil { return err } - return s.wdaDriver.Clear(packageName) + return s.WDADriver.Clear(packageName) } // PressButton Presses the corresponding hardware button on the device @@ -321,7 +304,7 @@ func (s *stubIOSDriver) PressButton(devBtn DeviceButton) error { if err != nil { return err } - return s.wdaDriver.PressButton(devBtn) + return s.WDADriver.PressButton(devBtn) } // PressBack Presses the back button @@ -330,7 +313,7 @@ func (s *stubIOSDriver) PressBack(opts ...option.ActionOption) error { if err != nil { return err } - return s.wdaDriver.PressBack(opts...) + return s.WDADriver.PressBack(opts...) } func (s *stubIOSDriver) PressKeyCode(keyCode KeyCode) (err error) { @@ -338,7 +321,7 @@ func (s *stubIOSDriver) PressKeyCode(keyCode KeyCode) (err error) { if err != nil { return err } - return s.wdaDriver.PressKeyCode(keyCode) + return s.WDADriver.PressKeyCode(keyCode) } func (s *stubIOSDriver) Screenshot() (*bytes.Buffer, error) { @@ -346,7 +329,7 @@ func (s *stubIOSDriver) Screenshot() (*bytes.Buffer, error) { if err != nil { return nil, err } - return s.wdaDriver.Screenshot() + return s.WDADriver.Screenshot() //screenshotService, err := instruments.NewScreenshotService(s.device.d) //if err != nil { // log.Error().Err(err).Msg("Starting screenshot service failed") @@ -367,7 +350,7 @@ func (s *stubIOSDriver) TapByText(text string, opts ...option.ActionOption) erro if err != nil { return err } - return s.wdaDriver.TapByText(text, opts...) + return s.WDADriver.TapByText(text, opts...) } func (s *stubIOSDriver) TapByTexts(actions ...TapTextAction) error { @@ -375,7 +358,7 @@ func (s *stubIOSDriver) TapByTexts(actions ...TapTextAction) error { if err != nil { return err } - return s.wdaDriver.TapByTexts(actions...) + return s.WDADriver.TapByTexts(actions...) } // AccessibleSource Return application elements accessibility tree @@ -384,7 +367,7 @@ func (s *stubIOSDriver) AccessibleSource() (string, error) { if err != nil { return "", err } - return s.wdaDriver.AccessibleSource() + return s.WDADriver.AccessibleSource() } // HealthCheck Health check might modify simulator state so it should only be called in-between testing sessions @@ -397,7 +380,7 @@ func (s *stubIOSDriver) HealthCheck() error { if err != nil { return err } - return s.wdaDriver.HealthCheck() + return s.WDADriver.HealthCheck() } func (s *stubIOSDriver) GetAppiumSettings() (map[string]interface{}, error) { @@ -405,7 +388,7 @@ func (s *stubIOSDriver) GetAppiumSettings() (map[string]interface{}, error) { if err != nil { return nil, err } - return s.wdaDriver.GetAppiumSettings() + return s.WDADriver.GetAppiumSettings() } func (s *stubIOSDriver) SetAppiumSettings(settings map[string]interface{}) (map[string]interface{}, error) { @@ -413,7 +396,7 @@ func (s *stubIOSDriver) SetAppiumSettings(settings map[string]interface{}) (map[ if err != nil { return nil, err } - return s.wdaDriver.SetAppiumSettings(settings) + return s.WDADriver.SetAppiumSettings(settings) } func (s *stubIOSDriver) IsHealthy() (bool, error) { @@ -421,7 +404,7 @@ func (s *stubIOSDriver) IsHealthy() (bool, error) { if err != nil { return false, err } - return s.wdaDriver.IsHealthy() + return s.WDADriver.IsHealthy() } // triggers the log capture and returns the log entries @@ -430,7 +413,7 @@ func (s *stubIOSDriver) StartCaptureLog(identifier ...string) (err error) { if err != nil { return err } - return s.wdaDriver.StartCaptureLog(identifier...) + return s.WDADriver.StartCaptureLog(identifier...) } func (s *stubIOSDriver) StopCaptureLog() (result interface{}, err error) { @@ -438,7 +421,7 @@ func (s *stubIOSDriver) StopCaptureLog() (result interface{}, err error) { if err != nil { return nil, err } - return s.wdaDriver.StopCaptureLog() + return s.WDADriver.StopCaptureLog() } func (s *stubIOSDriver) GetDriverResults() []*DriverRequests { @@ -446,7 +429,7 @@ func (s *stubIOSDriver) GetDriverResults() []*DriverRequests { if err != nil { return nil } - return s.wdaDriver.GetDriverResults() + return s.WDADriver.GetDriverResults() } func (s *stubIOSDriver) Source(srcOpt ...option.SourceOption) (string, error) { diff --git a/pkg/uixt/ios_driver_wda.go b/pkg/uixt/ios_driver_wda.go index f0acb053..7fb34930 100644 --- a/pkg/uixt/ios_driver_wda.go +++ b/pkg/uixt/ios_driver_wda.go @@ -23,19 +23,31 @@ import ( "github.com/httprunner/httprunner/v5/code" "github.com/httprunner/httprunner/v5/internal/builtin" "github.com/httprunner/httprunner/v5/internal/json" + "github.com/httprunner/httprunner/v5/pkg/ai" "github.com/httprunner/httprunner/v5/pkg/uixt/option" ) -type wdaDriver struct { +func NewWDADriver(device *IOSDevice) (*WDADriver, error) { + log.Info().Interface("device", device).Msg("init ios WDA driver") + driver := &WDADriver{ + IOSDevice: device, + Session: &Session{}, + } + driver.NewSession(nil) + return driver, nil +} + +type WDADriver struct { *IOSDevice *Session + DriverExt udid string mjpegHTTPConn net.Conn // via HTTP mjpegClient *http.Client mjpegUrl string } -func (wd *wdaDriver) resetSession() error { +func (wd *WDADriver) resetSession() error { capabilities := option.NewCapabilities() capabilities.WithDefaultAlertAction(option.AlertActionAccept) @@ -59,7 +71,7 @@ func (wd *wdaDriver) resetSession() error { return nil } -func (wd *wdaDriver) httpRequest(method string, rawURL string, rawBody []byte) (rawResp rawResponse, err error) { +func (wd *WDADriver) httpRequest(method string, rawURL string, rawBody []byte) (rawResp rawResponse, err error) { retryInterval := 3 * time.Second for retryCount := 1; retryCount <= 3; retryCount++ { rawResp, err = wd.Request(method, rawURL, rawBody) @@ -93,11 +105,11 @@ func (wd *wdaDriver) httpRequest(method string, rawURL string, rawBody []byte) ( return } -func (wd *wdaDriver) httpGET(pathElem ...string) (rawResp rawResponse, err error) { +func (wd *WDADriver) httpGET(pathElem ...string) (rawResp rawResponse, err error) { return wd.httpRequest(http.MethodGet, wd.concatURL(nil, pathElem...), nil) } -func (wd *wdaDriver) httpPOST(data interface{}, pathElem ...string) (rawResp rawResponse, err error) { +func (wd *WDADriver) httpPOST(data interface{}, pathElem ...string) (rawResp rawResponse, err error) { var bsJSON []byte = nil if data != nil { if bsJSON, err = json.Marshal(data); err != nil { @@ -107,15 +119,15 @@ func (wd *wdaDriver) httpPOST(data interface{}, pathElem ...string) (rawResp raw return wd.httpRequest(http.MethodPost, wd.concatURL(nil, pathElem...), bsJSON) } -func (wd *wdaDriver) httpDELETE(pathElem ...string) (rawResp rawResponse, err error) { +func (wd *WDADriver) httpDELETE(pathElem ...string) (rawResp rawResponse, err error) { return wd.httpRequest(http.MethodDelete, wd.concatURL(nil, pathElem...), nil) } -func (wd *wdaDriver) GetMjpegClient() *http.Client { +func (wd *WDADriver) GetMjpegClient() *http.Client { return wd.mjpegClient } -func (wd *wdaDriver) NewSession(capabilities option.Capabilities) (sessionInfo Session, err error) { +func (wd *WDADriver) NewSession(capabilities option.Capabilities) (sessionInfo Session, err error) { // [[FBRoute POST:@"/session"].withoutSession respondWithTarget:self action:@selector(handleCreateSession:)] data := make(map[string]interface{}) if len(capabilities) == 0 { @@ -136,7 +148,7 @@ func (wd *wdaDriver) NewSession(capabilities option.Capabilities) (sessionInfo S return } -func (wd *wdaDriver) DeleteSession() (err error) { +func (wd *WDADriver) DeleteSession() (err error) { if wd.mjpegClient != nil { wd.mjpegClient.CloseIdleConnections() } @@ -149,7 +161,7 @@ func (wd *wdaDriver) DeleteSession() (err error) { return } -func (wd *wdaDriver) Status() (deviceStatus DeviceStatus, err error) { +func (wd *WDADriver) Status() (deviceStatus DeviceStatus, err error) { // [[FBRoute GET:@"/status"].withoutSession respondWithTarget:self action:@selector(handleGetStatus:)] var rawResp rawResponse // Notice: use Driver.GET instead of httpGET to avoid loop calling @@ -164,7 +176,11 @@ func (wd *wdaDriver) Status() (deviceStatus DeviceStatus, err error) { return } -func (wd *wdaDriver) DeviceInfo() (deviceInfo DeviceInfo, err error) { +func (wd *WDADriver) GetDevice() IDevice { + return wd.IOSDevice +} + +func (wd *WDADriver) DeviceInfo() (deviceInfo DeviceInfo, err error) { // [[FBRoute GET:@"/wda/device/info"] respondWithTarget:self action:@selector(handleGetDeviceInfo:)] // [[FBRoute GET:@"/wda/device/info"].withoutSession var rawResp rawResponse @@ -179,7 +195,7 @@ func (wd *wdaDriver) DeviceInfo() (deviceInfo DeviceInfo, err error) { return } -func (wd *wdaDriver) Location() (location Location, err error) { +func (wd *WDADriver) Location() (location Location, err error) { // [[FBRoute GET:@"/wda/device/location"] respondWithTarget:self action:@selector(handleGetLocation:)] // [[FBRoute GET:@"/wda/device/location"].withoutSession var rawResp rawResponse @@ -194,7 +210,7 @@ func (wd *wdaDriver) Location() (location Location, err error) { return } -func (wd *wdaDriver) BatteryInfo() (batteryInfo BatteryInfo, err error) { +func (wd *WDADriver) BatteryInfo() (batteryInfo BatteryInfo, err error) { // [[FBRoute GET:@"/wda/batteryInfo"] respondWithTarget:self action:@selector(handleGetBatteryInfo:)] var rawResp rawResponse if rawResp, err = wd.httpGET("/session", wd.sessionID, "/wda/batteryInfo"); err != nil { @@ -208,7 +224,7 @@ func (wd *wdaDriver) BatteryInfo() (batteryInfo BatteryInfo, err error) { return } -func (wd *wdaDriver) WindowSize() (size Size, err error) { +func (wd *WDADriver) WindowSize() (size ai.Size, err error) { // [[FBRoute GET:@"/window/size"] respondWithTarget:self action:@selector(handleGetWindowSize:)] if !wd.windowSize.IsNil() { // use cached window size @@ -217,16 +233,16 @@ func (wd *wdaDriver) WindowSize() (size Size, err error) { var rawResp rawResponse if rawResp, err = wd.httpGET("/session", wd.sessionID, "/window/size"); err != nil { - return Size{}, errors.Wrap(err, "get window size failed by WDA request") + return ai.Size{}, errors.Wrap(err, "get window size failed by WDA request") } - reply := new(struct{ Value struct{ Size } }) + reply := new(struct{ Value struct{ ai.Size } }) if err = json.Unmarshal(rawResp, reply); err != nil { - return Size{}, errors.Wrap(err, "get window size failed by WDA response") + return ai.Size{}, errors.Wrap(err, "get window size failed by WDA response") } size = reply.Value.Size scale, err := wd.Scale() if err != nil { - return Size{}, errors.Wrap(err, "get window size scale failed") + return ai.Size{}, errors.Wrap(err, "get window size scale failed") } size.Height = size.Height * int(scale) size.Width = size.Width * int(scale) @@ -235,26 +251,21 @@ func (wd *wdaDriver) WindowSize() (size Size, err error) { return wd.windowSize, nil } -func (wd *wdaDriver) Screen() (screen Screen, err error) { +func (wd *WDADriver) Screen() (screen ai.Screen, err error) { // [[FBRoute GET:@"/wda/screen"] respondWithTarget:self action:@selector(handleGetScreen:)] var rawResp rawResponse if rawResp, err = wd.httpGET("/session", wd.sessionID, "/wda/screen"); err != nil { - return Screen{}, err + return ai.Screen{}, err } - reply := new(struct{ Value struct{ Screen } }) + reply := new(struct{ Value struct{ ai.Screen } }) if err = json.Unmarshal(rawResp, reply); err != nil { - return Screen{}, err + return ai.Screen{}, err } screen = reply.Value.Screen return } -func (wd *wdaDriver) GetTimestamp() (timestamp int64, err error) { - return 0, errors.Wrap(errDriverNotImplemented, - "GetTimestamp not implemented for ios") -} - -func (wd *wdaDriver) Scale() (float64, error) { +func (wd *WDADriver) Scale() (float64, error) { if !builtin.IsZeroFloat64(wd.scale) { return wd.scale, nil } @@ -266,11 +277,11 @@ func (wd *wdaDriver) Scale() (float64, error) { return screen.Scale, nil } -func (wd *wdaDriver) toScale(x float64) float64 { +func (wd *WDADriver) toScale(x float64) float64 { return x / wd.scale } -func (wd *wdaDriver) ActiveAppInfo() (info AppInfo, err error) { +func (wd *WDADriver) ActiveAppInfo() (info AppInfo, err error) { // [[FBRoute GET:@"/wda/activeAppInfo"] respondWithTarget:self action:@selector(handleActiveAppInfo:)] // [[FBRoute GET:@"/wda/activeAppInfo"].withoutSession var rawResp rawResponse @@ -285,7 +296,7 @@ func (wd *wdaDriver) ActiveAppInfo() (info AppInfo, err error) { return } -func (wd *wdaDriver) ActiveAppsList() (appsList []AppBaseInfo, err error) { +func (wd *WDADriver) ActiveAppsList() (appsList []AppBaseInfo, err error) { // [[FBRoute GET:@"/wda/apps/list"] respondWithTarget:self action:@selector(handleGetActiveAppsList:)] var rawResp rawResponse if rawResp, err = wd.httpGET("/session", wd.sessionID, "/wda/apps/list"); err != nil { @@ -299,7 +310,7 @@ func (wd *wdaDriver) ActiveAppsList() (appsList []AppBaseInfo, err error) { return } -func (wd *wdaDriver) AppState(bundleId string) (runState AppState, err error) { +func (wd *WDADriver) AppState(bundleId string) (runState AppState, err error) { // [[FBRoute POST:@"/wda/apps/state"] respondWithTarget:self action:@selector(handleSessionAppState:)] data := map[string]interface{}{"bundleId": bundleId} var rawResp rawResponse @@ -315,7 +326,7 @@ func (wd *wdaDriver) AppState(bundleId string) (runState AppState, err error) { return } -func (wd *wdaDriver) IsLocked() (locked bool, err error) { +func (wd *WDADriver) IsLocked() (locked bool, err error) { // [[FBRoute GET:@"/wda/locked"] respondWithTarget:self action:@selector(handleIsLocked:)] // [[FBRoute GET:@"/wda/locked"].withoutSession var rawResp rawResponse @@ -328,27 +339,27 @@ func (wd *wdaDriver) IsLocked() (locked bool, err error) { return } -func (wd *wdaDriver) Unlock() (err error) { +func (wd *WDADriver) Unlock() (err error) { // [[FBRoute POST:@"/wda/unlock"] respondWithTarget:self action:@selector(handleUnlock:)] // [[FBRoute POST:@"/wda/unlock"].withoutSession _, err = wd.httpPOST(nil, "/session", wd.sessionID, "/wda/unlock") return } -func (wd *wdaDriver) Lock() (err error) { +func (wd *WDADriver) Lock() (err error) { // [[FBRoute POST:@"/wda/lock"] respondWithTarget:self action:@selector(handleLock:)] // [[FBRoute POST:@"/wda/lock"].withoutSession _, err = wd.httpPOST(nil, "/session", wd.sessionID, "/wda/lock") return } -func (wd *wdaDriver) Homescreen() (err error) { +func (wd *WDADriver) Homescreen() (err error) { // [[FBRoute POST:@"/wda/homescreen"].withoutSession respondWithTarget:self action:@selector(handleHomescreenCommand:)] _, err = wd.httpPOST(nil, "/wda/homescreen") return } -func (wd *wdaDriver) AlertText() (text string, err error) { +func (wd *WDADriver) AlertText() (text string, err error) { // [[FBRoute GET:@"/alert/text"] respondWithTarget:self action:@selector(handleAlertGetTextCommand:)] // [[FBRoute GET:@"/alert/text"].withoutSession var rawResp rawResponse @@ -361,7 +372,7 @@ func (wd *wdaDriver) AlertText() (text string, err error) { return } -func (wd *wdaDriver) AlertButtons() (btnLabels []string, err error) { +func (wd *WDADriver) AlertButtons() (btnLabels []string, err error) { // [[FBRoute GET:@"/wda/alert/buttons"] respondWithTarget:self action:@selector(handleGetAlertButtonsCommand:)] var rawResp rawResponse if rawResp, err = wd.httpGET("/session", wd.sessionID, "/wda/alert/buttons"); err != nil { @@ -375,7 +386,7 @@ func (wd *wdaDriver) AlertButtons() (btnLabels []string, err error) { return } -func (wd *wdaDriver) AlertAccept(label ...string) (err error) { +func (wd *WDADriver) AlertAccept(label ...string) (err error) { // [[FBRoute POST:@"/alert/accept"] respondWithTarget:self action:@selector(handleAlertAcceptCommand:)] // [[FBRoute POST:@"/alert/accept"].withoutSession data := make(map[string]interface{}) @@ -386,7 +397,7 @@ func (wd *wdaDriver) AlertAccept(label ...string) (err error) { return } -func (wd *wdaDriver) AlertDismiss(label ...string) (err error) { +func (wd *WDADriver) AlertDismiss(label ...string) (err error) { // [[FBRoute POST:@"/alert/dismiss"] respondWithTarget:self action:@selector(handleAlertDismissCommand:)] // [[FBRoute POST:@"/alert/dismiss"].withoutSession data := make(map[string]interface{}) @@ -397,14 +408,14 @@ func (wd *wdaDriver) AlertDismiss(label ...string) (err error) { return } -func (wd *wdaDriver) AlertSendKeys(text string) (err error) { +func (wd *WDADriver) AlertSendKeys(text string) (err error) { // [[FBRoute POST:@"/alert/text"] respondWithTarget:self action:@selector(handleAlertSetTextCommand:)] data := map[string]interface{}{"value": strings.Split(text, "")} _, err = wd.httpPOST(data, "/session", wd.sessionID, "/alert/text") return } -func (wd *wdaDriver) AppLaunch(bundleId string) (err error) { +func (wd *WDADriver) AppLaunch(bundleId string) (err error) { // [[FBRoute POST:@"/wda/apps/launch"] respondWithTarget:self action:@selector(handleSessionAppLaunch:)] data := make(map[string]interface{}) data["bundleId"] = bundleId @@ -419,7 +430,7 @@ func (wd *wdaDriver) AppLaunch(bundleId string) (err error) { return nil } -func (wd *wdaDriver) AppLaunchUnattached(bundleId string) (err error) { +func (wd *WDADriver) AppLaunchUnattached(bundleId string) (err error) { // [[FBRoute POST:@"/wda/apps/launchUnattached"].withoutSession respondWithTarget:self action:@selector(handleLaunchUnattachedApp:)] data := map[string]interface{}{"bundleId": bundleId} _, err = wd.httpPOST(data, "/wda/apps/launchUnattached") @@ -430,7 +441,7 @@ func (wd *wdaDriver) AppLaunchUnattached(bundleId string) (err error) { return nil } -func (wd *wdaDriver) AppTerminate(bundleId string) (successful bool, err error) { +func (wd *WDADriver) AppTerminate(bundleId string) (successful bool, err error) { // [[FBRoute POST:@"/wda/apps/terminate"] respondWithTarget:self action:@selector(handleSessionAppTerminate:)] data := map[string]interface{}{"bundleId": bundleId} var rawResp rawResponse @@ -443,14 +454,14 @@ func (wd *wdaDriver) AppTerminate(bundleId string) (successful bool, err error) return } -func (wd *wdaDriver) AppActivate(bundleId string) (err error) { +func (wd *WDADriver) AppActivate(bundleId string) (err error) { // [[FBRoute POST:@"/wda/apps/activate"] respondWithTarget:self action:@selector(handleSessionAppActivate:)] data := map[string]interface{}{"bundleId": bundleId} _, err = wd.httpPOST(data, "/session", wd.sessionID, "/wda/apps/activate") return } -func (wd *wdaDriver) AppDeactivate(second float64) (err error) { +func (wd *WDADriver) AppDeactivate(second float64) (err error) { // [[FBRoute POST:@"/wda/deactivateApp"] respondWithTarget:self action:@selector(handleDeactivateAppCommand:)] if second < 3 { second = 3.0 @@ -460,7 +471,7 @@ func (wd *wdaDriver) AppDeactivate(second float64) (err error) { return } -func (wd *wdaDriver) GetForegroundApp() (appInfo AppInfo, err error) { +func (wd *WDADriver) GetForegroundApp() (appInfo AppInfo, err error) { activeAppInfo, err := wd.ActiveAppInfo() appInfo.BundleId = activeAppInfo.BundleId if err != nil { @@ -484,7 +495,7 @@ func (wd *wdaDriver) GetForegroundApp() (appInfo AppInfo, err error) { return appInfo, err } -func (wd *wdaDriver) AssertForegroundApp(bundleId string, viewControllerType ...string) error { +func (wd *WDADriver) AssertForegroundApp(bundleId string, viewControllerType ...string) error { log.Debug().Str("bundleId", bundleId). Strs("viewControllerType", viewControllerType). Msg("assert ios foreground bundleId") @@ -507,7 +518,7 @@ func (wd *wdaDriver) AssertForegroundApp(bundleId string, viewControllerType ... return nil } -func (wd *wdaDriver) Tap(x, y float64, opts ...option.ActionOption) (err error) { +func (wd *WDADriver) Tap(x, y float64, opts ...option.ActionOption) (err error) { // [[FBRoute POST:@"/wda/tap/:uuid"] respondWithTarget:self action:@selector(handleTap:)] actionOptions := option.NewActionOptions(opts...) @@ -531,7 +542,7 @@ func (wd *wdaDriver) Tap(x, y float64, opts ...option.ActionOption) (err error) return } -func (wd *wdaDriver) DoubleTap(x, y float64, opts ...option.ActionOption) (err error) { +func (wd *WDADriver) DoubleTap(x, y float64, opts ...option.ActionOption) (err error) { // [[FBRoute POST:@"/wda/doubleTap"] respondWithTarget:self action:@selector(handleDoubleTapCoordinate:)] actionOptions := option.NewActionOptions(opts...) x = wd.toScale(x) @@ -551,7 +562,7 @@ func (wd *wdaDriver) DoubleTap(x, y float64, opts ...option.ActionOption) (err e return } -func (wd *wdaDriver) TouchAndHold(x, y float64, opts ...option.ActionOption) (err error) { +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)) @@ -559,7 +570,7 @@ func (wd *wdaDriver) TouchAndHold(x, y float64, opts ...option.ActionOption) (er return wd.Tap(x, y, opts...) } -func (wd *wdaDriver) Drag(fromX, fromY, toX, toY float64, opts ...option.ActionOption) (err error) { +func (wd *WDADriver) Drag(fromX, fromY, toX, toY float64, opts ...option.ActionOption) (err error) { // [[FBRoute POST:@"/wda/dragfromtoforduration"] respondWithTarget:self action:@selector(handleDragCoordinate:)] actionOptions := option.NewActionOptions(opts...) @@ -596,11 +607,11 @@ func (wd *wdaDriver) Drag(fromX, fromY, toX, toY float64, opts ...option.ActionO return } -func (wd *wdaDriver) Swipe(fromX, fromY, toX, toY float64, opts ...option.ActionOption) error { +func (wd *WDADriver) Swipe(fromX, fromY, toX, toY float64, opts ...option.ActionOption) error { return wd.Drag(fromX, fromY, toX, toY, opts...) } -func (wd *wdaDriver) SetPasteboard(contentType PasteboardType, content string) (err error) { +func (wd *WDADriver) SetPasteboard(contentType PasteboardType, content string) (err error) { // [[FBRoute POST:@"/wda/setPasteboard"] respondWithTarget:self action:@selector(handleSetPasteboard:)] data := map[string]interface{}{ "contentType": contentType, @@ -610,7 +621,7 @@ func (wd *wdaDriver) SetPasteboard(contentType PasteboardType, content string) ( return } -func (wd *wdaDriver) GetPasteboard(contentType PasteboardType) (raw *bytes.Buffer, err error) { +func (wd *WDADriver) GetPasteboard(contentType PasteboardType) (raw *bytes.Buffer, err error) { // [[FBRoute POST:@"/wda/getPasteboard"] respondWithTarget:self action:@selector(handleGetPasteboard:)] data := map[string]interface{}{"contentType": contentType} var rawResp rawResponse @@ -623,15 +634,15 @@ func (wd *wdaDriver) GetPasteboard(contentType PasteboardType) (raw *bytes.Buffe return } -func (wd *wdaDriver) SetIme(ime string) error { +func (wd *WDADriver) SetIme(ime string) error { return errDriverNotImplemented } -func (wd *wdaDriver) PressKeyCode(keyCode KeyCode) (err error) { +func (wd *WDADriver) PressKeyCode(keyCode KeyCode) (err error) { return errDriverNotImplemented } -func (wd *wdaDriver) SendKeys(text string, opts ...option.ActionOption) (err error) { +func (wd *WDADriver) SendKeys(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, "")} @@ -643,7 +654,7 @@ func (wd *wdaDriver) SendKeys(text string, opts ...option.ActionOption) (err err return } -func (wd *wdaDriver) Backspace(count int, opts ...option.ActionOption) (err error) { +func (wd *WDADriver) Backspace(count int, opts ...option.ActionOption) (err error) { if count == 0 { return nil } @@ -657,16 +668,16 @@ func (wd *wdaDriver) Backspace(count int, opts ...option.ActionOption) (err erro return } -func (wd *wdaDriver) Input(text string, opts ...option.ActionOption) (err error) { +func (wd *WDADriver) Input(text string, opts ...option.ActionOption) (err error) { return wd.SendKeys(text, opts...) } -func (wd *wdaDriver) Clear(packageName string) error { +func (wd *WDADriver) Clear(packageName string) error { return errDriverNotImplemented } // PressBack simulates a short press on the BACK button. -func (wd *wdaDriver) PressBack(opts ...option.ActionOption) (err error) { +func (wd *WDADriver) PressBack(opts ...option.ActionOption) (err error) { actionOptions := option.NewActionOptions(opts...) windowSize, err := wd.WindowSize() @@ -702,27 +713,27 @@ func (wd *wdaDriver) PressBack(opts ...option.ActionOption) (err error) { return } -func (wd *wdaDriver) PressButton(devBtn DeviceButton) (err error) { +func (wd *WDADriver) PressButton(devBtn DeviceButton) (err error) { // [[FBRoute POST:@"/wda/pressButton"] respondWithTarget:self action:@selector(handlePressButtonCommand:)] data := map[string]interface{}{"name": devBtn} _, err = wd.httpPOST(data, "/session", wd.sessionID, "/wda/pressButton") return } -func (wd *wdaDriver) LoginNoneUI(packageName, phoneNumber string, captcha, password string) (info AppLoginInfo, err error) { +func (wd *WDADriver) LoginNoneUI(packageName, phoneNumber string, captcha, password string) (info AppLoginInfo, err error) { return info, errDriverNotImplemented } -func (wd *wdaDriver) LogoutNoneUI(packageName string) error { +func (wd *WDADriver) LogoutNoneUI(packageName string) error { return errDriverNotImplemented } -func (wd *wdaDriver) StartCamera() (err error) { +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) { +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 { @@ -734,7 +745,7 @@ func (wd *wdaDriver) StopCamera() (err error) { return nil } -func (wd *wdaDriver) Orientation() (orientation Orientation, err error) { +func (wd *WDADriver) Orientation() (orientation Orientation, err error) { // [[FBRoute GET:@"/orientation"] respondWithTarget:self action:@selector(handleGetOrientation:)] var rawResp rawResponse if rawResp, err = wd.httpGET("/session", wd.sessionID, "/orientation"); err != nil { @@ -748,14 +759,14 @@ func (wd *wdaDriver) Orientation() (orientation Orientation, err error) { return } -func (wd *wdaDriver) SetOrientation(orientation Orientation) (err error) { +func (wd *WDADriver) SetOrientation(orientation Orientation) (err error) { // [[FBRoute POST:@"/orientation"] respondWithTarget:self action:@selector(handleSetOrientation:)] data := map[string]interface{}{"orientation": orientation} _, err = wd.httpPOST(data, "/session", wd.sessionID, "/orientation") return } -func (wd *wdaDriver) Rotation() (rotation Rotation, err error) { +func (wd *WDADriver) Rotation() (rotation Rotation, err error) { // [[FBRoute GET:@"/rotation"] respondWithTarget:self action:@selector(handleGetRotation:)] var rawResp rawResponse if rawResp, err = wd.httpGET("/session", wd.sessionID, "/rotation"); err != nil { @@ -769,13 +780,13 @@ func (wd *wdaDriver) Rotation() (rotation Rotation, err error) { return } -func (wd *wdaDriver) SetRotation(rotation Rotation) (err error) { +func (wd *WDADriver) SetRotation(rotation Rotation) (err error) { // [[FBRoute POST:@"/rotation"] respondWithTarget:self action:@selector(handleSetRotation:)] _, err = wd.httpPOST(rotation, "/session", wd.sessionID, "/rotation") return } -func (wd *wdaDriver) Screenshot() (raw *bytes.Buffer, err error) { +func (wd *WDADriver) Screenshot() (raw *bytes.Buffer, err error) { // [[FBRoute GET:@"/screenshot"] respondWithTarget:self action:@selector(handleGetScreenshot:)] // [[FBRoute GET:@"/screenshot"].withoutSession respondWithTarget:self action:@selector(handleGetScreenshot:)] var rawResp rawResponse @@ -791,7 +802,7 @@ func (wd *wdaDriver) Screenshot() (raw *bytes.Buffer, err error) { return } -func (wd *wdaDriver) Source(srcOpt ...option.SourceOption) (source string, 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 tmp, _ := url.Parse(wd.concatURL(nil, "/session", wd.sessionID)) @@ -825,15 +836,15 @@ func (wd *wdaDriver) Source(srcOpt ...option.SourceOption) (source string, err e return } -func (wd *wdaDriver) TapByText(text string, opts ...option.ActionOption) error { +func (wd *WDADriver) TapByText(text string, opts ...option.ActionOption) error { return errDriverNotImplemented } -func (wd *wdaDriver) TapByTexts(actions ...TapTextAction) error { +func (wd *WDADriver) TapByTexts(actions ...TapTextAction) error { return errDriverNotImplemented } -func (wd *wdaDriver) AccessibleSource() (source string, err error) { +func (wd *WDADriver) AccessibleSource() (source string, err error) { // [[FBRoute GET:@"/wda/accessibleSource"] respondWithTarget:self action:@selector(handleGetAccessibleSourceCommand:)] // [[FBRoute GET:@"/wda/accessibleSource"].withoutSession var rawResp rawResponse @@ -848,13 +859,13 @@ func (wd *wdaDriver) AccessibleSource() (source string, err error) { return } -func (wd *wdaDriver) HealthCheck() (err error) { +func (wd *WDADriver) HealthCheck() (err error) { // [[FBRoute GET:@"/wda/healthcheck"].withoutSession respondWithTarget:self action:@selector(handleGetHealthCheck:)] _, err = wd.httpGET("/wda/healthcheck") return } -func (wd *wdaDriver) GetAppiumSettings() (settings map[string]interface{}, err error) { +func (wd *WDADriver) GetAppiumSettings() (settings map[string]interface{}, err error) { // [[FBRoute GET:@"/appium/settings"] respondWithTarget:self action:@selector(handleGetSettings:)] var rawResp rawResponse if rawResp, err = wd.httpGET("/session", wd.sessionID, "/appium/settings"); err != nil { @@ -868,7 +879,7 @@ func (wd *wdaDriver) GetAppiumSettings() (settings map[string]interface{}, err e return } -func (wd *wdaDriver) SetAppiumSettings(settings map[string]interface{}) (ret map[string]interface{}, err error) { +func (wd *WDADriver) SetAppiumSettings(settings map[string]interface{}) (ret map[string]interface{}, err error) { // [[FBRoute POST:@"/appium/settings"] respondWithTarget:self action:@selector(handleSetSettings:)] data := map[string]interface{}{"settings": settings} var rawResp rawResponse @@ -883,7 +894,7 @@ func (wd *wdaDriver) SetAppiumSettings(settings map[string]interface{}) (ret map return } -func (wd *wdaDriver) IsHealthy() (healthy bool, err error) { +func (wd *WDADriver) IsHealthy() (healthy bool, err error) { var rawResp rawResponse if rawResp, err = wd.httpGET("/health"); err != nil { return false, err @@ -894,17 +905,17 @@ func (wd *wdaDriver) IsHealthy() (healthy bool, err error) { return true, nil } -func (wd *wdaDriver) WdaShutdown() (err error) { +func (wd *WDADriver) WdaShutdown() (err error) { _, err = wd.httpGET("/wda/shutdown") return } -func (wd *wdaDriver) triggerWDALog(data map[string]interface{}) (rawResp []byte, err error) { +func (wd *WDADriver) triggerWDALog(data map[string]interface{}) (rawResp []byte, err error) { // [[FBRoute POST:@"/gtf/automation/log"].withoutSession respondWithTarget:self action:@selector(handleAutomationLog:)] return wd.httpPOST(data, "/gtf/automation/log") } -func (wd *wdaDriver) RecordScreen(folderPath string, duration time.Duration) (videoPath string, err error) { +func (wd *WDADriver) RecordScreen(folderPath string, duration time.Duration) (videoPath string, err error) { // 获取当前时间戳 timestamp := time.Now().Format("20060102_150405") + fmt.Sprintf("_%03d", time.Now().UnixNano()/1e6%1000) // 创建文件名 @@ -968,7 +979,7 @@ func (wd *wdaDriver) RecordScreen(folderPath string, duration time.Duration) (vi return filepath.Abs(fileName) } -func (wd *wdaDriver) StartCaptureLog(identifier ...string) error { +func (wd *WDADriver) StartCaptureLog(identifier ...string) error { log.Info().Msg("start WDA log recording") if identifier == nil { identifier = []string{""} @@ -988,7 +999,7 @@ type wdaResponse struct { SessionID string `json:"sessionId"` } -func (wd *wdaDriver) StopCaptureLog() (result interface{}, err error) { +func (wd *WDADriver) StopCaptureLog() (result interface{}, err error) { log.Info().Msg("stop log recording") data := map[string]interface{}{"action": "stop"} rawResp, err := wd.triggerWDALog(data) @@ -1007,18 +1018,22 @@ func (wd *wdaDriver) StopCaptureLog() (result interface{}, err error) { return reply.Value, nil } -func (wd *wdaDriver) GetSession() *Session { +func (wd *WDADriver) GetSession() *Session { return wd.Session } -func (wd *wdaDriver) GetDriverResults() []*DriverRequests { +func (wd *WDADriver) GetDriverResults() []*DriverRequests { defer func() { wd.requests = nil }() return wd.requests } -func (wd *wdaDriver) TearDown() error { +func (wd *WDADriver) Setup() error { + return nil +} + +func (wd *WDADriver) TearDown() error { wd.mjpegClient.CloseIdleConnections() wd.client.CloseIdleConnections() return nil diff --git a/pkg/uixt/ios_test.go b/pkg/uixt/ios_test.go index 922aa31a..ea0b1f62 100644 --- a/pkg/uixt/ios_test.go +++ b/pkg/uixt/ios_test.go @@ -471,7 +471,7 @@ func Test_remoteWD_AccessibleSource(t *testing.T) { func TestRecord(t *testing.T) { setup(t) - path, err := driver.(*wdaDriver).RecordScreen("", 5*time.Second) + path, err := driver.(*WDADriver).RecordScreen("", 5*time.Second) if err != nil { t.Fatal(err) } diff --git a/pkg/uixt/option/action.go b/pkg/uixt/option/action.go index 73eef304..5a99a73c 100644 --- a/pkg/uixt/option/action.go +++ b/pkg/uixt/option/action.go @@ -260,6 +260,9 @@ func NewActionOptions(opts ...ActionOption) *ActionOptions { for _, option := range opts { option(actionOptions) } + if actionOptions.MaxRetryTimes == 0 { + actionOptions.MaxRetryTimes = 1 + } return actionOptions } diff --git a/pkg/uixt/option/driver.go b/pkg/uixt/option/driver.go index b4f0f3c4..de800ab3 100644 --- a/pkg/uixt/option/driver.go +++ b/pkg/uixt/option/driver.go @@ -1,6 +1,8 @@ package option -import "github.com/httprunner/funplugin" +import ( + "github.com/httprunner/funplugin" +) type DriverOptions struct { Capabilities Capabilities diff --git a/pkg/uixt/types.go b/pkg/uixt/types.go index ca32df8f..0b26886d 100644 --- a/pkg/uixt/types.go +++ b/pkg/uixt/types.go @@ -2,7 +2,6 @@ package uixt import ( "fmt" - "math" ) type DeviceStatus struct { @@ -153,20 +152,6 @@ func (bs BatteryStatus) String() string { } } -type Size struct { - Width int `json:"width"` - Height int `json:"height"` -} - -func (s Size) IsNil() bool { - return s.Width == 0 && s.Height == 0 -} - -type Screen struct { - StatusBarSize Size `json:"statusBarSize"` - Scale float64 `json:"scale"` -} - type AppInfo struct { Name string `json:"name,omitempty"` AppBaseInfo @@ -274,18 +259,3 @@ const ( DirectionLeft Direction = "left" DirectionRight Direction = "right" ) - -type Point struct { - X int `json:"x"` // upper left X coordinate of selected element - Y int `json:"y"` // upper left Y coordinate of selected element -} - -type PointF struct { - X float64 `json:"x"` - Y float64 `json:"y"` -} - -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 -}