Files
cthulhu/internal/atspi/replaced_feed_native_test.go

138 lines
4.4 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"
"fmt"
"os"
"os/exec"
"testing"
"time"
)
func TestNativeBrowserMissingFeedRefreshesWholeBranch(t *testing.T) {
if os.Getenv("CTHULHU_NATIVE_BROWSER") != "1" {
t.Skip("requires private browser session")
}
ctx, cancel := context.WithTimeout(context.Background(), 25*time.Second)
defer cancel()
adapter := New()
if err := adapter.Start(ctx); err != nil {
t.Fatal(err)
}
defer adapter.Stop(context.Background())
keys := func(args ...string) {
t.Helper()
if output, err := exec.CommandContext(ctx, "xdotool", args...).CombinedOutput(); err != nil {
t.Fatalf("xdotool: %v: %s", err, output)
}
}
desktop, err := adapter.CaptureDesktop(ctx)
if err != nil {
t.Fatal(err)
}
start := findNamedReference(t, desktop, "Feed start")
bounds, err := desktop.Geometry(start)
if err != nil || !bounds.BoundsKnown {
t.Fatalf("feed start bounds unavailable: %+v, %v", bounds, err)
}
keys("key", "--clearmodifiers", "ctrl+l")
keys("mousemove", "--sync", fmt.Sprint(bounds.Bounds.X+bounds.Bounds.Width/2), fmt.Sprint(bounds.Bounds.Y+bounds.Bounds.Height/2), "click", "1")
focused := false
for !focused {
select {
case event := <-adapter.Events():
focused = event.Type == FocusedStateEvent && event.Detail1 == 1 && event.Source.ID() == start.ID()
case <-ctx.Done():
t.Fatal("feed start focus: ", ctx.Err())
}
}
tree, err := adapter.CaptureDocument(ctx, start.ID())
if err != nil {
t.Fatal(err)
}
root, _ := tree.Root()
doc, err := tree.Document(ctx, root)
if err != nil {
t.Fatal(err)
}
// Keep the captured semantics from before replacement. This explicitly
// models a provider omitting the structural events for an enclosing feed.
oldNodes := make([]TreeNode, 0, len(doc.Nodes))
for _, node := range doc.Nodes {
oldNodes = append(oldNodes, TreeNode{Snapshot: node.Snapshot, Children: node.Children, Geometry: node.Geometry, Text: node.Text})
}
missing := findNamedReference(t, tree, "Later post")
otherOld := findNamedReference(t, tree, "Earlier post")
keys("key", "--clearmodifiers", "F9")
replaced := false
for !replaced {
select {
case event := <-adapter.Events():
replaced = event.Type == AccessibleNameEvent && event.Source.Name == "Feed replacement complete"
case <-ctx.Done():
t.Fatal("feed replacement: ", ctx.Err())
}
}
if err := tree.Replace(root.ID(), oldNodes); err != nil {
t.Fatal(err)
}
// Use the current generation after restoring the deliberately stale cache.
missing, err = tree.Reference(missing.ID())
if err != nil {
t.Fatal(err)
}
_, err = adapter.ReadDocumentCaret(ctx, missing, 0, DocumentCaretBrailleLine)
if !errors.Is(err, ErrDeadObject) {
t.Fatalf("missing feed target = %v, want confirmed removal", err)
}
// libatspi can dispatch late defunct events during the failed native
// read. Restore the omitted-event scenario after that call, then replay
// its genuine missing-object response against the stale owned snapshot.
if restoreErr := tree.Replace(root.ID(), oldNodes); restoreErr != nil {
t.Fatal(restoreErr)
}
if replayErr := adapter.document_object_error(missing.ID(), err); !errors.Is(replayErr, ErrDeadObject) {
t.Fatal(replayErr)
}
tree, err = adapter.CaptureDocument(ctx, start.ID())
if err != nil {
t.Fatal(err)
}
if _, err := tree.Reference(otherOld.ID()); !errors.Is(err, ErrObjectNotFound) {
t.Fatalf("recapture retained another dead heading: %v", err)
}
root, err = tree.Root()
if err != nil {
t.Fatal(err)
}
fresh, err := tree.Document(ctx, root)
if err != nil {
t.Fatal(err)
}
expected := []string{"Later refreshed post", "Middle refreshed post", "Earlier refreshed post"}
headingIndex := 0
for index := len(fresh.Nodes) - 1; index >= 0; index-- {
node := fresh.Nodes[index]
if node.Snapshot.Role != "heading" {
continue
}
if headingIndex >= len(expected) || node.Snapshot.Name != expected[headingIndex] {
t.Fatalf("unexpected backward heading %q at index %d", node.Snapshot.Name, headingIndex)
}
text, err := adapter.ReadDocumentCaret(ctx, node.Reference, 0, DocumentCaretBrailleLine)
if err != nil || text.Text != expected[headingIndex] {
t.Fatalf("backward feed reading %q = %+v, %v", expected[headingIndex], text, err)
}
headingIndex++
}
if headingIndex != len(expected) {
t.Fatalf("backward headings = %d, want %d", headingIndex, len(expected))
}
}