// 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 ( "context" "errors" "testing" ) func reviewTree(t *testing.T) (*AccessibleTree, ObjectID) { t.Helper() id := func(path string) ObjectID { return NewObjectID(":1.42", path) } node := func(path, role string, children ...ObjectID) TreeNode { s := NewSnapshot(path, path, role, "", "") s.BusName = ":1.42" return TreeNode{Snapshot: s, Children: children} } tree, err := NewAccessibleTree(id("/desktop"), []TreeNode{ node("/desktop", "desktop frame", id("/window"), id("/other")), node("/window", "frame", id("/button"), id("/dialog")), node("/button", "push button"), node("/dialog", "dialog", id("/entry")), node("/entry", "entry"), node("/other", "frame"), }) if err != nil { t.Fatal(err) } return tree, id("/button") } func TestReviewWindowScopeAndOwnership(t *testing.T) { tree, focus := reviewTree(t) snapshot, err := tree.ReviewWindow(context.Background(), focus) nodes := snapshot.Nodes if err != nil { t.Fatal(err) } if len(nodes) != 4 || nodes[0].Snapshot.Path != "/window" || nodes[1].Snapshot.Path != "/button" || nodes[3].Snapshot.Path != "/entry" { t.Fatalf("unexpected window: %#v", nodes) } nodes[0].Children[0] = ObjectID{} focus.Path = "/entry" snapshot, err = tree.ReviewWindow(context.Background(), focus) nodes = snapshot.Nodes if err != nil || len(nodes) != 2 || nodes[0].Snapshot.Path != "/dialog" { t.Fatalf("dialog scope: %#v, %v", nodes, err) } focus.Path = "/button" if _, err := tree.ReviewWindow(context.Background(), focus); err != nil { t.Fatal("caller mutated tree:", err) } } func TestReviewWindowRejectsStaleDataAndCancellation(t *testing.T) { for _, tc := range []struct { name string invalidate func(*AccessibleTree, ObjectID) want error }{ {"children", func(tree *AccessibleTree, id ObjectID) { tree.InvalidateChildren(id) }, ErrIncompleteTree}, {"geometry", func(tree *AccessibleTree, id ObjectID) { tree.InvalidateGeometry(id) }, ErrStaleGeometry}, {"text", func(tree *AccessibleTree, id ObjectID) { tree.InvalidateText(id) }, ErrStaleText}, } { t.Run(tc.name, func(t *testing.T) { tree, focus := reviewTree(t) tc.invalidate(tree, focus) if snapshot, err := tree.ReviewWindow(context.Background(), focus); snapshot.Nodes != nil || !errors.Is(err, tc.want) { t.Fatalf("nodes=%v err=%v", snapshot.Nodes, err) } }) } tree, focus := reviewTree(t) ctx, cancel := context.WithCancel(context.Background()) cancel() if _, err := tree.ReviewWindow(ctx, focus); !errors.Is(err, context.Canceled) { t.Fatal(err) } focus.Path = "/desktop" if _, err := tree.ReviewWindow(context.Background(), focus); !errors.Is(err, ErrNoReviewWindow) { t.Fatal(err) } }