Files
fenrir/internal/app/punctuation_test.go

194 lines
7.1 KiB
Go

// Fenrir console screen reader.
// Copyright (c) 2026 Storm Dragon
// SPDX-License-Identifier: GPL-3.0-or-later
package app
import (
"context"
"path/filepath"
"reflect"
"strings"
"testing"
"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/punctuation"
"git.stormux.org/storm/fenrir/internal/screen"
)
func TestSharedSpeechPunctuationAndPreparedCharacterFeedback(t *testing.T) {
processor, _, err := punctuation.Load([]string{"../../config/punctuation/default.toml"})
if err != nil {
t.Fatal(err)
}
voice := &fakeVoice{}
echo := &echoSpeaker{Speaker: voice, punctuation: processor, punctuationLevel: "some"}
echo.Say("a/b")
if echo.last != "a/b" {
t.Fatal("clipboard echo lost original text", echo.last)
}
echo.punctuationLevel = "none"
echo.say_character('?')
echo.Say("a/b")
echo.punctuationLevel = "all"
echo.say_character('?')
echo.punctuationLevel = "some"
echo.say_character('?', " first character in cell path/to")
if echo.last != "question? first character in cell path/to" {
t.Fatal("clipboard echo lost raw table header", echo.last)
}
got := strings.Join(voice.messages, "|")
if got != "a slash b|question?|a b|question?|question? first character in cell path slash to" {
t.Fatal(got)
}
}
func TestSpeechKeepsEmbeddedPunctuationAndOriginalClipboardText(t *testing.T) {
processor, _, err := punctuation.Load([]string{"../../config/punctuation/default.toml"})
if err != nil {
t.Fatal(err)
}
for _, substitutions := range []bool{false, true} {
settings := config.Defaults()
settings.Reading.Emoticons = substitutions
settings.Reading.RepeatedSymbols = substitutions
voice := &fakeVoice{}
echo := &echoSpeaker{Speaker: voice, punctuation: processor, punctuationLevel: "some", settings: &settings}
for _, trial := range []struct{ input, want string }{
{"https://stormux.org", "https colon slash slash stormux dot org"},
{"2026.10.31", "2026 dot 10 dot 31"},
{"3.14", "3.14"},
} {
voice.messages = nil
echo.Say(trial.input)
if !reflect.DeepEqual(voice.messages, []string{trial.want}) {
t.Fatal("speech lost punctuation", trial.input, substitutions, voice.messages)
}
if echo.last != trial.input {
t.Fatal("clipboard echo lost original text", echo.last)
}
}
}
}
func TestDiffstatSpeechKeepsOriginalClipboardText(t *testing.T) {
processor, _, err := punctuation.Load([]string{"../../config/punctuation/default.toml"})
if err != nil {
t.Fatal(err)
}
settings := config.Defaults()
settings.Reading.RepeatedSymbols = true
settings.Reading.RepeatedSymbolsMin = 4
voice := &fakeVoice{}
echo := &echoSpeaker{Speaker: voice, punctuation: processor, punctuationLevel: "some", settings: &settings}
text := "docs/user.md | 7 +++++--"
echo.Say(text)
if len(voice.messages) != 1 {
t.Fatal("missing diffstat speech", voice.messages)
}
if got := strings.Join(strings.Fields(voice.messages[0]), " "); got != "docs slash user dot md 7 5 plus dash dash" {
t.Fatal("diffstat did not summarize", got)
}
if echo.last != text {
t.Fatal("clipboard echo lost original diffstat", echo.last)
}
}
func TestPackageProgressBarSpeechKeepsOriginalClipboardText(t *testing.T) {
processor, _, err := punctuation.Load([]string{"../../config/punctuation/default.toml"})
if err != nil {
t.Fatal(err)
}
settings := config.Defaults()
settings.Reading.RepeatedSymbols = true
voice := &fakeVoice{}
echo := &echoSpeaker{Speaker: voice, punctuation: processor, punctuationLevel: "some", settings: &settings}
for _, trial := range []struct{ bar, want string }{
{strings.Repeat("#", 120), "120 number"},
{strings.Repeat("#", 110) + strings.Repeat("-", 10), "110 number 10 dash"},
{strings.Repeat("#", 90) + strings.Repeat("-", 10), "90 number 10 dash"},
} {
voice.messages = nil
text := "(123/189) upgrading nodejs-lts-jod" + strings.Repeat(" ", 157) + "[" + trial.bar + "] 100%"
echo.Say(text)
if len(voice.messages) != 1 {
t.Fatal("missing progress speech", voice.messages)
}
want := "123 slash 189 upgrading nodejs dash lts dash jod " + trial.want + " 100 percent"
if got := strings.Join(strings.Fields(voice.messages[0]), " "); got != want {
t.Fatal("progress bar did not summarize", got)
}
if echo.last != text {
t.Fatal("clipboard echo lost original progress bar", echo.last)
}
}
}
func TestReviewCharactersUseProfileRegardlessOfLevelAndKeepBoundaries(t *testing.T) {
processor, _, err := punctuation.Load([]string{"../../config/punctuation/fr.toml"})
if err != nil {
t.Fatal(err)
}
for _, level := range punctuation.Levels() {
source := &fakeSource{snapshot: screen.Snapshot{Terminal: 2, Active: true, Rows: 1, Cols: 2, Cells: []rune("?/")}}
voice := &fakeVoice{}
keys := &fakeKeys{poll: func(_ bool, handle func(input.Decision)) error {
voice.messages = nil // Inspect command feedback after startup.
for _, id := range []command.ID{command.CurrentChar, command.NextChar, command.NextChar, command.Quit} {
handle(input.Decision{Command: id, Interrupt: true})
}
return nil
}}
settings := config.Defaults()
settings.Reading.Automatic = false
settings.Reading.PunctuationLevel = level
settings.Presentation.Sound.Enabled = false
if err := run(context.Background(), source, keys, voice, nil, RunOptions{Settings: settings, Punctuation: processor}); err != nil {
t.Fatal(err)
}
want := "point d'interrogation|barre oblique|end of screen"
if level == "all" {
want = "point d'interrogation|barre oblique|end espace of espace screen"
}
if got := strings.Join(voice.messages, "|"); got != want {
t.Fatal(level, got)
}
}
}
func TestQuickPunctuationAppliesLiveAndSavesWhileMuted(t *testing.T) {
processor, _, err := punctuation.Load([]string{"../../config/punctuation/default.toml"})
if err != nil {
t.Fatal(err)
}
path := filepath.Join(t.TempDir(), "settings.toml")
settings := config.Defaults()
settings.Reading.Automatic = false
voice := &fakeVoice{}
source := &fakeSource{snapshot: screen.Snapshot{Terminal: 2, Active: true, Rows: 1, Cols: 3, Cells: []rune("a/b")}}
keys := &fakeKeys{poll: func(_ bool, handle func(input.Decision)) error {
handle(input.Decision{Command: command.NextQuickCategory})
handle(input.Decision{Command: command.PreviousQuickValue})
voice.messages = nil
handle(input.Decision{Command: command.CurrentLine})
if !reflect.DeepEqual(voice.messages, []string{"a b"}) {
t.Fatal("punctuation did not apply live", voice.messages)
}
handle(input.Decision{Command: command.Mute})
handle(input.Decision{Command: command.NextQuickValue})
handle(input.Decision{Command: command.NextQuickValue})
handle(input.Decision{Command: command.SaveSettings})
handle(input.Decision{Command: command.Quit})
return nil
}}
if err := run(context.Background(), source, keys, voice, nil, RunOptions{Settings: settings, Punctuation: processor, ConfigPath: path}); err != nil {
t.Fatal(err)
}
saved, _, err := config.Load(path, nil)
if err != nil || saved.Reading.PunctuationLevel != "most" || saved.Reading.Automatic || !saved.Presentation.Speech.Enabled {
t.Fatal(saved, err)
}
}