Files
fenrir/internal/app/spellcheck_loop_test.go

99 lines
3.5 KiB
Go

// Copyright (c) 2026 Storm Dragon
// SPDX-License-Identifier: GPL-3.0-or-later
package app
import (
"context"
"git.stormux.org/storm/fenrir/internal/command"
"git.stormux.org/storm/fenrir/internal/config"
"git.stormux.org/storm/fenrir/internal/input"
"git.stormux.org/storm/fenrir/internal/screen"
"git.stormux.org/storm/fenrir/internal/spellcheck"
"path/filepath"
"strings"
"testing"
"time"
)
type LoopDictionary struct {
entered chan error
release <-chan struct{}
}
func (d *LoopDictionary) Check(string) (bool, error) {
d.entered <- nil
<-d.release
return false, nil
}
func (*LoopDictionary) Suggest(string) ([]string, error) { return nil, nil }
func (*LoopDictionary) Close() {}
func TestAutomaticSpellcheckDropsResultsAfterInputMuteSilenceAndScreenChanges(t *testing.T) {
for _, state := range []string{"enabled", "disabled", "muted", "temporary", "new input", "screen generation", "screen size"} {
t.Run(state, func(t *testing.T) {
settings := config.Defaults()
settings.Reading.Spellcheck = state != "disabled"
settings.Keyboard.CharacterEcho = false
settings.Reading.Automatic = false // Spellcheck is an independent choice.
settings.Presentation.Sound.Enabled = false
keys := &fakeKeys{events: make(chan error, 2)}
gate := make(chan struct{})
service := spellcheck.New_service(context.Background(), spellcheck.Options{ExceptionsPath: filepath.Join(t.TempDir(), "exceptions.json"), OpenDictionary: func(string) (spellcheck.Dictionary, error) { return &LoopDictionary{keys.events, gate}, nil }})
defer service.Close()
voice := &fakeVoice{}
source := &fakeSource{snapshot: screen.Snapshot{Terminal: 2, Active: true, Rows: 1, Cols: 32, X: 6, Cells: []rune("$ helo" + strings.Repeat(" ", 26))}, events: make(chan error, 2)}
reads, steps := 0, 0
source.afterRead = func() {
reads++
if reads == 3 && (state == "screen generation" || state == "screen size") {
close(gate)
}
}
keys.poll = func(_ bool, handle func(input.Decision)) error {
steps++
if steps == 1 {
voice.messages = nil
handle(input.Decision{Forward: true, Typing: true, WordBoundary: true, Interrupt: true, Keypress: true})
source.snapshot.Cells = []rune("$ helo " + strings.Repeat(" ", 25))
source.snapshot.X = 7
source.events <- nil
} else if steps == 2 {
switch state {
case "muted":
handle(input.Decision{Command: command.Mute, Interrupt: true})
case "temporary":
handle(input.Decision{Command: command.TempDisableSpeech, Interrupt: true})
case "new input":
handle(input.Decision{Forward: true, Typing: true, Interrupt: true, Keypress: true})
case "screen generation":
source.snapshot.Generation++
source.events <- nil
return nil
case "screen size":
source.snapshot.Cols++
source.snapshot.Cells = append(source.snapshot.Cells, ' ')
source.events <- nil
return nil
}
close(gate)
}
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), 150*time.Millisecond)
err := run(ctx, source, keys, voice, nil, RunOptions{Settings: settings, Spellcheck: service})
cancel()
if err != nil {
t.Fatal(err)
}
if state != "disabled" && steps < 2 {
t.Fatal("fixture did not reach the delayed dictionary result", reads, steps)
}
got := strings.Contains(strings.Join(voice.messages, "|"), "misspelled")
if got != (state == "enabled") {
t.Fatal(state, voice.messages, reads, steps)
}
})
}
}