OLD | NEW |
(Empty) | |
| 1 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 #include "components/ntp_snippets/sessions/foreign_sessions_suggestions_provider
.h" |
| 6 |
| 7 #include <map> |
| 8 #include <utility> |
| 9 |
| 10 #include "base/callback_forward.h" |
| 11 #include "base/memory/ptr_util.h" |
| 12 #include "base/strings/string_number_conversions.h" |
| 13 #include "components/ntp_snippets/category.h" |
| 14 #include "components/ntp_snippets/category_factory.h" |
| 15 #include "components/ntp_snippets/content_suggestions_provider.h" |
| 16 #include "components/ntp_snippets/mock_content_suggestions_provider_observer.h" |
| 17 #include "components/prefs/testing_pref_service.h" |
| 18 #include "components/sessions/core/serialized_navigation_entry.h" |
| 19 #include "components/sessions/core/serialized_navigation_entry_test_helper.h" |
| 20 #include "components/sessions/core/session_types.h" |
| 21 #include "components/sync_sessions/synced_session.h" |
| 22 #include "testing/gmock/include/gmock/gmock.h" |
| 23 #include "testing/gtest/include/gtest/gtest.h" |
| 24 |
| 25 using base::Time; |
| 26 using base::TimeDelta; |
| 27 using sessions::SerializedNavigationEntry; |
| 28 using sessions::SessionTab; |
| 29 using sessions::SessionWindow; |
| 30 using sync_sessions::SyncedSession; |
| 31 using testing::ElementsAre; |
| 32 using testing::IsEmpty; |
| 33 using testing::Property; |
| 34 using testing::Test; |
| 35 using testing::UnorderedElementsAre; |
| 36 using testing::_; |
| 37 |
| 38 namespace ntp_snippets { |
| 39 namespace { |
| 40 |
| 41 const char kUrl1[] = "http://www.fake1.com/"; |
| 42 const char kUrl2[] = "http://www.fake2.com/"; |
| 43 const char kUrl3[] = "http://www.fake3.com/"; |
| 44 const char kUrl4[] = "http://www.fake4.com/"; |
| 45 const char kUrl5[] = "http://www.fake5.com/"; |
| 46 const char kUrl6[] = "http://www.fake6.com/"; |
| 47 const char kUrl7[] = "http://www.fake7.com/"; |
| 48 const char kUrl8[] = "http://www.fake8.com/"; |
| 49 const char kUrl9[] = "http://www.fake9.com/"; |
| 50 const char kUrl10[] = "http://www.fake10.com/"; |
| 51 const char kUrl11[] = "http://www.fake11.com/"; |
| 52 const char kTitle[] = "title is ignored"; |
| 53 |
| 54 SessionWindow* GetOrCreateWindow(SyncedSession* session, int window_id) { |
| 55 if (session->windows.find(window_id) == session->windows.end()) { |
| 56 // The session deletes the windows it points at upon destruction. |
| 57 session->windows[window_id] = new SessionWindow(); |
| 58 } |
| 59 return session->windows[window_id]; |
| 60 } |
| 61 |
| 62 void AddTabToSession(SyncedSession* session, |
| 63 int window_id, |
| 64 const std::string& url, |
| 65 TimeDelta age) { |
| 66 SerializedNavigationEntry navigation = |
| 67 sessions::SerializedNavigationEntryTestHelper::CreateNavigation(url, |
| 68 kTitle); |
| 69 |
| 70 std::unique_ptr<SessionTab> tab = base::MakeUnique<SessionTab>(); |
| 71 tab->timestamp = Time::Now() - age; |
| 72 tab->navigations.push_back(navigation); |
| 73 |
| 74 SessionWindow* window = GetOrCreateWindow(session, window_id); |
| 75 // The window deletes the tabs it points at upon destruction. |
| 76 window->tabs.push_back(tab.release()); |
| 77 } |
| 78 |
| 79 class FakeForeignSessionsProvider : public ForeignSessionsProvider { |
| 80 public: |
| 81 ~FakeForeignSessionsProvider() override {} |
| 82 void SetAllForeignSessions(std::vector<const SyncedSession*> sessions) { |
| 83 sessions_ = sessions; |
| 84 change_callback_.Run(); |
| 85 } |
| 86 |
| 87 // ForeignSessionsProvider implementation. |
| 88 void SubscribeForForeignTabChange( |
| 89 const base::Closure& change_callback) override { |
| 90 change_callback_ = change_callback; |
| 91 } |
| 92 bool HasSessionsData() override { return true; } |
| 93 std::vector<const sync_sessions::SyncedSession*> GetAllForeignSessions() |
| 94 override { |
| 95 return sessions_; |
| 96 } |
| 97 |
| 98 private: |
| 99 std::vector<const SyncedSession*> sessions_; |
| 100 base::Closure change_callback_; |
| 101 }; |
| 102 } // namespace |
| 103 |
| 104 class ForeignSessionsSuggestionsProviderTest : public Test { |
| 105 public: |
| 106 ForeignSessionsSuggestionsProviderTest() { |
| 107 ForeignSessionsSuggestionsProvider::RegisterProfilePrefs( |
| 108 pref_service_.registry()); |
| 109 |
| 110 std::unique_ptr<FakeForeignSessionsProvider> |
| 111 fake_foreign_sessions_provider = |
| 112 base::MakeUnique<FakeForeignSessionsProvider>(); |
| 113 fake_foreign_sessions_provider_ = fake_foreign_sessions_provider.get(); |
| 114 |
| 115 // During the provider's construction the following mock calls occur. |
| 116 EXPECT_CALL(*observer(), OnNewSuggestions(_, category(), IsEmpty())); |
| 117 EXPECT_CALL(*observer(), OnCategoryStatusChanged( |
| 118 _, category(), CategoryStatus::AVAILABLE)); |
| 119 |
| 120 provider_ = base::MakeUnique<ForeignSessionsSuggestionsProvider>( |
| 121 &observer_, &category_factory_, |
| 122 std::move(fake_foreign_sessions_provider), &pref_service_); |
| 123 } |
| 124 |
| 125 protected: |
| 126 SyncedSession* GetOrCreateSession(int session_id) { |
| 127 if (sessions_map_.find(session_id) == sessions_map_.end()) { |
| 128 std::string id_as_string = base::IntToString(session_id); |
| 129 std::unique_ptr<SyncedSession> owned_session = |
| 130 base::MakeUnique<SyncedSession>(); |
| 131 owned_session->session_tag = id_as_string; |
| 132 owned_session->session_name = id_as_string; |
| 133 sessions_map_[session_id] = std::move(owned_session); |
| 134 } |
| 135 return sessions_map_[session_id].get(); |
| 136 } |
| 137 |
| 138 void AddTab(int session_id, |
| 139 int window_id, |
| 140 const std::string& url, |
| 141 TimeDelta age) { |
| 142 AddTabToSession(GetOrCreateSession(session_id), window_id, url, age); |
| 143 } |
| 144 |
| 145 void TriggerOnChange() { |
| 146 std::vector<const SyncedSession*> sessions; |
| 147 for (const auto& kv : sessions_map_) { |
| 148 sessions.push_back(kv.second.get()); |
| 149 } |
| 150 fake_foreign_sessions_provider_->SetAllForeignSessions(sessions); |
| 151 } |
| 152 |
| 153 void Dismiss(const std::string& url) { |
| 154 // The url of a given suggestion is used as the |within_category_id|. |
| 155 provider_->DismissSuggestion(provider_->MakeUniqueID(category(), url)); |
| 156 } |
| 157 |
| 158 Category category() { |
| 159 return category_factory_.FromKnownCategory(KnownCategories::FOREIGN_TABS); |
| 160 } |
| 161 |
| 162 MockContentSuggestionsProviderObserver* observer() { return &observer_; } |
| 163 |
| 164 private: |
| 165 FakeForeignSessionsProvider* fake_foreign_sessions_provider_; |
| 166 MockContentSuggestionsProviderObserver observer_; |
| 167 CategoryFactory category_factory_; |
| 168 TestingPrefServiceSimple pref_service_; |
| 169 std::unique_ptr<ForeignSessionsSuggestionsProvider> provider_; |
| 170 std::map<int, std::unique_ptr<SyncedSession>> sessions_map_; |
| 171 |
| 172 DISALLOW_COPY_AND_ASSIGN(ForeignSessionsSuggestionsProviderTest); |
| 173 }; |
| 174 |
| 175 TEST_F(ForeignSessionsSuggestionsProviderTest, Empty) { |
| 176 // When no sessions data is added, expect no suggestions. |
| 177 EXPECT_CALL(*observer(), OnNewSuggestions(_, category(), IsEmpty())); |
| 178 TriggerOnChange(); |
| 179 } |
| 180 |
| 181 TEST_F(ForeignSessionsSuggestionsProviderTest, Single) { |
| 182 // Expect a single valid tab because that is what has been added. |
| 183 EXPECT_CALL(*observer(), |
| 184 OnNewSuggestions( |
| 185 _, category(), |
| 186 ElementsAre(Property(&ContentSuggestion::url, GURL(kUrl1))))); |
| 187 AddTab(0, 0, kUrl1, TimeDelta::FromMinutes(1)); |
| 188 TriggerOnChange(); |
| 189 } |
| 190 |
| 191 TEST_F(ForeignSessionsSuggestionsProviderTest, Old) { |
| 192 // The only sessions data is too old to be suggested, so expect empty. |
| 193 EXPECT_CALL(*observer(), OnNewSuggestions(_, category(), IsEmpty())); |
| 194 AddTab(0, 0, kUrl1, TimeDelta::FromHours(4)); |
| 195 TriggerOnChange(); |
| 196 } |
| 197 |
| 198 TEST_F(ForeignSessionsSuggestionsProviderTest, Ordered) { |
| 199 // Suggestions ordering should be in reverse chronological order, or youngest |
| 200 // first. |
| 201 EXPECT_CALL(*observer(), |
| 202 OnNewSuggestions( |
| 203 _, category(), |
| 204 ElementsAre(Property(&ContentSuggestion::url, GURL(kUrl1)), |
| 205 Property(&ContentSuggestion::url, GURL(kUrl2)), |
| 206 Property(&ContentSuggestion::url, GURL(kUrl3)), |
| 207 Property(&ContentSuggestion::url, GURL(kUrl4))))); |
| 208 AddTab(0, 0, kUrl2, TimeDelta::FromMinutes(2)); |
| 209 AddTab(0, 0, kUrl4, TimeDelta::FromMinutes(4)); |
| 210 AddTab(0, 1, kUrl3, TimeDelta::FromMinutes(3)); |
| 211 AddTab(1, 0, kUrl1, TimeDelta::FromMinutes(1)); |
| 212 TriggerOnChange(); |
| 213 } |
| 214 |
| 215 TEST_F(ForeignSessionsSuggestionsProviderTest, MaxPerDevice) { |
| 216 // Each device, which is to equivalent a unique |session_tag|, has a limit to |
| 217 // the number of suggestions it is allowed to contribute. Here all four |
| 218 // suggestions are within the recency threshold, but only three are allowed |
| 219 // per device. As such, expect that the oldest of the four will not be |
| 220 // suggested. |
| 221 EXPECT_CALL(*observer(), |
| 222 OnNewSuggestions( |
| 223 _, category(), |
| 224 ElementsAre(Property(&ContentSuggestion::url, GURL(kUrl1)), |
| 225 Property(&ContentSuggestion::url, GURL(kUrl2)), |
| 226 Property(&ContentSuggestion::url, GURL(kUrl3))))); |
| 227 AddTab(0, 0, kUrl1, TimeDelta::FromMinutes(1)); |
| 228 AddTab(0, 0, kUrl2, TimeDelta::FromMinutes(2)); |
| 229 AddTab(0, 0, kUrl3, TimeDelta::FromMinutes(3)); |
| 230 AddTab(0, 0, kUrl4, TimeDelta::FromMinutes(4)); |
| 231 TriggerOnChange(); |
| 232 } |
| 233 |
| 234 TEST_F(ForeignSessionsSuggestionsProviderTest, MaxTotal) { |
| 235 // There's a limit to the total nubmer of suggestions that the provider will |
| 236 // ever return, which should be ten. Here there are eleven valid suggestion |
| 237 // entries, spread out over multiple devices/sessions to avoid the per device |
| 238 // cutoff. Expect that the least recent of the eleven to be dropped. |
| 239 EXPECT_CALL( |
| 240 *observer(), |
| 241 OnNewSuggestions( |
| 242 _, category(), |
| 243 ElementsAre(Property(&ContentSuggestion::url, GURL(kUrl1)), |
| 244 Property(&ContentSuggestion::url, GURL(kUrl2)), |
| 245 Property(&ContentSuggestion::url, GURL(kUrl3)), |
| 246 Property(&ContentSuggestion::url, GURL(kUrl4)), |
| 247 Property(&ContentSuggestion::url, GURL(kUrl5)), |
| 248 Property(&ContentSuggestion::url, GURL(kUrl6)), |
| 249 Property(&ContentSuggestion::url, GURL(kUrl7)), |
| 250 Property(&ContentSuggestion::url, GURL(kUrl8)), |
| 251 Property(&ContentSuggestion::url, GURL(kUrl9)), |
| 252 Property(&ContentSuggestion::url, GURL(kUrl10))))); |
| 253 AddTab(0, 0, kUrl1, TimeDelta::FromMinutes(1)); |
| 254 AddTab(0, 0, kUrl2, TimeDelta::FromMinutes(2)); |
| 255 AddTab(0, 0, kUrl3, TimeDelta::FromMinutes(3)); |
| 256 AddTab(1, 0, kUrl4, TimeDelta::FromMinutes(4)); |
| 257 AddTab(1, 0, kUrl5, TimeDelta::FromMinutes(5)); |
| 258 AddTab(1, 0, kUrl6, TimeDelta::FromMinutes(6)); |
| 259 AddTab(2, 0, kUrl7, TimeDelta::FromMinutes(7)); |
| 260 AddTab(2, 0, kUrl8, TimeDelta::FromMinutes(8)); |
| 261 AddTab(2, 0, kUrl9, TimeDelta::FromMinutes(9)); |
| 262 AddTab(3, 0, kUrl10, TimeDelta::FromMinutes(10)); |
| 263 AddTab(3, 0, kUrl11, TimeDelta::FromMinutes(11)); |
| 264 TriggerOnChange(); |
| 265 } |
| 266 |
| 267 TEST_F(ForeignSessionsSuggestionsProviderTest, Duplicates) { |
| 268 // The same url is never suggested more than once at a time. All the session |
| 269 // data has the same url so only expect a single suggestion. |
| 270 EXPECT_CALL(*observer(), |
| 271 OnNewSuggestions( |
| 272 _, category(), |
| 273 ElementsAre(Property(&ContentSuggestion::url, GURL(kUrl1))))); |
| 274 AddTab(0, 0, kUrl1, TimeDelta::FromMinutes(1)); |
| 275 AddTab(0, 1, kUrl1, TimeDelta::FromMinutes(2)); |
| 276 AddTab(1, 1, kUrl1, TimeDelta::FromMinutes(3)); |
| 277 TriggerOnChange(); |
| 278 } |
| 279 |
| 280 TEST_F(ForeignSessionsSuggestionsProviderTest, DuplicatesChangingOtherSession) { |
| 281 // Normally |kUrl4| wouldn't show up, because session_id=0 already has 3 |
| 282 // younger tabs, but session_id=1 has a younger |kUrl3| which gives |kUrl4| a |
| 283 // spot. |
| 284 EXPECT_CALL(*observer(), |
| 285 OnNewSuggestions( |
| 286 _, category(), |
| 287 ElementsAre(Property(&ContentSuggestion::url, GURL(kUrl3)), |
| 288 Property(&ContentSuggestion::url, GURL(kUrl1)), |
| 289 Property(&ContentSuggestion::url, GURL(kUrl2)), |
| 290 Property(&ContentSuggestion::url, GURL(kUrl4))))); |
| 291 AddTab(0, 0, kUrl1, TimeDelta::FromMinutes(1)); |
| 292 AddTab(0, 0, kUrl2, TimeDelta::FromMinutes(2)); |
| 293 AddTab(0, 0, kUrl3, TimeDelta::FromMinutes(3)); |
| 294 AddTab(0, 0, kUrl4, TimeDelta::FromMinutes(4)); |
| 295 AddTab(1, 0, kUrl3, TimeDelta::FromMinutes(0)); |
| 296 TriggerOnChange(); |
| 297 } |
| 298 |
| 299 TEST_F(ForeignSessionsSuggestionsProviderTest, Dismissed) { |
| 300 // Dimissed urls should not be suggested. |
| 301 EXPECT_CALL(*observer(), OnNewSuggestions(_, category(), IsEmpty())); |
| 302 Dismiss(kUrl1); |
| 303 AddTab(0, 0, kUrl1, TimeDelta::FromMinutes(1)); |
| 304 TriggerOnChange(); |
| 305 } |
| 306 |
| 307 TEST_F(ForeignSessionsSuggestionsProviderTest, DismissedChangingOwnSession) { |
| 308 // Similar to DuplicatesChangingOtherSession, without dismissals we would |
| 309 // expect urls 1-3. However, because of dismissals we reach all the down to |
| 310 // |kUrl5| before the per device cutoff is hit. |
| 311 EXPECT_CALL(*observer(), |
| 312 OnNewSuggestions( |
| 313 _, category(), |
| 314 ElementsAre(Property(&ContentSuggestion::url, GURL(kUrl2)), |
| 315 Property(&ContentSuggestion::url, GURL(kUrl3)), |
| 316 Property(&ContentSuggestion::url, GURL(kUrl5))))); |
| 317 Dismiss(kUrl1); |
| 318 Dismiss(kUrl4); |
| 319 AddTab(0, 0, kUrl1, TimeDelta::FromMinutes(1)); |
| 320 AddTab(0, 0, kUrl2, TimeDelta::FromMinutes(2)); |
| 321 AddTab(0, 0, kUrl3, TimeDelta::FromMinutes(3)); |
| 322 AddTab(0, 0, kUrl4, TimeDelta::FromMinutes(4)); |
| 323 AddTab(0, 0, kUrl5, TimeDelta::FromMinutes(5)); |
| 324 AddTab(0, 0, kUrl6, TimeDelta::FromMinutes(6)); |
| 325 TriggerOnChange(); |
| 326 } |
| 327 |
| 328 } // namespace ntp_snippets |
OLD | NEW |