Cthulhu is no longer a fork. Now it is a screen reader written in Golang.

This commit is contained in:
Storm Dragon committed 2026-10-14 00:16:34 -04:00
1 parent a998fe67de
commit 069fc674dc
708 files changed
+112482 -532

No files matched your search

+114
View File
@@ -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")
}
}