package input import "testing" func TestQuitConsumesBalancedChord(t *testing.T) { for _, modifierFirst := range []bool{false, true} { router := NewCommandRouter([]uint32{0xff63}, DefaultCommandBindings(DesktopKeyboardLayout)) check := func(symbol uint32, code uint16, pressed, consume bool, command Command) { t.Helper() gotConsume, gotCommand := router.Handle(symbol, code, 0, pressed) if gotConsume != consume || gotCommand != command { t.Fatalf("got %v %q, want %v %q", gotConsume, gotCommand, consume, command) } } check('q', 24, true, false, "") check('q', 24, false, false, "") check(0xff63, 118, true, true, "") check('q', 24, true, true, Quit) check('q', 24, true, true, "") // auto-repeat if modifierFirst { check(0xff63, 118, false, true, "") check('q', 24, false, true, "") } else { check('q', 24, false, true, "") check(0xff63, 118, false, true, "") } check('q', 24, true, false, "") } } func TestUnknownChordPassesThrough(t *testing.T) { router := NewCommandRouter([]uint32{0xff63}, DefaultCommandBindings(DesktopKeyboardLayout)) router.Handle(0xff63, 118, 0, true) if consumed, command := router.Handle('x', 53, 0, true); consumed || command != "" { t.Fatal("unknown command consumed") } router.Reset() if consumed, command := router.Handle('x', 53, 0, false); consumed || command != "" { t.Fatal("unknown command release consumed after pass-through reset") } router.Handle(0xff63, 118, 0, true) if consumed, command := router.Handle('q', 24, 4, true); consumed || command != "" { t.Fatal("Control+Insert+q became quit") } router.Reset() router.Handle(0xff63, 118, 0, true) if consumed, command := router.Handle('q', 24, 2|16, true); !consumed || command != Quit { t.Fatal("lock keys prevented quit chord") } } func TestBindingTableRoutesPreferencesOnPress(t *testing.T) { bindings := DefaultCommandBindings(DesktopKeyboardLayout) router := NewCommandRouter([]uint32{0xff63, 0xff9e}, bindings) bindings[CommandChord{Symbol: ' ', CthulhuModifier: true}] = Quit if consumed, command := router.Handle(0xff9e, 90, 0, true); !consumed || command != "" { t.Fatalf("modifier press = %v %q", consumed, command) } if consumed, command := router.Handle(' ', 65, 0, true); !consumed || command != Preferences { t.Fatalf("space press = %v %q", consumed, command) } if consumed, command := router.Handle(0xff9e, 90, 0, false); !consumed || command != "" { t.Fatalf("modifier release = %v %q", consumed, command) } if consumed, command := router.Handle(' ', 65, 0, false); !consumed || command != "" { t.Fatalf("space release = %v %q, want no command", consumed, command) } } func TestApplicationPreferencesShortcut(t *testing.T) { router := NewCommandRouter([]uint32{0xff63}, DefaultCommandBindings(DesktopKeyboardLayout)) if consumed, command := router.Handle(0xff63, 90, 0, true); !consumed || command != "" { t.Fatalf("modifier press = %v %q", consumed, command) } if consumed, command := router.Handle(' ', 65, controlModifier, true); !consumed || command != ApplicationPreferences { t.Fatalf("Cthulhu+Ctrl+Space = %v %q", consumed, command) } if consumed, command := router.Handle(' ', 65, controlModifier, false); !consumed || command != "" { t.Fatalf("shortcut release = %v %q", consumed, command) } } func TestApplicationPreferenceShiftShortcutsInBothLayouts(t *testing.T) { for _, layout := range []KeyboardLayout{DesktopKeyboardLayout, LaptopKeyboardLayout} { for _, tc := range []struct { modifiers uint16 want Command }{ {1, ApplicationPreferences}, {controlModifier | 1, ChooseApplicationPreferences}, } { router := NewCommandRouter([]uint32{0xff63}, DefaultCommandBindings(layout)) router.Handle(0xff63, 90, 0, true) if consumed, command := router.Handle(' ', 65, tc.modifiers, true); !consumed || command != tc.want { t.Fatalf("layout %v shortcut = %v %q, want %q", layout, consumed, command, tc.want) } if consumed, command := router.Handle(' ', 65, tc.modifiers, false); !consumed || command != "" { t.Fatalf("shortcut release = %v %q", consumed, command) } } } } func TestLaptopBindingRoutesWhereAmI(t *testing.T) { router := NewCommandRouter([]uint32{0xff63}, DefaultCommandBindings(LaptopKeyboardLayout)) router.Handle(0xff63, 118, 0, true) if consumed, command := router.Handle(returnKeySymbol, 36, 0, true); !consumed || command != WhereAmI { t.Fatalf("Return press = %v %q", consumed, command) } if consumed, command := router.Handle(returnKeySymbol, 36, 0, false); !consumed || command != "" { t.Fatalf("Return release = %v %q", consumed, command) } if consumed, command := router.Handle(0xff63, 118, 0, false); !consumed || command != "" { t.Fatalf("modifier release = %v %q, want no command", consumed, command) } } func TestCellFormulaBindingAndLaptopOverride(t *testing.T) { chord := CommandChord{Symbol: '=', CthulhuModifier: true} if got := DefaultCommandBindings(DesktopKeyboardLayout)[chord]; got != PresentCellFormula { t.Fatalf("desktop formula binding = %q", got) } if got := DefaultCommandBindings(LaptopKeyboardLayout)[chord]; got != "" { t.Fatalf("laptop formula should be unbound by default: %q", got) } overrides := []BindingOverride{{Layout: LaptopKeyboardLayout, Scope: "global", Command: PresentCellFormula, Keys: []string{"Cthulhu+="}}} if err := ValidateBindingOverrides(overrides); err != nil { t.Fatal(err) } bindings, err := ResolveCommandBindings(LaptopKeyboardLayout, false, overrides) if err != nil || bindings[chord] != PresentCellFormula { t.Fatalf("laptop formula override = %q, %v", bindings[chord], err) } } func TestCalcHeaderCommandsCanBeBoundInBothLayouts(t *testing.T) { commands := []Command{SetColumnHeadersRow, ClearColumnHeadersRow, SetRowHeadersColumn, ClearRowHeadersColumn} keys := []string{"Cthulhu+Ctrl+Shift+r", "Cthulhu+Ctrl+Alt+r", "Cthulhu+Ctrl+Shift+c", "Cthulhu+Ctrl+Alt+c"} for _, layout := range []KeyboardLayout{DesktopKeyboardLayout, LaptopKeyboardLayout} { var overrides []BindingOverride for index, command := range commands { overrides = append(overrides, BindingOverride{Layout: layout, Scope: "global", Command: command, Keys: []string{keys[index]}}) } if err := ValidateBindingOverrides(overrides); err != nil { t.Fatal(err) } bindings, err := ResolveCommandBindings(layout, false, overrides) if err != nil { t.Fatal(err) } for index, command := range commands { chord, _ := ParseCommandChord(keys[index]) if bindings[chord] != command { t.Fatalf("layout %v: %s did not bind", layout, command) } } if got := bindings[CommandChord{Symbol: 'c', Modifiers: 1, CthulhuModifier: true}]; got != PresentClipboard { t.Fatalf("header commands displaced clipboard: %s", got) } } } func TestCharacterAttributesShortcutInBothLayouts(t *testing.T) { for _, layout := range []KeyboardLayout{DesktopKeyboardLayout, LaptopKeyboardLayout} { bindings := DefaultCommandBindings(layout) if got := bindings[CommandChord{Symbol: 'f', CthulhuModifier: true}]; got != PresentCharacterAttributes { t.Fatalf("layout %v Cthulhu+F = %q", layout, got) } } } func TestBrowseReturnActivatesWithoutChangingDefaultReturn(t *testing.T) { for name, bindings := range map[string]CommandBindings{ "default": DefaultCommandBindings(DesktopKeyboardLayout), "browse": BrowseCommandBindings(DesktopKeyboardLayout), } { router := NewCommandRouter([]uint32{0xff63}, bindings) consumed, command := router.Handle(returnKeySymbol, 36, 0, true) if name == "browse" && (!consumed || command != DocumentActivate) { t.Fatalf("browse Return = %v %q", consumed, command) } if name == "default" && (consumed || command != "") { t.Fatalf("default Return = %v %q", consumed, command) } } } func TestDesktopBindingRoutesWhereAmIWithoutModifier(t *testing.T) { router := NewCommandRouter([]uint32{0xff63}, DefaultCommandBindings(DesktopKeyboardLayout)) if consumed, command := router.Handle(keypadEnterKeySymbol, 104, 0, true); !consumed || command != WhereAmI { t.Fatalf("KP Enter press = %v %q", consumed, command) } if consumed, command := router.Handle(keypadEnterKeySymbol, 104, 0, false); !consumed || command != "" { t.Fatalf("KP Enter release = %v %q, want no command", consumed, command) } } func TestWhereAmIBindingsAreLayoutSpecific(t *testing.T) { desktop := NewCommandRouter([]uint32{0xff63}, DefaultCommandBindings(DesktopKeyboardLayout)) desktop.Handle(0xff63, 118, 0, true) if consumed, command := desktop.Handle(returnKeySymbol, 36, 0, true); consumed || command != "" { t.Fatalf("desktop Cthulhu+Return = %v %q, want unbound", consumed, command) } laptop := NewCommandRouter([]uint32{0xff63}, DefaultCommandBindings(LaptopKeyboardLayout)) if consumed, command := laptop.Handle(keypadEnterKeySymbol, 104, 0, true); consumed || command != "" { t.Fatalf("laptop KP Enter = %v %q, want unbound", consumed, command) } } func TestObjectNavigationBindingsMatchPython(t *testing.T) { tests := []struct { symbol uint32 command Command }{ {upKeySymbol, ObjectParent}, {downKeySymbol, ObjectFirstChild}, {leftKeySymbol, ObjectPreviousSibling}, {rightKeySymbol, ObjectNextSibling}, } for _, test := range tests { router := NewCommandRouter([]uint32{0xff63}, DefaultCommandBindings(DesktopKeyboardLayout)) router.Handle(0xff63, 118, 0, true) if consumed, command := router.Handle(test.symbol, 111, controlModifier, true); !consumed || command != test.command { t.Fatalf("symbol %#x press = %v %q", test.symbol, consumed, command) } if consumed, command := router.Handle(test.symbol, 111, controlModifier, false); !consumed || command != "" { t.Fatalf("symbol %#x release = %v %q", test.symbol, consumed, command) } if consumed, command := router.Handle(0xff63, 118, controlModifier, false); !consumed || command != "" { t.Fatalf("symbol %#x modifier release = %v %q, want %q", test.symbol, consumed, command, Command("")) } } } func TestObjectNavigationRequiresControl(t *testing.T) { router := NewCommandRouter([]uint32{0xff63}, DefaultCommandBindings(DesktopKeyboardLayout)) router.Handle(0xff63, 118, 0, true) if consumed, command := router.Handle(upKeySymbol, 111, 0, true); !consumed || command != IncreaseSpeechSetting { t.Fatalf("Cthulhu+Up = %v %q, want speech adjustment", consumed, command) } } func TestControlPressDoesNotMakeObjectNavigationPassThrough(t *testing.T) { router := NewCommandRouter([]uint32{0xff63}, DefaultCommandBindings(DesktopKeyboardLayout)) router.Handle(0xff63, 118, 0, true) if consumed, command := router.Handle(leftControlKeySymbol, 37, 0, true); consumed || command != "" { t.Fatalf("Control press = %v %q", consumed, command) } if !router.IsModifier(leftControlKeySymbol) || !router.IsModifier(rightControlKeySymbol) { t.Fatal("Control key was not classified as a chord modifier") } if consumed, command := router.Handle(upKeySymbol, 111, controlModifier, true); !consumed || command != ObjectParent { t.Fatalf("Up press = %v %q", consumed, command) } } func TestBindingTableMatchesExactAdditionalModifiers(t *testing.T) { bindings := CommandBindings{{Symbol: 'x', Modifiers: 4, CthulhuModifier: true}: Preferences} router := NewCommandRouter([]uint32{0xff63}, bindings) router.Handle(0xff63, 118, 0, true) if consumed, command := router.Handle('x', 53, 0, true); consumed || command != "" { t.Fatal("binding matched without its Control modifier") } router.Reset() router.Handle(0xff63, 118, 0, true) if consumed, command := router.Handle('x', 53, 4, true); !consumed || command != Preferences { t.Fatal("binding did not match its Control modifier") } } func TestBrowseCommandsPreserveShiftAndReleaseOrder(t *testing.T) { for _, tc := range []struct { symbol uint32 mods uint16 want Command }{ {'h', 0, DocumentNextHeading}, {'h', 1, DocumentPreviousHeading}, {'k', 0, DocumentNextLink}, {'k', 1, DocumentPreviousLink}, {'b', 0, DocumentNextButton}, {'b', 1, DocumentPreviousButton}, {'x', 0, DocumentNextCheckBox}, {'x', 1, DocumentPreviousCheckBox}, {'l', 0, DocumentNextList}, {'l', 1, DocumentPreviousList}, {'s', 0, DocumentNextSeparator}, {'s', 1, DocumentPreviousSeparator}, {'q', 0, DocumentNextBlockQuote}, {'q', 1, DocumentPreviousBlockQuote}, {'c', 0, DocumentNextComboBox}, {'c', 1, DocumentPreviousComboBox}, {'e', 0, DocumentNextEntry}, {'e', 1, DocumentPreviousEntry}, {'f', 0, DocumentNextFormField}, {'f', 1, DocumentPreviousFormField}, {'g', 0, DocumentNextImage}, {'g', 1, DocumentPreviousImage}, {'m', 0, DocumentNextLandmark}, {'m', 1, DocumentPreviousLandmark}, {'i', 0, DocumentNextListItem}, {'i', 1, DocumentPreviousListItem}, {'d', 0, DocumentNextLiveRegion}, {'d', 1, DocumentPreviousLiveRegion}, {'p', 0, DocumentNextParagraph}, {'p', 1, DocumentPreviousParagraph}, {'r', 0, DocumentNextRadioButton}, {'r', 1, DocumentPreviousRadioButton}, {'t', 0, DocumentNextTable}, {'t', 1, DocumentPreviousTable}, {'u', 0, DocumentNextUnvisitedLink}, {'u', 1, DocumentPreviousUnvisitedLink}, {'v', 0, DocumentNextVisitedLink}, {'v', 1, DocumentPreviousVisitedLink}, {'a', 0, DocumentNextClickable}, {'a', 1, DocumentPreviousClickable}, {'o', 0, DocumentNextChunk}, {'o', 1, DocumentPreviousChunk}, {'1', 0, DocumentNextHeadingAtLevel1}, {'1', 1, DocumentPreviousHeadingAtLevel1}, {'2', 0, DocumentNextHeadingAtLevel2}, {'2', 1, DocumentPreviousHeadingAtLevel2}, {'3', 0, DocumentNextHeadingAtLevel3}, {'3', 1, DocumentPreviousHeadingAtLevel3}, {'4', 0, DocumentNextHeadingAtLevel4}, {'4', 1, DocumentPreviousHeadingAtLevel4}, {'5', 0, DocumentNextHeadingAtLevel5}, {'5', 1, DocumentPreviousHeadingAtLevel5}, {'6', 0, DocumentNextHeadingAtLevel6}, {'6', 1, DocumentPreviousHeadingAtLevel6}, {'h', 4, ""}, {'k', 8, ""}, } { router := NewCommandRouter([]uint32{0xff63}, BrowseCommandBindings(DesktopKeyboardLayout)) consumed, command := router.Handle(tc.symbol, 43, tc.mods, true) if consumed != (tc.want != "") || command != tc.want { t.Fatalf("press: %v %s", consumed, command) } consumed, command = router.Handle(tc.symbol, 43, tc.mods, false) if consumed != (tc.want != "") || command != "" { t.Fatalf("release: %v %s want %s", consumed, command, Command("")) } } } func TestSpeechSettingBindings(t *testing.T) { for _, layout := range []KeyboardLayout{DesktopKeyboardLayout, LaptopKeyboardLayout} { for _, bindings := range []CommandBindings{DefaultCommandBindings(layout), BrowseCommandBindings(layout)} { for _, test := range []struct { symbol uint32 modifiers uint16 command Command }{ {leftKeySymbol, 0, SelectPreviousSpeechSetting}, {rightKeySymbol, 0, SelectNextSpeechSetting}, {upKeySymbol, 0, IncreaseSpeechSetting}, {downKeySymbol, 0, DecreaseSpeechSetting}, {'s', controlModifier | 1, SaveSpeechSettings}, } { router := NewCommandRouter([]uint32{0xff63}, bindings) router.Handle(0xff63, 118, 0, true) if consumed, command := router.Handle(test.symbol, 111, test.modifiers, true); !consumed || command != test.command { t.Fatalf("press: %v %q", consumed, command) } router.Handle(0xff63, 118, 0, false) if consumed, command := router.Handle(test.symbol, 111, test.modifiers, false); !consumed || command != "" { t.Fatalf("release: %v %q", consumed, command) } } } } }