新增启动移动端服务方法
This commit is contained in:
@@ -182,3 +182,41 @@ func ReadResponse(conn net.Conn, debug bool) (string, []byte, error) {
|
|||||||
// unexpected token
|
// unexpected token
|
||||||
return st, nil, nil
|
return st, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LaunchUiautomator connects to adb server, transports to device, starts uiautomator2,
|
||||||
|
// and returns an io.Reader for streaming logs.
|
||||||
|
func LaunchUiautomator(addr, serial string) {
|
||||||
|
conn, err := DialADB(addr, 15*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("dial error:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// transport
|
||||||
|
adbSend(conn, "host:transport:"+serial)
|
||||||
|
resp, _ := adbReadResponse(conn)
|
||||||
|
fmt.Println("transport resp:", resp)
|
||||||
|
|
||||||
|
// shell
|
||||||
|
cmd := "shell:CLASSPATH=/data/local/tmp/u2.jar app_process / com.wetest.uia2.Main"
|
||||||
|
adbSend(conn, cmd)
|
||||||
|
resp, _ = adbReadResponse(conn)
|
||||||
|
fmt.Println("shell resp:", resp)
|
||||||
|
|
||||||
|
// 输出流
|
||||||
|
io.Copy(os.Stdout, conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func adbSend(conn net.Conn, cmd string) error {
|
||||||
|
length := fmt.Sprintf("%04x", len(cmd))
|
||||||
|
_, err := conn.Write([]byte(length + cmd))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func adbReadResponse(conn net.Conn) (string, error) {
|
||||||
|
buf := make([]byte, 4)
|
||||||
|
_, err := conn.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(buf), nil
|
||||||
|
}
|
||||||
|
|||||||
11
cmd/main.go
11
cmd/main.go
@@ -9,15 +9,16 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
addr := "127.0.0.1:5037"
|
||||||
|
serial := "emulator-5556"
|
||||||
|
FilePushInstall()
|
||||||
|
go adb.LaunchUiautomator(addr, serial)
|
||||||
|
select {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 文件推送加安装
|
// 文件推送加安装
|
||||||
func FilePushInstall() {
|
func FilePushInstall() {
|
||||||
addr := "127.0.0.1:5037"
|
addr := "127.0.0.1:5037"
|
||||||
// local := "./assets/app-uiautomator.apk"
|
|
||||||
// remote := "/sdcard/app-uiautomator.apk"
|
|
||||||
// mode := 0644
|
|
||||||
serial := "emulator-5556"
|
serial := "emulator-5556"
|
||||||
payload, _ := adb.ListDevicesRaw(addr, 15*time.Second)
|
payload, _ := adb.ListDevicesRaw(addr, 15*time.Second)
|
||||||
m := adb.ParseDevicesPayload(payload)
|
m := adb.ParseDevicesPayload(payload)
|
||||||
@@ -26,7 +27,7 @@ func FilePushInstall() {
|
|||||||
|
|
||||||
services.InstallServiceJar(addr, serial)
|
services.InstallServiceJar(addr, serial)
|
||||||
|
|
||||||
services.InstallServiceApk(addr, serial)
|
// services.InstallServiceApk(addr, serial)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 文件推送验证
|
// 文件推送验证
|
||||||
|
|||||||
223
libs/selector.go
Normal file
223
libs/selector.go
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
package libs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
MASK_TEXT uint32 = 0x00000001
|
||||||
|
MASK_TEXT_CONTAINS uint32 = 0x00000002
|
||||||
|
MASK_TEXT_MATCHES uint32 = 0x00000004
|
||||||
|
MASK_TEXT_STARTS_WITH uint32 = 0x00000008
|
||||||
|
MASK_CLASS_NAME uint32 = 0x00000010
|
||||||
|
MASK_CLASS_NAME_MATCHES uint32 = 0x00000020
|
||||||
|
MASK_DESCRIPTION uint32 = 0x00000040
|
||||||
|
MASK_DESCRIPTION_CONTAINS uint32 = 0x00000080
|
||||||
|
MASK_DESCRIPTION_MATCHES uint32 = 0x00000100
|
||||||
|
MASK_DESCRIPTION_STARTS_WITH uint32 = 0x00000200
|
||||||
|
MASK_CHECKABLE uint32 = 0x00000400
|
||||||
|
MASK_CHECKED uint32 = 0x00000800
|
||||||
|
MASK_CLICKABLE uint32 = 0x00001000
|
||||||
|
MASK_LONG_CLICKABLE uint32 = 0x00002000
|
||||||
|
MASK_SCROLLABLE uint32 = 0x00004000
|
||||||
|
MASK_ENABLED uint32 = 0x00008000
|
||||||
|
MASK_FOCUSABLE uint32 = 0x00010000
|
||||||
|
MASK_FOCUSED uint32 = 0x00020000
|
||||||
|
MASK_SELECTED uint32 = 0x00040000
|
||||||
|
MASK_PACKAGE_NAME uint32 = 0x00080000
|
||||||
|
MASK_PACKAGE_NAME_MATCHES uint32 = 0x00100000
|
||||||
|
MASK_RESOURCE_ID uint32 = 0x00200000
|
||||||
|
MASK_RESOURCE_ID_MATCHES uint32 = 0x00400000
|
||||||
|
MASK_INDEX uint32 = 0x00800000
|
||||||
|
MASK_INSTANCE uint32 = 0x01000000
|
||||||
|
)
|
||||||
|
|
||||||
|
// Relationship type for building child/sibling chains.
|
||||||
|
type relationType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
relChild relationType = "child"
|
||||||
|
relSibling relationType = "sibling"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Field metadata mirrors Python __fields: name -> (mask, default)
|
||||||
|
type fieldMeta struct {
|
||||||
|
Mask uint32
|
||||||
|
Default any
|
||||||
|
}
|
||||||
|
|
||||||
|
var allowedFields = map[string]fieldMeta{
|
||||||
|
"text": {MASK_TEXT, nil},
|
||||||
|
"textContains": {MASK_TEXT_CONTAINS, nil},
|
||||||
|
"textMatches": {MASK_TEXT_MATCHES, nil},
|
||||||
|
"textStartsWith": {MASK_TEXT_STARTS_WITH, nil},
|
||||||
|
"className": {MASK_CLASS_NAME, nil},
|
||||||
|
"classNameMatches": {MASK_CLASS_NAME_MATCHES, nil},
|
||||||
|
"description": {MASK_DESCRIPTION, nil},
|
||||||
|
"descriptionContains": {MASK_DESCRIPTION_CONTAINS, nil},
|
||||||
|
"descriptionMatches": {MASK_DESCRIPTION_MATCHES, nil},
|
||||||
|
"descriptionStartsWith": {MASK_DESCRIPTION_STARTS_WITH, nil},
|
||||||
|
"checkable": {MASK_CHECKABLE, false},
|
||||||
|
"checked": {MASK_CHECKED, false},
|
||||||
|
"clickable": {MASK_CLICKABLE, false},
|
||||||
|
"longClickable": {MASK_LONG_CLICKABLE, false},
|
||||||
|
"scrollable": {MASK_SCROLLABLE, false},
|
||||||
|
"enabled": {MASK_ENABLED, false},
|
||||||
|
"focusable": {MASK_FOCUSABLE, false},
|
||||||
|
"focused": {MASK_FOCUSED, false},
|
||||||
|
"selected": {MASK_SELECTED, false},
|
||||||
|
"packageName": {MASK_PACKAGE_NAME, nil},
|
||||||
|
"packageNameMatches": {MASK_PACKAGE_NAME_MATCHES, nil},
|
||||||
|
"resourceId": {MASK_RESOURCE_ID, nil},
|
||||||
|
"resourceIdMatches": {MASK_RESOURCE_ID_MATCHES, nil},
|
||||||
|
"index": {MASK_INDEX, 0},
|
||||||
|
"instance": {MASK_INSTANCE, 0},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Selector replicates Python Selector(dict) behavior with explicit APIs.
|
||||||
|
type Selector struct {
|
||||||
|
mask uint32
|
||||||
|
fields map[string]any // only allowedFields keys permitted
|
||||||
|
childOrSibling []relationType
|
||||||
|
childOrSiblingSelect []*Selector
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSelector initializes with kwargs like Python's Selector(**kwargs).
|
||||||
|
func NewSelector(kwargs map[string]any) (*Selector, error) {
|
||||||
|
s := &Selector{
|
||||||
|
mask: 0,
|
||||||
|
fields: make(map[string]any),
|
||||||
|
childOrSibling: make([]relationType, 0),
|
||||||
|
childOrSiblingSelect: make([]*Selector, 0),
|
||||||
|
}
|
||||||
|
for k, v := range kwargs {
|
||||||
|
if err := s.Set(k, v); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set assigns a field and updates the bitmask. Only allowed fields permitted.
|
||||||
|
func (s *Selector) Set(key string, val any) error {
|
||||||
|
meta, ok := allowedFields[key]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("%s is not allowed", key)
|
||||||
|
}
|
||||||
|
s.fields[key] = val
|
||||||
|
s.mask |= meta.Mask
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete removes a field and clears its bit in mask.
|
||||||
|
func (s *Selector) Delete(key string) error {
|
||||||
|
meta, ok := allowedFields[key]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("%s is not allowed", key)
|
||||||
|
}
|
||||||
|
delete(s.fields, key)
|
||||||
|
s.mask &^= meta.Mask // AND NOT
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get retrieves a field (nil if absent).
|
||||||
|
func (s *Selector) Get(key string) (any, bool) {
|
||||||
|
v, ok := s.fields[key]
|
||||||
|
return v, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mask returns current bit mask.
|
||||||
|
func (s *Selector) Mask() uint32 { return s.mask }
|
||||||
|
|
||||||
|
// Child appends a child selector (in-place) and returns the receiver.
|
||||||
|
func (s *Selector) Child(kwargs map[string]any) (*Selector, error) {
|
||||||
|
sub, err := NewSelector(kwargs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
s.childOrSibling = append(s.childOrSibling, relChild)
|
||||||
|
s.childOrSiblingSelect = append(s.childOrSiblingSelect, sub)
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sibling appends a sibling selector (in-place) and returns the receiver.
|
||||||
|
func (s *Selector) Sibling(kwargs map[string]any) (*Selector, error) {
|
||||||
|
sub, err := NewSelector(kwargs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
s.childOrSibling = append(s.childOrSibling, relSibling)
|
||||||
|
s.childOrSiblingSelect = append(s.childOrSiblingSelect, sub)
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone deep-copies selector, including child/sibling chains.
|
||||||
|
func (s *Selector) Clone() *Selector {
|
||||||
|
cp := &Selector{
|
||||||
|
mask: s.mask,
|
||||||
|
fields: make(map[string]any, len(s.fields)),
|
||||||
|
childOrSibling: make([]relationType, len(s.childOrSibling)),
|
||||||
|
childOrSiblingSelect: make([]*Selector, 0, len(s.childOrSiblingSelect)),
|
||||||
|
}
|
||||||
|
for k, v := range s.fields {
|
||||||
|
cp.fields[k] = v
|
||||||
|
}
|
||||||
|
copy(cp.childOrSibling, s.childOrSibling)
|
||||||
|
for _, sub := range s.childOrSiblingSelect {
|
||||||
|
cp.childOrSiblingSelect = append(cp.childOrSiblingSelect, sub.Clone())
|
||||||
|
}
|
||||||
|
return cp
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateInstance updates 'instance' on the last child/sibling selector if present;
|
||||||
|
// otherwise updates the current selector's 'instance'.
|
||||||
|
func (s *Selector) UpdateInstance(i int) error {
|
||||||
|
if len(s.childOrSiblingSelect) > 0 {
|
||||||
|
last := s.childOrSiblingSelect[len(s.childOrSiblingSelect)-1]
|
||||||
|
return last.Set("instance", i)
|
||||||
|
}
|
||||||
|
return s.Set("instance", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String prints Selector [k='v', ...] while skipping mask and empty child/sibling groups.
|
||||||
|
// Deterministic order for readability.
|
||||||
|
func (s *Selector) String() string {
|
||||||
|
parts := make([]string, 0, len(s.fields)+2)
|
||||||
|
|
||||||
|
// stable sort keys for consistent output
|
||||||
|
keys := make([]string, 0, len(s.fields))
|
||||||
|
for k := range s.fields {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
|
||||||
|
for _, k := range keys {
|
||||||
|
// repr-like rendering
|
||||||
|
parts = append(parts, fmt.Sprintf("%s=%q", k, fmtAny(s.fields[k])))
|
||||||
|
}
|
||||||
|
|
||||||
|
// child/sibling only if present
|
||||||
|
if len(s.childOrSibling) > 0 && len(s.childOrSiblingSelect) > 0 {
|
||||||
|
// render as chain: rel(type)->subselector
|
||||||
|
chunks := make([]string, 0, len(s.childOrSibling))
|
||||||
|
for i, rel := range s.childOrSibling {
|
||||||
|
chunks = append(chunks, fmt.Sprintf("%s(%s)", rel, s.childOrSiblingSelect[i].String()))
|
||||||
|
}
|
||||||
|
parts = append(parts, "chain=["+strings.Join(chunks, ", ")+"]")
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Selector [" + strings.Join(parts, ", ") + "]"
|
||||||
|
}
|
||||||
|
|
||||||
|
func fmtAny(v any) string {
|
||||||
|
switch t := v.(type) {
|
||||||
|
case string:
|
||||||
|
return t
|
||||||
|
case fmt.Stringer:
|
||||||
|
return t.String()
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("%v", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
8
test/test.py
Normal file
8
test/test.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import uiautomator2 as u2
|
||||||
|
|
||||||
|
d = u2.connect('emulator-5556')
|
||||||
|
# d.app_start('com.android.chrome', stop=True) # Start Bilibili
|
||||||
|
|
||||||
|
# d.implicitly_wait(10.0)
|
||||||
|
|
||||||
|
# d(text="向设备添加账号").click()
|
||||||
Reference in New Issue
Block a user