// 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) }