133 lines
5.2 KiB
Go
133 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"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.stormux.org/storm/cthulhu/internal/atspi"
|
|
"git.stormux.org/storm/cthulhu/internal/config"
|
|
"git.stormux.org/storm/cthulhu/internal/input"
|
|
"git.stormux.org/storm/cthulhu/internal/presentation"
|
|
"git.stormux.org/storm/cthulhu/internal/review"
|
|
)
|
|
|
|
func TestPhoneticHintsDelayAndEligibility(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name, text string
|
|
enabled, arrow, eligible bool
|
|
want []string
|
|
}{
|
|
{"enabled", "a", true, true, true, []string{"a", "alpha"}},
|
|
{"capital", "A", true, true, true, []string{"A", "alpha"}},
|
|
{"disabled", "a", false, true, true, []string{"a"}},
|
|
{"modified arrow or Home", "a", true, false, true, []string{"a"}},
|
|
{"typing or flat review", "a", true, true, false, []string{"a"}},
|
|
{"space", "space", true, true, true, []string{"space"}},
|
|
{"punctuation", "!", true, true, true, []string{"!"}},
|
|
{"non Latin letter", "猫", true, true, true, []string{"猫"}},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
speaker := &spellingTestSpeaker{}
|
|
worker := newSpeechWorker(slog.New(slog.DiscardHandler), speaker)
|
|
worker.reading = func() config.ReadingSettings { return config.ReadingSettings{PhoneticCharacterHints: test.enabled} }
|
|
worker.inputActivity = func() input.Activity { return input.Activity{PlainCharacterArrow: test.arrow, InputSequence: 1} }
|
|
start := time.Now()
|
|
if err := worker.deliver(context.Background(), speechRequest{Text: test.text, Character: true, PhoneticHint: test.eligible}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !reflect.DeepEqual(speaker.parts, test.want) {
|
|
t.Fatalf("speech = %v, want %v", speaker.parts, test.want)
|
|
}
|
|
if len(test.want) == 2 && time.Since(start) < phoneticHintDelay {
|
|
t.Fatal("hint arrived before its delay")
|
|
}
|
|
if !reflect.DeepEqual(speaker.characters, []string{test.text}) {
|
|
t.Fatalf("character speech changed: %v", speaker.characters)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
type phoneticHintSpeaker struct{ parts chan string }
|
|
|
|
func (*phoneticHintSpeaker) Connect(context.Context) error { return nil }
|
|
func (*phoneticHintSpeaker) Close(context.Context) error { return nil }
|
|
func (*phoneticHintSpeaker) Cancel(context.Context) error { return nil }
|
|
func (speaker *phoneticHintSpeaker) Speak(_ context.Context, text string) error {
|
|
speaker.parts <- text
|
|
return nil
|
|
}
|
|
func (speaker *phoneticHintSpeaker) SpeakCharacter(ctx context.Context, text string) error {
|
|
return speaker.Speak(ctx, text)
|
|
}
|
|
|
|
func TestPhoneticHintFreshInputDoesNotWaitForDelay(t *testing.T) {
|
|
speaker := &phoneticHintSpeaker{parts: make(chan string, 4)}
|
|
worker := newSpeechWorker(slog.New(slog.DiscardHandler), speaker)
|
|
worker.reading = func() config.ReadingSettings { return config.ReadingSettings{PhoneticCharacterHints: true} }
|
|
worker.inputActivity = func() input.Activity { return input.Activity{PlainCharacterArrow: true, InputSequence: 1} }
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
if err := worker.Start(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer worker.Stop(context.Background())
|
|
worker.Enqueue(speechRequest{Text: "a", Character: true, PhoneticHint: true})
|
|
if got := <-speaker.parts; got != "a" {
|
|
t.Fatal(got)
|
|
}
|
|
// Wait for the delay to be owned, then exercise the real input barrier.
|
|
deadline := time.After(time.Second)
|
|
for {
|
|
worker.mutex.Lock()
|
|
waiting := worker.hintCancel != nil
|
|
worker.mutex.Unlock()
|
|
if waiting {
|
|
break
|
|
}
|
|
select {
|
|
case <-deadline:
|
|
t.Fatal("hint delay was never registered")
|
|
case <-time.After(time.Millisecond):
|
|
}
|
|
}
|
|
if !worker.InterruptForInput(input.NewKeyEvent(1, true, "Right")) {
|
|
t.Fatal("fresh arrow did not interrupt pending hint")
|
|
}
|
|
worker.Enqueue(speechRequest{Text: "next"})
|
|
select {
|
|
case got := <-speaker.parts:
|
|
if got != "next" {
|
|
t.Fatalf("stale hint survived fresh input: %q", got)
|
|
}
|
|
case <-time.After(200 * time.Millisecond):
|
|
t.Fatal("fresh input waited for the phonetic delay")
|
|
}
|
|
}
|
|
|
|
func TestCharacterNavigationMarksHintEligibility(t *testing.T) {
|
|
queue := &fakeSpeechQueue{requests: make(chan speechRequest, 8), spoken: make(chan string, 8)}
|
|
state := &eventLoopState{}
|
|
state.presentationOptions = presentation.DefaultOptions()
|
|
presenter := eventPresenter{state: state, logger: slog.New(slog.DiscardHandler), speech: queue}
|
|
presenter.speak(atspi.Event{Type: atspi.CaretEvent, Source: atspi.Snapshot{Path: "/entry", Role: "entry", CaretCharacter: "a"}})
|
|
if request := <-queue.requests; !request.PhoneticHint || !request.Character {
|
|
t.Fatalf("native character = %+v", request)
|
|
}
|
|
for _, command := range []review.DocumentCaretCommand{review.CaretNextCharacter, review.CaretPreviousCharacter, review.CaretLineStart, review.CaretNextWord} {
|
|
presentDocumentCaretResult(slog.New(slog.DiscardHandler), queue, review.Result{Moved: true, DocumentCaretCommand: command, DocumentCaretText: "a"}, presentation.DefaultOptions())
|
|
request := <-queue.requests
|
|
want := command == review.CaretNextCharacter || command == review.CaretPreviousCharacter
|
|
if request.PhoneticHint != want {
|
|
t.Fatalf("%v hint = %t", command, request.PhoneticHint)
|
|
}
|
|
}
|
|
}
|