chromium/ash/system/phonehub/onboarding_nudge_controller_unittest.cc

// Copyright 2023 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/system/phonehub/onboarding_nudge_controller.h"

#include <memory>

#include "ash/constants/ash_features.h"
#include "ash/session/test_session_controller_client.h"
#include "ash/shell.h"
#include "ash/system/phonehub/phone_hub_tray.h"
#include "ash/system/status_area_widget_test_helper.h"
#include "ash/test/ash_test_base.h"
#include "base/functional/callback_helpers.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/simple_test_clock.h"
#include "base/time/time.h"
#include "chromeos/ash/components/multidevice/remote_device_test_util.h"
#include "chromeos/ash/components/phonehub/fake_phone_hub_manager.h"
#include "chromeos/ash/components/phonehub/feature_status.h"
#include "chromeos/ash/components/phonehub/feature_status_provider.h"
#include "chromeos/ash/components/phonehub/pref_names.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/testing_pref_service.h"
#include "components/session_manager/session_manager_types.h"
#include "ui/views/view.h"
#include "ui/views/widget/widget.h"

namespace ash {
namespace {

const char kPhoneBluetoothAddress[] = "23:45:67:89:AB:CD";
constexpr auto kTestTime = base::Time::FromSecondsSinceUnixEpoch(100000);

}  // namespace

class OnboardingNudgeControllerTest : public AshTestBase {
 public:
  OnboardingNudgeControllerTest() = default;
  OnboardingNudgeControllerTest(const OnboardingNudgeControllerTest&) = delete;
  OnboardingNudgeControllerTest& operator=(
      const OnboardingNudgeControllerTest&) = delete;
  ~OnboardingNudgeControllerTest() override = default;

  // AshTestBase:
  void SetUp() override {
    AshTestBase::SetUp();
    test_clock_ = std::make_unique<base::SimpleTestClock>();
    widget_ = CreateFramelessTestWidget();
    test_clock_->SetNow(kTestTime);
    controller_ = std::make_unique<OnboardingNudgeController>(
        /*phone_hub_tray=*/widget_->SetContentsView(
            std::make_unique<views::View>()),
        /*stop_animation_callback=*/base::DoNothing(),
        /*start_animation_callback=*/base::DoNothing(), test_clock_.get());
  }

 protected:
  OnboardingNudgeController* GetController() { return controller_.get(); }

  multidevice::RemoteDeviceRef CreatePhoneDeviceWithUniqueInstanceId(
      bool supports_better_together_host,
      bool supports_phone_hub_host,
      bool has_bluetooth_address,
      std::string instance_id) {
    multidevice::RemoteDeviceRefBuilder builder;

    builder.SetSoftwareFeatureState(
        multidevice::SoftwareFeature::kBetterTogetherHost,
        supports_better_together_host
            ? multidevice::SoftwareFeatureState::kSupported
            : multidevice::SoftwareFeatureState::kNotSupported);
    builder.SetSoftwareFeatureState(
        multidevice::SoftwareFeature::kPhoneHubHost,
        supports_phone_hub_host
            ? multidevice::SoftwareFeatureState::kSupported
            : multidevice::SoftwareFeatureState::kNotSupported);
    builder.SetBluetoothPublicAddress(
        has_bluetooth_address ? kPhoneBluetoothAddress : std::string());
    builder.SetInstanceId(instance_id);
    return builder.Build();
  }

  void AdvanceClock(base::TimeDelta delta) { test_clock_->Advance(delta); }

  PrefService* pref_service() {
    return Shell::Get()->session_controller()->GetActivePrefService();
  }

  base::test::ScopedFeatureList feature_list_;
  base::HistogramTester histogram_tester_;

 private:
  std::unique_ptr<base::SimpleTestClock> test_clock_;
  std::unique_ptr<views::Widget> widget_;
  std::unique_ptr<OnboardingNudgeController> controller_;
};

TEST_F(OnboardingNudgeControllerTest, OnboardingNudgeControllerExists) {
  OnboardingNudgeController* controller = GetController();
  ASSERT_TRUE(controller);
}

TEST_F(OnboardingNudgeControllerTest, ShowNudge) {
  GetController()->ShowNudgeIfNeeded();
  EXPECT_EQ(pref_service()->GetInteger(
                OnboardingNudgeController::kPhoneHubNudgeTotalAppearances),
            1);
  EXPECT_EQ(pref_service()->GetTime(
                OnboardingNudgeController::kPhoneHubNudgeLastShownTime),
            kTestTime);
  histogram_tester_.ExpectTotalCount("MultiDeviceSetup.NudgeShown", 1);

  // Advance the clock by 5 minutes, should not show nuge again.
  AdvanceClock(base::Minutes(5));
  GetController()->ShowNudgeIfNeeded();
  EXPECT_EQ(pref_service()->GetInteger(
                OnboardingNudgeController::kPhoneHubNudgeTotalAppearances),
            1);
  EXPECT_EQ(pref_service()->GetTime(
                OnboardingNudgeController::kPhoneHubNudgeLastShownTime),
            kTestTime);

  // Advance the clock by 24 hours, should show nuge again.
  AdvanceClock(base::Hours(24));
  GetController()->ShowNudgeIfNeeded();
  EXPECT_EQ(pref_service()->GetInteger(
                OnboardingNudgeController::kPhoneHubNudgeTotalAppearances),
            2);
  EXPECT_EQ(pref_service()->GetTime(
                OnboardingNudgeController::kPhoneHubNudgeLastShownTime),
            kTestTime + base::Minutes(5) + base::Hours(24));
  histogram_tester_.ExpectTotalCount("MultiDeviceSetup.NudgeShown", 2);

  // Advance the clock by 24 hours, should show nuge again.
  AdvanceClock(base::Hours(24));
  GetController()->ShowNudgeIfNeeded();
  EXPECT_EQ(pref_service()->GetInteger(
                OnboardingNudgeController::kPhoneHubNudgeTotalAppearances),
            3);
  EXPECT_EQ(pref_service()->GetTime(
                OnboardingNudgeController::kPhoneHubNudgeLastShownTime),
            kTestTime + base::Minutes(5) + base::Hours(24) + base::Hours(24));
  histogram_tester_.ExpectTotalCount("MultiDeviceSetup.NudgeShown", 3);

  // Should not show nudge again since the total appearances reach 3 times.
  AdvanceClock(base::Hours(24));
  GetController()->ShowNudgeIfNeeded();
  EXPECT_EQ(pref_service()->GetInteger(
                OnboardingNudgeController::kPhoneHubNudgeTotalAppearances),
            3);
  EXPECT_EQ(pref_service()->GetTime(
                OnboardingNudgeController::kPhoneHubNudgeLastShownTime),
            kTestTime + base::Minutes(5) + base::Hours(24) + base::Hours(24));
}

TEST_F(OnboardingNudgeControllerTest, HoverNudge) {
  GetController()->OnNudgeHoverStateChanged(false);
  EXPECT_TRUE(
      pref_service()
          ->GetTime(OnboardingNudgeController::kPhoneHubNudgeLastActionTime)
          .is_null());

  GetController()->OnNudgeHoverStateChanged(true);
  EXPECT_EQ(pref_service()->GetTime(
                OnboardingNudgeController::kPhoneHubNudgeLastActionTime),
            kTestTime);
}

TEST_F(OnboardingNudgeControllerTest, ClickNudgeAndPhoneHubIcon) {
  GetController()->ShowNudgeIfNeeded();

  AdvanceClock(base::Seconds(3));
  GetController()->OnNudgeClicked();
  EXPECT_EQ(pref_service()->GetTime(
                OnboardingNudgeController::kPhoneHubNudgeLastActionTime),
            kTestTime + base::Seconds(3));
  EXPECT_EQ(pref_service()->GetTime(
                OnboardingNudgeController::kPhoneHubNudgeLastClickTime),
            kTestTime + base::Seconds(3));

  // Simulate click on Phone Hub icon.
  GetController()->HideNudge();
  histogram_tester_.ExpectTimeBucketCount(
      "MultiDeviceSetup.NudgeActionDuration", base::Seconds(3), 1);
  histogram_tester_.ExpectBucketCount(
      "MultiDeviceSetup.NudgeShownTimesBeforeActed", 1, 1);
  histogram_tester_.ExpectTotalCount("MultiDeviceSetup.NudgeInteracted", 2);
  histogram_tester_.ExpectBucketCount(
      "MultiDeviceSetup.NudgeInteracted",
      phone_hub_metrics::MultideviceSetupNudgeInteraction::kNudgeClicked, 1);
  histogram_tester_.ExpectBucketCount(
      "MultiDeviceSetup.NudgeInteracted",
      phone_hub_metrics::MultideviceSetupNudgeInteraction::kPhoneHubIconClicked,
      1);

  AdvanceClock(base::Hours(24));
  GetController()->ShowNudgeIfNeeded();
  // Nudge has been clicked, would not show again.
  EXPECT_EQ(pref_service()->GetInteger(
                OnboardingNudgeController::kPhoneHubNudgeTotalAppearances),
            1);
  EXPECT_EQ(pref_service()->GetTime(
                OnboardingNudgeController::kPhoneHubNudgeLastShownTime),
            kTestTime);
}

TEST_F(OnboardingNudgeControllerTest, ClickPhoneHubIcon) {
  GetController()->ShowNudgeIfNeeded();

  AdvanceClock(base::Seconds(3));
  // Simulate Phone Hub icon clicked.
  GetController()->HideNudge();
  EXPECT_EQ(pref_service()->GetTime(
                OnboardingNudgeController::kPhoneHubNudgeLastActionTime),
            kTestTime + base::Seconds(3));
  EXPECT_EQ(pref_service()->GetTime(
                OnboardingNudgeController::kPhoneHubNudgeLastClickTime),
            kTestTime + base::Seconds(3));

  histogram_tester_.ExpectTimeBucketCount(
      "MultiDeviceSetup.NudgeActionDuration", base::Seconds(3), 1);
  histogram_tester_.ExpectBucketCount(
      "MultiDeviceSetup.NudgeShownTimesBeforeActed", 1, 1);
  histogram_tester_.ExpectTotalCount("MultiDeviceSetup.NudgeInteracted", 1);
  histogram_tester_.ExpectBucketCount(
      "MultiDeviceSetup.NudgeInteracted",
      phone_hub_metrics::MultideviceSetupNudgeInteraction::kNudgeClicked, 0);
  histogram_tester_.ExpectBucketCount(
      "MultiDeviceSetup.NudgeInteracted",
      phone_hub_metrics::MultideviceSetupNudgeInteraction::kPhoneHubIconClicked,
      1);
}

TEST_F(OnboardingNudgeControllerTest,
       AddToSyncedDeviceListWhenNewEligibleDeviceFound) {
  multidevice::RemoteDeviceRef device_1 = CreatePhoneDeviceWithUniqueInstanceId(
      /*supports_better_together_host=*/true,
      /*supports_phone_hub_host=*/true,
      /*has_bluetooth_address=*/true, "AAA");
  GetController()->OnEligiblePhoneHubHostFound({device_1});
  EXPECT_EQ(
      pref_service()->GetList(OnboardingNudgeController::kSyncedDevices).size(),
      1u);

  GetController()->ShowNudgeIfNeeded();
  EXPECT_EQ(pref_service()->GetInteger(
                OnboardingNudgeController::kPhoneHubNudgeTotalAppearances),
            1);

  AdvanceClock(base::Hours(24));
  GetController()->ShowNudgeIfNeeded();
  EXPECT_EQ(pref_service()->GetInteger(
                OnboardingNudgeController::kPhoneHubNudgeTotalAppearances),
            2);

  AdvanceClock(base::Hours(24));
  GetController()->ShowNudgeIfNeeded();
  EXPECT_EQ(pref_service()->GetInteger(
                OnboardingNudgeController::kPhoneHubNudgeTotalAppearances),
            3);

  AdvanceClock(base::Hours(24));
  GetController()->ShowNudgeIfNeeded();
  EXPECT_EQ(pref_service()->GetInteger(
                OnboardingNudgeController::kPhoneHubNudgeTotalAppearances),
            3);

  // Create eligible second device.
  multidevice::RemoteDeviceRef device_2 = CreatePhoneDeviceWithUniqueInstanceId(
      /*supports_better_together_host=*/true,
      /*supports_phone_hub_host=*/true,
      /*has_bluetooth_address=*/true, "AAB");

  GetController()->OnEligiblePhoneHubHostFound({device_1, device_2});
  EXPECT_EQ(pref_service()->GetInteger(
                OnboardingNudgeController::kPhoneHubNudgeTotalAppearances),
            0);
  EXPECT_TRUE(
      pref_service()
          ->GetTime(OnboardingNudgeController::kPhoneHubNudgeLastShownTime)
          .is_null());
  EXPECT_TRUE(
      pref_service()
          ->GetTime(OnboardingNudgeController::kPhoneHubNudgeLastActionTime)
          .is_null());
  EXPECT_TRUE(
      pref_service()
          ->GetTime(OnboardingNudgeController::kPhoneHubNudgeLastClickTime)
          .is_null());
  EXPECT_EQ(
      pref_service()->GetList(OnboardingNudgeController::kSyncedDevices).size(),
      2u);

  GetController()->ShowNudgeIfNeeded();
  EXPECT_EQ(pref_service()->GetInteger(
                OnboardingNudgeController::kPhoneHubNudgeTotalAppearances),
            1);
}

TEST_F(OnboardingNudgeControllerTest,
       DoNotAddToSyncedDeviceListIfAlreadyFound) {
  multidevice::RemoteDeviceRef device_1 = CreatePhoneDeviceWithUniqueInstanceId(
      /*supports_better_together_host=*/true,
      /*supports_phone_hub_host=*/true,
      /*has_bluetooth_address=*/true, "AAA");
  GetController()->OnEligiblePhoneHubHostFound({device_1});
  EXPECT_EQ(
      pref_service()->GetList(OnboardingNudgeController::kSyncedDevices).size(),
      1u);

  GetController()->ShowNudgeIfNeeded();
  EXPECT_EQ(pref_service()->GetInteger(
                OnboardingNudgeController::kPhoneHubNudgeTotalAppearances),
            1);

  // Synced device list should remain the same size.
  GetController()->OnEligiblePhoneHubHostFound({device_1});
  // Pref value is not reset if host list remains unchanged.
  EXPECT_EQ(pref_service()->GetInteger(
                OnboardingNudgeController::kPhoneHubNudgeTotalAppearances),
            1);
  EXPECT_EQ(
      pref_service()->GetList(OnboardingNudgeController::kSyncedDevices).size(),
      1u);
}
}  // namespace ash