blob: 0345ecb182d6a2cf9c6ca8cbc9be81ae30de98f5 [file] [log] [blame]
// 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