Merge pull request #1587 from httprunner/fix-android-ui

feat: add validator AssertAppInForeground and AssertAppNotInForeground
feat: save screenshots of all steps including ocr and cv recognition process data
This commit is contained in:
debugtalk
2023-04-18 20:33:12 +08:00
committed by GitHub
30 changed files with 393 additions and 165 deletions

View File

@@ -1,6 +1,6 @@
# Release History
## v4.3.3 (2023-04-14)
## v4.3.3 (2023-04-18)
**go version**
@@ -9,11 +9,13 @@
- feat: add adb `screencap` sub command
- feat: add `IsAppInForeground` to check if the given package is in foreground
- feat: check if app is in foreground when step failed
- feat: add validator AssertAppInForeground and AssertAppNotInForeground
- feat: save screenshots of all steps including ocr and cv recognition process data
- fix: adb driver for TapFloat
- fix: stop logcat only when enabled
- fix: do not fail case when kill logcat error
- fix: take screenshot after each step
- fix: screencap compatibility for shell v1 and v2
- fix: screencap compatibility for shell v1 and v2 protocol
## v4.3.2 (2022-12-26)

View File

@@ -1,6 +1,6 @@
config:
name: basic test with httpbin
base_url: http://httpbin.org/
base_url: https://httpbin.org/
teststeps:
-

View File

@@ -5,7 +5,7 @@ from httprunner import HttpRunner, Config, Step, RunRequest
class TestCaseValidate(HttpRunner):
config = Config("basic test with httpbin").base_url("http://httpbin.org/")
config = Config("basic test with httpbin").base_url("https://httpbin.org/")
teststeps = [
Step(

View File

@@ -1,44 +0,0 @@
//go:build localtest
package uitest
import (
"testing"
"github.com/httprunner/httprunner/v4/hrp"
"github.com/httprunner/httprunner/v4/hrp/pkg/uixt"
)
func TestAndroidDouYinLive(t *testing.T) {
testCase := &hrp.TestCase{
Config: hrp.NewConfig("通过 feed 头像进入抖音直播间").
SetAndroid(uixt.WithUIA2(false), uixt.WithAdbLogOn(true)),
TestSteps: []hrp.IStep{
hrp.NewStep("启动抖音").
Android().
Home().
AppTerminate("com.ss.android.ugc.aweme"). // 关闭已运行的抖音,确保启动抖音后在「抖音」首页
SwipeToTapApp("抖音", uixt.WithMaxRetryTimes(5)).
Sleep(10),
hrp.NewStep("处理青少年弹窗").
Android().
Tap("推荐").
TapByOCR("我知道了", uixt.WithIgnoreNotFoundError(true)).
Validate().
AssertOCRExists("首页", "抖音启动失败,「首页」不存在"),
hrp.NewStep("在推荐页上划,直到出现 feed 头像「直播」").
Android().
SwipeToTapText("直播", uixt.WithMaxRetryTimes(10), uixt.WithIdentifier("进入直播间")),
hrp.NewStep("向上滑动,等待 10s").
Android().
SwipeUp(uixt.WithIdentifier("第一次上划")).Sleep(10).ScreenShot(). // 上划 1 次,等待 10s截图保存
SwipeUp(uixt.WithIdentifier("第二次上划")).Sleep(10).ScreenShot(), // 再上划 1 次,等待 10s截图保存
},
}
runner := hrp.NewRunner(t).SetSaveTests(true)
err := runner.Run(testCase)
if err != nil {
t.Fatal(err)
}
}

View File

@@ -28,7 +28,15 @@
"params": 10
}
]
}
},
"validate": [
{
"check": "ui_foreground_app",
"assert": "equal",
"expect": "com.ss.android.ugc.aweme",
"msg": "app [com.ss.android.ugc.aweme] should be in foreground"
}
]
},
{
"name": "处理青少年弹窗",
@@ -102,6 +110,25 @@
]
},
"loops": 10
},
{
"name": "exit",
"android": {
"actions": [
{
"method": "app_terminate",
"params": "com.ss.android.ugc.aweme"
}
]
},
"validate": [
{
"check": "ui_foreground_app",
"assert": "not_equal",
"expect": "com.ss.android.ugc.aweme",
"msg": "app [com.ss.android.ugc.aweme] should not be in foreground"
}
]
}
]
}

View File

@@ -21,7 +21,9 @@ func TestAndroidDouyinFeedTest(t *testing.T) {
Android().
AppTerminate("com.ss.android.ugc.aweme").
AppLaunch("com.ss.android.ugc.aweme").
Sleep(10),
Sleep(10).
Validate().
AssertAppInForeground("com.ss.android.ugc.aweme"),
hrp.NewStep("处理青少年弹窗").
Android().
TapByOCR("我知道了", uixt.WithIgnoreNotFoundError(true)),
@@ -40,10 +42,15 @@ func TestAndroidDouyinFeedTest(t *testing.T) {
Android().
SwipeUp().
SleepRandom(0, 5, 0.7, 5, 10, 0.3),
hrp.NewStep("exit").
Android().
AppTerminate("com.ss.android.ugc.aweme").
Validate().
AssertAppNotInForeground("com.ss.android.ugc.aweme"),
},
}
if err := testCase.Dump2JSON("demo_feed_random_slide.json"); err != nil {
if err := testCase.Dump2JSON("demo_android_feed_swipe.json"); err != nil {
t.Fatal(err)
}

View File

@@ -0,0 +1,140 @@
{
"config": {
"name": "点播_抖音_滑动场景_随机间隔_android",
"variables": {
"device": "${ENV(SerialNumber)}"
},
"android": [
{
"serial": "$device"
}
]
},
"teststeps": [
{
"name": "启动抖音",
"android": {
"actions": [
{
"method": "app_terminate",
"params": "com.ss.android.ugc.aweme"
},
{
"method": "app_launch",
"params": "com.ss.android.ugc.aweme"
},
{
"method": "sleep",
"params": 5
}
]
},
"validate": [
{
"check": "ui_foreground_app",
"assert": "equal",
"expect": "com.ss.android.ugc.aweme",
"msg": "app [com.ss.android.ugc.aweme] should be in foreground"
}
]
},
{
"name": "处理青少年弹窗",
"android": {
"actions": [
{
"method": "tap_ocr",
"params": "我知道了",
"ignore_NotFoundError": true
}
]
}
},
{
"name": "在推荐页上划,直到出现「点击进入直播间」",
"android": {
"actions": [
{
"method": "swipe_to_tap_text",
"params": "点击进入直播间",
"identifier": "进入直播间",
"max_retry_times": 10
}
]
}
},
{
"name": "滑动 Feed 5 次60% 随机间隔 0-5s40% 随机间隔 5-10s",
"android": {
"actions": [
{
"method": "swipe",
"params": "up"
},
{
"method": "sleep_random",
"params": [
0,
5,
0.6,
5,
10,
0.4
]
}
]
},
"loops": 5
},
{
"name": "向上滑动,等待 10s",
"android": {
"actions": [
{
"method": "swipe",
"params": "up",
"identifier": "第一次上划"
},
{
"method": "sleep",
"params": 10
},
{
"method": "screenshot"
},
{
"method": "swipe",
"params": "up",
"identifier": "第二次上划"
},
{
"method": "sleep",
"params": 10
},
{
"method": "screenshot"
}
]
}
},
{
"name": "exit",
"android": {
"actions": [
{
"method": "app_terminate",
"params": "com.ss.android.ugc.aweme"
}
]
},
"validate": [
{
"check": "ui_foreground_app",
"assert": "not_equal",
"expect": "com.ss.android.ugc.aweme",
"msg": "app [com.ss.android.ugc.aweme] should not be in foreground"
}
]
}
]
}

View File

@@ -0,0 +1,59 @@
//go:build localtest
package uitest
import (
"testing"
"github.com/httprunner/httprunner/v4/hrp"
"github.com/httprunner/httprunner/v4/hrp/pkg/uixt"
)
func TestAndroidLiveSwipeTest(t *testing.T) {
testCase := &hrp.TestCase{
Config: hrp.NewConfig("点播_抖音_滑动场景_随机间隔_android").
WithVariables(map[string]interface{}{
"device": "${ENV(SerialNumber)}",
}).
SetAndroid(uixt.WithSerialNumber("$device")),
TestSteps: []hrp.IStep{
hrp.NewStep("启动抖音").
Android().
AppTerminate("com.ss.android.ugc.aweme").
AppLaunch("com.ss.android.ugc.aweme").
Sleep(5).
Validate().
AssertAppInForeground("com.ss.android.ugc.aweme"),
hrp.NewStep("处理青少年弹窗").
Android().
TapByOCR("我知道了", uixt.WithIgnoreNotFoundError(true)),
hrp.NewStep("在推荐页上划,直到出现「点击进入直播间」").
Android().
SwipeToTapText("点击进入直播间", uixt.WithMaxRetryTimes(10), uixt.WithIdentifier("进入直播间")),
hrp.NewStep("滑动 Feed 5 次60% 随机间隔 0-5s40% 随机间隔 5-10s").
Loop(5).
Android().
SwipeUp().
SleepRandom(0, 5, 0.6, 5, 10, 0.4),
hrp.NewStep("向上滑动,等待 10s").
Android().
SwipeUp(uixt.WithIdentifier("第一次上划")).Sleep(10).ScreenShot(). // 上划 1 次,等待 10s截图保存
SwipeUp(uixt.WithIdentifier("第二次上划")).Sleep(10).ScreenShot(), // 再上划 1 次,等待 10s截图保存
hrp.NewStep("exit").
Android().
AppTerminate("com.ss.android.ugc.aweme").
Validate().
AssertAppNotInForeground("com.ss.android.ugc.aweme"),
},
}
if err := testCase.Dump2JSON("demo_android_live_swipe.json"); err != nil {
t.Fatal(err)
}
runner := hrp.NewRunner(t).SetSaveTests(true)
err := runner.Run(testCase)
if err != nil {
t.Fatal(err)
}
}

View File

@@ -45,10 +45,7 @@ func TestIOSDouyinLive(t *testing.T) {
},
}
if err := testCase.Dump2JSON("demo_douyin_live.json"); err != nil {
t.Fatal(err)
}
if err := testCase.Dump2YAML("demo_douyin_live.yaml"); err != nil {
if err := testCase.Dump2JSON("demo_ios_live_swipe.json"); err != nil {
t.Fatal(err)
}

View File

@@ -150,11 +150,7 @@ func NewWorldCupLive(device uixt.Device, matchName, bundleID string, duration, i
func (wc *WorldCupLive) getCurrentLiveTime(utcTime time.Time) error {
utcTimeStr := utcTime.Format("15:04:05")
fileName := filepath.Join(
wc.resultDir, "screenshot", utcTimeStr)
ocrTexts, err := wc.driver.GetTextsByOCR(
uixt.WithScreenShot(fileName),
)
ocrTexts, err := wc.driver.GetTextsByOCR()
if err != nil {
log.Error().Err(err).Msg("get ocr texts failed")
return err

6
go.mod
View File

@@ -22,7 +22,7 @@ require (
github.com/olekukonko/tablewriter v0.0.5
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.13.0
github.com/rs/zerolog v1.28.0
github.com/rs/zerolog v1.29.1
github.com/satori/go.uuid v1.2.0
github.com/shirou/gopsutil v3.21.11+incompatible
github.com/spf13/cobra v1.5.0
@@ -56,7 +56,7 @@ require (
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/mattn/go-isatty v0.0.18 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
@@ -75,7 +75,7 @@ require (
github.com/yusufpapurcu/wmi v1.2.2 // indirect
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
golang.org/x/sync v0.0.0-20220907140024-f12130a52804 // indirect
golang.org/x/sys v0.5.0 // indirect
golang.org/x/sys v0.7.0 // indirect
golang.org/x/text v0.7.0 // indirect
golang.org/x/tools v0.1.12 // indirect
google.golang.org/appengine v1.6.7 // indirect

14
go.sum
View File

@@ -89,7 +89,7 @@ github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWH
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
@@ -301,8 +301,9 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
@@ -370,8 +371,8 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+tmc=
github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY=
github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc=
github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
@@ -626,8 +627,9 @@ golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

View File

@@ -10,7 +10,7 @@ func TestBoomerStandaloneRun(t *testing.T) {
defer removeHashicorpGoPlugin()
testcase1 := &TestCase{
Config: NewConfig("TestCase1").SetBaseURL("http://httpbin.org"),
Config: NewConfig("TestCase1").SetBaseURL("https://httpbin.org"),
TestSteps: []IStep{
NewStep("headers").
GET("/headers").

View File

@@ -3,9 +3,10 @@ package adb
import (
"fmt"
"io/ioutil"
"time"
"github.com/spf13/cobra"
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
)
var screencapAndroidDevicesCmd = &cobra.Command{
@@ -22,7 +23,7 @@ var screencapAndroidDevicesCmd = &cobra.Command{
return err
}
filepath := fmt.Sprintf("screencap_%d.png", time.Now().Unix())
filepath := fmt.Sprintf("%s.png", builtin.GenNameWithTimestamp("screencap_%d"))
if err = ioutil.WriteFile(filepath, res, 0o644); err != nil {
return err
}

View File

@@ -443,3 +443,10 @@ func Sign(ver string, ak string, sk string, body []byte) string {
signResult := sha256HMAC(signKey, body)
return fmt.Sprintf("%v/%v", signKeyInfo, string(signResult))
}
func GenNameWithTimestamp(tmpl string) string {
if !strings.Contains(tmpl, "%d") {
tmpl = tmpl + "_%d"
}
return fmt.Sprintf(tmpl, time.Now().Unix())
}

View File

@@ -124,8 +124,9 @@ var errorsMap = map[error]int{
AndroidCaptureLogError: 66,
// UI automation related
MobileUIDriverError: 70,
MobileUIValidationError: 75,
MobileUIDriverError: 70,
MobileUIValidationError: 75,
MobileUIAppNotInForegroundError: 76,
// OCR related
OCREnvMissedError: 80,

View File

@@ -1 +1 @@
v4.3.3.2304142356
v4.3.3.2304181958

View File

@@ -24,7 +24,7 @@ func TestLoadCurlCase(t *testing.T) {
if !assert.EqualValues(t, "GET", tCase.TestSteps[0].Request.Method) {
t.Fatal()
}
if !assert.Equal(t, "http://httpbin.org", tCase.TestSteps[0].Request.URL) {
if !assert.Equal(t, "https://httpbin.org", tCase.TestSteps[0].Request.URL) {
t.Fatal()
}

View File

@@ -11,6 +11,8 @@ import (
"time"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
)
type DeviceFileInfo struct {
@@ -538,7 +540,7 @@ func (d *Device) InstallAPK(apk io.ReadSeeker) (string, error) {
return string(raw), err
}
remote := fmt.Sprintf("/data/local/tmp/gadb_remote_%d.apk", time.Now().Unix())
remote := fmt.Sprintf("/data/local/tmp/%s.apk", builtin.GenNameWithTimestamp("gadb_remote_%d"))
err := d.Push(apk, remote, time.Now())
if err != nil {
return "", fmt.Errorf("error pushing: %v", err)

View File

@@ -368,6 +368,7 @@ func (ad *adbDriver) IsAppInForeground(packageName string) (bool, error) {
// adb shell dumpsys activity activities | grep mResumedActivity
output, err := ad.adbClient.RunShellCommand("dumpsys", "activity", "activities")
if err != nil {
log.Error().Err(err).Msg("failed to dumpsys activities")
return false, err
}

View File

@@ -41,10 +41,15 @@ const (
RecordStop MobileMethod = "record_stop"
// UI validation
SelectorName string = "ui_name"
SelectorLabel string = "ui_label"
SelectorOCR string = "ui_ocr"
SelectorImage string = "ui_image"
// selectors
SelectorName string = "ui_name"
SelectorLabel string = "ui_label"
SelectorOCR string = "ui_ocr"
SelectorImage string = "ui_image"
SelectorForegroundApp string = "ui_foreground_app"
// assertions
AssertionEqual string = "equal"
AssertionNotEqual string = "not_equal"
AssertionExists string = "exists"
AssertionNotExists string = "not_exists"
@@ -212,7 +217,7 @@ type DriverExt struct {
doneMjpegStream chan bool
scale float64
ocrService OCRService // used to get text from image
ScreenShots []string // save screenshots path
screenShots []string // cache screenshot paths
CVArgs
}
@@ -248,7 +253,9 @@ func NewDriverExt(device Device, driver WebDriver) (dExt *DriverExt, err error)
return dExt, nil
}
func (dExt *DriverExt) takeScreenShot() (raw *bytes.Buffer, err error) {
// TakeScreenShot takes screenshot and saves image file to $CWD/screenshots/ folder
// if fileName is empty, it will not save image file and only return raw image data
func (dExt *DriverExt) TakeScreenShot(fileName ...string) (raw *bytes.Buffer, err error) {
// wait for action done
time.Sleep(500 * time.Millisecond)
@@ -258,15 +265,34 @@ func (dExt *DriverExt) takeScreenShot() (raw *bytes.Buffer, err error) {
return dExt.frame, nil
}
if raw, err = dExt.Driver.Screenshot(); err != nil {
log.Error().Err(err).Msg("takeScreenShot failed")
log.Error().Err(err).Msg("capture screenshot data failed")
return nil, err
}
// save screenshot to file
if len(fileName) > 0 && fileName[0] != "" {
path := filepath.Join(env.ScreenShotsPath, fileName[0])
path, err := dExt.saveScreenShot(raw, path)
if err != nil {
log.Error().Err(err).Msg("save screenshot file failed")
return nil, err
}
dExt.screenShots = append(dExt.screenShots, path)
log.Info().Str("path", path).Msg("save screenshot file success")
}
return raw, nil
}
// saveScreenShot saves image file with file name
func saveScreenShot(raw *bytes.Buffer, fileName string) (string, error) {
img, format, err := image.Decode(raw)
func (dExt *DriverExt) saveScreenShot(raw *bytes.Buffer, fileName string) (string, error) {
// notice: screenshot data is a stream, so we need to copy it to a new buffer
copiedBuffer := &bytes.Buffer{}
if _, err := copiedBuffer.Write(raw.Bytes()); err != nil {
log.Error().Err(err).Msg("copy screenshot buffer failed")
}
img, format, err := image.Decode(copiedBuffer)
if err != nil {
return "", errors.Wrap(err, "decode screenshot image failed")
}
@@ -297,19 +323,11 @@ func saveScreenShot(raw *bytes.Buffer, fileName string) (string, error) {
return screenshotPath, nil
}
// ScreenShot takes screenshot and saves image file to $CWD/screenshots/ folder
func (dExt *DriverExt) ScreenShot(fileName string) (string, error) {
raw, err := dExt.takeScreenShot()
if err != nil {
return "", errors.Wrap(err, "screenshot failed")
}
fileName = filepath.Join(env.ScreenShotsPath, fileName)
path, err := saveScreenShot(raw, fileName)
if err != nil {
return "", errors.Wrap(err, "save screenshot failed")
}
return path, nil
func (dExt *DriverExt) GetScreenShots() []string {
defer func() {
dExt.screenShots = nil
}()
return dExt.screenShots
}
// isPathExists returns true if path exists, whether path is file or dir
@@ -349,6 +367,16 @@ func (dExt *DriverExt) IsImageExist(text string) bool {
return err == nil
}
func (dExt *DriverExt) IsAppInForeground(packageName string) bool {
// check if app is in foreground
yes, err := dExt.Driver.IsAppInForeground(packageName)
if !yes || err != nil {
log.Info().Str("packageName", packageName).Msg("app is not in foreground")
return false
}
return true
}
var errActionNotImplemented = errors.New("UI action not implemented")
func convertToFloat64(val interface{}) (float64, error) {
@@ -674,15 +702,9 @@ func (dExt *DriverExt) DoAction(action MobileAction) error {
}
}
case CtlScreenShot:
// take snapshot
log.Info().Msg("take snapshot for current screen")
screenshotPath, err := dExt.ScreenShot(fmt.Sprintf("screenshot_%d",
time.Now().Unix()))
if err != nil {
return errors.Wrap(err, "take screenshot failed")
}
log.Info().Str("path", screenshotPath).Msg("take screenshot")
dExt.ScreenShots = append(dExt.ScreenShots, screenshotPath)
// take screenshot
log.Info().Msg("take screenshot for current screen")
_, err := dExt.TakeScreenShot(builtin.GenNameWithTimestamp("screenshot_%d"))
return err
case CtlStartCamera:
return dExt.Driver.StartCamera()
@@ -700,18 +722,20 @@ func (dExt *DriverExt) getAbsScope(x1, y1, x2, y2 float64) (int, int, int, int)
}
func (dExt *DriverExt) DoValidation(check, assert, expected string, message ...string) bool {
var exists bool
if assert == AssertionExists {
exists = true
var exp bool
if assert == AssertionExists || assert == AssertionEqual {
exp = true
} else {
exists = false
exp = false
}
var result bool
switch check {
case SelectorOCR:
result = (dExt.IsOCRExist(expected) == exists)
result = (dExt.IsOCRExist(expected) == exp)
case SelectorImage:
result = (dExt.IsImageExist(expected) == exists)
result = (dExt.IsImageExist(expected) == exp)
case SelectorForegroundApp:
result = (dExt.IsAppInForeground(expected) == exp)
}
if !result {

View File

@@ -2,7 +2,6 @@ package uixt
import (
"bytes"
"fmt"
"math"
"strings"
"time"
@@ -437,7 +436,6 @@ type DataOptions struct {
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
ScreenShotFilename string // turn on screenshot and specify file name
}
type DataOption func(data *DataOptions)
@@ -514,16 +512,6 @@ func WithDataWaitTime(sec float64) DataOption {
}
}
func WithScreenShot(fileName ...string) DataOption {
return func(data *DataOptions) {
if len(fileName) > 0 {
data.ScreenShotFilename = fileName[0]
} else {
data.ScreenShotFilename = fmt.Sprintf("screenshot_%d", time.Now().Unix())
}
}
}
func NewDataOptions(options ...DataOption) *DataOptions {
dataOptions := &DataOptions{
Data: make(map[string]interface{}),

View File

@@ -175,14 +175,6 @@ func (s *veDEMOCRService) GetTexts(imageBuf *bytes.Buffer, options ...DataOption
dataOptions := NewDataOptions(options...)
if dataOptions.ScreenShotFilename != "" {
path, err := saveScreenShot(imageBuf, dataOptions.ScreenShotFilename)
if err != nil {
return nil, errors.Wrap(err, "save screenshot failed")
}
log.Debug().Str("path", path).Msg("save screenshot")
}
for _, ocrResult := range ocrResults {
rect := image.Rectangle{
// ocrResult.Points 顺序:左上 -> 右上 -> 右下 -> 左下
@@ -313,8 +305,7 @@ type OCRService interface {
func (dExt *DriverExt) GetTextsByOCR(options ...DataOption) (texts OCRTexts, err error) {
var bufSource *bytes.Buffer
if bufSource, err = dExt.takeScreenShot(); err != nil {
err = fmt.Errorf("takeScreenShot error: %v", err)
if bufSource, err = dExt.TakeScreenShot(builtin.GenNameWithTimestamp("step_%d_ocr")); err != nil {
return
}
@@ -329,8 +320,7 @@ func (dExt *DriverExt) GetTextsByOCR(options ...DataOption) (texts OCRTexts, err
func (dExt *DriverExt) FindTextByOCR(ocrText string, options ...DataOption) (x, y, width, height float64, err error) {
var bufSource *bytes.Buffer
if bufSource, err = dExt.takeScreenShot(); err != nil {
err = fmt.Errorf("takeScreenShot error: %v", err)
if bufSource, err = dExt.TakeScreenShot(builtin.GenNameWithTimestamp("step_%d_ocr")); err != nil {
return
}
@@ -348,8 +338,7 @@ func (dExt *DriverExt) FindTextByOCR(ocrText string, options ...DataOption) (x,
func (dExt *DriverExt) FindTextsByOCR(ocrTexts []string, options ...DataOption) (points [][]float64, err error) {
var bufSource *bytes.Buffer
if bufSource, err = dExt.takeScreenShot(); err != nil {
err = fmt.Errorf("takeScreenShot error: %v", err)
if bufSource, err = dExt.TakeScreenShot(builtin.GenNameWithTimestamp("step_%d_ocr")); err != nil {
return
}

View File

@@ -14,6 +14,8 @@ import (
"github.com/pkg/errors"
"gocv.io/x/gocv"
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
)
const (
@@ -101,7 +103,7 @@ func (dExt *DriverExt) FindAllImageRect(search string) (rects []image.Rectangle,
if bufSearch, err = getBufFromDisk(search); err != nil {
return nil, err
}
if bufSource, err = dExt.takeScreenShot(); err != nil {
if bufSource, err = dExt.TakeScreenShot(builtin.GenNameWithTimestamp("step_%d_cv")); err != nil {
return nil, err
}
@@ -116,7 +118,7 @@ func (dExt *DriverExt) FindImageRectInUIKit(imagePath string, options ...DataOpt
if bufSearch, err = getBufFromDisk(imagePath); err != nil {
return 0, 0, 0, 0, err
}
if bufSource, err = dExt.takeScreenShot(); err != nil {
if bufSource, err = dExt.TakeScreenShot(builtin.GenNameWithTimestamp("step_%d_cv")); err != nil {
return 0, 0, 0, 0, err
}

View File

@@ -63,7 +63,7 @@ func assertRunTestCases(t *testing.T) {
refCase := TestCasePath(demoTestCaseWithPluginJSONPath)
testcase1 := &TestCase{
Config: NewConfig("TestCase1").
SetBaseURL("http://httpbin.org"),
SetBaseURL("https://httpbin.org"),
TestSteps: []IStep{
NewStep("testcase1-step1").
GET("/headers").
@@ -77,7 +77,7 @@ func assertRunTestCases(t *testing.T) {
AssertEqual("headers.\"Content-Type\"", "application/json", "check http response Content-Type"),
NewStep("testcase1-step3").CallRefCase(
&TestCase{
Config: NewConfig("testcase1-step3-ref-case").SetBaseURL("http://httpbin.org"),
Config: NewConfig("testcase1-step3-ref-case").SetBaseURL("https://httpbin.org"),
TestSteps: []IStep{
NewStep("ip").
GET("/ip").

View File

@@ -2,11 +2,11 @@ package hrp
import (
"fmt"
"time"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
"github.com/httprunner/httprunner/v4/hrp/internal/code"
"github.com/httprunner/httprunner/v4/hrp/pkg/uixt"
)
@@ -468,6 +468,36 @@ func (s *StepMobileUIValidation) AssertImageNotExists(expectedImagePath string,
return s
}
func (s *StepMobileUIValidation) AssertAppInForeground(packageName string, msg ...string) *StepMobileUIValidation {
v := Validator{
Check: uixt.SelectorForegroundApp,
Assert: uixt.AssertionEqual,
Expect: packageName,
}
if len(msg) > 0 {
v.Message = msg[0]
} else {
v.Message = fmt.Sprintf("app [%s] should be in foreground", packageName)
}
s.step.Validators = append(s.step.Validators, v)
return s
}
func (s *StepMobileUIValidation) AssertAppNotInForeground(packageName string, msg ...string) *StepMobileUIValidation {
v := Validator{
Check: uixt.SelectorForegroundApp,
Assert: uixt.AssertionNotEqual,
Expect: packageName,
}
if len(msg) > 0 {
v.Message = msg[0]
} else {
v.Message = fmt.Sprintf("app [%s] should not be in foreground", packageName)
}
s.step.Validators = append(s.step.Validators, v)
return s
}
func (s *StepMobileUIValidation) Name() string {
return s.step.Name
}
@@ -542,7 +572,6 @@ func runStepMobileUI(s *SessionRunner, step *TStep) (stepResult *StepResult, err
Success: false,
ContentSize: 0,
}
screenshots := make([]string, 0)
// merge step variables with session variables
stepVariables, err := s.ParseStepVariables(step.Variables)
@@ -572,18 +601,14 @@ func runStepMobileUI(s *SessionRunner, step *TStep) (stepResult *StepResult, err
}
// take screenshot after each step
screenshotPath, err := uiDriver.ScreenShot(
fmt.Sprintf("step_%d", time.Now().Unix()))
_, err := uiDriver.TakeScreenShot(
builtin.GenNameWithTimestamp("step_%d_") + step.Name)
if err != nil {
log.Error().Err(err).Str("step", step.Name).Msg("take screenshot failed")
} else {
log.Info().Str("path", screenshotPath).Msg("take screenshot on step finished")
screenshots = append(screenshots, screenshotPath)
log.Error().Err(err).Str("step", step.Name).Msg("take screenshot failed on step finished")
}
// save attachments
screenshots = append(screenshots, uiDriver.ScreenShots...)
attachments["screenshots"] = screenshots
attachments["screenshots"] = uiDriver.GetScreenShots()
stepResult.Attachments = attachments
}()

View File

@@ -153,7 +153,7 @@ func TestRunRequestStatOn(t *testing.T) {
if !assert.Greater(t, stat["Total"], int64(1)) {
t.Fatal()
}
if !assert.Less(t, stat["Total"]-summary.Records[0].Elapsed, int64(3)) {
if !assert.Less(t, stat["Total"]-summary.Records[0].Elapsed, int64(100)) {
t.Fatal()
}
}
@@ -165,7 +165,7 @@ func TestRunCaseWithTimeout(t *testing.T) {
testcase1 := &TestCase{
Config: NewConfig("TestCase1").
SetTimeout(2 * time.Second). // set global timeout to 2s
SetBaseURL("http://httpbin.org"),
SetBaseURL("https://httpbin.org"),
TestSteps: []IStep{
NewStep("step1").
GET("/delay/1").
@@ -181,7 +181,7 @@ func TestRunCaseWithTimeout(t *testing.T) {
testcase2 := &TestCase{
Config: NewConfig("TestCase2").
SetTimeout(2 * time.Second). // set global timeout to 2s
SetBaseURL("http://httpbin.org"),
SetBaseURL("https://httpbin.org"),
TestSteps: []IStep{
NewStep("step1").
GET("/delay/3").
@@ -198,7 +198,7 @@ func TestRunCaseWithTimeout(t *testing.T) {
testcase3 := &TestCase{
Config: NewConfig("TestCase3").
SetTimeout(2 * time.Second).
SetBaseURL("http://httpbin.org"),
SetBaseURL("https://httpbin.org"),
TestSteps: []IStep{
NewStep("step2").
GET("/delay/3").

View File

@@ -8,7 +8,7 @@ class TestHttpSession(unittest.TestCase):
self.session = HttpSession()
def test_request_http(self):
self.session.request("get", "http://httpbin.org/get")
self.session.request("get", "https://httpbin.org/get")
address = self.session.data.address
self.assertGreater(len(address.server_ip), 0)
self.assertEqual(address.server_port, 80)
@@ -26,7 +26,7 @@ class TestHttpSession(unittest.TestCase):
def test_request_http_allow_redirects(self):
self.session.request(
"get",
"http://httpbin.org/redirect-to?url=https%3A%2F%2Fgithub.com",
"https://httpbin.org/redirect-to?url=https%3A%2F%2Fgithub.com",
allow_redirects=True,
)
address = self.session.data.address
@@ -50,7 +50,7 @@ class TestHttpSession(unittest.TestCase):
def test_request_http_not_allow_redirects(self):
self.session.request(
"get",
"http://httpbin.org/redirect-to?url=https%3A%2F%2Fgithub.com",
"https://httpbin.org/redirect-to?url=https%3A%2F%2Fgithub.com",
allow_redirects=False,
)
address = self.session.data.address

View File

@@ -10,7 +10,7 @@ Then you can write upload test script as below:
- test:
name: upload file
request:
url: http://httpbin.org/upload
url: https://httpbin.org/upload
method: POST
headers:
Cookie: session=AAA-BBB-CCC
@@ -31,7 +31,7 @@ For compatibility, you can also write upload test script in old way:
field2: "value2"
m_encoder: ${multipart_encoder(file=$file, field1=$field1, field2=$field2)}
request:
url: http://httpbin.org/upload
url: https://httpbin.org/upload
method: POST
headers:
Content-Type: ${multipart_content_type($m_encoder)}
@@ -75,7 +75,9 @@ def ensure_upload_ready():
sys.exit(1)
def prepare_upload_step(step: TStep, step_variables: VariablesMapping, functions: FunctionsMapping):
def prepare_upload_step(
step: TStep, step_variables: VariablesMapping, functions: FunctionsMapping
):
"""preprocess for upload test
replace `upload` info with MultipartEncoder
@@ -84,7 +86,7 @@ def prepare_upload_step(step: TStep, step_variables: VariablesMapping, functions
{
"variables": {},
"request": {
"url": "http://httpbin.org/upload",
"url": "https://httpbin.org/upload",
"method": "POST",
"headers": {
"Cookie": "session=AAA-BBB-CCC"