Cthulhu is no longer a fork. Now it is a screen reader written in Golang.
This commit is contained in:
1 parent
a998fe67de
commit
069fc674dc
708 files changed
+112482
-532
No files matched your search
@@ -0,0 +1,114 @@
|
||||
// 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/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)}
|
||||
handler := remoteSpeech{speech: output, braille: output}
|
||||
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)
|
||||
}
|
||||
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", "É", "é", "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")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user