187 lines
4.7 KiB
Go
187 lines
4.7 KiB
Go
package uiaselector
|
|
|
|
import "strings"
|
|
|
|
type MatchResult struct {
|
|
OK bool `json:"ok"`
|
|
SelectorID string `json:"selector_id"`
|
|
Matched bool `json:"matched"`
|
|
MatchCount int `json:"match_count"`
|
|
Matches []NodeMatch `json:"matches"`
|
|
Warnings []string `json:"warnings"`
|
|
Error MatchError `json:"error,omitempty"`
|
|
}
|
|
|
|
type MatchError struct {
|
|
Code string `json:"code"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
type NodeMatch struct {
|
|
Path string `json:"path"`
|
|
ControlType string `json:"control_type"`
|
|
AutomationID string `json:"automation_id"`
|
|
ClassName string `json:"class_name"`
|
|
FrameworkID string `json:"framework_id"`
|
|
HasName bool `json:"has_name"`
|
|
NameLength int `json:"name_length"`
|
|
Name string `json:"-"`
|
|
}
|
|
|
|
type nodeRef struct {
|
|
path string
|
|
node *Node
|
|
}
|
|
|
|
func MatchSelector(root *Node, catalog []Selector, selectorID string) MatchResult {
|
|
selector, ok := findSelector(catalog, selectorID)
|
|
if !ok {
|
|
return MatchResult{OK: false, SelectorID: selectorID, Error: MatchError{Code: "UNKNOWN_SELECTOR", Message: "selector is not in catalog"}}
|
|
}
|
|
if selector.AllowedUse != AllowedUseReadOnlyLocate {
|
|
return MatchResult{OK: false, SelectorID: selectorID, Error: MatchError{Code: "INVALID_SELECTOR", Message: "selector is not read-only"}}
|
|
}
|
|
refs := flatten(root)
|
|
matches := matchCriteria(refs, selector.Required)
|
|
if len(matches) == 0 {
|
|
for _, fallback := range selector.Fallback {
|
|
matches = matchFallback(refs, fallback)
|
|
if len(matches) > 0 {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
result := MatchResult{OK: true, SelectorID: selectorID, Matched: len(matches) > 0, MatchCount: len(matches), Warnings: []string{}}
|
|
for _, ref := range matches {
|
|
result.Matches = append(result.Matches, summarizeNode(ref))
|
|
}
|
|
if !result.Matched {
|
|
result.Warnings = append(result.Warnings, "selector_not_found")
|
|
}
|
|
return result
|
|
}
|
|
|
|
func findSelector(catalog []Selector, selectorID string) (Selector, bool) {
|
|
for _, selector := range catalog {
|
|
if selector.ID == selectorID {
|
|
return selector, true
|
|
}
|
|
}
|
|
return Selector{}, false
|
|
}
|
|
|
|
func flatten(root *Node) []nodeRef {
|
|
refs := []nodeRef{}
|
|
var walk func(node *Node, path string)
|
|
walk = func(node *Node, path string) {
|
|
if node == nil {
|
|
return
|
|
}
|
|
refs = append(refs, nodeRef{path: path, node: node})
|
|
for i := range node.Children {
|
|
walk(&node.Children[i], path+"/"+itoa(i))
|
|
}
|
|
}
|
|
walk(root, "root")
|
|
return refs
|
|
}
|
|
|
|
func itoa(value int) string {
|
|
if value == 0 {
|
|
return "0"
|
|
}
|
|
digits := []byte{}
|
|
for value > 0 {
|
|
digits = append([]byte{byte('0' + value%10)}, digits...)
|
|
value = value / 10
|
|
}
|
|
return string(digits)
|
|
}
|
|
|
|
func matchCriteria(refs []nodeRef, criteria Criteria) []nodeRef {
|
|
matches := []nodeRef{}
|
|
for _, ref := range refs {
|
|
if criteria.AutomationID != "" && ref.node.AutomationID != criteria.AutomationID {
|
|
continue
|
|
}
|
|
if criteria.ControlType != "" && ref.node.ControlType != criteria.ControlType {
|
|
continue
|
|
}
|
|
if criteria.FrameworkID != "" && ref.node.FrameworkID != criteria.FrameworkID {
|
|
continue
|
|
}
|
|
if criteria.ClassName != "" && ref.node.ClassName != criteria.ClassName {
|
|
continue
|
|
}
|
|
if criteria.ClassPrefix != "" && !strings.HasPrefix(ref.node.ClassName, criteria.ClassPrefix) {
|
|
continue
|
|
}
|
|
matches = append(matches, ref)
|
|
}
|
|
return matches
|
|
}
|
|
|
|
func matchFallback(refs []nodeRef, fallback FallbackCriteria) []nodeRef {
|
|
matches := []nodeRef{}
|
|
for _, ref := range refs {
|
|
if fallback.ControlType != "" && ref.node.ControlType != fallback.ControlType {
|
|
continue
|
|
}
|
|
if fallback.ClassPrefix != "" && !strings.HasPrefix(ref.node.ClassName, fallback.ClassPrefix) {
|
|
continue
|
|
}
|
|
if !hasChildAutomationIDs(ref.node, fallback.ChildAutomationIDContains) {
|
|
continue
|
|
}
|
|
if !hasChildControlTypes(ref.node, fallback.ChildControlTypeContains) {
|
|
continue
|
|
}
|
|
matches = append(matches, ref)
|
|
}
|
|
return matches
|
|
}
|
|
|
|
func hasChildAutomationIDs(node *Node, ids []string) bool {
|
|
for _, id := range ids {
|
|
found := false
|
|
for i := range node.Children {
|
|
if node.Children[i].AutomationID == id {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func hasChildControlTypes(node *Node, controlTypes []string) bool {
|
|
for _, controlType := range controlTypes {
|
|
found := false
|
|
for i := range node.Children {
|
|
if node.Children[i].ControlType == controlType {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func summarizeNode(ref nodeRef) NodeMatch {
|
|
return NodeMatch{
|
|
Path: ref.path,
|
|
ControlType: ref.node.ControlType,
|
|
AutomationID: ref.node.AutomationID,
|
|
ClassName: ref.node.ClassName,
|
|
FrameworkID: ref.node.FrameworkID,
|
|
HasName: ref.node.Name != "",
|
|
NameLength: len(ref.node.Name),
|
|
}
|
|
}
|