111 lines
2.8 KiB
Go
111 lines
2.8 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// Authors: Storm Dragon, Angus Kola
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package app
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.stormux.org/storm/cthulhu/internal/atspi"
|
|
)
|
|
|
|
func TestNativeBrowserSelection(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 := atspi.New()
|
|
if err := adapter.Start(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer adapter.Stop(context.Background())
|
|
keys := func(chord string) {
|
|
t.Helper()
|
|
if out, err := exec.CommandContext(ctx, "xdotool", "key", "--clearmodifiers", chord).CombinedOutput(); err != nil {
|
|
t.Fatalf("keys %s: %v %s", chord, err, out)
|
|
}
|
|
}
|
|
var entry atspi.Snapshot
|
|
for attempt := 0; attempt < 30 && !entry.ID().Valid(); attempt++ {
|
|
keys("Tab")
|
|
timer := time.NewTimer(250 * time.Millisecond)
|
|
waitFocus:
|
|
for {
|
|
select {
|
|
case event := <-adapter.Events():
|
|
if isFocusEvent(event.Type) && event.Source.Name == "Display name" && event.Source.Editable {
|
|
entry = event.Source
|
|
break waitFocus
|
|
}
|
|
case <-timer.C:
|
|
break waitFocus
|
|
case <-ctx.Done():
|
|
t.Fatal(ctx.Err())
|
|
}
|
|
}
|
|
timer.Stop()
|
|
}
|
|
if !entry.ID().Valid() {
|
|
t.Fatal("editable browser input not focused")
|
|
}
|
|
if out, err := exec.CommandContext(ctx, "xdotool", "type", "--clearmodifiers", "--delay", "20", "hello world").CombinedOutput(); err != nil {
|
|
t.Fatalf("type fixture text: %v %s", err, out)
|
|
}
|
|
waitSelection := func(want string) {
|
|
t.Helper()
|
|
deadline := time.NewTimer(3 * time.Second)
|
|
defer deadline.Stop()
|
|
for {
|
|
select {
|
|
case event := <-adapter.Events():
|
|
if event.Type == atspi.TextSelectionChangedEvent && event.Source.ID() == entry.ID() &&
|
|
strings.Contains(event.Source.SelectionText, want) {
|
|
if event.Source.SelectionEnd <= event.Source.SelectionStart {
|
|
t.Fatalf("selection range missing: %+v", event.Source)
|
|
}
|
|
return
|
|
}
|
|
case <-deadline.C:
|
|
t.Fatalf("no native selection containing %q", want)
|
|
case <-ctx.Done():
|
|
t.Fatal(ctx.Err())
|
|
}
|
|
}
|
|
}
|
|
keys("ctrl+a")
|
|
waitSelection("hello world")
|
|
keys("Right")
|
|
keys("shift+Left")
|
|
waitSelection("d")
|
|
keys("ctrl+shift+Left")
|
|
waitSelection("world")
|
|
waitMutation := func(eventType string) {
|
|
t.Helper()
|
|
deadline := time.NewTimer(3 * time.Second)
|
|
defer deadline.Stop()
|
|
for {
|
|
select {
|
|
case event := <-adapter.Events():
|
|
if event.Type == eventType && event.Source.ID() == entry.ID() {
|
|
return
|
|
}
|
|
case <-deadline.C:
|
|
t.Fatalf("no native %s in browser input", eventType)
|
|
case <-ctx.Done():
|
|
t.Fatal(ctx.Err())
|
|
}
|
|
}
|
|
}
|
|
keys("ctrl+x")
|
|
waitMutation(atspi.TextDeletedEvent)
|
|
keys("ctrl+v")
|
|
waitMutation(atspi.TextInsertedEvent)
|
|
}
|