Files

160 lines
5.5 KiB
Go

// Fenrir console screen reader.
// Copyright (c) 2026 Storm Dragon
// SPDX-License-Identifier: GPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along with
// this program. If not, see <https://www.gnu.org/licenses/gpl-3.0.html>.
package reader
import (
"reflect"
"testing"
"time"
"git.stormux.org/storm/fenrir/internal/command"
"git.stormux.org/storm/fenrir/internal/presentation"
)
func TestTypingEchoBooleanCombinations(t *testing.T) {
for _, character := range []bool{false, true} {
for _, word := range []bool{false, true} {
for _, capsOnly := range []bool{false, true} {
for _, capsLock := range []bool{false, true} {
r := New()
r.Configure_echo(character, word, capsOnly)
before := fixture("$ ")
before.X, before.CapsLock = 2, capsLock
r.Update(before)
r.Note_input(true, true, false)
after := fixture("$ a")
after.X, after.CapsLock = 3, capsLock
var want []presentation.Message
if character || capsOnly && capsLock {
want = []presentation.Message{presentation.Character_message('a')}
}
if got := r.Update(after); !reflect.DeepEqual(got, want) {
t.Fatal("character policy", character, word, capsOnly, capsLock, got, want)
}
r.Note_input(true, true, true)
after.X = 4 // Space advances over an already-blank cell.
want = nil
if word {
want = []presentation.Message{{Text: "a"}}
}
if got := r.Update(after); !reflect.DeepEqual(got, want) {
t.Fatal("word policy", character, word, capsOnly, capsLock, got, want)
}
}
}
}
}
}
func TestEchoSeparatesTypingNavigationDeletionAndIncomingOutput(t *testing.T) {
r := New()
r.Configure_echo(false, false, false)
before := fixture("$ a", "output")
before.X = 3
r.Update(before)
r.Note_input(true, false, false)
after := fixture("$ ", "output")
after.X = 2
if got := r.Update(after); !reflect.DeepEqual(got, []presentation.Message{presentation.Character_message('a')}) {
t.Fatal("typing settings disabled deletion feedback", got)
}
after.X = 1
if got := r.Update(after); !reflect.DeepEqual(got, []presentation.Message{presentation.Character_message(' ')}) {
t.Fatal("typing settings disabled cursor navigation", got)
}
r.Configure(true, false)
after.Y = 1
if got := r.Update(after); len(got) != 0 {
t.Fatal("cursor echo setting ignored", got)
}
changed := fixture("$ ", "output!")
changed.Y, changed.X = 1, 7
if got := r.Update(changed); !reflect.DeepEqual(got, []presentation.Message{presentation.Character_message('!')}) {
t.Fatal("incoming output was suppressed as typing", got)
}
r.Note_input(true, true, false)
if got := r.Update(changed); len(got) != 0 {
t.Fatal("un-echoed input spoke raw keys", got)
}
r.inputAt = time.Now().Add(-time.Second)
changed = fixture("$ ", "output!x")
changed.Y, changed.X = 1, 8
if got := r.Update(changed); !reflect.DeepEqual(got, []presentation.Message{presentation.Character_message('x')}) {
t.Fatal("stale input suppressed later output", got)
}
}
func TestWordEchoBatchUnicodeMidlineAndSpaceNavigation(t *testing.T) {
for _, character := range []bool{false, true} {
r := New()
r.Configure_echo(character, true, false)
before := fixture("$ ")
before.X = 2
r.Update(before)
for _, char := range "café two " {
r.Note_input(true, true, char == ' ')
}
after := fixture("$ café two ")
after.X = 11
want := []presentation.Message{{Text: "café"}, {Text: "two"}}
if character {
want = []presentation.Message{presentation.Character_message('c'), presentation.Character_message('a'), presentation.Character_message('f'), presentation.Character_message('é'), {Text: "café"}, presentation.Character_message('t'), presentation.Character_message('w'), presentation.Character_message('o'), {Text: "two"}}
}
if got := r.Update(after); !reflect.DeepEqual(got, want) {
t.Fatal("batched typing", character, got, want)
}
r.Note_input(true, false, false)
after.X = 12
if got := r.Update(after); !reflect.DeepEqual(got, []presentation.Message{presentation.Character_message(' ')}) {
t.Fatal("navigation triggered word echo", got)
}
}
r := New()
r.Configure_echo(false, true, false)
before := fixture("$ one two")
before.X = 4
r.Update(before)
r.Note_input(true, true, false)
after := fixture("$ onXe two")
after.X = 5
if got := r.Update(after); len(got) != 0 {
t.Fatal("midline insertion escaped disabled character echo", got)
}
r.Note_input(true, true, true)
after = fixture("$ onX e two")
after.X = 6
if got := r.Update(after); !reflect.DeepEqual(got, []presentation.Message{{Text: "onX"}}) {
t.Fatal("midline word completion", got)
}
}
func TestTypingEchoIndependentOfAutomaticReadingAndReview(t *testing.T) {
r := New()
r.Configure(false, false)
before := fixture("$ ")
before.X = 2
r.Update(before)
r.Command(command.CurrentLine)
r.Note_input(true, true, false)
after := fixture("$ a")
after.X = 3
if got := r.Update(after); !reflect.DeepEqual(got, []presentation.Message{presentation.Character_message('a')}) {
t.Fatal("automatic reading or review disabled typing echo", got)
}
}