138 lines
4.4 KiB
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))
|
|
}
|
|
}
|