From c768c189e436643c62abd0ff3d14a68384d22a94 Mon Sep 17 00:00:00 2001 From: debugtalk Date: Wed, 31 Aug 2022 21:21:55 +0800 Subject: [PATCH] feat: SwipeToTapApp, SwipeToTapText --- examples/uitest/demo_weixin_test.go | 45 ++++++++++++++++ hrp/step.go | 11 ++++ hrp/step_ios_ui.go | 84 ++++++++++++++++++++++++++++- 3 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 examples/uitest/demo_weixin_test.go diff --git a/examples/uitest/demo_weixin_test.go b/examples/uitest/demo_weixin_test.go new file mode 100644 index 00000000..0dce08a4 --- /dev/null +++ b/examples/uitest/demo_weixin_test.go @@ -0,0 +1,45 @@ +package uitest + +import ( + "fmt" + "testing" + + "github.com/httprunner/httprunner/v4/hrp" +) + +func TestIOSWeixinLive(t *testing.T) { + testCase := &hrp.TestCase{ + Config: hrp.NewConfig("通过 feed 卡片进入微信直播间"), + TestSteps: []hrp.IStep{ + hrp.NewStep("启动微信"). + IOS(). + Home(). + AppTerminate("com.tencent.xin"). // 关闭已运行的微信,确保启动微信后在「微信」首页 + SwipeToTapApp("微信", hrp.WithMaxRetryTimes(5)). + Validate(). + AssertLabelExists("通讯录", "微信启动失败,「通讯录」不存在"), + hrp.NewStep("进入直播页"). + IOS(). + Tap("发现"). // 进入「发现页」 + TapByOCR("视频号"). // 通过 OCR 识别「视频号」 + Validate(). + AssertLabelExists("视频号"), + hrp.NewStep("处理青少年弹窗"). + IOS(). + TapByOCR("我知道了", hrp.WithIgnoreNotFoundError(false)), + hrp.NewStep("在推荐页上划,直到出现「轻触进入直播间」"). + IOS(). + SwipeToTapText("轻触进入直播间", hrp.WithMaxRetryTimes(10)), + hrp.NewStep("向上滑动,等待 60s"). + IOS(). + SwipeUp().Sleep(60).ScreenShot(). // 上划 1 次,等待 60s,截图保存 + SwipeUp().Times(60).ScreenShot(), // 再上划 1 次,等待 60s,截图保存 + }, + } + fmt.Println(testCase) + + err := hrp.NewRunner(t).Run(testCase) + if err != nil { + t.Fatal(err) + } +} diff --git a/hrp/step.go b/hrp/step.go index 8e4715b7..34cdb879 100644 --- a/hrp/step.go +++ b/hrp/step.go @@ -49,18 +49,29 @@ const ( uiSelectorImage string = "ui_image" assertionExists string = "exists" assertionNotExists string = "not_exists" + + // custom actions + swipeToTapApp MobileMethod = "swipe_to_tap_app" // swipe left & right to find app and tap + swipeToTapText MobileMethod = "swipe_to_tap_text" // swipe up & down to find text and tap ) type MobileAction struct { Method MobileMethod `json:"method" yaml:"method"` Params interface{} `json:"params,omitempty" yaml:"params,omitempty"` + maxRetryTimes int // max retry times timeout int // TODO: wait timeout in seconds for mobile action ignoreNotFoundError bool // ignore error if target element not found } type ActionOption func(o *MobileAction) +func WithMaxRetryTimes(maxRetryTimes int) ActionOption { + return func(o *MobileAction) { + o.maxRetryTimes = maxRetryTimes + } +} + func WithTimeout(timeout int) ActionOption { return func(o *MobileAction) { o.timeout = timeout diff --git a/hrp/step_ios_ui.go b/hrp/step_ios_ui.go index 23bcf153..6b83ceb4 100644 --- a/hrp/step_ios_ui.go +++ b/hrp/step_ios_ui.go @@ -187,6 +187,38 @@ func (s *StepIOS) SwipeRight() *StepIOS { return &StepIOS{step: s.step} } +func (s *StepIOS) SwipeToTapApp(appName string, options ...ActionOption) *StepIOS { + action := MobileAction{ + Method: swipeToTapApp, + Params: appName, + } + for _, option := range options { + option(&action) + } + // default to retry 5 times + if action.maxRetryTimes == 0 { + action.maxRetryTimes = 5 + } + s.step.IOS.Actions = append(s.step.IOS.Actions, action) + return &StepIOS{step: s.step} +} + +func (s *StepIOS) SwipeToTapText(text string, options ...ActionOption) *StepIOS { + action := MobileAction{ + Method: swipeToTapText, + Params: text, + } + for _, option := range options { + option(&action) + } + // default to retry 10 times + if action.maxRetryTimes == 0 { + action.maxRetryTimes = 10 + } + s.step.IOS.Actions = append(s.step.IOS.Actions, action) + return &StepIOS{step: s.step} +} + func (s *StepIOS) Input(text string) *StepIOS { s.step.IOS.Actions = append(s.step.IOS.Actions, MobileAction{ Method: uiInput, @@ -519,12 +551,60 @@ func (ud *uiDriver) doAction(action MobileAction) error { if bundleId, ok := action.Params.(string); ok { return ud.AppLaunch(bundleId) } - return fmt.Errorf("app_launch params should be bundleId(string), got %v", action.Params) + return fmt.Errorf("invalid %s params, should be bundleId(string), got %v", + appLaunch, action.Params) case appLaunchUnattached: if bundleId, ok := action.Params.(string); ok { return ud.AppLaunchUnattached(bundleId) } - return fmt.Errorf("app_launch_unattached params should be bundleId(string), got %v", action.Params) + return fmt.Errorf("invalid %s params, should be bundleId(string), got %v", + appLaunchUnattached, action.Params) + case swipeToTapApp: + if appName, ok := action.Params.(string); ok { + var x, y, width, height float64 + findApp := func(d *uixt.DriverExt) error { + var err error + x, y, width, height, err = d.FindTextByOCR(appName) + return err + } + foundAppAction := func(d *uixt.DriverExt) error { + // click app to launch + return d.TapFloat(x+width*0.5, y+height*0.5-20) + } + + // go to home screen + if err := ud.WebDriver.Homescreen(); err != nil { + return errors.Wrap(err, "go to home screen failed") + } + + // swipe to first screen + for i := 0; i < 5; i++ { + ud.SwipeTo("right") + } + + // swipe next screen until app found + return ud.SwipeUntil("left", findApp, foundAppAction, action.maxRetryTimes) + } + return fmt.Errorf("invalid %s params, should be app name(string), got %v", + swipeToTapApp, action.Params) + case swipeToTapText: + if text, ok := action.Params.(string); ok { + var x, y, width, height float64 + findText := func(d *uixt.DriverExt) error { + var err error + x, y, width, height, err = d.FindTextByOCR(text) + return err + } + foundTextAction := func(d *uixt.DriverExt) error { + // tap text + return d.TapFloat(x+width*0.5, y+height*0.5) + } + + // swipe until live room found + return ud.SwipeUntil("up", findText, foundTextAction, 20) + } + return fmt.Errorf("invalid %s params, should be app text(string), got %v", + swipeToTapText, action.Params) case appTerminate: if bundleId, ok := action.Params.(string); ok { success, err := ud.AppTerminate(bundleId)