Files
cthulhu/internal/atspi/document_view_test.go

164 lines
5.8 KiB
Go

// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package atspi
import (
"context"
"errors"
"sync"
"testing"
)
func TestNavigationDocumentConcurrentReplacementKeepsViewsImmutable(t *testing.T) {
tree, _ := NewAccessibleTree(testID("/root"), documentTestNodes())
ref, _ := tree.Reference(testID("/heading"))
var workers sync.WaitGroup
workers.Add(1)
go func() {
defer workers.Done()
for range 100 {
nodes := documentTestNodes()
for index := range nodes {
nodes[index].Snapshot.Name = "Replacement"
}
if err := tree.Replace(testID("/root"), nodes); err != nil {
t.Error(err)
return
}
}
}()
defer workers.Wait()
for range 100 {
view, err := tree.NavigationDocument(context.Background(), ref)
if err != nil {
t.Fatal(err)
}
replaced := view.Context.Ancestors[0].Name == "Replacement"
for _, node := range view.Nodes {
if (node.Snapshot.Name == "Replacement") != replaced {
t.Fatal("mixed replacement data in shared view")
}
}
if err := tree.ValidateDocumentRange(context.Background(), view, 0, len(view.Nodes)-1); err != nil && !errors.Is(err, ErrStaleObject) {
t.Fatal(err)
}
}
}
func TestNavigationDocumentSharesOnlyImmutableData(t *testing.T) {
tree, _ := NewAccessibleTree(testID("/root"), documentTestNodes())
ref, _ := tree.Reference(testID("/heading"))
first, err := tree.NavigationDocument(context.Background(), ref)
if err != nil {
t.Fatal(err)
}
second, err := tree.NavigationDocument(context.Background(), ref)
if err != nil || !first.SameContent(second) || &first.Nodes[0] != &second.Nodes[0] {
t.Fatal("unchanged document data was rebuilt", err)
}
copy, err := tree.Document(context.Background(), ref)
if err != nil {
t.Fatal(err)
}
copy.Nodes[0].Snapshot.Name = "Changed copy"
if first.Nodes[0].Snapshot.Name == "Changed copy" {
t.Fatal("owned snapshot modified shared navigation data")
}
for index, node := range first.Nodes {
if got, ok := first.Index(node.Reference.ID()); !ok || got != index {
t.Fatalf("index for %v = %d, %v", node.Reference.ID(), got, ok)
}
if end := first.SubtreeEnd(index); end < index || end >= len(first.Nodes) {
t.Fatalf("invalid subtree end %d", end)
}
}
}
func TestNavigationDocumentValidatesOnlyTraversedBranches(t *testing.T) {
for _, test := range []struct {
name string
invalidate func(*AccessibleTree)
want error
}{
{"text", func(tree *AccessibleTree) { tree.InvalidateText(testID("/link")) }, ErrStaleText},
{"semantics", func(tree *AccessibleTree) { tree.InvalidateSnapshot(testID("/link")) }, ErrStaleSnapshot},
{"geometry", func(tree *AccessibleTree) { tree.InvalidateGeometry(testID("/inner")) }, ErrStaleGeometry},
{"children", func(tree *AccessibleTree) { tree.MarkChildrenChanged(testID("/inner")) }, ErrIncompleteTree},
} {
t.Run(test.name, func(t *testing.T) {
tree, _ := NewAccessibleTree(testID("/root"), documentTestNodes())
ref, _ := tree.Reference(testID("/heading"))
view, err := tree.NavigationDocument(context.Background(), ref)
if err != nil {
t.Fatal(err)
}
test.invalidate(tree)
current, err := tree.NavigationDocument(context.Background(), ref)
if err != nil || !current.SameContent(view) {
t.Fatal("unrelated dirty branch rebuilt document", err)
}
heading, _ := view.Index(ref.ID())
if err := tree.ValidateDocumentRange(context.Background(), view, heading, heading); err != nil {
t.Fatal("unrelated update blocked current heading", err)
}
if err := tree.ValidateDocumentRange(context.Background(), view, heading, len(view.Nodes)-1); !errors.Is(err, test.want) {
t.Fatalf("traversed stale branch: %v, want %v", err, test.want)
}
})
}
}
func TestNavigationDocumentRejectsRetiredViewsAndCanceledMoves(t *testing.T) {
tree, _ := NewAccessibleTree(testID("/root"), documentTestNodes())
ref, _ := tree.Reference(testID("/heading"))
view, _ := tree.NavigationDocument(context.Background(), ref)
ctx, cancel := context.WithCancel(context.Background())
cancel()
if err := tree.ValidateDocumentRange(ctx, view, 0, 0); !errors.Is(err, context.Canceled) {
t.Fatal(err)
}
if err := tree.Replace(testID("/root"), documentTestNodes()); err != nil {
t.Fatal(err)
}
if err := tree.ValidateDocumentRange(context.Background(), view, 0, 0); !errors.Is(err, ErrStaleObject) {
t.Fatal("replacement retained old view", err)
}
fresh, _ := tree.NavigationDocument(context.Background(), ref)
if fresh.SameContent(view) {
t.Fatal("replacement reused navigation data")
}
tree.Invalidate(testID("/link"))
if err := tree.ValidateDocumentRange(context.Background(), fresh, 0, 0); !errors.Is(err, ErrStaleObject) {
t.Fatal("removal retained old topology", err)
}
}
func TestNavigationDocumentValidatesDerivedRowDependencies(t *testing.T) {
node := func(path, role string, children ...ObjectID) TreeNode {
n := testNode(path, path, children...)
n.Snapshot.Role = role
return n
}
nodes := []TreeNode{node("/doc", "document web", testID("/table")), node("/table", "table", testID("/first"), testID("/second")), node("/first", "table cell", testID("/text")), node("/text", "static"), node("/second", "table cell")}
for _, index := range []int{2, 4} {
nodes[index].Snapshot.TablePositionKnown = true
nodes[index].Snapshot.TableColumnIndex = index/2 - 1
}
tree, err := NewAccessibleTree(testID("/doc"), nodes)
if err != nil {
t.Fatal(err)
}
ref, _ := tree.Reference(testID("/text"))
view, err := tree.NavigationDocument(context.Background(), ref)
if err != nil {
t.Fatal(err)
}
index, _ := view.Index(ref.ID())
tree.InvalidateText(testID("/second"))
if err := tree.ValidateDocumentRange(context.Background(), view, index, index); !errors.Is(err, ErrStaleText) {
t.Fatal("cached full row ignored a changed sibling cell", err)
}
}