package atspi import ( "context" "errors" "strings" "testing" "unicode/utf8" ) func TestBrailleCaptureKeepsTruncatedUnicodeOrigin(t *testing.T) { raw := strings.Repeat("猫", 2000) + " é" var snapshot Snapshot captureBrailleLine(&snapshot, raw, raw[:len(raw)-len("é")], 19) removed := utf8.RuneCountInString(raw) - utf8.RuneCountInString(snapshot.BrailleLine) if snapshot.BrailleLineStart != 19+removed || snapshot.BrailleCaret != 2021 || !strings.HasSuffix(snapshot.BrailleLine, " é") { t.Fatalf("origin=%d caret=%d", snapshot.BrailleLineStart, snapshot.BrailleCaret) } } func TestBrailleRouteRejectsChangedLineAndFocus(t *testing.T) { s := NewSnapshot("/entry", "Message", "entry", "", "é猫") s.BusName, s.TextCaretKnown = ":1.42", true s.StatesKnown, s.Focused = true, true captureBrailleLine(&s, "é猫", "é", 12) route := BrailleRoute{Focus: s.ID(), Line: "é猫", Start: 12, Offset: 14} if !validBrailleRoute(s, route) { t.Fatal("valid EOL route rejected") } for _, change := range []func(*Snapshot){ func(s *Snapshot) { s.BusName = ":1.43" }, func(s *Snapshot) { s.Path = "/other" }, func(s *Snapshot) { s.BrailleLine = "ab" }, func(s *Snapshot) { s.BrailleLineStart++ }, func(s *Snapshot) { s.Defunct = true }, func(s *Snapshot) { s.Focused = false }, func(s *Snapshot) { s.StatesKnown = false }, func(s *Snapshot) { s.Role = "password text" }, func(s *Snapshot) { s.TextCaretKnown = false }, } { changed := s change(&changed) if validBrailleRoute(changed, route) { t.Fatalf("accepted stale/protected route %+v", changed) } } for _, offset := range []int{-1, 11, 15} { route.Offset = offset if validBrailleRoute(s, route) { t.Fatalf("accepted offset %d", offset) } } } func assertNativeBrailleRouting(t *testing.T, ctx context.Context, adapter *Adapter) { t.Helper() wait := func(name string, offset int) Snapshot { t.Helper() for { select { case e := <-adapter.Events(): if e.Source.Name != name { continue } if offset < 0 && (e.Type == FocusEvent || e.Type == FocusedStateEvent && e.Detail1 == 1) { return e.Source } if offset >= 0 && e.Type == CaretEvent && e.Source.BrailleCaret == offset { return e.Source } case <-ctx.Done(): t.Fatalf("missing native braille event for %s offset %d", name, offset) } } } pressTreeProbeKey(t, "F6") s := wait("Caret review probe", -1) if !s.TextCaretKnown || s.BrailleLine != "Caret 猫 text" { t.Fatalf("native braille snapshot %+v", s) } route := BrailleRoute{Focus: s.ID(), Line: s.BrailleLine, Start: s.BrailleLineStart} for _, offset := range []int{1, 6, 7, 12, 6} { route.Offset = offset if err := adapter.RouteBraille(ctx, route); err != nil { t.Fatalf("route offset %d: %v", offset, err) } wait("Caret review probe", offset) } waitLine := func(line string) { t.Helper() for { select { case event := <-adapter.Events(): if event.Source.ID() == s.ID() && event.Source.BrailleLine == line { return } case <-ctx.Done(): t.Fatal("edited braille line was not captured") } } } pressTreeProbeKey(t, "z") waitLine("Caret z猫 text") if err := adapter.RouteBraille(ctx, route); !errors.Is(err, ErrStaleObject) { t.Fatalf("stale text route: %v", err) } pressTreeProbeKey(t, "BackSpace") waitLine("Caret 猫 text") pressTreeProbeKey(t, "F7") secret := wait("Protected text probe", -1) if secret.TextCaretKnown || secret.BrailleLine != "" { t.Fatal("captured protected braille text") } if err := adapter.RouteBraille(ctx, route); !errors.Is(err, ErrStaleObject) { t.Fatalf("old focus route: %v", err) } pressTreeProbeKey(t, "F8") wait("Numeric value probe", -1) }