mirror of
https://github.com/httprunner/httprunner.git
synced 2026-07-12 16:01:27 +08:00
feat: implement ios ui swipe
This commit is contained in:
1
go.mod
1
go.mod
@@ -5,6 +5,7 @@ go 1.16
|
|||||||
require (
|
require (
|
||||||
github.com/andybalholm/brotli v1.0.4
|
github.com/andybalholm/brotli v1.0.4
|
||||||
github.com/denisbrodbeck/machineid v1.0.1
|
github.com/denisbrodbeck/machineid v1.0.1
|
||||||
|
github.com/electricbubble/gwda v0.3.0
|
||||||
github.com/fatih/color v1.13.0
|
github.com/fatih/color v1.13.0
|
||||||
github.com/getsentry/sentry-go v0.13.0
|
github.com/getsentry/sentry-go v0.13.0
|
||||||
github.com/go-errors/errors v1.0.1
|
github.com/go-errors/errors v1.0.1
|
||||||
|
|||||||
9
go.sum
9
go.sum
@@ -99,6 +99,10 @@ github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6ps
|
|||||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||||
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM=
|
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM=
|
||||||
|
github.com/electricbubble/gidevice v0.0.4 h1:PbOt4AngNQTtO5j0vCZ3Xcj9mByDtZmjBYLTh8PJ9kc=
|
||||||
|
github.com/electricbubble/gidevice v0.0.4/go.mod h1:hWRHIPf4uyiEB56hnVHVvu6MoVg7RlJY8ZV2FVgLKZA=
|
||||||
|
github.com/electricbubble/gwda v0.3.0 h1:uQMZxmp5D51iMsXrWfi21MlftrkPmOeLDE+gtw06fg4=
|
||||||
|
github.com/electricbubble/gwda v0.3.0/go.mod h1:co3ynSIVXEyI3aKdzfjqkFDFloFcxhc+e27U0ajyZsM=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||||
@@ -270,6 +274,7 @@ github.com/iris-contrib/go.uuid v2.0.0+incompatible/go.mod h1:iz2lgM/1UnEf1kP0L/
|
|||||||
github.com/iris-contrib/jade v1.1.3/go.mod h1:H/geBymxJhShH5kecoiOCSssPX7QWYH7UaeZTSWddIk=
|
github.com/iris-contrib/jade v1.1.3/go.mod h1:H/geBymxJhShH5kecoiOCSssPX7QWYH7UaeZTSWddIk=
|
||||||
github.com/iris-contrib/pongo2 v0.0.1/go.mod h1:Ssh+00+3GAZqSQb30AvBRNxBx7rf0GqwkjqxNd0u65g=
|
github.com/iris-contrib/pongo2 v0.0.1/go.mod h1:Ssh+00+3GAZqSQb30AvBRNxBx7rf0GqwkjqxNd0u65g=
|
||||||
github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw=
|
github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw=
|
||||||
|
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||||
github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE=
|
github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE=
|
||||||
github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74=
|
github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74=
|
||||||
github.com/jinzhu/copier v0.3.2 h1:QdBOCbaouLDYaIPFfi1bKv5F5tPpeTwXe4sD0jqtz5w=
|
github.com/jinzhu/copier v0.3.2 h1:QdBOCbaouLDYaIPFfi1bKv5F5tPpeTwXe4sD0jqtz5w=
|
||||||
@@ -419,6 +424,8 @@ github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0
|
|||||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||||
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||||
|
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
|
||||||
|
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||||
github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g=
|
github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g=
|
||||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||||
@@ -900,6 +907,8 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
|
|||||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||||
|
howett.net/plist v0.0.0-20201203080718-1454fab16a06 h1:QDxUo/w2COstK1wIBYpzQlHX/NqaQTcf9jyz347nI58=
|
||||||
|
howett.net/plist v0.0.0-20201203080718-1454fab16a06/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0=
|
||||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ func NewRunner(t *testing.T) *HRPRunner {
|
|||||||
Transport: &http2.Transport{
|
Transport: &http2.Transport{
|
||||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||||
},
|
},
|
||||||
|
Jar: jar, // insert response cookies into request
|
||||||
Timeout: 120 * time.Second,
|
Timeout: 120 * time.Second,
|
||||||
},
|
},
|
||||||
// use default handshake timeout (no timeout limit) here, enable timeout at step level
|
// use default handshake timeout (no timeout limit) here, enable timeout at step level
|
||||||
@@ -69,6 +70,7 @@ type HRPRunner struct {
|
|||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
http2Client *http.Client
|
http2Client *http.Client
|
||||||
wsDialer *websocket.Dialer
|
wsDialer *websocket.Dialer
|
||||||
|
wdaClients map[string]*wdaClient // wda client used for iOS UI automation, key is udid
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetClientTransport configures transport of http client for high concurrency load testing
|
// SetClientTransport configures transport of http client for high concurrency load testing
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
package hrp
|
package hrp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/electricbubble/gwda"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
type IOSAction struct {
|
type IOSAction struct {
|
||||||
MobileAction
|
MobileAction
|
||||||
UDID string `json:"udid,omitempty" yaml:"udid,omitempty"`
|
UDID string `json:"udid,omitempty" yaml:"udid,omitempty"`
|
||||||
@@ -159,12 +167,172 @@ func (s *StepIOSValidation) Run(r *SessionRunner) (*StepResult, error) {
|
|||||||
return runStepIOS(r, s.step)
|
return runStepIOS(r, s.step)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *HRPRunner) InitWDAClient(udid string) (*wdaClient, error) {
|
||||||
|
// avoid duplicate init
|
||||||
|
if udid == "" && len(r.wdaClients) == 1 {
|
||||||
|
for _, v := range r.wdaClients {
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
targetDevice, err := getAttachedIOSDevice(udid)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// avoid duplicate init
|
||||||
|
if client, ok := r.wdaClients[targetDevice.SerialNumber()]; ok {
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// init WDA driver
|
||||||
|
driver, err := gwda.NewUSBDriver(nil, *targetDevice)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to init WDA driver")
|
||||||
|
}
|
||||||
|
|
||||||
|
// get device window size
|
||||||
|
windowSize, err := driver.WindowSize()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to get windows size")
|
||||||
|
}
|
||||||
|
|
||||||
|
// cache wda client
|
||||||
|
r.wdaClients = make(map[string]*wdaClient)
|
||||||
|
client := &wdaClient{
|
||||||
|
Driver: driver,
|
||||||
|
WindowSize: windowSize,
|
||||||
|
}
|
||||||
|
r.wdaClients[targetDevice.SerialNumber()] = client
|
||||||
|
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAttachedIOSDevice(udid string) (*gwda.Device, error) {
|
||||||
|
// get all attached deivces
|
||||||
|
devices, err := gwda.DeviceList()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to get attached ios devices list")
|
||||||
|
}
|
||||||
|
if len(devices) == 0 {
|
||||||
|
return nil, errors.New("no ios devices attached")
|
||||||
|
}
|
||||||
|
|
||||||
|
if udid == "" {
|
||||||
|
return &devices[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// find device by udid
|
||||||
|
for _, device := range devices {
|
||||||
|
if device.SerialNumber() == udid {
|
||||||
|
return &device, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("device %s is not attached", udid)
|
||||||
|
}
|
||||||
|
|
||||||
func runStepIOS(r *SessionRunner, step *TStep) (stepResult *StepResult, err error) {
|
func runStepIOS(r *SessionRunner, step *TStep) (stepResult *StepResult, err error) {
|
||||||
stepResult = &StepResult{
|
stepResult = &StepResult{
|
||||||
Name: step.Name,
|
Name: step.Name,
|
||||||
StepType: stepTypeAndroid,
|
StepType: stepTypeIOS,
|
||||||
Success: false,
|
Success: false,
|
||||||
ContentSize: 0,
|
ContentSize: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// init wdaClient driver
|
||||||
|
wdaClient, err := r.hrpRunner.InitWDAClient(step.IOS.UDID)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepare actions
|
||||||
|
var actions []MobileAction
|
||||||
|
if step.IOS.Actions == nil {
|
||||||
|
actions = []MobileAction{
|
||||||
|
{
|
||||||
|
Method: step.IOS.Method,
|
||||||
|
Params: step.IOS.Params,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
actions = step.IOS.Actions
|
||||||
|
}
|
||||||
|
|
||||||
|
// run actions
|
||||||
|
for _, action := range actions {
|
||||||
|
if err := wdaClient.doAction(action); err != nil {
|
||||||
|
return stepResult, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// do validation
|
||||||
|
// step.Validators
|
||||||
|
|
||||||
|
stepResult.Success = true
|
||||||
return stepResult, nil
|
return stepResult, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var errActionNotImplemented = errors.New("UI action not implemented")
|
||||||
|
|
||||||
|
type wdaClient struct {
|
||||||
|
Driver gwda.WebDriver
|
||||||
|
WindowSize gwda.Size
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *wdaClient) doAction(action MobileAction) error {
|
||||||
|
log.Info().Str("method", string(action.Method)).Interface("params", action.Params).Msg("start iOS UI action")
|
||||||
|
|
||||||
|
switch action.Method {
|
||||||
|
case appInstall:
|
||||||
|
// TODO
|
||||||
|
return errActionNotImplemented
|
||||||
|
case appStart:
|
||||||
|
// TODO
|
||||||
|
return errActionNotImplemented
|
||||||
|
case uiClick:
|
||||||
|
// TODO
|
||||||
|
return errActionNotImplemented
|
||||||
|
case uiDoubleClick:
|
||||||
|
// TODO
|
||||||
|
return errActionNotImplemented
|
||||||
|
case uiLongClick:
|
||||||
|
// TODO
|
||||||
|
return errActionNotImplemented
|
||||||
|
case uiSwipe:
|
||||||
|
width := w.WindowSize.Width
|
||||||
|
height := w.WindowSize.Height
|
||||||
|
|
||||||
|
var fromX, fromY, toX, toY int
|
||||||
|
if direction, ok := action.Params.(string); ok {
|
||||||
|
switch direction {
|
||||||
|
case "up":
|
||||||
|
fromX, fromY, toX, toY = width/2, height*1/4, width/2, height*3/4
|
||||||
|
case "down":
|
||||||
|
fromX, fromY, toX, toY = width/2, height*3/4, width/2, height*1/4
|
||||||
|
case "left":
|
||||||
|
fromX, fromY, toX, toY = width*3/4, height/2, width*1/4, height/2
|
||||||
|
case "right":
|
||||||
|
fromX, fromY, toX, toY = width*1/4, height/2, width*3/4, height/2
|
||||||
|
}
|
||||||
|
} else if params, ok := action.Params.([]int); ok {
|
||||||
|
if len(params) != 4 {
|
||||||
|
return fmt.Errorf("invalid swipe params: %v", params)
|
||||||
|
}
|
||||||
|
fromX, fromY, toX, toY = params[0], params[1], params[2], params[3]
|
||||||
|
}
|
||||||
|
return w.Driver.Swipe(fromX, fromY, toX, toY)
|
||||||
|
case uiInput:
|
||||||
|
// TODO
|
||||||
|
return errActionNotImplemented
|
||||||
|
case appClick:
|
||||||
|
// TODO
|
||||||
|
return errActionNotImplemented
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *wdaClient) doValidation() error {
|
||||||
|
// TODO
|
||||||
|
return errActionNotImplemented
|
||||||
|
}
|
||||||
|
|||||||
@@ -32,12 +32,12 @@ func TestIOSAction(t *testing.T) {
|
|||||||
Config: NewConfig("ios ui action"),
|
Config: NewConfig("ios ui action"),
|
||||||
TestSteps: []IStep{
|
TestSteps: []IStep{
|
||||||
NewStep("launch douyin").
|
NewStep("launch douyin").
|
||||||
IOS().UDID("xxx").Click("抖音").
|
IOS().Click("抖音").
|
||||||
Validate().
|
Validate().
|
||||||
AssertTextExists("首页", "首页 tab 不存在").
|
AssertTextExists("首页", "首页 tab 不存在").
|
||||||
AssertTextExists("消息", "消息 tab 不存在"),
|
AssertTextExists("消息", "消息 tab 不存在"),
|
||||||
NewStep("swipe up and down").
|
NewStep("swipe up and down").
|
||||||
IOS().UDID("xxx").SwipeUp().SwipeUp().SwipeDown(),
|
IOS().SwipeUp().SwipeUp().SwipeDown(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
tCase := testCase.ToTCase()
|
tCase := testCase.ToTCase()
|
||||||
|
|||||||
Reference in New Issue
Block a user