Merge pull request #1477 from xucong053/dev-v4.3-android

refactor: android ui automation
This commit is contained in:
debugtalk
2022-09-28 20:38:54 +08:00
committed by GitHub
31 changed files with 1946 additions and 1827 deletions

View File

@@ -17,7 +17,7 @@ jobs:
fail-fast: false
matrix:
go-version:
- 1.17.x
- 1.18.x
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
@@ -44,7 +44,7 @@ jobs:
fail-fast: false
matrix:
go-version:
- 1.17.x
- 1.18.x
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
@@ -76,7 +76,7 @@ jobs:
fail-fast: false
matrix:
go-version:
- 1.17.x
- 1.18.x
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:

View File

@@ -56,7 +56,7 @@ jobs:
fail-fast: false
matrix:
go-version:
- 1.17.x
- 1.18.x
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:

View File

@@ -63,8 +63,6 @@ jobs:
fail-fast: false
matrix:
go-version:
- 1.16.x
- 1.17.x
- 1.18.x
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}

View File

@@ -1,16 +0,0 @@
// NOTE: Generated By hrp v4.1.5, DO NOT EDIT!
package main
import (
"github.com/httprunner/funplugin/fungo"
)
func main() {
fungo.Register("SumTwoInt", SumTwoInt)
fungo.Register("SumInts", SumInts)
fungo.Register("Sum", Sum)
fungo.Register("SetupHookExample", SetupHookExample)
fungo.Register("TeardownHookExample", TeardownHookExample)
fungo.Register("GetUserAgent", GetUserAgent)
fungo.Serve()
}

View File

@@ -1,5 +1,5 @@
{
"project_name": "demo-with-go-plugin",
"create_time": "2022-07-26T10:30:29.31361+08:00",
"hrp_version": "v4.2.0"
"create_time": "2022-09-28T16:40:14.674398+08:00",
"hrp_version": "v4.3.0"
}

View File

@@ -1,23 +0,0 @@
# NOTE: Generated By hrp v4.1.6, DO NOT EDIT!
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from debugtalk import *
if __name__ == "__main__":
import funppy
funppy.register("get_user_agent", get_user_agent)
funppy.register("sleep", sleep)
funppy.register("sum", sum)
funppy.register("sum_ints", sum_ints)
funppy.register("sum_two_int", sum_two_int)
funppy.register("sum_two_string", sum_two_string)
funppy.register("sum_strings", sum_strings)
funppy.register("concatenate", concatenate)
funppy.register("setup_hook_example", setup_hook_example)
funppy.register("teardown_hook_example", teardown_hook_example)
funppy.serve()

View File

@@ -1,5 +1,5 @@
{
"project_name": "demo-with-py-plugin",
"create_time": "2022-07-26T10:30:30.601095+08:00",
"hrp_version": "v4.2.0"
"create_time": "2022-09-28T16:40:15.283869+08:00",
"hrp_version": "v4.3.0"
}

View File

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

View File

@@ -1,3 +1,5 @@
//go:build localtest
package uitest
import (

View File

@@ -1,3 +1,5 @@
//go:build localtest
package uitest
import (

View File

@@ -123,6 +123,27 @@ func (c *TConfig) SetIOS(options ...uixt.IOSDeviceOption) *TConfig {
return c
}
func (c *TConfig) SetAndroid(options ...uixt.AndroidDeviceOption) *TConfig {
uiaOptions := &uixt.AndroidDevice{}
for _, option := range options {
option(uiaOptions)
}
// each device can have its own settings
if uiaOptions.SerialNumber != "" {
c.Android = append(c.Android, uiaOptions)
return c
}
// device UDID is not specified, settings will be shared
if len(c.Android) == 0 {
c.Android = append(c.Android, uiaOptions)
} else {
c.Android[0] = uiaOptions
}
return c
}
type ThinkTimeConfig struct {
Strategy thinkTimeStrategy `json:"strategy,omitempty" yaml:"strategy,omitempty"` // default、random、multiply、ignore
Setting interface{} `json:"setting,omitempty" yaml:"setting,omitempty"` // random(map): {"min_percentage": 0.5, "max_percentage": 1.5}; 10、multiply(float64): 1.5

View File

@@ -338,14 +338,14 @@
</div>
</div>
</div>
{{ if .Attachment }}
{{ if .Attachments }}
<a class="button" href="#popup_attachment_{{$suite_index}}_{{$loop_index}}">traceback</a>
<div id="popup_attachment_{{$suite_index}}_{{$loop_index}}" class="overlay">
<div class="popup">
<h2>Traceback Message</h2>
<a class="close" href="#record_{{$suite_index}}_{{$loop_index}}">&times;</a>
<div class="content">
<pre>{{ .Attachment }}</pre>
<pre>{{ .Attachments }}</pre>
</div>
</div>
</div>

View File

@@ -42,7 +42,7 @@ func (ta *TouchAction) AddPointF(point PointF, startTime ...float64) *TouchActio
return ta.AddFloat(point.X, point.Y, startTime...)
}
func (d *uiaDriver) MultiPointerGesture(gesture1 *TouchAction, gesture2 *TouchAction, tas ...*TouchAction) (err error) {
func (ud *uiaDriver) MultiPointerGesture(gesture1 *TouchAction, gesture2 *TouchAction, tas ...*TouchAction) (err error) {
// Must provide coordinates for at least 2 pointers
actions := make([]*TouchAction, 0)
actions = append(actions, gesture1, gesture2)
@@ -53,7 +53,7 @@ func (d *uiaDriver) MultiPointerGesture(gesture1 *TouchAction, gesture2 *TouchAc
"actions": actions,
}
// register(postHandler, new MultiPointerGesture("/wd/hub/session/:sessionId/touch/multi/perform"))
_, err = d.httpPOST(data, "/session", d.sessionId, "/touch/multi/perform")
_, err = ud.httpPOST(data, "/session", ud.sessionId, "/touch/multi/perform")
return
}

View File

@@ -44,11 +44,15 @@ func InitUIAClient(device *AndroidDevice) (*DriverExt, error) {
fmt.Println(driver)
var driverExt *DriverExt
// TODO
// driverExt, err = Extend(driver)
// if err != nil {
// return nil, errors.Wrap(err, "failed to extend UIA Driver")
// }
driverExt, err = Extend(driver)
if err != nil {
return nil, errors.Wrap(err, "failed to extend UIA Driver")
}
if device.LogOn {
// TODO
}
return driverExt, nil
}
@@ -113,6 +117,7 @@ type AndroidDevice struct {
SerialNumber string `json:"serial,omitempty" yaml:"serial,omitempty"`
IP string `json:"ip,omitempty" yaml:"ip,omitempty"`
Port int `json:"port,omitempty" yaml:"port,omitempty"`
MjpegPort int `json:"mjpeg_port,omitempty" yaml:"mjpeg_port,omitempty"`
LogOn bool `json:"log_on,omitempty" yaml:"log_on,omitempty"`
}
@@ -149,12 +154,6 @@ func (dev *AndroidDevice) NewUSBDriver(capabilities Capabilities) (driver *uiaDr
driver.adbDevice = dev.d
driver.localPort = localPort
conn, err := net.Dial("tcp", fmt.Sprintf(":%d", localPort))
if err != nil {
return nil, fmt.Errorf("adb forward: %w", err)
}
driver.client = convertToHTTPClient(conn)
return driver, nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -4,73 +4,179 @@ import (
"bytes"
"encoding/base64"
"encoding/json"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
)
var errElementNotImplemented = errors.New("element method not implemented")
type uiaElement struct {
parent *uiaDriver
id string
}
func (e *uiaElement) Text() (text string, err error) {
// register(getHandler, new GetText("/wd/hub/session/:sessionId/element/:id/text"))
var rawResp rawResponse
if rawResp, err = e.parent.httpGET("/session", e.parent.sessionId, "/element", e.id, "/text"); err != nil {
return "", err
}
reply := new(struct{ Value string })
if err = json.Unmarshal(rawResp, reply); err != nil {
return "", err
}
text = reply.Value
func (ue uiaElement) Click() (err error) {
// register(postHandler, new Click("/wd/hub/session/:sessionId/element/:id/click"))
_, err = ue.parent.httpPOST(nil, "/session", ue.parent.sessionId, "/element", ue.id, "/click")
return
}
func (e *uiaElement) GetAttribute(name string) (attribute string, err error) {
// register(getHandler, new GetElementAttribute("/wd/hub/session/:sessionId/element/:id/attribute/:name"))
var rawResp rawResponse
if rawResp, err = e.parent.httpGET("/session", e.parent.sessionId, "/element", e.id, "/attribute", name); err != nil {
return "", err
func (ue uiaElement) SendKeys(text string, isReplace ...int) (err error) {
if len(isReplace) == 0 {
isReplace = []int{1}
}
reply := new(struct{ Value string })
if err = json.Unmarshal(rawResp, reply); err != nil {
return "", err
// register(postHandler, new SendKeysToElement("/wd/hub/session/:sessionId/element/:id/value"))
// https://github.com/appium/appium-uiutomator2-server/blob/master/app/src/main/java/io/appium/uiutomator2/handler/SendKeysToElement.java#L76-L85
data := map[string]interface{}{
"text": text,
"replace": isReplace[0] == 1,
}
attribute = reply.Value
_, err = ue.parent.httpPOST(data, "/session", ue.parent.sessionId, "/element", ue.id, "/value")
return
}
func (e *uiaElement) ContentDescription() (name string, err error) {
// register(getHandler, new GetName("/wd/hub/session/:sessionId/element/:id/name"))
var rawResp rawResponse
if rawResp, err = e.parent.httpGET("/session", e.parent.sessionId, "/element", e.id, "/name"); err != nil {
return "", err
}
reply := new(struct{ Value string })
if err = json.Unmarshal(rawResp, reply); err != nil {
return "", err
}
name = reply.Value
func (ue uiaElement) Clear() (err error) {
// TODO
return errElementNotImplemented
}
func (ue uiaElement) Tap(x, y int) (err error) {
// TODO
return errElementNotImplemented
}
func (ue uiaElement) TapFloat(x, y float64) (err error) {
// TODO
return errElementNotImplemented
}
func (ue uiaElement) DoubleTap() (err error) {
// TODO
return errElementNotImplemented
}
func (ue uiaElement) TouchAndHold(second ...float64) (err error) {
// TODO
return errElementNotImplemented
}
func (ue uiaElement) TwoFingerTap() (err error) {
// TODO
return errElementNotImplemented
}
func (ue uiaElement) TapWithNumberOfTaps(numberOfTaps, numberOfTouches int) (err error) {
//Todo: implement
log.Fatal().Msg("not support")
return
}
func (e *uiaElement) Size() (size Size, err error) {
// register(getHandler, new GetSize("/wd/hub/session/:sessionId/element/:id/size"))
var rawResp rawResponse
if rawResp, err = e.parent.httpGET("/session", e.parent.sessionId, "/element", e.id, "/size"); err != nil {
return Size{-1, -1}, err
}
reply := new(struct{ Value Size })
if err = json.Unmarshal(rawResp, reply); err != nil {
return Size{-1, -1}, err
}
size = reply.Value
return
func (ue uiaElement) ForceTouch(pressure float64, second ...float64) (err error) {
// TODO
return errElementNotImplemented
}
func (e *uiaElement) Rect() (rect Rect, err error) {
func (ue uiaElement) ForceTouchFloat(x, y, pressure float64, second ...float64) (err error) {
// TODO
return errElementNotImplemented
}
func (ue uiaElement) Drag(fromX, fromY, toX, toY int, steps ...float64) (err error) {
return ue.DragFloat(float64(fromX), float64(fromY), float64(toX), float64(toY), steps...)
}
func (ue uiaElement) DragFloat(fromX, fromY, toX, toY float64, steps ...float64) (err error) {
if len(steps) == 0 {
steps = []float64{12 * 10}
} else {
steps[0] = 12 * 10
}
data := map[string]interface{}{
"elementId": ue.id,
"endX": toX,
"endY": toY,
"steps": steps[0],
}
return ue.parent._drag(data)
}
func (ue uiaElement) Swipe(fromX, fromY, toX, toY int) error {
return ue.SwipeFloat(float64(fromX), float64(fromY), float64(toX), float64(toY))
}
func (ue uiaElement) SwipeFloat(fromX, fromY, toX, toY float64) error {
return ue.parent._swipe(fromX, fromY, toX, toY, 12, ue.id)
}
func (ue uiaElement) SwipeDirection(direction Direction, velocity ...float64) (err error) {
// TODO
return errElementNotImplemented
}
func (ue uiaElement) Pinch(scale, velocity float64) (err error) {
// TODO
return errElementNotImplemented
}
func (ue uiaElement) PinchToZoomOutByW3CAction(scale ...float64) (err error) {
// TODO
return errElementNotImplemented
}
func (ue uiaElement) Rotate(rotation float64, velocity ...float64) (err error) {
// TODO
return errElementNotImplemented
}
func (ue uiaElement) PickerWheelSelect(order PickerWheelOrder, offset ...int) (err error) {
// TODO
return errElementNotImplemented
}
func (ue uiaElement) scroll(data interface{}) (err error) {
// TODO
return errElementNotImplemented
}
func (ue uiaElement) ScrollElementByName(name string) (err error) {
// TODO
return errElementNotImplemented
}
func (ue uiaElement) ScrollElementByPredicate(predicate string) (err error) {
// TODO
return errElementNotImplemented
}
func (ue uiaElement) ScrollToVisible() (err error) {
// TODO
return errElementNotImplemented
}
func (ue uiaElement) ScrollDirection(direction Direction, distance ...float64) (err error) {
// TODO
return errElementNotImplemented
}
func (ue uiaElement) FindElement(by BySelector) (element WebElement, err error) {
method, selector := by.getMethodAndSelector()
return ue.parent._findElement(method, selector, ue.id)
}
func (ue uiaElement) FindElements(by BySelector) (elements []WebElement, err error) {
method, selector := by.getMethodAndSelector()
return ue.parent._findElements(method, selector, ue.id)
}
func (ue uiaElement) FindVisibleCells() (elements []WebElement, err error) {
// TODO
return elements, errElementNotImplemented
}
func (ue uiaElement) Rect() (rect Rect, err error) {
// register(getHandler, new GetRect("/wd/hub/session/:sessionId/element/:id/rect"))
var rawResp rawResponse
if rawResp, err = e.parent.httpGET("/session", e.parent.sessionId, "/element", e.id, "/rect"); err != nil {
if rawResp, err = ue.parent.httpGET("/session", ue.parent.sessionId, "/element", ue.id, "/rect"); err != nil {
return Rect{}, err
}
reply := new(struct{ Value Rect })
@@ -81,13 +187,103 @@ func (e *uiaElement) Rect() (rect Rect, err error) {
return
}
func (e *uiaElement) Screenshot() (raw *bytes.Buffer, err error) {
func (ue uiaElement) Location() (point Point, err error) {
// register(getHandler, new Location("/wd/hub/session/:sessionId/element/:id/location"))
var rawResp rawResponse
if rawResp, err = ue.parent.httpGET("/session", ue.parent.sessionId, "/element", ue.id, "/location"); err != nil {
return Point{-1, -1}, err
}
reply := new(struct{ Value Point })
if err = json.Unmarshal(rawResp, reply); err != nil {
return Point{-1, -1}, err
}
point = reply.Value
return
}
func (ue uiaElement) Size() (size Size, err error) {
// register(getHandler, new GetSize("/wd/hub/session/:sessionId/element/:id/size"))
var rawResp rawResponse
if rawResp, err = ue.parent.httpGET("/session", ue.parent.sessionId, "/element", ue.id, "/size"); err != nil {
return Size{-1, -1}, err
}
reply := new(struct{ Value Size })
if err = json.Unmarshal(rawResp, reply); err != nil {
return Size{-1, -1}, err
}
size = reply.Value
return
}
func (ue uiaElement) Text() (text string, err error) {
// register(getHandler, new GetText("/wd/hub/session/:sessionId/element/:id/text"))
var rawResp rawResponse
if rawResp, err = ue.parent.httpGET("/session", ue.parent.sessionId, "/element", ue.id, "/text"); err != nil {
return "", err
}
reply := new(struct{ Value string })
if err = json.Unmarshal(rawResp, reply); err != nil {
return "", err
}
text = reply.Value
return
}
func (ue uiaElement) Type() (elemType string, err error) {
// TODO
return elemType, errElementNotImplemented
}
func (ue uiaElement) IsEnabled() (enabled bool, err error) {
// TODO
return enabled, errElementNotImplemented
}
func (ue uiaElement) IsDisplayed() (displayed bool, err error) {
// TODO
return displayed, errElementNotImplemented
}
func (ue uiaElement) IsSelected() (selected bool, err error) {
// TODO
return selected, errElementNotImplemented
}
func (ue uiaElement) IsAccessible() (accessible bool, err error) {
// TODO
return accessible, errElementNotImplemented
}
func (ue uiaElement) IsAccessibilityContainer() (isAccessibilityContainer bool, err error) {
// TODO
return isAccessibilityContainer, errElementNotImplemented
}
func (ue uiaElement) GetAttribute(attr ElementAttribute) (value string, err error) {
// register(getHandler, new GetElementAttribute("/wd/hub/session/:sessionId/element/:id/attribute/:name"))
var rawResp rawResponse
if rawResp, err = ue.parent.httpGET("/session", ue.parent.sessionId, "/element", ue.id, "/attribute", attr.getAttributeName()); err != nil {
return "", err
}
reply := new(struct{ Value string })
if err = json.Unmarshal(rawResp, reply); err != nil {
return "", err
}
value = reply.Value
return
}
func (ue uiaElement) UID() (uid string) {
return ue.id
}
func (ue uiaElement) Screenshot() (raw *bytes.Buffer, err error) {
// W3C endpoint
// register(getHandler, new GetElementScreenshot("/wd/hub/session/:sessionId/element/:id/screenshot"))
// JSONWP endpoint
// register(getHandler, new GetElementScreenshot("/wd/hub/session/:sessionId/screenshot/:id"))
var rawResp rawResponse
if rawResp, err = e.parent.httpGET("/session", e.parent.sessionId, "/element", e.id, "/screenshot"); err != nil {
if rawResp, err = ue.parent.httpGET("/session", ue.parent.sessionId, "/element", ue.id, "/screenshot"); err != nil {
return nil, err
}
reply := new(struct{ Value string })
@@ -103,136 +299,3 @@ func (e *uiaElement) Screenshot() (raw *bytes.Buffer, err error) {
raw = bytes.NewBuffer(decodeStr)
return
}
func (e *uiaElement) Location() (point Point, err error) {
// register(getHandler, new Location("/wd/hub/session/:sessionId/element/:id/location"))
var rawResp rawResponse
if rawResp, err = e.parent.httpGET("/session", e.parent.sessionId, "/element", e.id, "/location"); err != nil {
return Point{-1, -1}, err
}
reply := new(struct{ Value Point })
if err = json.Unmarshal(rawResp, reply); err != nil {
return Point{-1, -1}, err
}
point = reply.Value
return
}
func (e *uiaElement) Click() (err error) {
// register(postHandler, new Click("/wd/hub/session/:sessionId/element/:id/click"))
_, err = e.parent.httpPOST(nil, "/session", e.parent.sessionId, "/element", e.id, "/click")
return
}
func (e *uiaElement) Clear() (err error) {
// register(postHandler, new Clear("/wd/hub/session/:sessionId/element/:id/clear"))
_, err = e.parent.httpPOST(nil, "/session", e.parent.sessionId, "/element", e.id, "/clear")
return
}
func (e *uiaElement) SendKeys(text string, isReplace ...bool) (err error) {
if len(isReplace) == 0 {
isReplace = []bool{true}
}
// register(postHandler, new SendKeysToElement("/wd/hub/session/:sessionId/element/:id/value"))
// https://github.com/appium/appium-uiautomator2-server/blob/master/app/src/main/java/io/appium/uiautomator2/handler/SendKeysToElement.java#L76-L85
data := map[string]interface{}{
"text": text,
"replace": isReplace[0],
}
_, err = e.parent.httpPOST(data, "/session", e.parent.sessionId, "/element", e.id, "/value")
return
}
func (e *uiaElement) FindElements(by AndroidBySelector) (elements []*uiaElement, err error) {
method, selector := by.getMethodAndSelector()
return e.parent._findElements(method, selector, e.id)
}
func (e *uiaElement) FindElement(by AndroidBySelector) (elem *uiaElement, err error) {
method, selector := by.getMethodAndSelector()
return e.parent._findElement(method, selector, e.id)
}
func (e *uiaElement) Swipe(startX, startY, endX, endY int, steps ...int) (err error) {
return e.SwipeFloat(float64(startX), float64(startY), float64(endX), float64(endY), steps...)
}
func (e *uiaElement) SwipeFloat(startX, startY, endX, endY float64, steps ...int) (err error) {
if len(steps) == 0 {
steps = []int{12}
}
return e.parent._swipe(startX, startY, endX, endY, steps[0], e.id)
}
func (e *uiaElement) SwipePoint(startPoint, endPoint Point, steps ...int) (err error) {
return e.Swipe(startPoint.X, startPoint.Y, endPoint.X, endPoint.Y, steps...)
}
func (e *uiaElement) SwipePointF(startPoint, endPoint PointF, steps ...int) (err error) {
return e.SwipeFloat(startPoint.X, startPoint.Y, endPoint.X, endPoint.Y, steps...)
}
func (e *uiaElement) Drag(endX, endY int, steps ...int) (err error) {
return e.DragFloat(float64(endX), float64(endY), steps...)
}
func (e *uiaElement) DragFloat(endX, endY float64, steps ...int) error {
if len(steps) == 0 {
steps = []int{12 * 10}
} else {
steps[0] = 12 * 10
}
data := map[string]interface{}{
"elementId": e.id,
"endX": endX,
"endY": endY,
"steps": steps[0],
}
return e.parent._drag(data)
}
func (e *uiaElement) DragPoint(endPoint Point, steps ...int) error {
return e.Drag(endPoint.X, endPoint.Y, steps...)
}
func (e *uiaElement) DragPointF(endPoint PointF, steps ...int) (err error) {
return e.DragFloat(endPoint.X, endPoint.Y, steps...)
}
func (e *uiaElement) DragTo(destElem *uiaElement, steps ...int) error {
if len(steps) == 0 {
steps = []int{12}
}
data := map[string]interface{}{
"elementId": e.id,
"destElId": destElem.id,
"steps": steps[0],
}
return e.parent._drag(data)
}
func (e *uiaElement) Flick(xOffset, yOffset, speed int) (err error) {
data := map[string]interface{}{
legacyWebElementIdentifier: e.id,
webElementIdentifier: e.id,
"xoffset": xOffset,
"yoffset": yOffset,
"speed": speed,
}
return e.parent._flick(data)
}
func (e *uiaElement) ScrollTo(by AndroidBySelector, maxSwipes ...int) (err error) {
if len(maxSwipes) == 0 {
maxSwipes = []int{0}
}
method, selector := by.getMethodAndSelector()
return e.parent._scrollTo(method, selector, maxSwipes[0], e.id)
}
func (e *uiaElement) ScrollToElement(element *uiaElement) (err error) {
// register(postHandler, new ScrollToElement("/wd/hub/session/:sessionId/appium/element/:id/scroll_to/:id2"))
_, err = e.parent.httpPOST(nil, "/session", e.parent.sessionId, "/appium/element", e.id, "/scroll_to", element.id)
return
}

File diff suppressed because it is too large Load Diff

View File

@@ -51,7 +51,7 @@ func (wd *Driver) httpDELETE(pathElem ...string) (rawResp rawResponse, err error
}
func (wd *Driver) httpRequest(method string, rawURL string, rawBody []byte) (rawResp rawResponse, err error) {
log.Debug().Str("method", method).Str("url", rawURL).Str("body", string(rawBody)).Msg("request WDA")
log.Debug().Str("method", method).Str("url", rawURL).Str("body", string(rawBody)).Msg("request driver agent")
var req *http.Request
if req, err = http.NewRequest(method, rawURL, bytes.NewBuffer(rawBody)); err != nil {
@@ -77,7 +77,7 @@ func (wd *Driver) httpRequest(method string, rawURL string, rawBody []byte) (raw
// avoid printing screenshot data
logger.Str("response", string(rawResp))
}
logger.Msg("get WDA response")
logger.Msg("get driver agent response")
if err != nil {
return nil, err
}

View File

@@ -1,3 +1,5 @@
//go:build localtest
package uixt
import (

View File

@@ -237,6 +237,11 @@ func (dExt *DriverExt) FindUIElement(param string) (ele WebElement, err error) {
selector = BySelector{
XPath: param,
}
} else if strings.HasPrefix(param, "com.") {
// name
selector = BySelector{
ResourceIdID: param,
}
} else {
// name
selector = BySelector{
@@ -299,7 +304,7 @@ func (dExt *DriverExt) IsImageExist(text string) bool {
var errActionNotImplemented = errors.New("UI action not implemented")
func (dExt *DriverExt) DoAction(action MobileAction) error {
log.Info().Str("method", string(action.Method)).Interface("params", action.Params).Msg("start iOS UI action")
log.Info().Str("method", string(action.Method)).Interface("params", action.Params).Msg("start UI action")
switch action.Method {
case AppInstall:
@@ -473,18 +478,13 @@ func (dExt *DriverExt) DoAction(action MobileAction) error {
dExt.ScreenShots = append(dExt.ScreenShots, screenshotPath)
return err
case CtlStartCamera:
// start camera, alias for app_launch com.apple.camera
return dExt.Driver.AppLaunch("com.apple.camera")
return dExt.Driver.StartCamera()
case CtlStopCamera:
// stop camera, alias for app_terminate com.apple.camera
success, err := dExt.Driver.AppTerminate("com.apple.camera")
if err != nil {
return errors.Wrap(err, "failed to terminate camera")
}
if !success {
log.Warn().Msg("camera was not running")
}
return nil
return dExt.Driver.StopCamera()
case RecordStart:
return dExt.Driver.StartRecording()
case RecordStop:
return dExt.Driver.StopRecording()
}
return nil
}

View File

@@ -148,6 +148,55 @@ type DeviceInfo struct {
Name string `json:"name"`
IsSimulator bool `json:"isSimulator"`
ThermalState int `json:"thermalState"`
// ANDROID_ID A 64-bit number (as a hex string) that is uniquely generated when the user
// first sets up the device and should remain constant for the lifetime of the user's device. The value
// may change if a factory reset is performed on the device.
AndroidID string `json:"androidId"`
// Build.MANUFACTURER value
Manufacturer string `json:"manufacturer"`
// Build.BRAND value
Brand string `json:"brand"`
// Current running OS's API VERSION
APIVersion string `json:"apiVersion"`
// The current version string, for example "1.0" or "3.4b5"
PlatformVersion string `json:"platformVersion"`
// the name of the current celluar network carrier
CarrierName string `json:"carrierName"`
// the real size of the default display
RealDisplaySize string `json:"realDisplaySize"`
// The logical density of the display in Density Independent Pixel units.
DisplayDensity int `json:"displayDensity"`
// available networks
Networks []networkInfo `json:"networks"`
// current system locale
Locale string `json:"locale"`
Bluetooth struct {
State string `json:"state"`
} `json:"bluetooth"`
}
type networkCapabilities struct {
TransportTypes string `json:"transportTypes"`
NetworkCapabilities string `json:"networkCapabilities"`
LinkUpstreamBandwidthKbps int `json:"linkUpstreamBandwidthKbps"`
LinkDownBandwidthKbps int `json:"linkDownBandwidthKbps"`
SignalStrength int `json:"signalStrength"`
SSID string `json:"SSID"`
}
type networkInfo struct {
Type int `json:"type"`
TypeName string `json:"typeName"`
Subtype int `json:"subtype"`
SubtypeName string `json:"subtypeName"`
IsConnected bool `json:"isConnected"`
DetailedState string `json:"detailedState"`
State string `json:"state"`
ExtraInfo string `json:"extraInfo"`
IsAvailable bool `json:"isAvailable"`
IsRoaming bool `json:"isRoaming"`
IsFailover bool `json:"isFailover"`
Capabilities networkCapabilities `json:"capabilities"`
}
type Location struct {
@@ -267,6 +316,11 @@ func (opt AppLaunchOption) WithEnvironment(env map[string]string) AppLaunchOptio
return opt
}
func (opt AppLaunchOption) WithAndroidBySelector(waitForComplete ...AndroidBySelector) AppLaunchOption {
opt["androidBySelector"] = waitForComplete
return opt
}
// PasteboardType The type of the item on the pasteboard.
type PasteboardType string
@@ -426,6 +480,13 @@ type BySelector struct {
ClassChain string `json:"class chain"`
XPath string `json:"xpath"` // not recommended, it's slow because it is not supported by XCTest natively
// Set the search criteria to match the given resource ResourceIdID.
ResourceIdID string `json:"id"`
// Set the search criteria to match the content-description property for a widget.
ContentDescription string `json:"accessibility id"`
UiAutomator string `json:"-android uiautomator"`
}
func (wl BySelector) getUsingAndValue() (using, value string) {
@@ -449,6 +510,24 @@ func (wl BySelector) getUsingAndValue() (using, value string) {
return
}
func (by BySelector) getMethodAndSelector() (method, selector string) {
vBy := reflect.ValueOf(by)
tBy := reflect.TypeOf(by)
for i := 0; i < vBy.NumField(); i++ {
vi := vBy.Field(i).Interface()
// switch vi := vi.(type) {
// case string:
// selector = vi
// }
selector = vi.(string)
if selector != "" && selector != "UNKNOWN" {
method = tBy.Field(i).Tag.Get("json")
return
}
}
return
}
type ElementAttribute map[string]interface{}
func (ea ElementAttribute) String() string {
@@ -800,6 +879,14 @@ type WebDriver interface {
// AppAuthReset Resets the authorization status for a protected resource. Available since Xcode 11.4
AppAuthReset(ProtectedResource) error
// StartCamera Starts a new camera for recording
StartCamera() error
// StopCamera Stops the camera for recording
StopCamera() error
StartRecording() error
StopRecording() error
// Tap Sends a tap event at the coordinate.
Tap(x, y int, options ...DataOption) error
TapFloat(x, y float64, options ...DataOption) error

View File

@@ -240,15 +240,18 @@ func (dExt *DriverExt) StartLogRecording(identifier string) error {
}
func (dExt *DriverExt) GetLogs() (interface{}, error) {
log.Info().Msg("stop WDA log recording")
data := map[string]interface{}{"action": "stop"}
reply, err := dExt.triggerWDALog(data)
if err != nil {
log.Error().Err(err).Msg("failed to get WDA logs")
return "", errors.Wrap(err, "failed to get WDA logs")
log.Info().Msg("stop log recording")
if _, ok := dExt.Driver.(*wdaDriver); ok {
data := map[string]interface{}{"action": "stop"}
reply, err := dExt.triggerWDALog(data)
if err != nil {
return "", errors.Wrap(err, "failed to get WDA logs")
}
return reply.Value, nil
} else {
// TODO: Android log recording
}
return reply.Value, nil
return "", nil
}
func (dExt *DriverExt) triggerWDALog(data map[string]interface{}) (*wdaResponse, error) {

View File

@@ -13,6 +13,7 @@ import (
giDevice "github.com/electricbubble/gidevice"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v4/hrp/internal/json"
)
@@ -557,6 +558,33 @@ func (wd *wdaDriver) IOHIDEvent(pageID EventPageID, usageID EventUsageID, durati
return
}
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) {
// stop camera, alias for app_terminate com.apple.camera
success, err := wd.AppTerminate("com.apple.camera")
if err != nil {
return errors.Wrap(err, "failed to terminate camera")
}
if !success {
log.Warn().Msg("camera was not running")
}
return nil
}
func (wd *wdaDriver) StartRecording() (err error) {
// TODO
return errDriverNotImplemented
}
func (wd *wdaDriver) StopRecording() (err error) {
// TODO
return errDriverNotImplemented
}
func (wd *wdaDriver) ExpectNotification(notifyName string, notifyType NotificationType, second ...int) (err error) {
// [[FBRoute POST:@"/wda/expectNotification"] respondWithTarget:self action:@selector(handleExpectNotification:)]
if len(second) == 0 {

View File

@@ -1,3 +1,4 @@
//go:build localtest
package uixt
import (

View File

@@ -1,3 +1,5 @@
//go:build localtest
package uixt
import (

View File

@@ -1,3 +1,5 @@
//go:build localtest
package uixt
import (

View File

@@ -1,3 +1,5 @@
//go:build localtest
package uixt
import (

View File

@@ -215,7 +215,7 @@ func (r *HRPRunner) Run(testcases ...ITestCase) error {
s.appendCaseSummary(caseSummary)
if err1 != nil || err2 != nil {
log.Error().Err(err1).Msg("[Run] run testcase failed")
runErr = err
runErr = err1
break
}
}

View File

@@ -9,6 +9,13 @@ import (
"github.com/httprunner/httprunner/v4/hrp/internal/uixt"
)
var (
WithSerialNumber = uixt.WithSerialNumber
WithAdbIP = uixt.WithAdbIP
WithAdbPort = uixt.WithAdbPort
WithAdbLogOn = uixt.WithAdbLogOn
)
type AndroidStep struct {
uixt.AndroidDevice `yaml:",inline"` // inline refers to https://pkg.go.dev/gopkg.in/yaml.v3#Marshal
uixt.MobileAction
@@ -33,6 +40,38 @@ func (s *StepAndroid) InstallApp(path string) *StepAndroid {
return &StepAndroid{step: s.step}
}
func (s *StepAndroid) AppLaunch(bundleId string) *StepAndroid {
s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{
Method: uixt.AppLaunch,
Params: bundleId,
})
return s
}
func (s *StepAndroid) AppLaunchUnattached(bundleId string) *StepAndroid {
s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{
Method: uixt.AppLaunchUnattached,
Params: bundleId,
})
return s
}
func (s *StepAndroid) AppTerminate(bundleId string) *StepAndroid {
s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{
Method: uixt.AppTerminate,
Params: bundleId,
})
return s
}
func (s *StepAndroid) Home() *StepAndroid {
s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{
Method: uixt.ACTION_Home,
Params: nil,
})
return &StepAndroid{step: s.step}
}
func (s *StepAndroid) StartAppByIntent(activity string) *StepAndroid {
s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{
Method: uixt.AppStart,
@@ -81,51 +120,101 @@ func (s *StepAndroid) Tap(params interface{}) *StepAndroid {
return &StepAndroid{step: s.step}
}
func (s *StepAndroid) DoubleTap(params interface{}) *StepAndroid {
s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{
// Tap taps on the target element by OCR recognition
func (s *StepAndroid) TapByOCR(ocrText string, options ...uixt.ActionOption) *StepAndroid {
action := uixt.MobileAction{
Method: uixt.ACTION_TapByOCR,
Params: ocrText,
}
for _, option := range options {
option(&action)
}
s.step.Android.Actions = append(s.step.Android.Actions, action)
return &StepAndroid{step: s.step}
}
// Tap taps on the target element by CV recognition
func (s *StepAndroid) TapByCV(imagePath string, options ...uixt.ActionOption) *StepAndroid {
action := uixt.MobileAction{
Method: uixt.ACTION_TapByCV,
Params: imagePath,
}
for _, option := range options {
option(&action)
}
s.step.Android.Actions = append(s.step.Android.Actions, action)
return &StepAndroid{step: s.step}
}
func (s *StepAndroid) DoubleTap(params string, options ...uixt.ActionOption) *StepAndroid {
action := uixt.MobileAction{
Method: uixt.ACTION_DoubleTap,
Params: params,
})
}
for _, option := range options {
option(&action)
}
s.step.Android.Actions = append(s.step.Android.Actions, action)
return &StepAndroid{step: s.step}
}
func (s *StepAndroid) Swipe(sx, sy, ex, ey int) *StepAndroid {
s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{
func (s *StepAndroid) Swipe(sx, sy, ex, ey int, options ...uixt.ActionOption) *StepAndroid {
action := uixt.MobileAction{
Method: uixt.ACTION_Swipe,
Params: []int{sx, sy, ex, ey},
})
}
for _, option := range options {
option(&action)
}
s.step.Android.Actions = append(s.step.Android.Actions, action)
return &StepAndroid{step: s.step}
}
func (s *StepAndroid) SwipeUp() *StepAndroid {
s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{
func (s *StepAndroid) SwipeUp(options ...uixt.ActionOption) *StepAndroid {
action := uixt.MobileAction{
Method: uixt.ACTION_Swipe,
Params: "up",
})
}
for _, option := range options {
option(&action)
}
s.step.Android.Actions = append(s.step.Android.Actions, action)
return &StepAndroid{step: s.step}
}
func (s *StepAndroid) SwipeDown() *StepAndroid {
s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{
func (s *StepAndroid) SwipeDown(options ...uixt.ActionOption) *StepAndroid {
action := uixt.MobileAction{
Method: uixt.ACTION_Swipe,
Params: "down",
})
}
for _, option := range options {
option(&action)
}
s.step.Android.Actions = append(s.step.Android.Actions, action)
return &StepAndroid{step: s.step}
}
func (s *StepAndroid) SwipeLeft() *StepAndroid {
s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{
func (s *StepAndroid) SwipeLeft(options ...uixt.ActionOption) *StepAndroid {
action := uixt.MobileAction{
Method: uixt.ACTION_Swipe,
Params: "left",
})
}
for _, option := range options {
option(&action)
}
s.step.Android.Actions = append(s.step.Android.Actions, action)
return &StepAndroid{step: s.step}
}
func (s *StepAndroid) SwipeRight() *StepAndroid {
s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{
func (s *StepAndroid) SwipeRight(options ...uixt.ActionOption) *StepAndroid {
action := uixt.MobileAction{
Method: uixt.ACTION_Swipe,
Params: "right",
})
}
for _, option := range options {
option(&action)
}
s.step.Android.Actions = append(s.step.Android.Actions, action)
return &StepAndroid{step: s.step}
}
@@ -137,6 +226,47 @@ func (s *StepAndroid) Input(text string) *StepAndroid {
return &StepAndroid{step: s.step}
}
// Sleep specify sleep seconds after last action
func (s *StepAndroid) Sleep(n float64) *StepAndroid {
s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{
Method: uixt.CtlSleep,
Params: n,
})
return &StepAndroid{step: s.step}
}
func (s *StepAndroid) ScreenShot() *StepAndroid {
s.step.Android.Actions = append(s.step.Android.Actions, uixt.MobileAction{
Method: uixt.CtlScreenShot,
Params: nil,
})
return &StepAndroid{step: s.step}
}
func (s *StepAndroid) SwipeToTapApp(appName string, options ...uixt.ActionOption) *StepAndroid {
action := uixt.MobileAction{
Method: uixt.ACTION_SwipeToTapApp,
Params: appName,
}
for _, option := range options {
option(&action)
}
s.step.Android.Actions = append(s.step.Android.Actions, action)
return &StepAndroid{step: s.step}
}
func (s *StepAndroid) SwipeToTapText(text string, options ...uixt.ActionOption) *StepAndroid {
action := uixt.MobileAction{
Method: uixt.ACTION_SwipeToTapText,
Params: text,
}
for _, option := range options {
option(&action)
}
s.step.Android.Actions = append(s.step.Android.Actions, action)
return &StepAndroid{step: s.step}
}
// Validate switches to step validation.
func (s *StepAndroid) Validate() *StepAndroidValidation {
return &StepAndroidValidation{
@@ -195,6 +325,96 @@ func (s *StepAndroidValidation) AssertNameNotExists(expectedName string, msg ...
return s
}
func (s *StepAndroidValidation) AssertLabelExists(expectedLabel string, msg ...string) *StepAndroidValidation {
v := Validator{
Check: uixt.SelectorLabel,
Assert: uixt.AssertionExists,
Expect: expectedLabel,
}
if len(msg) > 0 {
v.Message = msg[0]
} else {
v.Message = fmt.Sprintf("attribute label [%s] not found", expectedLabel)
}
s.step.Validators = append(s.step.Validators, v)
return s
}
func (s *StepAndroidValidation) AssertLabelNotExists(expectedLabel string, msg ...string) *StepAndroidValidation {
v := Validator{
Check: uixt.SelectorLabel,
Assert: uixt.AssertionNotExists,
Expect: expectedLabel,
}
if len(msg) > 0 {
v.Message = msg[0]
} else {
v.Message = fmt.Sprintf("attribute label [%s] should not exist", expectedLabel)
}
s.step.Validators = append(s.step.Validators, v)
return s
}
func (s *StepAndroidValidation) AssertOCRExists(expectedText string, msg ...string) *StepAndroidValidation {
v := Validator{
Check: uixt.SelectorOCR,
Assert: uixt.AssertionExists,
Expect: expectedText,
}
if len(msg) > 0 {
v.Message = msg[0]
} else {
v.Message = fmt.Sprintf("ocr text [%s] not found", expectedText)
}
s.step.Validators = append(s.step.Validators, v)
return s
}
func (s *StepAndroidValidation) AssertOCRNotExists(expectedText string, msg ...string) *StepAndroidValidation {
v := Validator{
Check: uixt.SelectorOCR,
Assert: uixt.AssertionNotExists,
Expect: expectedText,
}
if len(msg) > 0 {
v.Message = msg[0]
} else {
v.Message = fmt.Sprintf("ocr text [%s] should not exist", expectedText)
}
s.step.Validators = append(s.step.Validators, v)
return s
}
func (s *StepAndroidValidation) AssertImageExists(expectedImagePath string, msg ...string) *StepAndroidValidation {
v := Validator{
Check: uixt.SelectorImage,
Assert: uixt.AssertionExists,
Expect: expectedImagePath,
}
if len(msg) > 0 {
v.Message = msg[0]
} else {
v.Message = fmt.Sprintf("cv image [%s] not found", expectedImagePath)
}
s.step.Validators = append(s.step.Validators, v)
return s
}
func (s *StepAndroidValidation) AssertImageNotExists(expectedImagePath string, msg ...string) *StepAndroidValidation {
v := Validator{
Check: uixt.SelectorImage,
Assert: uixt.AssertionNotExists,
Expect: expectedImagePath,
}
if len(msg) > 0 {
v.Message = msg[0]
} else {
v.Message = fmt.Sprintf("cv image [%s] should not exist", expectedImagePath)
}
s.step.Validators = append(s.step.Validators, v)
return s
}
func (s *StepAndroidValidation) Name() string {
return s.step.Name
}

View File

@@ -1,3 +1,5 @@
//go:build localtest
package hrp
import (

View File

@@ -1,3 +1,4 @@
//go:build localtest
package hrp
import (
@@ -85,7 +86,7 @@ func TestIOSWeixinLive(t *testing.T) {
NewStep("进入直播页").
IOS().
Tap("发现").Sleep(5). // 进入「发现页」;等待 5 秒确保加载完成
TapByOCR("直播"). // 通过 OCR 识别「直播」
TapByOCR("直播"). // 通过 OCR 识别「直播」
Validate().
AssertLabelExists("直播"),
NewStep("向上滑动 5 次").