chromium/ash/rgb_keyboard/rgb_keyboard_manager_unittest.cc

// Copyright 2022 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/rgb_keyboard/rgb_keyboard_manager.h"

#include <stdint.h>

#include <memory>
#include <optional>

#include "ash/constants/ash_features.h"
#include "ash/ime/ime_controller_impl.h"
#include "ash/rgb_keyboard/histogram_util.h"
#include "base/memory/raw_ptr.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "chromeos/ash/components/dbus/rgbkbd/fake_rgbkbd_client.h"
#include "chromeos/ash/components/dbus/rgbkbd/rgbkbd_client.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace ash {

class RgbKeyboardManagerTest : public testing::Test {
 public:
  RgbKeyboardManagerTest() {
    scoped_feature_list_.InitWithFeatures(
        /*enabled_features=*/{features::kExperimentalRgbKeyboardPatterns},
        /*disabled_features=*/{});
    // ImeControllerImpl must be initialized before RgbKeyboardManager.
    ime_controller_ = std::make_unique<ImeControllerImpl>();
    // This is instantiating a global instance that will be deallocated in
    // the destructor of RgbKeyboardManagerTest.
    RgbkbdClient::InitializeFake();
    client_ = static_cast<FakeRgbkbdClient*>(RgbkbdClient::Get());
    // Default capabilities to 'RgbKeyboardCapabilities::kIndividualKey'
    InitializeManagerWithCapability(
        rgbkbd::RgbKeyboardCapabilities::kIndividualKey);
  }

  RgbKeyboardManagerTest(const RgbKeyboardManagerTest&) = delete;
  RgbKeyboardManagerTest& operator=(const RgbKeyboardManagerTest&) = delete;
  ~RgbKeyboardManagerTest() override {
    // Ordering for deletion is Manger -> Client -> IME Controller
    manager_.reset();
    RgbkbdClient::Shutdown();
    ime_controller_.reset();
  }

 protected:
  void InitializeManagerWithCapability(
      rgbkbd::RgbKeyboardCapabilities capability) {
    client_->set_rgb_keyboard_capabilities(capability);
    // |ime_controller_| is initialized in RgbKeyboardManagerTest's ctor.
    DCHECK(ime_controller_);
    manager_.reset();
    manager_ = std::make_unique<RgbKeyboardManager>(ime_controller_.get());
  }
  // ImeControllerImpl must be destroyed after RgbKeyboardManager.
  std::unique_ptr<ImeControllerImpl> ime_controller_;
  std::unique_ptr<RgbKeyboardManager> manager_;
  raw_ptr<FakeRgbkbdClient, DanglingUntriaged> client_;

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
};

TEST_F(RgbKeyboardManagerTest, GetKeyboardCapabilities) {
  // kIndividualKey is the default for this test suite.
  EXPECT_EQ(rgbkbd::RgbKeyboardCapabilities::kIndividualKey,
            client_->get_rgb_keyboard_capabilities());
}

class KeyboardCapabilityHistogramEmittedTest
    : public RgbKeyboardManagerTest,
      public testing::WithParamInterface<
          std::pair<rgbkbd::RgbKeyboardCapabilities,
                    ash::rgb_keyboard::metrics::RgbKeyboardCapabilityType>> {
 public:
  KeyboardCapabilityHistogramEmittedTest() {
    std::tie(capability_, metric_) = GetParam();
  }

 protected:
  rgbkbd::RgbKeyboardCapabilities capability_;
  ash::rgb_keyboard::metrics::RgbKeyboardCapabilityType metric_;
};

INSTANTIATE_TEST_SUITE_P(
    All,
    KeyboardCapabilityHistogramEmittedTest,
    testing::Values(
        std::make_pair(
            rgbkbd::RgbKeyboardCapabilities::kNone,
            ash::rgb_keyboard::metrics::RgbKeyboardCapabilityType::kNone),
        std::make_pair(rgbkbd::RgbKeyboardCapabilities::kIndividualKey,
                       ash::rgb_keyboard::metrics::RgbKeyboardCapabilityType::
                           kIndividualKey),
        std::make_pair(rgbkbd::RgbKeyboardCapabilities::kFourZoneFortyLed,
                       ash::rgb_keyboard::metrics::RgbKeyboardCapabilityType::
                           kFourZoneFortyLed),
        std::make_pair(rgbkbd::RgbKeyboardCapabilities::kFourZoneTwelveLed,
                       ash::rgb_keyboard::metrics::RgbKeyboardCapabilityType::
                           kFourZoneTwelveLed),
        std::make_pair(rgbkbd::RgbKeyboardCapabilities::kFourZoneFourLed,
                       ash::rgb_keyboard::metrics::RgbKeyboardCapabilityType::
                           kFourZoneFourLed)));

TEST_P(KeyboardCapabilityHistogramEmittedTest,
       KeyboardCapabilityHistogramEmitted) {
  base::HistogramTester histogram_tester;
  InitializeManagerWithCapability(capability_);
  EXPECT_EQ(capability_, client_->get_rgb_keyboard_capabilities());
  histogram_tester.ExpectBucketCount(
      rgb_keyboard::metrics::kRgbKeyboardCapabilityTypeHistogramName, metric_,
      1);
}

class RgbChangeTypeHistogramEmittedTest
    : public RgbKeyboardManagerTest,
      public testing::WithParamInterface<rgbkbd::RgbKeyboardCapabilities> {
 public:
  RgbChangeTypeHistogramEmittedTest() = default;
};

INSTANTIATE_TEST_SUITE_P(
    All,
    RgbChangeTypeHistogramEmittedTest,
    testing::Values(rgbkbd::RgbKeyboardCapabilities::kIndividualKey,
                    rgbkbd::RgbKeyboardCapabilities::kFourZoneFortyLed,
                    rgbkbd::RgbKeyboardCapabilities::kFourZoneTwelveLed,
                    rgbkbd::RgbKeyboardCapabilities::kFourZoneFourLed));

TEST_P(RgbChangeTypeHistogramEmittedTest, RgbChangeTypeHistogramEmitted) {
  base::HistogramTester histogram_tester;
  const auto capability = GetParam();
  const auto name =
      std::string(ash::rgb_keyboard::metrics::kRgbKeyboardHistogramPrefix +
                  ash::rgb_keyboard::metrics::GetCapabilityTypeStr(capability));
  InitializeManagerWithCapability(capability);
  manager_->SetStaticBackgroundColor(/*r=*/1, /*g=*/2, /*b=*/3);
  histogram_tester.ExpectBucketCount(
      name,
      ash::rgb_keyboard::metrics::RgbKeyboardBacklightChangeType::
          kStaticBackgroundColorChanged,
      1);
  manager_->SetRainbowMode();
  histogram_tester.ExpectBucketCount(
      name,
      ash::rgb_keyboard::metrics::RgbKeyboardBacklightChangeType::
          kRainbowModeSelected,
      1);
  manager_->SetZoneColor(/*zone=*/0, /*r=*/1, /*g=*/2, /*b=*/3);
  histogram_tester.ExpectBucketCount(
      name,
      ash::rgb_keyboard::metrics::RgbKeyboardBacklightChangeType::
          kStaticZoneColorChanged,
      1);
}

TEST_F(RgbKeyboardManagerTest, ZoneCountIsCorrect) {
  InitializeManagerWithCapability(
      rgbkbd::RgbKeyboardCapabilities::kIndividualKey);
  EXPECT_EQ(5, manager_->GetZoneCount());

  InitializeManagerWithCapability(
      rgbkbd::RgbKeyboardCapabilities::kFourZoneFortyLed);
  EXPECT_EQ(4, manager_->GetZoneCount());

  InitializeManagerWithCapability(
      rgbkbd::RgbKeyboardCapabilities::kFourZoneTwelveLed);
  EXPECT_EQ(4, manager_->GetZoneCount());

  InitializeManagerWithCapability(
      rgbkbd::RgbKeyboardCapabilities::kFourZoneFourLed);
  EXPECT_EQ(4, manager_->GetZoneCount());

  InitializeManagerWithCapability(rgbkbd::RgbKeyboardCapabilities::kNone);
  EXPECT_EQ(0, manager_->GetZoneCount());
}

TEST_F(RgbKeyboardManagerTest, SetStaticRgbValues) {
  const uint8_t expected_r = 1;
  const uint8_t expected_g = 2;
  const uint8_t expected_b = 3;

  manager_->SetStaticBackgroundColor(expected_r, expected_g, expected_b);
  const RgbColor& rgb_values = client_->recently_sent_rgb();

  EXPECT_EQ(expected_r, std::get<0>(rgb_values));
  EXPECT_EQ(expected_g, std::get<1>(rgb_values));
  EXPECT_EQ(expected_b, std::get<2>(rgb_values));
}

TEST_F(RgbKeyboardManagerTest, SetZoneRgbValues) {
  const int zone_1 = 0;
  const uint8_t expected_r_1 = 1;
  const uint8_t expected_g_1 = 2;
  const uint8_t expected_b_1 = 3;

  const int zone_2 = 3;
  const uint8_t expected_r_2 = 4;
  const uint8_t expected_g_2 = 5;
  const uint8_t expected_b_2 = 6;

  manager_->SetZoneColor(zone_1, expected_r_1, expected_g_1, expected_b_1);
  manager_->SetZoneColor(zone_2, expected_r_2, expected_g_2, expected_b_2);

  auto zone_colors = client_->get_zone_colors();

  EXPECT_EQ(2u, zone_colors.size());
  EXPECT_EQ(zone_colors[zone_1],
            std::make_tuple(expected_r_1, expected_g_1, expected_b_1));
  EXPECT_EQ(zone_colors[zone_2],
            std::make_tuple(expected_r_2, expected_g_2, expected_b_2));
}

TEST_F(RgbKeyboardManagerTest, SetInvalidZoneId) {
  const int invalid_zone = 100;
  const uint8_t expected_r = 1;
  const uint8_t expected_g = 2;
  const uint8_t expected_b = 3;

  manager_->SetZoneColor(invalid_zone, expected_r, expected_g, expected_b);
  auto zone_colors = client_->get_zone_colors();
  EXPECT_EQ(0u, zone_colors.size());

  const int valid_zone = 0;
  manager_->SetZoneColor(valid_zone, expected_r, expected_g, expected_b);
  zone_colors = client_->get_zone_colors();
  EXPECT_EQ(1u, zone_colors.size());
  EXPECT_EQ(zone_colors[valid_zone],
            std::make_tuple(expected_r, expected_g, expected_b));
}

TEST_F(RgbKeyboardManagerTest, SetRainbowMode) {
  EXPECT_FALSE(client_->is_rainbow_mode_set());

  manager_->SetRainbowMode();

  EXPECT_TRUE(client_->is_rainbow_mode_set());
}

TEST_F(RgbKeyboardManagerTest, RainbowModeResetsStatic) {
  EXPECT_FALSE(client_->is_rainbow_mode_set());

  const uint8_t expected_r = 1;
  const uint8_t expected_g = 2;
  const uint8_t expected_b = 3;

  manager_->SetStaticBackgroundColor(expected_r, expected_g, expected_b);
  const RgbColor& rgb_values = client_->recently_sent_rgb();

  EXPECT_EQ(expected_r, std::get<0>(rgb_values));
  EXPECT_EQ(expected_g, std::get<1>(rgb_values));
  EXPECT_EQ(expected_b, std::get<2>(rgb_values));

  manager_->SetRainbowMode();
  EXPECT_TRUE(client_->is_rainbow_mode_set());

  const RgbColor& updated_rgb_values = client_->recently_sent_rgb();
  EXPECT_EQ(0u, std::get<0>(updated_rgb_values));
  EXPECT_EQ(0u, std::get<1>(updated_rgb_values));
  EXPECT_EQ(0u, std::get<2>(updated_rgb_values));
}

TEST_F(RgbKeyboardManagerTest, StaticResetRainbowMode) {
  EXPECT_FALSE(client_->is_rainbow_mode_set());
  manager_->SetRainbowMode();
  EXPECT_TRUE(client_->is_rainbow_mode_set());

  const uint8_t expected_r = 1;
  const uint8_t expected_g = 2;
  const uint8_t expected_b = 3;

  manager_->SetStaticBackgroundColor(expected_r, expected_g, expected_b);
  const RgbColor& rgb_values = client_->recently_sent_rgb();

  EXPECT_FALSE(client_->is_rainbow_mode_set());

  EXPECT_EQ(expected_r, std::get<0>(rgb_values));
  EXPECT_EQ(expected_g, std::get<1>(rgb_values));
  EXPECT_EQ(expected_b, std::get<2>(rgb_values));
}

TEST_F(RgbKeyboardManagerTest, OnCapsLockChanged) {
  InitializeManagerWithCapability(
      rgbkbd::RgbKeyboardCapabilities::kIndividualKey);
  ime_controller_->UpdateCapsLockState(/*caps_enabled=*/true);
  EXPECT_TRUE(client_->get_caps_lock_state());
  ime_controller_->UpdateCapsLockState(/*caps_enabled=*/false);
  EXPECT_FALSE(client_->get_caps_lock_state());
}

TEST_F(RgbKeyboardManagerTest, OnLoginCapsLock) {
  // Simulate CapsLock enabled upon login.
  ime_controller_->SetCapsLockEnabled(/*caps_enabled=*/true);

  // Simulate RgbKeyboardManager starting up on login.
  InitializeManagerWithCapability(
      rgbkbd::RgbKeyboardCapabilities::kIndividualKey);
  EXPECT_TRUE(client_->get_caps_lock_state());
}

// TODO(jimmyxgong): This is just a stub test, there is only one enum available
// so just check num times the function has been called.
TEST_F(RgbKeyboardManagerTest, SetAnimationMode) {
  EXPECT_EQ(0, client_->animation_mode_call_count());

  manager_->SetAnimationMode(rgbkbd::RgbAnimationMode::kBasicTestPattern);

  EXPECT_EQ(1, client_->animation_mode_call_count());
}

TEST_F(RgbKeyboardManagerTest, SetCapsLockStateDisallowedForZonedKeyboards) {
  InitializeManagerWithCapability(
      rgbkbd::RgbKeyboardCapabilities::kFourZoneFortyLed);
  EXPECT_FALSE(client_->get_caps_lock_state());
  ime_controller_->UpdateCapsLockState(/*caps_enabled=*/true);
  // Caps lock state should still be false since RgbKeyboardManager should have
  // prevented the call to SetCapsLockState.
  EXPECT_FALSE(client_->get_caps_lock_state());
}

TEST_F(RgbKeyboardManagerTest, SetCapsLockStateAllowedForPerKeyKeboards) {
  InitializeManagerWithCapability(
      rgbkbd::RgbKeyboardCapabilities::kIndividualKey);
  EXPECT_FALSE(client_->get_caps_lock_state());
  ime_controller_->UpdateCapsLockState(/*caps_enabled=*/true);
  EXPECT_TRUE(client_->get_caps_lock_state());
}

// Following `RaceCondition*` tests verify that if a keyboard backlight color is
// set before `RgbKeyboardManager` is ready, that the color is set once
// `RgbKeyboardManager` is finally ready.
TEST_F(RgbKeyboardManagerTest, RaceConditionStaticSingleColor) {
  const uint8_t expected_r = 1;
  const uint8_t expected_g = 2;
  const uint8_t expected_b = 3;

  client_->set_should_run_rgb_keyboard_capabilities_callback(
      /*should_run_callback=*/false);
  InitializeManagerWithCapability(
      rgbkbd::RgbKeyboardCapabilities::kIndividualKey);

  manager_->SetStaticBackgroundColor(expected_r, expected_g, expected_b);
  {
    const RgbColor& rgb_values = client_->recently_sent_rgb();
    EXPECT_NE(expected_r, std::get<0>(rgb_values));
    EXPECT_NE(expected_g, std::get<1>(rgb_values));
    EXPECT_NE(expected_b, std::get<2>(rgb_values));
  }

  client_->set_should_run_rgb_keyboard_capabilities_callback(
      /*should_run_callback=*/true);
  client_->attempt_run_rgb_keyboard_capabilities_callback();
  {
    const RgbColor& rgb_values = client_->recently_sent_rgb();
    EXPECT_EQ(expected_r, std::get<0>(rgb_values));
    EXPECT_EQ(expected_g, std::get<1>(rgb_values));
    EXPECT_EQ(expected_b, std::get<2>(rgb_values));
  }
}

TEST_F(RgbKeyboardManagerTest, RaceConditionStaticZoneColor) {
  const int zone_1 = 0;
  const uint8_t expected_r_1 = 1;
  const uint8_t expected_g_1 = 2;
  const uint8_t expected_b_1 = 3;

  const int zone_2 = 3;
  const uint8_t expected_r_2 = 4;
  const uint8_t expected_g_2 = 5;
  const uint8_t expected_b_2 = 6;

  client_->set_should_run_rgb_keyboard_capabilities_callback(
      /*should_run_callback=*/false);
  InitializeManagerWithCapability(
      rgbkbd::RgbKeyboardCapabilities::kIndividualKey);

  manager_->SetZoneColor(zone_1, expected_r_1, expected_g_1, expected_b_1);
  manager_->SetZoneColor(zone_2, expected_r_2, expected_g_2, expected_b_2);

  {
    auto zone_colors = client_->get_zone_colors();
    EXPECT_EQ(0u, zone_colors.size());
  }

  client_->set_should_run_rgb_keyboard_capabilities_callback(
      /*should_run_callback=*/true);
  client_->attempt_run_rgb_keyboard_capabilities_callback();

  {
    auto zone_colors = client_->get_zone_colors();
    EXPECT_EQ(2u, zone_colors.size());
    EXPECT_EQ(zone_colors[zone_1],
              std::make_tuple(expected_r_1, expected_g_1, expected_b_1));
    EXPECT_EQ(zone_colors[zone_2],
              std::make_tuple(expected_r_2, expected_g_2, expected_b_2));
  }
}

TEST_F(RgbKeyboardManagerTest, RaceConditionStaticRainbow) {
  client_->set_should_run_rgb_keyboard_capabilities_callback(
      /*should_run_callback=*/false);
  InitializeManagerWithCapability(
      rgbkbd::RgbKeyboardCapabilities::kIndividualKey);

  manager_->SetRainbowMode();
  EXPECT_FALSE(client_->is_rainbow_mode_set());

  client_->set_should_run_rgb_keyboard_capabilities_callback(
      /*should_run_callback=*/true);
  client_->attempt_run_rgb_keyboard_capabilities_callback();
  EXPECT_TRUE(client_->is_rainbow_mode_set());
}

}  // namespace ash