Files

118 lines
3.7 KiB
Go

// Cthulhu screen reader.
// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along with
// this program. If not, see <https://www.gnu.org/licenses/gpl-3.0.html>.
package app
import (
"context"
"log/slog"
"testing"
"time"
"git.stormux.org/storm/cthulhu/internal/atspi"
"git.stormux.org/storm/cthulhu/internal/input"
"git.stormux.org/storm/cthulhu/internal/sound"
)
func TestRunSpeaksFocusEventsAndStops(t *testing.T) {
speaker := &fakeSpeaker{spoken: make(chan string, 3)}
adapter := &fakeAdapter{events: make(chan atspi.Event, 1)}
inputListener := &fakeInputListener{
events: make(chan input.Event, 1),
modifierKeycodes: input.NewModifierKeycodes(37),
}
ctx, cancel := context.WithCancel(context.Background())
completed := make(chan error, 1)
go func() {
completed <- Run(ctx, Options{
Logger: slog.New(slog.DiscardHandler),
newSpeaker: func() Speaker { return speaker },
newCommands: func() commandListener { return silentCommands{} },
newSound: func() soundService { return silentSound{} },
newAdapter: func() Adapter { return adapter },
newInput: func() InputListener { return inputListener },
})
}()
adapter.events <- atspi.NewEvent("focus:", atspi.NewSnapshot("/accessible/save", "Save", "push button", "", ""))
for index, expected := range []string{"", "Save", "push button"} {
select {
case text := <-speaker.spoken:
if index == 0 {
if !contains_string(startupMessages, text) {
t.Fatalf("startup speech = %q, want one of %q", text, startupMessages)
}
continue
}
if text != expected {
t.Fatalf("spoken text = %q, want %q", text, expected)
}
case <-time.After(time.Second):
t.Fatalf("timed out waiting for %q", expected)
}
}
cancel()
select {
case err := <-completed:
if err != nil {
t.Fatalf("Run: %v", err)
}
case <-time.After(time.Second):
t.Fatal("timed out waiting for Run to stop")
}
select {
case text := <-speaker.spoken:
if !contains_string(shutdownMessages, text) {
t.Fatalf("shutdown speech = %q, want one of %q", text, shutdownMessages)
}
default:
t.Fatal("shutdown message was not spoken")
}
if !adapter.stopped || !speaker.closed || !speaker.canceledOnClose {
t.Fatal("runtime did not stop both services")
}
if !inputListener.started || !inputListener.stopped {
t.Fatal("runtime did not run the X11 listener lifecycle")
}
}
func (silentSound) Start(context.Context) error { return nil }
func (silentSound) Stop(context.Context) error { return nil }
func (silentSound) Enqueue(sound.Cue) bool { return true }
func (silentSound) EnqueueBatch([]sound.Cue) bool { return true }
func (silentSound) Available(string) bool { return true }
func (rejectingSound) Enqueue(sound.Cue) bool { return false }
func (rejectingSound) EnqueueBatch([]sound.Cue) bool { return false }
func (silentCommands) Commands() <-chan input.CommandEvent { return nil }
func (silentCommands) LockStates() <-chan input.LockState { return nil }
func (silentCommands) Failures() <-chan error { return nil }
func (silentCommands) Start(context.Context) error { return nil }
func (silentCommands) Stop(context.Context) error { return nil }