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,173 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.stormux.org/storm/cthulhu/internal/atspi"
|
||||
"git.stormux.org/storm/cthulhu/internal/braille"
|
||||
"git.stormux.org/storm/cthulhu/internal/config"
|
||||
"log/slog"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestBrailleHelperStartsOnlyWhenEnabled(t *testing.T) {
|
||||
worker := &brailleWorker{logger: slog.New(slog.DiscardHandler),
|
||||
path: filepath.Join(t.TempDir(), "missing-helper"), wake: make(chan struct{}, 1)}
|
||||
worker.Enqueue("focus")
|
||||
if worker.frame.Text != "" {
|
||||
t.Fatal("disabled braille retained a frame")
|
||||
}
|
||||
if err := worker.Configure(context.Background(), false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := worker.Configure(context.Background(), true); err == nil {
|
||||
t.Fatal("enabled braille accepted a missing helper")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBrailleWordWrapUpdatesCurrentAndMessageFrames(t *testing.T) {
|
||||
worker := &brailleWorker{wake: make(chan struct{}, 1), cancel: func() {}, connected: true}
|
||||
worker.EnqueueFrame(braille.Frame{Text: "Focused long line", Cursor: 0})
|
||||
worker.SetWordWrap(true)
|
||||
if !worker.current().WordWrap || !worker.baseFrame.WordWrap {
|
||||
t.Fatal("word wrap did not update current and restoration frames")
|
||||
}
|
||||
if !worker.EnqueueMessage("Long external message") || !worker.current().WordWrap {
|
||||
t.Fatal("external message lost word wrapping")
|
||||
}
|
||||
options := config.DefaultSettings().Braille
|
||||
options.WordWrap, options.FlashPersistent = true, true
|
||||
if !worker.Flash("Long flash message", options) || !worker.current().WordWrap {
|
||||
t.Fatal("flash message lost word wrapping")
|
||||
}
|
||||
worker.SetWordWrap(false)
|
||||
if worker.current().WordWrap || worker.baseFrame.WordWrap {
|
||||
t.Fatal("disabled word wrap did not update current and restoration frames")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBrailleFlashRestoresFocusAndHonorsPreferences(t *testing.T) {
|
||||
worker := &brailleWorker{wake: make(chan struct{}, 1), cancel: func() {}, connected: true}
|
||||
worker.EnqueueFrame(braille.Frame{Text: "focus", Cursor: 0})
|
||||
options := config.DefaultSettings().Braille
|
||||
options.FlashTime = 10
|
||||
if !worker.Flash("notice", options) || worker.current().Text != "notice" {
|
||||
t.Fatalf("flash frame = %+v", worker.current())
|
||||
}
|
||||
deadline := time.After(time.Second)
|
||||
for worker.current().Text != "focus" {
|
||||
select {
|
||||
case <-deadline:
|
||||
t.Fatal("flash did not restore focus")
|
||||
case <-time.After(time.Millisecond):
|
||||
}
|
||||
}
|
||||
options.FlashMessages = false
|
||||
if worker.Flash("ignored", options) || worker.current().Text != "focus" {
|
||||
t.Fatal("disabled flash changed the display")
|
||||
}
|
||||
options.FlashMessages = true
|
||||
options.FlashPersistent = true
|
||||
if !worker.Flash("persistent", options) {
|
||||
t.Fatal("persistent flash rejected")
|
||||
}
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
if worker.current().Text != "persistent" {
|
||||
t.Fatal("persistent flash expired")
|
||||
}
|
||||
worker.EnqueueFrame(braille.Frame{Text: "new focus", Cursor: -1})
|
||||
if worker.current().Text != "new focus" {
|
||||
t.Fatal("focus did not replace persistent flash")
|
||||
}
|
||||
}
|
||||
|
||||
type recordingBrailleRouter struct {
|
||||
calls chan atspi.BrailleRoute
|
||||
entered chan struct{}
|
||||
}
|
||||
|
||||
func (r *recordingBrailleRouter) RouteBraille(ctx context.Context, route atspi.BrailleRoute) error {
|
||||
r.calls <- route
|
||||
if r.entered != nil {
|
||||
close(r.entered)
|
||||
<-ctx.Done()
|
||||
return ctx.Err()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestBrailleWorkerRoutesOnlyCurrentTextCells(t *testing.T) {
|
||||
router := &recordingBrailleRouter{calls: make(chan atspi.BrailleRoute, 8)}
|
||||
worker := &brailleWorker{router: router, logger: slog.New(slog.DiscardHandler), wake: make(chan struct{}, 1), cancel: func() {}}
|
||||
snapshot := atspi.Snapshot{BusName: ":1.42", Path: "/entry", Name: "Message", Role: "entry", Editable: true,
|
||||
TextCaretKnown: true, BrailleLine: "é猫", BrailleLineStart: 20, BrailleCaret: 21, Required: true}
|
||||
worker.EnqueueFrame(brailleFocusFrame(snapshot))
|
||||
id := worker.current().ID
|
||||
for _, event := range []braille.RoutingEvent{{ID: id, Offset: 0}, {ID: id, Offset: 13}, {ID: id, Offset: -1}, {ID: id + 1, Offset: 8}} {
|
||||
worker.route(context.Background(), event)
|
||||
}
|
||||
if len(router.calls) != 0 {
|
||||
t.Fatal("routed label, suffix, invalid offset or stale frame")
|
||||
}
|
||||
for _, offset := range []int{8, 9, 10, 12} {
|
||||
worker.route(context.Background(), braille.RoutingEvent{ID: id, Offset: offset})
|
||||
}
|
||||
for _, want := range []int{20, 21, 22, 22} {
|
||||
select {
|
||||
case got := <-router.calls:
|
||||
if got.Offset != want || got.Focus != snapshot.ID() || got.Line != "é猫" {
|
||||
t.Fatalf("route %+v want offset %d", got, want)
|
||||
}
|
||||
default:
|
||||
t.Fatal("routing missing")
|
||||
}
|
||||
}
|
||||
worker.EnqueueFrame(braille.Frame{Cursor: -1})
|
||||
worker.route(context.Background(), braille.RoutingEvent{ID: id, Offset: 8})
|
||||
if len(router.calls) != 0 {
|
||||
t.Fatal("routed cleared focus")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBrailleWorkerCancelsRoutingWhenFrameChanges(t *testing.T) {
|
||||
router := &recordingBrailleRouter{calls: make(chan atspi.BrailleRoute, 1), entered: make(chan struct{})}
|
||||
worker := &brailleWorker{router: router, logger: slog.New(slog.DiscardHandler), wake: make(chan struct{}, 1), cancel: func() {}}
|
||||
worker.EnqueueFrame(braille.Frame{Text: "abc", Cursor: 0, Routes: []braille.TextRoute{{Start: 0, End: 3, TextLength: 3, BusName: ":1.42", Path: "/entry", SourceText: "abc"}}})
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
worker.route(context.Background(), braille.RoutingEvent{ID: worker.current().ID, Offset: 1})
|
||||
}()
|
||||
select {
|
||||
case <-router.entered:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("route did not start")
|
||||
}
|
||||
worker.EnqueueFrame(braille.Frame{Cursor: -1})
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("route was not canceled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBrailleStopTimeoutRetainsJoinState(t *testing.T) {
|
||||
done := make(chan struct{})
|
||||
worker := &brailleWorker{cancel: func() {}, done: done}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
if err := worker.Stop(ctx); err != context.Canceled {
|
||||
t.Fatalf("stop error %v", err)
|
||||
}
|
||||
if worker.cancel == nil || worker.done == nil || !worker.stopping {
|
||||
t.Fatal("lost timed-out worker")
|
||||
}
|
||||
close(done)
|
||||
if err := worker.Stop(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if worker.cancel != nil || worker.done != nil {
|
||||
t.Fatal("completed stop retained session")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user