Cthulhu is no longer a fork. Now it is a screen reader written in Golang.
This commit is contained in:
1 parent
a998fe67de
commit
069fc674dc
708 files changed
+112482
-532
No files matched your search
@@ -0,0 +1,166 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.stormux.org/storm/cthulhu/internal/atspi"
|
||||
"git.stormux.org/storm/cthulhu/internal/review"
|
||||
)
|
||||
|
||||
func TestNativeLinkedHeadings(t *testing.T) {
|
||||
if os.Getenv("CTHULHU_NATIVE_LINKED_HEADINGS") != "1" {
|
||||
t.Skip("requires private linked heading browser session")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 40*time.Second)
|
||||
defer cancel()
|
||||
adapter := atspi.New()
|
||||
if err := adapter.Start(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer adapter.Stop(context.Background())
|
||||
if output, err := exec.CommandContext(ctx, "xdotool", "key", "--clearmodifiers", "Tab", "shift+Tab").CombinedOutput(); err != nil {
|
||||
t.Fatalf("fixture focus: %s, %v", output, err)
|
||||
}
|
||||
var focus atspi.ObjectID
|
||||
var bounds atspi.Rectangle
|
||||
for !focus.Valid() && ctx.Err() == nil {
|
||||
tree, err := adapter.CaptureDesktop(ctx)
|
||||
if err == nil {
|
||||
focus = linkedHeadingFixtureFocus(tree)
|
||||
if focus.Valid() {
|
||||
ref, _ := tree.Reference(focus)
|
||||
geometry, _ := tree.Geometry(ref)
|
||||
if geometry.BoundsKnown {
|
||||
bounds = geometry.Bounds
|
||||
}
|
||||
}
|
||||
}
|
||||
if !focus.Valid() {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
if !focus.Valid() {
|
||||
t.Fatal("linked heading fixture input absent from accessible desktop")
|
||||
}
|
||||
if !bounds.Valid() {
|
||||
t.Fatal("linked heading fixture input has no screen bounds")
|
||||
}
|
||||
keys := []string{"key", "--clearmodifiers", "ctrl+l"}
|
||||
if output, err := exec.CommandContext(ctx, "xdotool", keys...).CombinedOutput(); err != nil {
|
||||
t.Fatalf("focus browser chrome: %s, %v", output, err)
|
||||
}
|
||||
pointX, pointY := bounds.X+bounds.Width/2, bounds.Y+bounds.Height/2
|
||||
if output, err := exec.CommandContext(ctx, "xdotool", "mousemove", "--sync", fmt.Sprint(pointX), fmt.Sprint(pointY), "click", "1").CombinedOutput(); err != nil {
|
||||
t.Fatalf("focus fixture input: %s, %v", output, err)
|
||||
}
|
||||
focused := false
|
||||
deadline := time.NewTimer(5 * time.Second)
|
||||
defer deadline.Stop()
|
||||
for !focused {
|
||||
select {
|
||||
case event := <-adapter.Events():
|
||||
focused = event.Type == atspi.FocusedStateEvent && event.Detail1 == 1 && event.Source.ID() == focus
|
||||
case <-deadline.C:
|
||||
t.Fatal("fixture input did not emit native focus")
|
||||
case <-ctx.Done():
|
||||
t.Fatal(ctx.Err())
|
||||
}
|
||||
}
|
||||
for _, tc := range []struct {
|
||||
level int
|
||||
name, title string
|
||||
moves int
|
||||
}{
|
||||
{2, "Inside heading link", "Cthulhu Linked Heading Inside", 1},
|
||||
{3, "Outside heading link", "Cthulhu Linked Heading Outside", 1},
|
||||
{3, "#1 Username Example date", "Cthulhu Linked Heading Post", 2},
|
||||
} {
|
||||
tree, err := adapter.CaptureDocument(ctx, focus)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cursor, err := review.NewDocumentCursor(tree)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := cursor.SyncFocus(ctx, focus); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
move, err := cursor.Move(ctx, review.DocumentHeading, tc.level, false)
|
||||
for step := 1; step < tc.moves && err == nil; step++ {
|
||||
move, err = cursor.Move(ctx, review.DocumentHeading, tc.level, false)
|
||||
}
|
||||
if err != nil || !move.Moved || move.Position.Snapshot.Name != tc.name || move.Position.LinkedHeading == nil {
|
||||
t.Fatalf("heading level %d: %+v, %v", tc.level, move, err)
|
||||
}
|
||||
target, err := cursor.ActivationTarget()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
link, err := tree.Resolve(target)
|
||||
if err != nil || link.Role != "link" {
|
||||
t.Fatalf("activation target = %+v, %v", link, err)
|
||||
}
|
||||
if err := adapter.ActivateDocumentObject(ctx, target, focus); err != nil {
|
||||
t.Fatalf("activate %s: %v", tc.name, err)
|
||||
}
|
||||
for {
|
||||
output, err := exec.CommandContext(ctx, "xdotool", "search", "--onlyvisible", "--name", tc.title).Output()
|
||||
if err == nil && strings.TrimSpace(string(output)) != "" {
|
||||
break
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
t.Fatalf("activation did not open %s: %s", tc.title, output)
|
||||
}
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
tree, err := adapter.CaptureDocument(ctx, focus)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cursor, err := review.NewDocumentCursor(tree)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := cursor.SyncFocus(ctx, focus); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
move, err := cursor.Move(ctx, review.DocumentHeading, 5, false)
|
||||
if err != nil || !move.Moved || move.Position.LinkedHeading != nil {
|
||||
t.Fatalf("plain-text heading with multiple links: %+v, %v", move, err)
|
||||
}
|
||||
if target, err := cursor.ActivationTarget(); !errors.Is(err, review.ErrNoCurrent) {
|
||||
t.Fatalf("ambiguous activation target = %v, %v", target, err)
|
||||
}
|
||||
}
|
||||
|
||||
func linkedHeadingFixtureFocus(tree *atspi.AccessibleTree) atspi.ObjectID {
|
||||
root, err := tree.Root()
|
||||
if err != nil {
|
||||
return atspi.ObjectID{}
|
||||
}
|
||||
pending := []atspi.ObjectReference{root}
|
||||
for len(pending) != 0 {
|
||||
current := pending[len(pending)-1]
|
||||
pending = pending[:len(pending)-1]
|
||||
snapshot, err := tree.Resolve(current)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if snapshot.Name == "Probe start" && snapshot.Role == "entry" {
|
||||
return current.ID()
|
||||
}
|
||||
for child, ok, err := tree.FirstChild(current); err == nil && ok; child, ok, err = tree.NextSibling(child) {
|
||||
pending = append(pending, child)
|
||||
}
|
||||
}
|
||||
return atspi.ObjectID{}
|
||||
}
|
||||
Reference in new issue
Block a user