Files

194 lines
6.3 KiB
Go

// Cthulhu screen reader.
// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along with
// this program. If not, see <https://www.gnu.org/licenses/gpl-3.0.html>.
package review
import (
"errors"
"sync"
"testing"
"git.stormux.org/storm/cthulhu/internal/atspi"
)
func TestDirectionNames(t *testing.T) {
tests := map[Direction]string{
Parent: "parent",
FirstChild: "first-child",
PreviousSibling: "previous-sibling",
NextSibling: "next-sibling",
Direction(99): "direction(99)",
}
for direction, expected := range tests {
if got := direction.String(); got != expected {
t.Fatalf("direction %d = %q, want %q", direction, got, expected)
}
}
}
func objectID(path string) atspi.ObjectID {
return atspi.NewObjectID(":1.42", path)
}
func node(path, name string, children ...atspi.ObjectID) atspi.TreeNode {
snapshot := atspi.NewSnapshot(path, name, "push button", "", "")
snapshot.BusName = ":1.42"
return atspi.TreeNode{Snapshot: snapshot, Children: children}
}
func newTree(t *testing.T) *atspi.AccessibleTree {
t.Helper()
tree, err := atspi.NewAccessibleTree(objectID("/root"), []atspi.TreeNode{
node("/root", "Window", objectID("/first"), objectID("/second")),
node("/first", "First"),
node("/second", "Second", objectID("/grandchild")),
node("/grandchild", "Grandchild"),
})
if err != nil {
t.Fatal(err)
}
return tree
}
func TestCursorFollowsChangedFocusAndPreservesIndependentNavigation(t *testing.T) {
cursor, err := NewCursor(newTree(t))
if err != nil {
t.Fatal(err)
}
position, err := cursor.SyncFocus(objectID("/first"))
if err != nil || position.Current.Name != "First" || position.HasPrevious {
t.Fatalf("initial position = %#v, error = %v", position, err)
}
position, moved, err := cursor.Move(NextSibling)
if err != nil || !moved || position.Current.Name != "Second" || !position.HasPrevious || position.Previous.Name != "First" {
t.Fatalf("next position = %#v, moved = %v, error = %v", position, moved, err)
}
position, err = cursor.SyncFocus(objectID("/first"))
if err != nil || position.Current.Name != "Second" {
t.Fatalf("repeated focus pulled cursor back: position = %#v, error = %v", position, err)
}
position, err = cursor.SyncFocus(objectID("/grandchild"))
if err != nil || position.Current.Name != "Grandchild" || !position.HasPrevious || position.Previous.Name != "Second" {
t.Fatalf("changed focus position = %#v, error = %v", position, err)
}
}
func TestCursorTraversesRelationshipsAndPreservesBoundaries(t *testing.T) {
cursor, _ := NewCursor(newTree(t))
if _, err := cursor.SyncFocus(objectID("/second")); err != nil {
t.Fatal(err)
}
position, moved, err := cursor.Move(FirstChild)
if err != nil || !moved || position.Current.Name != "Grandchild" {
t.Fatalf("child position = %#v, moved = %v, error = %v", position, moved, err)
}
position, moved, err = cursor.Move(FirstChild)
if err != nil || moved || position.Current.Name != "Grandchild" {
t.Fatalf("child boundary = %#v, moved = %v, error = %v", position, moved, err)
}
position, moved, err = cursor.Move(Parent)
if err != nil || !moved || position.Current.Name != "Second" {
t.Fatalf("parent position = %#v, moved = %v, error = %v", position, moved, err)
}
position, moved, err = cursor.Move(PreviousSibling)
if err != nil || !moved || position.Current.Name != "First" {
t.Fatalf("previous position = %#v, moved = %v, error = %v", position, moved, err)
}
position, moved, err = cursor.Move(PreviousSibling)
if err != nil || moved || position.Current.Name != "First" {
t.Fatalf("previous boundary = %#v, moved = %v, error = %v", position, moved, err)
}
}
func TestCursorRepairsStaleCurrentFromUnchangedFocus(t *testing.T) {
tree := newTree(t)
cursor, _ := NewCursor(tree)
if _, err := cursor.SyncFocus(objectID("/first")); err != nil {
t.Fatal(err)
}
tree.Invalidate(objectID("/first"))
if _, err := cursor.Current(); !errors.Is(err, atspi.ErrStaleObject) {
t.Fatalf("current error = %v, want stale object", err)
}
if err := tree.Replace(objectID("/root"), []atspi.TreeNode{
node("/root", "Window", objectID("/first")),
node("/first", "Reintroduced"),
}); err != nil {
t.Fatal(err)
}
position, err := cursor.SyncFocus(objectID("/first"))
if err != nil || position.Current.Name != "Reintroduced" {
t.Fatalf("repaired position = %#v, error = %v", position, err)
}
}
func TestCursorErrorsDoNotMoveAndResetClearsState(t *testing.T) {
cursor, _ := NewCursor(newTree(t))
if _, _, err := cursor.Move(Parent); !errors.Is(err, ErrNoCurrent) {
t.Fatalf("empty move error = %v", err)
}
if _, err := cursor.SyncFocus(atspi.ObjectID{}); err == nil {
t.Fatal("invalid focus was accepted")
}
if _, err := cursor.SyncFocus(objectID("/first")); err != nil {
t.Fatal(err)
}
if _, _, err := cursor.Move(Direction(99)); !errors.Is(err, ErrInvalidDirection) {
t.Fatalf("invalid direction error = %v", err)
}
position, err := cursor.Current()
if err != nil || position.Current.Name != "First" {
t.Fatalf("position after invalid move = %#v, error = %v", position, err)
}
cursor.Reset()
if _, err := cursor.Current(); !errors.Is(err, ErrNoCurrent) {
t.Fatalf("reset current error = %v", err)
}
}
func TestCursorSerializesFocusAndNavigation(t *testing.T) {
cursor, _ := NewCursor(newTree(t))
if _, err := cursor.SyncFocus(objectID("/first")); err != nil {
t.Fatal(err)
}
var wait sync.WaitGroup
for index := 0; index < 20; index++ {
wait.Add(2)
go func() {
defer wait.Done()
_, _ = cursor.SyncFocus(objectID("/first"))
}()
go func() {
defer wait.Done()
_, _, _ = cursor.Move(NextSibling)
}()
}
wait.Wait()
if _, err := cursor.Current(); err != nil {
t.Fatal(err)
}
}
func TestNewCursorRejectsNilTree(t *testing.T) {
if _, err := NewCursor(nil); err == nil {
t.Fatal("nil tree was accepted")
}
}