// 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 . 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) } } }