Files
cthulhu/internal/app/command_input_test.go

110 lines
3.6 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"
"testing"
"time"
"git.stormux.org/storm/cthulhu/internal/atspi"
"git.stormux.org/storm/cthulhu/internal/config"
"git.stormux.org/storm/cthulhu/internal/input"
)
func TestRunPresentsLockStatesWithKeyEchoDisabled(t *testing.T) {
settings := config.DefaultSettings()
settings.EnableSound = false
settings.EnableBraille = false
settings.EnableKeyEcho = false
speaker := &fakeSpeaker{spoken: make(chan string, 4)}
commands := &fakeCommands{commands: make(chan input.CommandEvent), lockStates: make(chan input.LockState)}
ctx, cancel := context.WithCancel(context.Background())
completed := make(chan error, 1)
go func() {
completed <- Run(ctx, Options{
Logger: slog.New(slog.DiscardHandler), Settings: &settings,
newCommands: func() commandListener { return commands },
newSpeaker: func() Speaker { return speaker },
newAdapter: func() Adapter { return &fakeAdapter{events: make(chan atspi.Event)} },
newInput: func() InputListener { return &fakeInputListener{events: make(chan input.Event)} },
})
}()
defer func() {
cancel()
select {
case err := <-completed:
if err != nil {
t.Error(err)
}
case <-time.After(time.Second):
t.Error("reader did not stop")
}
}()
select {
case <-speaker.spoken:
case <-time.After(time.Second):
t.Fatal("missing startup speech")
}
for _, name := range []string{"Caps Lock", "Num Lock", "Scroll Lock"} {
for _, locked := range []bool{true, false} {
state := input.LockState{Name: name, Locked: locked, Timestamp: 100}
select {
case commands.lockStates <- state:
case <-time.After(time.Second):
t.Fatal("lock event was not received")
}
select {
case text := <-speaker.spoken:
if text != state.Text() {
t.Fatalf("speech = %q, want %q", text, state.Text())
}
case <-time.After(time.Second):
t.Fatal("lock state did not reach speech")
}
}
}
}
func TestCommandSpeechSurvivesDelayedInitiatingInput(t *testing.T) {
for _, cause := range []string{"current-time", "current-date", "version"} {
t.Run(cause, func(t *testing.T) {
worker := newSpeechWorker(slog.New(slog.DiscardHandler), nil)
worker.running = true
worker.ProtectCommand(100)
worker.Enqueue(speechRequest{Text: "Command result", Role: "system", Cause: cause})
for _, timestamp := range []uint32{80, 99, 100} {
if worker.InterruptForInput(input.Event{Pressed: true, Timestamp: timestamp}) {
t.Fatalf("command interrupted by initiating input at %d", timestamp)
}
}
if len(worker.pending) != 1 || worker.generation != 0 {
t.Fatal("command speech was discarded")
}
if !worker.InterruptForInput(input.Event{Pressed: true, Timestamp: 101}) || len(worker.pending) != 0 {
t.Fatal("later input did not interrupt command speech")
}
})
}
}
func TestCommandInputProtectionOrdersTimestampsAcrossWrap(t *testing.T) {
worker := newSpeechWorker(slog.New(slog.DiscardHandler), nil)
worker.ProtectCommand(^uint32(0) - 1)
worker.ProtectCommand(3)
worker.ProtectCommand(^uint32(0)) // An older queued notification cannot move the cutoff back.
for _, timestamp := range []uint32{^uint32(0), 2, 3} {
if worker.InterruptForInput(input.Event{Pressed: true, Timestamp: timestamp}) {
t.Fatalf("old input at %d interrupted", timestamp)
}
}
for _, timestamp := range []uint32{4, 0} {
if !worker.InterruptForInput(input.Event{Pressed: true, Timestamp: timestamp}) {
t.Fatalf("new or untimed input at %d ignored", timestamp)
}
}
}