| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ui/base/accelerators/accelerator_manager.h" |
| |
| #include "base/test/scoped_feature_list.h" |
| #include "build/build_config.h" |
| #include "build/chromeos_buildflags.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "ui/base/accelerators/test_accelerator_target.h" |
| #include "ui/base/ui_base_features.h" |
| #include "ui/events/event.h" |
| #include "ui/events/event_constants.h" |
| #include "ui/events/keycodes/dom/dom_code.h" |
| #include "ui/events/keycodes/keyboard_codes.h" |
| |
| namespace ui { |
| namespace test { |
| namespace { |
| |
| Accelerator GetAccelerator(KeyboardCode code, int mask) { |
| return Accelerator(code, mask); |
| } |
| |
| // Possible flags used for accelerators. |
| const int kAcceleratorModifiers[] = {EF_SHIFT_DOWN, EF_CONTROL_DOWN, |
| EF_ALT_DOWN, EF_COMMAND_DOWN}; |
| |
| // Returns a set of flags from id, where id is a bitmask into |
| // kAcceleratorModifiers used to determine which flags are set. |
| int BuildAcceleratorModifier(int id) { |
| int result = 0; |
| for (size_t i = 0; i < std::size(kAcceleratorModifiers); ++i) { |
| if (((1 << i) & id) != 0) |
| result |= kAcceleratorModifiers[i]; |
| } |
| return result; |
| } |
| |
| class AcceleratorManagerTest : public testing::Test { |
| public: |
| AcceleratorManagerTest() = default; |
| ~AcceleratorManagerTest() override = default; |
| |
| protected: |
| AcceleratorManager manager_; |
| }; |
| |
| TEST_F(AcceleratorManagerTest, Register) { |
| TestAcceleratorTarget target; |
| const Accelerator accelerator_a(VKEY_A, EF_NONE); |
| const Accelerator accelerator_b(VKEY_B, EF_NONE); |
| const Accelerator accelerator_c(VKEY_C, EF_NONE); |
| const Accelerator accelerator_d(VKEY_D, EF_NONE); |
| manager_.Register( |
| {accelerator_a, accelerator_b, accelerator_c, accelerator_d}, |
| AcceleratorManager::kNormalPriority, &target); |
| |
| // The registered accelerators are processed. |
| EXPECT_TRUE(manager_.Process(accelerator_a)); |
| EXPECT_TRUE(manager_.Process(accelerator_b)); |
| EXPECT_TRUE(manager_.Process(accelerator_c)); |
| EXPECT_TRUE(manager_.Process(accelerator_d)); |
| EXPECT_EQ(4, target.accelerator_count()); |
| } |
| |
| TEST_F(AcceleratorManagerTest, RegisterMultipleTarget) { |
| const Accelerator accelerator_a(VKEY_A, EF_NONE); |
| TestAcceleratorTarget target1; |
| manager_.Register({accelerator_a}, AcceleratorManager::kNormalPriority, |
| &target1); |
| TestAcceleratorTarget target2; |
| manager_.Register({accelerator_a}, AcceleratorManager::kNormalPriority, |
| &target2); |
| |
| // If multiple targets are registered with the same accelerator, the target |
| // registered later processes the accelerator. |
| EXPECT_TRUE(manager_.Process(accelerator_a)); |
| EXPECT_EQ(0, target1.accelerator_count()); |
| EXPECT_EQ(1, target2.accelerator_count()); |
| } |
| |
| TEST_F(AcceleratorManagerTest, Unregister) { |
| const Accelerator accelerator_a(VKEY_A, EF_NONE); |
| TestAcceleratorTarget target; |
| const Accelerator accelerator_b(VKEY_B, EF_NONE); |
| manager_.Register({accelerator_a, accelerator_b}, |
| AcceleratorManager::kNormalPriority, &target); |
| |
| // Unregistering a different accelerator does not affect the other |
| // accelerator. |
| manager_.Unregister(accelerator_b, &target); |
| EXPECT_TRUE(manager_.Process(accelerator_a)); |
| EXPECT_EQ(1, target.accelerator_count()); |
| |
| // The unregistered accelerator is no longer processed. |
| target.ResetCounts(); |
| manager_.Unregister(accelerator_a, &target); |
| EXPECT_FALSE(manager_.Process(accelerator_a)); |
| EXPECT_EQ(0, target.accelerator_count()); |
| } |
| |
| TEST_F(AcceleratorManagerTest, UnregisterAll) { |
| const Accelerator accelerator_a(VKEY_A, EF_NONE); |
| TestAcceleratorTarget target1; |
| const Accelerator accelerator_b(VKEY_B, EF_NONE); |
| manager_.Register({accelerator_a, accelerator_b}, |
| AcceleratorManager::kNormalPriority, &target1); |
| |
| const Accelerator accelerator_c(VKEY_C, EF_NONE); |
| TestAcceleratorTarget target2; |
| manager_.Register({accelerator_c}, AcceleratorManager::kNormalPriority, |
| &target2); |
| |
| manager_.UnregisterAll(&target1); |
| |
| // All the accelerators registered for |target1| are no longer processed. |
| EXPECT_FALSE(manager_.Process(accelerator_a)); |
| EXPECT_FALSE(manager_.Process(accelerator_b)); |
| EXPECT_EQ(0, target1.accelerator_count()); |
| |
| // UnregisterAll with a different target does not affect the other target. |
| EXPECT_TRUE(manager_.Process(accelerator_c)); |
| EXPECT_EQ(1, target2.accelerator_count()); |
| } |
| |
| TEST_F(AcceleratorManagerTest, Process) { |
| TestAcceleratorTarget target; |
| |
| // Test all cases of possible modifiers. |
| for (size_t i = 0; i < (1 << std::size(kAcceleratorModifiers)); ++i) { |
| const int modifiers = BuildAcceleratorModifier(i); |
| Accelerator accelerator(GetAccelerator(VKEY_A, modifiers)); |
| manager_.Register({accelerator}, AcceleratorManager::kNormalPriority, |
| &target); |
| |
| // The registered accelerator is processed. |
| const int last_count = target.accelerator_count(); |
| EXPECT_TRUE(manager_.Process(accelerator)) << i; |
| EXPECT_EQ(last_count + 1, target.accelerator_count()) << i; |
| |
| // The non-registered accelerators are not processed. |
| accelerator.set_key_state(Accelerator::KeyState::RELEASED); |
| EXPECT_FALSE(manager_.Process(accelerator)) << i; // different type |
| |
| EXPECT_FALSE(manager_.Process(GetAccelerator(VKEY_UNKNOWN, modifiers))) |
| << i; // different vkey |
| EXPECT_FALSE(manager_.Process(GetAccelerator(VKEY_B, modifiers))) |
| << i; // different vkey |
| EXPECT_FALSE(manager_.Process(GetAccelerator(VKEY_SHIFT, modifiers))) |
| << i; // different vkey |
| |
| for (size_t test_i = 0; test_i < (1 << std::size(kAcceleratorModifiers)); |
| ++test_i) { |
| if (test_i == i) |
| continue; |
| const int test_modifiers = BuildAcceleratorModifier(test_i); |
| const Accelerator test_accelerator( |
| GetAccelerator(VKEY_A, test_modifiers)); |
| EXPECT_FALSE(manager_.Process(test_accelerator)) << " i=" << i |
| << " test_i=" << test_i; |
| } |
| |
| EXPECT_EQ(last_count + 1, target.accelerator_count()) << i; |
| manager_.UnregisterAll(&target); |
| } |
| } |
| |
| #if BUILDFLAG(IS_CHROMEOS_ASH) |
| // TODO(crbug.com/1179893): Remove once the feature is enabled permanently. |
| TEST_F(AcceleratorManagerTest, NewMappingWithImprovedShortcutsDisabled) { |
| base::test::ScopedFeatureList scoped_feature_list; |
| scoped_feature_list.InitWithFeatures( |
| /*enabled_features=*/{features::kNewShortcutMapping}, |
| /*disabled_features=*/{::features::kImprovedKeyboardShortcuts}); |
| |
| // Test new mapping with a ASCII punctuation shortcut that doesn't involve |
| // shift. |
| TestAcceleratorTarget target; |
| { |
| // ']' + ctrl |
| const Accelerator accelerator(VKEY_OEM_6, EF_CONTROL_DOWN); |
| manager_.Register({accelerator}, AcceleratorManager::kNormalPriority, |
| &target); |
| KeyEvent event(ui::ET_KEY_PRESSED, ui::VKEY_1, ui::DomCode::NONE, |
| ui::EF_CONTROL_DOWN, ui::DomKey::Constant<']'>::Character, |
| base::TimeTicks()); |
| const Accelerator trigger(event); |
| EXPECT_TRUE(manager_.IsRegistered(trigger)); |
| EXPECT_TRUE(manager_.Process(trigger)); |
| } |
| |
| // Test new mapping with a ASCII punctuation shortcut that involves shift. |
| { |
| // ']' + ctrl + shift, which produces '}' on US layout. |
| const Accelerator accelerator(VKEY_OEM_6, EF_CONTROL_DOWN | EF_SHIFT_DOWN); |
| manager_.Register({accelerator}, AcceleratorManager::kNormalPriority, |
| &target); |
| KeyEvent event(ui::ET_KEY_PRESSED, ui::VKEY_1, ui::DomCode::NONE, |
| ui::EF_CONTROL_DOWN, ui::DomKey::Constant<'}'>::Character, |
| base::TimeTicks()); |
| const Accelerator trigger(event); |
| EXPECT_TRUE(manager_.IsRegistered(trigger)); |
| EXPECT_TRUE(manager_.Process(trigger)); |
| } |
| } |
| |
| // TODO(crbug.com/1179893): Remove once the feature is enabled permanently. |
| TEST_F(AcceleratorManagerTest, NewMappingSuperseded) { |
| base::test::ScopedFeatureList scoped_feature_list; |
| scoped_feature_list.InitAndEnableFeature(features::kNewShortcutMapping); |
| |
| // When kImprovedKeyboardShortcuts is enabled, it takes precedence |
| // over kNewShortcutMapping. Remove this test when kImprovedShortcutMapping |
| // is made permanent. |
| EXPECT_TRUE(::features::IsImprovedKeyboardShortcutsEnabled()); |
| EXPECT_FALSE(::features::IsNewShortcutMappingEnabled()); |
| } |
| |
| #endif // BUILDFLAG(IS_CHROMEOS_ASH) |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| |
| TEST_F(AcceleratorManagerTest, PositionalShortcuts_AllEqual) { |
| base::test::ScopedFeatureList scoped_feature_list; |
| scoped_feature_list.InitAndEnableFeature( |
| features::kImprovedKeyboardShortcuts); |
| |
| // Use a local instance so that the feature is enabled during construction. |
| AcceleratorManager manager; |
| manager.SetUsePositionalLookup(true); |
| |
| // Test what would be ctrl + ']' (VKEY_OEM_6) on a US keyboard. This |
| // should match. |
| TestAcceleratorTarget target; |
| const Accelerator accelerator(VKEY_OEM_6, EF_CONTROL_DOWN); |
| manager.Register({accelerator}, AcceleratorManager::kNormalPriority, &target); |
| KeyEvent event(ui::ET_KEY_PRESSED, VKEY_OEM_6, ui::DomCode::BRACKET_RIGHT, |
| ui::EF_CONTROL_DOWN, ui::DomKey::Constant<']'>::Character, |
| base::TimeTicks()); |
| const Accelerator trigger(event); |
| EXPECT_TRUE(manager.IsRegistered(trigger)); |
| EXPECT_TRUE(manager.Process(trigger)); |
| } |
| |
| TEST_F(AcceleratorManagerTest, PositionalShortcuts_MatchingDomCode) { |
| base::test::ScopedFeatureList scoped_feature_list; |
| scoped_feature_list.InitAndEnableFeature( |
| features::kImprovedKeyboardShortcuts); |
| |
| // Use a local instance so that the feature is enabled during construction. |
| AcceleratorManager manager; |
| manager.SetUsePositionalLookup(true); |
| |
| // Test what would be ctrl + ']' on a US keyboard with matching DomCode |
| // and different VKEY (eg. '+'). This is the use case of a positional key |
| // on the German keyboard. Since the DomCode matches, this should match. |
| TestAcceleratorTarget target; |
| const Accelerator accelerator(VKEY_OEM_6, EF_CONTROL_DOWN); |
| manager.Register({accelerator}, AcceleratorManager::kNormalPriority, &target); |
| KeyEvent event(ui::ET_KEY_PRESSED, VKEY_OEM_PLUS, ui::DomCode::BRACKET_RIGHT, |
| ui::EF_CONTROL_DOWN, ui::DomKey::Constant<']'>::Character, |
| base::TimeTicks()); |
| const Accelerator trigger(event); |
| EXPECT_TRUE(manager.IsRegistered(trigger)); |
| EXPECT_TRUE(manager.Process(trigger)); |
| } |
| |
| TEST_F(AcceleratorManagerTest, PositionalShortcuts_NotMatchingDomCode) { |
| base::test::ScopedFeatureList scoped_feature_list; |
| scoped_feature_list.InitAndEnableFeature( |
| features::kImprovedKeyboardShortcuts); |
| |
| // Use a local instance so that the feature is enabled during construction. |
| AcceleratorManager manager; |
| manager.SetUsePositionalLookup(true); |
| |
| // Test what would be ctrl + ']' on a US keyboard using positional mapping |
| // for a German layout. The accelerator is registered using the US VKEY and |
| // triggered with a KeyEvent with the US VKEY but a mismatched DomCode. This |
| // should not match. This prevents ghost shortcuts on non-US layouts. |
| TestAcceleratorTarget target; |
| const Accelerator accelerator(VKEY_OEM_6, EF_CONTROL_DOWN); |
| manager.Register({accelerator}, AcceleratorManager::kNormalPriority, &target); |
| KeyEvent event(ui::ET_KEY_PRESSED, VKEY_OEM_6, ui::DomCode::BRACKET_LEFT, |
| ui::EF_CONTROL_DOWN, ui::DomKey::Constant<']'>::Character, |
| base::TimeTicks()); |
| const Accelerator trigger(event); |
| EXPECT_FALSE(manager.IsRegistered(trigger)); |
| EXPECT_FALSE(manager.Process(trigger)); |
| } |
| |
| TEST_F(AcceleratorManagerTest, PositionalShortcuts_NonPositionalMatch) { |
| base::test::ScopedFeatureList scoped_feature_list; |
| scoped_feature_list.InitAndEnableFeature( |
| features::kImprovedKeyboardShortcuts); |
| |
| // Use a local instance so that the feature is enabled during construction. |
| AcceleratorManager manager; |
| manager.SetUsePositionalLookup(true); |
| |
| // Test ctrl + 'Z' for the German layout. Since 'Z' is not a positional |
| // key it should match based on the VKEY, regardless of the DomCode. In this |
| // case the 'Z' has DomCode US_Y (ie. QWERTZ keyboard), but it should still |
| // match. |
| TestAcceleratorTarget target; |
| const Accelerator accelerator(VKEY_Z, EF_CONTROL_DOWN); |
| manager.Register({accelerator}, AcceleratorManager::kNormalPriority, &target); |
| KeyEvent event(ui::ET_KEY_PRESSED, VKEY_Z, ui::DomCode::US_Y, |
| ui::EF_CONTROL_DOWN, ui::DomKey::Constant<']'>::Character, |
| base::TimeTicks()); |
| const Accelerator trigger(event); |
| EXPECT_TRUE(manager.IsRegistered(trigger)); |
| EXPECT_TRUE(manager.Process(trigger)); |
| } |
| |
| TEST_F(AcceleratorManagerTest, PositionalShortcuts_NonPositionalNonMatch) { |
| base::test::ScopedFeatureList scoped_feature_list; |
| scoped_feature_list.InitAndEnableFeature( |
| features::kImprovedKeyboardShortcuts); |
| |
| // Use a local instance so that the feature is enabled during construction. |
| AcceleratorManager manager; |
| manager.SetUsePositionalLookup(true); |
| |
| // Test ctrl + 'Z' for the German layout. The 'Y' key (in the US_Z position), |
| // should not match. Alphanumeric keys are not positional, and pressing the |
| // key with DomCode::US_Z should not match when it's mapped to VKEY_Y. |
| TestAcceleratorTarget target; |
| const Accelerator accelerator(VKEY_Z, EF_CONTROL_DOWN); |
| manager.Register({accelerator}, AcceleratorManager::kNormalPriority, &target); |
| KeyEvent event(ui::ET_KEY_PRESSED, VKEY_Y, ui::DomCode::US_Z, |
| ui::EF_CONTROL_DOWN, ui::DomKey::Constant<']'>::Character, |
| base::TimeTicks()); |
| const Accelerator trigger(event); |
| EXPECT_FALSE(manager.IsRegistered(trigger)); |
| EXPECT_FALSE(manager.Process(trigger)); |
| } |
| |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| |
| } // namespace |
| } // namespace test |
| } // namespace ui |