186 lines
6.9 KiB
Go
186 lines
6.9 KiB
Go
// Cthulhu screen reader.
|
|
// Copyright (c) 2026 Storm Dragon
|
|
// Authors: Storm Dragon, Angus Kola
|
|
//
|
|
// 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 input
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestReviewMultiPressSequences(t *testing.T) {
|
|
for _, layout := range []KeyboardLayout{DesktopKeyboardLayout, LaptopKeyboardLayout} {
|
|
for _, modifierFirst := range []bool{false, true} {
|
|
router := NewCommandRouter([]uint32{0xff63}, DefaultCommandBindings(layout))
|
|
now := time.Unix(100, 0)
|
|
router.now = func() time.Time { return now }
|
|
wordKey, charKey := uint32(0xff9d), uint32(0xff99)
|
|
if layout == LaptopKeyboardLayout {
|
|
wordKey, charKey = 'k', ','
|
|
}
|
|
press := func(symbol uint32, want Command) {
|
|
t.Helper()
|
|
if layout == LaptopKeyboardLayout {
|
|
router.Handle(0xff63, 118, 0, true)
|
|
}
|
|
if consumed, got := router.Handle(symbol, 30, 0, true); !consumed || got != want {
|
|
t.Fatalf("layout=%d press=%x got=%s want=%s", layout, symbol, got, want)
|
|
}
|
|
if consumed, got := router.Handle(symbol, 30, 0, true); !consumed || got != "" {
|
|
t.Fatal("held-key repeat dispatched")
|
|
}
|
|
if layout == LaptopKeyboardLayout && modifierFirst {
|
|
router.Handle(0xff63, 118, 0, false)
|
|
}
|
|
if consumed, got := router.Handle(symbol, 30, 0, false); !consumed || got != "" {
|
|
t.Fatal("unbalanced release")
|
|
}
|
|
if layout == LaptopKeyboardLayout && !modifierFirst {
|
|
router.Handle(0xff63, 118, 0, false)
|
|
}
|
|
router.Reset()
|
|
router.ReplaceBindings(BrowseCommandBindings(layout))
|
|
}
|
|
press(charKey, ReviewCurrentCharacter)
|
|
now = now.Add(500 * time.Millisecond)
|
|
press(charKey, ReviewPhoneticCharacter)
|
|
press(charKey, ReviewCurrentCharacter)
|
|
press(wordKey, ReviewCurrentWord) // Different chord resets the sequence.
|
|
press(wordKey, ReviewSpellWord)
|
|
press(wordKey, ReviewPhoneticWord)
|
|
press(wordKey, ReviewCurrentWord)
|
|
now = now.Add(501 * time.Millisecond)
|
|
press(wordKey, ReviewCurrentWord)
|
|
router.Handle(0xffbe, 67, 0, true) // Unbound intervening key.
|
|
router.Handle(0xffbe, 67, 0, false)
|
|
press(wordKey, ReviewCurrentWord)
|
|
now = now.Add(-time.Millisecond)
|
|
press(wordKey, ReviewCurrentWord) // Clock reversal must not count.
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFlatReviewLayoutBindingsAndBalancedRelease(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
desktop, laptop uint32
|
|
command Command
|
|
}{
|
|
{0xff95, 'u', ReviewPreviousLine}, {0xff97, 'i', ReviewCurrentLine}, {0xff9a, 'o', ReviewNextLine},
|
|
{0xff96, 'j', ReviewPreviousWord}, {0xff9d, 'k', ReviewCurrentWord}, {0xff98, 'l', ReviewNextWord},
|
|
{0xff9c, 'm', ReviewPreviousCharacter}, {0xff99, ',', ReviewCurrentCharacter}, {0xff9b, '.', ReviewNextCharacter},
|
|
} {
|
|
for _, layout := range []KeyboardLayout{DesktopKeyboardLayout, LaptopKeyboardLayout} {
|
|
for _, modifierFirst := range []bool{false, true} {
|
|
router := NewCommandRouter([]uint32{0xff63}, DefaultCommandBindings(layout))
|
|
symbol := tc.desktop
|
|
if layout == LaptopKeyboardLayout {
|
|
symbol = tc.laptop
|
|
if consumed, _ := router.Handle(symbol, 30, 0, true); consumed {
|
|
t.Fatal("unmodified laptop letter consumed")
|
|
}
|
|
router.Handle(symbol, 30, 0, false)
|
|
router.Handle(0xff63, 118, 0, true)
|
|
}
|
|
if consumed, cmd := router.Handle(symbol, 30, 0, true); !consumed || cmd != tc.command {
|
|
t.Fatalf("press: %v %q", consumed, cmd)
|
|
}
|
|
if consumed, cmd := router.Handle(symbol, 30, 0, true); !consumed || cmd != "" {
|
|
t.Fatalf("repeat: %v %q", consumed, cmd)
|
|
}
|
|
if layout == LaptopKeyboardLayout && modifierFirst {
|
|
router.Handle(0xff63, 118, 0, false)
|
|
}
|
|
consumed, cmd := router.Handle(symbol, 30, 0, false)
|
|
if !consumed {
|
|
t.Fatal("review release leaked")
|
|
}
|
|
if layout == LaptopKeyboardLayout && !modifierFirst {
|
|
if cmd != "" {
|
|
t.Fatal("command emitted while Insert held")
|
|
}
|
|
_, cmd = router.Handle(0xff63, 118, 0, false)
|
|
}
|
|
if cmd != "" {
|
|
t.Fatalf("release dispatched %q", cmd)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFlatReviewWindowBindingsAndBalancedRelease(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
symbol uint32
|
|
layout KeyboardLayout
|
|
modifiers uint16
|
|
command Command
|
|
}{
|
|
{0xff95, DesktopKeyboardLayout, 0, ReviewWindowStart},
|
|
{0xff9a, DesktopKeyboardLayout, 0, ReviewWindowEnd},
|
|
{'u', LaptopKeyboardLayout, controlModifier, ReviewWindowStart},
|
|
{'o', LaptopKeyboardLayout, controlModifier, ReviewWindowEnd},
|
|
} {
|
|
for _, releaseModifierFirst := range []bool{false, true} {
|
|
router := NewCommandRouter([]uint32{0xff63}, DefaultCommandBindings(tc.layout))
|
|
router.Handle(0xff63, 118, 0, true)
|
|
if consumed, command := router.Handle(tc.symbol, 30, tc.modifiers, true); !consumed || command != tc.command {
|
|
t.Fatalf("press: consumed=%v command=%q", consumed, command)
|
|
}
|
|
releases := []struct {
|
|
symbol uint32
|
|
code uint16
|
|
}{{tc.symbol, 30}, {0xff63, 118}}
|
|
if releaseModifierFirst {
|
|
releases[0], releases[1] = releases[1], releases[0]
|
|
}
|
|
for _, key := range releases {
|
|
if consumed, command := router.Handle(key.symbol, key.code, 0, false); !consumed || command != "" {
|
|
t.Fatalf("release: consumed=%v command=%q", consumed, command)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFlatReviewVerticalAndLineEndBindings(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
symbol uint32
|
|
layout KeyboardLayout
|
|
modifiers uint16
|
|
command Command
|
|
}{
|
|
{0xff96, DesktopKeyboardLayout, 0, ReviewWordAbove},
|
|
{0xff98, DesktopKeyboardLayout, 0, ReviewWordBelow},
|
|
{0xff9c, DesktopKeyboardLayout, 0, ReviewLineEnd},
|
|
{'j', LaptopKeyboardLayout, controlModifier, ReviewWordAbove},
|
|
{'l', LaptopKeyboardLayout, controlModifier, ReviewWordBelow},
|
|
{'m', LaptopKeyboardLayout, controlModifier, ReviewLineEnd},
|
|
} {
|
|
router := NewCommandRouter([]uint32{0xff63}, DefaultCommandBindings(tc.layout))
|
|
router.Handle(0xff63, 118, 0, true)
|
|
if consumed, command := router.Handle(tc.symbol, 30, tc.modifiers, true); !consumed || command != tc.command {
|
|
t.Fatalf("press: consumed=%v command=%q want=%q", consumed, command, tc.command)
|
|
}
|
|
if consumed, command := router.Handle(tc.symbol, 30, 0, false); !consumed || command != "" {
|
|
t.Fatalf("release: consumed=%v command=%q", consumed, command)
|
|
}
|
|
if consumed, command := router.Handle(0xff63, 118, 0, false); !consumed || command != "" {
|
|
t.Fatalf("modifier release: consumed=%v command=%q", consumed, command)
|
|
}
|
|
}
|
|
}
|