160 lines
5.5 KiB
Go
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)
|
|
}
|
|
}
|