Files
go-mobile-uiautomator/libs/selector.go
2025-11-08 18:58:20 +08:00

224 lines
7.0 KiB
Go

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)
}
}