Files
cthulhu/internal/review/document_caret_glyph_test.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)
}
}
}