Cthulhu is no longer a fork. Now it is a screen reader written in Golang.
This commit is contained in:
1 parent
a998fe67de
commit
069fc674dc
708 files changed
+112482
-532
No files matched your search
@@ -0,0 +1,137 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user