Chromium Code Reviews| 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..2027e28fa64828d730f8b8ea88fae1065299b106 |
| --- /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 equivilant a unique |session_tag|, has |
|
Marc Treib
2016/09/19 09:24:09
s/which is to equivalent equivilant/which is equiv
skym
2016/09/19 18:54:48
Done.
|
| + // 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 |