169 lines
5.7 KiB
Go
169 lines
5.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/presentation"
|
|
)
|
|
|
|
func TestProcessInputEventsInterruptsSpeechOnKeyPress(t *testing.T) {
|
|
speech := &fakeSpeechQueue{
|
|
spoken: make(chan string, 1),
|
|
interrupt: make(chan struct{}, 3),
|
|
}
|
|
listener := &fakeInputListener{
|
|
events: make(chan input.Event, 4),
|
|
modifierKeycodes: input.NewModifierKeycodes(37),
|
|
}
|
|
listener.events <- input.Event{Keycode: 37, Pressed: false}
|
|
listener.events <- input.NewKeyEvent(111, true, "Up")
|
|
listener.events <- input.NewKeyEvent(111, false, "Up")
|
|
listener.events <- input.Event{Keycode: 37, Pressed: true}
|
|
close(listener.events)
|
|
processInputEvents(context.Background(), slog.New(slog.DiscardHandler), speech, listener, input.EchoSettings{}, nil, nil)
|
|
if got := len(speech.interrupt); got != 2 {
|
|
t.Fatalf("interrupts = %d, want 2 for Up and Control presses", got)
|
|
}
|
|
}
|
|
|
|
func TestProcessInputEventsEchoesConfiguredNonPrintableKeys(t *testing.T) {
|
|
speech := &fakeSpeechQueue{
|
|
spoken: make(chan string, 3),
|
|
interrupt: make(chan struct{}, 4),
|
|
}
|
|
listener := &fakeInputListener{
|
|
events: make(chan input.Event, 5),
|
|
modifierKeycodes: input.NewModifierKeycodes(37),
|
|
}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
completed := make(chan struct{})
|
|
go func() {
|
|
processInputEvents(ctx, slog.New(slog.DiscardHandler), speech, listener, input.EchoSettings{
|
|
EnableKeyEcho: true, EnableModifierKeys: true, EnableFunctionKeys: true,
|
|
CthulhuModifierKeys: []string{"Insert", "KP_Insert"},
|
|
}, nil, nil)
|
|
close(completed)
|
|
}()
|
|
|
|
listener.events <- input.NewKeyEvent(37, true, "Control_L")
|
|
listener.events <- input.NewKeyEvent(118, true, "Insert")
|
|
listener.events <- input.NewKeyEvent(67, true, "F1")
|
|
listener.events <- input.NewKeyEvent(118, false, "Insert")
|
|
listener.events <- input.NewKeyEvent(68, true, "F2")
|
|
|
|
select {
|
|
case text := <-speech.spoken:
|
|
if text != "left control" {
|
|
t.Fatalf("modifier echo = %q", text)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatal("timed out waiting for modifier echo")
|
|
}
|
|
select {
|
|
case text := <-speech.spoken:
|
|
if text != "f2" {
|
|
t.Fatalf("Cthulhu chord key echo leaked as %q before f2", text)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatal("timed out waiting for key echo after Cthulhu modifier release")
|
|
}
|
|
cancel()
|
|
select {
|
|
case <-completed:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("timed out waiting for input event loop to stop")
|
|
}
|
|
}
|
|
|
|
func TestProcessEventsAppliesPrintableKeyEchoSettings(t *testing.T) {
|
|
speech := &fakeSpeechQueue{spoken: make(chan string, 1)}
|
|
adapter := &fakeAdapter{events: make(chan atspi.Event, 2)}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
completed := make(chan struct{})
|
|
options := presentation.DefaultOptions()
|
|
options.EnableAlphabeticKeys = false
|
|
go func() {
|
|
processEvents(ctx, slog.New(slog.DiscardHandler), speech, adapter, 0, options)
|
|
close(completed)
|
|
}()
|
|
|
|
snapshot := atspi.NewSnapshot("/accessible/editor", "", "text", "", "")
|
|
snapshot.Editable = true
|
|
adapter.events <- atspi.NewEventWithPayload(atspi.TextInsertedEvent, snapshot, "a")
|
|
adapter.events <- atspi.NewEventWithPayload(atspi.TextInsertedEvent, snapshot, "pasted text")
|
|
select {
|
|
case text := <-speech.spoken:
|
|
if text != "pasted text" {
|
|
t.Fatalf("disabled alphabetic key echo spoke %q before multi-character insertion", text)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatal("timed out waiting for multi-character insertion")
|
|
}
|
|
cancel()
|
|
select {
|
|
case <-completed:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("timed out waiting for event loop to stop")
|
|
}
|
|
}
|
|
|
|
func TestProcessEventsRetainsPriorLineForSentenceEchoOnNewline(t *testing.T) {
|
|
speech := &fakeSpeechQueue{spoken: make(chan string, 2)}
|
|
adapter := &fakeAdapter{events: make(chan atspi.Event, 2)}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
completed := make(chan struct{})
|
|
options := presentation.DefaultOptions()
|
|
options.EnableEchoBySentence = true
|
|
go func() {
|
|
processEvents(ctx, slog.New(slog.DiscardHandler), speech, adapter, 0, options)
|
|
close(completed)
|
|
}()
|
|
|
|
snapshot := atspi.NewSnapshot("/accessible/editor", "", "text", "", "Line without punctuation")
|
|
snapshot.Editable = true
|
|
snapshot.TextBeforeCaret = "Line without punctuation"
|
|
adapter.events <- atspi.NewEventWithPayload(atspi.TextInsertedEvent, snapshot, "n")
|
|
blankLine := atspi.NewSnapshot("/accessible/editor", "", "text", "", "")
|
|
blankLine.Editable = true
|
|
adapter.events <- atspi.NewEventWithPayload(atspi.TextInsertedEvent, blankLine, "\n")
|
|
|
|
for _, want := range []string{"n", "Line without punctuation"} {
|
|
select {
|
|
case text := <-speech.spoken:
|
|
if text != want {
|
|
t.Fatalf("typing echo = %q, want %q", text, want)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatalf("timed out waiting for typing echo %q", want)
|
|
}
|
|
}
|
|
cancel()
|
|
select {
|
|
case <-completed:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("timed out waiting for event loop to stop")
|
|
}
|
|
}
|