feat: load N13 UIA dump fixture
This commit is contained in:
109
internal/uiaselector/dump.go
Normal file
109
internal/uiaselector/dump.go
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
package uiaselector
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DumpResponse struct {
|
||||||
|
OK bool `json:"ok"`
|
||||||
|
Data *DumpData `json:"data"`
|
||||||
|
Error any `json:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DumpData struct {
|
||||||
|
Root *Node `json:"root"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Node struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
ControlType string `json:"control_type"`
|
||||||
|
AutomationID string `json:"automation_id"`
|
||||||
|
ClassName string `json:"class_name"`
|
||||||
|
FrameworkID string `json:"framework_id"`
|
||||||
|
IsEnabled *bool `json:"is_enabled"`
|
||||||
|
IsOffscreen *bool `json:"is_offscreen"`
|
||||||
|
Bounds any `json:"bounds"`
|
||||||
|
Children []Node `json:"children"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadDumpFile(path string) (*Node, error) {
|
||||||
|
content, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
content = normalizeDumpJSON(content)
|
||||||
|
var response DumpResponse
|
||||||
|
if err := json.Unmarshal(content, &response); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !response.OK {
|
||||||
|
return nil, fmt.Errorf("dump response ok=false: %v", response.Error)
|
||||||
|
}
|
||||||
|
if response.Data == nil || response.Data.Root == nil {
|
||||||
|
return nil, fmt.Errorf("dump response has no data.root")
|
||||||
|
}
|
||||||
|
return response.Data.Root, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CountNodes(root *Node) int {
|
||||||
|
if root == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
count := 1
|
||||||
|
for i := range root.Children {
|
||||||
|
count += CountNodes(&root.Children[i])
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeDumpJSON(content []byte) []byte {
|
||||||
|
content = bytes.TrimPrefix(content, []byte{0xEF, 0xBB, 0xBF})
|
||||||
|
source := string(content)
|
||||||
|
var out strings.Builder
|
||||||
|
out.Grow(len(source))
|
||||||
|
|
||||||
|
inString := false
|
||||||
|
escaped := false
|
||||||
|
for i := 0; i < len(source); {
|
||||||
|
ch := source[i]
|
||||||
|
if inString {
|
||||||
|
out.WriteByte(ch)
|
||||||
|
if escaped {
|
||||||
|
escaped = false
|
||||||
|
} else if ch == '\\' {
|
||||||
|
escaped = true
|
||||||
|
} else if ch == '"' {
|
||||||
|
inString = false
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ch == '"' {
|
||||||
|
inString = true
|
||||||
|
out.WriteByte(ch)
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(source[i:], "-Infinity"):
|
||||||
|
out.WriteString("null")
|
||||||
|
i += len("-Infinity")
|
||||||
|
case strings.HasPrefix(source[i:], "Infinity"):
|
||||||
|
out.WriteString("null")
|
||||||
|
i += len("Infinity")
|
||||||
|
case strings.HasPrefix(source[i:], "NaN"):
|
||||||
|
out.WriteString("null")
|
||||||
|
i += len("NaN")
|
||||||
|
default:
|
||||||
|
out.WriteByte(ch)
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return []byte(out.String())
|
||||||
|
}
|
||||||
19
internal/uiaselector/dump_test.go
Normal file
19
internal/uiaselector/dump_test.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package uiaselector
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestLoadDumpFixtureParsesAcceptedN12RTree(t *testing.T) {
|
||||||
|
root, err := LoadDumpFile("testdata/n12r-2026-07-09-uia-redacted.json")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("LoadDumpFile: %v", err)
|
||||||
|
}
|
||||||
|
if root.AutomationID != "frmMain" {
|
||||||
|
t.Fatalf("root automation_id = %q, want frmMain", root.AutomationID)
|
||||||
|
}
|
||||||
|
if root.ControlType != "Window" {
|
||||||
|
t.Fatalf("root control_type = %q, want Window", root.ControlType)
|
||||||
|
}
|
||||||
|
if got := CountNodes(root); got != 66 {
|
||||||
|
t.Fatalf("CountNodes = %d, want 66", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user