164 lines
5.8 KiB
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)
|
|
}
|
|
}
|