149 lines
4.1 KiB
Go
149 lines
4.1 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// Authors: Storm Dragon, Angus Kola
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package app
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.stormux.org/storm/cthulhu/internal/atspi"
|
|
"git.stormux.org/storm/cthulhu/internal/platform/xorg"
|
|
"git.stormux.org/storm/cthulhu/internal/presentation"
|
|
)
|
|
|
|
func TestNativeBrowserEditorTyping(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())
|
|
listener := xorg.New()
|
|
if err := listener.Start(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer listener.Stop(context.Background())
|
|
var focus atomic.Pointer[atspi.Snapshot]
|
|
var insertion atomic.Pointer[atspi.Event]
|
|
observed := &browserEventProbe{Adapter: adapter, events: make(chan atspi.Event, 64)}
|
|
relayDone := make(chan struct{})
|
|
go func() {
|
|
defer close(relayDone)
|
|
for {
|
|
select {
|
|
case event := <-adapter.Events():
|
|
if isFocusGainEvent(event) {
|
|
focus.Store(&event.Source)
|
|
}
|
|
if event.Type == atspi.TextInsertedEvent && event.Source.Role == "paragraph" {
|
|
insertion.Store(&event)
|
|
}
|
|
select {
|
|
case observed.events <- event:
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
defer func() { cancel(); <-relayDone }()
|
|
queue := &fakeSpeechQueue{spoken: make(chan string, 128)}
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
options := presentation.DefaultOptions()
|
|
options.EnableKeyEcho = false
|
|
processEventsWithInput(ctx, slog.New(slog.DiscardHandler), queue, observed, nil, nil,
|
|
20*time.Millisecond, nil, listener, nil, nil, options)
|
|
}()
|
|
defer func() { cancel(); <-done }()
|
|
keys := func(chord string) {
|
|
t.Helper()
|
|
if out, err := exec.CommandContext(ctx, "xdotool", "key", "--clearmodifiers", chord).CombinedOutput(); err != nil {
|
|
t.Fatalf("keys: %v %s", err, out)
|
|
}
|
|
}
|
|
collect := func() []string {
|
|
t.Helper()
|
|
timer := time.NewTimer(180 * time.Millisecond)
|
|
defer timer.Stop()
|
|
var texts []string
|
|
for {
|
|
select {
|
|
case text := <-queue.spoken:
|
|
texts = append(texts, text)
|
|
case <-timer.C:
|
|
return texts
|
|
case <-ctx.Done():
|
|
t.Fatal(ctx.Err())
|
|
}
|
|
}
|
|
}
|
|
for attempt := 0; attempt < 20; attempt++ {
|
|
keys("Tab")
|
|
collect()
|
|
if current := focus.Load(); current != nil && current.Name == "Message composer" {
|
|
break
|
|
}
|
|
}
|
|
editor := focus.Load()
|
|
if editor == nil || editor.Name != "Message composer" || !editor.Editable || editor.Role != "entry" {
|
|
t.Fatalf("composer focus = %+v", editor)
|
|
}
|
|
keys("End")
|
|
collect()
|
|
for _, letter := range []string{"h", "e", "l", "l", "o"} {
|
|
keys(letter)
|
|
if got := collect(); len(got) != 0 {
|
|
t.Fatalf("typing %q with echo disabled spoke %q", letter, got)
|
|
}
|
|
}
|
|
if event := insertion.Load(); event == nil || event.Source.EditableRoot != editor.ID() {
|
|
t.Fatalf("paragraph insertion lost editor ownership: %+v", event)
|
|
}
|
|
keys("Left")
|
|
if got := collect(); len(got) != 1 || got[0] != "o" {
|
|
t.Fatalf("character reading = %q", got)
|
|
}
|
|
keys("Down")
|
|
if got := collect(); len(got) != 1 || got[0] != "Following line" {
|
|
t.Fatalf("next line reading = %q", got)
|
|
}
|
|
keys("Up")
|
|
if got := collect(); len(got) != 1 || got[0] != "Draft:hello" {
|
|
t.Fatalf("line reading = %q", got)
|
|
}
|
|
keys("End")
|
|
collect()
|
|
keys("BackSpace")
|
|
if got := collect(); len(got) != 1 || got[0] != "o" {
|
|
t.Fatalf("deletion = %q", got)
|
|
}
|
|
keys("Tab")
|
|
if got := collect(); len(got) == 0 || !strings.Contains(strings.Join(got, " "), "Other editor") {
|
|
t.Fatalf("next editor focus = %q", got)
|
|
}
|
|
keys("x")
|
|
if got := collect(); len(got) != 0 {
|
|
t.Fatalf("typing in second editor spoke %q", got)
|
|
}
|
|
if current, event := focus.Load(), insertion.Load(); current == nil || current.Name != "Other editor" ||
|
|
event == nil || event.Source.EditableRoot != current.ID() || event.Source.EditableRoot == editor.ID() {
|
|
t.Fatalf("second editor ownership: focus=%+v insertion=%+v", current, event)
|
|
}
|
|
}
|