| // Copyright 2024 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ash/accelerators/suspend_state_machine.h" |
| |
| #include "ash/test/ash_test_base.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/time/time.h" |
| #include "chromeos/dbus/power/fake_power_manager_client.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "ui/base/accelerators/accelerator.h" |
| #include "ui/events/base_event_utils.h" |
| #include "ui/events/devices/stylus_state.h" |
| #include "ui/events/event_constants.h" |
| #include "ui/events/keycodes/dom/dom_code.h" |
| #include "ui/events/keycodes/dom/dom_key.h" |
| #include "ui/events/keycodes/keyboard_codes_posix.h" |
| #include "ui/events/types/event_type.h" |
| #include "ui/ozone/public/stub_input_controller.h" |
| |
| namespace ash::accelerators { |
| |
| namespace { |
| |
| class MockInputController : public ui::StubInputController { |
| public: |
| MOCK_METHOD(bool, AreAnyKeysPressed, (), (override)); |
| }; |
| |
| // Key representation in test cases. |
| struct TestKeyEvent { |
| ui::EventType type; |
| ui::DomCode code; |
| ui::DomKey key; |
| ui::KeyboardCode keycode; |
| ui::EventFlags flags = ui::EF_NONE; |
| |
| std::string ToString() const; |
| }; |
| |
| // Factory template of TestKeyEvents just to reduce a lot of code/data |
| // duplication. |
| template <ui::DomCode code, |
| ui::DomKey::Base key, |
| ui::KeyboardCode keycode, |
| ui::EventFlags modifier_flag = ui::EF_NONE, |
| ui::DomKey::Base shifted_key = key> |
| struct TestKey { |
| // Returns press key event. |
| static constexpr TestKeyEvent Pressed(ui::EventFlags flags = ui::EF_NONE) { |
| return {ui::EventType::kKeyPressed, code, |
| (flags & ui::EF_SHIFT_DOWN) ? shifted_key : key, keycode, |
| flags | modifier_flag}; |
| } |
| |
| // Returns release key event. |
| static constexpr TestKeyEvent Released(ui::EventFlags flags = ui::EF_NONE) { |
| // Note: modifier flag should not be present on release events. |
| return {ui::EventType::kKeyReleased, code, |
| (flags & ui::EF_SHIFT_DOWN) ? shifted_key : key, keycode, flags}; |
| } |
| }; |
| |
| // Short cut of TestKey construction for Character keys. |
| template <ui::DomCode code, |
| char key, |
| ui::KeyboardCode keycode, |
| char shifted_key = key> |
| using TestCharKey = TestKey<code, |
| ui::DomKey::FromCharacter(key), |
| keycode, |
| ui::EF_NONE, |
| ui::DomKey::FromCharacter(shifted_key)>; |
| |
| // Modifier keys. |
| using KeyLShift = TestKey<ui::DomCode::SHIFT_LEFT, |
| ui::DomKey::SHIFT, |
| ui::VKEY_SHIFT, |
| ui::EF_SHIFT_DOWN>; |
| using KeyRShift = TestKey<ui::DomCode::SHIFT_RIGHT, |
| ui::DomKey::SHIFT, |
| ui::VKEY_SHIFT, |
| ui::EF_SHIFT_DOWN>; |
| using KeyLMeta = TestKey<ui::DomCode::META_LEFT, |
| ui::DomKey::META, |
| ui::VKEY_LWIN, |
| ui::EF_COMMAND_DOWN>; |
| using KeyRMeta = TestKey<ui::DomCode::META_RIGHT, |
| ui::DomKey::META, |
| ui::VKEY_RWIN, |
| ui::EF_COMMAND_DOWN>; |
| using KeyLControl = TestKey<ui::DomCode::CONTROL_LEFT, |
| ui::DomKey::CONTROL, |
| ui::VKEY_CONTROL, |
| ui::EF_CONTROL_DOWN>; |
| using KeyRControl = TestKey<ui::DomCode::CONTROL_RIGHT, |
| ui::DomKey::CONTROL, |
| ui::VKEY_CONTROL, |
| ui::EF_CONTROL_DOWN>; |
| using KeyLAlt = TestKey<ui::DomCode::ALT_LEFT, |
| ui::DomKey::ALT, |
| ui::VKEY_MENU, |
| ui::EF_ALT_DOWN>; |
| using KeyRAlt = TestKey<ui::DomCode::ALT_RIGHT, |
| ui::DomKey::ALT, |
| ui::VKEY_MENU, |
| ui::EF_ALT_DOWN>; |
| |
| // Character keys. Shift chars are based on US layout. |
| using KeyA = TestCharKey<ui::DomCode::US_A, 'a', ui::VKEY_A, 'A'>; |
| using KeyB = TestCharKey<ui::DomCode::US_B, 'b', ui::VKEY_B, 'B'>; |
| using KeyC = TestCharKey<ui::DomCode::US_C, 'c', ui::VKEY_C, 'C'>; |
| |
| const bool kKeysPressed = true; |
| const bool kNoKeysPressed = false; |
| |
| using EventTypeVariant = std::variant<TestKeyEvent, bool>; |
| using SuspendStateMachineEvent = SuspendStateMachine::SuspendStateMachineEvent; |
| |
| } // namespace |
| |
| class SuspendStateMachineTest |
| : public AshTestBase, |
| public testing::WithParamInterface< |
| std::tuple<ui::Accelerator, std::vector<EventTypeVariant>>> { |
| public: |
| void SetUp() override { |
| AshTestBase::SetUp(); |
| |
| std::tie(trigger_accelerator_, events_) = GetParam(); |
| input_controller_ = std::make_unique<MockInputController>(); |
| suspend_state_machine_ = |
| std::make_unique<SuspendStateMachine>(input_controller_.get()); |
| } |
| |
| void TearDown() override { |
| suspend_state_machine_.reset(); |
| AshTestBase::TearDown(); |
| } |
| |
| protected: |
| std::unique_ptr<SuspendStateMachine> suspend_state_machine_; |
| std::unique_ptr<MockInputController> input_controller_; |
| std::vector<EventTypeVariant> events_; |
| ui::Accelerator trigger_accelerator_; |
| }; |
| |
| class SuccessfulSuspendStateMachineTest : public SuspendStateMachineTest {}; |
| |
| INSTANTIATE_TEST_SUITE_P( |
| All, |
| SuccessfulSuspendStateMachineTest, |
| testing::ValuesIn( |
| std::vector<std::tuple<ui::Accelerator, std::vector<EventTypeVariant>>>{ |
| // Standard activations of the suspend. |
| {{ui::VKEY_A, ui::EF_COMMAND_DOWN}, |
| {kKeysPressed, KeyLMeta::Released(), kNoKeysPressed, |
| KeyA::Released()}}, |
| {{ui::VKEY_B, ui::EF_COMMAND_DOWN}, |
| {kKeysPressed, KeyLMeta::Released(), kNoKeysPressed, |
| KeyB::Released()}}, |
| {{ui::VKEY_C, ui::EF_ALT_DOWN | ui::EF_CONTROL_DOWN}, |
| {kKeysPressed, KeyLControl::Released(), KeyLAlt::Released(), |
| kNoKeysPressed, KeyC::Released()}}, |
| |
| // Reversed ordering of releases, checks that ordering does not |
| // matter. |
| {{ui::VKEY_A, ui::EF_COMMAND_DOWN}, |
| {kKeysPressed, KeyA::Released(), kNoKeysPressed, |
| KeyLMeta::Released()}}, |
| {{ui::VKEY_B, ui::EF_COMMAND_DOWN}, |
| {kKeysPressed, KeyB::Released(), kNoKeysPressed, |
| KeyLMeta::Released()}}, |
| {{ui::VKEY_C, ui::EF_ALT_DOWN | ui::EF_CONTROL_DOWN}, |
| {kKeysPressed, KeyC::Released(), KeyLAlt::Released(), |
| kNoKeysPressed, KeyLControl::Released()}}, |
| |
| // Left vs right modifiers does not matter. |
| {{ui::VKEY_A, ui::EF_COMMAND_DOWN}, |
| {kKeysPressed, KeyRMeta::Released(), KeyLMeta::Released(), |
| kNoKeysPressed, KeyA::Released()}}, |
| {{ui::VKEY_A, ui::EF_ALT_DOWN}, |
| {kKeysPressed, KeyRAlt::Released(), KeyLAlt::Released(), |
| kNoKeysPressed, KeyA::Released()}}, |
| {{ui::VKEY_A, ui::EF_CONTROL_DOWN}, |
| {kKeysPressed, KeyLControl::Released(), KeyRControl::Released(), |
| kNoKeysPressed, KeyA::Released()}}, |
| {{ui::VKEY_A, ui::EF_SHIFT_DOWN}, |
| {kKeysPressed, KeyLShift::Released(), KeyRShift::Released(), |
| kNoKeysPressed, KeyA::Released()}}, |
| |
| // All modifiers in one accelerator. |
| {{ui::VKEY_A, ui::EF_COMMAND_DOWN | ui::EF_ALT_DOWN | |
| ui::EF_SHIFT_DOWN | ui::EF_CONTROL_DOWN}, |
| {kKeysPressed, KeyLControl::Released(), KeyLShift::Released(), |
| KeyLMeta::Released(), KeyRAlt::Released(), kNoKeysPressed, |
| KeyA::Released()}}, |
| |
| // No modifiers in accelerator. |
| {{ui::VKEY_A, ui::EF_NONE}, {kNoKeysPressed, KeyA::Released()}}, |
| |
| // Other keys currently held down, not triggered until all keys |
| // released. |
| {{ui::VKEY_A, ui::EF_ALT_DOWN}, |
| {kKeysPressed, KeyLAlt::Released(), KeyA::Released(), |
| kNoKeysPressed, KeyRAlt::Released()}}, |
| |
| })); |
| |
| TEST_P(SuccessfulSuspendStateMachineTest, SuspendTriggered) { |
| base::HistogramTester histogram_tester; |
| |
| suspend_state_machine_->StartObservingToTriggerSuspend(trigger_accelerator_); |
| histogram_tester.ExpectBucketCount("ChromeOS.Inputs.SuspendStateMachine", |
| SuspendStateMachineEvent::kTriggered, 1); |
| for (const auto& event : events_) { |
| ASSERT_EQ(0, power_manager_client()->num_request_suspend_calls()); |
| if (std::holds_alternative<bool>(event)) { |
| ON_CALL(*input_controller_, AreAnyKeysPressed()) |
| .WillByDefault(testing::Return(std::get<bool>(event))); |
| continue; |
| } else { |
| const TestKeyEvent& test_event = std::get<TestKeyEvent>(event); |
| ui::KeyEvent key_event(test_event.type, test_event.keycode, |
| test_event.code, test_event.flags, test_event.key, |
| ui::EventTimeForNow()); |
| suspend_state_machine_->OnEvent(&key_event); |
| } |
| } |
| |
| EXPECT_EQ(1, power_manager_client()->num_request_suspend_calls()); |
| histogram_tester.ExpectBucketCount("ChromeOS.Inputs.SuspendStateMachine", |
| SuspendStateMachineEvent::kSuspended, 1); |
| } |
| |
| class CancelledSuspendStateMachineTest : public SuspendStateMachineTest {}; |
| |
| INSTANTIATE_TEST_SUITE_P( |
| All, |
| CancelledSuspendStateMachineTest, |
| testing::ValuesIn( |
| std::vector<std::tuple<ui::Accelerator, std::vector<EventTypeVariant>>>{ |
| // Release a key not in the original accelerator. |
| {{ui::VKEY_A, ui::EF_COMMAND_DOWN}, |
| {kKeysPressed, KeyLMeta::Released(), KeyB::Released(), |
| kNoKeysPressed, KeyA::Released()}}, |
| |
| // Release a modifier not in the original accelerator. |
| {{ui::VKEY_A, ui::EF_COMMAND_DOWN}, |
| {kKeysPressed, KeyLControl::Released(), kNoKeysPressed, |
| KeyA::Released()}}, |
| |
| // Press a modifier again in the original accelerator. |
| {{ui::VKEY_A, ui::EF_COMMAND_DOWN}, |
| {kKeysPressed, KeyLMeta::Released(), KeyLMeta::Pressed(), |
| KeyLMeta::Released(), kNoKeysPressed, KeyA::Released()}}, |
| |
| // Press a key not in the original accelerator. |
| {{ui::VKEY_A, ui::EF_COMMAND_DOWN}, |
| {kKeysPressed, KeyLMeta::Released(), KeyB::Pressed(), |
| KeyB::Released(), kNoKeysPressed, KeyA::Released()}}, |
| {{ui::VKEY_A, ui::EF_COMMAND_DOWN}, |
| {kKeysPressed, KeyLMeta::Released(), KeyB::Pressed(), |
| kNoKeysPressed, KeyA::Released()}}, |
| })); |
| |
| TEST_P(CancelledSuspendStateMachineTest, SuspendNotTriggered) { |
| base::HistogramTester histogram_tester; |
| |
| suspend_state_machine_->StartObservingToTriggerSuspend(trigger_accelerator_); |
| histogram_tester.ExpectBucketCount("ChromeOS.Inputs.SuspendStateMachine", |
| SuspendStateMachineEvent::kTriggered, 1); |
| for (const auto& event : events_) { |
| ASSERT_EQ(0, power_manager_client()->num_request_suspend_calls()); |
| if (std::holds_alternative<bool>(event)) { |
| ON_CALL(*input_controller_, AreAnyKeysPressed()) |
| .WillByDefault(testing::Return(std::get<bool>(event))); |
| continue; |
| } else { |
| const TestKeyEvent& test_event = std::get<TestKeyEvent>(event); |
| ui::KeyEvent key_event(test_event.type, test_event.keycode, |
| test_event.code, test_event.flags, test_event.key, |
| ui::EventTimeForNow()); |
| suspend_state_machine_->OnEvent(&key_event); |
| } |
| } |
| |
| EXPECT_EQ(0, power_manager_client()->num_request_suspend_calls()); |
| histogram_tester.ExpectBucketCount("ChromeOS.Inputs.SuspendStateMachine", |
| SuspendStateMachineEvent::kCancelled, 1); |
| } |
| |
| } // namespace ash::accelerators |