Files
cthulhu/internal/review/document_activation_test.go

152 lines
6.2 KiB
Go

// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package review
import (
"context"
"errors"
"testing"
"time"
"git.stormux.org/storm/cthulhu/internal/atspi"
)
type refreshingActivator struct {
*readAllCapturer
target atspi.ObjectID
staleOnce bool
}
func TestActivationDoesNotRecaptureNonClickableText(t *testing.T) {
cursor := newCaretTextCursor(t, caretTextReader{webID("/text"): "Ordinary paragraph"})
capturer := &fakeCapturer{calls: make(chan captureCall, 1)}
worker, _ := NewWorker(capturer, time.Second)
worker.SyncFocus(webID("/text"))
result, _ := worker.activateDocumentCaret(context.Background(), cursor, 0)
if !errors.Is(result.Err, ErrNoDocumentAction) || len(capturer.calls) != 0 {
t.Fatalf("plain text triggered a capture: %+v", result)
}
}
func TestActivationRejectsReplacedDocumentGeneration(t *testing.T) {
tree := activationTree(t, true)
cursor, _ := NewDocumentCursor(tree)
cursor.SyncFocus(context.Background(), webID("/focused"))
cursor.places[0].object, _ = tree.Reference(webID("/selected"))
tree.Invalidate(webID("/doc"))
nodes := []atspi.TreeNode{
{Snapshot: atspi.Snapshot{BusName: webID("/doc").BusName, Path: "/doc", Role: "document web"}, Children: []atspi.ObjectID{webID("/focused"), webID("/selected")}},
{Snapshot: atspi.Snapshot{BusName: webID("/focused").BusName, Path: "/focused", Role: "link"}},
{Snapshot: atspi.Snapshot{BusName: webID("/selected").BusName, Path: "/selected", Role: "link"}},
}
if err := tree.Replace(webID("/doc"), nodes); err != nil {
t.Fatal(err)
}
activator := &refreshingActivator{readAllCapturer: &readAllCapturer{tree: tree}}
worker, _ := NewWorker(activator, time.Second)
worker.SyncFocus(webID("/focused"))
result, _ := worker.activateDocumentCaret(context.Background(), cursor, 0)
if result.Err == nil || activator.target.Valid() {
t.Fatalf("activation crossed a document generation: %+v, %v", result, activator.target)
}
}
func TestBrowseNamedCustomClickableAndActivateAncestor(t *testing.T) {
reader := caretTextReader{webID("/start"): "Before action", webID("/label"): "Visible label", webID("/after"): "After action"}
for _, explicit := range []bool{false, true} {
nodes := []atspi.TreeNode{
{Snapshot: atspi.Snapshot{BusName: webID("/doc").BusName, Path: "/doc", Role: "document web"}, Children: []atspi.ObjectID{webID("/start"), webID("/custom"), webID("/after")}},
{Snapshot: atspi.Snapshot{BusName: webID("/start").BusName, Path: "/start", Role: "static", CharacterCount: 13}},
{Snapshot: atspi.Snapshot{BusName: webID("/custom").BusName, Path: "/custom", Role: "section", Name: "Perform action", SupportsClick: true, ExplicitName: explicit}, Children: []atspi.ObjectID{webID("/label")}},
{Snapshot: atspi.Snapshot{BusName: webID("/label").BusName, Path: "/label", Role: "static", CharacterCount: 13}},
{Snapshot: atspi.Snapshot{BusName: webID("/after").BusName, Path: "/after", Role: "static", CharacterCount: 12}},
}
tree, err := atspi.NewAccessibleTree(webID("/doc"), nodes)
if err != nil {
t.Fatal(err)
}
cursor, _ := NewDocumentCursor(tree)
cursor.SyncFocus(context.Background(), webID("/start"))
move, err := cursor.MoveCaret(context.Background(), reader, CaretNextLine)
want := webID("/label")
if explicit {
want = webID("/custom")
}
if err != nil || !move.Moved || move.Position.Object.ID() != want {
t.Fatalf("custom clickable stop: %+v, %v", move, err)
}
target, err := cursor.ActivationTarget()
if err != nil || target.ID() != webID("/custom") {
t.Fatalf("activation target = %v, %v", target, err)
}
move, err = cursor.MoveCaret(context.Background(), reader, CaretNextLine)
if err != nil || move.Text != "After action" {
t.Fatalf("clickable trapped navigation: %+v, %v", move, err)
}
}
}
func (activator *refreshingActivator) ActivateDocumentObject(_ context.Context, target atspi.ObjectReference, _ atspi.ObjectID) error {
if activator.staleOnce {
activator.staleOnce = false
return atspi.ErrStaleObject
}
activator.target = target.ID()
return nil
}
func activationTree(t *testing.T, includeSelected bool) *atspi.AccessibleTree {
t.Helper()
root := atspi.TreeNode{Snapshot: atspi.Snapshot{BusName: webID("/doc").BusName, Path: "/doc", Role: "document web"}, Children: []atspi.ObjectID{webID("/focused")}}
nodes := []atspi.TreeNode{root, {Snapshot: atspi.Snapshot{BusName: webID("/focused").BusName, Path: "/focused", Role: "link"}}}
if includeSelected {
nodes[0].Children = append(nodes[0].Children, webID("/selected"))
nodes = append(nodes, atspi.TreeNode{Snapshot: atspi.Snapshot{BusName: webID("/selected").BusName, Path: "/selected", Role: "link"}})
}
tree, err := atspi.NewAccessibleTree(webID("/doc"), nodes)
if err != nil {
t.Fatal(err)
}
return tree
}
func TestActivationRecoversUncapturedAndStaleCursors(t *testing.T) {
for _, kind := range []string{"uncaptured", "stale", "removed", "native-stale"} {
t.Run(kind, func(t *testing.T) {
fresh := activationTree(t, kind != "removed")
activator := &refreshingActivator{readAllCapturer: &readAllCapturer{tree: fresh}, staleOnce: kind == "native-stale"}
worker, _ := NewWorker(activator, time.Second)
worker.SyncFocus(webID("/focused"))
var cursor *DocumentCursor
if kind != "uncaptured" {
old := activationTree(t, true)
cursor, _ = NewDocumentCursor(old)
cursor.SyncFocus(context.Background(), webID("/focused"))
cursor.places[0].object, _ = old.Reference(webID("/selected"))
if kind != "native-stale" {
old.Invalidate(webID("/selected"))
}
}
result, updated := worker.activateDocumentCaret(context.Background(), cursor, 0)
if kind == "removed" {
if result.Err == nil || activator.target.Valid() {
t.Fatal("activated a different link after selected link disappeared")
}
} else {
want := webID("/selected")
if kind == "uncaptured" {
want = webID("/focused")
}
if result.Err != nil || activator.target != want {
t.Fatalf("activation = %v, %v; want %v", activator.target, result.Err, want)
}
if current, err := updated.ActivationTarget(); err != nil || current.ID() != want {
t.Fatalf("cursor lost after activation: %v, %v", current, err)
}
}
})
}
}