chromium/chrome/browser/ui/webui/ash/settings/pages/bluetooth/fast_pair_saved_devices_handler_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 "chrome/browser/ui/webui/ash/settings/pages/bluetooth/fast_pair_saved_devices_handler.h"

#include "ash/quick_pair/common/logging.h"
#include "ash/quick_pair/common/mock_quick_pair_browser_delegate.h"
#include "ash/quick_pair/proto/fastpair.pb.h"
#include "ash/quick_pair/proto/fastpair_data.pb.h"
#include "ash/quick_pair/repository/fake_fast_pair_repository.h"
#include "ash/quick_pair/repository/fast_pair/mock_fast_pair_image_decoder.h"
#include "ash/quick_pair/repository/fast_pair_repository.h"
#include "base/base64.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
#include "base/task/thread_pool.h"
#include "base/task/thread_pool/thread_pool_instance.h"
#include "base/test/bind.h"
#include "base/test/gmock_callback_support.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/mock_callback.h"
#include "base/test/task_environment.h"
#include "content/public/test/test_web_ui.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/gfx/image/image_unittest_util.h"

namespace ash::settings {

namespace {

const char kLoadSavedDevicePage[] = "loadSavedDevicePage";
const char kRemoveSavedDevice[] = "removeSavedDevice";
const char kOptInStatusMessage[] = "fast-pair-saved-devices-opt-in-status";
const char kSavedDevicesListMessage[] = "fast-pair-saved-devices-list";

const char kSavedDeviceNameKey[] = "name";
const char kSavedDeviceImageUrlKey[] = "imageUrl";
const char kSavedDeviceAccountKeyKey[] = "accountKey";

const char kDisplayUrlBase64[] =
    "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/"
    "gAIDAAAA5klEQVR4nO3QQQkAIADAQLV/"
    "Z63gXiLcJRibYw8urdcBPzErMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMC"
    "swKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCsw"
    "KzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKz"
    "ArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCswKzArMCs4iV8Bx6UARfcA"
    "AAAASUVORK5CYII=";

const char kDeviceName1[] = "I16max";
const char kImageBytes1[] = "01010101001010101010101010101";
const std::vector<uint8_t> kAccountKey1 = {0xA0, 0xBA, 0xF0, 0xBB, 0x95, 0x1F,
                                           0xF7, 0xB6, 0xCF, 0x5E, 0x3F, 0x45,
                                           0x61, 0xC3, 0x32, 0x1D};
const char kDeviceName2[] = "JBL Flip 6";
const char kImageBytes2[] = "111110101001010101001010101111";
const std::vector<uint8_t> kAccountKey2 = {0xA1, 0xBA, 0xF0, 0xBB, 0x95, 0x1F,
                                           0xF7, 0xB6, 0xCD, 0x5E, 0x3F, 0x45,
                                           0x61, 0xC3, 0x32, 0x1D};
const char kDeviceName3[] = "Pixel Buds";
const char kImageBytes3[] = "00000010101100110101010010101001";
const std::vector<uint8_t> kAccountKey3 = {0xA6, 0xB0, 0xF0, 0xBB, 0x95, 0x1F,
                                           0xF7, 0xB6, 0xCD, 0x5E, 0x3F, 0x45,
                                           0x68, 0xC3, 0x32, 0x1D};
const char kDeviceName4[] = "Wyze Buds";
const char kImageBytes4[] = "11111000101010010101";
const std::vector<uint8_t> kAccountKey4 = {0xB0, 0xB6, 0xF0, 0xBB, 0x95, 0x1F,
                                           0xF7, 0xB6, 0xCF, 0x5E, 0x3F, 0x45,
                                           0x61, 0xC3, 0x32, 0x1D};

const char kDeviceName5[] = "B&O Beoplay E6";
const char kImageBytes5[] = "110000100010000100010100000001001000100001";
const std::vector<uint8_t> kAccountKey5 = {0xC0, 0xC6, 0xD0, 0xBB, 0x95, 0x1F,
                                           0xF7, 0xB6, 0xCF, 0x5E, 0x3F, 0x45,
                                           0x61, 0xC3, 0x32, 0x1D};

const char kDeviceName6[] = "LG HBS-830";
const char kImageBytes6[] = "11110100011111111010100101001010100101010011";
const std::vector<uint8_t> kAccountKey6 = {0xB5, 0xB6, 0xF0, 0xBB, 0x95, 0x1F,
                                           0xF7, 0xB8, 0xCF, 0x5E, 0x3F, 0x45,
                                           0x61, 0xC3, 0x36, 0x1D};

const char kSavedDeviceRemoveResultMetricName[] =
    "Bluetooth.ChromeOS.FastPair.SavedDevices.Remove.Result";
const char kSavedDevicesTotalUxLoadTimeMetricName[] =
    "Bluetooth.ChromeOS.FastPair.SavedDevices.TotalUxLoadTime";
const char kSavedDevicesCountMetricName[] =
    "Bluetooth.ChromeOS.FastPair.SavedDevices.DeviceCount";

nearby::fastpair::FastPairDevice CreateFastPairDevice(
    const std::string device_name,
    const std::string image_bytes,
    const std::vector<uint8_t>& account_key) {
  nearby::fastpair::StoredDiscoveryItem item;
  item.set_title(device_name);
  item.set_icon_png(image_bytes);

  nearby::fastpair::FastPairDevice device;
  device.set_account_key(std::string(account_key.begin(), account_key.end()));

  std::string item_str;
  item.SerializeToString(&item_str);
  device.set_discovery_item_bytes(item_str);
  return device;
}

std::string EncodeKey(const std::vector<uint8_t>& decoded_key) {
  return base::HexEncode(decoded_key);
}

void AssertDeviceInList(const base::Value& device,
                        const std::string& expected_device_name,
                        const std::string& expected_base64_image_url,
                        const std::vector<uint8_t> expected_account_key) {
  const auto* device_dict = device.GetIfDict();

  const base::Value* device_name = device_dict->Find(kSavedDeviceNameKey);
  const base::Value* device_url = device_dict->Find(kSavedDeviceImageUrlKey);
  const base::Value* device_account_key =
      device_dict->Find(kSavedDeviceAccountKeyKey);

  ASSERT_EQ(expected_device_name, device_name->GetString());
  ASSERT_EQ(expected_base64_image_url, device_url->GetString());
  ASSERT_EQ(EncodeKey(expected_account_key), device_account_key->GetString());
}

class TestFastPairSavedDevicesHandler : public FastPairSavedDevicesHandler {
 public:
  explicit TestFastPairSavedDevicesHandler(
      std::unique_ptr<quick_pair::FastPairImageDecoder> image_decoder)
      : FastPairSavedDevicesHandler(std::move(image_decoder)) {}
  ~TestFastPairSavedDevicesHandler() override = default;

  // Make public for testing.
  using FastPairSavedDevicesHandler::AllowJavascript;
  using FastPairSavedDevicesHandler::RegisterMessages;
  using FastPairSavedDevicesHandler::set_web_ui;
};

}  // namespace

class FastPairSavedDevicesHandlerTest : public testing::Test {
 public:
  FastPairSavedDevicesHandlerTest() = default;
  FastPairSavedDevicesHandlerTest(const FastPairSavedDevicesHandlerTest&) =
      delete;
  FastPairSavedDevicesHandlerTest& operator=(
      const FastPairSavedDevicesHandlerTest&) = delete;
  ~FastPairSavedDevicesHandlerTest() override = default;

  // testing::Test:
  void SetUp() override {
    test_web_ui_ = std::make_unique<content::TestWebUI>();

    test_image_ = gfx::test::CreateImage(100, 100);
    auto mock_decoder =
        std::make_unique<quick_pair::MockFastPairImageDecoder>();
    mock_decoder_ = mock_decoder.get();
    // On call to DecodeImage, run the third argument callback with test_image_.
    ON_CALL(*mock_decoder, DecodeImage(testing::_, testing::_, testing::_))
        .WillByDefault(base::test::RunOnceCallbackRepeatedly<2>(test_image_));

    handler_ = std::make_unique<TestFastPairSavedDevicesHandler>(
        std::move(mock_decoder));
    handler_->set_web_ui(test_web_ui_.get());
    handler_->RegisterMessages();
    fast_pair_repository_ =
        std::make_unique<quick_pair::FakeFastPairRepository>();
  }

  content::TestWebUI* test_web_ui() { return test_web_ui_.get(); }

  void InitializeSavedDevicesList(const std::string& device_name1,
                                  const std::string& device_image_bytes1,
                                  const std::vector<uint8_t>& account_key1,
                                  const std::string& device_name2,
                                  const std::string& device_image_bytes2,
                                  const std::vector<uint8_t>& account_key2,
                                  const std::string& device_name3,
                                  const std::string& device_image_bytes3,
                                  const std::vector<uint8_t>& account_key3,
                                  nearby::fastpair::OptInStatus opt_in_status) {
    std::vector<nearby::fastpair::FastPairDevice> devices{
        CreateFastPairDevice(/*device_name=*/device_name1,
                             /*image_bytes=*/device_image_bytes1,
                             /*account_key=*/account_key1),
        CreateFastPairDevice(/*device_name=*/device_name2,
                             /*image_bytes=*/device_image_bytes2,
                             /*account_key=*/account_key2),
        CreateFastPairDevice(/*device_name=*/device_name3,
                             /*image_bytes=*/device_image_bytes3,
                             /*account_key=*/account_key3)};
    fast_pair_repository_->SetSavedDevices(
        /*status=*/opt_in_status,
        /*devices=*/std::move(devices));
  }

  void VerifySavedDevicesList(
      const content::TestWebUI::CallData& saved_devices_list_call_data,
      const std::string& device_name1,
      const std::string& expected_device_url1,
      const std::vector<uint8_t>& account_key1,
      const std::string& device_name2,
      const std::string& expected_device_url2,
      const std::vector<uint8_t>& account_key2,
      const std::string& device_name3,
      const std::string& expected_device_url3,
      const std::vector<uint8_t>& account_key3) {
    // The call is structured such that the first argument is the name of the
    // first message being sent, and the second argument is the name of the
    // second message being sent.
    ASSERT_EQ(kSavedDevicesListMessage,
              saved_devices_list_call_data.arg1()->GetString());

    // Size should be in sync with size of devices list created in
    // |InitializeSavedDevicesList|
    const base::Value::List* saved_devices_list =
        saved_devices_list_call_data.arg2()->GetIfList();
    ASSERT_EQ(3u, saved_devices_list->size());

    AssertDeviceInList(/*device=*/*(saved_devices_list->begin()),
                       /*expected_device_name=*/device_name1,
                       /*expected_base64_image_url=*/expected_device_url1,
                       /*expected_account_key=*/account_key1);
    AssertDeviceInList(/*device=*/*(saved_devices_list->begin() + 1),
                       /*expected_device_name=*/device_name2,
                       /*expected_base64_image_url=*/expected_device_url2,
                       /*expected_account_key=*/account_key2);
    AssertDeviceInList(/*device=*/*(saved_devices_list->begin() + 2),
                       /*expected_device_name=*/device_name3,
                       /*expected_base64_image_url=*/expected_device_url3,
                       /*expected_account_key=*/account_key3);
  }

  void VerifyEmptySavedDevicesList(
      const content::TestWebUI::CallData& saved_devices_list_call_data) {
    ASSERT_EQ(kSavedDevicesListMessage,
              saved_devices_list_call_data.arg1()->GetString());
    const base::Value::List* saved_devices_list =
        saved_devices_list_call_data.arg2()->GetIfList();
    ASSERT_TRUE(saved_devices_list->empty());
  }

  void VerifyOptInStatus(
      const content::TestWebUI::CallData& opt_in_status_call_data,
      nearby::fastpair::OptInStatus opt_in_status) {
    // The call is structured such that the first argument is the name of the
    // first message being sent, and the second argument is the name of the
    // second message being sent.
    ASSERT_EQ(kOptInStatusMessage, opt_in_status_call_data.arg1()->GetString());
    ASSERT_EQ(static_cast<int>(opt_in_status),
              opt_in_status_call_data.arg2()->GetInt());
  }

  void LoadPage() {
    // `HandleReceivedMessages` has to use a Value::List due to the API.
    base::Value::List args;
    test_web_ui()->HandleReceivedMessage(kLoadSavedDevicePage, args);
  }

  void RemoveDevice(const std::vector<uint8_t>& account_key) {
    // `HandleReceivedMessages` has to use a Value::List due to the API.
    base::Value::List args;
    args.Append(EncodeKey(account_key));
    test_web_ui()->HandleReceivedMessage(kRemoveSavedDevice, args);
  }

  base::HistogramTester& histogram_tester() { return histogram_tester_; }

 protected:
  base::test::TaskEnvironment task_environment_;
  base::HistogramTester histogram_tester_;
  std::unique_ptr<quick_pair::FakeFastPairRepository> fast_pair_repository_;
  gfx::Image test_image_;
  raw_ptr<quick_pair::MockFastPairImageDecoder, DanglingUntriaged>
      mock_decoder_;
  std::unique_ptr<content::TestWebUI> test_web_ui_;
  std::unique_ptr<TestFastPairSavedDevicesHandler> handler_;
  base::WeakPtrFactory<FastPairSavedDevicesHandlerTest> weak_ptr_factory_{this};
};

TEST_F(FastPairSavedDevicesHandlerTest, GetSavedDevices) {
  histogram_tester().ExpectTotalCount(kSavedDevicesTotalUxLoadTimeMetricName,
                                      0);
  InitializeSavedDevicesList(
      /*device_name1=*/kDeviceName1, /*device_image_bytes1=*/kImageBytes1,
      /*account_key1=*/kAccountKey1, /*device_name2=*/kDeviceName2,
      /*device_image_bytes2=*/kImageBytes2, /*account_key2=*/kAccountKey2,
      /*device_name3=*/kDeviceName3, /*device_image_bytes3=*/kImageBytes3,
      /*account_key3=*/kAccountKey3,
      /*opt_in_status=*/nearby::fastpair::OptInStatus::STATUS_OPTED_IN);
  LoadPage();
  base::RunLoop().RunUntilIdle();

  EXPECT_EQ(2u, test_web_ui()->call_data().size());
  VerifyOptInStatus(*test_web_ui()->call_data()[0],
                    nearby::fastpair::OptInStatus::STATUS_OPTED_IN);

  // We mock the image decoder to return the same test image, which is why
  // the base64 encoded images are all the same here.
  VerifySavedDevicesList(
      *test_web_ui()->call_data()[1], /*device_name1=*/kDeviceName1,
      /*expected_device_url1=*/kDisplayUrlBase64, /*account_key1=*/kAccountKey1,
      /*device_name2=*/kDeviceName2, /*expected_device_url2=*/kDisplayUrlBase64,
      /*account_key2=*/kAccountKey2, /*device_name3=*/kDeviceName3,
      /*expected_device_url3=*/kDisplayUrlBase64,
      /*account_key3=*/kAccountKey3);
  histogram_tester().ExpectTotalCount(kSavedDevicesTotalUxLoadTimeMetricName,
                                      1);
}

// TODO(crbug.com/1362118): Fix flaky test.
TEST_F(FastPairSavedDevicesHandlerTest, DISABLED_ReloadBeforePageLoadsIgnored) {
  histogram_tester().ExpectTotalCount(kSavedDevicesTotalUxLoadTimeMetricName,
                                      0);
  InitializeSavedDevicesList(
      /*device_name1=*/kDeviceName1, /*device_image_bytes1=*/kImageBytes1,
      /*account_key1=*/kAccountKey1, /*device_name2=*/kDeviceName2,
      /*device_image_bytes2=*/kImageBytes2, /*account_key2=*/kAccountKey2,
      /*device_name3=*/kDeviceName3, /*device_image_bytes3=*/kImageBytes3,
      /*account_key3=*/kAccountKey3,
      /*opt_in_status=*/nearby::fastpair::OptInStatus::STATUS_OPTED_IN);

  // Simulate two page loads at the same time.
  base::ThreadPool::PostTask(
      FROM_HERE, {base::MayBlock()},
      base::BindOnce(&FastPairSavedDevicesHandlerTest::LoadPage,
                     weak_ptr_factory_.GetWeakPtr()));
  LoadPage();
  base::ThreadPoolInstance::Get()->FlushForTesting();

  // If we load the page while we are already fetching and decoding saved
  // device data, we should ignore the second load attempt and it should
  // behave like if only one call was made.
  EXPECT_EQ(2u, test_web_ui()->call_data().size());
  VerifyOptInStatus(*test_web_ui()->call_data()[0],
                    nearby::fastpair::OptInStatus::STATUS_OPTED_IN);
  VerifySavedDevicesList(
      *test_web_ui()->call_data()[1], /*device_name1=*/kDeviceName1,
      /*expected_device_url1=*/kDisplayUrlBase64, /*account_key1=*/kAccountKey1,
      /*device_name2=*/kDeviceName2, /*expected_device_url2=*/kDisplayUrlBase64,
      /*account_key2=*/kAccountKey2, /*device_name3=*/kDeviceName3,
      /*expected_device_url3=*/kDisplayUrlBase64,
      /*account_key3=*/kAccountKey3);
  histogram_tester().ExpectTotalCount(kSavedDevicesTotalUxLoadTimeMetricName,
                                      1);
}

TEST_F(FastPairSavedDevicesHandlerTest, ReloadAfterPageLoads) {
  histogram_tester().ExpectTotalCount(kSavedDevicesTotalUxLoadTimeMetricName,
                                      0);
  InitializeSavedDevicesList(
      /*device_name1=*/kDeviceName1, /*device_image_bytes1=*/kImageBytes1,
      /*account_key1=*/kAccountKey1, /*device_name2=*/kDeviceName2,
      /*device_image_bytes2=*/kImageBytes2, /*account_key2=*/kAccountKey2,
      /*device_name3=*/kDeviceName3, /*device_image_bytes3=*/kImageBytes3,
      /*account_key3=*/kAccountKey3,
      /*opt_in_status=*/nearby::fastpair::OptInStatus::STATUS_OPTED_IN);
  LoadPage();
  base::RunLoop().RunUntilIdle();

  // We only expect two calls at this point: one for the opt in status, one
  // for the saved device list.
  EXPECT_EQ(2u, test_web_ui()->call_data().size());
  VerifyOptInStatus(*test_web_ui()->call_data()[0],
                    nearby::fastpair::OptInStatus::STATUS_OPTED_IN);
  VerifySavedDevicesList(
      *test_web_ui()->call_data()[1], /*device_name1=*/kDeviceName1,
      /*expected_device_url1=*/kDisplayUrlBase64, /*account_key1=*/kAccountKey1,
      /*device_name2=*/kDeviceName2, /*expected_device_url2=*/kDisplayUrlBase64,
      /*account_key2=*/kAccountKey2, /*device_name3=*/kDeviceName3,
      /*expected_device_url3=*/kDisplayUrlBase64,
      /*account_key3=*/kAccountKey3);
  histogram_tester().ExpectTotalCount(kSavedDevicesTotalUxLoadTimeMetricName,
                                      1);

  LoadPage();
  base::RunLoop().RunUntilIdle();

  // We expect now the WebUI to have handled four calls: two opt in status,
  // two for saved device list.
  EXPECT_EQ(4u, test_web_ui()->call_data().size());
  VerifyOptInStatus(*test_web_ui()->call_data()[0],
                    nearby::fastpair::OptInStatus::STATUS_OPTED_IN);
  VerifySavedDevicesList(
      *test_web_ui()->call_data()[1], /*device_name1=*/kDeviceName1,
      /*expected_device_url1=*/kDisplayUrlBase64, /*account_key1=*/kAccountKey1,
      /*device_name2=*/kDeviceName2, /*expected_device_url2=*/kDisplayUrlBase64,
      /*account_key2=*/kAccountKey2, /*device_name3=*/kDeviceName3,
      /*expected_device_url3=*/kDisplayUrlBase64,
      /*account_key3=*/kAccountKey3);
  VerifyOptInStatus(*test_web_ui()->call_data()[2],
                    nearby::fastpair::OptInStatus::STATUS_OPTED_IN);
  VerifySavedDevicesList(
      *test_web_ui()->call_data()[1], /*device_name1=*/kDeviceName1,
      /*expected_device_url1=*/kDisplayUrlBase64, /*account_key1=*/kAccountKey1,
      /*device_name2=*/kDeviceName2, /*expected_device_url2=*/kDisplayUrlBase64,
      /*account_key2=*/kAccountKey2, /*device_name3=*/kDeviceName3,
      /*expected_device_url3=*/kDisplayUrlBase64,
      /*account_key3=*/kAccountKey3);
  histogram_tester().ExpectTotalCount(kSavedDevicesTotalUxLoadTimeMetricName,
                                      2);
}

TEST_F(FastPairSavedDevicesHandlerTest, EmptyListSentToWebUi) {
  histogram_tester().ExpectTotalCount(kSavedDevicesTotalUxLoadTimeMetricName,
                                      0);
  histogram_tester().ExpectBucketCount(kSavedDevicesCountMetricName,
                                       /*num_devices=*/0, 0);
  fast_pair_repository_->SetSavedDevices(
      /*status=*/nearby::fastpair::OptInStatus::STATUS_OPTED_OUT,
      /*devices=*/{});
  LoadPage();
  base::RunLoop().RunUntilIdle();

  EXPECT_EQ(2u, test_web_ui()->call_data().size());
  VerifyOptInStatus(*test_web_ui()->call_data()[0],
                    nearby::fastpair::OptInStatus::STATUS_OPTED_OUT);
  VerifyEmptySavedDevicesList(*test_web_ui()->call_data()[1]);
  histogram_tester().ExpectTotalCount(kSavedDevicesTotalUxLoadTimeMetricName,
                                      1);
  histogram_tester().ExpectBucketCount(kSavedDevicesCountMetricName,
                                       /*num_devices=*/0, 1);
}

TEST_F(FastPairSavedDevicesHandlerTest, SavedDevicesBecomesEmpty) {
  histogram_tester().ExpectTotalCount(kSavedDevicesTotalUxLoadTimeMetricName,
                                      0);
  histogram_tester().ExpectBucketCount(kSavedDevicesCountMetricName,
                                       /*num_devices=*/0, 0);
  InitializeSavedDevicesList(
      /*device_name1=*/kDeviceName1, /*device_image_bytes1=*/kImageBytes1,
      /*account_key1=*/kAccountKey1, /*device_name2=*/kDeviceName2,
      /*device_image_bytes2=*/kImageBytes2, /*account_key2=*/kAccountKey2,
      /*device_name3=*/kDeviceName3, /*device_image_bytes3=*/kImageBytes3,
      /*account_key3=*/kAccountKey3,
      /*opt_in_status=*/nearby::fastpair::OptInStatus::STATUS_OPTED_IN);
  LoadPage();
  base::RunLoop().RunUntilIdle();

  EXPECT_EQ(2u, test_web_ui()->call_data().size());
  VerifyOptInStatus(*test_web_ui()->call_data()[0],
                    nearby::fastpair::OptInStatus::STATUS_OPTED_IN);
  VerifySavedDevicesList(
      *test_web_ui()->call_data()[1], /*device_name1=*/kDeviceName1,
      /*expected_device_url1=*/kDisplayUrlBase64, /*account_key1=*/kAccountKey1,
      /*device_name2=*/kDeviceName2, /*expected_device_url2=*/kDisplayUrlBase64,
      /*account_key2=*/kAccountKey2, /*device_name3=*/kDeviceName3,
      /*expected_device_url3=*/kDisplayUrlBase64,
      /*account_key3=*/kAccountKey3);
  histogram_tester().ExpectTotalCount(kSavedDevicesTotalUxLoadTimeMetricName,
                                      1);
  histogram_tester().ExpectBucketCount(kSavedDevicesCountMetricName,
                                       /*num_devices=*/0, 0);

  fast_pair_repository_->SetSavedDevices(
      /*status=*/nearby::fastpair::OptInStatus::STATUS_OPTED_OUT,
      /*devices=*/{});
  LoadPage();
  base::RunLoop().RunUntilIdle();

  // At this point, we have had four total calls handled by our webui, but now
  // we should have the data reflect any empty list.
  EXPECT_EQ(4u, test_web_ui()->call_data().size());
  VerifyOptInStatus(*test_web_ui()->call_data()[2],
                    nearby::fastpair::OptInStatus::STATUS_OPTED_OUT);
  VerifyEmptySavedDevicesList(*test_web_ui()->call_data()[3]);
  histogram_tester().ExpectTotalCount(kSavedDevicesTotalUxLoadTimeMetricName,
                                      2);
  histogram_tester().ExpectBucketCount(kSavedDevicesCountMetricName,
                                       /*num_devices=*/0, 1);
}

TEST_F(FastPairSavedDevicesHandlerTest, SavedDevicesChanges) {
  histogram_tester().ExpectTotalCount(kSavedDevicesTotalUxLoadTimeMetricName,
                                      0);
  InitializeSavedDevicesList(
      /*device_name1=*/kDeviceName1, /*device_image_bytes1=*/kImageBytes1,
      /*account_key1=*/kAccountKey1, /*device_name2=*/kDeviceName2,
      /*device_image_bytes2=*/kImageBytes2, /*account_key2=*/kAccountKey2,
      /*device_name3=*/kDeviceName3, /*device_image_bytes3=*/kImageBytes3,
      /*account_key3=*/kAccountKey3,
      /*opt_in_status=*/nearby::fastpair::OptInStatus::STATUS_OPTED_IN);
  LoadPage();
  base::RunLoop().RunUntilIdle();

  EXPECT_EQ(2u, test_web_ui()->call_data().size());
  VerifyOptInStatus(*test_web_ui()->call_data()[0],
                    nearby::fastpair::OptInStatus::STATUS_OPTED_IN);
  VerifySavedDevicesList(
      *test_web_ui()->call_data()[1], /*device_name1=*/kDeviceName1,
      /*expected_device_url1=*/kDisplayUrlBase64, /*account_key1=*/kAccountKey1,
      /*device_name2=*/kDeviceName2, /*expected_device_url2=*/kDisplayUrlBase64,
      /*account_key2=*/kAccountKey2, /*device_name3=*/kDeviceName3,
      /*expected_device_url3=*/kDisplayUrlBase64,
      /*account_key3=*/kAccountKey3);
  histogram_tester().ExpectTotalCount(kSavedDevicesTotalUxLoadTimeMetricName,
                                      1);

  InitializeSavedDevicesList(
      /*device_name1=*/kDeviceName4, /*device_image_bytes1=*/kImageBytes4,
      /*account_key1=*/kAccountKey4, /*device_name2=*/kDeviceName5,
      /*device_image_bytes2=*/kImageBytes5, /*account_key2=*/kAccountKey5,
      /*device_name3=*/kDeviceName6, /*device_image_bytes3=*/kImageBytes6,
      /*account_key3=*/kAccountKey6,
      /*opt_in_status=*/nearby::fastpair::OptInStatus::STATUS_OPTED_IN);
  LoadPage();
  base::RunLoop().RunUntilIdle();

  EXPECT_EQ(4u, test_web_ui()->call_data().size());
  VerifyOptInStatus(*test_web_ui()->call_data()[2],
                    nearby::fastpair::OptInStatus::STATUS_OPTED_IN);
  VerifySavedDevicesList(
      *test_web_ui()->call_data()[3], /*device_name1=*/kDeviceName4,
      /*expected_device_url1=*/kDisplayUrlBase64, /*account_key1=*/kAccountKey4,
      /*device_name2=*/kDeviceName5, /*expected_device_url2=*/kDisplayUrlBase64,
      /*account_key2=*/kAccountKey5, /*device_name3=*/kDeviceName6,
      /*expected_device_url3=*/kDisplayUrlBase64,
      /*account_key3=*/kAccountKey6);
  histogram_tester().ExpectTotalCount(kSavedDevicesTotalUxLoadTimeMetricName,
                                      2);
}

TEST_F(FastPairSavedDevicesHandlerTest, EmptyImageSentToWebUi) {
  histogram_tester().ExpectTotalCount(kSavedDevicesTotalUxLoadTimeMetricName,
                                      0);
  ON_CALL(*mock_decoder_, DecodeImage(testing::_, testing::_, testing::_))
      .WillByDefault(base::test::RunOnceCallbackRepeatedly<2>(gfx::Image()));
  InitializeSavedDevicesList(
      /*device_name1=*/kDeviceName1, /*device_image_bytes1=*/kImageBytes1,
      /*account_key1=*/kAccountKey1, /*device_name2=*/kDeviceName2,
      /*device_image_bytes2=*/kImageBytes2, /*account_key2=*/kAccountKey2,
      /*device_name3=*/kDeviceName3, /*device_image_bytes3=*/kImageBytes3,
      /*account_key3=*/kAccountKey3,
      /*opt_in_status=*/nearby::fastpair::OptInStatus::STATUS_OPTED_IN);
  LoadPage();
  base::RunLoop().RunUntilIdle();

  EXPECT_EQ(2u, test_web_ui()->call_data().size());
  VerifyOptInStatus(*test_web_ui()->call_data()[0],
                    nearby::fastpair::OptInStatus::STATUS_OPTED_IN);

  // If we decode an empty image, we will send a null image url to the WebUI.
  VerifySavedDevicesList(
      *test_web_ui()->call_data()[1], /*device_name1=*/kDeviceName1,
      /*expected_device_url1=*/"", /*account_key1=*/kAccountKey1,
      /*device_name2=*/kDeviceName2, /*expected_device_url2=*/"",
      /*account_key2=*/kAccountKey2, /*device_name3=*/kDeviceName3,
      /*expected_device_url3=*/"", /*account_key3=*/kAccountKey3);
  histogram_tester().ExpectTotalCount(kSavedDevicesTotalUxLoadTimeMetricName,
                                      1);
}

TEST_F(FastPairSavedDevicesHandlerTest, RemoveSavedDevice) {
  histogram_tester().ExpectBucketCount(kSavedDevicesCountMetricName,
                                       /*num_devices=*/3, 0);
  InitializeSavedDevicesList(
      /*device_name1=*/kDeviceName1, /*device_image_bytes1=*/kImageBytes1,
      /*account_key1=*/kAccountKey1, /*device_name2=*/kDeviceName2,
      /*device_image_bytes2=*/kImageBytes2, /*account_key2=*/kAccountKey2,
      /*device_name3=*/kDeviceName3, /*device_image_bytes3=*/kImageBytes3,
      /*account_key3=*/kAccountKey3,
      /*opt_in_status=*/nearby::fastpair::OptInStatus::STATUS_OPTED_IN);
  LoadPage();
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(2u, test_web_ui()->call_data().size());
  VerifySavedDevicesList(
      *test_web_ui()->call_data()[1], /*device_name1=*/kDeviceName1,
      /*expected_device_url1=*/kDisplayUrlBase64, /*account_key1=*/kAccountKey1,
      /*device_name2=*/kDeviceName2, /*expected_device_url2=*/kDisplayUrlBase64,
      /*account_key2=*/kAccountKey2, /*device_name3=*/kDeviceName3,
      /*expected_device_url3=*/kDisplayUrlBase64,
      /*account_key3=*/kAccountKey3);
  histogram_tester().ExpectBucketCount(kSavedDeviceRemoveResultMetricName,
                                       /*success=*/true, 0);
  histogram_tester().ExpectBucketCount(kSavedDeviceRemoveResultMetricName,
                                       /*success=*/false, 0);
  histogram_tester().ExpectBucketCount(kSavedDevicesCountMetricName,
                                       /*num_devices=*/2, 0);
  histogram_tester().ExpectBucketCount(kSavedDevicesCountMetricName,
                                       /*num_devices=*/3, 1);

  RemoveDevice(kAccountKey3);
  base::RunLoop().RunUntilIdle();

  LoadPage();
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(4u, test_web_ui()->call_data().size());

  const base::Value::List* saved_devices_list =
      test_web_ui()->call_data()[3]->arg2()->GetIfList();
  ASSERT_EQ(2u, saved_devices_list->size());
  AssertDeviceInList(/*device=*/*(saved_devices_list->begin()),
                     /*expected_device_name=*/kDeviceName1,
                     /*expected_base64_image_url=*/kDisplayUrlBase64,
                     /*expected_account_key=*/kAccountKey1);
  AssertDeviceInList(/*device=*/*(saved_devices_list->begin() + 1),
                     /*expected_device_name=*/kDeviceName2,
                     /*expected_base64_image_url=*/kDisplayUrlBase64,
                     /*expected_account_key=*/kAccountKey2);

  histogram_tester().ExpectBucketCount(kSavedDeviceRemoveResultMetricName,
                                       /*success=*/true, 1);
  histogram_tester().ExpectBucketCount(kSavedDeviceRemoveResultMetricName,
                                       /*success=*/false, 0);
  histogram_tester().ExpectBucketCount(kSavedDevicesCountMetricName,
                                       /*num_devices=*/2, 1);
  histogram_tester().ExpectBucketCount(kSavedDevicesCountMetricName,
                                       /*num_devices=*/3, 1);
}

}  // namespace ash::settings