174 lines
6.0 KiB
Go
174 lines
6.0 KiB
Go
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")
|
|
}
|
|
}
|