138 lines
5.0 KiB
Go
138 lines
5.0 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// Authors: Storm Dragon, Angus Kola
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package review
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
"unicode/utf8"
|
|
|
|
"git.stormux.org/storm/cthulhu/internal/atspi"
|
|
)
|
|
|
|
func glyph_cursor(t *testing.T, parentRole string, modify func(*atspi.Snapshot)) (*DocumentCursor, caretTextReader) {
|
|
t.Helper()
|
|
reader := caretTextReader{webID("/before"): "Member", webID("/icon"): "\uf005", webID("/after"): "Join Date"}
|
|
node := func(path, role string, children ...string) atspi.TreeNode {
|
|
text := reader[webID(path)]
|
|
snapshot := atspi.NewSnapshot(path, text, role, "", text)
|
|
snapshot.BusName, snapshot.StatesKnown = webID(path).BusName, true
|
|
snapshot.CharacterCount = utf8.RuneCountInString(text)
|
|
result := atspi.TreeNode{Snapshot: snapshot}
|
|
for _, child := range children {
|
|
result.Children = append(result.Children, webID(child))
|
|
}
|
|
return result
|
|
}
|
|
root := node("/doc", "document web", "/before", "/rank", "/after")
|
|
root.Snapshot.Focusable = true // A focusable document must not protect every icon.
|
|
parent := node("/rank", parentRole, "/icon")
|
|
if parentRole == "pre" || parentRole == "code" {
|
|
parent.Snapshot.HTMLTag = parentRole
|
|
}
|
|
icon := node("/icon", "static")
|
|
if modify != nil {
|
|
modify(&icon.Snapshot)
|
|
}
|
|
tree, err := atspi.NewAccessibleTree(webID("/doc"), []atspi.TreeNode{
|
|
root, node("/before", "static"), parent, icon, node("/after", "static"),
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cursor, err := NewDocumentCursor(tree)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := cursor.SyncFocus(context.Background(), webID("/before")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return cursor, reader
|
|
}
|
|
|
|
func TestDocumentCaretSkipsDecorativeGlyph(t *testing.T) {
|
|
ctx := context.Background()
|
|
for _, command := range []DocumentCaretCommand{CaretNextLine, CaretNextCharacter, CaretNextWord} {
|
|
t.Run(command.String(), func(t *testing.T) {
|
|
cursor, reader := glyph_cursor(t, "section", nil)
|
|
if command != CaretNextLine {
|
|
cursor.places[0].offset = len("Member") - 1
|
|
}
|
|
move, err := cursor.MoveCaret(ctx, reader, command)
|
|
if err != nil || !move.Moved || move.Position.Object.ID() != webID("/after") {
|
|
t.Fatalf("forward: %+v, %v", move, err)
|
|
}
|
|
move, err = cursor.MoveCaret(ctx, reader, CaretPreviousLine)
|
|
if err != nil || !move.Moved || move.Text != "Member" {
|
|
t.Fatalf("backward: %+v, %v", move, err)
|
|
}
|
|
})
|
|
}
|
|
// Focus/routing may leave the cursor on an omitted icon. Both directions
|
|
// must still reach surrounding text rather than report a stale position.
|
|
for _, command := range []DocumentCaretCommand{CaretNextLine, CaretPreviousLine} {
|
|
cursor, reader := glyph_cursor(t, "section", nil)
|
|
if _, err := cursor.SyncFocus(ctx, webID("/icon")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
move, err := cursor.MoveCaret(ctx, reader, command)
|
|
want := "Join Date"
|
|
if command == CaretPreviousLine {
|
|
want = "Member"
|
|
}
|
|
if err != nil || !move.Moved || move.Text != want {
|
|
t.Fatalf("from icon %s: %+v, %v", command, move, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDocumentCaretPreservesMeaningfulGlyphs(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name, parentRole string
|
|
modify func(*atspi.Snapshot)
|
|
}{
|
|
{"link", "link", nil},
|
|
{"code", "code", nil},
|
|
{"pre", "pre", nil},
|
|
{"focusable", "section", func(s *atspi.Snapshot) { s.Focusable = true }},
|
|
{"clickable", "section", func(s *atspi.Snapshot) { s.SupportsClick = true }},
|
|
{"editable", "section", func(s *atspi.Snapshot) { s.Editable, s.Role = true, "entry" }},
|
|
{"named", "section", func(s *atspi.Snapshot) { s.ExplicitName = true }},
|
|
{"description", "section", func(s *atspi.Snapshot) { s.Description = "Rating" }},
|
|
{"unknown states", "section", func(s *atspi.Snapshot) { s.StatesKnown = false }},
|
|
{"ordinary symbol", "section", func(s *atspi.Snapshot) { s.Name, s.Text = "★", "★" }},
|
|
{"mixed text", "section", func(s *atspi.Snapshot) { s.Name, s.Text, s.CharacterCount = "Star \uf005", "Star \uf005", 6 }},
|
|
{"different text", "section", func(s *atspi.Snapshot) { s.Text = "X" }},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
cursor, reader := glyph_cursor(t, test.parentRole, test.modify)
|
|
move, err := cursor.MoveCaret(context.Background(), reader, CaretNextLine)
|
|
if err != nil || !move.Moved || move.Position.Object.ID() != webID("/icon") {
|
|
t.Fatalf("meaningful glyph omitted: %+v, %v", move, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestReadAllSkipsDecorativeGlyph(t *testing.T) {
|
|
cursor, reader := glyph_cursor(t, "section", nil)
|
|
capturer := &readAllCapturer{tree: cursor.tree, caretTextReader: reader}
|
|
worker, err := NewWorker(capturer, time.Second)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
worker.SyncFocus(webID("/before"))
|
|
result := worker.readAll(context.Background(), cursor)
|
|
if result.Err != nil || len(result.ReadAllLines) != 2 {
|
|
t.Fatalf("Read All: %+v", result)
|
|
}
|
|
for index, want := range []string{"Member", "Join Date"} {
|
|
if got := result.ReadAllLines[index].Contents[0].Text; got != want {
|
|
t.Fatalf("line %d: %q, want %q", index, got, want)
|
|
}
|
|
}
|
|
}
|