Files

137 lines
4.7 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 atspi
import (
"errors"
"math"
"sync"
"testing"
)
func TestRectangleIntersection(t *testing.T) {
tests := []struct {
name string
first, second, want Rectangle
ok bool
}{
{"overlap", Rectangle{10, 20, 30, 40}, Rectangle{20, 30, 50, 60}, Rectangle{20, 30, 20, 30}, true},
{"negative monitor", Rectangle{-100, -50, 90, 40}, Rectangle{-80, -30, 50, 50}, Rectangle{-80, -30, 50, 20}, true},
{"touching edge", Rectangle{0, 0, 10, 10}, Rectangle{10, 0, 5, 5}, Rectangle{}, false},
{"disjoint", Rectangle{0, 0, 10, 10}, Rectangle{20, 0, 5, 5}, Rectangle{}, false},
{"empty", Rectangle{}, Rectangle{0, 0, 10, 10}, Rectangle{}, false},
{"negative size", Rectangle{0, 0, -1, 10}, Rectangle{0, 0, 10, 10}, Rectangle{}, false},
{"unavailable position", Rectangle{math.MinInt32, 0, 10, 10}, Rectangle{math.MinInt32, 0, 10, 10}, Rectangle{}, false},
{"edge overflow", Rectangle{math.MaxInt32 - 5, 0, 10, 10}, Rectangle{math.MaxInt32 - 2, 1, 10, 10}, Rectangle{math.MaxInt32 - 2, 1, 7, 9}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, ok := tt.first.Intersection(tt.second)
if ok != tt.ok || got != tt.want {
t.Fatalf("intersection = %v %v, want %v %v", got, ok, tt.want, tt.ok)
}
})
}
}
func TestTreeGeometryInvalidationPreservesHierarchy(t *testing.T) {
nodes := []TreeNode{
testNode("/root", "Window", testID("/first"), testID("/second")),
testNode("/first", "First"),
testNode("/second", "Second", testID("/child")),
testNode("/child", "Child"),
}
geometry := Geometry{Bounds: Rectangle{-10, 20, 30, 40}, BoundsKnown: true, VisibilityKnown: true, Visible: true, Showing: true}
for i := range nodes {
nodes[i].Geometry = geometry
}
tree, err := NewAccessibleTree(testID("/root"), nodes)
if err != nil {
t.Fatal(err)
}
second, _ := tree.Reference(testID("/second"))
child, _, _ := tree.FirstChild(second)
first, _ := tree.Reference(testID("/first"))
if count := tree.InvalidateGeometry(second.ID()); count != 2 {
t.Fatalf("invalidated %d", count)
}
if count := tree.InvalidateGeometry(second.ID()); count != 0 {
t.Fatalf("repeat invalidated %d", count)
}
for _, ref := range []ObjectReference{second, child} {
if _, err := tree.Geometry(ref); !errors.Is(err, ErrStaleGeometry) {
t.Fatalf("geometry = %v", err)
}
if _, err := tree.Resolve(ref); err != nil {
t.Fatalf("geometry invalidation destroyed identity: %v", err)
}
}
if got, err := tree.Geometry(first); err != nil || got != geometry {
t.Fatalf("unrelated geometry = %v %v", got, err)
}
if got, ok, err := tree.FirstChild(second); err != nil || !ok || got != child {
t.Fatalf("child = %v %v %v", got, ok, err)
}
nodes[2].Geometry.Bounds.X = 25
if err := tree.Replace(testID("/root"), nodes); err != nil {
t.Fatal(err)
}
if got, err := tree.Geometry(second); err != nil || got.Bounds.X != 25 {
t.Fatalf("recapture = %v %v", got, err)
}
tree.Invalidate(second.ID())
if _, err := tree.Geometry(child); !errors.Is(err, ErrStaleObject) {
t.Fatalf("removed geometry = %v", err)
}
}
func TestTreeRejectsMalformedGeometry(t *testing.T) {
for _, geometry := range []Geometry{
{Showing: true},
{Bounds: Rectangle{0, 0, 10, 10}},
{BoundsKnown: true},
{Bounds: Rectangle{0, 0, 10, 10}, BoundsKnown: true, VisibilityKnown: true, Visible: true},
} {
node := testNode("/root", "Window")
node.Geometry = geometry
if _, err := NewAccessibleTree(testID("/root"), []TreeNode{node}); !errors.Is(err, ErrInvalidTree) {
t.Fatalf("accepted malformed geometry %v: %v", geometry, err)
}
}
}
func TestTreeGeometryConcurrentInvalidation(t *testing.T) {
tree := testTree(t)
ref, _ := tree.Reference(testID("/second"))
var readers sync.WaitGroup
for range 8 {
readers.Add(1)
go func() {
defer readers.Done()
for range 100 {
if _, err := tree.Geometry(ref); err != nil && !errors.Is(err, ErrStaleGeometry) {
t.Errorf("geometry: %v", err)
return
}
}
}()
}
tree.InvalidateGeometry(ref.ID())
readers.Wait()
}