chromium/ios/chrome/browser/ui/popup_menu/overflow_menu/destination_usage_history/destination_usage_history_unittest.mm

// 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.

#import "ios/chrome/browser/ui/popup_menu/overflow_menu/destination_usage_history/destination_usage_history.h"

#import "base/strings/string_number_conversions.h"
#import "base/strings/sys_string_conversions.h"
#import "base/time/time.h"
#import "base/values.h"
#import "components/prefs/pref_registry_simple.h"
#import "components/prefs/scoped_user_pref_update.h"
#import "components/prefs/testing_pref_service.h"
#import "ios/chrome/browser/shared/model/prefs/pref_names.h"
#import "ios/chrome/browser/shared/ui/symbols/symbols.h"
#import "ios/chrome/browser/ui/popup_menu/overflow_menu/destination_usage_history/constants.h"
#import "ios/chrome/browser/ui/popup_menu/overflow_menu/feature_flags.h"
#import "ios/chrome/browser/ui/popup_menu/overflow_menu/overflow_menu_constants.h"
#import "ios/chrome/browser/ui/popup_menu/overflow_menu/overflow_menu_swift.h"
#import "testing/platform_test.h"

namespace {

// The number of destinations immediately visible in the carousel when the
// overflow menu is opened.
//
// For the purposes of these unit tests, this value is
// statically declared below. In practice, this value is dynamically calculated
// based on device size.
static constexpr int kVisibleDestinationsCount = 5;

// A time delta from the Unix epoch to the beginning of the current day.
base::TimeDelta TodaysDay() {
  return base::Days(
      (base::Time::Now() - base::Time::UnixEpoch()).InDaysFloored());
}

}  // namespace

class DestinationUsageHistoryTest : public PlatformTest {
 public:
  DestinationUsageHistoryTest() {}

 protected:
  void TearDown() override {
    [destination_usage_history_ stop];

    PlatformTest::TearDown();
  }

  // Initializes `destination_usage_history_` with empty pref data and returns
  // the initial ranking.
  DestinationRanking InitializeDestinationUsageHistory(
      DestinationRanking default_destinations) {
    CreatePrefs();

    destination_usage_history_ =
        [[DestinationUsageHistory alloc] initWithPrefService:prefs_.get()];

    destination_usage_history_.visibleDestinationsCount =
        kVisibleDestinationsCount;

    [destination_usage_history_ start];

    DestinationRanking initial_ranking = [destination_usage_history_
        sortedDestinationsFromCurrentRanking:{}
                       availableDestinations:default_destinations];

    return initial_ranking;
  }

  // Initializes `destination_usage_history_` with past data and `ranking` and
  // returns the new ranking.
  DestinationRanking InitializeDestinationUsageHistoryWithData(
      DestinationRanking& ranking,
      base::Value::Dict& history,
      DestinationRanking default_destinations) {
    base::Value::List previous_ranking;

    for (overflow_menu::Destination destination : ranking) {
      previous_ranking.Append(
          overflow_menu::StringNameForDestination(destination));
    }

    CreatePrefsWithData(previous_ranking, history);

    destination_usage_history_ =
        [[DestinationUsageHistory alloc] initWithPrefService:prefs_.get()];

    destination_usage_history_.visibleDestinationsCount =
        kVisibleDestinationsCount;

    [destination_usage_history_ start];

    DestinationRanking initial_ranking = [destination_usage_history_
        sortedDestinationsFromCurrentRanking:ranking
                       availableDestinations:default_destinations];

    return initial_ranking;
  }

  // Create pref registry for tests.
  void CreatePrefs() {
    prefs_ = std::make_unique<TestingPrefServiceSimple>();
    prefs_->registry()->RegisterDictionaryPref(
        prefs::kOverflowMenuDestinationUsageHistory, PrefRegistry::LOSSY_PREF);
    prefs_->registry()->RegisterListPref(prefs::kOverflowMenuNewDestinations,
                                         PrefRegistry::LOSSY_PREF);
  }

  // Helper for CreateDestinationUsageHistoryWithData(), inserts day history
  // data and `ranking` for testing pref service.
  void CreatePrefsWithData(base::Value::List& stored_ranking,
                           base::Value::Dict& stored_history) {
    CreatePrefs();

    // Set the passed in `stored_history`.
    base::Value::Dict history = stored_history.Clone();

    prefs_->SetDict(prefs::kOverflowMenuDestinationUsageHistory,
                    std::move(history));
  }

  // Constructs '<day>.<destination>' dotted-path key for base::Value::Dict
  // searching.
  std::string DottedPath(std::string day,
                         overflow_menu::Destination destination) {
    std::string destination_name =
        overflow_menu::StringNameForDestination(destination);

    return day + "." + destination_name;
  }

  // Helper for DottedPath(...), converts day (int) to string, then calls
  // DottedPath(std::string day, overflow_menu::Destination destination).
  std::string DottedPath(int day, overflow_menu::Destination destination) {
    return DottedPath(base::NumberToString(day), destination);
  }

  DestinationRanking SampleDestinations() {
    return {
        overflow_menu::Destination::Bookmarks,
        overflow_menu::Destination::History,
        overflow_menu::Destination::ReadingList,
        overflow_menu::Destination::Passwords,
        overflow_menu::Destination::Downloads,
        overflow_menu::Destination::RecentTabs,
        overflow_menu::Destination::SiteInfo,
        overflow_menu::Destination::Settings,
    };
  }

  std::unique_ptr<TestingPrefServiceSimple> prefs_;
  DestinationUsageHistory* destination_usage_history_;
};

// Tests the initializer correctly creates a DestinationUsageHistory* with the
// specified Pref service.
TEST_F(DestinationUsageHistoryTest, InitWithPrefService) {
  InitializeDestinationUsageHistory(SampleDestinations());

  PrefService* pref_service = prefs_.get();

  EXPECT_NE(
      pref_service->FindPreference(prefs::kOverflowMenuDestinationUsageHistory),
      nullptr);
  EXPECT_TRUE(
      pref_service->HasPrefPath(prefs::kOverflowMenuDestinationUsageHistory));
}

// Tests the initializer correctly creates a DestinationUsageHistory* with the
// specified Pref service, when the prefs have existing data.
TEST_F(DestinationUsageHistoryTest, InitWithPrefServiceForDirtyPrefs) {
  // Construct existing usage data stored in prefs.
  std::vector<overflow_menu::Destination> ranking = {
      overflow_menu::Destination::Bookmarks,
      overflow_menu::Destination::History,
      overflow_menu::Destination::ReadingList,
      overflow_menu::Destination::Passwords,
      overflow_menu::Destination::Downloads,
      overflow_menu::Destination::RecentTabs,
      overflow_menu::Destination::SiteInfo,
      overflow_menu::Destination::Settings,
  };

  base::Value::Dict history;
  base::Value::Dict day_history;
  history.SetByDottedPath(
      DottedPath(TodaysDay().InDays(), overflow_menu::Destination::Bookmarks),
      std::move(day_history));

  // Create DestinationUsageHistory.
  InitializeDestinationUsageHistoryWithData(ranking, history,
                                            SampleDestinations());

  PrefService* pref_service = prefs_.get();

  EXPECT_NE(
      pref_service->FindPreference(prefs::kOverflowMenuDestinationUsageHistory),
      nullptr);
  EXPECT_TRUE(
      pref_service->HasPrefPath(prefs::kOverflowMenuDestinationUsageHistory));
}

// Tests that a new destination click is incremented and written to Chrome
// Prefs.
TEST_F(DestinationUsageHistoryTest, HandlesNewDestinationClick) {
  InitializeDestinationUsageHistory(SampleDestinations());

  // Click bookmarks destination.
  [destination_usage_history_
      recordClickForDestination:overflow_menu::Destination::Bookmarks];

  ScopedDictPrefUpdate update(prefs_.get(),
                              prefs::kOverflowMenuDestinationUsageHistory);

  std::optional<int> expected = update->FindIntByDottedPath(
      DottedPath(TodaysDay().InDays(), overflow_menu::Destination::Bookmarks));

  // Verify bookmarks entry exists.
  EXPECT_TRUE(expected.has_value());

  // Verify bookmarks entry has single click (plus the default seeded 20 clicks)
  // for today.
  EXPECT_EQ(expected.value(), 21);
}

// Tests that each destination in the history is populated with a default
// number of clicks.
TEST_F(DestinationUsageHistoryTest,
       InjectsDefaultClickCountForAllDestinations) {
  DestinationRanking sample_destinations = SampleDestinations();

  InitializeDestinationUsageHistory(sample_destinations);

  ScopedDictPrefUpdate update(prefs_.get(),
                              prefs::kOverflowMenuDestinationUsageHistory);

  for (overflow_menu::Destination destination : sample_destinations) {
    const std::string dotted_path =
        DottedPath(TodaysDay().InDays(), destination);

    std::optional<int> expected = update->FindIntByDottedPath(dotted_path);

    // Verify destination entry exists.
    EXPECT_TRUE(expected.has_value());

    // Verify destination  entry has single click (plus the default seeded 20
    // clicks) for today.
    EXPECT_EQ(expected.value(), 20);
  }
}

// Tests that an existing destination click is incremented.
TEST_F(DestinationUsageHistoryTest,
       HandlesExistingDestinationClickAndAddToPrefs) {
  // Construct existing usage data, where Bookmarks has been clicked 5 times,
  // stored in prefs.
  std::vector<overflow_menu::Destination> ranking = {
      overflow_menu::Destination::Bookmarks,
      overflow_menu::Destination::History,
      overflow_menu::Destination::ReadingList,
      overflow_menu::Destination::Passwords,
      overflow_menu::Destination::Downloads,
      overflow_menu::Destination::RecentTabs,
      overflow_menu::Destination::SiteInfo,
      overflow_menu::Destination::Settings,
  };
  base::Value::Dict history;
  base::Value::Dict day_history;
  day_history.Set(overflow_menu::StringNameForDestination(
                      overflow_menu::Destination::Bookmarks),
                  5);
  history.Set(base::NumberToString(TodaysDay().InDays()),
              std::move(day_history));

  // Create DestinationUsageHistory.
  InitializeDestinationUsageHistoryWithData(ranking, history,
                                            SampleDestinations());

  // Click bookmarks destination.
  [destination_usage_history_
      recordClickForDestination:overflow_menu::Destination::Bookmarks];

  ScopedDictPrefUpdate update(prefs_.get(),
                              prefs::kOverflowMenuDestinationUsageHistory);

  std::optional<int> expected = update->FindIntByDottedPath(
      DottedPath(TodaysDay().InDays(), overflow_menu::Destination::Bookmarks));

  // Verify bookmarks entry exists.
  EXPECT_TRUE(expected.has_value());

  // Verify bookmarks entry has single click (plus the existing 5 clicks)
  // for today.
  EXPECT_EQ(expected.value(), 6);
}

TEST_F(DestinationUsageHistoryTest, DoesNotSwapTwoShownDestinations) {
  DestinationRanking sample_destinations = SampleDestinations();

  DestinationRanking ranking = {
      overflow_menu::Destination::Bookmarks,
      overflow_menu::Destination::History,
      overflow_menu::Destination::ReadingList,
      overflow_menu::Destination::Passwords,
      overflow_menu::Destination::Downloads,
      overflow_menu::Destination::RecentTabs,
      overflow_menu::Destination::SiteInfo,
      overflow_menu::Destination::Settings,
  };
  base::Value::Dict history;

  DestinationRanking initial_ranking =
      InitializeDestinationUsageHistoryWithData(ranking, history,
                                                sample_destinations);
  // Click bookmarks Reading List five
  // times.
  [destination_usage_history_
      recordClickForDestination:overflow_menu::Destination::ReadingList];
  [destination_usage_history_
      recordClickForDestination:overflow_menu::Destination::ReadingList];
  [destination_usage_history_
      recordClickForDestination:overflow_menu::Destination::ReadingList];
  [destination_usage_history_
      recordClickForDestination:overflow_menu::Destination::ReadingList];
  [destination_usage_history_
      recordClickForDestination:overflow_menu::Destination::ReadingList];

  DestinationRanking sorted_ranking = [destination_usage_history_
      sortedDestinationsFromCurrentRanking:initial_ranking
                     availableDestinations:sample_destinations];

  EXPECT_EQ(initial_ranking, sorted_ranking);
}

TEST_F(DestinationUsageHistoryTest, DoesNotSwapTwoUnshownDestinations) {
  DestinationRanking sample_destinations = SampleDestinations();

  DestinationRanking ranking = {
      overflow_menu::Destination::Bookmarks,
      overflow_menu::Destination::History,
      overflow_menu::Destination::ReadingList,
      overflow_menu::Destination::Passwords,
      overflow_menu::Destination::Downloads,
      overflow_menu::Destination::RecentTabs,
      overflow_menu::Destination::SiteInfo,
      overflow_menu::Destination::Settings,
  };
  base::Value::Dict history;
  base::Value::Dict day_history;
  day_history.Set(overflow_menu::StringNameForDestination(
                      overflow_menu::Destination::Bookmarks),
                  3);
  day_history.Set(overflow_menu::StringNameForDestination(
                      overflow_menu::Destination::History),
                  3);
  day_history.Set(overflow_menu::StringNameForDestination(
                      overflow_menu::Destination::ReadingList),
                  3);
  day_history.Set(overflow_menu::StringNameForDestination(
                      overflow_menu::Destination::Passwords),
                  3);
  day_history.Set(overflow_menu::StringNameForDestination(
                      overflow_menu::Destination::Downloads),
                  3);
  day_history.Set(overflow_menu::StringNameForDestination(
                      overflow_menu::Destination::RecentTabs),
                  1);
  day_history.Set(overflow_menu::StringNameForDestination(
                      overflow_menu::Destination::SiteInfo),
                  1);
  day_history.Set(overflow_menu::StringNameForDestination(
                      overflow_menu::Destination::Settings),
                  1);

  history.Set(base::NumberToString(TodaysDay().InDays()),
              std::move(day_history));

  DestinationRanking initial_ranking =
      InitializeDestinationUsageHistoryWithData(ranking, history,
                                                sample_destinations);

  // Click Recent Tabs (currently in ranking position 6) once.
  [destination_usage_history_
      recordClickForDestination:overflow_menu::Destination::RecentTabs];

  // Click Site Inforamtion (currently in ranking position 7) once.
  [destination_usage_history_
      recordClickForDestination:overflow_menu::Destination::SiteInfo];

  // Click Settings (currently in last position) once.
  [destination_usage_history_
      recordClickForDestination:overflow_menu::Destination::Settings];

  DestinationRanking sorted_ranking = [destination_usage_history_
      sortedDestinationsFromCurrentRanking:initial_ranking
                     availableDestinations:sample_destinations];

  EXPECT_EQ(initial_ranking, sorted_ranking);
}

TEST_F(DestinationUsageHistoryTest, DeletesExpiredUsageData) {
  DestinationRanking sample_destinations = SampleDestinations();

  DestinationRanking ranking = {
      overflow_menu::Destination::Bookmarks,
      overflow_menu::Destination::History,
      overflow_menu::Destination::ReadingList,
      overflow_menu::Destination::Passwords,
      overflow_menu::Destination::Downloads,
      overflow_menu::Destination::RecentTabs,
      overflow_menu::Destination::SiteInfo,
      overflow_menu::Destination::Settings,
  };

  base::Value::Dict history;

  // Usage data just a bit older than 1 year.
  base::TimeDelta recently_expired_day = TodaysDay() - base::Days(366);
  base::Value::Dict recently_expired_day_history;
  recently_expired_day_history.Set(overflow_menu::StringNameForDestination(
                                       overflow_menu::Destination::Bookmarks),
                                   1);
  history.Set(base::NumberToString(recently_expired_day.InDays()),
              std::move(recently_expired_day_history));

  // Usage data almost 3 years old.
  base::TimeDelta expired_day = TodaysDay() - base::Days(1000);
  base::Value::Dict expired_day_history;
  expired_day_history.Set(overflow_menu::StringNameForDestination(
                              overflow_menu::Destination::Bookmarks),
                          1);
  history.Set(base::NumberToString(expired_day.InDays()),
              std::move(expired_day_history));

  InitializeDestinationUsageHistoryWithData(ranking, history,
                                            sample_destinations);

  [destination_usage_history_
      sortedDestinationsFromCurrentRanking:ranking
                     availableDestinations:sample_destinations];

  ScopedDictPrefUpdate update(prefs_.get(),
                              prefs::kOverflowMenuDestinationUsageHistory);

  // Has one entry for today's seeded history.
  EXPECT_EQ(update->size(), (size_t)1);
  EXPECT_NE(update->Find(base::NumberToString(TodaysDay().InDays())), nullptr);
  EXPECT_EQ(update->Find(base::NumberToString(recently_expired_day.InDays())),
            nullptr);
  EXPECT_EQ(update->Find(base::NumberToString(expired_day.InDays())), nullptr);
}

TEST_F(DestinationUsageHistoryTest, ClearsUsageData) {
  base::Value::Dict history;

  // Usage data for yesterday.
  base::TimeDelta day = TodaysDay() - base::Days(1);
  base::Value::Dict day_history;
  day_history.Set(overflow_menu::StringNameForDestination(
                      overflow_menu::Destination::Bookmarks),
                  1);
  history.Set(base::NumberToString(day.InDays()), std::move(day_history));

  DestinationRanking ranking;
  InitializeDestinationUsageHistoryWithData(ranking, history, {});

  [destination_usage_history_ clearStoredClickData];

  const base::Value::Dict& new_history =
      prefs_->GetDict(prefs::kOverflowMenuDestinationUsageHistory);
  EXPECT_EQ(new_history.size(), 0u);
}