Index: components/ntp_snippets/sessions/foreign_sessions_suggestions_provider_unittest.cc |
diff --git a/components/ntp_snippets/sessions/foreign_sessions_suggestions_provider_unittest.cc b/components/ntp_snippets/sessions/foreign_sessions_suggestions_provider_unittest.cc |
new file mode 100644 |
index 0000000000000000000000000000000000000000..ae22beb73c57d8dd50b8f246a1ab74286b3e0a9a |
--- /dev/null |
+++ b/components/ntp_snippets/sessions/foreign_sessions_suggestions_provider_unittest.cc |
@@ -0,0 +1,328 @@ |
+// Copyright 2016 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+#include "components/ntp_snippets/sessions/foreign_sessions_suggestions_provider.h" |
+ |
+#include <map> |
+#include <utility> |
+ |
+#include "base/callback_forward.h" |
+#include "base/memory/ptr_util.h" |
+#include "base/strings/string_number_conversions.h" |
+#include "components/ntp_snippets/category.h" |
+#include "components/ntp_snippets/category_factory.h" |
+#include "components/ntp_snippets/content_suggestions_provider.h" |
+#include "components/ntp_snippets/mock_content_suggestions_provider_observer.h" |
+#include "components/prefs/testing_pref_service.h" |
+#include "components/sessions/core/serialized_navigation_entry.h" |
+#include "components/sessions/core/serialized_navigation_entry_test_helper.h" |
+#include "components/sessions/core/session_types.h" |
+#include "components/sync_sessions/synced_session.h" |
+#include "testing/gmock/include/gmock/gmock.h" |
+#include "testing/gtest/include/gtest/gtest.h" |
+ |
+using base::Time; |
+using base::TimeDelta; |
+using sessions::SerializedNavigationEntry; |
+using sessions::SessionTab; |
+using sessions::SessionWindow; |
+using sync_sessions::SyncedSession; |
+using testing::ElementsAre; |
+using testing::IsEmpty; |
+using testing::Property; |
+using testing::Test; |
+using testing::UnorderedElementsAre; |
+using testing::_; |
+ |
+namespace ntp_snippets { |
+namespace { |
+ |
+const char kUrl1[] = "http://www.fake1.com/"; |
+const char kUrl2[] = "http://www.fake2.com/"; |
+const char kUrl3[] = "http://www.fake3.com/"; |
+const char kUrl4[] = "http://www.fake4.com/"; |
+const char kUrl5[] = "http://www.fake5.com/"; |
+const char kUrl6[] = "http://www.fake6.com/"; |
+const char kUrl7[] = "http://www.fake7.com/"; |
+const char kUrl8[] = "http://www.fake8.com/"; |
+const char kUrl9[] = "http://www.fake9.com/"; |
+const char kUrl10[] = "http://www.fake10.com/"; |
+const char kUrl11[] = "http://www.fake11.com/"; |
+const char kTitle[] = "title is ignored"; |
+ |
+SessionWindow* GetOrCreateWindow(SyncedSession* session, int window_id) { |
+ if (session->windows.find(window_id) == session->windows.end()) { |
+ // The session deletes the windows it points at upon destruction. |
+ session->windows[window_id] = new SessionWindow(); |
+ } |
+ return session->windows[window_id]; |
+} |
+ |
+void AddTabToSession(SyncedSession* session, |
+ int window_id, |
+ const std::string& url, |
+ TimeDelta age) { |
+ SerializedNavigationEntry navigation = |
+ sessions::SerializedNavigationEntryTestHelper::CreateNavigation(url, |
+ kTitle); |
+ |
+ std::unique_ptr<SessionTab> tab = base::MakeUnique<SessionTab>(); |
+ tab->timestamp = Time::Now() - age; |
+ tab->navigations.push_back(navigation); |
+ |
+ SessionWindow* window = GetOrCreateWindow(session, window_id); |
+ // The window deletes the tabs it points at upon destruction. |
+ window->tabs.push_back(tab.release()); |
+} |
+ |
+class FakeForeignSessionsProvider : public ForeignSessionsProvider { |
+ public: |
+ ~FakeForeignSessionsProvider() override {} |
+ void SetAllForeignSessions(std::vector<const SyncedSession*> sessions) { |
+ sessions_ = sessions; |
+ change_callback_.Run(); |
+ } |
+ |
+ // ForeignSessionsProvider implementation. |
+ void SubscribeForForeignTabChange( |
+ const base::Closure& change_callback) override { |
+ change_callback_ = change_callback; |
+ } |
+ bool HasSessionsData() override { return true; } |
+ std::vector<const sync_sessions::SyncedSession*> GetAllForeignSessions() |
+ override { |
+ return sessions_; |
+ } |
+ |
+ private: |
+ std::vector<const SyncedSession*> sessions_; |
+ base::Closure change_callback_; |
+}; |
+} // namespace |
+ |
+class ForeignSessionsSuggestionsProviderTest : public Test { |
+ public: |
+ ForeignSessionsSuggestionsProviderTest() { |
+ ForeignSessionsSuggestionsProvider::RegisterProfilePrefs( |
+ pref_service_.registry()); |
+ |
+ std::unique_ptr<FakeForeignSessionsProvider> |
+ fake_foreign_sessions_provider = |
+ base::MakeUnique<FakeForeignSessionsProvider>(); |
+ fake_foreign_sessions_provider_ = fake_foreign_sessions_provider.get(); |
+ |
+ // During the provider's construction the following mock calls occur. |
+ EXPECT_CALL(*observer(), OnNewSuggestions(_, category(), IsEmpty())); |
+ EXPECT_CALL(*observer(), OnCategoryStatusChanged( |
+ _, category(), CategoryStatus::AVAILABLE)); |
+ |
+ provider_ = base::MakeUnique<ForeignSessionsSuggestionsProvider>( |
+ &observer_, &category_factory_, |
+ std::move(fake_foreign_sessions_provider), &pref_service_); |
+ } |
+ |
+ protected: |
+ SyncedSession* GetOrCreateSession(int session_id) { |
+ if (sessions_map_.find(session_id) == sessions_map_.end()) { |
+ std::string id_as_string = base::IntToString(session_id); |
+ std::unique_ptr<SyncedSession> owned_session = |
+ base::MakeUnique<SyncedSession>(); |
+ owned_session->session_tag = id_as_string; |
+ owned_session->session_name = id_as_string; |
+ sessions_map_[session_id] = std::move(owned_session); |
+ } |
+ return sessions_map_[session_id].get(); |
+ } |
+ |
+ void AddTab(int session_id, |
+ int window_id, |
+ const std::string& url, |
+ TimeDelta age) { |
+ AddTabToSession(GetOrCreateSession(session_id), window_id, url, age); |
+ } |
+ |
+ void TriggerOnChange() { |
+ std::vector<const SyncedSession*> sessions; |
+ for (const auto& kv : sessions_map_) { |
+ sessions.push_back(kv.second.get()); |
+ } |
+ fake_foreign_sessions_provider_->SetAllForeignSessions(sessions); |
+ } |
+ |
+ void Dismiss(const std::string& url) { |
+ // The url of a given suggestion is used as the |within_category_id|. |
+ provider_->DismissSuggestion(provider_->MakeUniqueID(category(), url)); |
+ } |
+ |
+ Category category() { |
+ return category_factory_.FromKnownCategory(KnownCategories::FOREIGN_TABS); |
+ } |
+ |
+ MockContentSuggestionsProviderObserver* observer() { return &observer_; } |
+ |
+ private: |
+ FakeForeignSessionsProvider* fake_foreign_sessions_provider_; |
+ MockContentSuggestionsProviderObserver observer_; |
+ CategoryFactory category_factory_; |
+ TestingPrefServiceSimple pref_service_; |
+ std::unique_ptr<ForeignSessionsSuggestionsProvider> provider_; |
+ std::map<int, std::unique_ptr<SyncedSession>> sessions_map_; |
+ |
+ DISALLOW_COPY_AND_ASSIGN(ForeignSessionsSuggestionsProviderTest); |
+}; |
+ |
+TEST_F(ForeignSessionsSuggestionsProviderTest, Empty) { |
+ // When no sessions data is added, expect no suggestions. |
+ EXPECT_CALL(*observer(), OnNewSuggestions(_, category(), IsEmpty())); |
+ TriggerOnChange(); |
+} |
+ |
+TEST_F(ForeignSessionsSuggestionsProviderTest, Single) { |
+ // Expect a single valid tab because that is what has been added. |
+ EXPECT_CALL(*observer(), |
+ OnNewSuggestions( |
+ _, category(), |
+ ElementsAre(Property(&ContentSuggestion::url, GURL(kUrl1))))); |
+ AddTab(0, 0, kUrl1, TimeDelta::FromMinutes(1)); |
+ TriggerOnChange(); |
+} |
+ |
+TEST_F(ForeignSessionsSuggestionsProviderTest, Old) { |
+ // The only sessions data is too old to be suggested, so expect empty. |
+ EXPECT_CALL(*observer(), OnNewSuggestions(_, category(), IsEmpty())); |
+ AddTab(0, 0, kUrl1, TimeDelta::FromHours(4)); |
+ TriggerOnChange(); |
+} |
+ |
+TEST_F(ForeignSessionsSuggestionsProviderTest, Ordered) { |
+ // Suggestions ordering should be in reverse chronological order, or youngest |
+ // first. |
+ EXPECT_CALL(*observer(), |
+ OnNewSuggestions( |
+ _, category(), |
+ ElementsAre(Property(&ContentSuggestion::url, GURL(kUrl1)), |
+ Property(&ContentSuggestion::url, GURL(kUrl2)), |
+ Property(&ContentSuggestion::url, GURL(kUrl3)), |
+ Property(&ContentSuggestion::url, GURL(kUrl4))))); |
+ AddTab(0, 0, kUrl2, TimeDelta::FromMinutes(2)); |
+ AddTab(0, 0, kUrl4, TimeDelta::FromMinutes(4)); |
+ AddTab(0, 1, kUrl3, TimeDelta::FromMinutes(3)); |
+ AddTab(1, 0, kUrl1, TimeDelta::FromMinutes(1)); |
+ TriggerOnChange(); |
+} |
+ |
+TEST_F(ForeignSessionsSuggestionsProviderTest, MaxPerDevice) { |
+ // Each device, which is to equivalent a unique |session_tag|, has a limit to |
+ // the number of suggestions it is allowed to contribute. Here all four |
+ // suggestions are within the recency threshold, but only three are allowed |
+ // per device. As such, expect that the oldest of the four will not be |
+ // suggested. |
+ EXPECT_CALL(*observer(), |
+ OnNewSuggestions( |
+ _, category(), |
+ ElementsAre(Property(&ContentSuggestion::url, GURL(kUrl1)), |
+ Property(&ContentSuggestion::url, GURL(kUrl2)), |
+ Property(&ContentSuggestion::url, GURL(kUrl3))))); |
+ AddTab(0, 0, kUrl1, TimeDelta::FromMinutes(1)); |
+ AddTab(0, 0, kUrl2, TimeDelta::FromMinutes(2)); |
+ AddTab(0, 0, kUrl3, TimeDelta::FromMinutes(3)); |
+ AddTab(0, 0, kUrl4, TimeDelta::FromMinutes(4)); |
+ TriggerOnChange(); |
+} |
+ |
+TEST_F(ForeignSessionsSuggestionsProviderTest, MaxTotal) { |
+ // There's a limit to the total nubmer of suggestions that the provider will |
+ // ever return, which should be ten. Here there are eleven valid suggestion |
+ // entries, spread out over multiple devices/sessions to avoid the per device |
+ // cutoff. Expect that the least recent of the eleven to be dropped. |
+ EXPECT_CALL( |
+ *observer(), |
+ OnNewSuggestions( |
+ _, category(), |
+ ElementsAre(Property(&ContentSuggestion::url, GURL(kUrl1)), |
+ Property(&ContentSuggestion::url, GURL(kUrl2)), |
+ Property(&ContentSuggestion::url, GURL(kUrl3)), |
+ Property(&ContentSuggestion::url, GURL(kUrl4)), |
+ Property(&ContentSuggestion::url, GURL(kUrl5)), |
+ Property(&ContentSuggestion::url, GURL(kUrl6)), |
+ Property(&ContentSuggestion::url, GURL(kUrl7)), |
+ Property(&ContentSuggestion::url, GURL(kUrl8)), |
+ Property(&ContentSuggestion::url, GURL(kUrl9)), |
+ Property(&ContentSuggestion::url, GURL(kUrl10))))); |
+ AddTab(0, 0, kUrl1, TimeDelta::FromMinutes(1)); |
+ AddTab(0, 0, kUrl2, TimeDelta::FromMinutes(2)); |
+ AddTab(0, 0, kUrl3, TimeDelta::FromMinutes(3)); |
+ AddTab(1, 0, kUrl4, TimeDelta::FromMinutes(4)); |
+ AddTab(1, 0, kUrl5, TimeDelta::FromMinutes(5)); |
+ AddTab(1, 0, kUrl6, TimeDelta::FromMinutes(6)); |
+ AddTab(2, 0, kUrl7, TimeDelta::FromMinutes(7)); |
+ AddTab(2, 0, kUrl8, TimeDelta::FromMinutes(8)); |
+ AddTab(2, 0, kUrl9, TimeDelta::FromMinutes(9)); |
+ AddTab(3, 0, kUrl10, TimeDelta::FromMinutes(10)); |
+ AddTab(3, 0, kUrl11, TimeDelta::FromMinutes(11)); |
+ TriggerOnChange(); |
+} |
+ |
+TEST_F(ForeignSessionsSuggestionsProviderTest, Duplicates) { |
+ // The same url is never suggested more than once at a time. All the session |
+ // data has the same url so only expect a single suggestion. |
+ EXPECT_CALL(*observer(), |
+ OnNewSuggestions( |
+ _, category(), |
+ ElementsAre(Property(&ContentSuggestion::url, GURL(kUrl1))))); |
+ AddTab(0, 0, kUrl1, TimeDelta::FromMinutes(1)); |
+ AddTab(0, 1, kUrl1, TimeDelta::FromMinutes(2)); |
+ AddTab(1, 1, kUrl1, TimeDelta::FromMinutes(3)); |
+ TriggerOnChange(); |
+} |
+ |
+TEST_F(ForeignSessionsSuggestionsProviderTest, DuplicatesChangingOtherSession) { |
+ // Normally |kUrl4| wouldn't show up, because session_id=0 already has 3 |
+ // younger tabs, but session_id=1 has a younger |kUrl3| which gives |kUrl4| a |
+ // spot. |
+ EXPECT_CALL(*observer(), |
+ OnNewSuggestions( |
+ _, category(), |
+ ElementsAre(Property(&ContentSuggestion::url, GURL(kUrl3)), |
+ Property(&ContentSuggestion::url, GURL(kUrl1)), |
+ Property(&ContentSuggestion::url, GURL(kUrl2)), |
+ Property(&ContentSuggestion::url, GURL(kUrl4))))); |
+ AddTab(0, 0, kUrl1, TimeDelta::FromMinutes(1)); |
+ AddTab(0, 0, kUrl2, TimeDelta::FromMinutes(2)); |
+ AddTab(0, 0, kUrl3, TimeDelta::FromMinutes(3)); |
+ AddTab(0, 0, kUrl4, TimeDelta::FromMinutes(4)); |
+ AddTab(1, 0, kUrl3, TimeDelta::FromMinutes(0)); |
+ TriggerOnChange(); |
+} |
+ |
+TEST_F(ForeignSessionsSuggestionsProviderTest, Dismissed) { |
+ // Dimissed urls should not be suggested. |
+ EXPECT_CALL(*observer(), OnNewSuggestions(_, category(), IsEmpty())); |
+ Dismiss(kUrl1); |
+ AddTab(0, 0, kUrl1, TimeDelta::FromMinutes(1)); |
+ TriggerOnChange(); |
+} |
+ |
+TEST_F(ForeignSessionsSuggestionsProviderTest, DismissedChangingOwnSession) { |
+ // Similar to DuplicatesChangingOtherSession, without dismissals we would |
+ // expect urls 1-3. However, because of dismissals we reach all the down to |
+ // |kUrl5| before the per device cutoff is hit. |
+ EXPECT_CALL(*observer(), |
+ OnNewSuggestions( |
+ _, category(), |
+ ElementsAre(Property(&ContentSuggestion::url, GURL(kUrl2)), |
+ Property(&ContentSuggestion::url, GURL(kUrl3)), |
+ Property(&ContentSuggestion::url, GURL(kUrl5))))); |
+ Dismiss(kUrl1); |
+ Dismiss(kUrl4); |
+ AddTab(0, 0, kUrl1, TimeDelta::FromMinutes(1)); |
+ AddTab(0, 0, kUrl2, TimeDelta::FromMinutes(2)); |
+ AddTab(0, 0, kUrl3, TimeDelta::FromMinutes(3)); |
+ AddTab(0, 0, kUrl4, TimeDelta::FromMinutes(4)); |
+ AddTab(0, 0, kUrl5, TimeDelta::FromMinutes(5)); |
+ AddTab(0, 0, kUrl6, TimeDelta::FromMinutes(6)); |
+ TriggerOnChange(); |
+} |
+ |
+} // namespace ntp_snippets |