Files
fenrir/internal/app/indentation_test.go

213 lines
7.3 KiB
Go

// Copyright (c) 2026 Storm Dragon
// SPDX-License-Identifier: GPL-3.0-or-later
package app
import (
"context"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"time"
"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/interaction"
"git.stormux.org/storm/fenrir/internal/screen"
)
type unavailableIndentationSounds struct{ fakeSounds }
func (f *unavailableIndentationSounds) Play_indentation(int) bool { return false }
func TestReviewIndentationSpeechBeepCombinedFallbackAndSilence(t *testing.T) {
for _, mode := range []string{"none", "speech", "beeps", "both"} {
for _, state := range []string{"enabled", "disabled", "zero volume", "missing", "muted", "temporary silence"} {
t.Run(mode+"/"+state, func(t *testing.T) {
settings := config.Defaults()
settings.Reading.Indentation = mode
settings.Presentation.Sound.Enabled = state != "disabled"
if state == "zero volume" {
settings.Presentation.Sound.Volume = 0
}
sounds, voice := &fakeSounds{}, &fakeVoice{}
var player SoundPlayer = sounds
if state == "missing" {
player = &unavailableIndentationSounds{}
}
source := &fakeSource{snapshot: screen.Snapshot{Terminal: 2, Active: true, Rows: 2, Cols: 8, Cells: []rune("root leaf")}}
keys := &fakeKeys{poll: func(_ bool, handle func(input.Decision)) error {
if state == "muted" {
handle(input.Decision{Command: command.Mute})
}
if state == "temporary silence" {
handle(input.Decision{Command: command.TempDisableSpeech})
}
voice.messages, sounds.indentation = nil, nil
handle(input.Decision{Command: command.NextLine, Interrupt: true})
handle(input.Decision{Command: command.CurrentLine, Interrupt: true})
handle(input.Decision{Command: command.Quit})
return nil
}}
if err := run(context.Background(), source, keys, voice, nil, RunOptions{Settings: settings, Sounds: player}); err != nil {
t.Fatal(err)
}
wantTone := state == "enabled" && (mode == "beeps" || mode == "both")
if wantTone != reflect.DeepEqual(sounds.indentation, []int{4}) {
t.Fatal("tone", sounds.indentation)
}
wantSpeech := mode != "none" && (mode != "beeps" || state != "enabled") && state != "muted" && state != "temporary silence"
if strings.Contains(strings.Join(voice.messages, "|"), "indented 4 ") != wantSpeech {
t.Fatal("speech", voice.messages)
}
})
}
}
}
func TestQuickIndentationChoicesMenuCancelSaveAndRestart(t *testing.T) {
path := filepath.Join(t.TempDir(), "settings.toml")
base := config.Defaults()
base.General.ScriptKeys = []string{"KEY_COMPOSE", "KEY_META"}
if err := config.Save(path, base); err != nil {
t.Fatal(err)
}
voice := &fakeVoice{}
quick := new_quick_settings("speech-dispatcher")
quick.command(command.NextQuickCategory, &base, voice)
quick.command(command.NextQuickEntry, &base, voice)
for _, trial := range []struct {
id command.ID
want string
}{
{command.CurrentQuickEntry, "Indentation none"},
{command.NextQuickValue, "speech"}, {command.NextQuickValue, "beeps"},
{command.NextQuickValue, "speech and beeps"}, {command.NextQuickValue, "none"},
{command.PreviousQuickValue, "speech and beeps"},
} {
quick.command(trial.id, &base, voice)
if voice.messages[len(voice.messages)-1] != trial.want {
t.Fatal(voice.messages)
}
}
if len(voice.parameters) != 0 || base.Reading.Indentation != "both" {
t.Fatal(base, voice.parameters)
}
ui := new_internal_ui(context.Background(), &fakeKeys{}, voice)
ui.open_settings(base, path, base)
receive_ui(t, ui)
control := ui.session.Values()["reading.indentation"]
if control.Choices[control.Selected].ID != "both" {
t.Fatal("menu lost quick choice", control)
}
ui.handle(interaction.Key{Name: "escape"})
saved, _, err := config.Load(path, nil)
if err != nil || saved.Reading.Indentation != "none" {
t.Fatal("cancel saved choice", saved.Reading, err)
}
for session := range 2 {
voice = &fakeVoice{}
keys := &fakeKeys{poll: func(_ bool, handle func(input.Decision)) error {
handle(input.Decision{Command: command.NextQuickCategory})
handle(input.Decision{Command: command.NextQuickEntry})
if session == 0 {
for range 3 {
handle(input.Decision{Command: command.NextQuickValue})
}
handle(input.Decision{Command: command.SaveSettings})
} else if voice.messages[len(voice.messages)-1] != "Indentation speech and beeps" {
t.Fatal(voice.messages)
}
handle(input.Decision{Command: command.Quit})
return nil
}}
source := &fakeSource{snapshot: screen.Snapshot{Terminal: 2, Active: true, Rows: 1, Cols: 4, Cells: []rune("root")}}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
err = run(ctx, source, keys, voice, nil, RunOptions{Settings: saved, ConfigPath: path})
cancel()
if err != nil {
t.Fatal(err)
}
saved, _, err = config.Load(path, nil)
if err != nil || saved.Reading.Indentation != "both" || !reflect.DeepEqual(saved.General, base.General) {
t.Fatal(saved, err)
}
}
}
func TestMenuIndentationAppliesOnlyAfterSuccessfulSave(t *testing.T) {
for _, action := range []string{"save", "cancel", "failed save"} {
t.Run(action, func(t *testing.T) {
path := filepath.Join(t.TempDir(), "settings.toml")
if err := os.WriteFile(path, []byte("version=3\n[reading]\nindentation='none'\n"), 0600); err != nil {
t.Fatal(err)
}
source := &fakeSource{snapshot: screen.Snapshot{Terminal: 2, Active: true, Rows: 2, Cols: 8, Cells: []rune("root leaf")}}
voice, keys := &fakeVoice{}, &fakeKeys{events: make(chan error, 4)}
keys.onCapture = func(enabled bool) {
if !enabled {
keys.events <- nil
}
}
voice.onSay = func(text string) {
if strings.HasPrefix(text, "Fenrir Menu.") || strings.HasPrefix(text, "Could not save settings:") {
keys.events <- nil
}
}
step := 0
keys.poll = func(_ bool, handle func(input.Decision)) error {
step++
key := func(name string) { handle(input.Decision{UI: &interaction.Key{Name: name}}) }
switch step {
case 1:
handle(input.Decision{Command: command.FenrirMenu})
case 2:
for range 9 {
key("tab")
}
if voice.messages[len(voice.messages)-1] != "Indentation combo box none" {
t.Fatal(voice.messages)
}
key("down")
if action == "cancel" {
key("escape")
break
}
if action == "failed save" {
if err := os.WriteFile(path, []byte("version=3\n[reading]\nindentation='beeps'\n"), 0600); err != nil {
t.Fatal(err)
}
}
for range 32 {
key("tab")
if voice.messages[len(voice.messages)-1] == "OK button" {
break
}
}
key("enter")
case 3:
if action == "failed save" {
key("escape")
}
voice.messages = nil
handle(input.Decision{Command: command.NextLine})
if strings.Contains(strings.Join(voice.messages, "|"), "indented 4 ") != (action == "save") {
t.Fatal(action, voice.messages)
}
handle(input.Decision{Command: command.Quit})
}
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
if err := run(ctx, source, keys, voice, nil, RunOptions{Settings: config.Defaults(), ConfigPath: path}); err != nil || step != 3 {
t.Fatal(err, step)
}
})
}
}