Wine editor insertion events previously reached speech and Braille without checking the user's typing echo preferences. Turning off key echo or an individual character category therefore left Wine typing feedback enabled. Read the live settings for each insertion and reuse the native text presentation policy for alphabetic, numeric, punctuation, symbol, space and diacritical input. Honor character echo and the master key echo switch; apply the action-key setting to the helper's tab and new-line labels. Reject unexpected multi-character insertion labels rather than treating these ordered character events as paste notifications. Preserve navigation, selection and deletion feedback when typing echo is disabled. Retain repeated characters, case, uppercase/custom voice routing, focus ordering and the existing stale-source/password protections. Add regression coverage for live disable/re-enable, independent categories, Unicode, character-echo overrides, and unaffected editor actions. Extend the private D-Bus test to exercise settings changes through the real Wine service signature. Document behavior, current word/sentence echo limits, and live Notepad acceptance checks in the README. Validation: - Reproduced disabled typing reaching output with the new test before fixing. - Full go test -race ./... -count=1 passed outside the socket-restricted sandbox. - Private-session TestNativeSessionBusWineEvents passed with -race. - Focused typing/editor regressions passed, including a fresh pre-commit run. - go vet passed for internal/app, presentation, remote and wine. - Reader and BrlAPI helper builds passed; reader --version succeeded. - gofmt and git diff --check passed; independent review found no blockers. Live audible speech, physical keyboard and physical Braille acceptance remain pending. No running reader was replaced. Changes target deedra.
132 lines
5.2 KiB
Go
132 lines
5.2 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()
|
|
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")
|
|
}
|
|
}
|