mirror of
https://github.com/httprunner/httprunner.git
synced 2026-05-11 18:11:21 +08:00
Merge pull request #1477 from xucong053/dev-v4.3-android
refactor: android ui automation
This commit is contained in:
6
.github/workflows/hrp-scaffold.yml
vendored
6
.github/workflows/hrp-scaffold.yml
vendored
@@ -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:
|
||||
|
||||
2
.github/workflows/smoketest.yml
vendored
2
.github/workflows/smoketest.yml
vendored
@@ -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:
|
||||
|
||||
2
.github/workflows/unittest.yml
vendored
2
.github/workflows/unittest.yml
vendored
@@ -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 }}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
57
examples/uitest/demo_android_douyin_test.go
Normal file
57
examples/uitest/demo_android_douyin_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build localtest
|
||||
|
||||
package uitest
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build localtest
|
||||
|
||||
package uitest
|
||||
|
||||
import (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}}">×</a>
|
||||
<div class="content">
|
||||
<pre>{{ .Attachment }}</pre>
|
||||
<pre>{{ .Attachments }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build localtest
|
||||
|
||||
package uixt
|
||||
|
||||
import (
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
//go:build localtest
|
||||
package uixt
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build localtest
|
||||
|
||||
package uixt
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build localtest
|
||||
|
||||
package uixt
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build localtest
|
||||
|
||||
package uixt
|
||||
|
||||
import (
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build localtest
|
||||
|
||||
package hrp
|
||||
|
||||
import (
|
||||
|
||||
@@ -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 次").
|
||||
|
||||
Reference in New Issue
Block a user