137 lines
4.7 KiB
Go
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()
|
|
}
|