mirror of
https://github.com/httprunner/httprunner.git
synced 2026-09-06 16:07:45 +08:00
feat: 新增ui2控件点击
This commit is contained in:
@@ -34,56 +34,6 @@ type adbDriver struct {
|
|||||||
logcat *AdbLogcat
|
logcat *AdbLogcat
|
||||||
}
|
}
|
||||||
|
|
||||||
type Hierarchy struct {
|
|
||||||
XMLName xml.Name `xml:"hierarchy"`
|
|
||||||
Nodes []Node `xml:"node"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Node struct {
|
|
||||||
Index string `xml:"index,attr"`
|
|
||||||
Text string `xml:"text,attr"`
|
|
||||||
ResourceID string `xml:"resource-id,attr"`
|
|
||||||
Class string `xml:"class,attr"`
|
|
||||||
Package string `xml:"package,attr"`
|
|
||||||
ContentDesc string `xml:"content-desc,attr"`
|
|
||||||
Checkable string `xml:"checkable,attr"`
|
|
||||||
Checked string `xml:"checked,attr"`
|
|
||||||
Clickable string `xml:"clickable,attr"`
|
|
||||||
Enabled string `xml:"enabled,attr"`
|
|
||||||
Focusable string `xml:"focusable,attr"`
|
|
||||||
Focused string `xml:"focused,attr"`
|
|
||||||
Scrollable string `xml:"scrollable,attr"`
|
|
||||||
LongClickable string `xml:"long-clickable,attr"`
|
|
||||||
Password string `xml:"password,attr"`
|
|
||||||
Selected string `xml:"selected,attr"`
|
|
||||||
Bounds *Bounds `xml:"bounds,attr"`
|
|
||||||
Children []Node `xml:"node"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Bounds struct {
|
|
||||||
X1, Y1, X2, Y2 int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Bounds) UnmarshalXMLAttr(attr xml.Attr) error {
|
|
||||||
// 正则表达式用于解析格式为"[x1,y1][x2,y2]"
|
|
||||||
re := regexp.MustCompile(`\[(\d+),(\d+)]\[(\d+),(\d+)]`)
|
|
||||||
matches := re.FindStringSubmatch(attr.Value)
|
|
||||||
if matches == nil {
|
|
||||||
return fmt.Errorf("bounds format is incorrect")
|
|
||||||
}
|
|
||||||
// 转换字符串为整数
|
|
||||||
b.X1, _ = strconv.Atoi(matches[1])
|
|
||||||
b.Y1, _ = strconv.Atoi(matches[2])
|
|
||||||
b.X2, _ = strconv.Atoi(matches[3])
|
|
||||||
b.Y2, _ = strconv.Atoi(matches[4])
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Center 方法计算并返回 Bounds 中心点的坐标
|
|
||||||
func (b *Bounds) Center() (float64, float64) {
|
|
||||||
return float64(b.X1+b.X2) / 2, float64(b.Y1+b.Y2) / 2
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewAdbDriver() *adbDriver {
|
func NewAdbDriver() *adbDriver {
|
||||||
log.Info().Msg("init adb driver")
|
log.Info().Msg("init adb driver")
|
||||||
return &adbDriver{}
|
return &adbDriver{}
|
||||||
@@ -532,6 +482,11 @@ func (ad *adbDriver) Screenshot() (raw *bytes.Buffer, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ad *adbDriver) Source(srcOpt ...SourceOption) (source string, err error) {
|
func (ad *adbDriver) Source(srcOpt ...SourceOption) (source string, err error) {
|
||||||
|
_, err = ad.adbClient.RunShellCommand("rm", "-rf", "/sdcard/window_dump.xml")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 高版本报错 ERROR: null root node returned by UiTestAutomationBridge.
|
||||||
_, err = ad.adbClient.RunShellCommand("uiautomator", "dump")
|
_, err = ad.adbClient.RunShellCommand("uiautomator", "dump")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
@@ -562,7 +517,7 @@ func (ad *adbDriver) TapByText(text string, options ...ActionOption) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ad *adbDriver) tapByTextUsingHierarchy(hierarchy *Hierarchy, text string, options ...ActionOption) error {
|
func (ad *adbDriver) tapByTextUsingHierarchy(hierarchy *Hierarchy, text string, options ...ActionOption) error {
|
||||||
bounds := ad.searchNodes(hierarchy.Nodes, text, options...)
|
bounds := ad.searchNodes(hierarchy.Layout, text, options...)
|
||||||
actionOptions := NewActionOptions(options...)
|
actionOptions := NewActionOptions(options...)
|
||||||
if len(bounds) == 0 {
|
if len(bounds) == 0 {
|
||||||
if actionOptions.IgnoreNotFoundError {
|
if actionOptions.IgnoreNotFoundError {
|
||||||
@@ -596,11 +551,11 @@ func (ad *adbDriver) TapByTexts(actions ...TapTextAction) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ad *adbDriver) searchNodes(nodes []Node, text string, options ...ActionOption) []Bounds {
|
func (ad *adbDriver) searchNodes(nodes []Layout, text string, options ...ActionOption) []Bounds {
|
||||||
actionOptions := NewActionOptions(options...)
|
actionOptions := NewActionOptions(options...)
|
||||||
var results []Bounds
|
var results []Bounds
|
||||||
for _, node := range nodes {
|
for _, node := range nodes {
|
||||||
result := ad.searchNodes(node.Children, text, options...)
|
result := ad.searchNodes(node.Layout, text, options...)
|
||||||
results = append(results, result...)
|
results = append(results, result...)
|
||||||
if actionOptions.Regex {
|
if actionOptions.Regex {
|
||||||
// regex on, check if match regex
|
// regex on, check if match regex
|
||||||
@@ -610,7 +565,7 @@ func (ad *adbDriver) searchNodes(nodes []Node, text string, options ...ActionOpt
|
|||||||
} else {
|
} else {
|
||||||
// regex off, check if match exactly
|
// regex off, check if match exactly
|
||||||
if node.Text != text {
|
if node.Text != text {
|
||||||
ad.searchNodes(node.Children, text, options...)
|
ad.searchNodes(node.Layout, text, options...)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package uixt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Define a common struct for shared attributes
|
||||||
|
type Attributes struct {
|
||||||
|
Index int `xml:"index,attr"`
|
||||||
|
Package string `xml:"package,attr"`
|
||||||
|
Class string `xml:"class,attr"`
|
||||||
|
Text string `xml:"text,attr"`
|
||||||
|
ResourceId string `xml:"resource-id,attr"`
|
||||||
|
Checkable bool `xml:"checkable,attr"`
|
||||||
|
Checked bool `xml:"checked,attr"`
|
||||||
|
Clickable bool `xml:"clickable,attr"`
|
||||||
|
Enabled bool `xml:"enabled,attr"`
|
||||||
|
Focusable bool `xml:"focusable,attr"`
|
||||||
|
Focused bool `xml:"focused,attr"`
|
||||||
|
LongClickable bool `xml:"long-clickable,attr"`
|
||||||
|
Password bool `xml:"password,attr"`
|
||||||
|
Scrollable bool `xml:"scrollable,attr"`
|
||||||
|
Selected bool `xml:"selected,attr"`
|
||||||
|
Bounds *Bounds `xml:"bounds,attr"`
|
||||||
|
Displayed bool `xml:"displayed,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Hierarchy struct {
|
||||||
|
XMLName xml.Name `xml:"hierarchy"`
|
||||||
|
Attributes
|
||||||
|
Layout []Layout `xml:",any"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Layout struct {
|
||||||
|
Attributes
|
||||||
|
Layout []Layout `xml:",any"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Bounds struct {
|
||||||
|
X1, Y1, X2, Y2 int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bounds) Center() (float64, float64) {
|
||||||
|
return float64(b.X1+b.X2) / 2, float64(b.Y1+b.Y2) / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bounds) UnmarshalXMLAttr(attr xml.Attr) error {
|
||||||
|
// 正则表达式用于解析格式为"[x1,y1][x2,y2]"
|
||||||
|
re := regexp.MustCompile(`\[(\d+),(\d+)]\[(\d+),(\d+)]`)
|
||||||
|
matches := re.FindStringSubmatch(attr.Value)
|
||||||
|
if matches == nil {
|
||||||
|
return fmt.Errorf("bounds format is incorrect")
|
||||||
|
}
|
||||||
|
// 转换字符串为整数
|
||||||
|
b.X1, _ = strconv.Atoi(matches[1])
|
||||||
|
b.Y1, _ = strconv.Atoi(matches[2])
|
||||||
|
b.X2, _ = strconv.Atoi(matches[3])
|
||||||
|
b.Y2, _ = strconv.Atoi(matches[4])
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -132,6 +132,18 @@ func TestDriver_Source(t *testing.T) {
|
|||||||
t.Log(source)
|
t.Log(source)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDriver_TapByText(t *testing.T) {
|
||||||
|
driver, err := NewUIADriver(nil, uiaServerURL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = driver.TapByText("安装")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDriver_BatteryInfo(t *testing.T) {
|
func TestDriver_BatteryInfo(t *testing.T) {
|
||||||
driver, err := NewUIADriver(nil, uiaServerURL)
|
driver, err := NewUIADriver(nil, uiaServerURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -599,10 +600,35 @@ func (ud *uiaDriver) Source(srcOpt ...SourceOption) (source string, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ud *uiaDriver) sourceTree(srcOpt ...SourceOption) (sourceTree *Hierarchy, err error) {
|
||||||
|
source, err := ud.Source()
|
||||||
|
sourceTree = new(Hierarchy)
|
||||||
|
err = xml.Unmarshal([]byte(source), sourceTree)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func (ud *uiaDriver) TapByText(text string, options ...ActionOption) error {
|
func (ud *uiaDriver) TapByText(text string, options ...ActionOption) error {
|
||||||
return ud.adbDriver.TapByText(text, options...)
|
sourceTree, err := ud.sourceTree()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return ud.tapByTextUsingHierarchy(sourceTree, text, options...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ud *uiaDriver) TapByTexts(actions ...TapTextAction) error {
|
func (ud *uiaDriver) TapByTexts(actions ...TapTextAction) error {
|
||||||
return ud.adbDriver.TapByTexts(actions...)
|
sourceTree, err := ud.sourceTree()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, action := range actions {
|
||||||
|
err := ud.tapByTextUsingHierarchy(sourceTree, action.Text, action.Options...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user