152 lines
6.2 KiB
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)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|