From d06694312edd5e981a8a9c39e78970fd7a53f034 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Mon, 17 Oct 2022 20:02:39 +0800 Subject: [PATCH] feat: swipeToTapApp --- hrp/pkg/uixt/ext.go | 79 ++++++++++++--------------------- hrp/pkg/uixt/interface.go | 57 +++++++++++++++--------- hrp/pkg/uixt/swipe.go | 91 +++++++++++++++++++++++++++++++++----- hrp/pkg/uixt/swipe_test.go | 8 ++-- hrp/pkg/uixt/tap_test.go | 6 +-- 5 files changed, 153 insertions(+), 88 deletions(-) diff --git a/hrp/pkg/uixt/ext.go b/hrp/pkg/uixt/ext.go index 39438cb1..f7b353bf 100644 --- a/hrp/pkg/uixt/ext.go +++ b/hrp/pkg/uixt/ext.go @@ -386,45 +386,12 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { AppLaunchUnattached, action.Params) case ACTION_SwipeToTapApp: if appName, ok := action.Params.(string); ok { - if len(action.Scope) != 4 { - action.Scope = []float64{0, 0, 1, 1} - } - - identifierOption := WithDataIdentifier(action.Identifier) - indexOption := WithDataIndex(action.Index) - scopeOption := WithDataScope(dExt.GetAbsScope(action.Scope[0], action.Scope[1], action.Scope[2], action.Scope[3])) - - var point PointF - findApp := func(d *DriverExt) error { - var err error - point, err = d.GetTextXY(appName, scopeOption, indexOption) - return err - } - foundAppAction := func(d *DriverExt) error { - // click app to launch - return d.TapAbsXY(point.X, point.Y-25, identifierOption) - } - - // go to home screen - if err := dExt.Driver.Homescreen(); err != nil { - return errors.Wrap(err, "go to home screen failed") - } - - // swipe to first screen - for i := 0; i < 5; i++ { - dExt.SwipeRight() - } - - // default to retry 5 times - if action.MaxRetryTimes == 0 { - action.MaxRetryTimes = 5 - } - // swipe next screen until app found - return dExt.SwipeUntil("left", findApp, foundAppAction, action.MaxRetryTimes, action.WaitTime) + return dExt.swipeToTapApp(appName, action) } return fmt.Errorf("invalid %s params, should be app name(string), got %v", ACTION_SwipeToTapApp, action.Params) case ACTION_SwipeToTapText: + // TODO: merge to LoopUntil if text, ok := action.Params.(string); ok { if len(action.Scope) != 4 { action.Scope = []float64{0, 0, 1, 1} @@ -432,10 +399,20 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { identifierOption := WithDataIdentifier(action.Identifier) indexOption := WithDataIndex(action.Index) - scopeOption := WithDataScope(dExt.GetAbsScope(action.Scope[0], action.Scope[1], action.Scope[2], action.Scope[3])) + scopeOption := WithDataScope(dExt.getAbsScope(action.Scope[0], action.Scope[1], action.Scope[2], action.Scope[3])) + + // default to retry 10 times + if action.MaxRetryTimes == 0 { + action.MaxRetryTimes = 10 + } + maxRetryOption := WithDataMaxRetryTimes(action.MaxRetryTimes) + waitTimeOption := WithDataWaitTime(action.WaitTime) var point PointF - findText := func(d *DriverExt) error { + // findTextAction := func(d *DriverExt) error { + // return nil + // } + findTextCondition := func(d *DriverExt) error { var err error point, err = d.GetTextXY(text, indexOption, scopeOption) return err @@ -445,20 +422,16 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { return d.TapAbsXY(point.X, point.Y, identifierOption) } - // default to retry 10 times - if action.MaxRetryTimes == 0 { - action.MaxRetryTimes = 10 - } - if action.Direction != nil { - return dExt.SwipeUntil(action.Direction, findText, foundTextAction, action.MaxRetryTimes, action.WaitTime) + return dExt.SwipeUntil(action.Direction, findTextCondition, foundTextAction, maxRetryOption, waitTimeOption) } // swipe until found - return dExt.SwipeUntil("up", findText, foundTextAction, action.MaxRetryTimes, action.WaitTime) + return dExt.SwipeUntil("up", findTextCondition, foundTextAction, maxRetryOption, waitTimeOption) } return fmt.Errorf("invalid %s params, should be app text(string), got %v", ACTION_SwipeToTapText, action.Params) case ACTION_SwipeToTapTexts: + // TODO: merge to LoopUntil if texts, ok := action.Params.([]interface{}); ok { var textList []string for _, t := range texts { @@ -471,10 +444,16 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { action.Scope = []float64{0, 0, 1, 1} } - scopeOption := WithDataScope(dExt.GetAbsScope(action.Scope[0], action.Scope[1], action.Scope[2], action.Scope[3])) + scopeOption := WithDataScope(dExt.getAbsScope(action.Scope[0], action.Scope[1], action.Scope[2], action.Scope[3])) + // default to retry 10 times + if action.MaxRetryTimes == 0 { + action.MaxRetryTimes = 10 + } + maxRetryOption := WithDataMaxRetryTimes(action.MaxRetryTimes) + waitTimeOption := WithDataWaitTime(action.WaitTime) var point PointF - findText := func(d *DriverExt) error { + findTexts := func(d *DriverExt) error { var err error points, err := d.GetTextXYs(texts, scopeOption) if err != nil { @@ -498,10 +477,10 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { } if action.Direction != nil { - return dExt.SwipeUntil(action.Direction, findText, foundTextAction, action.MaxRetryTimes, action.WaitTime) + return dExt.SwipeUntil(action.Direction, findTexts, foundTextAction, maxRetryOption, waitTimeOption) } // swipe until found - return dExt.SwipeUntil("up", findText, foundTextAction, action.MaxRetryTimes, action.WaitTime) + return dExt.SwipeUntil("up", findTexts, foundTextAction, maxRetryOption, waitTimeOption) } return fmt.Errorf("invalid %s params, should be app text([]string), got %v", ACTION_SwipeToTapText, action.Params) @@ -553,7 +532,7 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { } indexOption := WithDataIndex(action.Index) - scopeOption := WithDataScope(dExt.GetAbsScope(action.Scope[0], action.Scope[1], action.Scope[2], action.Scope[3])) + scopeOption := WithDataScope(dExt.getAbsScope(action.Scope[0], action.Scope[1], action.Scope[2], action.Scope[3])) identifierOption := WithDataIdentifier(action.Identifier) IgnoreNotFoundErrorOption := WithDataIgnoreNotFoundError(action.IgnoreNotFoundError) return dExt.TapByOCR(ocrText, identifierOption, IgnoreNotFoundErrorOption, indexOption, scopeOption) @@ -648,7 +627,7 @@ func (dExt *DriverExt) DoAction(action MobileAction) error { return nil } -func (dExt *DriverExt) GetAbsScope(x1, y1, x2, y2 float64) (int, int, int, int) { +func (dExt *DriverExt) getAbsScope(x1, y1, x2, y2 float64) (int, int, int, int) { return int(x1 * float64(dExt.windowSize.Width) * dExt.scale), int(y1 * float64(dExt.windowSize.Height) * dExt.scale), int(x2 * float64(dExt.windowSize.Width) * dExt.scale), diff --git a/hrp/pkg/uixt/interface.go b/hrp/pkg/uixt/interface.go index f922ee00..547197a4 100644 --- a/hrp/pkg/uixt/interface.go +++ b/hrp/pkg/uixt/interface.go @@ -774,6 +774,15 @@ type Rect struct { Size } +type DataOptions struct { + Data map[string]interface{} // configurations used by ios/android driver + Scope []int // used by ocr to get text position in the scope + Index int // index of the target element, should start from 1 + IgnoreNotFoundError bool // ignore error if target element not found + MaxRetryTimes int // max retry times if target element not found + Interval float64 // interval between retries in seconds +} + type DataOption func(data *DataOptions) func WithCustomOption(key string, value interface{}) DataOption { @@ -830,42 +839,50 @@ func WithDataIgnoreNotFoundError(ignoreError bool) DataOption { } } -type DataOptions struct { - Data map[string]interface{} // configurations used by ios/android driver - Scope []int // used by ocr to get text position in the scope - Index int // index of the target element, should start from 1 - IgnoreNotFoundError bool // ignore error if target element not found +func WithDataMaxRetryTimes(maxRetryTimes int) DataOption { + return func(data *DataOptions) { + data.MaxRetryTimes = maxRetryTimes + } } -func NewData(d map[string]interface{}, options ...DataOption) *DataOptions { - data := &DataOptions{ - Data: d, +func WithDataWaitTime(sec float64) DataOption { + return func(data *DataOptions) { + data.Interval = sec + } +} + +func NewData(data map[string]interface{}, options ...DataOption) *DataOptions { + if data == nil { + data = make(map[string]interface{}) + } + dataOptions := &DataOptions{ + Data: data, } for _, option := range options { - option(data) + option(dataOptions) } - if len(data.Scope) == 0 { - data.Scope = []int{0, 0, math.MaxInt64, math.MaxInt64} // default scope + if len(dataOptions.Scope) == 0 { + dataOptions.Scope = []int{0, 0, math.MaxInt64, math.MaxInt64} // default scope } - if _, ok := data.Data["steps"]; !ok { - data.Data["steps"] = 12 // default steps + if _, ok := dataOptions.Data["steps"]; !ok { + dataOptions.Data["steps"] = 12 // default steps } - if _, ok := data.Data["duration"]; !ok { - data.Data["duration"] = 1.0 // default duration + if _, ok := dataOptions.Data["duration"]; !ok { + dataOptions.Data["duration"] = 1.0 // default duration } - if _, ok := data.Data["frequency"]; !ok { - data.Data["frequency"] = 60 // default frequency + if _, ok := dataOptions.Data["frequency"]; !ok { + dataOptions.Data["frequency"] = 60 // default frequency } - if _, ok := data.Data["isReplace"]; !ok { - data.Data["isReplace"] = true // default true + if _, ok := dataOptions.Data["isReplace"]; !ok { + dataOptions.Data["isReplace"] = true // default true } - return data + return dataOptions } // current implemeted device: IOSDevice, AndroidDevice diff --git a/hrp/pkg/uixt/swipe.go b/hrp/pkg/uixt/swipe.go index b52bd770..bd1aedf9 100644 --- a/hrp/pkg/uixt/swipe.go +++ b/hrp/pkg/uixt/swipe.go @@ -4,8 +4,10 @@ import ( "fmt" "time" - "github.com/httprunner/httprunner/v4/hrp/internal/builtin" + "github.com/pkg/errors" "github.com/rs/zerolog/log" + + "github.com/httprunner/httprunner/v4/hrp/internal/builtin" ) func assertRelative(p float64) bool { @@ -61,17 +63,19 @@ func (dExt *DriverExt) SwipeRight(options ...DataOption) (err error) { return dExt.SwipeRelative(0.5, 0.5, 0.9, 0.5, options...) } -// FindCondition indicates the condition to find a UI element -type FindCondition func(driver *DriverExt) error +type Action func(driver *DriverExt) error -// FoundAction indicates the action to do after a UI element is found -type FoundAction func(driver *DriverExt) error +// findCondition indicates the condition to find a UI element +// foundAction indicates the action to do after a UI element is found +func (dExt *DriverExt) SwipeUntil(direction interface{}, findCondition Action, foundAction Action, options ...DataOption) error { + d := NewData(nil, options...) + maxRetryTimes := d.MaxRetryTimes + interval := d.Interval -func (dExt *DriverExt) SwipeUntil(direction interface{}, condition FindCondition, action FoundAction, maxTimes int, waitTime float64) error { - for i := 0; i < maxTimes; i++ { - if err := condition(dExt); err == nil { + for i := 0; i < maxRetryTimes; i++ { + if err := findCondition(dExt); err == nil { // do action after found - return action(dExt) + return foundAction(dExt) } if d, ok := direction.(string); ok { if err := dExt.SwipeTo(d); err != nil { @@ -91,7 +95,72 @@ func (dExt *DriverExt) SwipeUntil(direction interface{}, condition FindCondition } } // wait for swipe action to completed and content to load completely - time.Sleep(time.Duration(1000*waitTime) * time.Millisecond) + time.Sleep(time.Duration(1000*interval) * time.Millisecond) } - return fmt.Errorf("swipe %s %d times, match condition failed", direction, maxTimes) + return fmt.Errorf("swipe %s %d times, match condition failed", direction, maxRetryTimes) +} + +func (dExt *DriverExt) LoopUntil(findAction, findCondition, foundAction Action, options ...DataOption) error { + d := NewData(nil, options...) + maxRetryTimes := d.MaxRetryTimes + interval := d.Interval + + for i := 0; i < maxRetryTimes; i++ { + if err := findCondition(dExt); err == nil { + // do action after found + return foundAction(dExt) + } + + if err := findAction(dExt); err != nil { + log.Error().Err(err).Msgf("find action failed") + } + + // wait interval between each findAction + time.Sleep(time.Duration(1000*interval) * time.Millisecond) + } + return fmt.Errorf("loop %d times, match find condition failed", maxRetryTimes) +} + +func (dExt *DriverExt) swipeToTapApp(appName string, action MobileAction) error { + if len(action.Scope) != 4 { + action.Scope = []float64{0, 0, 1, 1} + } + + identifierOption := WithDataIdentifier(action.Identifier) + indexOption := WithDataIndex(action.Index) + scopeOption := WithDataScope(dExt.getAbsScope(action.Scope[0], action.Scope[1], action.Scope[2], action.Scope[3])) + + // default to retry 5 times + if action.MaxRetryTimes == 0 { + action.MaxRetryTimes = 5 + } + maxRetryOption := WithDataMaxRetryTimes(action.MaxRetryTimes) + waitTimeOption := WithDataWaitTime(action.WaitTime) + + var point PointF + findAppAction := func(d *DriverExt) error { + return dExt.SwipeLeft() + } + findAppCondition := func(d *DriverExt) error { + var err error + point, err = d.GetTextXY(appName, scopeOption, indexOption) + return err + } + foundAppAction := func(d *DriverExt) error { + // click app to launch + return d.TapAbsXY(point.X, point.Y-25, identifierOption) + } + + // go to home screen + if err := dExt.Driver.Homescreen(); err != nil { + return errors.Wrap(err, "go to home screen failed") + } + + // swipe to first screen + for i := 0; i < 5; i++ { + dExt.SwipeRight() + } + + // swipe next screen until app found + return dExt.LoopUntil(findAppAction, findAppCondition, foundAppAction, maxRetryOption, waitTimeOption) } diff --git a/hrp/pkg/uixt/swipe_test.go b/hrp/pkg/uixt/swipe_test.go index 83502a8f..22950c44 100644 --- a/hrp/pkg/uixt/swipe_test.go +++ b/hrp/pkg/uixt/swipe_test.go @@ -18,7 +18,7 @@ func TestSwipeUntil(t *testing.T) { } foundAppAction := func(d *DriverExt) error { // click app, launch douyin - return d.TapAbsXY(point.X, point.Y, "") + return d.TapAbsXY(point.X, point.Y) } driverExt.Driver.Homescreen() @@ -29,7 +29,7 @@ func TestSwipeUntil(t *testing.T) { } // swipe until app found - err = driverExt.SwipeUntil("left", findApp, foundAppAction, 10) + err = driverExt.SwipeUntil("left", findApp, foundAppAction, WithDataMaxRetryTimes(10)) checkErr(t, err) findLive := func(d *DriverExt) error { @@ -39,10 +39,10 @@ func TestSwipeUntil(t *testing.T) { } foundLiveAction := func(d *DriverExt) error { // enter live room - return d.TapAbsXY(point.X, point.Y, "") + return d.TapAbsXY(point.X, point.Y) } // swipe until live room found - err = driverExt.SwipeUntil("up", findLive, foundLiveAction, 20) + err = driverExt.SwipeUntil("up", findLive, foundLiveAction, WithDataMaxRetryTimes(20)) checkErr(t, err) } diff --git a/hrp/pkg/uixt/tap_test.go b/hrp/pkg/uixt/tap_test.go index b1bf9ee9..c5365998 100644 --- a/hrp/pkg/uixt/tap_test.go +++ b/hrp/pkg/uixt/tap_test.go @@ -29,7 +29,7 @@ func TestDriverExt_TapXY(t *testing.T) { driverExt, err := iosDevice.NewDriver(nil) checkErr(t, err) - err = driverExt.TapXY(0.4, 0.5, "") + err = driverExt.TapXY(0.4, 0.5) checkErr(t, err) } @@ -37,7 +37,7 @@ func TestDriverExt_TapAbsXY(t *testing.T) { driverExt, err := iosDevice.NewDriver(nil) checkErr(t, err) - err = driverExt.TapAbsXY(100, 300, "") + err = driverExt.TapAbsXY(100, 300) checkErr(t, err) } @@ -46,6 +46,6 @@ func TestDriverExt_TapWithOCR(t *testing.T) { checkErr(t, err) // 需要点击文字上方的图标 - err = driverExt.TapOffset("抖音", 0.5, -1, "", false) + err = driverExt.TapOffset("抖音", 0.5, -1) checkErr(t, err) }