// 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 . 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() }