Reset master to testing content
This commit is contained in:
1 parent
0f2587b719
commit
42afa2a56c
1483 files changed
+145308
-938069
No files matched your search
@@ -0,0 +1,258 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// Authors: Storm Dragon, Angus Kola
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"path/filepath"
|
||||
"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/preferences"
|
||||
)
|
||||
|
||||
func TestEveryEchoPreferenceRoundTrips(t *testing.T) {
|
||||
settings := config.DefaultSettings()
|
||||
if !settings.EnableKeyEcho {
|
||||
t.Fatal("key echo must default on")
|
||||
}
|
||||
settings.Path = filepath.Join(t.TempDir(), "settings.toml")
|
||||
values := preferences.FromSettings(settings)
|
||||
// Flip each echo switch, including false defaults, to exercise both directions.
|
||||
value := reflect.ValueOf(&values).Elem()
|
||||
for i := 0; i < value.NumField(); i++ {
|
||||
if value.Field(i).Kind() == reflect.Bool {
|
||||
value.Field(i).SetBool(!value.Field(i).Bool())
|
||||
}
|
||||
}
|
||||
rate, pitch, volume := 100.0, 10.0, 10.0
|
||||
values.EchoVoice = config.Voice{Rate: &rate, AveragePitch: &pitch, Gain: &volume, Family: &config.VoiceFamily{Name: "Test Voice", Lang: "en", Variant: "test"}}
|
||||
values.EchoSpeechServer = "test-module"
|
||||
next, err := values.Merge(settings)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
store, err := config.NewStore(settings.Path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err = store.Load(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = store.Save(next); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
loaded, err := config.Load(settings.Path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := preferences.FromSettings(loaded); !reflect.DeepEqual(got, values) {
|
||||
t.Fatalf("round trip = %+v; want %+v", got, values)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyUpdatesRunningTextAndKeyboardEcho(t *testing.T) {
|
||||
settings := config.DefaultSettings()
|
||||
settings.EnableModifierKeys = false
|
||||
settings.Path = filepath.Join(t.TempDir(), "settings.toml")
|
||||
live := &liveSettings{}
|
||||
live.Store(settings)
|
||||
worker := newPreferencesWorker(slog.New(slog.DiscardHandler), settings, nil)
|
||||
worker.live = live
|
||||
queue := &fakeSpeechQueue{spoken: make(chan string, 10), interrupt: make(chan struct{}, 4)}
|
||||
adapter := &fakeAdapter{events: make(chan atspi.Event, 10)}
|
||||
listener := &fakeInputListener{events: make(chan input.Event, 10), modifierKeycodes: input.NewModifierKeycodes(37)}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
done := make(chan struct{}, 2)
|
||||
go func() {
|
||||
processEventsWithSettings(ctx, slog.New(slog.DiscardHandler), queue, adapter, nil, nil, 0, live)
|
||||
done <- struct{}{}
|
||||
}()
|
||||
go func() {
|
||||
processInputEvents(ctx, slog.New(slog.DiscardHandler), queue, listener, inputEchoSettingsFromConfig(settings), live, nil)
|
||||
done <- struct{}{}
|
||||
}()
|
||||
defer func() { cancel(); <-done; <-done }()
|
||||
expect := func(want string) {
|
||||
t.Helper()
|
||||
select {
|
||||
case got := <-queue.spoken:
|
||||
if got != want {
|
||||
t.Fatalf("speech %q, want %q", got, want)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatalf("missing %q", want)
|
||||
}
|
||||
}
|
||||
snapshot := atspi.NewSnapshot("/editor", "", "text", "", "")
|
||||
snapshot.Editable = true
|
||||
insert := func(text string) {
|
||||
adapter.events <- atspi.NewEventWithPayload(atspi.TextInsertedEvent, snapshot, text)
|
||||
}
|
||||
insert("a")
|
||||
expect("a")
|
||||
listener.events <- input.NewKeyEvent(67, true, "F1")
|
||||
expect("f1")
|
||||
<-queue.interrupt
|
||||
values := preferences.FromSettings(settings)
|
||||
values.EnableKeyEcho = false
|
||||
// Echo-only changes must work without a speech settings updater.
|
||||
if r := worker.apply(ctx, preferences.Request{Values: values, Save: true}); !r.Accepted || r.Close {
|
||||
t.Fatalf("Apply = %+v", r)
|
||||
}
|
||||
insert("b")
|
||||
insert("barrier")
|
||||
expect("barrier")
|
||||
listener.events <- input.NewKeyEvent(68, true, "F2")
|
||||
listener.events <- input.NewKeyEvent(37, true, "Control_L")
|
||||
for range 2 {
|
||||
select {
|
||||
case <-queue.interrupt:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("keyboard barrier missing")
|
||||
}
|
||||
}
|
||||
values.EnableKeyEcho = true
|
||||
if r := worker.apply(ctx, preferences.Request{Values: values, Save: true, Close: true}); !r.Accepted || !r.Close {
|
||||
t.Fatalf("OK = %+v", r)
|
||||
}
|
||||
insert("c")
|
||||
expect("c")
|
||||
listener.events <- input.NewKeyEvent(69, true, "F3")
|
||||
expect("f3")
|
||||
loaded, err := config.Load(settings.Path)
|
||||
if err != nil || !loaded.EnableKeyEcho {
|
||||
t.Fatalf("saved state: %+v, %v", loaded, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCustomEchoVoiceInheritanceAndRouting(t *testing.T) {
|
||||
settings := config.DefaultSettings()
|
||||
settings.UseCustomEchoVoice = true
|
||||
rate, pitch, volume := 65.0, 10.0, 10.0
|
||||
settings.DefaultVoice = config.Voice{Rate: &rate, Family: &config.VoiceFamily{Name: "base", Lang: "en"}}
|
||||
settings.EchoVoice = config.Voice{AveragePitch: &pitch, Gain: &volume}
|
||||
settings.UseCustomEchoSpeechServer = true
|
||||
settings.EchoSpeechServer = "echo-module"
|
||||
got := echoSpeechSettings(settings)
|
||||
if *got.Rate != 32 || *got.Pitch != 90 || *got.Volume != 100 || got.OutputModule != "echo-module" || got.SynthesisVoice != "base" {
|
||||
t.Fatalf("echo settings = %+v", got)
|
||||
}
|
||||
if !customEcho(settings, "key") || !customEcho(settings, "character") || customEcho(settings, "word") || customEcho(settings, "sentence") || customEcho(settings, "") {
|
||||
t.Fatal("wrong default custom voice routing")
|
||||
}
|
||||
settings.UseCustomEchoForWord = true
|
||||
settings.UseCustomEchoForSentence = true
|
||||
if !customEcho(settings, "word") || !customEcho(settings, "sentence") {
|
||||
t.Fatal("custom routing flags ignored")
|
||||
}
|
||||
settings.EnableKeyEcho = false
|
||||
if customEcho(settings, "key") {
|
||||
t.Fatal("custom echo survived master disable")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEditFieldsOnlyKeyNamesAndVoice(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
only, enabled, focused, known bool
|
||||
want bool
|
||||
}{
|
||||
{"all fields", false, true, false, false, true},
|
||||
{"disabled", true, false, true, true, false},
|
||||
{"editor", true, true, true, true, true},
|
||||
{"button", true, true, false, true, false},
|
||||
{"unknown", true, true, false, false, false},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
settings := config.DefaultSettings()
|
||||
settings.EnableKeyEcho = tc.enabled
|
||||
settings.KeyEchoInEditFieldsOnly = tc.only
|
||||
settings.UseCustomEchoVoice = true
|
||||
settings.UseCustomEchoForKey = true
|
||||
settings.UseCustomEchoForCharacter = false
|
||||
live := &liveSettings{}
|
||||
live.Store(settings)
|
||||
if tc.known {
|
||||
live.editFieldFocused = func() bool { return tc.focused }
|
||||
}
|
||||
listener := &fakeInputListener{events: make(chan input.Event, 1)}
|
||||
listener.events <- input.NewKeyEvent(67, true, "F1")
|
||||
close(listener.events)
|
||||
speech := &remoteSpeechQueue{}
|
||||
processInputEvents(context.Background(), slog.New(slog.DiscardHandler), speech, listener, inputEchoSettingsFromConfig(settings), live, nil)
|
||||
if (len(speech.requests) != 0) != tc.want {
|
||||
t.Fatalf("requests=%+v", speech.requests)
|
||||
}
|
||||
if tc.want && (speech.requests[0].Text != "f1" || !speech.requests[0].CustomEcho) {
|
||||
t.Fatalf("key voice lost: %+v", speech.requests[0])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEditFieldsOnlyPreferencesApplyAndReload(t *testing.T) {
|
||||
settings := config.DefaultSettings()
|
||||
settings.Path = filepath.Join(t.TempDir(), "settings.toml")
|
||||
live := &liveSettings{}
|
||||
live.Store(settings)
|
||||
worker := newPreferencesWorker(slog.New(slog.DiscardHandler), settings, nil)
|
||||
worker.live = live
|
||||
values := preferences.FromSettings(settings)
|
||||
values.KeyEchoInEditFieldsOnly = true
|
||||
response := worker.apply(context.Background(), preferences.Request{Values: values, Save: true})
|
||||
if !response.Accepted {
|
||||
t.Fatalf("Apply: %+v", response)
|
||||
}
|
||||
loaded, err := config.Load(settings.Path)
|
||||
if err != nil || !loaded.EnableKeyEcho || !loaded.KeyEchoInEditFieldsOnly {
|
||||
t.Fatalf("reload: %+v, %v", loaded, err)
|
||||
}
|
||||
if !live.Load().KeyEchoInEditFieldsOnly || !presentationSettingsFromConfig(live.Load(), false).KeyEchoInEditFieldsOnly {
|
||||
t.Fatal("Apply did not reach typing policy")
|
||||
}
|
||||
values.KeyEchoInEditFieldsOnly = false
|
||||
response = worker.apply(context.Background(), preferences.Request{Values: values, Save: true})
|
||||
if !response.Accepted || live.Load().KeyEchoInEditFieldsOnly {
|
||||
t.Fatal("could not return to unrestricted echo")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEditFieldsOnlyNativeTypingVoice(t *testing.T) {
|
||||
settings := config.DefaultSettings()
|
||||
settings.KeyEchoInEditFieldsOnly = true
|
||||
settings.UseCustomEchoVoice = true
|
||||
settings.UseCustomEchoForKey = true
|
||||
settings.UseCustomEchoForCharacter = false
|
||||
live := &liveSettings{}
|
||||
live.Store(settings)
|
||||
queue := &remoteSpeechQueue{}
|
||||
state := &eventLoopState{}
|
||||
state.presentationOptions = presentationSettingsFromConfig(settings, false)
|
||||
presenter := eventPresenter{state: state, live: live, speech: queue, logger: slog.New(slog.DiscardHandler)}
|
||||
source := atspi.Snapshot{Role: "text", Editable: true, Focused: true}
|
||||
presenter.speak(atspi.Event{Type: atspi.TextInsertedEvent, Source: source, Payload: "A"})
|
||||
if len(queue.requests) != 1 || !queue.requests[0].CustomEcho || !queue.requests[0].Character {
|
||||
t.Fatalf("typed key voice: %+v", queue.requests)
|
||||
}
|
||||
source.Role = "terminal"
|
||||
source.Editable = false
|
||||
presenter.speak(atspi.Event{Type: atspi.TextInsertedEvent, Source: source, Payload: "b"})
|
||||
if len(queue.requests) != 1 {
|
||||
t.Fatal("terminal echoed typed key")
|
||||
}
|
||||
source.Role = "section"
|
||||
source.LiveRegion = true
|
||||
source.LivePoliteness = "polite"
|
||||
presenter.speak(atspi.Event{Type: atspi.TextInsertedEvent, Source: source, Payload: "c"})
|
||||
if len(queue.requests) != 2 || queue.requests[1].Text != "c" || queue.requests[1].CustomEcho {
|
||||
t.Fatalf("live region changed: %+v", queue.requests)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user