110 lines
3.6 KiB
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)
|
|
}
|
|
}
|
|
}
|