Files
cthulhu/internal/atspi/current_line_native_test.go

153 lines
5.6 KiB
Go

package atspi
import (
"bufio"
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"strings"
"testing"
"time"
)
func TestNativeCurrentLine(t *testing.T) {
if os.Getenv("CTHULHU_NATIVE_CURRENT_LINE") != "1" {
t.Skip("requires private Xvfb and AT-SPI")
}
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())
fixture := exec.CommandContext(ctx, "/usr/bin/python3", "-u", "../../tests/current_line_fixture.py")
fixture.Stderr = os.Stderr
stdin, _ := fixture.StdinPipe()
stdout, _ := fixture.StdoutPipe()
if err := fixture.Start(); err != nil {
t.Fatal(err)
}
defer func() { stdin.Close(); fixture.Wait() }()
scanner := bufio.NewScanner(stdout)
ready := func() {
t.Helper()
if !scanner.Scan() || scanner.Text() != "ready" {
t.Fatalf("fixture not ready: %q %v", scanner.Text(), scanner.Err())
}
}
ready()
var focus ObjectID
lastControl := ""
for _, probe := range []struct {
control, text string
offset int
want string
password bool
}{
{"entry", "Café 猫 complete", 7, "Café 猫 complete", false},
{"text", "First\nSecond café 猫\nThird", 11, "Second café 猫", false},
{"entry", "Final line", 10, "Final line", false},
{"text", "First\n\nLast", 6, "", false},
{"entry", "", 0, "", false},
{"text", "Final\n", 6, "", false},
{"password", "private test text", 5, "", true},
{"button", "", 0, "Example button", false},
{"deep", "Deep local line", 3, "Deep local line", false},
} {
command, _ := json.Marshal(map[string]any{"action": "focus", "control": probe.control, "text": probe.text, "offset": probe.offset})
fmt.Fprintln(stdin, string(command))
ready()
if probe.control != lastControl {
for {
select {
case event := <-adapter.Events():
if event.Type == "object:state-changed:focused" && event.Detail1 == 1 &&
(event.Source.Name == map[string]string{"entry": "Entry", "text": "Text", "password": "Password", "button": "Example button", "deep": "Deep entry"}[probe.control]) {
focus = event.Source.ID()
goto focused
}
case <-ctx.Done():
t.Fatal(ctx.Err())
}
}
}
focused:
lastControl = probe.control
content, err := adapter.ReadFocusedLine(ctx, focus)
focusedSnapshot, snapshotErr := adapter.ReadFocusedSnapshot(ctx, focus)
if snapshotErr != nil || focusedSnapshot.Snapshot.ID() != focus {
t.Fatalf("focused snapshot: %+v %v", focusedSnapshot, snapshotErr)
}
if probe.password {
if err == nil || content.Text != "" || content.Snapshot.Name != "" {
t.Fatalf("password report: %+v %v", content, err)
}
if focusedSnapshot.Snapshot.Text != "" || focusedSnapshot.Snapshot.Role != "password text" {
t.Fatal("focused snapshot exposed password text")
}
continue
}
if err != nil || strings.TrimRight(content.Text, "\r\n") != probe.want {
t.Fatalf("%s offset %d: %+v %v, want %q", probe.control, probe.offset, content, err, probe.want)
}
if probe.control == "deep" {
if len(content.SourceContext) != 0 || len(focusedSnapshot.SourceContext) != 0 {
t.Fatal("truncated native ancestry authorized export")
}
} else if len(content.SourceContext) == 0 || content.SourceContext[0].ID() != focus ||
content.SourceContext[len(content.SourceContext)-1].Role != "application" {
t.Fatalf("native report lost complete source ancestry: %+v", content.SourceContext)
}
if probe.control != "deep" && (len(focusedSnapshot.SourceContext) == 0 || focusedSnapshot.SourceContext[0].ID() != focus ||
focusedSnapshot.SourceContext[len(focusedSnapshot.SourceContext)-1].Role != "application") {
t.Fatal("focused snapshot lost complete source ancestry")
}
if probe.control == "entry" && probe.want != "" {
for _, role := range []struct{ atk, want string }{{"PASSWORD_TEXT", "password text"}, {"UNKNOWN", "unknown"}, {"INVALID", "invalid"}} {
fmt.Fprintf(stdin, "{\"action\":\"privacy\",\"role\":%q}\n", role.atk)
ready()
private, err := adapter.ReadFocusedLine(ctx, focus)
if err != nil || private.Text != content.Text {
t.Fatal("ancestor privacy changed local text", err)
}
found := false
for _, ancestor := range private.SourceContext {
found = found || ancestor.Role == role.want
}
if len(private.SourceContext) != 0 && !found {
t.Fatal("native capture omitted a private ancestor", role.want)
}
privateSnapshot, err := adapter.ReadFocusedSnapshot(ctx, focus)
if err != nil || privateSnapshot.Snapshot.Text != focusedSnapshot.Snapshot.Text {
t.Fatal("ancestor privacy changed the focused snapshot", err)
}
found = false
for _, ancestor := range privateSnapshot.SourceContext {
found = found || ancestor.Role == role.want
}
if len(privateSnapshot.SourceContext) != 0 && !found {
t.Fatal("focused snapshot omitted a private ancestor", role.want)
}
}
fmt.Fprintln(stdin, "{\"action\":\"privacy\",\"role\":\"FILLER\"}")
ready()
}
if probe.control != "button" {
fmt.Fprintf(stdin, "{\"action\":\"caret\",\"control\":%q}\n", probe.control)
if !scanner.Scan() || scanner.Text() != fmt.Sprint(probe.offset) {
t.Fatalf("caret moved: %q, want %d", scanner.Text(), probe.offset)
}
ready()
}
}
if _, err := adapter.ReadFocusedLine(ctx, ObjectID{BusName: focus.BusName, Path: "/stale"}); err == nil {
t.Fatal("accepted stale native focus")
}
if _, err := adapter.ReadFocusedSnapshot(ctx, ObjectID{BusName: focus.BusName, Path: "/stale"}); err == nil {
t.Fatal("accepted stale focused snapshot")
}
}