133 lines
5.3 KiB
Go
133 lines
5.3 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"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.stormux.org/storm/cthulhu/internal/config"
|
|
"git.stormux.org/storm/cthulhu/internal/remote"
|
|
"github.com/godbus/dbus/v5"
|
|
)
|
|
|
|
type wineTestOutput struct {
|
|
speech chan speechRequest
|
|
braille chan string
|
|
}
|
|
|
|
func (output *wineTestOutput) Enqueue(request speechRequest) { output.speech <- request }
|
|
func (output *wineTestOutput) Interrupt() {}
|
|
func (output *wineTestOutput) EnqueueMessage(message string) bool {
|
|
output.braille <- message
|
|
return true
|
|
}
|
|
|
|
func TestNativeSessionBusWineEvents(t *testing.T) {
|
|
if os.Getenv("CTHULHU_NATIVE_DBUS_TEST") != "1" {
|
|
t.Skip("set CTHULHU_NATIVE_DBUS_TEST=1 inside a private session bus")
|
|
}
|
|
output := &wineTestOutput{speech: make(chan speechRequest, 32), braille: make(chan string, 32)}
|
|
settings := config.DefaultSettings()
|
|
settings.KeyEchoInEditFieldsOnly = true
|
|
live := &liveSettings{}
|
|
live.Store(settings)
|
|
handler := remoteSpeech{speech: output, braille: output, live: live}
|
|
owner := newWineEventPresenter(func(message string) bool {
|
|
return handler.presentMessage(message, "wine-accessible-event")
|
|
})
|
|
defer owner.Close()
|
|
handler.wine = owner
|
|
service := remote.New(slog.New(slog.DiscardHandler), handler)
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
if err := service.Start(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer service.Stop(context.Background())
|
|
client, err := dbus.ConnectSessionBus()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer client.Close()
|
|
object := client.Object(remote.BusName, remote.ObjectPath)
|
|
send := func(event remote.AccessibleEvent, want bool) {
|
|
t.Helper()
|
|
var accepted bool
|
|
if err := object.CallWithContext(ctx, remote.InterfaceName+".PresentAccessibleEvent", 0,
|
|
event.SourceID, event.EventType, event.Name, event.Role, event.Value, event.States,
|
|
event.Description, event.Position, event.Count, event.WindowTitle).Store(&accepted); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if accepted != want {
|
|
t.Fatalf("%s accepted=%v want=%v", event.EventType, accepted, want)
|
|
}
|
|
}
|
|
send(remote.AccessibleEvent{SourceID: "42:text", EventType: "focus", Role: "edit", Value: "hello"}, true)
|
|
owner.flush()
|
|
for _, text := range []string{"l", "l", "e selected", "e deleted"} {
|
|
send(remote.AccessibleEvent{SourceID: "42:text", EventType: "text", Name: text, States: "start=1 end=2 kind=5"}, true)
|
|
}
|
|
for _, text := range []string{"A", "a", "É", "é"} {
|
|
send(remote.AccessibleEvent{SourceID: "42:text", EventType: "text", Name: text, States: "start=1 end=1 kind=8"}, true)
|
|
}
|
|
settings.EnableKeyEcho = false
|
|
live.Store(settings)
|
|
for _, text := range []string{"A", "space", "tab", "new line"} {
|
|
send(remote.AccessibleEvent{SourceID: "42:text", EventType: "text", Name: text, States: "kind=8"}, false)
|
|
}
|
|
send(remote.AccessibleEvent{SourceID: "42:text", EventType: "text", Name: "e", States: "kind=7"}, true)
|
|
settings.EnableKeyEcho = true
|
|
settings.EnableAlphabeticKeys = false
|
|
live.Store(settings)
|
|
send(remote.AccessibleEvent{SourceID: "42:text", EventType: "text", Name: "A", States: "kind=8"}, false)
|
|
settings.EnableAlphabeticKeys = true
|
|
live.Store(settings)
|
|
send(remote.AccessibleEvent{SourceID: "42:text", EventType: "text", Name: "A", States: "kind=8"}, true)
|
|
send(remote.AccessibleEvent{SourceID: "check", EventType: "focus", Name: "Option", Role: "check box", States: "not checked"}, true)
|
|
owner.flush()
|
|
send(remote.AccessibleEvent{SourceID: "check", EventType: "name", Name: "Renamed", Role: "check box", States: "not checked"}, true)
|
|
send(remote.AccessibleEvent{SourceID: "check", EventType: "state", Name: "Renamed", Role: "check box", States: "checked, unavailable"}, true)
|
|
send(remote.AccessibleEvent{SourceID: "42:text", EventType: "text", Name: "stale"}, false)
|
|
owner.flush()
|
|
send(remote.AccessibleEvent{SourceID: "yes", EventType: "focus", Name: "Yes", Role: "client"}, true)
|
|
owner.flush()
|
|
owner.mu.Lock()
|
|
owner.lastTime = time.Now().Add(-time.Second)
|
|
owner.mu.Unlock()
|
|
send(remote.AccessibleEvent{SourceID: "no", EventType: "state", Name: "No", Role: "client"}, false)
|
|
send(remote.AccessibleEvent{SourceID: "yes", EventType: "state", Name: "Yes", Role: "client"}, false)
|
|
send(remote.AccessibleEvent{SourceID: "no", EventType: "focus", Name: "No", Role: "client"}, true)
|
|
owner.flush()
|
|
owner.Close()
|
|
for _, want := range []string{"edit, hello", "l", "l", "e selected", "e deleted", "A", "a", "É", "é", "e", "A", "Option, check box, not checked", "Renamed, check box, not checked", "Renamed, check box, checked, unavailable", "Yes", "No"} {
|
|
select {
|
|
case request := <-output.speech:
|
|
if request.Text != want || !request.Append {
|
|
t.Fatalf("speech=%+v want=%q", request, want)
|
|
}
|
|
if (want == "A" || want == "a" || want == "É" || want == "é") && !request.Character {
|
|
t.Fatalf("Wine typing lost character routing: %+v", request)
|
|
}
|
|
case <-ctx.Done():
|
|
t.Fatal("timed out awaiting speech queue")
|
|
}
|
|
select {
|
|
case message := <-output.braille:
|
|
if message != want {
|
|
t.Fatalf("braille=%q want=%q", message, want)
|
|
}
|
|
case <-ctx.Done():
|
|
t.Fatal("timed out awaiting braille queue")
|
|
}
|
|
}
|
|
if len(output.speech) != 0 || len(output.braille) != 0 {
|
|
t.Fatal("unexpected extra Wine output")
|
|
}
|
|
}
|