144 lines
5.3 KiB
Go
144 lines
5.3 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// Authors: Storm Dragon, Angus Kola
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package review
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.stormux.org/storm/cthulhu/internal/atspi"
|
|
)
|
|
|
|
func TestDocumentListPreservesCursorAndSelectsExactTarget(t *testing.T) {
|
|
tree, cursor := webCursor(t)
|
|
ctx := context.Background()
|
|
if _, err := cursor.SyncFocus(ctx, webID("/doc")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
capturer := &readAllCapturer{tree: tree}
|
|
worker, _ := NewWorker(capturer, time.Second)
|
|
worker.SyncFocus(webID("/doc"))
|
|
before := cursor.diagnostic_state()
|
|
list, err := worker.listDocument(ctx, cursor, &documentListRequest{ctx: ctx, target: DocumentHeading})
|
|
if err != nil || len(list.Entries) != 2 {
|
|
t.Fatalf("heading list: %+v %v", list, err)
|
|
}
|
|
if after := cursor.diagnostic_state(); after != before {
|
|
t.Fatalf("opening moved cursor: %+v -> %+v", before, after)
|
|
}
|
|
result, cursor := worker.selectDocumentList(ctx, cursor, &documentListSelection{list: list, index: 1})
|
|
if result.Err != nil || !result.Moved || result.DocumentPosition.Object != list.Entries[1].Object {
|
|
t.Fatalf("selection: %+v", result)
|
|
}
|
|
list, err = worker.listDocument(ctx, cursor, &documentListRequest{ctx: ctx, target: DocumentHeading})
|
|
if err != nil || list.Selected != 1 {
|
|
t.Fatalf("did not preserve current row: %+v %v", list, err)
|
|
}
|
|
filtered, err := worker.listDocument(ctx, cursor, &documentListRequest{ctx: ctx, target: DocumentHeading, level: 2})
|
|
if err != nil || len(filtered.Entries) != 2 || filtered.Entries[0].Snapshot.HeadingLevel != 2 {
|
|
t.Fatalf("level filter: %+v %v", filtered, err)
|
|
}
|
|
}
|
|
|
|
func TestDocumentListRejectsChangedTargetsAndOwnership(t *testing.T) {
|
|
for _, change := range []string{"target removed", "target reused", "target hidden", "focus changed", "document reloaded"} {
|
|
t.Run(change, func(t *testing.T) {
|
|
tree, cursor := webCursor(t)
|
|
ctx := context.Background()
|
|
worker, _ := NewWorker(&readAllCapturer{tree: tree}, time.Second)
|
|
worker.SyncFocus(webID("/doc"))
|
|
list, err := worker.listDocument(ctx, cursor, &documentListRequest{ctx: ctx, target: DocumentHeading})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
switch change {
|
|
case "target removed":
|
|
tree.Invalidate(list.Entries[0].Object.ID())
|
|
case "target reused":
|
|
tree.Invalidate(list.Entries[0].Object.ID())
|
|
if err := tree.Replace(webID("/root"), webNodes()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
case "target hidden":
|
|
nodes := webNodes()
|
|
nodes[3].Geometry = atspi.Geometry{VisibilityKnown: true}
|
|
if err := tree.Replace(webID("/root"), nodes); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
case "focus changed":
|
|
worker.SyncFocus(webID("/chrome"))
|
|
case "document reloaded":
|
|
worker.ResetDocument(webID("/doc"))
|
|
}
|
|
result, _ := worker.selectDocumentList(ctx, cursor, &documentListSelection{list: list, index: 0})
|
|
if result.Err == nil || result.Moved {
|
|
t.Fatalf("stale selection succeeded: %+v", result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDocumentListDescendantLabelsExcludeHiddenAndPasswordText(t *testing.T) {
|
|
node := func(path, role, text string, children ...string) atspi.TreeNode {
|
|
snapshot := atspi.Snapshot{BusName: webID(path).BusName, Path: path, Role: role, Text: text}
|
|
entry := atspi.TreeNode{Snapshot: snapshot}
|
|
for _, child := range children {
|
|
entry.Children = append(entry.Children, webID(child))
|
|
}
|
|
return entry
|
|
}
|
|
nodes := []atspi.TreeNode{
|
|
node("/doc", "document web", "", "/clickable"),
|
|
node("/clickable", "section", "", "/text", "/hidden", "/password"),
|
|
node("/text", "static", "Clickable text"),
|
|
node("/hidden", "static", "Hidden content"),
|
|
node("/password", "password text", "private fixture password"),
|
|
}
|
|
nodes[1].Snapshot.SupportsClick = true
|
|
nodes[3].Geometry = atspi.Geometry{VisibilityKnown: true}
|
|
tree, err := atspi.NewAccessibleTree(webID("/doc"), nodes)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
worker, _ := NewWorker(&readAllCapturer{tree: tree}, time.Second)
|
|
worker.SyncFocus(webID("/doc"))
|
|
ctx := context.Background()
|
|
list, err := worker.listDocument(ctx, nil, &documentListRequest{ctx: ctx, target: DocumentClickable})
|
|
if err != nil || len(list.Entries) != 1 || list.Entries[0].Label != "Clickable text" {
|
|
t.Fatalf("unsafe or missing descendant label: %+v %v", list, err)
|
|
}
|
|
}
|
|
|
|
func TestWorkerDocumentListUsesOrderedRequests(t *testing.T) {
|
|
tree, _ := webCursor(t)
|
|
worker, cancel := startWorker(t, &readAllCapturer{tree: tree}, time.Second)
|
|
defer cancel()
|
|
worker.SyncFocus(webID("/doc"))
|
|
ctx, stop := context.WithTimeout(context.Background(), time.Second)
|
|
defer stop()
|
|
list, err := worker.ListDocument(ctx, DocumentHeading, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := worker.SelectDocumentList(list, 1); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
result := waitResult(t, worker)
|
|
if result.Err != nil || !result.Moved || result.DocumentPosition.Object != list.Entries[1].Object {
|
|
t.Fatalf("queued selection: %+v", result)
|
|
}
|
|
}
|
|
|
|
func TestAdditionalDocumentListRoles(t *testing.T) {
|
|
for target, roles := range map[DocumentTarget][]string{DocumentAnnotation: {"content deletion", "content insertion", "mark"}, DocumentInternalFrame: {"internal frame"}, DocumentMath: {"math"}} {
|
|
for _, role := range roles {
|
|
if !documentTargetMatches(target, atspi.DocumentNode{Snapshot: atspi.Snapshot{Role: role}}, 0) {
|
|
t.Fatalf("missing %s", role)
|
|
}
|
|
}
|
|
}
|
|
}
|